github.com/pingcap/tiflow@v0.0.0-20240520035814-5bf52d54e205/dm/master/config_test.go (about)

     1  // Copyright 2019 PingCAP, Inc.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // See the License for the specific language governing permissions and
    12  // limitations under the License.
    13  
    14  package master
    15  
    16  import (
    17  	"crypto/rand"
    18  	"encoding/hex"
    19  	"flag"
    20  	"fmt"
    21  	"net/url"
    22  	"os"
    23  	"path"
    24  	"path/filepath"
    25  	"strings"
    26  	"testing"
    27  
    28  	capturer "github.com/kami-zh/go-capturer"
    29  	"github.com/pingcap/check"
    30  	"github.com/pingcap/tiflow/dm/pkg/log"
    31  	"github.com/pingcap/tiflow/dm/pkg/terror"
    32  	"github.com/stretchr/testify/require"
    33  	"go.etcd.io/etcd/server/v3/embed"
    34  )
    35  
    36  var _ = check.Suite(&testConfigSuite{})
    37  
    38  type testConfigSuite struct{}
    39  
    40  func (t *testConfigSuite) SetUpSuite(c *check.C) {
    41  	// initialized the logger to make genEmbedEtcdConfig working.
    42  	c.Assert(log.InitLogger(&log.Config{}), check.IsNil)
    43  }
    44  
    45  func (t *testConfigSuite) TestPrintSampleConfig(c *check.C) {
    46  	// test print sample config
    47  	out := capturer.CaptureStdout(func() {
    48  		cfg := NewConfig()
    49  		err := cfg.Parse([]string{"-print-sample-config"})
    50  		c.Assert(err, check.ErrorMatches, flag.ErrHelp.Error())
    51  	})
    52  	c.Assert(strings.TrimSpace(out), check.Equals, strings.TrimSpace(SampleConfig))
    53  }
    54  
    55  func (t *testConfigSuite) TestConfig(c *check.C) {
    56  	var (
    57  		err        error
    58  		cfg        = &Config{}
    59  		masterAddr = ":8261"
    60  		cases      = []struct {
    61  			args     []string
    62  			hasError bool
    63  			errorReg string
    64  		}{
    65  			{
    66  				[]string{"-V"},
    67  				true,
    68  				flag.ErrHelp.Error(),
    69  			},
    70  			{
    71  				[]string{"-print-sample-config"},
    72  				true,
    73  				flag.ErrHelp.Error(),
    74  			},
    75  			{
    76  				[]string{"invalid"},
    77  				true,
    78  				".*'invalid' is an invalid flag.*",
    79  			},
    80  		}
    81  	)
    82  
    83  	err = cfg.FromContent(SampleConfig)
    84  	c.Assert(err, check.IsNil)
    85  	err = cfg.Reload()
    86  	c.Assert(err, check.IsNil)
    87  	c.Assert(cfg.MasterAddr, check.Equals, masterAddr)
    88  
    89  	for _, tc := range cases {
    90  		cfg = NewConfig()
    91  		err = cfg.Parse(tc.args)
    92  		if tc.hasError {
    93  			c.Assert(err, check.ErrorMatches, tc.errorReg)
    94  		}
    95  	}
    96  }
    97  
    98  func (t *testConfigSuite) TestInvalidConfig(c *check.C) {
    99  	var (
   100  		err error
   101  		cfg = NewConfig()
   102  	)
   103  
   104  	filepath := path.Join(c.MkDir(), "test_invalid_config.toml")
   105  	// field still remain undecoded in config will cause verify failed
   106  	configContent := []byte(`
   107  master-addr = ":8261"
   108  advertise-addr = "127.0.0.1:8261"
   109  aaa = "xxx"`)
   110  	err = os.WriteFile(filepath, configContent, 0o644)
   111  	c.Assert(err, check.IsNil)
   112  	err = cfg.configFromFile(filepath)
   113  	c.Assert(err, check.NotNil)
   114  	c.Assert(err, check.ErrorMatches, "*master config contained unknown configuration options: aaa.*")
   115  
   116  	// invalid `master-addr`
   117  	filepath2 := path.Join(c.MkDir(), "test_invalid_config.toml")
   118  	configContent2 := []byte(`master-addr = ""`)
   119  	err = os.WriteFile(filepath2, configContent2, 0o644)
   120  	c.Assert(err, check.IsNil)
   121  	err = cfg.configFromFile(filepath2)
   122  	c.Assert(err, check.IsNil)
   123  	c.Assert(terror.ErrMasterHostPortNotValid.Equal(cfg.adjust()), check.IsTrue)
   124  }
   125  
   126  func (t *testConfigSuite) TestGenEmbedEtcdConfig(c *check.C) {
   127  	hostname, err := os.Hostname()
   128  	c.Assert(err, check.IsNil)
   129  
   130  	cfg1 := NewConfig()
   131  	cfg1.MasterAddr = ":8261"
   132  	cfg1.AdvertiseAddr = "127.0.0.1:8261"
   133  	cfg1.InitialClusterState = embed.ClusterStateFlagExisting
   134  	c.Assert(cfg1.adjust(), check.IsNil)
   135  	etcdCfg, err := cfg1.genEmbedEtcdConfig(embed.NewConfig())
   136  	c.Assert(err, check.IsNil)
   137  	c.Assert(etcdCfg.Name, check.Equals, fmt.Sprintf("dm-master-%s", hostname))
   138  	c.Assert(etcdCfg.Dir, check.Equals, fmt.Sprintf("default.%s", etcdCfg.Name))
   139  	c.Assert(etcdCfg.ListenClientUrls, check.DeepEquals, []url.URL{{Scheme: "http", Host: "0.0.0.0:8261"}})
   140  	c.Assert(etcdCfg.AdvertiseClientUrls, check.DeepEquals, []url.URL{{Scheme: "http", Host: "127.0.0.1:8261"}})
   141  	c.Assert(etcdCfg.ListenPeerUrls, check.DeepEquals, []url.URL{{Scheme: "http", Host: "127.0.0.1:8291"}})
   142  	c.Assert(etcdCfg.AdvertisePeerUrls, check.DeepEquals, []url.URL{{Scheme: "http", Host: "127.0.0.1:8291"}})
   143  	c.Assert(etcdCfg.InitialCluster, check.DeepEquals, fmt.Sprintf("dm-master-%s=http://127.0.0.1:8291", hostname))
   144  	c.Assert(etcdCfg.ClusterState, check.Equals, embed.ClusterStateFlagExisting)
   145  	c.Assert(etcdCfg.AutoCompactionMode, check.Equals, "periodic")
   146  	c.Assert(etcdCfg.AutoCompactionRetention, check.Equals, "1h")
   147  	c.Assert(etcdCfg.QuotaBackendBytes, check.Equals, int64(2*1024*1024*1024))
   148  
   149  	cfg2 := *cfg1
   150  	cfg2.MasterAddr = "127.0.0.1\n:8261"
   151  	cfg2.AdvertiseAddr = "127.0.0.1:8261"
   152  	_, err = cfg2.genEmbedEtcdConfig(embed.NewConfig())
   153  	c.Assert(terror.ErrMasterGenEmbedEtcdConfigFail.Equal(err), check.IsTrue)
   154  	c.Assert(err, check.ErrorMatches, "(?m).*invalid master-addr.*")
   155  	cfg2.MasterAddr = "172.100.8.8:8261"
   156  	cfg2.AdvertiseAddr = "172.100.8.8:8261"
   157  	etcdCfg, err = cfg2.genEmbedEtcdConfig(embed.NewConfig())
   158  	c.Assert(err, check.IsNil)
   159  	c.Assert(etcdCfg.ListenClientUrls, check.DeepEquals, []url.URL{{Scheme: "http", Host: "172.100.8.8:8261"}})
   160  	c.Assert(etcdCfg.AdvertiseClientUrls, check.DeepEquals, []url.URL{{Scheme: "http", Host: "172.100.8.8:8261"}})
   161  
   162  	cfg3 := *cfg1
   163  	cfg3.PeerUrls = "127.0.0.1:\n8291"
   164  	_, err = cfg3.genEmbedEtcdConfig(embed.NewConfig())
   165  	c.Assert(terror.ErrMasterGenEmbedEtcdConfigFail.Equal(err), check.IsTrue)
   166  	c.Assert(err, check.ErrorMatches, "(?m).*invalid peer-urls.*")
   167  	cfg3.PeerUrls = "http://172.100.8.8:8291"
   168  	etcdCfg, err = cfg3.genEmbedEtcdConfig(embed.NewConfig())
   169  	c.Assert(err, check.IsNil)
   170  	c.Assert(etcdCfg.ListenPeerUrls, check.DeepEquals, []url.URL{{Scheme: "http", Host: "172.100.8.8:8291"}})
   171  
   172  	cfg4 := *cfg1
   173  	cfg4.AdvertisePeerUrls = "127.0.0.1:\n8291"
   174  	_, err = cfg4.genEmbedEtcdConfig(embed.NewConfig())
   175  	c.Assert(terror.ErrMasterGenEmbedEtcdConfigFail.Equal(err), check.IsTrue)
   176  	c.Assert(err, check.ErrorMatches, "(?m).*invalid advertise-peer-urls.*")
   177  	cfg4.AdvertisePeerUrls = "http://172.100.8.8:8291"
   178  	etcdCfg, err = cfg4.genEmbedEtcdConfig(embed.NewConfig())
   179  	c.Assert(err, check.IsNil)
   180  	c.Assert(etcdCfg.AdvertisePeerUrls, check.DeepEquals, []url.URL{{Scheme: "http", Host: "172.100.8.8:8291"}})
   181  }
   182  
   183  func (t *testConfigSuite) TestParseURLs(c *check.C) {
   184  	cases := []struct {
   185  		str    string
   186  		urls   []url.URL
   187  		hasErr bool
   188  	}{
   189  		{}, // empty str
   190  		{
   191  			str:  "http://127.0.0.1:8291",
   192  			urls: []url.URL{{Scheme: "http", Host: "127.0.0.1:8291"}},
   193  		},
   194  		{
   195  			str:  "https://127.0.0.1:8291",
   196  			urls: []url.URL{{Scheme: "https", Host: "127.0.0.1:8291"}},
   197  		},
   198  		{
   199  			str: "http://127.0.0.1:8291,http://127.0.0.1:18291",
   200  			urls: []url.URL{
   201  				{Scheme: "http", Host: "127.0.0.1:8291"},
   202  				{Scheme: "http", Host: "127.0.0.1:18291"},
   203  			},
   204  		},
   205  		{
   206  			str: "https://127.0.0.1:8291,https://127.0.0.1:18291",
   207  			urls: []url.URL{
   208  				{Scheme: "https", Host: "127.0.0.1:8291"},
   209  				{Scheme: "https", Host: "127.0.0.1:18291"},
   210  			},
   211  		},
   212  		{
   213  			str:  "127.0.0.1:8291", // no scheme
   214  			urls: []url.URL{{Scheme: "http", Host: "127.0.0.1:8291"}},
   215  		},
   216  		{
   217  			str:  "http://:8291", // no IP
   218  			urls: []url.URL{{Scheme: "http", Host: "0.0.0.0:8291"}},
   219  		},
   220  		{
   221  			str:  ":8291", // no scheme, no IP
   222  			urls: []url.URL{{Scheme: "http", Host: "0.0.0.0:8291"}},
   223  		},
   224  		{
   225  			str:  "http://", // no IP, no port
   226  			urls: []url.URL{{Scheme: "http", Host: ""}},
   227  		},
   228  		{
   229  			str:    "http://\n127.0.0.1:8291", // invalid char in URL
   230  			hasErr: true,
   231  		},
   232  		{
   233  			str: ":8291,http://127.0.0.1:18291",
   234  			urls: []url.URL{
   235  				{Scheme: "http", Host: "0.0.0.0:8291"},
   236  				{Scheme: "http", Host: "127.0.0.1:18291"},
   237  			},
   238  		},
   239  		{
   240  			str: "LancedeMacBook-Pro.local:8661",
   241  			urls: []url.URL{
   242  				{Scheme: "http", Host: "LancedeMacBook-Pro.local:8661"},
   243  			},
   244  		},
   245  	}
   246  
   247  	for _, cs := range cases {
   248  		c.Logf("raw string %s", cs.str)
   249  		urls, err := parseURLs(cs.str)
   250  		if cs.hasErr {
   251  			c.Assert(terror.ErrMasterParseURLFail.Equal(err), check.IsTrue)
   252  		} else {
   253  			c.Assert(err, check.IsNil)
   254  			c.Assert(urls, check.DeepEquals, cs.urls)
   255  		}
   256  	}
   257  }
   258  
   259  func (t *testConfigSuite) TestAdjustAddr(c *check.C) {
   260  	cfg := NewConfig()
   261  	c.Assert(cfg.FromContent(SampleConfig), check.IsNil)
   262  	c.Assert(cfg.adjust(), check.IsNil)
   263  
   264  	// invalid `advertise-addr`
   265  	cfg.AdvertiseAddr = "127.0.0.1"
   266  	c.Assert(terror.ErrMasterAdvertiseAddrNotValid.Equal(cfg.adjust()), check.IsTrue)
   267  	cfg.AdvertiseAddr = "0.0.0.0:8261"
   268  	c.Assert(terror.ErrMasterAdvertiseAddrNotValid.Equal(cfg.adjust()), check.IsTrue)
   269  
   270  	// clear `advertise-addr`, still invalid because no `host` in `master-addr`.
   271  	cfg.AdvertiseAddr = ""
   272  	c.Assert(terror.ErrMasterHostPortNotValid.Equal(cfg.adjust()), check.IsTrue)
   273  
   274  	cfg.MasterAddr = "127.0.0.1:8261"
   275  	c.Assert(cfg.adjust(), check.IsNil)
   276  	c.Assert(cfg.AdvertiseAddr, check.Equals, cfg.MasterAddr)
   277  }
   278  
   279  func (t *testConfigSuite) TestAdjustOpenAPI(c *check.C) {
   280  	cfg := NewConfig()
   281  	c.Assert(cfg.FromContent(SampleConfig), check.IsNil)
   282  	c.Assert(cfg.adjust(), check.IsNil)
   283  
   284  	// test default value
   285  	c.Assert(cfg.OpenAPI, check.Equals, false)
   286  	c.Assert(cfg.ExperimentalFeatures.OpenAPI, check.Equals, false)
   287  
   288  	// adjust openapi from experimental-features
   289  	cfg.ExperimentalFeatures.OpenAPI = true
   290  	c.Assert(cfg.adjust(), check.IsNil)
   291  	c.Assert(cfg.OpenAPI, check.Equals, true)
   292  	c.Assert(cfg.ExperimentalFeatures.OpenAPI, check.Equals, false)
   293  
   294  	// test from flags
   295  	c.Assert(cfg.Parse([]string{"--openapi=false", "--master-addr=127.0.0.1:8261"}), check.IsNil)
   296  	c.Assert(cfg.adjust(), check.IsNil)
   297  	c.Assert(cfg.OpenAPI, check.Equals, false)
   298  	c.Assert(cfg.Parse([]string{"--openapi=true", "--master-addr=127.0.0.1:8261"}), check.IsNil)
   299  	c.Assert(cfg.adjust(), check.IsNil)
   300  	c.Assert(cfg.OpenAPI, check.Equals, true)
   301  }
   302  
   303  func TestAdjustSecretKeyPath(t *testing.T) {
   304  	cfg := &Config{}
   305  	require.NoError(t, cfg.adjustSecretKeyPath())
   306  
   307  	// non exist file
   308  	dir := t.TempDir()
   309  	cfg.SecretKeyPath = filepath.Join(dir, "non-exist")
   310  	err := cfg.adjustSecretKeyPath()
   311  	require.True(t, terror.ErrConfigSecretKeyPath.Equal(err))
   312  	require.ErrorContains(t, err, "no such file")
   313  	require.Nil(t, cfg.SecretKey)
   314  
   315  	// not hex string
   316  	require.NoError(t, os.WriteFile(filepath.Join(dir, "secret"), []byte("secret"), 0o644))
   317  	cfg.SecretKeyPath = filepath.Join(dir, "secret")
   318  	err = cfg.adjustSecretKeyPath()
   319  	require.True(t, terror.ErrConfigSecretKeyPath.Equal(err))
   320  	require.ErrorContains(t, err, "encoding/hex: invalid byte")
   321  	require.Nil(t, cfg.SecretKey)
   322  
   323  	// not enough length
   324  	require.NoError(t, os.WriteFile(filepath.Join(dir, "secret-2"), []byte("2334"), 0o644))
   325  	cfg.SecretKeyPath = filepath.Join(dir, "secret-2")
   326  	err = cfg.adjustSecretKeyPath()
   327  	require.True(t, terror.ErrConfigSecretKeyPath.Equal(err))
   328  	require.ErrorContains(t, err, "hex AES-256 key of length 64")
   329  	require.Nil(t, cfg.SecretKey)
   330  
   331  	// works
   332  	key := make([]byte, 32)
   333  	_, err = rand.Read(key)
   334  	require.NoError(t, err)
   335  	require.NoError(t, os.WriteFile(filepath.Join(dir, "secret-3"), []byte(" \t"+hex.EncodeToString(key)+"\r\n   \n"), 0o644))
   336  	cfg.SecretKeyPath = filepath.Join(dir, "secret-3")
   337  	require.NoError(t, cfg.adjustSecretKeyPath())
   338  	require.Equal(t, key, cfg.SecretKey)
   339  }