diff --git a/cli/azd/extensions/azure.ai.agents/go.mod b/cli/azd/extensions/azure.ai.agents/go.mod index 53bffe49b38..b7f7d18bc55 100644 --- a/cli/azd/extensions/azure.ai.agents/go.mod +++ b/cli/azd/extensions/azure.ai.agents/go.mod @@ -5,7 +5,6 @@ go 1.26.0 require ( github.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.0 github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1 - github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/appcontainers/armappcontainers v1.1.0 github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/authorization/armauthorization/v3 v3.0.0-beta.2 github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/cognitiveservices/armcognitiveservices/v2 v2.0.0 github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/containerregistry/armcontainerregistry v1.2.0 diff --git a/cli/azd/extensions/azure.ai.agents/go.sum b/cli/azd/extensions/azure.ai.agents/go.sum index d30737a772f..1f62dd30433 100644 --- a/cli/azd/extensions/azure.ai.agents/go.sum +++ b/cli/azd/extensions/azure.ai.agents/go.sum @@ -11,8 +11,6 @@ github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.2 h1:yz1bePFlP5Vws5+ github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.2/go.mod h1:Pa9ZNPuoNu/GztvBSKk9J1cDJW6vk/n0zLtV4mgd8N8= github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2 h1:9iefClla7iYpfYWdzPCRDozdmndjTm8DXdpCzPajMgA= github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2/go.mod h1:XtLgD3ZD34DAaVIIAyG3objl5DynM3CQ/vMcbBNJZGI= -github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/appcontainers/armappcontainers v1.1.0 h1:fdAOz6TFldGDoEcRa975i5L5QvWU8ptut+SJAIfuWUY= -github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/appcontainers/armappcontainers v1.1.0/go.mod h1:qV+BWew22CAalRTwJEAHs+aSLP49k/csNlspqhMIDRU= github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/appservice/armappservice/v2 v2.3.0 h1:JI8PcWOImyvIUEZ0Bbmfe05FOlWkMi2KhjG+cAKaUms= github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/appservice/armappservice/v2 v2.3.0/go.mod h1:nJLFPGJkyKfDDyJiPuHIXsCi/gpJkm07EvRgiX7SGlI= github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/authorization/armauthorization/v2 v2.2.0 h1:Hp+EScFOu9HeCbeW8WU2yQPJd4gGwhMgKxWe+G6jNzw= diff --git a/cli/azd/extensions/azure.ai.agents/internal/cmd/init.go b/cli/azd/extensions/azure.ai.agents/internal/cmd/init.go index 2fe1213c8e3..9d17d200a9c 100644 --- a/cli/azd/extensions/azure.ai.agents/internal/cmd/init.go +++ b/cli/azd/extensions/azure.ai.agents/internal/cmd/init.go @@ -45,7 +45,6 @@ type initFlags struct { model string manifestPointer string src string - host string env string } @@ -79,7 +78,6 @@ type GitHubUrlInfo struct { } const AiAgentHost = "azure.ai.agent" -const ContainerAppHost = "containerapp" // checkAiModelServiceAvailable is a temporary check to ensure the azd host supports // required gRPC services. Remove once azd core enforces requiredAzdVersion. @@ -354,9 +352,6 @@ func newInitCommand(rootFlags *rootFlagsDefinition) *cobra.Command { cmd.Flags().StringVarP(&flags.src, "src", "s", "", "Directory to download the agent definition to (defaults to 'src/')") - cmd.Flags().StringVarP(&flags.host, "host", "", "", - "For container based agents, can override the default host to target a container app instead. Accepted values: 'containerapp'") - cmd.Flags().StringVarP(&flags.env, "environment", "e", "", "The name of the azd environment to use.") return cmd @@ -384,14 +379,6 @@ func (a *InitAction) Run(ctx context.Context) error { isValidURL := false isValidFile := false - if a.flags.host != "" && a.flags.host != "containerapp" { - return exterrors.Validation( - exterrors.CodeUnsupportedHost, - fmt.Sprintf("unsupported host value: '%s' is not supported", a.flags.host), - "use '--host containerapp' or omit '--host'", - ) - } - if _, err := url.ParseRequestURI(a.flags.manifestPointer); err == nil { isValidURL = true } else if _, fileErr := os.Stat(a.flags.manifestPointer); fileErr == nil { @@ -424,7 +411,7 @@ func (a *InitAction) Run(ctx context.Context) error { } // Add the agent to the azd project (azure.yaml) services - if err := a.addToProject(ctx, targetDir, agentManifest, a.flags.host); err != nil { + if err := a.addToProject(ctx, targetDir, agentManifest); err != nil { return fmt.Errorf("failed to add agent to azure.yaml: %w", err) } @@ -1083,7 +1070,7 @@ func writeAgentDefinitionFile(targetDir string, agentManifest *agent_yaml.AgentM return nil } -func (a *InitAction) addToProject(ctx context.Context, targetDir string, agentManifest *agent_yaml.AgentManifest, host string) error { +func (a *InitAction) addToProject(ctx context.Context, targetDir string, agentManifest *agent_yaml.AgentManifest) error { // Convert the template to bytes templateBytes, err := json.Marshal(agentManifest.Template) if err != nil { @@ -1108,15 +1095,6 @@ func (a *InitAction) addToProject(ctx context.Context, targetDir string, agentMa return fmt.Errorf("failed to unmarshal JSON to AgentDefinition: %w", err) } - var serviceHost string - - switch host { - case "containerapp": - serviceHost = ContainerAppHost - default: - serviceHost = AiAgentHost - } - var agentConfig = project.ServiceTargetAgentConfig{} resourceDetails := []project.Resource{} @@ -1178,7 +1156,7 @@ func (a *InitAction) addToProject(ctx context.Context, targetDir string, agentMa serviceConfig := &azdext.ServiceConfig{ Name: strings.ReplaceAll(agentDef.Name, " ", ""), RelativePath: targetDir, - Host: serviceHost, + Host: AiAgentHost, Language: "docker", Config: agentConfigStruct, } diff --git a/cli/azd/extensions/azure.ai.agents/internal/cmd/listen.go b/cli/azd/extensions/azure.ai.agents/internal/cmd/listen.go index 07708b5e4d5..2327382a635 100644 --- a/cli/azd/extensions/azure.ai.agents/internal/cmd/listen.go +++ b/cli/azd/extensions/azure.ai.agents/internal/cmd/listen.go @@ -39,20 +39,19 @@ func newListenCommand() *cobra.Command { } defer azdClient.Close() - projectParser := &project.FoundryParser{AzdClient: azdClient} // IMPORTANT: service target name here must match the name used in the extension manifest. host := azdext.NewExtensionHost(azdClient). WithServiceTarget(AiAgentHost, func() azdext.ServiceTargetProvider { return project.NewAgentServiceTargetProvider(azdClient) }). WithProjectEventHandler("preprovision", func(ctx context.Context, args *azdext.ProjectEventArgs) error { - return preprovisionHandler(ctx, azdClient, projectParser, args) + return preprovisionHandler(ctx, azdClient, args) }). WithProjectEventHandler("predeploy", func(ctx context.Context, args *azdext.ProjectEventArgs) error { - return predeployHandler(ctx, azdClient, projectParser, args) + return predeployHandler(ctx, azdClient, args) }). WithProjectEventHandler("postdeploy", func(ctx context.Context, args *azdext.ProjectEventArgs) error { - return postdeployHandler(ctx, projectParser, args) + return postdeployHandler(ctx, azdClient, args) }) // Start listening for events @@ -66,11 +65,7 @@ func newListenCommand() *cobra.Command { } } -func preprovisionHandler(ctx context.Context, azdClient *azdext.AzdClient, projectParser *project.FoundryParser, args *azdext.ProjectEventArgs) error { - if err := projectParser.SetIdentity(ctx, args); err != nil { - return fmt.Errorf("failed to set identity: %w", err) - } - +func preprovisionHandler(ctx context.Context, azdClient *azdext.AzdClient, args *azdext.ProjectEventArgs) error { for _, svc := range args.Project.Services { switch svc.Host { case AiAgentHost: @@ -80,21 +75,13 @@ func preprovisionHandler(ctx context.Context, azdClient *azdext.AzdClient, proje if err := envUpdate(ctx, azdClient, args.Project, svc); err != nil { return fmt.Errorf("failed to update environment for service %q: %w", svc.Name, err) } - case ContainerAppHost: - if err := containerAgentHandling(ctx, azdClient, args.Project, svc); err != nil { - return fmt.Errorf("failed to handle container agent for service %q: %w", svc.Name, err) - } } } return nil } -func predeployHandler(ctx context.Context, azdClient *azdext.AzdClient, projectParser *project.FoundryParser, args *azdext.ProjectEventArgs) error { - if err := projectParser.SetIdentity(ctx, args); err != nil { - return fmt.Errorf("failed to set identity: %w", err) - } - +func predeployHandler(ctx context.Context, azdClient *azdext.AzdClient, args *azdext.ProjectEventArgs) error { for _, svc := range args.Project.Services { switch svc.Host { case AiAgentHost: @@ -110,14 +97,21 @@ func predeployHandler(ctx context.Context, azdClient *azdext.AzdClient, projectP return nil } -func postdeployHandler(ctx context.Context, projectParser *project.FoundryParser, args *azdext.ProjectEventArgs) error { - if err := projectParser.CoboPostDeploy(ctx, args); err != nil { - return err +func postdeployHandler(ctx context.Context, azdClient *azdext.AzdClient, args *azdext.ProjectEventArgs) error { + hasAgent := false + for _, svc := range args.Project.Services { + if svc.Host == AiAgentHost { + hasAgent = true + break + } + } + if !hasAgent { + return nil } // Ensure agent identity RBAC is configured when vnext is enabled. // Runs post-deploy because the platform provisions the identity during agent deployment. - if err := projectParser.EnsureAgentIdentityRBAC(ctx); err != nil { + if err := project.EnsureAgentIdentityRBAC(ctx, azdClient); err != nil { return fmt.Errorf("agent identity RBAC setup failed: %w", err) } @@ -233,35 +227,6 @@ func resourcesEnvUpdate(ctx context.Context, resources []project.Resource, azdCl return setEnvVar(ctx, azdClient, envName, "AI_PROJECT_DEPENDENT_RESOURCES", escapedJsonString) } -func containerAgentHandling(ctx context.Context, azdClient *azdext.AzdClient, project *azdext.ProjectConfig, svc *azdext.ServiceConfig) error { - servicePath := svc.RelativePath - fullPath := filepath.Join(project.Path, servicePath) - agentYamlPath := filepath.Join(fullPath, "agent.yaml") - - //nolint:gosec // agentYamlPath is resolved from project/service paths in current workspace - data, err := os.ReadFile(agentYamlPath) - if err != nil { - return nil - } - - var agentDef agent_yaml.AgentDefinition - if err := yaml.Unmarshal(data, &agentDef); err != nil { - return fmt.Errorf("YAML content is not valid: %w", err) - } - - // If there is an agent.yaml in the project, and it can be properly parsed into an agent definition, add the env var to enable container agents - currentEnvResponse, err := azdClient.Environment().GetCurrent(ctx, &azdext.EmptyRequest{}) - if err != nil { - return err - } - - if err := setEnvVar(ctx, azdClient, currentEnvResponse.Environment.Name, "ENABLE_CONTAINER_AGENTS", "true"); err != nil { - return err - } - - return nil -} - func setEnvVar(ctx context.Context, azdClient *azdext.AzdClient, envName string, key string, value string) error { _, err := azdClient.Environment().SetValue(ctx, &azdext.SetEnvRequest{ EnvName: envName, diff --git a/cli/azd/extensions/azure.ai.agents/internal/exterrors/codes.go b/cli/azd/extensions/azure.ai.agents/internal/exterrors/codes.go index 045e6ee226a..2c772c4b118 100644 --- a/cli/azd/extensions/azure.ai.agents/internal/exterrors/codes.go +++ b/cli/azd/extensions/azure.ai.agents/internal/exterrors/codes.go @@ -21,7 +21,6 @@ const ( CodeInvalidServiceConfig = "invalid_service_config" CodeInvalidAgentRequest = "invalid_agent_request" CodeInvalidSessionId = "invalid_session_id" - CodeUnsupportedHost = "unsupported_host" CodeUnsupportedAgentKind = "unsupported_agent_kind" CodeMissingAgentKind = "missing_agent_kind" CodeAgentDefinitionNotFound = "agent_definition_not_found" diff --git a/cli/azd/extensions/azure.ai.agents/internal/project/agent_identity_rbac.go b/cli/azd/extensions/azure.ai.agents/internal/project/agent_identity_rbac.go index 8c04ab403ae..458553740d2 100644 --- a/cli/azd/extensions/azure.ai.agents/internal/project/agent_identity_rbac.go +++ b/cli/azd/extensions/azure.ai.agents/internal/project/agent_identity_rbac.go @@ -105,8 +105,8 @@ func agentIdentityDisplayName(accountName, projectName string) string { // // The platform provisions the agent identity automatically when an agent is deployed. // This function assumes the identity already exists and assigns permissions to it. -func (p *FoundryParser) EnsureAgentIdentityRBAC(ctx context.Context) error { - azdEnvClient := p.AzdClient.Environment() +func EnsureAgentIdentityRBAC(ctx context.Context, azdClient *azdext.AzdClient) error { + azdEnvClient := azdClient.Environment() cEnvResponse, err := azdEnvClient.GetCurrent(ctx, &azdext.EmptyRequest{}) if err != nil { return fmt.Errorf("failed to get current environment: %w", err) @@ -137,7 +137,7 @@ func (p *FoundryParser) EnsureAgentIdentityRBAC(ctx context.Context) error { } // Get tenant ID and create credential - tenantResponse, err := p.AzdClient.Account().LookupTenant(ctx, &azdext.LookupTenantRequest{ + tenantResponse, err := azdClient.Account().LookupTenant(ctx, &azdext.LookupTenantRequest{ SubscriptionId: info.SubscriptionID, }) if err != nil { diff --git a/cli/azd/extensions/azure.ai.agents/internal/project/parser.go b/cli/azd/extensions/azure.ai.agents/internal/project/parser.go deleted file mode 100644 index c73d8b0e60b..00000000000 --- a/cli/azd/extensions/azure.ai.agents/internal/project/parser.go +++ /dev/null @@ -1,1197 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -package project - -import ( - "azureaiagent/internal/pkg/agents/agent_yaml" - "bytes" - "context" - "encoding/json" - "fmt" - "io" - "net/http" - "os" - "path/filepath" - "strings" - "time" - - "github.com/Azure/azure-sdk-for-go/sdk/azcore/arm" - "github.com/Azure/azure-sdk-for-go/sdk/azcore/policy" - "github.com/Azure/azure-sdk-for-go/sdk/azidentity" - "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/appcontainers/armappcontainers" - "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/authorization/armauthorization/v3" - "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/cognitiveservices/armcognitiveservices/v2" - "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources" - "github.com/azure/azure-dev/cli/azd/pkg/azdext" - "github.com/azure/azure-dev/cli/azd/pkg/graphsdk" - "github.com/braydonk/yaml" - "github.com/google/uuid" -) - -type FoundryParser struct { - AzdClient *azdext.AzdClient -} - -// Check if there is a service using containerapp host and contains agent.yaml file in the service path -func shouldRun(ctx context.Context, project *azdext.ProjectConfig) (bool, error) { - projectPath := project.Path - for _, service := range project.Services { - if service.Host == "containerapp" { - servicePath := filepath.Join(projectPath, service.RelativePath) - - agentYamlPath := filepath.Join(servicePath, "agent.yaml") - agentYmlPath := filepath.Join(servicePath, "agent.yml") - agentPath := "" - - if _, err := os.Stat(agentYamlPath); err == nil { - agentPath = agentYamlPath - } - - if _, err := os.Stat(agentYmlPath); err == nil { - agentPath = agentYmlPath - } - if agentPath != "" { - // read the file content into bytes and close the file - content, err := os.ReadFile(agentPath) - if err != nil { - return false, fmt.Errorf("failed to read agent yaml file: %w", err) - } - - err = agent_yaml.ValidateAgentDefinition(content) - if err != nil { - return false, fmt.Errorf("agent.yaml is not valid to run: %w", err) - } - - var genericTemplate map[string]any - if err := yaml.Unmarshal(content, &genericTemplate); err != nil { - return false, fmt.Errorf("YAML content is not valid to run: %w", err) - } - - kind, ok := genericTemplate["kind"].(string) - if !ok { - return false, fmt.Errorf("kind field is not a valid string to check should run: %w", err) - } - - return kind == string(agent_yaml.AgentKindHosted), nil - } - } - } - - return false, nil -} - -func (p *FoundryParser) SetIdentity(ctx context.Context, args *azdext.ProjectEventArgs) error { - shouldRun, err := shouldRun(ctx, args.Project) - if err != nil { - return fmt.Errorf("failed to determine if extension should attach: %w", err) - } - if !shouldRun { - return nil - } - - // Get aiFoundryProjectResourceId from environment config - azdEnvClient := p.AzdClient.Environment() - response, err := azdEnvClient.GetConfigString(ctx, &azdext.GetConfigStringRequest{ - Path: "infra.parameters.aiFoundryProjectResourceId", - }) - if err != nil { - return fmt.Errorf("failed to get environment config: %w", err) - } - aiFoundryProjectResourceID := response.Value - fmt.Println("✓ Retrieved aiFoundryProjectResourceId") - - // Extract subscription ID from resource ID - parts := strings.Split(aiFoundryProjectResourceID, "/") - if len(parts) < 3 { - return fmt.Errorf("invalid resource ID format: %s", aiFoundryProjectResourceID) - } - - // Find subscription ID - var subscriptionID string - for i, part := range parts { - if part == "subscriptions" && i+1 < len(parts) { - subscriptionID = parts[i+1] - break - } - } - - if subscriptionID == "" { - return fmt.Errorf("subscription ID not found in resource ID: %s", aiFoundryProjectResourceID) - } - - // Get the tenant ID - tenantResponse, err := p.AzdClient.Account().LookupTenant(ctx, &azdext.LookupTenantRequest{ - SubscriptionId: subscriptionID, - }) - if err != nil { - return fmt.Errorf("failed to get tenant ID: %w", err) - } - - cred, err := azidentity.NewAzureDeveloperCLICredential(&azidentity.AzureDeveloperCLICredentialOptions{ - TenantID: tenantResponse.TenantId, - AdditionallyAllowedTenants: []string{"*"}, - }) - if err != nil { - return fmt.Errorf("failed to create Azure credential: %w", err) - } - - // Get Microsoft Foundry Project's managed identity - fmt.Println("Retrieving Microsoft Foundry Project identity...") - projectPrincipalID, err := getProjectPrincipalID(ctx, cred, aiFoundryProjectResourceID, subscriptionID) - if err != nil { - return fmt.Errorf("failed to get Project principal ID: %w", err) - } - fmt.Printf("Principal ID: %s\n", projectPrincipalID) - - // Get Application ID from Principal ID - fmt.Println("Retrieving Application ID...") - projectClientID, err := getApplicationID(ctx, cred, projectPrincipalID) - if err != nil { - return fmt.Errorf("failed to get Application ID: %w", err) - } - - fmt.Printf("Application ID: %s\n", projectClientID) - - // Save to environment - cResponse, err := azdEnvClient.GetCurrent(ctx, &azdext.EmptyRequest{}) - if err != nil { - return fmt.Errorf("failed to get current environment: %w", err) - } - - _, err = azdEnvClient.SetValue(ctx, &azdext.SetEnvRequest{ - EnvName: cResponse.Environment.Name, - Key: "AZURE_AI_PROJECT_PRINCIPAL_ID", - Value: projectClientID, - }) - if err != nil { - return fmt.Errorf("failed to set AZURE_AI_PROJECT_PRINCIPAL_ID in environment: %w", err) - } - - fmt.Println("✓ Application ID saved to environment") - - return nil -} - -// getProjectPrincipalID retrieves the principal ID from the Microsoft Foundry Project using Azure SDK -func getProjectPrincipalID( - ctx context.Context, - cred *azidentity.AzureDeveloperCLICredential, - resourceID, - subscriptionID string) (string, error) { - // Create resources client - client, err := armresources.NewClient(subscriptionID, cred, nil) - if err != nil { - return "", fmt.Errorf("failed to create resources client: %w", err) - } - - // Get the resource - // API version for Microsoft Foundry projects (Machine Learning workspaces) - apiVersion := "2025-06-01" - resp, err := client.GetByID(ctx, resourceID, apiVersion, nil) - if err != nil { - return "", fmt.Errorf("failed to retrieve resource: %w", err) - } - - // Extract principal ID from identity - if resp.Identity == nil { - return "", fmt.Errorf("resource does not have an identity") - } - - if resp.Identity.PrincipalID == nil { - return "", fmt.Errorf("resource identity does not have a principal ID") - } - - principalID := *resp.Identity.PrincipalID - if principalID == "" { - return "", fmt.Errorf("principal ID is empty") - } - - return principalID, nil -} - -// getApplicationID retrieves the application ID from the principal ID using Microsoft Graph API -func getApplicationID( - ctx context.Context, cred *azidentity.AzureDeveloperCLICredential, principalID string) (string, error) { - // Create Graph client - graphClient, err := graphsdk.NewGraphClient(cred, nil) - if err != nil { - return "", fmt.Errorf("failed to create Graph client: %w", err) - } - - // Get service principal directly by object ID (principal ID) - servicePrincipal, err := graphClient. - ServicePrincipalById(principalID). - Get(ctx) - - if err != nil { - return "", fmt.Errorf("failed to retrieve service principal with principal ID '%s': %w", principalID, err) - } - - appID := servicePrincipal.AppId - if appID == "" { - return "", fmt.Errorf("application ID is empty") - } - - return appID, nil -} - -// getCognitiveServicesAccountLocation retrieves the location of a Cognitive Services account using Azure SDK -func getCognitiveServicesAccountLocation( - ctx context.Context, - cred *azidentity.AzureDeveloperCLICredential, - subscriptionID, - resourceGroupName, - accountName string) (string, error) { - // Create cognitive services accounts client - client, err := armcognitiveservices.NewAccountsClient(subscriptionID, cred, nil) - if err != nil { - return "", fmt.Errorf("failed to create cognitive services client: %w", err) - } - - // Get the account - resp, err := client.Get(ctx, resourceGroupName, accountName, nil) - if err != nil { - return "", fmt.Errorf("failed to get cognitive services account: %w", err) - } - - // Extract location - if resp.Location == nil { - return "", fmt.Errorf("cognitive services account does not have a location") - } - - location := *resp.Location - if location == "" { - return "", fmt.Errorf("location is empty") - } - - return location, nil -} - -///////////////////////////////////////////////////////////////////////////// - -// Config structures for JSON parsing -type AgentRegistrationPayload struct { - Description string `json:"description"` - Definition AgentDefinition `json:"definition"` -} - -type AgentDefinition struct { - Kind string `json:"kind"` - ContainerProtocolVersions []ContainerProtocolVersion `json:"container_protocol_versions"` - ContainerAppResourceID string `json:"container_app_resource_id"` - IngressSubdomainSuffix string `json:"ingress_subdomain_suffix"` -} - -type ContainerProtocolVersion struct { - Protocol string `json:"protocol"` - Version string `json:"version"` -} - -type AgentResponse struct { - Version string `json:"version"` -} - -type DataPlanePayload struct { - Agent AgentReference `json:"agent"` - Input string `json:"input"` - Stream bool `json:"stream"` -} - -type AgentReference struct { - Type string `json:"type"` - Name string `json:"name"` - Version string `json:"version"` -} - -type DataPlaneResponse struct { - Output string `json:"output"` -} - -func (p *FoundryParser) CoboPostDeploy(ctx context.Context, args *azdext.ProjectEventArgs) error { - shouldRun, err := shouldRun(ctx, args.Project) - if err != nil { - return fmt.Errorf("failed to determine if extension should attach: %w", err) - } - if !shouldRun { - return nil - } - - azdEnvClient := p.AzdClient.Environment() - cEnvResponse, err := azdEnvClient.GetCurrent(ctx, &azdext.EmptyRequest{}) - if err != nil { - return fmt.Errorf("failed to get current environment: %w", err) - } - envResponse, err := azdEnvClient.GetValues(ctx, &azdext.GetEnvironmentRequest{ - Name: cEnvResponse.Environment.Name, - }) - if err != nil { - return fmt.Errorf("failed to get environment values: %w", err) - } - values := envResponse.KeyValues - azdEnv := make(map[string]string, len(values)) - for _, kv := range values { - azdEnv[kv.Key] = kv.Value - } - - // Get required values from azd environment - containerAppPrincipalID := azdEnv["SERVICE_API_IDENTITY_PRINCIPAL_ID"] - aiFoundryProjectResourceID := azdEnv["AZURE_AI_PROJECT_ID"] - deploymentName := azdEnv["DEPLOYMENT_NAME"] - resourceID := azdEnv["SERVICE_API_RESOURCE_ID"] - agentName := azdEnv["AGENT_NAME"] - //aiProjectEndpoint := azdEnv["AI_PROJECT_ENDPOINT"] - - // Validate required variables - if err := validateRequired("AZURE_AI_PROJECT_ID", aiFoundryProjectResourceID); err != nil { - return err - } - - // Extract project information from resource IDs - parts := strings.Split(aiFoundryProjectResourceID, "/") - if len(parts) < 11 { - fmt.Fprintln(os.Stderr, "Error: Invalid Microsoft Foundry Project Resource ID format") - os.Exit(1) - } - - // Extract AI account resource ID by removing "/projects/project-name" from the project resource ID - parts = strings.Split(aiFoundryProjectResourceID, "/projects/") - aiAccountResourceId := parts[0] - - if err := validateRequired("AZURE_AI_PROJECT_ID", aiFoundryProjectResourceID); err != nil { - return err - } - if err := validateRequired("SERVICE_API_IDENTITY_PRINCIPAL_ID", containerAppPrincipalID); err != nil { - return err - } - if err := validateRequired("DEPLOYMENT_NAME", deploymentName); err != nil { - return err - } - if err := validateRequired("AGENT_NAME", agentName); err != nil { - return err - } - - projectSubscriptionID := parts[2] - projectResourceGroup := parts[4] - projectAIFoundryName := parts[8] - projectName := parts[10] - - // Get the tenant ID - tenantResponse, err := p.AzdClient.Account().LookupTenant(ctx, &azdext.LookupTenantRequest{ - SubscriptionId: projectSubscriptionID, - }) - if err != nil { - return fmt.Errorf("failed to get tenant ID: %w", err) - } - - cred, err := azidentity.NewAzureDeveloperCLICredential(&azidentity.AzureDeveloperCLICredentialOptions{ - TenantID: tenantResponse.TenantId, - AdditionallyAllowedTenants: []string{"*"}, - }) - if err != nil { - return fmt.Errorf("failed to create Azure credential: %w", err) - } - - // Get Microsoft Foundry region using SDK - aiFoundryRegion, err := getCognitiveServicesAccountLocation( - ctx, cred, projectSubscriptionID, projectResourceGroup, projectAIFoundryName) - if err != nil { - return fmt.Errorf("failed to get Microsoft Foundry region: %w", err) - } - - fmt.Printf("Microsoft Foundry region: %s\n", aiFoundryRegion) - fmt.Printf("Project: %s\n", projectName) - fmt.Printf("Deployment: %s\n", deploymentName) - fmt.Printf("Agent: %s\n", agentName) - - // Assign Azure AI User role - if err := assignAzureAIRole(ctx, cred, containerAppPrincipalID, aiAccountResourceId); err != nil { - fmt.Fprintf(os.Stderr, "Error: Failed to assign 'Azure AI User' role: %v\n", err) - fmt.Fprintln(os.Stderr, "This requires Owner or User Access Administrator role on the Microsoft Foundry Account.") - fmt.Fprintln(os.Stderr, "Manual command:") - fmt.Fprintf(os.Stderr, "az role assignment create \\\n") - fmt.Fprintf(os.Stderr, " --assignee %s \\\n", containerAppPrincipalID) - fmt.Fprintf(os.Stderr, " --role \"53ca6127-db72-4b80-b1b0-d745d6d5456d\" \\\n") - fmt.Fprintf(os.Stderr, " --scope \"%s\"\n", aiAccountResourceId) - return err - } - - if err := validateRequired("SERVICE_API_RESOURCE_ID", resourceID); err != nil { - return err - } - - // Deactivate hello-world revision - if err := deactivateHelloWorldRevision(ctx, cred, resourceID); err != nil { - fmt.Fprintf(os.Stderr, "Warning: Failed to deactivate hello-world revision: %v\n", err) - // Don't return error, just warn - this is not critical for the deployment - } - - // Verify authentication configuration - if err := verifyAuthConfiguration(ctx, cred, resourceID); err != nil { - fmt.Fprintf(os.Stderr, "Warning: Failed to verify authentication configuration: %v\n", err) - // Don't return error, just warn - this is not critical for the deployment - } - - // Get the Container App endpoint (FQDN) for testing using SDK - fmt.Println("Retrieving Container App endpoint...") - acaEndpoint, err := getContainerAppEndpoint(ctx, cred, resourceID, projectSubscriptionID) - if err != nil { - fmt.Fprintf(os.Stderr, "Warning: Failed to retrieve Container App endpoint: %v\n", err) - } else { - fmt.Printf("Container App endpoint: %s\n", acaEndpoint) - } - - // Get Microsoft Foundry Project endpoint using SDK - fmt.Println("Retrieving Microsoft Foundry Project API endpoint...") - aiFoundryProjectEndpoint, err := getAIFoundryProjectEndpoint( - ctx, cred, aiFoundryProjectResourceID, projectSubscriptionID) - if err != nil { - fmt.Fprintf(os.Stderr, "Warning: Failed to retrieve Microsoft Foundry Project API endpoint: %v\n", err) - } else { - fmt.Printf("Microsoft Foundry Project API endpoint: %s\n", aiFoundryProjectEndpoint) - } - - // Acquire AAD token using SDK - token, err := getAccessToken(ctx, cred, "https://ai.azure.com") - if err != nil { - fmt.Fprintf(os.Stderr, "Error: Failed to acquire access token: %v\n", err) - return fmt.Errorf("failed to acquire access token: %w", err) - } - - // Get latest revision and build ingress suffix using SDK - latestRevision, err := getLatestRevisionName(ctx, cred, resourceID, projectSubscriptionID) - if err != nil { - return fmt.Errorf("failed to get latest revision: %w", err) - } - - ingressSuffix := "--" + latestRevision[strings.LastIndex(latestRevision, "--")+2:] - if ingressSuffix == "--"+latestRevision { - ingressSuffix = "--" + latestRevision - } - - // Construct agent registration URI - workspaceName := fmt.Sprintf("%s@%s@AML", projectAIFoundryName, projectName) - apiPath := fmt.Sprintf( - "/agents/v2.0/subscriptions/%s/resourceGroups/%s/providers/Microsoft.MachineLearningServices/"+ - "workspaces/%s/agents/%s/versions?api-version=2025-05-15-preview", - projectSubscriptionID, projectResourceGroup, workspaceName, agentName) - - uri := "" - if aiFoundryProjectEndpoint != "" { - uri = aiFoundryProjectEndpoint + apiPath - } else { - uri = fmt.Sprintf("https://%s.api.azureml.ms%s", aiFoundryRegion, apiPath) - } - - // Register agent with retry logic - agentVersion := registerAgent(ctx, uri, token, resourceID, ingressSuffix) - - // Test authentication and agent - if agentVersion != "" { - testUnauthenticatedAccess(ctx, acaEndpoint) - testDataPlane(ctx, aiFoundryProjectEndpoint, token, agentName, agentVersion) - } - - // Print Azure Portal link - fmt.Println() - fmt.Println("======================================") - fmt.Println("Azure Portal") - fmt.Println("======================================") - fmt.Printf("https://portal.azure.com/#@/resource%s\n", resourceID) - - fmt.Println() - fmt.Println("✓ Post-deployment completed successfully") - - return nil -} - -// validateRequired checks if a required variable is set -func validateRequired(name, value string) error { - if value == "" { - return fmt.Errorf("%s not set", name) - } - return nil -} - -// assignAzureAIRole assigns the Azure AI User role to the container app identity using Azure SDK -func assignAzureAIRole(ctx context.Context, cred *azidentity.AzureDeveloperCLICredential, principalID, scope string) error { - fmt.Println() - fmt.Println("======================================") - fmt.Println("Assigning Azure AI Access Permissions") - fmt.Println("======================================") - - roleDefinitionID := "53ca6127-db72-4b80-b1b0-d745d6d5456d" // Azure AI User - - fmt.Println("Assigning 'Azure AI User' role to Container App identity...") - fmt.Printf("Principal ID: %s\n", principalID) - fmt.Printf("Scope: %s\n", scope) - fmt.Println() - - // Extract subscription ID from scope - // Scope format: /subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/... - parts := strings.Split(scope, "/") - var subscriptionID string - for i, part := range parts { - if part == "subscriptions" && i+1 < len(parts) { - subscriptionID = parts[i+1] - break - } - } - if subscriptionID == "" { - return fmt.Errorf("could not extract subscription ID from scope: %s", scope) - } - - // Create role assignments client - client, err := armauthorization.NewRoleAssignmentsClient(subscriptionID, cred, nil) - if err != nil { - return fmt.Errorf("failed to create role assignments client: %w", err) - } - - // Construct full role definition ID - fullRoleDefinitionID := fmt.Sprintf("%s/providers/Microsoft.Authorization/roleDefinitions/%s", scope, roleDefinitionID) - - // Check if the role assignment already exists - // Use atScope() to list all role assignments at this scope, then filter in code - pager := client.NewListForScopePager(scope, &armauthorization.RoleAssignmentsClientListForScopeOptions{ - Filter: new("atScope()"), - }) - - assignmentExists := false - var existingAssignmentId string - for pager.More() { - page, err := pager.NextPage(ctx) - if err != nil { - return fmt.Errorf("failed to list role assignments: %w", err) - } - for _, assignment := range page.Value { - if assignment.Properties != nil && - assignment.Properties.PrincipalID != nil && - assignment.Properties.RoleDefinitionID != nil { - // Filter by both principal ID and role definition ID - if *assignment.Properties.PrincipalID == principalID && - *assignment.Properties.RoleDefinitionID == fullRoleDefinitionID { - assignmentExists = true - if assignment.Name != nil { - existingAssignmentId = *assignment.Name - } - break - } - } - } - if assignmentExists { - break - } - } - - if assignmentExists { - fmt.Println("✓ Role assignment already exists") - if existingAssignmentId != "" { - fmt.Printf(" Assignment ID: %s\n", existingAssignmentId) - } - } else { - // Generate a unique name for the role assignment - roleAssignmentName := uuid.New().String() - // Create role assignment - parameters := armauthorization.RoleAssignmentCreateParameters{ - Properties: &armauthorization.RoleAssignmentProperties{ - RoleDefinitionID: new(fullRoleDefinitionID), - PrincipalID: new(principalID), - }, - } - - resp, err := client.Create(ctx, scope, roleAssignmentName, parameters, nil) - if err != nil { - // Check if the error is due to role assignment already existing (409 Conflict) - if strings.Contains(err.Error(), "RoleAssignmentExists") || strings.Contains(err.Error(), "409") { - fmt.Println("✓ Role assignment already exists (detected during creation)") - assignmentExists = true // Mark as existing so we skip waiting - } else { - return fmt.Errorf("failed to create role assignment: %w", err) - } - } else { - fmt.Println("✓ Successfully assigned 'Azure AI User' role") - - if resp.Name != nil { - fmt.Printf(" Assignment ID: %s\n", *resp.Name) - } - } - } - - // Only wait for propagation if we just created a new assignment - if !assignmentExists { - fmt.Println() - fmt.Println("⏳ Waiting 30 seconds for RBAC propagation...") - time.Sleep(30 * time.Second) - - // Validate the assignment - fmt.Println("Validating role assignment...") - pager = client.NewListForScopePager(scope, &armauthorization.RoleAssignmentsClientListForScopeOptions{ - Filter: new("atScope()"), - }) - - validated := false - for pager.More() { - page, err := pager.NextPage(ctx) - if err != nil { - fmt.Fprintln(os.Stderr, "Warning: Could not validate role assignment. It may still be propagating.") - break - } - - for _, assignment := range page.Value { - if assignment.Properties != nil && assignment.Properties.RoleDefinitionID != nil { - if strings.Contains(*assignment.Properties.RoleDefinitionID, roleDefinitionID) { - fmt.Println("✓ Role assignment validated successfully") - fmt.Printf(" Role: Azure AI User\n") - validated = true - break - } - } - } - if validated { - break - } - } - - if !validated { - fmt.Fprintln(os.Stderr, "Warning: Could not validate role assignment. It may still be propagating.") - } - } - - return nil -} - -// deactivateHelloWorldRevision deactivates the hello-world placeholder revision using Azure SDK -func deactivateHelloWorldRevision( - ctx context.Context, cred *azidentity.AzureDeveloperCLICredential, resourceID string) error { - fmt.Println() - fmt.Println("======================================") - fmt.Println("Deactivating Hello-World Revision") - fmt.Println("======================================") - fmt.Println("ℹ️ Azure Container Apps requires an image during provision, but with remote Docker") - fmt.Println(" build, the app image doesn't exist yet. A hello-world placeholder image is used") - fmt.Println(" during 'azd provision', then replaced with your app image during 'azd deploy'.") - fmt.Println(" Now that your app is deployed, we'll deactivate the placeholder revision.") - fmt.Println() - - // Parse resource ID - parsedResource, err := arm.ParseResourceID(resourceID) - if err != nil { - return fmt.Errorf("failed to parse resource ID: %w", err) - } - - subscription := parsedResource.SubscriptionID - resourceGroup := parsedResource.ResourceGroupName - appName := parsedResource.Name - - if subscription == "" || resourceGroup == "" || appName == "" { - return fmt.Errorf("could not parse subscription, resource group or app name from resource ID: %s", resourceID) - } - - // Create container apps revisions client - revisionsClient, err := armappcontainers.NewContainerAppsRevisionsClient(subscription, cred, nil) - if err != nil { - return fmt.Errorf("failed to create revisions client: %w", err) - } - - // List all revisions - pager := revisionsClient.NewListRevisionsPager(resourceGroup, appName, nil) - - var helloWorldRevision *armappcontainers.Revision - for pager.More() { - page, err := pager.NextPage(ctx) - if err != nil { - return fmt.Errorf("failed to list revisions: %w", err) - } - - for _, revision := range page.Value { - // Check if this is a hello-world revision - if revision.Properties != nil && - revision.Properties.Template != nil && - revision.Properties.Template.Containers != nil && - len(revision.Properties.Template.Containers) > 0 { - - container := revision.Properties.Template.Containers[0] - if container.Image != nil && - strings.Contains(*container.Image, "containerapps-helloworld") && - revision.Name != nil && - !strings.Contains(*revision.Name, "--azd-") { - helloWorldRevision = revision - break - } - } - } - if helloWorldRevision != nil { - break - } - } - - if helloWorldRevision == nil { - fmt.Println("No hello-world revision found (already removed or using custom image)") - return nil - } - - if helloWorldRevision.Name == nil { - return fmt.Errorf("revision name is nil") - } - - revisionName := *helloWorldRevision.Name - fmt.Printf("Found hello-world revision: %s\n", revisionName) - - if helloWorldRevision.Properties != nil && - helloWorldRevision.Properties.Template != nil && - helloWorldRevision.Properties.Template.Containers != nil && - len(helloWorldRevision.Properties.Template.Containers) > 0 { - if img := helloWorldRevision.Properties.Template.Containers[0].Image; img != nil { - fmt.Printf("Image: %s\n", *img) - } - } - - // Double-check before deactivating - if helloWorldRevision.Properties != nil && - helloWorldRevision.Properties.Template != nil && - helloWorldRevision.Properties.Template.Containers != nil && - len(helloWorldRevision.Properties.Template.Containers) > 0 { - - container := helloWorldRevision.Properties.Template.Containers[0] - if container.Image == nil || !strings.Contains(*container.Image, "containerapps-helloworld") { - fmt.Fprintln(os.Stderr, "Warning: Revision does not have hello-world image, skipping for safety") - return nil - } - } - - if strings.Contains(revisionName, "--azd-") { - fmt.Fprintln(os.Stderr, "Warning: Revision name contains '--azd-' pattern, skipping for safety") - return nil - } - - // Check if already inactive - if helloWorldRevision.Properties != nil && - helloWorldRevision.Properties.Active != nil && - !*helloWorldRevision.Properties.Active { - fmt.Println("Revision is already inactive") - return nil - } - - // Deactivate the revision - fmt.Println("Deactivating revision...") - _, err = revisionsClient.DeactivateRevision(ctx, resourceGroup, appName, revisionName, nil) - if err != nil { - return fmt.Errorf("failed to deactivate revision: %w", err) - } - - fmt.Println("✓ Hello-world revision deactivated successfully") - return nil -} - -// verifyAuthConfiguration verifies the authentication configuration using Azure SDK -func verifyAuthConfiguration(ctx context.Context, cred *azidentity.AzureDeveloperCLICredential, resourceID string) error { - fmt.Println() - fmt.Println("======================================") - fmt.Println("Verifying Authentication Configuration") - fmt.Println("======================================") - - // Parse resource ID - parsedResource, err := arm.ParseResourceID(resourceID) - if err != nil { - return fmt.Errorf("failed to parse resource ID: %w", err) - } - - subscription := parsedResource.SubscriptionID - resourceGroup := parsedResource.ResourceGroupName - appName := parsedResource.Name - - if subscription == "" || resourceGroup == "" || appName == "" { - return fmt.Errorf("could not parse subscription, resource group or app name from resource ID: %s", resourceID) - } - - // Create container apps auth configs client - authClient, err := armappcontainers.NewContainerAppsAuthConfigsClient(subscription, cred, nil) - if err != nil { - return fmt.Errorf("failed to create auth configs client: %w", err) - } - - // Get the auth config (named "current" by convention for the active config) - authConfig, err := authClient.Get(ctx, resourceGroup, appName, "current", nil) - if err != nil { - fmt.Fprintln(os.Stderr, "Warning: No authentication configuration found") - return nil - } - - // Check if Azure AD authentication is configured - if authConfig.Properties == nil || - authConfig.Properties.Platform == nil || - authConfig.Properties.Platform.Enabled == nil { - fmt.Fprintln(os.Stderr, "Warning: No authentication configuration found") - return nil - } - - if !*authConfig.Properties.Platform.Enabled { - fmt.Fprintln(os.Stderr, "Warning: Authentication is not enabled") - return nil - } - - // Check for Azure AD identity provider - if authConfig.Properties.IdentityProviders != nil && - authConfig.Properties.IdentityProviders.AzureActiveDirectory != nil && - authConfig.Properties.IdentityProviders.AzureActiveDirectory.Enabled != nil && - *authConfig.Properties.IdentityProviders.AzureActiveDirectory.Enabled { - - fmt.Println("✓ Azure AD authentication enabled") - - aadConfig := authConfig.Properties.IdentityProviders.AzureActiveDirectory - if aadConfig.Registration != nil && aadConfig.Registration.ClientID != nil { - fmt.Printf(" Client ID: %s\n", *aadConfig.Registration.ClientID) - } - - if authConfig.Properties.GlobalValidation != nil && - authConfig.Properties.GlobalValidation.UnauthenticatedClientAction != nil { - fmt.Printf( - " Unauthenticated Action: %s\n", - string(*authConfig.Properties.GlobalValidation.UnauthenticatedClientAction)) - } - } else { - fmt.Fprintln(os.Stderr, "Warning: Azure AD authentication is not configured") - } - - return nil -} - -// getContainerAppEndpoint retrieves the Container App FQDN using Azure SDK -func getContainerAppEndpoint( - ctx context.Context, - cred *azidentity.AzureDeveloperCLICredential, - resourceID, - subscriptionID string) (string, error) { - // Parse resource ID - parsedResource, err := arm.ParseResourceID(resourceID) - if err != nil { - return "", fmt.Errorf("failed to parse resource ID: %w", err) - } - - resourceGroup := parsedResource.ResourceGroupName - appName := parsedResource.Name - - if resourceGroup == "" || appName == "" { - return "", fmt.Errorf("could not parse resource group or app name from resource ID: %s", resourceID) - } - - // Create container apps client - client, err := armappcontainers.NewContainerAppsClient(subscriptionID, cred, nil) - if err != nil { - return "", fmt.Errorf("failed to create container apps client: %w", err) - } - - // Get the container app - containerApp, err := client.Get(ctx, resourceGroup, appName, nil) - if err != nil { - return "", fmt.Errorf("failed to get container app: %w", err) - } - - // Extract FQDN - if containerApp.Properties == nil || - containerApp.Properties.Configuration == nil || - containerApp.Properties.Configuration.Ingress == nil || - containerApp.Properties.Configuration.Ingress.Fqdn == nil { - return "", fmt.Errorf("container app does not have an ingress FQDN") - } - - fqdn := *containerApp.Properties.Configuration.Ingress.Fqdn - if fqdn == "" { - return "", fmt.Errorf("FQDN is empty") - } - - return "https://" + fqdn, nil -} - -// getAIFoundryProjectEndpoint retrieves the Microsoft Foundry Project API endpoint using Azure SDK -func getAIFoundryProjectEndpoint( - ctx context.Context, - cred *azidentity.AzureDeveloperCLICredential, - resourceID, - subscriptionID string) (string, error) { - // Create resources client - client, err := armresources.NewClient(subscriptionID, cred, nil) - if err != nil { - return "", fmt.Errorf("failed to create resources client: %w", err) - } - - // Get the resource - // API version for Microsoft Foundry projects (Machine Learning workspaces) - apiVersion := "2025-06-01" - resp, err := client.GetByID(ctx, resourceID, apiVersion, nil) - if err != nil { - return "", fmt.Errorf("failed to retrieve resource: %w", err) - } - - // Extract Microsoft Foundry API endpoint - if resp.Properties == nil { - return "", fmt.Errorf("resource does not have properties") - } - - // Parse properties as a map to access nested endpoints - propsMap, ok := resp.Properties.(map[string]any) - if !ok { - return "", fmt.Errorf("failed to parse resource properties") - } - - endpoints, ok := propsMap["endpoints"].(map[string]any) - if !ok { - return "", fmt.Errorf("resource does not have endpoints") - } - - aiFoundryAPI, ok := endpoints["AI Foundry API"].(string) - if !ok || aiFoundryAPI == "" { - return "", fmt.Errorf("AI Foundry API endpoint not found") - } - - return aiFoundryAPI, nil -} - -// getAccessToken retrieves an access token for the specified resource using Azure SDK -func getAccessToken(ctx context.Context, cred *azidentity.AzureDeveloperCLICredential, resource string) (string, error) { - // Get access token for the specified resource - token, err := cred.GetToken(ctx, policy.TokenRequestOptions{ - Scopes: []string{resource + "/.default"}, - }) - if err != nil { - return "", fmt.Errorf("failed to get access token: %w", err) - } - - return token.Token, nil -} - -// getLatestRevisionName retrieves the latest revision name for a Container App using Azure SDK -func getLatestRevisionName( - ctx context.Context, - cred *azidentity.AzureDeveloperCLICredential, - resourceID, - subscriptionID string) (string, error) { - // Parse resource ID - parsedResource, err := arm.ParseResourceID(resourceID) - if err != nil { - return "", fmt.Errorf("failed to parse resource ID: %w", err) - } - - resourceGroup := parsedResource.ResourceGroupName - appName := parsedResource.Name - - if resourceGroup == "" || appName == "" { - return "", fmt.Errorf("could not parse resource group or app name from resource ID: %s", resourceID) - } - - // Create container apps client - client, err := armappcontainers.NewContainerAppsClient(subscriptionID, cred, nil) - if err != nil { - return "", fmt.Errorf("failed to create container apps client: %w", err) - } - - // Get the container app - containerApp, err := client.Get(ctx, resourceGroup, appName, nil) - if err != nil { - return "", fmt.Errorf("failed to get container app: %w", err) - } - - // Extract latest revision name - if containerApp.Properties == nil || containerApp.Properties.LatestRevisionName == nil { - return "", fmt.Errorf("container app does not have a latest revision name") - } - - latestRevision := *containerApp.Properties.LatestRevisionName - if latestRevision == "" { - return "", fmt.Errorf("latest revision name is empty") - } - - return latestRevision, nil -} - -// registerAgent registers the agent with Microsoft Foundry -func registerAgent(ctx context.Context, uri, token, resourceID, ingressSuffix string) string { - fmt.Println() - fmt.Println("======================================") - fmt.Println("Registering Agent Version") - fmt.Println("======================================") - fmt.Printf("POST URL: %s\n", uri) - - payload := AgentRegistrationPayload{ - Description: "Test agent version description", - Definition: AgentDefinition{ - Kind: "container_app", - ContainerProtocolVersions: []ContainerProtocolVersion{ - {Protocol: "responses", Version: "v1"}, - }, - ContainerAppResourceID: resourceID, - IngressSubdomainSuffix: ingressSuffix, - }, - } - - payloadBytes, _ := json.MarshalIndent(payload, "", " ") - fmt.Println("Request Body:") - fmt.Println(string(payloadBytes)) - - maxRetries := 10 - retryDelay := 60 * time.Second - agentVersion := "" - - for attempt := range maxRetries { - if attempt > 0 { - fmt.Printf("Retry attempt %d of %d...\n", attempt, maxRetries-1) - } - - client := &http.Client{} - req, err := http.NewRequestWithContext(ctx, "POST", uri, bytes.NewBuffer(payloadBytes)) - if err != nil { - fmt.Fprintf(os.Stderr, "Error creating request: %v\n", err) - continue - } - - req.Header.Set("accept", "application/json") - req.Header.Set("authorization", "Bearer "+token) - req.Header.Set("content-type", "application/json") - - //nolint:gosec // URI is user-selected debug target and call is intentional for diagnostics - resp, err := client.Do(req) - if err != nil { - fmt.Fprintf(os.Stderr, "Error making request: %v\n", err) - continue - } - - body, _ := io.ReadAll(resp.Body) - if closeErr := resp.Body.Close(); closeErr != nil { - fmt.Fprintf(os.Stderr, "Warning: failed to close response body: %v\n", closeErr) - } - - fmt.Printf("Response Status: %d\n", resp.StatusCode) - fmt.Println("Response Body:") - fmt.Println(string(body)) - fmt.Println() - - if resp.StatusCode == 200 || resp.StatusCode == 201 { - fmt.Println("✓ Agent registered successfully") - - var agentResp AgentResponse - if err := json.Unmarshal(body, &agentResp); err == nil { - agentVersion = agentResp.Version - fmt.Printf("Agent version: %s\n", agentVersion) - } - break - } else if resp.StatusCode == 500 && attempt < maxRetries-1 { - fmt.Println("Warning: Registration failed with 500 error (permission propagation delay)") - fmt.Printf("Waiting %v before retry...\n", retryDelay) - time.Sleep(retryDelay) - } else { - fmt.Fprintln(os.Stderr, "Error: Registration failed") - if resp.StatusCode != 500 { - break - } - } - } - - return agentVersion -} - -// testUnauthenticatedAccess tests unauthenticated access (should return 401) -func testUnauthenticatedAccess(ctx context.Context, acaEndpoint string) { - fmt.Println() - fmt.Println("======================================") - fmt.Println("Testing Unauthenticated Access") - fmt.Println("======================================") - - uri := acaEndpoint + "/responses" - payload := []byte(`{"input": "test"}`) - - fmt.Printf("POST URL: %s\n", uri) - fmt.Printf("Request Body: %s\n", string(payload)) - - client := &http.Client{} - req, err := http.NewRequestWithContext(ctx, "POST", uri, bytes.NewBuffer(payload)) - if err != nil { - fmt.Fprintf(os.Stderr, "Error creating request: %v\n", err) - return - } - - req.Header.Set("content-type", "application/json") - - //nolint:gosec // endpoint is explicit caller input for connectivity verification - resp, err := client.Do(req) - if err != nil { - fmt.Fprintf(os.Stderr, "Error making request: %v\n", err) - return - } - defer resp.Body.Close() - - body, _ := io.ReadAll(resp.Body) - - fmt.Printf("Response Status: %d\n", resp.StatusCode) - fmt.Printf("Response Body: %s\n", string(body)) - fmt.Println() - - if resp.StatusCode == 401 { - fmt.Println("✓ Authentication enforced (got 401)") - } else { - //nolint:gosec // formatted warning only; no untrusted HTML rendering context - fmt.Fprintf(os.Stderr, "Warning: Expected 401, got %d\n", resp.StatusCode) - } -} - -// testDataPlane tests the agent data plane with authenticated request -func testDataPlane(ctx context.Context, endpoint, token, agentName, agentVersion string) { - fmt.Println() - fmt.Println("======================================") - fmt.Println("Testing Agent Data Plane") - fmt.Println("======================================") - - payload := DataPlanePayload{ - Agent: AgentReference{ - Type: "agent_reference", - Name: agentName, - Version: agentVersion, - }, - Input: "Tell me a joke.", - Stream: false, - } - - payloadBytes, _ := json.MarshalIndent(payload, "", " ") - uri := endpoint + "/openai/responses?api-version=2025-05-15-preview" - - fmt.Printf("POST URL: %s\n", uri) - fmt.Println("Request Body:") - fmt.Println(string(payloadBytes)) - - client := &http.Client{} - req, err := http.NewRequestWithContext(ctx, "POST", uri, bytes.NewBuffer(payloadBytes)) - if err != nil { - fmt.Fprintf(os.Stderr, "Error creating request: %v\n", err) - return - } - - req.Header.Set("accept", "application/json") - req.Header.Set("authorization", "Bearer "+token) - req.Header.Set("content-type", "application/json") - - //nolint:gosec // endpoint is explicit caller input for connectivity verification - resp, err := client.Do(req) - if err != nil { - fmt.Fprintf(os.Stderr, "Error making request: %v\n", err) - return - } - defer resp.Body.Close() - - body, _ := io.ReadAll(resp.Body) - - fmt.Printf("Response Status: %d\n", resp.StatusCode) - fmt.Println("Response Body:") - fmt.Println(string(body)) - fmt.Println() - - if resp.StatusCode == 200 || resp.StatusCode == 201 { - fmt.Println("✓ Agent responded successfully") - fmt.Println("Agent Output:") - - var dpResp DataPlaneResponse - if err := json.Unmarshal(body, &dpResp); err == nil { - fmt.Println(dpResp.Output) - } - } else { - fmt.Fprintln(os.Stderr, "Warning: Data plane call failed") - } -}