Skip to content

Commit

Permalink
Merge pull request #8 from Hekku2/feat/existing-analyzer
Browse files Browse the repository at this point in the history
Add support for using existing Cognitive Services Account
  • Loading branch information
Hekku2 authored Jun 30, 2024
2 parents 08cb27b + 098ca56 commit beeaf7b
Show file tree
Hide file tree
Showing 17 changed files with 267 additions and 70 deletions.
14 changes: 13 additions & 1 deletion Create-Environment.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@
#>
param(
[Parameter()][string]$SettingsFile = 'developer-settings.json',
[Parameter()][switch]$NoDiscord
[Parameter()][switch]$NoDiscord,
[Parameter()][switch]$DeleteOldRoleAssingments
)
$ErrorActionPreference = "Stop"
Set-StrictMode -Version Latest
Expand All @@ -35,6 +36,13 @@ else {
Write-Host 'Creating resource group if it doesn''t exist...'
New-AzResourceGroup -Name $settingsJson.ResourceGroup -Location $settingsJson.Location -Force

if ($DeleteOldRoleAssingments -and -not [string]::IsNullOrEmpty($settingsJson.ExistingCognitiveServicesAccountName)) {
Write-Host 'Removing unknown ''Cognitive Services User'' role assignments...'

$rg = $settingsJson.ExistingCognitiveServicesResourceGroup ?? $settingsJson.ResourceGroup
Remove-UnknownRoleAssingments -ResourceName $settingsJson.ExistingCognitiveServicesAccountName -ResourceGroupName $rg
}

Write-Host 'Deploying template...'
$parameters = @{
baseName = $settingsJson.ApplicationName
Expand All @@ -45,6 +53,10 @@ $parameters = @{
channelId = $settingsJson.DiscordChannelId
guildId = $settingsJson.DiscordGuildId
}
cognitiveService = @{
existingServiceName = $settingsJson.ExistingCognitiveServicesAccountName
existingServiceResourceGroup = $settingsJson.ExistingCognitiveServicesResourceGroup
}
}
New-AzResourceGroupDeployment `
-ResourceGroupName $settingsJson.ResourceGroup `
Expand Down
16 changes: 9 additions & 7 deletions developer-settings-sample.json
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
{
"ResourceGroup": "",
"ApplicationName": "",
"Location": "North Europe",
"DiscordToken": "",
"DiscordGuildId": 0,
"DiscordChannelId": 0,
"Tags": []
"ResourceGroup": "",
"ApplicationName": "",
"Location": "North Europe",
"DiscordToken": "",
"DiscordGuildId": 0,
"DiscordChannelId": 0,
"ExistingCognitiveServicesAccountName": "existing-account-name",
"ExistingCognitiveServicesResourceGroup": "existing-resource-group-name",
"Tags": []
}
5 changes: 4 additions & 1 deletion infra/functions.bicep
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ param identityName string
@description('Cognitive services account name.')
param cognitiveServicesAccountName string

@description('Resource group of the cognitive services account.')
param cognitiveServicesAccountResourceGroup string

var hostingPlanName = 'asp-${baseName}'
var functionAppName = 'func-${baseName}'
var storageBlobDataOwnerRoleDefinitionId = 'b7e6dc6d-f1e8-4753-8033-0f276bb0955b'
Expand All @@ -48,7 +51,7 @@ resource identity 'Microsoft.ManagedIdentity/userAssignedIdentities@2023-07-31-p

resource cognitiveServiceAccount 'Microsoft.CognitiveServices/accounts@2023-05-01' existing = {
name: cognitiveServicesAccountName
scope: resourceGroup()
scope: resourceGroup(cognitiveServicesAccountResourceGroup)
}

resource functionStorageAccount 'Microsoft.Storage/storageAccounts@2022-05-01' = {
Expand Down
39 changes: 39 additions & 0 deletions infra/image-analyzer-permissions.bicep
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/*
* Create a role assignment to assign the Cognitive Services User role to the given User Assigned Identities.
* This should be deployed to the resource group where Cognitive Services Account is. Identities can be in
* a different resource group.
*/

@description('Name of the Cognitive Services account.')
param cognitiveServiceName string

@description('Identities of which are assigned with Blob Data Reader to the containers.')
param cognitiveServiceUserIdentityName string
param cognitiveServiceUserIdentityResourceGroup string

var cognitiveServicesUserRoleDefinitionId = subscriptionResourceId(
'Microsoft.Authorization/roleDefinitions',
'a97b65f3-24c7-4388-baec-2e87135dc908'
)

resource identity 'Microsoft.ManagedIdentity/userAssignedIdentities@2023-07-31-preview' existing = {
name: cognitiveServiceUserIdentityName
scope: resourceGroup(cognitiveServiceUserIdentityResourceGroup)
}

resource cognitiveServiceAccount 'Microsoft.CognitiveServices/accounts@2023-05-01' existing = {
name: cognitiveServiceName
scope: resourceGroup()
}

resource cognitiveServiceAssignments 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
scope: cognitiveServiceAccount
name: guid(identity.id, cognitiveServicesUserRoleDefinitionId, cognitiveServiceAccount.id)
properties: {
principalId: identity.properties.principalId
principalType: 'ServicePrincipal'
roleDefinitionId: cognitiveServicesUserRoleDefinitionId
}
}

output cognitiveServiceAccountName string = cognitiveServiceAccount.name
51 changes: 51 additions & 0 deletions infra/image-analyzer-resolver.bicep
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { CognitiveServiceSettings } from 'types.bicep'

/*
* Either creates a new CognitiveService account or uses an existing one.
* and adds the necessary permissions to the user identity.
* This is seprate module because the existing CognitiveService account
* may be in a different resource group (scope).
*/

@minLength(5)
param baseName string

@description('Location for all resources.')
param location string = resourceGroup().location

@description('Settings for deciding which CognitiveService resource is used.')
param cognitiveService CognitiveServiceSettings

@description('Identities of which are assigned with Blob Data Reader to the containers.')
param cognitiveServiceUserIdentityName string

// NOTE: ?? '' - is used to circument type warnings.
var existingServiceResourceGroupName = cognitiveService.existingServiceResourceGroup ?? ''
var getExisting = cognitiveService.existingServiceName != null

resource old 'Microsoft.CognitiveServices/accounts@2023-05-01' existing = if (getExisting) {
name: cognitiveService.existingServiceName ?? ''
scope: resourceGroup(existingServiceResourceGroupName)
}

module rbacAssingments 'image-analyzer-permissions.bicep' = if (getExisting) {
scope: resourceGroup(existingServiceResourceGroupName)
name: 'cognitiveServiceUser-old'
params: {
cognitiveServiceName: old.name
cognitiveServiceUserIdentityName: cognitiveServiceUserIdentityName
cognitiveServiceUserIdentityResourceGroup: resourceGroup().name
}
}

module new 'image-analyzer.bicep' = if (!getExisting) {
name: 'newAnalyzer'
params: {
baseName: baseName
location: location
cognitiveServiceUserIdentityNames: [cognitiveServiceUserIdentityName]
}
}

output cognitiveServiceAccountName string = getExisting ? old.name : new.outputs.cognitiveServiceAccountName
output cognitiveServiceResourceGroup string = getExisting ? existingServiceResourceGroupName : resourceGroup().name
32 changes: 12 additions & 20 deletions infra/image-analyzer.bicep
Original file line number Diff line number Diff line change
@@ -1,25 +1,18 @@
/*
* Creates Cognitive Services Account for Computer Vision and gives access to the specified identities.
*/

@minLength(5)
param baseName string

param cognitiveServiceName string = 'aisa-${baseName}'

@description('Location for all resources.')
param location string = resourceGroup().location

@description('Identities of which are assigned with Blob Data Reader to the containers.')
param cognitiveServiceUserIdentityNames string[]

var cognitiveServicesUserRoleDefinitionId = subscriptionResourceId(
'Microsoft.Authorization/roleDefinitions',
'a97b65f3-24c7-4388-baec-2e87135dc908'
)

resource identities 'Microsoft.ManagedIdentity/userAssignedIdentities@2023-07-31-preview' existing = [
for identityName in cognitiveServiceUserIdentityNames: {
name: identityName
scope: resourceGroup()
}
]

var cognitiveServiceName = 'aisa-${baseName}'
resource cognitiveServiceAccount 'Microsoft.CognitiveServices/accounts@2023-05-01' = {
name: cognitiveServiceName
location: location
Expand All @@ -36,14 +29,13 @@ resource cognitiveServiceAccount 'Microsoft.CognitiveServices/accounts@2023-05-0
}
}

