From 7959ac1b3461c4e0609196c92a5b1108fb303e1a Mon Sep 17 00:00:00 2001 From: Dmitriy Gertsog Date: Mon, 20 Jan 2025 17:08:52 +0300 Subject: [PATCH] lib: common cmd parts to connect package Moves the common URL parsing logic, connect to etcd and tarantool from the `cmd` package to the `connect` package. Part of #TNTP-1081 --- cli/cluster/cmd/common.go | 64 +---- cli/cluster/cmd/failover.go | 7 +- cli/cluster/cmd/publish.go | 10 +- cli/cluster/cmd/replicaset.go | 42 ++- cli/cluster/cmd/show.go | 10 +- cli/cmd/cluster.go | 4 +- lib/connect/etcd.go | 64 +++++ lib/connect/etcd_test.go | 90 +++++++ lib/connect/tarantool.go | 70 +++++ lib/connect/tarantool_test.go | 216 +++++++++++++++ .../cmd/uri.go => lib/connect/uriopts.go | 63 +---- .../connect/uriopts_test.go | 252 +++++------------- 12 files changed, 559 insertions(+), 333 deletions(-) create mode 100644 lib/connect/etcd.go create mode 100644 lib/connect/etcd_test.go create mode 100644 lib/connect/tarantool.go create mode 100644 lib/connect/tarantool_test.go rename cli/cluster/cmd/uri.go => lib/connect/uriopts.go (70%) rename cli/cluster/cmd/uri_test.go => lib/connect/uriopts_test.go (54%) diff --git a/cli/cluster/cmd/common.go b/cli/cluster/cmd/common.go index e20436330..0b0066712 100644 --- a/cli/cluster/cmd/common.go +++ b/cli/cluster/cmd/common.go @@ -3,7 +3,6 @@ package cmd import ( "errors" "fmt" - "os" clientv3 "go.etcd.io/etcd/client/v3" @@ -142,59 +141,17 @@ type connectOpts struct { Password string } -// connectTarantool establishes a connection to Tarantool. -func connectTarantool(uriOpts UriOpts, connOpts connectOpts) (tarantool.Connector, error) { - addr, connectorOpts := MakeConnectOptsFromUriOpts(uriOpts) - if connectorOpts.User == "" && connectorOpts.Pass == "" { - connectorOpts.User = connOpts.Username - connectorOpts.Pass = connOpts.Password - if connectorOpts.User == "" { - connectorOpts.User = os.Getenv(connect.TarantoolUsernameEnv) - } - if connectorOpts.Pass == "" { - connectorOpts.Pass = os.Getenv(connect.TarantoolPasswordEnv) - } - } - - conn, err := tarantool.Connect(addr, connectorOpts) - if err != nil { - return nil, fmt.Errorf("failed to connect to tarantool: %w", err) - } - return conn, nil -} - -// connectEtcd establishes a connection to etcd. -func connectEtcd(uriOpts UriOpts, connOpts connectOpts) (*clientv3.Client, error) { - etcdOpts := MakeEtcdOptsFromUriOpts(uriOpts) - if etcdOpts.Username == "" && etcdOpts.Password == "" { - etcdOpts.Username = connOpts.Username - etcdOpts.Password = connOpts.Password - if etcdOpts.Username == "" { - etcdOpts.Username = os.Getenv(connect.EtcdUsernameEnv) - } - if etcdOpts.Password == "" { - etcdOpts.Password = os.Getenv(connect.EtcdPasswordEnv) - } - } - - etcdcli, err := libcluster.ConnectEtcd(etcdOpts) - if err != nil { - return nil, fmt.Errorf("failed to connect to etcd: %w", err) - } - return etcdcli, nil -} - // doOnStorage determines a storage based on the opts. -func doOnStorage(connOpts connectOpts, opts UriOpts, - tarantoolFunc func(tarantool.Connector) error, etcdFunc func(*clientv3.Client) error) error { - etcdcli, errEtcd := connectEtcd(opts, connOpts) - if errEtcd == nil { - return etcdFunc(etcdcli) +func doOnStorage(opts connect.UriOpts, + tarantoolFunc connect.TarantoolFunc, etcdFunc connect.EtcdFunc) error { + done, errEtcd := connect.RunOnEtcd(opts, etcdFunc) + if done { + return errEtcd } - conn, errTarantool := connectTarantool(opts, connOpts) - if errTarantool == nil { - return tarantoolFunc(conn) + done, errTarantool := connect.RunOnTarantool(opts, tarantoolFunc) + if done { + return errTarantool } return fmt.Errorf("failed to establish a connection to tarantool or etcd: %w, %w", @@ -205,8 +162,7 @@ func doOnStorage(connOpts connectOpts, opts UriOpts, func createPublisherAndCollector( publishers libcluster.DataPublisherFactory, collectors libcluster.CollectorFactory, - connOpts connectOpts, - opts UriOpts) (libcluster.DataPublisher, libcluster.Collector, func(), error) { + opts connect.UriOpts) (libcluster.DataPublisher, libcluster.Collector, func(), error) { prefix, key, timeout := opts.Prefix, opts.Key, opts.Timeout var ( @@ -254,7 +210,7 @@ func createPublisherAndCollector( return nil } - if err := doOnStorage(connOpts, opts, tarantoolFunc, etcdFunc); err != nil { + if err := doOnStorage(opts, tarantoolFunc, etcdFunc); err != nil { return nil, nil, nil, err } diff --git a/cli/cluster/cmd/failover.go b/cli/cluster/cmd/failover.go index ccfbc5f50..4b7222a33 100644 --- a/cli/cluster/cmd/failover.go +++ b/cli/cluster/cmd/failover.go @@ -9,6 +9,7 @@ import ( "github.com/apex/log" "github.com/google/uuid" libcluster "github.com/tarantool/tt/lib/cluster" + "github.com/tarantool/tt/lib/connect" "go.etcd.io/etcd/api/v3/mvccpb" clientv3 "go.etcd.io/etcd/client/v3" "gopkg.in/yaml.v2" @@ -63,7 +64,7 @@ type SwitchStatusCtx struct { TaskID string } -func makeEtcdOpts(uriOpts UriOpts) libcluster.EtcdOpts { +func makeEtcdOpts(uriOpts connect.UriOpts) libcluster.EtcdOpts { opts := libcluster.EtcdOpts{ Endpoints: []string{uriOpts.Endpoint}, Username: uriOpts.Username, @@ -81,7 +82,7 @@ func makeEtcdOpts(uriOpts UriOpts) libcluster.EtcdOpts { // Switch master instance. func Switch(uri *url.URL, switchCtx SwitchCtx) error { - uriOpts, err := ParseUriOpts(uri) + uriOpts, err := connect.ParseUriOpts(uri, "", "") if err != nil { return fmt.Errorf("invalid URL %q: %w", uri, err) } @@ -173,7 +174,7 @@ func Switch(uri *url.URL, switchCtx SwitchCtx) error { // SwitchStatus shows master switching status. func SwitchStatus(uri *url.URL, switchCtx SwitchStatusCtx) error { - uriOpts, err := ParseUriOpts(uri) + uriOpts, err := connect.ParseUriOpts(uri, "", "") if err != nil { return fmt.Errorf("invalid URL %q: %w", uri, err) } diff --git a/cli/cluster/cmd/publish.go b/cli/cluster/cmd/publish.go index 5000ce876..3328c3f77 100644 --- a/cli/cluster/cmd/publish.go +++ b/cli/cluster/cmd/publish.go @@ -5,6 +5,7 @@ import ( "net/url" libcluster "github.com/tarantool/tt/lib/cluster" + "github.com/tarantool/tt/lib/connect" ) // PublishCtx contains information abould cluster publish command execution @@ -33,7 +34,8 @@ type PublishCtx struct { // PublishUri publishes a configuration to URI. func PublishUri(publishCtx PublishCtx, uri *url.URL) error { - uriOpts, err := ParseUriOpts(uri) + uriOpts, err := connect.ParseUriOpts(uri, + publishCtx.Username, publishCtx.Password) if err != nil { return fmt.Errorf("invalid URL %q: %w", uri, err) } @@ -43,14 +45,10 @@ func PublishUri(publishCtx PublishCtx, uri *url.URL) error { return err } - connOpts := connectOpts{ - Username: publishCtx.Username, - Password: publishCtx.Password, - } publisher, collector, cancel, err := createPublisherAndCollector( publishCtx.Publishers, publishCtx.Collectors, - connOpts, uriOpts) + uriOpts) if err != nil { return err } diff --git a/cli/cluster/cmd/replicaset.go b/cli/cluster/cmd/replicaset.go index f544d73d3..ec10e2a76 100644 --- a/cli/cluster/cmd/replicaset.go +++ b/cli/cluster/cmd/replicaset.go @@ -10,6 +10,7 @@ import ( "github.com/tarantool/go-tarantool" "github.com/tarantool/tt/cli/replicaset" libcluster "github.com/tarantool/tt/lib/cluster" + "github.com/tarantool/tt/lib/connect" clientv3 "go.etcd.io/etcd/client/v3" ) @@ -107,7 +108,7 @@ func pickPatchKey(keys []string, force bool, pathMsg string) (int, error) { func createDataCollectorAndKeyPublisher( collectors libcluster.DataCollectorFactory, publishers libcluster.DataPublisherFactory, - opts UriOpts, connOpts connectOpts) ( + opts connect.UriOpts) ( libcluster.DataCollector, replicaset.DataPublisher, func(), error) { prefix, key, timeout := opts.Prefix, opts.Key, opts.Timeout var ( @@ -145,7 +146,7 @@ func createDataCollectorAndKeyPublisher( return nil } - if err := doOnStorage(connOpts, opts, tarantoolFunc, etcdFunc); err != nil { + if err := doOnStorage(opts, tarantoolFunc, etcdFunc); err != nil { return nil, nil, nil, err } @@ -154,17 +155,14 @@ func createDataCollectorAndKeyPublisher( // Promote promotes an instance by patching the cluster config. func Promote(uri *url.URL, ctx PromoteCtx) error { - opts, err := ParseUriOpts(uri) + opts, err := connect.ParseUriOpts(uri, + ctx.Username, ctx.Password) if err != nil { return fmt.Errorf("invalid URL %q: %w", uri, err) } - connOpts := connectOpts{ - Username: ctx.Username, - Password: ctx.Password, - } collector, publisher, closeFunc, err := createDataCollectorAndKeyPublisher( - ctx.Collectors, ctx.Publishers, opts, connOpts) + ctx.Collectors, ctx.Publishers, opts) if err != nil { return err } @@ -201,17 +199,14 @@ type DemoteCtx struct { // Demote demotes an instance by patching the cluster config. func Demote(uri *url.URL, ctx DemoteCtx) error { - opts, err := ParseUriOpts(uri) + opts, err := connect.ParseUriOpts(uri, + ctx.Username, ctx.Password) if err != nil { return fmt.Errorf("invalid URL %q: %w", uri, err) } - connOpts := connectOpts{ - Username: ctx.Username, - Password: ctx.Password, - } collector, publisher, closeFunc, err := createDataCollectorAndKeyPublisher( - ctx.Collectors, ctx.Publishers, opts, connOpts) + ctx.Collectors, ctx.Publishers, opts) if err != nil { return err } @@ -248,17 +243,14 @@ type ExpelCtx struct { // Expel expels an instance by patching the cluster config. func Expel(uri *url.URL, ctx ExpelCtx) error { - opts, err := ParseUriOpts(uri) + opts, err := connect.ParseUriOpts(uri, + ctx.Username, ctx.Password) if err != nil { return fmt.Errorf("invalid URL %q: %w", uri, err) } - connOpts := connectOpts{ - Username: ctx.Username, - Password: ctx.Password, - } collector, publisher, closeFunc, err := createDataCollectorAndKeyPublisher( - ctx.Collectors, ctx.Publishers, opts, connOpts) + ctx.Collectors, ctx.Publishers, opts) if err != nil { return err } @@ -302,17 +294,15 @@ type RolesChangeCtx struct { // ChangeRole adds/removes a role by patching the cluster config. func ChangeRole(uri *url.URL, ctx RolesChangeCtx, action replicaset.RolesChangerAction) error { - opts, err := ParseUriOpts(uri) + opts, err := connect.ParseUriOpts(uri, + ctx.Username, ctx.Password) + if err != nil { return fmt.Errorf("invalid URL %q: %w", uri, err) } - connOpts := connectOpts{ - Username: ctx.Username, - Password: ctx.Password, - } collector, publisher, closeFunc, err := createDataCollectorAndKeyPublisher( - ctx.Collectors, ctx.Publishers, opts, connOpts) + ctx.Collectors, ctx.Publishers, opts) if err != nil { return err } diff --git a/cli/cluster/cmd/show.go b/cli/cluster/cmd/show.go index dbfe6036c..4a360b7a6 100644 --- a/cli/cluster/cmd/show.go +++ b/cli/cluster/cmd/show.go @@ -6,6 +6,7 @@ import ( "github.com/tarantool/tt/cli/cluster" libcluster "github.com/tarantool/tt/lib/cluster" + "github.com/tarantool/tt/lib/connect" ) // ShowCtx contains information about cluster show command execution context. @@ -23,19 +24,16 @@ type ShowCtx struct { // ShowUri shows a configuration from URI. func ShowUri(showCtx ShowCtx, uri *url.URL) error { - uriOpts, err := ParseUriOpts(uri) + uriOpts, err := connect.ParseUriOpts(uri, + showCtx.Username, showCtx.Password) if err != nil { return fmt.Errorf("invalid URL %q: %w", uri, err) } - connOpts := connectOpts{ - Username: showCtx.Username, - Password: showCtx.Password, - } _, collector, cancel, err := createPublisherAndCollector( nil, showCtx.Collectors, - connOpts, uriOpts) + uriOpts) if err != nil { return err } diff --git a/cli/cmd/cluster.go b/cli/cmd/cluster.go index 62323f7bc..6af37082b 100644 --- a/cli/cmd/cluster.go +++ b/cli/cmd/cluster.go @@ -100,7 +100,7 @@ You could also specify etcd/tarantool username and password with environment var The priority of credentials: environment variables < command flags < URL credentials. -`, float64(clustercmd.DefaultUriTimeout)/float64(time.Second), +`, float64(libconnect.DefaultUriTimeout)/float64(time.Second), libconnect.EtcdUsernameEnv, libconnect.EtcdPasswordEnv, libconnect.TarantoolUsernameEnv, libconnect.TarantoolPasswordEnv) failoverUriHelp = fmt.Sprintf( @@ -129,7 +129,7 @@ You could also specify etcd/tarantool username and password with environment var The priority of credentials: environment variables < command flags < URL credentials. -`, float64(clustercmd.DefaultUriTimeout)/float64(time.Second), +`, float64(libconnect.DefaultUriTimeout)/float64(time.Second), libconnect.EtcdUsernameEnv, libconnect.EtcdPasswordEnv, libconnect.TarantoolUsernameEnv, libconnect.TarantoolPasswordEnv) ) diff --git a/lib/connect/etcd.go b/lib/connect/etcd.go new file mode 100644 index 000000000..8b0fe8776 --- /dev/null +++ b/lib/connect/etcd.go @@ -0,0 +1,64 @@ +package connect + +import ( + "fmt" + "os" + + libcluster "github.com/tarantool/tt/lib/cluster" + clientv3 "go.etcd.io/etcd/client/v3" +) + +// EtcdFunc is a function that can be called on an `etcd` connection. +type EtcdFunc func(*clientv3.Client) error + +// makeEtcdOptsFromUriOpts create etcd connect options from URI options. +func makeEtcdOptsFromUriOpts(src UriOpts) libcluster.EtcdOpts { + var endpoints []string + if src.Endpoint != "" { + endpoints = []string{src.Endpoint} + } + + return libcluster.EtcdOpts{ + Endpoints: endpoints, + Username: src.Username, + Password: src.Password, + KeyFile: src.KeyFile, + CertFile: src.CertFile, + CaPath: src.CaPath, + CaFile: src.CaFile, + SkipHostVerify: src.SkipHostVerify || src.SkipPeerVerify, + Timeout: src.Timeout, + } +} + +// connectEtcd establishes a connection to etcd. +func connectEtcd(uriOpts UriOpts) (*clientv3.Client, error) { + etcdOpts := makeEtcdOptsFromUriOpts(uriOpts) + if etcdOpts.Username == "" && etcdOpts.Password == "" { + if etcdOpts.Username == "" { + etcdOpts.Username = os.Getenv(EtcdUsernameEnv) + } + if etcdOpts.Password == "" { + etcdOpts.Password = os.Getenv(EtcdPasswordEnv) + } + } + + c, err := libcluster.ConnectEtcd(etcdOpts) + if err != nil { + return nil, fmt.Errorf("failed to connect to etcd: %w", err) + } + return c, nil +} + +// RunOnEtcd runs the provided function with etcd connection. +// Returns true if the function was executed. +func RunOnEtcd(opts UriOpts, f EtcdFunc) (bool, error) { + if f != nil { + c, err := connectEtcd(opts) + if err != nil { + return false, err + } + return true, f(c) + } + return false, nil +} diff --git a/lib/connect/etcd_test.go b/lib/connect/etcd_test.go new file mode 100644 index 000000000..3d25e385c --- /dev/null +++ b/lib/connect/etcd_test.go @@ -0,0 +1,90 @@ +package connect + +import ( + "testing" + "time" + + "github.com/stretchr/testify/assert" + libcluster "github.com/tarantool/tt/lib/cluster" +) + +func TestMakeEtcdOptsFromUriOpts(t *testing.T) { + cases := []struct { + Name string + UriOpts UriOpts + Expected libcluster.EtcdOpts + }{ + { + Name: "empty", + UriOpts: UriOpts{}, + Expected: libcluster.EtcdOpts{}, + }, + { + Name: "ignored", + UriOpts: UriOpts{ + Host: "foo", + Prefix: "foo", + Key: "bar", + Instance: "zoo", + Ciphers: "foo:bar:ciphers", + }, + Expected: libcluster.EtcdOpts{}, + }, + { + Name: "skip_host_verify", + UriOpts: UriOpts{ + SkipHostVerify: true, + }, + Expected: libcluster.EtcdOpts{ + SkipHostVerify: true, + }, + }, + { + Name: "skip_peer_verify", + UriOpts: UriOpts{ + SkipPeerVerify: true, + }, + Expected: libcluster.EtcdOpts{ + SkipHostVerify: true, + }, + }, + { + Name: "full", + UriOpts: UriOpts{ + Endpoint: "foo", + Host: "host", + Prefix: "prefix", + Key: "key", + Instance: "instance", + Username: "username", + Password: "password", + KeyFile: "key_file", + CertFile: "cert_file", + CaPath: "ca_path", + CaFile: "ca_file", + SkipHostVerify: true, + SkipPeerVerify: true, + Timeout: 2 * time.Second, + }, + Expected: libcluster.EtcdOpts{ + Endpoints: []string{"foo"}, + Username: "username", + Password: "password", + KeyFile: "key_file", + CertFile: "cert_file", + CaPath: "ca_path", + CaFile: "ca_file", + SkipHostVerify: true, + Timeout: 2 * time.Second, + }, + }, + } + + for _, tc := range cases { + t.Run(tc.Name, func(t *testing.T) { + etcdOpts := makeEtcdOptsFromUriOpts(tc.UriOpts) + + assert.Equal(t, tc.Expected, etcdOpts) + }) + } +} diff --git a/lib/connect/tarantool.go b/lib/connect/tarantool.go new file mode 100644 index 000000000..b82575393 --- /dev/null +++ b/lib/connect/tarantool.go @@ -0,0 +1,70 @@ +package connect + +import ( + "fmt" + "os" + + "github.com/tarantool/go-tarantool" +) + +// TarantoolFunc is a function that can be called on a `Tarantool` connection. +type TarantoolFunc func(tarantool.Connector) error + +var ( + // tarantoolConnect used for mocking purposes. + tarantoolConnect = tarantool.Connect +) + +// makeConnectOptsFromUriOpts create Tarantool connect options from +// URI options. +func makeConnectOptsFromUriOpts(src UriOpts) (string, tarantool.Opts) { + opts := tarantool.Opts{ + User: src.Username, + Pass: src.Password, + Ssl: tarantool.SslOpts{ + KeyFile: src.KeyFile, + CertFile: src.CertFile, + CaFile: src.CaFile, + Ciphers: src.Ciphers, + }, + Timeout: src.Timeout, + } + + if opts.Ssl != (tarantool.SslOpts{}) { + opts.Transport = "ssl" + } + + return fmt.Sprintf("tcp://%s", src.Host), opts +} + +// connectTarantool establishes a connection to Tarantool. +func connectTarantool(uriOpts UriOpts) (tarantool.Connector, error) { + addr, connectorOpts := makeConnectOptsFromUriOpts(uriOpts) + if connectorOpts.User == "" && connectorOpts.Pass == "" { + if connectorOpts.User == "" { + connectorOpts.User = os.Getenv(TarantoolUsernameEnv) + } + if connectorOpts.Pass == "" { + connectorOpts.Pass = os.Getenv(TarantoolPasswordEnv) + } + } + + conn, err := tarantoolConnect(addr, connectorOpts) + if err != nil { + return nil, fmt.Errorf("failed to connect to tarantool: %w", err) + } + return conn, nil +} + +// RunOnTarantool runs the provided function with Tarantool connection. +// Returns true if the function was executed. +func RunOnTarantool(opts UriOpts, f TarantoolFunc) (bool, error) { + if f != nil { + conn, err := connectTarantool(opts) + if err != nil { + return false, err + } + return true, f(conn) + } + return false, nil +} diff --git a/lib/connect/tarantool_test.go b/lib/connect/tarantool_test.go new file mode 100644 index 000000000..26fe832a4 --- /dev/null +++ b/lib/connect/tarantool_test.go @@ -0,0 +1,216 @@ +package connect + +import ( + "errors" + "net/url" + "os" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/tarantool/go-tarantool" +) + +func TestMakeConnectOptsFromUriOpts(t *testing.T) { + cases := []struct { + Name string + UriOpts UriOpts + Expected tarantool.Opts + ExpectedAddr string + }{ + { + Name: "empty", + UriOpts: UriOpts{}, + Expected: tarantool.Opts{}, + ExpectedAddr: "tcp://", + }, + { + Name: "ignored", + UriOpts: UriOpts{ + Endpoint: "localhost:3013", + Prefix: "foo", + Key: "bar", + Instance: "zoo", + CaPath: "ca_path", + SkipHostVerify: true, + SkipPeerVerify: true, + Timeout: 673, + }, + Expected: tarantool.Opts{ + Timeout: 673, + }, + ExpectedAddr: "tcp://", // is this ok? + }, + { + Name: "full", + UriOpts: UriOpts{ + Endpoint: "scheme://foo", + Host: "foo", + Prefix: "prefix", + Key: "key", + Instance: "instance", + Username: "username", + Password: "password", + KeyFile: "key_file", + CertFile: "cert_file", + CaPath: "ca_path", + CaFile: "ca_file", + Ciphers: "foo:bar:ciphers", + SkipHostVerify: true, + SkipPeerVerify: true, + Timeout: 2 * time.Second, + }, + Expected: tarantool.Opts{ + User: "username", + Pass: "password", + Timeout: 2 * time.Second, + Transport: "ssl", + Ssl: tarantool.SslOpts{ + KeyFile: "key_file", + CertFile: "cert_file", + CaFile: "ca_file", + Ciphers: "foo:bar:ciphers", + }, + }, + ExpectedAddr: "tcp://foo", + }, + } + + for _, tc := range cases { + t.Run(tc.Name, func(t *testing.T) { + addr, tntOpts := makeConnectOptsFromUriOpts(tc.UriOpts) + + assert.Equal(t, tc.Expected, tntOpts) + assert.Equal(t, tc.ExpectedAddr, addr) + }) + } +} + +type mockConnector struct { + t *testing.T + addr string + opts tarantool.Opts + needErr bool +} + +func (m *mockConnector) Connect(addr string, opts tarantool.Opts) (conn *tarantool.Connection, err error) { + assert.Equal(m.t, m.addr, addr) + assert.Equal(m.t, m.opts, opts) + if m.needErr { + return nil, errors.New("connect error") + } + return nil, nil +} + +func TestRunOnTarantool(t *testing.T) { + type args struct { + url string + f TarantoolFunc + user string + pwd string + env map[string]string + } + tests := []struct { + name string + args args + addr string + opts tarantool.Opts + want bool + wantErr bool + }{ + { + "Nil function", + args{ + url: "http://localhost:1234", + f: nil, + }, + "tcp://localhost:1234", + tarantool.Opts{ + Timeout: DefaultUriTimeout, + }, + false, + false, + }, + { + "Function called", + args{ + url: "http://user:pass@localhost:1234", + f: func(c tarantool.Connector) error { + return nil + }, + }, + "tcp://localhost:1234", + tarantool.Opts{ + Timeout: DefaultUriTimeout, + User: "user", + Pass: "pass", + }, + true, + false, + }, + { + "Environment passed", + args{ + url: "http://localhost:1234", + f: func(c tarantool.Connector) error { + return nil + }, + env: map[string]string{ + TarantoolUsernameEnv: "env_user", + TarantoolPasswordEnv: "env_pass", + }, + }, + "tcp://localhost:1234", + tarantool.Opts{ + Timeout: DefaultUriTimeout, + User: "env_user", + Pass: "env_pass", + }, + true, + false, + }, + { + "Error connections", + args{ + url: "http://localhost:1234", + f: func(c tarantool.Connector) error { + return nil + }, + }, + "tcp://localhost:1234", + tarantool.Opts{ + Timeout: DefaultUriTimeout, + }, + false, + true, + }, + } + mc := mockConnector{t: t} + tarantoolConnect = mc.Connect + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + uri, err := url.Parse(tt.args.url) + require.NoError(t, err) + + opts, err := ParseUriOpts(uri, tt.args.user, tt.args.pwd) + require.NoError(t, err) + + for k, v := range tt.args.env { + require.NoError(t, os.Setenv(k, v)) + defer os.Unsetenv(k) + } + mc.addr = tt.addr + mc.opts = tt.opts + mc.needErr = tt.wantErr + got, err := RunOnTarantool(opts, tt.args.f) + if (err != nil) != tt.wantErr { + t.Errorf("RunOnTarantool() error = %v, wantErr %v", err, tt.wantErr) + return + } + if got != tt.want { + t.Errorf("RunOnTarantool() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/cli/cluster/cmd/uri.go b/lib/connect/uriopts.go similarity index 70% rename from cli/cluster/cmd/uri.go rename to lib/connect/uriopts.go index 26da47a90..421d7af2b 100644 --- a/cli/cluster/cmd/uri.go +++ b/lib/connect/uriopts.go @@ -1,4 +1,4 @@ -package cmd +package connect import ( "fmt" @@ -6,10 +6,6 @@ import ( "strconv" "strings" "time" - - "github.com/tarantool/go-tarantool" - - libcluster "github.com/tarantool/tt/lib/cluster" ) const ( @@ -54,7 +50,8 @@ type UriOpts struct { } // ParseUriOpts parses options from a URI from a URL. -func ParseUriOpts(uri *url.URL) (UriOpts, error) { +// Accepts default username and password if they are not set in the URL. +func ParseUriOpts(uri *url.URL, username string, password string) (UriOpts, error) { if uri.Scheme == "" || uri.Host == "" { return UriOpts{}, fmt.Errorf("URL must contain the scheme and the host parts") @@ -79,7 +76,13 @@ func ParseUriOpts(uri *url.URL) (UriOpts, error) { Ciphers: values.Get("ssl_ciphers"), Timeout: DefaultUriTimeout, } - if password, ok := uri.User.Password(); ok { + if p, ok := uri.User.Password(); ok { + opts.Password = p + } + // Q: Do we should keep old logic to check both username and password are empty? + // ? What if would be required set password from CLI while username from URL? + if opts.Username == "" && opts.Password == "" { + opts.Username = username opts.Password = password } @@ -90,7 +93,7 @@ func ParseUriOpts(uri *url.URL) (UriOpts, error) { if verifyPeerStr != "" { verifyPeerStr = strings.ToLower(verifyPeerStr) if verify, err := strconv.ParseBool(verifyPeerStr); err == nil { - if verify == false { + if !verify { opts.SkipPeerVerify = true } } else { @@ -102,7 +105,7 @@ func ParseUriOpts(uri *url.URL) (UriOpts, error) { if verifyHostStr != "" { verifyHostStr = strings.ToLower(verifyHostStr) if verify, err := strconv.ParseBool(verifyHostStr); err == nil { - if verify == false { + if !verify { opts.SkipHostVerify = true } } else { @@ -122,45 +125,3 @@ func ParseUriOpts(uri *url.URL) (UriOpts, error) { return opts, nil } - -// MakeEtcdOptsFromUriOpts create etcd connect options from URI options. -func MakeEtcdOptsFromUriOpts(src UriOpts) libcluster.EtcdOpts { - var endpoints []string - if src.Endpoint != "" { - endpoints = []string{src.Endpoint} - } - - return libcluster.EtcdOpts{ - Endpoints: endpoints, - Username: src.Username, - Password: src.Password, - KeyFile: src.KeyFile, - CertFile: src.CertFile, - CaPath: src.CaPath, - CaFile: src.CaFile, - SkipHostVerify: src.SkipHostVerify || src.SkipPeerVerify, - Timeout: src.Timeout, - } -} - -// MakeConnectOptsFromUriOpts create Tarantool connect options from -// URI options. -func MakeConnectOptsFromUriOpts(src UriOpts) (string, tarantool.Opts) { - opts := tarantool.Opts{ - User: src.Username, - Pass: src.Password, - Ssl: tarantool.SslOpts{ - KeyFile: src.KeyFile, - CertFile: src.CertFile, - CaFile: src.CaFile, - Ciphers: src.Ciphers, - }, - Timeout: src.Timeout, - } - - if opts.Ssl != (tarantool.SslOpts{}) { - opts.Transport = "ssl" - } - - return fmt.Sprintf("tcp://%s", src.Host), opts -} diff --git a/cli/cluster/cmd/uri_test.go b/lib/connect/uriopts_test.go similarity index 54% rename from cli/cluster/cmd/uri_test.go rename to lib/connect/uriopts_test.go index 72a64c23d..5e502fe67 100644 --- a/cli/cluster/cmd/uri_test.go +++ b/lib/connect/uriopts_test.go @@ -1,4 +1,4 @@ -package cmd_test +package connect_test import ( "net/url" @@ -8,10 +8,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/tarantool/go-tarantool" - - "github.com/tarantool/tt/cli/cluster/cmd" - libcluster "github.com/tarantool/tt/lib/cluster" + "github.com/tarantool/tt/lib/connect" ) func TestParseUriOpts(t *testing.T) { @@ -19,27 +16,29 @@ func TestParseUriOpts(t *testing.T) { cases := []struct { Url string - Opts cmd.UriOpts + User string + Pwd string + Opts connect.UriOpts Err string }{ { Url: "", - Opts: cmd.UriOpts{}, + Opts: connect.UriOpts{}, Err: "URL must contain the scheme and the host parts", }, { Url: "host", - Opts: cmd.UriOpts{}, + Opts: connect.UriOpts{}, Err: "URL must contain the scheme and the host parts", }, { Url: "scheme:///prefix", - Opts: cmd.UriOpts{}, + Opts: connect.UriOpts{}, Err: "URL must contain the scheme and the host parts", }, { Url: "scheme://localhost", - Opts: cmd.UriOpts{ + Opts: connect.UriOpts{ Endpoint: "scheme://localhost", Host: "localhost", Timeout: defaultTimeout, @@ -48,7 +47,7 @@ func TestParseUriOpts(t *testing.T) { }, { Url: "scheme://localhost:3013", - Opts: cmd.UriOpts{ + Opts: connect.UriOpts{ Endpoint: "scheme://localhost:3013", Host: "localhost:3013", Timeout: defaultTimeout, @@ -57,7 +56,7 @@ func TestParseUriOpts(t *testing.T) { }, { Url: "scheme://user@localhost", - Opts: cmd.UriOpts{ + Opts: connect.UriOpts{ Endpoint: "scheme://localhost", Host: "localhost", Username: "user", @@ -67,7 +66,7 @@ func TestParseUriOpts(t *testing.T) { }, { Url: "scheme://user:pass@localhost", - Opts: cmd.UriOpts{ + Opts: connect.UriOpts{ Endpoint: "scheme://localhost", Host: "localhost", Username: "user", @@ -78,7 +77,7 @@ func TestParseUriOpts(t *testing.T) { }, { Url: "scheme://localhost/", - Opts: cmd.UriOpts{ + Opts: connect.UriOpts{ Endpoint: "scheme://localhost", Host: "localhost", Prefix: "/", @@ -88,7 +87,7 @@ func TestParseUriOpts(t *testing.T) { }, { Url: "scheme://localhost/prefix", - Opts: cmd.UriOpts{ + Opts: connect.UriOpts{ Endpoint: "scheme://localhost", Host: "localhost", Prefix: "/prefix", @@ -98,7 +97,7 @@ func TestParseUriOpts(t *testing.T) { }, { Url: "scheme://localhost/prefix?key=anykey", - Opts: cmd.UriOpts{ + Opts: connect.UriOpts{ Endpoint: "scheme://localhost", Host: "localhost", Prefix: "/prefix", @@ -109,7 +108,7 @@ func TestParseUriOpts(t *testing.T) { }, { Url: "scheme://localhost/prefix?name=anyname", - Opts: cmd.UriOpts{ + Opts: connect.UriOpts{ Endpoint: "scheme://localhost", Host: "localhost", Prefix: "/prefix", @@ -120,7 +119,7 @@ func TestParseUriOpts(t *testing.T) { }, { Url: "scheme://localhost?ssl_key_file=/any/kfile", - Opts: cmd.UriOpts{ + Opts: connect.UriOpts{ Endpoint: "scheme://localhost", Host: "localhost", KeyFile: "/any/kfile", @@ -130,7 +129,7 @@ func TestParseUriOpts(t *testing.T) { }, { Url: "scheme://localhost?ssl_cert_file=/any/certfile", - Opts: cmd.UriOpts{ + Opts: connect.UriOpts{ Endpoint: "scheme://localhost", Host: "localhost", CertFile: "/any/certfile", @@ -140,7 +139,7 @@ func TestParseUriOpts(t *testing.T) { }, { Url: "scheme://localhost?ssl_ca_path=/any/capath", - Opts: cmd.UriOpts{ + Opts: connect.UriOpts{ Endpoint: "scheme://localhost", Host: "localhost", CaPath: "/any/capath", @@ -150,7 +149,7 @@ func TestParseUriOpts(t *testing.T) { }, { Url: "scheme://localhost?ssl_ca_file=/any/cafile", - Opts: cmd.UriOpts{ + Opts: connect.UriOpts{ Endpoint: "scheme://localhost", Host: "localhost", CaFile: "/any/cafile", @@ -160,7 +159,7 @@ func TestParseUriOpts(t *testing.T) { }, { Url: "scheme://localhost?verify_peer=true&verify_host=true", - Opts: cmd.UriOpts{ + Opts: connect.UriOpts{ Endpoint: "scheme://localhost", Host: "localhost", Timeout: defaultTimeout, @@ -169,7 +168,7 @@ func TestParseUriOpts(t *testing.T) { }, { Url: "scheme://localhost?verify_peer=false", - Opts: cmd.UriOpts{ + Opts: connect.UriOpts{ Endpoint: "scheme://localhost", Host: "localhost", SkipPeerVerify: true, @@ -179,12 +178,12 @@ func TestParseUriOpts(t *testing.T) { }, { Url: "scheme://localhost?verify_peer=asd", - Opts: cmd.UriOpts{}, + Opts: connect.UriOpts{}, Err: "invalid verify_peer, boolean expected", }, { Url: "scheme://localhost?verify_host=false", - Opts: cmd.UriOpts{ + Opts: connect.UriOpts{ Endpoint: "scheme://localhost", Host: "localhost", SkipHostVerify: true, @@ -194,12 +193,12 @@ func TestParseUriOpts(t *testing.T) { }, { Url: "scheme://localhost?verify_host=asd", - Opts: cmd.UriOpts{}, + Opts: connect.UriOpts{}, Err: "invalid verify_host, boolean expected", }, { Url: "scheme://localhost?timeout=5.5", - Opts: cmd.UriOpts{ + Opts: connect.UriOpts{ Endpoint: "scheme://localhost", Host: "localhost", Timeout: time.Duration(float64(5.5) * float64(time.Second)), @@ -208,7 +207,7 @@ func TestParseUriOpts(t *testing.T) { }, { Url: "scheme://localhost?timeout=asd", - Opts: cmd.UriOpts{}, + Opts: connect.UriOpts{}, Err: "invalid timeout, float expected", }, { @@ -218,7 +217,7 @@ func TestParseUriOpts(t *testing.T) { "&ssl_ca_path=capath&ssl_ca_file=cafile" + "&ssl_ciphers=foo:bar:ciphers" + "&verify_peer=true&verify_host=false&timeout=2", - Opts: cmd.UriOpts{ + Opts: connect.UriOpts{ Endpoint: "scheme://localhost:2012", Host: "localhost:2012", Prefix: "/prefix", @@ -236,175 +235,58 @@ func TestParseUriOpts(t *testing.T) { }, Err: "", }, - } - - for _, tc := range cases { - t.Run(tc.Url, func(t *testing.T) { - uri, err := url.Parse(tc.Url) - require.NoError(t, err) - - opts, err := cmd.ParseUriOpts(uri) - if tc.Err != "" { - assert.ErrorContains(t, err, tc.Err) - } else { - assert.Equal(t, tc.Opts, opts) - } - }) - } -} - -func TestMakeEtcdOptsFromUriOpts(t *testing.T) { - cases := []struct { - Name string - UriOpts cmd.UriOpts - Expected libcluster.EtcdOpts - }{ - { - Name: "empty", - UriOpts: cmd.UriOpts{}, - Expected: libcluster.EtcdOpts{}, - }, { - Name: "ignored", - UriOpts: cmd.UriOpts{ - Host: "foo", - Prefix: "foo", - Key: "bar", - Instance: "zoo", - Ciphers: "foo:bar:ciphers", - }, - Expected: libcluster.EtcdOpts{}, - }, - { - Name: "skip_host_verify", - UriOpts: cmd.UriOpts{ - SkipHostVerify: true, - }, - Expected: libcluster.EtcdOpts{ - SkipHostVerify: true, - }, - }, - { - Name: "skip_peer_verify", - UriOpts: cmd.UriOpts{ - SkipPeerVerify: true, - }, - Expected: libcluster.EtcdOpts{ - SkipHostVerify: true, - }, - }, - { - Name: "full", - UriOpts: cmd.UriOpts{ - Endpoint: "foo", - Host: "host", - Prefix: "prefix", - Key: "key", - Instance: "instance", - Username: "username", - Password: "password", - KeyFile: "key_file", - CertFile: "cert_file", - CaPath: "ca_path", - CaFile: "ca_file", - SkipHostVerify: true, - SkipPeerVerify: true, - Timeout: 2 * time.Second, - }, - Expected: libcluster.EtcdOpts{ - Endpoints: []string{"foo"}, - Username: "username", - Password: "password", - KeyFile: "key_file", - CertFile: "cert_file", - CaPath: "ca_path", - CaFile: "ca_file", - SkipHostVerify: true, - Timeout: 2 * time.Second, + Url: "scheme://localhost", + User: "user", + Pwd: "pass", + Opts: connect.UriOpts{ + Endpoint: "scheme://localhost", + Host: "localhost", + Username: "user", + Password: "pass", + Timeout: defaultTimeout, }, - }, - } - - for _, tc := range cases { - t.Run(tc.Name, func(t *testing.T) { - etcdOpts := cmd.MakeEtcdOptsFromUriOpts(tc.UriOpts) - - assert.Equal(t, tc.Expected, etcdOpts) - }) - } -} - -func TestMakeConnectOptsFromUriOpts(t *testing.T) { - cases := []struct { - Name string - UriOpts cmd.UriOpts - Expected tarantool.Opts - ExpectedAddr string - }{ - { - Name: "empty", - UriOpts: cmd.UriOpts{}, - Expected: tarantool.Opts{}, - ExpectedAddr: "tcp://", + Err: "", }, { - Name: "ignored", - UriOpts: cmd.UriOpts{ - Endpoint: "localhost:3013", - Prefix: "foo", - Key: "bar", - Instance: "zoo", - CaPath: "ca_path", - SkipHostVerify: true, - SkipPeerVerify: true, - Timeout: 673, - }, - Expected: tarantool.Opts{ - Timeout: 673, + Url: "scheme://user:pass@localhost", + User: "ignored_user", + Pwd: "ignored_pass", + Opts: connect.UriOpts{ + Endpoint: "scheme://localhost", + Host: "localhost", + Username: "user", + Password: "pass", + Timeout: defaultTimeout, }, - ExpectedAddr: "tcp://", // is this ok? + Err: "", }, { - Name: "full", - UriOpts: cmd.UriOpts{ - Endpoint: "scheme://foo", - Host: "foo", - Prefix: "prefix", - Key: "key", - Instance: "instance", - Username: "username", - Password: "password", - KeyFile: "key_file", - CertFile: "cert_file", - CaPath: "ca_path", - CaFile: "ca_file", - Ciphers: "foo:bar:ciphers", - SkipHostVerify: true, - SkipPeerVerify: true, - Timeout: 2 * time.Second, - }, - Expected: tarantool.Opts{ - User: "username", - Pass: "password", - Timeout: 2 * time.Second, - Transport: "ssl", - Ssl: tarantool.SslOpts{ - KeyFile: "key_file", - CertFile: "cert_file", - CaFile: "ca_file", - Ciphers: "foo:bar:ciphers", - }, + Url: "scheme://user@localhost", + User: "ignored_user", + Pwd: "ignored_pass", + Opts: connect.UriOpts{ + Endpoint: "scheme://localhost", + Host: "localhost", + Username: "user", + Password: "", + Timeout: defaultTimeout, }, - ExpectedAddr: "tcp://foo", + Err: "", }, } for _, tc := range cases { - t.Run(tc.Name, func(t *testing.T) { - addr, tntOpts := cmd.MakeConnectOptsFromUriOpts(tc.UriOpts) + t.Run(tc.Url, func(t *testing.T) { + uri, err := url.Parse(tc.Url) + require.NoError(t, err) - assert.Equal(t, tc.Expected, tntOpts) - assert.Equal(t, tc.ExpectedAddr, addr) + opts, err := connect.ParseUriOpts(uri, tc.User, tc.Pwd) + if tc.Err != "" { + assert.ErrorContains(t, err, tc.Err) + } else { + assert.Equal(t, tc.Opts, opts) + } }) } }