diff --git a/ai-service.yaml b/ai-service.yaml index 76421e22..a9eb7f0a 100644 --- a/ai-service.yaml +++ b/ai-service.yaml @@ -15,7 +15,7 @@ spec: nodeSelector: "kubernetes.io/os": linux containers: - - name: order-service + - name: ai-service image: ghcr.io/azure-samples/aks-store-demo/ai-service:latest ports: - containerPort: 5001 @@ -35,8 +35,8 @@ spec: cpu: 20m memory: 50Mi limits: - cpu: 30m - memory: 85Mi + cpu: 50m + memory: 128Mi startupProbe: httpGet: path: /health @@ -49,15 +49,15 @@ spec: path: /health port: 5001 initialDelaySeconds: 3 - failureThreshold: 3 - periodSeconds: 5 + failureThreshold: 10 + periodSeconds: 10 livenessProbe: httpGet: path: /health port: 5001 initialDelaySeconds: 3 - failureThreshold: 5 - periodSeconds: 3 + failureThreshold: 10 + periodSeconds: 10 --- apiVersion: v1 kind: Service diff --git a/aks-store-all-in-one.yaml b/aks-store-all-in-one.yaml index c3f72cd2..627785bf 100644 --- a/aks-store-all-in-one.yaml +++ b/aks-store-all-in-one.yaml @@ -160,8 +160,8 @@ spec: cpu: 1m memory: 50Mi limits: - cpu: 75m - memory: 128Mi + cpu: 100m + memory: 256Mi startupProbe: httpGet: path: /health @@ -192,8 +192,8 @@ spec: cpu: 1m memory: 50Mi limits: - cpu: 75m - memory: 128Mi + cpu: 100m + memory: 256Mi --- apiVersion: v1 kind: Service @@ -251,6 +251,12 @@ spec: limits: cpu: 5m memory: 20Mi + startupProbe: + httpGet: + path: /health + port: 3001 + failureThreshold: 10 + periodSeconds: 5 readinessProbe: httpGet: path: /health @@ -536,4 +542,4 @@ spec: memory: 1Mi limits: cpu: 1m - memory: 7Mi \ No newline at end of file + memory: 7Mi diff --git a/azd-hooks/predeploy.ps1 b/azd-hooks/predeploy.ps1 index 60cf370c..a1faa24d 100644 --- a/azd-hooks/predeploy.ps1 +++ b/azd-hooks/predeploy.ps1 @@ -3,9 +3,26 @@ az aks get-credentials --resource-group ${AZURE_RESOURCE_GROUP} --name ${AZURE_AKS_CLUSTER_NAME} --overwrite-existing ########################################################### -# Create the custom-values.yaml file with base images +# Create the custom-values.yaml file ########################################################### +@" +namespace: ${env:AZURE_AKS_NAMESPACE} +"@ | Out-File -FilePath custom-values.yaml -Encoding utf8 + +########################################################### +# Add Azure Managed Identity and set to use AzureAD auth +########################################################### +if (![string]::IsNullOrEmpty($env:AZURE_IDENTITY_CLIENT_ID) -and ![string]::IsNullOrEmpty($env:AZURE_IDENTITY_NAME)) { +@" +useAzureAd: true +managedIdentityName: $($env:AZURE_IDENTITY_NAME) +managedIdentityClientId: $($env:AZURE_IDENTITY_CLIENT_ID) +"@ | Out-File -Append custom-values.yaml +} +########################################################### +# Add base images +########################################################### @" namespace: ${env:AZURE_AKS_NAMESPACE} productService: @@ -23,7 +40,7 @@ virtualCustomer: virtualWorker: image: repository: ${env:AZURE_REGISTRY_URI}/aks-store-demo/virtual-worker -"@ | Out-File -FilePath custom-values.yaml -Encoding utf8 +"@ | Out-File -Append custom-values.yaml ########################################################### # Add ai-service if Azure OpenAI endpoint is provided @@ -40,13 +57,8 @@ aiService: useAzureOpenAi: if ($env:AZURE_OPENAI_ENDPOINT) { 'true' } "@ | Out-File -Path custom-values.yaml -Append -Encoding utf8 - # If Azure identity exists, use it, otherwise use the Azure OpenAI API key - if ($env:AZURE_IDENTITY_CLIENT_ID) { - @" - managedIdentityClientId: ${env:AZURE_IDENTITY_CLIENT_ID} - useAzureAd: true -"@ | Out-File -Path custom-values.yaml -Append -Encoding utf8 - } else { + # If Azure identity does not exist, use the Azure OpenAI API key + if (($env:AZURE_IDENTITY_CLIENT_ID -eq $null) -and ($env:AZURE_IDENTITY_NAME -eq $null)) { $openAiKey = az keyvault secret show --name $env:AZURE_OPENAI_KEY --vault-name $env:AZURE_KEY_VAULT_NAME --query value -o tsv @" openAiKey: $openAiKey @@ -68,6 +80,13 @@ if ($env:AZURE_SERVICE_BUS_HOST) { $queuePassword = az keyvault secret show --name $env:AZURE_SERVICE_BUS_SENDER_KEY --vault-name $env:AZURE_KEY_VAULT_NAME --query value -o tsv @" queueHost: ${env:AZURE_SERVICE_BUS_HOST} +"@ | Out-File -Append custom-values.yaml + + + # If Azure identity does not exists, use the Azure Service Bus credentials + if (-not $env:AZURE_IDENTITY_CLIENT_ID -and -not $env:AZURE_IDENTITY_NAME) { + $queuePassword = az keyvault secret show --name $env:AZURE_SERVICE_BUS_SENDER_KEY --vault-name $env:AZURE_KEY_VAULT_NAME --query value -o tsv + @" queuePort: "5671" queueTransport: "tls" queueUsername: ${env:AZURE_SERVICE_BUS_SENDER_NAME} @@ -86,12 +105,19 @@ makelineService: # Add Azure Service Bus to makeline-service if provided if ($env:AZURE_SERVICE_BUS_URI) { + # If Azure identity exists just set the Azure Service Bus Hostname + if ($env:AZURE_IDENTITY_CLIENT_ID -and $env:AZURE_IDENTITY_NAME) { + @" + orderQueueHost: $($env:AZURE_SERVICE_BUS_HOST) +"@ | Out-File -Path custom-values.yaml -Append -Encoding utf8 + } else { $orderQueuePassword = az keyvault secret show --name $env:AZURE_SERVICE_BUS_LISTENER_KEY --vault-name $env:AZURE_KEY_VAULT_NAME --query value -o tsv @" orderQueueUri: ${env:AZURE_SERVICE_BUS_URI} orderQueueUsername: ${env:AZURE_SERVICE_BUS_LISTENER_NAME} orderQueuePassword: $orderQueuePassword "@ | Out-File -Path custom-values.yaml -Append -Encoding utf8 + } } # Add Azure Cosmos DB to makeline-service if provided @@ -100,13 +126,18 @@ if ($env:AZURE_COSMOS_DATABASE_URI) { @" orderDBApi: ${env:AZURE_DATABASE_API} orderDBUri: ${env:AZURE_COSMOS_DATABASE_URI} +"@ | Out-File -Path custom-values.yaml -Append -Encoding utf8 + +# If Azure identity does not exists, use the Azure Cosmos DB credentials + if (-not $env:AZURE_IDENTITY_CLIENT_ID -and -not $env:AZURE_IDENTITY_NAME) { + $orderDBPassword = az keyvault secret show --name $env:AZURE_COSMOS_DATABASE_KEY --vault-name $env:AZURE_KEY_VAULT_NAME --query value -o tsv + @" orderDBUsername: ${env:AZURE_COSMOS_DATABASE_NAME} orderDBPassword: $orderDBPassword "@ | Out-File -Path custom-values.yaml -Append -Encoding utf8 + } } - - ########################################################### # Do not deploy RabbitMQ when using Azure Service Bus ########################################################### diff --git a/azd-hooks/predeploy.sh b/azd-hooks/predeploy.sh index e748a1c8..01384c5c 100755 --- a/azd-hooks/predeploy.sh +++ b/azd-hooks/predeploy.sh @@ -3,11 +3,28 @@ az aks get-credentials --resource-group ${AZURE_RESOURCE_GROUP} --name ${AZURE_AKS_CLUSTER_NAME} --overwrite-existing ########################################################## -# Create the custom-values.yaml file with base images +# Create the custom-values.yaml file ########################################################## - cat << EOF > custom-values.yaml namespace: ${AZURE_AKS_NAMESPACE} +EOF + +########################################################### +# Add Azure Managed Identity and set to use AzureAD auth +########################################################### +if [ -n "${AZURE_IDENTITY_CLIENT_ID}" ] && [ -n "${AZURE_IDENTITY_NAME}" ]; then + cat << EOF >> custom-values.yaml +useAzureAd: true +managedIdentityName: ${AZURE_IDENTITY_NAME} +managedIdentityClientId: ${AZURE_IDENTITY_CLIENT_ID} +EOF +fi + +########################################################## +# Add base images +########################################################## +cat << EOF >> custom-values.yaml +namespace: ${AZURE_AKS_NAMESPACE} productService: image: repository: ${AZURE_REGISTRY_URI}/aks-store-demo/product-service @@ -40,15 +57,18 @@ aiService: useAzureOpenAi: true EOF - # If Azure identity exists, use it, otherwise use the Azure OpenAI API key - if [ -n "${AZURE_IDENTITY_CLIENT_ID}" ]; then + # If Azure identity does not exists, use the Azure OpenAI API key + if [ -z "${AZURE_IDENTITY_CLIENT_ID}" ] && [ -z "${AZURE_IDENTITY_NAME}" ]; then cat << EOF >> custom-values.yaml - useAzureAd: true - managedIdentityClientId: ${AZURE_IDENTITY_CLIENT_ID} + openAiKey: $(az keyvault secret show --name ${AZURE_OPENAI_KEY} --vault-name ${AZURE_KEY_VAULT_NAME} --query value -o tsv) EOF - else + fi + + # If DALL-E model endpoint and name exists + if [ -n "${AZURE_OPENAI_DALL_E_ENDPOINT}" ] && [ -n "${AZURE_OPENAI_DALL_E_MODEL_NAME}" ]; then cat << EOF >> custom-values.yaml - openAiKey: $(az keyvault secret show --name ${AZURE_OPENAI_KEY} --vault-name ${AZURE_KEY_VAULT_NAME} --query value -o tsv) + openAiDalleEndpoint: ${AZURE_OPENAI_DALL_E_ENDPOINT} + openAiDalleModelName: ${AZURE_OPENAI_DALL_E_MODEL_NAME} EOF fi fi @@ -67,11 +87,17 @@ EOF if [ -n "${AZURE_SERVICE_BUS_HOST}" ]; then cat << EOF >> custom-values.yaml queueHost: ${AZURE_SERVICE_BUS_HOST} +EOF + + # If Azure identity does not exists, use the Azure Service Bus credentials + if [ -z "${AZURE_IDENTITY_CLIENT_ID}" ] && [ -z "${AZURE_IDENTITY_NAME}" ]; then + cat << EOF >> custom-values.yaml queuePort: "5671" queueTransport: "tls" queueUsername: ${AZURE_SERVICE_BUS_SENDER_NAME} queuePassword: $(az keyvault secret show --name ${AZURE_SERVICE_BUS_SENDER_KEY} --vault-name ${AZURE_KEY_VAULT_NAME} --query value -o tsv) EOF + fi fi ########################################################### @@ -86,11 +112,18 @@ EOF # Add Azure Service Bus to makeline-service if provided if [ -n "${AZURE_SERVICE_BUS_URI}" ]; then - cat << EOF >> custom-values.yaml + # If Azure identity exists just set the Azure Service Bus Hostname + if [ -n "${AZURE_IDENTITY_CLIENT_ID}" ] && [ -n "${AZURE_IDENTITY_NAME}" ]; then + cat << EOF >> custom-values.yaml + orderQueueHost: ${AZURE_SERVICE_BUS_HOST} +EOF + else + cat << EOF >> custom-values.yaml orderQueueUri: ${AZURE_SERVICE_BUS_URI} orderQueueUsername: ${AZURE_SERVICE_BUS_LISTENER_NAME} orderQueuePassword: $(az keyvault secret show --name ${AZURE_SERVICE_BUS_LISTENER_KEY} --vault-name ${AZURE_KEY_VAULT_NAME} --query value -o tsv) EOF + fi fi # Add Azure Cosmos DB to makeline-service if provided @@ -98,9 +131,15 @@ if [ -n "${AZURE_COSMOS_DATABASE_URI}" ]; then cat << EOF >> custom-values.yaml orderDBApi: ${AZURE_DATABASE_API} orderDBUri: ${AZURE_COSMOS_DATABASE_URI} +EOF + + # If Azure identity does not exists, use the Azure Cosmos DB credentials + if [ -z "${AZURE_IDENTITY_CLIENT_ID}" ] && [ -z "${AZURE_IDENTITY_NAME}" ]; then + cat << EOF >> custom-values.yaml orderDBUsername: ${AZURE_COSMOS_DATABASE_NAME} orderDBPassword: $(az keyvault secret show --name ${AZURE_COSMOS_DATABASE_KEY} --vault-name ${AZURE_KEY_VAULT_NAME} --query value -o tsv) EOF + fi fi ########################################################### @@ -119,4 +158,4 @@ if [ -n "${AZURE_COSMOS_DATABASE_URI}" ]; then cat << EOF >> custom-values.yaml useMongoDB: false EOF -fi \ No newline at end of file +fi diff --git a/azd-hooks/preprovision.ps1 b/azd-hooks/preprovision.ps1 index 25d644ec..23cb1580 100644 --- a/azd-hooks/preprovision.ps1 +++ b/azd-hooks/preprovision.ps1 @@ -8,18 +8,6 @@ while ((az provider show --namespace "Microsoft.ContainerService" --query "regis Start-Sleep -Seconds 3 } -az feature register --namespace "Microsoft.ContainerService" --name "AKS-KedaPreview" -while ((az feature show --namespace "Microsoft.ContainerService" --name "AKS-KedaPreview" --query "properties.state" -o tsv) -ne "Registered") { - Write-Host "Waiting for AKS-KedaPreview feature registration..." - Start-Sleep -Seconds 3 -} - -az feature register --namespace "Microsoft.ContainerService" --name "AKS-PrometheusAddonPreview" -while ((az feature show --namespace "Microsoft.ContainerService" --name "AKS-PrometheusAddonPreview" --query "properties.state" -o tsv) -ne "Registered") { - Write-Host "Waiting for AKS-PrometheusAddonPreview feature registration..." - Start-Sleep -Seconds 3 -} - az feature register --namespace "Microsoft.ContainerService" --name "NetworkObservabilityPreview" while ((az feature show --namespace "Microsoft.ContainerService" --name "NetworkObservabilityPreview" --query "properties.state" -o tsv) -ne "Registered") { Write-Host "Waiting for NetworkObservabilityPreview feature registration..." diff --git a/azd-hooks/preprovision.sh b/azd-hooks/preprovision.sh index 01030c3c..e7e87864 100755 --- a/azd-hooks/preprovision.sh +++ b/azd-hooks/preprovision.sh @@ -1,5 +1,10 @@ #!/bin/bash +if ${AZURE_COSMOSDB_ACCOUNT_KIND} == "MongoDB" && ${DEPLOY_AZURE_WORKLOAD_IDENTITY} == "true"; then + echo "Azure CosmosDB account kind cannot be MongoDB when deploying Azure Workload Identity" + exit 1 +fi + echo "Ensuring Azure CLI extensions and dependencies are installed" az provider register --namespace "Microsoft.ContainerService" @@ -8,19 +13,6 @@ while [[ $(az provider show --namespace "Microsoft.ContainerService" --query "re sleep 3 done - -az feature register --namespace "Microsoft.ContainerService" --name "AKS-KedaPreview" -while [[ $(az feature show --namespace "Microsoft.ContainerService" --name "AKS-KedaPreview" --query "properties.state" -o tsv) != "Registered" ]]; do - echo "Waiting for AKS-KedaPreview feature registration..." - sleep 3 -done - -az feature register --namespace "Microsoft.ContainerService" --name "AKS-PrometheusAddonPreview" -while [[ $(az feature show --namespace "Microsoft.ContainerService" --name "AKS-PrometheusAddonPreview" --query "properties.state" -o tsv) != "Registered" ]]; do - echo "Waiting for AKS-PrometheusAddonPreview feature registration..." - sleep 3 -done - az feature register --namespace "Microsoft.ContainerService" --name "NetworkObservabilityPreview" while [[ $(az feature show --namespace "Microsoft.ContainerService" --name "NetworkObservabilityPreview" --query "properties.state" -o tsv) != "Registered" ]]; do echo "Waiting for NetworkObservabilityPreview feature registration..." diff --git a/azure.yaml b/azure.yaml index bc5d1786..cfae84da 100644 --- a/azure.yaml +++ b/azure.yaml @@ -65,5 +65,5 @@ services: releases: - name: demo chart: aks-store-demo/aks-store-demo-chart - version: 1.1.0 + version: 1.2.0 values: custom-values.yaml # This file is created by the predeploy hook \ No newline at end of file diff --git a/charts/aks-store-demo/Chart.yaml b/charts/aks-store-demo/Chart.yaml index 6f71bed8..863c3149 100644 --- a/charts/aks-store-demo/Chart.yaml +++ b/charts/aks-store-demo/Chart.yaml @@ -15,7 +15,7 @@ type: application # This is the chart version. This version number should be incremented each time you make changes # to the chart and its templates, including the app version. # Versions are expected to follow Semantic Versioning (https://semver.org/) -version: 1.1.0 +version: 1.2.0 # This is the version number of the application being deployed. This version number should be # incremented each time you make changes to the application. Versions are not expected to diff --git a/charts/aks-store-demo/templates/ai-service.yaml b/charts/aks-store-demo/templates/ai-service.yaml index c19fe9f8..5996232a 100644 --- a/charts/aks-store-demo/templates/ai-service.yaml +++ b/charts/aks-store-demo/templates/ai-service.yaml @@ -2,10 +2,10 @@ apiVersion: v1 kind: ConfigMap metadata: - name: ai-service-configmap + name: ai-service-configs data: - AZURE_OPENAI_DEPLOYMENT_NAME: "{{ .Values.aiService.modelDeploymentName }}" AZURE_OPENAI_ENDPOINT: "{{ .Values.aiService.openAiEndpoint }}" + AZURE_OPENAI_DEPLOYMENT_NAME: "{{ .Values.aiService.modelDeploymentName }}" {{- /* Use Azure OpenAI or OpenAI */}} @@ -18,27 +18,24 @@ data: {{- /* Use Azure AD or OpenAI API Key */}} - {{- if eq .Values.aiService.useAzureAd true }} + {{- if eq .Values.useAzureAd true }} USE_AZURE_AD: "True" {{- else }} USE_AZURE_AD: "False" {{- end }} ---- -{{- /* -If Azure AD is used, create a service account with managed identity -*/}} -{{- if eq .Values.aiService.useAzureAd true }} -apiVersion: v1 -kind: ServiceAccount -metadata: - name: ai-service-account - annotations: - azure.workload.identity/client-id: "{{ .Values.aiService.managedIdentityClientId }}" + {{- /* + If DALL-E model is used, set the model name + */}} + {{- if and .Values.aiService.openAiDalleEndpoint .Values.aiService.openAiDalleModelName }} + AZURE_OPENAI_DALLE_ENDPOINT: "{{ .Values.aiService.openAiDalleEndpoint }}" + AZURE_OPENAI_DALLE_DEPLOYMENT_NAME: "{{ .Values.aiService.openAiDalleModelName }}" + AZURE_OPENAI_API_VERSION: "{{ .Values.aiService.azureOpenAiApiVersion }}" + {{- end }} --- {{- /* If Azure AD is not used, create a secret with OpenAI API Key */}} -{{- else }} +{{- if eq .Values.useAzureAd false }} apiVersion: v1 kind: Secret metadata: @@ -63,27 +60,27 @@ spec: {{- /* If Azure AD is used, set the label to use workload identity */}} - {{- if eq .Values.aiService.useAzureAd true }} + {{- if eq .Values.useAzureAd true }} azure.workload.identity/use: "true" {{- end }} spec: {{- /* If Azure AD is used, use the service account */}} - {{- if eq .Values.aiService.useAzureAd true }} - serviceAccount: ai-service-account + {{- if eq .Values.useAzureAd true }} + serviceAccount: {{ .Values.managedIdentityName }} {{- end }} nodeSelector: "kubernetes.io/os": linux containers: - - name: order-service + - name: ai-service image: {{ .Values.aiService.image.repository }}:{{ .Values.aiService.image.tag }} ports: - containerPort: 5001 envFrom: - configMapRef: - name: ai-service-configmap - {{- if eq .Values.aiService.useAzureAd false }} + name: ai-service-configs + {{- if eq .Values.useAzureAd false }} - secretRef: name: ai-service-secrets {{- end }} @@ -92,8 +89,8 @@ spec: cpu: 20m memory: 50Mi limits: - cpu: 30m - memory: 85Mi + cpu: 50m + memory: 128Mi startupProbe: httpGet: path: /health @@ -107,17 +104,17 @@ spec: path: /health port: 5001 initialDelaySeconds: 3 - failureThreshold: 3 + failureThreshold: 10 timeoutSeconds: 3 - periodSeconds: 5 + periodSeconds: 10 livenessProbe: httpGet: path: /health port: 5001 - failureThreshold: 3 + failureThreshold: 10 initialDelaySeconds: 3 timeoutSeconds: 3 - periodSeconds: 3 + periodSeconds: 10 --- apiVersion: v1 kind: Service diff --git a/charts/aks-store-demo/templates/makeline-service.yaml b/charts/aks-store-demo/templates/makeline-service.yaml index d1ec1277..6dea85e4 100644 --- a/charts/aks-store-demo/templates/makeline-service.yaml +++ b/charts/aks-store-demo/templates/makeline-service.yaml @@ -1,15 +1,20 @@ apiVersion: v1 kind: ConfigMap metadata: - name: makeline-service-configmap + name: makeline-service-configs data: + {{- if eq .Values.useAzureAd false }} ORDER_QUEUE_URI: "{{ .Values.makelineService.orderQueueUri }}" ORDER_QUEUE_USERNAME: "{{ .Values.makelineService.orderQueueUsername }}" ORDER_QUEUE_PASSWORD: "{{ .Values.makelineService.orderQueuePassword }}" + {{- else }} + USE_WORKLOAD_IDENTITY_AUTH: "true" + ORDER_QUEUE_HOSTNAME: "{{ .Values.makelineService.orderQueueHost }}" + {{- end }} ORDER_QUEUE_NAME: "{{ .Values.makelineService.orderQueueName }}" ORDER_DB_URI: "{{ .Values.makelineService.orderDBUri }}" ORDER_DB_NAME: "{{ .Values.makelineService.orderDBName }}" - {{- if eq .Values.makelineService.orderDBApi "cosmosdbsql" }} + {{- if or (eq .Values.makelineService.orderDBApi "cosmosdbsql") (eq .Values.useAzureAd true) }} ORDER_DB_API: "{{ .Values.makelineService.orderDBApi }}" ORDER_DB_CONTAINER_NAME: "{{ .Values.makelineService.orderDBContainerName }}" ORDER_DB_PARTITION_KEY: "storeId" @@ -18,7 +23,7 @@ data: ORDER_DB_COLLECTION_NAME: "{{ .Values.makelineService.orderDBCollectionName }}" {{- end }} --- -{{- if and .Values.makelineService.orderDBUsername .Values.makelineService.orderDBPassword }} +{{- if and (eq .Values.useAzureAd false) .Values.makelineService.orderDBUsername .Values.makelineService.orderDBPassword }} apiVersion: v1 kind: Secret metadata: @@ -41,7 +46,13 @@ spec: metadata: labels: app: makeline-service + {{- if eq .Values.useAzureAd true }} + azure.workload.identity/use: "true" + {{- end }} spec: + {{- if eq .Values.useAzureAd true }} + serviceAccount: {{ .Values.managedIdentityName }} + {{- end }} nodeSelector: "kubernetes.io/os": linux containers: @@ -51,8 +62,8 @@ spec: - containerPort: 3001 envFrom: - configMapRef: - name: makeline-service-configmap - {{- if and .Values.makelineService.orderDBUsername .Values.makelineService.orderDBPassword }} + name: makeline-service-configs + {{- if and (eq .Values.useAzureAd false) .Values.makelineService.orderDBUsername .Values.makelineService.orderDBPassword }} - secretRef: name: makeline-service-secrets {{- end }} @@ -63,6 +74,12 @@ spec: limits: cpu: 5m memory: 20Mi + startupProbe: + httpGet: + path: /health + port: 3001 + failureThreshold: 10 + periodSeconds: 5 readinessProbe: httpGet: path: /health diff --git a/charts/aks-store-demo/templates/order-service.yaml b/charts/aks-store-demo/templates/order-service.yaml index 2c466bff..766c3b47 100644 --- a/charts/aks-store-demo/templates/order-service.yaml +++ b/charts/aks-store-demo/templates/order-service.yaml @@ -1,24 +1,30 @@ apiVersion: v1 kind: ConfigMap metadata: - name: order-service-configmap + name: order-service-configs data: - ORDER_QUEUE_HOSTNAME: "{{ .Values.orderService.queueHost }}" + {{- if eq .Values.useAzureAd true }} + USE_WORKLOAD_IDENTITY_AUTH: "true" + {{- else }} ORDER_QUEUE_PORT: "{{ .Values.orderService.queuePort }}" + {{- end }} {{- if .Values.orderService.queueTransport }} ORDER_QUEUE_TRANSPORT: "{{ .Values.orderService.queueTransport }}" {{- end }} + ORDER_QUEUE_HOSTNAME: "{{ .Values.orderService.queueHost }}" ORDER_QUEUE_NAME: "{{ .Values.orderService.queueName }}" FASTIFY_ADDRESS: "0.0.0.0" --- +{{- if eq .Values.useAzureAd false }} apiVersion: v1 kind: Secret metadata: - name: order-service-secret + name: order-service-secrets data: ORDER_QUEUE_USERNAME: "{{ .Values.orderService.queueUsername | b64enc }}" ORDER_QUEUE_PASSWORD: "{{ .Values.orderService.queuePassword | b64enc }}" --- +{{- end }} apiVersion: apps/v1 kind: Deployment metadata: @@ -32,7 +38,13 @@ spec: metadata: labels: app: order-service + {{- if eq .Values.useAzureAd true }} + azure.workload.identity/use: "true" + {{- end }} spec: + {{- if eq .Values.useAzureAd true }} + serviceAccount: {{ .Values.managedIdentityName }} + {{- end }} nodeSelector: "kubernetes.io/os": linux containers: @@ -42,9 +54,11 @@ spec: - containerPort: 3000 envFrom: - configMapRef: - name: order-service-configmap + name: order-service-configs + {{- if eq .Values.useAzureAd false }} - secretRef: - name: order-service-secret + name: order-service-secrets + {{- end }} resources: requests: cpu: 1m diff --git a/charts/aks-store-demo/templates/rabbitmq.yaml b/charts/aks-store-demo/templates/rabbitmq.yaml index 44fb5c94..bab4cfab 100644 --- a/charts/aks-store-demo/templates/rabbitmq.yaml +++ b/charts/aks-store-demo/templates/rabbitmq.yaml @@ -10,7 +10,7 @@ metadata: apiVersion: v1 kind: Secret metadata: - name: rabbitmq-secret + name: rabbitmq-secrets data: RABBITMQ_DEFAULT_USER: "{{ .Values.orderService.queueUsername | b64enc }}" RABBITMQ_DEFAULT_PASS: "{{ .Values.orderService.queuePassword | b64enc }}" @@ -42,7 +42,7 @@ spec: name: rabbitmq-http envFrom: - secretRef: - name: rabbitmq-secret + name: rabbitmq-secrets resources: requests: cpu: 10m diff --git a/charts/aks-store-demo/templates/service-account.yaml b/charts/aks-store-demo/templates/service-account.yaml new file mode 100644 index 00000000..fb01ba32 --- /dev/null +++ b/charts/aks-store-demo/templates/service-account.yaml @@ -0,0 +1,8 @@ +{{- if and .Values.managedIdentityName .Values.managedIdentityClientId }} +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ .Values.managedIdentityName }} + annotations: + azure.workload.identity/client-id: "{{ .Values.managedIdentityClientId }}" +{{- end }} \ No newline at end of file diff --git a/charts/aks-store-demo/templates/virtual-customer.yaml b/charts/aks-store-demo/templates/virtual-customer.yaml index cd11855f..9b3990c9 100644 --- a/charts/aks-store-demo/templates/virtual-customer.yaml +++ b/charts/aks-store-demo/templates/virtual-customer.yaml @@ -21,7 +21,7 @@ spec: - name: ORDER_SERVICE_URL value: http://order-service:3000/ - name: ORDERS_PER_HOUR - value: "100" + value: "200" resources: requests: cpu: 1m diff --git a/charts/aks-store-demo/templates/virtual-worker.yaml b/charts/aks-store-demo/templates/virtual-worker.yaml index 3a667bc5..dadb1e32 100644 --- a/charts/aks-store-demo/templates/virtual-worker.yaml +++ b/charts/aks-store-demo/templates/virtual-worker.yaml @@ -21,7 +21,7 @@ spec: - name: MAKELINE_SERVICE_URL value: http://makeline-service:3001 - name: ORDERS_PER_HOUR - value: "100" + value: "175" resources: requests: cpu: 1m diff --git a/charts/aks-store-demo/values.yaml b/charts/aks-store-demo/values.yaml index 7d3dad40..7a8e970d 100644 --- a/charts/aks-store-demo/values.yaml +++ b/charts/aks-store-demo/values.yaml @@ -9,6 +9,10 @@ namespace: "dev" useRabbitMQ: true # when true, local rabbitmq container will be used useMongoDB: true # when true, local mongodb container will be used +useAzureAd: false # when true, Azure AD auth will be used for ai-service, order-service, and makeline-service +managedIdentityName: "" # name of the Azure AD identity to use for the service account +managedIdentityClientId: "" # client ID of the managed identity to use for the service account + aiService: # Specifies whether a ai-service deployment and service should be created create: false @@ -16,9 +20,10 @@ aiService: openAiEndpoint: "" openAiKey: "" openAiOrgId: "" - managedIdentityClientId: "" useAzureOpenAi: false - useAzureAd: false + openAiDalleEndpoint: "" + openAiDalleModelName: "" + azureOpenAiApiVersion: "2024-02-15-preview" image: repository: "ghcr.io/azure-samples/aks-store-demo/ai-service" tag: "latest" @@ -39,6 +44,7 @@ makelineService: orderQueueUsername: "username" orderQueuePassword: "password" orderQueueName: "orders" + orderQueueHost: "" orderDBApi: "mongodb" orderDBUri: "mongodb://mongodb:27017" orderDBName: "orderdb" diff --git a/docs/azd.md b/docs/azd.md index a4a0ec27..072a1aa2 100644 --- a/docs/azd.md +++ b/docs/azd.md @@ -68,8 +68,9 @@ The following environment variables are used to define the deployment settings: | `DEPLOY_AZURE_CONTAINER_REGISTRY` | By default, all application containers will be sourced from the [GitHub Container Registry](https://github.com/orgs/Azure-Samples/packages?repo_name=aks-store-demo). If you want to deploy apps from an Azure Container registry instead, set this environment variable to `true` to provision an Azure Container Registry and enable authentication from the AKS cluster. When this is set to true, you also have an option to set `BUILD_CONTAINERS` to `true` to build containers from source using the `az acr build command`; otherwise, the containers will be imported from the [GitHub Container Registry](https://github.com/orgs/Azure-Samples/packages?repo_name=aks-store-demo) using the `az acr import` command. | | `DEPLOY_AZURE_WORKLOAD_IDENTITY` | Set to `true` to deploy Azure Managed Identities for services that support it and enables workload identity and OIDC Issuer URL on AKS. | | `DEPLOY_AZURE_OPENAI` | Set to `true` to deploy Azure OpenAI, the `ai-service` microservice with workload identity authentication if that option was set to true. | +| `DEPLOY_AZURE_OPENAI_DALL_E_MODEL` | Set to `true` to deploy the DALL-E 3 model on Azure OpenAI. | | `DEPLOY_AZURE_SERVICE_BUS` | Set to `true` to deploy Azure Service Bus and configures workload identity if that option is set to true. | -| `DEPLOY_AZURE_COSMOSDB` | The `makeline-service` supports both MongoDB and SQL API for accessing data in Azure CosmosDB. The default API is `MongoDB`, but you can use SQL API when using Azure CosmosDB. Set this to `true` to deploy Azure Cosmos DB. When this is set to true, you can also set `AZURE_COSMOSDB_ACCOUNT_KIND` to `GlobalDocumentDB` to use the SQL API for Azure Cosmos DB; otherwise, MongoDB API will be used. | +| `DEPLOY_AZURE_COSMOSDB` | Set to `true` to deploy Azure Cosmos DB. When this is set to true, you can also set `AZURE_COSMOSDB_ACCOUNT_KIND` to `GlobalDocumentDB` to use the SQL API for Azure Cosmos DB; otherwise, MongoDB API will be used. The `makeline-service` supports both MongoDB and SQL API for accessing data in Azure CosmosDB. The default API is `MongoDB`, but if `DEPLOY_AZURE_WORKLOAD_IDENTITY` is set this will default to SQL API so that Azure RBAC authentication can be enabled for the Azure CosmosDB. | | `DEPLOY_OBSERVABILITY_TOOLS` | Set to `true` to deploy Azure Log Analytics workspace, Azure Monitor managed service for Promethues, Azure Managed Grafana, and onboard the AKS cluster to Container Insights. | These environment variables listed above can be set with commands like this: @@ -85,11 +86,16 @@ azd env set DEPLOY_AZURE_WORKLOAD_IDENTITY true # deploys azure openai azd env set DEPLOY_AZURE_OPENAI true +# deploys the DALL-E 3 model on azure openai +azd env set DEPLOY_AZURE_OPENAI_DALL_E_MODEL true + # deploys azure service bus azd env set DEPLOY_AZURE_SERVICE_BUS true # deploys azure cosmos db with the sql api azd env set DEPLOY_AZURE_COSMOSDB true + +# note this is the default when DEPLOY_AZURE_WORKLOAD_IDENTITY is set to true azd env set AZURE_COSMOSDB_ACCOUNT_KIND GlobalDocumentDB # deploys aks observability tools diff --git a/infra/terraform/.terraform.lock.hcl b/infra/terraform/.terraform.lock.hcl index cbed2dfb..b7b26f40 100644 --- a/infra/terraform/.terraform.lock.hcl +++ b/infra/terraform/.terraform.lock.hcl @@ -2,67 +2,61 @@ # Manual edits may be lost in future updates. provider "registry.terraform.io/hashicorp/azurerm" { - version = "3.80.0" - constraints = "3.80.0" + version = "3.101.0" + constraints = "3.101.0" hashes = [ - "h1:B9BjHL/otNG7gG2jEPPCz554qbke3fXEH1kzlsg7pnU=", - "h1:INZGxLapgXtWJBI7MP6ltmfViGN4SeqHxw+XcE4vN28=", - "h1:hmO/U2cTWnKuRZBLQj68ZsdiknsAA838L3Oe/lTjpAc=", - "zh:11e3362a3d9ca9d6d57a3c863b26ddcb9e4366c6c99586a4149d2842976e998c", - "zh:3005446ee10379e5354b2388d788cad92068ff0988f50e49f6e8eeacdbd6e0a1", - "zh:45aa9f9d930e332fc68f2093232edf15ef0d087c9588317371838784382b9dbc", - "zh:4e1d57b6b909577b37e85117533eb0d6a2128e7ccfcf88e709ad890f86e67295", - "zh:5f777dbb7d72ab83a3a9b9412f8e341573fb34591c834fefa8fc7593e73232a2", - "zh:6fab9ea69a2177e29a0c0819f32c51245891fd19b807c4a3e2d4dd5c0a28b8fb", - "zh:77b1bdf671a83e17c82b30520312970109811c7725b61fcef69138244d7dd212", - "zh:8a1e8d9243f1149b7bcf7b8bda3cf5a1bcea6c709b55053fe7e8cd767dd5f632", - "zh:a7c1de65d5f1f241c4d3f30f24912cb9dc8315a1dc7d7bd0814cb4beb1e8f1fb", - "zh:b4385e7c84d708cd81647555dd12ade9fb128fd027eb59cd651f4cdcf98a437e", - "zh:d4ef3f5b126d5c9766300ffb6f1b3ed091484d46c742cfae2645261e749162e5", + "h1:EBBVeOrjBsTGX/0l3GCBoCk0K04MtKgJ36Sf4jwPh9g=", + "zh:38b02bce5cbe83f938a71716bbf9e8b07fed8b2c6b83c19b5e708eda7dee0f1d", + "zh:3ed094366ab35c4fcd632471a7e45a84ca6c72b00477cdf1276e541a0171b369", + "zh:62bf7cde429f465173e40eebb6840f4e380dfc9dcec2d89dbcb6ce5bce379e50", + "zh:90761096666575f0a21275822011e08d72389a575f45e4c1c8e1d26c3b794750", + "zh:9494acbacc2b67cf87ae510862ca2c826d0e04662274477f8de1707cefa7c0f3", + "zh:9a01128004eab67ed90e9decb92271c187e95e0d6e9f136b5bbc8bf3a2189d41", + "zh:9e4eed599cecc2b2aff4dc334b154aad0ad80b5a07439139fc28b22fcff0c8aa", + "zh:a5f940e5b8b813b18d9ecd974fdda1ae989870a8a5d897fda8cff4c5368e6e24", + "zh:bc7c6bfad523f6c0fad7ef9f8d4c264f72cb9f29fce3a69f8483c63e70eb5085", + "zh:d9ba2c6bd082775e6d2d6453486ebb3ecc86ecf127e1d86eddf1a952b545c04e", + "zh:e288cce3c324a26d1e01a83e3fe2215537075ab897364539b6cabba298122654", "zh:f569b65999264a9416862bca5cd2a6177d94ccb0424f3a4ef424428912b9cb3c", ] } provider "registry.terraform.io/hashicorp/local" { - version = "2.4.0" - constraints = "2.4.0" + version = "2.5.1" + constraints = "2.5.1" hashes = [ - "h1:7RnIbO3CFakblTJs7o0mUiY44dc9xGYsLhSNFSNS1Ds=", - "h1:R97FTYETo88sT2VHfMgkPU3lzCsZLunPftjSI5vfKe8=", - "h1:ZUEYUmm2t4vxwzxy1BvN1wL6SDWrDxfH7pxtzX8c6d0=", - "zh:53604cd29cb92538668fe09565c739358dc53ca56f9f11312b9d7de81e48fab9", - "zh:66a46e9c508716a1c98efbf793092f03d50049fa4a83cd6b2251e9a06aca2acf", - "zh:70a6f6a852dd83768d0778ce9817d81d4b3f073fab8fa570bff92dcb0824f732", + "h1:8oTPe2VUL6E2d3OcrvqyjI4Nn/Y/UEQN26WLk5O/B0g=", + "zh:0af29ce2b7b5712319bf6424cb58d13b852bf9a777011a545fac99c7fdcdf561", + "zh:126063ea0d79dad1f68fa4e4d556793c0108ce278034f101d1dbbb2463924561", + "zh:196bfb49086f22fd4db46033e01655b0e5e036a5582d250412cc690fa7995de5", + "zh:37c92ec084d059d37d6cffdb683ccf68e3a5f8d2eb69dd73c8e43ad003ef8d24", + "zh:4269f01a98513651ad66763c16b268f4c2da76cc892ccfd54b401fff6cc11667", + "zh:51904350b9c728f963eef0c28f1d43e73d010333133eb7f30999a8fb6a0cc3d8", + "zh:73a66611359b83d0c3fcba2984610273f7954002febb8a57242bbb86d967b635", "zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3", - "zh:82a803f2f484c8b766e2e9c32343e9c89b91997b9f8d2697f9f3837f62926b35", - "zh:9708a4e40d6cc4b8afd1352e5186e6e1502f6ae599867c120967aebe9d90ed04", - "zh:973f65ce0d67c585f4ec250c1e634c9b22d9c4288b484ee2a871d7fa1e317406", - "zh:c8fa0f98f9316e4cfef082aa9b785ba16e36ff754d6aba8b456dab9500e671c6", - "zh:cfa5342a5f5188b20db246c73ac823918c189468e1382cb3c48a9c0c08fc5bf7", - "zh:e0e2b477c7e899c63b06b38cd8684a893d834d6d0b5e9b033cedc06dd7ffe9e2", - "zh:f62d7d05ea1ee566f732505200ab38d94315a4add27947a60afa29860822d3fc", - "zh:fa7ce69dde358e172bd719014ad637634bbdabc49363104f4fca759b4b73f2ce", + "zh:7ae387993a92bcc379063229b3cce8af7eaf082dd9306598fcd42352994d2de0", + "zh:9e0f365f807b088646db6e4a8d4b188129d9ebdbcf2568c8ab33bddd1b82c867", + "zh:b5263acbd8ae51c9cbffa79743fbcadcb7908057c87eb22fd9048268056efbc4", + "zh:dfcd88ac5f13c0d04e24be00b686d069b4879cc4add1b7b1a8ae545783d97520", ] } provider "registry.terraform.io/hashicorp/random" { - version = "3.5.1" - constraints = "3.5.1" + version = "3.6.1" + constraints = "3.6.1" hashes = [ - "h1:3hjTP5tQBspPcFAJlfafnWrNrKnr7J4Cp0qB9jbqf30=", - "h1:IL9mSatmwov+e0+++YX2V6uel+dV6bn+fC/cnGDK3Ck=", - "h1:VSnd9ZIPyfKHOObuQCaKfnjIHRtR7qTw19Rz8tJxm+k=", - "zh:04e3fbd610cb52c1017d282531364b9c53ef72b6bc533acb2a90671957324a64", - "zh:119197103301ebaf7efb91df8f0b6e0dd31e6ff943d231af35ee1831c599188d", - "zh:4d2b219d09abf3b1bb4df93d399ed156cadd61f44ad3baf5cf2954df2fba0831", - "zh:6130bdde527587bbe2dcaa7150363e96dbc5250ea20154176d82bc69df5d4ce3", - "zh:6cc326cd4000f724d3086ee05587e7710f032f94fc9af35e96a386a1c6f2214f", + "h1:1OlP753r4lOKlBprL0HdZGWerm5DCabD5Mli8k8lWAg=", + "zh:2a0ec154e39911f19c8214acd6241e469157489fc56b6c739f45fbed5896a176", + "zh:57f4e553224a5e849c99131f5e5294be3a7adcabe2d867d8a4fef8d0976e0e52", + "zh:58f09948c608e601bd9d0a9e47dcb78e2b2c13b4bda4d8f097d09152ea9e91c5", + "zh:5c2a297146ed6fb3fe934c800e78380f700f49ff24dbb5fb5463134948e3a65f", "zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3", - "zh:b6d88e1d28cf2dfa24e9fdcc3efc77adcdc1c3c3b5c7ce503a423efbdd6de57b", - "zh:ba74c592622ecbcef9dc2a4d81ed321c4e44cddf7da799faa324da9bf52a22b2", - "zh:c7c5cde98fe4ef1143bd1b3ec5dc04baf0d4cc3ca2c5c7d40d17c0e9b2076865", - "zh:dac4bad52c940cd0dfc27893507c1e92393846b024c5a9db159a93c534a3da03", - "zh:de8febe2a2acd9ac454b844a4106ed295ae9520ef54dc8ed2faf29f12716b602", - "zh:eab0d0495e7e711cca367f7d4df6e322e6c562fc52151ec931176115b83ed014", + "zh:7ce41e26f0603e31cdac849085fc99e5cd5b3b73414c6c6d955c0ceb249b593f", + "zh:8c9e8d30c4ef08ee8bcc4294dbf3c2115cd7d9049c6ba21422bd3471d92faf8a", + "zh:93e91be717a7ffbd6410120eb925ebb8658cc8f563de35a8b53804d33c51c8b0", + "zh:982542e921970d727ce10ed64795bf36c4dec77a5db0741d4665230d12250a0d", + "zh:b9d1873f14d6033e216510ef541c891f44d249464f13cc07d3f782d09c7d18de", + "zh:cfe27faa0bc9556391c8803ade135a5856c34a3fe85b9ae3bdd515013c0c87c1", + "zh:e4aabf3184bbb556b89e4b195eab1514c86a2914dd01c23ad9813ec17e863a8a", ] } diff --git a/infra/terraform/cosmosdb.tf b/infra/terraform/cosmosdb.tf index e0c92eb6..610e4a60 100644 --- a/infra/terraform/cosmosdb.tf +++ b/infra/terraform/cosmosdb.tf @@ -1,12 +1,13 @@ resource "azurerm_cosmosdb_account" "example" { - count = local.deploy_azure_cosmosdb ? 1 : 0 - name = "db-${local.name}" - location = azurerm_resource_group.example.location - resource_group_name = azurerm_resource_group.example.name - offer_type = "Standard" - kind = local.cosmosdb_account_kind - - enable_automatic_failover = false + count = local.deploy_azure_cosmosdb ? 1 : 0 + name = "db-${local.name}" + location = azurerm_resource_group.example.location + resource_group_name = azurerm_resource_group.example.name + offer_type = "Standard" + kind = local.cosmosdb_account_kind + access_key_metadata_writes_enabled = !local.deploy_azure_workload_identity + minimal_tls_version = "Tls12" + automatic_failover_enabled = false dynamic "capabilities" { for_each = local.cosmosdb_account_kind == "MongoDB" ? ["EnableAggregationPipeline", "mongoEnableDocLevelTTL", "MongoDBv3.4", "EnableMongo"] : ["EnableAggregationPipeline"] diff --git a/infra/terraform/keyvault.tf b/infra/terraform/keyvault.tf index 0592b59c..c83badf3 100644 --- a/infra/terraform/keyvault.tf +++ b/infra/terraform/keyvault.tf @@ -1,4 +1,5 @@ resource "azurerm_key_vault" "example" { + count = !local.deploy_azure_workload_identity ? 1 : 0 name = "akv-${local.name}" location = azurerm_resource_group.example.location resource_group_name = azurerm_resource_group.example.name @@ -87,40 +88,40 @@ resource "azurerm_key_vault" "example" { } resource "azurerm_role_assignment" "example_akv_rbac" { - count = var.kv_rbac_enabled ? 1 : 0 + count = var.kv_rbac_enabled && !local.deploy_azure_workload_identity ? 1 : 0 principal_id = data.azurerm_client_config.current.object_id role_definition_name = "Key Vault Administrator" - scope = azurerm_key_vault.example.id + scope = azurerm_key_vault.example[0].id } resource "azurerm_key_vault_secret" "openai_key" { - count = local.deploy_azure_openai ? 1 : 0 + count = local.deploy_azure_openai && !local.deploy_azure_workload_identity ? 1 : 0 name = "AZURE-OPENAI-KEY" value = azurerm_cognitive_account.example[0].primary_access_key - key_vault_id = azurerm_key_vault.example.id + key_vault_id = azurerm_key_vault.example[0].id depends_on = [azurerm_role_assignment.example_akv_rbac] } resource "azurerm_key_vault_secret" "cosmosdb_key" { - count = local.deploy_azure_cosmosdb ? 1 : 0 + count = local.deploy_azure_cosmosdb && !local.deploy_azure_workload_identity ? 1 : 0 name = "AZURE-COSMOS-KEY" value = azurerm_cosmosdb_account.example[0].primary_key - key_vault_id = azurerm_key_vault.example.id + key_vault_id = azurerm_key_vault.example[0].id depends_on = [azurerm_role_assignment.example_akv_rbac] } resource "azurerm_key_vault_secret" "listener_key" { - count = local.deploy_azure_servicebus ? 1 : 0 + count = local.deploy_azure_servicebus && !local.deploy_azure_workload_identity ? 1 : 0 name = "AZURE-SERVICE-BUS-LISTENER-KEY" value = azurerm_servicebus_namespace_authorization_rule.example[0].primary_key - key_vault_id = azurerm_key_vault.example.id + key_vault_id = azurerm_key_vault.example[0].id depends_on = [azurerm_role_assignment.example_akv_rbac] } resource "azurerm_key_vault_secret" "sender_key" { - count = local.deploy_azure_servicebus ? 1 : 0 + count = local.deploy_azure_servicebus && !local.deploy_azure_workload_identity ? 1 : 0 name = "AZURE-SERVICE-BUS-SENDER-KEY" value = azurerm_servicebus_queue_authorization_rule.example[0].primary_key - key_vault_id = azurerm_key_vault.example.id + key_vault_id = azurerm_key_vault.example[0].id depends_on = [azurerm_role_assignment.example_akv_rbac] } diff --git a/infra/terraform/locals.tf b/infra/terraform/locals.tf index 3f7d64f9..2073f9af 100644 --- a/infra/terraform/locals.tf +++ b/infra/terraform/locals.tf @@ -1,11 +1,12 @@ locals { name = "${random_pet.example.id}${random_integer.example.result}" location = var.location - default_cosmosdb_account_kind = "MongoDB" + default_cosmosdb_account_kind = var.deploy_azure_workload_identity == "true" ? "GlobalDocumentDB" : "MongoDB" cosmosdb_account_kind = var.cosmosdb_account_kind != "" ? var.cosmosdb_account_kind : local.default_cosmosdb_account_kind deploy_azure_container_registry = var.deploy_azure_container_registry == "true" ? true : false deploy_azure_workload_identity = var.deploy_azure_workload_identity == "true" ? true : false deploy_azure_openai = var.deploy_azure_openai == "true" ? true : false + deploy_azure_openai_dalle_model = var.deploy_azure_openai_dalle_model == "true" ? true : false deploy_azure_servicebus = var.deploy_azure_servicebus == "true" ? true : false deploy_azure_cosmosdb = var.deploy_azure_cosmosdb == "true" ? true : false deploy_observability_tools = var.deploy_observability_tools == "true" ? true : false diff --git a/infra/terraform/main.tf b/infra/terraform/main.tf index 2ed638aa..d893216a 100644 --- a/infra/terraform/main.tf +++ b/infra/terraform/main.tf @@ -2,17 +2,17 @@ terraform { required_providers { azurerm = { source = "hashicorp/azurerm" - version = "=3.80.0" + version = "=3.101.0" } local = { source = "hashicorp/local" - version = "=2.4.0" + version = "=2.5.1" } random = { source = "hashicorp/random" - version = "=3.5.1" + version = "=3.6.1" } } } diff --git a/infra/terraform/main.tfvars.json b/infra/terraform/main.tfvars.json index 420dcfa6..4ac0afdc 100644 --- a/infra/terraform/main.tfvars.json +++ b/infra/terraform/main.tfvars.json @@ -6,6 +6,7 @@ "deploy_azure_container_registry": "${DEPLOY_AZURE_CONTAINER_REGISTRY}", "deploy_azure_workload_identity": "${DEPLOY_AZURE_WORKLOAD_IDENTITY}", "deploy_azure_openai": "${DEPLOY_AZURE_OPENAI}", + "deploy_azure_openai_dalle_model": "${DEPLOY_AZURE_OPENAI_DALL_E_MODEL}", "deploy_azure_servicebus": "${DEPLOY_AZURE_SERVICE_BUS}", "deploy_azure_cosmosdb": "${DEPLOY_AZURE_COSMOSDB}", "deploy_observability_tools": "${DEPLOY_OBSERVABILITY_TOOLS}" diff --git a/infra/terraform/observability.tf b/infra/terraform/observability.tf index 850d0efd..df15373b 100644 --- a/infra/terraform/observability.tf +++ b/infra/terraform/observability.tf @@ -15,10 +15,11 @@ resource "azurerm_log_analytics_workspace" "example" { } resource "azurerm_dashboard_grafana" "example" { - count = local.deploy_observability_tools ? 1 : 0 - name = "amg-${local.name}" - resource_group_name = azurerm_resource_group.example.name - location = azurerm_resource_group.example.location + count = local.deploy_observability_tools ? 1 : 0 + name = "amg-${local.name}" + resource_group_name = azurerm_resource_group.example.name + location = azurerm_resource_group.example.location + grafana_major_version = "10" identity { type = "SystemAssigned" diff --git a/infra/terraform/openai.tf b/infra/terraform/openai.tf index a856d4cd..0a3e26ba 100644 --- a/infra/terraform/openai.tf +++ b/infra/terraform/openai.tf @@ -6,9 +6,10 @@ resource "azurerm_cognitive_account" "example" { kind = "OpenAI" sku_name = "S0" custom_subdomain_name = "aoai-${local.name}" + local_auth_enabled = !local.deploy_azure_workload_identity } -resource "azurerm_cognitive_deployment" "example" { +resource "azurerm_cognitive_deployment" "gpt" { count = local.deploy_azure_openai ? 1 : 0 name = var.openai_model_name cognitive_account_id = azurerm_cognitive_account.example[0].id @@ -25,21 +26,21 @@ resource "azurerm_cognitive_deployment" "example" { } } -resource "azurerm_user_assigned_identity" "example" { - count = local.deploy_azure_openai && local.deploy_azure_workload_identity ? 1 : 0 - location = var.ai_location - name = "aoai-${local.name}" - resource_group_name = azurerm_resource_group.example.name -} +resource "azurerm_cognitive_deployment" "dalle" { + count = local.deploy_azure_openai && local.deploy_azure_openai_dalle_model ? 1 : 0 + name = var.openai_dalle_model_name + cognitive_account_id = azurerm_cognitive_account.example[0].id -resource "azurerm_federated_identity_credential" "example" { - count = local.deploy_azure_openai && local.deploy_azure_workload_identity ? 1 : 0 - name = "aoai-${local.name}" - resource_group_name = azurerm_resource_group.example.name - parent_id = azurerm_user_assigned_identity.example[0].id - audience = ["api://AzureADTokenExchange"] - issuer = azurerm_kubernetes_cluster.example.oidc_issuer_url - subject = "system:serviceaccount:${var.k8s_namespace}:ai-service-account" + model { + format = "OpenAI" + name = var.openai_dalle_model_name + version = var.openai_dalle_model_version + } + + scale { + type = "Standard" + capacity = var.openai_dalle_model_capacity + } } resource "azurerm_role_assignment" "example_aoai_me" { @@ -47,11 +48,4 @@ resource "azurerm_role_assignment" "example_aoai_me" { principal_id = data.azurerm_client_config.current.object_id role_definition_name = "Cognitive Services OpenAI User" scope = azurerm_cognitive_account.example[0].id -} - -resource "azurerm_role_assignment" "example_aoai_mi" { - count = local.deploy_azure_openai && local.deploy_azure_workload_identity ? 1 : 0 - principal_id = azurerm_user_assigned_identity.example[0].principal_id - role_definition_name = "Cognitive Services OpenAI User" - scope = azurerm_cognitive_account.example[0].id } \ No newline at end of file diff --git a/infra/terraform/outputs.tf b/infra/terraform/outputs.tf index 11890d5a..31e23e28 100644 --- a/infra/terraform/outputs.tf +++ b/infra/terraform/outputs.tf @@ -34,13 +34,25 @@ output "AZURE_OPENAI_ENDPOINT" { value = local.deploy_azure_openai ? azurerm_cognitive_account.example[0].endpoint : "" } +output "AZURE_OPENAI_DALL_E_MODEL_NAME" { + value = local.deploy_azure_openai_dalle_model ? var.openai_dalle_model_name : "" +} + +output "AZURE_OPENAI_DALL_E_ENDPOINT" { + value = local.deploy_azure_openai_dalle_model ? azurerm_cognitive_account.example[0].endpoint : "" +} + output "AZURE_OPENAI_KEY" { - value = local.deploy_azure_openai ? azurerm_key_vault_secret.openai_key[0].name : "" + value = local.deploy_azure_openai && !local.deploy_azure_workload_identity ? azurerm_key_vault_secret.openai_key[0].name : "" sensitive = true } +output "AZURE_IDENTITY_NAME" { + value = local.deploy_azure_workload_identity ? azurerm_user_assigned_identity.example[0].name : "" +} + output "AZURE_IDENTITY_CLIENT_ID" { - value = local.deploy_azure_openai && local.deploy_azure_workload_identity ? azurerm_user_assigned_identity.example[0].client_id : "" + value = local.deploy_azure_workload_identity ? azurerm_user_assigned_identity.example[0].client_id : "" } output "AZURE_SERVICE_BUS_HOST" { @@ -53,20 +65,20 @@ output "AZURE_SERVICE_BUS_URI" { } output "AZURE_SERVICE_BUS_LISTENER_NAME" { - value = local.deploy_azure_servicebus ? azurerm_servicebus_namespace_authorization_rule.example[0].name : "" + value = local.deploy_azure_servicebus && !local.deploy_azure_workload_identity ? azurerm_servicebus_namespace_authorization_rule.example[0].name : "" } output "AZURE_SERVICE_BUS_LISTENER_KEY" { - value = local.deploy_azure_servicebus ? azurerm_key_vault_secret.listener_key[0].name : "" + value = local.deploy_azure_servicebus && !local.deploy_azure_workload_identity ? azurerm_key_vault_secret.listener_key[0].name : "" sensitive = true } output "AZURE_SERVICE_BUS_SENDER_NAME" { - value = local.deploy_azure_servicebus ? azurerm_servicebus_queue_authorization_rule.example[0].name : "" + value = local.deploy_azure_servicebus && !local.deploy_azure_workload_identity ? azurerm_servicebus_queue_authorization_rule.example[0].name : "" } output "AZURE_SERVICE_BUS_SENDER_KEY" { - value = local.deploy_azure_servicebus ? azurerm_key_vault_secret.sender_key[0].name : "" + value = local.deploy_azure_servicebus && !local.deploy_azure_workload_identity ? azurerm_key_vault_secret.sender_key[0].name : "" sensitive = true } @@ -83,12 +95,12 @@ output "AZURE_DATABASE_API" { } output "AZURE_COSMOS_DATABASE_KEY" { - value = local.deploy_azure_cosmosdb ? azurerm_key_vault_secret.cosmosdb_key[0].name : "" + value = local.deploy_azure_cosmosdb && !local.deploy_azure_workload_identity ? azurerm_key_vault_secret.cosmosdb_key[0].name : "" sensitive = true } output "AZURE_KEY_VAULT_NAME" { - value = azurerm_key_vault.example.name + value = !local.deploy_azure_workload_identity ? azurerm_key_vault.example[0].name : "" } output "AZURE_REGISTRY_NAME" { diff --git a/infra/terraform/servicebus.tf b/infra/terraform/servicebus.tf index 3692e995..430381ed 100644 --- a/infra/terraform/servicebus.tf +++ b/infra/terraform/servicebus.tf @@ -4,10 +4,17 @@ resource "azurerm_servicebus_namespace" "example" { location = azurerm_resource_group.example.location resource_group_name = azurerm_resource_group.example.name sku = "Standard" + local_auth_enabled = !local.deploy_azure_workload_identity } -resource "azurerm_servicebus_namespace_authorization_rule" "example" { +resource "azurerm_servicebus_queue" "example" { count = local.deploy_azure_servicebus ? 1 : 0 + name = "orders" + namespace_id = azurerm_servicebus_namespace.example[0].id +} + +resource "azurerm_servicebus_namespace_authorization_rule" "example" { + count = local.deploy_azure_servicebus && !local.deploy_azure_workload_identity ? 1 : 0 name = "listener" namespace_id = azurerm_servicebus_namespace.example[0].id @@ -16,14 +23,8 @@ resource "azurerm_servicebus_namespace_authorization_rule" "example" { manage = false } -resource "azurerm_servicebus_queue" "example" { - count = local.deploy_azure_servicebus ? 1 : 0 - name = "orders" - namespace_id = azurerm_servicebus_namespace.example[0].id -} - resource "azurerm_servicebus_queue_authorization_rule" "example" { - count = local.deploy_azure_servicebus ? 1 : 0 + count = local.deploy_azure_servicebus && !local.deploy_azure_workload_identity ? 1 : 0 name = "sender" queue_id = azurerm_servicebus_queue.example[0].id diff --git a/infra/terraform/variables.tf b/infra/terraform/variables.tf index 9362ed11..0253ad7c 100644 --- a/infra/terraform/variables.tf +++ b/infra/terraform/variables.tf @@ -24,6 +24,7 @@ variable "openai_model_name" { type = string default = "gpt-35-turbo" } + variable "openai_model_version" { description = "value of azure openai model version" type = string @@ -36,6 +37,24 @@ variable "openai_model_capacity" { default = 30 } +variable "openai_dalle_model_name" { + description = "value of azure openai dall-e-3 model name" + type = string + default = "dall-e-3" +} + +variable "openai_dalle_model_version" { + description = "value of azure openai dall-e-3 model version" + type = string + default = "3.0" +} + +variable "openai_dalle_model_capacity" { + description = "value of azure openai dall-e-3 model capacity" + type = number + default = 1 +} + variable "k8s_namespace" { description = "value of kubernetes namespace" type = string @@ -71,6 +90,13 @@ variable "deploy_azure_openai" { default = "false" } +variable "deploy_azure_openai_dalle_model" { + description = "value of setting to deploy azure openai dall-e-3 model. this string value will be used to set the local boolean variable" + type = string + default = "false" +} + + variable "deploy_azure_servicebus" { description = "value of setting to deploy azure service bus. this string value will be used to set the local boolean variable" type = string diff --git a/infra/terraform/workloadidentity.tf b/infra/terraform/workloadidentity.tf new file mode 100644 index 00000000..a4c067bb --- /dev/null +++ b/infra/terraform/workloadidentity.tf @@ -0,0 +1,56 @@ +resource "azurerm_user_assigned_identity" "example" { + count = local.deploy_azure_workload_identity ? 1 : 0 + location = var.location + name = "mid-${local.name}" + resource_group_name = azurerm_resource_group.example.name +} + +resource "azurerm_federated_identity_credential" "example" { + count = local.deploy_azure_openai && local.deploy_azure_workload_identity ? 1 : 0 + name = azurerm_user_assigned_identity.example[0].name + resource_group_name = azurerm_resource_group.example.name + parent_id = azurerm_user_assigned_identity.example[0].id + audience = ["api://AzureADTokenExchange"] + issuer = azurerm_kubernetes_cluster.example.oidc_issuer_url + subject = "system:serviceaccount:${var.k8s_namespace}:${azurerm_user_assigned_identity.example[0].name}" +} + +resource "azurerm_role_assignment" "aoai_mid" { + count = local.deploy_azure_openai && local.deploy_azure_workload_identity ? 1 : 0 + principal_id = azurerm_user_assigned_identity.example[0].principal_id + role_definition_name = "Cognitive Services OpenAI User" + scope = azurerm_cognitive_account.example[0].id +} + +resource "azurerm_role_assignment" "servicebus_mid" { + count = local.deploy_azure_servicebus && local.deploy_azure_workload_identity ? 1 : 0 + principal_id = azurerm_user_assigned_identity.example[0].principal_id + role_definition_name = "Azure Service Bus Data Owner" + scope = azurerm_servicebus_namespace.example[0].id +} + +resource "azurerm_cosmosdb_sql_role_definition" "cosmosdb" { + count = local.deploy_azure_cosmosdb && local.deploy_azure_workload_identity ? 1 : 0 + resource_group_name = azurerm_resource_group.example.name + account_name = azurerm_cosmosdb_account.example[0].name + name = "CosmosDBDataContributor - ${azurerm_user_assigned_identity.example[0].name}" + type = "CustomRole" + assignable_scopes = [azurerm_cosmosdb_account.example[0].id] + + permissions { + data_actions = [ + "Microsoft.DocumentDB/databaseAccounts/readMetadata", + "Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers/*", + "Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers/items/*", + ] + } +} + +resource "azurerm_cosmosdb_sql_role_assignment" "cosmosdb_mid" { + count = local.deploy_azure_cosmosdb && local.deploy_azure_workload_identity ? 1 : 0 + resource_group_name = azurerm_resource_group.example.name + account_name = azurerm_cosmosdb_account.example[0].name + role_definition_id = azurerm_cosmosdb_sql_role_definition.cosmosdb[0].id + scope = azurerm_cosmosdb_account.example[0].id + principal_id = azurerm_user_assigned_identity.example[0].principal_id +} diff --git a/kustomize/base/makeline-service.yaml b/kustomize/base/makeline-service.yaml index 29b1fd3d..a9711e41 100644 --- a/kustomize/base/makeline-service.yaml +++ b/kustomize/base/makeline-service.yaml @@ -41,6 +41,12 @@ spec: limits: cpu: 5m memory: 20Mi + startupProbe: + httpGet: + path: /health + port: 3001 + failureThreshold: 10 + periodSeconds: 5 readinessProbe: httpGet: path: /health diff --git a/src/ai-service/README.md b/src/ai-service/README.md index 9765a24b..99868282 100644 --- a/src/ai-service/README.md +++ b/src/ai-service/README.md @@ -23,10 +23,13 @@ source .venv/bin/activate pip install -r requirements.txt export USE_AZURE_OPENAI=True # set to False if you are not using Azure OpenAI -export USE_AZURE_AD=False # set to True if you are using Azure OpenAI with Azure AD authentication +export USE_AZURE_AD=True # set to True if you are using Azure OpenAI with Azure AD authentication +export AZURE_OPENAI_API_VERSION=2024-02-15-preview # set to the version of the Azure OpenAI API you are using https://learn.microsoft.com/azure/ai-services/openai/reference#rest-api-versioning export AZURE_OPENAI_DEPLOYMENT_NAME= # required if using Azure OpenAI export AZURE_OPENAI_ENDPOINT= # required if using Azure OpenAI -export OPENAI_API_KEY= # always required +export AZURE_OPENAI_DALLE_ENDPOINT= # required if using Azure OpenAI's DALL-E model +export AZURE_OPENAI_DALLE_DEPLOYMENT_NAME= # required if using Azure OpenAI's DALL-E model +export OPENAI_API_KEY= # always required if using OpenAI if using Azure OpenAI, consider use Workload Identity https://learn.microsoft.com/azure/aks/open-ai-secure-access-quickstart export OPENAI_ORG_ID= # required if using OpenAI uvicorn main:app --host 127.0.0.1 --port 5001 diff --git a/src/ai-service/main.py b/src/ai-service/main.py index db3cbb24..4021d13a 100644 --- a/src/ai-service/main.py +++ b/src/ai-service/main.py @@ -1,17 +1,27 @@ from fastapi import FastAPI, status from routers.description_generator import description +from routers.image_generator import image from fastapi.middleware.cors import CORSMiddleware from fastapi.responses import JSONResponse import os app = FastAPI(version=os.environ.get("APP_VERSION", "0.1.0")) app.include_router(description) +app.include_router(image) app.add_middleware(CORSMiddleware, allow_origins=["*"], allow_methods=["*"], allow_headers=["*"]) - @app.get("/health", summary="check if server is healthy", operation_id="health") -async def get_products(): +async def get_health(): """ Returns status code 200 """ - return JSONResponse(content={"status": 'ok', "version": app.version}, status_code=status.HTTP_200_OK) + # Initialize the array with "description" + capabilities = ["description"] + + # Check if the environment variable is set + if os.environ.get("AZURE_OPENAI_DALLE_ENDPOINT") and os.environ.get("AZURE_OPENAI_DALLE_DEPLOYMENT_NAME"): + # If it is, add "image" to the array + capabilities.append("image") + + print("Generative AI capabilities: ", ", ".join(capabilities)) + return JSONResponse(content={"status": 'ok', "version": app.version, "capabilities": capabilities}, status_code=status.HTTP_200_OK) diff --git a/src/ai-service/requirements.txt b/src/ai-service/requirements.txt index 33cf90fc..ace26d02 100644 --- a/src/ai-service/requirements.txt +++ b/src/ai-service/requirements.txt @@ -5,5 +5,8 @@ pytest==7.3.1 httpx pyyaml semantic-kernel==0.4.2.dev0 -azure.identity==1.14.0 -requests==2.31.0 \ No newline at end of file +azure.identity==1.16.0 +requests==2.31.0 + +openai==1.23.2 +pillow==10.3.0 \ No newline at end of file diff --git a/src/ai-service/routers/image_generator.py b/src/ai-service/routers/image_generator.py new file mode 100644 index 00000000..faed4b33 --- /dev/null +++ b/src/ai-service/routers/image_generator.py @@ -0,0 +1,55 @@ +from azure.identity import DefaultAzureCredential, get_bearer_token_provider +from openai import AzureOpenAI +from fastapi import APIRouter, Request, status +from fastapi.responses import Response, JSONResponse +from typing import Any, List, Dict +import json +import os + +# Define the image API router +image: APIRouter = APIRouter(prefix="/generate", tags=["generate"]) + +# Define the Product class +class Product: + def __init__(self, product: Dict[str, List]) -> None: + self.name: str = product["name"] + self.description: List[str] = product["description"] + +# Define the post_image endpoint +@image.post("/image", summary="Get image for a product", operation_id="getImage") +async def post_image(request: Request) -> JSONResponse: + try: + # Parse the request body and create a Product object + body: dict = await request.json() + product: Product = Product(body) + name: str = product.name + description: List = product.description + + print("Calling OpenAI") + + api_version = os.environ.get("AZURE_OPENAI_API_VERSION") + endpoint = os.environ.get("AZURE_OPENAI_DALLE_ENDPOINT") + model_deployment_name = os.environ.get("AZURE_OPENAI_DALLE_DEPLOYMENT_NAME") + + token_provider = get_bearer_token_provider(DefaultAzureCredential(), "https://cognitiveservices.azure.com/.default") + + client = AzureOpenAI( + api_version=api_version, + azure_endpoint=endpoint, + azure_ad_token_provider=token_provider, + ) + + result = client.images.generate( + model=model_deployment_name, + prompt="Generate a cute photo realistic image of a product in its packaging in front of a plain background for a product called <"+ name + "> with a description <" + description + "> to be sold in an online pet supply store", + n=1 + ) + + json_response = json.loads(result.model_dump_json()) + print(json_response) + + # Return the image as a JSON response + return JSONResponse(content={"image": json_response["data"][0]["url"]}, status_code=status.HTTP_200_OK) + except Exception as e: + # Return an error message as a JSON response + return JSONResponse(content={"error": str(e)}, status_code=status.HTTP_500_INTERNAL_SERVER_ERROR) \ No newline at end of file diff --git a/src/ai-service/test-ai-service.http b/src/ai-service/test-ai-service.http index 457d7ea4..de6b681c 100644 --- a/src/ai-service/test-ai-service.http +++ b/src/ai-service/test-ai-service.http @@ -11,3 +11,13 @@ Content-Type: application/json "name": "Seafarer's Tug Rope", "tags": ["toy","dog"] } + +### Generate product image +POST /generate/image +Host: localhost:5001 +Content-Type: application/json + +{ + "name": "Seafarer's Tug Rope", + "description": "Engage your pup in a game of tug-of-war with the Seafarer's Tug Rope. Made from durable materials, this toy is perfect for interactive playtime and bonding with your furry friend." +} diff --git a/src/makeline-service/README.md b/src/makeline-service/README.md index 9ab8cf18..69aec57c 100644 --- a/src/makeline-service/README.md +++ b/src/makeline-service/README.md @@ -32,7 +32,7 @@ export ORDER_QUEUE_NAME=orders ### Option 2: Azure Service Bus -To run this against Azure Service Bus, you will need to create a Service Bus namespace and a queue. You can do this using the Azure CLI. +To run this against Azure Service Bus, you will need to create a Service Bus namespace and a queue. You can do this using the Azure CLI. ```bash RGNAME= @@ -43,13 +43,38 @@ az servicebus namespace create --name --resource-group $RGNAME az servicebus queue create --name orders --namespace-name --resource-group $RGNAME ``` -Once you have created the Service Bus namespace and queue, you will need to create a shared access policy with the **Listen** permission for the namespace. +Once you have created the Service Bus namespace and queue, you will need to decide on the authentication method. You can create a shared access policy with the **Send** permission for the queue or use Microsoft Entra Workload Identity for a passwordless experience (this is the recommended approach). + +If you choose to use Workload Identity, you will need to assign the `Azure Service Bus Data Receiver` role to the identity that is running the app, which in this case will be your account. You can do this using the Azure CLI. + +```bash +PRINCIPALID=$(az ad signed-in-user show --query objectId -o tsv) +SERVICEBUSBID=$(az servicebus namespace show --name --resource-group --query id -o tsv) + +az role assignment create --role "Azure Service Bus Data Receiver" --assignee $PRINCIPALID --scope $SERVICEBUSBID +``` + +Next, get the hostname for the Azure Service Bus. + +```bash +HOSTNAME=$(az servicebus namespace show --name --resource-group --query serviceBusEndpoint -o tsv | sed 's/https:\/\///;s/:443\///') +``` + +Finally, set the environment variables. + +```bash +export ORDER_QUEUE_HOSTNAME=$HOSTNAME +export ORDER_QUEUE_NAME=orders +export USE_WORKLOAD_IDENTITY_AUTH=true +``` + +If you choose to use a shared access policy, you can create one using the Azure CLI. Otherwise, you can skip this step and proceed to [provision a database](#database-options). ```bash az servicebus namespace authorization-rule create --name listener --namespace-name --resource-group $RGNAME --rights Listen ``` -Next, get the connection information for the Azure Service Bus queue and save the values to environment variables. +Next, get the connection information for the Azure Service Bus. ```bash HOSTNAME=$(az servicebus namespace show --name --resource-group $RGNAME --query serviceBusEndpoint -o tsv | sed 's/https:\/\///;s/:443\///') @@ -69,7 +94,7 @@ export ORDER_QUEUE_NAME=orders ## Database options -You also have the option to write orders to either MongoDB or Azure CosmosDB. +You also have the option to write orders to either MongoDB or Azure CosmosDB. ### Option 1: MongoDB @@ -83,9 +108,9 @@ export ORDER_DB_COLLECTION_NAME=orders ### Option 2: Azure CosmosDB -To run this against Azure CosmosDB, you will need to create the CosmosDB account, the database, and collection. You can do this using the Azure CLI. +To run this against Azure CosmosDB, you will need to create the CosmosDB account, the database, and collection/container. You can do this using the Azure CLI. -> Azure CosmosDB supports multiple APIs. This app supports both the MongoDB and SQL APIs. You will need to create the database and collection based on the API you want to use. +Run the following command to create the Azure CosmosDB Account. ```bash RGNAME= @@ -93,41 +118,90 @@ LOCNAME= COSMOSDBNAME= az group create --name $RGNAME --location $LOCNAME +``` -# if database requires MongoDB API -# create the database and collection -az cosmosdb create --name $COSMOSDBNAME --resource-group $RGNAME --kind MongoDB -az cosmosdb mongodb database create --account-name $COSMOSDBNAME --name orderdb --resource-group $RGNAME -az cosmosdb mongodb collection create --account-name $COSMOSDBNAME --database-name orderdb --name orders --resource-group $RGNAME +Also note that Azure CosmosDB supports multiple APIs. This app supports both the MongoDB and SQL APIs. Choosing one model over the other has significant implications on the authentication method your app can use. + +If you choose to use the MongoDB API, then you are limited to using account keys to authenticate. If you choose to use the SQL API, then you can use Microsoft Entra Workload Identity for a passwordless experience (this is the recommended approach). + +If you choose to use Workload Identity, you will need to do the following tasks: + +1. Assign the built-in `DocumentDB Account Contributor` role to the identity that is running the app, which in this case will be your account. +1. Create a custom role for reading and writing data using Azure RBAC and assign it to the identity that is running the app, which in this case will be your account. +1. Disable the account key authentication method for the CosmosDB account (this is highly recommended). + +You can do this using the Azure CLI. + +```bash +PRINCIPALID=$(az ad signed-in-user show --query objectId -o tsv) +COSMOSDBID=$(az cosmosdb show --name $COSMOSDBNAME --resource-group $RGNAME --query id -o tsv) +ROLEID=$(az role definition list -n "DocumentDB Account Contributor" --query "[].id" -o tsv) + +# grant yourself the DocumentDB Account Contributor role +az role assignment create --role $ROLEID --assignee $PRINCIPALID --scope $COSMOSDBID + +# create a custom role for reading and writing data +cat < customRole.json +{ + "RoleName": "MyCosmosDBDataContributor", + "Type": "CustomRole", + "AssignableScopes": ["${COSMOSDBID}"], + "Permissions": [{ + "DataActions": [ + "Microsoft.DocumentDB/databaseAccounts/readMetadata", + "Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers/*", + "Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers/items/*", + ] + }] +} +EOF +ROLEDEFINITIONID=$(az cosmosdb sql role definition create --account-name $COSMOSDBNAME --resource-group $RGNAME --body @customRole.json --query id -o tsv) + +# assign the custom role to yourself +az cosmosdb sql role assignment create --account-name $COSMOSDBNAME --resource-group $RGNAME --scope $COSMOSDBID--principal-id $PRINCIPALID --role-definition-id $ROLEDEFINITIONID + +# disable account key authentication +az cosmosdb update --name $COSMOSDBNAME --resource-group $RGNAME --disable-key-based-metadata-write-access +``` + +If you are using the SQL API, you will need to create the database and container. Run the following commands to create the database and container. -# if database requires SQL API -# create the database and container +```bash COSMOSDBPARTITIONKEY=storeId az cosmosdb create --name $COSMOSDBNAME --resource-group $RGNAME --kind GlobalDocumentDB az cosmosdb sql database create --account-name $COSMOSDBNAME --name orderdb --resource-group $RGNAME az cosmosdb sql container create --account-name $COSMOSDBNAME --database-name orderdb --name orders --resource-group $RGNAME --partition-key-path /$COSMOSDBPARTITIONKEY ``` -Next, get the connection information for the Azure Service Bus queue and save the values to environment variables. +If you are using the MongoDB API, you will need to create the database and collection. Run the following commands to create the database and collection. + +```bash +az cosmosdb create --name $COSMOSDBNAME --resource-group $RGNAME --kind MongoDB +az cosmosdb mongodb database create --account-name $COSMOSDBNAME --name orderdb --resource-group $RGNAME +az cosmosdb mongodb collection create --account-name $COSMOSDBNAME --database-name orderdb --name orders --resource-group $RGNAME +``` + +If you are not using Workload Identity authentication, get the connection information for the Azure Service Bus queue and save the values to environment variables. Otherwise, skip this step. ```bash COSMOSDBUSERNAME=$COSMOSDBNAME COSMOSDBPASSWORD=$(az cosmosdb keys list --name $COSMOSDBNAME --resource-group $RGNAME --query primaryMasterKey -o tsv) -``` +```` Finally, set the environment variables. ```bash -# if database requires MongoDB API +# if database requires SQL API with Workload Identity # set the following environment variables -export ORDER_DB_API=mongodb -export ORDER_DB_URI=mongodb://$COSMOSDBNAME.mongo.cosmos.azure.com:10255/?retryWrites=false +export ORDER_DB_API=cosmosdbsql +export ORDER_DB_URI=https://$COSMOSDBNAME.documents.azure.com:443/ export ORDER_DB_NAME=orderdb -export ORDER_DB_COLLECTION_NAME=orders -export ORDER_DB_USERNAME=$COSMOSDBUSERNAME -export ORDER_DB_PASSWORD=$COSMOSDBPASSWORD +export ORDER_DB_CONTAINER_NAME=orders +export USE_WORKLOAD_IDENTITY_AUTH="true" +export ORDER_DB_PARTITION_KEY=$COSMOSDBPARTITIONKEY +export ORDER_DB_PARTITION_VALUE="pets" -# if database requires SQL API +# if database requires SQL API with account key # set the following environment variables export ORDER_DB_API=cosmosdbsql export ORDER_DB_URI=https://$COSMOSDBNAME.documents.azure.com:443/ @@ -136,6 +210,15 @@ export ORDER_DB_CONTAINER_NAME=orders export ORDER_DB_PASSWORD=$COSMOSDBPASSWORD export ORDER_DB_PARTITION_KEY=$COSMOSDBPARTITIONKEY export ORDER_DB_PARTITION_VALUE="pets" + +# if database requires MongoDB API with account key +# set the following environment variables +export ORDER_DB_API=mongodb +export ORDER_DB_URI=mongodb://$COSMOSDBNAME.mongo.cosmos.azure.com:10255/?retryWrites=false +export ORDER_DB_NAME=orderdb +export ORDER_DB_COLLECTION_NAME=orders +export ORDER_DB_USERNAME=$COSMOSDBUSERNAME +export ORDER_DB_PASSWORD=$COSMOSDBPASSWORD ``` > NOTE: With Azure CosmosDB, you must ensure the orderdb database and an unsharded orders collection exist before running the app. Otherwise you will get a "server selection error". diff --git a/src/makeline-service/cosmosdb.go b/src/makeline-service/cosmosdb.go index 2989985e..05a91598 100644 --- a/src/makeline-service/cosmosdb.go +++ b/src/makeline-service/cosmosdb.go @@ -6,6 +6,7 @@ import ( "log" "strings" + "github.com/Azure/azure-sdk-for-go/sdk/azidentity" "github.com/Azure/azure-sdk-for-go/sdk/data/azcosmos" "github.com/gofrs/uuid" ) @@ -20,6 +21,33 @@ type CosmosDBOrderRepo struct { partitionKey PartitionKey } +func NewCosmosDBOrderRepoWithManagedIdentity(cosmosDbEndpoint string, dbName string, containerName string, partitionKey PartitionKey) (*CosmosDBOrderRepo, error) { + cred, err := azidentity.NewDefaultAzureCredential(nil) + if err != nil { + log.Printf("failed to create cosmosdb workload identity credential: %v\n", err) + return nil, err + } + + opts := azcosmos.ClientOptions{ + EnableContentResponseOnWrite: true, + } + + client, err := azcosmos.NewClient(cosmosDbEndpoint, cred, &opts) + if err != nil { + log.Printf("failed to create cosmosdb client: %v\n", err) + return nil, err + } + + // create a cosmos container + container, err := client.NewContainer(dbName, containerName) + if err != nil { + log.Printf("failed to create cosmosdb container: %v\n", err) + return nil, err + } + + return &CosmosDBOrderRepo{container, partitionKey}, nil +} + func NewCosmosDBOrderRepo(cosmosDbEndpoint string, dbName string, containerName string, cosmosDbKey string, partitionKey PartitionKey) (*CosmosDBOrderRepo, error) { cred, err := azcosmos.NewKeyCredential(cosmosDbKey) if err != nil { diff --git a/src/makeline-service/go.mod b/src/makeline-service/go.mod index 25b6b086..b5d52aaa 100644 --- a/src/makeline-service/go.mod +++ b/src/makeline-service/go.mod @@ -3,8 +3,10 @@ module aks-store-demo/makeline-service go 1.20 require ( + github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.5.2 github.com/Azure/azure-sdk-for-go/sdk/data/azcosmos v0.3.6 - github.com/Azure/go-amqp v1.0.1 + github.com/Azure/azure-sdk-for-go/sdk/messaging/azservicebus v1.7.0 + github.com/Azure/go-amqp v1.0.5 github.com/gin-contrib/cors v1.4.0 github.com/gin-gonic/gin v1.9.1 github.com/gofrs/uuid v4.4.0+incompatible @@ -13,8 +15,9 @@ require ( require ( github.com/Azure/azure-sdk-for-go v68.0.0+incompatible // indirect - github.com/Azure/azure-sdk-for-go/sdk/azcore v1.9.0 // indirect - github.com/Azure/azure-sdk-for-go/sdk/internal v1.5.1 // indirect + github.com/Azure/azure-sdk-for-go/sdk/azcore v1.11.0 // indirect + github.com/Azure/azure-sdk-for-go/sdk/internal v1.5.2 // indirect + github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2 // indirect github.com/bytedance/sonic v1.9.1 // indirect github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect github.com/gabriel-vasile/mimetype v1.4.2 // indirect @@ -23,16 +26,20 @@ require ( github.com/go-playground/universal-translator v0.18.1 // indirect github.com/go-playground/validator/v10 v10.16.0 // indirect github.com/goccy/go-json v0.10.2 // indirect + github.com/golang-jwt/jwt/v5 v5.2.1 // indirect github.com/golang/snappy v0.0.1 // indirect + github.com/google/uuid v1.6.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/compress v1.13.6 // indirect github.com/klauspost/cpuid/v2 v2.2.4 // indirect + github.com/kylelemons/godebug v1.1.0 // indirect github.com/leodido/go-urn v1.2.4 // indirect github.com/mattn/go-isatty v0.0.19 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect - github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe // indirect + github.com/montanaflynn/stats v0.7.0 // indirect github.com/pelletier/go-toml/v2 v2.0.8 // indirect + github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect github.com/pkg/errors v0.9.1 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/ugorji/go/codec v1.2.11 // indirect @@ -41,10 +48,10 @@ require ( github.com/xdg-go/stringprep v1.0.3 // indirect github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d // indirect golang.org/x/arch v0.3.0 // indirect - golang.org/x/crypto v0.17.0 // indirect - golang.org/x/net v0.19.0 // indirect + golang.org/x/crypto v0.21.0 // indirect + golang.org/x/net v0.22.0 // indirect golang.org/x/sync v0.0.0-20210220032951-036812b2e83c // indirect - golang.org/x/sys v0.15.0 // indirect + golang.org/x/sys v0.18.0 // indirect golang.org/x/text v0.14.0 // indirect google.golang.org/protobuf v1.33.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/src/makeline-service/go.sum b/src/makeline-service/go.sum index 11a52400..ad3b25b6 100644 --- a/src/makeline-service/go.sum +++ b/src/makeline-service/go.sum @@ -2,14 +2,26 @@ github.com/Azure/azure-sdk-for-go v68.0.0+incompatible h1:fcYLmCpyNYRnvJbPerq7U0 github.com/Azure/azure-sdk-for-go v68.0.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc= github.com/Azure/azure-sdk-for-go/sdk/azcore v1.9.0 h1:fb8kj/Dh4CSwgsOzHeZY4Xh68cFVbzXx+ONXGMY//4w= github.com/Azure/azure-sdk-for-go/sdk/azcore v1.9.0/go.mod h1:uReU2sSxZExRPBAg3qKzmAucSi51+SP1OhohieR821Q= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.11.0 h1:U/kwEXj0Y+1REAkV4kV8VO1CsEp8tSaQDG/7qC5XuqQ= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.11.0/go.mod h1:a6xsAQUZg+VsS3TJ05SRp524Hs4pZ/AeFSr5ENf0Yjo= github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.0.0 h1:Yoicul8bnVdQrhDMTHxdEckRGX01XvwXDHUT9zYZ3k0= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.5.2 h1:FDif4R1+UUR+00q6wquyX90K7A8dN+R5E8GEadoP7sU= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.5.2/go.mod h1:aiYBYui4BJ/BJCAIKs92XiPyQfTaBWqvHujDwKb6CBU= github.com/Azure/azure-sdk-for-go/sdk/data/azcosmos v0.3.6 h1:oBqQLSI1pZwGOdXJAoJJSzmff9tlfD4KroVfjQQmd0g= github.com/Azure/azure-sdk-for-go/sdk/data/azcosmos v0.3.6/go.mod h1:Beh5cHIXJ0oWEDWk9lNFtuklCojLLQ5hl+LqSNTTs0I= github.com/Azure/azure-sdk-for-go/sdk/internal v1.5.1 h1:6oNBlSdi1QqM1PNW7FPA6xOGA5UNsXnkaYZz9vdPGhA= github.com/Azure/azure-sdk-for-go/sdk/internal v1.5.1/go.mod h1:s4kgfzA0covAXNicZHDMN58jExvcng2mC/DepXiF1EI= +github.com/Azure/azure-sdk-for-go/sdk/internal v1.5.2 h1:LqbJ/WzJUwBf8UiaSzgX7aMclParm9/5Vgp+TY51uBQ= +github.com/Azure/azure-sdk-for-go/sdk/internal v1.5.2/go.mod h1:yInRyqWXAuaPrgI7p70+lDDgh3mlBohis29jGMISnmc= +github.com/Azure/azure-sdk-for-go/sdk/messaging/azservicebus v1.7.0 h1:QISzMrspEvZj4zrrN2wlNwfum5RmnKQhQNiSujwH7oU= +github.com/Azure/azure-sdk-for-go/sdk/messaging/azservicebus v1.7.0/go.mod h1:xNjFERdhyMqZncbNJSPBsTCddk5kwsUVUzELQPMj/LA= github.com/Azure/go-amqp v1.0.1 h1:Jf8OQCKzRDMZ3pCiH4onM7yrhl5curkRSGkRLTyP35o= github.com/Azure/go-amqp v1.0.1/go.mod h1:+bg0x3ce5+Q3ahCEXnCsGG3ETpDQe3MEVnOuT2ywPwc= +github.com/Azure/go-amqp v1.0.5 h1:po5+ljlcNSU8xtapHTe8gIc8yHxCzC03E8afH2g1ftU= +github.com/Azure/go-amqp v1.0.5/go.mod h1:vZAogwdrkbyK3Mla8m/CxSc/aKdnTZ4IbPxl51Y5WZE= github.com/AzureAD/microsoft-authentication-library-for-go v0.4.0 h1:WVsrXCnHlDDX8ls+tootqRE87/hL9S/g4ewig9RsD/c= +github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2 h1:XHOnouVk1mxXfQidrMEnLlPk9UMeRtyBTnEFtxkV0kU= +github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI= github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM= github.com/bytedance/sonic v1.9.1 h1:6iJ6NqdoxCDr6mbY8h18oSO+cShGSMRGCEo7F2h0x8s= github.com/bytedance/sonic v1.9.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U= @@ -47,6 +59,8 @@ github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MG github.com/gofrs/uuid v4.4.0+incompatible h1:3qXRTX8/NbyulANqlc0lchS1gqAVxRgsuW1YrTJupqA= github.com/gofrs/uuid v4.4.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= github.com/golang-jwt/jwt v3.2.1+incompatible h1:73Z+4BJcrTC+KczS6WvTPvRGOp1WmfEP4Q1lOd9Z/+c= +github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk= +github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/golang/snappy v0.0.1 h1:Qgr9rKW7uDUkrbSmQeiDsGa8SjGyCOGtuasMWwvp2P4= github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= @@ -55,6 +69,8 @@ github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/klauspost/compress v1.13.6 h1:P76CopJELS0TiO2mebmnzgWaajssP/EszplttgQxcgc= @@ -71,6 +87,7 @@ github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY= github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q= github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4= @@ -84,10 +101,14 @@ github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9G github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe h1:iruDEfMl2E6fbMZ9s0scYfZQ84/6SPL6zC8ACM2oIL0= github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc= +github.com/montanaflynn/stats v0.7.0 h1:r3y12KyNxj/Sb/iOE46ws+3mS1+MZca1wlHQFPsY/JU= +github.com/montanaflynn/stats v0.7.0/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow= github.com/pelletier/go-toml/v2 v2.0.1/go.mod h1:r9LEWfGN8R5k0VXJ+0BkIe7MYkRdwZOjgMj2KwnJFUo= github.com/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ= github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4= github.com/pkg/browser v0.0.0-20210115035449-ce105d075bb4 h1:Qj1ukM4GlMWXNdMBuXcXfz/Kw9s1qm0CLY32QxuSImI= +github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ= +github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= @@ -133,10 +154,14 @@ golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5y golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k= golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= +golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA= +golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c= golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U= +golang.org/x/net v0.22.0 h1:9sGLhx7iRIHEiX0oAJ3MRZMUCElJgy7Br1nO+AMN3Tc= +golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -145,9 +170,12 @@ golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= +golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= diff --git a/src/makeline-service/main.go b/src/makeline-service/main.go index 9e314dc2..e3778064 100644 --- a/src/makeline-service/main.go +++ b/src/makeline-service/main.go @@ -165,31 +165,57 @@ func updateOrder(c *gin.Context) { } // Gets an environment variable or exits if it is not set -func getEnvVar(varName string) string { +func getEnvVar(varName string, fallbackVarNames ...string) string { value := os.Getenv(varName) if value == "" { - log.Printf("%s is not set", varName) - os.Exit(1) + for _, fallbackVarName := range fallbackVarNames { + value = os.Getenv(fallbackVarName) + if value == "" { + break + } + } + if value == "" { + log.Printf("%s is not set", varName) + if len(fallbackVarNames) > 0 { + log.Printf("Tried fallback variables: %v", fallbackVarNames) + } + os.Exit(1) + } } return value } // Initializes the database based on the API type func initDatabase(apiType string) (*OrderService, error) { - dbURI := getEnvVar("ORDER_DB_URI") + dbURI := getEnvVar("AZURE_COSMOS_RESOURCEENDPOINT", "ORDER_DB_URI") dbName := getEnvVar("ORDER_DB_NAME") switch apiType { case AZURE_COSMOS_DB_SQL_API: containerName := getEnvVar("ORDER_DB_CONTAINER_NAME") - dbPassword := os.Getenv("ORDER_DB_PASSWORD") dbPartitionKey := getEnvVar("ORDER_DB_PARTITION_KEY") dbPartitionValue := getEnvVar("ORDER_DB_PARTITION_VALUE") - cosmosRepo, err := NewCosmosDBOrderRepo(dbURI, dbName, containerName, dbPassword, PartitionKey{dbPartitionKey, dbPartitionValue}) - if err != nil { - return nil, err + + // check if USE_WORKLOAD_IDENTITY_AUTH is set + useWorkloadIdentityAuth := os.Getenv("USE_WORKLOAD_IDENTITY_AUTH") + if useWorkloadIdentityAuth == "" { + useWorkloadIdentityAuth = "false" + } + + if useWorkloadIdentityAuth == "true" { + cosmosRepo, err := NewCosmosDBOrderRepoWithManagedIdentity(dbURI, dbName, containerName, PartitionKey{dbPartitionKey, dbPartitionValue}) + if err != nil { + return nil, err + } + return NewOrderService(cosmosRepo), nil + } else { + dbPassword := os.Getenv("ORDER_DB_PASSWORD") + cosmosRepo, err := NewCosmosDBOrderRepo(dbURI, dbName, containerName, dbPassword, PartitionKey{dbPartitionKey, dbPartitionValue}) + if err != nil { + return nil, err + } + return NewOrderService(cosmosRepo), nil } - return NewOrderService(cosmosRepo), nil default: collectionName := getEnvVar("ORDER_DB_COLLECTION_NAME") dbUsername := os.Getenv("ORDER_DB_USERNAME") diff --git a/src/makeline-service/orderqueue.go b/src/makeline-service/orderqueue.go index bdb43482..5db7848b 100644 --- a/src/makeline-service/orderqueue.go +++ b/src/makeline-service/orderqueue.go @@ -4,24 +4,22 @@ import ( "context" "encoding/json" "errors" + "fmt" "log" "math/rand" "os" "strconv" "time" + "github.com/Azure/azure-sdk-for-go/sdk/azidentity" + "github.com/Azure/azure-sdk-for-go/sdk/messaging/azservicebus" "github.com/Azure/go-amqp" ) func getOrdersFromQueue() ([]Order, error) { - var orders []Order + ctx := context.Background() - // Get order queue connection string from environment variable - orderQueueUri := os.Getenv("ORDER_QUEUE_URI") - if orderQueueUri == "" { - log.Printf("ORDER_QUEUE_URI is not set") - return nil, errors.New("ORDER_QUEUE_URI is not set") - } + var orders []Order // Get queue name from environment variable orderQueueName := os.Getenv("ORDER_QUEUE_NAME") @@ -30,98 +28,172 @@ func getOrdersFromQueue() ([]Order, error) { return nil, errors.New("ORDER_QUEUE_URI is not set") } - // Get queue username from environment variable - orderQueueUsername := os.Getenv("ORDER_QUEUE_USERNAME") - if orderQueueName == "" { - log.Printf("ORDER_QUEUE_USERNAME is not set") - return nil, errors.New("ORDER_QUEUE_URI is not set") + // check if USE_WORKLOAD_IDENTITY_AUTH is set + useWorkloadIdentityAuth := os.Getenv("USE_WORKLOAD_IDENTITY_AUTH") + if useWorkloadIdentityAuth == "" { + useWorkloadIdentityAuth = "false" } - // Get queue password from environment variable - orderQueuePassword := os.Getenv("ORDER_QUEUE_PASSWORD") - if orderQueuePassword == "" { - log.Printf("ORDER_QUEUE_PASSWORD is not set") - return nil, errors.New("ORDER_QUEUE_URI is not set") + orderQueueHostName := os.Getenv("AZURE_SERVICEBUS_FULLYQUALIFIEDNAMESPACE") + if orderQueueHostName == "" { + orderQueueHostName = os.Getenv("ORDER_QUEUE_HOSTNAME") } - ctx := context.Background() + if orderQueueHostName != "" && useWorkloadIdentityAuth == "true" { + cred, err := azidentity.NewDefaultAzureCredential(nil) + if err != nil { + log.Fatalf("failed to obtain a workload identity credential: %v", err) + } - // Connect to order queue - conn, err := amqp.Dial(ctx, orderQueueUri, &amqp.ConnOptions{ - SASLType: amqp.SASLTypePlain(orderQueueUsername, orderQueuePassword), - }) - if err != nil { - log.Printf("%s: %s", "Failed to connect to order queue", err) - return nil, errors.New("ORDER_QUEUE_URI is not set") - } - defer conn.Close() + client, err := azservicebus.NewClient(orderQueueHostName, cred, nil) + if err != nil { + log.Fatalf("failed to obtain a service bus client with workload identity credential: %v", err) + } else { + fmt.Println("successfully created a service bus client with workload identity credentials") + } - session, err := conn.NewSession(ctx, nil) - if err != nil { - log.Printf("Unable to create a new session") - } + receiver, err := client.NewReceiverForQueue(orderQueueName, nil) + if err != nil { + log.Fatalf("failed to create receiver: %v", err) + } + defer receiver.Close(context.TODO()) - { - // create a receiver - receiver, err := session.NewReceiver(ctx, orderQueueName, nil) + messages, err := receiver.ReceiveMessages(context.TODO(), 10, nil) if err != nil { - log.Printf("Creating receiver link: %s", err) - return nil, errors.New("ORDER_QUEUE_URI is not set") + log.Fatalf("failed to receive messages: %v", err) } - defer func() { - ctx, cancel := context.WithTimeout(ctx, 1*time.Second) - receiver.Close(ctx) - cancel() - }() - for { - log.Printf("getting orders") + for _, message := range messages { + log.Printf("message received: %s\n", string(message.Body)) - ctx, cancel := context.WithTimeout(ctx, 1*time.Second) - defer cancel() + // First, unmarshal the JSON data into a string + var jsonStr string + err = json.Unmarshal(message.Body, &jsonStr) + if err != nil { + log.Printf("failed to deserialize message: %s", err) + return nil, err + } - // receive next message - msg, err := receiver.Receive(ctx, nil) + // Then, unmarshal the string into an Order + order, err := unmarshalOrderFromQueue([]byte(jsonStr)) if err != nil { - if err.Error() == "context deadline exceeded" { - log.Printf("No more orders for you: %v", err.Error()) - break - } else { - return nil, errors.New("ORDER_QUEUE_URI is not set") - } + log.Printf("failed to unmarshal message: %v", err) + return nil, err } - messageBody := string(msg.GetData()) - log.Printf("Message received: %s\n", messageBody) + // Add order to []order slice + orders = append(orders, order) - // Create a random string to use as the order key - orderKey := strconv.Itoa(rand.Intn(100000)) + err = receiver.CompleteMessage(context.TODO(), message, nil) + if err != nil { + log.Fatalf("failed to complete message: %v", err) + } + } + } else { + // Get order queue connection string from environment variable + orderQueueUri := os.Getenv("ORDER_QUEUE_URI") + if orderQueueUri == "" { + log.Printf("ORDER_QUEUE_URI is not set") + return nil, errors.New("ORDER_QUEUE_URI is not set") + } - // Deserialize msg data to order and add to []order slice - var order Order - err = json.Unmarshal(msg.GetData(), &order) + // Get queue username from environment variable + orderQueueUsername := os.Getenv("ORDER_QUEUE_USERNAME") + if orderQueueName == "" { + log.Printf("ORDER_QUEUE_USERNAME is not set") + return nil, errors.New("ORDER_QUEUE_USERNAME is not set") + } + + // Get queue password from environment variable + orderQueuePassword := os.Getenv("ORDER_QUEUE_PASSWORD") + if orderQueuePassword == "" { + log.Printf("ORDER_QUEUE_PASSWORD is not set") + return nil, errors.New("ORDER_QUEUE_PASSWORD is not set") + } + + // Connect to order queue + conn, err := amqp.Dial(ctx, orderQueueUri, &amqp.ConnOptions{ + SASLType: amqp.SASLTypePlain(orderQueueUsername, orderQueuePassword), + }) + if err != nil { + log.Printf("%s: %s", "failed to connect to order queue", err) + return nil, err + } + defer conn.Close() + + session, err := conn.NewSession(ctx, nil) + if err != nil { + log.Printf("unable to create a new session") + } + { + // create a receiver + receiver, err := session.NewReceiver(ctx, orderQueueName, nil) if err != nil { - log.Printf("Failed to deserialize message: %s", err) - return nil, errors.New("ORDER_QUEUE_URI is not set") + log.Printf("creating receiver link: %s", err) + return nil, err } + defer func() { + ctx, cancel := context.WithTimeout(ctx, 1*time.Second) + receiver.Close(ctx) + cancel() + }() + + for { + log.Printf("getting orders") + + ctx, cancel := context.WithTimeout(ctx, 1*time.Second) + defer cancel() + + // receive next message + msg, err := receiver.Receive(ctx, nil) + if err != nil { + if err.Error() == "context deadline exceeded" { + log.Printf("no more orders for you: %v", err.Error()) + break + } else { + return nil, err + } + } - // add orderkey to order - order.OrderID = orderKey + messageBody := string(msg.GetData()) + log.Printf("message received: %s\n", messageBody) - // set the status to pending - order.Status = Pending + order, err := unmarshalOrderFromQueue(msg.GetData()) + if err != nil { + log.Printf("failed to unmarshal message: %s", err) + return nil, err + } - // Add order to []order slice - orders = append(orders, order) + // Add order to []order slice + orders = append(orders, order) - // accept message - if err = receiver.AcceptMessage(context.TODO(), msg); err != nil { - log.Printf("Failure accepting message: %s", err) - // remove the order from the slice so that we pick it up on the next run - orders = orders[:len(orders)-1] + // accept message + if err = receiver.AcceptMessage(context.TODO(), msg); err != nil { + log.Printf("failure accepting message: %s", err) + // remove the order from the slice so that we pick it up on the next run + orders = orders[:len(orders)-1] + } } } } return orders, nil } + +func unmarshalOrderFromQueue(data []byte) (Order, error) { + var order Order + + err := json.Unmarshal(data, &order) + if err != nil { + log.Printf("failed to unmarshal order: %v\n", err) + return Order{}, err + } + + // add orderkey to order + order.OrderID = strconv.Itoa(rand.Intn(100000)) + + // set the status to pending + order.Status = Pending + + return order, nil +} diff --git a/src/order-service/README.md b/src/order-service/README.md index 44b4cb02..44b23104 100644 --- a/src/order-service/README.md +++ b/src/order-service/README.md @@ -50,14 +50,46 @@ az servicebus namespace create --name --resource-group --resource-group ``` -Once you have created the Service Bus namespace and queue, you will need to create a shared access policy with the **Send** permission for the queue. +Once you have created the Service Bus namespace and queue, you will need to decide on the authentication method. You can create a shared access policy with the **Send** permission for the queue or use Microsoft Entra Workload Identity for a passwordless experience (this is the recommended approach). + +If you choose to use Managed Identity, you will need to assign the `Azure Service Bus Data Sender` role to the identity that is running the app, which in this case will be your account. You can do this using the Azure CLI. ```bash -az servicebus queue authorization-rule create --name sender --namespace-name --resource-group --queue-name orders --rights Send +PRINCIPALID=$(az ad signed-in-user show --query objectId -o tsv) +SERVICEBUSBID=$(az servicebus namespace show --name --resource-group --query id -o tsv) + +az role assignment create --role "Azure Service Bus Data Sender" --assignee $PRINCIPALID --scope $SERVICEBUSBID ``` Next, get the connection information for the Azure Service Bus queue and save the values to environment variables. +Next, get the hostname for the Azure Service Bus queue. + +```bash +HOSTNAME=$(az servicebus namespace show --name --resource-group --query serviceBusEndpoint -o tsv | sed 's/https:\/\///;s/:443\///') +``` + +Finally, save the environment variables to a `.env` file. + +```bash +cat << EOF > .env +USE_WORKLOAD_IDENTITY_AUTH=true +AZURE_SERVICEBUS_FULLYQUALIFIEDNAMESPACE=$HOSTNAME +ORDER_QUEUE_NAME=orders +EOF + +# load the environment variables +source .env +``` + +If you choose to use a shared access policy, you can create one using the Azure CLI. Otherwise, you can skip this step and proceed to [running the app locally](#running-the-app-locally). + +```bash +az servicebus queue authorization-rule create --name sender --namespace-name --resource-group --queue-name orders --rights Send +``` + +Next, get the connection information for the Azure Service Bus queue. + ```bash HOSTNAME=$(az servicebus namespace show --name --resource-group --query serviceBusEndpoint -o tsv | sed 's/https:\/\///;s/:443\///') @@ -69,7 +101,6 @@ Finally, save the environment variables to a `.env` file. ```bash cat << EOF > .env ORDER_QUEUE_HOSTNAME=$HOSTNAME -ORDER_QUEUE_HOST=$HOSTNAME ORDER_QUEUE_PORT=5671 ORDER_QUEUE_USERNAME=sender ORDER_QUEUE_PASSWORD="$PASSWORD" diff --git a/src/order-service/package-lock.json b/src/order-service/package-lock.json index 5d6a8d7b..b744342f 100644 --- a/src/order-service/package-lock.json +++ b/src/order-service/package-lock.json @@ -9,6 +9,8 @@ "version": "1.0.0", "license": "ISC", "dependencies": { + "@azure/identity": "^4.1.0", + "@azure/service-bus": "^7.9.4", "@fastify/autoload": "^5.0.0", "@fastify/cors": "^8.3.0", "@fastify/sensible": "^4.1.0", @@ -34,6 +36,282 @@ "node": ">=6.0.0" } }, + "node_modules/@azure/abort-controller": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-1.1.0.tgz", + "integrity": "sha512-TrRLIoSQVzfAJX9H1JeFjzAoDGcoK1IYX1UImfceTZpsyYfWr09Ss1aHW1y5TrrR3iq6RZLBwJ3E24uwPhwahw==", + "dependencies": { + "tslib": "^2.2.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/@azure/core-amqp": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@azure/core-amqp/-/core-amqp-4.2.1.tgz", + "integrity": "sha512-3ysUGVs8WPf4W2D+6UK3pxIoqeLTKU3mnE0c19JAKhnurv/KyW0ACaytpUeJNo5yMVf8c67FRGlhVAyVIODy9w==", + "dependencies": { + "@azure/abort-controller": "^2.0.0", + "@azure/core-auth": "^1.3.0", + "@azure/core-util": "^1.1.1", + "@azure/logger": "^1.0.0", + "buffer": "^6.0.0", + "events": "^3.0.0", + "process": "^0.11.10", + "rhea": "^3.0.0", + "rhea-promise": "^3.0.0", + "tslib": "^2.2.0", + "util": "^0.12.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@azure/core-amqp/node_modules/@azure/abort-controller": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-2.1.2.tgz", + "integrity": "sha512-nBrLsEWm4J2u5LpAPjxADTlq3trDgVZZXHNKabeXZtpq3d3AbN/KGO82R87rdDz5/lYB024rtEf10/q0urNgsA==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@azure/core-auth": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/@azure/core-auth/-/core-auth-1.7.2.tgz", + "integrity": "sha512-Igm/S3fDYmnMq1uKS38Ae1/m37B3zigdlZw+kocwEhh5GjyKjPrXKO2J6rzpC1wAxrNil/jX9BJRqBshyjnF3g==", + "dependencies": { + "@azure/abort-controller": "^2.0.0", + "@azure/core-util": "^1.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@azure/core-auth/node_modules/@azure/abort-controller": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-2.1.2.tgz", + "integrity": "sha512-nBrLsEWm4J2u5LpAPjxADTlq3trDgVZZXHNKabeXZtpq3d3AbN/KGO82R87rdDz5/lYB024rtEf10/q0urNgsA==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@azure/core-client": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/@azure/core-client/-/core-client-1.9.2.tgz", + "integrity": "sha512-kRdry/rav3fUKHl/aDLd/pDLcB+4pOFwPPTVEExuMyaI5r+JBbMWqRbCY1pn5BniDaU3lRxO9eaQ1AmSMehl/w==", + "dependencies": { + "@azure/abort-controller": "^2.0.0", + "@azure/core-auth": "^1.4.0", + "@azure/core-rest-pipeline": "^1.9.1", + "@azure/core-tracing": "^1.0.0", + "@azure/core-util": "^1.6.1", + "@azure/logger": "^1.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@azure/core-client/node_modules/@azure/abort-controller": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-2.1.2.tgz", + "integrity": "sha512-nBrLsEWm4J2u5LpAPjxADTlq3trDgVZZXHNKabeXZtpq3d3AbN/KGO82R87rdDz5/lYB024rtEf10/q0urNgsA==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@azure/core-paging": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/@azure/core-paging/-/core-paging-1.6.2.tgz", + "integrity": "sha512-YKWi9YuCU04B55h25cnOYZHxXYtEvQEbKST5vqRga7hWY9ydd3FZHdeQF8pyh+acWZvppw13M/LMGx0LABUVMA==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@azure/core-rest-pipeline": { + "version": "1.15.2", + "resolved": "https://registry.npmjs.org/@azure/core-rest-pipeline/-/core-rest-pipeline-1.15.2.tgz", + "integrity": "sha512-BmWfpjc/QXc2ipHOh6LbUzp3ONCaa6xzIssTU0DwH9bbYNXJlGUL6tujx5TrbVd/QQknmS+vlQJGrCq2oL1gZA==", + "dependencies": { + "@azure/abort-controller": "^2.0.0", + "@azure/core-auth": "^1.4.0", + "@azure/core-tracing": "^1.0.1", + "@azure/core-util": "^1.3.0", + "@azure/logger": "^1.0.0", + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@azure/core-rest-pipeline/node_modules/@azure/abort-controller": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-2.1.2.tgz", + "integrity": "sha512-nBrLsEWm4J2u5LpAPjxADTlq3trDgVZZXHNKabeXZtpq3d3AbN/KGO82R87rdDz5/lYB024rtEf10/q0urNgsA==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@azure/core-tracing": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@azure/core-tracing/-/core-tracing-1.1.2.tgz", + "integrity": "sha512-dawW9ifvWAWmUm9/h+/UQ2jrdvjCJ7VJEuCJ6XVNudzcOwm53BFZH4Q845vjfgoUAM8ZxokvVNxNxAITc502YA==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@azure/core-util": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@azure/core-util/-/core-util-1.9.0.tgz", + "integrity": "sha512-AfalUQ1ZppaKuxPPMsFEUdX6GZPB3d9paR9d/TTL7Ow2De8cJaC7ibi7kWVlFAVPCYo31OcnGymc0R89DX8Oaw==", + "dependencies": { + "@azure/abort-controller": "^2.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@azure/core-util/node_modules/@azure/abort-controller": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-2.1.2.tgz", + "integrity": "sha512-nBrLsEWm4J2u5LpAPjxADTlq3trDgVZZXHNKabeXZtpq3d3AbN/KGO82R87rdDz5/lYB024rtEf10/q0urNgsA==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@azure/core-xml": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/@azure/core-xml/-/core-xml-1.4.2.tgz", + "integrity": "sha512-CW3MZhApe/S4iikbYKE7s83fjDBPIr2kpidX+hlGRwh7N4o1nIpQ/PfJTeioqhfqdMvRtheEl+ft64fyTaLNaA==", + "dependencies": { + "fast-xml-parser": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@azure/identity": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@azure/identity/-/identity-4.1.0.tgz", + "integrity": "sha512-BhYkF8Xr2gXjyDxocm0pc9RI5J5a1jw8iW0dw6Bx95OGdYbuMyFZrrwNw4eYSqQ2BB6FZOqpJP3vjsAqRcvDhw==", + "dependencies": { + "@azure/abort-controller": "^1.0.0", + "@azure/core-auth": "^1.5.0", + "@azure/core-client": "^1.4.0", + "@azure/core-rest-pipeline": "^1.1.0", + "@azure/core-tracing": "^1.0.0", + "@azure/core-util": "^1.3.0", + "@azure/logger": "^1.0.0", + "@azure/msal-browser": "^3.11.1", + "@azure/msal-node": "^2.6.6", + "events": "^3.0.0", + "jws": "^4.0.0", + "open": "^8.0.0", + "stoppable": "^1.1.0", + "tslib": "^2.2.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@azure/logger": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@azure/logger/-/logger-1.1.2.tgz", + "integrity": "sha512-l170uE7bsKpIU6B/giRc9i4NI0Mj+tANMMMxf7Zi/5cKzEqPayP7+X1WPrG7e+91JgY8N+7K7nF2WOi7iVhXvg==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@azure/msal-browser": { + "version": "3.13.0", + "resolved": "https://registry.npmjs.org/@azure/msal-browser/-/msal-browser-3.13.0.tgz", + "integrity": "sha512-fD906nmJei3yE7la6DZTdUtXKvpwzJURkfsiz9747Icv4pit77cegSm6prJTKLQ1fw4iiZzrrWwxnhMLrTf5gQ==", + "dependencies": { + "@azure/msal-common": "14.9.0" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@azure/msal-common": { + "version": "14.9.0", + "resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-14.9.0.tgz", + "integrity": "sha512-yzBPRlWPnTBeixxLNI3BBIgF5/bHpbhoRVuuDBnYjCyWRavaPUsKAHUDYLqpGkBLDciA6TCc6GOxN4/S3WiSxg==", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@azure/msal-node": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/@azure/msal-node/-/msal-node-2.7.0.tgz", + "integrity": "sha512-wXD8LkUvHICeSWZydqg6o8Yvv+grlBEcmLGu+QEI4FcwFendbTEZrlSygnAXXSOCVaGAirWLchca35qrgpO6Jw==", + "dependencies": { + "@azure/msal-common": "14.9.0", + "jsonwebtoken": "^9.0.0", + "uuid": "^8.3.0" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/@azure/service-bus": { + "version": "7.9.4", + "resolved": "https://registry.npmjs.org/@azure/service-bus/-/service-bus-7.9.4.tgz", + "integrity": "sha512-LxFx6gVpMmxfVpTUEN4++CfXejGCCFvuFNSBovz70upmvZ/urBmqQoPOriXwezEQtaEo/NlFMIjR9AnJDmQIdA==", + "dependencies": { + "@azure/abort-controller": "^1.0.0", + "@azure/core-amqp": "^4.1.1", + "@azure/core-auth": "^1.3.0", + "@azure/core-client": "^1.0.0", + "@azure/core-paging": "^1.4.0", + "@azure/core-rest-pipeline": "^1.1.0", + "@azure/core-tracing": "^1.0.0", + "@azure/core-util": "^1.1.1", + "@azure/core-xml": "^1.0.0", + "@azure/logger": "^1.0.0", + "@types/is-buffer": "^2.0.0", + "buffer": "^6.0.0", + "is-buffer": "^2.0.3", + "jssha": "^3.1.0", + "long": "^5.2.0", + "process": "^0.11.10", + "rhea-promise": "^3.0.0", + "tslib": "^2.2.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/@babel/code-frame": { "version": "7.22.13", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.13.tgz", @@ -692,6 +970,22 @@ "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==", "dev": true }, + "node_modules/@types/is-buffer": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@types/is-buffer/-/is-buffer-2.0.2.tgz", + "integrity": "sha512-G6OXy83Va+xEo8XgqAJYOuvOMxeey9xM5XKkvwJNmN8rVdcB+r15HvHsG86hl86JvU0y1aa7Z2ERkNFYWw9ySg==", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/node": { + "version": "20.12.7", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.7.tgz", + "integrity": "sha512-wq0cICSkRLVaf3UGLMGItu/PtdY7oaXaI/RVU+xliKVOtRna3PRY57ZDfztpDL0n11vfymMUnXv8QwYCO7L1wg==", + "dependencies": { + "undici-types": "~5.26.4" + } + }, "node_modules/abort-controller": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", @@ -708,6 +1002,17 @@ "resolved": "https://registry.npmjs.org/abstract-logging/-/abstract-logging-2.0.1.tgz", "integrity": "sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA==" }, + "node_modules/agent-base": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.1.tgz", + "integrity": "sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==", + "dependencies": { + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/aggregate-error": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", @@ -830,6 +1135,20 @@ "node": ">=8.0.0" } }, + "node_modules/available-typed-arrays": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", + "dependencies": { + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/avvio": { "version": "8.2.1", "resolved": "https://registry.npmjs.org/avvio/-/avvio-8.2.1.tgz", @@ -956,6 +1275,11 @@ "ieee754": "^1.2.1" } }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==" + }, "node_modules/buffer-from": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", @@ -977,6 +1301,24 @@ "node": ">=8" } }, + "node_modules/call-bind": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", + "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/camelcase": { "version": "5.3.1", "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", @@ -1203,6 +1545,30 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/define-lazy-prop": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz", + "integrity": "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==", + "engines": { + "node": ">=8" + } + }, "node_modules/depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -1231,6 +1597,14 @@ "url": "https://github.com/motdotla/dotenv?sponsor=1" } }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, "node_modules/electron-to-chromium": { "version": "1.4.417", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.417.tgz", @@ -1251,6 +1625,25 @@ "once": "^1.4.0" } }, + "node_modules/es-define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", + "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", + "dependencies": { + "get-intrinsic": "^1.2.4" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/es6-error": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz", @@ -1369,6 +1762,27 @@ "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-2.2.0.tgz", "integrity": "sha512-cIusKBIt/R/oI6z/1nyfe2FvGKVTohVRfvkOhvx0nCEW+xf5NoCXjAHcWp93uOUBchzYcsvPlrapAdX1uW+YGg==" }, + "node_modules/fast-xml-parser": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.3.6.tgz", + "integrity": "sha512-M2SovcRxD4+vC493Uc2GZVcZaj66CCJhWurC4viynVSTvrpErCShNcDz1lAho6n9REQKvL/ll4A4/fw6Y9z8nw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + }, + { + "type": "paypal", + "url": "https://paypal.me/naturalintelligence" + } + ], + "dependencies": { + "strnum": "^1.0.5" + }, + "bin": { + "fxparser": "src/cli/cli.js" + } + }, "node_modules/fastify": { "version": "4.17.0", "resolved": "https://registry.npmjs.org/fastify/-/fastify-4.17.0.tgz", @@ -1491,6 +1905,14 @@ "integrity": "sha512-ENZS237/Hr8bjczn5eKuBohLgaD0JyUd0arxretR1f9RO46vZHA1b2y0VorgGV3WaOT3c+78P8h7v4JGJ1i/rg==", "dev": true }, + "node_modules/for-each": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", + "integrity": "sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==", + "dependencies": { + "is-callable": "^1.1.3" + } + }, "node_modules/foreground-child": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-2.0.0.tgz", @@ -1556,6 +1978,14 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/function-loop": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/function-loop/-/function-loop-2.0.1.tgz", @@ -1594,6 +2024,24 @@ "node": "6.* || 8.* || >= 10.*" } }, + "node_modules/get-intrinsic": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", + "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3", + "hasown": "^2.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/get-package-type": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", @@ -1642,6 +2090,17 @@ "node": ">=4" } }, + "node_modules/gopd": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", + "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", + "dependencies": { + "get-intrinsic": "^1.1.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/graceful-fs": { "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", @@ -1656,6 +2115,53 @@ "node": ">=8" } }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz", + "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/hasha": { "version": "5.2.2", "resolved": "https://registry.npmjs.org/hasha/-/hasha-5.2.2.tgz", @@ -1672,6 +2178,17 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/help-me": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/help-me/-/help-me-2.0.1.tgz", @@ -1702,6 +2219,30 @@ "node": ">= 0.8" } }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.4.tgz", + "integrity": "sha512-wlwpilI7YdjSkWaQ/7omYBMTliDcmCN8OLihO6I9B86g06lMyAoqgoDpV0XqoaPOKj+0DIdAvnsWfyAAhmimcg==", + "dependencies": { + "agent-base": "^7.0.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/ieee754": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", @@ -1761,6 +2302,21 @@ "node": ">= 0.10" } }, + "node_modules/is-arguments": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.1.tgz", + "integrity": "sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==", + "dependencies": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-binary-path": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", @@ -1772,6 +2328,39 @@ "node": ">=8" } }, + "node_modules/is-buffer": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.5.tgz", + "integrity": "sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "engines": { + "node": ">=4" + } + }, + "node_modules/is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-docker": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", @@ -1803,6 +2392,20 @@ "node": ">=8" } }, + "node_modules/is-generator-function": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.0.10.tgz", + "integrity": "sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A==", + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-glob": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", @@ -1834,6 +2437,20 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/is-typed-array": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.13.tgz", + "integrity": "sha512-uZ25/bUAlUY5fR4OKT4rZQEBrzQWYV9ZJYGGsUmEJ6thodVJ1HX64ePQ6Z0qPWP+m+Uq6e9UugrE38jeYsDSMw==", + "dependencies": { + "which-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-typedarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", @@ -1849,6 +2466,17 @@ "node": ">=0.10.0" } }, + "node_modules/is-wsl": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", + "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", + "dependencies": { + "is-docker": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/isbinaryfile": { "version": "4.0.10", "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-4.0.10.tgz", @@ -2037,6 +2665,73 @@ "node": ">=6" } }, + "node_modules/jsonwebtoken": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", + "integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==", + "dependencies": { + "jws": "^3.2.2", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/jsonwebtoken/node_modules/jwa": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", + "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==", + "dependencies": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jsonwebtoken/node_modules/jws": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", + "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", + "dependencies": { + "jwa": "^1.4.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jssha": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/jssha/-/jssha-3.3.1.tgz", + "integrity": "sha512-VCMZj12FCFMQYcFLPRm/0lOBbLi8uM2BhXPTqw3U4YAfs4AZfiApOoBLoN8cQE60Z50m1MYMTQVCfgF/KaCVhQ==", + "engines": { + "node": "*" + } + }, + "node_modules/jwa": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.0.tgz", + "integrity": "sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA==", + "dependencies": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz", + "integrity": "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==", + "dependencies": { + "jwa": "^2.0.0", + "safe-buffer": "^5.0.1" + } + }, "node_modules/leven": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", @@ -2100,6 +2795,46 @@ "integrity": "sha512-uHaJFihxmJcEX3kT4I23ABqKKalJ/zDrDg0lsFtc1h+3uw49SIJ5beyhx5ExVRti3AvKoOJngIj7xz3oylPdWQ==", "dev": true }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==" + }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==" + }, + "node_modules/long": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/long/-/long-5.2.3.tgz", + "integrity": "sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q==" + }, "node_modules/lru-cache": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", @@ -2362,6 +3097,22 @@ "wrappy": "1" } }, + "node_modules/open": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/open/-/open-8.4.2.tgz", + "integrity": "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==", + "dependencies": { + "define-lazy-prop": "^2.0.0", + "is-docker": "^2.1.1", + "is-wsl": "^2.2.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/opener": { "version": "1.5.2", "resolved": "https://registry.npmjs.org/opener/-/opener-1.5.2.tgz", @@ -2716,6 +3467,14 @@ "node": ">=8" } }, + "node_modules/possible-typed-array-names": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.0.0.tgz", + "integrity": "sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q==", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/process": { "version": "0.11.10", "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", @@ -2880,6 +3639,24 @@ "debug": "^4.3.3" } }, + "node_modules/rhea-promise": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/rhea-promise/-/rhea-promise-3.0.1.tgz", + "integrity": "sha512-Fcqgml7lgoyi7fH1ClsSyFr/xwToijEN3rULFgrIcL+7EHeduxkWogFxNHjFzHf2YGScAckJDaDxS1RdlTUQYw==", + "dependencies": { + "debug": "^3.1.0", + "rhea": "^3.0.0", + "tslib": "^2.2.0" + } + }, + "node_modules/rhea-promise/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dependencies": { + "ms": "^2.1.1" + } + }, "node_modules/rimraf": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", @@ -2960,6 +3737,22 @@ "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.6.0.tgz", "integrity": "sha512-RVnVQxTXuerk653XfuliOxBP81Sf0+qfQE73LIYKcyMYHG94AuH0kgrQpRDuTZnSmjpysHmzxJXKNfa6PjFhyQ==" }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/setprototypeof": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", @@ -3070,6 +3863,15 @@ "node": ">= 0.8" } }, + "node_modules/stoppable": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/stoppable/-/stoppable-1.1.0.tgz", + "integrity": "sha512-KXDYZ9dszj6bzvnEMRYvxgeTHU74QBFL54XKtP3nyMuJ81CFYtABZ3bAzL2EdFUaEwJOBOgENyFj3R7oTzDyyw==", + "engines": { + "node": ">=4", + "npm": ">=6" + } + }, "node_modules/string_decoder": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", @@ -3124,6 +3926,11 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/strnum": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-1.0.5.tgz", + "integrity": "sha512-J8bbNyKKXl5qYcR36TIO8W3mVGVHrmmxsd5PAItGkmyzwJvybiw2IVq5nqd0i4LSNSkB/sx9VHllbfFdr9k1JA==" + }, "node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -5204,6 +6011,11 @@ "node": ">= 8" } }, + "node_modules/tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" + }, "node_modules/type-fest": { "version": "0.8.1", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", @@ -5234,6 +6046,11 @@ "is-typedarray": "^1.0.0" } }, + "node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==" + }, "node_modules/unicode-length": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/unicode-length/-/unicode-length-2.1.0.tgz", @@ -5281,6 +6098,18 @@ "punycode": "^2.1.0" } }, + "node_modules/util": { + "version": "0.12.5", + "resolved": "https://registry.npmjs.org/util/-/util-0.12.5.tgz", + "integrity": "sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==", + "dependencies": { + "inherits": "^2.0.3", + "is-arguments": "^1.0.4", + "is-generator-function": "^1.0.7", + "is-typed-array": "^1.1.3", + "which-typed-array": "^1.1.2" + } + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -5290,7 +6119,6 @@ "version": "8.3.2", "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", - "dev": true, "bin": { "uuid": "dist/bin/uuid" } @@ -5332,6 +6160,24 @@ "integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==", "dev": true }, + "node_modules/which-typed-array": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.15.tgz", + "integrity": "sha512-oV0jmFtUky6CXfkqehVvBP/LSWJ2sy4vWMioiENyJLePrBO/yKyV9OyJySfAKosh+RYkIl5zJCNZ8/4JncrpdA==", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.7", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/wrap-ansi": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", diff --git a/src/order-service/package.json b/src/order-service/package.json index ffe2fb7f..2ffd39d6 100644 --- a/src/order-service/package.json +++ b/src/order-service/package.json @@ -15,6 +15,8 @@ "author": "", "license": "ISC", "dependencies": { + "@azure/identity": "^4.1.0", + "@azure/service-bus": "^7.9.4", "@fastify/autoload": "^5.0.0", "@fastify/cors": "^8.3.0", "@fastify/sensible": "^4.1.0", diff --git a/src/order-service/plugins/messagequeue.js b/src/order-service/plugins/messagequeue.js index 3825c024..ea110973 100644 --- a/src/order-service/plugins/messagequeue.js +++ b/src/order-service/plugins/messagequeue.js @@ -1,40 +1,74 @@ 'use strict' const fp = require('fastify-plugin') -const rhea = require('rhea') module.exports = fp(async function (fastify, opts) { fastify.decorate('sendMessage', function (message) { const body = message.toString() - console.log(`sending message ${body} to ${process.env.ORDER_QUEUE_NAME} on ${process.env.ORDER_QUEUE_HOSTNAME}`) - - const container = rhea.create_container() - var amqp_message = container.message; - - const connectOptions = { - hostname: process.env.ORDER_QUEUE_HOSTNAME, - host: process.env.ORDER_QUEUE_HOSTNAME, - port: process.env.ORDER_QUEUE_PORT, - username: process.env.ORDER_QUEUE_USERNAME, - password: process.env.ORDER_QUEUE_PASSWORD, - reconnect_limit: process.env.ORDER_QUEUE_RECONNECT_LIMIT || 0 - } - - if (process.env.ORDER_QUEUE_TRANSPORT !== undefined) { - connectOptions.transport = process.env.ORDER_QUEUE_TRANSPORT + if (process.env.ORDER_QUEUE_USERNAME && process.env.ORDER_QUEUE_PASSWORD) { + console.log('sending message ${body} to ${process.env.ORDER_QUEUE_NAME} on ${process.env.ORDER_QUEUE_HOSTNAME} using local auth credentials') + + const rhea = require('rhea') + const container = rhea.create_container() + var amqp_message = container.message; + + const connectOptions = { + hostname: process.env.ORDER_QUEUE_HOSTNAME, + host: process.env.ORDER_QUEUE_HOSTNAME, + port: process.env.ORDER_QUEUE_PORT, + username: process.env.ORDER_QUEUE_USERNAME, + password: process.env.ORDER_QUEUE_PASSWORD, + reconnect_limit: process.env.ORDER_QUEUE_RECONNECT_LIMIT || 0 + } + + if (process.env.ORDER_QUEUE_TRANSPORT !== undefined) { + connectOptions.transport = process.env.ORDER_QUEUE_TRANSPORT + } + + const connection = container.connect(connectOptions) + + container.once('sendable', function (context) { + const sender = context.sender; + sender.send({ + body: amqp_message.data_section(Buffer.from(body,'utf8')) + }); + sender.close(); + connection.close(); + }) + + connection.open_sender(process.env.ORDER_QUEUE_NAME) + } else if (process.env.USE_WORKLOAD_IDENTITY_AUTH === 'true') { + const { ServiceBusClient } = require("@azure/service-bus"); + const { DefaultAzureCredential } = require("@azure/identity"); + + const fullyQualifiedNamespace = process.env.ORDER_QUEUE_HOSTNAME || process.env.AZURE_SERVICEBUS_FULLYQUALIFIEDNAMESPACE; + + console.log('sending message ${body} to ${process.env.ORDER_QUEUE_NAME} on ${fullyQualifiedNamespace} using Microsoft Entra ID Workload Identity credentials') + + if (!fullyQualifiedNamespace) { + console.log('no hostname set for message queue. exiting.'); + return; + } + + const queueName = process.env.ORDER_QUEUE_NAME + + const credential = new DefaultAzureCredential(); + + async function sendMessage() { + const sbClient = new ServiceBusClient(fullyQualifiedNamespace, credential); + const sender = sbClient.createSender(queueName); + + try { + await sender.sendMessages({ body: body }); + } finally { + await sender.close(); + await sbClient.close(); + } + } + sendMessage().catch(console.error); + } else { + console.log('no credentials set for message queue. exiting.') + return } - - const connection = container.connect(connectOptions) - - container.once('sendable', function (context) { - const sender = context.sender; - sender.send({ - body: amqp_message.data_section(Buffer.from(body,'utf8')) - }); - sender.close(); - connection.close(); - }) - - connection.open_sender(process.env.ORDER_QUEUE_NAME) }) }) diff --git a/src/product-service/src/routes/ai.rs b/src/product-service/src/routes/ai.rs index b48132cb..33b32a49 100644 --- a/src/product-service/src/routes/ai.rs +++ b/src/product-service/src/routes/ai.rs @@ -40,9 +40,8 @@ pub async fn ai_health(data: web::Data) -> Result }; let status = resp.status(); if status.is_success() { - let body: HashMap = resp.json().await.map_err(ProxyError::from)?; - let body_json = serde_json::to_string(&body).unwrap(); - Ok(HttpResponse::Ok().body(body_json)) + let body_text = resp.text().await.map_err(ProxyError::from)?; + Ok(HttpResponse::Ok().body(body_text)) } else { Ok(HttpResponse::build(actix_web::http::StatusCode::NOT_FOUND).body("")) } @@ -66,6 +65,34 @@ pub async fn ai_generate_description(data: web::Data, mut payload: web } }; + let status = resp.status(); + let body: HashMap = resp.json().await.map_err(ProxyError::from)?; + let body_json = serde_json::to_string(&body).unwrap(); + if status.is_success() { + Ok(HttpResponse::Ok().body(body_json)) + } else { + Ok(HttpResponse::build(actix_web::http::StatusCode::INTERNAL_SERVER_ERROR).body(body_json)) + } +} + +pub async fn ai_generate_image(data: web::Data, mut payload: web::Payload) -> Result { + let mut body = web::BytesMut::new(); + while let Some(chunk) = payload.next().await { + let chunk = chunk?; + body.extend_from_slice(&chunk); + } + let client = reqwest::Client::new(); + let ai_service_url = data.settings.ai_service_url.to_owned(); + let resp = match client.post( ai_service_url + "/generate/image") + .body(body.to_vec()) + .send() + .await { + Ok(resp) => resp, + Err(e) => { + return Ok(HttpResponse::InternalServerError().json(e.to_string())) + } + }; + let status = resp.status(); let body: HashMap = resp.json().await.map_err(ProxyError::from)?; let body_json = serde_json::to_string(&body).unwrap(); diff --git a/src/product-service/src/startup.rs b/src/product-service/src/startup.rs index 40623fbb..dcb56bdb 100644 --- a/src/product-service/src/startup.rs +++ b/src/product-service/src/startup.rs @@ -49,6 +49,10 @@ pub fn run(mut settings: Settings) -> Result { "/ai/generate/description", web::post().to(ai_generate_description), ) + .route( + "/ai/generate/image", + web::post().to(ai_generate_image), + ) }) .listen(listener)? .run(); diff --git a/src/product-service/test-product-service.http b/src/product-service/test-product-service.http index 92529879..44678648 100644 --- a/src/product-service/test-product-service.http +++ b/src/product-service/test-product-service.http @@ -80,3 +80,14 @@ Content-Type: application/json "tags": ["toy","dog"] } + +### Get product image from ai service +POST /ai/generate/image +Host: localhost:3002 +Content-Type: application/json + +{ + "name": "Seafarer's Tug Rope", + "description": "Engage your pup in a game of tug-of-war with the Seafarer's Tug Rope. Made from durable materials, this toy is perfect for interactive playtime and bonding with your furry friend." +} + diff --git a/src/store-admin/Dockerfile b/src/store-admin/Dockerfile index 1d7cb718..a10a6023 100644 --- a/src/store-admin/Dockerfile +++ b/src/store-admin/Dockerfile @@ -33,5 +33,9 @@ ENV APP_VERSION=$APP_VERSION # Copy the nginx configuration template to the container COPY nginx.conf /etc/nginx/conf.d/nginx.conf.template +# Update the nginx configuration to use the app version number +# and Copy the nginx configuration template to the container +RUN envsubst '${APP_VERSION}' < /etc/nginx/conf.d/nginx.conf.template > /etc/nginx/conf.d/default.conf + # Start the app -CMD ["sh", "-c", "envsubst '${APP_VERSION}' < /etc/nginx/conf.d/nginx.conf.template > /etc/nginx/conf.d/default.conf && nginx -g 'daemon off;'"] +CMD ["nginx", "-g", "daemon off;"] \ No newline at end of file diff --git a/src/store-admin/nginx.conf b/src/store-admin/nginx.conf index b5a4a898..a0c5eedc 100644 --- a/src/store-admin/nginx.conf +++ b/src/store-admin/nginx.conf @@ -88,4 +88,9 @@ server { proxy_pass http://product-service:3002/ai/generate/description; proxy_http_version 1.1; } + + location /ai/generate/image { + proxy_pass http://product-service:3002/ai/generate/image; + proxy_http_version 1.1; + } } \ No newline at end of file diff --git a/src/store-admin/src/components/ProductForm.vue b/src/store-admin/src/components/ProductForm.vue index 88715b56..3533ca10 100644 --- a/src/store-admin/src/components/ProductForm.vue +++ b/src/store-admin/src/components/ProductForm.vue @@ -10,32 +10,50 @@
-
- - -
- -
- - -
- -
- - -
- -
- -