diff --git a/cli/azd/pkg/ai/util.go b/cli/azd/pkg/ai/util.go index d94c2a53e48..dc358d0ca54 100644 --- a/cli/azd/pkg/ai/util.go +++ b/cli/azd/pkg/ai/util.go @@ -4,8 +4,8 @@ import ( "fmt" ) -// AzureAiStudioLink returns a link to the Azure AI Studio for the given tenant, subscription, resource group, and workspace -func AzureAiStudioLink(tenantId string, subscriptionId string, resourceGroup string, workspaceName string) string { +// AiStudioWorkspaceLink returns a link to the Azure AI Studio workspace page +func AiStudioWorkspaceLink(tenantId string, subscriptionId string, resourceGroup string, workspaceName string) string { return fmt.Sprintf( //nolint:lll "https://ai.azure.com/build/overview?tid=%s&wsid=/subscriptions/%s/resourcegroups/%s/providers/Microsoft.MachineLearningServices/workspaces/%s", @@ -15,3 +15,25 @@ func AzureAiStudioLink(tenantId string, subscriptionId string, resourceGroup str workspaceName, ) } + +// AzureAiStudioDeploymentLink returns a link to the Azure AI Studio deployment page +func AiStudioDeploymentLink( + tenantId string, + subscriptionId string, + resourceGroup string, + workspaceName string, + endpointName string, + deploymentName string, +) string { + return fmt.Sprintf( + //nolint:lll + "https://ai.azure.com/projectdeployments/realtime/%s/%s/detail?wsid=/subscriptions/%s/resourceGroups/%s/providers/Microsoft.MachineLearningServices/workspaces/%s&tid=%s&deploymentName=%s", + endpointName, + deploymentName, + subscriptionId, + resourceGroup, + workspaceName, + tenantId, + deploymentName, + ) +} diff --git a/cli/azd/pkg/ai/util_test.go b/cli/azd/pkg/ai/util_test.go index 56d2ca8c94d..386f861a02e 100644 --- a/cli/azd/pkg/ai/util_test.go +++ b/cli/azd/pkg/ai/util_test.go @@ -6,7 +6,7 @@ import ( "github.com/stretchr/testify/require" ) -func Test_AzureAiStudioLink(t *testing.T) { +func Test_AiStudioLink(t *testing.T) { tenantId := "tenantId" subscriptionId := "subscriptionId" resourceGroup := "resourceGroup" @@ -14,7 +14,22 @@ func Test_AzureAiStudioLink(t *testing.T) { //nolint:lll expected := "https://ai.azure.com/build/overview?tid=tenantId&wsid=/subscriptions/subscriptionId/resourcegroups/resourceGroup/providers/Microsoft.MachineLearningServices/workspaces/workspaceName" - actual := AzureAiStudioLink(tenantId, subscriptionId, resourceGroup, workspaceName) + actual := AiStudioWorkspaceLink(tenantId, subscriptionId, resourceGroup, workspaceName) + + require.Equal(t, expected, actual) +} + +func Test_AiStudioDeploymentLink(t *testing.T) { + tenantId := "tenantId" + subscriptionId := "subscriptionId" + resourceGroup := "resourceGroup" + workspaceName := "workspaceName" + endpointName := "endpointName" + deploymentName := "deploymentName" + + //nolint:lll + expected := "https://ai.azure.com/projectdeployments/realtime/endpointName/deploymentName/detail?wsid=/subscriptions/subscriptionId/resourceGroups/resourceGroup/providers/Microsoft.MachineLearningServices/workspaces/workspaceName&tid=tenantId&deploymentName=deploymentName" + actual := AiStudioDeploymentLink(tenantId, subscriptionId, resourceGroup, workspaceName, endpointName, deploymentName) require.Equal(t, expected, actual) } diff --git a/cli/azd/pkg/project/service_models.go b/cli/azd/pkg/project/service_models.go index 457fcc5ded8..9013d180ccc 100644 --- a/cli/azd/pkg/project/service_models.go +++ b/cli/azd/pkg/project/service_models.go @@ -3,6 +3,7 @@ package project import ( "encoding/json" "fmt" + "regexp" "strings" "time" @@ -10,6 +11,9 @@ import ( "github.com/azure/azure-dev/cli/azd/pkg/output/ux" ) +// Some endpoints include a discriminator suffix that should be displayed instead of the default 'Endpoint' label. +var endpointPattern = regexp.MustCompile(`(.+):\s(.+)`) + // ServiceLifecycleEventArgs are the event arguments available when // any service lifecycle event has been triggered type ServiceLifecycleEventArgs struct { @@ -112,7 +116,17 @@ func (spr *ServiceDeployResult) ToString(currentIndentation string) string { builder.WriteString(fmt.Sprintf("%s- No endpoints were found\n", currentIndentation)) } else { for _, endpoint := range spr.Endpoints { - builder.WriteString(fmt.Sprintf("%s- Endpoint: %s\n", currentIndentation, output.WithLinkFormat(endpoint))) + label := "Endpoint" + url := endpoint + + // When the endpoint pattern is matched used the first sub match as the endpoint label. + matches := endpointPattern.FindStringSubmatch(endpoint) + if len(matches) == 3 { + label = matches[1] + url = matches[2] + } + + builder.WriteString(fmt.Sprintf("%s- %s: %s\n", currentIndentation, label, output.WithLinkFormat(url))) } } diff --git a/cli/azd/pkg/project/service_target_ai_endpoint.go b/cli/azd/pkg/project/service_target_ai_endpoint.go index fdcbea3161f..9a51e66aabb 100644 --- a/cli/azd/pkg/project/service_target_ai_endpoint.go +++ b/cli/azd/pkg/project/service_target_ai_endpoint.go @@ -217,21 +217,64 @@ func (m *aiEndpointTarget) Endpoints( return nil, fmt.Errorf("failed initializing AI project: %w", err) } + tenantId, has := m.env.LookupEnv(environment.TenantIdEnvVarName) + if !has { + return nil, fmt.Errorf( + "tenant ID not found. Ensure %s has been set in the environment.", + environment.TenantIdEnvVarName, + ) + } + workspaceScope, err := m.getWorkspaceScope(serviceConfig, targetResource) if err != nil { return nil, err } + workspaceLink := ai.AiStudioWorkspaceLink( + tenantId, + workspaceScope.SubscriptionId(), + workspaceScope.ResourceGroup(), + workspaceScope.Workspace(), + ) + + endpoints := []string{ + fmt.Sprintf("Workspace: %s", workspaceLink), + } + endpointName := filepath.Base(targetResource.ResourceName()) onlineEndpoint, err := m.aiHelper.GetEndpoint(ctx, workspaceScope, endpointName) if err != nil { return nil, err } - return []string{ + var deploymentName string + for key, value := range onlineEndpoint.Properties.Traffic { + if *value == 100 { + deploymentName = key + break + } + } + + if deploymentName != "" { + deploymentLink := ai.AiStudioDeploymentLink( + tenantId, + workspaceScope.SubscriptionId(), + workspaceScope.ResourceGroup(), + workspaceScope.Workspace(), + endpointName, + deploymentName, + ) + + endpoints = append(endpoints, fmt.Sprintf("Deployment: %s", deploymentLink)) + } + + endpoints = append( + endpoints, fmt.Sprintf("Scoring: %s", *onlineEndpoint.Properties.ScoringURI), fmt.Sprintf("Swagger: %s", *onlineEndpoint.Properties.SwaggerURI), - }, nil + ) + + return endpoints, nil } // getWorkspaceScope returns the scope for the workspace diff --git a/cli/azd/pkg/project/service_target_ai_endpoint_test.go b/cli/azd/pkg/project/service_target_ai_endpoint_test.go index 1aadf63801c..e11ac928b1a 100644 --- a/cli/azd/pkg/project/service_target_ai_endpoint_test.go +++ b/cli/azd/pkg/project/service_target_ai_endpoint_test.go @@ -24,6 +24,7 @@ func Test_MlEndpointTarget_Deploy(t *testing.T) { mockContext.Clock.Set(time.Now()) env := environment.NewWithValues("test", map[string]string{ AiProjectNameEnvVarName: "AI_WORKSPACE", + environment.TenantIdEnvVarName: "TENANT_ID", environment.SubscriptionIdEnvVarName: "SUBSCRIPTION_ID", environment.ResourceGroupEnvVarName: "RESOURCE_GROUP", }) @@ -85,6 +86,9 @@ func Test_MlEndpointTarget_Deploy(t *testing.T) { Properties: &armmachinelearning.OnlineEndpointProperties{ ScoringURI: convert.RefOf("https://SCRORING_URI"), SwaggerURI: convert.RefOf("https://SWAGGER_URI"), + Traffic: map[string]*int32{ + deploymentName: convert.RefOf(int32(100)), + }, }, } @@ -131,7 +135,7 @@ func Test_MlEndpointTarget_Deploy(t *testing.T) { require.NoError(t, err) require.NotNil(t, deployResult) require.IsType(t, &AiEndpointDeploymentResult{}, deployResult.Details) - require.Len(t, deployResult.Endpoints, 2) + require.Len(t, deployResult.Endpoints, 4) deploymentDetails := deployResult.Details.(*AiEndpointDeploymentResult)