go.uber.org/yarpc@v1.72.1/transport/tchannel/config_test.go (about)

     1  // Copyright (c) 2022 Uber Technologies, Inc.
     2  //
     3  // Permission is hereby granted, free of charge, to any person obtaining a copy
     4  // of this software and associated documentation files (the "Software"), to deal
     5  // in the Software without restriction, including without limitation the rights
     6  // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
     7  // copies of the Software, and to permit persons to whom the Software is
     8  // furnished to do so, subject to the following conditions:
     9  //
    10  // The above copyright notice and this permission notice shall be included in
    11  // all copies or substantial portions of the Software.
    12  //
    13  // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
    14  // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
    15  // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
    16  // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
    17  // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
    18  // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
    19  // THE SOFTWARE.
    20  
    21  package tchannel
    22  
    23  import (
    24  	"crypto/tls"
    25  	"errors"
    26  	"fmt"
    27  	"reflect"
    28  	"testing"
    29  
    30  	"github.com/stretchr/testify/assert"
    31  	"github.com/stretchr/testify/require"
    32  	tchanneltest "github.com/uber/tchannel-go/testutils"
    33  	"go.uber.org/yarpc"
    34  	yarpctls "go.uber.org/yarpc/api/transport/tls"
    35  	"go.uber.org/yarpc/yarpcconfig"
    36  )
    37  
    38  type badOption struct{}
    39  
    40  func (badOption) tchannelOption() {}
    41  
    42  func TestTransportSpecInvalidOption(t *testing.T) {
    43  	assert.Panics(t, func() {
    44  		TransportSpec(badOption{})
    45  	})
    46  }
    47  
    48  type fakeOutboundTLSConfigProvider struct {
    49  	returnErr         error
    50  	expectedSpiffeIDs []string
    51  }
    52  
    53  func (f fakeOutboundTLSConfigProvider) ClientTLSConfig(spiffeIDs []string) (*tls.Config, error) {
    54  	if f.returnErr != nil {
    55  		return nil, f.returnErr
    56  	}
    57  	if !reflect.DeepEqual(f.expectedSpiffeIDs, spiffeIDs) {
    58  		return nil, errors.New("spiffe IDs do not match")
    59  	}
    60  	return &tls.Config{}, nil
    61  }
    62  
    63  func TestTransportSpec(t *testing.T) {
    64  	someChannel := tchanneltest.NewServer(t, nil)
    65  	defer someChannel.Close()
    66  
    67  	type attrs map[string]interface{}
    68  
    69  	type wantTransport struct {
    70  		Address string
    71  		TLSMode yarpctls.Mode
    72  	}
    73  
    74  	type wantOutbound struct {
    75  		reuseBuffer bool
    76  	}
    77  
    78  	type inboundTest struct {
    79  		desc string            // description
    80  		cfg  attrs             // inbounds section of the config
    81  		env  map[string]string // environment variables
    82  		opts []Option          // transport spec options
    83  
    84  		empty bool // whether this test case is empty
    85  
    86  		wantErrors []string
    87  
    88  		// Inbounds don't have any properties that affect the Inbound object.
    89  		// We'll assert things about the transport only.
    90  		wantTransport *wantTransport
    91  	}
    92  
    93  	type outboundTest struct {
    94  		desc string            // description
    95  		cfg  attrs             // outbounds section of the config
    96  		env  map[string]string // environment variables
    97  		opts []Option          // transport spec options
    98  
    99  		empty bool // whether this test case is empty
   100  
   101  		wantErrors    []string
   102  		wantOutbounds map[string]wantOutbound
   103  	}
   104  
   105  	inboundTests := []inboundTest{
   106  		{desc: "no inbound", empty: true},
   107  		{
   108  			desc:          "simple inbound",
   109  			cfg:           attrs{"tchannel": attrs{"address": ":4040"}},
   110  			wantTransport: &wantTransport{Address: ":4040"},
   111  		},
   112  		{
   113  			desc: "inbound tls",
   114  			cfg: attrs{"tchannel": attrs{
   115  				"address": ":4040",
   116  				"tls":     attrs{"mode": "permissive"},
   117  			}},
   118  			opts:          []Option{InboundTLSConfiguration(&tls.Config{})},
   119  			wantTransport: &wantTransport{Address: ":4040", TLSMode: yarpctls.Permissive},
   120  		},
   121  		{
   122  			desc: "inbound tls mode override with option",
   123  			cfg: attrs{"tchannel": attrs{
   124  				"address": ":4040",
   125  				"tls":     attrs{"mode": "permissive"},
   126  			}},
   127  			opts:          []Option{InboundTLSMode(yarpctls.Disabled)},
   128  			wantTransport: &wantTransport{Address: ":4040", TLSMode: yarpctls.Disabled},
   129  		},
   130  		{
   131  			desc:          "inbound interpolation",
   132  			cfg:           attrs{"tchannel": attrs{"address": ":${PORT}"}},
   133  			env:           map[string]string{"PORT": "4041"},
   134  			wantTransport: &wantTransport{Address: ":4041"},
   135  		},
   136  		{
   137  			desc:       "empty address",
   138  			cfg:        attrs{"tchannel": attrs{"address": ""}},
   139  			wantErrors: []string{"inbound address is required"},
   140  		},
   141  		{
   142  			desc:       "missing address",
   143  			cfg:        attrs{"tchannel": attrs{}},
   144  			wantErrors: []string{"inbound address is required"},
   145  		},
   146  		{
   147  			desc: "too many inbounds",
   148  			cfg: attrs{
   149  				"tchannel":  attrs{"address": ":4040"},
   150  				"tchannel2": attrs{"address": ":4041", "type": "tchannel"},
   151  			},
   152  			wantErrors: []string{"at most one TChannel inbound may be specified"},
   153  		},
   154  		{
   155  			desc:       "WithChannel fails",
   156  			cfg:        attrs{"tchannel": attrs{"address": ":4040"}},
   157  			opts:       []Option{WithChannel(someChannel)},
   158  			wantErrors: []string{"TChannel TransportSpec does not accept WithChannel"},
   159  		},
   160  		{
   161  			desc:       "ServiceName fails",
   162  			cfg:        attrs{"tchannel": attrs{"address": ":4040"}},
   163  			opts:       []Option{ServiceName("zzzzzzzzz")},
   164  			wantErrors: []string{"TChannel TransportSpec does not accept ServiceName"},
   165  		},
   166  		{
   167  			desc:       "ListenAddr fails",
   168  			cfg:        attrs{"tchannel": attrs{"address": ":4040"}},
   169  			opts:       []Option{ListenAddr(":8080")},
   170  			wantErrors: []string{"TChannel TransportSpec does not accept ListenAddr"},
   171  		},
   172  	}
   173  
   174  	outboundTests := []outboundTest{
   175  		{desc: "no outbound", empty: true},
   176  		{
   177  			desc: "simple outbound",
   178  			cfg: attrs{
   179  				"myservice": attrs{
   180  					"tchannel": attrs{
   181  						"peer": "127.0.0.1:4040",
   182  					},
   183  				},
   184  			},
   185  			wantOutbounds: map[string]wantOutbound{"myservice": {}},
   186  		},
   187  		{
   188  			desc: "outbound interpolation",
   189  			env:  map[string]string{"SERVICE_PORT": "4040"},
   190  			cfg: attrs{
   191  				"myservice": attrs{
   192  					"tchannel": attrs{
   193  						"peer": "127.0.0.1:${SERVICE_PORT}",
   194  					},
   195  				},
   196  			},
   197  		},
   198  		{
   199  			desc: "outbound bad peer list",
   200  			cfg: attrs{
   201  				"myservice": attrs{
   202  					"tchannel": attrs{"least-pending": "wat"},
   203  				},
   204  			},
   205  			wantErrors: []string{
   206  				`failed to configure unary outbound for "myservice"`,
   207  				`failed to read attribute "least-pending": wat`,
   208  			},
   209  		},
   210  		{
   211  			desc: "fail TLS outbound with invalid tls mode",
   212  			cfg: attrs{
   213  				"myservice": attrs{
   214  					"tchannel": attrs{
   215  						"peer": "127.0.0.1:4040",
   216  						"tls": attrs{
   217  							"mode": "permissive",
   218  						},
   219  					},
   220  				},
   221  			},
   222  			wantErrors: []string{"outbound does not support permissive TLS mode"},
   223  		},
   224  		{
   225  			desc: "fail TLS outbound without outbound tls config provider",
   226  			cfg: attrs{
   227  				"myservice": attrs{
   228  					"tchannel": attrs{
   229  						"peer": "127.0.0.1:4040",
   230  						"tls": attrs{
   231  							"mode":       "enforced",
   232  							"spiffe-ids": []string{"test-spiffe"},
   233  						},
   234  					},
   235  				},
   236  			},
   237  			wantErrors: []string{"outbound TLS enforced but outbound TLS config provider is nil"},
   238  		},
   239  		{
   240  			desc: "TLS outbound without spiffe id",
   241  			cfg: attrs{
   242  				"myservice": attrs{
   243  					"tchannel": attrs{
   244  						"peer": "127.0.0.1:4040",
   245  						"tls": attrs{
   246  							"mode": "enforced",
   247  						},
   248  					},
   249  				},
   250  			},
   251  			opts:          []Option{OutboundTLSConfigProvider(&fakeOutboundTLSConfigProvider{})},
   252  			wantOutbounds: map[string]wantOutbound{"myservice": {}},
   253  		},
   254  		{
   255  			desc: "fail TLS outbound when tls config provider returns error",
   256  			cfg: attrs{
   257  				"myservice": attrs{
   258  					"tchannel": attrs{
   259  						"peer": "127.0.0.1:4040",
   260  						"tls": attrs{
   261  							"mode":       "enforced",
   262  							"spiffe-ids": []string{"test-spiffe"},
   263  						},
   264  					},
   265  				},
   266  			},
   267  			opts:       []Option{OutboundTLSConfigProvider(&fakeOutboundTLSConfigProvider{returnErr: errors.New("test error")})},
   268  			wantErrors: []string{"test error"},
   269  		},
   270  		{
   271  			desc: "simple TLS outbound",
   272  			cfg: attrs{
   273  				"myservice": attrs{
   274  					"tchannel": attrs{
   275  						"peer": "127.0.0.1:4040",
   276  						"tls": attrs{
   277  							"mode":       "enforced",
   278  							"spiffe-ids": []string{"test-spiffe"},
   279  						},
   280  					},
   281  				},
   282  			},
   283  			opts: []Option{OutboundTLSConfigProvider(
   284  				&fakeOutboundTLSConfigProvider{
   285  					expectedSpiffeIDs: []string{"test-spiffe"},
   286  				},
   287  			)},
   288  			wantOutbounds: map[string]wantOutbound{"myservice": {}},
   289  		},
   290  		{
   291  			desc: "outbound with buffer reuse",
   292  			cfg: attrs{
   293  				"myservice": attrs{
   294  					"tchannel": attrs{
   295  						"peer":                "127.0.0.1:4040",
   296  						"enable-buffer-reuse": true,
   297  					},
   298  				},
   299  			},
   300  			wantOutbounds: map[string]wantOutbound{"myservice": {reuseBuffer: true}},
   301  		},
   302  	}
   303  
   304  	runTest := func(t *testing.T, inbound inboundTest, outbound outboundTest) {
   305  		env := make(map[string]string)
   306  		for k, v := range inbound.env {
   307  			env[k] = v
   308  		}
   309  		for k, v := range outbound.env {
   310  			_, ok := env[k]
   311  			require.False(t, ok,
   312  				"invalid test: environment variable %q is defined multiple times", k)
   313  			env[k] = v
   314  		}
   315  		configurator := yarpcconfig.New(yarpcconfig.InterpolationResolver(mapResolver(env)))
   316  
   317  		opts := append(inbound.opts, outbound.opts...)
   318  		err := configurator.RegisterTransport(TransportSpec(opts...))
   319  		require.NoError(t, err, "failed to register transport spec")
   320  
   321  		cfgData := make(attrs)
   322  		if inbound.cfg != nil {
   323  			cfgData["inbounds"] = inbound.cfg
   324  		}
   325  		if outbound.cfg != nil {
   326  			cfgData["outbounds"] = outbound.cfg
   327  		}
   328  		cfg, err := configurator.LoadConfig("foo", cfgData)
   329  
   330  		if len(inbound.wantErrors) > 0 {
   331  			require.Error(t, err, "expected failure while loading config %+v", cfgData)
   332  			for _, msg := range inbound.wantErrors {
   333  				assert.Contains(t, err.Error(), msg)
   334  			}
   335  			return
   336  		}
   337  
   338  		if len(outbound.wantErrors) > 0 {
   339  			require.Error(t, err, "expected failure while loading config %+v", cfgData)
   340  			for _, msg := range outbound.wantErrors {
   341  				assert.Contains(t, err.Error(), msg)
   342  			}
   343  			return
   344  		}
   345  
   346  		require.NoError(t, err, "expected success while loading config %+v", cfgData)
   347  		if want := inbound.wantTransport; want != nil {
   348  			assert.Len(t, cfg.Inbounds, 1, "expected exactly one inbound in %+v", cfgData)
   349  			ib, ok := cfg.Inbounds[0].(*Inbound)
   350  			if assert.True(t, ok, "expected *Inbound, got %T", cfg.Inbounds[0]) {
   351  				trans := ib.transport
   352  				assert.Equal(t, "foo", trans.name, "service name must match")
   353  				assert.Equal(t, want.Address, trans.addr, "transport address must match")
   354  				require.NotNil(t, trans.inboundTLSMode, "tls mode is nil")
   355  				assert.Equal(t, want.TLSMode, *trans.inboundTLSMode, "tls mode must match")
   356  			}
   357  		}
   358  
   359  		for svc, want := range outbound.wantOutbounds {
   360  			ob, ok := cfg.Outbounds[svc].Unary.(*Outbound)
   361  			assert.True(t, ok, "expected *Outbound for %q, got %T", svc, cfg.Outbounds[svc].Unary)
   362  			assert.Equal(t, want.reuseBuffer, ob.reuseBuffer)
   363  		}
   364  
   365  		d := yarpc.NewDispatcher(cfg)
   366  		require.NoError(t, d.Start(), "failed to start dispatcher")
   367  		require.NoError(t, d.Stop(), "failed to stop dispatcher")
   368  	}
   369  
   370  	for _, inboundTT := range inboundTests {
   371  		for _, outboundTT := range outboundTests {
   372  			// Special case: No inbounds or outbounds so we have nothing to
   373  			// test.
   374  			if inboundTT.empty && outboundTT.empty {
   375  				continue
   376  			}
   377  
   378  			desc := fmt.Sprintf("%v/%v", inboundTT.desc, outboundTT.desc)
   379  			t.Run(desc, func(t *testing.T) {
   380  				runTest(t, inboundTT, outboundTT)
   381  			})
   382  		}
   383  	}
   384  }
   385  
   386  func mapResolver(m map[string]string) func(string) (string, bool) {
   387  	return func(k string) (v string, ok bool) {
   388  		if m != nil {
   389  			v, ok = m[k]
   390  		}
   391  		return
   392  	}
   393  }