Skip to content
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

Implement AppServiceFeature #47269

Merged
merged 5 commits into from
Nov 22, 2024
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
3 changes: 2 additions & 1 deletion eng/Packages.Data.props
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@
<PackageReference Update="Azure.Storage.Blobs" Version="12.21.1" />
<PackageReference Update="Azure.Storage.Queues" Version="12.19.1" />
<PackageReference Update="Azure.Storage.Files.Shares" Version="12.19.1" />
<PackageReference Update="Azure.AI.Inference" Version="1.0.0-beta.2" />
<PackageReference Update="Azure.AI.Inference" Version="1.0.0-beta.2" />
<PackageReference Update="Azure.AI.OpenAI" Version="2.0.0" />
<PackageReference Update="Azure.ResourceManager" Version="1.13.0" />
<PackageReference Update="Azure.ResourceManager.AppConfiguration" Version="1.3.2" />
Expand Down Expand Up @@ -158,6 +158,7 @@
<PackageReference Update="Azure.Provisioning.KeyVault" Version="1.0.0" />
<PackageReference Update="Azure.Provisioning.ServiceBus" Version="1.0.0" />
<PackageReference Update="Azure.Provisioning.Storage" Version="1.0.0" />
<PackageReference Update="Azure.Provisioning.AppService" Version="1.0.0" />
<PackageReference Update="Microsoft.Bcl.Numerics" Version="8.0.0" />

