зеркало из https://github.com/Azure/azure-dev.git
Merge branch 'main' of github.com:Azure/azure-dev into azd-pipeline-config
This commit is contained in:
Коммит
71233b738a
|
@ -29,7 +29,8 @@
|
|||
"golang.go",
|
||||
"ms-azuretools.vscode-bicep",
|
||||
"eamodio.gitlens",
|
||||
"hashicorp.terraform"
|
||||
"hashicorp.terraform",
|
||||
"jinliming2.vscode-go-template"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -27,3 +27,4 @@ stretchr
|
|||
theckman
|
||||
bmatcuk
|
||||
tonybaloney
|
||||
weilim
|
||||
|
|
|
@ -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 \
|
||||
|
|
|
@ -5,3 +5,5 @@ azd-record.exe
|
|||
build
|
||||
resource.syso
|
||||
versioninfo.json
|
||||
azd.sln
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
"recommendations": [
|
||||
"golang.go",
|
||||
"streetsidesoftware.code-spell-checker",
|
||||
"redhat.vscode-yaml"
|
||||
"redhat.vscode-yaml",
|
||||
"jinliming2.vscode-go-template"
|
||||
]
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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}
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -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 = "";
|
||||
}
|
||||
*/
|
15
cli/azd/internal/vsrpc/testdata/dotnet-azd-client/dotnet-azd-client.csproj
поставляемый
Normal file
15
cli/azd/internal/vsrpc/testdata/dotnet-azd-client/dotnet-azd-client.csproj
поставляемый
Normal file
|
@ -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
|
||||
|
|
Некоторые файлы не были показаны из-за слишком большого количества измененных файлов Показать больше
Загрузка…
Ссылка в новой задаче