diff --git a/cli/azd/pkg/tools/powershell/powershell.go b/cli/azd/pkg/tools/powershell/powershell.go index 659dac724b9..9a5c43c6043 100644 --- a/cli/azd/pkg/tools/powershell/powershell.go +++ b/cli/azd/pkg/tools/powershell/powershell.go @@ -5,29 +5,63 @@ package powershell import ( "context" + "fmt" + "strings" + "github.com/azure/azure-dev/cli/azd/internal" "github.com/azure/azure-dev/cli/azd/pkg/exec" + "github.com/azure/azure-dev/cli/azd/pkg/output" "github.com/azure/azure-dev/cli/azd/pkg/tools" ) // Creates a new PowershellScript command runner func NewPowershellScript(commandRunner exec.CommandRunner, cwd string, envVars []string) tools.Script { return &powershellScript{ - commandRunner: commandRunner, - cwd: cwd, - envVars: envVars, + commandRunner: commandRunner, + cwd: cwd, + envVars: envVars, + checkInstalled: checkPath, + } +} + +// for testing +func NewPowershellScriptWithMockCheckPath( + commandRunner exec.CommandRunner, + cwd string, + envVars []string, + mockCheckPath checkInstalled) tools.Script { + return &powershellScript{ + commandRunner: commandRunner, + cwd: cwd, + envVars: envVars, + checkInstalled: mockCheckPath, } } type powershellScript struct { - commandRunner exec.CommandRunner - cwd string - envVars []string + commandRunner exec.CommandRunner + cwd string + envVars []string + checkInstalled checkInstalled +} + +type checkInstalled func(options tools.ExecOptions) error + +func checkPath(options tools.ExecOptions) (err error) { + return tools.ToolInPath(strings.Split(options.UserPwsh, " ")[0]) } // Executes the specified powershell script // When interactive is true will attach to stdin, stdout & stderr func (bs *powershellScript) Execute(ctx context.Context, path string, options tools.ExecOptions) (exec.RunResult, error) { + if err := bs.checkInstalled(options); err != nil { + return exec.RunResult{}, &internal.ErrorWithSuggestion{ + Err: err, + Suggestion: fmt.Sprintf("PowerShell 7 is not installed or not in the path. To install PowerShell 7, visit %s", + output.WithLinkFormat("https://learn.microsoft.com/powershell/scripting/install/installing-powershell")), + } + } + runArgs := exec.NewRunArgs(options.UserPwsh, path). WithCwd(bs.cwd). WithEnv(bs.envVars). diff --git a/cli/azd/pkg/tools/powershell/powershell_test.go b/cli/azd/pkg/tools/powershell/powershell_test.go index d0c7d2ddc5a..317fac4f4ae 100644 --- a/cli/azd/pkg/tools/powershell/powershell_test.go +++ b/cli/azd/pkg/tools/powershell/powershell_test.go @@ -6,6 +6,7 @@ package powershell import ( "context" "errors" + "strings" "testing" "github.com/Azure/azure-sdk-for-go/sdk/azcore/to" @@ -29,7 +30,7 @@ func Test_Powershell_Execute(t *testing.T) { // #nosec G101 userPwsh := "pwsh -NoProfile" mockContext.CommandRunner.When(func(args exec.RunArgs, command string) bool { - return true + return strings.Contains(args.Cmd, userPwsh) }).RespondFn(func(args exec.RunArgs) (exec.RunResult, error) { require.Equal(t, userPwsh, args.Cmd) require.Equal(t, workingDir, args.Cwd) @@ -39,7 +40,13 @@ func Test_Powershell_Execute(t *testing.T) { return exec.NewRunResult(0, "", ""), nil }) - PowershellScript := NewPowershellScript(mockContext.CommandRunner, workingDir, env) + PowershellScript := NewPowershellScriptWithMockCheckPath( + mockContext.CommandRunner, + workingDir, + env, + func(options tools.ExecOptions) error { + return nil + }) runResult, err := PowershellScript.Execute( *mockContext.Context, scriptPath, @@ -59,7 +66,13 @@ func Test_Powershell_Execute(t *testing.T) { return exec.NewRunResult(1, "", "error message"), errors.New("error message") }) - PowershellScript := NewPowershellScript(mockContext.CommandRunner, workingDir, env) + PowershellScript := NewPowershellScriptWithMockCheckPath( + mockContext.CommandRunner, + workingDir, + env, + func(options tools.ExecOptions) error { + return nil + }) runResult, err := PowershellScript.Execute( *mockContext.Context, scriptPath, @@ -70,6 +83,19 @@ func Test_Powershell_Execute(t *testing.T) { require.Error(t, err) }) + t.Run("NoPowerShellInstalled", func(t *testing.T) { + mockContext := mocks.NewMockContext(context.Background()) + + PowershellScript := NewPowershellScript(mockContext.CommandRunner, workingDir, env) + _, err := PowershellScript.Execute( + *mockContext.Context, + scriptPath, + tools.ExecOptions{Interactive: to.Ptr(true)}, + ) + + require.Error(t, err) + }) + tests := []struct { name string value tools.ExecOptions @@ -89,7 +115,13 @@ func Test_Powershell_Execute(t *testing.T) { return exec.NewRunResult(0, "", ""), nil }) - PowershellScript := NewPowershellScript(mockContext.CommandRunner, workingDir, env) + PowershellScript := NewPowershellScriptWithMockCheckPath( + mockContext.CommandRunner, + workingDir, + env, + func(options tools.ExecOptions) error { + return nil + }) runResult, err := PowershellScript.Execute(*mockContext.Context, scriptPath, test.value) require.NotNil(t, runResult)