aspire: Consider Aspire Capability when detecting AppHost (#4447)

In addition to the `IsAspireHost` property we will now look at the
project capabilities to see if an `Aspire` capability is listed and if
treat the project as an AppHost project. This aligns `azd`'s behavior
with other tooling like Visual Studio which uses the project
capabilities to determine if the project is an App Host or not.

The .NET Team asked us to include this in our sniffing logic (but to
continue to check `IsAspireHost` as well).

Fixes #4364
This commit is contained in:
Matt Ellis 2024-11-06 14:26:08 -08:00 коммит произвёл GitHub
Родитель 209fd014d3
Коммит dd46ca9b18
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: B5690EEEBB952194
5 изменённых файлов: 94 добавлений и 22 удалений

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

@ -5,7 +5,6 @@ import (
"io/fs"
"log"
"path/filepath"
"strings"
"github.com/azure/azure-dev/cli/azd/pkg/tools/dotnet"
)
@ -26,7 +25,7 @@ func (ad *dotNetAppHostDetector) DetectProject(ctx context.Context, path string,
switch ext {
case ".csproj", ".fsproj", ".vbproj":
projectPath := filepath.Join(path, name)
if isAppHost, err := ad.isAppHostProject(ctx, filepath.Join(projectPath)); err != nil {
if isAppHost, err := ad.dotnetCli.IsAspireHostProject(ctx, filepath.Join(projectPath)); err != nil {
log.Printf("error checking if %s is an app host project: %v", projectPath, err)
} else if isAppHost {
return &Project{
@ -40,14 +39,3 @@ func (ad *dotNetAppHostDetector) DetectProject(ctx context.Context, path string,
return nil, nil
}
// isAppHostProject returns true if the project at the given path has an MS Build Property named "IsAspireHost" which is
// set to "true".
func (ad *dotNetAppHostDetector) isAppHostProject(ctx context.Context, projectPath string) (bool, error) {
value, err := ad.dotnetCli.GetMsBuildProperty(ctx, projectPath, "IsAspireHost")
if err != nil {
return false, err
}
return strings.TrimSpace(value) == "true", nil
}

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

@ -6,7 +6,6 @@ import (
"fmt"
"log"
"path/filepath"
"strings"
"github.com/azure/azure-dev/cli/azd/pkg/apphost"
"github.com/azure/azure-dev/cli/azd/pkg/environment/azdcontext"
@ -20,11 +19,11 @@ func appHostForProject(
) (*project.ServiceConfig, error) {
for _, service := range pc.Services {
if service.Language == project.ServiceLanguageDotNet {
isAppHost, err := dotnetCli.GetMsBuildProperty(ctx, service.Path(), "IsAspireHost")
isAppHost, err := dotnetCli.IsAspireHostProject(ctx, service.Path())
if err != nil {
log.Printf("error checking if %s is an app host project: %v", service.Path(), err)
}
if strings.TrimSpace(isAppHost) == "true" {
if isAppHost {
return service, nil
}
}

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

@ -83,7 +83,7 @@ func (ai *DotNetImporter) CanImport(ctx context.Context, projectPath string) (bo
return v.is, v.err
}
value, err := ai.dotnetCli.GetMsBuildProperty(ctx, projectPath, "IsAspireHost")
isAppHost, err := ai.dotnetCli.IsAspireHostProject(ctx, projectPath)
if err != nil {
ai.hostCheck[projectPath] = hostCheckResult{
is: false,
@ -94,11 +94,11 @@ func (ai *DotNetImporter) CanImport(ctx context.Context, projectPath string) (bo
}
ai.hostCheck[projectPath] = hostCheckResult{
is: strings.TrimSpace(value) == "true",
is: isAppHost,
err: nil,
}
return strings.TrimSpace(value) == "true", nil
return isAppHost, nil
}
func (ai *DotNetImporter) ProjectInfrastructure(ctx context.Context, svcConfig *ServiceConfig) (*Infra, error) {

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

@ -92,7 +92,7 @@ func TestImportManagerHasServiceErrorNoMultipleServicesWithAppHost(t *testing.T)
slices.Contains(args.Args, "--getProperty:IsAspireHost")
}).RespondFn(func(args exec.RunArgs) (exec.RunResult, error) {
return exec.RunResult{
Stdout: "true",
Stdout: aspireAppHostSniffResult,
ExitCode: 0,
}, nil
})
@ -145,7 +145,7 @@ func TestImportManagerHasServiceErrorAppHostMustTargetContainerApp(t *testing.T)
slices.Contains(args.Args, "--getProperty:IsAspireHost")
}).RespondFn(func(args exec.RunArgs) (exec.RunResult, error) {
return exec.RunResult{
Stdout: "true",
Stdout: aspireAppHostSniffResult,
ExitCode: 0,
}, nil
})
@ -278,7 +278,7 @@ func TestImportManagerProjectInfrastructureAspire(t *testing.T) {
slices.Contains(args.Args, "--getProperty:IsAspireHost")
}).RespondFn(func(args exec.RunArgs) (exec.RunResult, error) {
return exec.RunResult{
Stdout: "true",
Stdout: aspireAppHostSniffResult,
ExitCode: 0,
}, nil
})
@ -462,3 +462,52 @@ func TestImportManager_SynthAllInfrastructure_FromResources(t *testing.T) {
_, err = im.SynthAllInfrastructure(context.Background(), prjConfig)
assert.Error(t, err)
}
// aspireAppHostSniffResult is mock data that would be returned by `dotnet msbuild` when fetching information about an
// Aspire project. This is used to simulate the scenario where a project is an Aspire project. A real Aspire project would
// have many entries in the ProjectCapability array (unrelated to the Aspire capability), but most have been omitted for
// simplicity. An unrelated entry is included to ensure we are looking at the entire array of capabilities.
// nolint: lll
var aspireAppHostSniffResult string = `{
"Properties": {
"IsAspireHost": "true"
},
"Items": {
"ProjectCapability": [
{
"Identity": "LocalUserSecrets",
"FullPath": "/Users/matell/dd/ellismg/AspireBicep/AspireStarter/AspireStarter.AppHost/LocalUserSecrets",
"RootDir": "/",
"Filename": "LocalUserSecrets",
"Extension": "",
"RelativeDir": "",
"Directory": "Users/matell/dd/ellismg/AspireBicep/AspireStarter/AspireStarter.AppHost/",
"RecursiveDir": "",
"ModifiedTime": "",
"CreatedTime": "",
"AccessedTime": "",
"DefiningProjectFullPath": "/Users/matell/.nuget/packages/microsoft.extensions.configuration.usersecrets/8.0.0/buildTransitive/net6.0/Microsoft.Extensions.Configuration.UserSecrets.props",
"DefiningProjectDirectory": "/Users/matell/.nuget/packages/microsoft.extensions.configuration.usersecrets/8.0.0/buildTransitive/net6.0/",
"DefiningProjectName": "Microsoft.Extensions.Configuration.UserSecrets",
"DefiningProjectExtension": ".props"
},
{
"Identity": "Aspire",
"FullPath": "/Users/matell/dd/ellismg/AspireBicep/AspireStarter/AspireStarter.AppHost/Aspire",
"RootDir": "/",
"Filename": "Aspire",
"Extension": "",
"RelativeDir": "",
"Directory": "Users/matell/dd/ellismg/AspireBicep/AspireStarter/AspireStarter.AppHost/",
"RecursiveDir": "",
"ModifiedTime": "",
"CreatedTime": "",
"AccessedTime": "",
"DefiningProjectFullPath": "/Users/matell/.nuget/packages/aspire.hosting.apphost/8.2.0/build/Aspire.Hosting.AppHost.targets",
"DefiningProjectDirectory": "/Users/matell/.nuget/packages/aspire.hosting.apphost/8.2.0/build/",
"DefiningProjectName": "Aspire.Hosting.AppHost",
"DefiningProjectExtension": ".targets"
}
]
}
}`

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

@ -303,6 +303,42 @@ func (cli *Cli) GetMsBuildProperty(ctx context.Context, project string, property
return res.Stdout, nil
}
// IsAspireHostProject returns true if the project at the given path has an MS Build Property named "IsAspireHost" which is
// set to true or has a ProjectCapability named "Aspire".
func (cli *Cli) IsAspireHostProject(ctx context.Context, projectPath string) (bool, error) {
runArgs := newDotNetRunArgs("msbuild", projectPath, "--getProperty:IsAspireHost", "--getItem:ProjectCapability")
res, err := cli.commandRunner.Run(ctx, runArgs)
if err != nil {
return false, fmt.Errorf("running dotnet msbuild on project '%s': %w", projectPath, err)
}
var result struct {
Properties struct {
IsAspireHost string `json:"IsAspireHost"`
} `json:"Properties"`
Items struct {
ProjectCapability []struct {
Identity string `json:"Identity"`
} `json:"ProjectCapability"`
} `json:"Items"`
}
if err := json.Unmarshal([]byte(res.Stdout), &result); err != nil {
return false, fmt.Errorf("unmarshal dotnet msbuild output: %w", err)
}
hasAspireCapability := false
for _, capability := range result.Items.ProjectCapability {
if capability.Identity == "Aspire" {
hasAspireCapability = true
break
}
}
return result.Properties.IsAspireHost == "true" || hasAspireCapability, nil
}
func NewCli(commandRunner exec.CommandRunner) *Cli {
return &Cli{
commandRunner: commandRunner,