Skip to content

Commit

Permalink
cluster: collect configuration from tarantool
Browse files Browse the repository at this point in the history
@TarantoolBot document
Title: tt cluster show collects data from tarantool config storage

The patch adds ability to collect configuration from tarantool
config storage when showing an application/instance configuration.

The patch adds a new argument `ssl_ciphers` for `tt cluster show` and
`tt cluster publish` that could be specified via URI:

* ssl_siphers - a colon-separated (:) list of SSL cipher suites
the connection can use.

It works only with tarantool config storage at now.

As example:

```
tt cluster show tcp://login:pass@localhost:3013/prefix?ssl_ciphers=TLS_AES_128_GCM_SHA256
```

Follows up tarantool/tarantool#9410
Closes #684
  • Loading branch information
oleg-jukovec authored and LeonidVas committed Dec 14, 2023
1 parent 4d1a149 commit fab1554
Show file tree
Hide file tree
Showing 9 changed files with 343 additions and 9 deletions.
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ running apps.
- `tt env`: add current environment binaries location to the PATH variable.
- `tt cluster`: add an ability to specify a key for `show`/`publish` via URI.
- `tt cluster`: add an ability to publish/show configuration from tarantool
config storage via URI.
config storage.

## [2.0.0] - 2023-11-13

Expand Down
85 changes: 84 additions & 1 deletion cli/cluster/cluster.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ import (
"time"

"gopkg.in/yaml.v2"

"github.com/tarantool/tt/cli/connect"
"github.com/tarantool/tt/cli/connector"
)

const (
Expand Down Expand Up @@ -153,6 +156,24 @@ type ClusterConfig struct {
} `yaml:"request"`
} `yaml:"http"`
} `yaml:"etcd"`
Storage struct {
Prefix string `yaml:"prefix"`
Timeout float64 `yaml:"timeout"`
Endpoints []struct {
Uri string `yaml:"uri"`
Login string `yaml:"login"`
Password string `yaml:"password"`
Params struct {
Transport string `yaml:"transport"`
SslKeyFile string `yaml:"ssl_key_file"`
SslCertFile string `yaml:"ssl_cert_file"`
SslCaFile string `yaml:"ssl_ca_file"`
SslCiphers string `yaml:"ssl_ciphers"`
SslPassword string `yaml:"ssl_password"`
SslPasswordFile string `yaml:"ssl_password_file"`
} `yaml:"params"`
} `yaml:"endpoints"`
} `yaml:"storage"`
} `yaml:"config"`
// RawConfig is a configuration of the global scope.
RawConfig *Config `yaml:"-"`
Expand Down Expand Up @@ -305,13 +326,67 @@ func collectEtcdConfig(clusterConfig ClusterConfig) (*Config, error) {
if err != nil {
return nil, fmt.Errorf("unable to connect to etcd: %w", err)
}
defer etcd.Close()

etcdCollector := NewEtcdAllCollector(etcd, etcdConfig.Prefix, opts.Timeout)
etcdRawConfig, err := etcdCollector.Collect()
if err != nil {
return nil, fmt.Errorf("unable to get config from etcd: %w", err)
}
return etcdRawConfig, err
return etcdRawConfig, nil
}

