Skip to content

Commit 6574a45

Browse files
themilchenkooleg-jukovec
authored andcommitted
cluster: add command 'roles add'
@TarantoolBot document Title: `tt cluster rs roles add` adds role in config scope provided by flags. This patch introduces new command for the cluster replicaset module: ``` tt cluster replicaset roles add <URI> <ROLE_NAME> [flags] ``` URI is a target configuration source (etcd/tcs). ROLE_NAME is a role for adding to a Tarantool config storage. It is necessary to provide flag with destination in config for role addition. It can be one or more roles. Command supports following flags: - Flags for the destination of role addition: - `--global (-G)` for a global scope to add a role; - `--instance (-i) string` for an application name target to specify an instance to add a role; - `--replicaset (-r) string` for an application name target to specify a replicaset to add a role; - `--group (-g) string` for an application name target to specify a group. - Common flag to interact with config storages (etcd / tarantool config storage): - `--force (-f)` to skip selecting a key for patching; - `--username (-u)` is a username (used as etcd/tarantool config storage credentials); - `--password (-p)` is a password (used as etcd/tarantool config storage credentials). Closes #915
1 parent 8ab6a4d commit 6574a45

File tree

10 files changed

+881
-2
lines changed

10 files changed

+881
-2
lines changed

CHANGELOG.md

+10
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,16 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
66
and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
77

8+
## [Unreleased]
9+
10+
### Added
11+
12+
- `tt cluster replicaset roles add`: command to add roles in config scope provided by flags.
13+
14+
### Fixed
15+
16+
### Changed
17+
818
## [2.4.0] - 2024-08-07
919

1020
### Added

cli/cluster/cluster.go

+2-1
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,8 @@ func collectTarantoolConfig(collectors libcluster.CollectorFactory,
138138
time.Duration(tarantoolConfig.Timeout*float64(time.Second)))
139139
if err != nil {
140140
connectionErrors = append(connectionErrors,
141-
fmt.Errorf("error when creating a colletor for endpoint %q: %w", opt.addr, err))
141+
fmt.Errorf("error when creating a collector for endpoint %q: %w",
142+
opt.addr, err))
142143
continue
143144
}
144145

cli/cluster/cmd/replicaset.go

+59
Original file line numberDiff line numberDiff line change
@@ -271,3 +271,62 @@ func Expel(uri *url.URL, ctx ExpelCtx) error {
271271
}
272272
return err
273273
}
274+
275+
// RolesAddCtx describes the context to add role of instance.
276+
type RolesAddCtx struct {
277+
// InstName is an instance name in which add or remove role.
278+
InstName string
279+
// GroupName is an replicaset name in which add or remove role.
280+
GroupName string
281+
// ReplicasetName is an replicaset name in which add or remove role.
282+
ReplicasetName string
283+
// IsGlobal is an boolean value if role needs to add in global scope.
284+
IsGlobal bool
285+
// RoleName is a name of role to add.
286+
RoleName string
287+
// Publishers is data publisher factory.
288+
Publishers libcluster.DataPublisherFactory
289+
// Collectors is data collector factory.
290+
Collectors libcluster.DataCollectorFactory
291+
// Username defines a username for connection.
292+
Username string
293+
// Password defines a password for connection.
294+
Password string
295+
// Force true if the key selection for patching the config
296+
// should be skipped.
297+
Force bool
298+
}
299+
300+
// AddRole adds a role by patching the cluster config.
301+
func AddRole(uri *url.URL, ctx RolesAddCtx) error {
302+
opts, err := ParseUriOpts(uri)
303+
if err != nil {
304+
return fmt.Errorf("invalid URL %q: %w", uri, err)
305+
}
306+
connOpts := connectOpts{
307+
Username: ctx.Username,
308+
Password: ctx.Password,
309+
}
310+
311+
collector, publisher, closeFunc, err := createDataCollectorAndKeyPublisher(
312+
ctx.Collectors, ctx.Publishers, opts, connOpts)
313+
if err != nil {
314+
return err
315+
}
316+
defer closeFunc()
317+
318+
source := replicaset.NewCConfigSource(collector, publisher,
319+
replicaset.KeyPicker(pickPatchKey))
320+
err = source.AddRole(replicaset.RolesAddCtx{
321+
InstName: ctx.InstName,
322+
GroupName: ctx.GroupName,
323+
ReplicasetName: ctx.ReplicasetName,
324+
IsGlobal: ctx.IsGlobal,
325+
RoleName: ctx.RoleName,
326+
Force: ctx.Force,
327+
})
328+
if err == nil {
329+
log.Info("Done.")
330+
}
331+
return err
332+
}

