Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion cli/azd/internal/cmd/util.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,12 @@ func getResourceGroupFollowUp(
}

subscriptionId := env.GetSubscriptionId()
if resourceGroupName, err := resourceManager.GetResourceGroupName(ctx, subscriptionId, projectConfig); err == nil {
resourceGroupName, err := resourceManager.GetResourceGroupName(
ctx,
subscriptionId,
projectConfig.ResourceGroupName,
)
if err == nil {
suffix := ":\n" + azurePortalLink(portalUrlBase, subscriptionId, resourceGroupName)

if v, err := strconv.ParseBool(os.Getenv("AZD_DEMO_MODE")); err == nil && v {
Expand Down
5 changes: 5 additions & 0 deletions cli/azd/pkg/osutil/expandable_string.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,11 @@ type ExpandableString struct {
template string
}

// Empty returns true if the template is empty.
func (e ExpandableString) Empty() bool {
return e.template == ""
}

// Envsubst evaluates the template, substituting values as [envsubst.Eval] would.
func (e ExpandableString) Envsubst(mapping func(string) string) (string, error) {
return envsubst.Eval(e.template, mapping)
Expand Down
12 changes: 12 additions & 0 deletions cli/azd/pkg/osutil/expandable_string_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,15 @@ func TestExpandableStringYaml(t *testing.T) {

assert.Equal(t, "${foo}\n", string(marshalled))
}

func TestExpandableString_Empty(t *testing.T) {
t.Run("Empty", func(t *testing.T) {
e := NewExpandableString("")
assert.True(t, e.Empty())
})

t.Run("NonEmpty", func(t *testing.T) {
e := NewExpandableString("${ENV_VAR}")
assert.False(t, e.Empty())
})
}
18 changes: 14 additions & 4 deletions cli/azd/pkg/project/resource_manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,19 @@ import (
"github.com/azure/azure-dev/cli/azd/pkg/azureutil"
"github.com/azure/azure-dev/cli/azd/pkg/environment"
"github.com/azure/azure-dev/cli/azd/pkg/infra"
"github.com/azure/azure-dev/cli/azd/pkg/osutil"
"github.com/azure/azure-dev/cli/azd/pkg/tools/azcli"
)

// ResourceManager provides a layer to query for Azure resource for azd project and services
// This would typically be used during deployment when azd need to deploy applications
// to the Azure resource hosting the application
type ResourceManager interface {
GetResourceGroupName(ctx context.Context, subscriptionId string, projectConfig *ProjectConfig) (string, error)
GetResourceGroupName(
ctx context.Context,
subscriptionId string,
resourceGroupTemplate osutil.ExpandableString,
) (string, error)
GetServiceResources(
ctx context.Context,
subscriptionId string,
Expand Down Expand Up @@ -69,9 +74,9 @@ func NewResourceManager(
func (rm *resourceManager) GetResourceGroupName(
ctx context.Context,
subscriptionId string,
projectConfig *ProjectConfig,
resourceGroupTemplate osutil.ExpandableString,
) (string, error) {
name, err := projectConfig.ResourceGroupName.Envsubst(rm.env.Getenv)
name, err := resourceGroupTemplate.Envsubst(rm.env.Getenv)
if err != nil {
return "", err
}
Expand Down Expand Up @@ -197,7 +202,12 @@ func (rm *resourceManager) GetTargetResource(
subscriptionId string,
serviceConfig *ServiceConfig,
) (*environment.TargetResource, error) {
resourceGroupName, err := rm.GetResourceGroupName(ctx, subscriptionId, serviceConfig.Project)
resourceGroupTemplate := serviceConfig.ResourceGroupName
if resourceGroupTemplate.Empty() {
resourceGroupTemplate = serviceConfig.Project.ResourceGroupName
}

resourceGroupName, err := rm.GetResourceGroupName(ctx, subscriptionId, resourceGroupTemplate)
if err != nil {
return nil, err
}
Expand Down
177 changes: 177 additions & 0 deletions cli/azd/pkg/project/resource_manager_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
package project

import (
"context"
"fmt"
"net/http"
"strings"
"testing"

"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources"
"github.com/azure/azure-dev/cli/azd/pkg/convert"
"github.com/azure/azure-dev/cli/azd/pkg/environment"
"github.com/azure/azure-dev/cli/azd/pkg/osutil"
"github.com/azure/azure-dev/cli/azd/test/mocks"
"github.com/azure/azure-dev/cli/azd/test/mocks/mockazcli"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
)

type testInitFunc func(*mocks.MockContext)

// Validates that the resource group is correctly resolved from different configuration
// 1. Resource group referenced in service config
// 2. Resource group referenced in project
// 3. Resource group referenced in environment variable
// 4. Resource group tagged with azd-env-name
func Test_ResourceManager_GetTargetResource(t *testing.T) {
taggedResourceGroup := &armresources.ResourceGroup{
ID: convert.RefOf(fmt.Sprintf(
"/subscriptions/%s/resourceGroups/%s",
"SUBSCRIPTION_id",
"TAGGED_RESOURCE_GROUP",
)),
Name: convert.RefOf("TAGGED_RESOURCE_GROUP"),
Type: convert.RefOf("Microsoft.Resources/resourceGroups"),
Location: convert.RefOf("eastus2"),
}

fromProjectConfig := createTestServiceConfig("./src/api", ContainerAppTarget, ServiceLanguageJavaScript)
fromProjectConfig.Project.ResourceGroupName = osutil.NewExpandableString("PROJECT_RESOURCE_GROUP")

fromServiceConfig := createTestServiceConfig("./src/api", ContainerAppTarget, ServiceLanguageJavaScript)
fromServiceConfig.Project.ResourceGroupName = osutil.NewExpandableString("PROJECT_RESOURCE_GROUP")
fromServiceConfig.ResourceGroupName = osutil.NewExpandableString("SERVICE_RESOURCE_GROUP")

tests := []struct {
name string
env *environment.Environment
serviceConfig *ServiceConfig
expectedResourceGroup string
init testInitFunc
}{
{
name: "ResourceGroupFromTag",
init: func(mockContext *mocks.MockContext) {
setupGetResourceGroupMock(mockContext, taggedResourceGroup)
},
env: environment.NewWithValues("test", map[string]string{
environment.SubscriptionIdEnvVarName: "SUBSCRIPTION_ID",
}),
serviceConfig: createTestServiceConfig("./src/api", ContainerAppTarget, ServiceLanguageJavaScript),
expectedResourceGroup: "TAGGED_RESOURCE_GROUP",
},
{
name: "ResourceGroupFromEnvVar",
env: environment.NewWithValues("test", map[string]string{
environment.ResourceGroupEnvVarName: "ENV_VAR_RESOURCE_GROUP",
environment.SubscriptionIdEnvVarName: "SUBSCRIPTION_ID",
}),
serviceConfig: createTestServiceConfig("./src/api", ContainerAppTarget, ServiceLanguageJavaScript),
expectedResourceGroup: "ENV_VAR_RESOURCE_GROUP",
},
{
name: "ResourceGroupFromProject",
env: environment.NewWithValues("test", map[string]string{
environment.ResourceGroupEnvVarName: "ENV_VAR_RESOURCE_GROUP",
environment.SubscriptionIdEnvVarName: "SUBSCRIPTION_ID",
}),
serviceConfig: fromProjectConfig,
expectedResourceGroup: "PROJECT_RESOURCE_GROUP",
},
{
name: "ResourceGroupFromService",
env: environment.NewWithValues("test", map[string]string{
environment.ResourceGroupEnvVarName: "ENV_VAR_RESOURCE_GROUP",
environment.SubscriptionIdEnvVarName: "SUBSCRIPTION_ID",
}),
serviceConfig: fromServiceConfig,
expectedResourceGroup: "SERVICE_RESOURCE_GROUP",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
mockContext := mocks.NewMockContext(context.Background())
azCli := mockazcli.NewAzCliFromMockContext(mockContext)
mockDeploymentOperations := &mockDeploymentOperations{}

if tt.init != nil {
tt.init(mockContext)
}

expectedResource := &armresources.GenericResourceExpanded{
ID: convert.RefOf("RESOURCE_ID"),
Name: convert.RefOf("RESOURCE_NAME"),
Type: convert.RefOf("Microsoft.Web/sites"),
Location: convert.RefOf("eastus2"),
}

setupGetResourceMock(mockContext, expectedResource)

resourceManager := NewResourceManager(tt.env, azCli, mockDeploymentOperations)
targetResource, err := resourceManager.GetTargetResource(
*mockContext.Context,
tt.env.GetSubscriptionId(),
tt.serviceConfig,
)

require.NoError(t, err)
require.NotNil(t, targetResource)
require.Equal(t, tt.expectedResourceGroup, targetResource.ResourceGroupName())
require.Equal(t, "RESOURCE_NAME", targetResource.ResourceName())
require.Equal(t, tt.env.GetSubscriptionId(), targetResource.SubscriptionId())
})
}
}

type mockDeploymentOperations struct {
mock.Mock
}

func setupGetResourceGroupMock(mockContext *mocks.MockContext, resourceGroup *armresources.ResourceGroup) {
mockContext.HttpClient.When(func(request *http.Request) bool {
return strings.HasSuffix(request.URL.Path, "/resourcegroups") && strings.Contains(request.URL.RawQuery, "filter=")
}).RespondFn(func(request *http.Request) (*http.Response, error) {
result := armresources.ResourceGroupListResult{
Value: []*armresources.ResourceGroup{
resourceGroup,
},
}

return mocks.CreateHttpResponseWithBody(request, http.StatusOK, result)
})
}

func setupGetResourceMock(mockContext *mocks.MockContext, resource *armresources.GenericResourceExpanded) {
mockContext.HttpClient.When(func(request *http.Request) bool {
return strings.HasSuffix(request.URL.Path, "/resources") && strings.Contains(request.URL.RawQuery, "filter=")
}).RespondFn(func(request *http.Request) (*http.Response, error) {
result := armresources.ResourceListResult{
Value: []*armresources.GenericResourceExpanded{
resource,
},
}

return mocks.CreateHttpResponseWithBody(request, http.StatusOK, result)
})
}

func (m *mockDeploymentOperations) ListSubscriptionDeploymentOperations(
ctx context.Context,
subscriptionId string,
deploymentName string,
) ([]*armresources.DeploymentOperation, error) {
args := m.Called(ctx, subscriptionId, deploymentName)
return args.Get(0).([]*armresources.DeploymentOperation), args.Error(1)
}

func (m *mockDeploymentOperations) ListResourceGroupDeploymentOperations(
ctx context.Context,
subscriptionId string,
resourceGroupName string,
deploymentName string,
) ([]*armresources.DeploymentOperation, error) {
args := m.Called(ctx, subscriptionId, resourceGroupName, deploymentName)
return args.Get(0).([]*armresources.DeploymentOperation), args.Error(1)
}
2 changes: 2 additions & 0 deletions cli/azd/pkg/project/service_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ type ServiceConfig struct {
Project *ProjectConfig `yaml:"-"`
// The friendly name/key of the project from the azure.yaml file
Name string `yaml:"-"`
// The azure resource group to deploy the service to
ResourceGroupName osutil.ExpandableString `yaml:"resourceGroup,omitempty"`
// The name used to override the default azure resource name
ResourceName osutil.ExpandableString `yaml:"resourceName,omitempty"`
// The relative path to the project folder from the project root
Expand Down
13 changes: 12 additions & 1 deletion cli/azd/pkg/project/service_manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -474,8 +474,19 @@ func (sm *serviceManager) Deploy(
containerEnvName = parts[len(parts)-1]
}

// Get any explicitly configured resource group name
// 1. Service level override
// 2. Project level override
resourceGroupNameTemplate := serviceConfig.ResourceGroupName
if resourceGroupNameTemplate.Empty() {
resourceGroupNameTemplate = serviceConfig.Project.ResourceGroupName
}

resourceGroupName, err := sm.resourceManager.GetResourceGroupName(
ctx, sm.env.GetSubscriptionId(), serviceConfig.Project)
ctx,
sm.env.GetSubscriptionId(),
resourceGroupNameTemplate,
)
if err != nil {
task.SetError(fmt.Errorf("getting resource group name: %w", err))
return
Expand Down
4 changes: 2 additions & 2 deletions cli/azd/pkg/project/service_target_aks_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -925,9 +925,9 @@ type MockResourceManager struct {
func (m *MockResourceManager) GetResourceGroupName(
ctx context.Context,
subscriptionId string,
projectConfig *ProjectConfig,
resourceGroupTemplate osutil.ExpandableString,
) (string, error) {
args := m.Called(ctx, subscriptionId, projectConfig)
args := m.Called(ctx, subscriptionId, resourceGroupTemplate)
return args.String(0), args.Error(1)
}

Expand Down
5 changes: 5 additions & 0 deletions schemas/alpha/azure.yaml.json
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,11 @@
"host"
],
"properties": {
"resourceGroup": {
"type": "string",
"title": "Name of the Azure resource group that contains the resource",
"description": "By default, the CLI will discover the Azure resource within the default resource group. When specified, the CLI will instead find the Azure resource within the specified resource group. Supports environment variable substitution."
},
"resourceName": {
"type": "string",
"title": "Name of the Azure resource that implements the service",
Expand Down