Skip to content

Commit

Permalink
aspire: Consider Aspire Capability when detecting AppHost (Azure#4447)
Browse files Browse the repository at this point in the history
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 Azure#4364
  • Loading branch information
ellismg authored Nov 6, 2024
1 parent 209fd01 commit dd46ca9
Show file tree
Hide file tree
Showing 5 changed files with 94 additions and 22 deletions.
14 changes: 1 addition & 13 deletions cli/azd/internal/appdetect/dotnet_apphost.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import (
"io/fs"
"log"
"path/filepath"
"strings"

"github.com/azure/azure-dev/cli/azd/pkg/tools/dotnet"
)
Expand All @@ -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{
Expand All @@ -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
}
5 changes: 2 additions & 3 deletions cli/azd/internal/vsrpc/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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
}
}
Expand Down
6 changes: 3 additions & 3 deletions cli/azd/pkg/project/dotnet_importer.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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) {
Expand Down
55 changes: 52 additions & 3 deletions cli/azd/pkg/project/importer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
})
Expand Down Expand Up @@ -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
})
Expand Down Expand Up @@ -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
})
Expand Down Expand Up @@ -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"
}
]
}
}`
36 changes: 36 additions & 0 deletions cli/azd/pkg/tools/dotnet/dotnet.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down

0 comments on commit dd46ca9

Please sign in to comment.