-
Notifications
You must be signed in to change notification settings - Fork 338
fix: apply --deploy-mode flags to adopted azure.yaml (#8923) #8926
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
a4093ea
511d165
2016077
1c8c9f1
9e6b665
68ee902
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -846,6 +846,14 @@ func runInitFromAzureYaml( | |
| } | ||
| } | ||
|
|
||
| // Apply deploy-mode configuration to the adopted agent service (#8923). | ||
| // When the user passes --deploy-mode (and optionally --runtime / | ||
| // --entry-point), or when the service doesn't already specify its deploy | ||
| // mode, resolve code vs container configuration and update the service. | ||
| if err := applyDeployModeToAdoptedProject(ctx, flags, azdClient); err != nil { | ||
| return err | ||
| } | ||
|
|
||
| fmt.Printf( | ||
| "\nAdopted the sample's azure.yaml as the project manifest at %s.\n", | ||
| output.WithHighLightFormat("azure.yaml"), | ||
|
|
@@ -1162,3 +1170,269 @@ func printAdoptionNextSteps(ctx context.Context, azdClient *azdext.AzdClient, fo | |
| state, _ := nextstep.AssembleState(ctx, azdClient, stateOpts...) | ||
| _ = printAllNextIfTerminal(os.Stdout, nextstep.ResolveAfterInit(state, readmeExistsForProject(ctx, azdClient))) | ||
| } | ||
|
|
||
| // applyDeployModeToAdoptedProject locates the azure.ai.agent service in the | ||
| // adopted project and applies deploy-mode configuration (code or container) | ||
| // based on the --deploy-mode, --runtime, and --entry-point flags. When no | ||
| // explicit flag is passed and the service already has a codeConfiguration or | ||
| // docker property, the service is left unchanged (the sample is pre-configured). | ||
| func applyDeployModeToAdoptedProject( | ||
| ctx context.Context, | ||
| flags *initFlags, | ||
| azdClient *azdext.AzdClient, | ||
| ) error { | ||
| // Validate --image flag early (incompatible with --deploy-mode code). | ||
| if err := validateImageFlag(flags.image, flags.deployMode); err != nil { | ||
| return err | ||
| } | ||
|
|
||
| resp, err := azdClient.Project().Get(ctx, &azdext.EmptyRequest{}) | ||
| if err != nil { | ||
| return fmt.Errorf("reading adopted project: %w", err) | ||
| } | ||
|
|
||
| // Collect all agent services in the adopted project. | ||
| type agentEntry struct { | ||
| name string | ||
| svc *azdext.ServiceConfig | ||
| } | ||
| var agentServices []agentEntry | ||
| for name, svc := range resp.GetProject().GetServices() { | ||
| if svc.GetHost() == AiAgentHost { | ||
| agentServices = append(agentServices, agentEntry{name: name, svc: svc}) | ||
| } | ||
| } | ||
|
trangevi marked this conversation as resolved.
trangevi marked this conversation as resolved.
|
||
| if len(agentServices) == 0 { | ||
| // No agent service found -- nothing to configure. | ||
| return nil | ||
| } | ||
|
|
||
| // Apply configuration to each agent service. | ||
| for _, agent := range agentServices { | ||
| if err := applyDeployModeToService(ctx, flags, azdClient, agent.name, agent.svc); err != nil { | ||
| return err | ||
| } | ||
| } | ||
| return nil | ||
| } | ||
|
|
||
| // applyDeployModeToService applies deploy-mode configuration to a single agent service. | ||
| func applyDeployModeToService( | ||
| ctx context.Context, | ||
| flags *initFlags, | ||
| azdClient *azdext.AzdClient, | ||
| serviceName string, | ||
| svc *azdext.ServiceConfig, | ||
| ) error { | ||
| // Apply --image override to the agent service when provided. | ||
| if flags.image != "" { | ||
| imageValue, err := structpb.NewValue(flags.image) | ||
| if err != nil { | ||
| return fmt.Errorf("encoding image value: %w", err) | ||
| } | ||
| if _, err := azdClient.Project().SetServiceConfigValue(ctx, &azdext.SetServiceConfigValueRequest{ | ||
| ServiceName: serviceName, | ||
| Path: "image", | ||
| Value: imageValue, | ||
| }); err != nil { | ||
| return fmt.Errorf("writing image to agent service %q: %w", serviceName, err) | ||
| } | ||
| log.Printf("Applied --image %q to agent service %q", flags.image, serviceName) | ||
|
|
||
| // --image implies container deploy; apply container config and return. | ||
| return applyContainerDeployToService(ctx, azdClient, serviceName, svc) | ||
| } | ||
|
|
||
| // Check whether the service already specifies its deploy mode. | ||
| hasCodeConfig := adoptedServiceHasCodeConfig(svc) | ||
| hasDocker := adoptedServiceHasDocker(svc) | ||
|
|
||
| // When no explicit --deploy-mode flag is passed and the service is | ||
| // already configured, respect the sample's existing configuration. | ||
| if flags.deployMode == "" && (hasCodeConfig || hasDocker) { | ||
| return nil | ||
| } | ||
|
|
||
| // Use the service's subdirectory for language detection (not project root). | ||
| targetDir := svc.GetRelativePath() | ||
| if targetDir == "" { | ||
| targetDir = "." | ||
| } | ||
| showCodeDeploy := isPythonProject(targetDir) || isDotnetProject(targetDir) | ||
|
trangevi marked this conversation as resolved.
|
||
| // userProvidedManifest is true: -m was explicitly provided. | ||
| deployMode, err := promptDeployMode(ctx, azdClient, flags.noPrompt, showCodeDeploy, flags.deployMode, true) | ||
| if err != nil { | ||
| return fmt.Errorf("resolving deploy mode for adopted project: %w", err) | ||
| } | ||
|
|
||
| if deployMode == "code" { | ||
| return applyCodeDeployToService(ctx, flags, azdClient, serviceName, targetDir, svc) | ||
| } | ||
| return applyContainerDeployToService(ctx, azdClient, serviceName, svc) | ||
| } | ||
|
|
||
| // adoptedServiceHasCodeConfig checks whether the adopted agent service already | ||
| // declares a codeConfiguration in its properties. | ||
| func adoptedServiceHasCodeConfig(svc *azdext.ServiceConfig) bool { | ||
| props := svc.GetAdditionalProperties() | ||
| if props == nil { | ||
| return false | ||
| } | ||
| fields := props.GetFields() | ||
| if fields == nil { | ||
| return false | ||
| } | ||
| v, ok := fields["codeConfiguration"] | ||
| if !ok { | ||
| return false | ||
| } | ||
| // A null value doesn't count as having a codeConfiguration. | ||
| return v != nil && v.GetStructValue() != nil | ||
| } | ||
|
|
||
| // adoptedServiceHasDocker checks whether the adopted agent service already | ||
| // declares a docker configuration in its properties. We check | ||
| // additionalProperties rather than svc.GetDocker() because the gRPC mapper | ||
| // always returns a non-nil Docker pointer (even for the zero-value struct). | ||
| func adoptedServiceHasDocker(svc *azdext.ServiceConfig) bool { | ||
| props := svc.GetAdditionalProperties() | ||
| if props == nil { | ||
| return false | ||
| } | ||
| fields := props.GetFields() | ||
| if fields == nil { | ||
| return false | ||
| } | ||
| v, ok := fields["docker"] | ||
| if !ok { | ||
| return false | ||
| } | ||
| // A null value doesn't count as having docker configured. | ||
| return v != nil && v.GetStructValue() != nil | ||
| } | ||
|
|
||
| // applyCodeDeployToService writes codeConfiguration onto the adopted agent | ||
| // service and updates the service language from "docker" to the appropriate | ||
| // language for the selected runtime. | ||
| func applyCodeDeployToService( | ||
| ctx context.Context, | ||
| flags *initFlags, | ||
| azdClient *azdext.AzdClient, | ||
| serviceName string, | ||
| targetDir string, | ||
| svc *azdext.ServiceConfig, | ||
| ) error { | ||
| codeConfig, err := promptCodeConfig(ctx, azdClient, targetDir, flags.noPrompt, codeDeployOptions{ | ||
| runtime: flags.runtime, | ||
| entryPoint: flags.entryPoint, | ||
| depResolution: flags.depResolution, | ||
| }, true) // userProvidedManifest=true since -m was provided | ||
| if err != nil { | ||
| return fmt.Errorf("resolving code configuration for adopted project: %w", err) | ||
| } | ||
|
|
||
| // Write codeConfiguration onto the service (camelCase keys match the | ||
| // azure.yaml inline format read by the deploy path via JSON unmarshal). | ||
| codeConfigMap := map[string]any{ | ||
| "runtime": codeConfig.Runtime, | ||
| "entryPoint": codeConfig.EntryPoint, | ||
| } | ||
| if codeConfig.DependencyResolution != nil { | ||
| codeConfigMap["dependencyResolution"] = *codeConfig.DependencyResolution | ||
| } | ||
|
|
||
| codeConfigValue, err := structpb.NewValue(codeConfigMap) | ||
| if err != nil { | ||
| return fmt.Errorf("encoding codeConfiguration: %w", err) | ||
| } | ||
|
|
||
| if _, err := azdClient.Project().SetServiceConfigValue(ctx, &azdext.SetServiceConfigValueRequest{ | ||
| ServiceName: serviceName, | ||
| Path: "codeConfiguration", | ||
| Value: codeConfigValue, | ||
| }); err != nil { | ||
| return fmt.Errorf("writing codeConfiguration to agent service: %w", err) | ||
| } | ||
|
|
||
| // Update the service language to match the runtime. | ||
| language := "python" | ||
| if strings.HasPrefix(codeConfig.Runtime, "dotnet_") { | ||
| language = "csharp" | ||
| } | ||
| langValue, err := structpb.NewValue(language) | ||
| if err != nil { | ||
| return fmt.Errorf("encoding language value: %w", err) | ||
| } | ||
| if _, err := azdClient.Project().SetServiceConfigValue(ctx, &azdext.SetServiceConfigValueRequest{ | ||
| ServiceName: serviceName, | ||
| Path: "language", | ||
| Value: langValue, | ||
| }); err != nil { | ||
| return fmt.Errorf("updating service language to %s: %w", language, err) | ||
| } | ||
|
|
||
|
trangevi marked this conversation as resolved.
|
||
| // Remove docker property if it was previously set (switching from container to code). | ||
| if adoptedServiceHasDocker(svc) { | ||
| if _, err := azdClient.Project().UnsetServiceConfig(ctx, &azdext.UnsetServiceConfigRequest{ | ||
| ServiceName: serviceName, | ||
| Path: "docker", | ||
| }); err != nil { | ||
| log.Printf("warning: could not clear docker property on service %q: %v", serviceName, err) | ||
| } | ||
| } | ||
|
|
||
| log.Printf("Applied code deploy configuration (runtime=%s, entryPoint=%s) to service %q", | ||
| codeConfig.Runtime, codeConfig.EntryPoint, serviceName) | ||
| return nil | ||
| } | ||
|
|
||
| // applyContainerDeployToService sets the docker property on the adopted agent | ||
| // service and ensures the language is "docker". Removes any codeConfiguration | ||
| // if present. | ||
| func applyContainerDeployToService( | ||
| ctx context.Context, | ||
| azdClient *azdext.AzdClient, | ||
| serviceName string, | ||
| svc *azdext.ServiceConfig, | ||
| ) error { | ||
| // Set docker property with remote build enabled. | ||
| dockerMap := map[string]any{"remoteBuild": true} | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. applyContainerDeployToService always writes remoteBuild: true, but the code-based init path in init_from_code.go sets serviceConfig.Docker = &azdext.DockerProjectOptions{RemoteBuild: !networkInjected}, deliberately turning remote build off when the target Foundry project has VNET network injection, since the ACR Tasks worker can't reach a registry inside the VNET. The deploy path doesn't re-derive this; it surfaces guidance telling the user to flip docker.remoteBuild to false by hand. So a sample adopted with container deploy against a network-injected project will fail the first azd deploy and force a manual edit, while the same sample created via init_from_code would have worked. Can we mirror the networkInjected check here (thread the selected Foundry project through) so the two init paths agree? |
||
| dockerValue, err := structpb.NewValue(dockerMap) | ||
| if err != nil { | ||
| return fmt.Errorf("encoding docker configuration: %w", err) | ||
| } | ||
|
|
||
| if _, err := azdClient.Project().SetServiceConfigValue(ctx, &azdext.SetServiceConfigValueRequest{ | ||
| ServiceName: serviceName, | ||
| Path: "docker", | ||
| Value: dockerValue, | ||
| }); err != nil { | ||
| return fmt.Errorf("writing docker property to agent service: %w", err) | ||
| } | ||
|
|
||
| // Set language to docker. | ||
| langValue, err := structpb.NewValue("docker") | ||
| if err != nil { | ||
| return fmt.Errorf("encoding language value: %w", err) | ||
| } | ||
| if _, err := azdClient.Project().SetServiceConfigValue(ctx, &azdext.SetServiceConfigValueRequest{ | ||
| ServiceName: serviceName, | ||
| Path: "language", | ||
| Value: langValue, | ||
| }); err != nil { | ||
| return fmt.Errorf("updating service language to docker: %w", err) | ||
| } | ||
|
|
||
| // Remove codeConfiguration if present (switching from code to container). | ||
| if adoptedServiceHasCodeConfig(svc) { | ||
| if _, err := azdClient.Project().UnsetServiceConfig(ctx, &azdext.UnsetServiceConfigRequest{ | ||
| ServiceName: serviceName, | ||
| Path: "codeConfiguration", | ||
| }); err != nil { | ||
| log.Printf("warning: could not clear codeConfiguration on service %q: %v", serviceName, err) | ||
| } | ||
| } | ||
|
|
||
| log.Printf("Applied container deploy configuration to service %q", serviceName) | ||
| return nil | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: ranging over resp.GetProject().GetServices() (a map) gives nondeterministic order, so with more than one azure.ai.agent service the prompt order and log output vary run to run. Single-agent samples won't notice, but sorting agentServices by name before the apply loop would make the behavior deterministic.