From 99d6c0ddcafdec16035dc2c629a64a0d49547f86 Mon Sep 17 00:00:00 2001 From: Wei Lim Date: Fri, 25 Jul 2025 11:56:34 -0700 Subject: [PATCH 1/2] vsrpc: support non-aspire projects --- .../vsrpc/environment_service_create.go | 81 +++++++++++++------ .../vsrpc/environment_service_load.go | 15 ++-- cli/azd/internal/vsrpc/utils.go | 34 +++++--- 3 files changed, 92 insertions(+), 38 deletions(-) diff --git a/cli/azd/internal/vsrpc/environment_service_create.go b/cli/azd/internal/vsrpc/environment_service_create.go index 7cb31e7171b..c09cc72361f 100644 --- a/cli/azd/internal/vsrpc/environment_service_create.go +++ b/cli/azd/internal/vsrpc/environment_service_create.go @@ -12,10 +12,12 @@ import ( "path/filepath" "strings" + "github.com/azure/azure-dev/cli/azd/internal/names" "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/osutil" + "github.com/azure/azure-dev/cli/azd/pkg/project" "github.com/azure/azure-dev/cli/azd/pkg/tools/dotnet" ) @@ -64,32 +66,65 @@ func (s *environmentService) CreateEnvironmentAsync( // 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) { - _ = observer.OnNext(ctx, newImportantProgressMessage("Analyzing Aspire Application (this might take a moment...)")) - - manifest, err := apphost.ManifestFromAppHost(ctx, rc.HostProjectPath, c.dotnetCli, dotnetEnv) - if err != nil { - return false, fmt.Errorf("reading app host manifest: %w", err) - } - - projectName := azdcontext.ProjectName(strings.TrimSuffix(c.azdContext.ProjectDirectory(), ".AppHost")) - - // Write an azure.yaml file to the project. - files, err := apphost.GenerateProjectArtifacts( - ctx, - c.azdContext.ProjectDirectory(), - projectName, - manifest, - rc.HostProjectPath, - ) + isAspire, err := c.dotnetCli.IsAspireHostProject(ctx, rc.HostProjectPath) if err != nil { - return false, fmt.Errorf("generating project artifacts: %w", err) + return false, fmt.Errorf("checking if %s is an app host project: %w", rc.HostProjectPath, err) } - file := files["azure.yaml"] - projectFilePath := filepath.Join(c.azdContext.ProjectDirectory(), "azure.yaml") - - if err := os.WriteFile(projectFilePath, []byte(file.Contents), osutil.PermissionFile); err != nil { - return false, fmt.Errorf("writing azure.yaml: %w", err) + if isAspire { + _ = observer.OnNext(ctx, + newImportantProgressMessage("Analyzing Aspire Application (this might take a moment...)")) + + manifest, err := apphost.ManifestFromAppHost(ctx, rc.HostProjectPath, c.dotnetCli, dotnetEnv) + if err != nil { + return false, fmt.Errorf("reading app host manifest: %w", err) + } + + projectName := azdcontext.ProjectName(strings.TrimSuffix(c.azdContext.ProjectDirectory(), ".AppHost")) + + // Write an azure.yaml file to the project. + files, err := apphost.GenerateProjectArtifacts( + ctx, + c.azdContext.ProjectDirectory(), + projectName, + manifest, + rc.HostProjectPath, + ) + 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), osutil.PermissionFile); err != nil { + return false, fmt.Errorf("writing azure.yaml: %w", err) + } + } else { + rel, err := filepath.Rel(c.azdContext.ProjectDirectory(), rc.HostProjectPath) + if err != nil { + return false, fmt.Errorf("determining relative path: %w", err) + } + + projectName := azdcontext.ProjectName(c.azdContext.ProjectDirectory()) + + ext := filepath.Ext(rc.HostProjectPath) + serviceName := names.LabelName(strings.TrimSuffix(filepath.Base(rc.HostProjectPath), ext)) + + prjConfig := project.ProjectConfig{ + Name: projectName, + Services: map[string]*project.ServiceConfig{ + serviceName: { + Name: serviceName, + RelativePath: fmt.Sprintf("./%s", filepath.ToSlash(rel)), + }, + }, + } + + err = project.Save(ctx, &prjConfig, c.azdContext.ProjectPath()) + if err != nil { + return false, fmt.Errorf("saving project config: %w", err) + } } } else if err != nil { return false, fmt.Errorf("checking for project: %w", err) diff --git a/cli/azd/internal/vsrpc/environment_service_load.go b/cli/azd/internal/vsrpc/environment_service_load.go index 80a6cdee71f..3f657498790 100644 --- a/cli/azd/internal/vsrpc/environment_service_load.go +++ b/cli/azd/internal/vsrpc/environment_service_load.go @@ -120,14 +120,17 @@ func (s *environmentService) loadEnvironmentAsync( appHost, err := appHostForProject(ctx, c.projectConfig, c.dotnetCli) if err != nil { return nil, fmt.Errorf("failed to find Aspire app host: %w", err) - } + } else if appHost != nil { + manifest, err := c.dotnetImporter.ReadManifest(ctx, appHost) + if err != nil { + return nil, fmt.Errorf("reading app host manifest: %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) - ret.Services = servicesFromManifest(manifest) + return ret, nil + } + ret.Services = servicesFromProjectConfig(ctx, c.projectConfig) return ret, nil } diff --git a/cli/azd/internal/vsrpc/utils.go b/cli/azd/internal/vsrpc/utils.go index 18593ee8cd1..59a77c86992 100644 --- a/cli/azd/internal/vsrpc/utils.go +++ b/cli/azd/internal/vsrpc/utils.go @@ -17,6 +17,8 @@ import ( ) // appHostServiceForProject returns the ServiceConfig of the service for the AppHost project for the given azd project. +// +// If the project does not have an AppHost project, nil is returned. func appHostForProject( ctx context.Context, pc *project.ProjectConfig, dotnetCli *dotnet.Cli, ) (*project.ServiceConfig, error) { @@ -24,15 +26,16 @@ func appHostForProject( if service.Language == project.ServiceLanguageDotNet { 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) + return nil, fmt.Errorf("error checking if %s is an app host project: %w", service.Path(), err) } + if isAppHost { return service, nil } } } - return nil, fmt.Errorf("no app host project found for project: %s", pc.Name) + return nil, nil } func servicesFromManifest(manifest *apphost.Manifest) []*Service { @@ -48,6 +51,19 @@ func servicesFromManifest(manifest *apphost.Manifest) []*Service { return services } +func servicesFromProjectConfig(ctx context.Context, pc *project.ProjectConfig) []*Service { + var services []*Service + + for _, service := range pc.Services { + services = append(services, &Service{ + Name: service.Name, + Path: service.Path(), + }) + } + + return services +} + // azdContext resolves the azd context directory to use. // // - If the host project directory contains azure.yaml, the host project directory is used. @@ -75,16 +91,16 @@ func azdContext(hostProjectPath string) (*azdcontext.AzdContext, error) { return nil, err } + found := false for _, svc := range prjConfig.Services { - if svc.Language == project.ServiceLanguageDotNet && svc.Host == project.ContainerAppTarget { - if svc.Path() != hostProjectPath { - log.Printf("ignoring %s due to mismatch, using app host directory", azdCtx.ProjectPath()) - return azdcontext.NewAzdContextWithDirectory(hostProjectDir), nil - } + if svc.Path() == hostProjectPath { + found = true } + } - // there can only be one app host project - break + if !found { + log.Printf("ignoring %s due to non-matching project found, using app host directory", azdCtx.ProjectPath()) + return azdcontext.NewAzdContextWithDirectory(hostProjectDir), nil } log.Printf("use nearest directory: %s", azdCtx.ProjectDirectory()) From b5824b056b9580b80953a6bea1035ef15686b753 Mon Sep 17 00:00:00 2001 From: Wei Lim Date: Mon, 27 Oct 2025 16:34:02 -0700 Subject: [PATCH 2/2] update comments --- cli/azd/internal/vsrpc/environment_service_load.go | 9 +++++---- cli/azd/internal/vsrpc/models.go | 2 +- cli/azd/internal/vsrpc/utils.go | 2 +- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/cli/azd/internal/vsrpc/environment_service_load.go b/cli/azd/internal/vsrpc/environment_service_load.go index 3f657498790..40ac9846b32 100644 --- a/cli/azd/internal/vsrpc/environment_service_load.go +++ b/cli/azd/internal/vsrpc/environment_service_load.go @@ -120,17 +120,18 @@ func (s *environmentService) loadEnvironmentAsync( appHost, err := appHostForProject(ctx, c.projectConfig, c.dotnetCli) if err != nil { return nil, fmt.Errorf("failed to find Aspire app host: %w", err) - } else if appHost != nil { + } + + if appHost != nil { manifest, err := c.dotnetImporter.ReadManifest(ctx, appHost) if err != nil { return nil, fmt.Errorf("reading app host manifest: %w", err) } ret.Services = servicesFromManifest(manifest) - - return ret, nil + } else { + ret.Services = servicesFromProjectConfig(ctx, c.projectConfig) } - ret.Services = servicesFromProjectConfig(ctx, c.projectConfig) return ret, nil } diff --git a/cli/azd/internal/vsrpc/models.go b/cli/azd/internal/vsrpc/models.go index e1a7eeaa772..ed9463766b4 100644 --- a/cli/azd/internal/vsrpc/models.go +++ b/cli/azd/internal/vsrpc/models.go @@ -119,7 +119,7 @@ type RequestContext struct { // The active session. Session Session - // The app host project path. + // The host project path being operated on. HostProjectPath string } diff --git a/cli/azd/internal/vsrpc/utils.go b/cli/azd/internal/vsrpc/utils.go index 59a77c86992..d4186007a30 100644 --- a/cli/azd/internal/vsrpc/utils.go +++ b/cli/azd/internal/vsrpc/utils.go @@ -99,7 +99,7 @@ func azdContext(hostProjectPath string) (*azdcontext.AzdContext, error) { } if !found { - log.Printf("ignoring %s due to non-matching project found, using app host directory", azdCtx.ProjectPath()) + log.Printf("ignoring %s due to mismatch, using host project directory", azdCtx.ProjectPath()) return azdcontext.NewAzdContextWithDirectory(hostProjectDir), nil }