Skip to content

Commit 22c5bfa

Browse files
committed
aeon: connect implementation
Implement aeon console connection. The ‘Console’ module has been separate from the ‘Connect’ abstraction, to allow it being used independently of the transport layer. Closes #1050 @TarantoolBot document Title: Implement aeon console connection. Command allow connect to Enterprise Aeon database with specified URL. Available command options: - `sslkeyfile <private_key>` - path to private part of certificate. - `sslcertfile <pub_cert>` - path to public part of certificate. - `sslcafile <ca_file>` - path to root CA for self-signed certificate. - `transport [ssl|plain]` - connection mode.
1 parent e844f27 commit 22c5bfa

20 files changed

+1242
-23
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
2323
* `sslcertfile` - path to an SSL certificate file,
2424
* `sslcafile` - path to a trusted certificate authorities (CA) file,
2525
* `sslciphers` - colon-separated list of SSL cipher suites the connection.
26+
- `tt aeon connect`: add support to connect Aeon database.
2627

2728
### Changed
2829

cli/aeon/client.go

+199
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,199 @@
1+
package aeon
2+
3+
import (
4+
"context"
5+
"crypto/tls"
6+
"crypto/x509"
7+
"errors"
8+
"fmt"
9+
"os"
10+
"strings"
11+
"time"
12+
13+
"github.com/apex/log"
14+
15+
"github.com/tarantool/go-prompt"
16+
"github.com/tarantool/tt/cli/aeon/cmd"
17+
"github.com/tarantool/tt/cli/aeon/pb"
18+
"github.com/tarantool/tt/cli/connector"
19+
"google.golang.org/grpc"
20+
"google.golang.org/grpc/credentials"
21+
"google.golang.org/grpc/credentials/insecure"
22+
)
23+
24+
// Client structure with parameters for gRPC connection to Aeon.
25+
type Client struct {
26+
title string
27+
conn *grpc.ClientConn
28+
client pb.SQLServiceClient
29+
}
30+
31+
func makeAddress(ctx cmd.ConnectCtx) string {
32+
if ctx.Network == connector.UnixNetwork {
33+
if strings.HasPrefix(ctx.Address, "@") {
34+
return "unix-abstract:" + (ctx.Address)[1:]
35+
}
36+
return "unix:" + ctx.Address
37+
}
38+
return ctx.Address
39+
}
40+
41+
func getCertificate(args cmd.Ssl) (tls.Certificate, error) {
42+
if args.CertFile == "" && args.KeyFile == "" {
43+
return tls.Certificate{}, nil
44+
}
45+
tls_cert, err := tls.LoadX509KeyPair(args.CertFile, args.KeyFile)
46+
if err != nil {
47+
return tls_cert, fmt.Errorf("could not load client key pair: %w", err)
48+
}
49+
return tls_cert, nil
50+
}
51+
52+
func getTlsConfig(args cmd.Ssl) (*tls.Config, error) {
53+
if args.CaFile == "" {
54+
return &tls.Config{
55+
ClientAuth: tls.NoClientCert,
56+
}, nil
57+
}
58+
59+
ca, err := os.ReadFile(args.CaFile)
60+
if err != nil {
61+
return nil, fmt.Errorf("failed to read CA file: %w", err)
62+
}
63+
certPool := x509.NewCertPool()
64+
if !certPool.AppendCertsFromPEM(ca) {
65+
return nil, errors.New("failed to append CA data")
66+
}
67+
cert, err := getCertificate(args)
68+
if err != nil {
69+
return nil, fmt.Errorf("failed get certificate: %w", err)
70+
}
71+
return &tls.Config{
72+
Certificates: []tls.Certificate{cert},
73+
ClientAuth: tls.RequireAndVerifyClientCert,
74+
RootCAs: certPool,
75+
}, nil
76+
}
77+
78+
func getDialOpts(ctx cmd.ConnectCtx) (grpc.DialOption, error) {
79+
var creds credentials.TransportCredentials
80+
if ctx.Transport == cmd.TransportSsl {
81+
config, err := getTlsConfig(ctx.Ssl)
82+
if err != nil {
83+
return nil, fmt.Errorf("not tls config: %w", err)
84+
}
85+
creds = credentials.NewTLS(config)
86+
} else {
87+
creds = insecure.NewCredentials()
88+
}
89+
return grpc.WithTransportCredentials(creds), nil
90+
}
91+
92+
// NewAeonHandler create new grpc connection to Aeon server.
93+
func NewAeonHandler(ctx cmd.ConnectCtx) (*Client, error) {
94+
c := Client{title: ctx.Address}
95+
target := makeAddress(ctx)
96+
// var err error
97+
opt, err := getDialOpts(ctx)
98+
if err != nil {
99+
return nil, fmt.Errorf("%w", err)
100+
}
101+
c.conn, err = grpc.NewClient(target, opt)
102+
if err != nil {
103+
return nil, fmt.Errorf("fail to dial: %w", err)
104+
}
105+
if err := c.ping(); err == nil {
106+
log.Infof("Aeon responses at %q", target)
107+
} else {
108+
return nil, fmt.Errorf("can't ping to Aeon at %q: %w", target, err)
109+
}
110+
111+
c.client = pb.NewSQLServiceClient(c.conn)
112+
return &c, nil
113+
}
114+
115+
func (c *Client) ping() error {
116+
log.Infof("Start ping aeon server")
117+
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
118+
defer cancel()
119+
120+
diag := pb.NewDiagServiceClient(c.conn)
121+
_, err := diag.Ping(ctx, &pb.PingRequest{})
122+
if err != nil {
123+
log.Warnf("Aeon ping %s", err)
124+
}
125+
return err
126+
}
127+
128+
// Title implements console.Handler interface.
129+
func (c *Client) Title() string {
130+
return c.title
131+
}
132+
133+
// Validate implements console.Handler interface.
134+
func (c *Client) Validate(input string) bool {
135+
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
136+
defer cancel()
137+
138+
check, err := c.client.SQLCheck(ctx, &pb.SQLRequest{Query: input})
139+
if err != nil {
140+
log.Warnf("Aeon validate %s\nFor request: %q", err, input)
141+
return false
142+
}
143+
144+
return check.Status == pb.SQLCheckStatus_SQL_QUERY_VALID
145+
}
146+
147+
// Execute implements console.Handler interface.
148+
func (c *Client) Execute(input string) any {
149+
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
150+
defer cancel()
151+
152+
resp, err := c.client.SQL(ctx, &pb.SQLRequest{Query: input})
153+
if err != nil {
154+
return err
155+
}
156+
return parseSQLResponse(resp)
157+
}
158+
159+
// Stop implements console.Handler interface.
160+
func (c *Client) Close() {
161+
c.conn.Close()
162+
}
163+
164+
// Complete implements console.Handler interface.
165+
func (c *Client) Complete(input prompt.Document) []prompt.Suggest {
166+
// TODO: waiting until there is support from Aeon side.
167+
return nil
168+
}
169+
170+
// parseSQLResponse returns result as table in map.
171+
// Where keys is name of columns. And body is array of values.
172+
// On any issue return an error.
173+
func parseSQLResponse(resp *pb.SQLResponse) any {
174+
if resp.Error != nil {
175+
return ResultError{resp.Error}
176+
}
177+
if resp.TupleFormat == nil {
178+
return ResultType{}
179+
}
180+
res := ResultType{
181+
names: make([]string, len(resp.TupleFormat.Names)),
182+
rows: make([]ResultRow, len(resp.Tuples)),
183+
}
184+
for i, n := range resp.TupleFormat.Names {
185+
res.names[i] = n
186+
res.rows[i] = make([]any, 0, len(resp.TupleFormat.Names))
187+
}
188+
189+
for r, row := range resp.Tuples {
190+
for _, v := range row.Fields {
191+
val, err := decodeValue(v)
192+
if err != nil {
193+
return fmt.Errorf("tuple %d can't decode %s: %w", r, v.String(), err)
194+
}
195+
res.rows[r] = append(res.rows[r], val)
196+
}
197+
}
198+
return res
199+
}

