Merge branch 'main' of github.com:Azure/azure-dev into azd-pipeline-config

This commit is contained in:
Victor Vazquez 2024-02-27 05:41:43 +00:00
Родитель 3becf20b5b 7dcdf2d48e
Коммит 71233b738a
593 изменённых файлов: 53168 добавлений и 3558 удалений

Просмотреть файл

@ -29,7 +29,8 @@
"golang.go",
"ms-azuretools.vscode-bicep",
"eamodio.gitlens",
"hashicorp.terraform"
"hashicorp.terraform",
"jinliming2.vscode-go-template"
]
}
}

7
.github/CODEOWNERS поставляемый
Просмотреть файл

@ -7,12 +7,15 @@
/ext/ @karolz-ms @ellismg
/generators/repo/ @wbreza @ellismg @danieljurek @jongio
/generators/repo/ @wbreza @ellismg @danieljurek
/.github/ @danieljurek @ellismg
/eng/ @danieljurek @ellismg
/schemas/ @jongio @karolz-ms @ellismg @wbreza
/schemas/ @karolz-ms @ellismg @wbreza
/templates/ @jongio @wbreza
# Exclude @jongio from version bump PRs opened by dependabot
/templates/**/package-lock.json @wbreza

22
.github/workflows/schema-ci.yml поставляемый Normal file
Просмотреть файл

@ -0,0 +1,22 @@
name: schema-ci
on:
pull_request:
paths:
- "schemas/**"
branches: [main]
permissions:
contents: read
jobs:
schema-lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v3
with:
node-version: "18"
- run: npm install -g jsonlint
- name: Validate schemas JSON
run: jsonlint schemas/**/*.json -c

1
.vscode/cspell-github-user-aliases.txt поставляемый
Просмотреть файл

@ -27,3 +27,4 @@ stretchr
theckman
bmatcuk
tonybaloney
weilim

1
.vscode/cspell.global.yaml поставляемый
Просмотреть файл

@ -56,6 +56,7 @@ ignoreWords:
- conjunction
- containerregistry
- containerservice
- dapr
- databricks
- dedb
- devcenter

Просмотреть файл

@ -1,10 +1,10 @@
FROM --platform=amd64 mcr.microsoft.com/dotnet/sdk:6.0
FROM --platform=amd64 mcr.microsoft.com/dotnet/sdk:8.0
WORKDIR /app
RUN export DEBIAN_FRONTEND=noninteractive \
&& wget -q https://packages.microsoft.com/config/ubuntu/20.04/packages-microsoft-prod.deb && dpkg -i packages-microsoft-prod.deb \
&& apt-get update && apt-get install -y --no-install-recommends apt-utils && apt-get install -y apt-transport-https ca-certificates curl unzip procps gnupg2 software-properties-common lsb-release \
&& apt-get update && apt-get install -y --no-install-recommends apt-utils && apt-get install -y apt-transport-https ca-certificates curl gnupg unzip procps gnupg2 software-properties-common lsb-release \
# functions core tools
&& curl https://packages.microsoft.com/keys/microsoft.asc | gpg --dearmor > microsoft.gpg \
&& mv microsoft.gpg /etc/apt/trusted.gpg.d/microsoft.gpg \
@ -17,17 +17,19 @@ RUN export DEBIAN_FRONTEND=noninteractive \
# az cli
&& curl -sSL https://aka.ms/InstallAzureCLIDeb | bash \
# nodejs
&& curl -fsSL https://deb.nodesource.com/setup_18.x | bash - \
&& mkdir -p /etc/apt/keyrings \
&& curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg \
&& echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_18.x nodistro main" | tee /etc/apt/sources.list.d/nodesource.list \
&& apt-get update \
&& apt-get install -y nodejs \
# python
&& apt-get install -y --no-install-recommends python3-pip python-dev python3-venv \
&& python3 -m pip install --upgrade pip \
&& apt-get install -y --no-install-recommends python3-pip python3-venv \
&& echo 'alias python=python3' >> ~/.bashrc \
&& echo 'alias pip=pip3' >> ~/.bashrc \
# java
&& apt-get update && apt-get install -y msopenjdk-17 \
# terraform cli
&& apt-get update && apt-get install -y gnupg software-properties-common \
&& apt-get update && apt-get install -y software-properties-common \
&& wget -O- https://apt.releases.hashicorp.com/gpg | \
gpg --dearmor | \
tee /usr/share/keyrings/hashicorp-archive-keyring.gpg \

2
cli/azd/.gitignore поставляемый
Просмотреть файл

@ -5,3 +5,5 @@ azd-record.exe
build
resource.syso
versioninfo.json
azd.sln

23
cli/azd/.vscode/cspell-azd-dictionary.txt поставляемый
Просмотреть файл

@ -5,6 +5,7 @@ aiopg
alphafeatures
apimanagement
apims
apiservice
appconfiguration
appdetect
apphost
@ -19,8 +20,12 @@ armapimanagement
armappconfiguration
armappplatform
armcognitiveservices
asyncmy
armcosmos
armresourcegraph
armsql
aspnet
aspnetcore
asyncmy
asyncpg
azapi
AZCLI
@ -52,9 +57,10 @@ buildpacks
byoi
cflags
circleci
cmdsubst
cmdrecord
cmdsubst
cognitiveservices
conditionalize
consolesize
containerapp
containerapps
@ -70,6 +76,7 @@ devcentersdk
devel
discarder
docf
dockerfiles
dockerproject
doublestar
dskip
@ -96,9 +103,10 @@ hotspot
iidfile
ineffassign
javac
jquery
jmes
jquery
keychain
kubelogin
LASTEXITCODE
ldflags
lechnerc77
@ -113,8 +121,8 @@ mockarmresources
mockazcli
mongojs
mvnw
mysqldb
mysqlclient
mysqldb
nobanner
nodeapp
nolint
@ -130,6 +138,7 @@ otlp
otlpconfig
otlptrace
otlptracehttp
overriden
paketobuildpacks
pflag
posix
@ -147,10 +156,10 @@ reauthentication
relogin
remarshal
repourl
requirepass
resourcegraph
restoreapp
retriable
requirepass
rzip
secureobject
securestring
@ -178,12 +187,16 @@ tracetest
trafficmanager
Truef
typeflag
unhide
unmarshaled
upgrader
unmarshalling
unsetenvs
unsets
utsname
vsrpc
vuejs
webfrontend
westus2
wireinject
yacspin

7
cli/azd/.vscode/cspell.yaml поставляемый
Просмотреть файл

@ -71,6 +71,9 @@ overrides:
- keychain
- crusername
- azurecr
- filename: pkg/tools/dotnet/dotnet.go
words:
- PWORD
- filename: pkg/tools/kubectl/kubectl.go
words:
- tmpl
@ -85,6 +88,10 @@ overrides:
- filename: pkg/azsdk/storage/storage_blob_client.go
words:
- azblob
- filename: pkg/project/service_target_aks.go
words:
- kustomization
- templating
ignorePaths:
- "**/*_test.go"
- "**/mock*.go"

3
cli/azd/.vscode/extensions.json поставляемый
Просмотреть файл

@ -4,6 +4,7 @@
"recommendations": [
"golang.go",
"streetsidesoftware.code-spell-checker",
"redhat.vscode-yaml"
"redhat.vscode-yaml",
"jinliming2.vscode-go-template"
]
}

5
cli/azd/.vscode/settings.json поставляемый
Просмотреть файл

@ -7,5 +7,8 @@
"go.lintFlags": ["--fast"],
"go.lintOnSave": "package",
"go.lintTool": "golangci-lint",
"go.testTimeout": "10m"
"go.testTimeout": "10m",
"files.associations": {
"*.bicept": "go-template"
}
}

Просмотреть файл