resource cognitiveServiceAssignments 'Microsoft.Authorization/roleAssignments@2022-04-01' = [
module rbacAssingments 'image-analyzer-permissions.bicep' = [
for (identityName, i) in cognitiveServiceUserIdentityNames: {
scope: cognitiveServiceAccount
name: guid(identities[i].id, cognitiveServicesUserRoleDefinitionId, cognitiveServiceAccount.id)
properties: {
principalId: identities[i].properties.principalId
principalType: 'ServicePrincipal'
roleDefinitionId: cognitiveServicesUserRoleDefinitionId
name: 'cognitiveServiceUser-${i}'
params: {
cognitiveServiceName: cognitiveServiceAccount.name
cognitiveServiceUserIdentityName: cognitiveServiceUserIdentityNames[0]
cognitiveServiceUserIdentityResourceGroup: resourceGroup().name
}
}
]
Expand Down
3 changes: 2 additions & 1 deletion infra/image-storage.bicep
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,9 @@ resource identities 'Microsoft.ManagedIdentity/userAssignedIdentities@2023-07-31
}
]

var storageName = replace('stimages${baseName}', '-', '')
resource imageStorage 'Microsoft.Storage/storageAccounts@2022-05-01' = {
name: substring(replace('stimages${baseName}', '-', ''), 0, 24)
name: substring(storageName, 0, min(length(storageName), 24))
location: location
kind: 'StorageV2'
sku: {
Expand Down
19 changes: 11 additions & 8 deletions infra/main.bicep
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { DiscordSettings, ImageStorageSettings } from 'types.bicep'
import { DiscordSettings, ImageStorageSettings, CognitiveServiceSettings } from 'types.bicep'

@description('Base of the name for all resources.')
param baseName string
Expand All @@ -15,6 +15,9 @@ param webSitePackageLocation string = ''
@description('If true, messages are not sent to Discord. This should only be used when testing.')
param disableDiscordSending bool = false

@description('Settings for deciding which CognitiveService resource is used.')
param cognitiveService CognitiveServiceSettings

module appInsights 'app-insights.bicep' = {
name: 'application-insights'
params: {
Expand Down Expand Up @@ -70,14 +73,13 @@ var imageSettings = {
folderPath: 'root'
}

module imageAnalyzerModule 'image-analyzer.bicep' = {
name: 'image-analyzer'
module imageAnalyzerResolverModule 'image-analyzer-resolver.bicep' = {
name: 'image-analyzer-resolver'
params: {
location: location
baseName: baseName
cognitiveServiceUserIdentityNames: [
functionAppIdentity.name
]
location: location
cognitiveService: cognitiveService
cognitiveServiceUserIdentityName: functionAppIdentity.name
}
}

Expand All @@ -92,6 +94,7 @@ module functions 'functions.bicep' = {
webSitePackageLocation: webSitePackageLocation
disableDiscordSending: disableDiscordSending
identityName: functionAppIdentity.name
cognitiveServicesAccountName: imageAnalyzerModule.outputs.cognitiveServiceAccountName
cognitiveServicesAccountName: imageAnalyzerResolverModule.outputs.cognitiveServiceAccountName
cognitiveServicesAccountResourceGroup: imageAnalyzerResolverModule.outputs.cognitiveServiceResourceGroup
}
}
10 changes: 10 additions & 0 deletions infra/types.bicep
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,13 @@ type SecretKeyValue = {
@secure()
value: string
}

@description('Settings for deciding which CognitiveService resource is used.')
@export()
type CognitiveServiceSettings = {
@description('The name of the CognitiveService resource. If null, a new resource is created.')
existingServiceName: string?

@description('The resource group of the existing CognitiveService resource. If null, current resource group is used.')
existingServiceResourceGroup: string?
}
29 changes: 29 additions & 0 deletions scripts/FunctionUtil.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -86,3 +86,32 @@ function Get-FunctionBaseUrl() {
)
return "https://$($FunctionApp.HostNames[0])/api/"
}

<#
.SYNOPSIS
Removes unknown 'Cognitive Services User' assignments from the
Cognitive Services resource.
.DESCRIPTION
This is a support script for deployment. When Resource Assignment ID is
generated in image-analyzer-permissions.bicep, it uses the identity ID as
part of deployment ID. This can cause problems, because the principal ID is
different if the identity resource is recreated, but the resource ID is
the same.
#>
function Remove-UnknownRoleAssingments() {
param(
[Parameter(Mandatory)][string]$ResourceName,
[Parameter(Mandatory)][string]$ResourceGroupName
)
$resourceType = 'Microsoft.CognitiveServices/accounts'
$roleDefinitionName = 'Cognitive Services User'
Write-Host "Removing unknown 'Cognitive Services User' role assignments from Resource Group '$ResourceGroupName' resource '$ResourceName'..."
Get-AzRoleAssignment `
-ResourceName $ResourceName `
-ResourceGroupName $ResourceGroupName `
-ResourceType $resourceType `
-RoleDefinitionName $roleDefinitionName `
| Where-Object { $_.ObjectType -eq 'Unknown' } `
| Remove-AzRoleAssignment
}
27 changes: 27 additions & 0 deletions scripts/Get-ImageIndex.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<#
.SYNOPSIS
This script builds and publishes measurement listener to Azure
.DESCRIPTION
This assumes that dotnet sdk and Azure Powershell are installed
.PARAMETER SettinsFile
Settings file that contains environment settings. Defaults to 'developer-settings.json'
#>
param(
[Parameter()][string]$SettingsFile = 'developer-settings.json'
)
$ErrorActionPreference = "Stop"
Set-StrictMode -Version Latest

.$PSScriptRoot/FunctionUtil.ps1

$settingsJson = Get-DeveloperSettings -SettingsFile $SettingsFile
$appName = "func-$($settingsJson.ResourceGroup)"

$webApp = Get-AzWebApp -ResourceGroupName $settingsJson.ResourceGroup -Name $appName
$url = Get-FunctionBaseUrl -FunctionApp $webApp
$code = Get-FunctionCode -FunctionApp $webApp

$functionUrl = "$($url)GetImageIndex?code=$code"
Invoke-RestMethod -Method Get -Uri $functionUrl -ContentType 'application/json'
8 changes: 4 additions & 4 deletions src/Common/ImageAnalysis/IImageAnalysisService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@ namespace DiscordImagePoster.Common.ImageAnalysis;
public interface IImageAnalysisService
{
/// <summary>
/// Analyzes the image in stream.
/// Analyzes the image provided in BinaryData.
/// </summary>
/// <param name="stream">The stream containing the image.</param>
/// <returns>The analysis results.</returns>
Task<ImageAnalysisResults> AnalyzeImageAsync(Stream stream);
/// <param name="binaryData">The BinaryData containing the image.</param>
/// <returns>The analysis results. Can be null if image analysis is not done.</returns>
Task<ImageAnalysisResults?> AnalyzeImageAsync(BinaryData binaryData);
}
4 changes: 2 additions & 2 deletions src/Common/ImageAnalysis/ImageAnalysisService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,10 @@ public ImageAnalysisService(
}

/// <inheritdoc/>
public async Task<ImageAnalysisResults> AnalyzeImageAsync(Stream stream)
public async Task<ImageAnalysisResults?> AnalyzeImageAsync(BinaryData binaryData)
{
_logger.LogDebug("Analyzing image...");
var result = await _imageAnalysisClient.AnalyzeAsync(BinaryData.FromStream(stream), VisualFeatures.Caption | VisualFeatures.Tags);
var result = await _imageAnalysisClient.AnalyzeAsync(binaryData, VisualFeatures.Caption | VisualFeatures.Tags);
var tags = result.Value.Tags.Values.Select(tag => $"{tag.Name} {tag.Confidence}").ToArray();
_logger.LogDebug("Image analysis result with caption: {result}, and tags {tags}", result.Value.Caption.Text, tags);
_logger.LogTrace("Image analysis result: {result}", JsonSerializer.Serialize(result));
Expand Down
15 changes: 15 additions & 0 deletions src/Common/ImageAnalysis/NoOpImageAnalysisService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
using Microsoft.Extensions.Logging;

namespace DiscordImagePoster.Common.ImageAnalysis;

public class NoOpImageAnalysisService : IImageAnalysisService
{
public NoOpImageAnalysisService(ILogger<NoOpImageAnalysisService> logger)
{
}

public Task<ImageAnalysisResults?> AnalyzeImageAsync(BinaryData binaryData)
{
return Task.FromResult<ImageAnalysisResults?>(null);
}
}
Loading

0 comments on commit beeaf7b

Please sign in to comment.