<!-- Other approved packages -->
Expand Down
33 changes: 16 additions & 17 deletions sdk/cloudmachine/Azure.CloudMachine/src/CloudMachineWorkspace.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,7 @@ namespace Azure.CloudMachine;
/// </summary>
public class CloudMachineWorkspace : ClientWorkspace
{
private TokenCredential Credential { get; } = new ChainedTokenCredential(
new AzureCliCredential(),
new AzureDeveloperCliCredential()
);
private TokenCredential Credential { get; }

/// <summary>
/// The cloud machine ID.
Expand All @@ -42,20 +39,21 @@ public CloudMachineWorkspace(TokenCredential credential = default, IConfiguratio
{
Credential = credential;
}

string cmid;
if (configuration == default)
{
cmid = ReadOrCreateCmid();
}
else
{
cmid = configuration["CloudMachine:ID"];
if (cmid == null)
throw new Exception("CloudMachine:ID configuration value missing");
// This environment variable is set by the CloudMachine App Service feature during provisioning.
Credential = Environment.GetEnvironmentVariable("CLOUDMACHINE_MANAGED_IDENTITY_CLIENT_ID") switch
christothes marked this conversation as resolved.
Show resolved Hide resolved
{
string clientId when !string.IsNullOrEmpty(clientId) => new ManagedIdentityCredential(clientId),
_ => new ChainedTokenCredential(new AzureCliCredential(), new AzureDeveloperCliCredential())
};
}

Id = cmid!;
Id = configuration switch
{
null => ReadOrCreateCmid(),
_ => configuration["CloudMachine:ID"] ?? throw new Exception("CloudMachine:ID configuration value missing")
};
}

/// <summary>
Expand All @@ -69,7 +67,8 @@ public CloudMachineWorkspace(TokenCredential credential = default, IConfiguratio
public override ClientConnectionOptions GetConnectionOptions(Type clientType, string instanceId)
{
string clientId = clientType.FullName;
if (instanceId != null && instanceId.StartsWith("$")) clientId = $"{clientType.FullName}{instanceId}";
if (instanceId != null && instanceId.StartsWith("$"))
clientId = $"{clientType.FullName}{instanceId}";

switch (clientId)
{
Expand All @@ -78,13 +77,13 @@ public override ClientConnectionOptions GetConnectionOptions(Type clientType, st
case "Azure.Messaging.ServiceBus.ServiceBusClient":
return new ClientConnectionOptions(new($"https://{Id}.servicebus.windows.net"), Credential);
case "Azure.Messaging.ServiceBus.ServiceBusSender":
return new ClientConnectionOptions(instanceId?? "cm_servicebus_default_topic");
return new ClientConnectionOptions(instanceId ?? "cm_servicebus_default_topic");
case "Azure.Messaging.ServiceBus.ServiceBusProcessor":
return new ClientConnectionOptions("cm_servicebus_default_topic/cm_servicebus_subscription_default");
case "Azure.Messaging.ServiceBus.ServiceBusProcessor$private":
return new ClientConnectionOptions("cm_servicebus_topic_private/cm_servicebus_subscription_private");
case "Azure.Storage.Blobs.BlobContainerClient":
return new ClientConnectionOptions(new($"https://{Id}.blob.core.windows.net/{instanceId??"default"}"), Credential);
return new ClientConnectionOptions(new($"https://{Id}.blob.core.windows.net/{instanceId ?? "default"}"), Credential);
case "Azure.AI.OpenAI.AzureOpenAIClient":
return new ClientConnectionOptions(new($"https://{Id}.openai.azure.com"), Credential);
case "OpenAI.Chat.ChatClient":
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,15 @@ public FeatureCollection() { }
public System.Collections.Generic.IEnumerable<T> FindAll<T>() where T : Azure.Provisioning.CloudMachine.CloudMachineFeature { throw null; }
}
}
namespace Azure.CloudMachine.AppService
{
public partial class AppServiceFeature : Azure.Provisioning.CloudMachine.CloudMachineFeature
{
public AppServiceFeature(Azure.Provisioning.AppService.AppServiceSkuDescription? sku = null) { }
public Azure.Provisioning.AppService.AppServiceSkuDescription Sku { get { throw null; } set { } }
protected override Azure.Provisioning.Primitives.ProvisionableResource EmitCore(Azure.CloudMachine.CloudMachineInfrastructure infrastructure) { throw null; }
}
}
namespace Azure.CloudMachine.KeyVault
{
public partial class KeyVaultFeature : Azure.Provisioning.CloudMachine.CloudMachineFeature
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,15 @@ public FeatureCollection() { }
public System.Collections.Generic.IEnumerable<T> FindAll<T>() where T : Azure.Provisioning.CloudMachine.CloudMachineFeature { throw null; }
}
}
namespace Azure.CloudMachine.AppService
{
public partial class AppServiceFeature : Azure.Provisioning.CloudMachine.CloudMachineFeature
{
public AppServiceFeature(Azure.Provisioning.AppService.AppServiceSkuDescription? sku = null) { }
public Azure.Provisioning.AppService.AppServiceSkuDescription Sku { get { throw null; } set { } }
protected override Azure.Provisioning.Primitives.ProvisionableResource EmitCore(Azure.CloudMachine.CloudMachineInfrastructure infrastructure) { throw null; }
}
}
namespace Azure.CloudMachine.KeyVault
{
public partial class KeyVaultFeature : Azure.Provisioning.CloudMachine.CloudMachineFeature
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
<PackageReference Include="Azure.Provisioning.CognitiveServices" />
<PackageReference Include="Azure.Provisioning.ServiceBus" />
<PackageReference Include="Azure.Provisioning.EventGrid" />
<PackageReference Include="Azure.Provisioning.AppService" />
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using Azure.Provisioning.CloudMachine;
using Azure.Provisioning.Expressions;
using Azure.Provisioning.AppService;
using Azure.Provisioning.Primitives;
using Azure.Provisioning.Resources;

namespace Azure.CloudMachine.AppService;

public class AppServiceFeature : CloudMachineFeature
{
public AppServiceSkuDescription Sku { get; set; }

public AppServiceFeature(AppServiceSkuDescription? sku = default)
{
if (sku == default)
{
sku = new AppServiceSkuDescription { Tier = "Free", Name = "F1" };
}
Sku = sku;
}

protected override ProvisionableResource EmitCore(CloudMachineInfrastructure infrastructure)
{
//Add a App Service to the CloudMachine infrastructure.
AppServicePlan hostingPlan = new("cm_hosting_plan")
{
Name = infrastructure.Id,
Sku = Sku,
Kind = "app"
};
infrastructure.AddResource(hostingPlan);

WebSite appService = new("cm_website")
{
Name = infrastructure.Id,
Kind = "app",
Tags = { { "azd-service-name", infrastructure.Id } },
Identity = new()
{
ManagedServiceIdentityType = ManagedServiceIdentityType.UserAssigned,
UserAssignedIdentities = { { BicepFunction.Interpolate($"{infrastructure.Identity.Id}").Compile().ToString(), new UserAssignedIdentityDetails() } }
},
AppServicePlanId = hostingPlan.Id,
IsHttpsOnly = true,
IsEnabled = true,
SiteConfig = new()
{
IsHttp20Enabled = true,
MinTlsVersion = AppServiceSupportedTlsVersion.Tls1_2,
IsWebSocketsEnabled = true,
AppSettings = new()
{
// This is used by the CloudMachineWorkspace to detect that it is running in a deployed App Service.
// The ClientId is used to create a ManagedIdentityCredential so that it wires up to our CloudMachine user-assigned identity.
new AppServiceNameValuePair
{
Name = "CLOUDMACHINE_MANAGED_IDENTITY_CLIENT_ID",
Value = infrastructure.Identity.ClientId
},
}
}
};
infrastructure.AddResource(appService);

return appService;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,13 @@ protected override ProvisionableResource EmitCore(CloudMachineInfrastructure clo
cloudMachine.PrincipalIdParameter)
);

cloudMachine.AddResource(cloudMachine.CreateRoleAssignment(
cognitiveServices,
cognitiveServices.Id,
CognitiveServicesBuiltInRole.CognitiveServicesOpenAIContributor,
cloudMachine.Identity)
);

Emitted = cognitiveServices;

OpenAIModel? previous = null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
using System.Collections.Generic;
using Azure.Provisioning;
using Azure.Provisioning.CloudMachine;
using Azure.Core;
using System.Runtime.CompilerServices;

namespace Azure.CloudMachine;

Expand Down Expand Up @@ -221,7 +223,8 @@ public void AddFeature(CloudMachineFeature feature)
public void AddEndpoints<T>()
{
Type endpointsType = typeof(T);
if (!endpointsType.IsInterface) throw new InvalidOperationException("Endpoints type must be an interface.");
if (!endpointsType.IsInterface)
throw new InvalidOperationException("Endpoints type must be an interface.");
Endpoints.Add(endpointsType);
}

Expand All @@ -242,33 +245,35 @@ public ProvisioningPlan Build(ProvisioningBuildOptions? context = null)
//Add(PrincipalTypeParameter);
//Add(PrincipalNameParameter);

var storageBlobDataContributor = StorageBuiltInRole.StorageBlobDataContributor;
var storageTableDataContributor = StorageBuiltInRole.StorageTableDataContributor;
var azureServiceBusDataSender = ServiceBusBuiltInRole.AzureServiceBusDataSender;
var azureServiceBusDataOwner = ServiceBusBuiltInRole.AzureServiceBusDataOwner;

_infrastructure.Add(Identity);
_infrastructure.Add(_storage);
_infrastructure.Add(_storage.CreateRoleAssignment(StorageBuiltInRole.StorageBlobDataContributor, RoleManagementPrincipalType.User, PrincipalIdParameter));
_infrastructure.Add(_storage.CreateRoleAssignment(StorageBuiltInRole.StorageTableDataContributor, RoleManagementPrincipalType.User, PrincipalIdParameter));
_infrastructure.Add(_storage.CreateRoleAssignment(storageBlobDataContributor, RoleManagementPrincipalType.User, PrincipalIdParameter));
_infrastructure.Add(CreateRoleAssignment(_storage, _storage.Id, storageBlobDataContributor, Identity));
_infrastructure.Add(_storage.CreateRoleAssignment(storageTableDataContributor, RoleManagementPrincipalType.User, PrincipalIdParameter));
_infrastructure.Add(CreateRoleAssignment(_storage, _storage.Id, storageTableDataContributor, Identity));
_infrastructure.Add(_container);
_infrastructure.Add(_blobs);
_infrastructure.Add(_serviceBusNamespace);
_infrastructure.Add(_serviceBusNamespace.CreateRoleAssignment(ServiceBusBuiltInRole.AzureServiceBusDataOwner, RoleManagementPrincipalType.User, PrincipalIdParameter));
_infrastructure.Add(_serviceBusNamespace.CreateRoleAssignment(azureServiceBusDataOwner, RoleManagementPrincipalType.User, PrincipalIdParameter));
_infrastructure.Add(CreateRoleAssignment(_serviceBusNamespace,_serviceBusNamespace.Id, azureServiceBusDataOwner, Identity));
_infrastructure.Add(_serviceBusNamespaceAuthorizationRule);
_infrastructure.Add(_serviceBusTopic_private);
_infrastructure.Add(_serviceBusTopic_default);
_infrastructure.Add(_serviceBusSubscription_private);
_infrastructure.Add(_serviceBusSubscription_default);

// This is necessary until SystemTopic adds an AssignRole method.
var role = ServiceBusBuiltInRole.AzureServiceBusDataSender;
RoleAssignment roleAssignment = new RoleAssignment("cm_servicebus_role");
roleAssignment.Name = BicepFunction.CreateGuid(_serviceBusNamespace.Id, Identity.Id, BicepFunction.GetSubscriptionResourceId("Microsoft.Authorization/roleDefinitions", role.ToString()));
roleAssignment.Scope = new IdentifierExpression(_serviceBusNamespace.BicepIdentifier);
roleAssignment.PrincipalType = RoleManagementPrincipalType.ServicePrincipal;
roleAssignment.RoleDefinitionId = BicepFunction.GetSubscriptionResourceId("Microsoft.Authorization/roleDefinitions", role.ToString());
roleAssignment.PrincipalId = Identity.PrincipalId;
RoleAssignment roleAssignment = CreateRoleAssignment(_serviceBusNamespace, _serviceBusNamespace.Id, azureServiceBusDataSender, Identity);
_infrastructure.Add(roleAssignment);

CreateRoleAssignment(_serviceBusNamespace, _serviceBusNamespace.Id, azureServiceBusDataSender, Identity);
// the role assignment must exist before the system topic event subscription is created.
_eventGridSubscription_blobs.DependsOn.Add(roleAssignment);
_infrastructure.Add(_eventGridSubscription_blobs);

_infrastructure.Add(_eventGridTopic_blobs);

// Placeholders for now.
Expand All @@ -283,4 +288,21 @@ public ProvisioningPlan Build(ProvisioningBuildOptions? context = null)

return _infrastructure.Build(context);
}

// Temporary until the bug is fixed in the CDK generator which uses the PrincipalId instead of the Id in BicepFunction.CreateGuid.
internal RoleAssignment CreateRoleAssignment(ProvisionableResource resource, BicepValue<ResourceIdentifier> Id, object role, UserAssignedIdentity identity)
{
if (role is null) throw new ArgumentException("Role must not be null.", nameof(role));
var method = role.GetType().GetMethod("GetBuiltInRoleName", System.Reflection.BindingFlags.Static | System.Reflection.BindingFlags.Public);
string roleName = (string)method!.Invoke(null, [role])!;

return new($"{resource.BicepIdentifier}_{identity.BicepIdentifier}_{roleName}")
{
Name = BicepFunction.CreateGuid(Id, identity.Id, BicepFunction.GetSubscriptionResourceId("Microsoft.Authorization/roleDefinitions", role!.ToString()!)),
Scope = new IdentifierExpression(resource.BicepIdentifier),
PrincipalType = RoleManagementPrincipalType.ServicePrincipal,
RoleDefinitionId = BicepFunction.GetSubscriptionResourceId("Microsoft.Authorization/roleDefinitions", role.ToString()!),
PrincipalId = identity.PrincipalId
};
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

#nullable enable

using Azure.CloudMachine.AppService;
using Azure.CloudMachine.KeyVault;
using Azure.CloudMachine.OpenAI;
using NUnit.Framework;
Expand All @@ -19,6 +20,7 @@ public void GenerateBicep()
infrastructure.AddFeature(new KeyVaultFeature());
infrastructure.AddFeature(new OpenAIModel("gpt-35-turbo", "0125"));
infrastructure.AddFeature(new OpenAIModel("text-embedding-ada-002", "2", AIModelKind.Embedding));
infrastructure.AddFeature(new AppServiceFeature());
}, exitProcessIfHandled:false);
}

Expand Down