Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

azd integration with IoC enhancements #4132

Draft
wants to merge 7 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions cli/azd/.vscode/cspell.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ words:
- azcloud
- usgovcloudapi
- chinacloudapi
- wbreza
languageSettings:
- languageId: go
ignoreRegExpList:
Expand Down Expand Up @@ -109,6 +110,9 @@ overrides:
- filename: test/functional/testdata/samples/funcapp/getting_started.md
words:
- funcignore
- filename: internal/vsrpc/handler.go
words:
- arity
ignorePaths:
- "**/*_test.go"
- "**/mock*.go"
37 changes: 21 additions & 16 deletions cli/azd/cmd/cobra_builder.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package cmd

import (
"context"
"fmt"
"log"
"slices"
Expand All @@ -14,32 +15,33 @@ import (
"github.com/azure/azure-dev/cli/azd/pkg/output"
"github.com/azure/azure-dev/cli/azd/pkg/tools"
"github.com/spf13/cobra"
"github.com/wbreza/container/v4"
)

const cDocsFlagName = "docs"

// CobraBuilder manages the construction of the cobra command tree from nested ActionDescriptors
type CobraBuilder struct {
container *ioc.NestedContainer
container *container.Container
}

// Creates a new instance of the Cobra builder
func NewCobraBuilder(container *ioc.NestedContainer) *CobraBuilder {
func NewCobraBuilder(container *container.Container) *CobraBuilder {
return &CobraBuilder{
container: container,
}
}

// Builds a cobra Command for the specified action descriptor
func (cb *CobraBuilder) BuildCommand(descriptor *actions.ActionDescriptor) (*cobra.Command, error) {
func (cb *CobraBuilder) BuildCommand(ctx context.Context, 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)
childCmd, err := cb.BuildCommand(ctx, childDescriptor)
if err != nil {
return nil, err
}
Expand All @@ -50,7 +52,7 @@ func (cb *CobraBuilder) BuildCommand(descriptor *actions.ActionDescriptor) (*cob
// 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 {
if err := cb.bindCommand(ctx, cmd, descriptor); err != nil {
return nil, err
}
}
Expand Down Expand Up @@ -95,7 +97,6 @@ func (cb *CobraBuilder) configureActionResolver(cmd *cobra.Command, descriptor *
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()
Expand All @@ -104,11 +105,10 @@ func (cb *CobraBuilder) configureActionResolver(cmd *cobra.Command, descriptor *
}

// 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)
container.MustRegisterInstance(cmdContainer, cmd)
container.MustRegisterInstance(cmdContainer, args)
container.MustRegisterInstance(cmdContainer, cmdContainer)
container.MustRegisterInstanceAs[ioc.ServiceLocator](cmdContainer, cmdContainer)

// Register any required middleware registered for the current action descriptor
middlewareRunner := middleware.NewMiddlewareRunner(cmdContainer)
Expand Down Expand Up @@ -204,7 +204,7 @@ func (df *docsFlag) Set(value string) error {
}

// Binds the intersection of cobra command options and action descriptor options
func (cb *CobraBuilder) bindCommand(cmd *cobra.Command, descriptor *actions.ActionDescriptor) error {
func (cb *CobraBuilder) bindCommand(ctx context.Context, cmd *cobra.Command, descriptor *actions.ActionDescriptor) error {
actionName := createActionName(cmd)

// Automatically adds a consistent help flag
Expand All @@ -214,7 +214,7 @@ func (cb *CobraBuilder) bindCommand(cmd *cobra.Command, descriptor *actions.Acti
command: cmd,
consoleFn: func() input.Console {
var console input.Console
if err := cb.container.Resolve(&console); err != nil {
if err := cb.container.Resolve(context.Background(), &console); err != nil {
log.Panic("creating docs flag: %w", err)
}
return console
Expand All @@ -231,11 +231,16 @@ func (cb *CobraBuilder) bindCommand(cmd *cobra.Command, descriptor *actions.Acti

// Create, register and bind flags when required
if descriptor.Options.FlagsResolver != nil {
ioc.RegisterInstance(cb.container, cmd)
if err := cb.container.RegisterInstance(cmd); err != nil {
return err
}

// 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 {
if err := cb.container.InvokeAndRegister(ctx, container.RegisterOptions{
Resolver: descriptor.Options.FlagsResolver,
Lifetime: container.Singleton,
}); 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",
Expand Down Expand Up @@ -272,7 +277,7 @@ func (cb *CobraBuilder) bindCommand(cmd *cobra.Command, descriptor *actions.Acti
// 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 {
if err := cb.bindCommand(ctx, childCmd, childDescriptor); err != nil {
return err
}
}
Expand Down
83 changes: 41 additions & 42 deletions cli/azd/cmd/cobra_builder_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,11 @@ import (
"github.com/azure/azure-dev/cli/azd/cmd/middleware"
"github.com/azure/azure-dev/cli/azd/internal"
"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/test/mocks"
"github.com/spf13/cobra"
"github.com/stretchr/testify/require"
"github.com/wbreza/container/v4"
)

type contextKey string
Expand All @@ -24,7 +24,7 @@ const middlewareBName contextKey = "middleware-B"

func Test_BuildAndRunSimpleCommand(t *testing.T) {
ran := false
container := ioc.NewNestedContainer(nil)
container := container.New()

root := actions.NewActionDescriptor("root", &actions.ActionDescriptorOptions{
Command: &cobra.Command{
Expand All @@ -35,60 +35,62 @@ func Test_BuildAndRunSimpleCommand(t *testing.T) {
},
})

ctx := context.Background()
builder := NewCobraBuilder(container)
cmd, err := builder.BuildCommand(root)
cmd, err := builder.BuildCommand(ctx, root)

require.NotNil(t, cmd)
require.NoError(t, err)

// Disable args processing from os:args
cmd.SetArgs([]string{})
err = cmd.ExecuteContext(context.Background())
err = cmd.ExecuteContext(ctx)

require.NoError(t, err)
require.True(t, ran)
}

func Test_BuildAndRunSimpleAction(t *testing.T) {
container := ioc.NewNestedContainer(nil)
setup(container)
rootContainer := container.New()
setup(rootContainer)

root := actions.NewActionDescriptor("root", &actions.ActionDescriptorOptions{
ActionResolver: newTestAction,
FlagsResolver: newTestFlags,
})

builder := NewCobraBuilder(container)
cmd, err := builder.BuildCommand(root)
ctx := context.Background()
builder := NewCobraBuilder(rootContainer)
cmd, err := builder.BuildCommand(ctx, root)

require.NotNil(t, cmd)
require.NoError(t, err)

cmd.SetArgs([]string{"-r"})
err = cmd.ExecuteContext(context.Background())
err = cmd.ExecuteContext(ctx)

require.NoError(t, err)
}

func Test_BuildAndRunSimpleActionWithMiddleware(t *testing.T) {
container := ioc.NewNestedContainer(nil)
setup(container)
rootContainer := container.New()
setup(rootContainer)

root := actions.NewActionDescriptor("root", &actions.ActionDescriptorOptions{
ActionResolver: newTestAction,
FlagsResolver: newTestFlags,
}).UseMiddleware("A", newTestMiddlewareA)

builder := NewCobraBuilder(container)
cmd, err := builder.BuildCommand(root)
ctx := context.Background()
builder := NewCobraBuilder(rootContainer)
cmd, err := builder.BuildCommand(ctx, root)

require.NotNil(t, cmd)
require.NoError(t, err)

actionRan := false
middlewareRan := false

ctx := context.Background()
ctx = context.WithValue(ctx, actionName, &actionRan)
ctx = context.WithValue(ctx, middlewareAName, &middlewareRan)

Expand All @@ -101,8 +103,8 @@ func Test_BuildAndRunSimpleActionWithMiddleware(t *testing.T) {
}

func Test_BuildAndRunActionWithNestedMiddleware(t *testing.T) {
container := ioc.NewNestedContainer(nil)
setup(container)
rootContainer := container.New()
setup(rootContainer)

root := actions.NewActionDescriptor("root", nil).
UseMiddleware("A", newTestMiddlewareA)
Expand All @@ -112,8 +114,9 @@ func Test_BuildAndRunActionWithNestedMiddleware(t *testing.T) {
FlagsResolver: newTestFlags,
}).UseMiddleware("B", newTestMiddlewareB)

builder := NewCobraBuilder(container)
cmd, err := builder.BuildCommand(root)
ctx := context.Background()
builder := NewCobraBuilder(rootContainer)
cmd, err := builder.BuildCommand(ctx, root)

require.NotNil(t, cmd)
require.NoError(t, err)
Expand All @@ -122,7 +125,6 @@ func Test_BuildAndRunActionWithNestedMiddleware(t *testing.T) {
middlewareARan := false
middlewareBRan := false

ctx := context.Background()
ctx = context.WithValue(ctx, actionName, &actionRan)
ctx = context.WithValue(ctx, middlewareAName, &middlewareARan)
ctx = context.WithValue(ctx, middlewareBName, &middlewareBRan)
Expand All @@ -137,8 +139,8 @@ func Test_BuildAndRunActionWithNestedMiddleware(t *testing.T) {
}

func Test_BuildAndRunActionWithNestedAndConditionalMiddleware(t *testing.T) {
container := ioc.NewNestedContainer(nil)
setup(container)
rootContainer := container.New()
setup(rootContainer)

root := actions.NewActionDescriptor("root", nil).
// This middleware will always run because its registered at the root
Expand All @@ -154,8 +156,9 @@ func Test_BuildAndRunActionWithNestedAndConditionalMiddleware(t *testing.T) {
return false
})

builder := NewCobraBuilder(container)
cmd, err := builder.BuildCommand(root)
ctx := context.Background()
builder := NewCobraBuilder(rootContainer)
cmd, err := builder.BuildCommand(ctx, root)

require.NotNil(t, cmd)
require.NoError(t, err)
Expand All @@ -164,7 +167,6 @@ func Test_BuildAndRunActionWithNestedAndConditionalMiddleware(t *testing.T) {
middlewareARan := false
middlewareBRan := false

ctx := context.Background()
ctx = context.WithValue(ctx, actionName, &actionRan)
ctx = context.WithValue(ctx, middlewareAName, &middlewareARan)
ctx = context.WithValue(ctx, middlewareBName, &middlewareBRan)
Expand All @@ -179,7 +181,7 @@ func Test_BuildAndRunActionWithNestedAndConditionalMiddleware(t *testing.T) {
}

func Test_BuildCommandsWithAutomaticHelpAndOutputFlags(t *testing.T) {
container := ioc.NewNestedContainer(nil)
rootContainer := container.New()

root := actions.NewActionDescriptor("root", &actions.ActionDescriptorOptions{
OutputFormats: []output.Format{output.JsonFormat, output.TableFormat},
Expand All @@ -191,8 +193,9 @@ func Test_BuildCommandsWithAutomaticHelpAndOutputFlags(t *testing.T) {
},
})

cobraBuilder := NewCobraBuilder(container)
cmd, err := cobraBuilder.BuildCommand(root)
ctx := context.Background()
cobraBuilder := NewCobraBuilder(rootContainer)
cmd, err := cobraBuilder.BuildCommand(ctx, root)

require.NoError(t, err)
require.NotNil(t, cmd)
Expand All @@ -218,11 +221,9 @@ func Test_BuildCommandsWithAutomaticHelpAndOutputFlags(t *testing.T) {
}

func Test_RunDocsFlow(t *testing.T) {
container := ioc.NewNestedContainer(nil)
rootContainer := container.New()
testCtx := mocks.NewMockContext(context.Background())
container.MustRegisterSingleton(func() input.Console {
return testCtx.Console
})
container.MustRegisterInstanceAs[input.Console](rootContainer, testCtx.Console)

root := actions.NewActionDescriptor("root", &actions.ActionDescriptorOptions{
OutputFormats: []output.Format{output.JsonFormat, output.TableFormat},
Expand All @@ -239,8 +240,8 @@ func Test_RunDocsFlow(t *testing.T) {
calledUrl = url
}

cobraBuilder := NewCobraBuilder(container)
cmd, err := cobraBuilder.BuildCommand(root)
cobraBuilder := NewCobraBuilder(rootContainer)
cmd, err := cobraBuilder.BuildCommand(*testCtx.Context, root)

require.NoError(t, err)
require.NotNil(t, cmd)
Expand All @@ -252,11 +253,9 @@ func Test_RunDocsFlow(t *testing.T) {
}

func Test_RunDocsAndHelpFlow(t *testing.T) {
container := ioc.NewNestedContainer(nil)
rootContainer := container.New()
testCtx := mocks.NewMockContext(context.Background())
container.MustRegisterSingleton(func() input.Console {
return testCtx.Console
})
container.MustRegisterInstanceAs[input.Console](rootContainer, testCtx.Console)

root := actions.NewActionDescriptor("root", &actions.ActionDescriptorOptions{
OutputFormats: []output.Format{output.JsonFormat, output.TableFormat},
Expand All @@ -273,8 +272,8 @@ func Test_RunDocsAndHelpFlow(t *testing.T) {
calledUrl = url
}

cobraBuilder := NewCobraBuilder(container)
cmd, err := cobraBuilder.BuildCommand(root)
cobraBuilder := NewCobraBuilder(rootContainer)
cmd, err := cobraBuilder.BuildCommand(*testCtx.Context, root)

require.NoError(t, err)
require.NotNil(t, cmd)
Expand All @@ -286,13 +285,13 @@ func Test_RunDocsAndHelpFlow(t *testing.T) {
require.Equal(t, "", calledUrl)
}

func setup(container *ioc.NestedContainer) {
registerCommonDependencies(container)
func setup(rootContainer *container.Container) {
registerCommonDependencies(rootContainer)
globalOptions := &internal.GlobalCommandOptions{
EnableTelemetry: false,
EnableDebugLogging: false,
}
ioc.RegisterInstance(container, globalOptions)
container.MustRegisterInstance(rootContainer, globalOptions)
}

// Types for test
Expand Down
Loading
Loading