cli/aeon/cmd/connect.go

+4
Original file line numberDiff line numberDiff line change
@@ -16,4 +16,8 @@ type ConnectCtx struct {
1616
Ssl Ssl
1717
// Transport is a connection mode.
1818
Transport Transport
19+
// Network is kind of transport layer.
20+
Network string
21+
// Address is a connection URL, unix socket address and etc.
22+
Address string
1923
}

cli/aeon/decode.go

+100
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
package aeon
2+
3+
import (
4+
"fmt"
5+
"time"
6+
7+
"github.com/google/uuid"
8+
"github.com/tarantool/go-tarantool/v2/datetime"
9+
"github.com/tarantool/go-tarantool/v2/decimal"
10+
"github.com/tarantool/tt/cli/aeon/pb"
11+
)
12+
13+
// decodeValue convert a value obtained from protobuf into a value that can be used as an
14+
// argument to Tarantool functions.
15+
//
16+
// Copy from https://github.com/tarantool/aeon/blob/master/aeon/grpc/server/pb/decode.go
17+
func decodeValue(val *pb.Value) (any, error) {
18+
switch casted := val.Kind.(type) {
19+
case *pb.Value_UnsignedValue:
20+
return val.GetUnsignedValue(), nil
21+
case *pb.Value_StringValue:
22+
return val.GetStringValue(), nil
23+
case *pb.Value_NumberValue:
24+
return val.GetNumberValue(), nil
25+
case *pb.Value_IntegerValue:
26+
return val.GetIntegerValue(), nil
27+
case *pb.Value_BooleanValue:
28+
return val.GetBooleanValue(), nil
29+
case *pb.Value_VarbinaryValue:
30+
return val.GetVarbinaryValue(), nil
31+
case *pb.Value_DecimalValue:
32+
decStr := val.GetDecimalValue()
33+
res, err := decimal.MakeDecimalFromString(decStr)
34+
if err != nil {
35+
return nil, err
36+
}
37+
return res, nil
38+
case *pb.Value_UuidValue:
39+
uuidStr := val.GetUuidValue()
40+
res, err := uuid.Parse(uuidStr)
41+
if err != nil {
42+
return nil, err
43+
}
44+
return res, nil
45+
case *pb.Value_DatetimeValue:
46+
sec := casted.DatetimeValue.Seconds
47+
nsec := casted.DatetimeValue.Nsec
48+
t := time.Unix(sec, nsec)
49+
if len(casted.DatetimeValue.Location) > 0 {
50+
locStr := casted.DatetimeValue.Location
51+
loc, err := time.LoadLocation(locStr)
52+
if err != nil {
53+
return nil, err
54+
}
55+
t = t.In(loc)
56+
}
57+
res, err := datetime.MakeDatetime(t)
58+
if err != nil {
59+
return nil, err
60+
}
61+
return res, nil
62+
case *pb.Value_IntervalValue:
63+
res := datetime.Interval{
64+
Year: casted.IntervalValue.Year,
65+
Month: casted.IntervalValue.Month,
66+
Week: casted.IntervalValue.Week,
67+
Day: casted.IntervalValue.Day,
68+
Hour: casted.IntervalValue.Hour,
69+
Min: casted.IntervalValue.Min,
70+
Sec: casted.IntervalValue.Sec,
71+
Nsec: casted.IntervalValue.Nsec,
72+
Adjust: datetime.Adjust(casted.IntervalValue.Adjust)}
73+
return res, nil
74+
case *pb.Value_ArrayValue:
75+
array := val.GetArrayValue()
76+
res := make([]any, len(array.Fields))
77+
for k, v := range array.Fields {
78+
field, err := decodeValue(v)
79+
if err != nil {
80+
return nil, err
81+
}
82+
res[k] = field
83+
}
84+
return res, nil
85+
case *pb.Value_MapValue:
86+
res := make(map[any]any, len(casted.MapValue.Fields))
87+
for k, v := range casted.MapValue.Fields {
88+
item, err := decodeValue(v)
89+
if err != nil {
90+
return nil, err
91+
}
92+
res[k] = item
93+
}
94+
return res, nil
95+
case *pb.Value_NullValue:
96+
return nil, nil
97+
default:
98+
return nil, fmt.Errorf("unsupported type for value")
99+
}
100+
}