// collectTarantoolConfig collects a configuration from tarantool config
// storage with options from the tarantool configuration.
func collectTarantoolConfig(clusterConfig ClusterConfig) (*Config, error) {
tarantoolConfig := clusterConfig.Config.Storage
var opts []connector.ConnectOpts
for _, endpoint := range tarantoolConfig.Endpoints {
var network, address string
if !connect.IsBaseURI(endpoint.Uri) {
network = connector.TCPNetwork
address = endpoint.Uri
} else {
network, address = connect.ParseBaseURI(endpoint.Uri)
}

if endpoint.Params.Transport == "" || endpoint.Params.Transport != "ssl" {
opts = append(opts, connector.ConnectOpts{
Network: network,
Address: address,
Username: endpoint.Login,
Password: endpoint.Password,
})
} else {
opts = append(opts, connector.ConnectOpts{
Network: network,
Address: address,
Username: endpoint.Login,
Password: endpoint.Password,
Ssl: connector.SslOpts{
KeyFile: endpoint.Params.SslKeyFile,
CertFile: endpoint.Params.SslCertFile,
CaFile: endpoint.Params.SslCaFile,
Ciphers: endpoint.Params.SslCiphers,
},
})
}
}

pool, err := connector.ConnectPool(opts)
if err != nil {
return nil, fmt.Errorf("unable to connect to tarantool config storage: %w", err)
}
defer pool.Close()

tarantoolCollector := NewTarantoolAllCollector(pool,
tarantoolConfig.Prefix,
time.Duration(tarantoolConfig.Timeout*float64(time.Second)))
tarantoolRawConfig, err := tarantoolCollector.Collect()
if err != nil {
return nil, fmt.Errorf("failed to get config from tarantool config storage: %w", err)
}
return tarantoolRawConfig, nil
}

// GetClusterConfig returns a cluster configuration loaded from a path to
Expand Down Expand Up @@ -353,6 +428,14 @@ func GetClusterConfig(path string) (ClusterConfig, error) {
config.Merge(etcdConfig)
}

if len(clusterConfig.Config.Storage.Endpoints) > 0 {
tarantoolConfig, err := collectTarantoolConfig(clusterConfig)
if err != nil {
return ret, err
}
config.Merge(tarantoolConfig)
}

