Skip to content

Commit be252da

Browse files
oleg-jukovecpsergee
authored andcommitted
cluster: add tt cluster command
* `tt cluster show` to show a configuration. * `tt cluster publish` to publish a configuration. @TarantoolBot document Title: new module - `cluster` The patch introduces a new module `cluster`. The module has two commands: * show - to show a configuration for a cluster or an instance. * publish - to publish a cluster or an instance configuration. The target could be a cluster or an instance configuration. It could be a fully-merged (from all sources) configuration for an application or just a remote configuration part in case of etcd. Usage: * `tt cluster show (<APP_NAME> | <APP_NAME:INSTANCE_NAME> | <URI>) [--validate]` * The optional `--validate` flag enables validation for a showed configuration. * `tt cluster publish (<APP_NAME> | <APP_NAME:INSTANCE_NAME> | <URI>) file [--force]` * The optional `--force` flag skips validation of uploaded configuration. The `APP_NAME` is a name of an application. The `INSTANCE_NAME` is a name of an instance in the application. The `URI` specifies a etcd connection settings in the following format: `http(s)://[username:password@]host:port[/prefix][?arguments]` * `prefix` - a base path to Tarantool configuration in etcd. Possible arguments: * `name` - a name of an instance in the cluster configuration. * `timeout` - a request timeout in seconds (default 3.0). * `ssl_key_file` - a path to a private SSL key file. * `ssl_cert_file` - a path to an SSL certificate file. * `ssl_ca_file` - a path to a trusted certificate authorities (CA) file. * `ssl_ca_path` - a path to a trusted certificate authorities (CA) directory. * `verify_host` - set off (default true) verification of the certificate’s name against the host. * `verify_peer` - set off (default true) verification of the peer’s SSL certificate. Part of #506
1 parent c4671ec commit be252da

35 files changed

+2756
-165
lines changed

.codespellrc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
[codespell]
2-
skip = */rocks/third_party,*/cartridge/third_party,tarantool-static-build.patch
2+
skip = */rocks/third_party,*/cartridge/third_party,tarantool-static-build.patch,./cli/cluster/paths.go
33
count =
44
quiet-level = 3

.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
@@ -18,6 +18,8 @@ prompt line when using 'app:instance' target format.
1818
The binary has the name tt/tarantool_ + seven-digit hash.
1919
- New `tt pack` flag `--tarantool-version` is added to specify tarantool
2020
version for pack in docker. It is supported only with `--use-docker` enabled.
21+
- Module ``tt cluster``, to show or publish a cluster or an instance
22+
configuration.
2123

2224
### Fixed
2325

cli/cluster/cluster.go

