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
1 change: 1 addition & 0 deletions .vscode/cspell.misc.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,4 @@ overrides:
- myimage
- azureai
- entra
- flexconsumption
100 changes: 29 additions & 71 deletions cli/azd/pkg/azapi/azure_client_functions_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,16 @@ func Test_GetFunctionAppProperties(t *testing.T) {
Name: to.Ptr("FUNC_APP_NAME"),
Properties: &armappservice.SiteProperties{
DefaultHostName: to.Ptr("FUNC_APP_NAME.azurewebsites.net"),
//nolint:lll
ServerFarmID: to.Ptr(
"/subscriptions/SUBSCRIPTION_ID/resourceGroups/RESOURCE_GROUP_ID/providers/Microsoft.Web/serverfarms/FUNC_APP_PLAN",
),
HostNameSSLStates: []*armappservice.HostNameSSLState{
{
HostType: to.Ptr(armappservice.HostTypeRepository),
Name: to.Ptr("FUNC_APP_NAME.scm.azurewebsites.net"),
},
},
},
},
}
Expand Down Expand Up @@ -81,25 +91,37 @@ func Test_GetFunctionAppProperties(t *testing.T) {
})
}

func Test_DeployFunctionAppUsingZipFile(t *testing.T) {
func Test_DeployFunctionAppUsingZipFileRegular(t *testing.T) {
props := &AzCliFunctionAppProperties{
HostNames: []string{"FUNC_APP_NAME.azurewebsites.net"},
HostNameSslStates: []*armappservice.HostNameSSLState{
{
HostType: to.Ptr(armappservice.HostTypeStandard),
Name: to.Ptr("INVALID"),
},
{
HostType: to.Ptr(armappservice.HostTypeRepository),
Name: to.Ptr("FUNC_APP_NAME_SCM_HOST"),
},
},
}

t.Run("Success", func(t *testing.T) {
ran := false
mockContext := mocks.NewMockContext(context.Background())
azCli := newAzureClientFromMockContext(mockContext)

registerInfoMocks(mockContext, &ran)
registerDeployMocks(mockContext, &ran)
registerPollingMocks(mockContext, &ran)

zipFile := bytes.NewReader([]byte{})

res, err := azCli.DeployFunctionAppUsingZipFile(
res, err := azCli.DeployFunctionAppUsingZipFileRegular(
*mockContext.Context,
"SUBSCRIPTION_ID",
"RESOURCE_GROUP_ID",
props,
"FUNC_APP_NAME",
zipFile,
false,
)

require.NoError(t, err)
Expand All @@ -112,18 +134,16 @@ func Test_DeployFunctionAppUsingZipFile(t *testing.T) {
mockContext := mocks.NewMockContext(context.Background())
azCli := newAzureClientFromMockContext(mockContext)

registerInfoMocks(mockContext, &ran)
registerConflictMocks(mockContext, &ran)

zipFile := bytes.NewReader([]byte{})

res, err := azCli.DeployFunctionAppUsingZipFile(
res, err := azCli.DeployFunctionAppUsingZipFileRegular(
*mockContext.Context,
"SUBSCRIPTION_ID",
"RESOURCE_GROUP_ID",
props,
"FUNC_APP_NAME",
zipFile,
false,
)

require.Nil(t, res)
Expand All @@ -132,68 +152,6 @@ func Test_DeployFunctionAppUsingZipFile(t *testing.T) {
})
}

func registerInfoMocks(mockContext *mocks.MockContext, ran *bool) {
mockContext.HttpClient.When(func(request *http.Request) bool {
//nolint:lll
return request.Method == http.MethodGet &&
strings.Contains(
request.URL.Path,
"subscriptions/SUBSCRIPTION_ID/resourceGroups/RESOURCE_GROUP_ID/providers/Microsoft.Web/sites/FUNC_APP_NAME",
)
}).RespondFn(func(request *http.Request) (*http.Response, error) {
*ran = true
response, _ := mocks.CreateHttpResponseWithBody(
request,
http.StatusOK,
armappservice.WebAppsClientGetResponse{
Site: armappservice.Site{
Properties: &armappservice.SiteProperties{
HostNameSSLStates: []*armappservice.HostNameSSLState{
{
HostType: to.Ptr(armappservice.HostTypeStandard),
Name: to.Ptr("INVALID"),
},
{
HostType: to.Ptr(armappservice.HostTypeRepository),
Name: to.Ptr("FUNC_APP_NAME_SCM_HOST"),
},
},
//nolint:lll
ServerFarmID: to.Ptr(
"/subscriptions/SUBSCRIPTION_ID/resourceGroups/RESOURCE_GROUP_ID/providers/Microsoft.Web/serverfarms/FUNC_APP_PLAN_NAME",
),
},
},
},
)

return response, nil
})

mockContext.HttpClient.When(func(request *http.Request) bool {
//nolint:lll
return request.Method == http.MethodGet &&
strings.Contains(
request.URL.Path,
"/subscriptions/SUBSCRIPTION_ID/resourceGroups/RESOURCE_GROUP_ID/providers/Microsoft.Web/serverfarms/FUNC_APP_PLAN_NAME",
)
}).RespondFn(func(request *http.Request) (*http.Response, error) {
response, _ := mocks.CreateHttpResponseWithBody(
request,
http.StatusOK,
armappservice.PlansClientGetResponse{
Plan: armappservice.Plan{
SKU: &armappservice.SKUDescription{
Name: to.Ptr("Y1"),
Tier: to.Ptr("Dynamic"),
},
},
})

return response, nil
})
}

func registerConflictMocks(mockContext *mocks.MockContext, ran *bool) {
// Original call to start the deployment operation
mockContext.HttpClient.When(func(request *http.Request) bool {
Expand Down
93 changes: 63 additions & 30 deletions cli/azd/pkg/azapi/function_app.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,18 +7,21 @@ import (
"context"
"fmt"
"io"
"strings"

"github.com/Azure/azure-sdk-for-go/sdk/azcore/arm"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/to"
"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/appservice/armappservice/v2"
"github.com/azure/azure-dev/cli/azd/pkg/azsdk"
)

// AzCliFunctionAppProperties contains properties for a Function App.
type AzCliFunctionAppProperties struct {
HostNames []string
HostNames []string
ServerFarmID string
HostNameSslStates []*armappservice.HostNameSSLState
}

// GetFunctionAppProperties retrieves properties for a function app.
func (cli *AzureClient) GetFunctionAppProperties(
ctx context.Context,
subscriptionId string,
Expand All @@ -31,64 +34,84 @@ func (cli *AzureClient) GetFunctionAppProperties(
}

return &AzCliFunctionAppProperties{
HostNames: []string{*webApp.Properties.DefaultHostName},
HostNames: []string{*webApp.Properties.DefaultHostName},
ServerFarmID: *webApp.Properties.ServerFarmID,
HostNameSslStates: webApp.Properties.HostNameSSLStates,
}, nil
}

func (cli *AzureClient) DeployFunctionAppUsingZipFile(
// GetFunctionAppPlan retrieves the app service plan for a function app using pre-fetched properties.
func (cli *AzureClient) GetFunctionAppPlan(
ctx context.Context,
subscriptionId string,
resourceGroup string,
appName string,
deployZipFile io.ReadSeeker,
remoteBuild bool,
) (*string, error) {
app, err := cli.appService(ctx, subscriptionId, resourceGroup, appName)
props *AzCliFunctionAppProperties,
) (*armappservice.Plan, error) {
planId, err := arm.ParseResourceID(props.ServerFarmID)
if err != nil {
return nil, err
}

hostName, err := appServiceRepositoryHost(app, appName)
plansCred, err := cli.credentialProvider.CredentialForSubscription(ctx, planId.SubscriptionID)
if err != nil {
return nil, err
}

planId, err := arm.ParseResourceID(*app.Properties.ServerFarmID)
plansClient, err := armappservice.NewPlansClient(planId.SubscriptionID, plansCred, cli.armClientOptions)
if err != nil {
return nil, err
}

plansCred, err := cli.credentialProvider.CredentialForSubscription(ctx, planId.SubscriptionID)
planResp, err := plansClient.Get(ctx, planId.ResourceGroupName, planId.Name, nil)
if err != nil {
return nil, err
}

plansClient, err := armappservice.NewPlansClient(planId.SubscriptionID, plansCred, cli.armClientOptions)
return &planResp.Plan, nil
}

// DeployFunctionAppUsingZipFileFlexConsumption deploys to a Flex Consumption function app
// using pre-fetched properties.
func (cli *AzureClient) DeployFunctionAppUsingZipFileFlexConsumption(
ctx context.Context,
subscriptionId string,
props *AzCliFunctionAppProperties,
appName string,
deployZipFile io.ReadSeeker,
remoteBuild bool,
) (*string, error) {
hostName, err := functionAppRepositoryHost(props, appName)
if err != nil {
return nil, err
}

plan, err := plansClient.Get(ctx, planId.ResourceGroupName, planId.Name, nil)
cred, err := cli.credentialProvider.CredentialForSubscription(ctx, subscriptionId)
if err != nil {
return nil, err
}

if strings.ToLower(*plan.SKU.Tier) == "flexconsumption" {
cred, err := cli.credentialProvider.CredentialForSubscription(ctx, subscriptionId)
if err != nil {
return nil, err
}
client, err := azsdk.NewFuncAppHostClient(hostName, cred, cli.armClientOptions)
if err != nil {
return nil, fmt.Errorf("creating func app host client: %w", err)
}

client, err := azsdk.NewFuncAppHostClient(hostName, cred, cli.armClientOptions)
if err != nil {
return nil, fmt.Errorf("creating func app host client: %w", err)
}
response, err := client.Publish(ctx, deployZipFile, &azsdk.PublishOptions{RemoteBuild: remoteBuild})
if err != nil {
return nil, fmt.Errorf("publishing zip file: %w", err)
}
return to.Ptr(response.StatusText), nil
}

response, err := client.Publish(ctx, deployZipFile, &azsdk.PublishOptions{RemoteBuild: remoteBuild})
if err != nil {
return nil, fmt.Errorf("publishing zip file: %w", err)
}
return to.Ptr(response.StatusText), nil
// DeployFunctionAppUsingZipFileRegular deploys to a regular (non-Flex Consumption) function app
// using pre-fetched properties.
func (cli *AzureClient) DeployFunctionAppUsingZipFileRegular(
ctx context.Context,
subscriptionId string,
props *AzCliFunctionAppProperties,
appName string,
deployZipFile io.ReadSeeker,
) (*string, error) {
hostName, err := functionAppRepositoryHost(props, appName)
if err != nil {
return nil, err
}

client, err := cli.createZipDeployClient(ctx, subscriptionId, hostName)
Expand All @@ -103,3 +126,13 @@ func (cli *AzureClient) DeployFunctionAppUsingZipFile(

return to.Ptr(response.StatusText), nil
}

// functionAppRepositoryHost finds the SCM host name from function app properties.
func functionAppRepositoryHost(props *AzCliFunctionAppProperties, appName string) (string, error) {
for _, item := range props.HostNameSslStates {
if *item.HostType == armappservice.HostTypeRepository {
return *item.Name, nil
}
}
return "", fmt.Errorf("failed to find host name for function app %s", appName)
}
3 changes: 3 additions & 0 deletions cli/azd/pkg/project/service_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,9 @@ type ServiceConfig struct {
// Condition for deploying the service. When evaluated, the service is only deployed if the value
// is a truthy boolean (1, true, TRUE, True, yes). If not defined, the service is enabled by default.
Condition osutil.ExpandableString `yaml:"condition,omitempty"`
// Whether to build the service remotely. Only applicable to function app services.
// When set to nil (unset), the default behavior based on language is used.
Comment thread
weikanglim marked this conversation as resolved.
RemoteBuild *bool `yaml:"remoteBuild,omitempty"`
Comment thread
rajeshkamal5050 marked this conversation as resolved.

// AdditionalProperties captures any unknown YAML fields for extension support
AdditionalProperties map[string]interface{} `yaml:",inline"`
Expand Down
Loading
Loading