Skip to content

Commit fc666cb

Browse files
committed
cluster: add tt cluster command
* `tt cluster show` to show a configuration. * `tt cluster public` to public a configuration. Closes #506
1 parent f4d65c6 commit fc666cb

File tree

29 files changed

+1681
-47
lines changed

29 files changed

+1681
-47
lines changed

.github/actions/prepare-ce-test-env/action.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ inputs:
1414
description: Whether to skip etcd installation
1515
type: boolean
1616
required: false
17-
default: true
17+
default: false
1818

1919
runs:
2020
using: "composite"

.github/actions/prepare-ee-test-env/action.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ inputs:
1212
description: Whether to skip etcd installation
1313
type: boolean
1414
required: false
15-
default: true
15+
default: false
1616

1717
runs:
1818
using: "composite"

.github/workflows/full-ci.yml

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,6 @@ jobs:
3838
uses: ./.github/actions/prepare-ce-test-env
3939
with:
4040
tarantool-version: '${{ matrix.tarantool-version }}'
41-
skip-etcd-install: false
4241

4342
- name: Static code check
4443
uses: ./.github/actions/static-code-check
@@ -93,7 +92,6 @@ jobs:
9392
with:
9493
sdk-version: '${{ matrix.sdk-version }}'
9594
sdk-download-token: '${{ secrets.SDK_DOWNLOAD_TOKEN }}'
96-
skip-etcd-install: false
9795

9896
- name: Static code check
9997
uses: ./.github/actions/static-code-check

.github/workflows/tests.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,9 @@ jobs:
127127
brew install --overwrite go mage
128128
pip3 install -r test/requirements.txt
129129
130+
- name: Install etcd
131+
uses: ./.github/actions/setup-etcd
132+
130133
- name: Build tt
131134
env:
132135
TT_CLI_BUILD_SSL: 'static'

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ with the cartridge-cli. It is supported only by `tgz` type packing.
2626
modules into the pack bundle.
2727
- `tt connect`: added command `\shortcuts` listing all available
2828
shortcuts and hotkeys in go-prompt.
29+
- Module ``tt cluster``, to show or publish a cluster or an instance
30+
configuration.
2931

3032
### Fixed
3133

cli/cluster/cluster.go

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -191,7 +191,14 @@ func MakeClusterConfig(config *Config) (ClusterConfig, error) {
191191
}
192192

193193
err := yaml.Unmarshal([]byte(config.String()), &cconfig)
194-
return cconfig, err
194+
if err != nil {
195+
err = fmt.Errorf(
196+
"failed to parse a configuration data as a cluster config: %w",
197+
err)
198+
return cconfig, err
199+
200+
}
201+
return cconfig, nil
195202
}
196203

197204
// mergeExclude merges a high priority configuration with a low priority
@@ -372,3 +379,30 @@ func GetInstanceConfig(cluster ClusterConfig, instance string) (InstanceConfig,
372379

373380
return MakeInstanceConfig(iconfig)
374381
}
382+
383+
// ReplaceInstanceConfig replaces an instance configuration.
384+
func ReplaceInstanceConfig(cconfig ClusterConfig,
385+
instance string, iconfig *Config) (ClusterConfig, error) {
386+
for gname, group := range cconfig.Groups {
387+
for rname, replicaset := range group.Replicasets {
388+
for iname, _ := range replicaset.Instances {
389+
if instance == iname {
390+
path := []string{groupsLabel, gname,
391+
replicasetsLabel, rname,
392+
instancesLabel, iname,
393+
}
394+
newConfig := NewConfig()
395+
newConfig.Merge(cconfig.RawConfig)
396+
if err := newConfig.Set(path, iconfig); err != nil {
397+
err = fmt.Errorf("failed to set configuration: %w", err)
398+
return cconfig, err
399+
}
400+
return MakeClusterConfig(newConfig)
401+
}
402+
}
403+
}
404+
}
405+
406+
return cconfig,
407+
fmt.Errorf("cluster configuration have not an instance %q", instance)
408+
}