cli/cmd/cluster.go

+73
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,8 @@ var switchStatusCtx = clustercmd.SwitchStatusCtx{
6464
TaskID: "",
6565
}
6666

67+
var rolesAddCtx = clustercmd.RolesAddCtx{}
68+
6769
var (
6870
defaultSwitchTimeout uint64 = 30
6971
clusterIntegrityPrivateKey string
@@ -201,9 +203,49 @@ func newClusterReplicasetCmd() *cobra.Command {
201203
"skip selecting a key for patching")
202204
integrity.RegisterWithIntegrityFlag(expelCmd.Flags(), &clusterIntegrityPrivateKey)
203205

206+
rolesCmd := &cobra.Command{
207+
Use: "roles",
208+
Short: "Add or remove roles in cluster replicaset",
209+
}
210+
211+
addRolesCmd := &cobra.Command{
212+
Use: "add <URI> <ROLE_NAME> [flags]",
213+
Short: "Add role to an instance, group or instance",
214+
Long: "Add role to an instance, group or instance\n\n" + clusterUriHelp,
215+
Run: func(cmd *cobra.Command, args []string) {
216+
cmdCtx.CommandName = cmd.Name()
217+
err := modules.RunCmd(&cmdCtx, cmd.CommandPath(), &modulesInfo,
218+
internalClusterReplicasetRolesAddModule, args)
219+
util.HandleCmdErr(cmd, err)
220+
},
221+
Example: "tt cluster replicaset roles add http://user:pass@localhost:3301" +
222+
" roles.metrics-export --instance_name master",
223+
Args: cobra.ExactArgs(2),
224+
}
225+
226+
addRolesCmd.Flags().StringVarP(&rolesAddCtx.ReplicasetName, "replicaset", "r", "",
227+
"name of a target replicaset")
228+
addRolesCmd.Flags().StringVarP(&rolesAddCtx.GroupName, "group", "g", "",
229+
"name of a target group")
230+
addRolesCmd.Flags().StringVarP(&rolesAddCtx.InstName, "instance", "i", "",
231+
"name of a target instance")
232+
addRolesCmd.Flags().BoolVarP(&rolesAddCtx.IsGlobal, "global", "G", false,
233+
"global config context")
234+
235+
addRolesCmd.Flags().StringVarP(&rolesAddCtx.Username, "username", "u", "",
236+
"username (used as etcd/tarantool config storage credentials)")
237+
addRolesCmd.Flags().StringVarP(&rolesAddCtx.Password, "password", "p", "",
238+
"password (used as etcd/tarantool config storage credentials)")
239+
addRolesCmd.Flags().BoolVarP(&rolesAddCtx.Force, "force", "f", false,
240+
"skip selecting a key for patching")
241+
integrity.RegisterWithIntegrityFlag(addRolesCmd.Flags(), &clusterIntegrityPrivateKey)
242+
243+
rolesCmd.AddCommand(addRolesCmd)
244+
204245
cmd.AddCommand(promoteCmd)
205246
cmd.AddCommand(demoteCmd)
206247
cmd.AddCommand(expelCmd)
248+
cmd.AddCommand(rolesCmd)
207249

208250
return cmd
209251
}
@@ -479,6 +521,27 @@ func internalClusterReplicasetExpelModule(cmdCtx *cmdcontext.CmdCtx, args []stri
479521
return clustercmd.Expel(uri, expelCtx)
480522
}
481523