Lines changed: 46 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -54,8 +54,8 @@ func (config *InstanceConfig) UnmarshalYAML(unmarshal func(any) error) error {
5454
//
5555
// The `parsed` type helps to break the recursion because the type does
5656
// not have `UnmarshalYAML` call.
57-
type parsed InstanceConfig
58-
temp := parsed(*config)
57+
type instanceConfig InstanceConfig
58+
temp := instanceConfig(*config)
5959
if err := unmarshal(&temp); err != nil {
6060
return fmt.Errorf("failed to unmarshal InstanceConfig: %w", err)
6161
}
@@ -87,8 +87,8 @@ func (config *ReplicasetConfig) UnmarshalYAML(unmarshal func(any) error) error {
8787
//
8888
// The `parsed` type helps to break the recursion because the type does
8989
// not have `UnmarshalYAML` call.
90-
type parsed ReplicasetConfig
91-
temp := parsed(*config)
90+
type replicasetConfig ReplicasetConfig
91+
temp := replicasetConfig(*config)
9292
if err := unmarshal(&temp); err != nil {
9393
return fmt.Errorf("failed to unmarshal ReplicasetConfig: %w", err)
9494
}
@@ -120,8 +120,8 @@ func (config *GroupConfig) UnmarshalYAML(unmarshal func(any) error) error {
120120
//
121121
// The `parsed` type helps to break the recursion because the type does
122122
// not have `UnmarshalYAML` call.
123-
type parsed GroupConfig
124-
temp := parsed(*config)
123+
type groupConfig GroupConfig
124+
temp := groupConfig(*config)
125125
if err := unmarshal(&temp); err != nil {
126126
return fmt.Errorf("failed to unmarshal GroupConfig: %w", err)
127127
}
@@ -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
@@ -212,7 +219,9 @@ func findInstance(cluster ClusterConfig, name string) *Config {
212219
for iname, instance := range replicaset.Instances {
213220
if iname == name {
214221
config := NewConfig()
215-
config.Merge(instance.RawConfig)
222+
if instance.RawConfig != nil {
223+
config.Merge(instance.RawConfig)
224+
}
216225
mergeExclude(config, replicaset.RawConfig,
217226
[]string{instancesLabel})
218227
mergeExclude(config, group.RawConfig,
@@ -288,6 +297,8 @@ func collectEtcdConfig(clusterConfig ClusterConfig) (*Config, error) {
288297
fmtErr := "unable to parse a etcd request timeout: %w"
289298
return nil, fmt.Errorf(fmtErr, err)
290299
}
300+
} else {
301+
opts.Timeout = DefaultEtcdTimeout
291302
}
292303

293304
etcd, err := ConnectEtcd(opts)
@@ -372,3 +383,30 @@ func GetInstanceConfig(cluster ClusterConfig, instance string) (InstanceConfig,
372383

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

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 TestReplaceInstanceConfig_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 has not an instance \"any\"")
483+
}
484+
485+
func TestReplaceInstanceConfig_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/cmd/common.go

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
package cmd
2+
3+
import (
4+
"errors"
5+
"fmt"
6+
7+
"github.com/tarantool/tt/cli/cluster"
8+
)
9+
10+
// printRawClusterConfig prints a raw cluster configuration or an instance
11+
// configuration if the instance name is specified.
12+
func printRawClusterConfig(config *cluster.Config,
13+
instance string, validate bool) error {
14+
cconfig, err := cluster.MakeClusterConfig(config)
15+
if err != nil {
16+
return err
17+
}
18+
19+
if instance == "" {
20+
var err error
21+
if validate {
22+
err = validateClusterConfig(cconfig, false)
23+
}
24+
printConfig(cconfig.RawConfig)
25+
return err
26+
}
27+
28+
return printInstanceConfig(cconfig, instance, false, validate)
29+
}
30+
31+
// printClusterConfig prints a full-merged cluster configuration or an instance
32+
// configuration if the instance name is specified.
33+
func printClusterConfig(cconfig cluster.ClusterConfig,
34+
instance string, validate bool) error {
35+
if instance == "" {
36+
var err error
37+
if validate {
38+
err = validateClusterConfig(cconfig, true)
39+
}
40+
printConfig(cconfig.RawConfig)
41+
return err
42+
}
43+
44+
return printInstanceConfig(cconfig, instance, true, validate)
45+
}
46+
47+
// printInstanceConfig prints an instance configuration in the cluster.
48+
func printInstanceConfig(config cluster.ClusterConfig,
49+
instance string, full, validate bool) error {
50+
if !cluster.HasInstance(config, instance) {
51+
return fmt.Errorf("instance %q not found", instance)
52+
}
53+
54+
var (
55+
err error
56+
iconfig *cluster.Config
57+
)
58+
if full {
59+
ic, _ := cluster.GetInstanceConfig(config, instance)
60+
iconfig = ic.RawConfig
61+
} else {
62+
iconfig = cluster.Instantiate(config, instance)
63+
}
64+
65+
if validate {
66+
err = validateInstanceConfig(iconfig, instance)
67+
}
68+
printConfig(iconfig)
69+
return err
70+
}
71+
72+
// validateRawConfig validates a raw cluster or an instance configuration. The
73+
// configuration belongs to an instance if name != "".
74+
func validateRawConfig(config *cluster.Config, name string) error {
75+
if name == "" {
76+
return validateRawClusterConfig(config)
77+
} else {
78+
return validateInstanceConfig(config, name)
79+
}
80+
}
81+
82+
// validateRawClusterConfig validates a raw cluster configuration or an
83+
// instance configuration if the instance name is specified.
84+
func validateRawClusterConfig(config *cluster.Config) error {
85+
cconfig, err := cluster.MakeClusterConfig(config)
86+
if err != nil {
87+
return err
88+
}
89+
90+
return validateClusterConfig(cconfig, false)
91+
}
92+
93+
// validateClusterConfig validates a cluster configuration.
94+
func validateClusterConfig(cconfig cluster.ClusterConfig, full bool) error {
95+
var errs []error
96+
if err := cluster.Validate(cconfig.RawConfig, cluster.TarantoolSchema); err != nil {
97+
err = fmt.Errorf("an invalid cluster configuration: %s", err)
98+
errs = append(errs, err)
99+
}
100+
101+
for _, name := range cluster.Instances(cconfig) {
102+
var iconfig *cluster.Config
103+
if full {
104+
ic, err := cluster.GetInstanceConfig(cconfig, name)
105+
if err != nil {
106+
return err
107+
}
108+
iconfig = ic.RawConfig
109+
} else {
110+
iconfig = cluster.Instantiate(cconfig, name)
111+
}
112+
if err := validateInstanceConfig(iconfig, name); err != nil {
113+
errs = append(errs, err)
114+
}
115+
}
116+
117+
return errors.Join(errs...)
118+
}
119+
120+
// validateInstanceConfig validates an instance configuration.
121+
func validateInstanceConfig(config *cluster.Config, name string) error {
122+
if err := cluster.Validate(config, cluster.TarantoolSchema); err != nil {
123+
return fmt.Errorf("an invalid instance %q configuration: %w", name, err)
124+
}
125+
return nil
126+
}
127+
128+
// printConfig just prints a configuration to stdout.
129+
func printConfig(config *cluster.Config) {
130+
fmt.Print(config.String())
131+
}

0 commit comments

Comments
 (0)