cli/cluster/cluster_test.go

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -472,3 +472,35 @@ func TestGetInstanceConfig_noinstance(t *testing.T) {
472472

473473
assert.EqualError(t, err, expected)
474474
}
475+
476+
func TestReplcaseInstanceConfig_not_found(t *testing.T) {
477+
config := cluster.NewConfig()
478+
cconfig, err := cluster.MakeClusterConfig(config)
479+
require.NoError(t, err)
480+
481+
cconfig, err = cluster.ReplaceInstanceConfig(cconfig, "any", cluster.NewConfig())
482+
assert.EqualError(t, err, "cluster configuration have not an instance \"any\"")
483+
}
484+
485+
func TestReplcaseInstanceConfig_replcase(t *testing.T) {
486+
path := []string{"groups", "a", "replicasets", "b", "instances", "c", "foo"}
487+
config := cluster.NewConfig()
488+
err := config.Set(path, 1)
489+
require.NoError(t, err)
490+
old, err := cluster.MakeClusterConfig(config)
491+
require.NoError(t, err)
492+
493+
replace := cluster.NewConfig()
494+
err = replace.Set([]string{"foo"}, 2)
495+
require.NoError(t, err)
496+
497+
newConfig, err := cluster.ReplaceInstanceConfig(old, "c", replace)
498+
require.NoError(t, err)
499+
oldValue, err := old.RawConfig.Get(path)
500+
require.NoError(t, err)
501+
newValue, err := newConfig.RawConfig.Get(path)
502+
require.NoError(t, err)
503+
504+
assert.Equal(t, 1, oldValue)
505+
assert.Equal(t, 2, newValue)
506+
}

cli/cluster/config.go

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,12 @@ func (config *Config) Set(path []string, value any) error {
8787
if m, err := config.createMaps(path[0:last]); err != nil {
8888
return err
8989
} else {
90-
m[path[last]] = value
90+
key := path[last]
91+
if cfg, ok := value.(*Config); value != nil && ok {
92+
m[key] = cfg.paths
93+
} else {
94+
m[key] = value
95+
}
9196
}
9297

9398
return nil

cli/cluster/config_test.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,27 @@ func TestConfig_Set(t *testing.T) {
4242
}
4343
}
4444