524+
// internalClusterReplicasetRolesAddModule is a "cluster replicaset roles add" command.
525+
func internalClusterReplicasetRolesAddModule(cmdCtx *cmdcontext.CmdCtx, args []string) error {
526+
if err := checkRolesAddFlags(); err != nil {
527+
return err
528+
}
529+
530+
uri, err := parseUrl(args[0])
531+
if err != nil {
532+
return fmt.Errorf("failed to parse config source URI: %w", err)
533+
}
534+
535+
rolesAddCtx.Collectors, rolesAddCtx.Publishers, err = createDataCollectorsAndDataPublishers(
536+
cmdCtx.Integrity, clusterIntegrityPrivateKey)
537+
if err != nil {
538+
return err
539+
}
540+
541+
rolesAddCtx.RoleName = args[1]
542+
return clustercmd.AddRole(uri, rolesAddCtx)
543+
}
544+
482545
// internalClusterFailoverSwitchModule is as "cluster failover switch" command
483546
func internalClusterFailoverSwitchModule(cmdCtx *cmdcontext.CmdCtx, args []string) error {
484547
uri, err := parseUrl(args[0])
@@ -564,3 +627,13 @@ func parseAppStr(cmdCtx *cmdcontext.CmdCtx, appStr string) (string, string, stri
564627

565628
return configPath, appName, instName, nil
566629
}
630+
631+
// checkRolesAddFlags checks that flags from 'cluster rs roles add' command
632+
// have correct values.
633+
func checkRolesAddFlags() error {
634+
if rolesAddCtx.IsGlobal == false && rolesAddCtx.GroupName == "" &&
635+
rolesAddCtx.ReplicasetName == "" && rolesAddCtx.InstName == "" {
636+
return util.NewArgError("need to provide flag(s) with scope roles will added")
637+
}
638+
return nil
639+
}

cli/init/init.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ const (
2121
// InitCtx contains information for tt config creation.
2222
type InitCtx struct {
2323
// SkipConfig - if set, disables cartridge & tarantoolctl config analysis,
24-
// so init does not try to get directories information from exitsting config files.
24+
// so init does not try to get directories information from existing config files.
2525
SkipConfig bool
2626
// ForceMode, if set, tt config is re-written without a question.
2727
ForceMode bool

cli/replicaset/cconfig.go

+8
Original file line numberDiff line numberDiff line change
@@ -853,6 +853,14 @@ func patchCConfigElectionMode(config *libcluster.Config,
853853
return config, nil
854854
}
855855

856+
func patchCConfigAddRole(config *libcluster.Config, path []string,
857+
roleNames []string) (*libcluster.Config, error) {
858+
if err := config.Set(path, roleNames); err != nil {
859+
return nil, err
860+
}
861+
return config, nil
862+
}
863+
856864
// getCConfigInstance extracts an instance from the cluster config.
857865
func getCConfigInstance(
858866
config *libcluster.ClusterConfig, instName string) (cconfigInstance, error) {

cli/replicaset/configsource.go

+132
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package replicaset
33
import (
44
"errors"
55
"fmt"
6+
"slices"
67
"sort"
78
"strings"
89

@@ -12,6 +13,12 @@ import (
1213
// KeyPicker picks a key to patch.
1314
type KeyPicker func([]string, bool, string) (int, error)
1415

16+
// path describes a path of target with its depth level in config.
17+
type path struct {
18+
path []string
19+
depth int
20+
}
21+
1522
// CConfigSource describes the cluster config source.
1623
type CConfigSource struct {
1724
collector libcluster.DataCollector
@@ -108,6 +115,74 @@ func (c *CConfigSource) patchInstanceConfig(instanceName string, force bool,
108115
return nil
109116
}
110117

118+
// patchConfigWithRoles runs an config patching pipeline with adding roles.
119+
func (c *CConfigSource) patchConfigWithRoles(ctx RolesAddCtx,
120+
getPathFunc func(clusterConfig libcluster.ClusterConfig,
121+
ctx RolesAddCtx) (paths []path, err error),
122+
patchFunc func(config *libcluster.Config, path []string,
123+
roleNames []string) (*libcluster.Config, error),
124+
) error {
125+
configData, clusterConfig, err := collectCConfig(c.collector)
126+
if err != nil {
127+
return err
128+
}
129+
paths, err := getPathFunc(clusterConfig, ctx)
130+
if err != nil {
131+
return err
132+
}
133+
134+
var (
135+
target patchTarget
136+
patched *libcluster.Config
137+
)
138+
139+
for _, path := range paths {
140+
value, err := clusterConfig.RawConfig.Get(path.path)
141+
var notExistErr libcluster.NotExistError
142+
if err != nil && !errors.As(err, &notExistErr) {
143+
return err
144+
}
145+
var existingRoles []string
146+
if value != nil {
147+
existingRoles, err = parseRoles(value)
148+
if err != nil {
149+
return err
150+
}
151+
}
152+
if len(existingRoles) > 0 && slices.Index(existingRoles, ctx.RoleName) != -1 {
153+
return fmt.Errorf("role %q already exists in %s",
154+
ctx.RoleName, strings.Join(path.path, "/"))
155+
}
156+
// If role is not existing in requested path, append it.
157+
existingRoles = append(existingRoles, ctx.RoleName)
158+
159+
targets, err := getCConfigPatchTargets(configData, path.path, path.depth)
160+
if err != nil {
161+
return err
162+
}
163+
target, err = c.pickTarget(targets, ctx.Force, strings.Join(path.path, "/"))
164+
if err != nil {
165+
return err
166+
}
167+
168+
curPatched, err := patchFunc(target.config, path.path, existingRoles)
169+
if err != nil {
170+
return err
171+
}
172+
173+
if patched != nil {
174+
patched.Merge(curPatched)
175+
continue
176+
}
177+
patched = curPatched
178+
}
179+
err = c.publisher.Publish(target.key, target.revision, []byte(patched.String()))
180+
if err != nil {
181+
return fmt.Errorf("failed to publish the config: %w", err)
182+
}
183+
return nil
184+
}
185+
111186
// Promote patches a config to promote an instance.
112187
func (c *CConfigSource) Promote(ctx PromoteCtx) error {
113188
return c.patchInstanceConfig(
@@ -144,6 +219,63 @@ func (c *CConfigSource) Expel(ctx ExpelCtx) error {
144219
)
145220
}
146221

222+
// AddRole patches a config to add role to a config.
223+
func (c *CConfigSource) AddRole(ctx RolesAddCtx) error {
224+
return c.patchConfigWithRoles(ctx, getCConfigRolesPath, patchCConfigAddRole)
225+
}
226+
227+
// getCConfigRolesPath returns a path and it's minimum interesting depth
228+
// to patch the config for role addition.
229+
func getCConfigRolesPath(clusterConfig libcluster.ClusterConfig, ctx RolesAddCtx) ([]path, error) {
230+
var paths []path
231+
if ctx.IsGlobal {
232+
paths = append(paths, path{
233+
path: []string{"roles"},
234+
depth: 0,
235+
})
236+
}
237+
if ctx.GroupName != "" {
238+
p := []string{"groups", ctx.GroupName}
239+
if _, err := clusterConfig.RawConfig.Get(p); err != nil {
240+
var notExistErr libcluster.NotExistError
241+
if errors.As(err, &notExistErr) {
242+
return []path{}, fmt.Errorf("cannot find group %q", ctx.GroupName)
243+
}
244+
return []path{}, fmt.Errorf("failed to build a group path: %w", err)
245+
}
246+
paths = append(paths, path{
247+
path: append(p, "roles"),
248+
depth: len(p),
249+
})
250+
}
251+
if ctx.ReplicasetName != "" {
252+
var group string
253+
var ok bool
254+
if group, ok = libcluster.FindGroupByReplicaset(clusterConfig, ctx.ReplicasetName); !ok {
255+
return []path{}, fmt.Errorf("cannot find replicaset %q above group", ctx.ReplicasetName)
256+
}
257+
p := []string{"groups", group, "replicasets", ctx.ReplicasetName}
258+
paths = append(paths, path{
259+
path: append(p, "roles"),
260+
depth: len(p),
261+
})
262+
}
263+
if ctx.InstName != "" {
264+
var group, replicaset string
265+
var ok bool
266+
if group, replicaset, ok = libcluster.FindInstance(clusterConfig, ctx.InstName); !ok {
267+
return []path{}, fmt.Errorf("cannot find instance %q above group and/or replicaset",
268+
ctx.InstName)
269+
}
270+
p := []string{"groups", group, "replicasets", replicaset, "instances", ctx.InstName}
271+
paths = append(paths, path{
272+
path: append(p, "roles"),
273+
depth: len(p),
274+
})
275+
}
276+
return paths, nil
277+
}
278+
147279
// getCConfigPromotePath returns a path and it's minimum interesting depth
148280
// to patch the config for instance promoting.
149281
// For example, if we have the path "/groups/g/replicasets/r/leader" then

0 commit comments

Comments
 (0)