Skip to content

Commit

Permalink
Merge branch 'main' into bmw/promptServer
Browse files Browse the repository at this point in the history
  • Loading branch information
bwateratmsft committed Mar 29, 2024
2 parents eb2c438 + 85f86be commit bba65e3
Show file tree
Hide file tree
Showing 224 changed files with 11,915 additions and 45,547 deletions.
8 changes: 4 additions & 4 deletions .github/workflows/cli-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ jobs:
- uses: actions/setup-go@v3
with:
go-version: "^1.21.0"
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: golangci-lint
uses: golangci/golangci-lint-action@v3
with:
Expand All @@ -33,10 +33,10 @@ jobs:
cspell-lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: "18"
node-version: "20"
- run: npm install -g cspell
- name: Spell check for CLI source code
run: cspell lint '**/*.go' --config ./cli/azd/.vscode/cspell.yaml --root ./cli/azd --no-progress
Expand Down
6 changes: 3 additions & 3 deletions .github/workflows/cspell-misc.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,10 @@ jobs:
cspell-lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: "18"
node-version: "20"
- run: npm install -g cspell
- name: Spell check for general files
run: cspell lint '**/*' --config ./.vscode/cspell.misc.yaml --relative --no-progress
2 changes: 1 addition & 1 deletion .github/workflows/devcontainer-feature-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ jobs:
environment:
name: deploy-devcontainer-feature
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4

- name: "Publish Features"
uses: devcontainers/action@v1
Expand Down
6 changes: 3 additions & 3 deletions .github/workflows/devcontainer-feature-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ jobs:
"mcr.microsoft.com/devcontainers/base:debian",
]
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4

- name: "Install latest devcontainer CLI"
run: npm install -g @devcontainers/cli
Expand All @@ -37,7 +37,7 @@ jobs:
test-scenarios:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4

- name: "Install latest devcontainer CLI"
run: npm install -g @devcontainers/cli
Expand All @@ -49,7 +49,7 @@ jobs:
test-global:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4

- name: "Install latest devcontainer CLI"
run: npm install -g @devcontainers/cli
Expand Down
6 changes: 3 additions & 3 deletions .github/workflows/devops-ext-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,10 @@ jobs:
build-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: "18"
node-version: "20"
- name: Install dependencies
run: |
npm install -g npm
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/lint-bicep.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ jobs:
bicep-lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Lint .bicep files
run: $ErrorActionPreference='Continue'; eng/scripts/Test-BicepLint.ps1 -Verbose
shell: pwsh
4 changes: 2 additions & 2 deletions .github/workflows/schema-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,9 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v3
- uses: actions/setup-node@v4
with:
node-version: "18"
node-version: "20"
- run: npm install -g jsonlint
- name: Validate schemas JSON
run: jsonlint schemas/**/*.json -c
6 changes: 3 additions & 3 deletions .github/workflows/templates-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,10 @@ jobs:
cspell-lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: "18"
node-version: "20"
- run: npm install -g cspell
- name: Spell check for templates
run: cspell lint '**/*' --config ./templates/cspell.yaml --root ./templates --no-progress
12 changes: 6 additions & 6 deletions .github/workflows/vscode-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,10 @@ jobs:
cspell-lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: "18"
node-version: "20"
- run: npm install -g cspell
- name: Spell check for vscode extension
run: cspell lint '**/*.ts' --config ./ext/vscode/.vscode/cspell.yaml --root ./ext/vscode --no-progress
Expand All @@ -28,17 +28,17 @@ jobs:
runs-on: ${{ matrix.os }}
strategy:
matrix:
node-version: [18.x]
node-version: [20.x]
os: [macos-latest, ubuntu-latest, windows-latest]
include:
- os: ubuntu-latest
upload-artifact: true

steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4

- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v3
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}

Expand Down
2 changes: 1 addition & 1 deletion cli/azd/cmd/container.go
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ func registerCommonDependencies(container *ioc.NestedContainer) {
Stdin: cmd.InOrStdin(),
Stdout: cmd.OutOrStdout(),
Stderr: cmd.ErrOrStderr(),
}, formatter)
}, formatter, nil)
})