45+
func TestConfig_Set_config(t *testing.T) {
46+
base := cluster.NewConfig()
47+
err := base.Set([]string{"foo", "bar", "zoo", "foo"}, 1)
48+
require.NoError(t, err)
49+
50+
add := cluster.NewConfig()
51+
err = add.Set([]string{"foo", "bar"}, 2)
52+
require.NoError(t, err)
53+
54+
err = base.Set([]string{"foo", "bar", "zoo"}, add)
55+
require.NoError(t, err)
56+
57+
valueAdd, err := add.Get([]string{"foo", "bar"})
58+
require.NoError(t, err)
59+
assert.Equal(t, 2, valueAdd)
60+
61+
valueAdded, err := base.Get([]string{"foo", "bar", "zoo", "foo", "bar"})
62+
require.NoError(t, err)
63+
assert.Equal(t, 2, valueAdded)
64+
}
65+
4566
func TestConfig_Set_intersection(t *testing.T) {
4667
base := [][]string{
4768
nil,

cli/cluster/etcd.go

Lines changed: 108 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -92,13 +92,8 @@ func MakeEtcdOpts(config *Config) (EtcdOpts, error) {
9292
opts.SkipHostVerify = true
9393
}
9494
if parsed.Http.Request.Timeout != 0 {
95-
var err error
96-
timeout := fmt.Sprintf("%fs", parsed.Http.Request.Timeout)
97-
opts.Timeout, err = time.ParseDuration(timeout)
98-
if err != nil {
99-
fmtErr := "unable to parse a etcd request timeout: %w"
100-
return EtcdOpts{}, fmt.Errorf(fmtErr, err)
101-
}
95+
timeoutFloat := parsed.Http.Request.Timeout * float64(time.Second)
96+
opts.Timeout = time.Duration(timeoutFloat)
10297
}
10398

10499
return opts, nil
@@ -169,21 +164,25 @@ func NewEtcdCollector(getter EtcdGetter, prefix string, timeout time.Duration) E
169164
// Collect collects a configuration from the specified path with the specified
170165
// timeout.
171166
func (collector EtcdCollector) Collect() (*Config, error) {
172-
prefix := strings.TrimRight(collector.prefix, "/")
173-
key := fmt.Sprintf("%s/%s/", prefix, "config")
167+
prefix := getConfigPrefix(collector.prefix)
174168
ctx := context.Background()
175169
if collector.timeout != 0 {
176170
var cancel context.CancelFunc
177171
ctx, cancel = context.WithTimeout(ctx, collector.timeout)
178172
defer cancel()
179173
}
180174

181-
resp, err := collector.getter.Get(ctx, key, clientv3.WithPrefix())
175+
resp, err := collector.getter.Get(ctx, prefix, clientv3.WithPrefix())
182176
if err != nil {
183177
return nil, fmt.Errorf("failed to fetch data from etcd: %w", err)
184178
}
185179

186180
cconfig := NewConfig()
181+
if len(resp.Kvs) == 0 {
182+
return nil, fmt.Errorf("a configuration data not found in prefix %q",
183+
prefix)
184+
}
185+
187186
for _, kv := range resp.Kvs {
188187
if config, err := NewYamlCollector(kv.Value).Collect(); err != nil {
189188
fmtErr := "failed to decode etcd config for key %q: %w"
@@ -196,6 +195,99 @@ func (collector EtcdCollector) Collect() (*Config, error) {
196195
return cconfig, nil
197196
}
198197

198+
// EtcdDataPublisher publishes a data into etcd.
199+
type EtcdDataPublisher struct {
200+
getter EtcdTxnGetter
201+
prefix string
202+
timeout time.Duration
203+
}
204+
205+
// EtcdUpdater is the interface that adds Txn method to EtcdGetter.
206+
type EtcdTxnGetter interface {
207+
EtcdGetter
208+
// Txn creates a transaction.
209+
Txn(ctx context.Context) clientv3.Txn
210+
}
211+
212+
// NewEtcdDataPublisher creates a new EtcdDataPublisher object to publish
213+
// a data to etcd with the prefix during the timeout.
214+
func NewEtcdDataPublisher(getter EtcdTxnGetter,
215+
prefix string, timeout time.Duration) EtcdDataPublisher {
216+
return EtcdDataPublisher{
217+
getter: getter,
218+
prefix: prefix,
219+
timeout: timeout,
220+
}
221+
}
222+
223+
// Publish publishes the configuration into etcd to the given prefix.
224+
func (publisher EtcdDataPublisher) Publish(data []byte) error {
225+
if data == nil {
226+
return fmt.Errorf("failed to publish data into etcd: data does not exist")
227+
}
228+
229+
prefix := getConfigPrefix(publisher.prefix)
230+
key := prefix + "all"
231+
ctx := context.Background()
232+
if publisher.timeout != 0 {
233+
var cancel context.CancelFunc
234+
ctx, cancel = context.WithTimeout(ctx, publisher.timeout)
235+
defer cancel()
236+
}
237+
238+
for true {
239+
select {
240+
case <-ctx.Done():
241+
return ctx.Err()
242+
default:
243+
}
244+
245+
resp, err := publisher.getter.Get(ctx, prefix, clientv3.WithPrefix())
246+
if err != nil {
247+
return fmt.Errorf("failed to fetch data from etcd: %w", err)
248+
}
249+
250+
var (
251+
keys []string
252+
revisions []int64
253+
)
254+
for _, kv := range resp.Kvs {
255+
// We need to skip the target key since etcd transactions do not
256+
// support delete + put for the same key on a single transaction.
257+
if string(kv.Key) != key {
258+
keys = append(keys, string(kv.Key))
259+
revisions = append(revisions, kv.ModRevision)
260+
}
261+
}
262+
263+
var (
264+
cmps []clientv3.Cmp
265+
opts []clientv3.Op
266+
)
267+
for i, key := range keys {
268+
cmp := clientv3.Compare(clientv3.ModRevision(key), "=", revisions[i])
269+
cmps = append(cmps, cmp)
270+
opts = append(opts, clientv3.OpDelete(key))
271+
}
272+
273+
opts = append(opts, clientv3.OpPut(key, string(data)))
274+
txn := publisher.getter.Txn(ctx)
275+
if len(cmps) > 0 {
276+
txn = txn.If(cmps...)
277+
}
278+
tresp, err := txn.Then(opts...).Commit()
279+
280+
if err != nil {
281+
return fmt.Errorf("failed to put data into etcd: %w", err)
282+
}
283+
if tresp != nil && tresp.Succeeded {
284+
return nil
285+
}
286+
}
287+
// Unreachable.
288+
return nil
289+
}
290+
199291
// loadRootCA and the code below is a copy-paste from Go sources. We need an
200292
// ability to load ceritificates from a directory, but there is no such public
201293
// function in `crypto`.
@@ -253,3 +345,9 @@ func isSameDirSymlink(f fs.DirEntry, dir string) bool {
253345
target, err := os.Readlink(filepath.Join(dir, f.Name()))
254346
return err == nil && !strings.Contains(target, "/")
255347
}
348+
349+
// getConfigPrefix returns a full configuration prefix.
350+
func getConfigPrefix(basePrefix string) string {
351+
prefix := strings.TrimRight(basePrefix, "/")
352+
return fmt.Sprintf("%s/%s/", prefix, "config")
353+
}

0 commit comments

Comments
 (0)