go.temporal.io/server@v1.23.0/common/membership/ringpop/factory_test.go (about)

     1  // The MIT License
     2  //
     3  // Copyright (c) 2020 Temporal Technologies Inc.  All rights reserved.
     4  //
     5  // Copyright (c) 2020 Uber Technologies, Inc.
     6  //
     7  // Permission is hereby granted, free of charge, to any person obtaining a copy
     8  // of this software and associated documentation files (the "Software"), to deal
     9  // in the Software without restriction, including without limitation the rights
    10  // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
    11  // copies of the Software, and to permit persons to whom the Software is
    12  // furnished to do so, subject to the following conditions:
    13  //
    14  // The above copyright notice and this permission notice shall be included in
    15  // all copies or substantial portions of the Software.
    16  //
    17  // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
    18  // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
    19  // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
    20  // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
    21  // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
    22  // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
    23  // THE SOFTWARE.
    24  
    25  package ringpop
    26  
    27  import (
    28  	"context"
    29  	"crypto/tls"
    30  	"os"
    31  	"testing"
    32  	"time"
    33  
    34  	"github.com/golang/mock/gomock"
    35  	"github.com/stretchr/testify/require"
    36  	"github.com/stretchr/testify/suite"
    37  	"gopkg.in/yaml.v3"
    38  
    39  	"go.temporal.io/server/common/config"
    40  	"go.temporal.io/server/common/dynamicconfig"
    41  	"go.temporal.io/server/common/log"
    42  	"go.temporal.io/server/common/metrics"
    43  	"go.temporal.io/server/common/primitives"
    44  	"go.temporal.io/server/common/rpc/encryption"
    45  	"go.temporal.io/server/tests/testutils"
    46  )
    47  
    48  type (
    49  	RingpopSuite struct {
    50  		*require.Assertions
    51  		suite.Suite
    52  
    53  		controller *gomock.Controller
    54  
    55  		logger           log.Logger
    56  		internodeCertDir string
    57  		internodeChain   testutils.CertChain
    58  
    59  		membershipConfig         config.Membership
    60  		internodeConfigMutualTLS config.GroupTLS
    61  		internodeConfigServerTLS config.GroupTLS
    62  
    63  		mutualTLSFactoryA *factory
    64  		mutualTLSFactoryB *factory
    65  		serverTLSFactoryA *factory
    66  		serverTLSFactoryB *factory
    67  
    68  		insecureFactory *factory
    69  	}
    70  )
    71  
    72  const (
    73  	localhostIPv4 = "127.0.0.1"
    74  )
    75  
    76  var (
    77  	rpcTestCfgDefault = &config.RPC{
    78  		GRPCPort:       0,
    79  		MembershipPort: 7600,
    80  		BindOnIP:       localhostIPv4,
    81  	}
    82  	serverCfgInsecure = &config.Global{
    83  		Membership: config.Membership{
    84  			MaxJoinDuration:  5,
    85  			BroadcastAddress: localhostIPv4,
    86  		},
    87  	}
    88  )
    89  
    90  func TestRingpopSuite(t *testing.T) {
    91  	suite.Run(t, new(RingpopSuite))
    92  }
    93  
    94  func (s *RingpopSuite) SetupTest() {
    95  	s.Assertions = require.New(s.T())
    96  	s.logger = log.NewTestLogger()
    97  	s.controller = gomock.NewController(s.T())
    98  
    99  	var err error
   100  	s.internodeCertDir, err = os.MkdirTemp("", "RingpopSuiteInternode")
   101  	s.NoError(err)
   102  	s.internodeChain, err = testutils.GenerateTestChain(s.internodeCertDir, localhostIPv4)
   103  	s.NoError(err)
   104  
   105  	s.internodeConfigMutualTLS = config.GroupTLS{
   106  		Server: config.ServerTLS{
   107  			CertFile:          s.internodeChain.CertPubFile,
   108  			KeyFile:           s.internodeChain.CertKeyFile,
   109  			ClientCAFiles:     []string{s.internodeChain.CaPubFile},
   110  			RequireClientAuth: true,
   111  		},
   112  		Client: config.ClientTLS{
   113  			RootCAFiles: []string{s.internodeChain.CaPubFile},
   114  		},
   115  	}
   116  
   117  	s.internodeConfigServerTLS = config.GroupTLS{
   118  		Server: config.ServerTLS{
   119  			CertData: testutils.ConvertFileToBase64(s.internodeChain.CertPubFile),
   120  			KeyData:  testutils.ConvertFileToBase64(s.internodeChain.CertKeyFile),
   121  		},
   122  		Client: config.ClientTLS{
   123  			RootCAData: []string{testutils.ConvertFileToBase64(s.internodeChain.CaPubFile)},
   124  		},
   125  	}
   126  
   127  	s.setupInternodeRingpop()
   128  }
   129  
   130  func (s *RingpopSuite) TearDownSuite() {
   131  	_ = os.RemoveAll(s.internodeCertDir)
   132  }
   133  
   134  func (s *RingpopSuite) TestHostsMode() {
   135  	var cfg config.Membership
   136  	err := yaml.Unmarshal([]byte(getHostsConfig()), &cfg)
   137  	s.Nil(err)
   138  	s.Equal("1.2.3.4", cfg.BroadcastAddress)
   139  	s.Equal(time.Second*30, cfg.MaxJoinDuration)
   140  
   141  	params := factoryParams{
   142  		Config:      &cfg,
   143  		ServiceName: "test",
   144  		Logger:      log.Logger(log.NewNoopLogger()),
   145  	}
   146  	f, err := newFactory(params)
   147  	s.Nil(err)
   148  	s.NotNil(f)
   149  }
   150  
   151  func getHostsConfig() string {
   152  	return `name: "test"
   153  broadcastAddress: "1.2.3.4"
   154  maxJoinDuration: 30s`
   155  }
   156  
   157  func (s *RingpopSuite) TestInvalidBroadcastAddress() {
   158  	cfg := config.Membership{
   159  		MaxJoinDuration:  time.Minute,
   160  		BroadcastAddress: "oopsie",
   161  	}
   162  	logger := log.Logger(log.NewNoopLogger())
   163  	params := factoryParams{
   164  		Config:      &cfg,
   165  		ServiceName: "test",
   166  		Logger:      logger,
   167  	}
   168  	_, err := newFactory(params)
   169  
   170  	s.ErrorIs(err, errMalformedBroadcastAddress)
   171  	s.ErrorContains(err, "oopsie")
   172  }
   173  
   174  func newTestRingpopFactory(
   175  	serviceName primitives.ServiceName,
   176  	logger log.Logger,
   177  	rpcConfig *config.RPC,
   178  	tlsProvider encryption.TLSConfigProvider,
   179  	dc *dynamicconfig.Collection,
   180  ) *factory {
   181  	return &factory{
   182  		ServiceName: serviceName,
   183  		Logger:      logger,
   184  		RPCConfig:   rpcConfig,
   185  		TLSFactory:  tlsProvider,
   186  		DC:          dc,
   187  	}
   188  }
   189  
   190  func (s *RingpopSuite) TestRingpopMutualTLS() {
   191  	s.NoError(runRingpopTLSTest(&s.Suite, s.mutualTLSFactoryA, s.mutualTLSFactoryB))
   192  }
   193  
   194  func (s *RingpopSuite) TestRingpopServerTLS() {
   195  	s.NoError(runRingpopTLSTest(&s.Suite, s.serverTLSFactoryA, s.serverTLSFactoryB))
   196  }
   197  
   198  func (s *RingpopSuite) TestRingpopInvalidTLS() {
   199  	s.Error(runRingpopTLSTest(&s.Suite, s.insecureFactory, s.serverTLSFactoryB))
   200  }
   201  
   202  func runRingpopTLSTest(s *suite.Suite, serverA *factory, serverB *factory) error {
   203  	// Start two ringpop nodes
   204  	chA := serverA.getTChannel()
   205  	chB := serverB.getTChannel()
   206  	defer chA.Close()
   207  	defer chB.Close()
   208  
   209  	// Ping A through B to make sure B's dialer uses TLS to communicate with A
   210  	hostPortA := chA.PeerInfo().HostPort
   211  	if err := chB.Ping(context.Background(), hostPortA); err != nil {
   212  		return err
   213  	}
   214  
   215  	// Confirm that A's listener is actually using TLS
   216  	clientTLSConfig, err := serverB.TLSFactory.GetInternodeClientConfig()
   217  	s.NoError(err)
   218  
   219  	conn, err := tls.Dial("tcp", hostPortA, clientTLSConfig)
   220  	if err != nil {
   221  		return err
   222  	}
   223  	if conn != nil {
   224  		_ = conn.Close()
   225  	}
   226  	return nil
   227  }
   228  
   229  func (s *RingpopSuite) setupInternodeRingpop() {
   230  	provider, err := encryption.NewTLSConfigProviderFromConfig(serverCfgInsecure.TLS, metrics.NoopMetricsHandler, s.logger, nil)
   231  	s.NoError(err)
   232  	s.insecureFactory = newTestRingpopFactory("tester", s.logger, rpcTestCfgDefault, provider, dynamicconfig.NewNoopCollection())
   233  	s.NotNil(s.insecureFactory)
   234  
   235  	serverTLS := &config.Global{
   236  		Membership: s.membershipConfig,
   237  		TLS: config.RootTLS{
   238  			Internode: s.internodeConfigServerTLS,
   239  		},
   240  	}
   241  
   242  	mutualTLS := &config.Global{
   243  		Membership: s.membershipConfig,
   244  		TLS: config.RootTLS{
   245  			Internode: s.internodeConfigMutualTLS,
   246  		},
   247  	}
   248  
   249  	rpcCfgA := &config.RPC{GRPCPort: 0, MembershipPort: 7600, BindOnIP: localhostIPv4}
   250  	rpcCfgB := &config.RPC{GRPCPort: 0, MembershipPort: 7601, BindOnIP: localhostIPv4}
   251  
   252  	dc := dynamicconfig.NewCollection(dynamicconfig.StaticClient(map[dynamicconfig.Key]any{
   253  		dynamicconfig.EnableRingpopTLS: true,
   254  	}), s.logger)
   255  
   256  	provider, err = encryption.NewTLSConfigProviderFromConfig(mutualTLS.TLS, metrics.NoopMetricsHandler, s.logger, nil)
   257  	s.NoError(err)
   258  	s.mutualTLSFactoryA = newTestRingpopFactory("tester-A", s.logger, rpcCfgA, provider, dc)
   259  	s.NotNil(s.mutualTLSFactoryA)
   260  	s.mutualTLSFactoryB = newTestRingpopFactory("tester-B", s.logger, rpcCfgB, provider, dc)
   261  	s.NotNil(s.mutualTLSFactoryB)
   262  
   263  	provider, err = encryption.NewTLSConfigProviderFromConfig(serverTLS.TLS, metrics.NoopMetricsHandler, s.logger, nil)
   264  	s.NoError(err)
   265  	s.serverTLSFactoryA = newTestRingpopFactory("tester-A", s.logger, rpcCfgA, provider, dc)
   266  	s.NotNil(s.serverTLSFactoryA)
   267  	s.serverTLSFactoryB = newTestRingpopFactory("tester-B", s.logger, rpcCfgB, provider, dc)
   268  	s.NotNil(s.serverTLSFactoryB)
   269  }