/
cobra_builder.go
354 lines (299 loc) · 11.9 KB
/
cobra_builder.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
package cmd
import (
"fmt"
"log"
"slices"
"strconv"
"strings"
"github.com/azure/azure-dev/cli/azd/cmd/actions"
"github.com/azure/azure-dev/cli/azd/cmd/middleware"
"github.com/azure/azure-dev/cli/azd/pkg/input"
"github.com/azure/azure-dev/cli/azd/pkg/ioc"
"github.com/azure/azure-dev/cli/azd/pkg/output"
"github.com/azure/azure-dev/cli/azd/pkg/tools"
"github.com/spf13/cobra"
)
const cDocsFlagName = "docs"
// CobraBuilder manages the construction of the cobra command tree from nested ActionDescriptors
type CobraBuilder struct {
container *ioc.NestedContainer
}
// Creates a new instance of the Cobra builder
func NewCobraBuilder(container *ioc.NestedContainer) *CobraBuilder {
return &CobraBuilder{
container: container,
}
}
// Builds a cobra Command for the specified action descriptor
func (cb *CobraBuilder) BuildCommand(descriptor *actions.ActionDescriptor) (*cobra.Command, error) {
cmd := descriptor.Options.Command
if cmd.Use == "" {
cmd.Use = descriptor.Name
}
// Build the full command tree
for _, childDescriptor := range descriptor.Children() {
childCmd, err := cb.BuildCommand(childDescriptor)
if err != nil {
return nil, err
}
cmd.AddCommand(childCmd)
}
// Bind root command after command tree has been established
// This ensures the command path is ready and consistent across all nested commands
if descriptor.Parent() == nil {
if err := cb.bindCommand(cmd, descriptor); err != nil {
return nil, err
}
}
// Configure action resolver for leaf commands
if !cmd.HasSubCommands() {
if err := cb.configureActionResolver(cmd, descriptor); err != nil {
return nil, err
}
}
return cmd, nil
}
// Configures the cobra command 'RunE' function to running the composed middleware and action for the
// current action descriptor
func (cb *CobraBuilder) configureActionResolver(cmd *cobra.Command, descriptor *actions.ActionDescriptor) error {
// Dev Error: Either an action resolver or RunE must be set
if descriptor.Options.ActionResolver == nil && cmd.RunE == nil {
return fmt.Errorf(
//nolint:lll
"action descriptor for '%s' must be configured with either an ActionResolver or a Cobra RunE command",
cmd.CommandPath(),
)
}
// Dev Error: Both action resolver and RunE have been defined
if descriptor.Options.ActionResolver != nil && cmd.RunE != nil {
return fmt.Errorf(
//nolint:lll
"action descriptor for '%s' must be configured with either an ActionResolver or a Cobra RunE command but NOT both",
cmd.CommandPath(),
)
}
// Only bind command to action if an action resolver had been defined
// and when a RunE hasn't already been set
if descriptor.Options.ActionResolver == nil || cmd.RunE != nil {
return nil
}
cmd.RunE = func(cmd *cobra.Command, args []string) error {
// Register root go context that will be used for resolving singleton dependencies
ctx := tools.WithInstalledCheckCache(cmd.Context())
ioc.RegisterInstance(cb.container, ctx)
// Create new container scope for the current command
cmdContainer, err := cb.container.NewScope()
if err != nil {
return fmt.Errorf("failed creating new scope for command, %w", err)
}
// Registers the following to enable injection into actions that require them
ioc.RegisterInstance(cmdContainer, ctx)
ioc.RegisterInstance(cmdContainer, cmd)
ioc.RegisterInstance(cmdContainer, args)
ioc.RegisterInstance(cmdContainer, cmdContainer)
ioc.RegisterInstance[ioc.ServiceLocator](cmdContainer, cmdContainer)
// Register any required middleware registered for the current action descriptor
middlewareRunner := middleware.NewMiddlewareRunner(cmdContainer)
if err := cb.registerMiddleware(middlewareRunner, descriptor); err != nil {
return err
}
runOptions := &middleware.Options{
Name: cmd.Name(),
CommandPath: cmd.CommandPath(),
Aliases: cmd.Aliases,
Flags: cmd.Flags(),
Args: args,
}
// Set the container that should be used for resolving middleware components
runOptions.WithContainer(cmdContainer)
// Run the middleware chain with action
actionName := createActionName(cmd)
_, err = middlewareRunner.RunAction(ctx, runOptions, actionName)
// At this point, we know that there might be an error, so we can silence cobra from showing it after us.
cmd.SilenceErrors = true
return err
}
return nil
}
// docsFlag is a flag with a custom parsing implementation which changes the default behavior for printing help
// for all commands, when it is set as true.
// docsFlag keeps a reference to the cobra command where it belongs so it can update it.
// docsFlag also contains a callbacks to pull dependencies for the docs routine.
type docsFlag struct {
// reference to the command where the flag was added.
command *cobra.Command
consoleFn func() input.Console
value bool
defaultHelpFn func(*cobra.Command, []string)
}
// returns the flag value
func (df *docsFlag) String() string {
return fmt.Sprintf("%t", df.value)
}
// define flag type
func (df *docsFlag) Type() string {
return "bool"
}
// Set not only initialize the flag value, but it also turns the help flag true and defines the HelpFunc for the command.
// This wiring forces cobra to react as it the --help flag was provided and stop the command early to run the HelpFunc.
func (df *docsFlag) Set(value string) error {
v, err := strconv.ParseBool(value)
if err != nil {
return fmt.Errorf("invalid value for boolean --docs parameter")
}
df.value = v
if !df.value {
return nil
}
// Setting help to true will make cobra to stop and call the HelpFunc
if err = df.command.Flag("help").Value.Set("true"); err != nil {
// dev-issue: help flag should be already been added when
log.Panic("tried to set help after docs parameter: %w", err)
}
// keeping the default help function allows to set --help with higher priority and use it
// in case of finding --docs and --help
df.defaultHelpFn = df.command.HelpFunc()
// set help func for doing docs
df.command.SetHelpFunc(func(c *cobra.Command, args []string) {
console := df.consoleFn()
ctx := c.Context()
ctx = tools.WithInstalledCheckCache(ctx)
if slices.Contains(args, "--help") {
df.defaultHelpFn(c, args)
return
}
commandPath := strings.ReplaceAll(c.CommandPath(), " ", "-")
commandDocsUrl := cReferenceDocumentationUrl + commandPath
openWithDefaultBrowser(ctx, console, commandDocsUrl)
})
return nil
}
// Binds the intersection of cobra command options and action descriptor options
func (cb *CobraBuilder) bindCommand(cmd *cobra.Command, descriptor *actions.ActionDescriptor) error {
actionName := createActionName(cmd)
// Automatically adds a consistent help flag
cmd.Flags().BoolP("help", "h", false, fmt.Sprintf("Gets help for %s.", cmd.Name()))
// docs flags for all commands
docsFlag := &docsFlag{
command: cmd,
consoleFn: func() input.Console {
var console input.Console
if err := cb.container.Resolve(&console); err != nil {
log.Panic("creating docs flag: %w", err)
}
return console
},
}
flag := cmd.Flags().VarPF(
docsFlag, cDocsFlagName, "", fmt.Sprintf("Opens the documentation for %s in your web browser.", cmd.CommandPath()))
flag.NoOptDefVal = "true"
// Consistently registers output formats for the descriptor
if len(descriptor.Options.OutputFormats) > 0 {
output.AddOutputParam(cmd, descriptor.Options.OutputFormats, descriptor.Options.DefaultFormat)
}
// Create, register and bind flags when required
if descriptor.Options.FlagsResolver != nil {
ioc.RegisterInstance(cb.container, cmd)
// The flags resolver is constructed and bound to the cobra command via dependency injection
// This allows flags to be options and support any set of required dependencies
if err := cb.container.RegisterSingletonAndInvoke(descriptor.Options.FlagsResolver); err != nil {
return fmt.Errorf(
//nolint:lll
"failed registering FlagsResolver for action '%s'. Ensure the resolver is a valid go function and resolves without error. %w",
actionName,
err,
)
}
}
// Registers and bind action resolves when required
// Action resolvers are essential go functions that create the instance of the required actions.Action
// These functions are typically the constructor function for the action. ex) newDeployAction(...)
// Action resolvers can take any number of dependencies and instantiated via the IoC container
if descriptor.Options.ActionResolver != nil {
if err := cb.container.RegisterNamedTransient(actionName, descriptor.Options.ActionResolver); err != nil {
return fmt.Errorf(
//nolint:lll
"failed registering ActionResolver for action '%s'. Ensure the resolver is a valid go function and resolves without error. %w",
actionName,
err,
)
}
}
// Bind flag completions
// Since flags are lazily loaded we need to wait until after command flags are wired up before
// any flag completion functions are registered
for flag, completionFn := range descriptor.FlagCompletions() {
if err := cmd.RegisterFlagCompletionFunc(flag, completionFn); err != nil {
return fmt.Errorf("failed registering flag completion function for '%s', %w", flag, err)
}
}
// Bind the child commands for the current descriptor
for _, childDescriptor := range descriptor.Children() {
childCmd := childDescriptor.Options.Command
if err := cb.bindCommand(childCmd, childDescriptor); err != nil {
return err
}
}
if descriptor.Options.GroupingOptions.RootLevelHelp != actions.CmdGroupNone {
if cmd.Annotations == nil {
cmd.Annotations = make(map[string]string)
}
actions.SetGroupCommandAnnotation(cmd, descriptor.Options.GroupingOptions.RootLevelHelp)
}
// `generateCmdHelp` sets a default help section when `descriptor.Options.HelpOptions` is nil.
// This call ensures all commands gets the same help formatting.
cmd.SetHelpTemplate(generateCmdHelp(cmd, generateCmdHelpOptions{
Description: cmdHelpGenerator(descriptor.Options.HelpOptions.Description),
Usage: cmdHelpGenerator(descriptor.Options.HelpOptions.Usage),
Commands: cmdHelpGenerator(descriptor.Options.HelpOptions.Commands),
Flags: cmdHelpGenerator(descriptor.Options.HelpOptions.Flags),
Footer: cmdHelpGenerator(descriptor.Options.HelpOptions.Footer),
}))
return nil
}
// Registers all middleware components for the current command and any parent descriptors
// Middleware components are insure to run in the order that they were registered from the
// root registration, down through action groups and ultimately individual actions
func (cb *CobraBuilder) registerMiddleware(
middlewareRunner *middleware.MiddlewareRunner,
descriptor *actions.ActionDescriptor,
) error {
chain := []*actions.MiddlewareRegistration{}
current := descriptor
// Recursively loop through any action describer and their parents
for {
middleware := current.Middleware()
for i := len(middleware) - 1; i > -1; i-- {
registration := middleware[i]
// Only use the middleware when the predicate resolves truthy or if not defined
// Registration predicates are useful for when you want to selectively want to
// register a middleware based on the descriptor options
// Ex) Telemetry middleware registered for all actions except 'version'
if registration.Predicate == nil || registration.Predicate(descriptor) {
chain = append(chain, middleware[i])
}
}
if current.Parent() == nil {
break
}
current = current.Parent()
}
// Register middleware in reverse order so middleware registered
// higher up the command structure are resolved before lower registrations
for i := len(chain) - 1; i > -1; i-- {
registration := chain[i]
if err := middlewareRunner.Use(registration.Name, registration.Resolver); err != nil {
return err
}
}
return nil
}
// Composes a consistent action name for the specified cobra command
// ex) azd config list becomes 'azd-config-list-action'
func createActionName(cmd *cobra.Command) string {
actionName := cmd.CommandPath()
actionName = strings.TrimSpace(actionName)
actionName = strings.ReplaceAll(actionName, " ", "-")
actionName = fmt.Sprintf("%s-action", actionName)
return strings.ToLower(actionName)
}