container.MustRegisterSingleton(
Expand Down
25 changes: 20 additions & 5 deletions cli/azd/cmd/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ type initFlags struct {
subscription string
location string
global *internal.GlobalCommandOptions
fromCode bool
internal.EnvFlag
}

Expand All @@ -63,7 +64,7 @@ func (i *initFlags) Bind(local *pflag.FlagSet, global *internal.GlobalCommandOpt
"t",
"",
//nolint:lll
"The template to use when you initialize the project. You can use Full URI, <owner>/<repository>, or <repository> if it's part of the azure-samples organization.",
"Initializes a new application from a template. You can use Full URI, <owner>/<repository>, or <repository> if it's part of the azure-samples organization.",
)
local.StringVarP(
&i.templateBranch,
Expand All @@ -78,6 +79,13 @@ func (i *initFlags) Bind(local *pflag.FlagSet, global *internal.GlobalCommandOpt
"",
"Name or ID of an Azure subscription to use for the new environment",
)
local.BoolVarP(
&i.fromCode,
"from-code",
"",
false,
"Initializes a new application from your existing code.",
)
local.StringVarP(&i.location, "location", "l", "", "Azure location for the new environment")
i.EnvFlag.Bind(local, global)

Expand Down Expand Up @@ -159,8 +167,15 @@ func (i *initAction) Run(ctx context.Context) (*actions.ActionResult, error) {
initTypeSelect = initAppTemplate
}

if i.flags.templatePath == "" && existingProject {
// no explicit --template, and azure.yaml exists, only initialize environment
if i.flags.fromCode {
if i.flags.templatePath != "" {
return nil, errors.New("only one of init modes: --template, or --from-code should be set")
}
initTypeSelect = initFromApp
}

if i.flags.templatePath == "" && !i.flags.fromCode && existingProject {
// only initialize environment when no mode is set explicitly
initTypeSelect = initEnvironment
}

Expand Down Expand Up @@ -384,8 +399,8 @@ func getCmdInitHelpDescription(*cobra.Command) string {
return generateCmdHelpDescription("Initialize a new application in your current directory.",
[]string{
formatHelpNote(
fmt.Sprintf("Running %s without a template will prompt "+
"you to start with a minimal template or select from a curated list of presets.",
fmt.Sprintf("Running %s without flags specified will prompt "+
"you to initialize using your existing code, or from a template.",
output.WithHighLightFormat("init"),
)),
formatHelpNote(
Expand Down
5 changes: 3 additions & 2 deletions cli/azd/cmd/testdata/TestUsage-azd-init.snap
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@

Initialize a new application in your current directory.

Running init without a template will prompt you to start with a minimal template or select from a curated list of presets.
Running init without flags specified will prompt you to initialize using your existing code, or from a template.
To view all available sample templates, including those submitted by the azd community, visit: https://azure.github.io/awesome-azd.

Usage
Expand All @@ -11,10 +11,11 @@ Flags
-b, --branch string : The template branch to initialize from. Must be used with a template argument (--template or -t).
--docs : Opens the documentation for azd init in your web browser.
-e, --environment string : The name of the environment to use.
--from-code : Initializes a new application from your existing code.
-h, --help : Gets help for init.
-l, --location string : Azure location for the new environment
-s, --subscription string : Name or ID of an Azure subscription to use for the new environment
-t, --template string : The template to use when you initialize the project. You can use Full URI, <owner>/<repository>, or <repository> if it's part of the azure-samples organization.
-t, --template string : Initializes a new application from a template. You can use Full URI, <owner>/<repository>, or <repository> if it's part of the azure-samples organization.

Global Flags
-C, --cwd string : Sets the current working directory.
Expand Down
95 changes: 95 additions & 0 deletions cli/azd/docs/external-prompting.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
# External Prompting

## Problem

During operations, `azd` may need to prompt the user for information. For example, during `init` for an Aspire application, we prompt the user to select which services should be exposed to the Internet. The first time `azd provision` is run for an environment, we ask the user to select an subscription and location. In addition, the IaC we provision for an application may have parameters which `azd` needs to prompt the user for.

Today, this prompting happens via the terminal - we have a set of methods that can be called on `input.Console` that allow different types of prompts:

- `Prompt`: Asks the user for a single (freeform) text value.
- `Select`: Asks the user to pick a single value from a list of options.
- `MultiSelect`: Asks the user to pick zero or more values from a list of options.
- `Confirm`: Asks the user to confirm and operation with a given message.

The implementation of this interface uses a go library to provide a terminal experience (using ANSI escape sequences to provide a nice terminal interaction model) with a fallback to raw text input when the user is not connected to a proper terminal.

This is a reasonable experience for users interacting with `azd` via their terminal. However, `azd` also supports being used within IDEs (today Visual Studio Code, tomorrow Visual Studio as well) and there our terminal based prompting strategy is not ideal. VS Code is forced to run us in an interactive terminal and the user has to interact with `azd` via its terminal interface or specifically craft their calls of `azd` to not prompt the user. In Visual Studio, AZD is run in a background process, so no terminal interaction is possible.

In both cases it would be ideal if `azd` could delegate the prompting behavior back to the caller. This document outlines a solution, which provides a way for an external tool to provide a remote service that `azd` interacts with when it needs to prompt the user for information.

## Solution

Similar to our strategy for delegating authentication to an external host, we support delegating prompting to an external host via a special JSON based REST API, hosted over a local HTTP server. When run, `azd` looks for two special environment variables:

- `AZD_UI_PROMPT_ENDPOINT`
- `AZD_UI_PROMPT_KEY`

When both are set, instead of prompting using the command line - the implementation of our prompting methods now make a POST call to a special endpoint:

`${AZD_UI_PROMPT_ENDPOINT}/prompt?api-version=2024-02-14-preview`

Setting the following headers:

- `Content-Type: application/json`
- `Authorization: Bearer ${AZD_UI_PROMPT_KEY}`

The use of `AZD_UI_PROMPT_KEY` allows the host to block requests coming from other clients on the same machine (since the it is expected the host runs a ephemeral HTTP server listing on `127.0.0.1` on a random port). It is expected that the host will generate a random string and use this as a shared key for the lifetime of an `azd` invocation.

The body of the request contains a JSON object with all the information about the prompt that `azd` needs a response for:

```typescript
interface PromptRequest {
type: "string" | "password" | "directory" | "select" | "multiSelect" | "confirm"
options: {
message: string // the message to be displayed as part of the prompt
help?: string // optional help text that can be displayed upon request
choices?: PromptChoice[]
defaultValue?: string | string[] | boolean
}
}

interface PromptChoice {
value: string
detail?: string
}
```

The `password` type represents a string value which represents a password. The host may want to use a different UI element (perhaps one that uses `***` instead of characters) when prompting.

The server should respond with 200 OK and the body that represents the result:

```typescript
interface PromptResponse {
status: "success" | "cancelled" | "error"

// present when status is "success"
value?: string | string[]

// present when status is "error"
message?: string
}
```

### Success

When the host is able to prompt for the value a response with the `status` of `success` is sent.

When the type is `confirm` the value should be either `"true"` or `"false"` (a string, not a JSON boolean) indicating if the user confirmed the operation (`"true"`) or rejected it (`"false"`). Note that a user rejecting a confirm prompt still results in a `"success"` status (the value is simply `"false"`). In the case of `multiSelect` an array of string values is returned, each individual value is a value from the `choices` array that was selected by the user.

### Cancelled

The user may decline to provide a response (imagine hitting a cancel button on the dialog that is being used to present the question, or a user hitting something like CTRL+C in a terminal interaction to abort the question asking but not the entire application).

In this case the `status` is `cancelled`.

`azd` returns a special `Error` type internally in this case, which up-stack code can use.

### Error

Some error happened during prompting - the status is `error` and the `message` property is a human readable error message that `azd` returns as a go `error`.

Note that an error prompting leads to a successful result at the HTTP layer (200 OK) but with a special error object. `azd` treats other responses as if the server has an internal bug.

## Open Issues

- [ ] Some hosts, such as VS, may want to collect a set of prompts up front and present them all on a single page as part of an end to end - how would we support this? It may be that the answer is "that's a separate API" and this solution is simply focused on "when `azd` it self is driving and end to end workflow".
1 change: 1 addition & 0 deletions cli/azd/internal/repository/detect_confirm_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,7 @@ func Test_detectConfirm_confirm(t *testing.T) {
Stdin: strings.NewReader(strings.Join(tt.interactions, "\n") + "\n"),
Stdout: os.Stdout,
},
nil,
nil),
}
d.Init(tt.detection, dir)
Expand Down
1 change: 1 addition & 0 deletions cli/azd/internal/repository/infra_confirm_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,7 @@ func TestInitializer_infraSpecFromDetect(t *testing.T) {
Stdin: strings.NewReader(strings.Join(tt.interactions, "\n") + "\n"),
Stdout: os.Stdout,
},
nil,
nil),
}

Expand Down
3 changes: 2 additions & 1 deletion cli/azd/internal/repository/initializer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -375,7 +375,8 @@ func Test_Initializer_WriteCoreAssets(t *testing.T) {
envManager := &mockenv.MockEnvManager{}
envManager.On("Save", mock.Anything, mock.Anything).Return(nil)

i := NewInitializer(console, git.NewGitCli(realRunner), nil, lazy.From[environment.Manager](envManager))
i := NewInitializer(
console, git.NewGitCli(realRunner), nil, lazy.From[environment.Manager](envManager))
err := i.writeCoreAssets(context.Background(), azdCtx)
require.NoError(t, err)

Expand Down
15 changes: 1 addition & 14 deletions cli/azd/internal/repository/prompt_util.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,18 +12,6 @@ import (
"github.com/azure/azure-dev/cli/azd/pkg/input"
)

// dirSuggestions provides suggestion completions for directories given the current input directory.
func dirSuggestions(input string) []string {
completions := []string{}
matches, _ := filepath.Glob(input + "*")
for _, match := range matches {
if fs, err := os.Stat(match); err == nil && fs.IsDir() {
completions = append(completions, match)
}
}
return completions
}

// tabWrite transforms tabbed output into formatted strings with a given minimal padding.
// For more information, refer to the tabwriter package.
func tabWrite(selections []string, padding int) ([]string, error) {
Expand All @@ -47,9 +35,8 @@ func promptDir(
console input.Console,
message string) (string, error) {
for {
path, err := console.Prompt(ctx, input.ConsoleOptions{
path, err := console.PromptDir(ctx, input.ConsoleOptions{
Message: message,
Suggest: dirSuggestions,
})
if err != nil {
return "", err
Expand Down
Loading

0 comments on commit bba65e3

Please sign in to comment.