cli/aeon/results.go

+54
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
package aeon
2+
3+
import (
4+
"fmt"
5+
6+
"github.com/tarantool/tt/cli/aeon/pb"
7+
"github.com/tarantool/tt/cli/console"
8+
"github.com/tarantool/tt/cli/formatter"
9+
)
10+
11+
// ResultRow keeps values for one table row.
12+
type ResultRow []any
13+
14+
// ResultType is a custom type to format output with console.Formatter interface.
15+
type ResultType struct {
16+
names []string
17+
rows []ResultRow
18+
}
19+
20+
// ResultError wraps pb.Error to implement console.Formatter interface.
21+
type ResultError struct {
22+
*pb.Error
23+
}
24+
25+
// asYaml prepare results for formatter.MakeOutput.
26+
func (r ResultType) asYaml() string {
27+
yaml := "---\n"
28+
for _, row := range r.rows {
29+
mark := "-"
30+
for i, v := range row {
31+
n := r.names[i]
32+
yaml += fmt.Sprintf("%s %s: %v\n", mark, n, v)
33+
mark = " "
34+
}
35+
}
36+
return yaml
37+
}
38+
39+
// Format produce formatted string according required console.Format settings.
40+
func (r ResultType) Format(f console.Format) (string, error) {
41+
if len(r.names) == 0 {
42+
return "", nil
43+
}
44+
output, err := formatter.MakeOutput(f.Mode, r.asYaml(), f.Opts)
45+
if err != nil {
46+
return "", err
47+
}
48+
return output, nil
49+
}
50+
51+
// Format produce formatted string according required console.Format settings.
52+
func (e *ResultError) Format(_ console.Format) (string, error) {
53+
return fmt.Sprintf("---\nError: %s\n%q", e.Name, e.Msg), nil
54+
}

cli/aeon/results_export_test.go

+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package aeon
2+
3+
import "github.com/tarantool/tt/cli/aeon/pb"
4+
5+
func NewResultType(names []string, rows []ResultRow) ResultType {
6+
return ResultType{
7+
names: names,
8+
rows: rows,
9+
}
10+
}
11+
12+
func NewResultError(name string, msg string) ResultError {
13+
return ResultError{&pb.Error{
14+
Name: name,
15+
Msg: msg}}
16+
}

0 commit comments

Comments
 (0)