Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.

### Fixed

- `tt cluster publish` / `tt cluster show`: fix a connection timeout to an
`https://` Tarantool Config Storage or etcd endpoint with SSL enabled but no
client certificate configured.

## [2.14.0] - 2026-08-06

This release introduces cluster backup and restore: `tt backup` plans a
Expand Down
13 changes: 12 additions & 1 deletion lib/cluster/etcd.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ type EtcdOpts struct {
func ConnectEtcd(opts EtcdOpts) (*clientv3.Client, error) {
var tlsConfig *tls.Config = nil
if opts.KeyFile != "" || opts.CertFile != "" || opts.CaFile != "" ||
opts.CaPath != "" || opts.SkipHostVerify {
opts.CaPath != "" || opts.SkipHostVerify || hasSecureEndpoint(opts.Endpoints) {

tlsInfo := transport.TLSInfo{
CertFile: opts.CertFile,
Expand Down Expand Up @@ -89,6 +89,17 @@ func ConnectEtcd(opts EtcdOpts) (*clientv3.Client, error) {
})
}

// hasSecureEndpoint returns true if any of the endpoints uses the "https"
// scheme.
func hasSecureEndpoint(endpoints []string) bool {
for _, endpoint := range endpoints {
if strings.HasPrefix(endpoint, "https://") {
return true
}
}
return false
}

// EtcdGetter is the interface that wraps get from etcd method.
type EtcdGetter interface {
// Get retrieves key-value pairs for a key.
Expand Down
25 changes: 25 additions & 0 deletions lib/cluster/etcd_internal_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package cluster

import "testing"

func TestHasSecureEndpoint(t *testing.T) {
cases := []struct {
name string
endpoints []string
expected bool
}{
{"empty", nil, false},
{"plain http", []string{"http://localhost:2379"}, false},
{"plain no scheme", []string{"localhost:2379"}, false},
{"single https", []string{"https://localhost:2379"}, true},
{"mixed", []string{"http://localhost:2379", "https://localhost:2380"}, true},
}

for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
if got := hasSecureEndpoint(tc.endpoints); got != tc.expected {
t.Errorf("hasSecureEndpoint(%v) = %v, want %v", tc.endpoints, got, tc.expected)
}
})
}
}
5 changes: 5 additions & 0 deletions lib/cluster/tarantool.go
Original file line number Diff line number Diff line change
Expand Up @@ -581,6 +581,11 @@ func ConnectTarantool(uriOpts libconnect.UriOpts,
SslCaFile: uriOpts.CaFile,
SslCiphers: uriOpts.Ciphers,
}
if uriOpts.Scheme == "https" {
// The "https" scheme means the user explicitly requested a secure
// connection, even if no ssl_* URI parameters were passed.
dialOpts.Transport = "ssl"
}

dialer, err := dial.New(dialOpts)
if err != nil {
Expand Down
3 changes: 3 additions & 0 deletions lib/connect/uri.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,8 @@ const (
type UriOpts struct {
// Endpoint is a an endpoint to connect: [scheme://]host[:port].
Endpoint string
// Scheme is a scheme part of the URI (e.g. "tcp", "https").
Scheme string
// Host is a an address to connect: host[:port].
Host string
// Prefix is a configuration prefix.
Expand Down Expand Up @@ -235,6 +237,7 @@ func parseUriOpts(uri *url.URL) (UriOpts, error) {
opts := UriOpts{
Endpoint: endpoint.String(),
Host: uri.Host,
Scheme: uri.Scheme,
Prefix: uri.Path,
Tag: uri.Fragment,
Username: uri.User.Username(),
Expand Down
23 changes: 23 additions & 0 deletions lib/connect/uri_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -285,6 +285,7 @@ func TestParseUriOpts(t *testing.T) {
Url: "scheme://localhost",
Opts: connect.UriOpts{
Endpoint: "scheme://localhost",
Scheme: "scheme",
Host: "localhost",
Timeout: defaultTimeout,
},
Expand All @@ -294,6 +295,7 @@ func TestParseUriOpts(t *testing.T) {
Url: "scheme://localhost:3013",
Opts: connect.UriOpts{
Endpoint: "scheme://localhost:3013",
Scheme: "scheme",
Host: "localhost:3013",
Timeout: defaultTimeout,
},
Expand All @@ -303,6 +305,7 @@ func TestParseUriOpts(t *testing.T) {
Url: "scheme://user@localhost",
Opts: connect.UriOpts{
Endpoint: "scheme://localhost",
Scheme: "scheme",
Host: "localhost",
Username: "user",
Timeout: defaultTimeout,
Expand All @@ -313,6 +316,7 @@ func TestParseUriOpts(t *testing.T) {
Url: "scheme://user:pass@localhost",
Opts: connect.UriOpts{
Endpoint: "scheme://localhost",
Scheme: "scheme",
Host: "localhost",
Username: "user",
Password: "pass",
Expand All @@ -324,6 +328,7 @@ func TestParseUriOpts(t *testing.T) {
Url: "scheme://localhost/",
Opts: connect.UriOpts{
Endpoint: "scheme://localhost",
Scheme: "scheme",
Host: "localhost",
Prefix: "/",
Timeout: defaultTimeout,
Expand All @@ -334,6 +339,7 @@ func TestParseUriOpts(t *testing.T) {
Url: "scheme://localhost/prefix",
Opts: connect.UriOpts{
Endpoint: "scheme://localhost",
Scheme: "scheme",
Host: "localhost",
Prefix: "/prefix",
Timeout: defaultTimeout,
Expand All @@ -344,6 +350,7 @@ func TestParseUriOpts(t *testing.T) {
Url: "scheme://localhost/prefix#Fragment",
Opts: connect.UriOpts{
Endpoint: "scheme://localhost",
Scheme: "scheme",
Host: "localhost",
Prefix: "/prefix",
Tag: "Fragment",
Expand All @@ -355,6 +362,7 @@ func TestParseUriOpts(t *testing.T) {
Url: "scheme://localhost#Fragment",
Opts: connect.UriOpts{
Endpoint: "scheme://localhost",
Scheme: "scheme",
Host: "localhost",
Tag: "Fragment",
Timeout: defaultTimeout,
Expand All @@ -365,6 +373,7 @@ func TestParseUriOpts(t *testing.T) {
Url: "scheme://localhost/prefix?key=anykey",
Opts: connect.UriOpts{
Endpoint: "scheme://localhost",
Scheme: "scheme",
Host: "localhost",
Prefix: "/prefix",
Timeout: defaultTimeout,
Expand All @@ -376,6 +385,7 @@ func TestParseUriOpts(t *testing.T) {
Url: "scheme://localhost/prefix?name=anyname",
Opts: connect.UriOpts{
Endpoint: "scheme://localhost",
Scheme: "scheme",
Host: "localhost",
Prefix: "/prefix",
Timeout: defaultTimeout,
Expand All @@ -387,6 +397,7 @@ func TestParseUriOpts(t *testing.T) {
Url: "scheme://localhost?name=anyname#Fragment",
Opts: connect.UriOpts{
Endpoint: "scheme://localhost",
Scheme: "scheme",
Host: "localhost",
Tag: "Fragment",
Timeout: defaultTimeout,
Expand All @@ -398,6 +409,7 @@ func TestParseUriOpts(t *testing.T) {
Url: "scheme://localhost/prefix?name=",
Opts: connect.UriOpts{
Endpoint: "scheme://localhost",
Scheme: "scheme",
Host: "localhost",
Prefix: "/prefix",
Timeout: defaultTimeout,
Expand All @@ -409,6 +421,7 @@ func TestParseUriOpts(t *testing.T) {
Url: "scheme://localhost?ssl_key_file=/any/kfile",
Opts: connect.UriOpts{
Endpoint: "scheme://localhost",
Scheme: "scheme",
Host: "localhost",
KeyFile: "/any/kfile",
Timeout: defaultTimeout,
Expand All @@ -419,6 +432,7 @@ func TestParseUriOpts(t *testing.T) {
Url: "scheme://localhost?ssl_cert_file=/any/certfile",
Opts: connect.UriOpts{
Endpoint: "scheme://localhost",
Scheme: "scheme",
Host: "localhost",
CertFile: "/any/certfile",
Timeout: defaultTimeout,
Expand All @@ -429,6 +443,7 @@ func TestParseUriOpts(t *testing.T) {
Url: "scheme://localhost?ssl_ca_path=/any/capath",
Opts: connect.UriOpts{
Endpoint: "scheme://localhost",
Scheme: "scheme",
Host: "localhost",
CaPath: "/any/capath",
Timeout: defaultTimeout,
Expand All @@ -439,6 +454,7 @@ func TestParseUriOpts(t *testing.T) {
Url: "scheme://localhost?ssl_ca_file=/any/cafile",
Opts: connect.UriOpts{
Endpoint: "scheme://localhost",
Scheme: "scheme",
Host: "localhost",
CaFile: "/any/cafile",
Timeout: defaultTimeout,
Expand All @@ -449,6 +465,7 @@ func TestParseUriOpts(t *testing.T) {
Url: "scheme://localhost?verify_peer=true&verify_host=true",
Opts: connect.UriOpts{
Endpoint: "scheme://localhost",
Scheme: "scheme",
Host: "localhost",
Timeout: defaultTimeout,
},
Expand All @@ -458,6 +475,7 @@ func TestParseUriOpts(t *testing.T) {
Url: "scheme://localhost?verify_peer=&verify_host=",
Opts: connect.UriOpts{
Endpoint: "scheme://localhost",
Scheme: "scheme",
Host: "localhost",
Timeout: defaultTimeout,
},
Expand All @@ -467,6 +485,7 @@ func TestParseUriOpts(t *testing.T) {
Url: "scheme://localhost?verify_peer=false",
Opts: connect.UriOpts{
Endpoint: "scheme://localhost",
Scheme: "scheme",
Host: "localhost",
SkipPeerVerify: true,
Timeout: defaultTimeout,
Expand All @@ -482,6 +501,7 @@ func TestParseUriOpts(t *testing.T) {
Url: "scheme://localhost?verify_host=false",
Opts: connect.UriOpts{
Endpoint: "scheme://localhost",
Scheme: "scheme",
Host: "localhost",
SkipHostVerify: true,
Timeout: defaultTimeout,
Expand All @@ -497,6 +517,7 @@ func TestParseUriOpts(t *testing.T) {
Url: "scheme://localhost?timeout=5.5",
Opts: connect.UriOpts{
Endpoint: "scheme://localhost",
Scheme: "scheme",
Host: "localhost",
Timeout: time.Duration(float64(5.5) * float64(time.Second)),
},
Expand All @@ -506,6 +527,7 @@ func TestParseUriOpts(t *testing.T) {
Url: "scheme://localhost?timeout=",
Opts: connect.UriOpts{
Endpoint: "scheme://localhost",
Scheme: "scheme",
Host: "localhost",
Timeout: defaultTimeout,
},
Expand All @@ -526,6 +548,7 @@ func TestParseUriOpts(t *testing.T) {
"#Fragment",
Opts: connect.UriOpts{
Endpoint: "scheme://localhost:2012",
Scheme: "scheme",
Host: "localhost:2012",
Prefix: "/prefix",
Tag: "Fragment",
Expand Down
Loading