Skip to content

Commit

Permalink
cluster: publish/show configuration to tarantool
Browse files Browse the repository at this point in the history
@TarantoolBot document
Title: tt cluster could show/publish from/to tarantool data storage

The patch adds ability to show or publish data from tarantool data
storage via URI.

Follows up tarantool/tarantool-ee#598
Part of #684
  • Loading branch information
oleg-jukovec authored and LeonidVas committed Dec 14, 2023
1 parent 1672c5f commit 4d1a149
Show file tree
Hide file tree
Showing 15 changed files with 1,122 additions and 79 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ 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.

## [2.0.0] - 2023-11-13

Expand Down
95 changes: 95 additions & 0 deletions cli/cluster/cmd/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,13 @@ package cmd
import (
"errors"
"fmt"
"os"

clientv3 "go.etcd.io/etcd/client/v3"

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

// printRawClusterConfig prints a raw cluster configuration or an instance
Expand Down Expand Up @@ -129,3 +134,93 @@ func validateInstanceConfig(config *cluster.Config, name string) error {
func printConfig(config *cluster.Config) {
fmt.Print(config.String())
}

// connectOpts is additional connect options specified by a user.
type connectOpts struct {
Username string
Password string
}

// connectTarantool establishes a connection to Tarantool.
func connectTarantool(uriOpts UriOpts, connOpts connectOpts) (connector.Connector, error) {
connectorOpts := MakeConnectOptsFromUriOpts(uriOpts)
if connectorOpts.Username == "" && connectorOpts.Password == "" {
connectorOpts.Username = connOpts.Username
connectorOpts.Password = connOpts.Password
if connectorOpts.Username == "" {
connOpts.Username = os.Getenv(connect.TarantoolUsernameEnv)
}
if connectorOpts.Password == "" {
connOpts.Password = os.Getenv(connect.TarantoolPasswordEnv)
}
}

conn, err := connector.Connect(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 := cluster.ConnectEtcd(etcdOpts)
if err != nil {
return nil, fmt.Errorf("failed to connect to etcd: %w", err)
}
return etcdcli, nil
}

// createPublisher creates a new data publisher and collector based on UriOpts.
func createPublisherAndCollector(connOpts connectOpts,
opts UriOpts) (cluster.DataPublisher, cluster.Collector, func(), error) {
prefix, key, timeout := opts.Prefix, opts.Key, opts.Timeout

conn, errTarantool := connectTarantool(opts, connOpts)
if errTarantool == nil {
var (
publisher cluster.DataPublisher
collector cluster.Collector
)
if key == "" {
publisher = cluster.NewTarantoolAllDataPublisher(conn, prefix, timeout)
collector = cluster.NewTarantoolAllCollector(conn, prefix, timeout)
} else {
publisher = cluster.NewTarantoolKeyDataPublisher(conn, prefix, key, timeout)
collector = cluster.NewTarantoolKeyCollector(conn, prefix, key, timeout)
}
return publisher, collector, func() { conn.Close() }, nil
}

etcdcli, errEtcd := connectEtcd(opts, connOpts)
if errEtcd == nil {
var (
publisher cluster.DataPublisher
collector cluster.Collector
)
if key == "" {
publisher = cluster.NewEtcdAllDataPublisher(etcdcli, prefix, timeout)
collector = cluster.NewEtcdAllCollector(etcdcli, prefix, timeout)
} else {
publisher = cluster.NewEtcdKeyDataPublisher(etcdcli, prefix, key, timeout)
collector = cluster.NewEtcdKeyCollector(etcdcli, prefix, key, timeout)
}
return publisher, collector, func() { etcdcli.Close() }, nil
}

return nil, nil, nil,
fmt.Errorf("failed to establish a connection to tarantool or etcd: %w, %w",
errTarantool, errEtcd)
}
34 changes: 11 additions & 23 deletions cli/cluster/cmd/publish.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@ import (
// PublishCtx contains information abould cluster publish command execution
// context.
type PublishCtx struct {
// Username defines an etcd username.
// Username defines a username for connection.
Username string
// Password defines an etcd password.
// Password defines a password for connection.
Password string
// Force defines whether the publish should be forced and a validation step
// is omitted.
Expand All @@ -23,45 +23,33 @@ type PublishCtx struct {
Config *cluster.Config
}

// PublishEtcd publishes a configuration to etcd.
func PublishEtcd(publishCtx PublishCtx, uri *url.URL) error {
// PublishUri publishes a configuration to URI.
func PublishUri(publishCtx PublishCtx, uri *url.URL) error {
uriOpts, err := ParseUriOpts(uri)
if err != nil {
return fmt.Errorf("invalid URL %q: %w", uri, err)
}

etcdOpts := MakeEtcdOptsFromUriOpts(uriOpts)
if etcdOpts.Username == "" && etcdOpts.Password == "" {
etcdOpts.Username = publishCtx.Username
etcdOpts.Password = publishCtx.Password
}

instance := uriOpts.Instance
if err := publishCtxValidateConfig(publishCtx, instance); err != nil {
return err
}

etcdcli, err := cluster.ConnectEtcd(etcdOpts)
if err != nil {
return fmt.Errorf("failed to connect to etcd: %w", err)
connOpts := connectOpts{
Username: publishCtx.Username,
Password: publishCtx.Password,
}
defer etcdcli.Close()

prefix, key, timeout := uriOpts.Prefix, uriOpts.Key, etcdOpts.Timeout

var publisher cluster.DataPublisher
if key == "" {
publisher = cluster.NewEtcdAllDataPublisher(etcdcli, prefix, timeout)
} else {
publisher = cluster.NewEtcdKeyDataPublisher(etcdcli, prefix, key, timeout)
publisher, collector, cancel, err := createPublisherAndCollector(connOpts, uriOpts)
if err != nil {
return err
}
defer cancel()

if instance == "" {
// The easy case, just publish the configuration as is.
return publisher.Publish(publishCtx.Src)
}

collector := cluster.NewEtcdAllCollector(etcdcli, prefix, timeout)
return replaceInstanceConfig(instance, publishCtx.Config, collector, publisher)
}

Expand Down
32 changes: 11 additions & 21 deletions cli/cluster/cmd/show.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,45 +9,35 @@ import (

// ShowCtx contains information about cluster show command execution context.
type ShowCtx struct {
// Username defines an etcd username.
// Username defines a username for connection.
Username string
// Password defines an etcd password.
// Password defines a password for connection.
Password string
// Validate defines whether the command will check the showed
// configuration.
Validate bool
}

// ShowEtcd shows a configuration from etcd.
func ShowEtcd(showCtx ShowCtx, uri *url.URL) error {
// ShowUri shows a configuration from URI.
func ShowUri(showCtx ShowCtx, uri *url.URL) error {
uriOpts, err := ParseUriOpts(uri)
if err != nil {
return fmt.Errorf("invalid URL %q: %w", uri, err)
}

etcdOpts := MakeEtcdOptsFromUriOpts(uriOpts)
if etcdOpts.Username == "" && etcdOpts.Password == "" {
etcdOpts.Username = showCtx.Username
etcdOpts.Password = showCtx.Password
connOpts := connectOpts{
Username: showCtx.Username,
Password: showCtx.Password,
}

etcdcli, err := cluster.ConnectEtcd(etcdOpts)
_, collector, cancel, err := createPublisherAndCollector(connOpts, uriOpts)
if err != nil {
return fmt.Errorf("failed to connect to etcd: %w", err)
}
defer etcdcli.Close()

prefix, key, timeout := uriOpts.Prefix, uriOpts.Key, etcdOpts.Timeout
var collector cluster.Collector
if key == "" {
collector = cluster.NewEtcdAllCollector(etcdcli, prefix, timeout)
} else {
collector = cluster.NewEtcdKeyCollector(etcdcli, prefix, key, timeout)
return err
}
defer cancel()

config, err := collector.Collect()
if err != nil {
return fmt.Errorf("failed to collect a configuration from etcd: %w", err)
return fmt.Errorf("failed to collect a configuration: %w", err)
}

instance := uriOpts.Instance
Expand Down
22 changes: 21 additions & 1 deletion cli/cluster/cmd/uri.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"time"

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

const (
Expand All @@ -16,8 +17,10 @@ const (

// UriOpts is a universal list of connect options retrieved from an URI.
type UriOpts struct {
// Endpoint is a an endpoint to connect.
// Endpoint is a an endpoint to connect: [scheme://]host[:port].
Endpoint string
// Host is a an address to connect: host[:port].
Host string
// Prefix is a configuration prefix.
Prefix string
// Key is a target key.
Expand Down Expand Up @@ -60,6 +63,7 @@ func ParseUriOpts(uri *url.URL) (UriOpts, error) {
values := uri.Query()
opts := UriOpts{
Endpoint: endpoint.String(),
Host: uri.Host,
Prefix: uri.Path,
Key: values.Get("key"),
Instance: values.Get("name"),
Expand Down Expand Up @@ -133,3 +137,19 @@ func MakeEtcdOptsFromUriOpts(src UriOpts) cluster.EtcdOpts {
Timeout: src.Timeout,
}
}

// MakeConnectOptsFromUriOpts create Tarantool connect options from
// URI options.
func MakeConnectOptsFromUriOpts(src UriOpts) connector.ConnectOpts {
return connector.ConnectOpts{
Network: connector.TCPNetwork,
Address: src.Host,
Username: src.Username,
Password: src.Password,
Ssl: connector.SslOpts{
KeyFile: src.KeyFile,
CertFile: src.CertFile,
CaFile: src.CaFile,
},
}
}
Loading

0 comments on commit 4d1a149

Please sign in to comment.