@ -1,6 +1,6 @@
# Release History
## 1.6.0-beta.1 (Unreleased)
## 1.7.0-beta.1 (Unreleased)
### Features Added
@ -10,6 +10,62 @@
### Other Changes
## 1.6.1 (2024-02-15)
### Bugs Fixed
- [[3375]](https://github.com/Azure/azure-dev/pull/3375) Fixes issues deploying to AKS service targets
- [[3373]](https://github.com/Azure/azure-dev/pull/3373) Fixes resolution of AZD compatible templates within azure dev center catalogs
- [[3372]](https://github.com/Azure/azure-dev/pull/3372) Removes requirement for dev center projects to include an `infra` folder
## 1.6.0 (2024-02-13)
### Features Added
- [[3269]](https://github.com/Azure/azure-dev/pull/3269) Adds support for external/prebuilt container image references
- [[3251]](https://github.com/Azure/azure-dev/pull/3251) Adds additional configuration resolving container registry names
- [[3249]](https://github.com/Azure/azure-dev/pull/3249) Adds additional configuration resolving AKS cluster names
- [[3223]](https://github.com/Azure/azure-dev/pull/3223) Updates AKS core modules for `azd` to easily enable RBAC clusters
- [[3211]](https://github.com/Azure/azure-dev/pull/3211) Adds support for RBAC enabled AKS clusters using `kubelogin`
- [[3196]](https://github.com/Azure/azure-dev/pull/3196) Adds support for Helm and Kustomize for AKS service targets
- [[3173]](https://github.com/Azure/azure-dev/pull/3173) Adds support for defining customizable `azd up` workflows
- Dotnet Aspire additions:
- [[3164]](https://github.com/Azure/azure-dev/pull/3164) Azure Cosmos DB.
- [[3226]](https://github.com/Azure/azure-dev/pull/3226) Azure SQL Database.
- [[3276]](https://github.com/Azure/azure-dev/pull/3276) Secrets handling improvement.
- [[3155]](https://github.com/Azure/azure-dev/pull/3155) Adds support to define secrets and variables for `azd pipeline config`.
### Bugs Fixed
- [[3097]](https://github.com/Azure/azure-dev/pull/3097) For Dotnet Aspire projects, do not fail if folder `infra` is empty.
## 1.5.1 (2023-12-20)
### Features Added
- [[2998]](https://github.com/Azure/azure-dev/pull/2998) Adds support for Azure Storage Tables and Queues on Aspire projects.
- [[3052]](https://github.com/Azure/azure-dev/pull/3052) Adds `target` argument support for docker build.
- [[2488]](https://github.com/Azure/azure-dev/pull/2488) Adds support to override behavior of the KUBECONFIG environment variable on AKS.
- [[3075]](https://github.com/Azure/azure-dev/pull/3075) Adds support for `dockerfile.v0` on Aspire projects.
- [[2992]](https://github.com/Azure/azure-dev/pull/2992) Adds support for `dapr` on Aspire projects.
### Bugs Fixed
- [[2969]](https://github.com/Azure/azure-dev/pull/2969) Relax container names truncation logic for Aspire `redis.v0` and `postgres.database.v0`.
Truncation now happens above 30 characters instead of 12 characters.
- [[3035]](https://github.com/Azure/azure-dev/pull/3035) .NET Aspire issues after `azd pipeline config`.
- [[3038]](https://github.com/Azure/azure-dev/pull/3038) Fix init to not consider parent directories.
- [[3045]](https://github.com/Azure/azure-dev/pull/3045) Handle interrupt to unhide cursor.
- [[3069]](https://github.com/Azure/azure-dev/pull/3069) .NET Aspire, enable `admin user` for ACR.
- [[3049]](https://github.com/Azure/azure-dev/pull/3049) Persist location from provisioning manager.
- [[3056]](https://github.com/Azure/azure-dev/pull/3056) Fix `azd pipeline config` for resource group deployment.
- [[3106]](https://github.com/Azure/azure-dev/pull/3106) Fix `azd restore` on .NET projects.
- [[3041]](https://github.com/Azure/azure-dev/pull/3041) Ensure azd environment name is synchronized to .env file.
### Other Changes
- [[3044]](https://github.com/Azure/azure-dev/pull/3044) Sets allowInsecure to true for internal services on Aspire projects.
## 1.5.0 (2023-11-15)
### Features Added
@ -653,6 +709,7 @@ We plan to improve this behavior with [[#1126]](https://github.com/Azure/azure-d
- [[#115]](https://github.com/Azure/azure-dev/issues/115) Fix deploy error when using a resource name with capital letters.
### Other Changes
- [[#188]](https://github.com/Azure/azure-dev/issues/188) Update the minimum Bicep version to `v0.8.9`.
## 0.1.0-beta.2 (2022-07-13)

Просмотреть файл

@ -71,7 +71,7 @@ $tagsFlag = "-tags=cfi,cfg,osusergo"
# -s: Omit symbol table and debug information
# -w: Omit DWARF symbol table
# -X: Set variable at link time. Used to set the version in source.
$ldFlag = "-ldflags=`"-s -w -X 'github.com/azure/azure-dev/cli/azd/internal.Version=$Version (commit $SourceVersion)' "
$ldFlag = "-ldflags=-s -w -X 'github.com/azure/azure-dev/cli/azd/internal.Version=$Version (commit $SourceVersion)' "
if ($IsWindows) {
Write-Host "Building for windows"
@ -84,7 +84,7 @@ if ($IsWindows) {
"-trimpath",
$tagsFlag,
# -extldflags=-Wl,--high-entropy-va: Pass the high-entropy VA flag to the linker to enable high entropy virtual addresses
($ldFlag + "-linkmode=auto -extldflags=-Wl,--high-entropy-va`"")
($ldFlag + "-linkmode=auto -extldflags=-Wl,--high-entropy-va")
)
}
elseif ($IsLinux) {
@ -93,7 +93,7 @@ elseif ($IsLinux) {
"-buildmode=pie",
($tagsFlag + ",cfgo"),
# -extldflags=-Wl,--high-entropy-va: Pass the high-entropy VA flag to the linker to enable high entropy virtual addresses
($ldFlag + "-extldflags=-Wl,--high-entropy-va`"")
($ldFlag + "-extldflags=-Wl,--high-entropy-va")
)
}
elseif ($IsMacOS) {
@ -102,7 +102,7 @@ elseif ($IsMacOS) {
"-buildmode=pie",
($tagsFlag + ",cfgo"),
# -linkmode=auto: Link Go object files and C object files together
($ldFlag + "-linkmode=auto`"")
($ldFlag + "-linkmode=auto")
)
}
@ -116,7 +116,7 @@ function PrintFlags() {
foreach ($buildFlag in $buildFlags) {
# If the flag has a value, wrap it in quotes. This is not required when invoking directly below,
# but when repasted into a shell for execution, the quotes can help escape special characters such as ','.
$argWithValue = $buildFlag -split "="
$argWithValue = $buildFlag.Split('=', 2)
if ($argWithValue.Length -eq 2 -and !$argWithValue[1].StartsWith("`"")) {
$buildFlag = "$($argWithValue[0])=`"$($argWithValue[1])`""
}

Просмотреть файл

@ -34,6 +34,3 @@ type Action interface {
// It is currently valid to both return an error and a non-nil ActionResult.
Run(ctx context.Context) (*ActionResult, error)
}
// A function that lazily returns the specified action type T
type ActionInitializer[T Action] func() (T, error)

Просмотреть файл

@ -473,8 +473,16 @@ func (la *loginAction) login(ctx context.Context) error {
if useDevCode {
_, err := la.authManager.LoginWithDeviceCode(ctx, la.flags.tenantID, la.flags.scopes, func(url string) error {
if !la.flags.global.NoPrompt {
la.console.Message(ctx, "Then press enter and continue to log in from your browser...")
la.console.WaitForEnter()
openWithDefaultBrowser(ctx, la.console, url)
return nil
}
// For no-prompt, Just provide instructions without trying to open the browser
// If manual browsing is enabled, we don't want to open the browser automatically
la.console.Message(ctx, fmt.Sprintf("Then, go to: %s", url))
return nil
})
if err != nil {
return fmt.Errorf("logging in: %w", err)

Просмотреть файл

@ -97,7 +97,7 @@ func getTenantIdFromAzdEnv(
if err != nil {
return tenantId, fmt.Errorf(
"resolving the Azure Directory from azd environment (%s): %w",
azdEnv.GetEnvName(),
azdEnv.Name(),
err)
}

Просмотреть файл

@ -7,19 +7,19 @@ import (
"time"
"github.com/azure/azure-dev/cli/azd/cmd/actions"
"github.com/azure/azure-dev/cli/azd/cmd/middleware"
"github.com/azure/azure-dev/cli/azd/internal"
"github.com/azure/azure-dev/cli/azd/pkg/environment/azdcontext"
"github.com/azure/azure-dev/cli/azd/pkg/input"
"github.com/azure/azure-dev/cli/azd/pkg/output"
"github.com/azure/azure-dev/cli/azd/pkg/output/ux"
"github.com/azure/azure-dev/cli/azd/pkg/project"
"github.com/azure/azure-dev/cli/azd/pkg/workflow"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
)
type buildFlags struct {
*envFlag
*internal.EnvFlag
all bool
global *internal.GlobalCommandOptions
only bool
@ -27,7 +27,7 @@ type buildFlags struct {
func newBuildFlags(cmd *cobra.Command, global *internal.GlobalCommandOptions) *buildFlags {
flags := &buildFlags{
envFlag: &envFlag{},
EnvFlag: &internal.EnvFlag{},
}
flags.Bind(cmd.Flags(), global)
@ -36,7 +36,7 @@ func newBuildFlags(cmd *cobra.Command, global *internal.GlobalCommandOptions) *b
}
func (bf *buildFlags) Bind(local *pflag.FlagSet, global *internal.GlobalCommandOptions) {
bf.envFlag.Bind(local, global)
bf.EnvFlag.Bind(local, global)
bf.global = global
local.BoolVar(
@ -67,8 +67,7 @@ type buildAction struct {
console input.Console
formatter output.Formatter
writer io.Writer
middlewareRunner middleware.MiddlewareContext
restoreActionInitializer actions.ActionInitializer[*restoreAction]
workflowRunner *workflow.Runner
}
func newBuildAction(
@ -81,8 +80,7 @@ func newBuildAction(
console input.Console,
formatter output.Formatter,
writer io.Writer,
middlewareRunner middleware.MiddlewareContext,
restoreActionInitializer actions.ActionInitializer[*restoreAction],
workflowRunner *workflow.Runner,
) actions.Action {
return &buildAction{
@ -94,9 +92,8 @@ func newBuildAction(
console: console,
formatter: formatter,
writer: writer,
middlewareRunner: middlewareRunner,
restoreActionInitializer: restoreActionInitializer,
importManager: importManager,
workflowRunner: workflowRunner,
}
}
@ -106,17 +103,22 @@ type BuildResult struct {
}
func (ba *buildAction) Run(ctx context.Context) (*actions.ActionResult, error) {
// When the --only flag is NOT specified, we need to restore the project before building it.
if !ba.flags.only {
restoreAction, err := ba.restoreActionInitializer()
restoreAction.flags.all = ba.flags.all
restoreAction.args = ba.args
if err != nil {
return nil, err
restoreArgs := []string{"restore"}
restoreArgs = append(restoreArgs, ba.args...)
if ba.flags.all {
restoreArgs = append(restoreArgs, "--all")
}
buildOptions := &middleware.Options{CommandPath: "restore"}
_, err = ba.middlewareRunner.RunChildAction(ctx, buildOptions, restoreAction)
if err != nil {
// We restore the project by running a workflow that contains a restore command
workflow := &workflow.Workflow{
Steps: []*workflow.Step{
workflow.NewAzdCommandStep(restoreArgs...),
},
}
if err := ba.workflowRunner.Run(ctx, workflow); err != nil {
return nil, err
}
}

Просмотреть файл

@ -31,10 +31,10 @@ type CobraBuilder struct {
}
// Creates a new instance of the Cobra builder
func NewCobraBuilder(container *ioc.NestedContainer) *CobraBuilder {
func NewCobraBuilder(container *ioc.NestedContainer, runner *middleware.MiddlewareRunner) *CobraBuilder {
return &CobraBuilder{
container: container,
runner: middleware.NewMiddlewareRunner(container),
runner: runner,
}
}
@ -101,22 +101,31 @@ func (cb *CobraBuilder) configureActionResolver(cmd *cobra.Command, descriptor *
}
cmd.RunE = func(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()
ctx = tools.WithInstalledCheckCache(ctx)
// Registers the following to enable injection into actions that require them
ioc.RegisterInstance(cb.container, cb.runner)
ioc.RegisterInstance(cb.container, middleware.MiddlewareContext(cb.runner))
ioc.RegisterInstance(cb.container, cmd)
ioc.RegisterInstance(cb.container, args)
// Register root go context that will be used for resolving singleton dependencies
ctx := tools.WithInstalledCheckCache(cmd.Context())
ioc.RegisterInstance(cb.container, ctx)
// Register any required middleware registered for the current action descriptor
if err := cb.registerMiddleware(descriptor); err != nil {
return err
}
// Create new container scope for the current command
cmdContainer, err := cb.container.NewScope()
if err != nil {
return fmt.Errorf("failed creating new scope for command, %w", err)
}
// Registers the following to enable injection into actions that require them
ioc.RegisterInstance(cmdContainer, ctx)
ioc.RegisterInstance(cmdContainer, cmd)
ioc.RegisterInstance(cmdContainer, args)
ioc.RegisterInstance(cmdContainer, cmdContainer)
ioc.RegisterInstance[ioc.ServiceLocator](cmdContainer, cmdContainer)
actionName := createActionName(cmd)
var action actions.Action
if err := cb.container.ResolveNamed(actionName, &action); err != nil {
if err := cmdContainer.ResolveNamed(actionName, &action); err != nil {
if errors.Is(err, ioc.ErrResolveInstance) {
return fmt.Errorf(
//nolint:lll
@ -137,15 +146,16 @@ func (cb *CobraBuilder) configureActionResolver(cmd *cobra.Command, descriptor *
Args: args,
}
// Set the container that should be used for resolving middleware components
runOptions.WithContainer(cmdContainer)
// Run the middleware chain with action
log.Printf("Resolved action '%s'\n", actionName)
actionResult, err := cb.runner.RunAction(ctx, runOptions, action)
// At this point, we know that there might be an error, so we can silence cobra from showing it after us.
cmd.SilenceErrors = true
// TODO: Consider refactoring to move the UX writing to a middleware
invokeErr := cb.container.Invoke(func(console input.Console) {
invokeErr := cmdContainer.Invoke(func(console input.Console) {
var displayResult *ux.ActionResult
if actionResult != nil && actionResult.Message != nil {
displayResult = &ux.ActionResult{
@ -313,7 +323,7 @@ func (cb *CobraBuilder) bindCommand(cmd *cobra.Command, descriptor *actions.Acti
// These functions are typically the constructor function for the action. ex) newDeployAction(...)
// Action resolvers can take any number of dependencies and instantiated via the IoC container
if descriptor.Options.ActionResolver != nil {
if err := cb.container.RegisterNamedSingleton(actionName, descriptor.Options.ActionResolver); err != nil {
if err := cb.container.RegisterNamedTransient(actionName, descriptor.Options.ActionResolver); err != nil {
return fmt.Errorf(
//nolint:lll
"failed registering ActionResolver for action '%s'. Ensure the resolver is a valid go function and resolves without error. %w",

Просмотреть файл

@ -35,7 +35,7 @@ func Test_BuildAndRunSimpleCommand(t *testing.T) {
},
})
builder := NewCobraBuilder(container)
builder := NewCobraBuilder(container, middleware.NewMiddlewareRunner(container))
cmd, err := builder.BuildCommand(root)
require.NotNil(t, cmd)
@ -58,7 +58,7 @@ func Test_BuildAndRunSimpleAction(t *testing.T) {
FlagsResolver: newTestFlags,
})
builder := NewCobraBuilder(container)
builder := NewCobraBuilder(container, middleware.NewMiddlewareRunner(container))
cmd, err := builder.BuildCommand(root)
require.NotNil(t, cmd)
@ -79,7 +79,7 @@ func Test_BuildAndRunSimpleActionWithMiddleware(t *testing.T) {
FlagsResolver: newTestFlags,
}).UseMiddleware("A", newTestMiddlewareA)
builder := NewCobraBuilder(container)
builder := NewCobraBuilder(container, middleware.NewMiddlewareRunner(container))
cmd, err := builder.BuildCommand(root)
require.NotNil(t, cmd)
@ -112,7 +112,7 @@ func Test_BuildAndRunActionWithNestedMiddleware(t *testing.T) {
FlagsResolver: newTestFlags,
}).UseMiddleware("B", newTestMiddlewareB)
builder := NewCobraBuilder(container)
builder := NewCobraBuilder(container, middleware.NewMiddlewareRunner(container))
cmd, err := builder.BuildCommand(root)
require.NotNil(t, cmd)
@ -154,7 +154,7 @@ func Test_BuildAndRunActionWithNestedAndConditionalMiddleware(t *testing.T) {
return false
})
builder := NewCobraBuilder(container)
builder := NewCobraBuilder(container, middleware.NewMiddlewareRunner(container))
cmd, err := builder.BuildCommand(root)
require.NotNil(t, cmd)
@ -191,7 +191,7 @@ func Test_BuildCommandsWithAutomaticHelpAndOutputFlags(t *testing.T) {
},
})
cobraBuilder := NewCobraBuilder(container)
cobraBuilder := NewCobraBuilder(container, middleware.NewMiddlewareRunner(container))
cmd, err := cobraBuilder.BuildCommand(root)
require.NoError(t, err)
@ -220,7 +220,7 @@ func Test_BuildCommandsWithAutomaticHelpAndOutputFlags(t *testing.T) {
func Test_RunDocsFlow(t *testing.T) {
container := ioc.NewNestedContainer(nil)
testCtx := mocks.NewMockContext(context.Background())
container.RegisterSingleton(func() input.Console {
container.MustRegisterSingleton(func() input.Console {
return testCtx.Console
})
@ -239,7 +239,7 @@ func Test_RunDocsFlow(t *testing.T) {
calledUrl = url
}
cobraBuilder := NewCobraBuilder(container)
cobraBuilder := NewCobraBuilder(container, middleware.NewMiddlewareRunner(container))
cmd, err := cobraBuilder.BuildCommand(root)
require.NoError(t, err)
@ -254,7 +254,7 @@ func Test_RunDocsFlow(t *testing.T) {
func Test_RunDocsAndHelpFlow(t *testing.T) {
container := ioc.NewNestedContainer(nil)
testCtx := mocks.NewMockContext(context.Background())
container.RegisterSingleton(func() input.Console {
container.MustRegisterSingleton(func() input.Console {
return testCtx.Console
})
@ -273,7 +273,7 @@ func Test_RunDocsAndHelpFlow(t *testing.T) {
calledUrl = url
}
cobraBuilder := NewCobraBuilder(container)
cobraBuilder := NewCobraBuilder(container, middleware.NewMiddlewareRunner(container))
cmd, err := cobraBuilder.BuildCommand(root)
require.NoError(t, err)

Просмотреть файл

@ -4,6 +4,7 @@ import (
"context"
"fmt"
"io"
"log"
"os"
"strings"
@ -11,7 +12,9 @@ import (
"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resourcegraph/armresourcegraph"
"github.com/MakeNowJust/heredoc/v2"
"github.com/azure/azure-dev/cli/azd/cmd/actions"
"github.com/azure/azure-dev/cli/azd/cmd/middleware"
"github.com/azure/azure-dev/cli/azd/internal"
"github.com/azure/azure-dev/cli/azd/internal/cmd"
"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/alpha"
@ -25,11 +28,15 @@ import (
"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/pipeline"
@ -49,38 +56,28 @@ import (
"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/mattn/go-colorable"
"github.com/mattn/go-isatty"
"github.com/spf13/cobra"
"golang.org/x/exp/slices"
)
// Registers a singleton action initializer for the specified action name
// Registers a transient action initializer for the specified action name
// This returns a function that when called resolves the action
// This is to ensure pre-conditions are met for composite actions like 'up'
// This finds the action for a named instance and casts it to the correct type for injection
func registerAction[T actions.Action](container *ioc.NestedContainer, actionName string) {
container.RegisterSingleton(func() (T, error) {
return resolveAction[T](container, actionName)
})
}
// Registers a singleton action for the specified action name
// This finds the action for a named instance and casts it to the correct type for injection
func registerActionInitializer[T actions.Action](container *ioc.NestedContainer, actionName string) {
container.RegisterSingleton(func() actions.ActionInitializer[T] {
return func() (T, error) {
return resolveAction[T](container, actionName)
}
container.MustRegisterTransient(func(serviceLocator ioc.ServiceLocator) (T, error) {
return resolveAction[T](serviceLocator, actionName)
})
}
// Resolves the action instance for the specified action name
// This finds the action for a named instance and casts it to the correct type for injection
func resolveAction[T actions.Action](container *ioc.NestedContainer, actionName string) (T, error) {
func resolveAction[T actions.Action](serviceLocator ioc.ServiceLocator, actionName string) (T, error) {
var zero T
var action actions.Action
err := container.ResolveNamed(actionName, &action)
err := serviceLocator.ResolveNamed(actionName, &action)
if err != nil {
return zero, err
}
@ -95,9 +92,15 @@ func resolveAction[T actions.Action](container *ioc.NestedContainer, actionName
// Registers common Azd dependencies
func registerCommonDependencies(container *ioc.NestedContainer) {
container.RegisterSingleton(output.GetCommandFormatter)
// Core bootstrapping registrations
ioc.RegisterInstance(container, container)
container.MustRegisterSingleton(NewCobraBuilder)
container.MustRegisterSingleton(middleware.NewMiddlewareRunner)
container.RegisterSingleton(func(
// Standard Registrations
container.MustRegisterTransient(output.GetCommandFormatter)
container.MustRegisterScoped(func(
rootOptions *internal.GlobalCommandOptions,
formatter output.Formatter,
cmd *cobra.Command) input.Console {
@ -112,8 +115,7 @@ func registerCommonDependencies(container *ioc.NestedContainer) {
}
isTerminal := cmd.OutOrStdout() == os.Stdout &&
cmd.InOrStdin() == os.Stdin && isatty.IsTerminal(os.Stdin.Fd()) &&
isatty.IsTerminal(os.Stdout.Fd())
cmd.InOrStdin() == os.Stdin && input.IsTerminal(os.Stdout.Fd(), os.Stdin.Fd())
return input.NewConsole(rootOptions.NoPrompt, isTerminal, writer, input.ConsoleHandles{
Stdin: cmd.InOrStdin(),
@ -122,7 +124,8 @@ func registerCommonDependencies(container *ioc.NestedContainer) {
}, formatter)
})
container.RegisterSingleton(func(console input.Console, rootOptions *internal.GlobalCommandOptions) exec.CommandRunner {
container.MustRegisterSingleton(
func(console input.Console, rootOptions *internal.GlobalCommandOptions) exec.CommandRunner {
return exec.NewCommandRunner(
&exec.RunnerOptions{
Stdin: console.Handles().Stdin,
@ -130,23 +133,24 @@ func registerCommonDependencies(container *ioc.NestedContainer) {
Stderr: console.Handles().Stderr,
DebugLogging: rootOptions.EnableDebugLogging,
})
})
},
)
client := createHttpClient()
container.RegisterSingleton(func() httputil.HttpClient { return client })
container.RegisterSingleton(func() auth.HttpClient { return client })
container.RegisterSingleton(func() httputil.UserAgent {
ioc.RegisterInstance[httputil.HttpClient](container, client)
ioc.RegisterInstance[auth.HttpClient](container, client)
container.MustRegisterSingleton(func() httputil.UserAgent {
return httputil.UserAgent(internal.UserAgent())
})
// Auth
container.RegisterSingleton(auth.NewLoggedInGuard)
container.RegisterSingleton(auth.NewMultiTenantCredentialProvider)
container.RegisterSingleton(func(mgr *auth.Manager) CredentialProviderFn {
container.MustRegisterSingleton(auth.NewLoggedInGuard)
container.MustRegisterSingleton(auth.NewMultiTenantCredentialProvider)
container.MustRegisterSingleton(func(mgr *auth.Manager) CredentialProviderFn {
return mgr.CredentialForCurrentUser
})
container.RegisterSingleton(func(console input.Console) io.Writer {
container.MustRegisterSingleton(func(console input.Console) io.Writer {
writer := console.Handles().Stdout
if os.Getenv("NO_COLOR") != "" {
@ -156,24 +160,30 @@ func registerCommonDependencies(container *ioc.NestedContainer) {
return writer
})
container.RegisterSingleton(func(cmd *cobra.Command) envFlag {
envValue, err := cmd.Flags().GetString(environmentNameFlag)
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 {
panic("command asked for envFlag, but envFlag was not included in cmd.Flags().")
log.Printf("'%s'command asked for envFlag, but envFlag was not included in cmd.Flags().", cmd.CommandPath())
envValue = ""
}
return envFlag{environmentName: envValue}
return internal.EnvFlag{EnvironmentName: envValue}
})
container.RegisterSingleton(func(cmd *cobra.Command) CmdAnnotations {
container.MustRegisterSingleton(func(cmd *cobra.Command) CmdAnnotations {
return cmd.Annotations
})
// Azd Context
container.RegisterSingleton(azdcontext.NewAzdContext)
container.MustRegisterSingleton(azdcontext.NewAzdContext)
// Lazy loads the Azd context after the azure.yaml file becomes available
container.RegisterSingleton(func() *lazy.Lazy[*azdcontext.AzdContext] {
container.MustRegisterSingleton(func() *lazy.Lazy[*azdcontext.AzdContext] {
return lazy.NewLazy(func() (*azdcontext.AzdContext, error) {
return azdcontext.NewAzdContext()
})
@ -182,21 +192,21 @@ func registerCommonDependencies(container *ioc.NestedContainer) {
// 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.RegisterSingleton(
container.MustRegisterScoped(
func(ctx context.Context,
azdContext *azdcontext.AzdContext,
envManager environment.Manager,
lazyEnv *lazy.Lazy[*environment.Environment],
envFlags envFlag,
envFlags internal.EnvFlag,
) (*environment.Environment, error) {
if azdContext == nil {
return nil, azdcontext.ErrNoProject
}
environmentName := envFlags.environmentName
environmentName := envFlags.EnvironmentName
var err error
env, err := envManager.LoadOrCreateInteractive(ctx, environmentName)
env, err := envManager.LoadOrInitInteractive(ctx, environmentName)
if err != nil {
return nil, fmt.Errorf("loading environment: %w", err)
}
@ -208,7 +218,7 @@ func registerCommonDependencies(container *ioc.NestedContainer) {
return env, nil
},
)
container.RegisterSingleton(func(lazyEnvManager *lazy.Lazy[environment.Manager]) environment.EnvironmentResolver {
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 {
@ -229,13 +239,13 @@ func registerCommonDependencies(container *ioc.NestedContainer) {
}
})
container.RegisterSingleton(environment.NewLocalFileDataStore)
container.RegisterSingleton(environment.NewManager)
container.MustRegisterSingleton(environment.NewLocalFileDataStore)
container.MustRegisterSingleton(environment.NewManager)
container.RegisterSingleton(func() *lazy.Lazy[environment.LocalDataStore] {
container.MustRegisterSingleton(func(serviceLocator ioc.ServiceLocator) *lazy.Lazy[environment.LocalDataStore] {
return lazy.NewLazy(func() (environment.LocalDataStore, error) {
var localDataStore environment.LocalDataStore
err := container.Resolve(&localDataStore)
err := serviceLocator.Resolve(&localDataStore)
if err != nil {
return nil, err
}
@ -245,7 +255,8 @@ func registerCommonDependencies(container *ioc.NestedContainer) {
})
// Environment manager depends on azd context
container.RegisterSingleton(func(azdContext *lazy.Lazy[*azdcontext.AzdContext]) *lazy.Lazy[environment.Manager] {
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 {
@ -256,16 +267,17 @@ func registerCommonDependencies(container *ioc.NestedContainer) {
ioc.RegisterInstance(container, azdCtx)
var envManager environment.Manager
err = container.Resolve(&envManager)
err = serviceLocator.Resolve(&envManager)
if err != nil {
return nil, err
}
return envManager, nil
})
})
},
)
container.RegisterSingleton(func(
container.MustRegisterSingleton(func(
lazyProjectConfig *lazy.Lazy[*project.ProjectConfig],
userConfigManager config.UserConfigManager,
) (*state.RemoteConfig, error) {
@ -296,12 +308,12 @@ func registerCommonDependencies(container *ioc.NestedContainer) {
// Lazy loads an existing environment, erroring out if not available
// One can repeatedly call GetValue to wait until the environment is available.
container.RegisterSingleton(
container.MustRegisterScoped(
func(
ctx context.Context,
lazyEnvManager *lazy.Lazy[environment.Manager],
lazyAzdContext *lazy.Lazy[*azdcontext.AzdContext],
envFlags envFlag,
envFlags internal.EnvFlag,
) *lazy.Lazy[*environment.Environment] {
return lazy.NewLazy(func() (*environment.Environment, error) {
azdCtx, err := lazyAzdContext.GetValue()
@ -309,7 +321,7 @@ func registerCommonDependencies(container *ioc.NestedContainer) {
return nil, err
}
environmentName := envFlags.environmentName
environmentName := envFlags.EnvironmentName
if environmentName == "" {
environmentName, err = azdCtx.GetDefaultEnvironmentName()
if err != nil {
@ -333,7 +345,9 @@ func registerCommonDependencies(container *ioc.NestedContainer) {
)
// Project Config
container.RegisterSingleton(
// Required to be singleton (shared) because the project/service holds important event handlers
// from both hooks and internal that are used during azd lifecycle calls.
container.MustRegisterSingleton(
func(ctx context.Context, azdContext *azdcontext.AzdContext) (*project.ProjectConfig, error) {
if azdContext == nil {
return nil, azdcontext.ErrNoProject
@ -349,7 +363,13 @@ func registerCommonDependencies(container *ioc.NestedContainer) {
)
// Lazy loads the project config from the Azd Context when it becomes available
container.RegisterSingleton(func(lazyAzdContext *lazy.Lazy[*azdcontext.AzdContext]) *lazy.Lazy[*project.ProjectConfig] {
// Required to be singleton (shared) because the project/service holds important event handlers
// from both hooks and internal that are used during azd lifecycle calls.
container.MustRegisterSingleton(
func(
serviceLocator ioc.ServiceLocator,
lazyAzdContext *lazy.Lazy[*azdcontext.AzdContext],
) *lazy.Lazy[*project.ProjectConfig] {
return lazy.NewLazy(func() (*project.ProjectConfig, error) {
_, err := lazyAzdContext.GetValue()
if err != nil {
@ -357,13 +377,14 @@ func registerCommonDependencies(container *ioc.NestedContainer) {
}
var projectConfig *project.ProjectConfig
err = container.Resolve(&projectConfig)
err = serviceLocator.Resolve(&projectConfig)
return projectConfig, err
})
})
},
)
container.RegisterSingleton(func(
container.MustRegisterSingleton(func(
ctx context.Context,
credential azcore.TokenCredential,
httpClient httputil.HttpClient,
@ -375,60 +396,66 @@ func registerCommonDependencies(container *ioc.NestedContainer) {
return armresourcegraph.NewClient(credential, options)
})
container.RegisterSingleton(templates.NewTemplateManager)
container.RegisterSingleton(templates.NewSourceManager)
container.RegisterSingleton(project.NewResourceManager)
container.RegisterSingleton(func() *lazy.Lazy[project.ResourceManager] {
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 := container.Resolve(&resourceManager)
err := serviceLocator.Resolve(&resourceManager)
return resourceManager, err
})
})
container.RegisterSingleton(project.NewProjectManager)
container.RegisterSingleton(project.NewDotNetImporter)
container.RegisterSingleton(project.NewImportManager)
container.RegisterSingleton(project.NewServiceManager)
container.RegisterSingleton(func() *lazy.Lazy[project.ServiceManager] {
container.MustRegisterSingleton(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 := container.Resolve(&serviceManager)
err := serviceLocator.Resolve(&serviceManager)
return serviceManager, err
})
})
container.RegisterSingleton(repository.NewInitializer)
container.RegisterSingleton(alpha.NewFeaturesManager)
container.RegisterSingleton(config.NewUserConfigManager)
container.RegisterSingleton(config.NewManager)
container.RegisterSingleton(config.NewFileConfigManager)
container.RegisterSingleton(auth.NewManager)
container.RegisterSingleton(azcli.NewUserProfileService)
container.RegisterSingleton(account.NewSubscriptionsService)
container.RegisterSingleton(account.NewManager)
container.RegisterSingleton(account.NewSubscriptionsManager)
container.RegisterSingleton(account.NewSubscriptionCredentialProvider)
container.RegisterSingleton(azcli.NewManagedClustersService)
container.RegisterSingleton(azcli.NewAdService)
container.RegisterSingleton(azcli.NewContainerRegistryService)
container.RegisterSingleton(containerapps.NewContainerAppService)
container.RegisterSingleton(project.NewContainerHelper)
container.RegisterSingleton(azcli.NewSpringService)
container.RegisterSingleton(func() ioc.ServiceLocator {
return ioc.NewServiceLocator(container)
})
container.MustRegisterSingleton(repository.NewInitializer)
container.MustRegisterSingleton(alpha.NewFeaturesManager)
container.MustRegisterSingleton(config.NewUserConfigManager)
container.MustRegisterSingleton(config.NewManager)
container.MustRegisterSingleton(config.NewFileConfigManager)
container.MustRegisterSingleton(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.RegisterSingleton(func(subManager *account.SubscriptionsManager) account.SubscriptionTenantResolver {
container.MustRegisterSingleton(func(subManager *account.SubscriptionsManager) account.SubscriptionTenantResolver {
return subManager
})
container.RegisterSingleton(func(ctx context.Context, authManager *auth.Manager) (azcore.TokenCredential, error) {
container.MustRegisterSingleton(func(ctx context.Context, authManager *auth.Manager) (azcore.TokenCredential, error) {
return authManager.CredentialForCurrentUser(ctx, nil)
})
// Tools
container.RegisterSingleton(func(
container.MustRegisterSingleton(func(
rootOptions *internal.GlobalCommandOptions,
credentialProvider account.SubscriptionCredentialProvider,
httpClient httputil.HttpClient,
@ -438,31 +465,34 @@ func registerCommonDependencies(container *ioc.NestedContainer) {
EnableTelemetry: rootOptions.EnableTelemetry,
})
})
container.RegisterSingleton(azapi.NewDeployments)
container.RegisterSingleton(azapi.NewDeploymentOperations)
container.RegisterSingleton(docker.NewDocker)
container.RegisterSingleton(dotnet.NewDotNetCli)
container.RegisterSingleton(git.NewGitCli)
container.RegisterSingleton(github.NewGitHubCli)
container.RegisterSingleton(javac.NewCli)
container.RegisterSingleton(kubectl.NewKubectl)
container.RegisterSingleton(maven.NewMavenCli)
container.RegisterSingleton(npm.NewNpmCli)
container.RegisterSingleton(python.NewPythonCli)
container.RegisterSingleton(swa.NewSwaCli)
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)
// Provisioning
container.RegisterSingleton(infra.NewAzureResourceManager)
container.RegisterTransient(provisioning.NewManager)
container.RegisterSingleton(provisioning.NewPrincipalIdProvider)
container.RegisterSingleton(prompt.NewDefaultPrompter)
container.MustRegisterSingleton(infra.NewAzureResourceManager)
container.MustRegisterScoped(provisioning.NewManager)
container.MustRegisterScoped(provisioning.NewPrincipalIdProvider)
container.MustRegisterScoped(prompt.NewDefaultPrompter)
// Other
container.RegisterSingleton(createClock)
container.MustRegisterSingleton(createClock)
// Service Targets
serviceTargetMap := map[project.ServiceTargetKind]any{
"": project.NewAppServiceTarget,
project.NonSpecifiedTarget: project.NewAppServiceTarget,
project.AppServiceTarget: project.NewAppServiceTarget,
project.AzureFunctionTarget: project.NewFunctionAppTarget,
project.ContainerAppTarget: project.NewContainerAppTarget,
@ -473,14 +503,12 @@ func registerCommonDependencies(container *ioc.NestedContainer) {
}
for target, constructor := range serviceTargetMap {
if err := container.RegisterNamedSingleton(string(target), constructor); err != nil {
panic(fmt.Errorf("registering service target %s: %w", target, err))
}
container.MustRegisterNamedScoped(string(target), constructor)
}
// Languages
frameworkServiceMap := map[project.ServiceLanguageKind]any{
"": project.NewDotNetProject,
project.ServiceLanguageNone: project.NewNoOpProject,
project.ServiceLanguageDotNet: project.NewDotNetProject,
project.ServiceLanguageCsharp: project.NewDotNetProject,
project.ServiceLanguageFsharp: project.NewDotNetProject,
@ -492,14 +520,14 @@ func registerCommonDependencies(container *ioc.NestedContainer) {
}
for language, constructor := range frameworkServiceMap {
if err := container.RegisterNamedSingleton(string(language), constructor); err != nil {
panic(fmt.Errorf("registering framework service %s: %w", language, err))
}
container.MustRegisterNamedScoped(string(language), constructor)
}
container.MustRegisterNamedScoped(string(project.ServiceLanguageDocker), project.NewDockerProjectAsFrameworkService)
// Pipelines
container.RegisterSingleton(pipeline.NewPipelineManager)
container.RegisterSingleton(func(flags *pipelineConfigFlags) *pipeline.PipelineManagerArgs {
container.MustRegisterScoped(pipeline.NewPipelineManager)
container.MustRegisterSingleton(func(flags *pipelineConfigFlags) *pipeline.PipelineManagerArgs {
return &flags.PipelineManagerArgs
})
@ -511,22 +539,20 @@ func registerCommonDependencies(container *ioc.NestedContainer) {
}
for provider, constructor := range pipelineProviderMap {
if err := container.RegisterNamedSingleton(string(provider), constructor); err != nil {
panic(fmt.Errorf("registering pipeline provider %s: %w", provider, err))
}
container.MustRegisterNamedScoped(string(provider), constructor)
}
// Platform configuration
container.RegisterSingleton(func() *lazy.Lazy[*platform.Config] {
container.MustRegisterSingleton(func(serviceLocator ioc.ServiceLocator) *lazy.Lazy[*platform.Config] {
return lazy.NewLazy(func() (*platform.Config, error) {
var platformConfig *platform.Config
err := container.Resolve(&platformConfig)
err := serviceLocator.Resolve(&platformConfig)
return platformConfig, err
})
})
container.RegisterSingleton(func(
container.MustRegisterSingleton(func(
lazyProjectConfig *lazy.Lazy[*project.ProjectConfig],
userConfigManager config.UserConfigManager,
) (*platform.Config, error) {
@ -580,20 +606,36 @@ func registerCommonDependencies(container *ioc.NestedContainer) {
for provider, constructor := range platformProviderMap {
platformName := fmt.Sprintf("%s-platform", provider)
if err := container.RegisterNamedSingleton(platformName, constructor); err != nil {
panic(fmt.Errorf("registering platform provider %s: %w", provider, err))
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)
// Required for nested actions called from composite actions like 'up'
registerActionInitializer[*initAction](container, "azd-init-action")
registerActionInitializer[*provisionAction](container, "azd-provision-action")
registerActionInitializer[*restoreAction](container, "azd-restore-action")
registerActionInitializer[*buildAction](container, "azd-build-action")
registerActionInitializer[*packageAction](container, "azd-package-action")
registerActionInitializer[*deployAction](container, "azd-deploy-action")
registerAction[*provisionAction](container, "azd-provision-action")
registerAction[*cmd.ProvisionAction](container, "azd-provision-action")
registerAction[*downAction](container, "azd-down-action")
registerAction[*configShowAction](container, "azd-config-show-action")
}
// 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)
}

Просмотреть файл

@ -8,6 +8,7 @@ package cmd
import (
"crypto/tls"
"net/http"
"net/url"
"os"
"strconv"
"time"
@ -19,6 +20,17 @@ func createHttpClient() *http.Client {
transport := http.DefaultTransport.(*http.Transport).Clone()
// Allow for self-signed certificates, which is what the recording proxy uses.
transport.TLSClientConfig = &tls.Config{InsecureSkipVerify: true}
// AZD_TEST_HTTPS_PROXY is the proxy setting that only affects azd in record mode.
// This is useful since the recording proxy server isn't trusted by other processes currently.
if val, ok := os.LookupEnv("AZD_TEST_HTTPS_PROXY"); ok {
proxyUrl, err := url.Parse(val)
if err != nil {
panic(err)
}
transport.Proxy = http.ProxyURL(proxyUrl)
}
client := &http.Client{
Transport: transport,
}

Просмотреть файл

@ -22,7 +22,7 @@ type downFlags struct {
forceDelete bool
purgeDelete bool
global *internal.GlobalCommandOptions
envFlag
internal.EnvFlag
}
func (i *downFlags) Bind(local *pflag.FlagSet, global *internal.GlobalCommandOptions) {
@ -34,7 +34,7 @@ func (i *downFlags) Bind(local *pflag.FlagSet, global *internal.GlobalCommandOpt
//nolint:lll
"Does not require confirmation before it permanently deletes resources that are soft-deleted by default (for example, key vaults).",
)
i.envFlag.Bind(local, global)
i.EnvFlag.Bind(local, global)
i.global = global
}

Просмотреть файл

@ -95,12 +95,12 @@ func newEnvSetCmd() *cobra.Command {
}
type envSetFlags struct {
envFlag
internal.EnvFlag
global *internal.GlobalCommandOptions
}
func (f *envSetFlags) Bind(local *pflag.FlagSet, global *internal.GlobalCommandOptions) {
f.envFlag.Bind(local, global)
f.EnvFlag.Bind(local, global)
f.global = global
}
@ -327,7 +327,7 @@ func (en *envNewAction) Run(ctx context.Context) (*actions.ActionResult, error)
return nil, fmt.Errorf("creating new environment: %w", err)
}
if err := en.azdCtx.SetDefaultEnvironmentName(env.GetEnvName()); err != nil {
if err := en.azdCtx.SetDefaultEnvironmentName(env.Name()); err != nil {
return nil, fmt.Errorf("saving default environment: %w", err)
}
@ -337,13 +337,13 @@ func (en *envNewAction) Run(ctx context.Context) (*actions.ActionResult, error)
type envRefreshFlags struct {
hint string
global *internal.GlobalCommandOptions
envFlag
internal.EnvFlag
}
func (er *envRefreshFlags) Bind(local *pflag.FlagSet, global *internal.GlobalCommandOptions) {
local.StringVarP(&er.hint, "hint", "", "", "Hint to help identify the environment to refresh")
er.envFlag.Bind(local, global)
er.EnvFlag.Bind(local, global)
er.global = global
}
@ -371,14 +371,14 @@ func newEnvRefreshCmd() *cobra.Command {
return nil
}
if flagValue, err := cmd.Flags().GetString(environmentNameFlag); err == nil {
if flagValue, err := cmd.Flags().GetString(internal.EnvironmentNameFlagName); err == nil {
if flagValue != "" && args[0] != flagValue {
return errors.New(
"the --environment flag and an explicit environment name as an argument may not be used together")
}
}
return cmd.Flags().Set(environmentNameFlag, args[0])
return cmd.Flags().Set(internal.EnvironmentNameFlagName, args[0])
},
Annotations: map[string]string{},
}
@ -432,7 +432,7 @@ func newEnvRefreshAction(
func (ef *envRefreshAction) Run(ctx context.Context) (*actions.ActionResult, error) {
// Command title
ef.console.MessageUxItem(ctx, &ux.MessageTitle{
Title: fmt.Sprintf("Refreshing environment %s (azd env refresh)", ef.env.GetEnvName()),
Title: fmt.Sprintf("Refreshing environment %s (azd env refresh)", ef.env.Name()),
})
if err := ef.projectManager.Initialize(ctx, ef.projectConfig); err != nil {
@ -518,12 +518,12 @@ func newEnvGetValuesCmd() *cobra.Command {
}
type envGetValuesFlags struct {
envFlag
internal.EnvFlag
global *internal.GlobalCommandOptions
}
func (eg *envGetValuesFlags) Bind(local *pflag.FlagSet, global *internal.GlobalCommandOptions) {
eg.envFlag.Bind(local, global)
eg.EnvFlag.Bind(local, global)
eg.global = global
}
@ -563,8 +563,8 @@ func (eg *envGetValuesAction) Run(ctx context.Context) (*actions.ActionResult, e
// and later, when envManager.Get() is called with the empty string, azd returns an error.
// But if there is already an environment (default to be selected), azd must honor the --environment flag
// over the default environment.
if eg.flags.environmentName != "" {
name = eg.flags.environmentName
if eg.flags.EnvironmentName != "" {
name = eg.flags.EnvironmentName
}
env, err := eg.envManager.Get(ctx, name)
if errors.Is(err, environment.ErrNotFound) {

Просмотреть файл

@ -54,14 +54,14 @@ func newHooksRunCmd() *cobra.Command {
}
type hooksRunFlags struct {
envFlag
internal.EnvFlag
global *internal.GlobalCommandOptions
platform string
service string
}
func (f *hooksRunFlags) Bind(local *pflag.FlagSet, global *internal.GlobalCommandOptions) {
f.envFlag.Bind(local, global)
f.EnvFlag.Bind(local, global)
f.global = global
local.StringVar(&f.platform, "platform", "", "Forces hooks to run for the specified platform.")
@ -112,7 +112,7 @@ func (hra *hooksRunAction) Run(ctx context.Context) (*actions.ActionResult, erro
TitleNote: fmt.Sprintf(
"Finding and executing %s hooks for environment %s",
output.WithHighLightFormat(hookName),
output.WithHighLightFormat(hra.env.GetEnvName()),
output.WithHighLightFormat(hra.env.Name()),
),
})

Просмотреть файл

@ -6,13 +6,14 @@ 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/internal/cmd"
"github.com/azure/azure-dev/cli/azd/pkg/input"
"github.com/azure/azure-dev/cli/azd/pkg/output"
"github.com/spf13/cobra"
)
type infraCreateFlags struct {
provisionFlags
cmd.ProvisionFlags
}
func newInfraCreateFlags(cmd *cobra.Command, global *internal.GlobalCommandOptions) *infraCreateFlags {
@ -31,17 +32,17 @@ func newInfraCreateCmd() *cobra.Command {
}
type infraCreateAction struct {
infraCreate *provisionAction
infraCreate *cmd.ProvisionAction
console input.Console
}
func newInfraCreateAction(
createFlags *infraCreateFlags,
provision *provisionAction,
provision *cmd.ProvisionAction,
console input.Console,
) actions.Action {
// Required to ensure the sub action flags are bound correctly to the actions
provision.flags = &createFlags.provisionFlags
provision.SetFlags(&createFlags.ProvisionFlags)
return &infraCreateAction{
infraCreate: provision,

Просмотреть файл

@ -24,13 +24,13 @@ import (
type infraSynthFlags struct {
global *internal.GlobalCommandOptions
*envFlag
*internal.EnvFlag
force bool
}
func newInfraSynthFlags(cmd *cobra.Command, global *internal.GlobalCommandOptions) *infraSynthFlags {
flags := &infraSynthFlags{
envFlag: &envFlag{},
EnvFlag: &internal.EnvFlag{},
}
flags.Bind(cmd.Flags(), global)
@ -39,7 +39,7 @@ func newInfraSynthFlags(cmd *cobra.Command, global *internal.GlobalCommandOption
func (f *infraSynthFlags) Bind(local *pflag.FlagSet, global *internal.GlobalCommandOptions) {
f.global = global
f.envFlag.Bind(local, global)
f.EnvFlag.Bind(local, global)
local.BoolVar(&f.force, "force", false, "Overwrite any existing files without prompting")
}

Просмотреть файл

@ -53,7 +53,7 @@ type initFlags struct {
subscription string
location string
global *internal.GlobalCommandOptions
envFlag
internal.EnvFlag
}
func (i *initFlags) Bind(local *pflag.FlagSet, global *internal.GlobalCommandOptions) {
@ -79,7 +79,7 @@ func (i *initFlags) Bind(local *pflag.FlagSet, global *internal.GlobalCommandOpt
"Name or ID of an Azure subscription to use for the new environment",
)
local.StringVarP(&i.location, "location", "l", "", "Azure location for the new environment")
i.envFlag.Bind(local, global)
i.EnvFlag.Bind(local, global)
i.global = global
}
@ -125,11 +125,8 @@ func (i *initAction) Run(ctx context.Context) (*actions.ActionResult, error) {
return nil, fmt.Errorf("getting cwd: %w", err)
}
azdCtx, err := i.lazyAzdCtx.GetValue()
if err != nil {
azdCtx = azdcontext.NewAzdContextWithDirectory(wd)
azdCtx := azdcontext.NewAzdContextWithDirectory(wd)
i.lazyAzdCtx.SetValue(azdCtx)
}
if i.flags.templateBranch != "" && i.flags.templatePath == "" {
return nil,
@ -348,7 +345,7 @@ func (i *initAction) initializeEnv(
}
envSpec := environment.Spec{
Name: i.flags.environmentName,
Name: i.flags.EnvironmentName,
Subscription: i.flags.subscription,
Location: i.flags.location,
Examples: examples,
@ -359,7 +356,7 @@ func (i *initAction) initializeEnv(
return nil, fmt.Errorf("loading environment: %w", err)
}
if err := azdCtx.SetDefaultEnvironmentName(env.GetEnvName()); err != nil {
if err := azdCtx.SetDefaultEnvironmentName(env.Name()); err != nil {
return nil, fmt.Errorf("saving default environment: %w", err)
}

Просмотреть файл

@ -30,7 +30,7 @@ func NewDebugMiddleware(options *Options, console input.Console) Middleware {
// a debugger before continuing invocation of the action
func (m *DebugMiddleware) Run(ctx context.Context, next NextFn) (*actions.ActionResult, error) {
// Don't run for sub actions
if m.options.IsChildAction() {
if m.options.IsChildAction(ctx) {
return next(ctx)
}

Просмотреть файл

@ -2,8 +2,8 @@ package middleware
import (
"context"
"fmt"
"log"
"slices"
"github.com/azure/azure-dev/cli/azd/cmd/actions"
"github.com/azure/azure-dev/cli/azd/pkg/ioc"
@ -18,28 +18,29 @@ type Middleware interface {
Run(ctx context.Context, nextFn NextFn) (*actions.ActionResult, error)
}
// MiddlewareContext allow composite actions to orchestrate invoking child actions
type MiddlewareContext interface {
// Executes the middleware chain for the specified child action
RunChildAction(
ctx context.Context,
runOptions *Options,
action actions.Action,
) (*actions.ActionResult, error)
}
type childActionKeyType string
var childActionKey childActionKeyType = "child-action"
// Middleware Run options
type Options struct {
container *ioc.NestedContainer
CommandPath string
Name string
Aliases []string
Flags *pflag.FlagSet
Args []string
isChildAction bool
}
func (o *Options) IsChildAction() bool {
return o.isChildAction
func (o *Options) IsChildAction(ctx context.Context) bool {
value, ok := ctx.Value(childActionKey).(bool)
return ok && value
}
// Sets the container to be used for resolving middleware components
func (o *Options) WithContainer(container *ioc.NestedContainer) {
o.container = container
}
// Executes the next middleware in the command chain
@ -50,41 +51,16 @@ type NextFn func(ctx context.Context) (*actions.ActionResult, error)
type MiddlewareRunner struct {
chain []string
container *ioc.NestedContainer
actionCache map[actions.Action]*actions.ActionResult
}
// Creates a new middleware runner
func NewMiddlewareRunner(container *ioc.NestedContainer) *MiddlewareRunner {
return &MiddlewareRunner{
container: container,
chain: []string{},
actionCache: map[actions.Action]*actions.ActionResult{},
container: container,
}
}
// Executes the middleware chain for the specified child action
func (r *MiddlewareRunner) RunChildAction(
ctx context.Context,
runOptions *Options,
action actions.Action,
) (*actions.ActionResult, error) {
// If we have previously run this action then return the cached result
if cachedActionResult, has := r.actionCache[action]; has {
return cachedActionResult, nil
}
// If we have not previously run this action then execute it
runOptions.isChildAction = true
result, err := r.RunAction(ctx, runOptions, action)
// Cache the result on action success
if err == nil {
r.actionCache[action] = result
}
return result, err
}
// Executes the middleware chain for the specified action
func (r *MiddlewareRunner) RunAction(
ctx context.Context,
@ -96,7 +72,13 @@ func (r *MiddlewareRunner) RunAction(
var nextFn NextFn
actionContainer := ioc.NewNestedContainer(r.container)
// We need to get the actionContainer for the current executing scope
actionContainer := runOptions.container
if actionContainer == nil {
actionContainer = r.container
}
// Create a new context with the child container which will be leveraged on any child command/actions
ioc.RegisterInstance(actionContainer, runOptions)
// This recursive function executes the middleware chain in the order that
@ -134,10 +116,16 @@ func (r *MiddlewareRunner) RunAction(
// Registers middleware components that will be run for all actions
func (r *MiddlewareRunner) Use(name string, resolveFn any) error {
if err := r.container.RegisterNamedTransient(name, resolveFn); err != nil {
return fmt.Errorf("failed registering middleware '%s'. Ensure the resolver is a go function. %w", name, err)
return err
}
if !slices.Contains(r.chain, name) {
r.chain = append(r.chain, name)
}
return nil
}
func WithChildAction(ctx context.Context) context.Context {
return context.WithValue(ctx, childActionKey, true)
}

Просмотреть файл

@ -6,7 +6,6 @@ import (
"testing"
"github.com/azure/azure-dev/cli/azd/cmd/actions"
"github.com/azure/azure-dev/cli/azd/pkg/ioc"
"github.com/azure/azure-dev/cli/azd/test/mocks"
"github.com/stretchr/testify/require"
)
@ -19,7 +18,7 @@ func Test_Middleware_RunAction(t *testing.T) {
runLog := []string{}
mockContext := mocks.NewMockContext(context.Background())
middlewareRunner := NewMiddlewareRunner(ioc.NewNestedContainer(nil))
middlewareRunner := NewMiddlewareRunner(mockContext.Container)
_ = middlewareRunner.Use("test", func() Middleware {
return &testMiddleware{
@ -56,7 +55,7 @@ func Test_Middleware_RunAction(t *testing.T) {
runLog := []string{}
mockContext := mocks.NewMockContext(context.Background())
middlewareRunner := NewMiddlewareRunner(ioc.NewNestedContainer(nil))
middlewareRunner := NewMiddlewareRunner(mockContext.Container)
_ = middlewareRunner.Use("test", func() Middleware {
return &testMiddleware{
@ -86,7 +85,7 @@ func Test_Middleware_RunAction(t *testing.T) {
t.Run("multiple middleware components", func(t *testing.T) {
mockContext := mocks.NewMockContext(context.Background())
middlewareRunner := NewMiddlewareRunner(ioc.NewNestedContainer(nil))
middlewareRunner := NewMiddlewareRunner(mockContext.Container)
runLog := []string{}
_ = middlewareRunner.Use("A", func() Middleware {
@ -128,7 +127,7 @@ func Test_Middleware_RunAction(t *testing.T) {
t.Run("context propagated to action", func(t *testing.T) {
mockContext := mocks.NewMockContext(context.Background())
middlewareRunner := NewMiddlewareRunner(ioc.NewNestedContainer(nil))
middlewareRunner := NewMiddlewareRunner(mockContext.Container)
key := cxtKey{}
@ -157,25 +156,6 @@ func Test_Middleware_RunAction(t *testing.T) {
})
}
func Test_Middleware_RunChildAction(t *testing.T) {
mockContext := mocks.NewMockContext(context.Background())
middlewareRunner := NewMiddlewareRunner(ioc.NewNestedContainer(nil))
runLog := []string{}
action, actionRan := createAction(&runLog)
runOptions := &Options{Name: "test"}
require.False(t, runOptions.IsChildAction())
result, err := middlewareRunner.RunChildAction(*mockContext.Context, runOptions, action)
// Executing RunChildAction sets a marker on the options that this is a child action
require.True(t, runOptions.IsChildAction())
require.NotNil(t, result)
require.NoError(t, err)
require.True(t, *actionRan)
}
func createAction(runLog *[]string) (actions.Action, *bool) {
actionRan := false

Просмотреть файл

@ -49,7 +49,7 @@ func (m *TelemetryMiddleware) Run(ctx context.Context, next NextFn) (*actions.Ac
log.Printf("TraceID: %s", span.SpanContext().TraceID())
if !m.options.IsChildAction() {
if !m.options.IsChildAction(ctx) {
// Set the command name as a baggage item on the span context.
// This allow inner actions to have command name attached.
spanCtx = tracing.SetBaggageInContext(

Просмотреть файл

@ -34,7 +34,6 @@ func Test_Telemetry_Run(t *testing.T) {
options := &Options{
CommandPath: "azd provision",
Name: "provision",
isChildAction: false,
}
middleware := NewTelemetryMiddleware(options, lazyPlatformConfig)
@ -64,7 +63,6 @@ func Test_Telemetry_Run(t *testing.T) {
options := &Options{
CommandPath: "azd provision",
Name: "provision",
isChildAction: true,
}
middleware := NewTelemetryMiddleware(options, lazyPlatformConfig)
@ -77,7 +75,8 @@ func Test_Telemetry_Run(t *testing.T) {
return nil, nil
}
_, _ = middleware.Run(*mockContext.Context, nextFn)
ctx := WithChildAction(*mockContext.Context)
_, _ = middleware.Run(ctx, nextFn)
require.True(t, ran)
require.NotEqual(

Просмотреть файл

@ -28,7 +28,7 @@ type monitorFlags struct {
monitorLogs bool
monitorOverview bool
global *internal.GlobalCommandOptions
envFlag
internal.EnvFlag
}
func (m *monitorFlags) Bind(local *pflag.FlagSet, global *internal.GlobalCommandOptions) {
@ -40,7 +40,7 @@ func (m *monitorFlags) Bind(local *pflag.FlagSet, global *internal.GlobalCommand
)
local.BoolVar(&m.monitorLogs, "logs", false, "Open a browser to Application Insights Logs.")
local.BoolVar(&m.monitorOverview, "overview", false, "Open a browser to Application Insights Overview Dashboard.")
m.envFlag.Bind(local, global)
m.EnvFlag.Bind(local, global)
m.global = global
}
@ -101,7 +101,7 @@ func (m *monitorAction) Run(ctx context.Context) (*actions.ActionResult, error)
resourceManager := infra.NewAzureResourceManager(m.azCli, m.deploymentOperations)
resourceGroups, err := resourceManager.GetResourceGroupsForEnvironment(
ctx, m.env.GetSubscriptionId(), m.env.GetEnvName())
ctx, m.env.GetSubscriptionId(), m.env.Name())
if err != nil {
return nil, fmt.Errorf("discovering resource groups from deployment: %w", err)
}
@ -142,8 +142,7 @@ func (m *monitorAction) Run(ctx context.Context) (*actions.ActionResult, error)
for _, insightsResource := range insightsResources {
if m.flags.monitorLive {
openWithDefaultBrowser(ctx, m.console,
fmt.Sprintf("https://app.azure.com/%s%s/quickPulse", tenantId, insightsResource.Id),
)
fmt.Sprintf("https://app.azure.com/%s%s/quickPulse", tenantId, insightsResource.Id))
}
if m.flags.monitorLogs {
@ -155,8 +154,7 @@ func (m *monitorAction) Run(ctx context.Context) (*actions.ActionResult, error)
for _, portalResource := range portalResources {
if m.flags.monitorOverview {
openWithDefaultBrowser(ctx, m.console,
fmt.Sprintf("https://portal.azure.com/#@%s/dashboard/arm%s", tenantId, portalResource.Id),
)
fmt.Sprintf("https://portal.azure.com/#@%s/dashboard/arm%s", tenantId, portalResource.Id))
}
}

Просмотреть файл

@ -20,13 +20,13 @@ import (
type packageFlags struct {
all bool
global *internal.GlobalCommandOptions
*envFlag
*internal.EnvFlag
outputPath string
}
func newPackageFlags(cmd *cobra.Command, global *internal.GlobalCommandOptions) *packageFlags {
flags := &packageFlags{
envFlag: &envFlag{},
EnvFlag: &internal.EnvFlag{},
}
flags.Bind(cmd.Flags(), global)
@ -35,7 +35,7 @@ func newPackageFlags(cmd *cobra.Command, global *internal.GlobalCommandOptions)
}
func (pf *packageFlags) Bind(local *pflag.FlagSet, global *internal.GlobalCommandOptions) {
pf.envFlag.Bind(local, global)
pf.EnvFlag.Bind(local, global)
pf.global = global
local.BoolVar(

Просмотреть файл

@ -26,7 +26,7 @@ import (
type pipelineConfigFlags struct {
pipeline.PipelineManagerArgs
global *internal.GlobalCommandOptions
envFlag
internal.EnvFlag
}
func (pc *pipelineConfigFlags) Bind(local *pflag.FlagSet, global *internal.GlobalCommandOptions) {
@ -66,7 +66,7 @@ func (pc *pipelineConfigFlags) Bind(local *pflag.FlagSet, global *internal.Globa
// there no customer input using --provider
local.StringVar(&pc.PipelineProvider, "provider", "",
"The pipeline provider to use (github for Github Actions and azdo for Azure Pipelines).")
pc.envFlag.Bind(local, global)
pc.EnvFlag.Bind(local, global)
pc.global = global
}

Просмотреть файл

@ -26,7 +26,7 @@ type restoreFlags struct {
all bool
global *internal.GlobalCommandOptions
serviceName string
envFlag
internal.EnvFlag
}
func (r *restoreFlags) Bind(local *pflag.FlagSet, global *internal.GlobalCommandOptions) {
@ -50,7 +50,7 @@ func (r *restoreFlags) Bind(local *pflag.FlagSet, global *internal.GlobalCommand
func newRestoreFlags(cmd *cobra.Command, global *internal.GlobalCommandOptions) *restoreFlags {
flags := &restoreFlags{}
flags.Bind(cmd.Flags(), global)
flags.envFlag.Bind(cmd.Flags(), global)
flags.EnvFlag.Bind(cmd.Flags(), global)
flags.global = global
return flags
@ -92,6 +92,7 @@ func newRestoreAction(
projectManager project.ProjectManager,
serviceManager project.ServiceManager,
commandRunner exec.CommandRunner,
importManager *project.ImportManager,
) actions.Action {
return &restoreAction{
flags: flags,
@ -105,6 +106,7 @@ func newRestoreAction(
serviceManager: serviceManager,
env: env,
commandRunner: commandRunner,
importManager: importManager,
}
}

Просмотреть файл

@ -4,7 +4,6 @@
package cmd
import (
"context"
"errors"
"fmt"
"log"
@ -21,6 +20,7 @@ import (
"github.com/azure/azure-dev/cli/azd/pkg/platform"
"github.com/azure/azure-dev/cli/azd/internal"
"github.com/azure/azure-dev/cli/azd/internal/cmd"
"github.com/azure/azure-dev/cli/azd/internal/telemetry"
"github.com/azure/azure-dev/cli/azd/pkg/output"
"github.com/spf13/cobra"
@ -29,7 +29,13 @@ import (
// Creates the root Cobra command for AZD.
// staticHelp - False, except for running for doc generation
// middlewareChain - nil, except for running unit tests
func NewRootCmd(ctx context.Context, staticHelp bool, middlewareChain []*actions.MiddlewareRegistration) *cobra.Command {
// rootContainer - The IoC container to use for registering and resolving dependencies. If nil is provided, a new
// container empty will be created.
func NewRootCmd(
staticHelp bool,
middlewareChain []*actions.MiddlewareRegistration,
rootContainer *ioc.NestedContainer,
) *cobra.Command {
prevDir := ""
opts := &internal.GlobalCommandOptions{GenerateStaticHelp: staticHelp}
opts.EnableTelemetry = telemetry.IsTelemetryEnabled()
@ -134,6 +140,14 @@ func NewRootCmd(ctx context.Context, staticHelp bool, middlewareChain []*actions
},
})
root.Add("vs-server", &actions.ActionDescriptorOptions{
Command: newVsServerCmd(),
FlagsResolver: newVsServerFlags,
ActionResolver: newVsServerAction,
OutputFormats: []output.Format{output.NoneFormat},
DefaultFormat: output.NoneFormat,
})
root.Add("show", &actions.ActionDescriptorOptions{
Command: newShowCmd(),
FlagsResolver: newShowFlags,
@ -206,13 +220,13 @@ func NewRootCmd(ctx context.Context, staticHelp bool, middlewareChain []*actions
root.
Add("provision", &actions.ActionDescriptorOptions{
Command: newProvisionCmd(),
FlagsResolver: newProvisionFlags,
ActionResolver: newProvisionAction,
Command: cmd.NewProvisionCmd(),
FlagsResolver: cmd.NewProvisionFlags,
ActionResolver: cmd.NewProvisionAction,
OutputFormats: []output.Format{output.JsonFormat, output.NoneFormat},
DefaultFormat: output.NoneFormat,
HelpOptions: actions.ActionHelpOptions{
Description: getCmdProvisionHelpDescription,
Description: cmd.GetCmdProvisionHelpDescription,
Footer: getCmdHelpDefaultFooter,
},
GroupingOptions: actions.CommandGroupOptions{
@ -246,14 +260,14 @@ func NewRootCmd(ctx context.Context, staticHelp bool, middlewareChain []*actions
root.
Add("deploy", &actions.ActionDescriptorOptions{
Command: newDeployCmd(),
FlagsResolver: newDeployFlags,
ActionResolver: newDeployAction,
Command: cmd.NewDeployCmd(),
FlagsResolver: cmd.NewDeployFlags,
ActionResolver: cmd.NewDeployAction,
OutputFormats: []output.Format{output.JsonFormat, output.NoneFormat},
DefaultFormat: output.NoneFormat,
HelpOptions: actions.ActionHelpOptions{
Description: getCmdDeployHelpDescription,
Footer: getCmdDeployHelpFooter,
Description: cmd.GetCmdDeployHelpDescription,
Footer: cmd.GetCmdDeployHelpFooter,
},
GroupingOptions: actions.CommandGroupOptions{
RootLevelHelp: actions.CmdGroupManage,
@ -322,19 +336,26 @@ func NewRootCmd(ctx context.Context, staticHelp bool, middlewareChain []*actions
return !descriptor.Options.DisableTelemetry
})
// Register common dependencies for the IoC container
ioc.RegisterInstance(ioc.Global, ctx)
registerCommonDependencies(ioc.Global)
// Register common dependencies for the IoC rootContainer
if rootContainer == nil {
rootContainer = ioc.NewNestedContainer(nil)
}
ioc.RegisterNamedInstance(rootContainer, "root-cmd", rootCmd)
registerCommonDependencies(rootContainer)
// Initialize the platform specific components for the IoC container
// Only container resolution errors will return an error
// Invalid configurations will fall back to default platform
if _, err := platform.Initialize(ioc.Global, azd.PlatformKindDefault); err != nil {
if _, err := platform.Initialize(rootContainer, azd.PlatformKindDefault); err != nil {
panic(err)
}
// Compose the hierarchy of action descriptions into cobra commands
cobraBuilder := NewCobraBuilder(ioc.Global)
var cobraBuilder *CobraBuilder
if err := rootContainer.Resolve(&cobraBuilder); err != nil {
panic(err)
}
cmd, err := cobraBuilder.BuildCommand(root)
if err != nil {

Просмотреть файл

@ -29,11 +29,11 @@ import (
type showFlags struct {
global *internal.GlobalCommandOptions
envFlag
internal.EnvFlag
}
func (s *showFlags) Bind(local *pflag.FlagSet, global *internal.GlobalCommandOptions) {
s.envFlag.Bind(local, global)
s.EnvFlag.Bind(local, global)
s.global = global
}
@ -98,6 +98,10 @@ func newShowAction(
}
func (s *showAction) Run(ctx context.Context) (*actions.ActionResult, error) {
s.console.ShowSpinner(ctx, "Gathering information about your app and its resources...", input.Step)
defer s.console.StopSpinner(ctx, "", input.Step)
res := contracts.ShowResult{
Name: s.projectConfig.Name,
Services: make(map[string]contracts.ShowService),
@ -131,7 +135,7 @@ func (s *showAction) Run(ctx context.Context) (*actions.ActionResult, error) {
// having an environment injected into us so we can handle cases where the current environment doesn't exist (if we
// injected an environment, we'd prompt the user to see if they want to created one and we'd prefer not to have show
// interact with the user).
environmentName := s.flags.environmentName
environmentName := s.flags.EnvironmentName
if environmentName == "" {
var err error
@ -143,7 +147,7 @@ func (s *showAction) Run(ctx context.Context) (*actions.ActionResult, error) {
}
var subId, rgName string
if env, err := s.envManager.Get(ctx, environmentName); err != nil {
if errors.Is(err, environment.ErrNotFound) && s.flags.environmentName != "" {
if errors.Is(err, environment.ErrNotFound) && s.flags.EnvironmentName != "" {
return nil, fmt.Errorf(
`"environment '%s' does not exist. You can create it with "azd env new"`, environmentName,
)
@ -155,7 +159,7 @@ func (s *showAction) Run(ctx context.Context) (*actions.ActionResult, error) {
} else {
azureResourceManager := infra.NewAzureResourceManager(s.azCli, s.deploymentOperations)
resourceManager := project.NewResourceManager(env, s.azCli, s.deploymentOperations)
envName := env.GetEnvName()
envName := env.Name()
rgName, err = azureResourceManager.FindResourceGroupForEnvironment(ctx, subId, envName)
if err == nil {
@ -181,7 +185,7 @@ func (s *showAction) Run(ctx context.Context) (*actions.ActionResult, error) {
} else {
log.Printf(
"ignoring error determining resource group for environment %s, resource ids will not be available: %v",
env.GetEnvName(),
env.Name(),
err)
}
}

17
cli/azd/cmd/testdata/TestUsage-azd-up.snap поставляемый
Просмотреть файл

@ -1,5 +1,20 @@
Executes the azd package, azd provision and azd deploy commands in a single step.
Runs a workflow to package, provision and deploy your application in a single step.
The up workflow can be customized by adding a workflows section to your azure.yaml.
For example, modify the workflow to provision before packaging and deploying:
-------------------------
# azure.yaml
workflows:
up:
- azd: provision
- azd: package --all
- azd: deploy --all
-------------------------
Any azd command and flags are supported in the workflow steps.
Usage
azd up [flags]

Просмотреть файл

@ -5,9 +5,10 @@ import (
"fmt"
"time"
"github.com/MakeNowJust/heredoc/v2"
"github.com/azure/azure-dev/cli/azd/cmd/actions"
"github.com/azure/azure-dev/cli/azd/cmd/middleware"
"github.com/azure/azure-dev/cli/azd/internal"
"github.com/azure/azure-dev/cli/azd/internal/cmd"
"github.com/azure/azure-dev/cli/azd/pkg/auth"
"github.com/azure/azure-dev/cli/azd/pkg/environment"
"github.com/azure/azure-dev/cli/azd/pkg/infra/provisioning"
@ -15,26 +16,26 @@ import (
"github.com/azure/azure-dev/cli/azd/pkg/output"
"github.com/azure/azure-dev/cli/azd/pkg/output/ux"
"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/workflow"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
)
type upFlags struct {
provisionFlags
deployFlags
cmd.ProvisionFlags
cmd.DeployFlags
global *internal.GlobalCommandOptions
envFlag
internal.EnvFlag
}
func (u *upFlags) Bind(local *pflag.FlagSet, global *internal.GlobalCommandOptions) {
u.envFlag.Bind(local, global)
u.EnvFlag.Bind(local, global)
u.global = global
u.provisionFlags.bindNonCommon(local, global)
u.provisionFlags.setCommon(&u.envFlag)
u.deployFlags.bindNonCommon(local, global)
u.deployFlags.setCommon(&u.envFlag)
u.ProvisionFlags.BindNonCommon(local, global)
u.ProvisionFlags.SetCommon(&u.EnvFlag)
u.DeployFlags.BindNonCommon(local, global)
u.DeployFlags.SetCommon(&u.EnvFlag)
}
func newUpFlags(cmd *cobra.Command, global *internal.GlobalCommandOptions) *upFlags {
@ -53,67 +54,45 @@ func newUpCmd() *cobra.Command {
type upAction struct {
flags *upFlags
console input.Console
env *environment.Environment
projectConfig *project.ProjectConfig
packageActionInitializer actions.ActionInitializer[*packageAction]
provisionActionInitializer actions.ActionInitializer[*provisionAction]
deployActionInitializer actions.ActionInitializer[*deployAction]
console input.Console
runner middleware.MiddlewareContext
prompters prompt.Prompter
provisioningManager *provisioning.Manager
importManager *project.ImportManager
workflowRunner *workflow.Runner
}
var defaultUpWorkflow = &workflow.Workflow{
Name: "up",
Steps: []*workflow.Step{
{AzdCommand: workflow.Command{Args: []string{"package", "--all"}}},
{AzdCommand: workflow.Command{Args: []string{"provision"}}},
{AzdCommand: workflow.Command{Args: []string{"deploy", "--all"}}},
},
}
func newUpAction(
flags *upFlags,
console input.Console,
env *environment.Environment,
_ auth.LoggedInGuard,
projectConfig *project.ProjectConfig,
packageActionInitializer actions.ActionInitializer[*packageAction],
provisionActionInitializer actions.ActionInitializer[*provisionAction],
deployActionInitializer actions.ActionInitializer[*deployAction],
console input.Console,
runner middleware.MiddlewareContext,
prompters prompt.Prompter,
provisioningManager *provisioning.Manager,
importManager *project.ImportManager,
workflowRunner *workflow.Runner,
) actions.Action {
return &upAction{
flags: flags,
console: console,
env: env,
projectConfig: projectConfig,
packageActionInitializer: packageActionInitializer,
provisionActionInitializer: provisionActionInitializer,
deployActionInitializer: deployActionInitializer,
console: console,
runner: runner,
prompters: prompters,
provisioningManager: provisioningManager,
importManager: importManager,
workflowRunner: workflowRunner,
}
}
func (u *upAction) Run(ctx context.Context) (*actions.ActionResult, error) {
if u.flags.provisionFlags.noProgress {
fmt.Fprintln(
u.console.Handles().Stderr,
//nolint:lll
output.WithWarningFormat(
"WARNING: The '--no-progress' flag is deprecated and will be removed in a future release.",
),
)
// this flag actually isn't used by the provision command, we set it to false to hide the extra warning
u.flags.provisionFlags.noProgress = false
}
if u.flags.deployFlags.serviceName != "" {
fmt.Fprintln(
u.console.Handles().Stderr,
//nolint:lll
output.WithWarningFormat("WARNING: The '--service' flag is deprecated and will be removed in a future release."))
}
infra, err := u.importManager.ProjectInfrastructure(ctx, u.projectConfig)
if err != nil {
return nil, err
@ -127,61 +106,52 @@ func (u *upAction) Run(ctx context.Context) (*actions.ActionResult, error) {
startTime := time.Now()
packageAction, err := u.packageActionInitializer()
if err != nil {
return nil, err
}
packageOptions := &middleware.Options{CommandPath: "package"}
_, err = u.runner.RunChildAction(ctx, packageOptions, packageAction)
if err != nil {
return nil, err
upWorkflow, has := u.projectConfig.Workflows["up"]
if !has {
upWorkflow = defaultUpWorkflow
} else {
u.console.Message(ctx, output.WithGrayFormat("Note: Running custom 'up' workflow from azure.yaml"))
}
provision, err := u.provisionActionInitializer()
if err != nil {
return nil, err
}
provision.flags = &u.flags.provisionFlags
provisionOptions := &middleware.Options{CommandPath: "provision"}
provisionResult, err := u.runner.RunChildAction(ctx, provisionOptions, provision)
if err != nil {
return nil, err
}
// Print an additional newline to separate provision from deploy
u.console.Message(ctx, "")
deploy, err := u.deployActionInitializer()
if err != nil {
return nil, err
}
deploy.flags = &u.flags.deployFlags
// move flag to args to avoid extra deprecation flag warning
if deploy.flags.serviceName != "" {
deploy.args = []string{deploy.flags.serviceName}
deploy.flags.serviceName = ""
}
deployOptions := &middleware.Options{CommandPath: "deploy"}
_, err = u.runner.RunChildAction(ctx, deployOptions, deploy)
if err != nil {
if err := u.workflowRunner.Run(ctx, upWorkflow); err != nil {
return nil, err
}
return &actions.ActionResult{
Message: &actions.ResultMessage{
Header: fmt.Sprintf("Your application was provisioned and deployed to Azure in %s.",
Header: fmt.Sprintf("Your up workflow to provision and deploy to Azure completed in %s.",
ux.DurationAsText(since(startTime))),
FollowUp: provisionResult.Message.FollowUp,
},
}, nil
}
func getCmdUpHelpDescription(c *cobra.Command) string {
return generateCmdHelpDescription(
fmt.Sprintf("Executes the %s, %s and %s commands in a single step.",
output.WithHighLightFormat("azd package"),
output.WithHighLightFormat("azd provision"),
output.WithHighLightFormat("azd deploy")), nil)
heredoc.Docf(
`Runs a workflow to %s, %s and %s your application in a single step.
The %s workflow can be customized by adding a %s section to your %s.
For example, modify the workflow to provision before packaging and deploying:
-------------------------
%s
workflows:
up:
- azd: provision
- azd: package --all
- azd: deploy --all
-------------------------
Any azd command and flags are supported in the workflow steps.`,
output.WithHighLightFormat("package"),
output.WithHighLightFormat("provision"),
output.WithHighLightFormat("deploy"),
output.WithHighLightFormat("up"),
output.WithHighLightFormat("workflows"),
output.WithHighLightFormat("azure.yaml"),
output.WithGrayFormat("# azure.yaml"),
),
nil,
)
}

Просмотреть файл

@ -2,7 +2,6 @@ package cmd
import (
"bytes"
"context"
"html/template"
"strings"
"testing"
@ -22,7 +21,7 @@ import (
func TestUsage(t *testing.T) {
// disable rich formatting output
t.Setenv("TERM", "dumb")
root := NewRootCmd(context.Background(), false, nil)
root := NewRootCmd(false, nil, nil)
usageSnapshot(t, root)
}

Просмотреть файл

@ -13,7 +13,6 @@ import (
"time"
"github.com/AlecAivazis/survey/v2"
"github.com/azure/azure-dev/cli/azd/internal"
"github.com/azure/azure-dev/cli/azd/internal/tracing"
"github.com/azure/azure-dev/cli/azd/pkg/environment"
azdExec "github.com/azure/azure-dev/cli/azd/pkg/exec"
@ -21,7 +20,6 @@ import (
"github.com/azure/azure-dev/cli/azd/pkg/output"
"github.com/azure/azure-dev/cli/azd/pkg/project"
"github.com/cli/browser"
"github.com/spf13/pflag"
)
// CmdAnnotations on a command
@ -29,22 +27,6 @@ type CmdAnnotations map[string]string
type Asker func(p survey.Prompt, response interface{}) error
const environmentNameFlag string = "environment"
type envFlag struct {
environmentName string
}
func (e *envFlag) Bind(local *pflag.FlagSet, global *internal.GlobalCommandOptions) {
local.StringVarP(
&e.environmentName,
environmentNameFlag,
"e",
// Set the default value to AZURE_ENV_NAME value if available
os.Getenv(environment.EnvNameEnvVarName),
"The name of the environment to use.")
}
func getResourceGroupFollowUp(
ctx context.Context,
formatter output.Formatter,

87
cli/azd/cmd/vs_server.go Normal file
Просмотреть файл

@ -0,0 +1,87 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
package cmd
import (
"context"
"encoding/json"
"fmt"
"net"
"os"
"github.com/azure/azure-dev/cli/azd/cmd/actions"
"github.com/azure/azure-dev/cli/azd/internal"
"github.com/azure/azure-dev/cli/azd/internal/vsrpc"
"github.com/azure/azure-dev/cli/azd/pkg/contracts"
"github.com/azure/azure-dev/cli/azd/pkg/ioc"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
)
type vsServerFlags struct {
global *internal.GlobalCommandOptions
port int
}
func (s *vsServerFlags) Bind(local *pflag.FlagSet, global *internal.GlobalCommandOptions) {
s.global = global
local.IntVar(&s.port, "port", 0, "Port to listen on (0 for random port)")
}
func newVsServerFlags(cmd *cobra.Command, global *internal.GlobalCommandOptions) *vsServerFlags {
flags := &vsServerFlags{}
flags.Bind(cmd.Flags(), global)
return flags
}
func newVsServerCmd() *cobra.Command {
cmd := &cobra.Command{
Hidden: true,
Use: "vs-server",
Short: "Run Server",
}
return cmd
}
type vsServerAction struct {
rootContainer *ioc.NestedContainer
flags *vsServerFlags
}
func newVsServerAction(rootContainer *ioc.NestedContainer, flags *vsServerFlags) actions.Action {
return &vsServerAction{
rootContainer: rootContainer,
flags: flags,
}
}
func (s *vsServerAction) Run(ctx context.Context) (*actions.ActionResult, error) {
listener, err := net.Listen("tcp", fmt.Sprintf("127.0.0.1:%d", s.flags.port))
if err != nil {
panic(err)
}
var versionRes contracts.VersionResult
versionSpec := internal.VersionInfo()
versionRes.Azd.Commit = versionSpec.Commit
versionRes.Azd.Version = versionSpec.Version.String()
res := contracts.VsServerResult{
Port: listener.Addr().(*net.TCPAddr).Port,
Pid: os.Getpid(),
VersionResult: versionRes,
}
resString, err := json.Marshal(res)
if err != nil {
return nil, err
}
fmt.Printf("%s\n", string(resString))
return nil, vsrpc.NewServer(s.rootContainer).Serve(listener)
}

Просмотреть файл

@ -5,7 +5,6 @@ package main
import (
"bytes"
"context"
"fmt"
"io"
"io/fs"
@ -54,7 +53,7 @@ func main() {
// staticHelp is true to inform commands to use generate help text instead
// of generating help text that includes execution-specific state.
cmd := azd.NewRootCmd(context.Background(), true, nil)
cmd := azd.NewRootCmd(true, nil, nil)
basename := strings.Replace(cmd.CommandPath(), " ", "_", -1) + ".md"
filename := filepath.Join("./md", basename)

Просмотреть файл

@ -0,0 +1,83 @@
# External Authentication
## Problem
As part of its operation, `azd` needs to make calls to different Azure services. For example `azd provison` calls the ARM control plane to submit a deployment. `azd deploy` may need to make management or data plane calls to deploy the customer code that it has built.
When using the CLI directly, it is natural to use the authentication information for the current logged in user (managed by `azd auth login`) but `azd` can also be used on behalf of another tool. For example, when using `azd` via the Visual Studio Code extension, it would be ideal if the operations used the same principal that the user is logged in with in the IDE.
The typical solution in the Azure SDKs is to have a credential type per authentication source (e.g we have a VisualStudioCodeCredential, a VisualStudioCredential) and have the tool select which credential type to use. In practice this has been fragile for a few reasons:
1. The impelemtnation of these credentials is often complex and hard to maintain and breaks over time.
2. We need a new credential type per dev tool and the dev tools needs to communicate with `azd` which credential to use.
Instead of the above strategy, we'd like a way for `azd` to hand off authentication requests to an external process (i.e. the process that launched `azd` to complete some end to end operation). We would like this solution to be simple and be implementable by multiple hosts without any changes to `azd`.
## Solution
We have introduced a feature similar to managed identity - `azd` can proxy GetToken requests from it's `TokenCredential` interface to a remote service, which will fetch a token and then return it to `azd`.
When run, `azd` looks for two special environment variables:
- `AZD_AUTH_ENDPOINT`
- `AZD_AUTH_KEY`
When both are set, instead of using the built in authentication information, a special `TokenCredential` instance is constructed and used. The implementation of `GetToken` of this credential makes a POST call to a special endpoint:
`${AZD_AUTH_ENDPOINT}/token?api-version=2023-07-12-preview`
Setting the following headers:
- `Content-Type: application/json`
- `Authorization: Bearer ${AZD_AUTH_KEY}`
The use of `AZD_AUTH_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 maps to the data passed to `GetToken` via the GetTokenOptions struct (we considered version 1.7.0 of the Azure SDK for Go core package):
```jsonc
{
"scopes": [ "scope1" /*, "scope2", ... */ ],
"tenantId": "<string>", // optional, used to override the default tenant.
}
```
The server should take this request and fetch a token using the given configuation and return it back to the client. The shape of the response looks like one of the following:
### Success
```jsonc
{
"result": "success",
"token": "<string>", // the access token.
"expiresOn": "<string>" // the expiration time of the token, expressed in RFC3339 format.
}
```
### Failure
```jsonc
{
"result": "error",
"code": "string", // one of "GetTokenError" or "NotSignedInError"
"message": "string" // a human readable error message.
}
```
`NotSignedInError` is the code that is returned whent the auth server detects that the user is not signed in, and can be used by the client to provide a better error experience. Other failures are returned as a `GetTokenError` and the message can match the error message returned by `GetToken` on the server.
The message is returned as is as the `error` for the `GetToken` call on the client side.
## Implementation
The `azd` CLI implements the client side of this feature in the [`pkg/auth/remote_credential.go`](../pkg/auth/remote_credential.go).
The VS Code implementation of the server is in [src/utils/authServer.ts](../../../ext/vscode/src/utils/).
## Open Issues
- [ ] As of `azcore@1.8.0`, there are now new additional properties on `TokenRequestOptions`: `EnableCAE` and `Claims` which are not yet supported by the external authentication flow. We need to support these properties in the external authentication flow (to do so we should bump the api-version and add the new parameters. They are both optional).
- [ ] Perhaps we should allow the host to respond to an OPTIONS request to `/token` to discover the API versions that the server supports, so we can call the latest version that the server supports, or fail if there server does not support some minimum version.
- [ ] How might we run this prototocol over JSON-RPC 2.0 as we do in our `vs-server` instead of HTTP?

Просмотреть файл

@ -175,7 +175,9 @@ var allDetectors = []projectDetector{
// TODO(ellismg): Remove ambient authority.
dotnetCli: dotnet.NewDotNetCli(exec.NewCommandRunner(nil)),
},
&dotNetDetector{},
&dotNetDetector{
dotnetCli: dotnet.NewDotNetCli(exec.NewCommandRunner(nil)),
},
&pythonDetector{},
&javaScriptDetector{},
}
@ -197,6 +199,17 @@ func DetectDirectory(ctx context.Context, directory string, options ...DetectDir
return detectAny(ctx, config.detectors, directory, entries)
}
func DetectAspireHosts(ctx context.Context, root string, dotnetCli dotnet.DotNetCli) ([]Project, error) {
config := newConfig()
config.detectors = []projectDetector{
&dotNetAppHostDetector{
dotnetCli: dotnetCli,
},
}
return detectUnder(ctx, root, config)
}
func detectUnder(ctx context.Context, root string, config detectConfig) ([]Project, error) {
projects := []Project{}

Просмотреть файл

@ -34,7 +34,7 @@ func TestDetect(t *testing.T) {
{
Language: DotNet,
Path: "dotnet",
DetectionRule: "Inferred by presence of: dotnettestapp.csproj, program.cs",
DetectionRule: "Inferred by presence of: dotnettestapp.csproj, Program.cs",
},
{
Language: Java,
@ -104,7 +104,7 @@ func TestDetect(t *testing.T) {
{
Language: DotNet,
Path: "dotnet",
DetectionRule: "Inferred by presence of: dotnettestapp.csproj, program.cs",
DetectionRule: "Inferred by presence of: dotnettestapp.csproj, Program.cs",
},
{
Language: Java,
@ -123,7 +123,7 @@ func TestDetect(t *testing.T) {
{
Language: DotNet,
Path: "dotnet",
DetectionRule: "Inferred by presence of: dotnettestapp.csproj, program.cs",
DetectionRule: "Inferred by presence of: dotnettestapp.csproj, Program.cs",
},
{
Language: Java,
@ -145,7 +145,7 @@ func TestDetect(t *testing.T) {
{
Language: DotNet,
Path: "dotnet",
DetectionRule: "Inferred by presence of: dotnettestapp.csproj, program.cs",
DetectionRule: "Inferred by presence of: dotnettestapp.csproj, Program.cs",
},
{
Language: Java,
@ -191,7 +191,7 @@ func TestDetectDocker(t *testing.T) {
require.Equal(t, projects[0], Project{
Language: DotNet,
Path: filepath.Join(dir, "dotnet"),
DetectionRule: "Inferred by presence of: dotnettestapp.csproj, program.cs",
DetectionRule: "Inferred by presence of: dotnettestapp.csproj, Program.cs",
Docker: &Docker{
Path: filepath.Join(dir, "dotnet", "Dockerfile"),
},
@ -218,7 +218,7 @@ func TestDetectNested(t *testing.T) {
require.Equal(t, projects[0], Project{
Language: DotNet,
Path: filepath.Join(src, "dotnet"),
DetectionRule: "Inferred by presence of: dotnettestapp.csproj, program.cs",
DetectionRule: "Inferred by presence of: dotnettestapp.csproj, Program.cs",
})
}

Просмотреть файл

@ -4,11 +4,15 @@ import (
"context"
"fmt"
"io/fs"
"log"
"path/filepath"
"strings"
"github.com/azure/azure-dev/cli/azd/pkg/tools/dotnet"
)
type dotNetDetector struct {
dotnetCli dotnet.DotNetCli
}
func (dd *dotNetDetector) Language() Language {
@ -28,8 +32,7 @@ func (dd *dotNetDetector) DetectProject(ctx context.Context, path string, entrie
// This detection logic doesn't work if Program.cs has been renamed, or moved into a different directory.
// A true detection of an "Application" is much harder since ASP .NET applications are just libraries
// that are ran with "dotnet run".
name = strings.ToLower(name)
switch name {
switch strings.ToLower(name) {
case "program.cs", "program.fs", "program.vb":
hasStartupFile = true
startUpFileName = name
@ -43,6 +46,13 @@ func (dd *dotNetDetector) DetectProject(ctx context.Context, path string, entrie
}
if hasProjectFile && hasStartupFile {
projectPath := filepath.Join(path, projFileName)
if isWasm, err := dd.isWasmProject(ctx, projectPath); err != nil {
log.Printf("error checking if %s is a browser-wasm project: %v", projectPath, err)
} else if isWasm { // Web assembly projects currently not supported as a hosted application project
return nil, filepath.SkipDir
}
return &Project{
Language: DotNet,
Path: path,
@ -52,3 +62,12 @@ func (dd *dotNetDetector) DetectProject(ctx context.Context, path string, entrie
return nil, nil
}
func (ad *dotNetDetector) isWasmProject(ctx context.Context, projectPath string) (bool, error) {
value, err := ad.dotnetCli.GetMsBuildProperty(ctx, projectPath, "RuntimeIdentifier")
if err != nil {
return false, err
}
return strings.TrimSpace(value) == "browser-wasm", nil
}

Просмотреть файл

@ -2,7 +2,7 @@
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net6.0</TargetFramework>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>

Просмотреть файл

@ -0,0 +1,42 @@
package cmd
import (
"fmt"
"slices"
"strings"
"github.com/azure/azure-dev/cli/azd/pkg/output"
)
// formatHelpNote provides the expected format in description notes using `•`.
func formatHelpNote(note string) string {
return fmt.Sprintf(" • %s", note)
}
// generateCmdHelpDescription construct a help text block from a title and description notes.
func generateCmdHelpDescription(title string, notes []string) string {
var note string
if len(notes) > 0 {
note = fmt.Sprintf("%s\n\n", strings.Join(notes, "\n"))
}
return fmt.Sprintf("%s\n\n%s", title, note)
}
// generateCmdHelpSamplesBlock converts the samples within the input `samples` to a help text block describing each sample
// title and the command to run it.
func generateCmdHelpSamplesBlock(samples map[string]string) string {
SamplesCount := len(samples)
if SamplesCount == 0 {
return ""
}
var lines []string
for title, command := range samples {
lines = append(lines, fmt.Sprintf(" %s\n %s", title, command))
}
// sorting lines to keep a deterministic output, as map[string]string is not ordered
slices.Sort(lines)
return fmt.Sprintf("%s\n%s\n",
output.WithBold(output.WithUnderline("Examples")),
strings.Join(lines, "\n\n"),
)
}

Просмотреть файл

@ -11,7 +11,6 @@ import (
"time"
"github.com/azure/azure-dev/cli/azd/cmd/actions"
"github.com/azure/azure-dev/cli/azd/cmd/middleware"
"github.com/azure/azure-dev/cli/azd/internal"
"github.com/azure/azure-dev/cli/azd/pkg/account"
"github.com/azure/azure-dev/cli/azd/pkg/alpha"
@ -27,20 +26,20 @@ import (
"github.com/spf13/pflag"
)
type deployFlags struct {
type DeployFlags struct {
serviceName string
all bool
All bool
fromPackage string
global *internal.GlobalCommandOptions
*envFlag
*internal.EnvFlag
}
func (d *deployFlags) Bind(local *pflag.FlagSet, global *internal.GlobalCommandOptions) {
d.bindNonCommon(local, global)
func (d *DeployFlags) Bind(local *pflag.FlagSet, global *internal.GlobalCommandOptions) {
d.BindNonCommon(local, global)
d.bindCommon(local, global)
}
func (d *deployFlags) bindNonCommon(
func (d *DeployFlags) BindNonCommon(
local *pflag.FlagSet,
global *internal.GlobalCommandOptions) {
local.StringVar(
@ -55,12 +54,12 @@ func (d *deployFlags) bindNonCommon(
d.global = global
}
func (d *deployFlags) bindCommon(local *pflag.FlagSet, global *internal.GlobalCommandOptions) {
d.envFlag = &envFlag{}
d.envFlag.Bind(local, global)
func (d *DeployFlags) bindCommon(local *pflag.FlagSet, global *internal.GlobalCommandOptions) {
d.EnvFlag = &internal.EnvFlag{}
d.EnvFlag.Bind(local, global)
local.BoolVar(
&d.all,
&d.All,
"all",
false,
"Deploys all services that are listed in "+azdcontext.ProjectFileName,
@ -73,18 +72,25 @@ func (d *deployFlags) bindCommon(local *pflag.FlagSet, global *internal.GlobalCo
)
}
func (d *deployFlags) setCommon(envFlag *envFlag) {
d.envFlag = envFlag
func (d *DeployFlags) SetCommon(envFlag *internal.EnvFlag) {
d.EnvFlag = envFlag
}
func newDeployFlags(cmd *cobra.Command, global *internal.GlobalCommandOptions) *deployFlags {
flags := &deployFlags{}
func NewDeployFlags(cmd *cobra.Command, global *internal.GlobalCommandOptions) *DeployFlags {
flags := &DeployFlags{}
flags.Bind(cmd.Flags(), global)
return flags
}
func newDeployCmd() *cobra.Command {
func NewDeployFlagsFromEnvAndOptions(envFlag *internal.EnvFlag, global *internal.GlobalCommandOptions) *DeployFlags {
return &DeployFlags{
EnvFlag: envFlag,
global: global,
}
}
func NewDeployCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "deploy <service>",
Short: "Deploy the application's code to Azure.",
@ -94,8 +100,8 @@ func newDeployCmd() *cobra.Command {
return cmd
}
type deployAction struct {
flags *deployFlags
type DeployAction struct {
flags *DeployFlags
args []string
projectConfig *project.ProjectConfig
azdCtx *azdcontext.AzdContext
@ -109,14 +115,12 @@ type deployAction struct {
writer io.Writer
console input.Console
commandRunner exec.CommandRunner
middlewareRunner middleware.MiddlewareContext
packageActionInitializer actions.ActionInitializer[*packageAction]
alphaFeatureManager *alpha.FeatureManager
importManager *project.ImportManager
}
func newDeployAction(
flags *deployFlags,
func NewDeployAction(
flags *DeployFlags,
args []string,
projectConfig *project.ProjectConfig,
projectManager project.ProjectManager,
@ -130,12 +134,10 @@ func newDeployAction(
console input.Console,
formatter output.Formatter,
writer io.Writer,
middlewareRunner middleware.MiddlewareContext,
packageActionInitializer actions.ActionInitializer[*packageAction],
alphaFeatureManager *alpha.FeatureManager,
importManager *project.ImportManager,
) actions.Action {
return &deployAction{
return &DeployAction{
flags: flags,
args: args,
projectConfig: projectConfig,
@ -150,8 +152,6 @@ func newDeployAction(
writer: writer,
console: console,
commandRunner: commandRunner,
middlewareRunner: middlewareRunner,
packageActionInitializer: packageActionInitializer,
alphaFeatureManager: alphaFeatureManager,
importManager: importManager,
}
@ -162,7 +162,7 @@ type DeploymentResult struct {
Services map[string]*project.ServiceDeployResult `json:"services"`
}
func (da *deployAction) Run(ctx context.Context) (*actions.ActionResult, error) {
func (da *DeployAction) Run(ctx context.Context) (*actions.ActionResult, error) {
targetServiceName := da.flags.serviceName
if len(da.args) == 1 {
targetServiceName = da.args[0]
@ -183,13 +183,13 @@ func (da *deployAction) Run(ctx context.Context) (*actions.ActionResult, error)
da.projectConfig,
string(project.ServiceEventDeploy),
targetServiceName,
da.flags.all,
da.flags.All,
)
if err != nil {
return nil, err
}
if da.flags.all && da.flags.fromPackage != "" {
if da.flags.All && da.flags.fromPackage != "" {
return nil, errors.New(
"'--from-package' cannot be specified when '--all' is set. Specify a specific service by passing a <service>")
}
@ -313,7 +313,7 @@ func (da *deployAction) Run(ctx context.Context) (*actions.ActionResult, error)
}, nil
}
func getCmdDeployHelpDescription(*cobra.Command) string {
func GetCmdDeployHelpDescription(*cobra.Command) string {
return generateCmdHelpDescription("Deploy application to Azure.", []string{
formatHelpNote(
"By default, deploys all services listed in 'azure.yaml' in the current directory," +
@ -325,7 +325,7 @@ func getCmdDeployHelpDescription(*cobra.Command) string {
})
}
func getCmdDeployHelpFooter(*cobra.Command) string {
func GetCmdDeployHelpFooter(*cobra.Command) string {
return generateCmdHelpSamplesBlock(map[string]string{
"Deploy all services in the current project to Azure.": output.WithHighLightFormat(
"azd deploy --all",

Просмотреть файл

@ -16,6 +16,7 @@ import (
"github.com/azure/azure-dev/cli/azd/pkg/input"
"github.com/azure/azure-dev/cli/azd/pkg/output"
"github.com/azure/azure-dev/cli/azd/pkg/output/ux"
"github.com/azure/azure-dev/cli/azd/pkg/password"
"github.com/azure/azure-dev/cli/azd/pkg/project"
"github.com/azure/azure-dev/cli/azd/pkg/tools/azcli"
"github.com/spf13/cobra"
@ -23,12 +24,12 @@ import (
"go.uber.org/multierr"
)
type provisionFlags struct {
type ProvisionFlags struct {
noProgress bool
preview bool
ignoreDeploymentState bool
global *internal.GlobalCommandOptions
*envFlag
*internal.EnvFlag
}
const (
@ -38,19 +39,19 @@ const (
azurePortalURL = "https://ms.portal.azure.com/"
)
func (i *provisionFlags) Bind(local *pflag.FlagSet, global *internal.GlobalCommandOptions) {
i.bindNonCommon(local, global)
func (i *ProvisionFlags) Bind(local *pflag.FlagSet, global *internal.GlobalCommandOptions) {
i.BindNonCommon(local, global)
i.bindCommon(local, global)
}
func (i *provisionFlags) bindNonCommon(local *pflag.FlagSet, global *internal.GlobalCommandOptions) {
func (i *ProvisionFlags) BindNonCommon(local *pflag.FlagSet, global *internal.GlobalCommandOptions) {
local.BoolVar(&i.noProgress, "no-progress", false, "Suppresses progress information.")
//deprecate:Flag hide --no-progress
_ = local.MarkHidden("no-progress")
i.global = global
}
func (i *provisionFlags) bindCommon(local *pflag.FlagSet, global *internal.GlobalCommandOptions) {
func (i *ProvisionFlags) bindCommon(local *pflag.FlagSet, global *internal.GlobalCommandOptions) {
local.BoolVar(&i.preview, "preview", false, "Preview changes to Azure resources.")
local.BoolVar(
&i.ignoreDeploymentState,
@ -58,34 +59,44 @@ func (i *provisionFlags) bindCommon(local *pflag.FlagSet, global *internal.Globa
false,
"Do not use latest Deployment State (bicep only).")
i.envFlag = &envFlag{}
i.envFlag.Bind(local, global)
i.EnvFlag = &internal.EnvFlag{}
i.EnvFlag.Bind(local, global)
}
func (i *provisionFlags) setCommon(envFlag *envFlag) {
i.envFlag = envFlag
func (i *ProvisionFlags) SetCommon(envFlag *internal.EnvFlag) {
i.EnvFlag = envFlag
}
func newProvisionFlags(cmd *cobra.Command, global *internal.GlobalCommandOptions) *provisionFlags {
flags := &provisionFlags{}
func NewProvisionFlags(cmd *cobra.Command, global *internal.GlobalCommandOptions) *ProvisionFlags {
flags := &ProvisionFlags{}
flags.Bind(cmd.Flags(), global)
return flags
}
func newProvisionCmd() *cobra.Command {
func NewProvisionFlagsFromEnvAndOptions(envFlag *internal.EnvFlag, global *internal.GlobalCommandOptions) *ProvisionFlags {
flags := &ProvisionFlags{
EnvFlag: envFlag,
global: global,
}
return flags
}
func NewProvisionCmd() *cobra.Command {
return &cobra.Command{
Use: "provision",
Short: "Provision the Azure resources for an application.",
}
}
type provisionAction struct {
flags *provisionFlags
type ProvisionAction struct {
flags *ProvisionFlags
provisionManager *provisioning.Manager
projectManager project.ProjectManager
resourceManager project.ResourceManager
env *environment.Environment
envManager environment.Manager
formatter output.Formatter
projectConfig *project.ProjectConfig
writer io.Writer
@ -94,25 +105,27 @@ type provisionAction struct {
importManager *project.ImportManager
}
func newProvisionAction(
flags *provisionFlags,
func NewProvisionAction(
flags *ProvisionFlags,
provisionManager *provisioning.Manager,
projectManager project.ProjectManager,
importManager *project.ImportManager,
resourceManager project.ResourceManager,
projectConfig *project.ProjectConfig,
env *environment.Environment,
envManager environment.Manager,
console input.Console,
formatter output.Formatter,
writer io.Writer,
subManager *account.SubscriptionsManager,
) actions.Action {
return &provisionAction{
return &ProvisionAction{
flags: flags,
provisionManager: provisionManager,
projectManager: projectManager,
resourceManager: resourceManager,
env: env,
envManager: envManager,
formatter: formatter,
projectConfig: projectConfig,
writer: writer,
@ -122,7 +135,16 @@ func newProvisionAction(
}
}
func (p *provisionAction) Run(ctx context.Context) (*actions.ActionResult, error) {
// SetFlags sets the flags for the provision action. Panics if `flags` is nil
func (p *ProvisionAction) SetFlags(flags *ProvisionFlags) {
if flags == nil {
panic("flags is nil")
}
p.flags = flags
}
func (p *ProvisionAction) Run(ctx context.Context) (*actions.ActionResult, error) {
if p.flags.noProgress {
fmt.Fprintln(
p.console.Handles().Stderr,
@ -159,6 +181,37 @@ func (p *provisionAction) Run(ctx context.Context) (*actions.ActionResult, error
}
defer func() { _ = infra.Cleanup() }()
wroteNewInput := false
for inputName, inputInfo := range infra.Inputs {
inputConfigKey := fmt.Sprintf("inputs.%s", inputName)
if _, has := p.env.Config.GetString(inputConfigKey); !has {
// No value found, so we need to generate one, and store it in the config bag.
//
// TODO(ellismg): Today this dereference is safe because when loading a manifest we validate that every
// input has a generate block with a min length property. We would like to relax this in Preview 3 to
// to support cases where this is not the case (and we'd prompt for the value). When we do that, we'll need
// to audit these dereferences to check for nil.
val, err := password.FromAlphabet(password.LettersAndDigits, *inputInfo.Default.Generate.MinLength)
if err != nil {
return nil, fmt.Errorf("generating value for input %s: %w", inputName, err)
}
if err := p.env.Config.Set(inputConfigKey, val); err != nil {
return nil, fmt.Errorf("saving value for input %s: %w", inputName, err)
}
wroteNewInput = true
}
}
if wroteNewInput {
if err := p.envManager.Save(ctx, p.env); err != nil {
return nil, fmt.Errorf("saving environment: %w", err)
}
}
infraOptions := infra.Options
infraOptions.IgnoreDeploymentState = p.flags.ignoreDeploymentState
if err := p.provisionManager.Initialize(ctx, p.projectConfig.Path, infraOptions); err != nil {
@ -335,7 +388,7 @@ func deployResultToUx(previewResult *provisioning.DeployPreviewResult) ux.UxItem
}
}
func getCmdProvisionHelpDescription(c *cobra.Command) string {
func GetCmdProvisionHelpDescription(c *cobra.Command) string {
return generateCmdHelpDescription(fmt.Sprintf(
"Provision the Azure resources for an application."+
" This step may take a while depending on the resources provisioned."+

Просмотреть файл

@ -0,0 +1,112 @@
package cmd
import (
"context"
"errors"
"fmt"
"time"
"github.com/azure/azure-dev/cli/azd/internal/tracing"
"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/azure/azure-dev/cli/azd/pkg/project"
)
func getResourceGroupFollowUp(
ctx context.Context,
formatter output.Formatter,
projectConfig *project.ProjectConfig,
resourceManager project.ResourceManager,
env *environment.Environment,
whatIf bool,
) (followUp string) {
if formatter.Kind() == output.JsonFormat {
return followUp
}
subscriptionId := env.GetSubscriptionId()
if resourceGroupName, err := resourceManager.GetResourceGroupName(ctx, subscriptionId, projectConfig); err == nil {
defaultFollowUpText := fmt.Sprintf(
"You can view the resources created under the resource group %s in Azure Portal:", resourceGroupName)
if whatIf {
defaultFollowUpText = fmt.Sprintf(
"You can view the current resources under the resource group %s in Azure Portal:", resourceGroupName)
}
followUp = fmt.Sprintf("%s\n%s",
defaultFollowUpText,
azurePortalLink(subscriptionId, resourceGroupName))
}
return followUp
}
func azurePortalLink(subscriptionId, resourceGroupName string) string {
if subscriptionId == "" || resourceGroupName == "" {
return ""
}
return output.WithLinkFormat(fmt.Sprintf(
"https://portal.azure.com/#@/resource/subscriptions/%s/resourceGroups/%s/overview",
subscriptionId,
resourceGroupName))
}
func serviceNameWarningCheck(console input.Console, serviceNameFlag string, commandName string) {
if serviceNameFlag == "" {
return
}
fmt.Fprintln(
console.Handles().Stderr,
output.WithWarningFormat("WARNING: The `--service` flag is deprecated and will be removed in a future release."),
)
fmt.Fprintf(console.Handles().Stderr, "Next time use `azd %s <service>`.\n\n", commandName)
}
func getTargetServiceName(
ctx context.Context,
projectManager project.ProjectManager,
importManager *project.ImportManager,
projectConfig *project.ProjectConfig,
commandName string,
targetServiceName string,
allFlagValue bool,
) (string, error) {
if allFlagValue && targetServiceName != "" {
return "", fmt.Errorf("cannot specify both --all and <service>")
}
if !allFlagValue && targetServiceName == "" {
targetService, err := projectManager.DefaultServiceFromWd(ctx, projectConfig)
if errors.Is(err, project.ErrNoDefaultService) {
return "", fmt.Errorf(
"current working directory is not a project or service directory. Specify a service name to %s a service, "+
"or specify --all to %s all services",
commandName,
commandName,
)
} else if err != nil {
return "", err
}
if targetService != nil {
targetServiceName = targetService.Name
}
}
if targetServiceName != "" {
if has, err := importManager.HasService(ctx, projectConfig, targetServiceName); err != nil {
return "", err
} else if !has {
return "", fmt.Errorf("service name '%s' doesn't exist", targetServiceName)
}
}
return targetServiceName, nil
}
// Calculate the total time since t, excluding user interaction time.
func since(t time.Time) time.Duration {
userInteractTime := tracing.InteractTimeMs.Load()
return time.Since(t) - time.Duration(userInteractTime)*time.Millisecond
}

Просмотреть файл

@ -0,0 +1,29 @@
package internal
import (
"os"
"github.com/spf13/pflag"
)
// EnvFlag is a flag that represents the environment name. Actions which inject an environment should also use this flag
// so the user can control what environment is loaded in a uniform way across all our commands.
type EnvFlag struct {
EnvironmentName string
}
// EnvironmentNameFlagName is the full name of the flag as it appears on the command line.
const EnvironmentNameFlagName string = "environment"
// envNameEnvVarName is the same as environment.EnvNameEnvVarName, but duplicated here to prevent an import cycle.
const envNameEnvVarName = "AZURE_ENV_NAME"
func (e *EnvFlag) Bind(local *pflag.FlagSet, global *GlobalCommandOptions) {
local.StringVarP(
&e.EnvironmentName,
EnvironmentNameFlagName,
"e",
// Set the default value to AZURE_ENV_NAME value if available
os.Getenv(envNameEnvVarName),
"The name of the environment to use.")
}

Просмотреть файл

@ -6,6 +6,7 @@ import (
"fmt"
"os"
"path/filepath"
"strings"
"time"
"github.com/azure/azure-dev/cli/azd/internal"
@ -22,7 +23,6 @@ import (
"github.com/azure/azure-dev/cli/azd/pkg/output/ux"
"github.com/azure/azure-dev/cli/azd/pkg/project"
"github.com/otiai10/copy"
"golang.org/x/exp/slices"
)
var languageMap = map[appdetect.Language]project.ServiceLanguageKind{
@ -88,15 +88,50 @@ func (i *Initializer) InitFromApp(
continue
}
manifest, err := apphost.ManifestFromAppHost(ctx, prj.Path, i.dotnetCli)
manifest, err := apphost.ManifestFromAppHost(ctx, prj.Path, i.dotnetCli, "")
if err != nil {
return fmt.Errorf("failed to generate manifest from app host project: %w", err)
}
appHostManifests[prj.Path] = manifest
// Load projects referenced by the App Host,
// ensuring that projects are located under the azd project directory.
const parentDir = ".." + string(os.PathSeparator)
relParentCount := 0
relParentProject := ""
// Use canonical paths for Rel comparison due to absolute paths provided by ManifestFromAppHost
// being possibly symlinked paths.
compWd, err := filepath.EvalSymlinks(wd)
if err != nil {
return err
}
for _, path := range apphost.ProjectPaths(manifest) {
normalPath, err := filepath.EvalSymlinks(path)
if err != nil {
return err
}
rel, err := filepath.Rel(compWd, normalPath)
if err != nil {
return err
}
if parentCount := countPrefix(rel, parentDir); parentCount > relParentCount {
relParentCount = parentCount
relParentProject = rel
}
appHostForProject[filepath.Dir(path)] = prj.Path
}
if relParentCount > 0 {
return fmt.Errorf(
"found project %s located not under the current directory. To fix, rerun `azd init` in directory %s",
relParentProject,
filepath.Clean(filepath.Join(wd, strings.Repeat(parentDir, relParentCount))))
}
}
// Filter out all the projects owned by an App Host.
@ -117,22 +152,52 @@ func (i *Initializer) InitFromApp(
}
i.console.StopSpinner(ctx, title, input.StepDone)
isDotNetAppHost := func(p appdetect.Project) bool { return p.Language == appdetect.DotNetAppHost }
if idx := slices.IndexFunc(projects, isDotNetAppHost); idx >= 0 {
// TODO(ellismg): We will have to figure out how to relax this over time.
if len(projects) != 1 {
return errors.New("only a single Aspire project is supported at this time")
var prjAppHost []appdetect.Project
for _, prj := range projects {
if prj.Language == appdetect.DotNetAppHost {
prjAppHost = append(prjAppHost, prj)
}
}
if len(prjAppHost) > 1 {
relPaths := make([]string, 0, len(prjAppHost))
for _, appHost := range prjAppHost {
rel, _ := filepath.Rel(wd, appHost.Path)
relPaths = append(relPaths, rel)
}
return fmt.Errorf(
"only a single Aspire app host project is supported at this time, found multiple: %s",
ux.ListAsText(relPaths))
}
if len(prjAppHost) == 1 {
appHost := prjAppHost[0]
otherProjects := make([]string, 0, len(projects))
for _, prj := range projects {
if prj.Language != appdetect.DotNetAppHost {
rel, _ := filepath.Rel(wd, prj.Path)
otherProjects = append(otherProjects, rel)
}
}
if len(otherProjects) > 0 {
i.console.Message(
ctx,
output.WithWarningFormat(
"\nIgnoring other projects present but not referenced by app host: %s",
ux.ListAsText(otherProjects)))
}
detect := detectConfirmAppHost{console: i.console}
detect.Init(projects[idx], wd)
detect.Init(appHost, wd)
if err := detect.Confirm(ctx); err != nil {
return err
}
// Figure out what services to expose.
ingressSelector := apphost.NewIngressSelector(appHostManifests[projects[idx].Path], i.console)
ingressSelector := apphost.NewIngressSelector(appHostManifests[appHost.Path], i.console)
tracing.SetUsageAttributes(fields.AppInitLastStep.String("modify"))
exposed, err := ingressSelector.SelectPublicServices(ctx)
@ -168,8 +233,8 @@ func (i *Initializer) InitFromApp(
ctx,
azdCtx.ProjectDirectory(),
filepath.Base(azdCtx.ProjectDirectory()),
appHostManifests[projects[idx].Path],
projects[idx].Path,
appHostManifests[appHost.Path],
appHost.Path,
)
if err != nil {
return err
@ -403,3 +468,14 @@ func prjConfigFromDetect(
return config, nil
}
func countPrefix(s string, prefix string) int {
count := 0
for strings.HasPrefix(s, prefix) {
count++
// len(s) >= len(prefix) guaranteed by HasPrefix
s = s[len(prefix):]
}
return count
}

Просмотреть файл

@ -77,8 +77,12 @@ func (d *detectConfirmAppHost) render(ctx context.Context) error {
d.console.Message(ctx, " "+color.BlueString(projectDisplayName(d.AppHost)))
d.console.Message(ctx, " "+"Detected in: "+output.WithHighLightFormat(relSafe(d.root, d.AppHost.Path)))
d.console.Message(ctx, "")
d.console.Message(ctx,
"azd will generate the files necessary to host your app on Azure using "+color.MagentaString("Azure Container Apps")+".\n")
d.console.Message(
ctx,
"azd will generate the files necessary to host your app on Azure using "+color.MagentaString(
"Azure Container Apps",
)+".\n",
)
return nil
}

Просмотреть файл

@ -14,7 +14,6 @@ import (
"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/infra/provisioning/bicep"
"github.com/azure/azure-dev/cli/azd/pkg/input"
"github.com/azure/azure-dev/cli/azd/pkg/lazy"
"github.com/azure/azure-dev/cli/azd/pkg/osutil"
@ -341,7 +340,7 @@ func (i *Initializer) InitializeMinimal(ctx context.Context, azdCtx *azdcontext.
// Default infra path if not specified
infraPath := projectConfig.Infra.Path
if infraPath == "" {
infraPath = bicep.Defaults.Path
infraPath = project.DefaultPath
}
err = os.MkdirAll(infraPath, osutil.PermissionDirectory)
@ -351,7 +350,7 @@ func (i *Initializer) InitializeMinimal(ctx context.Context, azdCtx *azdcontext.
module := projectConfig.Infra.Module
if projectConfig.Infra.Module == "" {
module = bicep.Defaults.Module
module = project.DefaultModule
}
mainPath := filepath.Join(infraPath, module)

Просмотреть файл

@ -3,6 +3,7 @@ package scaffold
import (
"encoding/json"
"fmt"
"regexp"
"strings"
)
@ -102,16 +103,14 @@ func lowerCase(r byte) byte {
// 3. Bicep resource token (13 characters) + separator '-' (1 character) -- total of 14 characters
//
// Which leaves us with: 32 - 4 - 14 = 14 characters.
// We allow 2 additional characters for wiggle-room. We've seen failures when container app name is exactly at 32.
const containerAppNameInfixMaxLen = 12
// ContainerAppName returns a name that is valid to be used as an infix for a container app resource.
//
// The name is treated to only contain alphanumeric and dash characters, with no repeated dashes, and no dashes
// as the first or last character.
func ContainerAppName(name string) string {
if len(name) > containerAppNameInfixMaxLen {
name = name[:containerAppNameInfixMaxLen]
// We allow 2 additional characters for wiggle-room. We've seen failures when container app name is exactly at 32.
const containerAppNameMaxLen = 30
func containerAppName(name string, maxLen int) string {
if len(name) > maxLen {
name = name[:maxLen]
}
// trim to allowed characters:
@ -143,6 +142,63 @@ func ContainerAppName(name string) string {
return sb.String()
}
// ContainerAppName returns a suitable name a container app resource.
//
// The name is treated to only contain alphanumeric and dash characters, with no repeated dashes, and no dashes
// as the first or last character.
func ContainerAppName(name string) string {
return containerAppName(name, containerAppNameMaxLen)
}
// ContainerAppSecretName returns a suitable name a container app secret name.
//
// The name is treated to only contain lowercase alphanumeric and dash characters, and must start and end with an
// alphanumeric character
func ContainerAppSecretName(name string) string {
return strings.ReplaceAll(strings.ToLower(name), "_", "-")
}
// alphanumericAndDashesRegex is a regular expression pattern used to match alphanumeric characters and dashes enclosed
// in square brackets.
var alphanumericAndDashesRegex = regexp.MustCompile(`(\['[a-zA-Z0-9\-]+'\])`)
// ToDotNotation receives a string and if it is on the form of "${inputs['resourceName']['inputName']}" it returns a new
// string using dot notation, i.e. "${inputs.resourceName.InputName}".
// Otherwise, the original string is returned adding quotes.
// Note: If resourceName or inputName container `-`
func ToDotNotation(s string) string {
if strings.HasPrefix(s, "${inputs['") && strings.HasSuffix(s, "']}") {
updated := alphanumericAndDashesRegex.ReplaceAllStringFunc(s, func(sub string) string {
noBrackets := strings.TrimRight(strings.TrimLeft(sub, "['"), "']")
if !strings.Contains(noBrackets, "-") {
return "." + noBrackets
}
return sub
})
return strings.TrimRight(strings.TrimLeft(updated, "${"), "}")
}
return fmt.Sprintf("'%s'", s)
}
// camelCaseRegex is a regular expression used to match camel case patterns.
// It matches a lowercase letter or digit followed by an uppercase letter.
var camelCaseRegex = regexp.MustCompile(`([a-z0-9])([A-Z])`)
// EnvFormat takes an input parameter like `fooParam` which is expected to be in camel case and returns it in
// upper snake case with env var template, like `${AZURE_FOO_PARAM}`.
func EnvFormat(src string) string {
snake := strings.ToUpper(camelCaseRegex.ReplaceAllString(src, "${1}_${2}"))
return fmt.Sprintf("${AZURE_%s}", snake)
}
// ContainerAppInfix returns a suitable infix for a container app resource.
//
// The name is treated to only contain alphanumeric and dash characters, with no repeated dashes, and no dashes
// as the first or last character.
func ContainerAppInfix(name string) string {
return containerAppName(name, containerAppNameInfixMaxLen)
}
// Formats a parameter value for use in a bicep file.
// If the value is a string, it is quoted inline with no indentation.
// Otherwise, the value is marshaled with indentation specified by prefix and indent.

Просмотреть файл

@ -6,7 +6,7 @@ import (
"github.com/stretchr/testify/assert"
)
func Test_ContainerAppName(t *testing.T) {
func Test_containerAppName(t *testing.T) {
tests := []struct {
name string
in string
@ -20,7 +20,7 @@ func Test_ContainerAppName(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
actual := ContainerAppName(tt.in)
actual := containerAppName(tt.in, 12)
assert.Equal(t, tt.want, actual)
})
}
@ -64,3 +64,90 @@ func Test_AlphaUpperSnake(t *testing.T) {
})
}
}
func Test_ToDotNotation(t *testing.T) {
tests := []struct {
name string
input string
expected string
}{
{
name: "valid input",
input: "${inputs['my-input']['myinput']}",
expected: "inputs['my-input'].myinput",
},
{
name: "non inputs",
input: "my-input",
expected: "'my-input'",
},
{
name: "input with hyphen",
input: "${inputs['my-input-with-hyphen']['other-foo']}",
expected: "inputs['my-input-with-hyphen']['other-foo']",
},
{
name: "input with hyphen 2",
input: "${inputs['my']['other-foo']}",
expected: "inputs.my['other-foo']",
},
{
name: "input with multiple levels",
input: "${inputs['level1']['level2']}",
expected: "inputs.level1.level2",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
actual := ToDotNotation(tt.input)
assert.Equal(t, tt.expected, actual)
})
}
}
func Test_EnvFormat(t *testing.T) {
tests := []struct {
name string
input string
expected string
}{
{
name: "no uppercase letters",
input: "myenv",
expected: "${AZURE_MYENV}",
},
{
name: "single uppercase letter",
input: "myEnv",
expected: "${AZURE_MY_ENV}",
},
{
name: "multiple uppercase letters",
input: "myEnvFormat",
expected: "${AZURE_MY_ENV_FORMAT}",
},
{
name: "uppercase letters at the beginning",
input: "EnvFormat",
expected: "${AZURE_ENV_FORMAT}",
},
{
name: "uppercase letters at the end",
input: "envFormaT",
expected: "${AZURE_ENV_FORMA_T}",
},
{
name: "uppercase letters in the middle",
input: "envFormatString",
expected: "${AZURE_ENV_FORMAT_STRING}",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
actual := EnvFormat(tt.input)
assert.Equal(t, tt.expected, actual)
})
}
}

Просмотреть файл

@ -55,7 +55,7 @@ func copyFS(embedFs fs.FS, root string, target string) error {
func Load() (*template.Template, error) {
funcMap := template.FuncMap{
"bicepName": BicepName,
"containerAppName": ContainerAppName,
"containerAppInfix": ContainerAppInfix,
"upper": strings.ToUpper,
"lower": strings.ToLower,
"formatParam": FormatParameter,

Просмотреть файл

@ -0,0 +1,139 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
package vsrpc
import (
"context"
"errors"
"fmt"
"net/http"
"os"
"path/filepath"
"github.com/azure/azure-dev/cli/azd/internal/appdetect"
"github.com/azure/azure-dev/cli/azd/pkg/apphost"
"github.com/azure/azure-dev/cli/azd/pkg/environment/azdcontext"
"github.com/azure/azure-dev/cli/azd/pkg/project"
"github.com/azure/azure-dev/cli/azd/pkg/tools/dotnet"
)
// aspireService is the RPC server for the '/AspireService/v1.0' endpoint.
type aspireService struct {
server *Server
}
func newAspireService(server *Server) *aspireService {
return &aspireService{
server: server,
}
}
// GetAspireHostAsync is the server implementation of:
// ValueTask<AspireHost> GetAspireHostAsync(Session session, string aspireEnv, CancellationToken cancellationToken).
func (s *aspireService) GetAspireHostAsync(
ctx context.Context, sessionId Session, aspireEnv string, observer IObserver[ProgressMessage],
) (*AspireHost, error) {
session, err := s.server.validateSession(ctx, sessionId)
if err != nil {
return nil, err
}
var c struct {
azdContext *azdcontext.AzdContext `container:"type"`
dotnetCli dotnet.DotNetCli `container:"type"`
}
container, err := session.newContainer()
if err != nil {
return nil, err
}
if err := container.Fill(&c); err != nil {
return nil, err
}
// If there is an azure.yaml, load it and return the services.
if _, err := os.Stat(c.azdContext.ProjectPath()); err == nil {
var cc struct {
projectConfig *project.ProjectConfig `container:"type"`
}
if err := container.Fill(&cc); err != nil {
return nil, err
}
appHost, err := appHostForProject(ctx, cc.projectConfig, c.dotnetCli)
if err != nil {
return nil, err
}
hostInfo := &AspireHost{
Name: filepath.Base(filepath.Dir(appHost.Path())),
Path: appHost.Path(),
}
manifest, err := apphost.ManifestFromAppHost(ctx, appHost.Path(), c.dotnetCli, aspireEnv)
if err != nil {
return nil, fmt.Errorf("failed to load app host manifest: %w", err)
}
hostInfo.Services = servicesFromManifest(manifest)
return hostInfo, nil
} else if errors.Is(err, os.ErrNotExist) {
hosts, err := appdetect.DetectAspireHosts(ctx, c.azdContext.ProjectDirectory(), c.dotnetCli)
if err != nil {
return nil, fmt.Errorf("failed to discover app host project under %s: %w", c.azdContext.ProjectPath(), err)
}
if len(hosts) == 0 {
return nil, fmt.Errorf("no app host projects found under %s", c.azdContext.ProjectPath())
}
if len(hosts) > 1 {
return nil, fmt.Errorf("multiple app host projects found under %s", c.azdContext.ProjectPath())
}
hostInfo := &AspireHost{
Name: filepath.Base(filepath.Dir(hosts[0].Path)),
Path: hosts[0].Path,
}
manifest, err := apphost.ManifestFromAppHost(ctx, hosts[0].Path, c.dotnetCli, aspireEnv)
if err != nil {
return nil, fmt.Errorf("failed to load app host manifest: %w", err)
}
hostInfo.Services = servicesFromManifest(manifest)
return hostInfo, nil
} else {
return nil, fmt.Errorf("failed to stat project path: %w", err)
}
}
// RenameAspireHostAsync is the server implementation of:
// ValueTask RenameAspireHostAsync(Session session, string newPath, CancellationToken cancellationToken).
func (s *aspireService) RenameAspireHostAsync(
ctx context.Context, sessionId Session, newPath string, observer IObserver[ProgressMessage],
) error {
_, err := s.server.validateSession(ctx, sessionId)
if err != nil {
return err
}
// TODO(azure/azure-dev#3283): What should this do? Rewrite azure.yaml? We'll end up losing comments...
return errors.New("not implemented")
}
// ServeHTTP implements http.Handler.
func (s *aspireService) ServeHTTP(w http.ResponseWriter, r *http.Request) {
serveRpc(w, r, map[string]Handler{
"GetAspireHostAsync": HandlerFunc3(s.GetAspireHostAsync),
"RenameAspireHostAsync": HandlerAction3(s.RenameAspireHostAsync),
})
}

Просмотреть файл

@ -0,0 +1,62 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
package vsrpc
import (
"context"
"net/http"
"sync"
"time"
)
// debugService is the RPC server for the '/TestDebugService/v1.0' endpoint. It is only exposed when
// AZD_DEBUG_SERVER_DEBUG_ENDPOINTS is set to true as per [strconv.ParseBool]. It is also used by our
// unit tests.
type debugService struct {
// When non-nil, TestCancelAsync will call `Done` on this wait group before waiting to observe
// cancellation. This allows test code to orchestrate when it sends the cancellation message and to
// know the RPC is ready to observe it.
wg *sync.WaitGroup
}
func newDebugService() *debugService {
return &debugService{}
}
// TestCancelAsync is the server implementation of:
// ValueTask<bool> InitializeAsync(int, CancellationToken);
//
// It waits for the given timeoutMs, and then returns true. However, if the context is cancelled before the timeout,
// it returns false and ctx.Err() which should cause the client to throw a TaskCanceledException.
func (s *debugService) TestCancelAsync(ctx context.Context, timeoutMs int) (bool, error) {
if s.wg != nil {
s.wg.Done()
}
select {
case <-ctx.Done():
return false, ctx.Err()
case <-time.After(time.Duration(timeoutMs) * time.Millisecond):
return true, nil
}
}
// TestCancelAsync is the server implementation of:
// ValueTask<bool> TestIObserver(int, CancellationToken);
//
// It emits a sequence of integers to the observer, from 0 to max, and then completes the observer, before returning.
func (s *debugService) TestIObserverAsync(ctx context.Context, max int, observer IObserver[int]) error {
for i := 0; i < max; i++ {
_ = observer.OnNext(ctx, i)
}
_ = observer.OnCompleted(ctx)
return nil
}
// ServeHTTP implements http.Handler.
func (s *debugService) ServeHTTP(w http.ResponseWriter, r *http.Request) {
serveRpc(w, r, map[string]Handler{
"TestCancelAsync": HandlerFunc1(s.TestCancelAsync),
"TestIObserverAsync": HandlerAction2(s.TestIObserverAsync),
})
}

Просмотреть файл

@ -0,0 +1,7 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
// Package vsrpc provides the RPC server that Visual Studio uses to interact with azd programmatically.
//
// The RPC server is implemented using JSON-RPC 2.0 over WebSockets.
package vsrpc

Просмотреть файл

@ -0,0 +1,121 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
package vsrpc
import (
"context"
"errors"
"fmt"
"net/http"
"github.com/azure/azure-dev/cli/azd/pkg/environment"
"github.com/azure/azure-dev/cli/azd/pkg/environment/azdcontext"
)
// environmentService is the RPC server for the '/EnvironmentService/v1.0' endpoint.
type environmentService struct {
server *Server
}
func newEnvironmentService(server *Server) *environmentService {
return &environmentService{
server: server,
}
}
// GetEnvironmentsAsync is the server implementation of:
// ValueTask<IEnumerable<EnvironmentInfo>> GetEnvironmentsAsync(Session, IObserver<ProgressMessage>, CancellationToken);
func (s *environmentService) GetEnvironmentsAsync(
ctx context.Context, sessionId Session, observer IObserver[ProgressMessage],
) ([]*EnvironmentInfo, error) {
session, err := s.server.validateSession(ctx, sessionId)
if err != nil {
return nil, err
}
var c struct {
envManager environment.Manager `container:"type"`
}
container, err := session.newContainer()
if err != nil {
return nil, err
}
if err := container.Fill(&c); err != nil {
return nil, err
}
envs, err := c.envManager.List(ctx)
if err != nil {
return nil, fmt.Errorf("listing environments: %w", err)
}
infos := make([]*EnvironmentInfo, len(envs))
for i, env := range envs {
infos[i] = &EnvironmentInfo{
Name: env.Name,
IsCurrent: env.IsDefault,
DotEnvPath: env.DotEnvPath,
}
}
return infos, nil
}
// SetCurrentEnvironmentAsync is the server implementation of:
// ValueTask<bool> SetCurrentEnvironmentAsync(Session, string, IObserver<ProgressMessage>, CancellationToken);
func (s *environmentService) SetCurrentEnvironmentAsync(
ctx context.Context, sessionId Session, name string, observer IObserver[ProgressMessage],
) (bool, error) {
session, err := s.server.validateSession(ctx, sessionId)
if err != nil {
return false, err
}
var c struct {
azdCtx *azdcontext.AzdContext `container:"type"`
}
container, err := session.newContainer()
if err != nil {
return false, err
}
if err := container.Fill(&c); err != nil {
return false, err
}
if err := c.azdCtx.SetDefaultEnvironmentName(name); err != nil {
return false, fmt.Errorf("saving default environment: %w", err)
}
return true, nil
}
// DeleteEnvironmentAsync is the server implementation of:
// ValueTask<bool> DeleteEnvironmentAsync(Session, string, IObserver<ProgressMessage>, CancellationToken);
func (s *environmentService) DeleteEnvironmentAsync(
ctx context.Context, sessionId Session, name string, observer IObserver[ProgressMessage],
) (bool, error) {
_, err := s.server.validateSession(ctx, sessionId)
if err != nil {
return false, err
}
// TODO(azure/azure-dev#3285): Implement this.
return false, errors.New("not implemented")
}
// ServeHTTP implements http.Handler.
func (s *environmentService) ServeHTTP(w http.ResponseWriter, r *http.Request) {
serveRpc(w, r, map[string]Handler{
"CreateEnvironmentAsync": HandlerFunc3(s.CreateEnvironmentAsync),
"GetEnvironmentsAsync": HandlerFunc2(s.GetEnvironmentsAsync),
"LoadEnvironmentAsync": HandlerFunc3(s.LoadEnvironmentAsync),
"OpenEnvironmentAsync": HandlerFunc3(s.OpenEnvironmentAsync),
"SetCurrentEnvironmentAsync": HandlerFunc3(s.SetCurrentEnvironmentAsync),
"DeleteEnvironmentAsync": HandlerFunc3(s.DeleteEnvironmentAsync),
"RefreshEnvironmentAsync": HandlerFunc3(s.RefreshEnvironmentAsync),
"DeployAsync": HandlerFunc3(s.DeployAsync),
})
}

Просмотреть файл

@ -0,0 +1,136 @@
package vsrpc
import (
"context"
"errors"
"fmt"
"io/fs"
"os"
"path/filepath"
"github.com/azure/azure-dev/cli/azd/internal/appdetect"
"github.com/azure/azure-dev/cli/azd/pkg/apphost"
"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/tools/dotnet"
)
// CreateEnvironmentAsync is the server implementation of:
// ValueTask<bool> CreateEnvironmentAsync(Session, Environment, IObserver<ProgressMessage>, CancellationToken);
func (s *environmentService) CreateEnvironmentAsync(
ctx context.Context, sessionId Session, newEnv Environment, observer IObserver[ProgressMessage],
) (bool, error) {
session, err := s.server.validateSession(ctx, sessionId)
if err != nil {
return false, err
}
envSpec := environment.Spec{
Name: newEnv.Name,
Subscription: newEnv.Properties["Subscription"],
Location: newEnv.Properties["Location"],
}
var c struct {
azdContext *azdcontext.AzdContext `container:"type"`
dotnetCli dotnet.DotNetCli `container:"type"`
envManager environment.Manager `container:"type"`
}
container, err := session.newContainer()
if err != nil {
return false, err
}
if err := container.Fill(&c); err != nil {
return false, err
}
// We had thought at one point that we would introduce `ASPIRE_ENVIRONMENT` as a sibling to `ASPNETCORE_ENVIRONMENT` and
// `DOTNET_ENVIRONMENT` and was aspire specific. We no longer intend to do this (because having both DOTNET and
// ASPNETCORE versions is already confusing enough). For now, we'll use `ASPIRE_ENVIRONMENT` to seed the initial values of
// `DOTNET_ENVIRONMENT`, but allow them to be overriden at environment construction time.
//
// We only retain `DOTNET_ENVIRONMENT` in the .env file.
dotnetEnv := newEnv.Properties["ASPIRE_ENVIRONMENT"]
if v, has := newEnv.Values["DOTNET_ENVIRONMENT"]; has {
dotnetEnv = v
}
// If an azure.yaml doesn't already exist, we need to create one. Creating an environment implies initializing the
// azd project if it does not already exist.
if _, err := os.Stat(c.azdContext.ProjectPath()); errors.Is(err, fs.ErrNotExist) {
// Write an azure.yaml file to the project.
hosts, err := appdetect.DetectAspireHosts(ctx, c.azdContext.ProjectDirectory(), c.dotnetCli)
if err != nil {
return false, fmt.Errorf("failed to discover app host project under %s: %w", c.azdContext.ProjectPath(), err)
}
if len(hosts) == 0 {
return false, fmt.Errorf("no app host projects found under %s", c.azdContext.ProjectPath())
}
if len(hosts) > 1 {
return false, fmt.Errorf("multiple app host projects found under %s", c.azdContext.ProjectPath())
}
manifest, err := apphost.ManifestFromAppHost(ctx, hosts[0].Path, c.dotnetCli, dotnetEnv)
if err != nil {
return false, fmt.Errorf("reading app host manifest: %w", err)
}
files, err := apphost.GenerateProjectArtifacts(
ctx,
c.azdContext.ProjectDirectory(),
filepath.Base(c.azdContext.ProjectDirectory()),
manifest,
hosts[0].Path)
if err != nil {
return false, fmt.Errorf("generating project artifacts: %w", err)
}
file := files["azure.yaml"]
projectFilePath := filepath.Join(c.azdContext.ProjectDirectory(), "azure.yaml")
if err := os.WriteFile(projectFilePath, []byte(file.Contents), file.Mode); err != nil {
return false, fmt.Errorf("writing azure.yaml: %w", err)
}
} else if err != nil {
return false, fmt.Errorf("checking for project: %w", err)
}
azdEnv, err := c.envManager.Create(ctx, envSpec)
if err != nil {
return false, fmt.Errorf("creating new environment: %w", err)
}
if dotnetEnv != "" {
azdEnv.DotenvSet("DOTNET_ENVIRONMENT", dotnetEnv)
}
for key, value := range newEnv.Values {
azdEnv.DotenvSet(key, value)
}
var servicesToExpose = make([]string, 0)
for _, svc := range newEnv.Services {
if svc.IsExternal {
servicesToExpose = append(servicesToExpose, svc.Name)
}
}
if err := azdEnv.Config.Set("services.app.config.exposedServices", servicesToExpose); err != nil {
return false, fmt.Errorf("setting exposed services: %w", err)
}
if err := c.envManager.Save(ctx, azdEnv); err != nil {
return false, fmt.Errorf("saving new environment: %w", err)
}
if err := c.azdContext.SetDefaultEnvironmentName(newEnv.Name); err != nil {
return false, fmt.Errorf("saving default environment: %w", err)
}
return true, nil
}

Просмотреть файл

@ -0,0 +1,97 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
package vsrpc
import (
"context"
"github.com/azure/azure-dev/cli/azd/cmd/actions"
"github.com/azure/azure-dev/cli/azd/internal"
"github.com/azure/azure-dev/cli/azd/internal/cmd"
"github.com/azure/azure-dev/cli/azd/pkg/ioc"
)
// DeployAsync is the server implementation of:
// ValueTask<Environment> DeployAsync(Session, string, IObserver<ProgressMessage>, CancellationToken)
//
// While it is named simply `DeployAsync`, it behaves as if the user had run `azd provision` and `azd deploy`.
func (s *environmentService) DeployAsync(
ctx context.Context, sessionId Session, name string, observer IObserver[ProgressMessage],
) (*Environment, error) {
session, err := s.server.validateSession(ctx, sessionId)
if err != nil {
return nil, err
}
outputWriter := &lineWriter{
next: &messageWriter{
ctx: ctx,
observer: observer,
},
}
container, err := session.newContainer()
if err != nil {
return nil, err
}
container.outWriter.AddWriter(outputWriter)
defer container.outWriter.RemoveWriter(outputWriter)
provisionFlags := cmd.NewProvisionFlagsFromEnvAndOptions(
&internal.EnvFlag{
EnvironmentName: name,
},
&internal.GlobalCommandOptions{
Cwd: session.rootPath,
NoPrompt: true,
},
)
deployFlags := cmd.NewDeployFlagsFromEnvAndOptions(
&internal.EnvFlag{
EnvironmentName: name,
},
&internal.GlobalCommandOptions{
Cwd: session.rootPath,
NoPrompt: true,
},
)
deployFlags.All = true
container.MustRegisterScoped(func() internal.EnvFlag {
return internal.EnvFlag{
EnvironmentName: name,
}
})
ioc.RegisterInstance[*cmd.ProvisionFlags](container.NestedContainer, provisionFlags)
ioc.RegisterInstance[*cmd.DeployFlags](container.NestedContainer, deployFlags)
ioc.RegisterInstance[[]string](container.NestedContainer, []string{})
container.MustRegisterNamedTransient("provisionAction", cmd.NewProvisionAction)
container.MustRegisterNamedTransient("deployAction", cmd.NewDeployAction)
var c struct {
deployAction actions.Action `container:"name"`
provisionAction actions.Action `container:"name"`
}
if err := container.Fill(&c); err != nil {
return nil, err
}
if _, err := c.provisionAction.Run(ctx); err != nil {
return nil, err
}
if _, err := c.deployAction.Run(ctx); err != nil {
return nil, err
}
if err := outputWriter.Flush(ctx); err != nil {
return nil, err
}
return s.refreshEnvironmentAsync(ctx, container, name, observer)
}

Просмотреть файл

@ -0,0 +1,155 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
package vsrpc
import (
"context"
"fmt"
"slices"
"strings"
"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/project"
"github.com/azure/azure-dev/cli/azd/pkg/tools/dotnet"
)
// OpenEnvironmentAsync is the server implementation of:
// ValueTask<Environment> OpenEnvironmentAsync(Session, string, IObserver<ProgressMessage>, CancellationToken);
//
// OpenEnvironmentAsync loads the specified environment, without connecting to Azure or fetching a manifest (unless it is
// already cached) and is faster than `LoadEnvironmentAsync` in cases where we have not cached the manifest. This means
// the Services array of the returned environment may be empty.
func (s *environmentService) OpenEnvironmentAsync(
ctx context.Context, sessionId Session, name string, observer IObserver[ProgressMessage],
) (*Environment, error) {
session, err := s.server.validateSession(ctx, sessionId)
if err != nil {
return nil, err
}
container, err := session.newContainer()
if err != nil {
return nil, err
}
return s.loadEnvironmentAsync(ctx, container, name, false)
}
// LoadEnvironmentAsync is the server implementation of:
// ValueTask<Environment> LoadEnvironmentAsync(Session, string, IObserver<ProgressMessage>, CancellationToken);
//
// LoadEnvironmentAsync loads the specified environment, without connecting to Azure. Because of this, certain properties of
// the environment (like service endpoints) may not be available. Use `RefreshEnvironmentAsync` to load the environment and
// fetch information from Azure.
func (s *environmentService) LoadEnvironmentAsync(
ctx context.Context, sessionId Session, name string, observer IObserver[ProgressMessage],
) (*Environment, error) {
session, err := s.server.validateSession(ctx, sessionId)
if err != nil {
return nil, err
}
container, err := session.newContainer()
if err != nil {
return nil, err
}
return s.loadEnvironmentAsync(ctx, container, name, true)
}
func (s *environmentService) loadEnvironmentAsync(
ctx context.Context, container *container, name string, mustLoadServices bool,
) (*Environment, error) {
var c struct {
azdCtx *azdcontext.AzdContext `container:"type"`
envManager environment.Manager `container:"type"`
projectConfig *project.ProjectConfig `container:"type"`
dotnetCli dotnet.DotNetCli `container:"type"`
dotnetImporter *project.DotNetImporter `container:"type"`
}
if err := container.Fill(&c); err != nil {
return nil, err
}
e, err := c.envManager.Get(ctx, name)
if err != nil {
return nil, fmt.Errorf("getting environment: %w", err)
}
currentEnv, err := c.azdCtx.GetDefaultEnvironmentName()
if err != nil {
return nil, fmt.Errorf("getting default environment: %w", err)
}
ret := &Environment{
Name: name,
Properties: map[string]string{
"Subscription": e.GetSubscriptionId(),
"Location": e.GetLocation(),
},
IsCurrent: name == currentEnv,
Values: e.Dotenv(),
}
// NOTE(ellismg): The IaC for Aspire Apps exposes these properties - we use them instead of trying to discover the
// deployed resources (perhaps by considering the resources in the resource group associated with the environment or
// by looking at the deployment). This was the quickest path to get the information that VS needed for the spike,
// but we might want to revisit this strategy. A nice thing about this strategy is it means we can return the data
// promptly, which is nice for VS.
if v := e.Getenv("AZURE_CONTAINER_APPS_ENVIRONMENT_ID"); v != "" {
parts := strings.Split(v, "/")
ret.Properties["ContainerAppsEnvironment"] = parts[len(parts)-1]
}
if v := e.Getenv("AZURE_CONTAINER_REGISTRY_ENDPOINT"); v != "" {
ret.Properties["ContainerRegistry"] = strings.TrimSuffix(v, ".azurecr.io")
}
if v := e.Getenv("AZURE_LOG_ANALYTICS_WORKSPACE_NAME"); v != "" {
ret.Properties["LogAnalyticsWorkspace"] = v
}
// If we would have to discover the app host or load the manifest from disk and the caller did not request it
// skip this somewhat expensive operation, at the expense of not building out the services array.
if !mustLoadServices {
return ret, nil
}
appHost, err := appHostForProject(ctx, c.projectConfig, c.dotnetCli)
if err != nil {
return nil, fmt.Errorf("failed to find Aspire app host: %w", err)
}
manifest, err := c.dotnetImporter.ReadManifest(ctx, appHost)
if err != nil {
return nil, fmt.Errorf("reading app host manifest: %w", err)
}
ret.Services = servicesFromManifest(manifest)
var exposedServices []string
// TODO(azure/azure-dev#3284): We need to use the service name of the apphost from azure.yaml instead of assuming
// it will always be "app". "app" is just the default we use when creating an azure.yaml for the user.
val, has := e.Config.Get("services.app.config.exposedServices")
if has {
if v, ok := val.([]any); ok {
for _, svc := range v {
if s, ok := svc.(string); ok {
exposedServices = append(exposedServices, s)
}
}
}
}
for _, svc := range ret.Services {
if slices.Contains(exposedServices, svc.Name) {
svc.IsExternal = true
}
}
return ret, nil
}

Просмотреть файл

@ -0,0 +1,201 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
package vsrpc
import (
"context"
"fmt"
"log"
"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources"
"github.com/azure/azure-dev/cli/azd/internal"
"github.com/azure/azure-dev/cli/azd/pkg/convert"
"github.com/azure/azure-dev/cli/azd/pkg/environment"
"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/infra/provisioning/bicep"
"github.com/azure/azure-dev/cli/azd/pkg/project"
"github.com/azure/azure-dev/cli/azd/pkg/tools/azcli"
)
// RefreshEnvironmentAsync is the server implementation of:
// ValueTask<Environment> RefreshEnvironmentAsync(Session, string, IObserver<ProgressMessage>, CancellationToken);
//
// RefreshEnvironmentAsync loads the specified environment, and fetches information about it from Azure. If you are willing
// to accept some loss of information in favor of a faster load time, use `LoadEnvironmentAsync` instead, which does not
// contact azure to compute service endpoints or last deployment information.
func (s *environmentService) RefreshEnvironmentAsync(
ctx context.Context, sessionId Session, name string, observer IObserver[ProgressMessage],
) (*Environment, error) {
session, err := s.server.validateSession(ctx, sessionId)
if err != nil {
return nil, err
}
container, err := session.newContainer()
if err != nil {
return nil, err
}
return s.refreshEnvironmentAsync(ctx, container, name, observer)
}
func (s *environmentService) refreshEnvironmentAsync(
ctx context.Context, container *container, name string, observer IObserver[ProgressMessage],
) (*Environment, error) {
env, err := s.loadEnvironmentAsync(ctx, container, name, true)
if err != nil {
return nil, err
}
var c struct {
projectManager project.ProjectManager `container:"type"`
projectConfig *project.ProjectConfig `container:"type"`
importManager *project.ImportManager `container:"type"`
bicep provisioning.Provider `container:"name"`
azureResourceManager *infra.AzureResourceManager `container:"type"`
azcli azcli.AzCli `container:"type"`
resourceManager project.ResourceManager `container:"type"`
serviceManager project.ServiceManager `container:"type"`
envManager environment.Manager `container:"type"`
}
container.MustRegisterScoped(func() internal.EnvFlag {
return internal.EnvFlag{
EnvironmentName: name,
}
})
if err := container.Fill(&c); err != nil {
return nil, err
}
bicepProvider := c.bicep.(*bicep.BicepProvider)
if err := c.projectManager.Initialize(ctx, c.projectConfig); err != nil {
return nil, err
}
infra, err := c.importManager.ProjectInfrastructure(ctx, c.projectConfig)
if err != nil {
return nil, err
}
defer func() { _ = infra.Cleanup() }()
if err := bicepProvider.Initialize(ctx, c.projectConfig.Path, infra.Options); err != nil {
return nil, fmt.Errorf("initializing provisioning manager: %w", err)
}
_ = observer.OnNext(ctx, newInfoProgressMessage("Loading latest deployment information"))
deployment, err := bicepProvider.LastDeployment(ctx)
if err != nil {
log.Printf("failed to get latest deployment result: %v", err)
} else {
env.LastDeployment = &DeploymentResult{
DeploymentId: *deployment.ID,
Success: *deployment.Properties.ProvisioningState == armresources.ProvisioningStateSucceeded,
Time: *deployment.Properties.Timestamp,
}
}
stableServices, err := c.importManager.ServiceStable(ctx, c.projectConfig)
if err != nil {
return nil, err
}
subId := env.Properties["Subscription"]
envName := env.Name
nameIdx := make(map[string]int, len(env.Services)) // maps service name to index in env.Services slice
for idx, svc := range env.Services {
nameIdx[svc.Name] = idx
}
_ = observer.OnNext(ctx, newInfoProgressMessage("Loading server resources"))
rgName, err := c.azureResourceManager.FindResourceGroupForEnvironment(ctx, subId, envName)
if err == nil {
env.Properties["ResourceGroup"] = rgName
for _, serviceConfig := range stableServices {
svcName := serviceConfig.Name
_ = observer.OnNext(ctx, newInfoProgressMessage("Loading server resources for service "+svcName))
resources, err := c.resourceManager.GetServiceResources(ctx, subId, rgName, serviceConfig)
if err == nil {
resourceIds := make([]string, len(resources))
for idx, res := range resources {
resourceIds[idx] = res.Id
}
if svcIdx, has := nameIdx[svcName]; has {
resSvc := env.Services[svcIdx]
if len(resourceIds) > 0 {
resSvc.ResourceId = convert.RefOf(resourceIds[0])
}
resSvc.Endpoint = convert.RefOf(s.serviceEndpoint(
ctx, subId, serviceConfig, c.resourceManager, c.serviceManager,
))
}
} else {
log.Printf("ignoring error determining resource id for service %s: %v", svcName, err)
}
}
resources, err := c.azcli.ListResourceGroupResources(ctx, subId, rgName, nil)
if err == nil {
for _, res := range resources {
env.Resources = append(env.Resources, &Resource{
Id: res.Id,
Name: res.Name,
Type: res.Type,
})
}
} else {
log.Printf("ignoring error loading resources for environment %s: %v", envName, err)
}
} else {
log.Printf(
"ignoring error determining resource group for environment %s, resources will not be available: %v",
env.Name,
err)
}
return env, nil
}
func (s *environmentService) serviceEndpoint(
ctx context.Context,
subId string,
serviceConfig *project.ServiceConfig,
resourceManager project.ResourceManager,
serviceManager project.ServiceManager,
) string {
targetResource, err := resourceManager.GetTargetResource(ctx, subId, serviceConfig)
if err != nil {
log.Printf("error: getting target-resource. Endpoints will be empty: %v", err)
return ""
}
st, err := serviceManager.GetServiceTarget(ctx, serviceConfig)
if err != nil {
log.Printf("error: getting service target. Endpoints will be empty: %v", err)
return ""
}
endpoints, err := st.Endpoints(ctx, serviceConfig, targetResource)
if err != nil {
log.Printf("error: getting service endpoints. Endpoints might be empty: %v", err)
}
if len(endpoints) == 0 {
return ""
}
return endpoints[0]
}

Просмотреть файл

@ -0,0 +1,222 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
package vsrpc
import (
"context"
"encoding/json"
"errors"
"fmt"
"go.lsp.dev/jsonrpc2"
)
const (
// requestCanceledErrorCode is the error code that is used when a request is cancelled. StreamJsonRpc understands this
// error code and causes the Task to throw a TaskCanceledException instead of a normal RemoteInvocationException error.
requestCanceledErrorCode jsonrpc2.Code = -32800
)
// Handler is the type of function that handles incoming RPC requests.
type Handler func(ctx context.Context, conn jsonrpc2.Conn, reply jsonrpc2.Replier, req jsonrpc2.Request) error
// HandlerAction0 is a helper for creating a Handler from a function that takes no arguments and returns an error.
func HandlerAction0(f func(context.Context) error) Handler {
return func(ctx context.Context, conn jsonrpc2.Conn, reply jsonrpc2.Replier, req jsonrpc2.Request) error {
err := f(ctx)
if err != nil && errors.Is(err, ctx.Err()) {
err = &jsonrpc2.Error{
Code: requestCanceledErrorCode,
Message: err.Error(),
}
}
return reply(ctx, nil, err)
}
}
// HandlerAction1 is a helper for creating a Handler from a function that takes one argument and returns an error.
func HandlerAction1[T1 any](f func(context.Context, T1) error) Handler {
return func(ctx context.Context, conn jsonrpc2.Conn, reply jsonrpc2.Replier, req jsonrpc2.Request) error {
t1, err := unmarshalArg[T1](conn, req, 0)
if err != nil {
return reply(ctx, nil, err)
}
err = f(ctx, t1)
if err != nil && errors.Is(err, ctx.Err()) {
err = &jsonrpc2.Error{
Code: requestCanceledErrorCode,
Message: err.Error(),
}
}
return reply(ctx, nil, err)
}
}
// HandlerAction2 is a helper for creating a Handler from a function that takes two arguments and returns an error.
func HandlerAction2[T1 any, T2 any](f func(context.Context, T1, T2) error) Handler {
return func(ctx context.Context, conn jsonrpc2.Conn, reply jsonrpc2.Replier, req jsonrpc2.Request) error {
t1, err := unmarshalArg[T1](conn, req, 0)
if err != nil {
return reply(ctx, nil, err)
}
t2, err := unmarshalArg[T2](conn, req, 1)
if err != nil {
return reply(ctx, nil, err)
}
err = f(ctx, t1, t2)
if err != nil && errors.Is(err, ctx.Err()) {
err = &jsonrpc2.Error{
Code: requestCanceledErrorCode,
Message: err.Error(),
}
}
return reply(ctx, nil, err)
}
}
// HandlerAction3 is a helper for creating a Handler from a function that takes two arguments and returns an error.
func HandlerAction3[T1 any, T2 any, T3 any](f func(context.Context, T1, T2, T3) error) Handler {
return func(ctx context.Context, conn jsonrpc2.Conn, reply jsonrpc2.Replier, req jsonrpc2.Request) error {
t1, err := unmarshalArg[T1](conn, req, 0)
if err != nil {
return reply(ctx, nil, err)
}
t2, err := unmarshalArg[T2](conn, req, 1)
if err != nil {
return reply(ctx, nil, err)
}
t3, err := unmarshalArg[T3](conn, req, 2)
if err != nil {
return reply(ctx, nil, err)
}
err = f(ctx, t1, t2, t3)
if err != nil && errors.Is(err, ctx.Err()) {
err = &jsonrpc2.Error{
Code: requestCanceledErrorCode,
Message: err.Error(),
}
}
return reply(ctx, nil, err)
}
}
// HandlerFunc0 is a helper for creating a Handler from a function that takes no arguments and returns a value and an error.
func HandlerFunc0[TRet any](f func(context.Context) (TRet, error)) Handler {
return func(ctx context.Context, conn jsonrpc2.Conn, reply jsonrpc2.Replier, req jsonrpc2.Request) error {
ret, err := f(ctx)
if err != nil && errors.Is(err, ctx.Err()) {
err = &jsonrpc2.Error{
Code: requestCanceledErrorCode,
Message: err.Error(),
}
}
return reply(ctx, ret, err)
}
}
// HandlerFunc1 is a helper for creating a Handler from a function that takes one argument and returns a value and an error.
func HandlerFunc1[T1 any, TRet any](f func(context.Context, T1) (TRet, error)) Handler {
return func(ctx context.Context, conn jsonrpc2.Conn, reply jsonrpc2.Replier, req jsonrpc2.Request) error {
t1, err := unmarshalArg[T1](conn, req, 0)
if err != nil {
return reply(ctx, nil, err)
}
ret, err := f(ctx, t1)
if err != nil && errors.Is(err, ctx.Err()) {
err = &jsonrpc2.Error{
Code: requestCanceledErrorCode,
Message: err.Error(),
}
}
return reply(ctx, ret, err)
}
}
// HandlerFunc2 is a helper for creating a Handler from a function that takes two arguments and returns a value and an error.
func HandlerFunc2[T1 any, T2 any, TRet any](f func(context.Context, T1, T2) (TRet, error)) Handler {
return func(ctx context.Context, conn jsonrpc2.Conn, reply jsonrpc2.Replier, req jsonrpc2.Request) error {
t1, err := unmarshalArg[T1](conn, req, 0)
if err != nil {
return reply(ctx, nil, err)
}
t2, err := unmarshalArg[T2](conn, req, 1)
if err != nil {
return reply(ctx, nil, err)
}
ret, err := f(ctx, t1, t2)
if err != nil && errors.Is(err, ctx.Err()) {
err = &jsonrpc2.Error{
Code: requestCanceledErrorCode,
Message: err.Error(),
}
}
return reply(ctx, ret, err)
}
}
// HandlerFunc3 is a helper for creating a Handler from a function that takes three arguments and returns a value and an
// error.
func HandlerFunc3[T1 any, T2 any, T3 any, TRet any](f func(context.Context, T1, T2, T3) (TRet, error)) Handler {
return func(ctx context.Context, conn jsonrpc2.Conn, reply jsonrpc2.Replier, req jsonrpc2.Request) error {
t1, err := unmarshalArg[T1](conn, req, 0)
if err != nil {
return reply(ctx, nil, err)
}
t2, err := unmarshalArg[T2](conn, req, 1)
if err != nil {
return reply(ctx, nil, err)
}
t3, err := unmarshalArg[T3](conn, req, 2)
if err != nil {
return reply(ctx, nil, err)
}
ret, err := f(ctx, t1, t2, t3)
if err != nil && errors.Is(err, ctx.Err()) {
err = &jsonrpc2.Error{
Code: requestCanceledErrorCode,
Message: err.Error(),
}
}
return reply(ctx, ret, err)
}
}
// unmarshalArg returns the i'th member of the Params property of a request, after JSON unmarshalling it as an instance of T.
// If an error is returned, it is of type *jsonrpc.Error with a code of jsonrpc2.InvalidParams.
func unmarshalArg[T any](conn jsonrpc2.Conn, req jsonrpc2.Request, index int) (T, error) {
var args []json.RawMessage
if err := json.Unmarshal(req.Params(), &args); err != nil {
return *new(T), jsonrpc2.NewError(
jsonrpc2.InvalidParams, fmt.Sprintf("unmarshalling params as array: %s", err.Error()))
}
if index >= len(args) {
return *new(T), jsonrpc2.NewError(
jsonrpc2.InvalidParams, fmt.Sprintf("param out of range, len: %d index: %d", len(args), index))
}
var arg T
if err := json.Unmarshal(args[index], &arg); err != nil {
return *new(T), jsonrpc2.NewError(
jsonrpc2.InvalidParams, fmt.Sprintf("unmarshalling param: %s", err.Error()))
}
if v, ok := (any(&arg)).(connectionObserver); ok {
v.attachConnection(conn)
}
return arg, nil
}

Просмотреть файл

@ -0,0 +1,223 @@
package vsrpc
import (
"context"
"errors"
"testing"
"github.com/stretchr/testify/require"
"go.lsp.dev/jsonrpc2"
)
// newHandlerForCaseFunc is a function that constructs a new Handler for a given test case.
type newHandlerForCaseFunc func(t *testing.T, tc handlerTestCase) Handler
type handlerTestCase struct {
// name is the name of the test, as passed to (*testing.T).Run().
name string
// expected is the result the handler should return on success.
expected any
// err is the error returned by the handler when non nil.
err error
// cancel is true when the handler should be cancelled.
cancel bool
// params are the params that are passed to the call, this should be an slice of values.
params []any
}
// runHandlerSuite runs through a suite of tests for a handler. It exercises cases where the handler
// returns a value, returns and error not notices it has been cancelled and returns a special error to
// the caller.
func runHandlerSuite(t *testing.T, newHandler newHandlerForCaseFunc, params []any, expected any) {
cases := []handlerTestCase{
{
name: "Success",
expected: expected,
params: params,
},
{
name: "Error",
err: errors.New("expected error"),
params: params,
},
{
name: "Canceled",
cancel: true,
params: params,
},
}
for _, tc := range cases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
h := newHandler(t, tc)
call, err := jsonrpc2.NewCall(jsonrpc2.NewNumberID(1), "Test", tc.params)
require.NoError(t, err)
ctx := context.Background()
if tc.err != nil {
_ = h(ctx, nil, validateError(t, tc.err), call)
} else if tc.cancel {
ctx, cancel := context.WithCancel(ctx)
cancel()
_ = h(ctx, nil, validateCancel(t), call)
} else {
_ = h(ctx, nil, validateResult(t, tc.expected), call)
}
})
}
}
func TestHandler(t *testing.T) {
t.Parallel()
t.Run("HandlerAction0", func(t *testing.T) { runHandlerSuite(t, newHandlerAction0, nil, nil) })
t.Run("HandlerAction1", func(t *testing.T) { runHandlerSuite(t, newHandlerAction1, []any{"arg0"}, nil) })
t.Run("HandlerAction2", func(t *testing.T) { runHandlerSuite(t, newHandlerAction2, []any{"arg0", "arg1"}, nil) })
t.Run("HandlerAction3", func(t *testing.T) { runHandlerSuite(t, newHandlerAction3, []any{"arg0", "arg1", "arg2"}, nil) })
t.Run("HandlerFunc0", func(t *testing.T) { runHandlerSuite(t, newHandlerFunc0, nil, "ok") })
t.Run("HandlerFunc1", func(t *testing.T) { runHandlerSuite(t, newHandlerFunc1, []any{"arg0"}, "ok") })
t.Run("HandlerFunc2", func(t *testing.T) { runHandlerSuite(t, newHandlerFunc2, []any{"arg0", "arg1"}, "ok") })
t.Run("HandlerFunc3", func(t *testing.T) { runHandlerSuite(t, newHandlerFunc3, []any{"arg0", "arg1", "arg2"}, "ok") })
}
func newHandlerAction0(t *testing.T, tc handlerTestCase) Handler {
return HandlerAction0(func(ctx context.Context) error {
select {
case <-ctx.Done():
return ctx.Err()
default:
return tc.err
}
})
}
func newHandlerAction1(t *testing.T, tc handlerTestCase) Handler {
return HandlerAction1(func(ctx context.Context, arg0 string) error {
validateParam(t, tc.params, 0, arg0)
select {
case <-ctx.Done():
return ctx.Err()
default:
return tc.err
}
})
}
func newHandlerAction2(t *testing.T, tc handlerTestCase) Handler {
return HandlerAction2(func(ctx context.Context, arg0, arg1 string) error {
validateParam(t, tc.params, 0, arg0)
validateParam(t, tc.params, 1, arg1)
select {
case <-ctx.Done():
return ctx.Err()
default:
return tc.err
}
})
}
func newHandlerAction3(t *testing.T, tc handlerTestCase) Handler {
return HandlerAction3(func(ctx context.Context, arg0, arg1, arg2 string) error {
validateParam(t, tc.params, 0, arg0)
validateParam(t, tc.params, 1, arg1)
validateParam(t, tc.params, 2, arg2)
select {
case <-ctx.Done():
return ctx.Err()
default:
return tc.err
}
})
}
func newHandlerFunc0(t *testing.T, tc handlerTestCase) Handler {
return HandlerFunc0(func(ctx context.Context) (any, error) {
select {
case <-ctx.Done():
return nil, ctx.Err()
default:
return tc.expected, tc.err
}
})
}
func newHandlerFunc1(t *testing.T, tc handlerTestCase) Handler {
return HandlerFunc1(func(ctx context.Context, arg0 string) (any, error) {
validateParam(t, tc.params, 0, arg0)
select {
case <-ctx.Done():
return nil, ctx.Err()
default:
return tc.expected, tc.err
}
})
}
func newHandlerFunc2(t *testing.T, tc handlerTestCase) Handler {
return HandlerFunc2(func(ctx context.Context, arg0, arg1 string) (any, error) {
validateParam(t, tc.params, 0, arg0)
validateParam(t, tc.params, 1, arg1)
select {
case <-ctx.Done():
return nil, ctx.Err()
default:
return tc.expected, tc.err
}
})
}
func newHandlerFunc3(t *testing.T, tc handlerTestCase) Handler {
return HandlerFunc3(func(ctx context.Context, arg0, arg1, arg2 string) (any, error) {
validateParam(t, tc.params, 0, arg0)
validateParam(t, tc.params, 1, arg1)
validateParam(t, tc.params, 2, arg2)
select {
case <-ctx.Done():
return nil, ctx.Err()
default:
return tc.expected, tc.err
}
})
}
func validateParam(t *testing.T, params any, n int, expected any) {
require.IsType(t, []any(nil), params)
args := params.([]any)
require.GreaterOrEqual(t, len(args), n)
require.Equal(t, expected, args[n])
}
func validateError(t *testing.T, expected error) jsonrpc2.Replier {
return func(ctx context.Context, result any, err error) error {
require.Nil(t, result)
require.Equal(t, expected, err)
return nil
}
}
func validateCancel(t *testing.T) jsonrpc2.Replier {
return func(ctx context.Context, result any, err error) error {
require.Nil(t, result)
var rpcErr *jsonrpc2.Error
require.True(t, errors.As(err, &rpcErr))
require.Equal(t, requestCanceledErrorCode, rpcErr.Code)
return nil
}
}
func validateResult(t *testing.T, expected any) jsonrpc2.Replier {
return func(ctx context.Context, result any, err error) error {
require.Nil(t, err)
require.Equal(t, expected, result)
return nil
}
}

Просмотреть файл

@ -0,0 +1,70 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
package vsrpc
import (
"context"
"io"
"sync"
)
// messageWriter is an io.Writer that writes to an IObserver[ProgressMessage], emitting a message for each write.
type messageWriter struct {
ctx context.Context
observer IObserver[ProgressMessage]
}
// lineWriter is an io.Writer that writes to another io.Writer, emitting a message for each line written.
type lineWriter struct {
next io.Writer
buf []byte
// bufMu protects access to buf.
bufMu sync.Mutex
}
func (lw *lineWriter) Write(p []byte) (int, error) {
lw.bufMu.Lock()
defer lw.bufMu.Unlock()
for i, b := range p {
lw.buf = append(lw.buf, b)
if b == '\n' {
_, err := lw.next.Write(lw.buf)
if err != nil {
return i + 1, err
}
lw.buf = nil
}
}
return len(p), nil
}
// Flush sends any remaining output to the observer.
func (mw *lineWriter) Flush(ctx context.Context) error {
mw.bufMu.Lock()
defer mw.bufMu.Unlock()
if len(mw.buf) > 0 {
buf := mw.buf
mw.buf = nil
_, err := mw.next.Write(buf)
return err
}
return nil
}
// Write implements io.Writer.
func (mw *messageWriter) Write(p []byte) (int, error) {
err := mw.observer.OnNext(mw.ctx, newInfoProgressMessage(string(p)))
if err != nil {
return 0, err
}
return len(p), nil
}

Просмотреть файл

@ -0,0 +1,92 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
package vsrpc
import (
"time"
)
type AspireHost struct {
Name string
Path string
Services []*Service
}
type Environment struct {
Name string
IsCurrent bool
Properties map[string]string
Services []*Service
Values map[string]string
LastDeployment *DeploymentResult `json:",omitempty"`
Resources []*Resource
}
type Resource struct {
Name string
Type string
Id string
}
type EnvironmentInfo struct {
Name string
IsCurrent bool
DotEnvPath string
}
type Service struct {
Name string
IsExternal bool
Path string
Endpoint *string `json:",omitempty"`
ResourceId *string `json:",omitempty"`
}
type DeploymentResult struct {
Success bool
Time time.Time
Message string
DeploymentId string
}
type ProgressMessage struct {
Message string
Severity MessageSeverity
Time time.Time
Kind MessageKind
Code string
AdditionalInfoLink string
}
func newInfoProgressMessage(message string) ProgressMessage {
return ProgressMessage{
Message: message,
Severity: Info,
Time: time.Now(),
Kind: Logging,
}
}
type MessageSeverity int
const (
Info MessageSeverity = iota
Warning
Error
)
type MessageKind int
const (
Logging MessageKind = iota
Important
)
// Session represents an active connection to the server. It is returned by InitializeAsync and holds an opaque
// connection id that the server can use to identify the client across multiple RPC calls (since our service is exposed
// over multiple endpoints a single client may have multiple connections to the server, and we want a way to correlate them
// so we can cache state across connections).
type Session struct {
Id string
}

Просмотреть файл

@ -0,0 +1,65 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
package vsrpc
import (
"context"
"encoding/json"
"errors"
"fmt"
"go.lsp.dev/jsonrpc2"
)
// connectionObserver is an interface that can be implemented by types that want to be notified when they are deserialized
// during a JSON RPC call.
type connectionObserver interface {
attachConnection(c jsonrpc2.Conn)
}
// IObserver is treated special by our JSON-RPC implementation and plays nicely with StreamJsonRpc's ideas on how to
// marshal an IObserver<T> in .NET.
//
// The way this works is that that we can send a notification back to to the server with the method
// `$/invokeProxy/{handle}/{onCompleted|onNext}`. When marshalled as an argument, the wire format of IObserver<T> is:
//
// {
// "__jsonrpc_marshaled": 1,
// "handle": <some-integer>
// }
type IObserver[T any] struct {
handle int
c jsonrpc2.Conn
}
func (o *IObserver[T]) OnNext(ctx context.Context, value T) error {
return o.c.Notify(ctx, fmt.Sprintf("$/invokeProxy/%d/onNext", o.handle), []any{value})
}
func (o *IObserver[T]) OnCompleted(ctx context.Context) error {
return o.c.Notify(ctx, fmt.Sprintf("$/invokeProxy/%d/onCompleted", o.handle), nil)
}
func (o *IObserver[T]) UnmarshalJSON(data []byte) error {
var wire map[string]int
if err := json.Unmarshal(data, &wire); err != nil {
return err
}
if v, has := wire["__jsonrpc_marshaled"]; !has || v != 1 {
return errors.New("expected __jsonrpc_marshaled=1")
}
if v, has := wire["handle"]; !has {
return errors.New("expected handle")
} else {
o.handle = v
}
return nil
}
func (o *IObserver[T]) attachConnection(c jsonrpc2.Conn) {
o.c = c
}

Просмотреть файл

@ -0,0 +1,76 @@
package vsrpc
import (
"testing"
"github.com/stretchr/testify/require"
"go.lsp.dev/jsonrpc2"
)
func TestUnmarshalIObserver(t *testing.T) {
t.Parallel()
t.Run("Ok", func(t *testing.T) {
req, err := jsonrpc2.NewCall(jsonrpc2.NewNumberID(1), "Test", []any{
map[string]any{
"__jsonrpc_marshaled": 1,
"handle": 1,
},
})
require.NoError(t, err)
// As part of unmarshaling, we expect the IObserver to have it's connection parameter set to be set so it can send
// messages back later. Create a new dummy jsonrpc2.Conn so we can validate this.
con := jsonrpc2.NewConn(nil)
o, err := unmarshalArg[IObserver[int]](con, req, 0)
require.NoError(t, err)
require.Equal(t, con, o.c, "rpc connection was not attached during unmarshaling!")
})
t.Run("No Tag", func(t *testing.T) {
req, err := jsonrpc2.NewCall(jsonrpc2.NewNumberID(1), "Test", []any{
map[string]any{
"handle": 1,
},
})
require.NoError(t, err)
_, err = unmarshalArg[IObserver[int]](nil, req, 0)
require.Error(t, err)
})
t.Run("Bad Tag", func(t *testing.T) {
req, err := jsonrpc2.NewCall(jsonrpc2.NewNumberID(1), "Test", []any{
map[string]any{
"__jsonrpc_marshaled": 0,
"handle": 1,
},
})
require.NoError(t, err)
_, err = unmarshalArg[IObserver[int]](nil, req, 0)
require.Error(t, err)
})
t.Run("No Handle", func(t *testing.T) {
req, err := jsonrpc2.NewCall(jsonrpc2.NewNumberID(1), "Test", []any{
map[string]any{
"__jsonrpc_marshaled": 1,
},
})
require.NoError(t, err)
_, err = unmarshalArg[IObserver[int]](nil, req, 0)
require.Error(t, err)
})
t.Run("Bad Format", func(t *testing.T) {
req, err := jsonrpc2.NewCall(jsonrpc2.NewNumberID(1), "Test", []any{
"not-an-observer",
})
require.NoError(t, err)
_, err = unmarshalArg[IObserver[int]](nil, req, 0)
require.Error(t, err)
})
}

Просмотреть файл

@ -0,0 +1,157 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
package vsrpc
import (
"context"
"encoding/json"
"log"
"net"
"net/http"
"os"
"strconv"
"sync"
"time"
"github.com/azure/azure-dev/cli/azd/pkg/ioc"
"github.com/gorilla/websocket"
"go.lsp.dev/jsonrpc2"
)
type Server struct {
// sessions is a map of session IDs to server sessions.
sessions map[string]*serverSession
// sessionsMu protects access to sessions.
sessionsMu sync.Mutex
// rootContainer contains all the core registrations for the azd components.
// It is not expected to be modified throughout the lifetime of the server.
rootContainer *ioc.NestedContainer
}
func NewServer(rootContainer *ioc.NestedContainer) *Server {
return &Server{
sessions: make(map[string]*serverSession),
rootContainer: rootContainer,
}
}
// upgrader is the websocket.Upgrader used by the server to upgrade each request to a websocket connection.
var upgrader = websocket.Upgrader{}
// Serve calls http.Serve with the given listener and a handler that serves the VS RPC protocol.
func (s *Server) Serve(l net.Listener) error {
mux := http.NewServeMux()
mux.Handle("/AspireService/v1.0", newAspireService(s))
mux.Handle("/ServerService/v1.0", newServerService(s))
mux.Handle("/EnvironmentService/v1.0", newEnvironmentService(s))
// Expose a few special test endpoints that can be used to debug our special RPC behavior around cancellation and
// IObservers. This is useful for both developers unit testing in VS Code (where they can set this value in launch.json
// as well as tests where we can set this value with t.SetEnv()).
if on, err := strconv.ParseBool(os.Getenv("AZD_DEBUG_SERVER_DEBUG_ENDPOINTS")); err == nil && on {
mux.Handle("/TestDebugService/v1.0", newDebugService())
}
server := http.Server{
ReadHeaderTimeout: 1 * time.Second,
Handler: mux,
}
return server.Serve(l)
}
// serveRpc upgrades the HTTP connection to a WebSocket connection and then serves a set of named method using JSON-RPC 2.0.
func serveRpc(w http.ResponseWriter, r *http.Request, handlers map[string]Handler) {
c, err := upgrader.Upgrade(w, r, nil)
if err != nil {
log.Print("upgrade:", err)
return
}
defer c.Close()
rpcServer := jsonrpc2.NewConn(newWebSocketStream(c))
cancelers := make(map[jsonrpc2.ID]context.CancelFunc)
cancelersMu := sync.Mutex{}
rpcServer.Go(r.Context(), func(ctx context.Context, reply jsonrpc2.Replier, req jsonrpc2.Request) error {
log.Printf("handling rpc %s", req.Method())
// Observe cancellation messages from the client to us. The protocol is a message sent to the `$/cancelRequest`
// method with an `id` parameter that is the ID of the request to cancel. For each inflight RPC we track the
// corresponding cancel function in `cancelers` and use it to cancel the request when we observe it. When an RPC
// completes, we remove the cancel function from the map and invoke it, as the context package recommends.
//
// Inside the handler itself, we observe the error returned by the actual RPC function and if it matches the
// value of ctx.Err() we know that the function observed an responded to cancellation. We then return an RPC error
// with a special code that StreamJsonRpc understands as a cancellation error.
if req.Method() == "$/cancelRequest" {
var cancelArgs struct {
Id *int32 `json:"id"`
}
if err := json.Unmarshal(req.Params(), &cancelArgs); err != nil {
return reply(ctx, nil, jsonrpc2.ErrInvalidParams)
}
if cancelArgs.Id == nil {
return reply(ctx, nil, jsonrpc2.ErrInvalidParams)
}
id := jsonrpc2.NewNumberID(*cancelArgs.Id)
cancelersMu.Lock()
cancel, has := cancelers[id]
cancelersMu.Unlock()
if has {
cancel()
// We'll remove the cancel function from the map once the function observes cancellation and the handler
// returns, no need to remove it eagerly here.
}
return reply(ctx, nil, nil)
}
handler, ok := handlers[req.Method()]
if !ok {
return reply(ctx, nil, jsonrpc2.ErrMethodNotFound)
}
go func() {
start := time.Now()
var childCtx context.Context = ctx
// If this is a call, create a new context and cancel function to track the request and allow it to be
// canceled.
call, isCall := req.(*jsonrpc2.Call)
if isCall {
ctx, cancel := context.WithCancel(ctx)
childCtx = ctx
cancelersMu.Lock()
cancelers[call.ID()] = cancel
cancelersMu.Unlock()
}
err := handler(childCtx, rpcServer, reply, req)
if isCall {
cancelersMu.Lock()
cancel, has := cancelers[call.ID()]
cancelersMu.Unlock()
if has {
cancel()
cancelersMu.Lock()
delete(cancelers, call.ID())
cancelersMu.Unlock()
}
}
if err != nil {
log.Printf("handled rpc %s in %s with err: %v", req.Method(), time.Since(start), err)
} else {
log.Printf("handled rpc %s in %s", req.Method(), time.Since(start))
}
}()
return nil
})
<-rpcServer.Done()
}

Просмотреть файл

@ -0,0 +1,76 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
package vsrpc
import (
"context"
"log"
"net/http"
"os"
)
// serverService is the RPC server for the '/ServerService/v1.0' endpoint.
type serverService struct {
server *Server
}
func newServerService(server *Server) *serverService {
return &serverService{
server: server,
}
}
// InitializeAsync is the server implementation of:
// ValueTask<Session> InitializeAsync(string rootPath, CancellationToken cancellationToken);
func (s *serverService) InitializeAsync(ctx context.Context, rootPath string) (*Session, error) {
// TODO(azure/azure-dev#3288): Ideally the Chdir would be be something we injected into components instead of it being
// ambient authority. We'll get there, but for now let's also just Chdir into the root folder so places where we use
// a relative path will work.
//
// In practice we do not expect multiple clients with different root paths to be calling into the same server. If you
// need that today, launch a new server for each root path...
if err := os.Chdir(rootPath); err != nil {
return nil, err
}
id, session, err := s.server.newSession()
if err != nil {
return nil, err
}
session.rootPath = rootPath
session.rootContainer = s.server.rootContainer
return &Session{
Id: id,
}, nil
}
// StopAsync is the server implementation of:
// ValueTask StopAsync(CancellationToken cancellationToken);
func (s *serverService) StopAsync(ctx context.Context) error {
// TODO(azure/azure-dev#3286): Need to think about how shutdown works. For now it is probably best to just have the
// client terminate `azd` once they know all outstanding RPCs have completed instead of trying to do a graceful
// shutdown on our end.
return nil
}
// ServeHTTP implements http.Handler.
func (s *serverService) ServeHTTP(w http.ResponseWriter, r *http.Request) {
serveRpc(w, r, map[string]Handler{
"InitializeAsync": HandlerFunc1(s.InitializeAsync),
"StopAsync": HandlerAction0(s.StopAsync),
})
}
// newWriter returns a *writerMultiplexer that has a default writer that writes to log.Printf with the given prefix.
func newWriter(prefix string) *writerMultiplexer {
wm := &writerMultiplexer{}
wm.AddWriter(writerFunc(func(p []byte) (n int, err error) {
log.Printf("%s%s", prefix, string(p))
return n, nil
}))
return wm
}

Просмотреть файл

@ -0,0 +1,148 @@
package vsrpc
import (
"context"
"crypto/rand"
"encoding/base64"
"fmt"
"io"
"os"
"strings"
"github.com/azure/azure-dev/cli/azd/internal"
"github.com/azure/azure-dev/cli/azd/pkg/environment/azdcontext"
"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/lazy"
"github.com/azure/azure-dev/cli/azd/pkg/output"
"github.com/mattn/go-colorable"
"go.lsp.dev/jsonrpc2"
)
// serverSession represents a logical connection from a single client with a given azd project path.
// Since our RPCs are split over multiple HTTP endpoints, we need a way to correlate these multiple connections together.
// This is the purpose of the session.
// Sessions are assigned a unique ID when they are created and are stored in the sessions map.
type serverSession struct {
id string
// rootPath is the path to the root of the solution.
rootPath string
// root container points to server.rootContainer
rootContainer *ioc.NestedContainer
}
// newSession creates a new session and returns the session ID and session. newSession is safe to call by multiple
// goroutines. A Session can be recovered from an id with [sessionFromId].
func (s *Server) newSession() (string, *serverSession, error) {
b := make([]byte, 8)
_, err := rand.Read(b)
if err != nil {
return "", nil, err
}
id := base64.StdEncoding.EncodeToString(b)
session := &serverSession{}
s.sessionsMu.Lock()
defer s.sessionsMu.Unlock()
s.sessions[id] = session
return id, session, nil
}
// sessionFromId fetches the session with the given ID, if it exists. sessionFromId is safe to call by multiple goroutines.
func (s *Server) sessionFromId(id string) (*serverSession, bool) {
s.sessionsMu.Lock()
defer s.sessionsMu.Unlock()
session, ok := s.sessions[id]
return session, ok
}
// validateSession ensures the session id is valid and returns the corresponding and serverSession object. If there
// is an error it will be of type *jsonrpc2.Error.
func (s *Server) validateSession(ctx context.Context, session Session) (*serverSession, error) {
if session.Id == "" {
return nil, jsonrpc2.NewError(jsonrpc2.InvalidParams, "session.Id is required")
}
serverSession, has := s.sessionFromId(session.Id)
if !has {
return nil, jsonrpc2.NewError(jsonrpc2.InvalidParams, "session.Id is invalid")
}
serverSession.id = session.Id
return serverSession, nil
}
type container struct {
*ioc.NestedContainer
outWriter *writerMultiplexer
errWriter *writerMultiplexer
}
// newContainer creates a new container for the session.
func (s *serverSession) newContainer() (*container, error) {
c, err := s.rootContainer.NewScopeRegistrationsOnly()
if err != nil {
return nil, err
}
id := s.id
azdCtx := azdcontext.NewAzdContextWithDirectory(s.rootPath)
outWriter := newWriter(fmt.Sprintf("[%s stdout] ", id))
errWriter := newWriter(fmt.Sprintf("[%s stderr] ", id))
// Useful for debugging, direct all the output to the console, so you can see it in VS Code.
outWriter.AddWriter(&lineWriter{
next: writerFunc(func(p []byte) (n int, err error) {
os.Stdout.Write([]byte(fmt.Sprintf("[%s stdout] %s", id, string(p))))
return n, nil
}),
})
errWriter.AddWriter(&lineWriter{
next: writerFunc(func(p []byte) (n int, err error) {
os.Stdout.Write([]byte(fmt.Sprintf("[%s stderr] %s", id, string(p))))
return n, nil
}),
})
c.MustRegisterScoped(func() input.Console {
stdout := outWriter
stderr := errWriter
stdin := strings.NewReader("")
writer := colorable.NewNonColorable(stdout)
return input.NewConsole(true, false, writer, input.ConsoleHandles{
Stdin: stdin,
Stdout: stdout,
Stderr: stderr,
}, &output.NoneFormatter{})
})
c.MustRegisterScoped(func(console input.Console) io.Writer {
return colorable.NewNonColorable(console.Handles().Stdout)
})
c.MustRegisterScoped(func() *internal.GlobalCommandOptions {
return &internal.GlobalCommandOptions{
NoPrompt: true,
}
})
c.MustRegisterScoped(func() *azdcontext.AzdContext {
return azdCtx
})
c.MustRegisterScoped(func() *lazy.Lazy[*azdcontext.AzdContext] {
return lazy.From(azdCtx)
})
return &container{
NestedContainer: c,
outWriter: outWriter,
errWriter: errWriter,
}, nil
}

Просмотреть файл

@ -0,0 +1,134 @@
package vsrpc
import (
"context"
"encoding/json"
"errors"
"net/http/httptest"
"net/url"
"sync"
"testing"
"github.com/gorilla/websocket"
"github.com/stretchr/testify/require"
"go.lsp.dev/jsonrpc2"
)
func TestCancellation(t *testing.T) {
// wg controls when cancellation is sent by the client. We wait until the server RPC has started
// to run before requesting cancellation so we ensure we are testing our logic.
var wg sync.WaitGroup
wg.Add(1)
debugService := newDebugService()
debugService.wg = &wg
debugServer := httptest.NewServer(debugService)
defer debugServer.Close()
// Connect to the server and start running a JSON-RPC 2.0 connection so we can send and recieve messages.
serverUrl, err := url.Parse(debugServer.URL)
require.NoError(t, err)
serverUrl.Scheme = "ws"
wsConn, _, err := websocket.DefaultDialer.Dial(serverUrl.String(), nil)
require.NoError(t, err)
rpcConn := jsonrpc2.NewConn(newWebSocketStream(wsConn))
rpcConn.Go(context.Background(), nil)
// Call blocks until the response is returned from the server, so spin off a goroutine that will make
// the call and the shuttle the response back to us.
result := make(chan struct {
res bool
err error
})
go func() {
var res bool
_, err := rpcConn.Call(context.Background(), "TestCancelAsync", []any{10000}, &res)
result <- struct {
res bool
err error
}{res, err}
close(result)
}()
// Wait until the server starts processing the RPC, then request it be cancelled. We know the
// id of the inflight call is 1 because the jsonrpc2 package assigns ids starting at 1.
wg.Wait()
err = rpcConn.Notify(context.Background(), "$/cancelRequest", struct {
Id int `json:"id"`
}{Id: 1})
require.NoError(t, err)
// Now, wait for the RPC to either complete (if we have a bug) or to observe cancellation and have
// the results sent back here.
res := <-result
var rpcErr *jsonrpc2.Error
require.False(t, res.res, "call should have been aborted, and returned false")
require.True(t, errors.As(res.err, &rpcErr))
require.Equal(t, requestCanceledErrorCode, rpcErr.Code)
}
func TestObserverable(t *testing.T) {
debugServer := httptest.NewServer(newDebugService())
defer debugServer.Close()
// Connect to the server and start running a JSON-RPC 2.0 connection so we can send and recieve messages.
serverUrl, err := url.Parse(debugServer.URL)
require.NoError(t, err)
serverUrl.Scheme = "ws"
wsConn, _, err := websocket.DefaultDialer.Dial(serverUrl.String(), nil)
require.NoError(t, err)
// The IObserver machinary ends up sending RPCs back to the client, capture them so we can validate they are
// correct later.
var onNextParams []json.RawMessage
var onCompletedParams []json.RawMessage
rpcConn := jsonrpc2.NewConn(newWebSocketStream(wsConn))
rpcConn.Go(context.Background(), func(ctx context.Context, reply jsonrpc2.Replier, req jsonrpc2.Request) error {
switch req.Method() {
case "$/invokeProxy/1/onNext":
onNextParams = append(onNextParams, req.Params())
case "$/invokeProxy/1/onCompleted":
onCompletedParams = append(onCompletedParams, req.Params())
default:
require.Fail(t, "unexpected rpc %s delivered", req.Method())
}
return nil
})
// The second argument is the wire form of an IObserver as marshalled by StreamJsonRpc. We use the handle when sending
// messages back to the client.
args := []any{
10,
map[string]any{
"__jsonrpc_marshaled": 1,
"handle": 1,
},
}
require.NoError(t, err)
_, err = rpcConn.Call(context.Background(), "TestIObserverAsync", args, nil)
require.NoError(t, err)
require.Len(t, onNextParams, 10)
require.Len(t, onCompletedParams, 1)
// Ensure the correct integers were sent back in the correct order, this should match
// the order the were emited by the server.
for idx, params := range onNextParams {
var args []int
require.NoError(t, json.Unmarshal(params, &args))
require.Len(t, args, 1)
require.Equal(t, idx, args[0])
}
// The onCompleted message takes no parameters and the args value is empty.
require.Len(t, onCompletedParams[0], 0)
}

Просмотреть файл

@ -0,0 +1,58 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
package vsrpc
import (
"context"
"encoding/json"
"fmt"
"github.com/gorilla/websocket"
"go.lsp.dev/jsonrpc2"
)
// wsStream adapts the websocket.Conn to jsonrpc2.Stream interface
type wsStream struct {
c *websocket.Conn
}
// Close implements jsonrpc2.Stream.
func (*wsStream) Close() error {
// TODO(azure/azure-dev#3286): Need to think about what to do here. Close the conn?
return nil
}
// Read implements jsonrpc2.Stream.
func (s *wsStream) Read(ctx context.Context) (jsonrpc2.Message, int64, error) {
mt, data, err := s.c.ReadMessage()
if err != nil {
return nil, 0, err
}
if mt != websocket.TextMessage {
return nil, 0, fmt.Errorf("unexpected message type: %v", mt)
}
msg, err := jsonrpc2.DecodeMessage(data)
if err != nil {
return nil, 0, err
}
return msg, int64(len(data)), nil
}
// Write implements jsonrpc2.Stream.
func (s *wsStream) Write(ctx context.Context, msg jsonrpc2.Message) (int64, error) {
data, err := json.Marshal(msg)
if err != nil {
return 0, fmt.Errorf("marshaling message: %w", err)
}
if err := s.c.WriteMessage(websocket.TextMessage, data); err != nil {
return 0, err
}
return int64(len(data)), nil
}
func newWebSocketStream(c *websocket.Conn) *wsStream {
return &wsStream{c: c}
}

5
cli/azd/internal/vsrpc/testdata/dotnet-azd-client/.gitignore поставляемый Normal file
Просмотреть файл

@ -0,0 +1,5 @@
[B|b]in/
[O|o]bj/
# This was added after the template was checked in, you can modify
# so you can modify the file without git pending the edits.
Settings.cs

Просмотреть файл

@ -0,0 +1,26 @@
{
"version": "0.2.0",
"configurations": [
{
// Use IntelliSense to find out which attributes exist for C# debugging
// Use hover for the description of the existing attributes
// For further information visit https://github.com/dotnet/vscode-csharp/blob/main/debugger-launchjson.md
"name": ".NET Core Launch (console)",
"type": "coreclr",
"request": "launch",
"preLaunchTask": "build",
// If you have changed target frameworks, make sure to update the program path.
"program": "${workspaceFolder}/bin/Debug/net8.0/dotnet-azd-client.dll",
"args": [],
"cwd": "${workspaceFolder}",
// For more information about the 'console' field, see https://aka.ms/VSCode-CS-LaunchJson-Console
"console": "internalConsole",
"stopAtEntry": false
},
{
"name": ".NET Core Attach",
"type": "coreclr",
"request": "attach"
}
]
}

Просмотреть файл

@ -0,0 +1,41 @@
{
"version": "2.0.0",
"tasks": [
{
"label": "build",
"command": "dotnet",
"type": "process",
"args": [
"build",
"${workspaceFolder}/dotnet-azd-client.csproj",
"/property:GenerateFullPaths=true",
"/consoleloggerparameters:NoSummary;ForceNoAlign"
],
"problemMatcher": "$msCompile"
},
{
"label": "publish",
"command": "dotnet",
"type": "process",
"args": [
"publish",
"${workspaceFolder}/dotnet-azd-client.csproj",
"/property:GenerateFullPaths=true",
"/consoleloggerparameters:NoSummary;ForceNoAlign"
],
"problemMatcher": "$msCompile"
},
{
"label": "watch",
"command": "dotnet",
"type": "process",
"args": [
"watch",
"run",
"--project",
"${workspaceFolder}/dotnet-azd-client.csproj"
],
"problemMatcher": "$msCompile"
}
]
}

112
cli/azd/internal/vsrpc/testdata/dotnet-azd-client/AzdServices.cs поставляемый Normal file
Просмотреть файл

@ -0,0 +1,112 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
public class EnvironmentInfo
{
public EnvironmentInfo(string name, bool isCurrent = false)
{
Name = name;
IsCurrent = isCurrent;
}
public string Name { get; }
public bool IsCurrent { get; }
}
public class Environment {
public string Name { get; set; } = "";
public bool IsCurrent { get; set; } = false;
public Dictionary<string, string> Properties { get; set; } = new Dictionary<string, string>();
public Service[] Services { get; set; } = [];
public Environment(string name) {
Name = name;
}
}
public class AspireHost {
public string Name { get; set; } = "";
public string Path { get; set; } = "";
public Service[] Services { get; set; } = [];
public string? Kind { get; set; }
public string? Endpoint { get; set; }
public string? ResourceId { get; set; }
}
public class Service {
public string Name { get; set; } = "";
public bool IsExternal { get; set; }
public string? Kind { get; set;}
public string? Endpoint { get; set;}
public string? ResourceId { get; set;}
}
public class Session {
public string Id { get; set; } = "";
}
public class ProgressMessage
{
public ProgressMessage(
string message, MessageSeverity severity, DateTime time, MessageKind kind, string code, string additionalInfoLink)
{
Message = message;
Severity = severity;
Time = time;
Kind = kind;
Code = code;
AdditionalInfoLink = additionalInfoLink;
}
public string Message;
public MessageSeverity Severity;
public DateTime Time;
public MessageKind Kind;
public string Code;
public string AdditionalInfoLink;
public override string ToString() => $"{Time}: {Severity} {Message}";
}
public enum MessageSeverity
{
Info = 0,
Warning = 1,
Error = 2,
}
public enum MessageKind
{
Logging = 0,
Important = 1
}
// To expose this, ensure that AZD_DEBUG_SERVER_DEBUG_ENDPOINTS is set to true when running `azd vs-server`.
public interface IDebugService {
ValueTask<bool> TestCancelAsync(int timeoutMs, CancellationToken cancellationToken);
ValueTask TestIObserverAsync(int max, IObserver<int> observer, CancellationToken cancellationToken);
}
public interface IServerService {
ValueTask<Session> InitializeAsync(string rootPath, CancellationToken cancellationToken);
}
public interface IEnvironmentService {
ValueTask<IEnumerable<EnvironmentInfo>> GetEnvironmentsAsync(Session s, IObserver<ProgressMessage> outputObserver, CancellationToken cancellationToken);
ValueTask<Environment> OpenEnvironmentAsync(Session s, string envName, IObserver<ProgressMessage> outputObserver, CancellationToken cancellationToken);
ValueTask<Environment> LoadEnvironmentAsync(Session s, string envName, IObserver<ProgressMessage> outputObserver, CancellationToken cancellationToken);
ValueTask<Environment> RefreshEnvironmentAsync(Session s, string envName, IObserver<ProgressMessage> outputObserver, CancellationToken cancellationToken);
ValueTask<bool> CreateEnvironmentAsync(Session s, Environment newEnv,IObserver<ProgressMessage> outputObserver, CancellationToken cancellationToken);
ValueTask<bool> SetCurrentEnvironmentAsync(Session s, string envName, IObserver<ProgressMessage> outputObserver, CancellationToken cancellationToken);
ValueTask<Environment> DeployAsync(Session s, string envName, IObserver<ProgressMessage> outputObserver, CancellationToken cancellationToken);
}
public interface IAspireService {
ValueTask<AspireHost> GetAspireHostAsync(Session s, string aspireEnvironment, IObserver<ProgressMessage> outputObserver, CancellationToken cancellationToken);
}

211
cli/azd/internal/vsrpc/testdata/dotnet-azd-client/Program.cs поставляемый Normal file
Просмотреть файл

@ -0,0 +1,211 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
using System.Net.WebSockets;
using StreamJsonRpc;
ClientWebSocket wsClientES = new ClientWebSocket();
await wsClientES.ConnectAsync(new Uri("ws://127.0.0.1:8080/EnvironmentService/v1.0"), CancellationToken.None);
ClientWebSocket wsClientAS = new ClientWebSocket();
await wsClientAS.ConnectAsync(new Uri("ws://127.0.0.1:8080/AspireService/v1.0"), CancellationToken.None);
ClientWebSocket wsClientSS = new ClientWebSocket();
await wsClientSS.ConnectAsync(new Uri("ws://127.0.0.1:8080/ServerService/v1.0"), CancellationToken.None);
ClientWebSocket wsClientDS = new ClientWebSocket();
await wsClientDS.ConnectAsync(new Uri("ws://127.0.0.1:8080/TestDebugService/v1.0"), CancellationToken.None);
IEnvironmentService esSvc = JsonRpc.Attach<IEnvironmentService>(new WebSocketMessageHandler(wsClientES));
IAspireService asSvc = JsonRpc.Attach<IAspireService>(new WebSocketMessageHandler(wsClientAS));
IServerService ssSvc = JsonRpc.Attach<IServerService>(new WebSocketMessageHandler(wsClientSS));
IDebugService dsSvc = JsonRpc.Attach<IDebugService>(new WebSocketMessageHandler(wsClientDS));
{
Console.WriteLine("== Testing Cancel ==");
var cts = new CancellationTokenSource();
var t = dsSvc.TestCancelAsync(1000 * 10, cts.Token);
await Task.Delay(1000);
Console.WriteLine("Cancelling");
cts.Cancel();
Console.WriteLine("Observing Task");
try {
var result = await t;
Console.WriteLine($"TestCancelAsync completed with result: {result}");
} catch (TaskCanceledException) {
Console.WriteLine($"TestCancelAsync was cancelled");
} catch (Exception e) {
Console.WriteLine($"TestCancelAsync threw unexpected exception: {e}");
}
}
{
Console.WriteLine("== Testing IObservable ==");
var intObserver = new WriterObserver<int>();
var t = dsSvc.TestIObserverAsync(10, intObserver, CancellationToken.None);
await Task.Delay(2000);
await t;
Console.WriteLine("== Done Testing IObservable ==");
}
await RunLifecycle();
/*
* Run an end to end life cycle test for AZD, we do the following steps:
* 1. Initialize a new session.
* 2. Call GetAspireHostAsync to fetch information about the host and print it.
* 3. Call GetEnvironmentsAsync to fetch information about each environment and print it.
* 4. If the Environment set in the EnvironmentName variable does not exist, create it.
* 5. Call GetEnvironmentsAsync to fetch information about each environment and print it.
* 6. Call OpenEnvironmentAsync to fetch brief information about the specific environment and print it.
* 7. Call LoadEnvironmentAsync to fetch more detailed information about the specific environment and print it.
* 8. Call RefreshEnvironmentAsync to fetch even more detailed information about the specific environment and print it.
* 9. Call DeployAsync to deploy the specific environment, writing the output to stdout and then printing the result.
* 10. Call SetCurrentEnvironmentAsync to set the specific environment as the current environment.
*/
#pragma warning disable CS8321 // The local function 'RunLifecycle' is declared but never used
async Task RunLifecycle() {
#pragma warning restore CS8321 // The local function 'RunLifecycle' is declared but never used
if (string.IsNullOrEmpty(Settings.RootPath)) {
throw new ArgumentException("RootPath must be set");
}
if (string.IsNullOrEmpty(Settings.EnvironmentName)) {
throw new ArgumentException("EnvironmentName must be set");
}
if (string.IsNullOrEmpty(Settings.SubscriptionId)) {
throw new ArgumentException("SubscriptionId must be set");
}
if (string.IsNullOrEmpty(Settings.Location)) {
throw new ArgumentException("Location must be set");
}
bool hasEnvironment = false;
IObserver<ProgressMessage> observer = new WriterObserver<ProgressMessage>();
Console.WriteLine($"== Initializing ==");
var session = await ssSvc.InitializeAsync(Settings.RootPath, CancellationToken.None);
Console.WriteLine($"== Done Initializing ==");
{
Console.WriteLine("== Getting Host Info: Production ==");
var result = await asSvc.GetAspireHostAsync(session, "Production", observer, CancellationToken.None);
Console.WriteLine($"Aspire Host: {result.Name} ({result.Path})");
foreach (Service s in result.Services) {
Console.WriteLine($" Service: {s.Name} ({s.IsExternal})");
}
Console.WriteLine("== Got Host Info ==");
}
{
Console.WriteLine("== Getting Environments ==");
foreach (var e in await esSvc.GetEnvironmentsAsync(session, observer, CancellationToken.None)) {
Console.WriteLine($"Environment: {e.Name} IsCurrent: {e.IsCurrent}");
hasEnvironment = hasEnvironment || e.Name == Settings.EnvironmentName;
}
Console.WriteLine("== Got Environments ==");
}
if (!hasEnvironment)
{
Console.WriteLine($"== Creating Environment: {Settings.EnvironmentName} ==");
Environment e = new Environment(Settings.EnvironmentName) {
Properties = new Dictionary<string, string>() {
{ "ASPIRE_ENVIRONMENT", "Production" },
{ "Subscription", Settings.SubscriptionId },
{ "Location", Settings.Location}
},
Services = [
new Service() {
Name = "apiservice",
IsExternal = false,
},
new Service() {
Name = "webfrontend",
IsExternal = true,
}
],
};
var result = await esSvc.CreateEnvironmentAsync(session, e, observer, CancellationToken.None);
Console.WriteLine($"Created environment: {result}");
Console.WriteLine("== Done Creating Environment ==");
}
{
Console.WriteLine("== Getting Environments ==");
foreach (var e in await esSvc.GetEnvironmentsAsync(session, observer, CancellationToken.None)) {
Console.WriteLine($"Environment: {e.Name} IsCurrent: {e.IsCurrent}");
}
Console.WriteLine("== Done Getting Environments ==");
}
{
Console.WriteLine($"== Opening Environment: {Settings.EnvironmentName} ==");
var result = await esSvc.OpenEnvironmentAsync(session, Settings.EnvironmentName, observer, CancellationToken.None);
WriteEnvironment(result);
Console.WriteLine($"== Done Environment: {Settings.EnvironmentName} ==");
}
{
Console.WriteLine($"== Loading Environment: {Settings.EnvironmentName} ==");
var result = await esSvc.LoadEnvironmentAsync(session, Settings.EnvironmentName, observer, CancellationToken.None);
WriteEnvironment(result);
Console.WriteLine($"== Done Loading Environment: {Settings.EnvironmentName} ==");
}
{
Console.WriteLine($"== Refreshing Environment: {Settings.EnvironmentName} ==");
var result = await esSvc.RefreshEnvironmentAsync(session, Settings.EnvironmentName, observer, CancellationToken.None);
WriteEnvironment(result);
Console.WriteLine($"== Done Refreshing Environment: {Settings.EnvironmentName} ==");
}
{
Console.WriteLine($"== Deploying Environment: {Settings.EnvironmentName} ==");
var result = await esSvc.DeployAsync(session, Settings.EnvironmentName, observer, CancellationToken.None);
WriteEnvironment(result);
Console.WriteLine("== Done Deploying Environment ==");
}
{
Console.WriteLine($"== Setting Current Environment: {Settings.EnvironmentName} ==");
var result = await esSvc.SetCurrentEnvironmentAsync(session, Settings.EnvironmentName, observer, CancellationToken.None);
Console.WriteLine($"Result: {result}");
Console.WriteLine("== Done Setting Current Environment ==");
}
}
#pragma warning disable CS8321 // The local function 'WriteEnvironment' is declared but never used
void WriteEnvironment(Environment e) {
#pragma warning restore CS8321 // The local function 'WriteEnvironment' is declared but never used
Console.WriteLine($"Environment: {e.Name} {e.IsCurrent}");
foreach (Service s in e.Services) {
Console.WriteLine($" Service: {s.Name}");
Console.WriteLine($" External: {s.IsExternal}");
Console.WriteLine($" Endpoint: {s.Endpoint}");
Console.WriteLine($" Resource: {s.ResourceId}");
}
foreach (KeyValuePair<string, string> kvp in e.Properties) {
Console.WriteLine($" Property: {kvp.Key} = {kvp.Value}");
}
}
class WriterObserver<ProgressMessage> : IObserver<ProgressMessage>
{
public void OnCompleted() => Console.WriteLine("Completed");
public void OnError(Exception error) => Console.WriteLine($"Error: {error}");
public void OnNext(ProgressMessage value) {
var msg = value!.ToString()!;
if (msg[msg.Length-1] == '\n') {
Console.Write(msg);
} else {
Console.WriteLine(msg);
}
}
}

Просмотреть файл

@ -0,0 +1,15 @@
// Copy this file's contents to Settings.cs and fill in the values. Settings.cs is in the .gitignore file so it won't be
// Checked in.
/*
public static class Settings {
// The name of the environment to create and use as part of the RunLifecycle function.
public const string EnvironmentName = "";
// The Root Path to the Aspire solution to use as part of the RunLifecycle function. The test expects this to be
// a folder with the output of `dotnet new aspire-starter` in it.
public const string RootPath = "";
// The SubscriptionId to use as part of the RunLifecycle function when creating a new environment.
public const string SubscriptionId = "";
// The Location to use as part of the RunLifecycle function when creating a new environment.
public const string Location = "";
}
*/

Просмотреть файл

@ -0,0 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<RootNamespace>dotnet_azd_client</RootNamespace>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="StreamJsonRpc" Version="2.17.8" />
</ItemGroup>
</Project>

Просмотреть файл

@ -1,9 +1,9 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.1.31903.286
VisualStudioVersion = 17.5.002.0
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Todo.Api", "Todo.Api.csproj", "{A40339D0-DEF8-4CF7-9E57-CA227AA78056}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "dotnet-azd-client", "dotnet-azd-client.csproj", "{52FFE814-829C-42EA-A6FB-4EF1AAFE3EB4}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
@ -11,15 +11,15 @@ Global
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{A40339D0-DEF8-4CF7-9E57-CA227AA78056}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{A40339D0-DEF8-4CF7-9E57-CA227AA78056}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A40339D0-DEF8-4CF7-9E57-CA227AA78056}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A40339D0-DEF8-4CF7-9E57-CA227AA78056}.Release|Any CPU.Build.0 = Release|Any CPU
{52FFE814-829C-42EA-A6FB-4EF1AAFE3EB4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{52FFE814-829C-42EA-A6FB-4EF1AAFE3EB4}.Debug|Any CPU.Build.0 = Debug|Any CPU
{52FFE814-829C-42EA-A6FB-4EF1AAFE3EB4}.Release|Any CPU.ActiveCfg = Release|Any CPU
{52FFE814-829C-42EA-A6FB-4EF1AAFE3EB4}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {9DE28BD5-F5D0-4A5F-98AC-5404AC5F2FC1}
SolutionGuid = {F9693DD1-6A70-4356-99B2-9A29497E7D12}
EndGlobalSection
EndGlobal

Просмотреть файл

@ -0,0 +1,46 @@
package vsrpc
import (
"context"
"fmt"
"log"
"strings"
"github.com/azure/azure-dev/cli/azd/pkg/apphost"
"github.com/azure/azure-dev/cli/azd/pkg/project"
"github.com/azure/azure-dev/cli/azd/pkg/tools/dotnet"
)
// appHostServiceForProject returns the ServiceConfig of the service for the AppHost project for the given azd project.
func appHostForProject(
ctx context.Context, pc *project.ProjectConfig, dotnetCli dotnet.DotNetCli,
) (*project.ServiceConfig, error) {
for _, service := range pc.Services {
if service.Language == project.ServiceLanguageDotNet {
isAppHost, err := dotnetCli.GetMsBuildProperty(ctx, service.Path(), "IsAspireHost")
if err != nil {
log.Printf("error checking if %s is an app host project: %v", service.Path(), err)
}
if strings.TrimSpace(isAppHost) == "true" {
return service, nil
}
}
}
return nil, fmt.Errorf("no app host project found for project: %s", pc.Name)
}
func servicesFromManifest(manifest *apphost.Manifest) []*Service {
var services []*Service
for name, res := range manifest.Resources {
if res.Type == "project.v0" {
services = append(services, &Service{
Name: name,
Path: *res.Path,
})
}
}
return services
}

Просмотреть файл

@ -0,0 +1,56 @@
package vsrpc
import (
"io"
"sync"
)
// writerMultiplexer is an io.Writer that writes to multiple io.Writers and allows these writers to be added and removed
// dynamically.
type writerMultiplexer struct {
writers []io.Writer
mu sync.Mutex
}
// Write writes the given bytes to all the writers in the multiplexer.
func (m *writerMultiplexer) Write(p []byte) (n int, err error) {
m.mu.Lock()
defer m.mu.Unlock()
for _, w := range m.writers {
n, err = w.Write(p)
if err != nil {
return n, err
}
}
return n, nil
}
// AddWriter adds a writer to the multiplexer.
func (m *writerMultiplexer) AddWriter(w io.Writer) {
m.mu.Lock()
defer m.mu.Unlock()
m.writers = append(m.writers, w)
}
// RemoveWriter removes a writer from the multiplexer.
func (m *writerMultiplexer) RemoveWriter(w io.Writer) {
m.mu.Lock()
defer m.mu.Unlock()
for i, writer := range m.writers {
if writer == w {
m.writers = append(m.writers[:i], m.writers[i+1:]...)
return
}
}
}
// writerFunc is an io.Writer implemented by a function.
type writerFunc func(p []byte) (n int, err error)
// Write implements the io.Writer interface.
func (f writerFunc) Write(p []byte) (n int, err error) {
return f(p)
}

Просмотреть файл

@ -0,0 +1,32 @@
package vsrpc
import (
"bytes"
"testing"
"github.com/stretchr/testify/require"
)
func TestWriterMutiplexer(t *testing.T) {
w := &writerMultiplexer{}
var buf1 bytes.Buffer
var buf2 bytes.Buffer
var buf3 bytes.Buffer
w.AddWriter(&buf1)
w.AddWriter(&buf2)
_, err := w.Write([]byte("hello\n"))
require.NoError(t, err)
w.AddWriter(&buf3)
w.RemoveWriter(&buf2)
_, err = w.Write([]byte("world\n"))
require.NoError(t, err)
require.Equal(t, "hello\nworld\n", buf1.String())
require.Equal(t, "hello\n", buf2.String())
require.Equal(t, "world\n", buf3.String())
}

Просмотреть файл

@ -58,7 +58,7 @@ func main() {
latest := make(chan semver.Version)
go fetchLatestVersion(latest)
cmdErr := cmd.NewRootCmd(ctx, false, nil).ExecuteContext(ctx)
cmdErr := cmd.NewRootCmd(false, nil, nil).ExecuteContext(ctx)
if !isJsonOutput() {
if firstNotice := telemetry.FirstNotice(); firstNotice != "" {

Просмотреть файл

@ -317,7 +317,7 @@ func (m *SubscriptionsManager) ListLocations(
var err error
msg := "Retrieving locations..."
m.console.ShowSpinner(ctx, msg, input.Step)
defer m.console.StopSpinner(ctx, msg, input.GetStepResultFormat(err))
defer m.console.StopSpinner(ctx, "", input.GetStepResultFormat(err))
return m.listLocations(ctx, subscriptionId)
}

Просмотреть файл

@ -17,18 +17,21 @@ type FeatureManager struct {
userConfigCache config.Config
// used for mocking alpha features on testing
alphaFeaturesResolver func() []Feature
withSync *sync.Once
}
// NewFeaturesManager creates the alpha features manager from the user configuration
func NewFeaturesManager(configManager config.UserConfigManager) *FeatureManager {
return &FeatureManager{
configManager: configManager,
withSync: &sync.Once{},
}
}
func NewFeaturesManagerWithConfig(config config.Config) *FeatureManager {
return &FeatureManager{
userConfigCache: config,
withSync: &sync.Once{},
}
}
@ -59,8 +62,6 @@ func (m *FeatureManager) ListFeatures() (map[string]Feature, error) {
return result, nil
}
var withSync *sync.Once = &sync.Once{}
func (m *FeatureManager) initConfigCache() {
if m.userConfigCache == nil {
config, err := m.configManager.Load()
@ -74,7 +75,7 @@ func (m *FeatureManager) initConfigCache() {
// IsEnabled search and find out if the AlphaFeatureId is currently enabled
func (m *FeatureManager) IsEnabled(featureId FeatureId) bool {
// guard from using the alphaFeatureManager from multiple routines. Only the first one will create the cache.
withSync.Do(m.initConfigCache)
m.withSync.Do(m.initConfigCache)
// For testing, and in CI, allow enabling alpha features via the environment.
envName := fmt.Sprintf("AZD_ALPHA_ENABLE_%s", strings.ToUpper(string(featureId)))

Просмотреть файл

@ -1,6 +1,7 @@
package alpha
import (
"sync"
"testing"
"github.com/azure/azure-dev/cli/azd/pkg/config"
@ -31,11 +32,7 @@ func Test_AlphaToggle(t *testing.T) {
})
// We don't need the user-config
alphaManager := &FeatureManager{
alphaFeaturesResolver: mockAlphaFeatures,
userConfigCache: mockConfig,
}
alphaManager := newFeatureManagerForTest(mockAlphaFeatures, mockConfig)
alphaF, err := alphaManager.ListFeatures()
require.NoError(t, err)
require.True(t, len(alphaF) == 1)
@ -62,11 +59,7 @@ func Test_AlphaToggle(t *testing.T) {
})
// We don't need the user-config
alphaManager := &FeatureManager{
alphaFeaturesResolver: mockAlphaFeatures,
userConfigCache: mockConfig,
}
alphaManager := newFeatureManagerForTest(mockAlphaFeatures, mockConfig)
alphaF, err := alphaManager.ListFeatures()
require.NoError(t, err)
require.True(t, len(alphaF) == 1)
@ -97,11 +90,7 @@ func Test_AlphaToggle(t *testing.T) {
})
// We don't need the user-config
alphaManager := &FeatureManager{
alphaFeaturesResolver: mockAlphaFeatures,
userConfigCache: mockConfig,
}
alphaManager := newFeatureManagerForTest(mockAlphaFeatures, mockConfig)
alphaF, err := alphaManager.ListFeatures()
require.NoError(t, err)
require.True(t, len(alphaF) == 2)
@ -136,11 +125,7 @@ func Test_AlphaToggle(t *testing.T) {
})
// We don't need the user-config
alphaManager := &FeatureManager{
alphaFeaturesResolver: mockAlphaFeatures,
userConfigCache: mockConfig,
}
alphaManager := newFeatureManagerForTest(mockAlphaFeatures, mockConfig)
alphaF, err := alphaManager.ListFeatures()
require.NoError(t, err)
require.True(t, len(alphaF) == 2)
@ -160,3 +145,11 @@ func Test_AlphaToggle(t *testing.T) {
})
}
func newFeatureManagerForTest(alphaFeatureResolver func() []Feature, config config.Config) *FeatureManager {
return &FeatureManager{
alphaFeaturesResolver: alphaFeatureResolver,
userConfigCache: config,
withSync: &sync.Once{},
}
}

Просмотреть файл

@ -0,0 +1,57 @@
package apphost
import (
"fmt"
"strings"
)
// evalString evaluates a given string expression, using the provided evalExpr function to produce values for expressions
// in the string. It supports strings that contain expressions of the form "{expression}" where "expression" is any string
// that does not contain a '}' character. The evalExpr function is called with the expression (without the enclosing '{'
// and '}' characters) and should return the value to be substituted into the string. If the evalExpr function returns
// an error, evalString will return that error. The '{' and '}' characters can be escaped by doubling them, e.g.
// "{{" and "}}". If a string is malformed (e.g. an unmatched '{' or '}' character), evalString will return an error.
func EvalString(src string, evalExpr func(string) (string, error)) (string, error) {
var res strings.Builder
for i := 0; i < len(src); i++ {
switch src[i] {
case '{':
if i+1 < len(src) && src[i+1] == '{' {
res.WriteByte('{')
i++
continue
}
closed := false
for j := i + 1; j < len(src); j++ {
if src[j] == '}' {
v, err := evalExpr(src[i+1 : j])
if err != nil {
return "", err
}
res.WriteString(v)
i = j
closed = true
break
}
}
if !closed {
return "", fmt.Errorf("unclosed '{' at position %d", i)
}
case '}':
if i+1 < len(src) && src[i+1] == '}' {
res.WriteByte('}')
i++
continue
}
return "", fmt.Errorf("unexpected '}' at position %d", i)
default:
res.WriteByte(src[i])
}
}
return res.String(), nil
}

Просмотреть файл

@ -0,0 +1,60 @@
package apphost
import (
"fmt"
"testing"
"github.com/stretchr/testify/assert"
)
func TestEvalString(t *testing.T) {
cases := []struct {
name string
src string
want string
}{
{name: "simple", src: "a string with no replacements", want: "a string with no replacements"},
{name: "replacement", src: "{this.one.has.a.replacement}", want: "this.one.has.a.replacement"},
{name: "complex", src: "this {one} has {many} replacements", want: "this one has many replacements"},
{name: "escape", src: "this {{one}} is {{escaped}}", want: "this {one} is {escaped}"},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
res, err := EvalString(c.src, func(s string) (string, error) {
return s, nil
})
assert.NoError(t, err)
assert.Equal(t, c.want, res)
})
}
errorCases := []struct {
name string
src string
}{
{name: "unclosed open", src: "this { is unclosed"},
{name: "unmatched close", src: "this } is unmatched"},
{name: "unmatched escaped close", src: "this {}} is unmatched"},
{name: "unmatched escaped open", src: "this {{} is unmatched"},
}
for _, c := range errorCases {
t.Run(c.name, func(t *testing.T) {
res, err := EvalString(c.src, func(s string) (string, error) {
return s, nil
})
assert.Error(t, err)
assert.Equal(t, "", res)
})
}
res, err := EvalString("{this.one.has.a.replacement}", func(s string) (string, error) {
return "", fmt.Errorf("this should cause evalString to fail")
})
assert.Error(t, err)
assert.Equal(t, "", res)
}

Разница между файлами не показана из-за своего большого размера Загрузить разницу

Просмотреть файл

@ -0,0 +1,441 @@
package apphost
import (
"context"
_ "embed"
"io/fs"
"os"
"path/filepath"
"runtime"
"testing"
"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/dotnet"
"github.com/azure/azure-dev/cli/azd/test/mocks"
"github.com/azure/azure-dev/cli/azd/test/snapshot"
"github.com/stretchr/testify/require"
)
//go:embed testdata/aspire-docker.json
var aspireDockerManifest []byte
//go:embed testdata/aspire-storage.json
var aspireStorageManifest []byte
//go:embed testdata/aspire-bicep.json
var aspireBicepManifest []byte
//go:embed testdata/aspire-escaping.json
var aspireEscapingManifest []byte
//go:embed testdata/aspire-container.json
var aspireContainerManifest []byte
// mockPublishManifest mocks the dotnet run --publisher manifest command to return a fixed manifest.
func mockPublishManifest(mockCtx *mocks.MockContext, manifest []byte, files map[string]string) {
mockCtx.CommandRunner.When(func(args exec.RunArgs, command string) bool {
return args.Cmd == "dotnet" && args.Args[0] == "run" && args.Args[3] == "--publisher" && args.Args[4] == "manifest"
}).RespondFn(func(args exec.RunArgs) (exec.RunResult, error) {
err := os.WriteFile(args.Args[6], manifest, osutil.PermissionFile)
if err != nil {
return exec.RunResult{
ExitCode: -1,
Stderr: err.Error(),
}, err
}
publishDir := filepath.Dir(args.Args[6])
for name, contents := range files {
err := os.WriteFile(filepath.Join(publishDir, name), []byte(contents), osutil.PermissionFile)
if err != nil {
return exec.RunResult{
ExitCode: -1,
Stderr: err.Error(),
}, err
}
}
return exec.RunResult{}, nil
})
}
func TestAspireEscaping(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("Skipping due to EOL issues on Windows with the baselines")
}
ctx := context.Background()
mockCtx := mocks.NewMockContext(ctx)
mockPublishManifest(mockCtx, aspireEscapingManifest, nil)
mockCli := dotnet.NewDotNetCli(mockCtx.CommandRunner)
m, err := ManifestFromAppHost(ctx, filepath.Join("testdata", "AspireDocker.AppHost.csproj"), mockCli, "")
require.NoError(t, err)
for _, name := range []string{"api"} {
t.Run(name, func(t *testing.T) {
tmpl, err := ContainerAppManifestTemplateForProject(m, name)
require.NoError(t, err)
snapshot.SnapshotT(t, tmpl)
})
}
}
func TestAspireStorageGeneration(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("Skipping due to EOL issues on Windows with the baselines")
}
ctx := context.Background()
mockCtx := mocks.NewMockContext(ctx)
mockPublishManifest(mockCtx, aspireStorageManifest, nil)
mockCli := dotnet.NewDotNetCli(mockCtx.CommandRunner)
m, err := ManifestFromAppHost(ctx, filepath.Join("testdata", "AspireDocker.AppHost.csproj"), mockCli, "")
require.NoError(t, err)
files, err := BicepTemplate(m)
require.NoError(t, err)
err = fs.WalkDir(files, ".", func(path string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
if d.IsDir() {
return nil
}
contents, err := fs.ReadFile(files, path)
if err != nil {
return err
}
t.Run(path, func(t *testing.T) {
snapshot.SnapshotT(t, string(contents))
})
return nil
})
require.NoError(t, err)
}
func TestAspireBicepGeneration(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("Skipping due to EOL issues on Windows with the baselines")
}
ctx := context.Background()
mockCtx := mocks.NewMockContext(ctx)
filesFromManifest := make(map[string]string)
ignoredBicepContent := "bicep file contents"
filesFromManifest["test.bicep"] = ignoredBicepContent
filesFromManifest["aspire.hosting.azure.bicep.postgres.bicep"] = ignoredBicepContent
filesFromManifest["aspire.hosting.azure.bicep.servicebus.bicep"] = ignoredBicepContent
filesFromManifest["aspire.hosting.azure.bicep.appinsights.bicep"] = ignoredBicepContent
filesFromManifest["aspire.hosting.azure.bicep.sql.bicep"] = ignoredBicepContent
mockPublishManifest(mockCtx, aspireBicepManifest, filesFromManifest)
mockCli := dotnet.NewDotNetCli(mockCtx.CommandRunner)
m, err := ManifestFromAppHost(ctx, filepath.Join("testdata", "AspireDocker.AppHost.csproj"), mockCli, "")
require.NoError(t, err)
files, err := BicepTemplate(m)
require.NoError(t, err)
err = fs.WalkDir(files, ".", func(path string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
if d.IsDir() {
return nil
}
contents, err := fs.ReadFile(files, path)
if err != nil {
return err
}
t.Run(path, func(t *testing.T) {
snapshot.SnapshotT(t, string(contents))
})
return nil
})
require.NoError(t, err)
}
func TestAspireDockerGeneration(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("Skipping due to EOL issues on Windows with the baselines")
}
ctx := context.Background()
mockCtx := mocks.NewMockContext(ctx)
mockPublishManifest(mockCtx, aspireDockerManifest, nil)
mockCli := dotnet.NewDotNetCli(mockCtx.CommandRunner)
m, err := ManifestFromAppHost(ctx, filepath.Join("testdata", "AspireDocker.AppHost.csproj"), mockCli, "")
require.NoError(t, err)
// The App Host manifest does not set the external bit for project resources. Instead, `azd` or whatever tool consumes
// the manifest should prompt the user to select which services should be exposed. For this test, we manually set the
// external bit on the resources on the webfrontend resource to simulate the user selecting the webfrontend to be
// exposed.
for _, value := range m.Resources["nodeapp"].Bindings {
value.External = true
}
for _, name := range []string{"nodeapp"} {
t.Run(name, func(t *testing.T) {
tmpl, err := ContainerAppManifestTemplateForProject(m, name)
require.NoError(t, err)
snapshot.SnapshotT(t, tmpl)
})
}
files, err := BicepTemplate(m)
require.NoError(t, err)
err = fs.WalkDir(files, ".", func(path string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
if d.IsDir() {
return nil
}
contents, err := fs.ReadFile(files, path)
if err != nil {
return err
}
t.Run(path, func(t *testing.T) {
snapshot.SnapshotT(t, string(contents))
})
return nil
})
require.NoError(t, err)
}
func TestAspireContainerGeneration(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("Skipping due to EOL issues on Windows with the baselines")
}
ctx := context.Background()
mockCtx := mocks.NewMockContext(ctx)
mockPublishManifest(mockCtx, aspireContainerManifest, nil)
mockCli := dotnet.NewDotNetCli(mockCtx.CommandRunner)
m, err := ManifestFromAppHost(ctx, filepath.Join("testdata", "AspireDocker.AppHost.csproj"), mockCli, "")
require.NoError(t, err)
files, err := BicepTemplate(m)
require.NoError(t, err)
err = fs.WalkDir(files, ".", func(path string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
if d.IsDir() {
return nil
}
contents, err := fs.ReadFile(files, path)
if err != nil {
return err
}
t.Run(path, func(t *testing.T) {
snapshot.SnapshotT(t, string(contents))
})
return nil
})
require.NoError(t, err)
}
func TestBuildEnvResolveServiceToConnectionString(t *testing.T) {
// Create a mock infraGenerator instance
mockGenerator := &infraGenerator{
resourceTypes: map[string]string{
"service": "postgres.database.v0",
},
}
// Define test input
env := map[string]string{
"VAR1": "value1",
"VAR2": "value2",
"VAR3": `complex {service.connectionString} expression`,
}
expected := map[string]string{
"VAR1": "value1",
"VAR2": "value2",
}
expectedSecrets := map[string]string{
"VAR3": `complex {{ connectionString "service" }} expression`,
}
manifestCtx := &genContainerAppManifestTemplateContext{
Env: make(map[string]string),
Secrets: make(map[string]string),
KeyVaultSecrets: make(map[string]string),
}
// Call the method being tested
err := mockGenerator.buildEnvBlock(env, manifestCtx)
require.NoError(t, err)
require.Equal(t, expected, manifestCtx.Env)
require.Equal(t, expectedSecrets, manifestCtx.Secrets)
}
func TestAddContainerAppService(t *testing.T) {
// Create a mock infraGenerator instance
mockGenerator := &infraGenerator{
bicepContext: genBicepTemplateContext{
StorageAccounts: make(map[string]genStorageAccount),
},
}
// Call the method being tested
mockGenerator.addStorageBlob("storage", "blob")
mockGenerator.addStorageAccount("storage")
mockGenerator.addStorageQueue("storage", "quue")
mockGenerator.addStorageAccount("storage")
mockGenerator.addStorageTable("storage", "table")
mockGenerator.addStorageAccount("storage2")
mockGenerator.addStorageAccount("storage3")
mockGenerator.addStorageTable("storage4", "table")
mockGenerator.addStorageTable("storage2", "table")
mockGenerator.addStorageQueue("storage", "quue2")
require.Equal(t, 1, len(mockGenerator.bicepContext.StorageAccounts["storage"].Blobs))
require.Equal(t, 2, len(mockGenerator.bicepContext.StorageAccounts["storage"].Queues))
require.Equal(t, 1, len(mockGenerator.bicepContext.StorageAccounts["storage"].Tables))
require.Equal(t, 0, len(mockGenerator.bicepContext.StorageAccounts["storage2"].Blobs))
require.Equal(t, 0, len(mockGenerator.bicepContext.StorageAccounts["storage2"].Queues))
require.Equal(t, 1, len(mockGenerator.bicepContext.StorageAccounts["storage2"].Tables))
require.Equal(t, 0, len(mockGenerator.bicepContext.StorageAccounts["storage3"].Blobs))
require.Equal(t, 0, len(mockGenerator.bicepContext.StorageAccounts["storage3"].Queues))
require.Equal(t, 0, len(mockGenerator.bicepContext.StorageAccounts["storage3"].Tables))
require.Equal(t, 0, len(mockGenerator.bicepContext.StorageAccounts["storage4"].Blobs))
require.Equal(t, 0, len(mockGenerator.bicepContext.StorageAccounts["storage4"].Queues))
require.Equal(t, 1, len(mockGenerator.bicepContext.StorageAccounts["storage4"].Tables))
}
func TestEvaluateForOutputs(t *testing.T) {
value := "{resource.outputs.output1} and {resource.secretOutputs.output2}"
expectedOutputs := map[string]genOutputParameter{
"RESOURCE_OUTPUT1": {
Type: "string",
Value: "resource.outputs.output1",
},
"RESOURCE_OUTPUT2": {
Type: "string",
Value: "resource.secretOutputs.output2",
},
}
outputs, err := evaluateForOutputs(value)
require.NoError(t, err)
require.Equal(t, expectedOutputs, outputs)
}
func TestInjectValueForBicepParameter(t *testing.T) {
resourceName := "example"
param := knownParameterKeyVault
expectedParameter := `"exampleParameter"`
value, inject, err := injectValueForBicepParameter(resourceName, param, "exampleParameter")
require.NoError(t, err)
require.Equal(t, expectedParameter, value)
require.False(t, inject)
expectedParameter = "resources.outputs.SERVICE_BINDING_EXAMPLEKV_NAME"
value, inject, err = injectValueForBicepParameter(resourceName, param, "")
require.NoError(t, err)
require.Equal(t, expectedParameter, value)
require.True(t, inject)
param = knownParameterPrincipalId
expectedParameter = `"exampleParameter"`
value, inject, err = injectValueForBicepParameter(resourceName, param, "exampleParameter")
require.NoError(t, err)
require.Equal(t, expectedParameter, value)
require.False(t, inject)
expectedParameter = knownInjectedValuePrincipalId
value, inject, err = injectValueForBicepParameter(resourceName, param, "")
require.NoError(t, err)
require.Equal(t, expectedParameter, value)
require.True(t, inject)
param = knownParameterPrincipalType
expectedParameter = `"exampleParameter"`
value, inject, err = injectValueForBicepParameter(resourceName, param, "exampleParameter")
require.NoError(t, err)
require.Equal(t, expectedParameter, value)
require.False(t, inject)
param = knownParameterPrincipalType
expectedParameter = knownInjectedValuePrincipalType
value, inject, err = injectValueForBicepParameter(resourceName, param, "")
require.NoError(t, err)
require.Equal(t, expectedParameter, value)
require.True(t, inject)
param = knownParameterPrincipalName
expectedParameter = `"exampleParameter"`
value, inject, err = injectValueForBicepParameter(resourceName, param, "exampleParameter")
require.NoError(t, err)
require.Equal(t, expectedParameter, value)
require.False(t, inject)
param = knownParameterPrincipalName
expectedParameter = knownInjectedValuePrincipalName
value, inject, err = injectValueForBicepParameter(resourceName, param, "")
require.NoError(t, err)
require.Equal(t, expectedParameter, value)
require.True(t, inject)
param = knownParameterLogAnalytics
expectedParameter = `"exampleParameter"`
value, inject, err = injectValueForBicepParameter(resourceName, param, "exampleParameter")
require.NoError(t, err)
require.Equal(t, expectedParameter, value)
require.False(t, inject)
param = knownParameterLogAnalytics
expectedParameter = knownInjectedValueLogAnalytics
value, inject, err = injectValueForBicepParameter(resourceName, param, "")
require.NoError(t, err)
require.Equal(t, expectedParameter, value)
require.True(t, inject)
param = "otherParam"
expectedParameter = `"exampleParameter"`
value, inject, err = injectValueForBicepParameter(resourceName, param, "exampleParameter")
require.NoError(t, err)
require.Equal(t, expectedParameter, value)
require.False(t, inject)
expectedParameter = `["exampleParameter"]`
value, inject, err = injectValueForBicepParameter(resourceName, param, []string{"exampleParameter"})
require.NoError(t, err)
require.Equal(t, expectedParameter, value)
require.False(t, inject)
expectedParameter = `true`
value, inject, err = injectValueForBicepParameter(resourceName, param, true)
require.NoError(t, err)
require.Equal(t, expectedParameter, value)
require.False(t, inject)
expectedParameter = `""`
value, inject, err = injectValueForBicepParameter(resourceName, param, "")
require.NoError(t, err)
require.Equal(t, expectedParameter, value)
require.False(t, inject)
}

Просмотреть файл

@ -3,7 +3,13 @@ package apphost
type genAppInsight struct{}
type genStorageAccount struct {
Containers []string
Blobs []string
Tables []string
Queues []string
}
type genCosmosAccount struct {
Databases []string
}
type genServiceBus struct {
@ -15,14 +21,22 @@ type genContainerAppEnvironmentServices struct {
Type string
}
type genKeyVault struct{}
type genKeyVault struct {
// when true, the bicep definition for tags is not generated
NoTags bool
// when provided, the principalId from the user provisioning the key vault gets read access
ReadAccessPrincipalId bool
}
type genContainerApp struct {
Image string
Ingress *genContainerServiceIngress
Dapr *genContainerAppManifestTemplateContextDapr
Env map[string]string
Secrets map[string]string
Ingress *genContainerAppIngress
}
type genContainerServiceIngress struct {
type genContainerAppIngress struct {
External bool
TargetPort int
Transport string
@ -33,6 +47,14 @@ type genContainer struct {
Image string
Env map[string]string
Bindings map[string]*Binding
Inputs map[string]Input
}
type genDockerfile struct {
Path string
Context string
Env map[string]string
Bindings map[string]*Binding
}
type genProject struct {
@ -41,22 +63,83 @@ type genProject struct {
Bindings map[string]*Binding
}
type genAppConfig struct{}
type genDapr struct {
AppId string
Application string
AppPort *int
AppProtocol *string
DaprHttpMaxRequestSize *int
DaprHttpReadBufferSize *int
EnableApiLogging *bool
LogLevel *string
}
type genDaprComponentMetadata struct {
SecretKeyRef *string
Value *string
}
type genDaprComponentSecret struct {
Value string
}
type genDaprComponent struct {
Metadata map[string]genDaprComponentMetadata
Secrets map[string]genDaprComponentSecret
Type string
Version string
}
type genInput struct {
Secret bool
DefaultMinLength int
}
type genSqlServer struct {
Databases []string
}
type genOutputParameter struct {
Type string
Value string
}
type genBicepModules struct {
Path string
Params map[string]string
}
type genBicepTemplateContext struct {
HasContainerRegistry bool
HasContainerEnvironment bool
HasDaprStore bool
HasLogAnalyticsWorkspace bool
RequiresPrincipalId bool
AppInsights map[string]genAppInsight
ServiceBuses map[string]genServiceBus
StorageAccounts map[string]genStorageAccount
KeyVaults map[string]genKeyVault
ContainerAppEnvironmentServices map[string]genContainerAppEnvironmentServices
ContainerApps map[string]genContainerApp
AppConfigs map[string]genAppConfig
DaprComponents map[string]genDaprComponent
CosmosDbAccounts map[string]genCosmosAccount
SqlServers map[string]genSqlServer
InputParameters map[string]Input
OutputParameters map[string]genOutputParameter
OutputSecretParameters map[string]genOutputParameter
BicepModules map[string]genBicepModules
}
type genContainerAppManifestTemplateContext struct {
Name string
Ingress *genContainerAppManifestTemplateContextIngress
Ingress *genContainerAppIngress
Env map[string]string
Secrets map[string]string
KeyVaultSecrets map[string]string
Dapr *genContainerAppManifestTemplateContextDapr
}
type genProjectFileContext struct {
@ -64,9 +147,12 @@ type genProjectFileContext struct {
Services map[string]string
}
type genContainerAppManifestTemplateContextIngress struct {
External bool
Transport string
TargetPort int
AllowInsecure bool
type genContainerAppManifestTemplateContextDapr struct {
AppId string
AppPort *int
AppProtocol *string
EnableApiLogging *bool
HttpMaxRequestSize *int
HttpReadBufferSize *int
LogLevel *string
}

Просмотреть файл

@ -8,21 +8,30 @@ import (
"path/filepath"
"strconv"
"github.com/azure/azure-dev/cli/azd/pkg/osutil"
"github.com/azure/azure-dev/cli/azd/pkg/tools/dotnet"
"github.com/psanford/memfs"
)
type Manifest struct {
Schema string `json:"$schema"`
Resources map[string]*Resource `json:"resources"`
// BicepFiles holds any bicep files generated by Aspire next to the manifest file.
BicepFiles *memfs.FS `json:"-"`
}
type Resource struct {
// Type is present on all resource types
Type string `json:"type"`
// Path is present on a project.v0 resource and is the full path to the project file.
// Path is present on a project.v0 resource and is the path to the project file, and on a dockerfile.v0
// resource and is the path to the Dockerfile (including the "Dockerfile" filename).
// For a bicep.v0 resource, it is the path to the bicep file.
Path *string `json:"path,omitempty"`
// Context is present on a dockerfile.v0 resource and is the path to the context directory.
Context *string `json:"context,omitempty"`
// Parent is present on a resource which is a child of another. It is the name of the parent resource. For example, a
// postgres.database.v0 is a child of a postgres.server.v0, and so it would have a parent of which is the name of
// the server resource.
@ -31,13 +40,14 @@ type Resource struct {
// Image is present on a container.v0 resource and is the image to use for the container.
Image *string `json:"image,omitempty"`
// Bindings is present on container.v0 and project.v0 resources, and is a map of binding names to binding details.
// Bindings is present on container.v0, project.v0 and dockerfile.v0 resources, and is a map of binding names to
// binding details.
Bindings map[string]*Binding `json:"bindings,omitempty"`
// Env is present on a project.v0 and container.v0 resource, and is a map of environment variable names to value
// expressions. The value expressions are simple expressions like "{redis.connectionString}" or "{postgres.port}" to
// allow referencing properties of other resources. The set of properties supported in these expressions
// depends on the type of resource you are referencing.
// Env is present on project.v0, container.v0 and dockerfile.v0 resources, and is a map of environment variable
// names to value expressions. The value expressions are simple expressions like "{redis.connectionString}" or
// "{postgres.port}" to allow referencing properties of other resources. The set of properties supported in these
// expressions depends on the type of resource you are referencing.
Env map[string]string `json:"env,omitempty"`
// Queues is optionally present on a azure.servicebus.v0 resource, and is a list of queue names to create.
@ -49,6 +59,37 @@ type Resource struct {
// Some resources just represent connections to existing resources that need not be provisioned. These resources have
// a "connectionString" property which is the connection string that should be used during binding.
ConnectionString *string `json:"connectionString,omitempty"`
// Dapr is present on dapr.v0 resources.
Dapr *DaprResourceMetadata `json:"dapr,omitempty"`
// DaprComponent is present on dapr.component.v0 resources.
DaprComponent *DaprComponentResourceMetadata `json:"daprComponent,omitempty"`
// Inputs is present on resources that need inputs from during the provisioning process (e.g asking for an API key, or
// a password for a database).
Inputs map[string]Input `json:"inputs,omitempty"`
// For a bicep.v0 resource, defines the input parameters for the bicep file.
Params map[string]any `json:"params,omitempty"`
// parameter.v0 uses value field to define the value of the parameter.
Value string
}
type DaprResourceMetadata struct {
AppId *string `json:"appId,omitempty"`
Application *string `json:"application,omitempty"`
AppPort *int `json:"appPort,omitempty"`
AppProtocol *string `json:"appProtocol,omitempty"`
DaprHttpMaxRequestSize *int `json:"daprHttpMaxRequestSize,omitempty"`
DaprHttpReadBufferSize *int `json:"daprHttpReadBufferSize,omitempty"`
EnableApiLogging *bool `json:"enableApiLogging,omitempty"`
LogLevel *string `json:"logLevel,omitempty"`
}
type DaprComponentResourceMetadata struct {
Type *string `json:"type"`
}
type Reference struct {
@ -63,8 +104,24 @@ type Binding struct {
External bool `json:"external"`
}
type Input struct {
Type string `json:"type"`
Secret bool `json:"secret"`
Default *InputDefault `json:"default,omitempty"`
}
type InputDefault struct {
Generate *InputDefaultGenerate `json:"generate,omitempty"`
}
type InputDefaultGenerate struct {
MinLength *int `json:"minLength,omitempty"`
}
// ManifestFromAppHost returns the Manifest from the given app host.
func ManifestFromAppHost(ctx context.Context, appHostProject string, dotnetCli dotnet.DotNetCli) (*Manifest, error) {
func ManifestFromAppHost(
ctx context.Context, appHostProject string, dotnetCli dotnet.DotNetCli, dotnetEnv string,
) (*Manifest, error) {
tempDir, err := os.MkdirTemp("", "azd-provision")
if err != nil {
return nil, fmt.Errorf("creating temp directory for apphost-manifest.json: %w", err)
@ -73,7 +130,7 @@ func ManifestFromAppHost(ctx context.Context, appHostProject string, dotnetCli d
manifestPath := filepath.Join(tempDir, "apphost-manifest.json")
if err := dotnetCli.PublishAppHostManifest(ctx, appHostProject, manifestPath); err != nil {
if err := dotnetCli.PublishAppHostManifest(ctx, appHostProject, manifestPath, dotnetEnv); err != nil {
return nil, fmt.Errorf("generating app host manifest: %w", err)
}
@ -88,6 +145,8 @@ func ManifestFromAppHost(ctx context.Context, appHostProject string, dotnetCli d
}
// Make all paths absolute, to simplify logic for consumers.
// Note that since we created a temp dir, and `dotnet run --publisher` returns relative paths to the temp dir,
// the resulting path may be a symlinked path that isn't safe for Rel comparisons with the azd root directory.
manifestDir := filepath.Dir(manifestPath)
// The manifest writer writes paths relative to the manifest file. When we use a fixed manifest, the manifest is
@ -96,12 +155,43 @@ func ManifestFromAppHost(ctx context.Context, appHostProject string, dotnetCli d
manifestDir = filepath.Dir(appHostProject)
}
for _, res := range manifest.Resources {
manifest.BicepFiles = memfs.New()
for resourceName, res := range manifest.Resources {
if res.Path != nil {
if res.Type == "azure.bicep.v0" {
e := manifest.BicepFiles.MkdirAll(resourceName, osutil.PermissionDirectory)
if e != nil {
return nil, e
}
// try reading as a generated bicep adding the tem-manifest dir
content, e := os.ReadFile(filepath.Join(manifestDir, *res.Path))
if e != nil {
// second try reading as relative (external bicep reference)
content, e = os.ReadFile(*res.Path)
if e != nil {
return nil, fmt.Errorf("did not find bicep at generated path or at: %s. Error: %w", *res.Path, e)
}
}
*res.Path = filepath.Join(resourceName, filepath.Base(*res.Path))
e = manifest.BicepFiles.WriteFile(*res.Path, content, osutil.PermissionFile)
if e != nil {
return nil, e
}
// move on to the next resource
continue
}
if !filepath.IsAbs(*res.Path) {
*res.Path = filepath.Join(manifestDir, *res.Path)
}
}
if res.Type == "dockerfile.v0" {
if !filepath.IsAbs(*res.Context) {
*res.Context = filepath.Join(manifestDir, *res.Context)
}
}
}
return &manifest, nil

Некоторые файлы не были показаны из-за слишком большого количества измененных файлов Показать больше