From 619caa3831f4cb30bf3d5b27da10558b5f0089cb Mon Sep 17 00:00:00 2001 From: Victor Vazquez Date: Mon, 3 Jun 2024 05:40:41 +0000 Subject: [PATCH 01/13] Adding support for commandRunner for merging system environment and to emulate az with azd --- cli/azd/cmd/auth_token.go | 13 +++ cli/azd/cmd/az_cli_emulate_account.go | 112 +++++++++++++++++++++++++ cli/azd/cmd/root.go | 1 + cli/azd/cmd/version.go | 14 ++++ cli/azd/pkg/contracts/auth_token.go | 10 +++ cli/azd/pkg/exec/az_emulator.go | 65 +++++++++++++++ cli/azd/pkg/exec/command_runner.go | 116 ++++++++++++-------------- cli/azd/pkg/exec/runargs.go | 21 +++++ 8 files changed, 288 insertions(+), 64 deletions(-) create mode 100644 cli/azd/cmd/az_cli_emulate_account.go create mode 100644 cli/azd/pkg/exec/az_emulator.go diff --git a/cli/azd/cmd/auth_token.go b/cli/azd/cmd/auth_token.go index 548758756fb..c99b42c3490 100644 --- a/cli/azd/cmd/auth_token.go +++ b/cli/azd/cmd/auth_token.go @@ -18,6 +18,7 @@ import ( "github.com/azure/azure-dev/cli/azd/pkg/cloud" "github.com/azure/azure-dev/cli/azd/pkg/contracts" "github.com/azure/azure-dev/cli/azd/pkg/environment" + "github.com/azure/azure-dev/cli/azd/pkg/exec" "github.com/azure/azure-dev/cli/azd/pkg/output" "github.com/spf13/cobra" "github.com/spf13/pflag" @@ -47,6 +48,9 @@ func (f *authTokenFlags) Bind(local *pflag.FlagSet, global *internal.GlobalComma f.global = global local.StringArrayVar(&f.scopes, "scope", nil, "The scope to use when requesting an access token") local.StringVar(&f.tenantID, "tenant-id", "", "The tenant id to use when requesting an access token.") + if exec.IsAzEmulator() { + local.StringVar(&f.tenantID, "tenant", "", "The tenant id to use when requesting an access token.") + } } type CredentialProviderFn func(context.Context, *auth.CredentialForCurrentUserOptions) (azcore.TokenCredential, error) @@ -169,6 +173,15 @@ func (a *authTokenAction) Run(ctx context.Context) (*actions.ActionResult, error return nil, fmt.Errorf("fetching token: %w", err) } + if exec.IsAzEmulator() { + res := contracts.AzEmulateAuthTokenResult{ + AccessToken: token.Token, + ExpiresOn: contracts.RFC3339Time(token.ExpiresOn), + } + + return nil, a.formatter.Format(res, a.writer, nil) + } + res := contracts.AuthTokenResult{ Token: token.Token, ExpiresOn: contracts.RFC3339Time(token.ExpiresOn), diff --git a/cli/azd/cmd/az_cli_emulate_account.go b/cli/azd/cmd/az_cli_emulate_account.go new file mode 100644 index 00000000000..47cb4196f08 --- /dev/null +++ b/cli/azd/cmd/az_cli_emulate_account.go @@ -0,0 +1,112 @@ +package cmd + +import ( + "context" + "encoding/json" + "fmt" + + "github.com/azure/azure-dev/cli/azd/cmd/actions" + "github.com/azure/azure-dev/cli/azd/internal" + "github.com/azure/azure-dev/cli/azd/pkg/account" + "github.com/azure/azure-dev/cli/azd/pkg/environment" + "github.com/azure/azure-dev/cli/azd/pkg/input" + "github.com/azure/azure-dev/cli/azd/pkg/output" + "github.com/spf13/cobra" + "github.com/spf13/pflag" +) + +func azCliEmulateAccountCommands(root *actions.ActionDescriptor) *actions.ActionDescriptor { + group := root.Add("account", &actions.ActionDescriptorOptions{ + Command: &cobra.Command{ + Use: "account", + Short: "Emulates az account commands", + Hidden: true, + }, + }) + + group.Add("show", &actions.ActionDescriptorOptions{ + Command: newAccountShowCmd(), + FlagsResolver: newAccountShowFlags, + ActionResolver: newAccountAction, + OutputFormats: []output.Format{output.JsonFormat}, + DefaultFormat: output.JsonFormat, + }) + + group.Add("get-access-token", &actions.ActionDescriptorOptions{ + Command: &cobra.Command{ + Use: "get-access-token", + Hidden: true, + }, + FlagsResolver: newAuthTokenFlags, + ActionResolver: newAuthTokenAction, + OutputFormats: []output.Format{output.JsonFormat}, + DefaultFormat: output.JsonFormat, + }) + + return group +} + +type accountShowFlags struct { + global *internal.GlobalCommandOptions + internal.EnvFlag +} + +func (s *accountShowFlags) Bind(local *pflag.FlagSet, global *internal.GlobalCommandOptions) { + s.EnvFlag.Bind(local, global) + s.global = global +} + +func newAccountShowFlags(cmd *cobra.Command, global *internal.GlobalCommandOptions) *accountShowFlags { + flags := &accountShowFlags{} + flags.Bind(cmd.Flags(), global) + + return flags +} + +func newAccountShowCmd() *cobra.Command { + return &cobra.Command{ + Use: "show", + Hidden: true, + } +} + +type accountShowAction struct { + env *environment.Environment + console input.Console + subManager *account.SubscriptionsManager +} + +func newAccountAction( + console input.Console, + env *environment.Environment, + subManager *account.SubscriptionsManager, +) actions.Action { + return &accountShowAction{ + console: console, + env: env, + subManager: subManager, + } +} + +type accountShowOutput struct { + Id string `json:"id"` + TenantId string `json:"tenantId"` +} + +func (s *accountShowAction) Run(ctx context.Context) (*actions.ActionResult, error) { + subId := s.env.GetSubscriptionId() + tenantId, err := s.subManager.LookupTenant(ctx, subId) + if err != nil { + return nil, err + } + o := accountShowOutput{ + Id: subId, + TenantId: tenantId, + } + output, err := json.Marshal(o) + if err != nil { + return nil, err + } + fmt.Fprint(s.console.Handles().Stdout, string(output)) + return nil, nil +} diff --git a/cli/azd/cmd/root.go b/cli/azd/cmd/root.go index d38ea8cc78c..88db17b59e9 100644 --- a/cli/azd/cmd/root.go +++ b/cli/azd/cmd/root.go @@ -125,6 +125,7 @@ func NewRootCmd( templatesActions(root) authActions(root) hooksActions(root) + azCliEmulateAccountCommands(root) root.Add("version", &actions.ActionDescriptorOptions{ Command: &cobra.Command{ diff --git a/cli/azd/cmd/version.go b/cli/azd/cmd/version.go index fb464321fc2..ff9b0eb9ce2 100644 --- a/cli/azd/cmd/version.go +++ b/cli/azd/cmd/version.go @@ -11,6 +11,7 @@ import ( "github.com/azure/azure-dev/cli/azd/cmd/actions" "github.com/azure/azure-dev/cli/azd/internal" "github.com/azure/azure-dev/cli/azd/pkg/contracts" + "github.com/azure/azure-dev/cli/azd/pkg/exec" "github.com/azure/azure-dev/cli/azd/pkg/input" "github.com/azure/azure-dev/cli/azd/pkg/output" "github.com/spf13/cobra" @@ -54,6 +55,19 @@ func newVersionAction( } func (v *versionAction) Run(ctx context.Context) (*actions.ActionResult, error) { + // fake-az in env makes azd to simulate az cli output. + // This is to make tools like terraform to use azd when they thing they are using az. + if exec.IsAzEmulator() { + fmt.Fprintf(v.console.Handles().Stdout, `{ + "azure-cli": "2.61.0", + "azure-cli-core": "2.61.0", + "azure-cli-telemetry": "1.1.0", + "extensions": {} + } + `) + return nil, nil + } + switch v.formatter.Kind() { case output.NoneFormat: fmt.Fprintf(v.console.Handles().Stdout, "azd version %s\n", internal.Version) diff --git a/cli/azd/pkg/contracts/auth_token.go b/cli/azd/pkg/contracts/auth_token.go index ee2a9937bfb..097537ef5f7 100644 --- a/cli/azd/pkg/contracts/auth_token.go +++ b/cli/azd/pkg/contracts/auth_token.go @@ -6,6 +6,8 @@ import ( "encoding/json" "fmt" "time" + + "github.com/azure/azure-dev/cli/azd/pkg/exec" ) // AuthTokenResult is the value returned by `azd get-access-token`. It matches the shape of `azcore.AccessToken` @@ -17,11 +19,19 @@ type AuthTokenResult struct { ExpiresOn RFC3339Time `json:"expiresOn"` } +type AzEmulateAuthTokenResult struct { + AccessToken string `json:"accessToken"` + ExpiresOn RFC3339Time `json:"expiresOn"` +} + // RFC3339Time is a time.Time that uses time.RFC3339 format when marshaling to JSON, not time.RFC3339Nano as // the standard library time.Time does. type RFC3339Time time.Time func (r RFC3339Time) MarshalJSON() ([]byte, error) { + if exec.IsAzEmulator() { + return []byte(fmt.Sprintf(`"%s"`, time.Time(r).Format("2006-01-02 15:04:05.000000"))), nil + } return []byte(fmt.Sprintf(`"%s"`, time.Time(r).Format(time.RFC3339))), nil } diff --git a/cli/azd/pkg/exec/az_emulator.go b/cli/azd/pkg/exec/az_emulator.go new file mode 100644 index 00000000000..eb50d6b093f --- /dev/null +++ b/cli/azd/pkg/exec/az_emulator.go @@ -0,0 +1,65 @@ +package exec + +import ( + "fmt" + "io" + "os" + "os/exec" + "path/filepath" + "strings" + + "github.com/azure/azure-dev/cli/azd/pkg/config" + "github.com/azure/azure-dev/cli/azd/pkg/osutil" +) + +const ( + emulatorEnvName string = "AZURE_AZ_EMULATOR" +) + +// IsAzEmulator returns true if the AZURE_AZ_EMULATOR environment variable is defined. +// It does not matter the value of the environment variable, as long as it is defined. +func IsAzEmulator() bool { + _, emulateEnvVarDefined := os.LookupEnv(emulatorEnvName) + return emulateEnvVarDefined +} + +func emulateAz(cmd *CmdTree) error { + return nil +} + +// creates a copy of azd binary and renames it to az and returns the path to it +func emulateAzFromPath() (string, error) { + path, err := exec.LookPath("azd") + if err != nil { + return "", fmt.Errorf("azd binary not found in PATH: %w", err) + } + azdConfigPath, err := config.GetUserConfigDir() + if err != nil { + return "", fmt.Errorf("could not get user config dir: %w", err) + } + emuPath := filepath.Join(azdConfigPath, "bin", "azEmulate") + err = os.MkdirAll(emuPath, osutil.PermissionDirectoryOwnerOnly) + if err != nil { + return "", fmt.Errorf("could not create directory for azEmulate: %w", err) + } + emuPath = filepath.Join(emuPath, strings.ReplaceAll(filepath.Base(path), "azd", "az")) + + srcFile, err := os.Open(path) + if err != nil { + return "", fmt.Errorf("opening src: %w", err) + } + defer srcFile.Close() + + destFile, err := os.OpenFile(emuPath, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0755) + if err != nil { + return "", fmt.Errorf("creating dest: %w", err) + } + defer destFile.Close() + + _, err = io.Copy(destFile, srcFile) + if err != nil { + return "", fmt.Errorf("copying binary: %w", err) + } + + return emuPath, nil +} diff --git a/cli/azd/pkg/exec/command_runner.go b/cli/azd/pkg/exec/command_runner.go index 3ed638217b9..f85b7093108 100644 --- a/cli/azd/pkg/exec/command_runner.go +++ b/cli/azd/pkg/exec/command_runner.go @@ -97,7 +97,39 @@ func (r *commandRunner) Run(ctx context.Context, args RunArgs) (RunResult, error if err != nil { return RunResult{}, err } + return r.runImpl(ctx, cmd, args) +} +func arrayToMap(env []string) map[string]string { + envMap := make(map[string]string, len(env)) + for _, envVar := range env { + keyAndValue := strings.SplitN(envVar, "=", 2) + value := "" + if len(keyAndValue) > 1 { + value = keyAndValue[1] + } + envMap[keyAndValue[0]] = value + } + return envMap +} + +func mergeInjectEnv(initialEnv []string) []string { + systemEnv := arrayToMap(os.Environ()) + // create a map from initial Env to check if the key is already present + sourceEnv := arrayToMap(initialEnv) + mergedEnv := initialEnv + + for key, val := range systemEnv { + _, isInInitialEnv := sourceEnv[key] + if !isInInitialEnv { + mergedEnv = append(mergedEnv, fmt.Sprintf("%s=%s", key, val)) + continue + } + } + return mergedEnv +} + +func (r *commandRunner) runImpl(ctx context.Context, cmd CmdTree, args RunArgs) (RunResult, error) { cmd.Dir = args.Cwd var stdin io.Reader @@ -109,7 +141,24 @@ func (r *commandRunner) Run(ctx context.Context, args RunArgs) (RunResult, error var stdout, stderr bytes.Buffer - cmd.Env = appendEnv(args.Env) + // args.Env == nil makes the cmd to inherit the environment variables from the parent process + cmdEnv := args.Env + if args.MergeSystemEnv { + cmdEnv = mergeInjectEnv(cmdEnv) + } + if args.AzEmulator { + // makes azd to emulate azd in this env + cmdEnv = append(cmdEnv, emulatorEnvName+"=true") + emuPath, err := emulateAzFromPath() + if err != nil { + return RunResult{}, err + } + defer os.Remove(emuPath) + // replaces PATH with the path to the emulated az + cmdEnv = append(cmdEnv, "PATH="+emuPath) + } + + cmd.Env = cmdEnv if args.Interactive { cmd.Stdin = r.stdin @@ -155,7 +204,7 @@ func (r *commandRunner) Run(ctx context.Context, args RunArgs) (RunResult, error cmd.Kill() }() - err = cmd.Wait() + err := cmd.Wait() var result RunResult @@ -194,68 +243,7 @@ func (r *commandRunner) RunList(ctx context.Context, commands []string, args Run if err != nil { return NewRunResult(-1, "", ""), err } - - process.Cmd.Dir = args.Cwd - process.Env = appendEnv(args.Env) - - var stdOutBuf bytes.Buffer - var stdErrBuf bytes.Buffer - - if process.Stdout == nil { - process.Stdout = &stdOutBuf - } - - if process.Stderr == nil { - process.Stderr = &stdErrBuf - } - - debugLogging := r.debugLogging - if args.DebugLogging != nil { - debugLogging = *args.DebugLogging - } - - logMsg := logBuilder{ - // use the actual shell command invoked in the log message - args: process.Cmd.Args, - env: args.Env, - } - defer func() { - logMsg.Write(debugLogging, args.SensitiveData) - }() - - if err := process.Start(); err != nil { - logMsg.err = err - return NewRunResult(-1, "", ""), fmt.Errorf("error starting process: %w", err) - } - defer process.Kill() - - err = process.Wait() - result := NewRunResult( - process.ProcessState.ExitCode(), - stdOutBuf.String(), - stdErrBuf.String(), - ) - logMsg.result = &result - - var exitErr *exec.ExitError - if errors.As(err, &exitErr) { - err = NewExitError( - *exitErr, - args.Cmd, - result.Stdout, - result.Stderr, - true) - } - - return result, err -} - -func appendEnv(env []string) []string { - if len(env) > 0 { - return append(os.Environ(), env...) - } - - return nil + return r.runImpl(ctx, process, args) } // logBuilder builds messages for running of commands. diff --git a/cli/azd/pkg/exec/runargs.go b/cli/azd/pkg/exec/runargs.go index e3959408569..cfe72661685 100644 --- a/cli/azd/pkg/exec/runargs.go +++ b/cli/azd/pkg/exec/runargs.go @@ -32,6 +32,13 @@ type RunArgs struct { // When set will call the command with the specified StdOut StdOut io.Writer + + // When set, any calls within the command to `az` will be sent to `azd` + AzEmulator bool + + // When set, the command will merge the system environment variables with the provided environment variables + // giving priority to the provided environment variables. + MergeSystemEnv bool } // NewRunArgs creates a new instance with the specified cmd and args @@ -70,6 +77,20 @@ func (b RunArgs) WithEnv(env []string) RunArgs { return b } +// Makes any call to `az` from the command to call azd with az emulator. +// Commands depending on az cli for auth can use this to use azd instead with the help of the command runner. +func (b RunArgs) WithAzEmulator() RunArgs { + b.AzEmulator = true + return b +} + +// Merges the system environment variables with the provided environment variables +// giving priority to the provided environment variables. +func (b RunArgs) WithSystemEnvMerged() RunArgs { + b.MergeSystemEnv = true + return b +} + // Updates whether or not this will be an interactive commands // Interactive command sets stdin, stdout & stderr to the OS console/terminal func (b RunArgs) WithInteractive(interactive bool) RunArgs { From 791fa3d6fef49927f525d988a8984da70646a4fd Mon Sep 17 00:00:00 2001 From: Victor Vazquez Date: Mon, 3 Jun 2024 05:57:50 +0000 Subject: [PATCH 02/13] lint --- cli/azd/pkg/exec/az_emulator.go | 4 ---- cli/azd/pkg/exec/util_test.go | 16 ---------------- 2 files changed, 20 deletions(-) diff --git a/cli/azd/pkg/exec/az_emulator.go b/cli/azd/pkg/exec/az_emulator.go index eb50d6b093f..03af64f1721 100644 --- a/cli/azd/pkg/exec/az_emulator.go +++ b/cli/azd/pkg/exec/az_emulator.go @@ -23,10 +23,6 @@ func IsAzEmulator() bool { return emulateEnvVarDefined } -func emulateAz(cmd *CmdTree) error { - return nil -} - // creates a copy of azd binary and renames it to az and returns the path to it func emulateAzFromPath() (string, error) { path, err := exec.LookPath("azd") diff --git a/cli/azd/pkg/exec/util_test.go b/cli/azd/pkg/exec/util_test.go index 64254c0ac86..0d45cb6d809 100644 --- a/cli/azd/pkg/exec/util_test.go +++ b/cli/azd/pkg/exec/util_test.go @@ -6,10 +6,8 @@ package exec import ( "bytes" "context" - "os" "regexp" "runtime" - "sort" "testing" "time" @@ -84,20 +82,6 @@ func TestKillCommand(t *testing.T) { require.LessOrEqual(t, since, 10*time.Second) } -func TestAppendEnv(t *testing.T) { - require.Nil(t, appendEnv([]string{})) - require.Nil(t, appendEnv(nil)) - - expectedEnv := os.Environ() - expectedEnv = append(expectedEnv, "azd_random_var=world") - sort.Strings(expectedEnv) - - actualEnv := appendEnv([]string{"azd_random_var=world"}) - sort.Strings(actualEnv) - - require.Equal(t, expectedEnv, actualEnv) -} - func TestRunList(t *testing.T) { runner := NewCommandRunner(nil) res, err := runner.RunList(context.Background(), []string{ From d110a34de13027341de211fdcf9c038ab6df08a2 Mon Sep 17 00:00:00 2001 From: Victor Vazquez Date: Mon, 3 Jun 2024 19:57:27 +0000 Subject: [PATCH 03/13] merge system env for tools using custom env --- cli/azd/pkg/exec/runargs_test.go | 1 + cli/azd/pkg/tools/bash/bash.go | 1 + cli/azd/pkg/tools/bicep/bicep.go | 2 +- cli/azd/pkg/tools/dotnet/dotnet.go | 6 +++--- cli/azd/pkg/tools/git/git.go | 2 +- cli/azd/pkg/tools/github/github.go | 2 +- cli/azd/pkg/tools/powershell/powershell.go | 2 +- cli/azd/pkg/tools/terraform/terraform.go | 4 +++- 8 files changed, 12 insertions(+), 8 deletions(-) diff --git a/cli/azd/pkg/exec/runargs_test.go b/cli/azd/pkg/exec/runargs_test.go index 041086ff46f..e1d31761ee5 100644 --- a/cli/azd/pkg/exec/runargs_test.go +++ b/cli/azd/pkg/exec/runargs_test.go @@ -24,6 +24,7 @@ func TestNewRunArgs(t *testing.T) { runArgs := NewRunArgs("az", "login"). WithCwd("cwd"). WithEnv([]string{"foo", "bar"}). + WithSystemEnvMerged(). WithInteractive(true). WithShell(true). WithDebugLogging(true). diff --git a/cli/azd/pkg/tools/bash/bash.go b/cli/azd/pkg/tools/bash/bash.go index 3b8e26e8381..2a4acb0df17 100644 --- a/cli/azd/pkg/tools/bash/bash.go +++ b/cli/azd/pkg/tools/bash/bash.go @@ -40,6 +40,7 @@ func (bs *bashScript) Execute(ctx context.Context, path string, options tools.Ex runArgs = runArgs. WithCwd(bs.cwd). WithEnv(bs.envVars). + WithSystemEnvMerged(). WithShell(true) if options.Interactive != nil { diff --git a/cli/azd/pkg/tools/bicep/bicep.go b/cli/azd/pkg/tools/bicep/bicep.go index c9cd63bc8b8..489a594bccd 100644 --- a/cli/azd/pkg/tools/bicep/bicep.go +++ b/cli/azd/pkg/tools/bicep/bicep.go @@ -305,7 +305,7 @@ func (cli *bicepCli) BuildBicepParam(ctx context.Context, file string, env []str func (cli *bicepCli) runCommand(ctx context.Context, env []string, args ...string) (exec.RunResult, error) { runArgs := exec.NewRunArgs(cli.path, args...) if env != nil { - runArgs = runArgs.WithEnv(env) + runArgs = runArgs.WithEnv(env).WithSystemEnvMerged() } return cli.runner.Run(ctx, runArgs) } diff --git a/cli/azd/pkg/tools/dotnet/dotnet.go b/cli/azd/pkg/tools/dotnet/dotnet.go index 893fada1edc..0ea57f6938b 100644 --- a/cli/azd/pkg/tools/dotnet/dotnet.go +++ b/cli/azd/pkg/tools/dotnet/dotnet.go @@ -169,7 +169,7 @@ func (cli *dotNetCli) PublishAppHostManifest( } if envArgs != nil { - runArgs = runArgs.WithEnv(envArgs) + runArgs = runArgs.WithEnv(envArgs).WithSystemEnvMerged() } _, err := cli.commandRunner.Run(ctx, runArgs) @@ -199,7 +199,7 @@ func (cli *dotNetCli) PublishContainer( runArgs = runArgs.WithEnv([]string{ fmt.Sprintf("SDK_CONTAINER_REGISTRY_UNAME=%s", username), fmt.Sprintf("SDK_CONTAINER_REGISTRY_PWORD=%s", password), - }) + }).WithSystemEnvMerged() result, err := cli.commandRunner.Run(ctx, runArgs) if err != nil { @@ -321,7 +321,7 @@ func newDotNetRunArgs(args ...string) exec.RunArgs { runArgs = runArgs.WithEnv([]string{ "DOTNET_CLI_WORKLOAD_UPDATE_NOTIFY_DISABLE=1", - }) + }).WithSystemEnvMerged() return runArgs } diff --git a/cli/azd/pkg/tools/git/git.go b/cli/azd/pkg/tools/git/git.go index 6b194764573..cfbe101e003 100644 --- a/cli/azd/pkg/tools/git/git.go +++ b/cli/azd/pkg/tools/git/git.go @@ -316,7 +316,7 @@ func newRunArgs(args ...string) exec.RunArgs { if github.RunningOnCodespaces() { // azd running git in codespaces should not use the Codespaces token. // As azd needs bigger access across repos. And the token in codespaces is mono-repo by default - runArgs = runArgs.WithEnv([]string{"GITHUB_TOKEN=", "GH_TOKEN="}) + runArgs = runArgs.WithEnv([]string{"GITHUB_TOKEN=", "GH_TOKEN="}).WithSystemEnvMerged() } return runArgs diff --git a/cli/azd/pkg/tools/github/github.go b/cli/azd/pkg/tools/github/github.go index 80469cfeb96..a36444ee1fb 100644 --- a/cli/azd/pkg/tools/github/github.go +++ b/cli/azd/pkg/tools/github/github.go @@ -410,7 +410,7 @@ func (cli *ghCli) newRunArgs(args ...string) exec.RunArgs { runArgs := exec.NewRunArgs(cli.path, args...) if RunningOnCodespaces() { - runArgs = runArgs.WithEnv([]string{"GITHUB_TOKEN=", "GH_TOKEN="}) + runArgs = runArgs.WithEnv([]string{"GITHUB_TOKEN=", "GH_TOKEN="}).WithSystemEnvMerged() } return runArgs diff --git a/cli/azd/pkg/tools/powershell/powershell.go b/cli/azd/pkg/tools/powershell/powershell.go index ca9fad66290..0dcf5727f11 100644 --- a/cli/azd/pkg/tools/powershell/powershell.go +++ b/cli/azd/pkg/tools/powershell/powershell.go @@ -27,7 +27,7 @@ type powershellScript struct { func (bs *powershellScript) Execute(ctx context.Context, path string, options tools.ExecOptions) (exec.RunResult, error) { runArgs := exec.NewRunArgs("pwsh", path). WithCwd(bs.cwd). - WithEnv(bs.envVars). + WithEnv(bs.envVars).WithSystemEnvMerged(). WithShell(true) if options.Interactive != nil { diff --git a/cli/azd/pkg/tools/terraform/terraform.go b/cli/azd/pkg/tools/terraform/terraform.go index ab1d854d8e7..f2a97a0ec86 100644 --- a/cli/azd/pkg/tools/terraform/terraform.go +++ b/cli/azd/pkg/tools/terraform/terraform.go @@ -95,7 +95,8 @@ func (cli *terraformCli) SetEnv(env []string) { func (cli *terraformCli) runCommand(ctx context.Context, args ...string) (exec.RunResult, error) { runArgs := exec. NewRunArgs("terraform", args...). - WithEnv(cli.env) + WithEnv(cli.env). + WithSystemEnvMerged() return cli.commandRunner.Run(ctx, runArgs) } @@ -104,6 +105,7 @@ func (cli *terraformCli) runInteractive(ctx context.Context, args ...string) (ex runArgs := exec. NewRunArgs("terraform", args...). WithEnv(cli.env). + WithSystemEnvMerged(). WithInteractive(true) return cli.commandRunner.Run(ctx, runArgs) From 86b5cc597a41b5c59b74da757eee532fe53283da Mon Sep 17 00:00:00 2001 From: Victor Vazquez Date: Mon, 3 Jun 2024 20:58:42 +0000 Subject: [PATCH 04/13] move emulator to terraform only --- cli/azd/pkg/{exec => tools/terraform}/az_emulator.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) rename cli/azd/pkg/{exec => tools/terraform}/az_emulator.go (97%) diff --git a/cli/azd/pkg/exec/az_emulator.go b/cli/azd/pkg/tools/terraform/az_emulator.go similarity index 97% rename from cli/azd/pkg/exec/az_emulator.go rename to cli/azd/pkg/tools/terraform/az_emulator.go index 03af64f1721..fbfd7b37aec 100644 --- a/cli/azd/pkg/exec/az_emulator.go +++ b/cli/azd/pkg/tools/terraform/az_emulator.go @@ -1,4 +1,4 @@ -package exec +package terraform import ( "fmt" @@ -18,7 +18,7 @@ const ( // IsAzEmulator returns true if the AZURE_AZ_EMULATOR environment variable is defined. // It does not matter the value of the environment variable, as long as it is defined. -func IsAzEmulator() bool { +func isAzEmulator() bool { _, emulateEnvVarDefined := os.LookupEnv(emulatorEnvName) return emulateEnvVarDefined } From 3102262aa962c023946f6c50921344d0278b67a5 Mon Sep 17 00:00:00 2001 From: Victor Vazquez Date: Mon, 3 Jun 2024 21:19:46 +0000 Subject: [PATCH 05/13] move emulator for terraform alone --- cli/azd/cmd/auth_token.go | 6 +- cli/azd/cmd/version.go | 4 +- cli/azd/pkg/contracts/auth_token.go | 4 +- cli/azd/pkg/exec/command_runner.go | 116 ++++++++++++--------- cli/azd/pkg/exec/runargs.go | 21 ---- cli/azd/pkg/exec/runargs_test.go | 1 - cli/azd/pkg/osutil/env.go | 16 +++ cli/azd/pkg/tools/bash/bash.go | 1 - cli/azd/pkg/tools/bicep/bicep.go | 2 +- cli/azd/pkg/tools/dotnet/dotnet.go | 6 +- cli/azd/pkg/tools/git/git.go | 2 +- cli/azd/pkg/tools/github/github.go | 2 +- cli/azd/pkg/tools/powershell/powershell.go | 2 +- cli/azd/pkg/tools/terraform/az_emulator.go | 11 -- cli/azd/pkg/tools/terraform/terraform.go | 23 +++- 15 files changed, 112 insertions(+), 105 deletions(-) diff --git a/cli/azd/cmd/auth_token.go b/cli/azd/cmd/auth_token.go index c99b42c3490..1073ade12c5 100644 --- a/cli/azd/cmd/auth_token.go +++ b/cli/azd/cmd/auth_token.go @@ -18,7 +18,7 @@ import ( "github.com/azure/azure-dev/cli/azd/pkg/cloud" "github.com/azure/azure-dev/cli/azd/pkg/contracts" "github.com/azure/azure-dev/cli/azd/pkg/environment" - "github.com/azure/azure-dev/cli/azd/pkg/exec" + "github.com/azure/azure-dev/cli/azd/pkg/osutil" "github.com/azure/azure-dev/cli/azd/pkg/output" "github.com/spf13/cobra" "github.com/spf13/pflag" @@ -48,7 +48,7 @@ func (f *authTokenFlags) Bind(local *pflag.FlagSet, global *internal.GlobalComma f.global = global local.StringArrayVar(&f.scopes, "scope", nil, "The scope to use when requesting an access token") local.StringVar(&f.tenantID, "tenant-id", "", "The tenant id to use when requesting an access token.") - if exec.IsAzEmulator() { + if osutil.IsAzEmulator() { local.StringVar(&f.tenantID, "tenant", "", "The tenant id to use when requesting an access token.") } } @@ -173,7 +173,7 @@ func (a *authTokenAction) Run(ctx context.Context) (*actions.ActionResult, error return nil, fmt.Errorf("fetching token: %w", err) } - if exec.IsAzEmulator() { + if osutil.IsAzEmulator() { res := contracts.AzEmulateAuthTokenResult{ AccessToken: token.Token, ExpiresOn: contracts.RFC3339Time(token.ExpiresOn), diff --git a/cli/azd/cmd/version.go b/cli/azd/cmd/version.go index ff9b0eb9ce2..d1cafc9afaf 100644 --- a/cli/azd/cmd/version.go +++ b/cli/azd/cmd/version.go @@ -11,8 +11,8 @@ import ( "github.com/azure/azure-dev/cli/azd/cmd/actions" "github.com/azure/azure-dev/cli/azd/internal" "github.com/azure/azure-dev/cli/azd/pkg/contracts" - "github.com/azure/azure-dev/cli/azd/pkg/exec" "github.com/azure/azure-dev/cli/azd/pkg/input" + "github.com/azure/azure-dev/cli/azd/pkg/osutil" "github.com/azure/azure-dev/cli/azd/pkg/output" "github.com/spf13/cobra" "github.com/spf13/pflag" @@ -57,7 +57,7 @@ func newVersionAction( func (v *versionAction) Run(ctx context.Context) (*actions.ActionResult, error) { // fake-az in env makes azd to simulate az cli output. // This is to make tools like terraform to use azd when they thing they are using az. - if exec.IsAzEmulator() { + if osutil.IsAzEmulator() { fmt.Fprintf(v.console.Handles().Stdout, `{ "azure-cli": "2.61.0", "azure-cli-core": "2.61.0", diff --git a/cli/azd/pkg/contracts/auth_token.go b/cli/azd/pkg/contracts/auth_token.go index 097537ef5f7..13c70c5e8b2 100644 --- a/cli/azd/pkg/contracts/auth_token.go +++ b/cli/azd/pkg/contracts/auth_token.go @@ -7,7 +7,7 @@ import ( "fmt" "time" - "github.com/azure/azure-dev/cli/azd/pkg/exec" + "github.com/azure/azure-dev/cli/azd/pkg/osutil" ) // AuthTokenResult is the value returned by `azd get-access-token`. It matches the shape of `azcore.AccessToken` @@ -29,7 +29,7 @@ type AzEmulateAuthTokenResult struct { type RFC3339Time time.Time func (r RFC3339Time) MarshalJSON() ([]byte, error) { - if exec.IsAzEmulator() { + if osutil.IsAzEmulator() { return []byte(fmt.Sprintf(`"%s"`, time.Time(r).Format("2006-01-02 15:04:05.000000"))), nil } return []byte(fmt.Sprintf(`"%s"`, time.Time(r).Format(time.RFC3339))), nil diff --git a/cli/azd/pkg/exec/command_runner.go b/cli/azd/pkg/exec/command_runner.go index f85b7093108..3ed638217b9 100644 --- a/cli/azd/pkg/exec/command_runner.go +++ b/cli/azd/pkg/exec/command_runner.go @@ -97,39 +97,7 @@ func (r *commandRunner) Run(ctx context.Context, args RunArgs) (RunResult, error if err != nil { return RunResult{}, err } - return r.runImpl(ctx, cmd, args) -} -func arrayToMap(env []string) map[string]string { - envMap := make(map[string]string, len(env)) - for _, envVar := range env { - keyAndValue := strings.SplitN(envVar, "=", 2) - value := "" - if len(keyAndValue) > 1 { - value = keyAndValue[1] - } - envMap[keyAndValue[0]] = value - } - return envMap -} - -func mergeInjectEnv(initialEnv []string) []string { - systemEnv := arrayToMap(os.Environ()) - // create a map from initial Env to check if the key is already present - sourceEnv := arrayToMap(initialEnv) - mergedEnv := initialEnv - - for key, val := range systemEnv { - _, isInInitialEnv := sourceEnv[key] - if !isInInitialEnv { - mergedEnv = append(mergedEnv, fmt.Sprintf("%s=%s", key, val)) - continue - } - } - return mergedEnv -} - -func (r *commandRunner) runImpl(ctx context.Context, cmd CmdTree, args RunArgs) (RunResult, error) { cmd.Dir = args.Cwd var stdin io.Reader @@ -141,24 +109,7 @@ func (r *commandRunner) runImpl(ctx context.Context, cmd CmdTree, args RunArgs) var stdout, stderr bytes.Buffer - // args.Env == nil makes the cmd to inherit the environment variables from the parent process - cmdEnv := args.Env - if args.MergeSystemEnv { - cmdEnv = mergeInjectEnv(cmdEnv) - } - if args.AzEmulator { - // makes azd to emulate azd in this env - cmdEnv = append(cmdEnv, emulatorEnvName+"=true") - emuPath, err := emulateAzFromPath() - if err != nil { - return RunResult{}, err - } - defer os.Remove(emuPath) - // replaces PATH with the path to the emulated az - cmdEnv = append(cmdEnv, "PATH="+emuPath) - } - - cmd.Env = cmdEnv + cmd.Env = appendEnv(args.Env) if args.Interactive { cmd.Stdin = r.stdin @@ -204,7 +155,7 @@ func (r *commandRunner) runImpl(ctx context.Context, cmd CmdTree, args RunArgs) cmd.Kill() }() - err := cmd.Wait() + err = cmd.Wait() var result RunResult @@ -243,7 +194,68 @@ func (r *commandRunner) RunList(ctx context.Context, commands []string, args Run if err != nil { return NewRunResult(-1, "", ""), err } - return r.runImpl(ctx, process, args) + + process.Cmd.Dir = args.Cwd + process.Env = appendEnv(args.Env) + + var stdOutBuf bytes.Buffer + var stdErrBuf bytes.Buffer + + if process.Stdout == nil { + process.Stdout = &stdOutBuf + } + + if process.Stderr == nil { + process.Stderr = &stdErrBuf + } + + debugLogging := r.debugLogging + if args.DebugLogging != nil { + debugLogging = *args.DebugLogging + } + + logMsg := logBuilder{ + // use the actual shell command invoked in the log message + args: process.Cmd.Args, + env: args.Env, + } + defer func() { + logMsg.Write(debugLogging, args.SensitiveData) + }() + + if err := process.Start(); err != nil { + logMsg.err = err + return NewRunResult(-1, "", ""), fmt.Errorf("error starting process: %w", err) + } + defer process.Kill() + + err = process.Wait() + result := NewRunResult( + process.ProcessState.ExitCode(), + stdOutBuf.String(), + stdErrBuf.String(), + ) + logMsg.result = &result + + var exitErr *exec.ExitError + if errors.As(err, &exitErr) { + err = NewExitError( + *exitErr, + args.Cmd, + result.Stdout, + result.Stderr, + true) + } + + return result, err +} + +func appendEnv(env []string) []string { + if len(env) > 0 { + return append(os.Environ(), env...) + } + + return nil } // logBuilder builds messages for running of commands. diff --git a/cli/azd/pkg/exec/runargs.go b/cli/azd/pkg/exec/runargs.go index cfe72661685..e3959408569 100644 --- a/cli/azd/pkg/exec/runargs.go +++ b/cli/azd/pkg/exec/runargs.go @@ -32,13 +32,6 @@ type RunArgs struct { // When set will call the command with the specified StdOut StdOut io.Writer - - // When set, any calls within the command to `az` will be sent to `azd` - AzEmulator bool - - // When set, the command will merge the system environment variables with the provided environment variables - // giving priority to the provided environment variables. - MergeSystemEnv bool } // NewRunArgs creates a new instance with the specified cmd and args @@ -77,20 +70,6 @@ func (b RunArgs) WithEnv(env []string) RunArgs { return b } -// Makes any call to `az` from the command to call azd with az emulator. -// Commands depending on az cli for auth can use this to use azd instead with the help of the command runner. -func (b RunArgs) WithAzEmulator() RunArgs { - b.AzEmulator = true - return b -} - -// Merges the system environment variables with the provided environment variables -// giving priority to the provided environment variables. -func (b RunArgs) WithSystemEnvMerged() RunArgs { - b.MergeSystemEnv = true - return b -} - // Updates whether or not this will be an interactive commands // Interactive command sets stdin, stdout & stderr to the OS console/terminal func (b RunArgs) WithInteractive(interactive bool) RunArgs { diff --git a/cli/azd/pkg/exec/runargs_test.go b/cli/azd/pkg/exec/runargs_test.go index e1d31761ee5..041086ff46f 100644 --- a/cli/azd/pkg/exec/runargs_test.go +++ b/cli/azd/pkg/exec/runargs_test.go @@ -24,7 +24,6 @@ func TestNewRunArgs(t *testing.T) { runArgs := NewRunArgs("az", "login"). WithCwd("cwd"). WithEnv([]string{"foo", "bar"}). - WithSystemEnvMerged(). WithInteractive(true). WithShell(true). WithDebugLogging(true). diff --git a/cli/azd/pkg/osutil/env.go b/cli/azd/pkg/osutil/env.go index 010a7c358ae..3bc5a0e4c58 100644 --- a/cli/azd/pkg/osutil/env.go +++ b/cli/azd/pkg/osutil/env.go @@ -4,6 +4,7 @@ package osutil import ( + "fmt" "os" "runtime" ) @@ -25,3 +26,18 @@ func GetNewLineSeparator() string { return "\n" } } + +const ( + emulatorEnvName string = "AZURE_AZ_EMULATOR" +) + +// IsAzEmulator returns true if the AZURE_AZ_EMULATOR environment variable is defined. +// It does not matter the value of the environment variable, as long as it is defined. +func IsAzEmulator() bool { + _, emulateEnvVarDefined := os.LookupEnv(emulatorEnvName) + return emulateEnvVarDefined +} + +func AzEmulateKey() string { + return fmt.Sprintf("%s=%s", emulatorEnvName, "true") +} diff --git a/cli/azd/pkg/tools/bash/bash.go b/cli/azd/pkg/tools/bash/bash.go index 2a4acb0df17..3b8e26e8381 100644 --- a/cli/azd/pkg/tools/bash/bash.go +++ b/cli/azd/pkg/tools/bash/bash.go @@ -40,7 +40,6 @@ func (bs *bashScript) Execute(ctx context.Context, path string, options tools.Ex runArgs = runArgs. WithCwd(bs.cwd). WithEnv(bs.envVars). - WithSystemEnvMerged(). WithShell(true) if options.Interactive != nil { diff --git a/cli/azd/pkg/tools/bicep/bicep.go b/cli/azd/pkg/tools/bicep/bicep.go index 489a594bccd..c9cd63bc8b8 100644 --- a/cli/azd/pkg/tools/bicep/bicep.go +++ b/cli/azd/pkg/tools/bicep/bicep.go @@ -305,7 +305,7 @@ func (cli *bicepCli) BuildBicepParam(ctx context.Context, file string, env []str func (cli *bicepCli) runCommand(ctx context.Context, env []string, args ...string) (exec.RunResult, error) { runArgs := exec.NewRunArgs(cli.path, args...) if env != nil { - runArgs = runArgs.WithEnv(env).WithSystemEnvMerged() + runArgs = runArgs.WithEnv(env) } return cli.runner.Run(ctx, runArgs) } diff --git a/cli/azd/pkg/tools/dotnet/dotnet.go b/cli/azd/pkg/tools/dotnet/dotnet.go index 0ea57f6938b..893fada1edc 100644 --- a/cli/azd/pkg/tools/dotnet/dotnet.go +++ b/cli/azd/pkg/tools/dotnet/dotnet.go @@ -169,7 +169,7 @@ func (cli *dotNetCli) PublishAppHostManifest( } if envArgs != nil { - runArgs = runArgs.WithEnv(envArgs).WithSystemEnvMerged() + runArgs = runArgs.WithEnv(envArgs) } _, err := cli.commandRunner.Run(ctx, runArgs) @@ -199,7 +199,7 @@ func (cli *dotNetCli) PublishContainer( runArgs = runArgs.WithEnv([]string{ fmt.Sprintf("SDK_CONTAINER_REGISTRY_UNAME=%s", username), fmt.Sprintf("SDK_CONTAINER_REGISTRY_PWORD=%s", password), - }).WithSystemEnvMerged() + }) result, err := cli.commandRunner.Run(ctx, runArgs) if err != nil { @@ -321,7 +321,7 @@ func newDotNetRunArgs(args ...string) exec.RunArgs { runArgs = runArgs.WithEnv([]string{ "DOTNET_CLI_WORKLOAD_UPDATE_NOTIFY_DISABLE=1", - }).WithSystemEnvMerged() + }) return runArgs } diff --git a/cli/azd/pkg/tools/git/git.go b/cli/azd/pkg/tools/git/git.go index cfbe101e003..6b194764573 100644 --- a/cli/azd/pkg/tools/git/git.go +++ b/cli/azd/pkg/tools/git/git.go @@ -316,7 +316,7 @@ func newRunArgs(args ...string) exec.RunArgs { if github.RunningOnCodespaces() { // azd running git in codespaces should not use the Codespaces token. // As azd needs bigger access across repos. And the token in codespaces is mono-repo by default - runArgs = runArgs.WithEnv([]string{"GITHUB_TOKEN=", "GH_TOKEN="}).WithSystemEnvMerged() + runArgs = runArgs.WithEnv([]string{"GITHUB_TOKEN=", "GH_TOKEN="}) } return runArgs diff --git a/cli/azd/pkg/tools/github/github.go b/cli/azd/pkg/tools/github/github.go index a36444ee1fb..80469cfeb96 100644 --- a/cli/azd/pkg/tools/github/github.go +++ b/cli/azd/pkg/tools/github/github.go @@ -410,7 +410,7 @@ func (cli *ghCli) newRunArgs(args ...string) exec.RunArgs { runArgs := exec.NewRunArgs(cli.path, args...) if RunningOnCodespaces() { - runArgs = runArgs.WithEnv([]string{"GITHUB_TOKEN=", "GH_TOKEN="}).WithSystemEnvMerged() + runArgs = runArgs.WithEnv([]string{"GITHUB_TOKEN=", "GH_TOKEN="}) } return runArgs diff --git a/cli/azd/pkg/tools/powershell/powershell.go b/cli/azd/pkg/tools/powershell/powershell.go index 0dcf5727f11..ca9fad66290 100644 --- a/cli/azd/pkg/tools/powershell/powershell.go +++ b/cli/azd/pkg/tools/powershell/powershell.go @@ -27,7 +27,7 @@ type powershellScript struct { func (bs *powershellScript) Execute(ctx context.Context, path string, options tools.ExecOptions) (exec.RunResult, error) { runArgs := exec.NewRunArgs("pwsh", path). WithCwd(bs.cwd). - WithEnv(bs.envVars).WithSystemEnvMerged(). + WithEnv(bs.envVars). WithShell(true) if options.Interactive != nil { diff --git a/cli/azd/pkg/tools/terraform/az_emulator.go b/cli/azd/pkg/tools/terraform/az_emulator.go index fbfd7b37aec..b6ddb38d934 100644 --- a/cli/azd/pkg/tools/terraform/az_emulator.go +++ b/cli/azd/pkg/tools/terraform/az_emulator.go @@ -12,17 +12,6 @@ import ( "github.com/azure/azure-dev/cli/azd/pkg/osutil" ) -const ( - emulatorEnvName string = "AZURE_AZ_EMULATOR" -) - -// IsAzEmulator returns true if the AZURE_AZ_EMULATOR environment variable is defined. -// It does not matter the value of the environment variable, as long as it is defined. -func isAzEmulator() bool { - _, emulateEnvVarDefined := os.LookupEnv(emulatorEnvName) - return emulateEnvVarDefined -} - // creates a copy of azd binary and renames it to az and returns the path to it func emulateAzFromPath() (string, error) { path, err := exec.LookPath("azd") diff --git a/cli/azd/pkg/tools/terraform/terraform.go b/cli/azd/pkg/tools/terraform/terraform.go index f2a97a0ec86..cafc1d31709 100644 --- a/cli/azd/pkg/tools/terraform/terraform.go +++ b/cli/azd/pkg/tools/terraform/terraform.go @@ -8,8 +8,10 @@ import ( "encoding/json" "fmt" "log" + "os" "github.com/azure/azure-dev/cli/azd/pkg/exec" + "github.com/azure/azure-dev/cli/azd/pkg/osutil" "github.com/azure/azure-dev/cli/azd/pkg/tools" "github.com/blang/semver/v4" ) @@ -95,20 +97,31 @@ func (cli *terraformCli) SetEnv(env []string) { func (cli *terraformCli) runCommand(ctx context.Context, args ...string) (exec.RunResult, error) { runArgs := exec. NewRunArgs("terraform", args...). - WithEnv(cli.env). - WithSystemEnvMerged() + WithEnv(cli.env) - return cli.commandRunner.Run(ctx, runArgs) + return withAzEmulator(ctx, cli.commandRunner, runArgs) } func (cli *terraformCli) runInteractive(ctx context.Context, args ...string) (exec.RunResult, error) { runArgs := exec. NewRunArgs("terraform", args...). WithEnv(cli.env). - WithSystemEnvMerged(). WithInteractive(true) - return cli.commandRunner.Run(ctx, runArgs) + return withAzEmulator(ctx, cli.commandRunner, runArgs) +} + +func withAzEmulator(ctx context.Context, commandRunner exec.CommandRunner, runArgs exec.RunArgs) (exec.RunResult, error) { + azEmulatorPath, err := emulateAzFromPath() + if err != nil { + return exec.RunResult{}, fmt.Errorf("emulating az path: %w", err) + } + defer os.RemoveAll(azEmulatorPath) + runArgs.AppendParams( + osutil.AzEmulateKey(), + fmt.Sprintf("PATH=%s", azEmulatorPath), + ) + return commandRunner.Run(ctx, runArgs) } func (cli *terraformCli) unmarshalCliVersion(ctx context.Context, component string) (string, error) { From 139f60ce1401dafd7f9aa16729b5c64e6532fffe Mon Sep 17 00:00:00 2001 From: Victor Vazquez Date: Mon, 3 Jun 2024 21:20:53 +0000 Subject: [PATCH 06/13] rev --- cli/azd/pkg/exec/util_test.go | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/cli/azd/pkg/exec/util_test.go b/cli/azd/pkg/exec/util_test.go index 0d45cb6d809..64254c0ac86 100644 --- a/cli/azd/pkg/exec/util_test.go +++ b/cli/azd/pkg/exec/util_test.go @@ -6,8 +6,10 @@ package exec import ( "bytes" "context" + "os" "regexp" "runtime" + "sort" "testing" "time" @@ -82,6 +84,20 @@ func TestKillCommand(t *testing.T) { require.LessOrEqual(t, since, 10*time.Second) } +func TestAppendEnv(t *testing.T) { + require.Nil(t, appendEnv([]string{})) + require.Nil(t, appendEnv(nil)) + + expectedEnv := os.Environ() + expectedEnv = append(expectedEnv, "azd_random_var=world") + sort.Strings(expectedEnv) + + actualEnv := appendEnv([]string{"azd_random_var=world"}) + sort.Strings(actualEnv) + + require.Equal(t, expectedEnv, actualEnv) +} + func TestRunList(t *testing.T) { runner := NewCommandRunner(nil) res, err := runner.RunList(context.Background(), []string{ From 5a98a5468510515cc19cd2b9f9e9ad0e3d4e342f Mon Sep 17 00:00:00 2001 From: Victor Vazquez Date: Tue, 4 Jun 2024 02:01:13 +0000 Subject: [PATCH 07/13] move emulator to different layer --- cli/azd/cmd/auth_token.go | 9 - cli/azd/cmd/az_cli_emulate_account.go | 112 ---- cli/azd/cmd/root.go | 1 - cli/azd/cmd/version.go | 14 - cli/azd/emulator/account.go | 225 ++++++++ cli/azd/emulator/container.go | 755 ++++++++++++++++++++++++++ cli/azd/emulator/root.go | 21 + cli/azd/emulator/version.go | 26 + cli/azd/main.go | 10 + cli/azd/pkg/contracts/auth_token.go | 10 - 10 files changed, 1037 insertions(+), 146 deletions(-) delete mode 100644 cli/azd/cmd/az_cli_emulate_account.go create mode 100644 cli/azd/emulator/account.go create mode 100644 cli/azd/emulator/container.go create mode 100644 cli/azd/emulator/root.go create mode 100644 cli/azd/emulator/version.go diff --git a/cli/azd/cmd/auth_token.go b/cli/azd/cmd/auth_token.go index 1073ade12c5..1d5364518c9 100644 --- a/cli/azd/cmd/auth_token.go +++ b/cli/azd/cmd/auth_token.go @@ -173,15 +173,6 @@ func (a *authTokenAction) Run(ctx context.Context) (*actions.ActionResult, error return nil, fmt.Errorf("fetching token: %w", err) } - if osutil.IsAzEmulator() { - res := contracts.AzEmulateAuthTokenResult{ - AccessToken: token.Token, - ExpiresOn: contracts.RFC3339Time(token.ExpiresOn), - } - - return nil, a.formatter.Format(res, a.writer, nil) - } - res := contracts.AuthTokenResult{ Token: token.Token, ExpiresOn: contracts.RFC3339Time(token.ExpiresOn), diff --git a/cli/azd/cmd/az_cli_emulate_account.go b/cli/azd/cmd/az_cli_emulate_account.go deleted file mode 100644 index 47cb4196f08..00000000000 --- a/cli/azd/cmd/az_cli_emulate_account.go +++ /dev/null @@ -1,112 +0,0 @@ -package cmd - -import ( - "context" - "encoding/json" - "fmt" - - "github.com/azure/azure-dev/cli/azd/cmd/actions" - "github.com/azure/azure-dev/cli/azd/internal" - "github.com/azure/azure-dev/cli/azd/pkg/account" - "github.com/azure/azure-dev/cli/azd/pkg/environment" - "github.com/azure/azure-dev/cli/azd/pkg/input" - "github.com/azure/azure-dev/cli/azd/pkg/output" - "github.com/spf13/cobra" - "github.com/spf13/pflag" -) - -func azCliEmulateAccountCommands(root *actions.ActionDescriptor) *actions.ActionDescriptor { - group := root.Add("account", &actions.ActionDescriptorOptions{ - Command: &cobra.Command{ - Use: "account", - Short: "Emulates az account commands", - Hidden: true, - }, - }) - - group.Add("show", &actions.ActionDescriptorOptions{ - Command: newAccountShowCmd(), - FlagsResolver: newAccountShowFlags, - ActionResolver: newAccountAction, - OutputFormats: []output.Format{output.JsonFormat}, - DefaultFormat: output.JsonFormat, - }) - - group.Add("get-access-token", &actions.ActionDescriptorOptions{ - Command: &cobra.Command{ - Use: "get-access-token", - Hidden: true, - }, - FlagsResolver: newAuthTokenFlags, - ActionResolver: newAuthTokenAction, - OutputFormats: []output.Format{output.JsonFormat}, - DefaultFormat: output.JsonFormat, - }) - - return group -} - -type accountShowFlags struct { - global *internal.GlobalCommandOptions - internal.EnvFlag -} - -func (s *accountShowFlags) Bind(local *pflag.FlagSet, global *internal.GlobalCommandOptions) { - s.EnvFlag.Bind(local, global) - s.global = global -} - -func newAccountShowFlags(cmd *cobra.Command, global *internal.GlobalCommandOptions) *accountShowFlags { - flags := &accountShowFlags{} - flags.Bind(cmd.Flags(), global) - - return flags -} - -func newAccountShowCmd() *cobra.Command { - return &cobra.Command{ - Use: "show", - Hidden: true, - } -} - -type accountShowAction struct { - env *environment.Environment - console input.Console - subManager *account.SubscriptionsManager -} - -func newAccountAction( - console input.Console, - env *environment.Environment, - subManager *account.SubscriptionsManager, -) actions.Action { - return &accountShowAction{ - console: console, - env: env, - subManager: subManager, - } -} - -type accountShowOutput struct { - Id string `json:"id"` - TenantId string `json:"tenantId"` -} - -func (s *accountShowAction) Run(ctx context.Context) (*actions.ActionResult, error) { - subId := s.env.GetSubscriptionId() - tenantId, err := s.subManager.LookupTenant(ctx, subId) - if err != nil { - return nil, err - } - o := accountShowOutput{ - Id: subId, - TenantId: tenantId, - } - output, err := json.Marshal(o) - if err != nil { - return nil, err - } - fmt.Fprint(s.console.Handles().Stdout, string(output)) - return nil, nil -} diff --git a/cli/azd/cmd/root.go b/cli/azd/cmd/root.go index 88db17b59e9..d38ea8cc78c 100644 --- a/cli/azd/cmd/root.go +++ b/cli/azd/cmd/root.go @@ -125,7 +125,6 @@ func NewRootCmd( templatesActions(root) authActions(root) hooksActions(root) - azCliEmulateAccountCommands(root) root.Add("version", &actions.ActionDescriptorOptions{ Command: &cobra.Command{ diff --git a/cli/azd/cmd/version.go b/cli/azd/cmd/version.go index d1cafc9afaf..fb464321fc2 100644 --- a/cli/azd/cmd/version.go +++ b/cli/azd/cmd/version.go @@ -12,7 +12,6 @@ import ( "github.com/azure/azure-dev/cli/azd/internal" "github.com/azure/azure-dev/cli/azd/pkg/contracts" "github.com/azure/azure-dev/cli/azd/pkg/input" - "github.com/azure/azure-dev/cli/azd/pkg/osutil" "github.com/azure/azure-dev/cli/azd/pkg/output" "github.com/spf13/cobra" "github.com/spf13/pflag" @@ -55,19 +54,6 @@ func newVersionAction( } func (v *versionAction) Run(ctx context.Context) (*actions.ActionResult, error) { - // fake-az in env makes azd to simulate az cli output. - // This is to make tools like terraform to use azd when they thing they are using az. - if osutil.IsAzEmulator() { - fmt.Fprintf(v.console.Handles().Stdout, `{ - "azure-cli": "2.61.0", - "azure-cli-core": "2.61.0", - "azure-cli-telemetry": "1.1.0", - "extensions": {} - } - `) - return nil, nil - } - switch v.formatter.Kind() { case output.NoneFormat: fmt.Fprintf(v.console.Handles().Stdout, "azd version %s\n", internal.Version) diff --git a/cli/azd/emulator/account.go b/cli/azd/emulator/account.go new file mode 100644 index 00000000000..32968a297fd --- /dev/null +++ b/cli/azd/emulator/account.go @@ -0,0 +1,225 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package emulator + +import ( + + // Importing for infrastructure provider plugin registrations + + "context" + "encoding/json" + "fmt" + "os" + + "github.com/Azure/azure-sdk-for-go/sdk/azcore" + azcloud "github.com/Azure/azure-sdk-for-go/sdk/azcore/cloud" + "github.com/Azure/azure-sdk-for-go/sdk/azcore/policy" + "github.com/azure/azure-dev/cli/azd/pkg/account" + "github.com/azure/azure-dev/cli/azd/pkg/auth" + "github.com/azure/azure-dev/cli/azd/pkg/cloud" + "github.com/azure/azure-dev/cli/azd/pkg/environment" + "github.com/azure/azure-dev/cli/azd/pkg/ioc" + "github.com/spf13/cobra" +) + +func accountCommands() *cobra.Command { + accountGroup := &cobra.Command{ + Use: "account", + } + accountGroup.AddCommand(showCmd()) + accountGroup.AddCommand(accessTokenCmd()) + return accountGroup +} + +type accountShowOutput struct { + Id string `json:"id"` + TenantId string `json:"tenantId"` +} + +func showCmd() *cobra.Command { + showCmd := &cobra.Command{ + Use: "show", + RunE: func(cmd *cobra.Command, args []string) error { + + ctx := context.Background() + rootContainer := ioc.NewNestedContainer(nil) + ioc.RegisterInstance(rootContainer, ctx) + registerCommonDependencies(rootContainer) + + var subManager *account.SubscriptionsManager + if err := rootContainer.Resolve(&subManager); err != nil { + return err + } + var env *environment.Environment + if err := rootContainer.Resolve(&env); err != nil { + return err + } + + subId := env.GetSubscriptionId() + tenantId, err := subManager.LookupTenant(ctx, subId) + if err != nil { + return err + } + o := accountShowOutput{ + Id: subId, + TenantId: tenantId, + } + output, err := json.Marshal(o) + if err != nil { + return err + } + fmt.Println(string(output)) + return nil + }, + } + return showCmd +} + +func LoginScopes(cloud *cloud.Cloud) []string { + resourceManagerUrl := cloud.Configuration.Services[azcloud.ResourceManager].Endpoint + return []string{ + fmt.Sprintf("%s//.default", resourceManagerUrl), + } +} + +func accessTokenCmd() *cobra.Command { + accessTokenCmd := &cobra.Command{ + Use: "get-access-token", + RunE: func(cmd *cobra.Command, args []string) error { + + ctx := context.Background() + rootContainer := ioc.NewNestedContainer(nil) + ioc.RegisterInstance(rootContainer, ctx) + registerCommonDependencies(rootContainer) + + var cloud *cloud.Cloud + if err := rootContainer.Resolve(&cloud); err != nil { + return err + } + var envResolver environment.EnvironmentResolver + if err := rootContainer.Resolve(&envResolver); err != nil { + return err + } + var subResolver account.SubscriptionTenantResolver + if err := rootContainer.Resolve(&subResolver); err != nil { + return err + } + var credentialProvider CredentialProviderFn + if err := rootContainer.Resolve(&credentialProvider); err != nil { + return err + } + + scopes, err := cmd.Flags().GetStringArray("scope") + if err != nil { + return err + } + if len(scopes) == 0 { + scopes = auth.LoginScopes(cloud) + } + + var cred azcore.TokenCredential + tenantId := cmd.Flag("tenant").Value.String() + // 2) From azd env + if tenantId == "" { + tenantIdFromAzdEnv, err := getTenantIdFromAzdEnv(ctx, envResolver, subResolver) + if err != nil { + return err + } + tenantId = tenantIdFromAzdEnv + } + // 3) From system env + if tenantId == "" { + tenantIdFromSysEnv, err := getTenantIdFromEnv(ctx, subResolver) + if err != nil { + return err + } + tenantId = tenantIdFromSysEnv + } + + // If tenantId is still empty, the fallback is to use current logged in user's home-tenant id. + cred, err = credentialProvider(ctx, &auth.CredentialForCurrentUserOptions{ + NoPrompt: true, + TenantID: tenantId, + }) + if err != nil { + return err + } + + token, err := cred.GetToken(ctx, policy.TokenRequestOptions{ + Scopes: scopes, + }) + if err != nil { + return fmt.Errorf("fetching token: %w", err) + } + + type azEmulateAuthTokenResult struct { + AccessToken string `json:"accessToken"` + ExpiresOn string `json:"expiresOn"` + } + res := azEmulateAuthTokenResult{ + AccessToken: token.Token, + ExpiresOn: token.ExpiresOn.Format("2006-01-02 15:04:05.000000"), + } + output, err := json.Marshal(res) + if err != nil { + return err + } + fmt.Println(string(output)) + return nil + }, + } + accessTokenCmd.Flags().StringP("output", "o", "", "Output format.") + accessTokenCmd.Flags().StringArray( + "scope", []string{}, "Space-separated AAD scopes in AAD v2.0. Default to Azure Resource Manager") + accessTokenCmd.Flags().StringP("tenant", "t", "", + "Tenant ID for which the token is acquired. Only available for user"+ + " and service principal account, not for MSI or Cloud Shell account.") + return accessTokenCmd +} + +func getTenantIdFromAzdEnv( + ctx context.Context, + envResolver environment.EnvironmentResolver, + subResolver account.SubscriptionTenantResolver) (tenantId string, err error) { + azdEnv, err := envResolver(ctx) + if err != nil { + // No azd env, return empty tenantId + return tenantId, nil + } + + subIdAtAzdEnv := azdEnv.GetSubscriptionId() + if subIdAtAzdEnv == "" { + // azd env found, but missing or empty subscriptionID + return tenantId, nil + } + + tenantId, err = subResolver.LookupTenant(ctx, subIdAtAzdEnv) + if err != nil { + return tenantId, fmt.Errorf( + "resolving the Azure Directory from azd environment (%s): %w", + azdEnv.Name(), + err) + } + + return tenantId, nil +} + +func getTenantIdFromEnv( + ctx context.Context, + subResolver account.SubscriptionTenantResolver) (tenantId string, err error) { + + subIdAtSysEnv, found := os.LookupEnv(environment.SubscriptionIdEnvVarName) + if !found { + // no env var from system + return tenantId, nil + } + + tenantId, err = subResolver.LookupTenant(ctx, subIdAtSysEnv) + if err != nil { + return tenantId, fmt.Errorf( + "resolving the Azure Directory from system environment (%s): %w", environment.SubscriptionIdEnvVarName, err) + } + + return tenantId, nil +} diff --git a/cli/azd/emulator/container.go b/cli/azd/emulator/container.go new file mode 100644 index 00000000000..d5a724f5187 --- /dev/null +++ b/cli/azd/emulator/container.go @@ -0,0 +1,755 @@ +package emulator + +import ( + "context" + "fmt" + "io" + "log" + "net/http" + "net/url" + "os" + "strings" + + "github.com/Azure/azure-sdk-for-go/sdk/azcore" + "github.com/Azure/azure-sdk-for-go/sdk/azcore/arm" + "github.com/MakeNowJust/heredoc/v2" + "github.com/azure/azure-dev/cli/azd/cmd/middleware" + "github.com/azure/azure-dev/cli/azd/internal" + "github.com/azure/azure-dev/cli/azd/internal/repository" + "github.com/azure/azure-dev/cli/azd/pkg/account" + "github.com/azure/azure-dev/cli/azd/pkg/ai" + "github.com/azure/azure-dev/cli/azd/pkg/alpha" + "github.com/azure/azure-dev/cli/azd/pkg/auth" + "github.com/azure/azure-dev/cli/azd/pkg/azapi" + "github.com/azure/azure-dev/cli/azd/pkg/azd" + "github.com/azure/azure-dev/cli/azd/pkg/azsdk" + "github.com/azure/azure-dev/cli/azd/pkg/cloud" + "github.com/azure/azure-dev/cli/azd/pkg/config" + "github.com/azure/azure-dev/cli/azd/pkg/containerapps" + "github.com/azure/azure-dev/cli/azd/pkg/devcenter" + "github.com/azure/azure-dev/cli/azd/pkg/environment" + "github.com/azure/azure-dev/cli/azd/pkg/environment/azdcontext" + "github.com/azure/azure-dev/cli/azd/pkg/exec" + "github.com/azure/azure-dev/cli/azd/pkg/helm" + "github.com/azure/azure-dev/cli/azd/pkg/httputil" + "github.com/azure/azure-dev/cli/azd/pkg/infra" + "github.com/azure/azure-dev/cli/azd/pkg/infra/provisioning" + "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/keyvault" + "github.com/azure/azure-dev/cli/azd/pkg/kubelogin" + "github.com/azure/azure-dev/cli/azd/pkg/kustomize" + "github.com/azure/azure-dev/cli/azd/pkg/lazy" + "github.com/azure/azure-dev/cli/azd/pkg/output" + "github.com/azure/azure-dev/cli/azd/pkg/platform" + "github.com/azure/azure-dev/cli/azd/pkg/project" + "github.com/azure/azure-dev/cli/azd/pkg/prompt" + "github.com/azure/azure-dev/cli/azd/pkg/state" + "github.com/azure/azure-dev/cli/azd/pkg/templates" + "github.com/azure/azure-dev/cli/azd/pkg/tools/azcli" + "github.com/azure/azure-dev/cli/azd/pkg/tools/docker" + "github.com/azure/azure-dev/cli/azd/pkg/tools/dotnet" + "github.com/azure/azure-dev/cli/azd/pkg/tools/git" + "github.com/azure/azure-dev/cli/azd/pkg/tools/github" + "github.com/azure/azure-dev/cli/azd/pkg/tools/javac" + "github.com/azure/azure-dev/cli/azd/pkg/tools/kubectl" + "github.com/azure/azure-dev/cli/azd/pkg/tools/maven" + "github.com/azure/azure-dev/cli/azd/pkg/tools/npm" + "github.com/azure/azure-dev/cli/azd/pkg/tools/python" + "github.com/azure/azure-dev/cli/azd/pkg/tools/swa" + "github.com/azure/azure-dev/cli/azd/pkg/workflow" + "github.com/benbjohnson/clock" + "github.com/mattn/go-colorable" + "github.com/spf13/cobra" + "golang.org/x/exp/slices" +) + +func createHttpClient() *http.Client { + return http.DefaultClient +} + +func createClock() clock.Clock { + return clock.New() +} + +type CredentialProviderFn func(context.Context, *auth.CredentialForCurrentUserOptions) (azcore.TokenCredential, error) + +type CmdAnnotations map[string]string + +// Registers common Azd dependencies +func registerCommonDependencies(container *ioc.NestedContainer) { + // Core bootstrapping registrations + ioc.RegisterInstance(container, container) + + // Standard Registrations + container.MustRegisterTransient(output.GetCommandFormatter) + + container.MustRegisterScoped(func( + rootOptions *internal.GlobalCommandOptions, + formatter output.Formatter, + cmd *cobra.Command) input.Console { + writer := cmd.OutOrStdout() + // When using JSON formatting, we want to ensure we always write messages from the console to stderr. + if formatter != nil && formatter.Kind() == output.JsonFormat { + writer = cmd.ErrOrStderr() + } + + if os.Getenv("NO_COLOR") != "" { + writer = colorable.NewNonColorable(writer) + } + + isTerminal := cmd.OutOrStdout() == os.Stdout && + cmd.InOrStdin() == os.Stdin && input.IsTerminal(os.Stdout.Fd(), os.Stdin.Fd()) + + return input.NewConsole(rootOptions.NoPrompt, isTerminal, input.Writers{Output: writer}, input.ConsoleHandles{ + Stdin: cmd.InOrStdin(), + Stdout: cmd.OutOrStdout(), + Stderr: cmd.ErrOrStderr(), + }, formatter, nil) + }) + + container.MustRegisterSingleton( + func(console input.Console, rootOptions *internal.GlobalCommandOptions) exec.CommandRunner { + return exec.NewCommandRunner( + &exec.RunnerOptions{ + Stdin: console.Handles().Stdin, + Stdout: console.Handles().Stdout, + Stderr: console.Handles().Stderr, + DebugLogging: rootOptions.EnableDebugLogging, + }) + }, + ) + + client := createHttpClient() + ioc.RegisterInstance[httputil.HttpClient](container, client) + ioc.RegisterInstance[auth.HttpClient](container, client) + container.MustRegisterSingleton(func() httputil.UserAgent { + return httputil.UserAgent(internal.UserAgent()) + }) + + // Auth + container.MustRegisterSingleton(auth.NewLoggedInGuard) + container.MustRegisterSingleton(auth.NewMultiTenantCredentialProvider) + container.MustRegisterSingleton(func(mgr *auth.Manager) CredentialProviderFn { + return mgr.CredentialForCurrentUser + }) + + container.MustRegisterSingleton(func(console input.Console) io.Writer { + writer := console.Handles().Stdout + + if os.Getenv("NO_COLOR") != "" { + writer = colorable.NewNonColorable(writer) + } + + return writer + }) + + container.MustRegisterScoped(func(cmd *cobra.Command) internal.EnvFlag { + // The env flag `-e, --environment` is available on most azd commands but not all + // This is typically used to override the default environment and is used for bootstrapping other components + // such as the azd environment. + // If the flag is not available, don't panic, just return an empty string which will then allow for our default + // semantics to follow. + envValue, err := cmd.Flags().GetString(internal.EnvironmentNameFlagName) + if err != nil { + log.Printf("'%s'command asked for envFlag, but envFlag was not included in cmd.Flags().", cmd.CommandPath()) + envValue = "" + } + + return internal.EnvFlag{EnvironmentName: envValue} + }) + + container.MustRegisterSingleton(func(cmd *cobra.Command) CmdAnnotations { + return cmd.Annotations + }) + + // Azd Context + container.MustRegisterSingleton(func(lazyAzdContext *lazy.Lazy[*azdcontext.AzdContext]) (*azdcontext.AzdContext, error) { + return lazyAzdContext.GetValue() + }) + + // Lazy loads the Azd context after the azure.yaml file becomes available + container.MustRegisterSingleton(func() *lazy.Lazy[*azdcontext.AzdContext] { + return lazy.NewLazy(azdcontext.NewAzdContext) + }) + + // Register an initialized environment based on the specified environment flag, or the default environment. + // Note that referencing an *environment.Environment in a command automatically triggers a UI prompt if the + // environment is uninitialized or a default environment doesn't yet exist. + container.MustRegisterScoped( + func(ctx context.Context, + azdContext *azdcontext.AzdContext, + envManager environment.Manager, + lazyEnv *lazy.Lazy[*environment.Environment], + envFlags internal.EnvFlag, + ) (*environment.Environment, error) { + if azdContext == nil { + return nil, azdcontext.ErrNoProject + } + + environmentName := envFlags.EnvironmentName + var err error + + env, err := envManager.LoadOrInitInteractive(ctx, environmentName) + if err != nil { + return nil, fmt.Errorf("loading environment: %w", err) + } + + // Reset lazy env value after loading or creating environment + // This allows any previous lazy instances (such as hooks) to now point to the same instance + lazyEnv.SetValue(env) + + return env, nil + }, + ) + container.MustRegisterScoped(func(lazyEnvManager *lazy.Lazy[environment.Manager]) environment.EnvironmentResolver { + return func(ctx context.Context) (*environment.Environment, error) { + azdCtx, err := azdcontext.NewAzdContext() + if err != nil { + return nil, err + } + defaultEnv, err := azdCtx.GetDefaultEnvironmentName() + if err != nil { + return nil, err + } + + // We need to lazy load the environment manager since it depends on azd context + envManager, err := lazyEnvManager.GetValue() + if err != nil { + return nil, err + } + + return envManager.Get(ctx, defaultEnv) + } + }) + + container.MustRegisterSingleton(environment.NewLocalFileDataStore) + container.MustRegisterSingleton(environment.NewManager) + + container.MustRegisterSingleton(func(serviceLocator ioc.ServiceLocator) *lazy.Lazy[environment.LocalDataStore] { + return lazy.NewLazy(func() (environment.LocalDataStore, error) { + var localDataStore environment.LocalDataStore + err := serviceLocator.Resolve(&localDataStore) + if err != nil { + return nil, err + } + + return localDataStore, nil + }) + }) + + // Environment manager depends on azd context + container.MustRegisterSingleton( + func(serviceLocator ioc.ServiceLocator, azdContext *lazy.Lazy[*azdcontext.AzdContext]) *lazy.Lazy[environment.Manager] { + return lazy.NewLazy(func() (environment.Manager, error) { + azdCtx, err := azdContext.GetValue() + if err != nil { + return nil, err + } + + // Register the Azd context instance as a singleton in the container if now available + ioc.RegisterInstance(container, azdCtx) + + var envManager environment.Manager + err = serviceLocator.Resolve(&envManager) + if err != nil { + return nil, err + } + + return envManager, nil + }) + }, + ) + + container.MustRegisterSingleton(func( + lazyProjectConfig *lazy.Lazy[*project.ProjectConfig], + userConfigManager config.UserConfigManager, + ) (*state.RemoteConfig, error) { + var remoteStateConfig *state.RemoteConfig + + userConfig, err := userConfigManager.Load() + if err != nil { + return nil, fmt.Errorf("loading user config: %w", err) + } + + // The project config may not be available yet + // Ex) Within init phase of fingerprinting + projectConfig, _ := lazyProjectConfig.GetValue() + + // Lookup remote state config in the following precedence: + // 1. Project azure.yaml + // 2. User configuration + if projectConfig != nil && projectConfig.State != nil && projectConfig.State.Remote != nil { + remoteStateConfig = projectConfig.State.Remote + } else { + if _, err := userConfig.GetSection("state.remote", &remoteStateConfig); err != nil { + return nil, fmt.Errorf("getting remote state config: %w", err) + } + } + + return remoteStateConfig, nil + }) + + // Lazy loads an existing environment, erroring out if not available + // One can repeatedly call GetValue to wait until the environment is available. + container.MustRegisterScoped( + func( + ctx context.Context, + lazyEnvManager *lazy.Lazy[environment.Manager], + lazyAzdContext *lazy.Lazy[*azdcontext.AzdContext], + envFlags internal.EnvFlag, + ) *lazy.Lazy[*environment.Environment] { + return lazy.NewLazy(func() (*environment.Environment, error) { + azdCtx, err := lazyAzdContext.GetValue() + if err != nil { + return nil, err + } + + environmentName := envFlags.EnvironmentName + if environmentName == "" { + environmentName, err = azdCtx.GetDefaultEnvironmentName() + if err != nil { + return nil, err + } + } + + envManager, err := lazyEnvManager.GetValue() + if err != nil { + return nil, err + } + + env, err := envManager.Get(ctx, environmentName) + if err != nil { + return nil, err + } + + return env, err + }) + }, + ) + + // Project Config + container.MustRegisterScoped( + func(lazyConfig *lazy.Lazy[*project.ProjectConfig]) (*project.ProjectConfig, error) { + return lazyConfig.GetValue() + }, + ) + + // Lazy loads the project config from the Azd Context when it becomes available + container.MustRegisterScoped( + func( + ctx context.Context, + lazyAzdContext *lazy.Lazy[*azdcontext.AzdContext], + ) *lazy.Lazy[*project.ProjectConfig] { + return lazy.NewLazy(func() (*project.ProjectConfig, error) { + azdCtx, err := lazyAzdContext.GetValue() + if err != nil { + return nil, err + } + + projectConfig, err := project.Load(ctx, azdCtx.ProjectPath()) + if err != nil { + return nil, err + } + + return projectConfig, nil + }) + }, + ) + + container.MustRegisterSingleton(func( + ctx context.Context, + userConfigManager config.UserConfigManager, + lazyProjectConfig *lazy.Lazy[*project.ProjectConfig], + lazyAzdContext *lazy.Lazy[*azdcontext.AzdContext], + lazyLocalEnvStore *lazy.Lazy[environment.LocalDataStore], + ) (*cloud.Cloud, error) { + + // Precedence for cloud configuration: + // 1. Local environment config (.azure//config.json) + // 2. Project config (azure.yaml) + // 3. User config (~/.azure/config.json) + // Default if no cloud configured: Azure Public Cloud + + validClouds := fmt.Sprintf( + "Valid cloud names are '%s', '%s', '%s'.", + cloud.AzurePublicName, + cloud.AzureChinaCloudName, + cloud.AzureUSGovernmentName, + ) + + // Local Environment Configuration (.azure//config.json) + localEnvStore, _ := lazyLocalEnvStore.GetValue() + if azdCtx, err := lazyAzdContext.GetValue(); err == nil { + if azdCtx != nil && localEnvStore != nil { + if defaultEnvName, err := azdCtx.GetDefaultEnvironmentName(); err == nil { + if env, err := localEnvStore.Get(ctx, defaultEnvName); err == nil { + if cloudConfigurationNode, exists := env.Config.Get(cloud.ConfigPath); exists { + if value, err := cloud.ParseCloudConfig(cloudConfigurationNode); err == nil { + cloudConfig, err := cloud.NewCloud(value) + if err == nil { + return cloudConfig, nil + } + + return nil, &internal.ErrorWithSuggestion{ + Err: err, + Suggestion: fmt.Sprintf( + "Set the cloud configuration by editing the 'cloud' node in the config.json file for the %s environment\n%s", + defaultEnvName, + validClouds, + ), + } + } + } + } + } + } + } + + // Project Configuration (azure.yaml) + projConfig, err := lazyProjectConfig.GetValue() + if err == nil && projConfig != nil && projConfig.Cloud != nil { + if value, err := cloud.ParseCloudConfig(projConfig.Cloud); err == nil { + if cloudConfig, err := cloud.ParseCloudConfig(value); err == nil { + if cloud, err := cloud.NewCloud(cloudConfig); err == nil { + return cloud, nil + } else { + return nil, &internal.ErrorWithSuggestion{ + Err: err, + //nolint:lll + Suggestion: fmt.Sprintf("Set the cloud configuration by editing the 'cloud' node in the project YAML file\n%s", validClouds), + } + } + } + } + } + + // User Configuration (~/.azure/config.json) + if azdConfig, err := userConfigManager.Load(); err == nil { + if cloudConfigNode, exists := azdConfig.Get(cloud.ConfigPath); exists { + if value, err := cloud.ParseCloudConfig(cloudConfigNode); err == nil { + if cloud, err := cloud.NewCloud(value); err == nil { + return cloud, nil + } else { + return nil, &internal.ErrorWithSuggestion{ + Err: err, + Suggestion: fmt.Sprintf("Set the cloud configuration using 'azd config set cloud.name '.\n%s", validClouds), + } + } + } + } + } + + return cloud.NewCloud(&cloud.Config{Name: cloud.AzurePublicName}) + }) + + container.MustRegisterSingleton(func(cloud *cloud.Cloud) cloud.PortalUrlBase { + return cloud.PortalUrlBase + }) + + container.MustRegisterSingleton(func( + httpClient httputil.HttpClient, + userAgent httputil.UserAgent, + cloud *cloud.Cloud, + ) *azsdk.ClientOptionsBuilderFactory { + return azsdk.NewClientOptionsBuilderFactory(httpClient, string(userAgent), cloud) + }) + + container.MustRegisterSingleton(func( + clientOptionsBuilderFactory *azsdk.ClientOptionsBuilderFactory, + ) *azcore.ClientOptions { + return clientOptionsBuilderFactory.NewClientOptionsBuilder(). + WithPerCallPolicy(azsdk.NewMsCorrelationPolicy()). + BuildCoreClientOptions() + }) + + container.MustRegisterSingleton(func( + clientOptionsBuilderFactory *azsdk.ClientOptionsBuilderFactory, + ) *arm.ClientOptions { + return clientOptionsBuilderFactory.NewClientOptionsBuilder(). + WithPerCallPolicy(azsdk.NewMsCorrelationPolicy()). + BuildArmClientOptions() + }) + + container.MustRegisterSingleton(templates.NewTemplateManager) + container.MustRegisterSingleton(templates.NewSourceManager) + container.MustRegisterScoped(project.NewResourceManager) + container.MustRegisterScoped(func(serviceLocator ioc.ServiceLocator) *lazy.Lazy[project.ResourceManager] { + return lazy.NewLazy(func() (project.ResourceManager, error) { + var resourceManager project.ResourceManager + err := serviceLocator.Resolve(&resourceManager) + + return resourceManager, err + }) + }) + container.MustRegisterScoped(project.NewProjectManager) + // Currently caches manifest across command executions + container.MustRegisterSingleton(project.NewDotNetImporter) + container.MustRegisterScoped(project.NewImportManager) + container.MustRegisterScoped(project.NewServiceManager) + + // Even though the service manager is scoped based on its use of environment we can still + // register its internal cache as a singleton to ensure operation caching is consistent across all instances + container.MustRegisterSingleton(func() project.ServiceOperationCache { + return project.ServiceOperationCache{} + }) + + container.MustRegisterScoped(func(serviceLocator ioc.ServiceLocator) *lazy.Lazy[project.ServiceManager] { + return lazy.NewLazy(func() (project.ServiceManager, error) { + var serviceManager project.ServiceManager + err := serviceLocator.Resolve(&serviceManager) + + return serviceManager, err + }) + }) + container.MustRegisterSingleton(repository.NewInitializer) + container.MustRegisterSingleton(alpha.NewFeaturesManager) + container.MustRegisterSingleton(config.NewUserConfigManager) + container.MustRegisterSingleton(config.NewManager) + container.MustRegisterSingleton(config.NewFileConfigManager) + container.MustRegisterScoped(func() (auth.ExternalAuthConfiguration, error) { + cert := os.Getenv("AZD_AUTH_CERT") + endpoint := os.Getenv("AZD_AUTH_ENDPOINT") + key := os.Getenv("AZD_AUTH_KEY") + + client := &http.Client{} + if len(cert) > 0 { + transport, err := httputil.TlsEnabledTransport(cert) + if err != nil { + return auth.ExternalAuthConfiguration{}, + fmt.Errorf("parsing AZD_AUTH_CERT: %w", err) + } + client.Transport = transport + + endpointUrl, err := url.Parse(endpoint) + if err != nil { + return auth.ExternalAuthConfiguration{}, + fmt.Errorf("invalid AZD_AUTH_ENDPOINT value '%s': %w", endpoint, err) + } + + if endpointUrl.Scheme != "https" { + return auth.ExternalAuthConfiguration{}, + fmt.Errorf("invalid AZD_AUTH_ENDPOINT value '%s': scheme must be 'https' when certificate is provided", + endpoint) + } + } + return auth.ExternalAuthConfiguration{ + Endpoint: endpoint, + Client: client, + Key: key, + }, nil + }) + container.MustRegisterScoped(auth.NewManager) + container.MustRegisterSingleton(azcli.NewUserProfileService) + container.MustRegisterSingleton(account.NewSubscriptionsService) + container.MustRegisterSingleton(account.NewManager) + container.MustRegisterSingleton(account.NewSubscriptionsManager) + container.MustRegisterSingleton(account.NewSubscriptionCredentialProvider) + container.MustRegisterSingleton(azcli.NewManagedClustersService) + container.MustRegisterSingleton(azcli.NewAdService) + container.MustRegisterSingleton(azcli.NewContainerRegistryService) + container.MustRegisterSingleton(containerapps.NewContainerAppService) + container.MustRegisterSingleton(keyvault.NewKeyVaultService) + container.MustRegisterScoped(project.NewContainerHelper) + container.MustRegisterSingleton(azcli.NewSpringService) + + container.MustRegisterSingleton(func(subManager *account.SubscriptionsManager) account.SubscriptionTenantResolver { + return subManager + }) + + container.MustRegisterSingleton(func() *internal.GlobalCommandOptions { + return &internal.GlobalCommandOptions{} + }) + + container.MustRegisterSingleton(func() *cobra.Command { + return &cobra.Command{} + }) + + // Tools + container.MustRegisterSingleton(func( + rootOptions *internal.GlobalCommandOptions, + credentialProvider account.SubscriptionCredentialProvider, + httpClient httputil.HttpClient, + armClientOptions *arm.ClientOptions, + ) azcli.AzCli { + return azcli.NewAzCli( + credentialProvider, + httpClient, + azcli.NewAzCliArgs{ + EnableDebug: rootOptions.EnableDebugLogging, + EnableTelemetry: rootOptions.EnableTelemetry, + }, + armClientOptions, + ) + }) + container.MustRegisterSingleton(azapi.NewDeployments) + container.MustRegisterSingleton(azapi.NewDeploymentOperations) + container.MustRegisterSingleton(docker.NewDocker) + container.MustRegisterSingleton(dotnet.NewDotNetCli) + container.MustRegisterSingleton(git.NewGitCli) + container.MustRegisterSingleton(github.NewGitHubCli) + container.MustRegisterSingleton(javac.NewCli) + container.MustRegisterSingleton(kubectl.NewKubectl) + container.MustRegisterSingleton(maven.NewMavenCli) + container.MustRegisterSingleton(kubelogin.NewCli) + container.MustRegisterSingleton(helm.NewCli) + container.MustRegisterSingleton(kustomize.NewCli) + container.MustRegisterSingleton(npm.NewNpmCli) + container.MustRegisterSingleton(python.NewPythonCli) + container.MustRegisterSingleton(swa.NewSwaCli) + container.MustRegisterScoped(ai.NewPythonBridge) + container.MustRegisterScoped(project.NewAiHelper) + + // Provisioning + container.MustRegisterSingleton(infra.NewAzureResourceManager) + container.MustRegisterScoped(provisioning.NewManager) + container.MustRegisterScoped(provisioning.NewPrincipalIdProvider) + container.MustRegisterScoped(prompt.NewDefaultPrompter) + + // Other + container.MustRegisterSingleton(createClock) + + // Service Targets + serviceTargetMap := map[project.ServiceTargetKind]any{ + project.NonSpecifiedTarget: project.NewAppServiceTarget, + project.AppServiceTarget: project.NewAppServiceTarget, + project.AzureFunctionTarget: project.NewFunctionAppTarget, + project.ContainerAppTarget: project.NewContainerAppTarget, + project.StaticWebAppTarget: project.NewStaticWebAppTarget, + project.AksTarget: project.NewAksTarget, + project.SpringAppTarget: project.NewSpringAppTarget, + project.DotNetContainerAppTarget: project.NewDotNetContainerAppTarget, + project.AiEndpointTarget: project.NewAiEndpointTarget, + } + + for target, constructor := range serviceTargetMap { + container.MustRegisterNamedScoped(string(target), constructor) + } + + // Languages + frameworkServiceMap := map[project.ServiceLanguageKind]any{ + project.ServiceLanguageNone: project.NewNoOpProject, + project.ServiceLanguageDotNet: project.NewDotNetProject, + project.ServiceLanguageCsharp: project.NewDotNetProject, + project.ServiceLanguageFsharp: project.NewDotNetProject, + project.ServiceLanguagePython: project.NewPythonProject, + project.ServiceLanguageJavaScript: project.NewNpmProject, + project.ServiceLanguageTypeScript: project.NewNpmProject, + project.ServiceLanguageJava: project.NewMavenProject, + project.ServiceLanguageDocker: project.NewDockerProject, + project.ServiceLanguageSwa: project.NewSwaProject, + } + + for language, constructor := range frameworkServiceMap { + container.MustRegisterNamedScoped(string(language), constructor) + } + + container.MustRegisterNamedScoped(string(project.ServiceLanguageDocker), project.NewDockerProjectAsFrameworkService) + + // Platform configuration + container.MustRegisterSingleton(func(lazyConfig *lazy.Lazy[*platform.Config]) (*platform.Config, error) { + return lazyConfig.GetValue() + }) + + container.MustRegisterSingleton(func( + lazyProjectConfig *lazy.Lazy[*project.ProjectConfig], + userConfigManager config.UserConfigManager, + ) *lazy.Lazy[*platform.Config] { + return lazy.NewLazy(func() (*platform.Config, error) { + // First check `azure.yaml` for platform configuration section + projectConfig, err := lazyProjectConfig.GetValue() + if err == nil && projectConfig != nil && projectConfig.Platform != nil { + return projectConfig.Platform, nil + } + + // Fallback to global user configuration + config, err := userConfigManager.Load() + if err != nil { + return nil, fmt.Errorf("loading user config: %w", err) + } + + var platformConfig *platform.Config + _, err = config.GetSection("platform", &platformConfig) + if err != nil { + return nil, fmt.Errorf("getting platform config: %w", err) + } + + // If we still don't have a platform configuration, check the OS environment + // We check the OS environment instead of AZD environment because the global platform configuration + // cannot be known at this time in the azd bootstrapping process. + if platformConfig == nil { + if envPlatformType, has := os.LookupEnv(environment.PlatformTypeEnvVarName); has { + platformConfig = &platform.Config{ + Type: platform.PlatformKind(envPlatformType), + } + } + } + + if platformConfig == nil || platformConfig.Type == "" { + return nil, platform.ErrPlatformConfigNotFound + } + + // Validate platform type + supportedPlatformKinds := []string{ + string(devcenter.PlatformKindDevCenter), + string(azd.PlatformKindDefault), + } + if !slices.Contains(supportedPlatformKinds, string(platformConfig.Type)) { + return nil, fmt.Errorf( + heredoc.Doc(`platform type '%s' is not supported. Valid values are '%s'. + Run %s to set or %s to reset. (%w)`), + platformConfig.Type, + strings.Join(supportedPlatformKinds, ","), + output.WithBackticks("azd config set platform.type "), + output.WithBackticks("azd config unset platform.type"), + platform.ErrPlatformNotSupported, + ) + } + + return platformConfig, nil + }) + }) + + // Platform Providers + platformProviderMap := map[platform.PlatformKind]any{ + azd.PlatformKindDefault: azd.NewDefaultPlatform, + devcenter.PlatformKindDevCenter: devcenter.NewPlatform, + } + + for provider, constructor := range platformProviderMap { + platformName := fmt.Sprintf("%s-platform", provider) + container.MustRegisterNamedSingleton(platformName, constructor) + } + + container.MustRegisterSingleton(func(s ioc.ServiceLocator) (workflow.AzdCommandRunner, error) { + var rootCmd *cobra.Command + if err := s.ResolveNamed("root-cmd", &rootCmd); err != nil { + return nil, err + } + return &workflowCmdAdapter{cmd: rootCmd}, nil + + }) + container.MustRegisterSingleton(workflow.NewRunner) +} + +// workflowCmdAdapter adapts a cobra command to the workflow.AzdCommandRunner interface +type workflowCmdAdapter struct { + cmd *cobra.Command +} + +func (w *workflowCmdAdapter) SetArgs(args []string) { + w.cmd.SetArgs(args) +} + +// ExecuteContext implements workflow.AzdCommandRunner +func (w *workflowCmdAdapter) ExecuteContext(ctx context.Context) error { + childCtx := middleware.WithChildAction(ctx) + return w.cmd.ExecuteContext(childCtx) +} + +// ArmClientInitializer is a function definition for all Azure SDK ARM Client +type ArmClientInitializer[T comparable] func( + subscriptionId string, + credentials azcore.TokenCredential, + armClientOptions *arm.ClientOptions, +) (T, error) diff --git a/cli/azd/emulator/root.go b/cli/azd/emulator/root.go new file mode 100644 index 00000000000..5a6524092a7 --- /dev/null +++ b/cli/azd/emulator/root.go @@ -0,0 +1,21 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package emulator + +import ( + + // Importing for infrastructure provider plugin registrations + + "github.com/spf13/cobra" +) + +func NewRootCmd() *cobra.Command { + rootCmd := &cobra.Command{ + Use: "az", + Short: "Emulation mode for Azure CLI", + } + rootCmd.AddCommand(versionCmd()) + rootCmd.AddCommand(accountCommands()) + return rootCmd +} diff --git a/cli/azd/emulator/version.go b/cli/azd/emulator/version.go new file mode 100644 index 00000000000..1a3ce5b96c6 --- /dev/null +++ b/cli/azd/emulator/version.go @@ -0,0 +1,26 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package emulator + +import ( + + // Importing for infrastructure provider plugin registrations + + "fmt" + + "github.com/spf13/cobra" +) + +const ( + emulatedAzVersion = `{"azure-cli": "2.61.0","azure-cli-core": "2.61.0","azure-cli-telemetry": "1.1.0","extensions": {}}` +) + +func versionCmd() *cobra.Command { + return &cobra.Command{ + Use: "version", + Run: func(cmd *cobra.Command, args []string) { + fmt.Println(emulatedAzVersion) + }, + } +} diff --git a/cli/azd/main.go b/cli/azd/main.go index d65c7f5160d..f6c0cbe5699 100644 --- a/cli/azd/main.go +++ b/cli/azd/main.go @@ -24,6 +24,7 @@ import ( azcorelog "github.com/Azure/azure-sdk-for-go/sdk/azcore/log" "github.com/azure/azure-dev/cli/azd/cmd" + "github.com/azure/azure-dev/cli/azd/emulator" "github.com/azure/azure-dev/cli/azd/internal" "github.com/azure/azure-dev/cli/azd/internal/telemetry" "github.com/azure/azure-dev/cli/azd/pkg/config" @@ -55,6 +56,15 @@ func main() { log.Printf("azd version: %s", internal.Version) + if osutil.IsAzEmulator() { + log.Println("Running az emulation") + emulateErr := emulator.NewRootCmd().ExecuteContext(ctx) + if emulateErr != nil { + os.Exit(1) + } + os.Exit(0) + } + ts := telemetry.GetTelemetrySystem() latest := make(chan semver.Version) diff --git a/cli/azd/pkg/contracts/auth_token.go b/cli/azd/pkg/contracts/auth_token.go index 13c70c5e8b2..ee2a9937bfb 100644 --- a/cli/azd/pkg/contracts/auth_token.go +++ b/cli/azd/pkg/contracts/auth_token.go @@ -6,8 +6,6 @@ import ( "encoding/json" "fmt" "time" - - "github.com/azure/azure-dev/cli/azd/pkg/osutil" ) // AuthTokenResult is the value returned by `azd get-access-token`. It matches the shape of `azcore.AccessToken` @@ -19,19 +17,11 @@ type AuthTokenResult struct { ExpiresOn RFC3339Time `json:"expiresOn"` } -type AzEmulateAuthTokenResult struct { - AccessToken string `json:"accessToken"` - ExpiresOn RFC3339Time `json:"expiresOn"` -} - // RFC3339Time is a time.Time that uses time.RFC3339 format when marshaling to JSON, not time.RFC3339Nano as // the standard library time.Time does. type RFC3339Time time.Time func (r RFC3339Time) MarshalJSON() ([]byte, error) { - if osutil.IsAzEmulator() { - return []byte(fmt.Sprintf(`"%s"`, time.Time(r).Format("2006-01-02 15:04:05.000000"))), nil - } return []byte(fmt.Sprintf(`"%s"`, time.Time(r).Format(time.RFC3339))), nil } From 98915265dda73ce0aca24c64179830e4fd506547 Mon Sep 17 00:00:00 2001 From: Victor Vazquez Date: Tue, 4 Jun 2024 02:10:09 +0000 Subject: [PATCH 08/13] use unique folder per run --- cli/azd/pkg/tools/terraform/az_emulator.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/cli/azd/pkg/tools/terraform/az_emulator.go b/cli/azd/pkg/tools/terraform/az_emulator.go index b6ddb38d934..ec4be9fc2cd 100644 --- a/cli/azd/pkg/tools/terraform/az_emulator.go +++ b/cli/azd/pkg/tools/terraform/az_emulator.go @@ -22,7 +22,10 @@ func emulateAzFromPath() (string, error) { if err != nil { return "", fmt.Errorf("could not get user config dir: %w", err) } - emuPath := filepath.Join(azdConfigPath, "bin", "azEmulate") + emuPath, err := os.MkdirTemp(filepath.Join(azdConfigPath, "bin"), "azEmulate") + if err != nil { + return "", fmt.Errorf("could not create directory for azEmulate: %w", err) + } err = os.MkdirAll(emuPath, osutil.PermissionDirectoryOwnerOnly) if err != nil { return "", fmt.Errorf("could not create directory for azEmulate: %w", err) From df9e2f6a03f91d520f41a6997f85b7be27b41928 Mon Sep 17 00:00:00 2001 From: Victor Vazquez Date: Tue, 4 Jun 2024 02:11:55 +0000 Subject: [PATCH 09/13] rev --- cli/azd/cmd/auth_token.go | 4 ---- 1 file changed, 4 deletions(-) diff --git a/cli/azd/cmd/auth_token.go b/cli/azd/cmd/auth_token.go index 1d5364518c9..548758756fb 100644 --- a/cli/azd/cmd/auth_token.go +++ b/cli/azd/cmd/auth_token.go @@ -18,7 +18,6 @@ import ( "github.com/azure/azure-dev/cli/azd/pkg/cloud" "github.com/azure/azure-dev/cli/azd/pkg/contracts" "github.com/azure/azure-dev/cli/azd/pkg/environment" - "github.com/azure/azure-dev/cli/azd/pkg/osutil" "github.com/azure/azure-dev/cli/azd/pkg/output" "github.com/spf13/cobra" "github.com/spf13/pflag" @@ -48,9 +47,6 @@ func (f *authTokenFlags) Bind(local *pflag.FlagSet, global *internal.GlobalComma f.global = global local.StringArrayVar(&f.scopes, "scope", nil, "The scope to use when requesting an access token") local.StringVar(&f.tenantID, "tenant-id", "", "The tenant id to use when requesting an access token.") - if osutil.IsAzEmulator() { - local.StringVar(&f.tenantID, "tenant", "", "The tenant id to use when requesting an access token.") - } } type CredentialProviderFn func(context.Context, *auth.CredentialForCurrentUserOptions) (azcore.TokenCredential, error) From e6afa00bf1fffc5fe4b1fb53d8ffeefe0868b90c Mon Sep 17 00:00:00 2001 From: Victor Vazquez Date: Tue, 4 Jun 2024 02:12:54 +0000 Subject: [PATCH 10/13] copyright --- cli/azd/emulator/container.go | 3 +++ cli/azd/pkg/tools/terraform/az_emulator.go | 3 +++ 2 files changed, 6 insertions(+) diff --git a/cli/azd/emulator/container.go b/cli/azd/emulator/container.go index d5a724f5187..c0c36d7a86e 100644 --- a/cli/azd/emulator/container.go +++ b/cli/azd/emulator/container.go @@ -1,3 +1,6 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + package emulator import ( diff --git a/cli/azd/pkg/tools/terraform/az_emulator.go b/cli/azd/pkg/tools/terraform/az_emulator.go index ec4be9fc2cd..55ad1cc336a 100644 --- a/cli/azd/pkg/tools/terraform/az_emulator.go +++ b/cli/azd/pkg/tools/terraform/az_emulator.go @@ -1,3 +1,6 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + package terraform import ( From 2144ff3b537390baac9fc7c83755a181099147b7 Mon Sep 17 00:00:00 2001 From: Victor Vazquez Date: Tue, 4 Jun 2024 03:48:44 +0000 Subject: [PATCH 11/13] use Executable --- cli/azd/pkg/tools/terraform/az_emulator.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/cli/azd/pkg/tools/terraform/az_emulator.go b/cli/azd/pkg/tools/terraform/az_emulator.go index 55ad1cc336a..a394e8b28c7 100644 --- a/cli/azd/pkg/tools/terraform/az_emulator.go +++ b/cli/azd/pkg/tools/terraform/az_emulator.go @@ -7,7 +7,6 @@ import ( "fmt" "io" "os" - "os/exec" "path/filepath" "strings" @@ -17,7 +16,7 @@ import ( // creates a copy of azd binary and renames it to az and returns the path to it func emulateAzFromPath() (string, error) { - path, err := exec.LookPath("azd") + path, err := os.Executable() if err != nil { return "", fmt.Errorf("azd binary not found in PATH: %w", err) } From 1aee2fdf741eadb207116a39016e374e2d8892f5 Mon Sep 17 00:00:00 2001 From: Victor Vazquez Date: Tue, 4 Jun 2024 04:57:35 +0000 Subject: [PATCH 12/13] manual test completed --- cli/azd/emulator/account.go | 1 + cli/azd/emulator/version.go | 4 +++- cli/azd/pkg/tools/terraform/az_emulator.go | 4 ++-- cli/azd/pkg/tools/terraform/terraform.go | 2 +- 4 files changed, 7 insertions(+), 4 deletions(-) diff --git a/cli/azd/emulator/account.go b/cli/azd/emulator/account.go index 32968a297fd..08cab4bc6f4 100644 --- a/cli/azd/emulator/account.go +++ b/cli/azd/emulator/account.go @@ -73,6 +73,7 @@ func showCmd() *cobra.Command { return nil }, } + showCmd.Flags().StringP("output", "o", "", "Output format") return showCmd } diff --git a/cli/azd/emulator/version.go b/cli/azd/emulator/version.go index 1a3ce5b96c6..ecb9ab3786a 100644 --- a/cli/azd/emulator/version.go +++ b/cli/azd/emulator/version.go @@ -17,10 +17,12 @@ const ( ) func versionCmd() *cobra.Command { - return &cobra.Command{ + cmd := &cobra.Command{ Use: "version", Run: func(cmd *cobra.Command, args []string) { fmt.Println(emulatedAzVersion) }, } + cmd.Flags().StringP("output", "o", "", "Output format") + return cmd } diff --git a/cli/azd/pkg/tools/terraform/az_emulator.go b/cli/azd/pkg/tools/terraform/az_emulator.go index a394e8b28c7..066a7fef534 100644 --- a/cli/azd/pkg/tools/terraform/az_emulator.go +++ b/cli/azd/pkg/tools/terraform/az_emulator.go @@ -32,7 +32,7 @@ func emulateAzFromPath() (string, error) { if err != nil { return "", fmt.Errorf("could not create directory for azEmulate: %w", err) } - emuPath = filepath.Join(emuPath, strings.ReplaceAll(filepath.Base(path), "azd", "az")) + emuPath = filepath.Join(emuPath, strings.ReplaceAll(filepath.Base(path), filepath.Base(path), "az")) srcFile, err := os.Open(path) if err != nil { @@ -51,5 +51,5 @@ func emulateAzFromPath() (string, error) { return "", fmt.Errorf("copying binary: %w", err) } - return emuPath, nil + return filepath.Dir(emuPath), nil } diff --git a/cli/azd/pkg/tools/terraform/terraform.go b/cli/azd/pkg/tools/terraform/terraform.go index cafc1d31709..897c1951212 100644 --- a/cli/azd/pkg/tools/terraform/terraform.go +++ b/cli/azd/pkg/tools/terraform/terraform.go @@ -117,7 +117,7 @@ func withAzEmulator(ctx context.Context, commandRunner exec.CommandRunner, runAr return exec.RunResult{}, fmt.Errorf("emulating az path: %w", err) } defer os.RemoveAll(azEmulatorPath) - runArgs.AppendParams( + runArgs.Env = append(runArgs.Env, osutil.AzEmulateKey(), fmt.Sprintf("PATH=%s", azEmulatorPath), ) From 33132a65343247392c2668c2fd4232bcee296e73 Mon Sep 17 00:00:00 2001 From: Victor Vazquez Date: Tue, 4 Jun 2024 05:59:34 +0000 Subject: [PATCH 13/13] test --- cli/azd/pkg/tools/terraform/terraform_test.go | 26 ++++++++++++++++--- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/cli/azd/pkg/tools/terraform/terraform_test.go b/cli/azd/pkg/tools/terraform/terraform_test.go index d1fcac299ca..c8d114f6b8d 100644 --- a/cli/azd/pkg/tools/terraform/terraform_test.go +++ b/cli/azd/pkg/tools/terraform/terraform_test.go @@ -2,6 +2,8 @@ package terraform import ( "context" + "path/filepath" + "strings" "testing" "github.com/azure/azure-dev/cli/azd/pkg/exec" @@ -11,16 +13,32 @@ import ( func Test_WithEnv(t *testing.T) { ran := false - expectedEnvVars := []string{"TF_DATA_DIR=MYDIR"} + expectedEnvVars := []string{"TF_DATA_DIR=MYDIR", "AZURE_AZ_EMULATOR=true"} mockContext := mocks.NewMockContext(context.Background()) mockContext.CommandRunner.When(func(args exec.RunArgs, command string) bool { return args.Cmd == "terraform" }).RespondFn(func(args exec.RunArgs) (exec.RunResult, error) { ran = true - require.Len(t, expectedEnvVars, 1) - require.Equal(t, expectedEnvVars, args.Env) - + require.GreaterOrEqual(t, len(args.Env), 2) + for _, expectedEnvVar := range expectedEnvVars { + require.Contains(t, args.Env, expectedEnvVar) + } + var pathKey, pathValue string + for _, envV := range args.Env { + parts := strings.Split(envV, "=") + pathKey = parts[0] + if pathKey == "PATH" { + if len(parts) > 1 { + pathValue = parts[1] + } + break + } + } + // can't match pathValue as it is different depending on the OS. + // So just check that it is not empty and contains the path to emulate + require.NotEmpty(t, pathValue) + require.Contains(t, pathValue, string(filepath.Separator)+"azEmulate") return exec.NewRunResult(0, "", ""), nil })