diff --git a/.all-contributorsrc b/.all-contributorsrc index 0f9e3035..c29451ca 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -224,6 +224,15 @@ "contributions": [ "code" ] + }, + { + "login": "michaelmsonne", + "name": "Michael Morten Sonne | Microsoft MVP", + "avatar_url": "https://avatars.githubusercontent.com/u/49366751?v=4", + "profile": "https://blog.sonnes.cloud", + "contributions": [ + "bug" + ] } ], "commitConvention": "angular", diff --git a/README.md b/README.md index e0e3bcf6..6adb8e06 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ ![GitHub Release](https://img.shields.io/github/v/release/microsoft/AzUrlShortener) ![.NET](https://img.shields.io/badge/10.0-512BD4?logo=dotnet&logoColor=fff) [![Build](https://github.com/microsoft/AzUrlShortener/actions/workflows/build.yml/badge.svg?branch=main)](https://github.com/microsoft/AzUrlShortener/actions/workflows/build.yml) ![GitHub License](https://img.shields.io/github/license/microsoft/AzUrlShortener) [![Discord](https://img.shields.io/badge/Discord-%235865F2.svg?&logo=discord&logoColor=white)](https://discord.gg/6zA3jKw) -[![All Contributors](https://img.shields.io/badge/all_contributors-23-orange.svg?style=flat-square)](#contributors-) +[![All Contributors](https://img.shields.io/badge/all_contributors-24-orange.svg?style=flat-square)](#contributors-) @@ -116,6 +116,7 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d ddematheu2
ddematheu2

🐛 💻 David Oliva Paredes
David Oliva Paredes

💻 + Michael Morten Sonne | Microsoft MVP
Michael Morten Sonne | Microsoft MVP

🐛 diff --git a/doc/faq.md b/doc/faq.md index efd319d5..77565e15 100644 --- a/doc/faq.md +++ b/doc/faq.md @@ -10,6 +10,7 @@ - [How does it work?](./how-it-works.md) - [Security Considerations](./security-considerations.md) - [How to make the api public](./how-to-set-api-public.md) +- [How to deploy to App Service (budget friendlier version)](./how-deploy-budget-solution.md) ## How to run AzUrlShortener locally diff --git a/doc/how-deploy-budget-solution.md b/doc/how-deploy-budget-solution.md new file mode 100644 index 00000000..9d2527e1 --- /dev/null +++ b/doc/how-deploy-budget-solution.md @@ -0,0 +1,100 @@ + + +# Deploy the budget solution + +This guide explains how to deploy the budget-friendly version of AzUrlShortener. This deployment uses an Azure App Service for the admin website, an Azure Functions app for redirects, and an API container running as a private sidecar in the admin App Service. + +## Prerequisites + +Install the following tools before you begin: + +- [Azure CLI](https://learn.microsoft.com/en-us/cli/azure/install-azure-cli) +- [Azure Developer CLI](https://learn.microsoft.com/en-us/azure/developer/azure-developer-cli/install-azd) +- Git, if you are cloning the repository + +You also need: + +- An Azure subscription with permission to create resource groups and the resources in this deployment. +- A local copy of your fork or clone of the AzUrlShortener repository. +- The custom domain you will use for short URLs, including `https://`. +- The default redirect URL, including `https://`. + +## First deployment + +1. Sign in to Azure with both the Azure CLI and Azure Developer CLI: + + ```bash + az login + azd auth login + ``` + + Use the same account, or an account with access to the target subscription. + +1. Change to the budget deployment directory: + + ```bash + cd src/budget-deployment + ``` + + If you cloned the repository elsewhere, replace the path with your local repository path. + +1. Provision the Azure resources and deploy the application: + + ```bash + azd up + ``` + + During this command, `azd` asks you to select an Azure subscription and location, and to provide values for: + + - **Environment name**: Used in the resource group name. For example, an environment named `budget-prod` creates the resource group `rg-budget-prod`. + - **Custom domain**: The complete domain for your short URLs, such as `https://c5m.ca`. + - **Default redirect URL**: The destination used when a vanity URL does not exist or no vanity is supplied. + + `azd up` creates the resource group and required storage, App Service, Functions, and container registry resources. After provisioning, the post-provision hook builds the API image in Azure Container Registry and restarts the admin App Service so it can load the sidecar. + +1. Open the URLs printed by `azd` when deployment completes. The admin website URL is the App Service URL, and the redirect service URL is the Azure Functions URL. + +## Update an existing deployment + +After making application changes, sign in and move to the deployment directory: + +```bash +az login +azd auth login +cd /home/frank/gh/AzUrlShortener/src/budget-deployment +``` + +Deploy only the admin website and its related application files with: + +```bash +azd deploy admin +``` + +To deploy all application services instead, use: + +```bash +azd deploy +``` + +Use `azd provision` when the infrastructure definition has changed and the Azure resources need to be updated. + +## Troubleshooting the API sidecar + +The API sidecar image is normally built automatically by the `azd up` post-provision hook. If the image is missing or the hook did not complete, run the hook manually from `src/budget-deployment`: + +```bash +sh hooks/deploy-api-sidecar.sh +``` + +The script builds the image with Azure Container Registry Tasks, so Docker does not need to be running locally. It then restarts the admin App Service. + +If the admin website still has the old sidecar after the image is available, restart it manually: + +```bash +az webapp restart \ + --name \ + --resource-group . + The API sidecar is private and is reachable only by the admin application through `http://localhost:8081`; it is not exposed directly to the internet. + + +Replace `` and `` with the values from the deployment output or the Azure portal. The API sidecar is private and is reachable only by the admin application through `http://localhost:8081`; it is not exposed directly to the internet. \ No newline at end of file diff --git a/prompt.txt b/prompt.txt new file mode 100644 index 00000000..f2354469 --- /dev/null +++ b/prompt.txt @@ -0,0 +1,11 @@ +In this folder `src/budget-deployment` I have the deployment IaC to deploy my entire solution and it works. But right now it deploys the web site admin: `../TinyBlazorAdmin/Cloud5mins.ShortenerTools.TinyBlazorAdmin.csproj` + +with it's api: `../Api/Cloud5mins.ShortenerTools.Api.csproj` in two individual app service. + +It would be better if they were deploy in the same App Service using sidecar. + +I would like to have the "api" NOT reachable from the internet. Like with a docker compose setup when a backend container is only accessible by the frontend container. + +Here admin is the frontend and api is the backend + +I saw that post that explains a few things about sidecar deployments in Azure App Service https://azure.github.io/AppService/2025/03/06/Sidecars-Deep-Dive-Part1.html \ No newline at end of file diff --git a/src/.dockerignore b/src/.dockerignore new file mode 100644 index 00000000..2c5d0f71 --- /dev/null +++ b/src/.dockerignore @@ -0,0 +1,13 @@ +# Exclude build output and other large/unnecessary directories from the +# Docker build context (used by both local 'docker build' and 'az acr build'). +**/bin/ +**/obj/ +**/.git/ +**/.azure/ +**/node_modules/ +# Exclude other deployment configs – only src/ content is needed +budget-deployment/ +AppHost/ +infra/ +tools/ +*.md diff --git a/src/.gitignore b/src/.gitignore index 8e843802..1c037d0f 100644 --- a/src/.gitignore +++ b/src/.gitignore @@ -1 +1,2 @@ .azure +publish/ diff --git a/src/Api/Cloud5mins.ShortenerTools.Api.csproj b/src/Api/Cloud5mins.ShortenerTools.Api.csproj index 19e21d56..e492bbf0 100644 --- a/src/Api/Cloud5mins.ShortenerTools.Api.csproj +++ b/src/Api/Cloud5mins.ShortenerTools.Api.csproj @@ -1,10 +1,11 @@ - + net10.0 enable enable true + 500bdb23-5095-412f-b1e9-421be27fb629 diff --git a/src/Api/Dockerfile b/src/Api/Dockerfile new file mode 100644 index 00000000..43ad5950 --- /dev/null +++ b/src/Api/Dockerfile @@ -0,0 +1,32 @@ +# Build stage – uses the full SDK image +FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build +WORKDIR /src + +# Copy NuGet central version management before restore +COPY ["Directory.Packages.props", "./"] + +# Copy project files and restore (layer-cache friendly) +COPY ["Api/Cloud5mins.ShortenerTools.Api.csproj", "Api/"] +COPY ["Core/Cloud5mins.ShortenerTools.Core.csproj", "Core/"] +COPY ["ServiceDefaults/Cloud5mins.ShortenerTools.ServiceDefaults.csproj", "ServiceDefaults/"] +RUN dotnet restore "Api/Cloud5mins.ShortenerTools.Api.csproj" + +# Copy everything and publish +COPY . . +RUN dotnet publish "Api/Cloud5mins.ShortenerTools.Api.csproj" \ + -c Release \ + -o /app/publish \ + /p:UseAppHost=false + +# Runtime stage – lightweight ASP.NET image +FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS final +WORKDIR /app + +# Listen on HTTP only; App Service routes external traffic to the admin container. +# Use a dedicated internal port so the main admin app (which listens on 8080) +# can coexist with the sidecar container in the same site unit. +ENV ASPNETCORE_HTTP_PORTS=8081 +EXPOSE 8081 + +COPY --from=build /app/publish . +ENTRYPOINT ["dotnet", "Cloud5mins.ShortenerTools.Api.dll"] diff --git a/src/Api/Program.cs b/src/Api/Program.cs index 3a93d85a..de3bb9a0 100644 --- a/src/Api/Program.cs +++ b/src/Api/Program.cs @@ -6,7 +6,7 @@ // Learn more about configuring OpenAPI at https://aka.ms/aspnet/openapi builder.Services.AddOpenApi(); -builder.AddAzureTableClient("strTables"); +builder.AddAzureTableServiceClient("strTables"); builder.Services.AddTransient(sp => { diff --git a/src/Api/ShortenerEnpoints.cs b/src/Api/ShortenerEnpoints.cs index d5e702b3..b56c996a 100644 --- a/src/Api/ShortenerEnpoints.cs +++ b/src/Api/ShortenerEnpoints.cs @@ -10,8 +10,7 @@ public static class ShortenerEnpoints { public static void MapShortenerEnpoints(this IEndpointRouteBuilder app) { - var endpoints = app.MapGroup("api") - .WithOpenApi(); + var endpoints = app.MapGroup("api"); // GETS diff --git a/src/AppHost/Cloud5mins.ShortenerTools.AppHost.csproj b/src/AppHost/Cloud5mins.ShortenerTools.AppHost.csproj index 0f8183cf..4801274d 100644 --- a/src/AppHost/Cloud5mins.ShortenerTools.AppHost.csproj +++ b/src/AppHost/Cloud5mins.ShortenerTools.AppHost.csproj @@ -1,4 +1,4 @@ - + Exe diff --git a/src/AppHost/Program.cs b/src/AppHost/Program.cs index 073ad77f..5d7a1a9f 100644 --- a/src/AppHost/Program.cs +++ b/src/AppHost/Program.cs @@ -6,13 +6,20 @@ var customDomain = builder.AddParameter("CustomDomain"); var defaultRedirectUrl = builder.AddParameter("DefaultRedirectUrl"); +// To use an existing storage account, you can provide the name and resource group of the existing storage account. +// var existingStorageName = builder.AddParameter("existingStorageName"); +// var existingStorageResourceGroup = builder.AddParameter("existingStorageResourceGroup"); + var urlStorage = builder.AddAzureStorage("url-data"); + // .AsExisting(existingStorageName, existingStorageResourceGroup); if (builder.Environment.IsDevelopment()) { urlStorage.RunAsEmulator(); } + + var strTables = urlStorage.AddTables("strTables"); var azFuncLight = builder.AddAzureFunctionsProject("azfunc-light") diff --git a/src/AppHost/infra/admin.tmpl.yaml b/src/AppHost/infra/admin.tmpl.yaml new file mode 100644 index 00000000..4a85ff1d --- /dev/null +++ b/src/AppHost/infra/admin.tmpl.yaml @@ -0,0 +1,48 @@ +api-version: 2024-02-02-preview +location: {{ .Env.AZURE_LOCATION }} +identity: + type: UserAssigned + userAssignedIdentities: + ? "{{ .Env.AZURE_CONTAINER_REGISTRY_MANAGED_IDENTITY_ID }}" + : {} +properties: + environmentId: {{ .Env.AZURE_CONTAINER_APPS_ENVIRONMENT_ID }} + configuration: + activeRevisionsMode: single + runtime: + dotnet: + autoConfigureDataProtection: true + ingress: + external: true + targetPort: {{ targetPortOrDefault 8080 }} + transport: http + allowInsecure: false + registries: + - server: {{ .Env.AZURE_CONTAINER_REGISTRY_ENDPOINT }} + identity: {{ .Env.AZURE_CONTAINER_REGISTRY_MANAGED_IDENTITY_ID }} + template: + containers: + - image: {{ .Image }} + name: admin + env: + - name: AZURE_CLIENT_ID + value: {{ .Env.MANAGED_IDENTITY_CLIENT_ID }} + - name: API_HTTP + value: http://api.internal.{{ .Env.AZURE_CONTAINER_APPS_ENVIRONMENT_DEFAULT_DOMAIN }} + - name: API_HTTPS + value: https://api.internal.{{ .Env.AZURE_CONTAINER_APPS_ENVIRONMENT_DEFAULT_DOMAIN }} + - name: ASPNETCORE_FORWARDEDHEADERS_ENABLED + value: "true" + - name: HTTP_PORTS + value: '{{ targetPortOrDefault 0 }}' + - name: OTEL_DOTNET_EXPERIMENTAL_OTLP_RETRY + value: in_memory + - name: services__api__http__0 + value: http://api.internal.{{ .Env.AZURE_CONTAINER_APPS_ENVIRONMENT_DEFAULT_DOMAIN }} + - name: services__api__https__0 + value: https://api.internal.{{ .Env.AZURE_CONTAINER_APPS_ENVIRONMENT_DEFAULT_DOMAIN }} + scale: + minReplicas: 1 +tags: + azd-service-name: admin + aspire-resource-name: admin diff --git a/src/AppHost/infra/api.tmpl.yaml b/src/AppHost/infra/api.tmpl.yaml new file mode 100644 index 00000000..d7e65517 --- /dev/null +++ b/src/AppHost/infra/api.tmpl.yaml @@ -0,0 +1,51 @@ +api-version: 2024-02-02-preview +location: {{ .Env.AZURE_LOCATION }} +identity: + type: UserAssigned + userAssignedIdentities: + ? "{{ .Env.AZURE_CONTAINER_REGISTRY_MANAGED_IDENTITY_ID }}" + : {} +properties: + environmentId: {{ .Env.AZURE_CONTAINER_APPS_ENVIRONMENT_ID }} + configuration: + activeRevisionsMode: single + runtime: + dotnet: + autoConfigureDataProtection: true + ingress: + external: false + targetPort: {{ targetPortOrDefault 8080 }} + transport: http + allowInsecure: true + registries: + - server: {{ .Env.AZURE_CONTAINER_REGISTRY_ENDPOINT }} + identity: {{ .Env.AZURE_CONTAINER_REGISTRY_MANAGED_IDENTITY_ID }} + secrets: + - name: connectionstrings--strtables + value: '{{ .Env.URL_DATA_TABLEENDPOINT }}' + template: + containers: + - image: {{ .Image }} + name: api + env: + - name: AZURE_CLIENT_ID + value: {{ .Env.MANAGED_IDENTITY_CLIENT_ID }} + - name: ASPNETCORE_FORWARDEDHEADERS_ENABLED + value: "true" + - name: CustomDomain + value: '{{ parameter "CustomDomain" }}' + - name: DefaultRedirectUrl + value: '{{ parameter "DefaultRedirectUrl" }}' + - name: HTTP_PORTS + value: '{{ targetPortOrDefault 0 }}' + - name: OTEL_DOTNET_EXPERIMENTAL_OTLP_RETRY + value: in_memory + - name: STRTABLES_URI + value: '{{ .Env.URL_DATA_TABLEENDPOINT }}' + - name: ConnectionStrings__strTables + secretRef: connectionstrings--strtables + scale: + minReplicas: 1 +tags: + azd-service-name: api + aspire-resource-name: api diff --git a/src/AppHost/infra/azfunc-light.tmpl.yaml b/src/AppHost/infra/azfunc-light.tmpl.yaml new file mode 100644 index 00000000..f48e7e30 --- /dev/null +++ b/src/AppHost/infra/azfunc-light.tmpl.yaml @@ -0,0 +1,66 @@ +api-version: 2024-02-02-preview +location: {{ .Env.AZURE_LOCATION }} +identity: + type: UserAssigned + userAssignedIdentities: + ? "{{ .Env.AZURE_CONTAINER_REGISTRY_MANAGED_IDENTITY_ID }}" + : {} +properties: + environmentId: {{ .Env.AZURE_CONTAINER_APPS_ENVIRONMENT_ID }} + configuration: + activeRevisionsMode: single + runtime: + dotnet: + autoConfigureDataProtection: true + ingress: + external: true + targetPort: 8080 + transport: http + allowInsecure: false + registries: + - server: {{ .Env.AZURE_CONTAINER_REGISTRY_ENDPOINT }} + identity: {{ .Env.AZURE_CONTAINER_REGISTRY_MANAGED_IDENTITY_ID }} + template: + containers: + - image: {{ .Image }} + name: azfunc-light + env: + - name: AZURE_CLIENT_ID + value: {{ .Env.MANAGED_IDENTITY_CLIENT_ID }} + - name: ASPNETCORE_FORWARDEDHEADERS_ENABLED + value: "true" + - name: ASPNETCORE_URLS + value: http://+:8080 + - name: Aspire__Azure__Data__Tables__AzureWebJobsStorage__ServiceUri + value: '{{ .Env.FUNCSTORAGEA17CA_TABLEENDPOINT }}' + - name: Aspire__Azure__Data__Tables__strTables__ServiceUri + value: '{{ .Env.URL_DATA_TABLEENDPOINT }}' + - name: Aspire__Azure__Storage__Blobs__AzureWebJobsStorage__ServiceUri + value: '{{ .Env.FUNCSTORAGEA17CA_BLOBENDPOINT }}' + - name: Aspire__Azure__Storage__Files__DataLake__AzureWebJobsStorage__ServiceUri + value: '{{ .Env.FUNCSTORAGEA17CA_DATALAKEENDPOINT }}' + - name: Aspire__Azure__Storage__Queues__AzureWebJobsStorage__ServiceUri + value: '{{ .Env.FUNCSTORAGEA17CA_QUEUEENDPOINT }}' + - name: AzureFunctionsJobHost__telemetryMode + value: OpenTelemetry + - name: AzureWebJobsStorage__blobServiceUri + value: '{{ .Env.FUNCSTORAGEA17CA_BLOBENDPOINT }}' + - name: AzureWebJobsStorage__dataLakeServiceUri + value: '{{ .Env.FUNCSTORAGEA17CA_DATALAKEENDPOINT }}' + - name: AzureWebJobsStorage__queueServiceUri + value: '{{ .Env.FUNCSTORAGEA17CA_QUEUEENDPOINT }}' + - name: AzureWebJobsStorage__tableServiceUri + value: '{{ .Env.FUNCSTORAGEA17CA_TABLEENDPOINT }}' + - name: DefaultRedirectUrl + value: '{{ parameter "DefaultRedirectUrl" }}' + - name: FUNCTIONS_WORKER_RUNTIME + value: dotnet-isolated + - name: OTEL_DOTNET_EXPERIMENTAL_OTLP_RETRY + value: in_memory + - name: strTables__tableServiceUri + value: '{{ .Env.URL_DATA_TABLEENDPOINT }}' + scale: + minReplicas: 1 +tags: + azd-service-name: azfunc-light + aspire-resource-name: azfunc-light diff --git a/src/AzUrlShortener.sln b/src/AzUrlShortener.sln index a000c143..1e6500c2 100644 --- a/src/AzUrlShortener.sln +++ b/src/AzUrlShortener.sln @@ -1,4 +1,4 @@ - + Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 17 VisualStudioVersion = 17.0.31903.59 @@ -18,35 +18,87 @@ EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 Release|Any CPU = Release|Any CPU - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE + Release|x64 = Release|x64 + Release|x86 = Release|x86 EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution {99DF620C-9FF5-4233-B556-E5E9E143AB60}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {99DF620C-9FF5-4233-B556-E5E9E143AB60}.Debug|Any CPU.Build.0 = Debug|Any CPU + {99DF620C-9FF5-4233-B556-E5E9E143AB60}.Debug|x64.ActiveCfg = Debug|Any CPU + {99DF620C-9FF5-4233-B556-E5E9E143AB60}.Debug|x64.Build.0 = Debug|Any CPU + {99DF620C-9FF5-4233-B556-E5E9E143AB60}.Debug|x86.ActiveCfg = Debug|Any CPU + {99DF620C-9FF5-4233-B556-E5E9E143AB60}.Debug|x86.Build.0 = Debug|Any CPU {99DF620C-9FF5-4233-B556-E5E9E143AB60}.Release|Any CPU.ActiveCfg = Release|Any CPU {99DF620C-9FF5-4233-B556-E5E9E143AB60}.Release|Any CPU.Build.0 = Release|Any CPU + {99DF620C-9FF5-4233-B556-E5E9E143AB60}.Release|x64.ActiveCfg = Release|Any CPU + {99DF620C-9FF5-4233-B556-E5E9E143AB60}.Release|x64.Build.0 = Release|Any CPU + {99DF620C-9FF5-4233-B556-E5E9E143AB60}.Release|x86.ActiveCfg = Release|Any CPU + {99DF620C-9FF5-4233-B556-E5E9E143AB60}.Release|x86.Build.0 = Release|Any CPU {1C59956F-01A4-4F22-8B75-39CC48A8290C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {1C59956F-01A4-4F22-8B75-39CC48A8290C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1C59956F-01A4-4F22-8B75-39CC48A8290C}.Debug|x64.ActiveCfg = Debug|Any CPU + {1C59956F-01A4-4F22-8B75-39CC48A8290C}.Debug|x64.Build.0 = Debug|Any CPU + {1C59956F-01A4-4F22-8B75-39CC48A8290C}.Debug|x86.ActiveCfg = Debug|Any CPU + {1C59956F-01A4-4F22-8B75-39CC48A8290C}.Debug|x86.Build.0 = Debug|Any CPU {1C59956F-01A4-4F22-8B75-39CC48A8290C}.Release|Any CPU.ActiveCfg = Release|Any CPU {1C59956F-01A4-4F22-8B75-39CC48A8290C}.Release|Any CPU.Build.0 = Release|Any CPU + {1C59956F-01A4-4F22-8B75-39CC48A8290C}.Release|x64.ActiveCfg = Release|Any CPU + {1C59956F-01A4-4F22-8B75-39CC48A8290C}.Release|x64.Build.0 = Release|Any CPU + {1C59956F-01A4-4F22-8B75-39CC48A8290C}.Release|x86.ActiveCfg = Release|Any CPU + {1C59956F-01A4-4F22-8B75-39CC48A8290C}.Release|x86.Build.0 = Release|Any CPU {3DC7E590-FE84-4D0B-B989-9CD6B31B325A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {3DC7E590-FE84-4D0B-B989-9CD6B31B325A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3DC7E590-FE84-4D0B-B989-9CD6B31B325A}.Debug|x64.ActiveCfg = Debug|Any CPU + {3DC7E590-FE84-4D0B-B989-9CD6B31B325A}.Debug|x64.Build.0 = Debug|Any CPU + {3DC7E590-FE84-4D0B-B989-9CD6B31B325A}.Debug|x86.ActiveCfg = Debug|Any CPU + {3DC7E590-FE84-4D0B-B989-9CD6B31B325A}.Debug|x86.Build.0 = Debug|Any CPU {3DC7E590-FE84-4D0B-B989-9CD6B31B325A}.Release|Any CPU.ActiveCfg = Release|Any CPU {3DC7E590-FE84-4D0B-B989-9CD6B31B325A}.Release|Any CPU.Build.0 = Release|Any CPU + {3DC7E590-FE84-4D0B-B989-9CD6B31B325A}.Release|x64.ActiveCfg = Release|Any CPU + {3DC7E590-FE84-4D0B-B989-9CD6B31B325A}.Release|x64.Build.0 = Release|Any CPU + {3DC7E590-FE84-4D0B-B989-9CD6B31B325A}.Release|x86.ActiveCfg = Release|Any CPU + {3DC7E590-FE84-4D0B-B989-9CD6B31B325A}.Release|x86.Build.0 = Release|Any CPU {6567F739-8BCC-4B4B-8CCC-EEEB9B28465F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {6567F739-8BCC-4B4B-8CCC-EEEB9B28465F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6567F739-8BCC-4B4B-8CCC-EEEB9B28465F}.Debug|x64.ActiveCfg = Debug|Any CPU + {6567F739-8BCC-4B4B-8CCC-EEEB9B28465F}.Debug|x64.Build.0 = Debug|Any CPU + {6567F739-8BCC-4B4B-8CCC-EEEB9B28465F}.Debug|x86.ActiveCfg = Debug|Any CPU + {6567F739-8BCC-4B4B-8CCC-EEEB9B28465F}.Debug|x86.Build.0 = Debug|Any CPU {6567F739-8BCC-4B4B-8CCC-EEEB9B28465F}.Release|Any CPU.ActiveCfg = Release|Any CPU {6567F739-8BCC-4B4B-8CCC-EEEB9B28465F}.Release|Any CPU.Build.0 = Release|Any CPU + {6567F739-8BCC-4B4B-8CCC-EEEB9B28465F}.Release|x64.ActiveCfg = Release|Any CPU + {6567F739-8BCC-4B4B-8CCC-EEEB9B28465F}.Release|x64.Build.0 = Release|Any CPU + {6567F739-8BCC-4B4B-8CCC-EEEB9B28465F}.Release|x86.ActiveCfg = Release|Any CPU + {6567F739-8BCC-4B4B-8CCC-EEEB9B28465F}.Release|x86.Build.0 = Release|Any CPU {85C6D9A8-EB37-4FA3-9E83-7178825C9EA5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {85C6D9A8-EB37-4FA3-9E83-7178825C9EA5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {85C6D9A8-EB37-4FA3-9E83-7178825C9EA5}.Debug|x64.ActiveCfg = Debug|Any CPU + {85C6D9A8-EB37-4FA3-9E83-7178825C9EA5}.Debug|x64.Build.0 = Debug|Any CPU + {85C6D9A8-EB37-4FA3-9E83-7178825C9EA5}.Debug|x86.ActiveCfg = Debug|Any CPU + {85C6D9A8-EB37-4FA3-9E83-7178825C9EA5}.Debug|x86.Build.0 = Debug|Any CPU {85C6D9A8-EB37-4FA3-9E83-7178825C9EA5}.Release|Any CPU.ActiveCfg = Release|Any CPU {85C6D9A8-EB37-4FA3-9E83-7178825C9EA5}.Release|Any CPU.Build.0 = Release|Any CPU + {85C6D9A8-EB37-4FA3-9E83-7178825C9EA5}.Release|x64.ActiveCfg = Release|Any CPU + {85C6D9A8-EB37-4FA3-9E83-7178825C9EA5}.Release|x64.Build.0 = Release|Any CPU + {85C6D9A8-EB37-4FA3-9E83-7178825C9EA5}.Release|x86.ActiveCfg = Release|Any CPU + {85C6D9A8-EB37-4FA3-9E83-7178825C9EA5}.Release|x86.Build.0 = Release|Any CPU {88EAB6E9-CD20-4A6B-86A0-38CACA87FC7E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {88EAB6E9-CD20-4A6B-86A0-38CACA87FC7E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {88EAB6E9-CD20-4A6B-86A0-38CACA87FC7E}.Debug|x64.ActiveCfg = Debug|Any CPU + {88EAB6E9-CD20-4A6B-86A0-38CACA87FC7E}.Debug|x64.Build.0 = Debug|Any CPU + {88EAB6E9-CD20-4A6B-86A0-38CACA87FC7E}.Debug|x86.ActiveCfg = Debug|Any CPU + {88EAB6E9-CD20-4A6B-86A0-38CACA87FC7E}.Debug|x86.Build.0 = Debug|Any CPU {88EAB6E9-CD20-4A6B-86A0-38CACA87FC7E}.Release|Any CPU.ActiveCfg = Release|Any CPU {88EAB6E9-CD20-4A6B-86A0-38CACA87FC7E}.Release|Any CPU.Build.0 = Release|Any CPU + {88EAB6E9-CD20-4A6B-86A0-38CACA87FC7E}.Release|x64.ActiveCfg = Release|Any CPU + {88EAB6E9-CD20-4A6B-86A0-38CACA87FC7E}.Release|x64.Build.0 = Release|Any CPU + {88EAB6E9-CD20-4A6B-86A0-38CACA87FC7E}.Release|x86.ActiveCfg = Release|Any CPU + {88EAB6E9-CD20-4A6B-86A0-38CACA87FC7E}.Release|x86.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE EndGlobalSection EndGlobal diff --git a/src/Core/Domain/ClickDateList.cs b/src/Core/Domain/ClickDateList.cs index 0cf13b43..27208e57 100644 --- a/src/Core/Domain/ClickDateList.cs +++ b/src/Core/Domain/ClickDateList.cs @@ -2,11 +2,10 @@ namespace Cloud5mins.ShortenerTools.Core.Domain { public class ClickDateList { - public List Items { get; set; } - public string Url { get; set; } + public List Items { get; set; } = new(); + public string Url { get; set; } = string.Empty; public ClickDateList() { - Url = string.Empty; } public ClickDateList(List list) { diff --git a/src/Core/Domain/ClickStatsEntity.cs b/src/Core/Domain/ClickStatsEntity.cs index da0a7943..94a59d17 100644 --- a/src/Core/Domain/ClickStatsEntity.cs +++ b/src/Core/Domain/ClickStatsEntity.cs @@ -5,9 +5,9 @@ namespace Cloud5mins.ShortenerTools.Core.Domain; public class ClickStatsEntity : ITableEntity { - public string Datetime { get; set; } - public string PartitionKey { get; set; } - public string RowKey { get; set; } + public string Datetime { get; set; } = string.Empty; + public string PartitionKey { get; set; } = string.Empty; + public string RowKey { get; set; } = string.Empty; public DateTimeOffset? Timestamp { get; set; } public ETag ETag { get; set; } diff --git a/src/Core/Domain/ClickStatsEntityList.cs b/src/Core/Domain/ClickStatsEntityList.cs index b3b25852..ca0b8c15 100644 --- a/src/Core/Domain/ClickStatsEntityList.cs +++ b/src/Core/Domain/ClickStatsEntityList.cs @@ -2,7 +2,7 @@ namespace Cloud5mins.ShortenerTools.Core.Domain { public class ClickStatsEntityList { - public List ClickStatsList { get; set; } + public List ClickStatsList { get; set; } = new(); public ClickStatsEntityList() { } public ClickStatsEntityList(List list) diff --git a/src/Core/Domain/DetailledBadRequest.cs b/src/Core/Domain/DetailledBadRequest.cs index c66c7d3d..26e9e32c 100644 --- a/src/Core/Domain/DetailledBadRequest.cs +++ b/src/Core/Domain/DetailledBadRequest.cs @@ -2,6 +2,6 @@ namespace Cloud5mins.ShortenerTools.Core.Domain { public class DetailedBadRequest { - public string Message { get; set; } + public string Message { get; set; } = string.Empty; } } \ No newline at end of file diff --git a/src/Core/Domain/NextId.cs b/src/Core/Domain/NextId.cs index 8edc0f5f..19e7c9b3 100644 --- a/src/Core/Domain/NextId.cs +++ b/src/Core/Domain/NextId.cs @@ -6,8 +6,8 @@ namespace Cloud5mins.ShortenerTools.Core.Domain; public class NextId : ITableEntity { public int Id { get; set; } - public string PartitionKey { get; set; } - public string RowKey { get; set; } + public string PartitionKey { get; set; } = string.Empty; + public string RowKey { get; set; } = string.Empty; public DateTimeOffset? Timestamp { get; set; } public ETag ETag { get; set; } } diff --git a/src/Core/Domain/ShortUrlEntity.cs b/src/Core/Domain/ShortUrlEntity.cs index 4841bc5d..b355246a 100644 --- a/src/Core/Domain/ShortUrlEntity.cs +++ b/src/Core/Domain/ShortUrlEntity.cs @@ -2,16 +2,14 @@ using Azure.Data.Tables; using System.Runtime.Serialization; using System.Text.Json; -using Azure; -using Azure.Data.Tables; namespace Cloud5mins.ShortenerTools.Core.Domain { public class ShortUrlEntity : ITableEntity { - public string Url { get; set; } - private string _activeUrl { get; set; } + public string Url { get; set; } = string.Empty; + private string _activeUrl { get; set; } = string.Empty; public string ActiveUrl { @@ -24,16 +22,16 @@ public string ActiveUrl } - public string Title { get; set; } + public string Title { get; set; } = string.Empty; - public string ShortUrl { get; set; } + public string ShortUrl { get; set; } = string.Empty; public int Clicks { get; set; } public bool? IsArchived { get; set; } - public string SchedulesPropertyRaw { get; set; } + public string SchedulesPropertyRaw { get; set; } = string.Empty; - private List _schedules { get; set; } + private List _schedules { get; set; } = new(); //[IgnoreProperty] [IgnoreDataMember] @@ -49,7 +47,7 @@ public List Schedules } else { - _schedules = JsonSerializer.Deserialize(SchedulesPropertyRaw).ToList(); + _schedules = JsonSerializer.Deserialize(SchedulesPropertyRaw)?.ToList() ?? new List(); } } return _schedules; @@ -60,12 +58,12 @@ public List Schedules } } - public string PartitionKey { get; set; } - public string RowKey { get; set; } + public string PartitionKey { get; set; } = string.Empty; + public string RowKey { get; set; } = string.Empty; public DateTimeOffset? Timestamp { get; set; } public ETag ETag { get; set; } - public string CreatedDate { get; set; } + public string CreatedDate { get; set; } = string.Empty; @@ -76,17 +74,17 @@ public ShortUrlEntity(string longUrl, string endUrl) Initialize(longUrl, endUrl, string.Empty, null); } - public ShortUrlEntity(string longUrl, string endUrl, Schedule[] schedules) + public ShortUrlEntity(string longUrl, string endUrl, Schedule[]? schedules) { Initialize(longUrl, endUrl, string.Empty, schedules); } - public ShortUrlEntity(string longUrl, string endUrl, string title, Schedule[] schedules) + public ShortUrlEntity(string longUrl, string endUrl, string title, Schedule[]? schedules) { Initialize(longUrl, endUrl, title, schedules); } - private void Initialize(string longUrl, string endUrl, string title, Schedule[] schedules) + private void Initialize(string longUrl, string endUrl, string title, Schedule[]? schedules) { PartitionKey = endUrl.First().ToString(); RowKey = endUrl; diff --git a/src/Core/Domain/ShortenerSettings.cs b/src/Core/Domain/ShortenerSettings.cs index b702f8d4..ceb52e15 100644 --- a/src/Core/Domain/ShortenerSettings.cs +++ b/src/Core/Domain/ShortenerSettings.cs @@ -2,8 +2,8 @@ namespace Cloud5mins.ShortenerTools.Core.Domain { public class ShortenerSettings { - public string DefaultRedirectUrl { get; set; } - public string CustomDomain { get; set; } - public string DataStorage { get; set; } + public string DefaultRedirectUrl { get; set; } = string.Empty; + public string CustomDomain { get; set; } = string.Empty; + public string DataStorage { get; set; } = string.Empty; } } \ No newline at end of file diff --git a/src/Core/Messages/ListResponse.cs b/src/Core/Messages/ListResponse.cs index a20619fc..66556c7e 100644 --- a/src/Core/Messages/ListResponse.cs +++ b/src/Core/Messages/ListResponse.cs @@ -4,7 +4,7 @@ namespace Cloud5mins.ShortenerTools.Core.Messages { public class ListResponse { - public List UrlList { get; set; } + public List UrlList { get; set; } = new(); public ListResponse() { } public ListResponse(List list) diff --git a/src/Core/Messages/ShortRequest.cs b/src/Core/Messages/ShortRequest.cs index c366338d..2cf5092c 100644 --- a/src/Core/Messages/ShortRequest.cs +++ b/src/Core/Messages/ShortRequest.cs @@ -4,12 +4,12 @@ namespace Cloud5mins.ShortenerTools.Core.Messages { public class ShortRequest { - public string Vanity { get; set; } + public string Vanity { get; set; } = string.Empty; - public string Url { get; set; } + public string Url { get; set; } = string.Empty; - public string Title { get; set; } + public string Title { get; set; } = string.Empty; - public Schedule[] Schedules { get; set; } + public Schedule[] Schedules { get; set; } = Array.Empty(); } } \ No newline at end of file diff --git a/src/Core/Messages/ShortResponse.cs b/src/Core/Messages/ShortResponse.cs index a4cf2df6..7a475688 100644 --- a/src/Core/Messages/ShortResponse.cs +++ b/src/Core/Messages/ShortResponse.cs @@ -2,9 +2,9 @@ namespace Cloud5mins.ShortenerTools.Core.Messages { public class ShortResponse { - public string ShortUrl { get; set; } - public string LongUrl { get; set; } - public string Title { get; set; } + public string ShortUrl { get; set; } = string.Empty; + public string LongUrl { get; set; } = string.Empty; + public string Title { get; set; } = string.Empty; public ShortResponse() { } public ShortResponse(string host, string longUrl, string endUrl, string title) diff --git a/src/Core/Messages/ShortUrlRequest.cs b/src/Core/Messages/ShortUrlRequest.cs index 0300e83e..32f76d30 100644 --- a/src/Core/Messages/ShortUrlRequest.cs +++ b/src/Core/Messages/ShortUrlRequest.cs @@ -7,7 +7,7 @@ namespace Cloud5mins.ShortenerTools.Core.Messages { public class ShortUrlRequest { - private string _vanity; + private string _vanity = string.Empty; public string? Title { get; set; } @@ -24,9 +24,9 @@ public string Vanity } [Required] - public string Url { get; set; } + public string Url { get; set; } = string.Empty; - private List _schedules; + private List _schedules = new(); public List Schedules { diff --git a/src/Core/Service/AzStrorageTablesService.cs b/src/Core/Service/AzStrorageTablesService.cs index 4d2ea59e..d78e0f47 100644 --- a/src/Core/Service/AzStrorageTablesService.cs +++ b/src/Core/Service/AzStrorageTablesService.cs @@ -111,7 +111,7 @@ public async Task UpdateShortUrlEntity(ShortUrlEntity urlEntity) public async Task GetShortUrlEntityByVanity(string vanity) { var tblUrls = GetUrlsTable(); - ShortUrlEntity shortUrlEntity = null; + ShortUrlEntity? shortUrlEntity = null; var result = tblUrls.QueryAsync(e => e.RowKey == vanity); await foreach (var entity in result) @@ -126,14 +126,13 @@ public async Task GetShortUrlEntity(ShortUrlEntity row) { TableClient tblUrls = GetUrlsTable(); var response = await tblUrls.GetEntityAsync(row.PartitionKey, row.RowKey); - ShortUrlEntity eShortUrl = response.Value as ShortUrlEntity; - return eShortUrl; + return response.Value; } public async Task IfShortUrlEntityExistByVanity(string vanity) { - ShortUrlEntity shortUrlEntity = await GetShortUrlEntityByVanity(vanity); + ShortUrlEntity? shortUrlEntity = await GetShortUrlEntityByVanity(vanity); return (shortUrlEntity != null); } @@ -155,11 +154,11 @@ public async Task ArchiveShortUrlEntity(ShortUrlEntity urlEntity - public async Task> GetAllStatsByVanity(string vanity, string startDate, string endDate) + public async Task> GetAllStatsByVanity(string vanity, string? startDate, string? endDate) { var tblStats = GetStatsTable(); - var sDate = DateOnly.ParseExact(startDate, "yyyy-MM-dd", CultureInfo.InvariantCulture); - var eDate = DateOnly.ParseExact(endDate, "yyyy-MM-dd", CultureInfo.InvariantCulture); + var sDate = DateOnly.ParseExact(startDate ?? DateOnly.MinValue.ToString("yyyy-MM-dd"), "yyyy-MM-dd", CultureInfo.InvariantCulture); + var eDate = DateOnly.ParseExact(endDate ?? DateOnly.MaxValue.ToString("yyyy-MM-dd"), "yyyy-MM-dd", CultureInfo.InvariantCulture); var lstUrlStats = new List(); AsyncPageable queryResult; diff --git a/src/Core/Service/Utility.cs b/src/Core/Service/Utility.cs index 68b14daa..a3c23e03 100644 --- a/src/Core/Service/Utility.cs +++ b/src/Core/Service/Utility.cs @@ -94,8 +94,8 @@ public async static Task ExtractUrlsDataFromCSV(string fileFullName) { data.NextId = new NextId { - PartitionKey = csv.GetField("PartitionKey"), - RowKey = csv.GetField("RowKey"), + PartitionKey = csv.GetField("PartitionKey") ?? String.Empty, + RowKey = csv.GetField("RowKey") ?? String.Empty, Id = csv.GetField("Id") }; } @@ -103,11 +103,11 @@ public async static Task ExtractUrlsDataFromCSV(string fileFullName) { var record = new ShortUrlEntity { - PartitionKey = csv.GetField("PartitionKey"), - RowKey = csv.GetField("RowKey"), + PartitionKey = csv.GetField("PartitionKey") ?? String.Empty, + RowKey = csv.GetField("RowKey") ?? String.Empty, Clicks = csv.GetField("Clicks") ?? 0, Title = csv.GetField("Title") ?? String.Empty, - Url = csv.GetField("Url"), + Url = csv.GetField("Url") ?? String.Empty, SchedulesPropertyRaw = csv.GetField("SchedulesPropertyRaw") ?? String.Empty, IsArchived = csv.GetField("IsArchived") ?? false }; diff --git a/src/Directory.Packages.props b/src/Directory.Packages.props index b23f817d..42224a2b 100644 --- a/src/Directory.Packages.props +++ b/src/Directory.Packages.props @@ -1,39 +1,32 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/FunctionsLight/Cloud5mins.ShortenerTools.FunctionsLight.csproj b/src/FunctionsLight/Cloud5mins.ShortenerTools.FunctionsLight.csproj index d6e1e138..ced86731 100644 --- a/src/FunctionsLight/Cloud5mins.ShortenerTools.FunctionsLight.csproj +++ b/src/FunctionsLight/Cloud5mins.ShortenerTools.FunctionsLight.csproj @@ -1,4 +1,4 @@ - + net10.0 v4 @@ -6,6 +6,7 @@ enable enable true + 713be2c6-6051-4bcf-ac1c-2ab228a0e42d diff --git a/src/FunctionsLight/Program.cs b/src/FunctionsLight/Program.cs index 6e68fedc..d236cb08 100644 --- a/src/FunctionsLight/Program.cs +++ b/src/FunctionsLight/Program.cs @@ -3,7 +3,7 @@ var builder = FunctionsApplication.CreateBuilder(args); -builder.AddAzureTableClient("strTables"); +builder.AddAzureTableServiceClient("strTables"); builder.ConfigureFunctionsWebApplication(); diff --git a/src/TinyBlazorAdmin/Cloud5mins.ShortenerTools.TinyBlazorAdmin.csproj b/src/TinyBlazorAdmin/Cloud5mins.ShortenerTools.TinyBlazorAdmin.csproj index 25d231f4..d332ee3a 100644 --- a/src/TinyBlazorAdmin/Cloud5mins.ShortenerTools.TinyBlazorAdmin.csproj +++ b/src/TinyBlazorAdmin/Cloud5mins.ShortenerTools.TinyBlazorAdmin.csproj @@ -1,14 +1,15 @@ - + net10.0 enable enable true + 5a6db536-4d9e-4bfd-8006-85420b82f175 - + diff --git a/src/TinyBlazorAdmin/Components/Layout/MainLayout.razor b/src/TinyBlazorAdmin/Components/Layout/MainLayout.razor index 20a76e93..907a5cf2 100644 --- a/src/TinyBlazorAdmin/Components/Layout/MainLayout.razor +++ b/src/TinyBlazorAdmin/Components/Layout/MainLayout.razor @@ -1,25 +1,28 @@ @inherits LayoutComponentBase - - Tiny Blazor Admin - - - - -
- @Body -
-
-
- - - - + + Tiny Blazor Admin + + + + +
+ @Body +
+
+
+ + + + Documentation and demos + + + + About Blazor + + +
@@ -27,13 +30,13 @@
- An unhandled error has occurred. - Reload - 🗙 + An unhandled error has occurred. + Reload + 🗙
@code { - public DesignThemeModes Mode { get; set; } - public OfficeColor? OfficeColor { get; set; } + public DesignThemeModes Mode { get; set; } + public OfficeColor? OfficeColor { get; set; } } \ No newline at end of file diff --git a/src/TinyBlazorAdmin/Components/Pages/UrlManager.razor b/src/TinyBlazorAdmin/Components/Pages/UrlManager.razor index 74f1dd42..10fa37af 100644 --- a/src/TinyBlazorAdmin/Components/Pages/UrlManager.razor +++ b/src/TinyBlazorAdmin/Components/Pages/UrlManager.razor @@ -84,9 +84,8 @@ IQueryable? filteredUrlList => urlList?.Where(x => x.RowKey.Contains(vanityFilter, StringComparison.CurrentCultureIgnoreCase) && x.Title.Contains(titleFilter, StringComparison.CurrentCultureIgnoreCase) ); PaginationState pagination = new PaginationState { ItemsPerPage = 20 }; - ShortUrlRequest shortUrlRequest; - ShortUrlEntity editedUrl; - public List gridData { get; set; } + ShortUrlRequest shortUrlRequest = new(); + public List gridData { get; set; } = new(); GridSort sortByClicks = GridSort.ByDescending(p => p.Clicks); GridSort sortBySchedules = GridSort.ByDescending(p => p.Schedules.Count); @@ -156,7 +155,6 @@ } } - [Inject] public IJSRuntime JsRuntime { get; set; } public async Task CopyToClipboardAsync(string url) { await JSRuntime.InvokeVoidAsync("clipboardCopy.copyText", url); diff --git a/src/TinyBlazorAdmin/Components/Shared/ScheduleComponent.razor b/src/TinyBlazorAdmin/Components/Shared/ScheduleComponent.razor index c4348957..439341f6 100644 --- a/src/TinyBlazorAdmin/Components/Shared/ScheduleComponent.razor +++ b/src/TinyBlazorAdmin/Components/Shared/ScheduleComponent.razor @@ -33,7 +33,7 @@ @code { [Parameter] - public Schedule? schedule { get; set; } + public Schedule schedule { get; set; } = new(); private DateTime startDate; private DateTime startTime; diff --git a/src/TinyBlazorAdmin/Components/Shared/SchedulesComponent.razor b/src/TinyBlazorAdmin/Components/Shared/SchedulesComponent.razor index 45428eea..dea0dda7 100644 --- a/src/TinyBlazorAdmin/Components/Shared/SchedulesComponent.razor +++ b/src/TinyBlazorAdmin/Components/Shared/SchedulesComponent.razor @@ -37,7 +37,7 @@ @code { [Parameter] - public List schedules { get; set; } + public List schedules { get; set; } = new(); private void AddScheduleClick(){ diff --git a/src/TinyBlazorAdmin/UrlManagerClient.cs b/src/TinyBlazorAdmin/UrlManagerClient.cs index a2593c57..28a9f887 100644 --- a/src/TinyBlazorAdmin/UrlManagerClient.cs +++ b/src/TinyBlazorAdmin/UrlManagerClient.cs @@ -9,7 +9,7 @@ public class UrlManagerClient(HttpClient httpClient) public async Task?> GetUrls() { - IQueryable urlList = null; + IQueryable? urlList = null; try{ using var response = await httpClient.GetAsync("/api/UrlList"); if(response.IsSuccessStatusCode){ diff --git a/src/azure.yaml b/src/azure.yaml index 929a3b2b..f0ab76bb 100644 --- a/src/azure.yaml +++ b/src/azure.yaml @@ -1,6 +1,6 @@ # yaml-language-server: $schema=https://raw.githubusercontent.com/Azure/azure-dev/main/schemas/v1.0/azure.yaml.json -name: src +name: azurlshortener services: app: language: dotnet diff --git a/src/budget-deployment/azure.yaml b/src/budget-deployment/azure.yaml new file mode 100644 index 00000000..ed36068a --- /dev/null +++ b/src/budget-deployment/azure.yaml @@ -0,0 +1,32 @@ +# yaml-language-server: $schema=https://raw.githubusercontent.com/Azure/azure-dev/main/schemas/v1.0/azure.yaml.json + + +name: budget-azUrl +infra: + provider: bicep + path: ./infra +services: + azfunc-light: + project: ../FunctionsLight/Cloud5mins.ShortenerTools.FunctionsLight.csproj + language: dotnet + host: function + + # The API is no longer a standalone App Service. + # It runs as a private sidecar container on the admin App Service + # (only reachable via http://localhost:8081 from within the same site unit). + # The sidecar image is built and pushed by the postprovision hook below. + + admin: + project: ../TinyBlazorAdmin/Cloud5mins.ShortenerTools.TinyBlazorAdmin.csproj + language: dotnet + host: appservice + +hooks: + # After infrastructure is provisioned, build the API container image with + # ACR Tasks (no local Docker daemon required) and restart the admin site so + # App Service pulls the fresh image for the sidecar container. + postprovision: + posix: + shell: sh + run: hooks/deploy-api-sidecar.sh + interactive: false \ No newline at end of file diff --git a/src/budget-deployment/hooks/deploy-api-sidecar.sh b/src/budget-deployment/hooks/deploy-api-sidecar.sh new file mode 100755 index 00000000..e9873756 --- /dev/null +++ b/src/budget-deployment/hooks/deploy-api-sidecar.sh @@ -0,0 +1,112 @@ +#!/bin/sh +# deploy-api-sidecar.sh +# +# Builds the API container image using ACR Tasks (no local Docker daemon needed), +# pushes it to the project's ACR, then restarts the admin App Service so that +# App Service pulls the fresh image for the API sidecar container. +# +# Environment variables provided by azd: +# AZURE_CONTAINER_REGISTRY_NAME – ACR name (output from Bicep) +# AZURE_ADMIN_APP_NAME – Admin App Service name (output from Bicep) +# AZURE_RESOURCE_GROUP – Resource group name (set by azd) + +set -e + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +# Docker build context = src/ (solution root, two levels up from this script) +SRC_DIR="$(cd "$SCRIPT_DIR/../.." && pwd)" + +# azd may not export these variables into the shell inside a hook, even when the +# environment file exists. Load the environment file explicitly from the project +# root's .azure directory, not from the hooks folder. +ENV_FILE="" +if [ -n "${AZURE_ENV_NAME:-}" ] && [ -f "$SCRIPT_DIR/../.azure/${AZURE_ENV_NAME}/.env" ]; then + ENV_FILE="$SCRIPT_DIR/../.azure/${AZURE_ENV_NAME}/.env" +fi + +if [ -z "$ENV_FILE" ]; then + for candidate in "$SCRIPT_DIR/../.azure"/*/.env; do + if [ -f "$candidate" ]; then + ENV_FILE="$candidate" + break + fi + done +fi + +if [ -n "$ENV_FILE" ]; then + set -a + . "$ENV_FILE" + set +a +fi + +# Fallback for azd hook shells that don't export env vars automatically. +if [ -z "$AZURE_RESOURCE_GROUP" ] && [ -n "$AZURE_ENV_NAME" ]; then + # This project names the resource group as rg- + AZURE_RESOURCE_GROUP="rg-${AZURE_ENV_NAME}" +fi + +if [ -z "$AZURE_RESOURCE_GROUP" ]; then + AZURE_RESOURCE_GROUP="$(azd env get-values 2>/dev/null | awk -F= '/^AZURE_RESOURCE_GROUP=/{print $2}' | tr -d '"' || true)" +fi + +if [ -z "$AZURE_ADMIN_APP_NAME" ]; then + AZURE_ADMIN_APP_NAME="$(azd env get-values 2>/dev/null | awk -F= '/^AZURE_ADMIN_APP_NAME=/{print $2}' | tr -d '"' || true)" +fi + +if [ -z "$AZURE_CONTAINER_REGISTRY_NAME" ]; then + AZURE_CONTAINER_REGISTRY_NAME="$(azd env get-values 2>/dev/null | awk -F= '/^AZURE_CONTAINER_REGISTRY_NAME=/{print $2}' | tr -d '"' || true)" +fi + +if [ -z "$AZURE_RESOURCE_GROUP" ]; then + echo "ERROR: AZURE_RESOURCE_GROUP is not set. Run 'azd provision' first." + exit 1 +fi + +if [ -z "$AZURE_ADMIN_APP_NAME" ]; then + echo "ERROR: AZURE_ADMIN_APP_NAME is not set. Run 'azd provision' first." + exit 1 +fi + +if [ -z "$AZURE_CONTAINER_REGISTRY_NAME" ]; then + AZURE_CONTAINER_REGISTRY_NAME="$(az acr list -g "$AZURE_RESOURCE_GROUP" --query "[0].name" -o tsv 2>/dev/null || true)" +fi + +if [ -z "$AZURE_CONTAINER_REGISTRY_NAME" ]; then + echo "ERROR: AZURE_CONTAINER_REGISTRY_NAME is not set and no registry was found in resource group '$AZURE_RESOURCE_GROUP'. Run 'azd provision' first." + exit 1 +fi + +DOCKERFILE_PATH="Api/Dockerfile" +if [ ! -f "$SRC_DIR/$DOCKERFILE_PATH" ]; then + echo "ERROR: Dockerfile not found at '$SRC_DIR/$DOCKERFILE_PATH'." + exit 1 +fi + +echo "" +echo "==> Building and pushing API sidecar image to ACR: $AZURE_CONTAINER_REGISTRY_NAME" +echo " Build context: $SRC_DIR" +echo " Dockerfile: $DOCKERFILE_PATH" +echo "" + +# 'az acr build' runs the Docker build inside Azure (no local Docker required). +# Ensure the current working directory matches the build context so the relative +# Dockerfile path resolves consistently regardless of the hook's shell location. +( + cd "$SRC_DIR" + az acr build \ + --registry "$AZURE_CONTAINER_REGISTRY_NAME" \ + --image "api:latest" \ + --file "$DOCKERFILE_PATH" \ + . +) + +echo "" +echo "==> Restarting admin site '$AZURE_ADMIN_APP_NAME' to pull the new API sidecar image..." +az webapp restart \ + --name "$AZURE_ADMIN_APP_NAME" \ + --resource-group "$AZURE_RESOURCE_GROUP" + +echo "" +echo "==> API sidecar deployment complete." +echo " The API is accessible only via http://localhost:8081 from within the" +echo " site unit and is NOT reachable from the internet." diff --git a/src/budget-deployment/infra/funcstoragea17ca-roles/funcstoragea17ca-roles.module.bicep b/src/budget-deployment/infra/funcstoragea17ca-roles/funcstoragea17ca-roles.module.bicep new file mode 100644 index 00000000..a3d4d0de --- /dev/null +++ b/src/budget-deployment/infra/funcstoragea17ca-roles/funcstoragea17ca-roles.module.bicep @@ -0,0 +1,52 @@ +@description('The location for the resource(s) to be deployed.') +param location string = resourceGroup().location + +param funcstoragea17ca_outputs_name string + +param principalType string + +param principalId string + +resource funcstoragea17ca 'Microsoft.Storage/storageAccounts@2024-01-01' existing = { + name: funcstoragea17ca_outputs_name +} + +resource funcstoragea17ca_StorageBlobDataContributor 'Microsoft.Authorization/roleAssignments@2022-04-01' = { + name: guid(funcstoragea17ca.id, principalId, subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'ba92f5b4-2d11-453d-a403-e96b0029c9fe')) + properties: { + principalId: principalId + roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'ba92f5b4-2d11-453d-a403-e96b0029c9fe') + principalType: principalType + } + scope: funcstoragea17ca +} + +resource funcstoragea17ca_StorageTableDataContributor 'Microsoft.Authorization/roleAssignments@2022-04-01' = { + name: guid(funcstoragea17ca.id, principalId, subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '0a9a7e1f-b9d0-4cc4-a60d-0319b160aaa3')) + properties: { + principalId: principalId + roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '0a9a7e1f-b9d0-4cc4-a60d-0319b160aaa3') + principalType: principalType + } + scope: funcstoragea17ca +} + +resource funcstoragea17ca_StorageQueueDataContributor 'Microsoft.Authorization/roleAssignments@2022-04-01' = { + name: guid(funcstoragea17ca.id, principalId, subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '974c5e8b-45b9-4653-ba55-5f855dd0fb88')) + properties: { + principalId: principalId + roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '974c5e8b-45b9-4653-ba55-5f855dd0fb88') + principalType: principalType + } + scope: funcstoragea17ca +} + +resource funcstoragea17ca_StorageAccountContributor 'Microsoft.Authorization/roleAssignments@2022-04-01' = { + name: guid(funcstoragea17ca.id, principalId, subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '17d1049b-9a84-46fb-8f53-869881c3d3ab')) + properties: { + principalId: principalId + roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '17d1049b-9a84-46fb-8f53-869881c3d3ab') + principalType: principalType + } + scope: funcstoragea17ca +} \ No newline at end of file diff --git a/src/budget-deployment/infra/funcstoragea17ca/funcstoragea17ca.module.bicep b/src/budget-deployment/infra/funcstoragea17ca/funcstoragea17ca.module.bicep new file mode 100644 index 00000000..335395d2 --- /dev/null +++ b/src/budget-deployment/infra/funcstoragea17ca/funcstoragea17ca.module.bicep @@ -0,0 +1,35 @@ +@description('The location for the resource(s) to be deployed.') +param location string = resourceGroup().location + +resource funcstoragea17ca 'Microsoft.Storage/storageAccounts@2024-01-01' = { + name: take('funcstoragea17ca${uniqueString(resourceGroup().id)}', 24) + kind: 'StorageV2' + location: location + sku: { + name: 'Standard_GRS' + } + properties: { + accessTier: 'Hot' + allowSharedKeyAccess: false + isHnsEnabled: false + minimumTlsVersion: 'TLS1_2' + networkAcls: { + defaultAction: 'Allow' + } + } + tags: { + 'aspire-resource-name': 'funcstoragea17ca' + } +} + +output blobEndpoint string = funcstoragea17ca.properties.primaryEndpoints.blob + +output dataLakeEndpoint string = funcstoragea17ca.properties.primaryEndpoints.dfs + +output queueEndpoint string = funcstoragea17ca.properties.primaryEndpoints.queue + +output tableEndpoint string = funcstoragea17ca.properties.primaryEndpoints.table + +output name string = funcstoragea17ca.name + +output id string = funcstoragea17ca.id \ No newline at end of file diff --git a/src/budget-deployment/infra/main.bicep b/src/budget-deployment/infra/main.bicep new file mode 100644 index 00000000..75d5f1d8 --- /dev/null +++ b/src/budget-deployment/infra/main.bicep @@ -0,0 +1,93 @@ +targetScope = 'subscription' + +@minLength(1) +@maxLength(64) +@description('Name of the environment that can be used as part of naming resource convention, the name of the resource group for your application will use this name, prefixed with rg-') +param environmentName string + +@minLength(1) +@description('The location used for all deployed resources') +param location string + +@description('Id of the user or app to assign application roles') +param principalId string = '' + +param CustomDomain string +param DefaultRedirectUrl string + +var tags = { + 'azd-env-name': environmentName +} + +resource rg 'Microsoft.Resources/resourceGroups@2022-09-01' = { + name: 'rg-${environmentName}' + location: location + tags: tags +} + +module resources './resources.bicep' = { + scope: rg + name: 'resources' + params: { + location: location + tags: tags + principalId: principalId + CustomDomain: CustomDomain + DefaultRedirectUrl: DefaultRedirectUrl + urlDataTableEndpoint: url_data.outputs.tableEndpoint + funcStorageBlobEndpoint: funcstoragea17ca.outputs.blobEndpoint + funcStorageQueueEndpoint: funcstoragea17ca.outputs.queueEndpoint + funcStorageTableEndpoint: funcstoragea17ca.outputs.tableEndpoint + } +} + +module funcstoragea17ca './funcstoragea17ca/funcstoragea17ca.module.bicep' = { + name: 'funcstoragea17ca' + scope: rg + params: { + location: location + } +} + +module funcstoragea17ca_roles './funcstoragea17ca-roles/funcstoragea17ca-roles.module.bicep' = { + name: 'funcstoragea17ca-roles' + scope: rg + params: { + funcstoragea17ca_outputs_name: funcstoragea17ca.outputs.name + location: location + principalId: resources.outputs.MANAGED_IDENTITY_PRINCIPAL_ID + principalType: 'ServicePrincipal' + } +} + +module url_data './url-data/url-data.module.bicep' = { + name: 'url-data' + scope: rg + params: { + location: location + } +} + +module url_data_roles './url-data-roles/url-data-roles.module.bicep' = { + name: 'url-data-roles' + scope: rg + params: { + location: location + principalId: resources.outputs.MANAGED_IDENTITY_PRINCIPAL_ID + principalType: 'ServicePrincipal' + url_data_outputs_name: url_data.outputs.name + } +} + +output MANAGED_IDENTITY_CLIENT_ID string = resources.outputs.MANAGED_IDENTITY_CLIENT_ID +output MANAGED_IDENTITY_NAME string = resources.outputs.MANAGED_IDENTITY_NAME +output AZURE_APP_SERVICE_PLAN_NAME string = resources.outputs.AZURE_APP_SERVICE_PLAN_NAME +output AZURE_FUNCTION_APP_NAME string = resources.outputs.AZURE_FUNCTION_APP_NAME +output AZURE_ADMIN_APP_NAME string = resources.outputs.AZURE_ADMIN_APP_NAME +output AZURE_CONTAINER_REGISTRY_NAME string = resources.outputs.AZURE_CONTAINER_REGISTRY_NAME +output AZURE_CONTAINER_REGISTRY_ENDPOINT string = resources.outputs.AZURE_CONTAINER_REGISTRY_ENDPOINT +output FUNCSTORAGEA17CA_BLOBENDPOINT string = funcstoragea17ca.outputs.blobEndpoint +output FUNCSTORAGEA17CA_DATALAKEENDPOINT string = funcstoragea17ca.outputs.dataLakeEndpoint +output FUNCSTORAGEA17CA_QUEUEENDPOINT string = funcstoragea17ca.outputs.queueEndpoint +output FUNCSTORAGEA17CA_TABLEENDPOINT string = funcstoragea17ca.outputs.tableEndpoint +output URL_DATA_TABLEENDPOINT string = url_data.outputs.tableEndpoint diff --git a/src/budget-deployment/infra/main.parameters.json b/src/budget-deployment/infra/main.parameters.json new file mode 100644 index 00000000..396f07bc --- /dev/null +++ b/src/budget-deployment/infra/main.parameters.json @@ -0,0 +1,21 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "principalId": { + "value": "${AZURE_PRINCIPAL_ID}" + }, + "CustomDomain": { + "value": "${AZURE_CUSTOM_DOMAIN}" + }, + "DefaultRedirectUrl": { + "value": "${AZURE_DEFAULT_REDIRECT_URL}" + }, + "environmentName": { + "value": "${AZURE_ENV_NAME}" + }, + "location": { + "value": "${AZURE_LOCATION}" + } + } +} diff --git a/src/budget-deployment/infra/resources.bicep b/src/budget-deployment/infra/resources.bicep new file mode 100644 index 00000000..a9533435 --- /dev/null +++ b/src/budget-deployment/infra/resources.bicep @@ -0,0 +1,271 @@ +@description('The location used for all deployed resources') +param location string = resourceGroup().location + +@description('Id of the user or app to assign application roles') +param principalId string = '' + +@description('Tags that will be applied to all resources') +param tags object = {} + +@description('Custom domain passed to API service') +param CustomDomain string + +@description('Default redirect URL passed to API and function services') +param DefaultRedirectUrl string + +@description('Table service endpoint of the url-data storage account (for URL shortener data)') +param urlDataTableEndpoint string + +@description('Blob service endpoint of the func storage account (AzureWebJobsStorage)') +param funcStorageBlobEndpoint string + +@description('Queue service endpoint of the func storage account (AzureWebJobsStorage)') +param funcStorageQueueEndpoint string + +@description('Table service endpoint of the func storage account (AzureWebJobsStorage)') +param funcStorageTableEndpoint string + +var resourceToken = uniqueString(resourceGroup().id) + +resource managedIdentity 'Microsoft.ManagedIdentity/userAssignedIdentities@2023-01-31' = { + name: 'mi-${resourceToken}' + location: location + tags: tags +} + +resource appServicePlan 'Microsoft.Web/serverfarms@2023-12-01' = { + name: 'asp-${resourceToken}' + location: location + sku: { + name: 'B1' + tier: 'Basic' + size: 'B1' + family: 'B' + capacity: 1 + } + // Linux is required for App Service sidecar containers + kind: 'linux' + properties: { + reserved: true + } + tags: union(tags, { + 'aspire-resource-name': 'budget-appservice-plan' + }) +} + +// Container registry to host the API sidecar image +resource containerRegistry 'Microsoft.ContainerRegistry/registries@2023-07-01' = { + name: 'acr${resourceToken}' + location: location + sku: { + name: 'Basic' + } + properties: { + adminUserEnabled: false + } + tags: tags +} + +// Allow the managed identity to pull images from ACR +resource acrPullRole 'Microsoft.Authorization/roleAssignments@2022-04-01' = { + // AcrPull built-in role: 7f951dda-4ed3-4680-a7ca-43fe172d538d + name: guid(containerRegistry.id, managedIdentity.id, '7f951dda-4ed3-4680-a7ca-43fe172d538d') + scope: containerRegistry + properties: { + principalId: managedIdentity.properties.principalId + roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '7f951dda-4ed3-4680-a7ca-43fe172d538d') + principalType: 'ServicePrincipal' + } +} + +resource functionConsumptionPlan 'Microsoft.Web/serverfarms@2023-12-01' = { + name: 'asp-func-${resourceToken}' + location: location + sku: { + name: 'Y1' + tier: 'Dynamic' + size: 'Y1' + family: 'Y' + capacity: 0 + } + kind: 'functionapp' + properties: { + reserved: false + } + tags: union(tags, { + 'aspire-resource-name': 'budget-functions-plan' + }) +} + +resource functionSite 'Microsoft.Web/sites@2023-12-01' = { + name: 'func-${resourceToken}' + location: location + kind: 'functionapp' + identity: { + type: 'UserAssigned' + userAssignedIdentities: { + '${managedIdentity.id}': {} + } + } + properties: { + serverFarmId: functionConsumptionPlan.id + httpsOnly: true + siteConfig: { + ftpsState: 'Disabled' + minTlsVersion: '1.2' + appSettings: [ + { + name: 'FUNCTIONS_EXTENSION_VERSION' + value: '~4' + } + { + name: 'FUNCTIONS_WORKER_RUNTIME' + value: 'dotnet-isolated' + } + { + name: 'AZURE_CLIENT_ID' + value: managedIdentity.properties.clientId + } + { + name: 'DefaultRedirectUrl' + value: DefaultRedirectUrl + } + // AzureWebJobsStorage uses identity-based access (no connection string) + { + name: 'AzureWebJobsStorage__blobServiceUri' + value: funcStorageBlobEndpoint + } + { + name: 'AzureWebJobsStorage__queueServiceUri' + value: funcStorageQueueEndpoint + } + { + name: 'AzureWebJobsStorage__tableServiceUri' + value: funcStorageTableEndpoint + } + { + name: 'AzureWebJobsStorage__credential' + value: 'managedidentity' + } + { + name: 'AzureWebJobsStorage__clientId' + value: managedIdentity.properties.clientId + } + // strTables connection used by AddAzureTableClient("strTables") + { + name: 'ConnectionStrings__strTables' + value: urlDataTableEndpoint + } + ] + } + } + tags: union(tags, { + 'azd-service-name': 'azfunc-light' + 'aspire-resource-name': 'azfunc-light' + }) +} + +// apiSite removed – the API is now deployed as a sidecar container on adminSite. +// See the apiSidecar sitecontainer resource below. + +// Admin site – Linux code-based app (main container receives all external traffic). +// The API runs as a sidecar container on the same site unit and is only +// reachable via localhost:8080 (never from the internet). +resource adminSite 'Microsoft.Web/sites@2023-12-01' = { + name: 'admin-${resourceToken}' + location: location + kind: 'app,linux' + identity: { + type: 'UserAssigned' + userAssignedIdentities: { + '${managedIdentity.id}': {} + } + } + properties: { + serverFarmId: appServicePlan.id + httpsOnly: true + siteConfig: { + // Linux App Service must be explicitly configured for the .NET runtime; + // otherwise it defaults to the generic PHP container and the admin app + // never receives the published Blazor app bundle. + linuxFxVersion: 'DOTNETCORE|10.0' + ftpsState: 'Disabled' + minTlsVersion: '1.2' + appSettings: [ + // ── Admin settings ────────────────────────────────────────────────── + { + name: 'AZURE_CLIENT_ID' + value: managedIdentity.properties.clientId + } + // Aspire service-discovery: route API calls to the sidecar on localhost. + // The main admin app listens on 8080, so the private sidecar needs its + // own internal port to avoid a bind conflict in the shared site unit. + { + name: 'services__api__http__0' + value: 'http://localhost:8081' + } + // ── API settings (inherited by the sidecar via + // inheritAppSettingsAndConnectionStrings: true) ────────────────── + { + name: 'CustomDomain' + value: CustomDomain + } + { + name: 'DefaultRedirectUrl' + value: DefaultRedirectUrl + } + // strTables connection used by AddAzureTableClient("strTables") + { + name: 'ConnectionStrings__strTables' + value: urlDataTableEndpoint + } + ] + } + } + tags: union(tags, { + 'azd-service-name': 'admin' + 'aspire-resource-name': 'admin' + }) +} + +// API sidecar container – shares the same network namespace as adminSite. +// App Service only routes inbound internet traffic to the main (admin) container; +// this container is exclusively reachable via http://localhost:8081 from within +// the site unit. +resource apiSidecar 'Microsoft.Web/sites/sitecontainers@2024-04-01' = { + parent: adminSite + name: 'api' + properties: { + // Image is pushed to ACR by the postprovision hook. + // 'latest' is refreshed by restarting the site after each push. + image: '${containerRegistry.properties.loginServer}/api:latest' + isMain: false + targetPort: '8081' + // Pull image using the site's user-assigned managed identity. + // App Service passes all app settings from the main site into the sidecar + // unless a specific override is defined. + authType: 'UserAssigned' + userManagedIdentityClientId: managedIdentity.properties.clientId + } + dependsOn: [ + acrPullRole + ] +} + +resource principalRoleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = if (!empty(principalId)) { + name: guid(resourceGroup().id, principalId, subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'b24988ac-6180-42a0-ab88-20f7382dd24c')) + scope: resourceGroup() + properties: { + principalId: principalId + roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'b24988ac-6180-42a0-ab88-20f7382dd24c') + } +} + +output MANAGED_IDENTITY_CLIENT_ID string = managedIdentity.properties.clientId +output MANAGED_IDENTITY_NAME string = managedIdentity.name +output MANAGED_IDENTITY_PRINCIPAL_ID string = managedIdentity.properties.principalId +output AZURE_APP_SERVICE_PLAN_NAME string = appServicePlan.name +output AZURE_FUNCTION_APP_NAME string = functionSite.name +output AZURE_ADMIN_APP_NAME string = adminSite.name +output AZURE_CONTAINER_REGISTRY_NAME string = containerRegistry.name +output AZURE_CONTAINER_REGISTRY_ENDPOINT string = containerRegistry.properties.loginServer diff --git a/src/budget-deployment/infra/url-data-roles/url-data-roles.module.bicep b/src/budget-deployment/infra/url-data-roles/url-data-roles.module.bicep new file mode 100644 index 00000000..e029cdc0 --- /dev/null +++ b/src/budget-deployment/infra/url-data-roles/url-data-roles.module.bicep @@ -0,0 +1,42 @@ +@description('The location for the resource(s) to be deployed.') +param location string = resourceGroup().location + +param url_data_outputs_name string + +param principalType string + +param principalId string + +resource url_data 'Microsoft.Storage/storageAccounts@2024-01-01' existing = { + name: url_data_outputs_name +} + +resource url_data_StorageBlobDataContributor 'Microsoft.Authorization/roleAssignments@2022-04-01' = { + name: guid(url_data.id, principalId, subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'ba92f5b4-2d11-453d-a403-e96b0029c9fe')) + properties: { + principalId: principalId + roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'ba92f5b4-2d11-453d-a403-e96b0029c9fe') + principalType: principalType + } + scope: url_data +} + +resource url_data_StorageTableDataContributor 'Microsoft.Authorization/roleAssignments@2022-04-01' = { + name: guid(url_data.id, principalId, subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '0a9a7e1f-b9d0-4cc4-a60d-0319b160aaa3')) + properties: { + principalId: principalId + roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '0a9a7e1f-b9d0-4cc4-a60d-0319b160aaa3') + principalType: principalType + } + scope: url_data +} + +resource url_data_StorageQueueDataContributor 'Microsoft.Authorization/roleAssignments@2022-04-01' = { + name: guid(url_data.id, principalId, subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '974c5e8b-45b9-4653-ba55-5f855dd0fb88')) + properties: { + principalId: principalId + roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '974c5e8b-45b9-4653-ba55-5f855dd0fb88') + principalType: principalType + } + scope: url_data +} \ No newline at end of file diff --git a/src/budget-deployment/infra/url-data/url-data.module.bicep b/src/budget-deployment/infra/url-data/url-data.module.bicep new file mode 100644 index 00000000..6da8ab17 --- /dev/null +++ b/src/budget-deployment/infra/url-data/url-data.module.bicep @@ -0,0 +1,35 @@ +@description('The location for the resource(s) to be deployed.') +param location string = resourceGroup().location + +resource url_data 'Microsoft.Storage/storageAccounts@2024-01-01' = { + name: take('urldata${uniqueString(resourceGroup().id)}', 24) + kind: 'StorageV2' + location: location + sku: { + name: 'Standard_GRS' + } + properties: { + accessTier: 'Hot' + allowSharedKeyAccess: false + isHnsEnabled: false + minimumTlsVersion: 'TLS1_2' + networkAcls: { + defaultAction: 'Allow' + } + } + tags: { + 'aspire-resource-name': 'url-data' + } +} + +output blobEndpoint string = url_data.properties.primaryEndpoints.blob + +output dataLakeEndpoint string = url_data.properties.primaryEndpoints.dfs + +output queueEndpoint string = url_data.properties.primaryEndpoints.queue + +output tableEndpoint string = url_data.properties.primaryEndpoints.table + +output name string = url_data.name + +output id string = url_data.id \ No newline at end of file diff --git a/src/infra/funcstoragea17ca-roles/funcstoragea17ca-roles.module.bicep b/src/infra/funcstoragea17ca-roles/funcstoragea17ca-roles.module.bicep new file mode 100644 index 00000000..a3d4d0de --- /dev/null +++ b/src/infra/funcstoragea17ca-roles/funcstoragea17ca-roles.module.bicep @@ -0,0 +1,52 @@ +@description('The location for the resource(s) to be deployed.') +param location string = resourceGroup().location + +param funcstoragea17ca_outputs_name string + +param principalType string + +param principalId string + +resource funcstoragea17ca 'Microsoft.Storage/storageAccounts@2024-01-01' existing = { + name: funcstoragea17ca_outputs_name +} + +resource funcstoragea17ca_StorageBlobDataContributor 'Microsoft.Authorization/roleAssignments@2022-04-01' = { + name: guid(funcstoragea17ca.id, principalId, subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'ba92f5b4-2d11-453d-a403-e96b0029c9fe')) + properties: { + principalId: principalId + roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'ba92f5b4-2d11-453d-a403-e96b0029c9fe') + principalType: principalType + } + scope: funcstoragea17ca +} + +resource funcstoragea17ca_StorageTableDataContributor 'Microsoft.Authorization/roleAssignments@2022-04-01' = { + name: guid(funcstoragea17ca.id, principalId, subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '0a9a7e1f-b9d0-4cc4-a60d-0319b160aaa3')) + properties: { + principalId: principalId + roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '0a9a7e1f-b9d0-4cc4-a60d-0319b160aaa3') + principalType: principalType + } + scope: funcstoragea17ca +} + +resource funcstoragea17ca_StorageQueueDataContributor 'Microsoft.Authorization/roleAssignments@2022-04-01' = { + name: guid(funcstoragea17ca.id, principalId, subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '974c5e8b-45b9-4653-ba55-5f855dd0fb88')) + properties: { + principalId: principalId + roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '974c5e8b-45b9-4653-ba55-5f855dd0fb88') + principalType: principalType + } + scope: funcstoragea17ca +} + +resource funcstoragea17ca_StorageAccountContributor 'Microsoft.Authorization/roleAssignments@2022-04-01' = { + name: guid(funcstoragea17ca.id, principalId, subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '17d1049b-9a84-46fb-8f53-869881c3d3ab')) + properties: { + principalId: principalId + roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '17d1049b-9a84-46fb-8f53-869881c3d3ab') + principalType: principalType + } + scope: funcstoragea17ca +} \ No newline at end of file diff --git a/src/infra/funcstoragea17ca/funcstoragea17ca.module.bicep b/src/infra/funcstoragea17ca/funcstoragea17ca.module.bicep index 01fb9a78..335395d2 100644 --- a/src/infra/funcstoragea17ca/funcstoragea17ca.module.bicep +++ b/src/infra/funcstoragea17ca/funcstoragea17ca.module.bicep @@ -1,10 +1,6 @@ @description('The location for the resource(s) to be deployed.') param location string = resourceGroup().location -param principalId string - -param principalType string - resource funcstoragea17ca 'Microsoft.Storage/storageAccounts@2024-01-01' = { name: take('funcstoragea17ca${uniqueString(resourceGroup().id)}', 24) kind: 'StorageV2' @@ -15,6 +11,7 @@ resource funcstoragea17ca 'Microsoft.Storage/storageAccounts@2024-01-01' = { properties: { accessTier: 'Hot' allowSharedKeyAccess: false + isHnsEnabled: false minimumTlsVersion: 'TLS1_2' networkAcls: { defaultAction: 'Allow' @@ -25,53 +22,14 @@ resource funcstoragea17ca 'Microsoft.Storage/storageAccounts@2024-01-01' = { } } -resource blobs 'Microsoft.Storage/storageAccounts/blobServices@2024-01-01' = { - name: 'default' - parent: funcstoragea17ca -} - -resource funcstoragea17ca_StorageBlobDataContributor 'Microsoft.Authorization/roleAssignments@2022-04-01' = { - name: guid(funcstoragea17ca.id, principalId, subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'ba92f5b4-2d11-453d-a403-e96b0029c9fe')) - properties: { - principalId: principalId - roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'ba92f5b4-2d11-453d-a403-e96b0029c9fe') - principalType: principalType - } - scope: funcstoragea17ca -} - -resource funcstoragea17ca_StorageTableDataContributor 'Microsoft.Authorization/roleAssignments@2022-04-01' = { - name: guid(funcstoragea17ca.id, principalId, subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '0a9a7e1f-b9d0-4cc4-a60d-0319b160aaa3')) - properties: { - principalId: principalId - roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '0a9a7e1f-b9d0-4cc4-a60d-0319b160aaa3') - principalType: principalType - } - scope: funcstoragea17ca -} +output blobEndpoint string = funcstoragea17ca.properties.primaryEndpoints.blob -resource funcstoragea17ca_StorageQueueDataContributor 'Microsoft.Authorization/roleAssignments@2022-04-01' = { - name: guid(funcstoragea17ca.id, principalId, subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '974c5e8b-45b9-4653-ba55-5f855dd0fb88')) - properties: { - principalId: principalId - roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '974c5e8b-45b9-4653-ba55-5f855dd0fb88') - principalType: principalType - } - scope: funcstoragea17ca -} +output dataLakeEndpoint string = funcstoragea17ca.properties.primaryEndpoints.dfs -resource funcstoragea17ca_StorageAccountContributor 'Microsoft.Authorization/roleAssignments@2022-04-01' = { - name: guid(funcstoragea17ca.id, principalId, subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '17d1049b-9a84-46fb-8f53-869881c3d3ab')) - properties: { - principalId: principalId - roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '17d1049b-9a84-46fb-8f53-869881c3d3ab') - principalType: principalType - } - scope: funcstoragea17ca -} +output queueEndpoint string = funcstoragea17ca.properties.primaryEndpoints.queue -output blobEndpoint string = funcstoragea17ca.properties.primaryEndpoints.blob +output tableEndpoint string = funcstoragea17ca.properties.primaryEndpoints.table -output queueEndpoint string = funcstoragea17ca.properties.primaryEndpoints.queue +output name string = funcstoragea17ca.name -output tableEndpoint string = funcstoragea17ca.properties.primaryEndpoints.table \ No newline at end of file +output id string = funcstoragea17ca.id \ No newline at end of file diff --git a/src/infra/main.bicep b/src/infra/main.bicep index b43172b8..25faaf92 100644 --- a/src/infra/main.bicep +++ b/src/infra/main.bicep @@ -39,6 +39,14 @@ module funcstoragea17ca 'funcstoragea17ca/funcstoragea17ca.module.bicep' = { scope: rg params: { location: location + } +} +module funcstoragea17ca_roles 'funcstoragea17ca-roles/funcstoragea17ca-roles.module.bicep' = { + name: 'funcstoragea17ca-roles' + scope: rg + params: { + funcstoragea17ca_outputs_name: funcstoragea17ca.outputs.name + location: location principalId: resources.outputs.MANAGED_IDENTITY_PRINCIPAL_ID principalType: 'ServicePrincipal' } @@ -46,10 +54,18 @@ module funcstoragea17ca 'funcstoragea17ca/funcstoragea17ca.module.bicep' = { module url_data 'url-data/url-data.module.bicep' = { name: 'url-data' scope: rg + params: { + location: location + } +} +module url_data_roles 'url-data-roles/url-data-roles.module.bicep' = { + name: 'url-data-roles' + scope: rg params: { location: location principalId: resources.outputs.MANAGED_IDENTITY_PRINCIPAL_ID principalType: 'ServicePrincipal' + url_data_outputs_name: url_data.outputs.name } } @@ -63,6 +79,7 @@ output AZURE_CONTAINER_APPS_ENVIRONMENT_NAME string = resources.outputs.AZURE_CO output AZURE_CONTAINER_APPS_ENVIRONMENT_ID string = resources.outputs.AZURE_CONTAINER_APPS_ENVIRONMENT_ID output AZURE_CONTAINER_APPS_ENVIRONMENT_DEFAULT_DOMAIN string = resources.outputs.AZURE_CONTAINER_APPS_ENVIRONMENT_DEFAULT_DOMAIN output FUNCSTORAGEA17CA_BLOBENDPOINT string = funcstoragea17ca.outputs.blobEndpoint +output FUNCSTORAGEA17CA_DATALAKEENDPOINT string = funcstoragea17ca.outputs.dataLakeEndpoint output FUNCSTORAGEA17CA_QUEUEENDPOINT string = funcstoragea17ca.outputs.queueEndpoint output FUNCSTORAGEA17CA_TABLEENDPOINT string = funcstoragea17ca.outputs.tableEndpoint output URL_DATA_TABLEENDPOINT string = url_data.outputs.tableEndpoint diff --git a/src/infra/main.json b/src/infra/main.json new file mode 100644 index 00000000..91a34417 --- /dev/null +++ b/src/infra/main.json @@ -0,0 +1,685 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2018-05-01/subscriptionDeploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.45.15.27210", + "templateHash": "11546803755779573508" + } + }, + "parameters": { + "environmentName": { + "type": "string", + "minLength": 1, + "maxLength": 64, + "metadata": { + "description": "Name of the environment that can be used as part of naming resource convention, the name of the resource group for your application will use this name, prefixed with rg-" + } + }, + "location": { + "type": "string", + "minLength": 1, + "metadata": { + "description": "The location used for all deployed resources" + } + }, + "principalId": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "Id of the user or app to assign application roles" + } + }, + "CustomDomain": { + "type": "string" + }, + "DefaultRedirectUrl": { + "type": "string" + } + }, + "variables": { + "tags": { + "azd-env-name": "[parameters('environmentName')]" + } + }, + "resources": [ + { + "type": "Microsoft.Resources/resourceGroups", + "apiVersion": "2022-09-01", + "name": "[format('rg-{0}', parameters('environmentName'))]", + "location": "[parameters('location')]", + "tags": "[variables('tags')]" + }, + { + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "resources", + "resourceGroup": "[format('rg-{0}', parameters('environmentName'))]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "location": { + "value": "[parameters('location')]" + }, + "tags": { + "value": "[variables('tags')]" + }, + "principalId": { + "value": "[parameters('principalId')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.45.15.27210", + "templateHash": "17949110531045892075" + } + }, + "parameters": { + "location": { + "type": "string", + "defaultValue": "[resourceGroup().location]", + "metadata": { + "description": "The location used for all deployed resources" + } + }, + "principalId": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "Id of the user or app to assign application roles" + } + }, + "tags": { + "type": "object", + "defaultValue": {}, + "metadata": { + "description": "Tags that will be applied to all resources" + } + } + }, + "variables": { + "resourceToken": "[uniqueString(resourceGroup().id)]" + }, + "resources": [ + { + "type": "Microsoft.App/managedEnvironments/dotNetComponents", + "apiVersion": "2024-02-02-preview", + "name": "[format('{0}/{1}', format('cae-{0}', variables('resourceToken')), 'aspire-dashboard')]", + "properties": { + "componentType": "AspireDashboard" + }, + "dependsOn": [ + "[resourceId('Microsoft.App/managedEnvironments', format('cae-{0}', variables('resourceToken')))]" + ] + }, + { + "type": "Microsoft.ManagedIdentity/userAssignedIdentities", + "apiVersion": "2023-01-31", + "name": "[format('mi-{0}', variables('resourceToken'))]", + "location": "[parameters('location')]", + "tags": "[parameters('tags')]" + }, + { + "type": "Microsoft.ContainerRegistry/registries", + "apiVersion": "2023-07-01", + "name": "[replace(format('acr-{0}', variables('resourceToken')), '-', '')]", + "location": "[parameters('location')]", + "sku": { + "name": "Basic" + }, + "tags": "[parameters('tags')]" + }, + { + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2022-04-01", + "scope": "[resourceId('Microsoft.ContainerRegistry/registries', replace(format('acr-{0}', variables('resourceToken')), '-', ''))]", + "name": "[guid(resourceId('Microsoft.ContainerRegistry/registries', replace(format('acr-{0}', variables('resourceToken')), '-', '')), resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', format('mi-{0}', variables('resourceToken'))), subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '7f951dda-4ed3-4680-a7ca-43fe172d538d'))]", + "properties": { + "principalId": "[reference(resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', format('mi-{0}', variables('resourceToken'))), '2023-01-31').principalId]", + "principalType": "ServicePrincipal", + "roleDefinitionId": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '7f951dda-4ed3-4680-a7ca-43fe172d538d')]" + }, + "dependsOn": [ + "[resourceId('Microsoft.ContainerRegistry/registries', replace(format('acr-{0}', variables('resourceToken')), '-', ''))]", + "[resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', format('mi-{0}', variables('resourceToken')))]" + ] + }, + { + "type": "Microsoft.OperationalInsights/workspaces", + "apiVersion": "2022-10-01", + "name": "[format('law-{0}', variables('resourceToken'))]", + "location": "[parameters('location')]", + "properties": { + "sku": { + "name": "PerGB2018" + } + }, + "tags": "[parameters('tags')]" + }, + { + "type": "Microsoft.App/managedEnvironments", + "apiVersion": "2024-02-02-preview", + "name": "[format('cae-{0}', variables('resourceToken'))]", + "location": "[parameters('location')]", + "properties": { + "workloadProfiles": [ + { + "workloadProfileType": "Consumption", + "name": "consumption" + } + ], + "appLogsConfiguration": { + "destination": "log-analytics", + "logAnalyticsConfiguration": { + "customerId": "[reference(resourceId('Microsoft.OperationalInsights/workspaces', format('law-{0}', variables('resourceToken'))), '2022-10-01').customerId]", + "sharedKey": "[listKeys(resourceId('Microsoft.OperationalInsights/workspaces', format('law-{0}', variables('resourceToken'))), '2022-10-01').primarySharedKey]" + } + } + }, + "tags": "[parameters('tags')]", + "dependsOn": [ + "[resourceId('Microsoft.OperationalInsights/workspaces', format('law-{0}', variables('resourceToken')))]" + ] + } + ], + "outputs": { + "MANAGED_IDENTITY_CLIENT_ID": { + "type": "string", + "value": "[reference(resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', format('mi-{0}', variables('resourceToken'))), '2023-01-31').clientId]" + }, + "MANAGED_IDENTITY_NAME": { + "type": "string", + "value": "[format('mi-{0}', variables('resourceToken'))]" + }, + "MANAGED_IDENTITY_PRINCIPAL_ID": { + "type": "string", + "value": "[reference(resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', format('mi-{0}', variables('resourceToken'))), '2023-01-31').principalId]" + }, + "AZURE_LOG_ANALYTICS_WORKSPACE_NAME": { + "type": "string", + "value": "[format('law-{0}', variables('resourceToken'))]" + }, + "AZURE_LOG_ANALYTICS_WORKSPACE_ID": { + "type": "string", + "value": "[resourceId('Microsoft.OperationalInsights/workspaces', format('law-{0}', variables('resourceToken')))]" + }, + "AZURE_CONTAINER_REGISTRY_ENDPOINT": { + "type": "string", + "value": "[reference(resourceId('Microsoft.ContainerRegistry/registries', replace(format('acr-{0}', variables('resourceToken')), '-', '')), '2023-07-01').loginServer]" + }, + "AZURE_CONTAINER_REGISTRY_MANAGED_IDENTITY_ID": { + "type": "string", + "value": "[resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', format('mi-{0}', variables('resourceToken')))]" + }, + "AZURE_CONTAINER_REGISTRY_NAME": { + "type": "string", + "value": "[replace(format('acr-{0}', variables('resourceToken')), '-', '')]" + }, + "AZURE_CONTAINER_APPS_ENVIRONMENT_NAME": { + "type": "string", + "value": "[format('cae-{0}', variables('resourceToken'))]" + }, + "AZURE_CONTAINER_APPS_ENVIRONMENT_ID": { + "type": "string", + "value": "[resourceId('Microsoft.App/managedEnvironments', format('cae-{0}', variables('resourceToken')))]" + }, + "AZURE_CONTAINER_APPS_ENVIRONMENT_DEFAULT_DOMAIN": { + "type": "string", + "value": "[reference(resourceId('Microsoft.App/managedEnvironments', format('cae-{0}', variables('resourceToken'))), '2024-02-02-preview').defaultDomain]" + } + } + } + }, + "dependsOn": [ + "[subscriptionResourceId('Microsoft.Resources/resourceGroups', format('rg-{0}', parameters('environmentName')))]" + ] + }, + { + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "funcstoragea17ca", + "resourceGroup": "[format('rg-{0}', parameters('environmentName'))]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "location": { + "value": "[parameters('location')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.45.15.27210", + "templateHash": "5132421617093321654" + } + }, + "parameters": { + "location": { + "type": "string", + "defaultValue": "[resourceGroup().location]", + "metadata": { + "description": "The location for the resource(s) to be deployed." + } + } + }, + "resources": [ + { + "type": "Microsoft.Storage/storageAccounts", + "apiVersion": "2024-01-01", + "name": "[take(format('funcstoragea17ca{0}', uniqueString(resourceGroup().id)), 24)]", + "kind": "StorageV2", + "location": "[parameters('location')]", + "sku": { + "name": "Standard_GRS" + }, + "properties": { + "accessTier": "Hot", + "allowSharedKeyAccess": false, + "isHnsEnabled": false, + "minimumTlsVersion": "TLS1_2", + "networkAcls": { + "defaultAction": "Allow" + } + }, + "tags": { + "aspire-resource-name": "funcstoragea17ca" + } + } + ], + "outputs": { + "blobEndpoint": { + "type": "string", + "value": "[reference(resourceId('Microsoft.Storage/storageAccounts', take(format('funcstoragea17ca{0}', uniqueString(resourceGroup().id)), 24)), '2024-01-01').primaryEndpoints.blob]" + }, + "dataLakeEndpoint": { + "type": "string", + "value": "[reference(resourceId('Microsoft.Storage/storageAccounts', take(format('funcstoragea17ca{0}', uniqueString(resourceGroup().id)), 24)), '2024-01-01').primaryEndpoints.dfs]" + }, + "queueEndpoint": { + "type": "string", + "value": "[reference(resourceId('Microsoft.Storage/storageAccounts', take(format('funcstoragea17ca{0}', uniqueString(resourceGroup().id)), 24)), '2024-01-01').primaryEndpoints.queue]" + }, + "tableEndpoint": { + "type": "string", + "value": "[reference(resourceId('Microsoft.Storage/storageAccounts', take(format('funcstoragea17ca{0}', uniqueString(resourceGroup().id)), 24)), '2024-01-01').primaryEndpoints.table]" + }, + "name": { + "type": "string", + "value": "[take(format('funcstoragea17ca{0}', uniqueString(resourceGroup().id)), 24)]" + }, + "id": { + "type": "string", + "value": "[resourceId('Microsoft.Storage/storageAccounts', take(format('funcstoragea17ca{0}', uniqueString(resourceGroup().id)), 24))]" + } + } + } + }, + "dependsOn": [ + "[subscriptionResourceId('Microsoft.Resources/resourceGroups', format('rg-{0}', parameters('environmentName')))]" + ] + }, + { + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "funcstoragea17ca-roles", + "resourceGroup": "[format('rg-{0}', parameters('environmentName'))]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "funcstoragea17ca_outputs_name": { + "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, format('rg-{0}', parameters('environmentName'))), 'Microsoft.Resources/deployments', 'funcstoragea17ca'), '2025-04-01').outputs.name.value]" + }, + "location": { + "value": "[parameters('location')]" + }, + "principalId": { + "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, format('rg-{0}', parameters('environmentName'))), 'Microsoft.Resources/deployments', 'resources'), '2025-04-01').outputs.MANAGED_IDENTITY_PRINCIPAL_ID.value]" + }, + "principalType": { + "value": "ServicePrincipal" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.45.15.27210", + "templateHash": "6014125912618429667" + } + }, + "parameters": { + "location": { + "type": "string", + "defaultValue": "[resourceGroup().location]", + "metadata": { + "description": "The location for the resource(s) to be deployed." + } + }, + "funcstoragea17ca_outputs_name": { + "type": "string" + }, + "principalType": { + "type": "string" + }, + "principalId": { + "type": "string" + } + }, + "resources": [ + { + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2022-04-01", + "scope": "[resourceId('Microsoft.Storage/storageAccounts', parameters('funcstoragea17ca_outputs_name'))]", + "name": "[guid(resourceId('Microsoft.Storage/storageAccounts', parameters('funcstoragea17ca_outputs_name')), parameters('principalId'), subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'ba92f5b4-2d11-453d-a403-e96b0029c9fe'))]", + "properties": { + "principalId": "[parameters('principalId')]", + "roleDefinitionId": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'ba92f5b4-2d11-453d-a403-e96b0029c9fe')]", + "principalType": "[parameters('principalType')]" + } + }, + { + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2022-04-01", + "scope": "[resourceId('Microsoft.Storage/storageAccounts', parameters('funcstoragea17ca_outputs_name'))]", + "name": "[guid(resourceId('Microsoft.Storage/storageAccounts', parameters('funcstoragea17ca_outputs_name')), parameters('principalId'), subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '0a9a7e1f-b9d0-4cc4-a60d-0319b160aaa3'))]", + "properties": { + "principalId": "[parameters('principalId')]", + "roleDefinitionId": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '0a9a7e1f-b9d0-4cc4-a60d-0319b160aaa3')]", + "principalType": "[parameters('principalType')]" + } + }, + { + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2022-04-01", + "scope": "[resourceId('Microsoft.Storage/storageAccounts', parameters('funcstoragea17ca_outputs_name'))]", + "name": "[guid(resourceId('Microsoft.Storage/storageAccounts', parameters('funcstoragea17ca_outputs_name')), parameters('principalId'), subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '974c5e8b-45b9-4653-ba55-5f855dd0fb88'))]", + "properties": { + "principalId": "[parameters('principalId')]", + "roleDefinitionId": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '974c5e8b-45b9-4653-ba55-5f855dd0fb88')]", + "principalType": "[parameters('principalType')]" + } + }, + { + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2022-04-01", + "scope": "[resourceId('Microsoft.Storage/storageAccounts', parameters('funcstoragea17ca_outputs_name'))]", + "name": "[guid(resourceId('Microsoft.Storage/storageAccounts', parameters('funcstoragea17ca_outputs_name')), parameters('principalId'), subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '17d1049b-9a84-46fb-8f53-869881c3d3ab'))]", + "properties": { + "principalId": "[parameters('principalId')]", + "roleDefinitionId": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '17d1049b-9a84-46fb-8f53-869881c3d3ab')]", + "principalType": "[parameters('principalType')]" + } + } + ] + } + }, + "dependsOn": [ + "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, format('rg-{0}', parameters('environmentName'))), 'Microsoft.Resources/deployments', 'funcstoragea17ca')]", + "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, format('rg-{0}', parameters('environmentName'))), 'Microsoft.Resources/deployments', 'resources')]", + "[subscriptionResourceId('Microsoft.Resources/resourceGroups', format('rg-{0}', parameters('environmentName')))]" + ] + }, + { + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "url-data", + "resourceGroup": "[format('rg-{0}', parameters('environmentName'))]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "location": { + "value": "[parameters('location')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.45.15.27210", + "templateHash": "10282375894748324618" + } + }, + "parameters": { + "location": { + "type": "string", + "defaultValue": "[resourceGroup().location]", + "metadata": { + "description": "The location for the resource(s) to be deployed." + } + } + }, + "resources": [ + { + "type": "Microsoft.Storage/storageAccounts", + "apiVersion": "2024-01-01", + "name": "[take(format('urldata{0}', uniqueString(resourceGroup().id)), 24)]", + "kind": "StorageV2", + "location": "[parameters('location')]", + "sku": { + "name": "Standard_GRS" + }, + "properties": { + "accessTier": "Hot", + "allowSharedKeyAccess": false, + "isHnsEnabled": false, + "minimumTlsVersion": "TLS1_2", + "networkAcls": { + "defaultAction": "Allow" + } + }, + "tags": { + "aspire-resource-name": "url-data" + } + } + ], + "outputs": { + "blobEndpoint": { + "type": "string", + "value": "[reference(resourceId('Microsoft.Storage/storageAccounts', take(format('urldata{0}', uniqueString(resourceGroup().id)), 24)), '2024-01-01').primaryEndpoints.blob]" + }, + "dataLakeEndpoint": { + "type": "string", + "value": "[reference(resourceId('Microsoft.Storage/storageAccounts', take(format('urldata{0}', uniqueString(resourceGroup().id)), 24)), '2024-01-01').primaryEndpoints.dfs]" + }, + "queueEndpoint": { + "type": "string", + "value": "[reference(resourceId('Microsoft.Storage/storageAccounts', take(format('urldata{0}', uniqueString(resourceGroup().id)), 24)), '2024-01-01').primaryEndpoints.queue]" + }, + "tableEndpoint": { + "type": "string", + "value": "[reference(resourceId('Microsoft.Storage/storageAccounts', take(format('urldata{0}', uniqueString(resourceGroup().id)), 24)), '2024-01-01').primaryEndpoints.table]" + }, + "name": { + "type": "string", + "value": "[take(format('urldata{0}', uniqueString(resourceGroup().id)), 24)]" + }, + "id": { + "type": "string", + "value": "[resourceId('Microsoft.Storage/storageAccounts', take(format('urldata{0}', uniqueString(resourceGroup().id)), 24))]" + } + } + } + }, + "dependsOn": [ + "[subscriptionResourceId('Microsoft.Resources/resourceGroups', format('rg-{0}', parameters('environmentName')))]" + ] + }, + { + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "url-data-roles", + "resourceGroup": "[format('rg-{0}', parameters('environmentName'))]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "location": { + "value": "[parameters('location')]" + }, + "principalId": { + "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, format('rg-{0}', parameters('environmentName'))), 'Microsoft.Resources/deployments', 'resources'), '2025-04-01').outputs.MANAGED_IDENTITY_PRINCIPAL_ID.value]" + }, + "principalType": { + "value": "ServicePrincipal" + }, + "url_data_outputs_name": { + "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, format('rg-{0}', parameters('environmentName'))), 'Microsoft.Resources/deployments', 'url-data'), '2025-04-01').outputs.name.value]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.45.15.27210", + "templateHash": "5468789998190738231" + } + }, + "parameters": { + "location": { + "type": "string", + "defaultValue": "[resourceGroup().location]", + "metadata": { + "description": "The location for the resource(s) to be deployed." + } + }, + "url_data_outputs_name": { + "type": "string" + }, + "principalType": { + "type": "string" + }, + "principalId": { + "type": "string" + } + }, + "resources": [ + { + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2022-04-01", + "scope": "[resourceId('Microsoft.Storage/storageAccounts', parameters('url_data_outputs_name'))]", + "name": "[guid(resourceId('Microsoft.Storage/storageAccounts', parameters('url_data_outputs_name')), parameters('principalId'), subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'ba92f5b4-2d11-453d-a403-e96b0029c9fe'))]", + "properties": { + "principalId": "[parameters('principalId')]", + "roleDefinitionId": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'ba92f5b4-2d11-453d-a403-e96b0029c9fe')]", + "principalType": "[parameters('principalType')]" + } + }, + { + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2022-04-01", + "scope": "[resourceId('Microsoft.Storage/storageAccounts', parameters('url_data_outputs_name'))]", + "name": "[guid(resourceId('Microsoft.Storage/storageAccounts', parameters('url_data_outputs_name')), parameters('principalId'), subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '0a9a7e1f-b9d0-4cc4-a60d-0319b160aaa3'))]", + "properties": { + "principalId": "[parameters('principalId')]", + "roleDefinitionId": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '0a9a7e1f-b9d0-4cc4-a60d-0319b160aaa3')]", + "principalType": "[parameters('principalType')]" + } + }, + { + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2022-04-01", + "scope": "[resourceId('Microsoft.Storage/storageAccounts', parameters('url_data_outputs_name'))]", + "name": "[guid(resourceId('Microsoft.Storage/storageAccounts', parameters('url_data_outputs_name')), parameters('principalId'), subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '974c5e8b-45b9-4653-ba55-5f855dd0fb88'))]", + "properties": { + "principalId": "[parameters('principalId')]", + "roleDefinitionId": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '974c5e8b-45b9-4653-ba55-5f855dd0fb88')]", + "principalType": "[parameters('principalType')]" + } + } + ] + } + }, + "dependsOn": [ + "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, format('rg-{0}', parameters('environmentName'))), 'Microsoft.Resources/deployments', 'resources')]", + "[subscriptionResourceId('Microsoft.Resources/resourceGroups', format('rg-{0}', parameters('environmentName')))]", + "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, format('rg-{0}', parameters('environmentName'))), 'Microsoft.Resources/deployments', 'url-data')]" + ] + } + ], + "outputs": { + "MANAGED_IDENTITY_CLIENT_ID": { + "type": "string", + "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, format('rg-{0}', parameters('environmentName'))), 'Microsoft.Resources/deployments', 'resources'), '2025-04-01').outputs.MANAGED_IDENTITY_CLIENT_ID.value]" + }, + "MANAGED_IDENTITY_NAME": { + "type": "string", + "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, format('rg-{0}', parameters('environmentName'))), 'Microsoft.Resources/deployments', 'resources'), '2025-04-01').outputs.MANAGED_IDENTITY_NAME.value]" + }, + "AZURE_LOG_ANALYTICS_WORKSPACE_NAME": { + "type": "string", + "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, format('rg-{0}', parameters('environmentName'))), 'Microsoft.Resources/deployments', 'resources'), '2025-04-01').outputs.AZURE_LOG_ANALYTICS_WORKSPACE_NAME.value]" + }, + "AZURE_CONTAINER_REGISTRY_ENDPOINT": { + "type": "string", + "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, format('rg-{0}', parameters('environmentName'))), 'Microsoft.Resources/deployments', 'resources'), '2025-04-01').outputs.AZURE_CONTAINER_REGISTRY_ENDPOINT.value]" + }, + "AZURE_CONTAINER_REGISTRY_MANAGED_IDENTITY_ID": { + "type": "string", + "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, format('rg-{0}', parameters('environmentName'))), 'Microsoft.Resources/deployments', 'resources'), '2025-04-01').outputs.AZURE_CONTAINER_REGISTRY_MANAGED_IDENTITY_ID.value]" + }, + "AZURE_CONTAINER_REGISTRY_NAME": { + "type": "string", + "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, format('rg-{0}', parameters('environmentName'))), 'Microsoft.Resources/deployments', 'resources'), '2025-04-01').outputs.AZURE_CONTAINER_REGISTRY_NAME.value]" + }, + "AZURE_CONTAINER_APPS_ENVIRONMENT_NAME": { + "type": "string", + "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, format('rg-{0}', parameters('environmentName'))), 'Microsoft.Resources/deployments', 'resources'), '2025-04-01').outputs.AZURE_CONTAINER_APPS_ENVIRONMENT_NAME.value]" + }, + "AZURE_CONTAINER_APPS_ENVIRONMENT_ID": { + "type": "string", + "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, format('rg-{0}', parameters('environmentName'))), 'Microsoft.Resources/deployments', 'resources'), '2025-04-01').outputs.AZURE_CONTAINER_APPS_ENVIRONMENT_ID.value]" + }, + "AZURE_CONTAINER_APPS_ENVIRONMENT_DEFAULT_DOMAIN": { + "type": "string", + "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, format('rg-{0}', parameters('environmentName'))), 'Microsoft.Resources/deployments', 'resources'), '2025-04-01').outputs.AZURE_CONTAINER_APPS_ENVIRONMENT_DEFAULT_DOMAIN.value]" + }, + "FUNCSTORAGEA17CA_BLOBENDPOINT": { + "type": "string", + "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, format('rg-{0}', parameters('environmentName'))), 'Microsoft.Resources/deployments', 'funcstoragea17ca'), '2025-04-01').outputs.blobEndpoint.value]" + }, + "FUNCSTORAGEA17CA_DATALAKEENDPOINT": { + "type": "string", + "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, format('rg-{0}', parameters('environmentName'))), 'Microsoft.Resources/deployments', 'funcstoragea17ca'), '2025-04-01').outputs.dataLakeEndpoint.value]" + }, + "FUNCSTORAGEA17CA_QUEUEENDPOINT": { + "type": "string", + "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, format('rg-{0}', parameters('environmentName'))), 'Microsoft.Resources/deployments', 'funcstoragea17ca'), '2025-04-01').outputs.queueEndpoint.value]" + }, + "FUNCSTORAGEA17CA_TABLEENDPOINT": { + "type": "string", + "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, format('rg-{0}', parameters('environmentName'))), 'Microsoft.Resources/deployments', 'funcstoragea17ca'), '2025-04-01').outputs.tableEndpoint.value]" + }, + "URL_DATA_TABLEENDPOINT": { + "type": "string", + "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, format('rg-{0}', parameters('environmentName'))), 'Microsoft.Resources/deployments', 'url-data'), '2025-04-01').outputs.tableEndpoint.value]" + } + } +} \ No newline at end of file diff --git a/src/infra/resources.bicep b/src/infra/resources.bicep index 6bedbd68..61533054 100644 --- a/src/infra/resources.bicep +++ b/src/infra/resources.bicep @@ -72,15 +72,6 @@ resource containerAppEnvironment 'Microsoft.App/managedEnvironments@2024-02-02-p } -resource explicitContributorUserRoleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = { - name: guid(containerAppEnvironment.id, principalId, subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'b24988ac-6180-42a0-ab88-20f7382dd24c')) - scope: containerAppEnvironment - properties: { - principalId: principalId - roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'b24988ac-6180-42a0-ab88-20f7382dd24c') - } -} - output MANAGED_IDENTITY_CLIENT_ID string = managedIdentity.properties.clientId output MANAGED_IDENTITY_NAME string = managedIdentity.name output MANAGED_IDENTITY_PRINCIPAL_ID string = managedIdentity.properties.principalId diff --git a/src/infra/url-data-roles/url-data-roles.module.bicep b/src/infra/url-data-roles/url-data-roles.module.bicep new file mode 100644 index 00000000..e029cdc0 --- /dev/null +++ b/src/infra/url-data-roles/url-data-roles.module.bicep @@ -0,0 +1,42 @@ +@description('The location for the resource(s) to be deployed.') +param location string = resourceGroup().location + +param url_data_outputs_name string + +param principalType string + +param principalId string + +resource url_data 'Microsoft.Storage/storageAccounts@2024-01-01' existing = { + name: url_data_outputs_name +} + +resource url_data_StorageBlobDataContributor 'Microsoft.Authorization/roleAssignments@2022-04-01' = { + name: guid(url_data.id, principalId, subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'ba92f5b4-2d11-453d-a403-e96b0029c9fe')) + properties: { + principalId: principalId + roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'ba92f5b4-2d11-453d-a403-e96b0029c9fe') + principalType: principalType + } + scope: url_data +} + +resource url_data_StorageTableDataContributor 'Microsoft.Authorization/roleAssignments@2022-04-01' = { + name: guid(url_data.id, principalId, subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '0a9a7e1f-b9d0-4cc4-a60d-0319b160aaa3')) + properties: { + principalId: principalId + roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '0a9a7e1f-b9d0-4cc4-a60d-0319b160aaa3') + principalType: principalType + } + scope: url_data +} + +resource url_data_StorageQueueDataContributor 'Microsoft.Authorization/roleAssignments@2022-04-01' = { + name: guid(url_data.id, principalId, subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '974c5e8b-45b9-4653-ba55-5f855dd0fb88')) + properties: { + principalId: principalId + roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '974c5e8b-45b9-4653-ba55-5f855dd0fb88') + principalType: principalType + } + scope: url_data +} \ No newline at end of file diff --git a/src/infra/url-data/url-data.module.bicep b/src/infra/url-data/url-data.module.bicep index 248c278c..6da8ab17 100644 --- a/src/infra/url-data/url-data.module.bicep +++ b/src/infra/url-data/url-data.module.bicep @@ -1,10 +1,6 @@ @description('The location for the resource(s) to be deployed.') param location string = resourceGroup().location -param principalId string - -param principalType string - resource url_data 'Microsoft.Storage/storageAccounts@2024-01-01' = { name: take('urldata${uniqueString(resourceGroup().id)}', 24) kind: 'StorageV2' @@ -15,6 +11,7 @@ resource url_data 'Microsoft.Storage/storageAccounts@2024-01-01' = { properties: { accessTier: 'Hot' allowSharedKeyAccess: false + isHnsEnabled: false minimumTlsVersion: 'TLS1_2' networkAcls: { defaultAction: 'Allow' @@ -25,43 +22,14 @@ resource url_data 'Microsoft.Storage/storageAccounts@2024-01-01' = { } } -resource blobs 'Microsoft.Storage/storageAccounts/blobServices@2024-01-01' = { - name: 'default' - parent: url_data -} +output blobEndpoint string = url_data.properties.primaryEndpoints.blob -resource url_data_StorageBlobDataContributor 'Microsoft.Authorization/roleAssignments@2022-04-01' = { - name: guid(url_data.id, principalId, subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'ba92f5b4-2d11-453d-a403-e96b0029c9fe')) - properties: { - principalId: principalId - roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'ba92f5b4-2d11-453d-a403-e96b0029c9fe') - principalType: principalType - } - scope: url_data -} +output dataLakeEndpoint string = url_data.properties.primaryEndpoints.dfs -resource url_data_StorageTableDataContributor 'Microsoft.Authorization/roleAssignments@2022-04-01' = { - name: guid(url_data.id, principalId, subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '0a9a7e1f-b9d0-4cc4-a60d-0319b160aaa3')) - properties: { - principalId: principalId - roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '0a9a7e1f-b9d0-4cc4-a60d-0319b160aaa3') - principalType: principalType - } - scope: url_data -} - -resource url_data_StorageQueueDataContributor 'Microsoft.Authorization/roleAssignments@2022-04-01' = { - name: guid(url_data.id, principalId, subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '974c5e8b-45b9-4653-ba55-5f855dd0fb88')) - properties: { - principalId: principalId - roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '974c5e8b-45b9-4653-ba55-5f855dd0fb88') - principalType: principalType - } - scope: url_data -} +output queueEndpoint string = url_data.properties.primaryEndpoints.queue -output blobEndpoint string = url_data.properties.primaryEndpoints.blob +output tableEndpoint string = url_data.properties.primaryEndpoints.table -output queueEndpoint string = url_data.properties.primaryEndpoints.queue +output name string = url_data.name -output tableEndpoint string = url_data.properties.primaryEndpoints.table \ No newline at end of file +output id string = url_data.id \ No newline at end of file diff --git a/src/next-steps.md b/src/next-steps.md index 4a558546..21e65176 100644 --- a/src/next-steps.md +++ b/src/next-steps.md @@ -17,10 +17,11 @@ To troubleshoot any issues, see [troubleshooting](#troubleshooting). ### Configure CI/CD pipeline -1. Create a workflow pipeline file locally. The following starters are available: - - [Deploy with GitHub Actions](https://github.com/Azure-Samples/azd-starter-bicep/blob/main/.github/workflows/azure-dev.yml) - - [Deploy with Azure Pipelines](https://github.com/Azure-Samples/azd-starter-bicep/blob/main/.azdo/pipelines/azure-dev.yml) -2. Run `azd pipeline config -e ` to configure the deployment pipeline to connect securely to Azure. An environment name is specified here to configure the pipeline with a different environment for isolation purposes. Run `azd env list` and `azd env set` to reselect the default environment after this step. +Run `azd pipeline config -e ` to configure the deployment pipeline to connect securely to Azure. An environment name is specified here to configure the pipeline with a different environment for isolation purposes. Run `azd env list` and `azd env set` to reselect the default environment after this step. + +- Deploying with `GitHub Actions`: Select `GitHub` when prompted for a provider. If your project lacks the `azure-dev.yml` file, accept the prompt to add it and proceed with pipeline configuration. + +- Deploying with `Azure DevOps Pipeline`: Select `Azure DevOps` when prompted for a provider. If your project lacks the `azure-dev.yml` file, accept the prompt to add it and proceed with pipeline configuration. ## What was added @@ -34,7 +35,7 @@ To describe the infrastructure and application, an `azure.yaml` was added with t This file contains a single service, which references your project's App Host. When needed, `azd` generates the required infrastructure as code in memory and uses it. -If you would like to see or modify the infrastructure that `azd` uses, run `azd infra synth` to persist it to disk. +If you would like to see or modify the infrastructure that `azd` uses, run `azd infra gen` to generate it to disk. If you do this, some additional directories will be created: @@ -46,9 +47,7 @@ If you do this, some additional directories will be created: In addition, for each project resource referenced by your app host, a `containerApp.tmpl.yaml` file will be created in a directory named `manifests` next the project file. This file contains the infrastructure as code for running the project on Azure Container Apps. -*Note*: Once you have synthesized your infrastructure to disk, changes made to your App Host will not be reflected in the infrastructure. You can re-generate the infrastructure by running `azd infra synth` again. It will prompt you before overwriting files. You can pass `--force` to force `azd infra synth` to overwrite the files without prompting. - -*Note*: `azd infra synth` is currently an alpha feature and must be explicitly enabled by running `azd config set alpha.infraSynth on`. You only need to do this once. +*Note*: Once you have generated your infrastructure to disk, those files are the source of truth for azd. Any changes made to `azure.yaml` or your App Host will not be reflected in the infrastructure until you regenerate it with `azd infra gen` again. It will prompt you before overwriting files. You can pass `--force` to force `azd infra gen` to overwrite the files without prompting. ## Billing