defaultEnvConfig, err := defaultEnvCollector.Collect()
if err != nil {
fmtErr := "failed to collect a config from default environment variables: %w"
Expand Down
68 changes: 61 additions & 7 deletions cli/cluster/cluster_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -91,17 +91,47 @@ func TestMakeClusterConfig_settings(t *testing.T) {
expected.RawConfig = config
expected.Groups = nil
expected.Config.Etcd.Endpoints = []string{"a", "b"}
expected.Config.Etcd.Username = "user"
expected.Config.Etcd.Password = "pass"
expected.Config.Etcd.Prefix = "/prefix"
expected.Config.Etcd.Ssl.KeyFile = "keyfile"
expected.Config.Etcd.Ssl.CertFile = "certfile"
expected.Config.Etcd.Ssl.CaPath = "cafile"
expected.Config.Etcd.Ssl.CaFile = "capath"
expected.Config.Etcd.Username = "etcd_user"
expected.Config.Etcd.Password = "etcd_pass"
expected.Config.Etcd.Prefix = "/etcd_prefix"
expected.Config.Etcd.Ssl.KeyFile = "etcd_keyfile"
expected.Config.Etcd.Ssl.CertFile = "etcd_certfile"
expected.Config.Etcd.Ssl.CaPath = "etcd_cafile"
expected.Config.Etcd.Ssl.CaFile = "etcd_capath"
expected.Config.Etcd.Ssl.VerifyPeer = true
expected.Config.Etcd.Ssl.VerifyHost = true
expected.Config.Etcd.Http.Request.Timeout = 123

expected.Config.Storage.Prefix = "/tt_prefix"
expected.Config.Storage.Timeout = 234
expected.Config.Storage.Endpoints = []struct {
Uri string `yaml:"uri"`
Login string `yaml:"login"`
Password string `yaml:"password"`
Params struct {
Transport string `yaml:"transport"`
SslKeyFile string `yaml:"ssl_key_file"`
SslCertFile string `yaml:"ssl_cert_file"`
SslCaFile string `yaml:"ssl_ca_file"`
SslCiphers string `yaml:"ssl_ciphers"`
SslPassword string `yaml:"ssl_password"`
SslPasswordFile string `yaml:"ssl_password_file"`
} `yaml:"params"`
}{
{
Uri: "tt_uri",
Login: "tt_login",
Password: "tt_password",
},
}
expected.Config.Storage.Endpoints[0].Params.Transport = "tt_transport"
expected.Config.Storage.Endpoints[0].Params.SslKeyFile = "tt_key_file"
expected.Config.Storage.Endpoints[0].Params.SslCertFile = "tt_cert_file"
expected.Config.Storage.Endpoints[0].Params.SslCaFile = "tt_ca_file"
expected.Config.Storage.Endpoints[0].Params.SslCiphers = "tt_ciphers"
expected.Config.Storage.Endpoints[0].Params.SslPassword = "tt_password"
expected.Config.Storage.Endpoints[0].Params.SslPasswordFile = "tt_password_file"

config.Set([]string{"config", "etcd", "endpoints"},
[]any{expected.Config.Etcd.Endpoints[0], expected.Config.Etcd.Endpoints[1]})
config.Set([]string{"config", "etcd", "username"},
Expand All @@ -125,6 +155,30 @@ func TestMakeClusterConfig_settings(t *testing.T) {
config.Set([]string{"config", "etcd", "http", "request", "timeout"},
int(expected.Config.Etcd.Http.Request.Timeout))

config.Set([]string{"config", "storage", "prefix"},
expected.Config.Storage.Prefix)
config.Set([]string{"config", "storage", "timeout"},
int(expected.Config.Storage.Timeout))

config.Set([]string{"config", "storage", "endpoints"},
[]any{
map[any]any{
"uri": "tt_uri",
"login": "tt_login",
"password": "tt_password",
"params": map[any]any{
"transport": "tt_transport",
"ssl_key_file": "tt_key_file",
"ssl_cert_file": "tt_cert_file",
"ssl_ca_file": "tt_ca_file",
"ssl_ciphers": "tt_ciphers",
"ssl_password": "tt_password",
"ssl_password_file": "tt_password_file",
},
},
},
)

cconfig, err := cluster.MakeClusterConfig(config)
require.NoError(t, err)
assert.Equal(t, expected.Config, cconfig.Config)
Expand Down
5 changes: 5 additions & 0 deletions cli/cluster/cmd/uri.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,9 @@ type UriOpts struct {
CaPath string
// CaFile is a path to a trusted certificate authorities (CA) file.
CaFile string
// Ciphers is a colon-separated (:) list of SSL cipher suites the
// connection can use.
Ciphers string
// SkipHostVerify controls whether a client verifies the server's
// host name. This is dangerous option so by default it is false.
SkipHostVerify bool
Expand Down Expand Up @@ -72,6 +75,7 @@ func ParseUriOpts(uri *url.URL) (UriOpts, error) {
CertFile: values.Get("ssl_cert_file"),
CaPath: values.Get("ssl_ca_path"),
CaFile: values.Get("ssl_ca_file"),
Ciphers: values.Get("ssl_ciphers"),
Timeout: DefaultUriTimeout,
}
if password, ok := uri.User.Password(); ok {
Expand Down Expand Up @@ -150,6 +154,7 @@ func MakeConnectOptsFromUriOpts(src UriOpts) connector.ConnectOpts {
KeyFile: src.KeyFile,
CertFile: src.CertFile,
CaFile: src.CaFile,
Ciphers: src.Ciphers,
},
}
}
5 changes: 5 additions & 0 deletions cli/cluster/cmd/uri_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,7 @@ func TestParseUriOpts(t *testing.T) {
"?key=anykey&name=anyname" +
"&ssl_key_file=kfile&ssl_cert_file=certfile" +
"&ssl_ca_path=capath&ssl_ca_file=cafile" +
"&ssl_ciphers=foo:bar:ciphers" +
"&verify_peer=true&verify_host=false&timeout=2",
Opts: cmd.UriOpts{
Endpoint: "scheme://localhost:2012",
Expand All @@ -228,6 +229,7 @@ func TestParseUriOpts(t *testing.T) {
CertFile: "certfile",
CaPath: "capath",
CaFile: "cafile",
Ciphers: "foo:bar:ciphers",
SkipHostVerify: true,
Timeout: time.Duration(2 * time.Second),
},
Expand Down Expand Up @@ -268,6 +270,7 @@ func TestMakeEtcdOptsFromUriOpts(t *testing.T) {
Prefix: "foo",
Key: "bar",
Instance: "zoo",
Ciphers: "foo:bar:ciphers",
},
Expected: cluster.EtcdOpts{},
},
Expand Down Expand Up @@ -373,6 +376,7 @@ func TestMakeConnectOptsFromUriOpts(t *testing.T) {
CertFile: "cert_file",
CaPath: "ca_path",
CaFile: "ca_file",
Ciphers: "foo:bar:ciphers",
SkipHostVerify: true,
SkipPeerVerify: true,
Timeout: 2 * time.Second,
Expand All @@ -386,6 +390,7 @@ func TestMakeConnectOptsFromUriOpts(t *testing.T) {
KeyFile: "key_file",
CertFile: "cert_file",
CaFile: "ca_file",
Ciphers: "foo:bar:ciphers",
},
},
},
Expand Down
1 change: 1 addition & 0 deletions cli/cmd/cluster.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ Possible arguments:
* ssl_cert_file - a path to an SSL certificate file.
* ssl_ca_file - a path to a trusted certificate authorities (CA) file.
* ssl_ca_path - a path to a trusted certificate authorities (CA) directory.
* ssl_ciphers - a colon-separated (:) list of SSL cipher suites the connection can use.
* verify_host - set off (default true) verification of the certificate’s name against the host.
* verify_peer - set off (default true) verification of the peer’s SSL certificate.
Expand Down
76 changes: 76 additions & 0 deletions cli/connector/integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,82 @@ func TestConnect_textTls(t *testing.T) {
require.ErrorContains(t, err, expected)
}

var poolCases = []struct {
Name string
Opts []ConnectOpts
} {
{
Name: "single",
Opts: []ConnectOpts{
ConnectOpts{
Network: "tcp",
Address: "unreachetable",
Username: "test",
Password: "password",
},
ConnectOpts{
Network: "tcp",
Address: server,
Username: "test",
Password: "password",
},
},
},
{
Name: "with_invalid",
Opts: []ConnectOpts{
ConnectOpts{
Network: "tcp",
Address: server,
Username: "test",
Password: "password",
},
},
},
}

func TestPoolConnect_success(t *testing.T) {
for _, tc := range poolCases {
t.Run(tc.Name, func(t *testing.T) {
pool, err := ConnectPool(tc.Opts)
require.NoError(t, err)
require.NotNil(t, pool)
pool.Close()
})
}
}

func TestPoolEval_success(t *testing.T) {
for _, tc := range poolCases {
t.Run(tc.Name, func(t *testing.T) {
pool, err := ConnectPool(tc.Opts)
require.NoError(t, err)
require.NotNil(t, pool)
defer pool.Close()

ret, err := pool.Eval("return ...", []any{"foo"}, RequestOpts{})
assert.NoError(t, err)
assert.Equal(t, ret, []any{"foo"})
})
}
}

func TestPoolEval_error(t *testing.T) {
for _, tc := range poolCases {
t.Run(tc.Name, func(t *testing.T) {
pool, err := ConnectPool(tc.Opts)
require.NoError(t, err)
require.NotNil(t, pool)
defer pool.Close()

for i := 0; i < 10; i++ {
_, err = pool.Eval("error('foo')", []any{"foo"}, RequestOpts{})
assert.ErrorContains(t, err, "foo")
}
})
}
}

func runTestMain(m *testing.M) int {
inst, err := test_helpers.StartTarantool(test_helpers.StartOpts{
InitScript: "testdata/config.lua",
Expand Down
Loading

0 comments on commit fab1554

Please sign in to comment.