Skip to content

Commit 2e7cb98

Browse files
authored
feat: allow using glob pattern as an argument (#348)
1 parent 7df5aa2 commit 2e7cb98

21 files changed

+146
-47
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
### Added
11+
12+
- Allow using unix style glob pattern as an argument. For example, if there are two servers available with titles `server-1` and `server-2`, these servers can be stopped with `upctl server stop server-*` command.
13+
1014
## [3.13.0] - 2024-12-13
1115

1216
### Added

internal/commands/runcommand.go

Lines changed: 38 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,7 @@ func commandRunE(command Command, service internal.AllServices, config *config.C
2626
switch typedCommand := command.(type) {
2727
case NoArgumentCommand:
2828
cmdLogger.Debug("executing without arguments", "arguments", args)
29-
// need to pass in fake arguments here, to actually trigger execution
30-
results, err := execute(typedCommand, executor, []string{""}, 1,
29+
results, err := execute(typedCommand, executor, nil, resolveNone, 1,
3130
func(exec Executor, _ string) (output.Output, error) {
3231
return typedCommand.ExecuteWithoutArguments(exec)
3332
})
@@ -41,7 +40,7 @@ func commandRunE(command Command, service internal.AllServices, config *config.C
4140
if len(args) != 1 || args[0] == "" {
4241
return fmt.Errorf("exactly one positional argument is required")
4342
}
44-
results, err := execute(typedCommand, executor, args, 1, typedCommand.ExecuteSingleArgument)
43+
results, err := execute(typedCommand, executor, args, resolveOnly, 1, typedCommand.ExecuteSingleArgument)
4544
if err != nil {
4645
return err
4746
}
@@ -52,7 +51,7 @@ func commandRunE(command Command, service internal.AllServices, config *config.C
5251
if len(args) < 1 {
5352
return fmt.Errorf("at least one positional argument is required")
5453
}
55-
results, err := execute(typedCommand, executor, args, typedCommand.MaximumExecutions(), typedCommand.Execute)
54+
results, err := execute(typedCommand, executor, args, resolveAll, typedCommand.MaximumExecutions(), typedCommand.Execute)
5655
if err != nil {
5756
return err
5857
}
@@ -71,27 +70,50 @@ type resolvedArgument struct {
7170
Original string
7271
}
7372

74-
func resolveArguments(nc Command, exec Executor, args []string) (out []resolvedArgument, err error) {
73+
type resolveMode string
74+
75+
const (
76+
resolveAll resolveMode = "all"
77+
resolveNone resolveMode = "none"
78+
resolveOnly resolveMode = "only"
79+
)
80+
81+
func resolveArguments(nc Command, exec Executor, args []string, mode resolveMode) (out []resolvedArgument, err error) {
82+
if mode == resolveNone {
83+
return nil, nil
84+
}
85+
7586
if resolve, ok := nc.(resolver.ResolutionProvider); ok {
7687
argumentResolver, err := resolve.Get(exec.Context(), exec.All())
7788
if err != nil {
7889
return nil, fmt.Errorf("cannot get resolver: %w", err)
7990
}
8091
for _, arg := range args {
8192
resolved := argumentResolver(arg)
82-
value, err := resolved.GetOnly()
83-
out = append(out, resolvedArgument{Resolved: value, Error: err, Original: arg})
93+
if mode == resolveOnly {
94+
value, err := resolved.GetOnly()
95+
out = append(out, resolvedArgument{Resolved: value, Error: err, Original: arg})
96+
}
97+
if mode == resolveAll {
98+
values, err := resolved.GetAll()
99+
if err != nil {
100+
out = append(out, resolvedArgument{Resolved: "", Error: err, Original: arg})
101+
}
102+
for _, value := range values {
103+
out = append(out, resolvedArgument{Resolved: value, Error: err, Original: arg})
104+
}
105+
}
84106
}
85107
} else {
86108
for _, arg := range args {
87109
out = append(out, resolvedArgument{Resolved: arg, Original: arg})
88110
}
89111
}
90-
return
112+
return out, nil
91113
}
92114

93-
func execute(command Command, executor Executor, args []string, parallelRuns int, executeCommand func(exec Executor, arg string) (output.Output, error)) ([]output.Output, error) {
94-
resolvedArgs, err := resolveArguments(command, executor, args)
115+
func execute(command Command, executor Executor, args []string, mode resolveMode, parallelRuns int, executeCommand func(exec Executor, arg string) (output.Output, error)) ([]output.Output, error) {
116+
resolvedArgs, err := resolveArguments(command, executor, args, mode)
95117
if err != nil {
96118
// If authentication failed, return helpful message instead of the raw error.
97119
if clierrors.CheckAuthenticationFailed(err) {
@@ -112,6 +134,11 @@ func execute(command Command, executor Executor, args []string, parallelRuns int
112134
// make a copy of the original args to pass into the workers
113135
argQueue := resolvedArgs
114136

137+
// The worker logic below expects at least one argument to be present. Add an empty argument if none are present to execute commands that do not expect arguments.
138+
if argQueue == nil {
139+
argQueue = []resolvedArgument{{}}
140+
}
141+
115142
outputs := make([]output.Output, 0, len(args))
116143
executor.Debug("starting work", "workers", workerCount)
117144
for {
@@ -157,7 +184,7 @@ func execute(command Command, executor Executor, args []string, parallelRuns int
157184
outputs = append(outputs, res.Result)
158185
}
159186

160-
if len(outputs) >= len(args) {
187+
if len(outputs) >= len(resolvedArgs) {
161188
executor.Debug("execute done")
162189
// We're done, update ui for the last time and render the results
163190
executor.StopProgressLog()

internal/commands/runcommand_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -245,7 +245,7 @@ func TestExecute_Resolution(t *testing.T) {
245245
cfg := config.New()
246246
cfg.Viper().Set(config.KeyOutput, config.ValueOutputJSON)
247247
executor := NewExecutor(cfg, mService, flume.New("test"))
248-
outputs, err := execute(cmd, executor, []string{"a", "b", "failtoresolve", "c"}, 10, func(_ Executor, arg string) (output.Output, error) {
248+
outputs, err := execute(cmd, executor, []string{"a", "b", "failtoresolve", "c"}, resolveOnly, 10, func(_ Executor, arg string) (output.Output, error) {
249249
return output.OnlyMarshaled{Value: arg}, nil
250250
})
251251
assert.Len(t, outputs, 4)
@@ -281,7 +281,7 @@ func TestExecute_Error(t *testing.T) {
281281
cfg := config.New()
282282
cfg.Viper().Set(config.KeyOutput, config.ValueOutputJSON)
283283
executor := NewExecutor(cfg, mService, flume.New("test"))
284-
outputs, err := execute(cmd, executor, []string{"a", "b", "failToExecute", "c"}, 10, cmd.Execute)
284+
outputs, err := execute(cmd, executor, []string{"a", "b", "failToExecute", "c"}, resolveOnly, 10, cmd.Execute)
285285
assert.Len(t, outputs, 4)
286286
assert.NoError(t, err)
287287

internal/resolver/account.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ func (s CachingAccount) Get(ctx context.Context, svc internal.AllServices) (Reso
2121
return func(arg string) Resolved {
2222
rv := Resolved{Arg: arg}
2323
for _, account := range accounts {
24-
rv.AddMatch(account.Username, MatchArgWithWhitespace(arg, account.Username))
24+
rv.AddMatch(account.Username, MatchTitle(arg, account.Username))
2525
}
2626
return rv
2727
}, nil

internal/resolver/database.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ func (s CachingDatabase) Get(ctx context.Context, svc internal.AllServices) (Res
2222
return func(arg string) Resolved {
2323
rv := Resolved{Arg: arg}
2424
for _, db := range databases {
25-
rv.AddMatch(db.UUID, MatchArgWithWhitespace(arg, db.Title))
25+
rv.AddMatch(db.UUID, MatchTitle(arg, db.Title))
2626
rv.AddMatch(db.UUID, MatchUUID(arg, db.UUID))
2727
}
2828
return rv

internal/resolver/error.go

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ package resolver
22

33
import "fmt"
44

5-
// AmbiguousResolutionError is a resolver error when multiple matching entries have been found
5+
// AmbiguousResolutionError is a resolver error when multiple matching entries have been found.
66
type AmbiguousResolutionError string
77

88
var _ error = AmbiguousResolutionError("")
@@ -11,7 +11,16 @@ func (s AmbiguousResolutionError) Error() string {
1111
return fmt.Sprintf("'%v' is ambiguous, found multiple matches", string(s))
1212
}
1313

14-
// NotFoundError is a resolver error when no matching entries have been found
14+
// NonGlobMultipleMatchesError is a resolver error when multiple matching entries have been found with non-glob argument.
15+
type NonGlobMultipleMatchesError string
16+
17+
var _ error = NonGlobMultipleMatchesError("")
18+
19+
func (s NonGlobMultipleMatchesError) Error() string {
20+
return fmt.Sprintf("'%v' is not a glob pattern, but matches multiple values. To target multiple resources with single argument, use a glob pattern, e.g. server-*", string(s))
21+
}
22+
23+
// NotFoundError is a resolver error when no matching entries have been found.
1524
type NotFoundError string
1625

1726
var _ error = NotFoundError("")

internal/resolver/gateway.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ func (s CachingGateway) Get(ctx context.Context, svc internal.AllServices) (Reso
2121
return func(arg string) Resolved {
2222
rv := Resolved{Arg: arg}
2323
for _, gtw := range gateways {
24-
rv.AddMatch(gtw.UUID, MatchArgWithWhitespace(arg, gtw.Name))
24+
rv.AddMatch(gtw.UUID, MatchTitle(arg, gtw.Name))
2525
rv.AddMatch(gtw.UUID, MatchUUID(arg, gtw.UUID))
2626
}
2727
return rv

internal/resolver/ipaddress.go

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,7 @@ func (s CachingIPAddress) Get(ctx context.Context, svc internal.AllServices) (Re
2121
return func(arg string) Resolved {
2222
rv := Resolved{Arg: arg}
2323
for _, ipAddress := range ipaddresses.IPAddresses {
24-
rv.AddMatch(ipAddress.Address, MatchArgWithWhitespace(arg, ipAddress.PTRRecord))
25-
rv.AddMatch(ipAddress.Address, MatchArgWithWhitespace(arg, ipAddress.Address))
24+
rv.AddMatch(ipAddress.Address, MatchTitle(arg, ipAddress.PTRRecord, ipAddress.Address))
2625
}
2726
return rv
2827
}, nil

internal/resolver/kubernetes.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ func (s CachingKubernetes) Get(ctx context.Context, svc service.AllServices) (Re
2323
return func(arg string) Resolved {
2424
rv := Resolved{Arg: arg}
2525
for _, cluster := range clusters {
26-
rv.AddMatch(cluster.UUID, MatchArgWithWhitespace(arg, cluster.Name))
26+
rv.AddMatch(cluster.UUID, MatchTitle(arg, cluster.Name))
2727
rv.AddMatch(cluster.UUID, MatchUUID(arg, cluster.UUID))
2828
}
2929
return rv

internal/resolver/loadbalancer.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ func (s CachingLoadBalancer) Get(ctx context.Context, svc internal.AllServices)
2222
return func(arg string) Resolved {
2323
rv := Resolved{Arg: arg}
2424
for _, lb := range loadbalancers {
25-
rv.AddMatch(lb.UUID, MatchArgWithWhitespace(arg, lb.Name))
25+
rv.AddMatch(lb.UUID, MatchTitle(arg, lb.Name))
2626
rv.AddMatch(lb.UUID, MatchUUID(arg, lb.UUID))
2727
}
2828
return rv

0 commit comments

Comments
 (0)