From 86d22793cc59a189da4f2d213e1488a2e0124e09 Mon Sep 17 00:00:00 2001 From: Shubhangi-Microsoft Date: Tue, 24 Feb 2026 18:17:49 +0530 Subject: [PATCH 01/72] refactor(frontend): complete Tasks 1-5 UI refactoring Task 1: Centralize state with Redux Toolkit (4 slices, typed hooks, selectors) Task 2: Centralized HTTP client with interceptors (httpClient singleton) Task 3: Extract 18 reusable components from monolithic files Task 4: Wrap all components with React.memo + displayName, useCallback throughout Task 5: Extract business logic into 7 custom hooks (useChatOrchestrator, etc.) - App.tsx reduced from ~787 to ~85 lines - Zero raw fetch calls (except /.auth/me) - All console.log downgraded to console.debug - Build: 0 TypeScript errors --- content-gen/src/app/frontend/src/App.tsx | 884 ++---------------- .../src/app/frontend/src/api/httpClient.ts | 197 ++++ content-gen/src/app/frontend/src/api/index.ts | 118 +-- .../app/frontend/src/components/AppHeader.tsx | 68 ++ .../frontend/src/components/BriefReview.tsx | 50 +- .../frontend/src/components/ChatHistory.tsx | 457 ++------- .../app/frontend/src/components/ChatInput.tsx | 148 +++ .../app/frontend/src/components/ChatPanel.tsx | 405 ++------ .../src/components/ComplianceSection.tsx | 192 ++++ .../src/components/ConfirmedBriefView.tsx | 6 +- .../src/components/ConversationItem.tsx | 297 ++++++ .../src/components/ImagePreviewCard.tsx | 99 ++ .../src/components/InlineContentPreview.tsx | 437 +-------- .../frontend/src/components/MessageBubble.tsx | 118 +++ .../frontend/src/components/ProductCard.tsx | 136 +++ .../frontend/src/components/ProductReview.tsx | 126 +-- .../src/components/SelectedProductView.tsx | 95 +- .../src/components/SuggestionCard.tsx | 84 ++ .../src/components/TypingIndicator.tsx | 76 ++ .../frontend/src/components/ViolationCard.tsx | 66 ++ .../frontend/src/components/WelcomeCard.tsx | 83 +- .../app/frontend/src/hooks/useAutoScroll.ts | 30 + .../frontend/src/hooks/useChatOrchestrator.ts | 547 +++++++++++ .../src/hooks/useContentGeneration.ts | 170 ++++ .../src/hooks/useConversationActions.ts | 296 ++++++ .../frontend/src/hooks/useCopyToClipboard.ts | 32 + .../src/app/frontend/src/hooks/useDebounce.ts | 29 + .../app/frontend/src/hooks/useWindowSize.ts | 19 + content-gen/src/app/frontend/src/main.tsx | 10 +- .../src/app/frontend/src/store/appSlice.ts | 100 ++ .../frontend/src/store/chatHistorySlice.ts | 138 +++ .../src/app/frontend/src/store/chatSlice.ts | 67 ++ .../app/frontend/src/store/contentSlice.ts | 61 ++ .../src/app/frontend/src/store/hooks.ts | 9 + .../src/app/frontend/src/store/index.ts | 77 ++ .../src/app/frontend/src/store/selectors.ts | 36 + .../src/app/frontend/src/store/store.ts | 21 + .../app/frontend/src/utils/contentErrors.ts | 31 + .../app/frontend/src/utils/downloadImage.ts | 94 ++ 39 files changed, 3613 insertions(+), 2296 deletions(-) create mode 100644 content-gen/src/app/frontend/src/api/httpClient.ts create mode 100644 content-gen/src/app/frontend/src/components/AppHeader.tsx create mode 100644 content-gen/src/app/frontend/src/components/ChatInput.tsx create mode 100644 content-gen/src/app/frontend/src/components/ComplianceSection.tsx create mode 100644 content-gen/src/app/frontend/src/components/ConversationItem.tsx create mode 100644 content-gen/src/app/frontend/src/components/ImagePreviewCard.tsx create mode 100644 content-gen/src/app/frontend/src/components/MessageBubble.tsx create mode 100644 content-gen/src/app/frontend/src/components/ProductCard.tsx create mode 100644 content-gen/src/app/frontend/src/components/SuggestionCard.tsx create mode 100644 content-gen/src/app/frontend/src/components/TypingIndicator.tsx create mode 100644 content-gen/src/app/frontend/src/components/ViolationCard.tsx create mode 100644 content-gen/src/app/frontend/src/hooks/useAutoScroll.ts create mode 100644 content-gen/src/app/frontend/src/hooks/useChatOrchestrator.ts create mode 100644 content-gen/src/app/frontend/src/hooks/useContentGeneration.ts create mode 100644 content-gen/src/app/frontend/src/hooks/useConversationActions.ts create mode 100644 content-gen/src/app/frontend/src/hooks/useCopyToClipboard.ts create mode 100644 content-gen/src/app/frontend/src/hooks/useDebounce.ts create mode 100644 content-gen/src/app/frontend/src/hooks/useWindowSize.ts create mode 100644 content-gen/src/app/frontend/src/store/appSlice.ts create mode 100644 content-gen/src/app/frontend/src/store/chatHistorySlice.ts create mode 100644 content-gen/src/app/frontend/src/store/chatSlice.ts create mode 100644 content-gen/src/app/frontend/src/store/contentSlice.ts create mode 100644 content-gen/src/app/frontend/src/store/hooks.ts create mode 100644 content-gen/src/app/frontend/src/store/index.ts create mode 100644 content-gen/src/app/frontend/src/store/selectors.ts create mode 100644 content-gen/src/app/frontend/src/store/store.ts create mode 100644 content-gen/src/app/frontend/src/utils/contentErrors.ts create mode 100644 content-gen/src/app/frontend/src/utils/downloadImage.ts diff --git a/content-gen/src/app/frontend/src/App.tsx b/content-gen/src/app/frontend/src/App.tsx index 9a769bcca..c0cd14934 100644 --- a/content-gen/src/app/frontend/src/App.tsx +++ b/content-gen/src/app/frontend/src/App.tsx @@ -1,860 +1,80 @@ -import { useState, useCallback, useEffect, useRef } from 'react'; -import { - Text, - Avatar, - Button, - Tooltip, - tokens, -} from '@fluentui/react-components'; -import { - History24Regular, - History24Filled, -} from '@fluentui/react-icons'; -import { v4 as uuidv4 } from 'uuid'; +import { useEffect, useRef } from 'react'; import { ChatPanel } from './components/ChatPanel'; import { ChatHistory } from './components/ChatHistory'; -import type { ChatMessage, CreativeBrief, Product, GeneratedContent } from './types'; -import ContosoLogo from './styles/images/contoso.svg'; +import { AppHeader } from './components/AppHeader'; +import { + useAppDispatch, + useAppSelector, + fetchAppConfig, + fetchUserInfo, + selectUserName, + selectShowChatHistory, +} from './store'; +import { useChatOrchestrator } from './hooks/useChatOrchestrator'; +import { useContentGeneration } from './hooks/useContentGeneration'; +import { useConversationActions } from './hooks/useConversationActions'; function App() { - const [conversationId, setConversationId] = useState(() => uuidv4()); - const [conversationTitle, setConversationTitle] = useState(null); - const [userId, setUserId] = useState(''); - const [userName, setUserName] = useState(''); - const [messages, setMessages] = useState([]); - const [isLoading, setIsLoading] = useState(false); - const [generationStatus, setGenerationStatus] = useState(''); - - // Feature flags from config - const [imageGenerationEnabled, setImageGenerationEnabled] = useState(true); - - // Brief confirmation flow - const [pendingBrief, setPendingBrief] = useState(null); - const [confirmedBrief, setConfirmedBrief] = useState(null); - const [awaitingClarification, setAwaitingClarification] = useState(false); - - // Product selection - const [selectedProducts, setSelectedProducts] = useState([]); - const [availableProducts, setAvailableProducts] = useState([]); - - // Generated content - const [generatedContent, setGeneratedContent] = useState(null); + const dispatch = useAppDispatch(); + const userName = useAppSelector(selectUserName); + const showChatHistory = useAppSelector(selectShowChatHistory); - // Trigger for refreshing chat history - const [historyRefreshTrigger, setHistoryRefreshTrigger] = useState(0); - - // Toggle for showing/hiding chat history panel - const [showChatHistory, setShowChatHistory] = useState(true); - - // Abort controller for cancelling ongoing requests + // Shared abort controller for chat & content-generation const abortControllerRef = useRef(null); - // Fetch app config on mount - useEffect(() => { - const fetchConfig = async () => { - try { - const { getAppConfig } = await import('./api'); - const config = await getAppConfig(); - setImageGenerationEnabled(config.enable_image_generation); - } catch (err) { - console.error('Error fetching config:', err); - // Default to enabled if config fetch fails - setImageGenerationEnabled(true); - } - }; - fetchConfig(); - }, []); - - // Fetch current user on mount - using /.auth/me (Azure App Service built-in auth endpoint) + // Business-logic hooks + const { sendMessage } = useChatOrchestrator(abortControllerRef); + const { generateContent, stopGeneration } = useContentGeneration(abortControllerRef); + const { + selectConversation, + newConversation, + confirmBrief, + cancelBrief, + productsStartOver, + selectProduct, + toggleHistory, + } = useConversationActions(); + + // Fetch app config & current user on mount useEffect(() => { - const fetchUser = async () => { - try { - const response = await fetch('/.auth/me'); - if (response.ok) { - const payload = await response.json(); - - // Extract user ID from objectidentifier claim - const userClaims = payload[0]?.user_claims || []; - const objectIdClaim = userClaims.find( - (claim: { typ: string; val: string }) => - claim.typ === 'http://schemas.microsoft.com/identity/claims/objectidentifier' - ); - setUserId(objectIdClaim?.val || 'anonymous'); - - // Extract display name from 'name' claim - const nameClaim = userClaims.find( - (claim: { typ: string; val: string }) => claim.typ === 'name' - ); - setUserName(nameClaim?.val || ''); - } - } catch (err) { - console.error('Error fetching user:', err); - setUserId('anonymous'); - setUserName(''); - } - }; - fetchUser(); - }, []); - - // Handle selecting a conversation from history - const handleSelectConversation = useCallback(async (selectedConversationId: string) => { - try { - const response = await fetch(`/api/conversations/${selectedConversationId}?user_id=${encodeURIComponent(userId)}`); - if (response.ok) { - const data = await response.json(); - setConversationId(selectedConversationId); - setConversationTitle(null); // Will use title from conversation list - const loadedMessages: ChatMessage[] = (data.messages || []).map((msg: { role: string; content: string; timestamp?: string; agent?: string }, index: number) => ({ - id: `${selectedConversationId}-${index}`, - role: msg.role as 'user' | 'assistant', - content: msg.content, - timestamp: msg.timestamp || new Date().toISOString(), - agent: msg.agent, - })); - setMessages(loadedMessages); - setPendingBrief(null); - setAwaitingClarification(false); - setConfirmedBrief(data.brief || null); - - // Restore availableProducts so product/color name detection works - // when regenerating images in a restored conversation - if (data.brief) { - try { - const productsResponse = await fetch('/api/products'); - if (productsResponse.ok) { - const productsData = await productsResponse.json(); - setAvailableProducts(productsData.products || []); - } - } catch (err) { - console.error('Error loading products for restored conversation:', err); - } - } - - if (data.generated_content) { - const gc = data.generated_content; - let textContent = gc.text_content; - if (typeof textContent === 'string') { - try { - textContent = JSON.parse(textContent); - } catch { - } - } - - let imageUrl: string | undefined = gc.image_url; - if (imageUrl && imageUrl.includes('blob.core.windows.net')) { - const parts = imageUrl.split('/'); - const filename = parts[parts.length - 1]; - const convId = parts[parts.length - 2]; - imageUrl = `/api/images/${convId}/${filename}`; - } - if (!imageUrl && gc.image_base64) { - imageUrl = `data:image/png;base64,${gc.image_base64}`; - } - - const restoredContent: GeneratedContent = { - text_content: typeof textContent === 'object' && textContent ? { - headline: textContent?.headline, - body: textContent?.body, - cta_text: textContent?.cta, - tagline: textContent?.tagline, - } : undefined, - image_content: (imageUrl || gc.image_prompt) ? { - image_url: imageUrl, - prompt_used: gc.image_prompt, - alt_text: gc.image_revised_prompt || 'Generated marketing image', - } : undefined, - violations: gc.violations || [], - requires_modification: gc.requires_modification || false, - error: gc.error, - image_error: gc.image_error, - text_error: gc.text_error, - }; - setGeneratedContent(restoredContent); - - if (gc.selected_products && Array.isArray(gc.selected_products)) { - setSelectedProducts(gc.selected_products); - } else { - setSelectedProducts([]); - } - } else { - setGeneratedContent(null); - setSelectedProducts([]); - } - } - } catch (error) { - console.error('Error loading conversation:', error); - } - }, [userId]); - - // Handle starting a new conversation - const handleNewConversation = useCallback(() => { - setConversationId(uuidv4()); - setConversationTitle(null); - setMessages([]); - setPendingBrief(null); - setAwaitingClarification(false); - setConfirmedBrief(null); - setGeneratedContent(null); - setSelectedProducts([]); - }, []); - - const handleSendMessage = useCallback(async (content: string) => { - const userMessage: ChatMessage = { - id: uuidv4(), - role: 'user', - content, - timestamp: new Date().toISOString(), - }; - - setMessages(prev => [...prev, userMessage]); - setIsLoading(true); - - // Create new abort controller for this request - abortControllerRef.current = new AbortController(); - const signal = abortControllerRef.current.signal; - - try { - // Import dynamically to avoid SSR issues - const { streamChat, parseBrief, selectProducts } = await import('./api'); - - // If we have a pending brief and user is providing feedback, update the brief - if (pendingBrief && !confirmedBrief) { - // User is refining the brief or providing clarification - const refinementKeywords = ['change', 'update', 'modify', 'add', 'remove', 'delete', 'set', 'make', 'should be']; - const isRefinement = refinementKeywords.some(kw => content.toLowerCase().includes(kw)); - - // If awaiting clarification, treat ANY response as a brief update - if (isRefinement || awaitingClarification) { - // Send the refinement request to update the brief - // Combine original brief context with the refinement request - const refinementPrompt = `Current creative brief:\n${JSON.stringify(pendingBrief, null, 2)}\n\nUser requested change: ${content}\n\nPlease update the brief accordingly and return the complete updated brief.`; - - setGenerationStatus('Updating creative brief...'); - const parsed = await parseBrief(refinementPrompt, conversationId, userId, signal); - if (parsed.generated_title && !conversationTitle) { - setConversationTitle(parsed.generated_title); - } - if (parsed.brief) { - setPendingBrief(parsed.brief); - } - - // Check if we still need more clarification - if (parsed.requires_clarification && parsed.clarifying_questions) { - setAwaitingClarification(true); - setGenerationStatus(''); - - const assistantMessage: ChatMessage = { - id: uuidv4(), - role: 'assistant', - content: parsed.clarifying_questions, - agent: 'PlanningAgent', - timestamp: new Date().toISOString(), - }; - setMessages(prev => [...prev, assistantMessage]); - } else { - // Brief is now complete - setAwaitingClarification(false); - setGenerationStatus(''); - - const assistantMessage: ChatMessage = { - id: uuidv4(), - role: 'assistant', - content: "I've updated the brief based on your feedback. Please review the changes above. Let me know if you'd like any other modifications, or click **Confirm Brief** when you're satisfied.", - agent: 'PlanningAgent', - timestamp: new Date().toISOString(), - }; - setMessages(prev => [...prev, assistantMessage]); - } - } else { - // General question or comment while brief is pending - let fullContent = ''; - let currentAgent = ''; - let messageAdded = false; - - setGenerationStatus('Processing your question...'); - for await (const response of streamChat(content, conversationId, userId, signal)) { - if (response.type === 'agent_response') { - fullContent = response.content; - currentAgent = response.agent || ''; - - if ((response.is_final || response.requires_user_input) && !messageAdded) { - const assistantMessage: ChatMessage = { - id: uuidv4(), - role: 'assistant', - content: fullContent, - agent: currentAgent, - timestamp: new Date().toISOString(), - }; - setMessages(prev => [...prev, assistantMessage]); - messageAdded = true; - } - } else if (response.type === 'error') { - const errorMessage: ChatMessage = { - id: uuidv4(), - role: 'assistant', - content: response.content || 'An error occurred while processing your request.', - timestamp: new Date().toISOString(), - }; - setMessages(prev => [...prev, errorMessage]); - messageAdded = true; - } - } - setGenerationStatus(''); - } - } else if (confirmedBrief && !generatedContent) { - // Brief confirmed, in product selection phase - treat messages as product selection requests - setGenerationStatus('Finding products...'); - const result = await selectProducts(content, selectedProducts, conversationId, userId, signal); - - // Update selected products with the result - setSelectedProducts(result.products || []); - setGenerationStatus(''); - - const assistantMessage: ChatMessage = { - id: uuidv4(), - role: 'assistant', - content: result.message || 'Products updated.', - agent: 'ProductAgent', - timestamp: new Date().toISOString(), - }; - setMessages(prev => [...prev, assistantMessage]); - } else if (generatedContent && confirmedBrief) { - // Content has been generated - check if user wants to modify the image - const imageModificationKeywords = [ - 'change', 'modify', 'update', 'replace', 'show', 'display', 'use', - 'instead', 'different', 'another', 'make it', 'make the', - 'kitchen', 'dining', 'living', 'bedroom', 'bathroom', 'outdoor', 'office', - 'room', 'scene', 'setting', 'background', 'style', 'color', 'lighting' - ]; - const isImageModification = imageModificationKeywords.some(kw => content.toLowerCase().includes(kw)); - - if (isImageModification) { - // User wants to modify the image - use regeneration endpoint - const { streamRegenerateImage } = await import('./api'); - - setGenerationStatus('Regenerating image with your changes...'); - - let responseData: GeneratedContent | null = null; - let messageContent = ''; - - // Detect if the user's prompt mentions a different product/color name - // BEFORE the API call so the correct product is sent and persisted - const mentionedProduct = availableProducts.find(p => - content.toLowerCase().includes(p.product_name.toLowerCase()) - ); - const productsForRequest = mentionedProduct ? [mentionedProduct] : selectedProducts; - - // Get previous prompt from image_content if available - const previousPrompt = generatedContent.image_content?.prompt_used; - - for await (const response of streamRegenerateImage( - content, - confirmedBrief, - productsForRequest, - previousPrompt, - conversationId, - userId, - signal - )) { - if (response.type === 'heartbeat') { - setGenerationStatus(response.message || 'Regenerating image...'); - } else if (response.type === 'agent_response' && response.is_final) { - try { - const parsedContent = JSON.parse(response.content); - - // Update generatedContent with new image - if (parsedContent.image_url || parsedContent.image_base64) { - // Replace old color/product name in text_content when switching products - const oldName = selectedProducts[0]?.product_name; - const newName = mentionedProduct?.product_name; - const nameRegex = oldName - ? new RegExp(oldName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'gi') - : undefined; - const swapName = (s?: string) => { - if (!s || !oldName || !newName || oldName === newName || !nameRegex) return s; - return s.replace(nameRegex, () => newName); - }; - const tc = generatedContent.text_content; - - responseData = { - ...generatedContent, - text_content: mentionedProduct ? { ...tc, headline: swapName(tc?.headline), body: swapName(tc?.body), tagline: swapName(tc?.tagline), cta_text: swapName(tc?.cta_text) } : tc, - image_content: { - ...generatedContent.image_content, - image_url: parsedContent.image_url || generatedContent.image_content?.image_url, - image_base64: parsedContent.image_base64, - prompt_used: parsedContent.image_prompt || generatedContent.image_content?.prompt_used, - }, - }; - setGeneratedContent(responseData); - - // Update the selected product/color name now that the new image is ready - if (mentionedProduct) { - setSelectedProducts([mentionedProduct]); - } - - // Update the confirmed brief to include the modification - // This ensures subsequent "Regenerate" clicks use the updated visual guidelines - const updatedBrief = { - ...confirmedBrief, - visual_guidelines: `${confirmedBrief.visual_guidelines}. User modification: ${content}`, - }; - setConfirmedBrief(updatedBrief); - - messageContent = parsedContent.message || 'Image regenerated with your requested changes.'; - } else if (parsedContent.error) { - messageContent = parsedContent.error; - } else { - messageContent = parsedContent.message || 'I processed your request.'; - } - } catch { - messageContent = response.content || 'Image regenerated.'; - } - } else if (response.type === 'error') { - messageContent = response.content || 'An error occurred while regenerating the image.'; - } - } - - setGenerationStatus(''); - - const assistantMessage: ChatMessage = { - id: uuidv4(), - role: 'assistant', - content: messageContent, - agent: 'ImageAgent', - timestamp: new Date().toISOString(), - }; - setMessages(prev => [...prev, assistantMessage]); - } else { - // General question after content generation - use regular chat - let fullContent = ''; - let currentAgent = ''; - let messageAdded = false; - - setGenerationStatus('Processing your request...'); - for await (const response of streamChat(content, conversationId, userId, signal)) { - if (response.type === 'agent_response') { - fullContent = response.content; - currentAgent = response.agent || ''; - - if ((response.is_final || response.requires_user_input) && !messageAdded) { - const assistantMessage: ChatMessage = { - id: uuidv4(), - role: 'assistant', - content: fullContent, - agent: currentAgent, - timestamp: new Date().toISOString(), - }; - setMessages(prev => [...prev, assistantMessage]); - messageAdded = true; - } - } else if (response.type === 'error') { - const errorMessage: ChatMessage = { - id: uuidv4(), - role: 'assistant', - content: response.content || 'An error occurred.', - timestamp: new Date().toISOString(), - }; - setMessages(prev => [...prev, errorMessage]); - messageAdded = true; - } - } - setGenerationStatus(''); - } - } else { - // Check if this looks like a creative brief - const briefKeywords = ['campaign', 'marketing', 'target audience', 'objective', 'deliverable']; - const isBriefLike = briefKeywords.some(kw => content.toLowerCase().includes(kw)); - - if (isBriefLike && !confirmedBrief) { - // Parse as a creative brief - setGenerationStatus('Analyzing creative brief...'); - const parsed = await parseBrief(content, conversationId, userId, signal); - - // Set conversation title from generated title - if (parsed.generated_title && !conversationTitle) { - setConversationTitle(parsed.generated_title); - } - - // Check if request was blocked due to harmful content - if (parsed.rai_blocked) { - // Show the refusal message without any brief UI - setGenerationStatus(''); - - const assistantMessage: ChatMessage = { - id: uuidv4(), - role: 'assistant', - content: parsed.message, - agent: 'ContentSafety', - timestamp: new Date().toISOString(), - }; - setMessages(prev => [...prev, assistantMessage]); - } else if (parsed.requires_clarification && parsed.clarifying_questions) { - // Set partial brief for display but show clarifying questions - if (parsed.brief) { - setPendingBrief(parsed.brief); - } - setAwaitingClarification(true); - setGenerationStatus(''); - - const assistantMessage: ChatMessage = { - id: uuidv4(), - role: 'assistant', - content: parsed.clarifying_questions, - agent: 'PlanningAgent', - timestamp: new Date().toISOString(), - }; - setMessages(prev => [...prev, assistantMessage]); - } else { - // Brief is complete, show for confirmation - if (parsed.brief) { - setPendingBrief(parsed.brief); - } - setAwaitingClarification(false); - setGenerationStatus(''); - - const assistantMessage: ChatMessage = { - id: uuidv4(), - role: 'assistant', - content: "I've parsed your creative brief. Please review the details below and let me know if you'd like to make any changes. You can say things like \"change the target audience to...\" or \"add a call to action...\". When everything looks good, click **Confirm Brief** to proceed.", - agent: 'PlanningAgent', - timestamp: new Date().toISOString(), - }; - setMessages(prev => [...prev, assistantMessage]); - } - } else { - // Stream chat response - let fullContent = ''; - let currentAgent = ''; - let messageAdded = false; - - setGenerationStatus('Processing your request...'); - for await (const response of streamChat(content, conversationId, userId, signal)) { - if (response.type === 'agent_response') { - fullContent = response.content; - currentAgent = response.agent || ''; - - // Add message when final OR when requiring user input (interactive response) - if ((response.is_final || response.requires_user_input) && !messageAdded) { - const assistantMessage: ChatMessage = { - id: uuidv4(), - role: 'assistant', - content: fullContent, - agent: currentAgent, - timestamp: new Date().toISOString(), - }; - setMessages(prev => [...prev, assistantMessage]); - messageAdded = true; - } - } else if (response.type === 'error') { - // Handle error responses - const errorMessage: ChatMessage = { - id: uuidv4(), - role: 'assistant', - content: response.content || 'An error occurred while processing your request.', - timestamp: new Date().toISOString(), - }; - setMessages(prev => [...prev, errorMessage]); - messageAdded = true; - } - } - setGenerationStatus(''); - } - } - } catch (error) { - // Check if this was a user-initiated cancellation - if (error instanceof Error && error.name === 'AbortError') { - console.log('Request cancelled by user'); - const cancelMessage: ChatMessage = { - id: uuidv4(), - role: 'assistant', - content: 'Generation stopped.', - timestamp: new Date().toISOString(), - }; - setMessages(prev => [...prev, cancelMessage]); - } else { - console.error('Error sending message:', error); - const errorMessage: ChatMessage = { - id: uuidv4(), - role: 'assistant', - content: 'Sorry, there was an error processing your request. Please try again.', - timestamp: new Date().toISOString(), - }; - setMessages(prev => [...prev, errorMessage]); - } - } finally { - setIsLoading(false); - setGenerationStatus(''); - abortControllerRef.current = null; - // Trigger refresh of chat history after message is sent - setHistoryRefreshTrigger(prev => prev + 1); - } - }, [conversationId, userId, confirmedBrief, pendingBrief, selectedProducts, generatedContent, availableProducts]); - - const handleBriefConfirm = useCallback(async () => { - if (!pendingBrief) return; - - try { - const { confirmBrief } = await import('./api'); - await confirmBrief(pendingBrief, conversationId, userId); - setConfirmedBrief(pendingBrief); - setPendingBrief(null); - setAwaitingClarification(false); - - const productsResponse = await fetch('/api/products'); - if (productsResponse.ok) { - const productsData = await productsResponse.json(); - setAvailableProducts(productsData.products || []); - } - - const assistantMessage: ChatMessage = { - id: uuidv4(), - role: 'assistant', - content: "Great! Your creative brief has been confirmed. Here are the available products for your campaign. Select the ones you'd like to feature, or tell me what you're looking for.", - agent: 'ProductAgent', - timestamp: new Date().toISOString(), - }; - setMessages(prev => [...prev, assistantMessage]); - } catch (error) { - console.error('Error confirming brief:', error); - } - }, [conversationId, userId, pendingBrief]); - - const handleBriefCancel = useCallback(() => { - setPendingBrief(null); - setAwaitingClarification(false); - const assistantMessage: ChatMessage = { - id: uuidv4(), - role: 'assistant', - content: 'No problem. Please provide your creative brief again or ask me any questions.', - timestamp: new Date().toISOString(), - }; - setMessages(prev => [...prev, assistantMessage]); - }, []); - - const handleProductsStartOver = useCallback(() => { - setSelectedProducts([]); - setConfirmedBrief(null); - const assistantMessage: ChatMessage = { - id: uuidv4(), - role: 'assistant', - content: 'Starting over. Please provide your creative brief to begin a new campaign.', - timestamp: new Date().toISOString(), - }; - setMessages(prev => [...prev, assistantMessage]); - }, []); - - const handleProductSelect = useCallback((product: Product) => { - setSelectedProducts(prev => { - const isSelected = prev.some(p => (p.sku || p.product_name) === (product.sku || product.product_name)); - if (isSelected) { - // Deselect - but user must have at least one selected to proceed - return []; - } else { - // Single selection mode - replace any existing selection - return [product]; - } - }); - }, []); - - const handleStopGeneration = useCallback(() => { - if (abortControllerRef.current) { - abortControllerRef.current.abort(); - } - }, []); - - const handleGenerateContent = useCallback(async () => { - if (!confirmedBrief) return; - - setIsLoading(true); - setGenerationStatus('Starting content generation...'); - - // Create new abort controller for this request - abortControllerRef.current = new AbortController(); - const signal = abortControllerRef.current.signal; - - try { - const { streamGenerateContent } = await import('./api'); - - for await (const response of streamGenerateContent( - confirmedBrief, - selectedProducts, - true, - conversationId, - userId, - signal - )) { - // Handle heartbeat events to show progress - if (response.type === 'heartbeat') { - // Use the message from the heartbeat directly - it contains the stage description - const statusMessage = response.content || 'Generating content...'; - const elapsed = (response as { elapsed?: number }).elapsed || 0; - setGenerationStatus(`${statusMessage} (${elapsed}s)`); - continue; - } - - if (response.is_final && response.type !== 'error') { - setGenerationStatus('Processing results...'); - try { - const rawContent = JSON.parse(response.content); - - // Parse text_content if it's a string (from orchestrator) - let textContent = rawContent.text_content; - if (typeof textContent === 'string') { - try { - textContent = JSON.parse(textContent); - } catch { - // Keep as string if not valid JSON - } - } - - // Build image_url: prefer blob URL, fallback to base64 data URL - let imageUrl: string | undefined; - if (rawContent.image_url) { - imageUrl = rawContent.image_url; - } else if (rawContent.image_base64) { - imageUrl = `data:image/png;base64,${rawContent.image_base64}`; - } - - const content: GeneratedContent = { - text_content: typeof textContent === 'object' ? { - headline: textContent?.headline, - body: textContent?.body, - cta_text: textContent?.cta, - tagline: textContent?.tagline, - } : undefined, - image_content: (imageUrl || rawContent.image_prompt) ? { - image_url: imageUrl, - prompt_used: rawContent.image_prompt, - alt_text: rawContent.image_revised_prompt || 'Generated marketing image', - } : undefined, - violations: rawContent.violations || [], - requires_modification: rawContent.requires_modification || false, - // Capture any generation errors - error: rawContent.error, - image_error: rawContent.image_error, - text_error: rawContent.text_error, - }; - setGeneratedContent(content); - setGenerationStatus(''); - - // Content is displayed via InlineContentPreview - no need for a separate chat message - } catch (parseError) { - console.error('Error parsing generated content:', parseError); - } - } else if (response.type === 'error') { - setGenerationStatus(''); - const errorMessage: ChatMessage = { - id: uuidv4(), - role: 'assistant', - content: `Error generating content: ${response.content}`, - timestamp: new Date().toISOString(), - }; - setMessages(prev => [...prev, errorMessage]); - } - } - } catch (error) { - // Check if this was a user-initiated cancellation - if (error instanceof Error && error.name === 'AbortError') { - console.log('Content generation cancelled by user'); - const cancelMessage: ChatMessage = { - id: uuidv4(), - role: 'assistant', - content: 'Content generation stopped.', - timestamp: new Date().toISOString(), - }; - setMessages(prev => [...prev, cancelMessage]); - } else { - console.error('Error generating content:', error); - const errorMessage: ChatMessage = { - id: uuidv4(), - role: 'assistant', - content: 'Sorry, there was an error generating content. Please try again.', - timestamp: new Date().toISOString(), - }; - setMessages(prev => [...prev, errorMessage]); - } - } finally { - setIsLoading(false); - setGenerationStatus(''); - abortControllerRef.current = null; - } - }, [confirmedBrief, selectedProducts, conversationId]); + dispatch(fetchAppConfig()); + dispatch(fetchUserInfo()); + }, [dispatch]); return (
{/* Header */} -
-
- Contoso - - Contoso - -
-
- -
-
- + + {/* Main Content */}
{/* Chat Panel - main area */}
- + {/* Chat History Sidebar - RIGHT side */} {showChatHistory && (
)} diff --git a/content-gen/src/app/frontend/src/api/httpClient.ts b/content-gen/src/app/frontend/src/api/httpClient.ts new file mode 100644 index 000000000..87b271c18 --- /dev/null +++ b/content-gen/src/app/frontend/src/api/httpClient.ts @@ -0,0 +1,197 @@ +/** + * Centralized HTTP client with interceptors. + * + * - Singleton — use the default `httpClient` export everywhere. + * - Request interceptors automatically attach auth headers + * (X-Ms-Client-Principal-Id) so callers never need to remember. + * - Response interceptors provide uniform error handling. + * - Built-in query-param serialization, configurable timeout, and base URL. + */ + +/* ------------------------------------------------------------------ */ +/* Types */ +/* ------------------------------------------------------------------ */ + +/** Options accepted by every request method. */ +export interface RequestOptions extends Omit { + /** Query parameters – appended to the URL automatically. */ + params?: Record; + /** Per-request timeout in ms (default: client-level `timeout`). */ + timeout?: number; +} + +type RequestInterceptor = (url: string, init: RequestInit) => RequestInit | Promise; +type ResponseInterceptor = (response: Response) => Response | Promise; + +/* ------------------------------------------------------------------ */ +/* HttpClient */ +/* ------------------------------------------------------------------ */ + +export class HttpClient { + private baseUrl: string; + private defaultTimeout: number; + private requestInterceptors: RequestInterceptor[] = []; + private responseInterceptors: ResponseInterceptor[] = []; + + constructor(baseUrl = '', timeout = 60_000) { + this.baseUrl = baseUrl; + this.defaultTimeout = timeout; + } + + /* ---------- interceptor registration ---------- */ + + onRequest(fn: RequestInterceptor): void { + this.requestInterceptors.push(fn); + } + + onResponse(fn: ResponseInterceptor): void { + this.responseInterceptors.push(fn); + } + + /* ---------- public request helpers ---------- */ + + async get(path: string, opts: RequestOptions = {}): Promise { + const res = await this.request(path, { ...opts, method: 'GET' }); + return res.json() as Promise; + } + + async post(path: string, body?: unknown, opts: RequestOptions = {}): Promise { + const res = await this.request(path, { + ...opts, + method: 'POST', + body: body != null ? JSON.stringify(body) : undefined, + headers: { + ...(body != null ? { 'Content-Type': 'application/json' } : {}), + ...opts.headers, + }, + }); + return res.json() as Promise; + } + + async put(path: string, body?: unknown, opts: RequestOptions = {}): Promise { + const res = await this.request(path, { + ...opts, + method: 'PUT', + body: body != null ? JSON.stringify(body) : undefined, + headers: { + ...(body != null ? { 'Content-Type': 'application/json' } : {}), + ...opts.headers, + }, + }); + return res.json() as Promise; + } + + async delete(path: string, opts: RequestOptions = {}): Promise { + const res = await this.request(path, { ...opts, method: 'DELETE' }); + return res.json() as Promise; + } + + /** + * Low-level request that returns the raw `Response`. + * Useful for streaming (SSE) endpoints where the caller needs `response.body`. + */ + async raw(path: string, opts: RequestOptions & { method?: string; body?: BodyInit | null } = {}): Promise { + return this.request(path, opts); + } + + /* ---------- internal plumbing ---------- */ + + private buildUrl(path: string, params?: Record): string { + const url = `${this.baseUrl}${path}`; + if (!params) return url; + + const qs = new URLSearchParams(); + for (const [key, value] of Object.entries(params)) { + if (value !== undefined) { + qs.set(key, String(value)); + } + } + const queryString = qs.toString(); + return queryString ? `${url}?${queryString}` : url; + } + + private async request(path: string, opts: RequestOptions & { method?: string; body?: BodyInit | null } = {}): Promise { + const { params, timeout, ...fetchOpts } = opts; + const url = this.buildUrl(path, params); + const effectiveTimeout = timeout ?? this.defaultTimeout; + + // Build the init object + let init: RequestInit = { ...fetchOpts }; + + // Run request interceptors + for (const interceptor of this.requestInterceptors) { + init = await interceptor(url, init); + } + + // Timeout via AbortController (merged with caller-supplied signal) + const timeoutCtrl = new AbortController(); + const callerSignal = init.signal; + + // If caller already passed a signal, listen for its abort + if (callerSignal) { + if (callerSignal.aborted) { + timeoutCtrl.abort(callerSignal.reason); + } else { + callerSignal.addEventListener('abort', () => timeoutCtrl.abort(callerSignal.reason), { once: true }); + } + } + + const timer = effectiveTimeout > 0 + ? setTimeout(() => timeoutCtrl.abort(new DOMException('Request timed out', 'TimeoutError')), effectiveTimeout) + : undefined; + + init.signal = timeoutCtrl.signal; + + try { + let response = await fetch(url, init); + + // Run response interceptors + for (const interceptor of this.responseInterceptors) { + response = await interceptor(response); + } + + return response; + } finally { + if (timer !== undefined) clearTimeout(timer); + } + } +} + +/* ------------------------------------------------------------------ */ +/* Singleton instance with default interceptors */ +/* ------------------------------------------------------------------ */ + +const httpClient = new HttpClient('/api'); + +// ---- request interceptor: auth headers ---- +httpClient.onRequest((_url, init) => { + const headers = new Headers(init.headers); + + // Attach userId from Redux store (lazy import to avoid circular deps). + // Falls back to 'anonymous' if store isn't ready yet. + try { + // eslint-disable-next-line @typescript-eslint/no-require-imports + const { store } = require('../store/store'); + const userId: string = store.getState().app.userId || 'anonymous'; + headers.set('X-Ms-Client-Principal-Id', userId); + } catch { + headers.set('X-Ms-Client-Principal-Id', 'anonymous'); + } + + return { ...init, headers }; +}); + +// ---- response interceptor: uniform error handling ---- +httpClient.onResponse((response) => { + if (!response.ok) { + // Don't throw for streaming endpoints — callers handle those manually. + // Clone so the body remains readable for callers that want custom handling. + const cloned = response.clone(); + console.error( + `[httpClient] ${response.status} ${response.statusText} – ${cloned.url}`, + ); + } + return response; +}); + +export default httpClient; diff --git a/content-gen/src/app/frontend/src/api/index.ts b/content-gen/src/app/frontend/src/api/index.ts index 1525bd366..a39c86318 100644 --- a/content-gen/src/app/frontend/src/api/index.ts +++ b/content-gen/src/app/frontend/src/api/index.ts @@ -9,20 +9,13 @@ import type { ParsedBriefResponse, AppConfig, } from '../types'; - -const API_BASE = '/api'; +import httpClient from './httpClient'; /** * Get application configuration including feature flags */ export async function getAppConfig(): Promise { - const response = await fetch(`${API_BASE}/config`); - - if (!response.ok) { - throw new Error(`Failed to get config: ${response.statusText}`); - } - - return response.json(); + return httpClient.get('/config'); } /** @@ -34,22 +27,11 @@ export async function parseBrief( userId?: string, signal?: AbortSignal ): Promise { - const response = await fetch(`${API_BASE}/brief/parse`, { - signal, - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - brief_text: briefText, - conversation_id: conversationId, - user_id: userId || 'anonymous', - }), - }); - - if (!response.ok) { - throw new Error(`Failed to parse brief: ${response.statusText}`); - } - - return response.json(); + return httpClient.post('/brief/parse', { + brief_text: briefText, + conversation_id: conversationId, + user_id: userId || 'anonymous', + }, { signal }); } /** @@ -60,21 +42,11 @@ export async function confirmBrief( conversationId?: string, userId?: string ): Promise<{ status: string; conversation_id: string; brief: CreativeBrief }> { - const response = await fetch(`${API_BASE}/brief/confirm`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - brief, - conversation_id: conversationId, - user_id: userId || 'anonymous', - }), + return httpClient.post('/brief/confirm', { + brief, + conversation_id: conversationId, + user_id: userId || 'anonymous', }); - - if (!response.ok) { - throw new Error(`Failed to confirm brief: ${response.statusText}`); - } - - return response.json(); } /** @@ -87,23 +59,12 @@ export async function selectProducts( userId?: string, signal?: AbortSignal ): Promise<{ products: Product[]; action: string; message: string; conversation_id: string }> { - const response = await fetch(`${API_BASE}/products/select`, { - signal, - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - request, - current_products: currentProducts, - conversation_id: conversationId, - user_id: userId || 'anonymous', - }), - }); - - if (!response.ok) { - throw new Error(`Failed to select products: ${response.statusText}`); - } - - return response.json(); + return httpClient.post('/products/select', { + request, + current_products: currentProducts, + conversation_id: conversationId, + user_id: userId || 'anonymous', + }, { signal }); } /** @@ -115,9 +76,9 @@ export async function* streamChat( userId?: string, signal?: AbortSignal ): AsyncGenerator { - const response = await fetch(`${API_BASE}/chat`, { - signal, + const response = await httpClient.raw('/chat', { method: 'POST', + signal, headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ message, @@ -174,27 +135,16 @@ export async function* streamGenerateContent( signal?: AbortSignal ): AsyncGenerator { // Use polling-based approach for reliability with long-running tasks - const startResponse = await fetch(`${API_BASE}/generate/start`, { - signal, - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - brief, - products: products || [], - generate_images: generateImages, - conversation_id: conversationId, - user_id: userId || 'anonymous', - }), - }); - - if (!startResponse.ok) { - throw new Error(`Content generation failed to start: ${startResponse.statusText}`); - } - - const startData = await startResponse.json(); + const startData = await httpClient.post<{ task_id: string }>('/generate/start', { + brief, + products: products || [], + generate_images: generateImages, + conversation_id: conversationId, + user_id: userId || 'anonymous', + }, { signal }); const taskId = startData.task_id; - console.log(`Generation started with task ID: ${taskId}`); + console.debug(`Generation started with task ID: ${taskId}`); // Yield initial status yield { @@ -223,12 +173,10 @@ export async function* streamGenerateContent( } try { - const statusResponse = await fetch(`${API_BASE}/generate/status/${taskId}`, { signal }); - if (!statusResponse.ok) { - throw new Error(`Failed to get task status: ${statusResponse.statusText}`); - } - - const statusData = await statusResponse.json(); + const statusData = await httpClient.get<{ status: string; result?: unknown; error?: string }>( + `/generate/status/${taskId}`, + { signal }, + ); if (statusData.status === 'completed') { // Yield the final result @@ -300,9 +248,9 @@ export async function* streamRegenerateImage( userId?: string, signal?: AbortSignal ): AsyncGenerator { - const response = await fetch(`${API_BASE}/regenerate`, { - signal, + const response = await httpClient.raw('/regenerate', { method: 'POST', + signal, headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ modification_request: modificationRequest, diff --git a/content-gen/src/app/frontend/src/components/AppHeader.tsx b/content-gen/src/app/frontend/src/components/AppHeader.tsx new file mode 100644 index 000000000..810f0a072 --- /dev/null +++ b/content-gen/src/app/frontend/src/components/AppHeader.tsx @@ -0,0 +1,68 @@ +import { memo } from 'react'; +import { + Text, + Avatar, + Button, + Tooltip, + tokens, +} from '@fluentui/react-components'; +import { + History24Regular, + History24Filled, +} from '@fluentui/react-icons'; +import ContosoLogo from '../styles/images/contoso.svg'; + +export interface AppHeaderProps { + userName?: string | null; + showChatHistory: boolean; + onToggleChatHistory: () => void; +} + +/** + * Top-level application header with logo, title, history toggle and avatar. + */ +export const AppHeader = memo(function AppHeader({ userName, showChatHistory, onToggleChatHistory }: AppHeaderProps) { + return ( +
+
+ Contoso + + Contoso + +
+ +
+ +
+
+ ); +}); +AppHeader.displayName = 'AppHeader'; diff --git a/content-gen/src/app/frontend/src/components/BriefReview.tsx b/content-gen/src/app/frontend/src/components/BriefReview.tsx index 6ea755905..88fbae5c7 100644 --- a/content-gen/src/app/frontend/src/components/BriefReview.tsx +++ b/content-gen/src/app/frontend/src/components/BriefReview.tsx @@ -1,3 +1,4 @@ +import { memo, useMemo } from 'react'; import { Button, Text, @@ -25,34 +26,38 @@ const fieldLabels: Record = { cta: 'Call to Action', }; -export function BriefReview({ +export const BriefReview = memo(function BriefReview({ brief, onConfirm, onStartOver, isAwaitingResponse = false, }: BriefReviewProps) { - const allFields: (keyof CreativeBrief)[] = [ - 'overview', 'objectives', 'target_audience', 'key_message', - 'tone_and_style', 'deliverable', 'timelines', 'visual_guidelines', 'cta' - ]; - const populatedFields = allFields.filter(key => brief[key]?.trim()).length; - const missingFields = allFields.filter(key => !brief[key]?.trim()); + const { populatedFields, missingFields, populatedDisplayFields } = useMemo(() => { + const allFields: (keyof CreativeBrief)[] = [ + 'overview', 'objectives', 'target_audience', 'key_message', + 'tone_and_style', 'deliverable', 'timelines', 'visual_guidelines', 'cta' + ]; + const populated = allFields.filter(key => brief[key]?.trim()).length; + const missing = allFields.filter(key => !brief[key]?.trim()); - // Define the order and labels for display in the card - const displayOrder: { key: keyof CreativeBrief; label: string }[] = [ - { key: 'overview', label: 'Campaign Objective' }, - { key: 'objectives', label: 'Objectives' }, - { key: 'target_audience', label: 'Target Audience' }, - { key: 'key_message', label: 'Key Message' }, - { key: 'tone_and_style', label: 'Tone & Style' }, - { key: 'visual_guidelines', label: 'Visual Guidelines' }, - { key: 'deliverable', label: 'Deliverables' }, - { key: 'timelines', label: 'Timelines' }, - { key: 'cta', label: 'Call to Action' }, - ]; + const displayOrder: { key: keyof CreativeBrief; label: string }[] = [ + { key: 'overview', label: 'Campaign Objective' }, + { key: 'objectives', label: 'Objectives' }, + { key: 'target_audience', label: 'Target Audience' }, + { key: 'key_message', label: 'Key Message' }, + { key: 'tone_and_style', label: 'Tone & Style' }, + { key: 'visual_guidelines', label: 'Visual Guidelines' }, + { key: 'deliverable', label: 'Deliverables' }, + { key: 'timelines', label: 'Timelines' }, + { key: 'cta', label: 'Call to Action' }, + ]; - // Filter to only populated fields - const populatedDisplayFields = displayOrder.filter(({ key }) => brief[key]?.trim()); + return { + populatedFields: populated, + missingFields: missing, + populatedDisplayFields: displayOrder.filter(({ key }) => brief[key]?.trim()), + }; + }, [brief]); return (
); -} +}); +BriefReview.displayName = 'BriefReview'; diff --git a/content-gen/src/app/frontend/src/components/ChatHistory.tsx b/content-gen/src/app/frontend/src/components/ChatHistory.tsx index f258d2a48..37dd7b705 100644 --- a/content-gen/src/app/frontend/src/components/ChatHistory.tsx +++ b/content-gen/src/app/frontend/src/components/ChatHistory.tsx @@ -1,4 +1,4 @@ -import { useState, useEffect, useCallback, useRef } from 'react'; +import { useEffect, useCallback, useMemo, memo } from 'react'; import { Button, Text, @@ -10,7 +10,6 @@ import { MenuPopover, MenuList, MenuItem, - Input, Dialog, DialogSurface, DialogTitle, @@ -22,154 +21,110 @@ import { Chat24Regular, MoreHorizontal20Regular, Compose20Regular, - Delete20Regular, - Edit20Regular, DismissCircle20Regular, } from '@fluentui/react-icons'; - -interface ConversationSummary { - id: string; - title: string; - lastMessage: string; - timestamp: string; - messageCount: number; -} +import { + useAppDispatch, + useAppSelector, + fetchConversations, + deleteConversation, + renameConversation, + clearAllConversations, + setShowAll as setShowAllAction, + setIsClearAllDialogOpen, + selectConversations, + selectIsHistoryLoading, + selectHistoryError, + selectShowAll, + selectIsClearAllDialogOpen, + selectIsClearing, + selectConversationId, + selectConversationTitle, + selectMessages, + selectIsLoading, + selectHistoryRefreshTrigger, +} from '../store'; +import type { ConversationSummary } from '../store'; +import { ConversationItem } from './ConversationItem'; interface ChatHistoryProps { - currentConversationId: string; - currentConversationTitle?: string | null; - currentMessages?: { role: string; content: string }[]; // Current session messages onSelectConversation: (conversationId: string) => void; onNewConversation: () => void; - refreshTrigger?: number; // Increment to trigger refresh - isGenerating?: boolean; // True when content generation is in progress } -export function ChatHistory({ - currentConversationId, - currentConversationTitle, - currentMessages = [], +export const ChatHistory = memo(function ChatHistory({ onSelectConversation, onNewConversation, - refreshTrigger = 0, - isGenerating = false }: ChatHistoryProps) { - const [conversations, setConversations] = useState([]); - const [isLoading, setIsLoading] = useState(true); - const [error, setError] = useState(null); - const [showAll, setShowAll] = useState(false); - const [isClearAllDialogOpen, setIsClearAllDialogOpen] = useState(false); - const [isClearing, setIsClearing] = useState(false); + const dispatch = useAppDispatch(); + const conversations = useAppSelector(selectConversations); + const isLoading = useAppSelector(selectIsHistoryLoading); + const error = useAppSelector(selectHistoryError); + const showAll = useAppSelector(selectShowAll); + const isClearAllDialogOpen = useAppSelector(selectIsClearAllDialogOpen); + const isClearing = useAppSelector(selectIsClearing); + const currentConversationId = useAppSelector(selectConversationId); + const currentConversationTitle = useAppSelector(selectConversationTitle); + const currentMessages = useAppSelector(selectMessages); + const isGenerating = useAppSelector(selectIsLoading); + const refreshTrigger = useAppSelector(selectHistoryRefreshTrigger); + const INITIAL_COUNT = 5; const handleClearAllConversations = useCallback(async () => { - setIsClearing(true); try { - const response = await fetch('/api/conversations', { - method: 'DELETE', - }); - if (response.ok) { - setConversations([]); - onNewConversation(); - setIsClearAllDialogOpen(false); - } else { - console.error('Failed to clear all conversations'); - } + await dispatch(clearAllConversations()).unwrap(); + onNewConversation(); } catch (err) { console.error('Error clearing all conversations:', err); - } finally { - setIsClearing(false); } - }, [onNewConversation]); + }, [dispatch, onNewConversation]); const handleDeleteConversation = useCallback(async (conversationId: string) => { try { - const response = await fetch(`/api/conversations/${conversationId}`, { - method: 'DELETE', - }); - if (response.ok) { - setConversations(prev => prev.filter(c => c.id !== conversationId)); - if (conversationId === currentConversationId) { - onNewConversation(); - } - } else { - console.error('Failed to delete conversation'); + await dispatch(deleteConversation(conversationId)).unwrap(); + if (conversationId === currentConversationId) { + onNewConversation(); } } catch (err) { console.error('Error deleting conversation:', err); } - }, [currentConversationId, onNewConversation]); + }, [dispatch, currentConversationId, onNewConversation]); const handleRenameConversation = useCallback(async (conversationId: string, newTitle: string) => { try { - const response = await fetch(`/api/conversations/${conversationId}`, { - method: 'PUT', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ title: newTitle }), - }); - - if (response.ok) { - setConversations(prev => prev.map(c => - c.id === conversationId ? { ...c, title: newTitle } : c - )); - } else { - console.error('Failed to rename conversation'); - } + await dispatch(renameConversation({ conversationId, newTitle })).unwrap(); } catch (err) { console.error('Error renaming conversation:', err); } - }, []); - - const loadConversations = useCallback(async () => { - setIsLoading(true); - setError(null); - try { - // Backend gets user from auth headers, no need to pass user_id - const response = await fetch('/api/conversations'); - if (response.ok) { - const data = await response.json(); - setConversations(data.conversations || []); - } else { - // If no conversations endpoint, use empty list - setConversations([]); - } - } catch (err) { - console.error('Error loading conversations:', err); - setError('Unable to load conversation history'); - setConversations([]); - } finally { - setIsLoading(false); - } - }, []); + }, [dispatch]); useEffect(() => { - loadConversations(); - }, [loadConversations, refreshTrigger]); + dispatch(fetchConversations()); + }, [dispatch, refreshTrigger]); // Reset showAll when conversations change significantly useEffect(() => { - setShowAll(false); - }, [refreshTrigger]); + dispatch(setShowAllAction(false)); + }, [dispatch, refreshTrigger]); // Build the current session conversation summary if it has messages - const currentSessionConversation: ConversationSummary | null = + const currentSessionConversation = useMemo(() => currentMessages.length > 0 && currentConversationTitle ? { id: currentConversationId, title: currentConversationTitle, lastMessage: currentMessages[currentMessages.length - 1]?.content?.substring(0, 100) || '', timestamp: new Date().toISOString(), messageCount: currentMessages.length, - } : null; + } : null, + [currentMessages, currentConversationId, currentConversationTitle], + ); // Merge current session with saved conversations, updating the current one with live data - const displayConversations = (() => { - // Find if current conversation exists in saved list + const displayConversations = useMemo(() => { const existingIndex = conversations.findIndex(c => c.id === currentConversationId); if (existingIndex >= 0 && currentSessionConversation) { - // Update the saved conversation with current session data (live message count) const updated = [...conversations]; updated[existingIndex] = { ...updated[existingIndex], @@ -178,14 +133,23 @@ export function ChatHistory({ }; return updated; } else if (currentSessionConversation) { - // Add current session at the top if it has messages and isn't saved yet return [currentSessionConversation, ...conversations]; } return conversations; - })(); + }, [conversations, currentConversationId, currentSessionConversation, currentMessages]); + + const visibleConversations = useMemo( + () => showAll ? displayConversations : displayConversations.slice(0, INITIAL_COUNT), + [showAll, displayConversations], + ); + const hasMore = useMemo( + () => displayConversations.length > INITIAL_COUNT, + [displayConversations.length], + ); - const visibleConversations = showAll ? displayConversations : displayConversations.slice(0, INITIAL_COUNT); - const hasMore = displayConversations.length > INITIAL_COUNT; + const handleRefreshConversations = useCallback(() => { + dispatch(fetchConversations()); + }, [dispatch]); return (
} - onClick={() => setIsClearAllDialogOpen(true)} + onClick={() => dispatch(setIsClearAllDialogOpen(true))} disabled={displayConversations.length === 0} > Clear all chat history @@ -276,7 +240,7 @@ export function ChatHistory({ }}> {error} Retry @@ -304,7 +268,7 @@ export function ChatHistory({ onSelect={() => onSelectConversation(conversation.id)} onDelete={handleDeleteConversation} onRename={handleRenameConversation} - onRefresh={loadConversations} + onRefresh={handleRefreshConversations} disabled={isGenerating} /> ))} @@ -327,7 +291,7 @@ export function ChatHistory({ }}> {hasMore && ( setShowAll(!showAll)} + onClick={isGenerating ? undefined : () => dispatch(setShowAllAction(!showAll))} style={{ fontSize: '13px', color: isGenerating ? tokens.colorNeutralForegroundDisabled : tokens.colorBrandForeground1, @@ -339,15 +303,15 @@ export function ChatHistory({ )} @@ -357,7 +321,7 @@ export function ChatHistory({
{/* Clear All Confirmation Dialog */} - !isClearing && setIsClearAllDialogOpen(data.open)}> + !isClearing && dispatch(setIsClearAllDialogOpen(data.open))}> Clear all chat history @@ -368,7 +332,7 @@ export function ChatHistory({ -
); -} - -interface ConversationItemProps { - conversation: ConversationSummary; - isActive: boolean; - onSelect: () => void; - onDelete: (conversationId: string) => void; - onRename: (conversationId: string, newTitle: string) => void; - onRefresh: () => void; - disabled?: boolean; -} - -function ConversationItem({ - conversation, - isActive, - onSelect, - onDelete, - onRename, - onRefresh, - disabled = false, -}: ConversationItemProps) { - const [isMenuOpen, setIsMenuOpen] = useState(false); - const [isRenameDialogOpen, setIsRenameDialogOpen] = useState(false); - const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false); - const [renameValue, setRenameValue] = useState(conversation.title || ''); - const [renameError, setRenameError] = useState(''); - const renameInputRef = useRef(null); - - const handleRenameClick = () => { - setRenameValue(conversation.title || ''); - setRenameError(''); - setIsRenameDialogOpen(true); - setIsMenuOpen(false); - }; - - const handleRenameConfirm = async () => { - const trimmedValue = renameValue.trim(); - - // Validate before API call - if (trimmedValue.length < 5) { - setRenameError('Conversation name must be at least 5 characters'); - return; - } - if (trimmedValue.length > 50) { - setRenameError('Conversation name cannot exceed 50 characters'); - return; - } - if (!/[a-zA-Z0-9]/.test(trimmedValue)) { - setRenameError('Conversation name must contain at least one letter or number'); - return; - } - - if (trimmedValue === conversation.title) { - setIsRenameDialogOpen(false); - setRenameError(''); - return; - } - - await onRename(conversation.id, trimmedValue); - onRefresh(); - setIsRenameDialogOpen(false); - setRenameError(''); - }; - - const handleDeleteClick = () => { - setIsDeleteDialogOpen(true); - setIsMenuOpen(false); - }; - - const handleDeleteConfirm = async () => { - await onDelete(conversation.id); - setIsDeleteDialogOpen(false); - }; - - useEffect(() => { - if (isRenameDialogOpen && renameInputRef.current) { - renameInputRef.current.focus(); - renameInputRef.current.select(); - } - }, [isRenameDialogOpen]); - - return ( - <> -
- - {conversation.title || 'Untitled'} - - - setIsMenuOpen(data.open)}> - - -
+}); - setIsRenameDialogOpen(data.open)}> - - Rename conversation - - - { - const newValue = e.target.value; - setRenameValue(newValue); - if (newValue.trim() === '') { - setRenameError('Conversation name cannot be empty or contain only spaces'); - } else if (newValue.trim().length < 5) { - setRenameError('Conversation name must be at least 5 characters'); - } else if (!/[a-zA-Z0-9]/.test(newValue)) { - setRenameError('Conversation name must contain at least one letter or number'); - } else if (newValue.length > 50) { - setRenameError('Conversation name cannot exceed 50 characters'); - } else { - setRenameError(''); - } - }} - onKeyDown={(e) => { - if (e.key === 'Enter' && renameValue.trim()) { - handleRenameConfirm(); - } else if (e.key === 'Escape') { - setIsRenameDialogOpen(false); - } - }} - placeholder="Enter conversation name" - style={{ width: '100%' }} - /> - - Maximum 50 characters ({renameValue.length}/50) - - {renameError && ( - - {renameError} - - )} - - - - - - - - - - setIsDeleteDialogOpen(data.open)}> - - Delete conversation - - - - Are you sure you want to delete "{conversation.title || 'Untitled'}"? This action cannot be undone. - - - - - - - - - - - ); -} +ChatHistory.displayName = 'ChatHistory'; diff --git a/content-gen/src/app/frontend/src/components/ChatInput.tsx b/content-gen/src/app/frontend/src/components/ChatInput.tsx new file mode 100644 index 000000000..a27f747fd --- /dev/null +++ b/content-gen/src/app/frontend/src/components/ChatInput.tsx @@ -0,0 +1,148 @@ +import { memo, useState, useCallback } from 'react'; +import { + Button, + Text, + Tooltip, + tokens, +} from '@fluentui/react-components'; +import { + Send20Regular, + Add20Regular, +} from '@fluentui/react-icons'; + +export interface ChatInputProps { + /** Called with the trimmed message text when the user submits. */ + onSendMessage: (message: string) => void; + /** Called when the user clicks the "New chat" button. */ + onNewConversation?: () => void; + /** Disables the input and buttons while a request is in flight. */ + disabled?: boolean; + /** Allows the parent to drive the input value (e.g. from WelcomeCard suggestions). */ + value?: string; + /** Notifies the parent when the user types. */ + onChange?: (value: string) => void; +} + +/** + * Chat input bar with send & new-chat buttons, plus an AI disclaimer. + */ +export const ChatInput = memo(function ChatInput({ + onSendMessage, + onNewConversation, + disabled = false, + value: controlledValue, + onChange: controlledOnChange, +}: ChatInputProps) { + const [internalValue, setInternalValue] = useState(''); + + // Support both controlled & uncontrolled modes + const inputValue = controlledValue ?? internalValue; + const setInputValue = (v: string) => { + controlledOnChange?.(v); + if (controlledValue === undefined) setInternalValue(v); + }; + + const handleSubmit = useCallback((e: React.FormEvent) => { + e.preventDefault(); + if (inputValue.trim() && !disabled) { + onSendMessage(inputValue.trim()); + setInputValue(''); + } + }, [inputValue, disabled, onSendMessage, setInputValue]); + + const handleKeyDown = useCallback((e: React.KeyboardEvent) => { + if (e.key === 'Enter' && !e.shiftKey) { + e.preventDefault(); + handleSubmit(e); + } + }, [handleSubmit]); + + return ( +
+ {/* Input Box */} +
+ setInputValue(e.target.value)} + onKeyDown={handleKeyDown} + placeholder="Type a message" + disabled={disabled} + style={{ + flex: 1, + border: 'none', + outline: 'none', + backgroundColor: 'transparent', + fontFamily: 'var(--fontFamilyBase)', + fontSize: '14px', + color: tokens.colorNeutralForeground1, + }} + /> + + {/* Icons on the right */} +
+ +
+
+ + {/* Disclaimer */} + + AI generated content may be incorrect + +
+ ); +}); +ChatInput.displayName = 'ChatInput'; diff --git a/content-gen/src/app/frontend/src/components/ChatPanel.tsx b/content-gen/src/app/frontend/src/components/ChatPanel.tsx index bf757acf9..0a6c39208 100644 --- a/content-gen/src/app/frontend/src/components/ChatPanel.tsx +++ b/content-gen/src/app/frontend/src/components/ChatPanel.tsx @@ -1,105 +1,101 @@ -import { useState, useRef, useEffect } from 'react'; -import { - Button, - Text, - Badge, - tokens, - Tooltip, -} from '@fluentui/react-components'; -import { - Send20Regular, - Stop24Regular, - Add20Regular, - Copy20Regular, -} from '@fluentui/react-icons'; -import ReactMarkdown from 'react-markdown'; -import type { ChatMessage, CreativeBrief, Product, GeneratedContent } from '../types'; +import { useState, useMemo, useCallback, memo } from 'react'; +import type { Product } from '../types'; import { BriefReview } from './BriefReview'; import { ConfirmedBriefView } from './ConfirmedBriefView'; import { SelectedProductView } from './SelectedProductView'; import { ProductReview } from './ProductReview'; import { InlineContentPreview } from './InlineContentPreview'; import { WelcomeCard } from './WelcomeCard'; +import { MessageBubble } from './MessageBubble'; +import { TypingIndicator } from './TypingIndicator'; +import { ChatInput } from './ChatInput'; +import { useAutoScroll } from '../hooks/useAutoScroll'; +import { + useAppSelector, + selectMessages, + selectIsLoading, + selectGenerationStatus, + selectPendingBrief, + selectConfirmedBrief, + selectGeneratedContent, + selectSelectedProducts, + selectAvailableProducts, + selectImageGenerationEnabled, +} from '../store'; interface ChatPanelProps { - messages: ChatMessage[]; onSendMessage: (message: string) => void; - isLoading: boolean; - generationStatus?: string; onStopGeneration?: () => void; - // Inline component props - pendingBrief?: CreativeBrief | null; - confirmedBrief?: CreativeBrief | null; - generatedContent?: GeneratedContent | null; - selectedProducts?: Product[]; - availableProducts?: Product[]; onBriefConfirm?: () => void; onBriefCancel?: () => void; onGenerateContent?: () => void; onRegenerateContent?: () => void; onProductsStartOver?: () => void; onProductSelect?: (product: Product) => void; - // Feature flags - imageGenerationEnabled?: boolean; - // New chat onNewConversation?: () => void; } -export function ChatPanel({ - messages, +export const ChatPanel = memo(function ChatPanel({ onSendMessage, - isLoading, - generationStatus, onStopGeneration, - pendingBrief, - confirmedBrief, - generatedContent, - selectedProducts = [], - availableProducts = [], onBriefConfirm, onBriefCancel, onGenerateContent, onRegenerateContent, onProductsStartOver, onProductSelect, - imageGenerationEnabled = true, onNewConversation, }: ChatPanelProps) { - const [inputValue, setInputValue] = useState(''); - const messagesEndRef = useRef(null); - const messagesContainerRef = useRef(null); - const inputContainerRef = useRef(null); + const messages = useAppSelector(selectMessages); + const isLoading = useAppSelector(selectIsLoading); + const generationStatus = useAppSelector(selectGenerationStatus); + const pendingBrief = useAppSelector(selectPendingBrief); + const confirmedBrief = useAppSelector(selectConfirmedBrief); + const generatedContent = useAppSelector(selectGeneratedContent); + const selectedProducts = useAppSelector(selectSelectedProducts); + const availableProducts = useAppSelector(selectAvailableProducts); + const imageGenerationEnabled = useAppSelector(selectImageGenerationEnabled); - // Scroll to bottom when messages change - useEffect(() => { - messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }); - }, [messages, pendingBrief, confirmedBrief, generatedContent, isLoading, generationStatus]); + const [inputValue, setInputValue] = useState(''); - const handleSubmit = (e: React.FormEvent) => { - e.preventDefault(); - if (inputValue.trim() && !isLoading) { - onSendMessage(inputValue.trim()); - setInputValue(''); - } - }; + // Auto-scroll to bottom when messages or state changes + const messagesEndRef = useAutoScroll([ + messages, pendingBrief, confirmedBrief, generatedContent, isLoading, generationStatus, + ]); // Determine if we should show inline components - const showBriefReview = pendingBrief && onBriefConfirm && onBriefCancel; - const showProductReview = confirmedBrief && !generatedContent && onGenerateContent; - const showContentPreview = generatedContent && onRegenerateContent; - const showWelcome = messages.length === 0 && !showBriefReview && !showProductReview && !showContentPreview; + const showBriefReview = useMemo( + () => !!(pendingBrief && onBriefConfirm && onBriefCancel), + [pendingBrief, onBriefConfirm, onBriefCancel], + ); + const showProductReview = useMemo( + () => !!(confirmedBrief && !generatedContent && onGenerateContent), + [confirmedBrief, generatedContent, onGenerateContent], + ); + const showContentPreview = useMemo( + () => !!(generatedContent && onRegenerateContent), + [generatedContent, onRegenerateContent], + ); + const showWelcome = useMemo( + () => messages.length === 0 && !showBriefReview && !showProductReview && !showContentPreview, + [messages.length, showBriefReview, showProductReview, showContentPreview], + ); // Handle suggestion click from welcome card - const handleSuggestionClick = (prompt: string) => { + const handleSuggestionClick = useCallback((prompt: string) => { setInputValue(prompt); - }; + }, []); + + const isInputDisabled = useMemo(() => isLoading, [isLoading]); + + const startOverFallback = useCallback(() => {}, []); + const effectiveProductsStartOver = onProductsStartOver || startOverFallback; return (
{/* Messages Area */}
)} @@ -145,8 +141,8 @@ export function ChatPanel({ {})} + onConfirm={onGenerateContent!} + onStartOver={effectiveProductsStartOver} isAwaitingResponse={isLoading} onProductSelect={onProductSelect} disabled={isLoading} @@ -156,79 +152,20 @@ export function ChatPanel({ {/* Inline Content Preview */} {showContentPreview && ( 0 ? selectedProducts[0] : undefined} imageGenerationEnabled={imageGenerationEnabled} /> )} - {/* Loading/Typing Indicator - Coral Style */} + {/* Loading/Typing Indicator */} {isLoading && ( -
-
- - - - - -
- - {generationStatus || 'Thinking...'} - - {onStopGeneration && ( - - - - )} -
+ )} )} @@ -236,202 +173,16 @@ export function ChatPanel({
- {/* Input Area - Simple single-line like Figma */} -
- {/* Input Box */} -
- setInputValue(e.target.value)} - onKeyDown={(e) => { - if (e.key === 'Enter' && !e.shiftKey) { - e.preventDefault(); - handleSubmit(e); - } - }} - placeholder="Type a message" - disabled={isLoading} - style={{ - flex: 1, - border: 'none', - outline: 'none', - backgroundColor: 'transparent', - fontFamily: 'var(--fontFamilyBase)', - fontSize: '14px', - color: tokens.colorNeutralForeground1, - }} - /> - - {/* Icons on the right */} -
- -
-
- - {/* Disclaimer - Outside the input box */} - - AI generated content may be incorrect - -
+ {/* Input Area */} +
); -} - -// Copy function for messages -const handleCopy = (text: string) => { - navigator.clipboard.writeText(text).catch((err) => { - console.error('Failed to copy text:', err); - }); -}; - -function MessageBubble({ message }: { message: ChatMessage }) { - const isUser = message.role === 'user'; - const [copied, setCopied] = useState(false); +}); - const onCopy = () => { - handleCopy(message.content); - setCopied(true); - setTimeout(() => setCopied(false), 2000); - }; - - return ( -
- {/* Agent badge for assistant messages */} - {!isUser && message.agent && ( - - {message.agent} - - )} - - {/* Message content with markdown */} -
- - {message.content} - - - {/* Footer for assistant messages - Coral style */} - {!isUser && ( -
- - AI-generated content may be incorrect - - -
- -
-
- )} -
-
- ); -} +ChatPanel.displayName = 'ChatPanel'; diff --git a/content-gen/src/app/frontend/src/components/ComplianceSection.tsx b/content-gen/src/app/frontend/src/components/ComplianceSection.tsx new file mode 100644 index 000000000..f593a9f4d --- /dev/null +++ b/content-gen/src/app/frontend/src/components/ComplianceSection.tsx @@ -0,0 +1,192 @@ +import { memo } from 'react'; +import { + Text, + Badge, + Button, + Tooltip, + Accordion, + AccordionItem, + AccordionHeader, + AccordionPanel, + tokens, +} from '@fluentui/react-components'; +import { + ArrowSync20Regular, + CheckmarkCircle20Regular, + Warning20Regular, + Info20Regular, + ErrorCircle20Regular, + Copy20Regular, +} from '@fluentui/react-icons'; +import type { ComplianceViolation } from '../types'; +import { ViolationCard } from './ViolationCard'; + +export interface ComplianceSectionProps { + violations: ComplianceViolation[]; + requiresModification: boolean; + /** Callback to copy generated text. */ + onCopyText: () => void; + /** Callback to regenerate content. */ + onRegenerate: () => void; + /** Whether regeneration is in progress. */ + isLoading?: boolean; + /** Whether the copy-text button shows "Copied!". */ + copied?: boolean; +} + +/** + * Compliance callout (action-needed / review-recommended), status footer + * with badges and actions, and the collapsible violations accordion. + */ +export const ComplianceSection = memo(function ComplianceSection({ + violations, + requiresModification, + onCopyText, + onRegenerate, + isLoading, + copied = false, +}: ComplianceSectionProps) { + return ( + <> + {/* User guidance callout */} + {requiresModification ? ( +
+ + Action needed: This content has compliance issues that must be + addressed before use. Please review the details in the Compliance Guidelines + section below and regenerate with modifications, or manually edit the content to + resolve the flagged items. + +
+ ) : violations.length > 0 ? ( +
+ + Optional review: This content is approved but has minor + suggestions for improvement. You can use it as-is or review the recommendations + in the Compliance Guidelines section below. + +
+ ) : null} + + {/* Footer with actions */} +
+
+ {requiresModification ? ( + }> + Requires Modification + + ) : violations.length > 0 ? ( + }> + Review Recommended + + ) : ( + } + > + Approved + + )} +
+ +
+ +
+
+ + {/* AI disclaimer */} + + AI-generated content may be incorrect + + + {/* Collapsible Compliance Accordion */} + {violations.length > 0 && ( + + + +
+ {requiresModification ? ( + + ) : violations.some((v) => v.severity === 'error') ? ( + + ) : violations.some((v) => v.severity === 'warning') ? ( + + ) : ( + + )} + + Compliance Guidelines ({violations.length}{' '} + {violations.length === 1 ? 'item' : 'items'}) + +
+
+ +
+ {violations.map((violation, index) => ( + + ))} +
+
+
+
+ )} + + ); +}); +ComplianceSection.displayName = 'ComplianceSection'; diff --git a/content-gen/src/app/frontend/src/components/ConfirmedBriefView.tsx b/content-gen/src/app/frontend/src/components/ConfirmedBriefView.tsx index e7feb9416..f0806979b 100644 --- a/content-gen/src/app/frontend/src/components/ConfirmedBriefView.tsx +++ b/content-gen/src/app/frontend/src/components/ConfirmedBriefView.tsx @@ -1,3 +1,4 @@ +import { memo } from 'react'; import { Text, Badge, @@ -24,7 +25,7 @@ const briefFields: { key: keyof CreativeBrief; label: string }[] = [ { key: 'cta', label: 'Call to Action' }, ]; -export function ConfirmedBriefView({ brief }: ConfirmedBriefViewProps) { +export const ConfirmedBriefView = memo(function ConfirmedBriefView({ brief }: ConfirmedBriefViewProps) { return (
); -} +}); +ConfirmedBriefView.displayName = 'ConfirmedBriefView'; diff --git a/content-gen/src/app/frontend/src/components/ConversationItem.tsx b/content-gen/src/app/frontend/src/components/ConversationItem.tsx new file mode 100644 index 000000000..16b5ebc5e --- /dev/null +++ b/content-gen/src/app/frontend/src/components/ConversationItem.tsx @@ -0,0 +1,297 @@ +import { memo, useState, useEffect, useRef, useCallback } from 'react'; +import { + Button, + Text, + tokens, + Menu, + MenuTrigger, + MenuPopover, + MenuList, + MenuItem, + Input, + Dialog, + DialogSurface, + DialogTitle, + DialogBody, + DialogActions, + DialogContent, +} from '@fluentui/react-components'; +import { + MoreHorizontal20Regular, + Delete20Regular, + Edit20Regular, +} from '@fluentui/react-icons'; +import type { ConversationSummary } from '../store'; + +export interface ConversationItemProps { + conversation: ConversationSummary; + isActive: boolean; + onSelect: () => void; + onDelete: (conversationId: string) => void; + onRename: (conversationId: string, newTitle: string) => void; + onRefresh: () => void; + disabled?: boolean; +} + +/** + * A single row in the chat-history sidebar — + * title, context-menu (rename / delete) and confirmation dialogs. + */ +export const ConversationItem = memo(function ConversationItem({ + conversation, + isActive, + onSelect, + onDelete, + onRename, + onRefresh, + disabled = false, +}: ConversationItemProps) { + const [isMenuOpen, setIsMenuOpen] = useState(false); + const [isRenameDialogOpen, setIsRenameDialogOpen] = useState(false); + const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false); + const [renameValue, setRenameValue] = useState(conversation.title || ''); + const [renameError, setRenameError] = useState(''); + const renameInputRef = useRef(null); + + const handleRenameClick = useCallback(() => { + setRenameValue(conversation.title || ''); + setRenameError(''); + setIsRenameDialogOpen(true); + setIsMenuOpen(false); + }, [conversation.title]); + + const handleRenameConfirm = useCallback(async () => { + const trimmedValue = renameValue.trim(); + + if (trimmedValue.length < 5) { + setRenameError('Conversation name must be at least 5 characters'); + return; + } + if (trimmedValue.length > 50) { + setRenameError('Conversation name cannot exceed 50 characters'); + return; + } + if (!/[a-zA-Z0-9]/.test(trimmedValue)) { + setRenameError('Conversation name must contain at least one letter or number'); + return; + } + + if (trimmedValue === conversation.title) { + setIsRenameDialogOpen(false); + setRenameError(''); + return; + } + + await onRename(conversation.id, trimmedValue); + onRefresh(); + setIsRenameDialogOpen(false); + setRenameError(''); + }, [renameValue, conversation.id, conversation.title, onRename, onRefresh]); + + const handleDeleteClick = useCallback(() => { + setIsDeleteDialogOpen(true); + setIsMenuOpen(false); + }, []); + + const handleDeleteConfirm = useCallback(async () => { + await onDelete(conversation.id); + setIsDeleteDialogOpen(false); + }, [conversation.id, onDelete]); + + useEffect(() => { + if (isRenameDialogOpen && renameInputRef.current) { + renameInputRef.current.focus(); + renameInputRef.current.select(); + } + }, [isRenameDialogOpen]); + + return ( + <> +
+ + {conversation.title || 'Untitled'} + + + setIsMenuOpen(data.open)}> + + +
+ + {/* Rename dialog */} + setIsRenameDialogOpen(data.open)}> + + Rename conversation + + + { + const newValue = e.target.value; + setRenameValue(newValue); + if (newValue.trim() === '') { + setRenameError('Conversation name cannot be empty or contain only spaces'); + } else if (newValue.trim().length < 5) { + setRenameError('Conversation name must be at least 5 characters'); + } else if (!/[a-zA-Z0-9]/.test(newValue)) { + setRenameError('Conversation name must contain at least one letter or number'); + } else if (newValue.length > 50) { + setRenameError('Conversation name cannot exceed 50 characters'); + } else { + setRenameError(''); + } + }} + onKeyDown={(e) => { + if (e.key === 'Enter' && renameValue.trim()) { + handleRenameConfirm(); + } else if (e.key === 'Escape') { + setIsRenameDialogOpen(false); + } + }} + placeholder="Enter conversation name" + style={{ width: '100%' }} + /> + + Maximum 50 characters ({renameValue.length}/50) + + {renameError && ( + + {renameError} + + )} + + + + + + + + + + {/* Delete dialog */} + setIsDeleteDialogOpen(data.open)}> + + Delete conversation + + + + Are you sure you want to delete "{conversation.title || 'Untitled'}"? This action + cannot be undone. + + + + + + + + + + + ); +}); +ConversationItem.displayName = 'ConversationItem'; diff --git a/content-gen/src/app/frontend/src/components/ImagePreviewCard.tsx b/content-gen/src/app/frontend/src/components/ImagePreviewCard.tsx new file mode 100644 index 000000000..b4e1ee50a --- /dev/null +++ b/content-gen/src/app/frontend/src/components/ImagePreviewCard.tsx @@ -0,0 +1,99 @@ +import { memo } from 'react'; +import { + Button, + Text, + Tooltip, + tokens, +} from '@fluentui/react-components'; +import { ArrowDownload20Regular } from '@fluentui/react-icons'; + +export interface ImagePreviewCardProps { + imageUrl: string; + altText?: string; + productName?: string; + tagline?: string; + isSmall?: boolean; + onDownload: () => void; +} + +/** + * Image preview with download button overlay and a product-name / tagline + * text banner below the image. + */ +export const ImagePreviewCard = memo(function ImagePreviewCard({ + imageUrl, + altText = 'Generated marketing image', + productName = 'Your Product', + tagline, + isSmall = false, + onDownload, +}: ImagePreviewCardProps) { + return ( +
+ {/* Image container */} +
+ {altText} + + +
+ + {/* Text banner below image */} +
+ + {productName} + + {tagline && ( + + {tagline} + + )} +
+
+ ); +}); +ImagePreviewCard.displayName = 'ImagePreviewCard'; diff --git a/content-gen/src/app/frontend/src/components/InlineContentPreview.tsx b/content-gen/src/app/frontend/src/components/InlineContentPreview.tsx index 3ee0eead2..c0f2367c8 100644 --- a/content-gen/src/app/frontend/src/components/InlineContentPreview.tsx +++ b/content-gen/src/app/frontend/src/components/InlineContentPreview.tsx @@ -1,27 +1,17 @@ -import { useState, useEffect } from 'react'; +import { memo, useCallback, useMemo } from 'react'; import { - Button, Text, - Badge, Divider, tokens, - Tooltip, - Accordion, - AccordionItem, - AccordionHeader, - AccordionPanel, } from '@fluentui/react-components'; -import { - ArrowSync20Regular, - CheckmarkCircle20Regular, - Warning20Regular, - Info20Regular, - ErrorCircle20Regular, - Copy20Regular, - ArrowDownload20Regular, - ShieldError20Regular, -} from '@fluentui/react-icons'; -import type { GeneratedContent, ComplianceViolation, Product } from '../types'; +import { ShieldError20Regular } from '@fluentui/react-icons'; +import type { GeneratedContent, Product } from '../types'; +import { useWindowSize } from '../hooks/useWindowSize'; +import { isContentFilterError, getErrorMessage } from '../utils/contentErrors'; +import { downloadImage } from '../utils/downloadImage'; +import { useCopyToClipboard } from '../hooks/useCopyToClipboard'; +import { ImagePreviewCard } from './ImagePreviewCard'; +import { ComplianceSection } from './ComplianceSection'; interface InlineContentPreviewProps { content: GeneratedContent; @@ -31,20 +21,7 @@ interface InlineContentPreviewProps { imageGenerationEnabled?: boolean; } -// Custom hook for responsive breakpoints -function useWindowSize() { - const [windowWidth, setWindowWidth] = useState(typeof window !== 'undefined' ? window.innerWidth : 1200); - - useEffect(() => { - const handleResize = () => setWindowWidth(window.innerWidth); - window.addEventListener('resize', handleResize); - return () => window.removeEventListener('resize', handleResize); - }, []); - - return windowWidth; -} - -export function InlineContentPreview({ +export const InlineContentPreview = memo(function InlineContentPreview({ content, onRegenerate, isLoading, @@ -52,141 +29,36 @@ export function InlineContentPreview({ imageGenerationEnabled = true, }: InlineContentPreviewProps) { const { text_content, image_content, violations, requires_modification, error, image_error, text_error } = content; - const [copied, setCopied] = useState(false); + const { copied, copy } = useCopyToClipboard(); const windowWidth = useWindowSize(); const isSmall = windowWidth < 768; - // Helper to detect content filter errors - const isContentFilterError = (errorMessage?: string): boolean => { - if (!errorMessage) return false; - const filterPatterns = [ - 'content_filter', 'ContentFilter', 'content management policy', - 'ResponsibleAI', 'responsible_ai_policy', 'content filtering', - 'filtered', 'safety system', 'self_harm', 'sexual', 'violence', 'hate', - ]; - return filterPatterns.some(pattern => - errorMessage.toLowerCase().includes(pattern.toLowerCase()) - ); - }; - - const getErrorMessage = (errorMessage?: string): { title: string; description: string } => { - if (isContentFilterError(errorMessage)) { - return { - title: 'Content Filtered', - description: 'Your request was blocked by content safety filters. Please try modifying your creative brief.', - }; - } - return { - title: 'Generation Failed', - description: errorMessage || 'An error occurred. Please try again.', - }; - }; - - const handleCopyText = () => { + const handleCopyText = useCallback(() => { const textToCopy = [ text_content?.headline && `✨ ${text_content.headline} ✨`, text_content?.body, text_content?.tagline, ].filter(Boolean).join('\n\n'); - - navigator.clipboard.writeText(textToCopy); - setCopied(true); - setTimeout(() => setCopied(false), 2000); - }; + copy(textToCopy); + }, [text_content, copy]); - const handleDownloadImage = async () => { + const handleDownloadImage = useCallback(() => { if (!image_content?.image_url) return; - - try { - const canvas = document.createElement('canvas'); - const ctx = canvas.getContext('2d'); - if (!ctx) return; - - const img = new Image(); - img.crossOrigin = 'anonymous'; - - img.onload = () => { - // Calculate banner height - const bannerHeight = Math.max(60, img.height * 0.1); - const padding = Math.max(16, img.width * 0.03); - - // Set canvas size to include bottom banner - canvas.width = img.width; - canvas.height = img.height + bannerHeight; - - // Draw the image at the top - ctx.drawImage(img, 0, 0); - - // Draw white banner at the bottom - ctx.fillStyle = '#ffffff'; - ctx.fillRect(0, img.height, img.width, bannerHeight); - - // Draw banner border line - ctx.strokeStyle = '#e5e5e5'; - ctx.lineWidth = 1; - ctx.beginPath(); - ctx.moveTo(0, img.height); - ctx.lineTo(img.width, img.height); - ctx.stroke(); - - // Draw text in the banner - const headlineText = selectedProduct?.product_name || text_content?.headline || 'Your Product'; - const headlineFontSize = Math.max(18, Math.min(36, img.width * 0.04)); - const taglineText = text_content?.tagline || ''; - const taglineFontSize = Math.max(12, Math.min(20, img.width * 0.025)); - - // Draw headline - ctx.font = `600 ${headlineFontSize}px Georgia, serif`; - ctx.fillStyle = '#1a1a1a'; - ctx.fillText(headlineText, padding, img.height + padding + headlineFontSize * 0.8, img.width - padding * 2); - - // Draw tagline if available - if (taglineText) { - ctx.font = `400 italic ${taglineFontSize}px -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif`; - ctx.fillStyle = '#666666'; - ctx.fillText(taglineText, padding, img.height + padding + headlineFontSize + taglineFontSize * 0.8 + 4, img.width - padding * 2); - } - - canvas.toBlob((blob) => { - if (blob) { - const url = URL.createObjectURL(blob); - const link = document.createElement('a'); - link.href = url; - link.download = 'generated-marketing-image.png'; - link.click(); - URL.revokeObjectURL(url); - } - }, 'image/png'); - }; - - img.onerror = () => { - if (image_content?.image_url) { - const link = document.createElement('a'); - link.href = image_content.image_url; - link.download = 'generated-image.png'; - link.click(); - } - }; - - img.src = image_content.image_url; - } catch { - if (image_content?.image_url) { - const link = document.createElement('a'); - link.href = image_content.image_url; - link.download = 'generated-image.png'; - link.click(); - } - } - }; + downloadImage( + image_content.image_url, + selectedProduct?.product_name || text_content?.headline || 'Your Product', + text_content?.tagline, + ); + }, [image_content, selectedProduct, text_content]); // Get product display name - const getProductDisplayName = () => { + const productDisplayName = useMemo(() => { if (selectedProduct) { return selectedProduct.product_name; } return text_content?.headline || 'Your Content'; - }; + }, [selectedProduct, text_content?.headline]); return (
- ✨ Discover the serene elegance of {getProductDisplayName()}. + ✨ Discover the serene elegance of {productDisplayName}. )} @@ -285,74 +157,16 @@ export function InlineContentPreview({
)} - {/* Image Preview - with bottom banner for text */} + {/* Image Preview */} {imageGenerationEnabled && image_content?.image_url && ( -
- {/* Image container */} -
- {image_content.alt_text - - {/* Download button on image */} - -
- - {/* Text banner below image */} -
- - {selectedProduct?.product_name || text_content?.headline || 'Your Product'} - - {text_content?.tagline && ( - - {text_content.tagline} - - )} -
-
+ )} {/* Image Error State */} @@ -383,179 +197,16 @@ export function InlineContentPreview({ - {/* User guidance callout for compliance status */} - {requires_modification ? ( -
- - Action needed: This content has compliance issues that must be addressed before use. - Please review the details in the Compliance Guidelines section below and regenerate with modifications, - or manually edit the content to resolve the flagged items. - -
- ) : violations.length > 0 ? ( -
- - Optional review: This content is approved but has minor suggestions for improvement. - You can use it as-is or review the recommendations in the Compliance Guidelines section below. - -
- ) : null} - - {/* Footer with actions */} -
-
- {/* Approval Status Badge */} - {requires_modification ? ( - }> - Requires Modification - - ) : violations.length > 0 ? ( - }> - Review Recommended - - ) : ( - }> - Approved - - )} -
- -
- -
-
- - {/* AI disclaimer */} - - AI-generated content may be incorrect - - - {/* Collapsible Compliance Section */} - {violations.length > 0 && ( - - - -
- {requires_modification ? ( - - ) : violations.some(v => v.severity === 'error') ? ( - - ) : violations.some(v => v.severity === 'warning') ? ( - - ) : ( - - )} - - Compliance Guidelines ({violations.length} {violations.length === 1 ? 'item' : 'items'}) - -
-
- -
- {violations.map((violation, index) => ( - - ))} -
-
-
-
- )} -
- ); -} - -function ViolationCard({ violation }: { violation: ComplianceViolation }) { - const getSeverityStyles = () => { - switch (violation.severity) { - case 'error': - return { - icon: , - bg: '#fde7e9', - }; - case 'warning': - return { - icon: , - bg: '#fff4ce', - }; - case 'info': - return { - icon: , - bg: '#deecf9', - }; - } - }; - - const { icon, bg } = getSeverityStyles(); - - return ( -
- {icon} -
- - {violation.message} - - - {violation.suggestion} - -
+ {/* Compliance + Footer + Accordion */} +
); -} +}); +InlineContentPreview.displayName = 'InlineContentPreview'; diff --git a/content-gen/src/app/frontend/src/components/MessageBubble.tsx b/content-gen/src/app/frontend/src/components/MessageBubble.tsx new file mode 100644 index 000000000..58d3cb3c2 --- /dev/null +++ b/content-gen/src/app/frontend/src/components/MessageBubble.tsx @@ -0,0 +1,118 @@ +import { memo } from 'react'; +import { + Text, + Badge, + Button, + Tooltip, + tokens, +} from '@fluentui/react-components'; +import { Copy20Regular } from '@fluentui/react-icons'; +import ReactMarkdown from 'react-markdown'; +import type { ChatMessage } from '../types'; +import { useCopyToClipboard } from '../hooks/useCopyToClipboard'; + +export interface MessageBubbleProps { + message: ChatMessage; +} + +/** + * Renders a single chat message — user or assistant. + * + * - User messages: right-aligned, brand-coloured bubble. + * - Assistant messages: left-aligned, full-width, with optional agent badge, + * markdown rendering, copy button and AI disclaimer. + */ +export const MessageBubble = memo(function MessageBubble({ message }: MessageBubbleProps) { + const isUser = message.role === 'user'; + const { copied, copy } = useCopyToClipboard(); + + return ( +
+ {/* Agent badge for assistant messages */} + {!isUser && message.agent && ( + + {message.agent} + + )} + + {/* Message content with markdown */} +
+ {message.content} + + {/* Footer for assistant messages */} + {!isUser && ( +
+ + AI-generated content may be incorrect + + +
+ +
+
+ )} +
+
+ ); +}); +MessageBubble.displayName = 'MessageBubble'; diff --git a/content-gen/src/app/frontend/src/components/ProductCard.tsx b/content-gen/src/app/frontend/src/components/ProductCard.tsx new file mode 100644 index 000000000..050de19f7 --- /dev/null +++ b/content-gen/src/app/frontend/src/components/ProductCard.tsx @@ -0,0 +1,136 @@ +import { memo, useMemo } from 'react'; +import { + Text, + tokens, +} from '@fluentui/react-components'; +import { Box20Regular } from '@fluentui/react-icons'; +import type { Product } from '../types'; + +export interface ProductCardProps { + product: Product; + /** Visual size variant — "normal" for product review grid, "compact" for selected-product view. */ + size?: 'normal' | 'compact'; + /** Whether the card is currently selected (shows brand border). */ + isSelected?: boolean; + /** Click handler. Omit for read-only cards. */ + onClick?: () => void; + disabled?: boolean; +} + +/** + * Reusable product card with image/placeholder, name, tags and price. + * Used by both ProductReview (selectable) and SelectedProductView (read-only). + */ +export const ProductCard = memo(function ProductCard({ + product, + size = 'normal', + isSelected = false, + onClick, + disabled = false, +}: ProductCardProps) { + const isCompact = size === 'compact'; + const imgSize = isCompact ? 56 : 80; + const isInteractive = useMemo(() => !!onClick && !disabled, [onClick, disabled]); + + return ( +
+ {/* Image or placeholder */} + {product.image_url ? ( + {product.product_name} + ) : ( +
+ +
+ )} + + {/* Product info */} +
+ + {product.product_name} + + + {product.tags || product.description || 'soft white, airy, minimal, fresh'} + + + ${product.price?.toFixed(2) || '59.95'} USD + +
+
+ ); +}); +ProductCard.displayName = 'ProductCard'; diff --git a/content-gen/src/app/frontend/src/components/ProductReview.tsx b/content-gen/src/app/frontend/src/components/ProductReview.tsx index 9c0d9e960..7c8a12ce5 100644 --- a/content-gen/src/app/frontend/src/components/ProductReview.tsx +++ b/content-gen/src/app/frontend/src/components/ProductReview.tsx @@ -1,3 +1,4 @@ +import { memo, useMemo, useCallback } from 'react'; import { Button, Text, @@ -5,9 +6,9 @@ import { } from '@fluentui/react-components'; import { Sparkle20Regular, - Box20Regular, } from '@fluentui/react-icons'; import type { Product } from '../types'; +import { ProductCard } from './ProductCard'; interface ProductReviewProps { products: Product[]; @@ -19,7 +20,7 @@ interface ProductReviewProps { disabled?: boolean; } -export function ProductReview({ +export const ProductReview = memo(function ProductReview({ products, onConfirm, onStartOver: _onStartOver, @@ -28,18 +29,24 @@ export function ProductReview({ onProductSelect, disabled = false, }: ProductReviewProps) { - const displayProducts = availableProducts.length > 0 ? availableProducts : products; - const selectedProductIds = new Set(products.map(p => p.sku || p.product_name)); + const displayProducts = useMemo( + () => availableProducts.length > 0 ? availableProducts : products, + [availableProducts, products], + ); + const selectedProductIds = useMemo( + () => new Set(products.map(p => p.sku || p.product_name)), + [products], + ); - const isProductSelected = (product: Product): boolean => { + const isProductSelected = useCallback((product: Product): boolean => { return selectedProductIds.has(product.sku || product.product_name); - }; + }, [selectedProductIds]); - const handleProductClick = (product: Product) => { + const handleProductClick = useCallback((product: Product) => { if (onProductSelect) { onProductSelect(product); } - }; + }, [onProductSelect]); return (
{displayProducts.map((product, index) => ( -
); -} - -interface ProductCardGridProps { - product: Product; - isSelected: boolean; - onClick: () => void; - disabled?: boolean; -} - -function ProductCardGrid({ product, isSelected, onClick, disabled = false }: ProductCardGridProps) { - return ( -
- {product.image_url ? ( - {product.product_name} - ) : ( -
- -
- )} - -
- - {product.product_name} - - - {product.tags || product.description || 'soft white, airy, minimal, fresh'} - - - ${product.price?.toFixed(2) || '59.95'} USD - -
-
- ); -} +}); +ProductReview.displayName = 'ProductReview'; diff --git a/content-gen/src/app/frontend/src/components/SelectedProductView.tsx b/content-gen/src/app/frontend/src/components/SelectedProductView.tsx index a4c4540f6..743a5f86f 100644 --- a/content-gen/src/app/frontend/src/components/SelectedProductView.tsx +++ b/content-gen/src/app/frontend/src/components/SelectedProductView.tsx @@ -1,19 +1,19 @@ +import { memo } from 'react'; import { - Text, Badge, tokens, } from '@fluentui/react-components'; import { Checkmark20Regular, - Box20Regular, } from '@fluentui/react-icons'; import type { Product } from '../types'; +import { ProductCard } from './ProductCard'; interface SelectedProductViewProps { products: Product[]; } -export function SelectedProductView({ products }: SelectedProductViewProps) { +export const SelectedProductView = memo(function SelectedProductView({ products }: SelectedProductViewProps) { if (products.length === 0) return null; return ( @@ -50,89 +50,14 @@ export function SelectedProductView({ products }: SelectedProductViewProps) { overflowY: 'auto', }}> {products.map((product, index) => ( -
- {product.image_url ? ( - {product.product_name} - ) : ( -
- -
- )} - -
- - {product.product_name} - - - {product.tags || product.description || 'soft white, airy, minimal, fresh'} - - - ${product.price?.toFixed(2) || '59.95'} USD - -
-
+ ))}
); -} +}); +SelectedProductView.displayName = 'SelectedProductView'; diff --git a/content-gen/src/app/frontend/src/components/SuggestionCard.tsx b/content-gen/src/app/frontend/src/components/SuggestionCard.tsx new file mode 100644 index 000000000..d557936a5 --- /dev/null +++ b/content-gen/src/app/frontend/src/components/SuggestionCard.tsx @@ -0,0 +1,84 @@ +import { memo } from 'react'; +import { + Card, + Text, + tokens, +} from '@fluentui/react-components'; + +export interface SuggestionCardProps { + title: string; + prompt: string; + icon: string; + isSelected?: boolean; + onClick: () => void; +} + +/** + * A single suggestion prompt card shown on the WelcomeCard screen. + * Handles its own hover / selected styling. + */ +export const SuggestionCard = memo(function SuggestionCard({ + title, + icon, + isSelected = false, + onClick, +}: SuggestionCardProps) { + return ( + { + if (!isSelected) { + e.currentTarget.style.backgroundColor = tokens.colorBrandBackground2; + } + }} + onMouseLeave={(e) => { + if (!isSelected) { + e.currentTarget.style.backgroundColor = tokens.colorNeutralBackground1; + } + }} + > +
+
+ Prompt icon +
+
+ + {title} + +
+
+
+ ); +}); +SuggestionCard.displayName = 'SuggestionCard'; diff --git a/content-gen/src/app/frontend/src/components/TypingIndicator.tsx b/content-gen/src/app/frontend/src/components/TypingIndicator.tsx new file mode 100644 index 000000000..36fdbbf89 --- /dev/null +++ b/content-gen/src/app/frontend/src/components/TypingIndicator.tsx @@ -0,0 +1,76 @@ +import { memo, useMemo } from 'react'; +import { + Button, + Text, + Tooltip, + tokens, +} from '@fluentui/react-components'; +import { Stop24Regular } from '@fluentui/react-icons'; + +export interface TypingIndicatorProps { + /** Status text shown next to the dots (e.g. "Generating image…"). Falls back to "Thinking…". */ + statusText?: string; + /** Callback wired to the Stop button. If omitted the button is hidden. */ + onStop?: () => void; +} + +/** + * Animated "thinking" indicator with optional status text and a Stop button. + */ +export const TypingIndicator = memo(function TypingIndicator({ statusText, onStop }: TypingIndicatorProps) { + const dotStyle = useMemo(() => (delay: string): React.CSSProperties => ({ + width: '8px', + height: '8px', + borderRadius: '50%', + backgroundColor: tokens.colorBrandBackground, + animation: 'pulse 1.4s infinite ease-in-out', + animationDelay: delay, + }), []); + + return ( +
+
+ + + + + +
+ + + {statusText || 'Thinking...'} + + + {onStop && ( + + + + )} +
+ ); +}); +TypingIndicator.displayName = 'TypingIndicator'; diff --git a/content-gen/src/app/frontend/src/components/ViolationCard.tsx b/content-gen/src/app/frontend/src/components/ViolationCard.tsx new file mode 100644 index 000000000..52914c7a6 --- /dev/null +++ b/content-gen/src/app/frontend/src/components/ViolationCard.tsx @@ -0,0 +1,66 @@ +import { memo } from 'react'; +import { + Text, +} from '@fluentui/react-components'; +import { + ErrorCircle20Regular, + Warning20Regular, + Info20Regular, +} from '@fluentui/react-icons'; +import type { ComplianceViolation } from '../types'; + +export interface ViolationCardProps { + violation: ComplianceViolation; +} + +/** + * A single compliance violation row with severity-coloured icon and background. + */ +export const ViolationCard = memo(function ViolationCard({ violation }: ViolationCardProps) { + const getSeverityStyles = () => { + switch (violation.severity) { + case 'error': + return { + icon: , + bg: '#fde7e9', + }; + case 'warning': + return { + icon: , + bg: '#fff4ce', + }; + case 'info': + return { + icon: , + bg: '#deecf9', + }; + } + }; + + const { icon, bg } = getSeverityStyles(); + + return ( +
+ {icon} +
+ + {violation.message} + + + {violation.suggestion} + +
+
+ ); +}); +ViolationCard.displayName = 'ViolationCard'; diff --git a/content-gen/src/app/frontend/src/components/WelcomeCard.tsx b/content-gen/src/app/frontend/src/components/WelcomeCard.tsx index ab5740708..cf89dca2c 100644 --- a/content-gen/src/app/frontend/src/components/WelcomeCard.tsx +++ b/content-gen/src/app/frontend/src/components/WelcomeCard.tsx @@ -1,8 +1,9 @@ +import { memo, useMemo, useCallback } from 'react'; import { - Card, Text, tokens, } from '@fluentui/react-components'; +import { SuggestionCard } from './SuggestionCard'; import FirstPromptIcon from '../styles/images/firstprompt.png'; import SecondPromptIcon from '../styles/images/secondprompt.png'; @@ -30,8 +31,15 @@ interface WelcomeCardProps { currentInput?: string; } -export function WelcomeCard({ onSuggestionClick, currentInput = '' }: WelcomeCardProps) { - const selectedIndex = suggestions.findIndex(s => s.prompt === currentInput); +export const WelcomeCard = memo(function WelcomeCard({ onSuggestionClick, currentInput = '' }: WelcomeCardProps) { + const selectedIndex = useMemo( + () => suggestions.findIndex(s => s.prompt === currentInput), + [currentInput], + ); + + const handleSuggestionClick = useCallback((prompt: string) => { + onSuggestionClick(prompt); + }, [onSuggestionClick]); return (
{ const isSelected = index === selectedIndex; return ( - onSuggestionClick(suggestion.prompt)} - style={{ - padding: 'clamp(12px, 2vw, 16px)', - cursor: 'pointer', - backgroundColor: isSelected ? tokens.colorBrandBackground2 : tokens.colorNeutralBackground1, - border: 'none', - borderRadius: '16px', - transition: 'all 0.2s ease', - }} - onMouseEnter={(e) => { - if (!isSelected) { - e.currentTarget.style.backgroundColor = tokens.colorBrandBackground2; - } - }} - onMouseLeave={(e) => { - if (!isSelected) { - e.currentTarget.style.backgroundColor = tokens.colorNeutralBackground1; - } - }} - > -
-
- Prompt icon -
-
- - {suggestion.title} - -
-
-
+ title={suggestion.title} + prompt={suggestion.prompt} + icon={suggestion.icon} + isSelected={isSelected} + onClick={() => handleSuggestionClick(suggestion.prompt)} + /> ); })}
); -} +}); +WelcomeCard.displayName = 'WelcomeCard'; diff --git a/content-gen/src/app/frontend/src/hooks/useAutoScroll.ts b/content-gen/src/app/frontend/src/hooks/useAutoScroll.ts new file mode 100644 index 000000000..8b1a2a9d1 --- /dev/null +++ b/content-gen/src/app/frontend/src/hooks/useAutoScroll.ts @@ -0,0 +1,30 @@ +import { useEffect, useRef } from 'react'; + +/** + * Scrolls a sentinel element into view whenever any dependency changes. + * + * @param deps - React dependency list that triggers the scroll. + * @returns A ref to attach to a zero-height element at the bottom of the + * scrollable container (the "scroll anchor"). + * + * @example + * ```tsx + * const endRef = useAutoScroll([messages, isLoading]); + * return ( + *
+ * {messages.map(m => )} + *
+ *
+ * ); + * ``` + */ +export function useAutoScroll(deps: React.DependencyList) { + const endRef = useRef(null); + + // eslint-disable-next-line react-hooks/exhaustive-deps + useEffect(() => { + endRef.current?.scrollIntoView({ behavior: 'smooth' }); + }, deps); + + return endRef; +} diff --git a/content-gen/src/app/frontend/src/hooks/useChatOrchestrator.ts b/content-gen/src/app/frontend/src/hooks/useChatOrchestrator.ts new file mode 100644 index 000000000..0c4313587 --- /dev/null +++ b/content-gen/src/app/frontend/src/hooks/useChatOrchestrator.ts @@ -0,0 +1,547 @@ +import { useCallback, type MutableRefObject } from 'react'; +import { v4 as uuidv4 } from 'uuid'; + +import type { ChatMessage, GeneratedContent } from '../types'; +import { + useAppDispatch, + useAppSelector, + selectConversationId, + selectUserId, + selectPendingBrief, + selectConfirmedBrief, + selectAwaitingClarification, + selectSelectedProducts, + selectAvailableProducts, + selectGeneratedContent, + addMessage, + setIsLoading, + setGenerationStatus, + setPendingBrief, + setConfirmedBrief, + setAwaitingClarification, + setSelectedProducts, + setGeneratedContent, + incrementHistoryRefresh, + selectConversationTitle, + setConversationTitle, +} from '../store'; + +/* ------------------------------------------------------------------ */ +/* Helper: create a ChatMessage literal */ +/* ------------------------------------------------------------------ */ +function msg( + role: 'user' | 'assistant', + content: string, + agent?: string, +): ChatMessage { + return { + id: uuidv4(), + role, + content, + agent, + timestamp: new Date().toISOString(), + }; +} + +/* ------------------------------------------------------------------ */ +/* Hook */ +/* ------------------------------------------------------------------ */ + +/** + * Orchestrates the entire "send a message" flow. + * + * Depending on the current conversation phase it will: + * - refine a pending brief (PlanningAgent) + * - answer a general question while a brief is pending (streamChat) + * - forward a product-selection request (ProductAgent) + * - regenerate an image (ImageAgent) + * - parse a new creative brief (PlanningAgent) + * - fall through to generic chat (streamChat) + * + * All Redux reads/writes happen inside the hook so the caller is kept + * thin and declarative. + * + * @param abortControllerRef Shared ref that lets the parent (or sibling + * hooks) cancel the in-flight request. + * @returns `{ sendMessage }` — the callback to wire into `ChatPanel`. + */ +export function useChatOrchestrator( + abortControllerRef: MutableRefObject, +) { + const dispatch = useAppDispatch(); + const conversationId = useAppSelector(selectConversationId); + const userId = useAppSelector(selectUserId); + const pendingBrief = useAppSelector(selectPendingBrief); + const confirmedBrief = useAppSelector(selectConfirmedBrief); + const awaitingClarification = useAppSelector(selectAwaitingClarification); + const selectedProducts = useAppSelector(selectSelectedProducts); + const availableProducts = useAppSelector(selectAvailableProducts); + const generatedContent = useAppSelector(selectGeneratedContent); + const conversationTitle = useAppSelector(selectConversationTitle); + + const sendMessage = useCallback( + async (content: string) => { + dispatch(addMessage(msg('user', content))); + dispatch(setIsLoading(true)); + + // Create new abort controller for this request + abortControllerRef.current = new AbortController(); + const signal = abortControllerRef.current.signal; + + try { + // Dynamic imports to keep the initial bundle lean + const { streamChat, parseBrief, selectProducts } = await import( + '../api' + ); + + /* ---------------------------------------------------------- */ + /* Branch 1 – pending brief, not yet confirmed */ + /* ---------------------------------------------------------- */ + if (pendingBrief && !confirmedBrief) { + const refinementKeywords = [ + 'change', 'update', 'modify', 'add', 'remove', 'delete', + 'set', 'make', 'should be', + ]; + const isRefinement = refinementKeywords.some((kw) => + content.toLowerCase().includes(kw), + ); + + if (isRefinement || awaitingClarification) { + // --- 1-a Refine the brief -------------------------------- + const refinementPrompt = `Current creative brief:\n${JSON.stringify(pendingBrief, null, 2)}\n\nUser requested change: ${content}\n\nPlease update the brief accordingly and return the complete updated brief.`; + + dispatch(setGenerationStatus('Updating creative brief...')); + const parsed = await parseBrief( + refinementPrompt, + conversationId, + userId, + signal, + ); + + if (parsed.generated_title && !conversationTitle) { + dispatch(setConversationTitle(parsed.generated_title)); + } + + if (parsed.brief) { + dispatch(setPendingBrief(parsed.brief)); + } + + if (parsed.requires_clarification && parsed.clarifying_questions) { + dispatch(setAwaitingClarification(true)); + dispatch(setGenerationStatus('')); + dispatch( + addMessage( + msg('assistant', parsed.clarifying_questions, 'PlanningAgent'), + ), + ); + } else { + dispatch(setAwaitingClarification(false)); + dispatch(setGenerationStatus('')); + dispatch( + addMessage( + msg( + 'assistant', + "I've updated the brief based on your feedback. Please review the changes above. Let me know if you'd like any other modifications, or click **Confirm Brief** when you're satisfied.", + 'PlanningAgent', + ), + ), + ); + } + } else { + // --- 1-b General question while brief is pending ----------- + let fullContent = ''; + let currentAgent = ''; + let messageAdded = false; + + dispatch(setGenerationStatus('Processing your question...')); + for await (const response of streamChat( + content, + conversationId, + userId, + signal, + )) { + if (response.type === 'agent_response') { + fullContent = response.content; + currentAgent = response.agent || ''; + if ( + (response.is_final || response.requires_user_input) && + !messageAdded + ) { + dispatch( + addMessage(msg('assistant', fullContent, currentAgent)), + ); + messageAdded = true; + } + } else if (response.type === 'error') { + dispatch( + addMessage( + msg( + 'assistant', + response.content || + 'An error occurred while processing your request.', + ), + ), + ); + messageAdded = true; + } + } + dispatch(setGenerationStatus('')); + } + + /* ---------------------------------------------------------- */ + /* Branch 2 – brief confirmed, in product selection */ + /* ---------------------------------------------------------- */ + } else if (confirmedBrief && !generatedContent) { + dispatch(setGenerationStatus('Finding products...')); + const result = await selectProducts( + content, + selectedProducts, + conversationId, + userId, + signal, + ); + dispatch(setSelectedProducts(result.products || [])); + dispatch(setGenerationStatus('')); + dispatch( + addMessage( + msg( + 'assistant', + result.message || 'Products updated.', + 'ProductAgent', + ), + ), + ); + + /* ---------------------------------------------------------- */ + /* Branch 3 – content generated, post-generation phase */ + /* ---------------------------------------------------------- */ + } else if (generatedContent && confirmedBrief) { + const imageModificationKeywords = [ + 'change', 'modify', 'update', 'replace', 'show', 'display', + 'use', 'instead', 'different', 'another', 'make it', 'make the', + 'kitchen', 'dining', 'living', 'bedroom', 'bathroom', 'outdoor', + 'office', 'room', 'scene', 'setting', 'background', 'style', + 'color', 'lighting', + ]; + const isImageModification = imageModificationKeywords.some((kw) => + content.toLowerCase().includes(kw), + ); + + if (isImageModification) { + // --- 3-a Regenerate image -------------------------------- + const { streamRegenerateImage } = await import('../api'); + dispatch( + setGenerationStatus('Regenerating image with your changes...'), + ); + + let responseData: GeneratedContent | null = null; + let messageContent = ''; + + // Detect if user mentions a different product + const mentionedProduct = availableProducts.find((p) => + content.toLowerCase().includes(p.product_name.toLowerCase()), + ); + const productsForRequest = mentionedProduct + ? [mentionedProduct] + : selectedProducts; + + const previousPrompt = + generatedContent.image_content?.prompt_used; + + for await (const response of streamRegenerateImage( + content, + confirmedBrief, + productsForRequest, + previousPrompt, + conversationId, + userId, + signal, + )) { + if (response.type === 'heartbeat') { + dispatch( + setGenerationStatus( + response.message || 'Regenerating image...', + ), + ); + } else if ( + response.type === 'agent_response' && + response.is_final + ) { + try { + const parsedContent = JSON.parse(response.content); + + if ( + parsedContent.image_url || + parsedContent.image_base64 + ) { + // Replace old product name in text_content when switching + const oldName = selectedProducts[0]?.product_name; + const newName = mentionedProduct?.product_name; + const nameRegex = oldName + ? new RegExp( + oldName.replace( + /[.*+?^${}()|[\]\\]/g, + '\\$&', + ), + 'gi', + ) + : undefined; + const swapName = (s?: string) => { + if ( + !s || + !oldName || + !newName || + oldName === newName || + !nameRegex + ) + return s; + return s.replace(nameRegex, () => newName); + }; + const tc = generatedContent.text_content; + + responseData = { + ...generatedContent, + text_content: mentionedProduct + ? { + ...tc, + headline: swapName(tc?.headline), + body: swapName(tc?.body), + tagline: swapName(tc?.tagline), + cta_text: swapName(tc?.cta_text), + } + : tc, + image_content: { + ...generatedContent.image_content, + image_url: + parsedContent.image_url || + generatedContent.image_content?.image_url, + image_base64: parsedContent.image_base64, + prompt_used: + parsedContent.image_prompt || + generatedContent.image_content?.prompt_used, + }, + }; + dispatch(setGeneratedContent(responseData)); + + if (mentionedProduct) { + dispatch(setSelectedProducts([mentionedProduct])); + } + + // Update confirmed brief to include the modification + const updatedBrief = { + ...confirmedBrief, + visual_guidelines: `${confirmedBrief.visual_guidelines}. User modification: ${content}`, + }; + dispatch(setConfirmedBrief(updatedBrief)); + + messageContent = + parsedContent.message || + 'Image regenerated with your requested changes.'; + } else if (parsedContent.error) { + messageContent = parsedContent.error; + } else { + messageContent = + parsedContent.message || 'I processed your request.'; + } + } catch { + messageContent = + response.content || 'Image regenerated.'; + } + } else if (response.type === 'error') { + messageContent = + response.content || + 'An error occurred while regenerating the image.'; + } + } + + dispatch(setGenerationStatus('')); + dispatch( + addMessage(msg('assistant', messageContent, 'ImageAgent')), + ); + } else { + // --- 3-b General question after content generation -------- + let fullContent = ''; + let currentAgent = ''; + let messageAdded = false; + + dispatch(setGenerationStatus('Processing your request...')); + for await (const response of streamChat( + content, + conversationId, + userId, + signal, + )) { + if (response.type === 'agent_response') { + fullContent = response.content; + currentAgent = response.agent || ''; + if ( + (response.is_final || response.requires_user_input) && + !messageAdded + ) { + dispatch( + addMessage(msg('assistant', fullContent, currentAgent)), + ); + messageAdded = true; + } + } else if (response.type === 'error') { + dispatch( + addMessage( + msg( + 'assistant', + response.content || 'An error occurred.', + ), + ), + ); + messageAdded = true; + } + } + dispatch(setGenerationStatus('')); + } + + /* ---------------------------------------------------------- */ + /* Branch 4 – default: initial flow */ + /* ---------------------------------------------------------- */ + } else { + const briefKeywords = [ + 'campaign', 'marketing', 'target audience', 'objective', + 'deliverable', + ]; + const isBriefLike = briefKeywords.some((kw) => + content.toLowerCase().includes(kw), + ); + + if (isBriefLike && !confirmedBrief) { + // --- 4-a Parse as creative brief -------------------------- + dispatch(setGenerationStatus('Analyzing creative brief...')); + const parsed = await parseBrief( + content, + conversationId, + userId, + signal, + ); + + if (parsed.generated_title && !conversationTitle) { + dispatch(setConversationTitle(parsed.generated_title)); + } + + if (parsed.rai_blocked) { + dispatch(setGenerationStatus('')); + dispatch( + addMessage( + msg('assistant', parsed.message, 'ContentSafety'), + ), + ); + } else if ( + parsed.requires_clarification && + parsed.clarifying_questions + ) { + if (parsed.brief) { + dispatch(setPendingBrief(parsed.brief)); + } + dispatch(setAwaitingClarification(true)); + dispatch(setGenerationStatus('')); + dispatch( + addMessage( + msg( + 'assistant', + parsed.clarifying_questions, + 'PlanningAgent', + ), + ), + ); + } else { + if (parsed.brief) { + dispatch(setPendingBrief(parsed.brief)); + } + dispatch(setAwaitingClarification(false)); + dispatch(setGenerationStatus('')); + dispatch( + addMessage( + msg( + 'assistant', + "I've parsed your creative brief. Please review the details below and let me know if you'd like to make any changes. You can say things like \"change the target audience to...\" or \"add a call to action...\". When everything looks good, click **Confirm Brief** to proceed.", + 'PlanningAgent', + ), + ), + ); + } + } else { + // --- 4-b Generic chat ----------------------------------- + let fullContent = ''; + let currentAgent = ''; + let messageAdded = false; + + dispatch(setGenerationStatus('Processing your request...')); + for await (const response of streamChat( + content, + conversationId, + userId, + signal, + )) { + if (response.type === 'agent_response') { + fullContent = response.content; + currentAgent = response.agent || ''; + if ( + (response.is_final || response.requires_user_input) && + !messageAdded + ) { + dispatch( + addMessage(msg('assistant', fullContent, currentAgent)), + ); + messageAdded = true; + } + } else if (response.type === 'error') { + dispatch( + addMessage( + msg( + 'assistant', + response.content || + 'An error occurred while processing your request.', + ), + ), + ); + messageAdded = true; + } + } + dispatch(setGenerationStatus('')); + } + } + } catch (error) { + if (error instanceof Error && error.name === 'AbortError') { + console.debug('Request cancelled by user'); + dispatch(addMessage(msg('assistant', 'Generation stopped.'))); + } else { + console.error('Error sending message:', error); + dispatch( + addMessage( + msg( + 'assistant', + 'Sorry, there was an error processing your request. Please try again.', + ), + ), + ); + } + } finally { + dispatch(setIsLoading(false)); + dispatch(setGenerationStatus('')); + abortControllerRef.current = null; + dispatch(incrementHistoryRefresh()); + } + }, + [ + conversationId, + userId, + confirmedBrief, + pendingBrief, + selectedProducts, + generatedContent, + availableProducts, + dispatch, + awaitingClarification, + conversationTitle, + abortControllerRef, + ], + ); + + return { sendMessage }; +} diff --git a/content-gen/src/app/frontend/src/hooks/useContentGeneration.ts b/content-gen/src/app/frontend/src/hooks/useContentGeneration.ts new file mode 100644 index 000000000..53513c2d8 --- /dev/null +++ b/content-gen/src/app/frontend/src/hooks/useContentGeneration.ts @@ -0,0 +1,170 @@ +import { useCallback, type MutableRefObject } from 'react'; +import { v4 as uuidv4 } from 'uuid'; + +import type { ChatMessage, GeneratedContent } from '../types'; +import { + useAppDispatch, + useAppSelector, + selectConfirmedBrief, + selectSelectedProducts, + selectConversationId, + selectUserId, + addMessage, + setIsLoading, + setGenerationStatus, + setGeneratedContent, +} from '../store'; + +/** + * Handles the full content-generation lifecycle (start → poll → result) + * and exposes a way to abort the in-flight request. + * + * @param abortControllerRef Shared ref so the UI can cancel either + * chat-orchestration **or** content-generation with one button. + */ +export function useContentGeneration( + abortControllerRef: MutableRefObject, +) { + const dispatch = useAppDispatch(); + const confirmedBrief = useAppSelector(selectConfirmedBrief); + const selectedProducts = useAppSelector(selectSelectedProducts); + const conversationId = useAppSelector(selectConversationId); + const userId = useAppSelector(selectUserId); + + /** Kick off polling-based content generation. */ + const generateContent = useCallback(async () => { + if (!confirmedBrief) return; + + dispatch(setIsLoading(true)); + dispatch(setGenerationStatus('Starting content generation...')); + + abortControllerRef.current = new AbortController(); + const signal = abortControllerRef.current.signal; + + try { + const { streamGenerateContent } = await import('../api'); + + for await (const response of streamGenerateContent( + confirmedBrief, + selectedProducts, + true, + conversationId, + userId, + signal, + )) { + // Heartbeat → update the status bar + if (response.type === 'heartbeat') { + const statusMessage = response.content || 'Generating content...'; + const elapsed = (response as { elapsed?: number }).elapsed || 0; + dispatch(setGenerationStatus(`${statusMessage} (${elapsed}s)`)); + continue; + } + + if (response.is_final && response.type !== 'error') { + dispatch(setGenerationStatus('Processing results...')); + try { + const rawContent = JSON.parse(response.content); + + // Parse text_content if it's a string (from orchestrator) + let textContent = rawContent.text_content; + if (typeof textContent === 'string') { + try { + textContent = JSON.parse(textContent); + } catch { + // Keep as string if not valid JSON + } + } + + // Build image_url: prefer blob URL, fallback to base64 data URL + let imageUrl: string | undefined; + if (rawContent.image_url) { + imageUrl = rawContent.image_url; + } else if (rawContent.image_base64) { + imageUrl = `data:image/png;base64,${rawContent.image_base64}`; + } + + const genContent: GeneratedContent = { + text_content: + typeof textContent === 'object' + ? { + headline: textContent?.headline, + body: textContent?.body, + cta_text: textContent?.cta, + tagline: textContent?.tagline, + } + : undefined, + image_content: + imageUrl || rawContent.image_prompt + ? { + image_url: imageUrl, + prompt_used: rawContent.image_prompt, + alt_text: + rawContent.image_revised_prompt || + 'Generated marketing image', + } + : undefined, + violations: rawContent.violations || [], + requires_modification: + rawContent.requires_modification || false, + error: rawContent.error, + image_error: rawContent.image_error, + text_error: rawContent.text_error, + }; + dispatch(setGeneratedContent(genContent)); + dispatch(setGenerationStatus('')); + } catch (parseError) { + console.error('Error parsing generated content:', parseError); + } + } else if (response.type === 'error') { + dispatch(setGenerationStatus('')); + const errorMessage: ChatMessage = { + id: uuidv4(), + role: 'assistant', + content: `Error generating content: ${response.content}`, + timestamp: new Date().toISOString(), + }; + dispatch(addMessage(errorMessage)); + } + } + } catch (error) { + if (error instanceof Error && error.name === 'AbortError') { + console.debug('Content generation cancelled by user'); + const cancelMessage: ChatMessage = { + id: uuidv4(), + role: 'assistant', + content: 'Content generation stopped.', + timestamp: new Date().toISOString(), + }; + dispatch(addMessage(cancelMessage)); + } else { + console.error('Error generating content:', error); + const errorMessage: ChatMessage = { + id: uuidv4(), + role: 'assistant', + content: + 'Sorry, there was an error generating content. Please try again.', + timestamp: new Date().toISOString(), + }; + dispatch(addMessage(errorMessage)); + } + } finally { + dispatch(setIsLoading(false)); + dispatch(setGenerationStatus('')); + abortControllerRef.current = null; + } + }, [ + confirmedBrief, + selectedProducts, + conversationId, + dispatch, + userId, + abortControllerRef, + ]); + + /** Abort whichever request is currently in-flight. */ + const stopGeneration = useCallback(() => { + abortControllerRef.current?.abort(); + }, [abortControllerRef]); + + return { generateContent, stopGeneration }; +} diff --git a/content-gen/src/app/frontend/src/hooks/useConversationActions.ts b/content-gen/src/app/frontend/src/hooks/useConversationActions.ts new file mode 100644 index 000000000..76ab731ec --- /dev/null +++ b/content-gen/src/app/frontend/src/hooks/useConversationActions.ts @@ -0,0 +1,296 @@ +import { useCallback } from 'react'; +import { v4 as uuidv4 } from 'uuid'; + +import type { ChatMessage, Product, CreativeBrief, GeneratedContent } from '../types'; +import httpClient from '../api/httpClient'; +import { + useAppDispatch, + useAppSelector, + selectUserId, + selectConversationId, + selectPendingBrief, + selectSelectedProducts, + resetChat, + resetContent, + setConversationId, + setConversationTitle, + setMessages, + addMessage, + setPendingBrief, + setConfirmedBrief, + setAwaitingClarification, + setSelectedProducts, + setAvailableProducts, + setGeneratedContent, + toggleChatHistory, +} from '../store'; + +/* ------------------------------------------------------------------ */ +/* Helper: create a ChatMessage literal */ +/* ------------------------------------------------------------------ */ +function msg( + role: 'user' | 'assistant', + content: string, + agent?: string, +): ChatMessage { + return { + id: uuidv4(), + role, + content, + agent, + timestamp: new Date().toISOString(), + }; +} + +/* ------------------------------------------------------------------ */ +/* Hook */ +/* ------------------------------------------------------------------ */ + +/** + * Encapsulates every conversation-level user action: + * + * - Loading a saved conversation from history + * - Starting a brand-new conversation + * - Confirming / cancelling a creative brief + * - Starting over with products + * - Toggling a product selection + * - Toggling the chat-history sidebar + * + * All Redux reads/writes are internal so the consumer stays declarative. + */ +export function useConversationActions() { + const dispatch = useAppDispatch(); + const userId = useAppSelector(selectUserId); + const conversationId = useAppSelector(selectConversationId); + const pendingBrief = useAppSelector(selectPendingBrief); + const selectedProducts = useAppSelector(selectSelectedProducts); + + /* ------------------------------------------------------------ */ + /* Select (load) a conversation from history */ + /* ------------------------------------------------------------ */ + const selectConversation = useCallback( + async (selectedConversationId: string) => { + try { + const data = await httpClient.get<{ + messages?: { + role: string; + content: string; + timestamp?: string; + agent?: string; + }[]; + brief?: unknown; + generated_content?: Record; + }>(`/conversations/${selectedConversationId}`, { + params: { user_id: userId }, + }); + + dispatch(setConversationId(selectedConversationId)); + dispatch(setConversationTitle(null)); // Will use title from conversation list + + const loadedMessages: ChatMessage[] = (data.messages || []).map( + (m, index) => ({ + id: `${selectedConversationId}-${index}`, + role: m.role as 'user' | 'assistant', + content: m.content, + timestamp: m.timestamp || new Date().toISOString(), + agent: m.agent, + }), + ); + dispatch(setMessages(loadedMessages)); + dispatch(setPendingBrief(null)); + dispatch(setAwaitingClarification(false)); + dispatch( + setConfirmedBrief( + (data.brief as CreativeBrief) || null, + ), + ); + + // Restore availableProducts so product/color name detection works + // when regenerating images in a restored conversation + if (data.brief) { + try { + const productsData = await httpClient.get<{ + products?: Product[]; + }>('/products'); + dispatch(setAvailableProducts(productsData.products || [])); + } catch (err) { + console.error( + 'Error loading products for restored conversation:', + err, + ); + } + } + + if (data.generated_content) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const gc = data.generated_content as any; + let textContent = gc.text_content; + if (typeof textContent === 'string') { + try { + textContent = JSON.parse(textContent); + } catch { + // keep as-is + } + } + + let imageUrl: string | undefined = gc.image_url; + if (imageUrl && imageUrl.includes('blob.core.windows.net')) { + const parts = imageUrl.split('/'); + const filename = parts[parts.length - 1]; + const convId = parts[parts.length - 2]; + imageUrl = `/api/images/${convId}/${filename}`; + } + if (!imageUrl && gc.image_base64) { + imageUrl = `data:image/png;base64,${gc.image_base64}`; + } + + const restoredContent: GeneratedContent = { + text_content: + typeof textContent === 'object' && textContent + ? { + headline: textContent?.headline, + body: textContent?.body, + cta_text: textContent?.cta, + tagline: textContent?.tagline, + } + : undefined, + image_content: + imageUrl || gc.image_prompt + ? { + image_url: imageUrl, + prompt_used: gc.image_prompt, + alt_text: + gc.image_revised_prompt || + 'Generated marketing image', + } + : undefined, + violations: gc.violations || [], + requires_modification: gc.requires_modification || false, + error: gc.error, + image_error: gc.image_error, + text_error: gc.text_error, + }; + dispatch(setGeneratedContent(restoredContent)); + + if ( + gc.selected_products && + Array.isArray(gc.selected_products) + ) { + dispatch(setSelectedProducts(gc.selected_products)); + } else { + dispatch(setSelectedProducts([])); + } + } else { + dispatch(setGeneratedContent(null)); + dispatch(setSelectedProducts([])); + } + } catch (error) { + console.error('Error loading conversation:', error); + } + }, + [userId, dispatch], + ); + + /* ------------------------------------------------------------ */ + /* Start a new conversation */ + /* ------------------------------------------------------------ */ + const newConversation = useCallback(() => { + dispatch(resetChat()); + dispatch(resetContent()); + }, [dispatch]); + + /* ------------------------------------------------------------ */ + /* Brief lifecycle */ + /* ------------------------------------------------------------ */ + const confirmBrief = useCallback(async () => { + if (!pendingBrief) return; + + try { + const { confirmBrief: confirmBriefApi } = await import('../api'); + await confirmBriefApi(pendingBrief, conversationId, userId); + dispatch(setConfirmedBrief(pendingBrief)); + dispatch(setPendingBrief(null)); + dispatch(setAwaitingClarification(false)); + + const productsData = await httpClient.get<{ products?: Product[] }>( + '/products', + ); + dispatch(setAvailableProducts(productsData.products || [])); + + dispatch( + addMessage( + msg( + 'assistant', + "Great! Your creative brief has been confirmed. Here are the available products for your campaign. Select the ones you'd like to feature, or tell me what you're looking for.", + 'ProductAgent', + ), + ), + ); + } catch (error) { + console.error('Error confirming brief:', error); + } + }, [conversationId, userId, pendingBrief, dispatch]); + + const cancelBrief = useCallback(() => { + dispatch(setPendingBrief(null)); + dispatch(setAwaitingClarification(false)); + dispatch( + addMessage( + msg( + 'assistant', + 'No problem. Please provide your creative brief again or ask me any questions.', + ), + ), + ); + }, [dispatch]); + + /* ------------------------------------------------------------ */ + /* Product actions */ + /* ------------------------------------------------------------ */ + const productsStartOver = useCallback(() => { + dispatch(setSelectedProducts([])); + dispatch(setConfirmedBrief(null)); + dispatch( + addMessage( + msg( + 'assistant', + 'Starting over. Please provide your creative brief to begin a new campaign.', + ), + ), + ); + }, [dispatch]); + + const selectProduct = useCallback( + (product: Product) => { + const isSelected = selectedProducts.some( + (p) => + (p.sku || p.product_name) === + (product.sku || product.product_name), + ); + if (isSelected) { + dispatch(setSelectedProducts([])); + } else { + // Single selection mode — replace any existing selection + dispatch(setSelectedProducts([product])); + } + }, + [selectedProducts, dispatch], + ); + + /* ------------------------------------------------------------ */ + /* Sidebar toggle */ + /* ------------------------------------------------------------ */ + const toggleHistory = useCallback(() => { + dispatch(toggleChatHistory()); + }, [dispatch]); + + return { + selectConversation, + newConversation, + confirmBrief, + cancelBrief, + productsStartOver, + selectProduct, + toggleHistory, + }; +} diff --git a/content-gen/src/app/frontend/src/hooks/useCopyToClipboard.ts b/content-gen/src/app/frontend/src/hooks/useCopyToClipboard.ts new file mode 100644 index 000000000..08d024d8d --- /dev/null +++ b/content-gen/src/app/frontend/src/hooks/useCopyToClipboard.ts @@ -0,0 +1,32 @@ +import { useState, useCallback, useRef, useEffect } from 'react'; + +/** + * Copy text to the clipboard and expose a transient `copied` flag. + * + * @param resetTimeout - Milliseconds before `copied` resets to `false` (default 2 000). + * @returns `{ copied, copy }` — `copy(text)` writes to the clipboard and + * flips `copied` to `true` for `resetTimeout` ms. + */ +export function useCopyToClipboard(resetTimeout = 2000) { + const [copied, setCopied] = useState(false); + const timerRef = useRef>(); + + const copy = useCallback( + (text: string) => { + navigator.clipboard.writeText(text).catch((err) => { + console.error('Failed to copy text:', err); + }); + setCopied(true); + clearTimeout(timerRef.current); + timerRef.current = setTimeout(() => setCopied(false), resetTimeout); + }, + [resetTimeout], + ); + + // Cleanup on unmount + useEffect(() => { + return () => clearTimeout(timerRef.current); + }, []); + + return { copied, copy }; +} diff --git a/content-gen/src/app/frontend/src/hooks/useDebounce.ts b/content-gen/src/app/frontend/src/hooks/useDebounce.ts new file mode 100644 index 000000000..c0c7e5ab1 --- /dev/null +++ b/content-gen/src/app/frontend/src/hooks/useDebounce.ts @@ -0,0 +1,29 @@ +import { useState, useEffect } from 'react'; + +/** + * Returns a debounced copy of `value` that only updates after `delay` ms of + * inactivity. + * + * @param value - The source value to debounce. + * @param delay - Debounce window in milliseconds. + * + * @example + * ```ts + * const [search, setSearch] = useState(''); + * const debouncedSearch = useDebounce(search, 300); + * + * useEffect(() => { + * fetchResults(debouncedSearch); + * }, [debouncedSearch]); + * ``` + */ +export function useDebounce(value: T, delay: number): T { + const [debounced, setDebounced] = useState(value); + + useEffect(() => { + const timer = setTimeout(() => setDebounced(value), delay); + return () => clearTimeout(timer); + }, [value, delay]); + + return debounced; +} diff --git a/content-gen/src/app/frontend/src/hooks/useWindowSize.ts b/content-gen/src/app/frontend/src/hooks/useWindowSize.ts new file mode 100644 index 000000000..da662eb41 --- /dev/null +++ b/content-gen/src/app/frontend/src/hooks/useWindowSize.ts @@ -0,0 +1,19 @@ +import { useState, useEffect } from 'react'; + +/** + * Returns the current window inner-width, updating on resize. + * Falls back to 1200 during SSR. + */ +export function useWindowSize(): number { + const [windowWidth, setWindowWidth] = useState( + typeof window !== 'undefined' ? window.innerWidth : 1200, + ); + + useEffect(() => { + const handleResize = () => setWindowWidth(window.innerWidth); + window.addEventListener('resize', handleResize); + return () => window.removeEventListener('resize', handleResize); + }, []); + + return windowWidth; +} diff --git a/content-gen/src/app/frontend/src/main.tsx b/content-gen/src/app/frontend/src/main.tsx index e59c93df9..db4a8119d 100644 --- a/content-gen/src/app/frontend/src/main.tsx +++ b/content-gen/src/app/frontend/src/main.tsx @@ -1,13 +1,17 @@ import React from 'react'; import ReactDOM from 'react-dom/client'; import { FluentProvider, webLightTheme } from '@fluentui/react-components'; +import { Provider } from 'react-redux'; +import { store } from './store'; import App from './App'; import './styles/global.css'; ReactDOM.createRoot(document.getElementById('root')!).render( - - - + + + + + ); diff --git a/content-gen/src/app/frontend/src/store/appSlice.ts b/content-gen/src/app/frontend/src/store/appSlice.ts new file mode 100644 index 000000000..59876ec03 --- /dev/null +++ b/content-gen/src/app/frontend/src/store/appSlice.ts @@ -0,0 +1,100 @@ +/** + * App slice — application-level state (user info, config, feature flags, UI toggles). + * createSlice + createAsyncThunk replaces manual dispatch + string constants. + * Granular selectors — each component subscribes only to the state it needs. + */ +import { createSlice, createAsyncThunk, type PayloadAction } from '@reduxjs/toolkit'; + +/* ------------------------------------------------------------------ */ +/* Async Thunks */ +/* ------------------------------------------------------------------ */ + +export const fetchAppConfig = createAsyncThunk( + 'app/fetchAppConfig', + async () => { + const { getAppConfig } = await import('../api'); + const config = await getAppConfig(); + return config; + }, +); + +export const fetchUserInfo = createAsyncThunk( + 'app/fetchUserInfo', + async () => { + const response = await fetch('/.auth/me'); + if (!response.ok) return { userId: 'anonymous', userName: '' }; + + const payload = await response.json(); + const claims: { typ: string; val: string }[] = payload[0]?.user_claims || []; + + const objectId = claims.find( + (c) => c.typ === 'http://schemas.microsoft.com/identity/claims/objectidentifier', + )?.val || 'anonymous'; + + const name = claims.find((c) => c.typ === 'name')?.val || ''; + + return { userId: objectId, userName: name }; + }, +); + +/* ------------------------------------------------------------------ */ +/* Slice */ +/* ------------------------------------------------------------------ */ + +interface AppState { + userId: string; + userName: string; + isLoading: boolean; + imageGenerationEnabled: boolean; + showChatHistory: boolean; + generationStatus: string; +} + +const initialState: AppState = { + userId: '', + userName: '', + isLoading: false, + imageGenerationEnabled: true, + showChatHistory: true, + generationStatus: '', +}; + +const appSlice = createSlice({ + name: 'app', + initialState, + reducers: { + setIsLoading(state, action: PayloadAction) { + state.isLoading = action.payload; + }, + setGenerationStatus(state, action: PayloadAction) { + state.generationStatus = action.payload; + }, + toggleChatHistory(state) { + state.showChatHistory = !state.showChatHistory; + }, + setShowChatHistory(state, action: PayloadAction) { + state.showChatHistory = action.payload; + }, + }, + extraReducers: (builder) => { + builder + .addCase(fetchAppConfig.fulfilled, (state, action) => { + state.imageGenerationEnabled = action.payload.enable_image_generation; + }) + .addCase(fetchAppConfig.rejected, (state) => { + state.imageGenerationEnabled = true; // default when fetch fails + }) + .addCase(fetchUserInfo.fulfilled, (state, action) => { + state.userId = action.payload.userId; + state.userName = action.payload.userName; + }) + .addCase(fetchUserInfo.rejected, (state) => { + state.userId = 'anonymous'; + state.userName = ''; + }); + }, +}); + +export const { setIsLoading, setGenerationStatus, toggleChatHistory, setShowChatHistory } = + appSlice.actions; +export default appSlice.reducer; diff --git a/content-gen/src/app/frontend/src/store/chatHistorySlice.ts b/content-gen/src/app/frontend/src/store/chatHistorySlice.ts new file mode 100644 index 000000000..4f088c3a1 --- /dev/null +++ b/content-gen/src/app/frontend/src/store/chatHistorySlice.ts @@ -0,0 +1,138 @@ +/** + * Chat history slice — conversation list CRUD via async thunks. + * createAsyncThunk replaces inline fetch + manual state updates in ChatHistory.tsx. + * Granular selectors for each piece of history state. + */ +import { createSlice, createAsyncThunk, type PayloadAction } from '@reduxjs/toolkit'; +import httpClient from '../api/httpClient'; + +export interface ConversationSummary { + id: string; + title: string; + lastMessage: string; + timestamp: string; + messageCount: number; +} + +interface ChatHistoryState { + conversations: ConversationSummary[]; + isLoading: boolean; + error: string | null; + showAll: boolean; + isClearAllDialogOpen: boolean; + isClearing: boolean; +} + +const initialState: ChatHistoryState = { + conversations: [], + isLoading: true, + error: null, + showAll: false, + isClearAllDialogOpen: false, + isClearing: false, +}; + +/* ------------------------------------------------------------------ */ +/* Async Thunks */ +/* ------------------------------------------------------------------ */ + +export const fetchConversations = createAsyncThunk( + 'chatHistory/fetchConversations', + async () => { + const data = await httpClient.get<{ conversations?: ConversationSummary[] }>('/conversations'); + return (data.conversations || []) as ConversationSummary[]; + }, +); + +export const deleteConversation = createAsyncThunk( + 'chatHistory/deleteConversation', + async (conversationId: string) => { + await httpClient.delete(`/conversations/${conversationId}`); + return conversationId; + }, +); + +export const renameConversation = createAsyncThunk( + 'chatHistory/renameConversation', + async ({ conversationId, newTitle }: { conversationId: string; newTitle: string }) => { + await httpClient.put(`/conversations/${conversationId}`, { title: newTitle }); + return { conversationId, newTitle }; + }, +); + +export const clearAllConversations = createAsyncThunk( + 'chatHistory/clearAllConversations', + async () => { + await httpClient.delete('/conversations'); + }, +); + +/* ------------------------------------------------------------------ */ +/* Slice */ +/* ------------------------------------------------------------------ */ + +const chatHistorySlice = createSlice({ + name: 'chatHistory', + initialState, + reducers: { + setShowAll(state, action: PayloadAction) { + state.showAll = action.payload; + }, + setConversations(state, action: PayloadAction) { + state.conversations = action.payload; + }, + upsertConversation(state, action: PayloadAction) { + const idx = state.conversations.findIndex((c) => c.id === action.payload.id); + if (idx >= 0) { + state.conversations[idx] = action.payload; + } else { + state.conversations.unshift(action.payload); + } + }, + setIsClearAllDialogOpen(state, action: PayloadAction) { + state.isClearAllDialogOpen = action.payload; + }, + }, + extraReducers: (builder) => { + builder + // Fetch + .addCase(fetchConversations.pending, (state) => { + state.isLoading = true; + state.error = null; + }) + .addCase(fetchConversations.fulfilled, (state, action) => { + state.conversations = action.payload; + state.isLoading = false; + }) + .addCase(fetchConversations.rejected, (state) => { + state.error = 'Unable to load conversation history'; + state.conversations = []; + state.isLoading = false; + }) + // Delete single + .addCase(deleteConversation.fulfilled, (state, action) => { + state.conversations = state.conversations.filter((c) => c.id !== action.payload); + }) + // Rename + .addCase(renameConversation.fulfilled, (state, action) => { + const conv = state.conversations.find((c) => c.id === action.payload.conversationId); + if (conv) conv.title = action.payload.newTitle; + }) + // Clear all + .addCase(clearAllConversations.pending, (state) => { + state.isClearing = true; + }) + .addCase(clearAllConversations.fulfilled, (state) => { + state.conversations = []; + state.isClearing = false; + state.isClearAllDialogOpen = false; + }) + .addCase(clearAllConversations.rejected, (state) => { + state.isClearing = false; + }); + }, +}); + +export const { setShowAll, setConversations, upsertConversation, setIsClearAllDialogOpen } = + chatHistorySlice.actions; +export default chatHistorySlice.reducer; diff --git a/content-gen/src/app/frontend/src/store/chatSlice.ts b/content-gen/src/app/frontend/src/store/chatSlice.ts new file mode 100644 index 000000000..71b25330e --- /dev/null +++ b/content-gen/src/app/frontend/src/store/chatSlice.ts @@ -0,0 +1,67 @@ +/** + * Chat slice — conversation state, messages, clarification flow. + * Typed createSlice replaces scattered useState-based state in App.tsx. + * Granular selectors for each piece of chat state. + */ +import { createSlice, type PayloadAction } from '@reduxjs/toolkit'; +import { v4 as uuidv4 } from 'uuid'; +import type { ChatMessage } from '../types'; + +interface ChatState { + conversationId: string; + conversationTitle: string | null; + messages: ChatMessage[]; + awaitingClarification: boolean; + historyRefreshTrigger: number; +} + +const initialState: ChatState = { + conversationId: uuidv4(), + conversationTitle: null, + messages: [], + awaitingClarification: false, + historyRefreshTrigger: 0, +}; + +const chatSlice = createSlice({ + name: 'chat', + initialState, + reducers: { + setConversationId(state, action: PayloadAction) { + state.conversationId = action.payload; + }, + setConversationTitle(state, action: PayloadAction) { + state.conversationTitle = action.payload; + }, + setMessages(state, action: PayloadAction) { + state.messages = action.payload; + }, + addMessage(state, action: PayloadAction) { + state.messages.push(action.payload); + }, + setAwaitingClarification(state, action: PayloadAction) { + state.awaitingClarification = action.payload; + }, + incrementHistoryRefresh(state) { + state.historyRefreshTrigger += 1; + }, + /** Reset chat to a fresh conversation. Optionally provide a new ID. */ + resetChat(state, action: PayloadAction) { + state.conversationId = action.payload ?? uuidv4(); + state.conversationTitle = null; + state.messages = []; + state.awaitingClarification = false; + }, + }, +}); + +export const { + setConversationId, + setConversationTitle, + setMessages, + addMessage, + setAwaitingClarification, + incrementHistoryRefresh, + resetChat, +} = chatSlice.actions; +export default chatSlice.reducer; diff --git a/content-gen/src/app/frontend/src/store/contentSlice.ts b/content-gen/src/app/frontend/src/store/contentSlice.ts new file mode 100644 index 000000000..15736efd5 --- /dev/null +++ b/content-gen/src/app/frontend/src/store/contentSlice.ts @@ -0,0 +1,61 @@ +/** + * Content slice — creative brief, product selection, generated content. + * Typed createSlice with granular selectors. + */ +import { createSlice, type PayloadAction } from '@reduxjs/toolkit'; +import type { CreativeBrief, Product, GeneratedContent } from '../types'; + +interface ContentState { + pendingBrief: CreativeBrief | null; + confirmedBrief: CreativeBrief | null; + selectedProducts: Product[]; + availableProducts: Product[]; + generatedContent: GeneratedContent | null; +} + +const initialState: ContentState = { + pendingBrief: null, + confirmedBrief: null, + selectedProducts: [], + availableProducts: [], + generatedContent: null, +}; + +const contentSlice = createSlice({ + name: 'content', + initialState, + reducers: { + setPendingBrief(state, action: PayloadAction) { + state.pendingBrief = action.payload; + }, + setConfirmedBrief(state, action: PayloadAction) { + state.confirmedBrief = action.payload; + }, + setSelectedProducts(state, action: PayloadAction) { + state.selectedProducts = action.payload; + }, + setAvailableProducts(state, action: PayloadAction) { + state.availableProducts = action.payload; + }, + setGeneratedContent(state, action: PayloadAction) { + state.generatedContent = action.payload; + }, + resetContent(state) { + state.pendingBrief = null; + state.confirmedBrief = null; + state.selectedProducts = []; + state.availableProducts = []; + state.generatedContent = null; + }, + }, +}); + +export const { + setPendingBrief, + setConfirmedBrief, + setSelectedProducts, + setAvailableProducts, + setGeneratedContent, + resetContent, +} = contentSlice.actions; +export default contentSlice.reducer; diff --git a/content-gen/src/app/frontend/src/store/hooks.ts b/content-gen/src/app/frontend/src/store/hooks.ts new file mode 100644 index 000000000..c9c663095 --- /dev/null +++ b/content-gen/src/app/frontend/src/store/hooks.ts @@ -0,0 +1,9 @@ +/** + * Typed Redux hooks for type-safe store access throughout the app. + * Use useAppDispatch and useAppSelector instead of raw useDispatch/useSelector. + */ +import { useDispatch, useSelector } from 'react-redux'; +import type { RootState, AppDispatch } from './store'; + +export const useAppDispatch = useDispatch.withTypes(); +export const useAppSelector = useSelector.withTypes(); diff --git a/content-gen/src/app/frontend/src/store/index.ts b/content-gen/src/app/frontend/src/store/index.ts new file mode 100644 index 000000000..a0b395487 --- /dev/null +++ b/content-gen/src/app/frontend/src/store/index.ts @@ -0,0 +1,77 @@ +/** + * Barrel export for the Redux store. + * Import everything you need from '../store'. + */ +export { store } from './store'; +export type { RootState, AppDispatch } from './store'; +export { useAppDispatch, useAppSelector } from './hooks'; + +// App slice – actions & thunks +export { + fetchAppConfig, + fetchUserInfo, + setIsLoading, + setGenerationStatus, + toggleChatHistory, + setShowChatHistory, +} from './appSlice'; + +// Chat slice – actions +export { + setConversationId, + setConversationTitle, + setMessages, + addMessage, + setAwaitingClarification, + incrementHistoryRefresh, + resetChat, +} from './chatSlice'; + +// Content slice – actions +export { + setPendingBrief, + setConfirmedBrief, + setSelectedProducts, + setAvailableProducts, + setGeneratedContent, + resetContent, +} from './contentSlice'; + +// Chat History slice – actions & thunks +export { + fetchConversations, + deleteConversation, + renameConversation, + clearAllConversations, + setShowAll, + setConversations, + upsertConversation, + setIsClearAllDialogOpen, +} from './chatHistorySlice'; +export type { ConversationSummary } from './chatHistorySlice'; + +// All selectors (centralized to avoid circular store ↔ slice imports) +export { + selectUserId, + selectUserName, + selectIsLoading, + selectGenerationStatus, + selectImageGenerationEnabled, + selectShowChatHistory, + selectConversationId, + selectConversationTitle, + selectMessages, + selectAwaitingClarification, + selectHistoryRefreshTrigger, + selectPendingBrief, + selectConfirmedBrief, + selectSelectedProducts, + selectAvailableProducts, + selectGeneratedContent, + selectConversations, + selectIsHistoryLoading, + selectHistoryError, + selectShowAll, + selectIsClearAllDialogOpen, + selectIsClearing, +} from './selectors'; diff --git a/content-gen/src/app/frontend/src/store/selectors.ts b/content-gen/src/app/frontend/src/store/selectors.ts new file mode 100644 index 000000000..8fb2329c4 --- /dev/null +++ b/content-gen/src/app/frontend/src/store/selectors.ts @@ -0,0 +1,36 @@ +/** + * All Redux selectors in one place. + * Importing RootState here (and ONLY here) avoids the circular dependency + * between store.ts ↔ slice files that confuses VS Code's TypeScript server. + */ +import type { RootState } from './store'; + +/* ---- App selectors ---- */ +export const selectUserId = (state: RootState) => state.app.userId; +export const selectUserName = (state: RootState) => state.app.userName; +export const selectIsLoading = (state: RootState) => state.app.isLoading; +export const selectGenerationStatus = (state: RootState) => state.app.generationStatus; +export const selectImageGenerationEnabled = (state: RootState) => state.app.imageGenerationEnabled; +export const selectShowChatHistory = (state: RootState) => state.app.showChatHistory; + +/* ---- Chat selectors ---- */ +export const selectConversationId = (state: RootState) => state.chat.conversationId; +export const selectConversationTitle = (state: RootState) => state.chat.conversationTitle; +export const selectMessages = (state: RootState) => state.chat.messages; +export const selectAwaitingClarification = (state: RootState) => state.chat.awaitingClarification; +export const selectHistoryRefreshTrigger = (state: RootState) => state.chat.historyRefreshTrigger; + +/* ---- Content selectors ---- */ +export const selectPendingBrief = (state: RootState) => state.content.pendingBrief; +export const selectConfirmedBrief = (state: RootState) => state.content.confirmedBrief; +export const selectSelectedProducts = (state: RootState) => state.content.selectedProducts; +export const selectAvailableProducts = (state: RootState) => state.content.availableProducts; +export const selectGeneratedContent = (state: RootState) => state.content.generatedContent; + +/* ---- Chat History selectors ---- */ +export const selectConversations = (state: RootState) => state.chatHistory.conversations; +export const selectIsHistoryLoading = (state: RootState) => state.chatHistory.isLoading; +export const selectHistoryError = (state: RootState) => state.chatHistory.error; +export const selectShowAll = (state: RootState) => state.chatHistory.showAll; +export const selectIsClearAllDialogOpen = (state: RootState) => state.chatHistory.isClearAllDialogOpen; +export const selectIsClearing = (state: RootState) => state.chatHistory.isClearing; diff --git a/content-gen/src/app/frontend/src/store/store.ts b/content-gen/src/app/frontend/src/store/store.ts new file mode 100644 index 000000000..81e515742 --- /dev/null +++ b/content-gen/src/app/frontend/src/store/store.ts @@ -0,0 +1,21 @@ +/** + * Redux store — central state for the application. + * configureStore combines all domain-specific slices. + */ +import { configureStore } from '@reduxjs/toolkit'; +import appReducer from './appSlice'; +import chatReducer from './chatSlice'; +import contentReducer from './contentSlice'; +import chatHistoryReducer from './chatHistorySlice'; + +export const store = configureStore({ + reducer: { + app: appReducer, + chat: chatReducer, + content: contentReducer, + chatHistory: chatHistoryReducer, + }, +}); + +export type RootState = ReturnType; +export type AppDispatch = typeof store.dispatch; diff --git a/content-gen/src/app/frontend/src/utils/contentErrors.ts b/content-gen/src/app/frontend/src/utils/contentErrors.ts new file mode 100644 index 000000000..dc9ca497a --- /dev/null +++ b/content-gen/src/app/frontend/src/utils/contentErrors.ts @@ -0,0 +1,31 @@ +/** + * Detect whether an error message originates from a content-safety filter. + */ +export function isContentFilterError(errorMessage?: string): boolean { + if (!errorMessage) return false; + const filterPatterns = [ + 'content_filter', 'ContentFilter', 'content management policy', + 'ResponsibleAI', 'responsible_ai_policy', 'content filtering', + 'filtered', 'safety system', 'self_harm', 'sexual', 'violence', 'hate', + ]; + return filterPatterns.some((pattern) => + errorMessage.toLowerCase().includes(pattern.toLowerCase()), + ); +} + +/** + * Return a user-friendly title/description for a generation error. + */ +export function getErrorMessage(errorMessage?: string): { title: string; description: string } { + if (isContentFilterError(errorMessage)) { + return { + title: 'Content Filtered', + description: + 'Your request was blocked by content safety filters. Please try modifying your creative brief.', + }; + } + return { + title: 'Generation Failed', + description: errorMessage || 'An error occurred. Please try again.', + }; +} diff --git a/content-gen/src/app/frontend/src/utils/downloadImage.ts b/content-gen/src/app/frontend/src/utils/downloadImage.ts new file mode 100644 index 000000000..08e752c20 --- /dev/null +++ b/content-gen/src/app/frontend/src/utils/downloadImage.ts @@ -0,0 +1,94 @@ +/** + * Download the generated marketing image with a product-name / tagline + * banner composited at the bottom. + * + * Falls back to a plain download when canvas compositing fails. + */ +export async function downloadImage( + imageUrl: string, + productName?: string, + tagline?: string, +): Promise { + try { + const canvas = document.createElement('canvas'); + const ctx = canvas.getContext('2d'); + if (!ctx) return; + + const img = new Image(); + img.crossOrigin = 'anonymous'; + + img.onload = () => { + const bannerHeight = Math.max(60, img.height * 0.1); + const padding = Math.max(16, img.width * 0.03); + + canvas.width = img.width; + canvas.height = img.height + bannerHeight; + + // Draw the image at the top + ctx.drawImage(img, 0, 0); + + // White banner at the bottom + ctx.fillStyle = '#ffffff'; + ctx.fillRect(0, img.height, img.width, bannerHeight); + + ctx.strokeStyle = '#e5e5e5'; + ctx.lineWidth = 1; + ctx.beginPath(); + ctx.moveTo(0, img.height); + ctx.lineTo(img.width, img.height); + ctx.stroke(); + + // Headline text + const headlineText = productName || 'Your Product'; + const headlineFontSize = Math.max(18, Math.min(36, img.width * 0.04)); + const taglineFontSize = Math.max(12, Math.min(20, img.width * 0.025)); + + ctx.font = `600 ${headlineFontSize}px Georgia, serif`; + ctx.fillStyle = '#1a1a1a'; + ctx.fillText( + headlineText, + padding, + img.height + padding + headlineFontSize * 0.8, + img.width - padding * 2, + ); + + // Tagline + if (tagline) { + ctx.font = `400 italic ${taglineFontSize}px -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif`; + ctx.fillStyle = '#666666'; + ctx.fillText( + tagline, + padding, + img.height + padding + headlineFontSize + taglineFontSize * 0.8 + 4, + img.width - padding * 2, + ); + } + + canvas.toBlob((blob) => { + if (blob) { + const url = URL.createObjectURL(blob); + const link = document.createElement('a'); + link.href = url; + link.download = 'generated-marketing-image.png'; + link.click(); + URL.revokeObjectURL(url); + } + }, 'image/png'); + }; + + img.onerror = () => { + plainDownload(imageUrl); + }; + + img.src = imageUrl; + } catch { + plainDownload(imageUrl); + } +} + +function plainDownload(url: string) { + const link = document.createElement('a'); + link.href = url; + link.download = 'generated-image.png'; + link.click(); +} From 653846de0a30b27a1091f218b76a9b2fcf31b98d Mon Sep 17 00:00:00 2001 From: Shubhangi-Microsoft Date: Thu, 26 Feb 2026 14:45:59 +0530 Subject: [PATCH 02/72] =?UTF-8?q?refactor(frontend):=20Task=206=20?= =?UTF-8?q?=E2=80=94=20Modularize=20utility=20functions=20by=20domain?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit New utility modules in utils/: - messageUtils.ts: createMessage() factory, formatContentForClipboard() - contentParsing.ts: parseTextContent(), resolveImageUrl(), buildGeneratedContent() - sseParser.ts: parseSSEStream() — eliminates duplicated SSE decode loop - generationStages.ts: getGenerationStage() — pure progress-stage mapper - briefFields.ts: BRIEF_FIELD_LABELS, BRIEF_DISPLAY_ORDER, BRIEF_FIELD_KEYS - stringUtils.ts: escapeRegex(), createNameSwapper(), matchesAnyKeyword() - apiUtils.ts: retryRequest (exponential backoff), RequestCache, throttle - index.ts: barrel export for all utils Deduplicated code: - msg() helper was identical in 2 hooks → createMessage() in messageUtils - SSE stream parser was identical in 2 API functions → parseSSEStream() - GeneratedContent parsing was near-identical in 3 hooks → buildGeneratedContent() - Brief field constants duplicated in 2 components → shared briefFields - Keyword matching pattern repeated 3x → matchesAnyKeyword() Internal helpers marked non-exported: - rewriteBlobUrl() in contentParsing.ts - defaultShouldRetry(), sleep() in apiUtils.ts - plainDownload() in downloadImage.ts (already was) Build: 0 TypeScript errors --- .coverage | Bin 0 -> 53248 bytes .../src/app/frontend/package-lock.json | 120 ++++++++++- content-gen/src/app/frontend/package.json | 2 + content-gen/src/app/frontend/src/api/index.ts | 79 +------ .../frontend/src/components/BriefReview.tsx | 38 +--- .../src/components/ConfirmedBriefView.tsx | 15 +- .../frontend/src/hooks/useChatOrchestrator.ts | 105 +++------ .../src/hooks/useContentGeneration.ts | 81 +------ .../src/hooks/useConversationActions.ts | 74 +------ .../src/app/frontend/src/utils/apiUtils.ts | 202 ++++++++++++++++++ .../src/app/frontend/src/utils/briefFields.ts | 46 ++++ .../app/frontend/src/utils/contentParsing.ts | 108 ++++++++++ .../frontend/src/utils/generationStages.ts | 33 +++ .../src/app/frontend/src/utils/index.ts | 34 +++ .../app/frontend/src/utils/messageUtils.ts | 44 ++++ .../src/app/frontend/src/utils/sseParser.ts | 48 +++++ .../src/app/frontend/src/utils/stringUtils.ts | 44 ++++ 17 files changed, 741 insertions(+), 332 deletions(-) create mode 100644 .coverage create mode 100644 content-gen/src/app/frontend/src/utils/apiUtils.ts create mode 100644 content-gen/src/app/frontend/src/utils/briefFields.ts create mode 100644 content-gen/src/app/frontend/src/utils/contentParsing.ts create mode 100644 content-gen/src/app/frontend/src/utils/generationStages.ts create mode 100644 content-gen/src/app/frontend/src/utils/index.ts create mode 100644 content-gen/src/app/frontend/src/utils/messageUtils.ts create mode 100644 content-gen/src/app/frontend/src/utils/sseParser.ts create mode 100644 content-gen/src/app/frontend/src/utils/stringUtils.ts diff --git a/.coverage b/.coverage new file mode 100644 index 0000000000000000000000000000000000000000..51fea3fcb04caf1151acc6e59af73d9b154294f3 GIT binary patch literal 53248 zcmeI)%WoS+90%~-y7k76Bd3ZYE98*5AjhiX$|@4C0|bafMI|T%#0ht7kL?BTuCtFk z4hXqals|&Mu@VPPT=@NFcD;5ISG`qQzN^^T$IR^fKJ&7>ZNGc^#Eq301)(Qn@yOb? zY}@)&2+OiI=(R`B&|O z%J-YD)2?*r0yYRh00IywjKKV9&1vlI+0TECWq+#TP!3dR-nTyg_T>2VM4TRf_T)sE zbKRMvFndTJW*NdBvM0Ov*;YHTAZ+>^YdQM9a64X zjj4!P=z21|62GXcwoZ`VEmoJYIYEUeckFWtahPUriBOGHsQiJ7Of3%GVXNBu{kN*q z*x9jPDjkyy2lTVK5)GZmLwP)+f(Ai2;}G2muis2YItjaZAQyB!ds4p%6i5DJu^AedoAhYnQzINCQmbusB(UgvE~{1 z)|6S-*-yMak^h`{Gj1HpVQw4;>vFEk8wY8yZ`?Mwv}GReN^@r9+@dB|;=5;wS~PxJ zY!wHNm{oW{X{{qjtA z6z1vm-8fq8R8~68;`ExX!`IjM8y+RAorFixbSi?89x|L;n8u;rygX(eW*9Q$fhK}9 zA=UJ)_BO68)j{;9L4UQc=^hg)8XzOw4{&H5jP}Y*SOrzN)Psu0=vx($&lVp}7 zpCz*~l3t;3j+}Dicz<0uX}VrF-PxEscb&%Wu01y+g6AcE-CGHQ&*Vek+CuQ=)t||D zB5u~5PgBL4_sWfrch^;%tI+H6d-=uo9bM%=OIqKfNuOV(l`8X@yvkpxtD|!{rP*XA z-H?VX&B0~`5S(x*DY5JaNv!)XAL{POYfoNZQ$T*4*;A^PPEN!7n;?o@T2^`4iayHD zisnQ{JPSol(GP;DlD=6RD97W7?&(HC*Wu=>^hsVubj4+i&zrhV+2X>}{3#8(Y<$s< z=ShEo6iphG5e?C6WoCt@S9!+D%J0m`giH10&pR@U zy)M7=w`BhIY|RgBr}5x{Jx|gRY$m5(M4tpanZ=qO-dX8c*DmIr>}#5(=_Ar|=VI_B zLq0Tj;2F_0d*9GysnQQf)fRujx9k5}^n(oo5P$##AOHafKmY;|fB*y_0Dd?6EIgkAr~ zq91G!fB*y_009U<00Izz00bZa0SFXHpjz6mq<;#a|AGH@k%B`k5P$##AOHafKmY;| zfB*y_009VG7vSgr^q>A=g8&2|009U<00Izz00bZa0SG{#zyf&wU*N_?Xb^w^1Rwwb z2tWV=5P$##AOL|>0MGx?6(9fs2tWV=5P$##AOHafKmY>87r^uX;= 8" } }, + "node_modules/@reduxjs/toolkit": { + "version": "2.11.2", + "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.11.2.tgz", + "integrity": "sha512-Kd6kAHTA6/nUpp8mySPqj3en3dm0tdMIgbttnQ1xFMVpufoj+ADi8pXLBsd4xzTRHQa7t/Jv8W5UnCuW4kuWMQ==", + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.0.0", + "@standard-schema/utils": "^0.3.0", + "immer": "^11.0.0", + "redux": "^5.0.1", + "redux-thunk": "^3.1.0", + "reselect": "^5.1.0" + }, + "peerDependencies": { + "react": "^16.9.0 || ^17.0.0 || ^18 || ^19", + "react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + }, + "react-redux": { + "optional": true + } + } + }, "node_modules/@rolldown/pluginutils": { "version": "1.0.0-beta.27", "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", @@ -2961,6 +2991,18 @@ "win32" ] }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "license": "MIT" + }, + "node_modules/@standard-schema/utils": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@standard-schema/utils/-/utils-0.3.0.tgz", + "integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==", + "license": "MIT" + }, "node_modules/@swc/helpers": { "version": "0.5.17", "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.17.tgz", @@ -3069,6 +3111,7 @@ "integrity": "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "undici-types": "~7.16.0" } @@ -3084,6 +3127,7 @@ "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.27.tgz", "integrity": "sha512-cisd7gxkzjBKU2GgdYrTdtQx1SORymWyaAFhaxQPK9bYO9ot3Y5OikQRvY0VYQtvwjeQnizCINJAenh/V7MK2w==", "license": "MIT", + "peer": true, "dependencies": { "@types/prop-types": "*", "csstype": "^3.2.2" @@ -3094,6 +3138,7 @@ "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz", "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", "license": "MIT", + "peer": true, "peerDependencies": { "@types/react": "^18.0.0" } @@ -3104,6 +3149,12 @@ "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", "license": "MIT" }, + "node_modules/@types/use-sync-external-store": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz", + "integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==", + "license": "MIT" + }, "node_modules/@types/uuid": { "version": "10.0.0", "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-10.0.0.tgz", @@ -3151,6 +3202,7 @@ "integrity": "sha512-4Z+L8I2OqhZV8qA132M4wNL30ypZGYOQVBfMgxDH/K5UX0PNqTu1c6za9ST5r9+tavvHiTWmBnKzpCJ/GlVFtg==", "dev": true, "license": "BSD-2-Clause", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "7.18.0", "@typescript-eslint/types": "7.18.0", @@ -3337,6 +3389,7 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -3484,6 +3537,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.8.25", "caniuse-lite": "^1.0.30001754", @@ -3757,7 +3811,8 @@ "version": "8.6.0", "resolved": "https://registry.npmjs.org/embla-carousel/-/embla-carousel-8.6.0.tgz", "integrity": "sha512-SjWyZBHJPbqxHOzckOfo8lHisEaJWmwd23XppYFYVh10bU66/Pn5tkVkbkCMZVdbUE5eTCI2nD8OyIP4Z+uwkA==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/embla-carousel-autoplay": { "version": "8.6.0", @@ -3849,6 +3904,7 @@ "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", @@ -4387,6 +4443,16 @@ "node": ">= 4" } }, + "node_modules/immer": { + "version": "11.1.4", + "resolved": "https://registry.npmjs.org/immer/-/immer-11.1.4.tgz", + "integrity": "sha512-XREFCPo6ksxVzP4E0ekD5aMdf8WMwmdNaz6vuvxgI40UaEiu6q3p8X52aU6GdyvLY3XXX/8R7JOTXStz/nBbRw==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } + }, "node_modules/import-fresh": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", @@ -5613,6 +5679,7 @@ "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", "license": "MIT", + "peer": true, "dependencies": { "loose-envify": "^1.1.0" }, @@ -5625,6 +5692,7 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", "license": "MIT", + "peer": true, "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.2" @@ -5675,6 +5743,30 @@ "react": ">=18" } }, + "node_modules/react-redux": { + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz", + "integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==", + "license": "MIT", + "peer": true, + "dependencies": { + "@types/use-sync-external-store": "^0.0.6", + "use-sync-external-store": "^1.4.0" + }, + "peerDependencies": { + "@types/react": "^18.2.25 || ^19", + "react": "^18.0 || ^19", + "redux": "^5.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "redux": { + "optional": true + } + } + }, "node_modules/react-refresh": { "version": "0.17.0", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", @@ -5685,6 +5777,22 @@ "node": ">=0.10.0" } }, + "node_modules/redux": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", + "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==", + "license": "MIT", + "peer": true + }, + "node_modules/redux-thunk": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-3.1.0.tgz", + "integrity": "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==", + "license": "MIT", + "peerDependencies": { + "redux": "^5.0.0" + } + }, "node_modules/remark-parse": { "version": "11.0.0", "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-11.0.0.tgz", @@ -5718,6 +5826,12 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/reselect": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/reselect/-/reselect-5.1.1.tgz", + "integrity": "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==", + "license": "MIT" + }, "node_modules/resolve-from": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", @@ -6056,6 +6170,7 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -6147,6 +6262,7 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -6346,6 +6462,7 @@ "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", @@ -6439,6 +6556,7 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, diff --git a/content-gen/src/app/frontend/package.json b/content-gen/src/app/frontend/package.json index 2479885d7..d7b11ff63 100644 --- a/content-gen/src/app/frontend/package.json +++ b/content-gen/src/app/frontend/package.json @@ -12,9 +12,11 @@ "dependencies": { "@fluentui/react-components": "^9.54.0", "@fluentui/react-icons": "^2.0.245", + "@reduxjs/toolkit": "^2.11.2", "react": "^18.3.1", "react-dom": "^18.3.1", "react-markdown": "^9.0.1", + "react-redux": "^9.2.0", "uuid": "^10.0.0" }, "devDependencies": { diff --git a/content-gen/src/app/frontend/src/api/index.ts b/content-gen/src/app/frontend/src/api/index.ts index a39c86318..ee4bba2e6 100644 --- a/content-gen/src/app/frontend/src/api/index.ts +++ b/content-gen/src/app/frontend/src/api/index.ts @@ -10,6 +10,8 @@ import type { AppConfig, } from '../types'; import httpClient from './httpClient'; +import { parseSSEStream } from '../utils/sseParser'; +import { getGenerationStage } from '../utils/generationStages'; /** * Get application configuration including feature flags @@ -96,31 +98,7 @@ export async function* streamChat( throw new Error('No response body'); } - const decoder = new TextDecoder(); - let buffer = ''; - - while (true) { - const { done, value } = await reader.read(); - if (done) break; - - buffer += decoder.decode(value, { stream: true }); - const lines = buffer.split('\n\n'); - buffer = lines.pop() || ''; - - for (const line of lines) { - if (line.startsWith('data: ')) { - const data = line.slice(6); - if (data === '[DONE]') { - return; - } - try { - yield JSON.parse(data) as AgentResponse; - } catch { - console.error('Failed to parse SSE data:', data); - } - } - } - } + yield* parseSSEStream(reader); } /** @@ -189,31 +167,8 @@ export async function* streamGenerateContent( } else if (statusData.status === 'failed') { throw new Error(statusData.error || 'Generation failed'); } else if (statusData.status === 'running') { - // Determine progress stage based on elapsed time - // Typical generation: 0-10s briefing, 10-25s copy, 25-45s image, 45-60s compliance const elapsedSeconds = attempts; - let stage: number; - let stageMessage: string; - - if (elapsedSeconds < 10) { - stage = 0; - stageMessage = 'Analyzing creative brief...'; - } else if (elapsedSeconds < 25) { - stage = 1; - stageMessage = 'Generating marketing copy...'; - } else if (elapsedSeconds < 35) { - stage = 2; - stageMessage = 'Creating image prompt...'; - } else if (elapsedSeconds < 55) { - stage = 3; - stageMessage = 'Generating image with AI...'; - } else if (elapsedSeconds < 70) { - stage = 4; - stageMessage = 'Running compliance check...'; - } else { - stage = 5; - stageMessage = 'Finalizing content...'; - } + const { stage, message: stageMessage } = getGenerationStage(elapsedSeconds); // Send status update every second for smoother progress yield { @@ -271,29 +226,5 @@ export async function* streamRegenerateImage( throw new Error('No response body'); } - const decoder = new TextDecoder(); - let buffer = ''; - - while (true) { - const { done, value } = await reader.read(); - if (done) break; - - buffer += decoder.decode(value, { stream: true }); - const lines = buffer.split('\n\n'); - buffer = lines.pop() || ''; - - for (const line of lines) { - if (line.startsWith('data: ')) { - const data = line.slice(6); - if (data === '[DONE]') { - return; - } - try { - yield JSON.parse(data) as AgentResponse; - } catch { - console.error('Failed to parse SSE data:', data); - } - } - } - } + yield* parseSSEStream(reader); } \ No newline at end of file diff --git a/content-gen/src/app/frontend/src/components/BriefReview.tsx b/content-gen/src/app/frontend/src/components/BriefReview.tsx index 88fbae5c7..627db0df9 100644 --- a/content-gen/src/app/frontend/src/components/BriefReview.tsx +++ b/content-gen/src/app/frontend/src/components/BriefReview.tsx @@ -5,6 +5,7 @@ import { tokens, } from '@fluentui/react-components'; import type { CreativeBrief } from '../types'; +import { BRIEF_FIELD_LABELS, BRIEF_DISPLAY_ORDER, BRIEF_FIELD_KEYS } from '../utils'; interface BriefReviewProps { brief: CreativeBrief; @@ -13,19 +14,6 @@ interface BriefReviewProps { isAwaitingResponse?: boolean; } -// Mapping of field keys to user-friendly labels for the 9 key areas -const fieldLabels: Record = { - overview: 'Overview', - objectives: 'Objectives', - target_audience: 'Target Audience', - key_message: 'Key Message', - tone_and_style: 'Tone and Style', - deliverable: 'Deliverable', - timelines: 'Timelines', - visual_guidelines: 'Visual Guidelines', - cta: 'Call to Action', -}; - export const BriefReview = memo(function BriefReview({ brief, onConfirm, @@ -33,29 +21,13 @@ export const BriefReview = memo(function BriefReview({ isAwaitingResponse = false, }: BriefReviewProps) { const { populatedFields, missingFields, populatedDisplayFields } = useMemo(() => { - const allFields: (keyof CreativeBrief)[] = [ - 'overview', 'objectives', 'target_audience', 'key_message', - 'tone_and_style', 'deliverable', 'timelines', 'visual_guidelines', 'cta' - ]; - const populated = allFields.filter(key => brief[key]?.trim()).length; - const missing = allFields.filter(key => !brief[key]?.trim()); - - const displayOrder: { key: keyof CreativeBrief; label: string }[] = [ - { key: 'overview', label: 'Campaign Objective' }, - { key: 'objectives', label: 'Objectives' }, - { key: 'target_audience', label: 'Target Audience' }, - { key: 'key_message', label: 'Key Message' }, - { key: 'tone_and_style', label: 'Tone & Style' }, - { key: 'visual_guidelines', label: 'Visual Guidelines' }, - { key: 'deliverable', label: 'Deliverables' }, - { key: 'timelines', label: 'Timelines' }, - { key: 'cta', label: 'Call to Action' }, - ]; + const populated = BRIEF_FIELD_KEYS.filter(key => brief[key]?.trim()).length; + const missing = BRIEF_FIELD_KEYS.filter(key => !brief[key]?.trim()); return { populatedFields: populated, missingFields: missing, - populatedDisplayFields: displayOrder.filter(({ key }) => brief[key]?.trim()), + populatedDisplayFields: BRIEF_DISPLAY_ORDER.filter(({ key }) => brief[key]?.trim()), }; }, [brief]); @@ -121,7 +93,7 @@ export const BriefReview = memo(function BriefReview({ {populatedFields < 5 ? ( <> I've captured {populatedFields} of 9 key areas. Would you like to add more details? - You are missing: {missingFields.map(f => fieldLabels[f]).join(', ')}. + You are missing: {missingFields.map(f => BRIEF_FIELD_LABELS[f]).join(', ')}.

You can tell me things like:
    diff --git a/content-gen/src/app/frontend/src/components/ConfirmedBriefView.tsx b/content-gen/src/app/frontend/src/components/ConfirmedBriefView.tsx index f0806979b..e6697151a 100644 --- a/content-gen/src/app/frontend/src/components/ConfirmedBriefView.tsx +++ b/content-gen/src/app/frontend/src/components/ConfirmedBriefView.tsx @@ -8,23 +8,12 @@ import { Checkmark20Regular, } from '@fluentui/react-icons'; import type { CreativeBrief } from '../types'; +import { BRIEF_DISPLAY_ORDER } from '../utils'; interface ConfirmedBriefViewProps { brief: CreativeBrief; } -const briefFields: { key: keyof CreativeBrief; label: string }[] = [ - { key: 'overview', label: 'Overview' }, - { key: 'objectives', label: 'Objectives' }, - { key: 'target_audience', label: 'Target Audience' }, - { key: 'key_message', label: 'Key Message' }, - { key: 'tone_and_style', label: 'Tone & Style' }, - { key: 'deliverable', label: 'Deliverable' }, - { key: 'timelines', label: 'Timelines' }, - { key: 'visual_guidelines', label: 'Visual Guidelines' }, - { key: 'cta', label: 'Call to Action' }, -]; - export const ConfirmedBriefView = memo(function ConfirmedBriefView({ brief }: ConfirmedBriefViewProps) { return (
    - {briefFields.map(({ key, label }) => { + {BRIEF_DISPLAY_ORDER.map(({ key, label }) => { const value = brief[key]; if (!value?.trim()) return null; diff --git a/content-gen/src/app/frontend/src/hooks/useChatOrchestrator.ts b/content-gen/src/app/frontend/src/hooks/useChatOrchestrator.ts index 0c4313587..d88fe834f 100644 --- a/content-gen/src/app/frontend/src/hooks/useChatOrchestrator.ts +++ b/content-gen/src/app/frontend/src/hooks/useChatOrchestrator.ts @@ -1,7 +1,7 @@ import { useCallback, type MutableRefObject } from 'react'; -import { v4 as uuidv4 } from 'uuid'; -import type { ChatMessage, GeneratedContent } from '../types'; +import type { GeneratedContent } from '../types'; +import { createMessage, matchesAnyKeyword, createNameSwapper } from '../utils'; import { useAppDispatch, useAppSelector, @@ -26,23 +26,6 @@ import { setConversationTitle, } from '../store'; -/* ------------------------------------------------------------------ */ -/* Helper: create a ChatMessage literal */ -/* ------------------------------------------------------------------ */ -function msg( - role: 'user' | 'assistant', - content: string, - agent?: string, -): ChatMessage { - return { - id: uuidv4(), - role, - content, - agent, - timestamp: new Date().toISOString(), - }; -} - /* ------------------------------------------------------------------ */ /* Hook */ /* ------------------------------------------------------------------ */ @@ -81,7 +64,7 @@ export function useChatOrchestrator( const sendMessage = useCallback( async (content: string) => { - dispatch(addMessage(msg('user', content))); + dispatch(addMessage(createMessage('user', content))); dispatch(setIsLoading(true)); // Create new abort controller for this request @@ -101,10 +84,8 @@ export function useChatOrchestrator( const refinementKeywords = [ 'change', 'update', 'modify', 'add', 'remove', 'delete', 'set', 'make', 'should be', - ]; - const isRefinement = refinementKeywords.some((kw) => - content.toLowerCase().includes(kw), - ); + ] as const; + const isRefinement = matchesAnyKeyword(content, refinementKeywords); if (isRefinement || awaitingClarification) { // --- 1-a Refine the brief -------------------------------- @@ -131,7 +112,7 @@ export function useChatOrchestrator( dispatch(setGenerationStatus('')); dispatch( addMessage( - msg('assistant', parsed.clarifying_questions, 'PlanningAgent'), + createMessage('assistant', parsed.clarifying_questions, 'PlanningAgent'), ), ); } else { @@ -139,7 +120,7 @@ export function useChatOrchestrator( dispatch(setGenerationStatus('')); dispatch( addMessage( - msg( + createMessage( 'assistant', "I've updated the brief based on your feedback. Please review the changes above. Let me know if you'd like any other modifications, or click **Confirm Brief** when you're satisfied.", 'PlanningAgent', @@ -168,14 +149,14 @@ export function useChatOrchestrator( !messageAdded ) { dispatch( - addMessage(msg('assistant', fullContent, currentAgent)), + addMessage(createMessage('assistant', fullContent, currentAgent)), ); messageAdded = true; } } else if (response.type === 'error') { dispatch( addMessage( - msg( + createMessage( 'assistant', response.content || 'An error occurred while processing your request.', @@ -204,7 +185,7 @@ export function useChatOrchestrator( dispatch(setGenerationStatus('')); dispatch( addMessage( - msg( + createMessage( 'assistant', result.message || 'Products updated.', 'ProductAgent', @@ -222,10 +203,8 @@ export function useChatOrchestrator( 'kitchen', 'dining', 'living', 'bedroom', 'bathroom', 'outdoor', 'office', 'room', 'scene', 'setting', 'background', 'style', 'color', 'lighting', - ]; - const isImageModification = imageModificationKeywords.some((kw) => - content.toLowerCase().includes(kw), - ); + ] as const; + const isImageModification = matchesAnyKeyword(content, imageModificationKeywords); if (isImageModification) { // --- 3-a Regenerate image -------------------------------- @@ -275,28 +254,10 @@ export function useChatOrchestrator( parsedContent.image_base64 ) { // Replace old product name in text_content when switching - const oldName = selectedProducts[0]?.product_name; - const newName = mentionedProduct?.product_name; - const nameRegex = oldName - ? new RegExp( - oldName.replace( - /[.*+?^${}()|[\]\\]/g, - '\\$&', - ), - 'gi', - ) - : undefined; - const swapName = (s?: string) => { - if ( - !s || - !oldName || - !newName || - oldName === newName || - !nameRegex - ) - return s; - return s.replace(nameRegex, () => newName); - }; + const swapName = createNameSwapper( + selectedProducts[0]?.product_name, + mentionedProduct?.product_name, + ); const tc = generatedContent.text_content; responseData = { @@ -304,10 +265,10 @@ export function useChatOrchestrator( text_content: mentionedProduct ? { ...tc, - headline: swapName(tc?.headline), - body: swapName(tc?.body), - tagline: swapName(tc?.tagline), - cta_text: swapName(tc?.cta_text), + headline: swapName?.(tc?.headline) ?? tc?.headline, + body: swapName?.(tc?.body) ?? tc?.body, + tagline: swapName?.(tc?.tagline) ?? tc?.tagline, + cta_text: swapName?.(tc?.cta_text) ?? tc?.cta_text, } : tc, image_content: { @@ -356,7 +317,7 @@ export function useChatOrchestrator( dispatch(setGenerationStatus('')); dispatch( - addMessage(msg('assistant', messageContent, 'ImageAgent')), + addMessage(createMessage('assistant', messageContent, 'ImageAgent')), ); } else { // --- 3-b General question after content generation -------- @@ -379,14 +340,14 @@ export function useChatOrchestrator( !messageAdded ) { dispatch( - addMessage(msg('assistant', fullContent, currentAgent)), + addMessage(createMessage('assistant', fullContent, currentAgent)), ); messageAdded = true; } } else if (response.type === 'error') { dispatch( addMessage( - msg( + createMessage( 'assistant', response.content || 'An error occurred.', ), @@ -405,10 +366,8 @@ export function useChatOrchestrator( const briefKeywords = [ 'campaign', 'marketing', 'target audience', 'objective', 'deliverable', - ]; - const isBriefLike = briefKeywords.some((kw) => - content.toLowerCase().includes(kw), - ); + ] as const; + const isBriefLike = matchesAnyKeyword(content, briefKeywords); if (isBriefLike && !confirmedBrief) { // --- 4-a Parse as creative brief -------------------------- @@ -428,7 +387,7 @@ export function useChatOrchestrator( dispatch(setGenerationStatus('')); dispatch( addMessage( - msg('assistant', parsed.message, 'ContentSafety'), + createMessage('assistant', parsed.message, 'ContentSafety'), ), ); } else if ( @@ -442,7 +401,7 @@ export function useChatOrchestrator( dispatch(setGenerationStatus('')); dispatch( addMessage( - msg( + createMessage( 'assistant', parsed.clarifying_questions, 'PlanningAgent', @@ -457,7 +416,7 @@ export function useChatOrchestrator( dispatch(setGenerationStatus('')); dispatch( addMessage( - msg( + createMessage( 'assistant', "I've parsed your creative brief. Please review the details below and let me know if you'd like to make any changes. You can say things like \"change the target audience to...\" or \"add a call to action...\". When everything looks good, click **Confirm Brief** to proceed.", 'PlanningAgent', @@ -486,14 +445,14 @@ export function useChatOrchestrator( !messageAdded ) { dispatch( - addMessage(msg('assistant', fullContent, currentAgent)), + addMessage(createMessage('assistant', fullContent, currentAgent)), ); messageAdded = true; } } else if (response.type === 'error') { dispatch( addMessage( - msg( + createMessage( 'assistant', response.content || 'An error occurred while processing your request.', @@ -509,12 +468,12 @@ export function useChatOrchestrator( } catch (error) { if (error instanceof Error && error.name === 'AbortError') { console.debug('Request cancelled by user'); - dispatch(addMessage(msg('assistant', 'Generation stopped.'))); + dispatch(addMessage(createMessage('assistant', 'Generation stopped.'))); } else { console.error('Error sending message:', error); dispatch( addMessage( - msg( + createMessage( 'assistant', 'Sorry, there was an error processing your request. Please try again.', ), diff --git a/content-gen/src/app/frontend/src/hooks/useContentGeneration.ts b/content-gen/src/app/frontend/src/hooks/useContentGeneration.ts index 53513c2d8..6b01c54b4 100644 --- a/content-gen/src/app/frontend/src/hooks/useContentGeneration.ts +++ b/content-gen/src/app/frontend/src/hooks/useContentGeneration.ts @@ -1,7 +1,6 @@ import { useCallback, type MutableRefObject } from 'react'; -import { v4 as uuidv4 } from 'uuid'; -import type { ChatMessage, GeneratedContent } from '../types'; +import { createMessage, buildGeneratedContent } from '../utils'; import { useAppDispatch, useAppSelector, @@ -64,52 +63,7 @@ export function useContentGeneration( dispatch(setGenerationStatus('Processing results...')); try { const rawContent = JSON.parse(response.content); - - // Parse text_content if it's a string (from orchestrator) - let textContent = rawContent.text_content; - if (typeof textContent === 'string') { - try { - textContent = JSON.parse(textContent); - } catch { - // Keep as string if not valid JSON - } - } - - // Build image_url: prefer blob URL, fallback to base64 data URL - let imageUrl: string | undefined; - if (rawContent.image_url) { - imageUrl = rawContent.image_url; - } else if (rawContent.image_base64) { - imageUrl = `data:image/png;base64,${rawContent.image_base64}`; - } - - const genContent: GeneratedContent = { - text_content: - typeof textContent === 'object' - ? { - headline: textContent?.headline, - body: textContent?.body, - cta_text: textContent?.cta, - tagline: textContent?.tagline, - } - : undefined, - image_content: - imageUrl || rawContent.image_prompt - ? { - image_url: imageUrl, - prompt_used: rawContent.image_prompt, - alt_text: - rawContent.image_revised_prompt || - 'Generated marketing image', - } - : undefined, - violations: rawContent.violations || [], - requires_modification: - rawContent.requires_modification || false, - error: rawContent.error, - image_error: rawContent.image_error, - text_error: rawContent.text_error, - }; + const genContent = buildGeneratedContent(rawContent); dispatch(setGeneratedContent(genContent)); dispatch(setGenerationStatus('')); } catch (parseError) { @@ -117,35 +71,22 @@ export function useContentGeneration( } } else if (response.type === 'error') { dispatch(setGenerationStatus('')); - const errorMessage: ChatMessage = { - id: uuidv4(), - role: 'assistant', - content: `Error generating content: ${response.content}`, - timestamp: new Date().toISOString(), - }; - dispatch(addMessage(errorMessage)); + dispatch(addMessage(createMessage( + 'assistant', + `Error generating content: ${response.content}`, + ))); } } } catch (error) { if (error instanceof Error && error.name === 'AbortError') { console.debug('Content generation cancelled by user'); - const cancelMessage: ChatMessage = { - id: uuidv4(), - role: 'assistant', - content: 'Content generation stopped.', - timestamp: new Date().toISOString(), - }; - dispatch(addMessage(cancelMessage)); + dispatch(addMessage(createMessage('assistant', 'Content generation stopped.'))); } else { console.error('Error generating content:', error); - const errorMessage: ChatMessage = { - id: uuidv4(), - role: 'assistant', - content: - 'Sorry, there was an error generating content. Please try again.', - timestamp: new Date().toISOString(), - }; - dispatch(addMessage(errorMessage)); + dispatch(addMessage(createMessage( + 'assistant', + 'Sorry, there was an error generating content. Please try again.', + ))); } } finally { dispatch(setIsLoading(false)); diff --git a/content-gen/src/app/frontend/src/hooks/useConversationActions.ts b/content-gen/src/app/frontend/src/hooks/useConversationActions.ts index 76ab731ec..97b3b9f90 100644 --- a/content-gen/src/app/frontend/src/hooks/useConversationActions.ts +++ b/content-gen/src/app/frontend/src/hooks/useConversationActions.ts @@ -1,7 +1,7 @@ import { useCallback } from 'react'; -import { v4 as uuidv4 } from 'uuid'; -import type { ChatMessage, Product, CreativeBrief, GeneratedContent } from '../types'; +import type { ChatMessage, Product, CreativeBrief } from '../types'; +import { createMessage, buildGeneratedContent } from '../utils'; import httpClient from '../api/httpClient'; import { useAppDispatch, @@ -25,23 +25,6 @@ import { toggleChatHistory, } from '../store'; -/* ------------------------------------------------------------------ */ -/* Helper: create a ChatMessage literal */ -/* ------------------------------------------------------------------ */ -function msg( - role: 'user' | 'assistant', - content: string, - agent?: string, -): ChatMessage { - return { - id: uuidv4(), - role, - content, - agent, - timestamp: new Date().toISOString(), - }; -} - /* ------------------------------------------------------------------ */ /* Hook */ /* ------------------------------------------------------------------ */ @@ -124,52 +107,7 @@ export function useConversationActions() { if (data.generated_content) { // eslint-disable-next-line @typescript-eslint/no-explicit-any const gc = data.generated_content as any; - let textContent = gc.text_content; - if (typeof textContent === 'string') { - try { - textContent = JSON.parse(textContent); - } catch { - // keep as-is - } - } - - let imageUrl: string | undefined = gc.image_url; - if (imageUrl && imageUrl.includes('blob.core.windows.net')) { - const parts = imageUrl.split('/'); - const filename = parts[parts.length - 1]; - const convId = parts[parts.length - 2]; - imageUrl = `/api/images/${convId}/${filename}`; - } - if (!imageUrl && gc.image_base64) { - imageUrl = `data:image/png;base64,${gc.image_base64}`; - } - - const restoredContent: GeneratedContent = { - text_content: - typeof textContent === 'object' && textContent - ? { - headline: textContent?.headline, - body: textContent?.body, - cta_text: textContent?.cta, - tagline: textContent?.tagline, - } - : undefined, - image_content: - imageUrl || gc.image_prompt - ? { - image_url: imageUrl, - prompt_used: gc.image_prompt, - alt_text: - gc.image_revised_prompt || - 'Generated marketing image', - } - : undefined, - violations: gc.violations || [], - requires_modification: gc.requires_modification || false, - error: gc.error, - image_error: gc.image_error, - text_error: gc.text_error, - }; + const restoredContent = buildGeneratedContent(gc, true); dispatch(setGeneratedContent(restoredContent)); if ( @@ -219,7 +157,7 @@ export function useConversationActions() { dispatch( addMessage( - msg( + createMessage( 'assistant', "Great! Your creative brief has been confirmed. Here are the available products for your campaign. Select the ones you'd like to feature, or tell me what you're looking for.", 'ProductAgent', @@ -236,7 +174,7 @@ export function useConversationActions() { dispatch(setAwaitingClarification(false)); dispatch( addMessage( - msg( + createMessage( 'assistant', 'No problem. Please provide your creative brief again or ask me any questions.', ), @@ -252,7 +190,7 @@ export function useConversationActions() { dispatch(setConfirmedBrief(null)); dispatch( addMessage( - msg( + createMessage( 'assistant', 'Starting over. Please provide your creative brief to begin a new campaign.', ), diff --git a/content-gen/src/app/frontend/src/utils/apiUtils.ts b/content-gen/src/app/frontend/src/utils/apiUtils.ts new file mode 100644 index 000000000..1f60ff1c9 --- /dev/null +++ b/content-gen/src/app/frontend/src/utils/apiUtils.ts @@ -0,0 +1,202 @@ +/** + * Production-grade API utilities — retry with exponential backoff, + * request deduplication cache, and throttle. + * + * These harden the HTTP layer for unreliable networks and prevent + * accidental duplicate requests (e.g. double-clicks, React strict mode). + */ + +/* ------------------------------------------------------------------ */ +/* retryRequest — exponential backoff wrapper */ +/* ------------------------------------------------------------------ */ + +export interface RetryOptions { + /** Maximum number of attempts (including the first). Default: 3. */ + maxAttempts?: number; + /** Initial delay in ms before the first retry. Default: 1 000. */ + initialDelayMs?: number; + /** Maximum delay cap in ms. Default: 30 000. */ + maxDelayMs?: number; + /** Multiplier applied to the delay after each failure. Default: 2. */ + backoffFactor?: number; + /** Optional predicate — return `true` if the request should be retried. */ + shouldRetry?: (error: unknown, attempt: number) => boolean; + /** Optional `AbortSignal` to cancel outstanding retries. */ + signal?: AbortSignal; +} + +/** + * Execute `fn` with automatic retries and exponential backoff. + * + * ```ts + * const data = await retryRequest(() => httpClient.get('/health'), { + * maxAttempts: 4, + * initialDelayMs: 500, + * }); + * ``` + */ +export async function retryRequest( + fn: () => Promise, + opts: RetryOptions = {}, +): Promise { + const { + maxAttempts = 3, + initialDelayMs = 1_000, + maxDelayMs = 30_000, + backoffFactor = 2, + shouldRetry = defaultShouldRetry, + signal, + } = opts; + + let delay = initialDelayMs; + + for (let attempt = 1; attempt <= maxAttempts; attempt++) { + try { + return await fn(); + } catch (error) { + if (attempt >= maxAttempts || !shouldRetry(error, attempt)) throw error; + if (signal?.aborted) throw new DOMException('Retry aborted', 'AbortError'); + + await sleep(delay, signal); + delay = Math.min(delay * backoffFactor, maxDelayMs); + } + } + + // Unreachable — the loop always returns or throws. + throw new Error('retryRequest: exhausted all attempts'); +} + +/* ------------------------------------------------------------------ */ +/* RequestCache — deduplication / in-flight coalescing */ +/* ------------------------------------------------------------------ */ + +/** + * Simple in-memory cache that de-duplicates concurrent identical requests. + * + * If a request with the same `key` is already in flight, all callers + * share the same promise. Once settled the entry is automatically + * evicted (or kept for `ttlMs` when configured). + * + * ```ts + * const cache = new RequestCache(); + * const data = await cache.dedupe('config', () => httpClient.get('/config')); + * ``` + */ +export class RequestCache { + private inflight = new Map>(); + private store = new Map(); + private ttlMs: number; + + constructor(ttlMs = 0) { + this.ttlMs = ttlMs; + } + + async dedupe(key: string, fn: () => Promise): Promise { + // Return from TTL cache if still fresh + const cached = this.store.get(key); + if (cached && cached.expiresAt > Date.now()) { + return cached.value as T; + } + + // De-duplicate in-flight requests + const existing = this.inflight.get(key); + if (existing) return existing as Promise; + + const promise = fn().finally(() => { + this.inflight.delete(key); + }); + + this.inflight.set(key, promise); + + if (this.ttlMs > 0) { + promise.then((value) => { + this.store.set(key, { value, expiresAt: Date.now() + this.ttlMs }); + }).catch(() => { /* don't cache failures */ }); + } + + return promise as Promise; + } + + /** Manually evict a cache entry. */ + invalidate(key: string): void { + this.store.delete(key); + this.inflight.delete(key); + } + + /** Evict all cache entries. */ + clear(): void { + this.store.clear(); + this.inflight.clear(); + } +} + +/* ------------------------------------------------------------------ */ +/* throttle — limits invocation frequency */ +/* ------------------------------------------------------------------ */ + +/** + * Classic trailing-edge throttle. + * + * Ensures `fn` is called at most once every `limitMs` milliseconds. + * The first invocation fires immediately; subsequent calls within the + * window are silently dropped and the **last** one fires when the + * window closes. + */ +export function throttle void>( + fn: T, + limitMs: number, +): (...args: Parameters) => void { + let timer: ReturnType | null = null; + let lastArgs: Parameters | null = null; + let lastCallTime = 0; + + return (...args: Parameters) => { + const now = Date.now(); + const remaining = limitMs - (now - lastCallTime); + + if (remaining <= 0) { + // Window has passed — fire immediately + lastCallTime = now; + fn(...args); + } else { + // Inside the window — schedule a trailing call + lastArgs = args; + if (!timer) { + timer = setTimeout(() => { + lastCallTime = Date.now(); + timer = null; + if (lastArgs) { + fn(...lastArgs); + lastArgs = null; + } + }, remaining); + } + } + }; +} + +/* ------------------------------------------------------------------ */ +/* Internal helpers (not exported) */ +/* ------------------------------------------------------------------ */ + +/** Default retry predicate — retry on network errors & 5xx, not on 4xx. */ +function defaultShouldRetry(error: unknown, _attempt: number): boolean { + if (error instanceof DOMException && error.name === 'AbortError') return false; + if (error instanceof Response) return error.status >= 500; + return true; // network errors, timeouts, etc. +} + +/** Abort-aware sleep. */ +function sleep(ms: number, signal?: AbortSignal): Promise { + return new Promise((resolve, reject) => { + if (signal?.aborted) { + reject(new DOMException('Sleep aborted', 'AbortError')); + return; + } + const timer = setTimeout(resolve, ms); + signal?.addEventListener('abort', () => { + clearTimeout(timer); + reject(new DOMException('Sleep aborted', 'AbortError')); + }, { once: true }); + }); +} diff --git a/content-gen/src/app/frontend/src/utils/briefFields.ts b/content-gen/src/app/frontend/src/utils/briefFields.ts new file mode 100644 index 000000000..beb44a88e --- /dev/null +++ b/content-gen/src/app/frontend/src/utils/briefFields.ts @@ -0,0 +1,46 @@ +/** + * Brief-field metadata shared between BriefReview and ConfirmedBriefView. + * + * Eliminates the duplicated field-label arrays. + */ +import type { CreativeBrief } from '../types'; + +/** + * Canonical map from `CreativeBrief` keys to user-friendly labels. + * Used by BriefReview (completeness gauges) and ConfirmedBriefView. + */ +export const BRIEF_FIELD_LABELS: Record = { + overview: 'Overview', + objectives: 'Objectives', + target_audience: 'Target Audience', + key_message: 'Key Message', + tone_and_style: 'Tone & Style', + deliverable: 'Deliverable', + timelines: 'Timelines', + visual_guidelines: 'Visual Guidelines', + cta: 'Call to Action', +}; + +/** + * Display order for brief fields in review UIs. + * + * The first element in each tuple is the `CreativeBrief` key, the second + * is the UI label (which may differ slightly from `BRIEF_FIELD_LABELS` + * for contextual reasons, e.g. "Campaign Objective" vs "Overview"). + */ +export const BRIEF_DISPLAY_ORDER: { key: keyof CreativeBrief; label: string }[] = [ + { key: 'overview', label: 'Campaign Objective' }, + { key: 'objectives', label: 'Objectives' }, + { key: 'target_audience', label: 'Target Audience' }, + { key: 'key_message', label: 'Key Message' }, + { key: 'tone_and_style', label: 'Tone & Style' }, + { key: 'visual_guidelines', label: 'Visual Guidelines' }, + { key: 'deliverable', label: 'Deliverables' }, + { key: 'timelines', label: 'Timelines' }, + { key: 'cta', label: 'Call to Action' }, +]; + +/** + * The canonical list of all nine brief field keys, in display order. + */ +export const BRIEF_FIELD_KEYS: (keyof CreativeBrief)[] = BRIEF_DISPLAY_ORDER.map((f) => f.key); diff --git a/content-gen/src/app/frontend/src/utils/contentParsing.ts b/content-gen/src/app/frontend/src/utils/contentParsing.ts new file mode 100644 index 000000000..629f887da --- /dev/null +++ b/content-gen/src/app/frontend/src/utils/contentParsing.ts @@ -0,0 +1,108 @@ +/** + * Content parsing utilities — raw API response → typed domain objects. + * + * Centralizes the duplicated `textContent` string-to-object parsing, + * image URL resolution (blob rewriting, base64 fallback), and the + * `GeneratedContent` assembly that was copy-pasted across + * useContentGeneration, useConversationActions, and useChatOrchestrator. + */ +import type { GeneratedContent } from '../types'; + +/* ------------------------------------------------------------------ */ +/* Internal helpers (not exported — reduces public API surface) */ +/* ------------------------------------------------------------------ */ + +/** + * Rewrite Azure Blob Storage URLs to the application's proxy endpoint + * so the browser can fetch images without CORS issues. + */ +function rewriteBlobUrl(url: string): string { + if (!url.includes('blob.core.windows.net')) return url; + const parts = url.split('/'); + const filename = parts[parts.length - 1]; + const convId = parts[parts.length - 2]; + return `/api/images/${convId}/${filename}`; +} + +/* ------------------------------------------------------------------ */ +/* Exported utilities */ +/* ------------------------------------------------------------------ */ + +/** + * Parse `text_content` which may arrive as a JSON string or an object. + * Returns an object with known fields, or `undefined` if unusable. + */ +export function parseTextContent( + raw: unknown, +): { headline?: string; body?: string; cta_text?: string; tagline?: string } | undefined { + let textContent = raw; + + if (typeof textContent === 'string') { + try { + textContent = JSON.parse(textContent); + } catch { + // Not valid JSON — treat as unusable + return undefined; + } + } + + if (typeof textContent !== 'object' || textContent === null) return undefined; + + const tc = textContent as Record; + return { + headline: tc.headline as string | undefined, + body: tc.body as string | undefined, + cta_text: (tc.cta_text ?? tc.cta) as string | undefined, + tagline: tc.tagline as string | undefined, + }; +} + +/** + * Resolve the best available image URL from a raw API response. + * + * Priority: explicit `image_url` (with blob rewrite) → base64 data URI. + * Pass `rewriteBlobs: true` (default) when restoring from a saved + * conversation; `false` when the response just came from the live API. + */ +export function resolveImageUrl( + raw: { image_url?: string; image_base64?: string }, + rewriteBlobs = false, +): string | undefined { + let url = raw.image_url; + if (url && rewriteBlobs) { + url = rewriteBlobUrl(url); + } + if (url) return url; + if (raw.image_base64) return `data:image/png;base64,${raw.image_base64}`; + return undefined; +} + +/** + * Build a fully-typed `GeneratedContent` from an arbitrary raw API payload. + * + * @param raw The parsed JSON object from the backend. + * @param rewriteBlobs Pass `true` when restoring from a saved conversation + * so Azure Blob URLs get proxied. + */ +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export function buildGeneratedContent(raw: any, rewriteBlobs = false): GeneratedContent { + const textContent = parseTextContent(raw.text_content); + const imageUrl = resolveImageUrl(raw, rewriteBlobs); + + return { + text_content: textContent, + image_content: + imageUrl || raw.image_prompt + ? { + image_url: imageUrl, + prompt_used: raw.image_prompt, + alt_text: raw.image_revised_prompt || 'Generated marketing image', + } + : undefined, + violations: raw.violations || [], + requires_modification: raw.requires_modification || false, + error: raw.error, + image_error: raw.image_error, + text_error: raw.text_error, + }; +} diff --git a/content-gen/src/app/frontend/src/utils/generationStages.ts b/content-gen/src/app/frontend/src/utils/generationStages.ts new file mode 100644 index 000000000..03399bc0d --- /dev/null +++ b/content-gen/src/app/frontend/src/utils/generationStages.ts @@ -0,0 +1,33 @@ +/** + * Generation progress stage mapping. + * + * Pure function that converts elapsed seconds into a human-readable + * stage label + ordinal — used by the polling loop in `streamGenerateContent`. + */ + +export interface GenerationStage { + /** Ordinal stage index (0–5) for progress indicators. */ + stage: number; + /** Human-readable status message. */ + message: string; +} + +/** + * Map elapsed seconds to the current generation stage. + * + * Typical generation timeline: + * - 0 – 10 s → Briefing analysis + * - 10 – 25 s → Copy generation + * - 25 – 35 s → Image prompt creation + * - 35 – 55 s → Image generation + * - 55 – 70 s → Compliance check + * - 70 s+ → Finalizing + */ +export function getGenerationStage(elapsedSeconds: number): GenerationStage { + if (elapsedSeconds < 10) return { stage: 0, message: 'Analyzing creative brief...' }; + if (elapsedSeconds < 25) return { stage: 1, message: 'Generating marketing copy...' }; + if (elapsedSeconds < 35) return { stage: 2, message: 'Creating image prompt...' }; + if (elapsedSeconds < 55) return { stage: 3, message: 'Generating image with AI...' }; + if (elapsedSeconds < 70) return { stage: 4, message: 'Running compliance check...' }; + return { stage: 5, message: 'Finalizing content...' }; +} diff --git a/content-gen/src/app/frontend/src/utils/index.ts b/content-gen/src/app/frontend/src/utils/index.ts new file mode 100644 index 000000000..a3240ff6a --- /dev/null +++ b/content-gen/src/app/frontend/src/utils/index.ts @@ -0,0 +1,34 @@ +/** + * Barrel export for all utility modules. + * + * Import everything you need from '../utils'. + */ + +// Message factories & formatting +export { createMessage, formatContentForClipboard } from './messageUtils'; + +// Content parsing (raw API → typed domain objects) +export { parseTextContent, resolveImageUrl, buildGeneratedContent } from './contentParsing'; + +// SSE stream parser +export { parseSSEStream } from './sseParser'; + +// Generation progress stages +export { getGenerationStage } from './generationStages'; +export type { GenerationStage } from './generationStages'; + +// Brief-field metadata +export { BRIEF_FIELD_LABELS, BRIEF_DISPLAY_ORDER, BRIEF_FIELD_KEYS } from './briefFields'; + +// String utilities +export { escapeRegex, createNameSwapper, matchesAnyKeyword } from './stringUtils'; + +// Production API utilities +export { retryRequest, RequestCache, throttle } from './apiUtils'; +export type { RetryOptions } from './apiUtils'; + +// Content error detection +export { isContentFilterError, getErrorMessage } from './contentErrors'; + +// Image download +export { downloadImage } from './downloadImage'; diff --git a/content-gen/src/app/frontend/src/utils/messageUtils.ts b/content-gen/src/app/frontend/src/utils/messageUtils.ts new file mode 100644 index 000000000..5d7530b1f --- /dev/null +++ b/content-gen/src/app/frontend/src/utils/messageUtils.ts @@ -0,0 +1,44 @@ +/** + * Message utilities — ChatMessage factory and formatting helpers. + * + * Replaces duplicated `msg()` helpers in useChatOrchestrator and + * useConversationActions with a single, tested source of truth. + */ +import { v4 as uuidv4 } from 'uuid'; +import type { ChatMessage } from '../types'; + +/** + * Create a `ChatMessage` literal with a fresh UUID and ISO timestamp. + */ +export function createMessage( + role: 'user' | 'assistant', + content: string, + agent?: string, +): ChatMessage { + return { + id: uuidv4(), + role, + content, + agent, + timestamp: new Date().toISOString(), + }; +} + +/** + * Assemble a copyable plain-text string from generated text content. + * + * Used by `InlineContentPreview` to copy headline + body + tagline + * to clipboard. + */ +export function formatContentForClipboard( + textContent?: { headline?: string; body?: string; tagline?: string }, +): string { + if (!textContent) return ''; + return [ + textContent.headline && `✨ ${textContent.headline} ✨`, + textContent.body, + textContent.tagline, + ] + .filter(Boolean) + .join('\n\n'); +} diff --git a/content-gen/src/app/frontend/src/utils/sseParser.ts b/content-gen/src/app/frontend/src/utils/sseParser.ts new file mode 100644 index 000000000..7b11201f0 --- /dev/null +++ b/content-gen/src/app/frontend/src/utils/sseParser.ts @@ -0,0 +1,48 @@ +/** + * SSE (Server-Sent Events) stream parser. + * + * Eliminates the duplicated TextDecoder + buffer + line-split logic + * that was copy-pasted in `streamChat` and `streamRegenerateImage`. + */ +import type { AgentResponse } from '../types'; + +/** + * Parse an SSE stream from a `ReadableStreamDefaultReader` into an + * `AsyncGenerator` of `AgentResponse` objects. + * + * Protocol assumed: + * - Events delimited by `\n\n` + * - Each event starts with `data: ` + * - `data: [DONE]` terminates the stream + * + * @param reader The reader obtained via `response.body.getReader()` + */ +export async function* parseSSEStream( + reader: ReadableStreamDefaultReader, +): AsyncGenerator { + const decoder = new TextDecoder(); + let buffer = ''; + + while (true) { + const { done, value } = await reader.read(); + if (done) break; + + buffer += decoder.decode(value, { stream: true }); + const lines = buffer.split('\n\n'); + buffer = lines.pop() || ''; + + for (const line of lines) { + if (line.startsWith('data: ')) { + const data = line.slice(6); + if (data === '[DONE]') { + return; + } + try { + yield JSON.parse(data) as AgentResponse; + } catch { + console.error('Failed to parse SSE data:', data); + } + } + } + } +} diff --git a/content-gen/src/app/frontend/src/utils/stringUtils.ts b/content-gen/src/app/frontend/src/utils/stringUtils.ts new file mode 100644 index 000000000..16c88eed3 --- /dev/null +++ b/content-gen/src/app/frontend/src/utils/stringUtils.ts @@ -0,0 +1,44 @@ +/** + * String utilities — regex escaping, name swapping, keyword matching. + * + * Extracts the duplicated keyword-matching pattern and the regex-escape + + * swapName closure from useChatOrchestrator into reusable, testable functions. + */ + +/** + * Escape a string so it can be safely embedded in a `RegExp` pattern. + */ +export function escapeRegex(str: string): string { + return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); +} + +/** + * Create a function that replaces all case-insensitive occurrences of + * `oldName` with `newName` in a string. + * + * Returns `undefined` if no swap is possible (names are the same, etc.). + */ +export function createNameSwapper( + oldName: string | undefined, + newName: string | undefined, +): ((text?: string) => string | undefined) | undefined { + if (!oldName || !newName || oldName === newName) return undefined; + + const regex = new RegExp(escapeRegex(oldName), 'gi'); + return (text?: string) => { + if (!text) return text; + return text.replace(regex, () => newName); + }; +} + +/** + * Check whether `text` contains **any** of the given keywords + * (case-insensitive substring match). + * + * Used for intent classification (brief detection, refinement detection, + * image modification detection) repeated 3× in useChatOrchestrator. + */ +export function matchesAnyKeyword(text: string, keywords: readonly string[]): boolean { + const lower = text.toLowerCase(); + return keywords.some((kw) => lower.includes(kw)); +} From 8007ba08a580fa42d8cad5c81db4c5b67bdd725b Mon Sep 17 00:00:00 2001 From: Shubhangi-Microsoft Date: Mon, 2 Mar 2026 14:05:20 +0530 Subject: [PATCH 03/72] refactor(frontend): remove dead code, deduplicate error handling, tighten utility exports --- content-gen/src/app/frontend/src/api/index.ts | 2 -- .../frontend/src/components/ChatHistory.tsx | 12 +++++------ .../frontend/src/hooks/useChatOrchestrator.ts | 7 ++---- .../src/hooks/useContentGeneration.ts | 14 +++++------- .../src/hooks/useConversationActions.ts | 15 ++++++------- .../frontend/src/hooks/useCopyToClipboard.ts | 4 ++-- .../frontend/src/store/chatHistorySlice.ts | 13 +----------- .../src/app/frontend/src/store/index.ts | 2 -- .../src/styles/images/SamplePrompt.png | Bin 4938 -> 0 bytes .../src/app/frontend/src/utils/index.ts | 4 ++-- .../app/frontend/src/utils/messageUtils.ts | 20 +++++------------- .../src/app/frontend/src/utils/stringUtils.ts | 3 ++- 12 files changed, 31 insertions(+), 65 deletions(-) delete mode 100644 content-gen/src/app/frontend/src/styles/images/SamplePrompt.png diff --git a/content-gen/src/app/frontend/src/api/index.ts b/content-gen/src/app/frontend/src/api/index.ts index ee4bba2e6..37031126b 100644 --- a/content-gen/src/app/frontend/src/api/index.ts +++ b/content-gen/src/app/frontend/src/api/index.ts @@ -122,8 +122,6 @@ export async function* streamGenerateContent( }, { signal }); const taskId = startData.task_id; - console.debug(`Generation started with task ID: ${taskId}`); - // Yield initial status yield { type: 'status', diff --git a/content-gen/src/app/frontend/src/components/ChatHistory.tsx b/content-gen/src/app/frontend/src/components/ChatHistory.tsx index 37dd7b705..8faa1f24e 100644 --- a/content-gen/src/app/frontend/src/components/ChatHistory.tsx +++ b/content-gen/src/app/frontend/src/components/ChatHistory.tsx @@ -75,8 +75,8 @@ export const ChatHistory = memo(function ChatHistory({ try { await dispatch(clearAllConversations()).unwrap(); onNewConversation(); - } catch (err) { - console.error('Error clearing all conversations:', err); + } catch { + // Error clearing all conversations } }, [dispatch, onNewConversation]); @@ -86,16 +86,16 @@ export const ChatHistory = memo(function ChatHistory({ if (conversationId === currentConversationId) { onNewConversation(); } - } catch (err) { - console.error('Error deleting conversation:', err); + } catch { + // Error deleting conversation } }, [dispatch, currentConversationId, onNewConversation]); const handleRenameConversation = useCallback(async (conversationId: string, newTitle: string) => { try { await dispatch(renameConversation({ conversationId, newTitle })).unwrap(); - } catch (err) { - console.error('Error renaming conversation:', err); + } catch { + // Error renaming conversation } }, [dispatch]); diff --git a/content-gen/src/app/frontend/src/hooks/useChatOrchestrator.ts b/content-gen/src/app/frontend/src/hooks/useChatOrchestrator.ts index d88fe834f..9980e978a 100644 --- a/content-gen/src/app/frontend/src/hooks/useChatOrchestrator.ts +++ b/content-gen/src/app/frontend/src/hooks/useChatOrchestrator.ts @@ -1,7 +1,7 @@ import { useCallback, type MutableRefObject } from 'react'; import type { GeneratedContent } from '../types'; -import { createMessage, matchesAnyKeyword, createNameSwapper } from '../utils'; +import { createMessage, createErrorMessage, matchesAnyKeyword, createNameSwapper } from '../utils'; import { useAppDispatch, useAppSelector, @@ -467,14 +467,11 @@ export function useChatOrchestrator( } } catch (error) { if (error instanceof Error && error.name === 'AbortError') { - console.debug('Request cancelled by user'); dispatch(addMessage(createMessage('assistant', 'Generation stopped.'))); } else { - console.error('Error sending message:', error); dispatch( addMessage( - createMessage( - 'assistant', + createErrorMessage( 'Sorry, there was an error processing your request. Please try again.', ), ), diff --git a/content-gen/src/app/frontend/src/hooks/useContentGeneration.ts b/content-gen/src/app/frontend/src/hooks/useContentGeneration.ts index 6b01c54b4..423d8a561 100644 --- a/content-gen/src/app/frontend/src/hooks/useContentGeneration.ts +++ b/content-gen/src/app/frontend/src/hooks/useContentGeneration.ts @@ -1,6 +1,6 @@ import { useCallback, type MutableRefObject } from 'react'; -import { createMessage, buildGeneratedContent } from '../utils'; +import { createMessage, createErrorMessage, buildGeneratedContent } from '../utils'; import { useAppDispatch, useAppSelector, @@ -66,25 +66,21 @@ export function useContentGeneration( const genContent = buildGeneratedContent(rawContent); dispatch(setGeneratedContent(genContent)); dispatch(setGenerationStatus('')); - } catch (parseError) { - console.error('Error parsing generated content:', parseError); + } catch { + // Content parse failure — non-critical, generation result may be malformed } } else if (response.type === 'error') { dispatch(setGenerationStatus('')); - dispatch(addMessage(createMessage( - 'assistant', + dispatch(addMessage(createErrorMessage( `Error generating content: ${response.content}`, ))); } } } catch (error) { if (error instanceof Error && error.name === 'AbortError') { - console.debug('Content generation cancelled by user'); dispatch(addMessage(createMessage('assistant', 'Content generation stopped.'))); } else { - console.error('Error generating content:', error); - dispatch(addMessage(createMessage( - 'assistant', + dispatch(addMessage(createErrorMessage( 'Sorry, there was an error generating content. Please try again.', ))); } diff --git a/content-gen/src/app/frontend/src/hooks/useConversationActions.ts b/content-gen/src/app/frontend/src/hooks/useConversationActions.ts index 97b3b9f90..a20629a4a 100644 --- a/content-gen/src/app/frontend/src/hooks/useConversationActions.ts +++ b/content-gen/src/app/frontend/src/hooks/useConversationActions.ts @@ -96,11 +96,8 @@ export function useConversationActions() { products?: Product[]; }>('/products'); dispatch(setAvailableProducts(productsData.products || [])); - } catch (err) { - console.error( - 'Error loading products for restored conversation:', - err, - ); + } catch { + // Non-critical — product load failure for restored conversation } } @@ -122,8 +119,8 @@ export function useConversationActions() { dispatch(setGeneratedContent(null)); dispatch(setSelectedProducts([])); } - } catch (error) { - console.error('Error loading conversation:', error); + } catch { + // Error loading conversation — swallowed silently } }, [userId, dispatch], @@ -164,8 +161,8 @@ export function useConversationActions() { ), ), ); - } catch (error) { - console.error('Error confirming brief:', error); + } catch { + // Error confirming brief — swallowed silently } }, [conversationId, userId, pendingBrief, dispatch]); diff --git a/content-gen/src/app/frontend/src/hooks/useCopyToClipboard.ts b/content-gen/src/app/frontend/src/hooks/useCopyToClipboard.ts index 08d024d8d..5887c9532 100644 --- a/content-gen/src/app/frontend/src/hooks/useCopyToClipboard.ts +++ b/content-gen/src/app/frontend/src/hooks/useCopyToClipboard.ts @@ -13,8 +13,8 @@ export function useCopyToClipboard(resetTimeout = 2000) { const copy = useCallback( (text: string) => { - navigator.clipboard.writeText(text).catch((err) => { - console.error('Failed to copy text:', err); + navigator.clipboard.writeText(text).catch(() => { + // Clipboard write failure — non-critical }); setCopied(true); clearTimeout(timerRef.current); diff --git a/content-gen/src/app/frontend/src/store/chatHistorySlice.ts b/content-gen/src/app/frontend/src/store/chatHistorySlice.ts index 4f088c3a1..9874d45bc 100644 --- a/content-gen/src/app/frontend/src/store/chatHistorySlice.ts +++ b/content-gen/src/app/frontend/src/store/chatHistorySlice.ts @@ -78,17 +78,6 @@ const chatHistorySlice = createSlice({ setShowAll(state, action: PayloadAction) { state.showAll = action.payload; }, - setConversations(state, action: PayloadAction) { - state.conversations = action.payload; - }, - upsertConversation(state, action: PayloadAction) { - const idx = state.conversations.findIndex((c) => c.id === action.payload.id); - if (idx >= 0) { - state.conversations[idx] = action.payload; - } else { - state.conversations.unshift(action.payload); - } - }, setIsClearAllDialogOpen(state, action: PayloadAction) { state.isClearAllDialogOpen = action.payload; }, @@ -133,6 +122,6 @@ const chatHistorySlice = createSlice({ }, }); -export const { setShowAll, setConversations, upsertConversation, setIsClearAllDialogOpen } = +export const { setShowAll, setIsClearAllDialogOpen } = chatHistorySlice.actions; export default chatHistorySlice.reducer; diff --git a/content-gen/src/app/frontend/src/store/index.ts b/content-gen/src/app/frontend/src/store/index.ts index a0b395487..6e85accf0 100644 --- a/content-gen/src/app/frontend/src/store/index.ts +++ b/content-gen/src/app/frontend/src/store/index.ts @@ -44,8 +44,6 @@ export { renameConversation, clearAllConversations, setShowAll, - setConversations, - upsertConversation, setIsClearAllDialogOpen, } from './chatHistorySlice'; export type { ConversationSummary } from './chatHistorySlice'; diff --git a/content-gen/src/app/frontend/src/styles/images/SamplePrompt.png b/content-gen/src/app/frontend/src/styles/images/SamplePrompt.png deleted file mode 100644 index 9a57c67965c7e119704e1fcbcb0728dc016d34c6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4938 zcmV-Q6SeG#P)uHZn41`pLiGc!F@vyX@oMK_SaIr|La8j@U7T8%)EK;(!Y>+pQ z1r#*d*|N&(Vg#{?*9M2Gj}F(?%12C zrPlxM?(e+M=_{f;?M}PXew>@4F`N0$+lyLNPpc?;)DCBMz7*lUHo=sds4CD)cg)F@ z<+;3Cf41|`rzxQrN2M^`V%SHqI zz?i(R0Nzl(rD)JoChrmr&_M)K_E>P1=p$@<65!1@-+Wjp_2ghMcocoU-|y4b))w&* zV5uDRC}Gx4%qzYQ+rdLcBlg@_+{;{n2qfT{>w`_A{w66tf(SWXd%d2VbL#-&FRL74g!Of1Fj|am+^7++ z+!{A4{s7q*<3~&|0A>kLIGTPJpXF5H`|tkT;}p%_MQNIHH!K0@`ak%K_^5MgW55GcVv>i48gFnrbW~wu_ zu>i%o$OK{0_u}t~00{IAlQjeEF#Ud}346?jfiS1r?KX8f9pixWoW$LUzVh$NkZui5 z3ZBNT{Urb<{WT|MOa$X5s&$b?k{8EBVToY%@Wi>@#$)pLFm%Fg?tIR~;QQYAIng(6 zeq%pX#u0+=JxpV^QC{FaKZzGTuZxZ=vOr!ebB)!bfHCCm4I~p}J7DgfaQ^$SJ8}1% zColSexO-1i6M?K@++PBCZhNDnEDPk|7s#T|9Airj@ilzpdjKxyArbsIj({={6TpuZ zba}={Z|K7Hc(LD3*nKBJsB7eez;$neWx-zT*;VmT^qUJy&;|FV?dB0mH5yk zATVft!G;@DvnlJ2;|2UiC*=+p$!MerXdCY9am zl*XL=rq&qxAjz(jZnuTDM7eM`ju8VWO;HbXu%}6;L>$?8vXR|p499MxY`_kE_br0h zwn(GFP*ovtVUYL!{52|icT<#<@Mo3Wqit@` zeH@JL!Smn99+lJ+}OZ%XGPSZUbiQgBO=3EsLbbRZnGCXRV9O`qbQlnQmkeQ4-s&cfAZjRF~~pP`Qn9h zqcHBM2N<^`=Ly^zg$Rbz;hTeq@_0fDU?WII+wGHp_nD#pCiGx7tdUudEm`pVB#$QP zea@cCA`G~8#2HA3=88m}HQGEwgK{o{IwsK;#To3#WJd8+YcXp_XY)V4_sJ0p9^nRS z$76NP7bUzE-t6_v`leBx&IAg%rE)1V3cTe9Hi9+Tf;=JSL%&#MbAxT5ZeIwKXUMAMT%*aDZKp55xqt>2q_W&TU6%Rhtw)UbeYXLq7@!$F&jrwb9{>3LKx%z)3Q?P@`B4v80Q*yoa<^ZvT5*t7w z4YnYobN$#4C4k`xgyXKHX_^GYWolu4z>BDHI=UknBCD@8_q-VUVn;xf`kI=PI#jOt zGgwy(F9H0!7iF)7HB`orZAGpcfB+kY;`avBGN@2@T2hkJJuqui&%OOfd-23q`^&o% zz|9?2wLBPY#Pk^-Dm$_Cm7}2$ItX&MTgxFUxh=BWe(q^u=L52ZFaM@WZv^1Ak-ZlF zD$0}*0`w6|-9Q0%(5H0LZo2flineYMt@bBj9Qu1m0C^7h(w?P)5+&0fQ5cLgX0s_+ z(x@a#?Vi02*%|k}3ZJQBoff|Ggi3E-Aj458yDJ8dPb@L;CJSRrDb2-Zm;jh2idMCd z65A29)m6)%`qt`dIPpjVC?Tx&%4a-y0c3pkN86*aVLc5v8A?J(92eT|Adm{Gzucm7 zgBJef2})NlLe!O%UzuD&1P((u2CFPFR0|K_kg6@Es!bd*1wSiFCW3ixJKhmCz$soM z2~Ou72w-D>M#QB{tS0}4M&A?4gVTJ<+8hS^FsKvubE7sJ)zXSZ*MbUGu9C#Z# z$T~#|$V!CRthQniF$H$punmDDdoH!CwkDs466{R?k^!M_!g;Uv%~~@gD`)m{LXib`<{+2&+K8`Ib6)y;pTU;qE70RFOwJ#$|G6Im6IH98Mq6H?d$VLU|M zZAi1|CbWzy_WV5N8&3ghFCDYE^xNnEiSFv% zsFlU4rl~oi$$D!B3*cf18NIRRqGxz~X~I+|@BeyriFRo_2%w$KH-2AJPDx45@8Wrs zgCK$+M3OqOF(U3+t3>oxB9BMaI4y9r*HhUhEo6!gWLtC>t`D@=K#X-O9dQ4g|HqN# zZ5xDq{2TuP@T*p*>d@3!j;lrjj7zr#JP1)Sco;l`!Zhw(LX2|q#MgEQ&n+4)0k(=I z0VWG7PmUD~5bAzr(OGf|azE%}oPr3T2U1XovZ0iB&9NEOMz%OJg|KyMl@_LM(t+t4 zv@mfK@kyUL8G@Arsu4qqpgscrTL8bl+9XNNAG3HH=kAQLZqSN+#BmPrJN>sz1ZaT& z>S(dwQvz&6VZ%%7^R)HEI2J`|{@_05Tbur7nRkcngh|YXj(g<{o;QHtXtG5U(YhGD z$=DuaWi7mEW(4?>9(xVoH&=&zgaE1}m~%Ssvr}gWKn)uWx@#CbV{Q*T(Fj-c=KE_B zy1E!o!yVv#F8UBHNdbU;7M2YyV^A zn|&Na8QaBVcsG4uch)(;(TM1(E+le`k8;mwHboU@kaVf4ZXZ0i(Yb(#zIYp(62QEv z6v;^idkWpxP=#)e%#X7_?Dm$}A4Dc-^1(pyJ?#@AU5%VHOo+HYQ2aSX{r`0$-^hjK zsC9GV0=1~3=d_mu7E-VZB3obUb}9lh~ohuI7rjkfyD5x7Kpa&&f}qQDNA zZ5XQxiQeEVX*Cjp3>c-|U&?)J#D#dK|ArUJjX3zs+ptI{oNPo#LKiyW3*8!;6fkEXu~Gi)M_9U z)8tcRY7#?+YU)?(nxfATYtbefqGjFxq&UCgU$zGZjg25Zh{+6KLy*pD`9%$ELBT&I zL@$x&PamU=-+GbE+%X!rjo2Hk*C}y8#1g?QaR!n7E>dooL$>5~reOIUxyd-> z5MlBAe?eZ{<^3`e@TLzh(}#OS{?6{t5&$JO_P+cRqs~#f_4xA?%^ltSCvIaA!0uSs z)l+B?E!fsT(Z#=CrSt#%K5g6@41d=o(d4=6YV5%?qPf|t^hdvc!CKZ5_Z^1~ikTH^ zdZ=MK;I0SYZ~Vqz63rZOewsc60;n*^Z3+~8k4@dBuWhNfzK*aJ8zgEjrNN+}A}_3t z*2ECEX#*oixjJS?J8|qa333SFiN!kSk`4cYPFe6Evi8A`fH%}#c#z&Zb%x@JS-4{3 zc8sz^?XmYAU?RJaGi3*h#h%3r12$K;M;ToMIM0QsZ zyDDw!pirDBMml!)Ti`Yc5g4pNJzoQSxuxY=M88x*0QLg!e)0>n@iU(_*6nh}qdE4k z3B0cakRhEO`{JmQVhd;cimS^2%#{MWq1i%4s9`2rnYGl~fwm=aJD|}`(S-jwe()^` zaSd}9+-GA~^=!y1h7KI(UGeqV2kHBt_(Ll1{%MLUUyqD!9td|3n-4((#AP*`x;05y z4Z2vwPyA9u-~LKbgE4illXh~}l4^Tsix%~8)9Gd5xPTWG00BzlT%Vz}*(F-J|98yh zfhEO8I1=#I6lp|Z8Pf5IcFSa0I<^(qMFN82o?Sn*p;9@*+q(wk7XdPf)|iBZM* z0RTtKFUi)pj%TSm`IBPZW0xuNk8aW=F=-sD4l>S88y%Z3S;{}22A4f1~-Y1MBlP0ms&{Iyh>~ zP-Jln<*!VFbYrF^BdmWaV?75;4AV@&ds7wp=(!1`g%DCd#n_SC3XGh-`}gWPdh-ib zo3b0TN@KPWMld>y$)5DdaN`wx2IR{%nl}pt6mnZ}7^=30uBPTd%U|9E@ik|^#pa@a za@#S=rjR}Xv&C8YrHF18;kN5tTKzJO*>?C5@bm@avRC7i9&!d=cE}>P)fqL<$V#}j zHAJ$HB@Zy3V2Z1s{y$bhjf_ncYypPHQq&Y(mCZ7Z-!$^;FcN>8Ai#cs_L~5|FrA*m zWY1ZE%1-FAz@S*_1R}`zWStEViC2h%ZUJkT{MP@&%!2i1!|w2Sbytz!0`m;<-}CWV z7{GHhX4}<*zS^x$W2Vz$2b@m(gqUiQcx!LSdjQoG5nOTI)CrLL&m@vuMao651hZ#?ljwe00b0jbY2gQj{pDw07*qo IM6N<$f}h89BLDyZ diff --git a/content-gen/src/app/frontend/src/utils/index.ts b/content-gen/src/app/frontend/src/utils/index.ts index a3240ff6a..2541cc3b7 100644 --- a/content-gen/src/app/frontend/src/utils/index.ts +++ b/content-gen/src/app/frontend/src/utils/index.ts @@ -5,7 +5,7 @@ */ // Message factories & formatting -export { createMessage, formatContentForClipboard } from './messageUtils'; +export { createMessage, createErrorMessage } from './messageUtils'; // Content parsing (raw API → typed domain objects) export { parseTextContent, resolveImageUrl, buildGeneratedContent } from './contentParsing'; @@ -21,7 +21,7 @@ export type { GenerationStage } from './generationStages'; export { BRIEF_FIELD_LABELS, BRIEF_DISPLAY_ORDER, BRIEF_FIELD_KEYS } from './briefFields'; // String utilities -export { escapeRegex, createNameSwapper, matchesAnyKeyword } from './stringUtils'; +export { createNameSwapper, matchesAnyKeyword } from './stringUtils'; // Production API utilities export { retryRequest, RequestCache, throttle } from './apiUtils'; diff --git a/content-gen/src/app/frontend/src/utils/messageUtils.ts b/content-gen/src/app/frontend/src/utils/messageUtils.ts index 5d7530b1f..45a7ea5ac 100644 --- a/content-gen/src/app/frontend/src/utils/messageUtils.ts +++ b/content-gen/src/app/frontend/src/utils/messageUtils.ts @@ -25,20 +25,10 @@ export function createMessage( } /** - * Assemble a copyable plain-text string from generated text content. - * - * Used by `InlineContentPreview` to copy headline + body + tagline - * to clipboard. + * Shorthand for creating an assistant error message. + * Consolidates the repeated `createMessage('assistant', errorText)` pattern + * used in error catch blocks across multiple hooks. */ -export function formatContentForClipboard( - textContent?: { headline?: string; body?: string; tagline?: string }, -): string { - if (!textContent) return ''; - return [ - textContent.headline && `✨ ${textContent.headline} ✨`, - textContent.body, - textContent.tagline, - ] - .filter(Boolean) - .join('\n\n'); +export function createErrorMessage(content: string): ChatMessage { + return createMessage('assistant', content); } diff --git a/content-gen/src/app/frontend/src/utils/stringUtils.ts b/content-gen/src/app/frontend/src/utils/stringUtils.ts index 16c88eed3..387e07ff5 100644 --- a/content-gen/src/app/frontend/src/utils/stringUtils.ts +++ b/content-gen/src/app/frontend/src/utils/stringUtils.ts @@ -7,8 +7,9 @@ /** * Escape a string so it can be safely embedded in a `RegExp` pattern. + * @internal — only used by `createNameSwapper` within this module. */ -export function escapeRegex(str: string): string { +function escapeRegex(str: string): string { return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); } From 50bac33eb2d6873a960da9ce43c35582294620c1 Mon Sep 17 00:00:00 2001 From: Shubhangi-Microsoft Date: Mon, 2 Mar 2026 16:59:11 +0530 Subject: [PATCH 04/72] refactor(frontend): dedup streamChat, remove dead code, tighten imports and memoization --- content-gen/src/app/frontend/src/App.tsx | 2 - content-gen/src/app/frontend/src/api/index.ts | 4 +- .../app/frontend/src/components/ChatPanel.tsx | 6 - .../src/components/InlineContentPreview.tsx | 3 +- .../frontend/src/components/ProductReview.tsx | 2 - .../frontend/src/components/ViolationCard.tsx | 8 +- .../frontend/src/hooks/useChatOrchestrator.ts | 154 ++++++------------ .../src/hooks/useConversationActions.ts | 14 -- .../src/app/frontend/src/hooks/useDebounce.ts | 29 ---- .../src/app/frontend/src/utils/index.ts | 4 - .../src/app/frontend/src/utils/sseParser.ts | 2 +- 11 files changed, 55 insertions(+), 173 deletions(-) delete mode 100644 content-gen/src/app/frontend/src/hooks/useDebounce.ts diff --git a/content-gen/src/app/frontend/src/App.tsx b/content-gen/src/app/frontend/src/App.tsx index c0cd14934..4bc73f5fc 100644 --- a/content-gen/src/app/frontend/src/App.tsx +++ b/content-gen/src/app/frontend/src/App.tsx @@ -32,7 +32,6 @@ function App() { newConversation, confirmBrief, cancelBrief, - productsStartOver, selectProduct, toggleHistory, } = useConversationActions(); @@ -63,7 +62,6 @@ function App() { onBriefCancel={cancelBrief} onGenerateContent={generateContent} onRegenerateContent={generateContent} - onProductsStartOver={productsStartOver} onProductSelect={selectProduct} onNewConversation={newConversation} /> diff --git a/content-gen/src/app/frontend/src/api/index.ts b/content-gen/src/app/frontend/src/api/index.ts index 37031126b..b139f3e29 100644 --- a/content-gen/src/app/frontend/src/api/index.ts +++ b/content-gen/src/app/frontend/src/api/index.ts @@ -10,8 +10,7 @@ import type { AppConfig, } from '../types'; import httpClient from './httpClient'; -import { parseSSEStream } from '../utils/sseParser'; -import { getGenerationStage } from '../utils/generationStages'; +import { parseSSEStream, getGenerationStage } from '../utils'; /** * Get application configuration including feature flags @@ -178,7 +177,6 @@ export async function* streamGenerateContent( } as AgentResponse; } } catch (error) { - console.error(`Error polling task ${taskId}:`, error); // Continue polling on transient errors if (attempts >= maxAttempts) { throw error; diff --git a/content-gen/src/app/frontend/src/components/ChatPanel.tsx b/content-gen/src/app/frontend/src/components/ChatPanel.tsx index 0a6c39208..d6ac60896 100644 --- a/content-gen/src/app/frontend/src/components/ChatPanel.tsx +++ b/content-gen/src/app/frontend/src/components/ChatPanel.tsx @@ -30,7 +30,6 @@ interface ChatPanelProps { onBriefCancel?: () => void; onGenerateContent?: () => void; onRegenerateContent?: () => void; - onProductsStartOver?: () => void; onProductSelect?: (product: Product) => void; onNewConversation?: () => void; } @@ -42,7 +41,6 @@ export const ChatPanel = memo(function ChatPanel({ onBriefCancel, onGenerateContent, onRegenerateContent, - onProductsStartOver, onProductSelect, onNewConversation, }: ChatPanelProps) { @@ -88,9 +86,6 @@ export const ChatPanel = memo(function ChatPanel({ const isInputDisabled = useMemo(() => isLoading, [isLoading]); - const startOverFallback = useCallback(() => {}, []); - const effectiveProductsStartOver = onProductsStartOver || startOverFallback; - return (
    {/* Messages Area */} @@ -142,7 +137,6 @@ export const ChatPanel = memo(function ChatPanel({ products={selectedProducts} availableProducts={availableProducts} onConfirm={onGenerateContent!} - onStartOver={effectiveProductsStartOver} isAwaitingResponse={isLoading} onProductSelect={onProductSelect} disabled={isLoading} diff --git a/content-gen/src/app/frontend/src/components/InlineContentPreview.tsx b/content-gen/src/app/frontend/src/components/InlineContentPreview.tsx index c0f2367c8..41144f181 100644 --- a/content-gen/src/app/frontend/src/components/InlineContentPreview.tsx +++ b/content-gen/src/app/frontend/src/components/InlineContentPreview.tsx @@ -7,8 +7,7 @@ import { import { ShieldError20Regular } from '@fluentui/react-icons'; import type { GeneratedContent, Product } from '../types'; import { useWindowSize } from '../hooks/useWindowSize'; -import { isContentFilterError, getErrorMessage } from '../utils/contentErrors'; -import { downloadImage } from '../utils/downloadImage'; +import { isContentFilterError, getErrorMessage, downloadImage } from '../utils'; import { useCopyToClipboard } from '../hooks/useCopyToClipboard'; import { ImagePreviewCard } from './ImagePreviewCard'; import { ComplianceSection } from './ComplianceSection'; diff --git a/content-gen/src/app/frontend/src/components/ProductReview.tsx b/content-gen/src/app/frontend/src/components/ProductReview.tsx index 7c8a12ce5..7e3f31dce 100644 --- a/content-gen/src/app/frontend/src/components/ProductReview.tsx +++ b/content-gen/src/app/frontend/src/components/ProductReview.tsx @@ -13,7 +13,6 @@ import { ProductCard } from './ProductCard'; interface ProductReviewProps { products: Product[]; onConfirm: () => void; - onStartOver: () => void; isAwaitingResponse?: boolean; availableProducts?: Product[]; onProductSelect?: (product: Product) => void; @@ -23,7 +22,6 @@ interface ProductReviewProps { export const ProductReview = memo(function ProductReview({ products, onConfirm, - onStartOver: _onStartOver, isAwaitingResponse = false, availableProducts = [], onProductSelect, diff --git a/content-gen/src/app/frontend/src/components/ViolationCard.tsx b/content-gen/src/app/frontend/src/components/ViolationCard.tsx index 52914c7a6..479bbbfd6 100644 --- a/content-gen/src/app/frontend/src/components/ViolationCard.tsx +++ b/content-gen/src/app/frontend/src/components/ViolationCard.tsx @@ -1,4 +1,4 @@ -import { memo } from 'react'; +import { memo, useMemo } from 'react'; import { Text, } from '@fluentui/react-components'; @@ -17,7 +17,7 @@ export interface ViolationCardProps { * A single compliance violation row with severity-coloured icon and background. */ export const ViolationCard = memo(function ViolationCard({ violation }: ViolationCardProps) { - const getSeverityStyles = () => { + const { icon, bg } = useMemo(() => { switch (violation.severity) { case 'error': return { @@ -35,9 +35,7 @@ export const ViolationCard = memo(function ViolationCard({ violation }: Violatio bg: '#deecf9', }; } - }; - - const { icon, bg } = getSeverityStyles(); + }, [violation.severity]); return (
    , + dispatch: AppDispatch, +): Promise { + let fullContent = ''; + let currentAgent = ''; + let messageAdded = false; + + for await (const response of stream) { + if (response.type === 'agent_response') { + fullContent = response.content; + currentAgent = response.agent || ''; + if ((response.is_final || response.requires_user_input) && !messageAdded) { + dispatch(addMessage(createMessage('assistant', fullContent, currentAgent))); + messageAdded = true; + } + } else if (response.type === 'error') { + dispatch( + addMessage( + createMessage( + 'assistant', + response.content || 'An error occurred while processing your request.', + ), + ), + ); + messageAdded = true; + } + } +} /* ------------------------------------------------------------------ */ /* Hook */ @@ -130,42 +166,11 @@ export function useChatOrchestrator( } } else { // --- 1-b General question while brief is pending ----------- - let fullContent = ''; - let currentAgent = ''; - let messageAdded = false; - dispatch(setGenerationStatus('Processing your question...')); - for await (const response of streamChat( - content, - conversationId, - userId, - signal, - )) { - if (response.type === 'agent_response') { - fullContent = response.content; - currentAgent = response.agent || ''; - if ( - (response.is_final || response.requires_user_input) && - !messageAdded - ) { - dispatch( - addMessage(createMessage('assistant', fullContent, currentAgent)), - ); - messageAdded = true; - } - } else if (response.type === 'error') { - dispatch( - addMessage( - createMessage( - 'assistant', - response.content || - 'An error occurred while processing your request.', - ), - ), - ); - messageAdded = true; - } - } + await consumeStreamChat( + streamChat(content, conversationId, userId, signal), + dispatch, + ); dispatch(setGenerationStatus('')); } @@ -321,41 +326,11 @@ export function useChatOrchestrator( ); } else { // --- 3-b General question after content generation -------- - let fullContent = ''; - let currentAgent = ''; - let messageAdded = false; - dispatch(setGenerationStatus('Processing your request...')); - for await (const response of streamChat( - content, - conversationId, - userId, - signal, - )) { - if (response.type === 'agent_response') { - fullContent = response.content; - currentAgent = response.agent || ''; - if ( - (response.is_final || response.requires_user_input) && - !messageAdded - ) { - dispatch( - addMessage(createMessage('assistant', fullContent, currentAgent)), - ); - messageAdded = true; - } - } else if (response.type === 'error') { - dispatch( - addMessage( - createMessage( - 'assistant', - response.content || 'An error occurred.', - ), - ), - ); - messageAdded = true; - } - } + await consumeStreamChat( + streamChat(content, conversationId, userId, signal), + dispatch, + ); dispatch(setGenerationStatus('')); } @@ -426,42 +401,11 @@ export function useChatOrchestrator( } } else { // --- 4-b Generic chat ----------------------------------- - let fullContent = ''; - let currentAgent = ''; - let messageAdded = false; - dispatch(setGenerationStatus('Processing your request...')); - for await (const response of streamChat( - content, - conversationId, - userId, - signal, - )) { - if (response.type === 'agent_response') { - fullContent = response.content; - currentAgent = response.agent || ''; - if ( - (response.is_final || response.requires_user_input) && - !messageAdded - ) { - dispatch( - addMessage(createMessage('assistant', fullContent, currentAgent)), - ); - messageAdded = true; - } - } else if (response.type === 'error') { - dispatch( - addMessage( - createMessage( - 'assistant', - response.content || - 'An error occurred while processing your request.', - ), - ), - ); - messageAdded = true; - } - } + await consumeStreamChat( + streamChat(content, conversationId, userId, signal), + dispatch, + ); dispatch(setGenerationStatus('')); } } diff --git a/content-gen/src/app/frontend/src/hooks/useConversationActions.ts b/content-gen/src/app/frontend/src/hooks/useConversationActions.ts index a20629a4a..6cd0bfd01 100644 --- a/content-gen/src/app/frontend/src/hooks/useConversationActions.ts +++ b/content-gen/src/app/frontend/src/hooks/useConversationActions.ts @@ -182,19 +182,6 @@ export function useConversationActions() { /* ------------------------------------------------------------ */ /* Product actions */ /* ------------------------------------------------------------ */ - const productsStartOver = useCallback(() => { - dispatch(setSelectedProducts([])); - dispatch(setConfirmedBrief(null)); - dispatch( - addMessage( - createMessage( - 'assistant', - 'Starting over. Please provide your creative brief to begin a new campaign.', - ), - ), - ); - }, [dispatch]); - const selectProduct = useCallback( (product: Product) => { const isSelected = selectedProducts.some( @@ -224,7 +211,6 @@ export function useConversationActions() { newConversation, confirmBrief, cancelBrief, - productsStartOver, selectProduct, toggleHistory, }; diff --git a/content-gen/src/app/frontend/src/hooks/useDebounce.ts b/content-gen/src/app/frontend/src/hooks/useDebounce.ts deleted file mode 100644 index c0c7e5ab1..000000000 --- a/content-gen/src/app/frontend/src/hooks/useDebounce.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { useState, useEffect } from 'react'; - -/** - * Returns a debounced copy of `value` that only updates after `delay` ms of - * inactivity. - * - * @param value - The source value to debounce. - * @param delay - Debounce window in milliseconds. - * - * @example - * ```ts - * const [search, setSearch] = useState(''); - * const debouncedSearch = useDebounce(search, 300); - * - * useEffect(() => { - * fetchResults(debouncedSearch); - * }, [debouncedSearch]); - * ``` - */ -export function useDebounce(value: T, delay: number): T { - const [debounced, setDebounced] = useState(value); - - useEffect(() => { - const timer = setTimeout(() => setDebounced(value), delay); - return () => clearTimeout(timer); - }, [value, delay]); - - return debounced; -} diff --git a/content-gen/src/app/frontend/src/utils/index.ts b/content-gen/src/app/frontend/src/utils/index.ts index 2541cc3b7..aaefb147b 100644 --- a/content-gen/src/app/frontend/src/utils/index.ts +++ b/content-gen/src/app/frontend/src/utils/index.ts @@ -23,10 +23,6 @@ export { BRIEF_FIELD_LABELS, BRIEF_DISPLAY_ORDER, BRIEF_FIELD_KEYS } from './bri // String utilities export { createNameSwapper, matchesAnyKeyword } from './stringUtils'; -// Production API utilities -export { retryRequest, RequestCache, throttle } from './apiUtils'; -export type { RetryOptions } from './apiUtils'; - // Content error detection export { isContentFilterError, getErrorMessage } from './contentErrors'; diff --git a/content-gen/src/app/frontend/src/utils/sseParser.ts b/content-gen/src/app/frontend/src/utils/sseParser.ts index 7b11201f0..7767c0b5e 100644 --- a/content-gen/src/app/frontend/src/utils/sseParser.ts +++ b/content-gen/src/app/frontend/src/utils/sseParser.ts @@ -40,7 +40,7 @@ export async function* parseSSEStream( try { yield JSON.parse(data) as AgentResponse; } catch { - console.error('Failed to parse SSE data:', data); + // Malformed SSE frame — skip silently } } } From 3216286ef17c58aa1db1237de0189666b127fb79 Mon Sep 17 00:00:00 2001 From: Shubhangi-Microsoft Date: Mon, 2 Mar 2026 17:31:35 +0530 Subject: [PATCH 05/72] refactor(frontend): GenerationStatus enum, delete dead code, dedup validation, hooks barrel --- content-gen/src/app/frontend/src/App.tsx | 4 +- .../app/frontend/src/components/ChatInput.tsx | 4 +- .../app/frontend/src/components/ChatPanel.tsx | 10 +- .../src/components/ConversationItem.tsx | 55 +++-- .../src/components/InlineContentPreview.tsx | 3 +- .../frontend/src/components/MessageBubble.tsx | 2 +- .../src/components/SuggestionCard.tsx | 1 - .../frontend/src/components/WelcomeCard.tsx | 12 +- .../src/app/frontend/src/hooks/index.ts | 10 + .../frontend/src/hooks/useChatOrchestrator.ts | 44 ++-- .../src/hooks/useContentGeneration.ts | 16 +- .../src/app/frontend/src/store/appSlice.ts | 61 +++++- .../src/app/frontend/src/store/index.ts | 5 +- .../src/app/frontend/src/store/selectors.ts | 1 + .../src/app/frontend/src/types/index.ts | 20 -- .../src/app/frontend/src/utils/apiUtils.ts | 202 ------------------ 16 files changed, 143 insertions(+), 307 deletions(-) create mode 100644 content-gen/src/app/frontend/src/hooks/index.ts delete mode 100644 content-gen/src/app/frontend/src/utils/apiUtils.ts diff --git a/content-gen/src/app/frontend/src/App.tsx b/content-gen/src/app/frontend/src/App.tsx index 4bc73f5fc..28d241c28 100644 --- a/content-gen/src/app/frontend/src/App.tsx +++ b/content-gen/src/app/frontend/src/App.tsx @@ -11,9 +11,7 @@ import { selectUserName, selectShowChatHistory, } from './store'; -import { useChatOrchestrator } from './hooks/useChatOrchestrator'; -import { useContentGeneration } from './hooks/useContentGeneration'; -import { useConversationActions } from './hooks/useConversationActions'; +import { useChatOrchestrator, useContentGeneration, useConversationActions } from './hooks'; function App() { diff --git a/content-gen/src/app/frontend/src/components/ChatInput.tsx b/content-gen/src/app/frontend/src/components/ChatInput.tsx index a27f747fd..778de0e29 100644 --- a/content-gen/src/app/frontend/src/components/ChatInput.tsx +++ b/content-gen/src/app/frontend/src/components/ChatInput.tsx @@ -37,10 +37,10 @@ export const ChatInput = memo(function ChatInput({ // Support both controlled & uncontrolled modes const inputValue = controlledValue ?? internalValue; - const setInputValue = (v: string) => { + const setInputValue = useCallback((v: string) => { controlledOnChange?.(v); if (controlledValue === undefined) setInternalValue(v); - }; + }, [controlledOnChange, controlledValue]); const handleSubmit = useCallback((e: React.FormEvent) => { e.preventDefault(); diff --git a/content-gen/src/app/frontend/src/components/ChatPanel.tsx b/content-gen/src/app/frontend/src/components/ChatPanel.tsx index d6ac60896..c94792432 100644 --- a/content-gen/src/app/frontend/src/components/ChatPanel.tsx +++ b/content-gen/src/app/frontend/src/components/ChatPanel.tsx @@ -9,12 +9,12 @@ import { WelcomeCard } from './WelcomeCard'; import { MessageBubble } from './MessageBubble'; import { TypingIndicator } from './TypingIndicator'; import { ChatInput } from './ChatInput'; -import { useAutoScroll } from '../hooks/useAutoScroll'; +import { useAutoScroll } from '../hooks'; import { useAppSelector, selectMessages, selectIsLoading, - selectGenerationStatus, + selectGenerationStatusLabel, selectPendingBrief, selectConfirmedBrief, selectGeneratedContent, @@ -46,7 +46,7 @@ export const ChatPanel = memo(function ChatPanel({ }: ChatPanelProps) { const messages = useAppSelector(selectMessages); const isLoading = useAppSelector(selectIsLoading); - const generationStatus = useAppSelector(selectGenerationStatus); + const generationStatus = useAppSelector(selectGenerationStatusLabel); const pendingBrief = useAppSelector(selectPendingBrief); const confirmedBrief = useAppSelector(selectConfirmedBrief); const generatedContent = useAppSelector(selectGeneratedContent); @@ -84,8 +84,6 @@ export const ChatPanel = memo(function ChatPanel({ setInputValue(prompt); }, []); - const isInputDisabled = useMemo(() => isLoading, [isLoading]); - return (
    {/* Messages Area */} @@ -171,7 +169,7 @@ export const ChatPanel = memo(function ChatPanel({ diff --git a/content-gen/src/app/frontend/src/components/ConversationItem.tsx b/content-gen/src/app/frontend/src/components/ConversationItem.tsx index 16b5ebc5e..c40d89d1d 100644 --- a/content-gen/src/app/frontend/src/components/ConversationItem.tsx +++ b/content-gen/src/app/frontend/src/components/ConversationItem.tsx @@ -23,6 +23,23 @@ import { } from '@fluentui/react-icons'; import type { ConversationSummary } from '../store'; +/* ------------------------------------------------------------------ */ +/* Validation constants & helper */ +/* ------------------------------------------------------------------ */ + +const NAME_MIN_LENGTH = 5; +const NAME_MAX_LENGTH = 50; + +/** Returns an error message, or `''` when the value is valid. */ +function validateConversationName(value: string): string { + const trimmed = value.trim(); + if (trimmed === '') return 'Conversation name cannot be empty or contain only spaces'; + if (trimmed.length < NAME_MIN_LENGTH) return `Conversation name must be at least ${NAME_MIN_LENGTH} characters`; + if (value.length > NAME_MAX_LENGTH) return `Conversation name cannot exceed ${NAME_MAX_LENGTH} characters`; + if (!/[a-zA-Z0-9]/.test(trimmed)) return 'Conversation name must contain at least one letter or number'; + return ''; +} + export interface ConversationItemProps { conversation: ConversationSummary; isActive: boolean; @@ -61,21 +78,13 @@ export const ConversationItem = memo(function ConversationItem({ }, [conversation.title]); const handleRenameConfirm = useCallback(async () => { - const trimmedValue = renameValue.trim(); - - if (trimmedValue.length < 5) { - setRenameError('Conversation name must be at least 5 characters'); - return; - } - if (trimmedValue.length > 50) { - setRenameError('Conversation name cannot exceed 50 characters'); - return; - } - if (!/[a-zA-Z0-9]/.test(trimmedValue)) { - setRenameError('Conversation name must contain at least one letter or number'); + const error = validateConversationName(renameValue); + if (error) { + setRenameError(error); return; } + const trimmedValue = renameValue.trim(); if (trimmedValue === conversation.title) { setIsRenameDialogOpen(false); setRenameError(''); @@ -194,21 +203,11 @@ export const ConversationItem = memo(function ConversationItem({ { const newValue = e.target.value; setRenameValue(newValue); - if (newValue.trim() === '') { - setRenameError('Conversation name cannot be empty or contain only spaces'); - } else if (newValue.trim().length < 5) { - setRenameError('Conversation name must be at least 5 characters'); - } else if (!/[a-zA-Z0-9]/.test(newValue)) { - setRenameError('Conversation name must contain at least one letter or number'); - } else if (newValue.length > 50) { - setRenameError('Conversation name cannot exceed 50 characters'); - } else { - setRenameError(''); - } + setRenameError(validateConversationName(newValue)); }} onKeyDown={(e) => { if (e.key === 'Enter' && renameValue.trim()) { @@ -228,7 +227,7 @@ export const ConversationItem = memo(function ConversationItem({ display: 'block', }} > - Maximum 50 characters ({renameValue.length}/50) + Maximum {NAME_MAX_LENGTH} characters ({renameValue.length}/{NAME_MAX_LENGTH}) {renameError && ( 50 - } + disabled={!!validateConversationName(renameValue)} > Rename diff --git a/content-gen/src/app/frontend/src/components/InlineContentPreview.tsx b/content-gen/src/app/frontend/src/components/InlineContentPreview.tsx index 41144f181..dc0f27491 100644 --- a/content-gen/src/app/frontend/src/components/InlineContentPreview.tsx +++ b/content-gen/src/app/frontend/src/components/InlineContentPreview.tsx @@ -6,9 +6,8 @@ import { } from '@fluentui/react-components'; import { ShieldError20Regular } from '@fluentui/react-icons'; import type { GeneratedContent, Product } from '../types'; -import { useWindowSize } from '../hooks/useWindowSize'; +import { useWindowSize, useCopyToClipboard } from '../hooks'; import { isContentFilterError, getErrorMessage, downloadImage } from '../utils'; -import { useCopyToClipboard } from '../hooks/useCopyToClipboard'; import { ImagePreviewCard } from './ImagePreviewCard'; import { ComplianceSection } from './ComplianceSection'; diff --git a/content-gen/src/app/frontend/src/components/MessageBubble.tsx b/content-gen/src/app/frontend/src/components/MessageBubble.tsx index 58d3cb3c2..95431a7d9 100644 --- a/content-gen/src/app/frontend/src/components/MessageBubble.tsx +++ b/content-gen/src/app/frontend/src/components/MessageBubble.tsx @@ -9,7 +9,7 @@ import { import { Copy20Regular } from '@fluentui/react-icons'; import ReactMarkdown from 'react-markdown'; import type { ChatMessage } from '../types'; -import { useCopyToClipboard } from '../hooks/useCopyToClipboard'; +import { useCopyToClipboard } from '../hooks'; export interface MessageBubbleProps { message: ChatMessage; diff --git a/content-gen/src/app/frontend/src/components/SuggestionCard.tsx b/content-gen/src/app/frontend/src/components/SuggestionCard.tsx index d557936a5..55e8e1570 100644 --- a/content-gen/src/app/frontend/src/components/SuggestionCard.tsx +++ b/content-gen/src/app/frontend/src/components/SuggestionCard.tsx @@ -7,7 +7,6 @@ import { export interface SuggestionCardProps { title: string; - prompt: string; icon: string; isSelected?: boolean; onClick: () => void; diff --git a/content-gen/src/app/frontend/src/components/WelcomeCard.tsx b/content-gen/src/app/frontend/src/components/WelcomeCard.tsx index cf89dca2c..aa1388324 100644 --- a/content-gen/src/app/frontend/src/components/WelcomeCard.tsx +++ b/content-gen/src/app/frontend/src/components/WelcomeCard.tsx @@ -1,4 +1,4 @@ -import { memo, useMemo, useCallback } from 'react'; +import { memo, useMemo } from 'react'; import { Text, tokens, @@ -7,13 +7,13 @@ import { SuggestionCard } from './SuggestionCard'; import FirstPromptIcon from '../styles/images/firstprompt.png'; import SecondPromptIcon from '../styles/images/secondprompt.png'; -interface SuggestionCard { +interface SuggestionData { title: string; prompt: string; icon: string; } -const suggestions: SuggestionCard[] = [ +const suggestions: SuggestionData[] = [ { title: "I need to create a social media post about paint products for home remodels. The campaign is titled \"Brighten Your Springtime\" and the audience is new homeowners. I need marketing copy plus an image. The image should be an informal living room with tasteful furnishings.", prompt: "I need to create a social media post about paint products for home remodels. The campaign is titled \"Brighten Your Springtime\" and the audience is new homeowners. I need marketing copy plus an image. The image should be an informal living room with tasteful furnishings.", @@ -37,9 +37,6 @@ export const WelcomeCard = memo(function WelcomeCard({ onSuggestionClick, curren [currentInput], ); - const handleSuggestionClick = useCallback((prompt: string) => { - onSuggestionClick(prompt); - }, [onSuggestionClick]); return (
    handleSuggestionClick(suggestion.prompt)} + onClick={() => onSuggestionClick(suggestion.prompt)} /> ); })} diff --git a/content-gen/src/app/frontend/src/hooks/index.ts b/content-gen/src/app/frontend/src/hooks/index.ts new file mode 100644 index 000000000..f23f96430 --- /dev/null +++ b/content-gen/src/app/frontend/src/hooks/index.ts @@ -0,0 +1,10 @@ +/** + * Barrel export for all custom hooks. + * Import hooks from '../hooks' instead of individual files. + */ +export { useAutoScroll } from './useAutoScroll'; +export { useChatOrchestrator } from './useChatOrchestrator'; +export { useContentGeneration } from './useContentGeneration'; +export { useConversationActions } from './useConversationActions'; +export { useCopyToClipboard } from './useCopyToClipboard'; +export { useWindowSize } from './useWindowSize'; diff --git a/content-gen/src/app/frontend/src/hooks/useChatOrchestrator.ts b/content-gen/src/app/frontend/src/hooks/useChatOrchestrator.ts index 6089d088c..f4edc088c 100644 --- a/content-gen/src/app/frontend/src/hooks/useChatOrchestrator.ts +++ b/content-gen/src/app/frontend/src/hooks/useChatOrchestrator.ts @@ -16,6 +16,7 @@ import { addMessage, setIsLoading, setGenerationStatus, + GenerationStatus, setPendingBrief, setConfirmedBrief, setAwaitingClarification, @@ -127,7 +128,7 @@ export function useChatOrchestrator( // --- 1-a Refine the brief -------------------------------- const refinementPrompt = `Current creative brief:\n${JSON.stringify(pendingBrief, null, 2)}\n\nUser requested change: ${content}\n\nPlease update the brief accordingly and return the complete updated brief.`; - dispatch(setGenerationStatus('Updating creative brief...')); + dispatch(setGenerationStatus(GenerationStatus.UPDATING_BRIEF)); const parsed = await parseBrief( refinementPrompt, conversationId, @@ -145,7 +146,7 @@ export function useChatOrchestrator( if (parsed.requires_clarification && parsed.clarifying_questions) { dispatch(setAwaitingClarification(true)); - dispatch(setGenerationStatus('')); + dispatch(setGenerationStatus(GenerationStatus.IDLE)); dispatch( addMessage( createMessage('assistant', parsed.clarifying_questions, 'PlanningAgent'), @@ -153,7 +154,7 @@ export function useChatOrchestrator( ); } else { dispatch(setAwaitingClarification(false)); - dispatch(setGenerationStatus('')); + dispatch(setGenerationStatus(GenerationStatus.IDLE)); dispatch( addMessage( createMessage( @@ -166,19 +167,19 @@ export function useChatOrchestrator( } } else { // --- 1-b General question while brief is pending ----------- - dispatch(setGenerationStatus('Processing your question...')); + dispatch(setGenerationStatus(GenerationStatus.PROCESSING_QUESTION)); await consumeStreamChat( streamChat(content, conversationId, userId, signal), dispatch, ); - dispatch(setGenerationStatus('')); + dispatch(setGenerationStatus(GenerationStatus.IDLE)); } /* ---------------------------------------------------------- */ /* Branch 2 – brief confirmed, in product selection */ /* ---------------------------------------------------------- */ } else if (confirmedBrief && !generatedContent) { - dispatch(setGenerationStatus('Finding products...')); + dispatch(setGenerationStatus(GenerationStatus.FINDING_PRODUCTS)); const result = await selectProducts( content, selectedProducts, @@ -187,7 +188,7 @@ export function useChatOrchestrator( signal, ); dispatch(setSelectedProducts(result.products || [])); - dispatch(setGenerationStatus('')); + dispatch(setGenerationStatus(GenerationStatus.IDLE)); dispatch( addMessage( createMessage( @@ -215,7 +216,7 @@ export function useChatOrchestrator( // --- 3-a Regenerate image -------------------------------- const { streamRegenerateImage } = await import('../api'); dispatch( - setGenerationStatus('Regenerating image with your changes...'), + setGenerationStatus(GenerationStatus.REGENERATING_IMAGE), ); let responseData: GeneratedContent | null = null; @@ -243,9 +244,10 @@ export function useChatOrchestrator( )) { if (response.type === 'heartbeat') { dispatch( - setGenerationStatus( - response.message || 'Regenerating image...', - ), + setGenerationStatus({ + status: GenerationStatus.POLLING, + label: response.message || 'Regenerating image...', + }), ); } else if ( response.type === 'agent_response' && @@ -320,18 +322,18 @@ export function useChatOrchestrator( } } - dispatch(setGenerationStatus('')); + dispatch(setGenerationStatus(GenerationStatus.IDLE)); dispatch( addMessage(createMessage('assistant', messageContent, 'ImageAgent')), ); } else { // --- 3-b General question after content generation -------- - dispatch(setGenerationStatus('Processing your request...')); + dispatch(setGenerationStatus(GenerationStatus.PROCESSING_REQUEST)); await consumeStreamChat( streamChat(content, conversationId, userId, signal), dispatch, ); - dispatch(setGenerationStatus('')); + dispatch(setGenerationStatus(GenerationStatus.IDLE)); } /* ---------------------------------------------------------- */ @@ -346,7 +348,7 @@ export function useChatOrchestrator( if (isBriefLike && !confirmedBrief) { // --- 4-a Parse as creative brief -------------------------- - dispatch(setGenerationStatus('Analyzing creative brief...')); + dispatch(setGenerationStatus(GenerationStatus.ANALYZING_BRIEF)); const parsed = await parseBrief( content, conversationId, @@ -359,7 +361,7 @@ export function useChatOrchestrator( } if (parsed.rai_blocked) { - dispatch(setGenerationStatus('')); + dispatch(setGenerationStatus(GenerationStatus.IDLE)); dispatch( addMessage( createMessage('assistant', parsed.message, 'ContentSafety'), @@ -373,7 +375,7 @@ export function useChatOrchestrator( dispatch(setPendingBrief(parsed.brief)); } dispatch(setAwaitingClarification(true)); - dispatch(setGenerationStatus('')); + dispatch(setGenerationStatus(GenerationStatus.IDLE)); dispatch( addMessage( createMessage( @@ -388,7 +390,7 @@ export function useChatOrchestrator( dispatch(setPendingBrief(parsed.brief)); } dispatch(setAwaitingClarification(false)); - dispatch(setGenerationStatus('')); + dispatch(setGenerationStatus(GenerationStatus.IDLE)); dispatch( addMessage( createMessage( @@ -401,12 +403,12 @@ export function useChatOrchestrator( } } else { // --- 4-b Generic chat ----------------------------------- - dispatch(setGenerationStatus('Processing your request...')); + dispatch(setGenerationStatus(GenerationStatus.PROCESSING_REQUEST)); await consumeStreamChat( streamChat(content, conversationId, userId, signal), dispatch, ); - dispatch(setGenerationStatus('')); + dispatch(setGenerationStatus(GenerationStatus.IDLE)); } } } catch (error) { @@ -423,7 +425,7 @@ export function useChatOrchestrator( } } finally { dispatch(setIsLoading(false)); - dispatch(setGenerationStatus('')); + dispatch(setGenerationStatus(GenerationStatus.IDLE)); abortControllerRef.current = null; dispatch(incrementHistoryRefresh()); } diff --git a/content-gen/src/app/frontend/src/hooks/useContentGeneration.ts b/content-gen/src/app/frontend/src/hooks/useContentGeneration.ts index 423d8a561..895c20025 100644 --- a/content-gen/src/app/frontend/src/hooks/useContentGeneration.ts +++ b/content-gen/src/app/frontend/src/hooks/useContentGeneration.ts @@ -11,6 +11,7 @@ import { addMessage, setIsLoading, setGenerationStatus, + GenerationStatus, setGeneratedContent, } from '../store'; @@ -35,7 +36,7 @@ export function useContentGeneration( if (!confirmedBrief) return; dispatch(setIsLoading(true)); - dispatch(setGenerationStatus('Starting content generation...')); + dispatch(setGenerationStatus(GenerationStatus.STARTING_GENERATION)); abortControllerRef.current = new AbortController(); const signal = abortControllerRef.current.signal; @@ -55,22 +56,25 @@ export function useContentGeneration( if (response.type === 'heartbeat') { const statusMessage = response.content || 'Generating content...'; const elapsed = (response as { elapsed?: number }).elapsed || 0; - dispatch(setGenerationStatus(`${statusMessage} (${elapsed}s)`)); + dispatch(setGenerationStatus({ + status: GenerationStatus.POLLING, + label: `${statusMessage} (${elapsed}s)`, + })); continue; } if (response.is_final && response.type !== 'error') { - dispatch(setGenerationStatus('Processing results...')); + dispatch(setGenerationStatus(GenerationStatus.PROCESSING_RESULTS)); try { const rawContent = JSON.parse(response.content); const genContent = buildGeneratedContent(rawContent); dispatch(setGeneratedContent(genContent)); - dispatch(setGenerationStatus('')); + dispatch(setGenerationStatus(GenerationStatus.IDLE)); } catch { // Content parse failure — non-critical, generation result may be malformed } } else if (response.type === 'error') { - dispatch(setGenerationStatus('')); + dispatch(setGenerationStatus(GenerationStatus.IDLE)); dispatch(addMessage(createErrorMessage( `Error generating content: ${response.content}`, ))); @@ -86,7 +90,7 @@ export function useContentGeneration( } } finally { dispatch(setIsLoading(false)); - dispatch(setGenerationStatus('')); + dispatch(setGenerationStatus(GenerationStatus.IDLE)); abortControllerRef.current = null; } }, [ diff --git a/content-gen/src/app/frontend/src/store/appSlice.ts b/content-gen/src/app/frontend/src/store/appSlice.ts index 59876ec03..265522ebd 100644 --- a/content-gen/src/app/frontend/src/store/appSlice.ts +++ b/content-gen/src/app/frontend/src/store/appSlice.ts @@ -5,6 +5,46 @@ */ import { createSlice, createAsyncThunk, type PayloadAction } from '@reduxjs/toolkit'; +/* ------------------------------------------------------------------ */ +/* Generation-status enum */ +/* ------------------------------------------------------------------ */ + +/** + * Finite set of generation-status values. Components that read + * `generationStatus` can compare against these constants instead of + * relying on magic strings. + * + * `IDLE` means "no status to display". Every other member maps to a + * user-facing label via {@link GENERATION_STATUS_LABELS}. + */ +export enum GenerationStatus { + IDLE = '', + UPDATING_BRIEF = 'UPDATING_BRIEF', + PROCESSING_QUESTION = 'PROCESSING_QUESTION', + FINDING_PRODUCTS = 'FINDING_PRODUCTS', + REGENERATING_IMAGE = 'REGENERATING_IMAGE', + PROCESSING_REQUEST = 'PROCESSING_REQUEST', + ANALYZING_BRIEF = 'ANALYZING_BRIEF', + STARTING_GENERATION = 'STARTING_GENERATION', + PROCESSING_RESULTS = 'PROCESSING_RESULTS', + /** Used for heartbeat polling where the label is dynamic. */ + POLLING = 'POLLING', +} + +/** Display strings shown in the UI for each status. */ +export const GENERATION_STATUS_LABELS: Record = { + [GenerationStatus.IDLE]: '', + [GenerationStatus.UPDATING_BRIEF]: 'Updating creative brief...', + [GenerationStatus.PROCESSING_QUESTION]: 'Processing your question...', + [GenerationStatus.FINDING_PRODUCTS]: 'Finding products...', + [GenerationStatus.REGENERATING_IMAGE]: 'Regenerating image with your changes...', + [GenerationStatus.PROCESSING_REQUEST]: 'Processing your request...', + [GenerationStatus.ANALYZING_BRIEF]: 'Analyzing creative brief...', + [GenerationStatus.STARTING_GENERATION]: 'Starting content generation...', + [GenerationStatus.PROCESSING_RESULTS]: 'Processing results...', + [GenerationStatus.POLLING]: 'Generating content...', +}; + /* ------------------------------------------------------------------ */ /* Async Thunks */ /* ------------------------------------------------------------------ */ @@ -47,7 +87,10 @@ interface AppState { isLoading: boolean; imageGenerationEnabled: boolean; showChatHistory: boolean; - generationStatus: string; + /** Current generation status enum value. */ + generationStatus: GenerationStatus; + /** Dynamic label override (used with GenerationStatus.POLLING). */ + generationStatusLabel: string; } const initialState: AppState = { @@ -56,7 +99,8 @@ const initialState: AppState = { isLoading: false, imageGenerationEnabled: true, showChatHistory: true, - generationStatus: '', + generationStatus: GenerationStatus.IDLE, + generationStatusLabel: '', }; const appSlice = createSlice({ @@ -66,8 +110,17 @@ const appSlice = createSlice({ setIsLoading(state, action: PayloadAction) { state.isLoading = action.payload; }, - setGenerationStatus(state, action: PayloadAction) { - state.generationStatus = action.payload; + setGenerationStatus( + state, + action: PayloadAction, + ) { + if (typeof action.payload === 'string') { + state.generationStatus = action.payload; + state.generationStatusLabel = GENERATION_STATUS_LABELS[action.payload]; + } else { + state.generationStatus = action.payload.status; + state.generationStatusLabel = action.payload.label; + } }, toggleChatHistory(state) { state.showChatHistory = !state.showChatHistory; diff --git a/content-gen/src/app/frontend/src/store/index.ts b/content-gen/src/app/frontend/src/store/index.ts index 6e85accf0..5e28f210c 100644 --- a/content-gen/src/app/frontend/src/store/index.ts +++ b/content-gen/src/app/frontend/src/store/index.ts @@ -6,7 +6,7 @@ export { store } from './store'; export type { RootState, AppDispatch } from './store'; export { useAppDispatch, useAppSelector } from './hooks'; -// App slice – actions & thunks +// App slice – actions, thunks & enums export { fetchAppConfig, fetchUserInfo, @@ -14,6 +14,8 @@ export { setGenerationStatus, toggleChatHistory, setShowChatHistory, + GenerationStatus, + GENERATION_STATUS_LABELS, } from './appSlice'; // Chat slice – actions @@ -54,6 +56,7 @@ export { selectUserName, selectIsLoading, selectGenerationStatus, + selectGenerationStatusLabel, selectImageGenerationEnabled, selectShowChatHistory, selectConversationId, diff --git a/content-gen/src/app/frontend/src/store/selectors.ts b/content-gen/src/app/frontend/src/store/selectors.ts index 8fb2329c4..fe122ee2d 100644 --- a/content-gen/src/app/frontend/src/store/selectors.ts +++ b/content-gen/src/app/frontend/src/store/selectors.ts @@ -10,6 +10,7 @@ export const selectUserId = (state: RootState) => state.app.userId; export const selectUserName = (state: RootState) => state.app.userName; export const selectIsLoading = (state: RootState) => state.app.isLoading; export const selectGenerationStatus = (state: RootState) => state.app.generationStatus; +export const selectGenerationStatusLabel = (state: RootState) => state.app.generationStatusLabel; export const selectImageGenerationEnabled = (state: RootState) => state.app.imageGenerationEnabled; export const selectShowChatHistory = (state: RootState) => state.app.showChatHistory; diff --git a/content-gen/src/app/frontend/src/types/index.ts b/content-gen/src/app/frontend/src/types/index.ts index 91c40c3a3..0a29d07df 100644 --- a/content-gen/src/app/frontend/src/types/index.ts +++ b/content-gen/src/app/frontend/src/types/index.ts @@ -47,14 +47,6 @@ export interface ChatMessage { violations?: ComplianceViolation[]; } -export interface Conversation { - id: string; - user_id: string; - messages: ChatMessage[]; - brief?: CreativeBrief; - updated_at: string; -} - export interface AgentResponse { type: 'agent_response' | 'error' | 'status' | 'heartbeat'; agent?: string; @@ -72,18 +64,6 @@ export interface AgentResponse { }; } -export interface BrandGuidelines { - tone: string; - voice: string; - primary_color: string; - secondary_color: string; - prohibited_words: string[]; - required_disclosures: string[]; - max_headline_length: number; - max_body_length: number; - require_cta: boolean; -} - export interface ParsedBriefResponse { brief?: CreativeBrief; requires_confirmation: boolean; diff --git a/content-gen/src/app/frontend/src/utils/apiUtils.ts b/content-gen/src/app/frontend/src/utils/apiUtils.ts deleted file mode 100644 index 1f60ff1c9..000000000 --- a/content-gen/src/app/frontend/src/utils/apiUtils.ts +++ /dev/null @@ -1,202 +0,0 @@ -/** - * Production-grade API utilities — retry with exponential backoff, - * request deduplication cache, and throttle. - * - * These harden the HTTP layer for unreliable networks and prevent - * accidental duplicate requests (e.g. double-clicks, React strict mode). - */ - -/* ------------------------------------------------------------------ */ -/* retryRequest — exponential backoff wrapper */ -/* ------------------------------------------------------------------ */ - -export interface RetryOptions { - /** Maximum number of attempts (including the first). Default: 3. */ - maxAttempts?: number; - /** Initial delay in ms before the first retry. Default: 1 000. */ - initialDelayMs?: number; - /** Maximum delay cap in ms. Default: 30 000. */ - maxDelayMs?: number; - /** Multiplier applied to the delay after each failure. Default: 2. */ - backoffFactor?: number; - /** Optional predicate — return `true` if the request should be retried. */ - shouldRetry?: (error: unknown, attempt: number) => boolean; - /** Optional `AbortSignal` to cancel outstanding retries. */ - signal?: AbortSignal; -} - -/** - * Execute `fn` with automatic retries and exponential backoff. - * - * ```ts - * const data = await retryRequest(() => httpClient.get('/health'), { - * maxAttempts: 4, - * initialDelayMs: 500, - * }); - * ``` - */ -export async function retryRequest( - fn: () => Promise, - opts: RetryOptions = {}, -): Promise { - const { - maxAttempts = 3, - initialDelayMs = 1_000, - maxDelayMs = 30_000, - backoffFactor = 2, - shouldRetry = defaultShouldRetry, - signal, - } = opts; - - let delay = initialDelayMs; - - for (let attempt = 1; attempt <= maxAttempts; attempt++) { - try { - return await fn(); - } catch (error) { - if (attempt >= maxAttempts || !shouldRetry(error, attempt)) throw error; - if (signal?.aborted) throw new DOMException('Retry aborted', 'AbortError'); - - await sleep(delay, signal); - delay = Math.min(delay * backoffFactor, maxDelayMs); - } - } - - // Unreachable — the loop always returns or throws. - throw new Error('retryRequest: exhausted all attempts'); -} - -/* ------------------------------------------------------------------ */ -/* RequestCache — deduplication / in-flight coalescing */ -/* ------------------------------------------------------------------ */ - -/** - * Simple in-memory cache that de-duplicates concurrent identical requests. - * - * If a request with the same `key` is already in flight, all callers - * share the same promise. Once settled the entry is automatically - * evicted (or kept for `ttlMs` when configured). - * - * ```ts - * const cache = new RequestCache(); - * const data = await cache.dedupe('config', () => httpClient.get('/config')); - * ``` - */ -export class RequestCache { - private inflight = new Map>(); - private store = new Map(); - private ttlMs: number; - - constructor(ttlMs = 0) { - this.ttlMs = ttlMs; - } - - async dedupe(key: string, fn: () => Promise): Promise { - // Return from TTL cache if still fresh - const cached = this.store.get(key); - if (cached && cached.expiresAt > Date.now()) { - return cached.value as T; - } - - // De-duplicate in-flight requests - const existing = this.inflight.get(key); - if (existing) return existing as Promise; - - const promise = fn().finally(() => { - this.inflight.delete(key); - }); - - this.inflight.set(key, promise); - - if (this.ttlMs > 0) { - promise.then((value) => { - this.store.set(key, { value, expiresAt: Date.now() + this.ttlMs }); - }).catch(() => { /* don't cache failures */ }); - } - - return promise as Promise; - } - - /** Manually evict a cache entry. */ - invalidate(key: string): void { - this.store.delete(key); - this.inflight.delete(key); - } - - /** Evict all cache entries. */ - clear(): void { - this.store.clear(); - this.inflight.clear(); - } -} - -/* ------------------------------------------------------------------ */ -/* throttle — limits invocation frequency */ -/* ------------------------------------------------------------------ */ - -/** - * Classic trailing-edge throttle. - * - * Ensures `fn` is called at most once every `limitMs` milliseconds. - * The first invocation fires immediately; subsequent calls within the - * window are silently dropped and the **last** one fires when the - * window closes. - */ -export function throttle void>( - fn: T, - limitMs: number, -): (...args: Parameters) => void { - let timer: ReturnType | null = null; - let lastArgs: Parameters | null = null; - let lastCallTime = 0; - - return (...args: Parameters) => { - const now = Date.now(); - const remaining = limitMs - (now - lastCallTime); - - if (remaining <= 0) { - // Window has passed — fire immediately - lastCallTime = now; - fn(...args); - } else { - // Inside the window — schedule a trailing call - lastArgs = args; - if (!timer) { - timer = setTimeout(() => { - lastCallTime = Date.now(); - timer = null; - if (lastArgs) { - fn(...lastArgs); - lastArgs = null; - } - }, remaining); - } - } - }; -} - -/* ------------------------------------------------------------------ */ -/* Internal helpers (not exported) */ -/* ------------------------------------------------------------------ */ - -/** Default retry predicate — retry on network errors & 5xx, not on 4xx. */ -function defaultShouldRetry(error: unknown, _attempt: number): boolean { - if (error instanceof DOMException && error.name === 'AbortError') return false; - if (error instanceof Response) return error.status >= 500; - return true; // network errors, timeouts, etc. -} - -/** Abort-aware sleep. */ -function sleep(ms: number, signal?: AbortSignal): Promise { - return new Promise((resolve, reject) => { - if (signal?.aborted) { - reject(new DOMException('Sleep aborted', 'AbortError')); - return; - } - const timer = setTimeout(resolve, ms); - signal?.addEventListener('abort', () => { - clearTimeout(timer); - reject(new DOMException('Sleep aborted', 'AbortError')); - }, { once: true }); - }); -} From bfce2efbe4719f849bba36a01291d18c07aef4e4 Mon Sep 17 00:00:00 2001 From: Shubhangi-Microsoft Date: Mon, 2 Mar 2026 18:21:59 +0530 Subject: [PATCH 06/72] refactor: clean up unused barrel exports and standardize httpClient imports --- content-gen/src/app/frontend/src/api/index.ts | 1 + .../src/app/frontend/src/hooks/useConversationActions.ts | 2 +- content-gen/src/app/frontend/src/store/chatHistorySlice.ts | 2 +- content-gen/src/app/frontend/src/store/index.ts | 2 -- content-gen/src/app/frontend/src/utils/index.ts | 1 - 5 files changed, 3 insertions(+), 5 deletions(-) diff --git a/content-gen/src/app/frontend/src/api/index.ts b/content-gen/src/app/frontend/src/api/index.ts index b139f3e29..a7e46207b 100644 --- a/content-gen/src/app/frontend/src/api/index.ts +++ b/content-gen/src/app/frontend/src/api/index.ts @@ -10,6 +10,7 @@ import type { AppConfig, } from '../types'; import httpClient from './httpClient'; +export { default as httpClient } from './httpClient'; import { parseSSEStream, getGenerationStage } from '../utils'; /** diff --git a/content-gen/src/app/frontend/src/hooks/useConversationActions.ts b/content-gen/src/app/frontend/src/hooks/useConversationActions.ts index 6cd0bfd01..48346600d 100644 --- a/content-gen/src/app/frontend/src/hooks/useConversationActions.ts +++ b/content-gen/src/app/frontend/src/hooks/useConversationActions.ts @@ -2,7 +2,7 @@ import { useCallback } from 'react'; import type { ChatMessage, Product, CreativeBrief } from '../types'; import { createMessage, buildGeneratedContent } from '../utils'; -import httpClient from '../api/httpClient'; +import { httpClient } from '../api'; import { useAppDispatch, useAppSelector, diff --git a/content-gen/src/app/frontend/src/store/chatHistorySlice.ts b/content-gen/src/app/frontend/src/store/chatHistorySlice.ts index 9874d45bc..b97b14e31 100644 --- a/content-gen/src/app/frontend/src/store/chatHistorySlice.ts +++ b/content-gen/src/app/frontend/src/store/chatHistorySlice.ts @@ -4,7 +4,7 @@ * Granular selectors for each piece of history state. */ import { createSlice, createAsyncThunk, type PayloadAction } from '@reduxjs/toolkit'; -import httpClient from '../api/httpClient'; +import { httpClient } from '../api'; export interface ConversationSummary { id: string; diff --git a/content-gen/src/app/frontend/src/store/index.ts b/content-gen/src/app/frontend/src/store/index.ts index 5e28f210c..7bbcd99d6 100644 --- a/content-gen/src/app/frontend/src/store/index.ts +++ b/content-gen/src/app/frontend/src/store/index.ts @@ -15,7 +15,6 @@ export { toggleChatHistory, setShowChatHistory, GenerationStatus, - GENERATION_STATUS_LABELS, } from './appSlice'; // Chat slice – actions @@ -55,7 +54,6 @@ export { selectUserId, selectUserName, selectIsLoading, - selectGenerationStatus, selectGenerationStatusLabel, selectImageGenerationEnabled, selectShowChatHistory, diff --git a/content-gen/src/app/frontend/src/utils/index.ts b/content-gen/src/app/frontend/src/utils/index.ts index aaefb147b..66eded22e 100644 --- a/content-gen/src/app/frontend/src/utils/index.ts +++ b/content-gen/src/app/frontend/src/utils/index.ts @@ -15,7 +15,6 @@ export { parseSSEStream } from './sseParser'; // Generation progress stages export { getGenerationStage } from './generationStages'; -export type { GenerationStage } from './generationStages'; // Brief-field metadata export { BRIEF_FIELD_LABELS, BRIEF_DISPLAY_ORDER, BRIEF_FIELD_KEYS } from './briefFields'; From f336f3bd8c312685d93f3fedce61da08ea2d236f Mon Sep 17 00:00:00 2001 From: Shubhangi-Microsoft Date: Tue, 3 Mar 2026 10:09:10 +0530 Subject: [PATCH 07/72] refactor: extract AI_DISCLAIMER constant, remove dead selector and trivial useMemo --- .../frontend/src/components/BriefReview.tsx | 4 ++-- .../frontend/src/components/ChatHistory.tsx | 5 +---- .../app/frontend/src/components/ChatInput.tsx | 3 ++- .../app/frontend/src/components/ChatPanel.tsx | 22 +++++-------------- .../src/components/ComplianceSection.tsx | 3 ++- .../frontend/src/components/MessageBubble.tsx | 3 ++- .../frontend/src/components/ProductCard.tsx | 4 ++-- .../frontend/src/components/ProductReview.tsx | 3 ++- .../src/app/frontend/src/store/selectors.ts | 1 - .../src/app/frontend/src/utils/index.ts | 3 +++ 10 files changed, 21 insertions(+), 30 deletions(-) diff --git a/content-gen/src/app/frontend/src/components/BriefReview.tsx b/content-gen/src/app/frontend/src/components/BriefReview.tsx index 627db0df9..b4a1ce1de 100644 --- a/content-gen/src/app/frontend/src/components/BriefReview.tsx +++ b/content-gen/src/app/frontend/src/components/BriefReview.tsx @@ -5,7 +5,7 @@ import { tokens, } from '@fluentui/react-components'; import type { CreativeBrief } from '../types'; -import { BRIEF_FIELD_LABELS, BRIEF_DISPLAY_ORDER, BRIEF_FIELD_KEYS } from '../utils'; +import { BRIEF_FIELD_LABELS, BRIEF_DISPLAY_ORDER, BRIEF_FIELD_KEYS, AI_DISCLAIMER } from '../utils'; interface BriefReviewProps { brief: CreativeBrief; @@ -154,7 +154,7 @@ export const BriefReview = memo(function BriefReview({ paddingTop: '8px', }}> - AI-generated content may be incorrect + {AI_DISCLAIMER}
    diff --git a/content-gen/src/app/frontend/src/components/ChatHistory.tsx b/content-gen/src/app/frontend/src/components/ChatHistory.tsx index 8faa1f24e..c393fdec9 100644 --- a/content-gen/src/app/frontend/src/components/ChatHistory.tsx +++ b/content-gen/src/app/frontend/src/components/ChatHistory.tsx @@ -142,10 +142,7 @@ export const ChatHistory = memo(function ChatHistory({ () => showAll ? displayConversations : displayConversations.slice(0, INITIAL_COUNT), [showAll, displayConversations], ); - const hasMore = useMemo( - () => displayConversations.length > INITIAL_COUNT, - [displayConversations.length], - ); + const hasMore = displayConversations.length > INITIAL_COUNT; const handleRefreshConversations = useCallback(() => { dispatch(fetchConversations()); diff --git a/content-gen/src/app/frontend/src/components/ChatInput.tsx b/content-gen/src/app/frontend/src/components/ChatInput.tsx index 778de0e29..5a3efb41f 100644 --- a/content-gen/src/app/frontend/src/components/ChatInput.tsx +++ b/content-gen/src/app/frontend/src/components/ChatInput.tsx @@ -5,6 +5,7 @@ import { Tooltip, tokens, } from '@fluentui/react-components'; +import { AI_DISCLAIMER } from '../utils'; import { Send20Regular, Add20Regular, @@ -140,7 +141,7 @@ export const ChatInput = memo(function ChatInput({ fontSize: '12px', }} > - AI generated content may be incorrect + {AI_DISCLAIMER}
    ); diff --git a/content-gen/src/app/frontend/src/components/ChatPanel.tsx b/content-gen/src/app/frontend/src/components/ChatPanel.tsx index c94792432..a10709cae 100644 --- a/content-gen/src/app/frontend/src/components/ChatPanel.tsx +++ b/content-gen/src/app/frontend/src/components/ChatPanel.tsx @@ -1,4 +1,4 @@ -import { useState, useMemo, useCallback, memo } from 'react'; +import { useState, useCallback, memo } from 'react'; import type { Product } from '../types'; import { BriefReview } from './BriefReview'; import { ConfirmedBriefView } from './ConfirmedBriefView'; @@ -62,22 +62,10 @@ export const ChatPanel = memo(function ChatPanel({ ]); // Determine if we should show inline components - const showBriefReview = useMemo( - () => !!(pendingBrief && onBriefConfirm && onBriefCancel), - [pendingBrief, onBriefConfirm, onBriefCancel], - ); - const showProductReview = useMemo( - () => !!(confirmedBrief && !generatedContent && onGenerateContent), - [confirmedBrief, generatedContent, onGenerateContent], - ); - const showContentPreview = useMemo( - () => !!(generatedContent && onRegenerateContent), - [generatedContent, onRegenerateContent], - ); - const showWelcome = useMemo( - () => messages.length === 0 && !showBriefReview && !showProductReview && !showContentPreview, - [messages.length, showBriefReview, showProductReview, showContentPreview], - ); + const showBriefReview = !!(pendingBrief && onBriefConfirm && onBriefCancel); + const showProductReview = !!(confirmedBrief && !generatedContent && onGenerateContent); + const showContentPreview = !!(generatedContent && onRegenerateContent); + const showWelcome = messages.length === 0 && !showBriefReview && !showProductReview && !showContentPreview; // Handle suggestion click from welcome card const handleSuggestionClick = useCallback((prompt: string) => { diff --git a/content-gen/src/app/frontend/src/components/ComplianceSection.tsx b/content-gen/src/app/frontend/src/components/ComplianceSection.tsx index f593a9f4d..216a11f60 100644 --- a/content-gen/src/app/frontend/src/components/ComplianceSection.tsx +++ b/content-gen/src/app/frontend/src/components/ComplianceSection.tsx @@ -1,4 +1,5 @@ import { memo } from 'react'; +import { AI_DISCLAIMER } from '../utils'; import { Text, Badge, @@ -145,7 +146,7 @@ export const ComplianceSection = memo(function ComplianceSection({ marginTop: '8px', }} > - AI-generated content may be incorrect + {AI_DISCLAIMER} {/* Collapsible Compliance Accordion */} diff --git a/content-gen/src/app/frontend/src/components/MessageBubble.tsx b/content-gen/src/app/frontend/src/components/MessageBubble.tsx index 95431a7d9..52340ba50 100644 --- a/content-gen/src/app/frontend/src/components/MessageBubble.tsx +++ b/content-gen/src/app/frontend/src/components/MessageBubble.tsx @@ -10,6 +10,7 @@ import { Copy20Regular } from '@fluentui/react-icons'; import ReactMarkdown from 'react-markdown'; import type { ChatMessage } from '../types'; import { useCopyToClipboard } from '../hooks'; +import { AI_DISCLAIMER } from '../utils'; export interface MessageBubbleProps { message: ChatMessage; @@ -91,7 +92,7 @@ export const MessageBubble = memo(function MessageBubble({ message }: MessageBub fontSize: '11px', }} > - AI-generated content may be incorrect + {AI_DISCLAIMER}
    diff --git a/content-gen/src/app/frontend/src/components/ProductCard.tsx b/content-gen/src/app/frontend/src/components/ProductCard.tsx index 050de19f7..873ae6620 100644 --- a/content-gen/src/app/frontend/src/components/ProductCard.tsx +++ b/content-gen/src/app/frontend/src/components/ProductCard.tsx @@ -1,4 +1,4 @@ -import { memo, useMemo } from 'react'; +import { memo } from 'react'; import { Text, tokens, @@ -30,7 +30,7 @@ export const ProductCard = memo(function ProductCard({ }: ProductCardProps) { const isCompact = size === 'compact'; const imgSize = isCompact ? 56 : 80; - const isInteractive = useMemo(() => !!onClick && !disabled, [onClick, disabled]); + const isInteractive = !!onClick && !disabled; return (
    - AI-generated content may be incorrect + {AI_DISCLAIMER}
    diff --git a/content-gen/src/app/frontend/src/store/selectors.ts b/content-gen/src/app/frontend/src/store/selectors.ts index fe122ee2d..a837844c5 100644 --- a/content-gen/src/app/frontend/src/store/selectors.ts +++ b/content-gen/src/app/frontend/src/store/selectors.ts @@ -9,7 +9,6 @@ import type { RootState } from './store'; export const selectUserId = (state: RootState) => state.app.userId; export const selectUserName = (state: RootState) => state.app.userName; export const selectIsLoading = (state: RootState) => state.app.isLoading; -export const selectGenerationStatus = (state: RootState) => state.app.generationStatus; export const selectGenerationStatusLabel = (state: RootState) => state.app.generationStatusLabel; export const selectImageGenerationEnabled = (state: RootState) => state.app.imageGenerationEnabled; export const selectShowChatHistory = (state: RootState) => state.app.showChatHistory; diff --git a/content-gen/src/app/frontend/src/utils/index.ts b/content-gen/src/app/frontend/src/utils/index.ts index 66eded22e..9b7b9e747 100644 --- a/content-gen/src/app/frontend/src/utils/index.ts +++ b/content-gen/src/app/frontend/src/utils/index.ts @@ -27,3 +27,6 @@ export { isContentFilterError, getErrorMessage } from './contentErrors'; // Image download export { downloadImage } from './downloadImage'; + +// Shared UI constants +export const AI_DISCLAIMER = 'AI-generated content may be incorrect'; From ddf6a0ca52d1ec8514629623e19f7bed1c31b7b7 Mon Sep 17 00:00:00 2001 From: Shubhangi-Microsoft Date: Tue, 3 Mar 2026 12:09:30 +0530 Subject: [PATCH 08/72] chore: remove 3 unused barrel exports (setShowChatHistory, parseTextContent, resolveImageUrl) --- content-gen/src/app/frontend/src/store/appSlice.ts | 5 +---- content-gen/src/app/frontend/src/store/index.ts | 1 - content-gen/src/app/frontend/src/utils/index.ts | 2 +- 3 files changed, 2 insertions(+), 6 deletions(-) diff --git a/content-gen/src/app/frontend/src/store/appSlice.ts b/content-gen/src/app/frontend/src/store/appSlice.ts index 265522ebd..6d94c5833 100644 --- a/content-gen/src/app/frontend/src/store/appSlice.ts +++ b/content-gen/src/app/frontend/src/store/appSlice.ts @@ -125,9 +125,6 @@ const appSlice = createSlice({ toggleChatHistory(state) { state.showChatHistory = !state.showChatHistory; }, - setShowChatHistory(state, action: PayloadAction) { - state.showChatHistory = action.payload; - }, }, extraReducers: (builder) => { builder @@ -148,6 +145,6 @@ const appSlice = createSlice({ }, }); -export const { setIsLoading, setGenerationStatus, toggleChatHistory, setShowChatHistory } = +export const { setIsLoading, setGenerationStatus, toggleChatHistory } = appSlice.actions; export default appSlice.reducer; diff --git a/content-gen/src/app/frontend/src/store/index.ts b/content-gen/src/app/frontend/src/store/index.ts index 7bbcd99d6..1a7a98623 100644 --- a/content-gen/src/app/frontend/src/store/index.ts +++ b/content-gen/src/app/frontend/src/store/index.ts @@ -13,7 +13,6 @@ export { setIsLoading, setGenerationStatus, toggleChatHistory, - setShowChatHistory, GenerationStatus, } from './appSlice'; diff --git a/content-gen/src/app/frontend/src/utils/index.ts b/content-gen/src/app/frontend/src/utils/index.ts index 9b7b9e747..94ba048c3 100644 --- a/content-gen/src/app/frontend/src/utils/index.ts +++ b/content-gen/src/app/frontend/src/utils/index.ts @@ -8,7 +8,7 @@ export { createMessage, createErrorMessage } from './messageUtils'; // Content parsing (raw API → typed domain objects) -export { parseTextContent, resolveImageUrl, buildGeneratedContent } from './contentParsing'; +export { buildGeneratedContent } from './contentParsing'; // SSE stream parser export { parseSSEStream } from './sseParser'; From 4f22f79235a4bf35f8e0e79faba9e6f423b4454f Mon Sep 17 00:00:00 2001 From: Shubhangi-Microsoft Date: Tue, 3 Mar 2026 17:31:45 +0530 Subject: [PATCH 09/72] refactor: eliminate remaining Task 2/6/8 issues - Route fetch('/.auth/me') through platformClient (HttpClient with empty baseUrl) - Un-export parseTextContent/resolveImageUrl (module-internal only) - DRY userId || 'anonymous' x6 into normalizeUserId() helper - Remove redundant duplicate prompt field from WelcomeCard suggestions --- .../src/app/frontend/src/api/httpClient.ts | 6 ++++++ content-gen/src/app/frontend/src/api/index.ts | 17 +++++++++++------ .../app/frontend/src/components/WelcomeCard.tsx | 7 ++----- .../src/app/frontend/src/store/appSlice.ts | 3 ++- .../app/frontend/src/utils/contentParsing.ts | 6 +++--- 5 files changed, 24 insertions(+), 15 deletions(-) diff --git a/content-gen/src/app/frontend/src/api/httpClient.ts b/content-gen/src/app/frontend/src/api/httpClient.ts index 87b271c18..4689a6d98 100644 --- a/content-gen/src/app/frontend/src/api/httpClient.ts +++ b/content-gen/src/app/frontend/src/api/httpClient.ts @@ -163,6 +163,12 @@ export class HttpClient { const httpClient = new HttpClient('/api'); +/** + * Client for Azure platform endpoints (/.auth/me, etc.) — no base URL prefix. + * Shares the same interceptor pattern but targets the host root. + */ +export const platformClient = new HttpClient('', 10_000); + // ---- request interceptor: auth headers ---- httpClient.onRequest((_url, init) => { const headers = new Headers(init.headers); diff --git a/content-gen/src/app/frontend/src/api/index.ts b/content-gen/src/app/frontend/src/api/index.ts index a7e46207b..4a0797318 100644 --- a/content-gen/src/app/frontend/src/api/index.ts +++ b/content-gen/src/app/frontend/src/api/index.ts @@ -13,6 +13,11 @@ import httpClient from './httpClient'; export { default as httpClient } from './httpClient'; import { parseSSEStream, getGenerationStage } from '../utils'; +/** Normalize optional userId to a safe fallback. */ +function normalizeUserId(userId?: string): string { + return userId || 'anonymous'; +} + /** * Get application configuration including feature flags */ @@ -32,7 +37,7 @@ export async function parseBrief( return httpClient.post('/brief/parse', { brief_text: briefText, conversation_id: conversationId, - user_id: userId || 'anonymous', + user_id: normalizeUserId(userId), }, { signal }); } @@ -47,7 +52,7 @@ export async function confirmBrief( return httpClient.post('/brief/confirm', { brief, conversation_id: conversationId, - user_id: userId || 'anonymous', + user_id: normalizeUserId(userId), }); } @@ -65,7 +70,7 @@ export async function selectProducts( request, current_products: currentProducts, conversation_id: conversationId, - user_id: userId || 'anonymous', + user_id: normalizeUserId(userId), }, { signal }); } @@ -85,7 +90,7 @@ export async function* streamChat( body: JSON.stringify({ message, conversation_id: conversationId, - user_id: userId || 'anonymous', + user_id: normalizeUserId(userId), }), }); @@ -118,7 +123,7 @@ export async function* streamGenerateContent( products: products || [], generate_images: generateImages, conversation_id: conversationId, - user_id: userId || 'anonymous', + user_id: normalizeUserId(userId), }, { signal }); const taskId = startData.task_id; @@ -210,7 +215,7 @@ export async function* streamRegenerateImage( products: products || [], previous_image_prompt: previousImagePrompt, conversation_id: conversationId, - user_id: userId || 'anonymous', + user_id: normalizeUserId(userId), }), }); diff --git a/content-gen/src/app/frontend/src/components/WelcomeCard.tsx b/content-gen/src/app/frontend/src/components/WelcomeCard.tsx index aa1388324..b56b781c4 100644 --- a/content-gen/src/app/frontend/src/components/WelcomeCard.tsx +++ b/content-gen/src/app/frontend/src/components/WelcomeCard.tsx @@ -9,19 +9,16 @@ import SecondPromptIcon from '../styles/images/secondprompt.png'; interface SuggestionData { title: string; - prompt: string; icon: string; } const suggestions: SuggestionData[] = [ { title: "I need to create a social media post about paint products for home remodels. The campaign is titled \"Brighten Your Springtime\" and the audience is new homeowners. I need marketing copy plus an image. The image should be an informal living room with tasteful furnishings.", - prompt: "I need to create a social media post about paint products for home remodels. The campaign is titled \"Brighten Your Springtime\" and the audience is new homeowners. I need marketing copy plus an image. The image should be an informal living room with tasteful furnishings.", icon: FirstPromptIcon, }, { title: "Generate a social media campaign with ad copy and an image. This is for \"Back to School\" and the audience is parents of school age children. Tone is playful and humorous. The image must have minimal kids accessories in a children's bedroom. Show the room in a wide view.", - prompt: "Generate a social media campaign with ad copy and an image. This is for \"Back to School\" and the audience is parents of school age children. Tone is playful and humorous. The image must have minimal kids accessories in a children's bedroom. Show the room in a wide view.", icon: SecondPromptIcon, } ]; @@ -33,7 +30,7 @@ interface WelcomeCardProps { export const WelcomeCard = memo(function WelcomeCard({ onSuggestionClick, currentInput = '' }: WelcomeCardProps) { const selectedIndex = useMemo( - () => suggestions.findIndex(s => s.prompt === currentInput), + () => suggestions.findIndex(s => s.title === currentInput), [currentInput], ); @@ -99,7 +96,7 @@ export const WelcomeCard = memo(function WelcomeCard({ onSuggestionClick, curren title={suggestion.title} icon={suggestion.icon} isSelected={isSelected} - onClick={() => onSuggestionClick(suggestion.prompt)} + onClick={() => onSuggestionClick(suggestion.title)} /> ); })} diff --git a/content-gen/src/app/frontend/src/store/appSlice.ts b/content-gen/src/app/frontend/src/store/appSlice.ts index 6d94c5833..0c2eda834 100644 --- a/content-gen/src/app/frontend/src/store/appSlice.ts +++ b/content-gen/src/app/frontend/src/store/appSlice.ts @@ -61,7 +61,8 @@ export const fetchAppConfig = createAsyncThunk( export const fetchUserInfo = createAsyncThunk( 'app/fetchUserInfo', async () => { - const response = await fetch('/.auth/me'); + const { platformClient } = await import('../api/httpClient'); + const response = await platformClient.raw('/.auth/me'); if (!response.ok) return { userId: 'anonymous', userName: '' }; const payload = await response.json(); diff --git a/content-gen/src/app/frontend/src/utils/contentParsing.ts b/content-gen/src/app/frontend/src/utils/contentParsing.ts index 629f887da..e59ac85e3 100644 --- a/content-gen/src/app/frontend/src/utils/contentParsing.ts +++ b/content-gen/src/app/frontend/src/utils/contentParsing.ts @@ -25,14 +25,14 @@ function rewriteBlobUrl(url: string): string { } /* ------------------------------------------------------------------ */ -/* Exported utilities */ +/* Parsing helpers (module-internal — not re-exported) */ /* ------------------------------------------------------------------ */ /** * Parse `text_content` which may arrive as a JSON string or an object. * Returns an object with known fields, or `undefined` if unusable. */ -export function parseTextContent( +function parseTextContent( raw: unknown, ): { headline?: string; body?: string; cta_text?: string; tagline?: string } | undefined { let textContent = raw; @@ -64,7 +64,7 @@ export function parseTextContent( * Pass `rewriteBlobs: true` (default) when restoring from a saved * conversation; `false` when the response just came from the live API. */ -export function resolveImageUrl( +function resolveImageUrl( raw: { image_url?: string; image_base64?: string }, rewriteBlobs = false, ): string | undefined { From 8a7d3b42e0ccd71c76314f0b7737b76029d8679c Mon Sep 17 00:00:00 2001 From: Shubhangi-Microsoft Date: Tue, 3 Mar 2026 17:46:53 +0530 Subject: [PATCH 10/72] refactor: extract readSSEResponse helper, un-export GENERATION_STATUS_LABELS --- content-gen/src/app/frontend/src/api/index.ts | 43 ++++++++++--------- .../src/app/frontend/src/store/appSlice.ts | 2 +- 2 files changed, 24 insertions(+), 21 deletions(-) diff --git a/content-gen/src/app/frontend/src/api/index.ts b/content-gen/src/app/frontend/src/api/index.ts index 4a0797318..5bb5d4017 100644 --- a/content-gen/src/app/frontend/src/api/index.ts +++ b/content-gen/src/app/frontend/src/api/index.ts @@ -18,6 +18,27 @@ function normalizeUserId(userId?: string): string { return userId || 'anonymous'; } +/** + * Validate an SSE response, extract its body reader, and yield parsed events. + * Consolidates the duplicated response → reader → parseSSEStream pipeline + * used by streamChat and streamRegenerateImage. + */ +async function* readSSEResponse( + response: Response, + context: string, +): AsyncGenerator { + if (!response.ok) { + throw new Error(`${context}: ${response.statusText}`); + } + + const reader = response.body?.getReader(); + if (!reader) { + throw new Error('No response body'); + } + + yield* parseSSEStream(reader); +} + /** * Get application configuration including feature flags */ @@ -94,16 +115,7 @@ export async function* streamChat( }), }); - if (!response.ok) { - throw new Error(`Chat request failed: ${response.statusText}`); - } - - const reader = response.body?.getReader(); - if (!reader) { - throw new Error('No response body'); - } - - yield* parseSSEStream(reader); + yield* readSSEResponse(response, 'Chat request failed'); } /** @@ -219,14 +231,5 @@ export async function* streamRegenerateImage( }), }); - if (!response.ok) { - throw new Error(`Regeneration request failed: ${response.statusText}`); - } - - const reader = response.body?.getReader(); - if (!reader) { - throw new Error('No response body'); - } - - yield* parseSSEStream(reader); + yield* readSSEResponse(response, 'Regeneration request failed'); } \ No newline at end of file diff --git a/content-gen/src/app/frontend/src/store/appSlice.ts b/content-gen/src/app/frontend/src/store/appSlice.ts index 0c2eda834..952b6ae1e 100644 --- a/content-gen/src/app/frontend/src/store/appSlice.ts +++ b/content-gen/src/app/frontend/src/store/appSlice.ts @@ -32,7 +32,7 @@ export enum GenerationStatus { } /** Display strings shown in the UI for each status. */ -export const GENERATION_STATUS_LABELS: Record = { +const GENERATION_STATUS_LABELS: Record = { [GenerationStatus.IDLE]: '', [GenerationStatus.UPDATING_BRIEF]: 'Updating creative brief...', [GenerationStatus.PROCESSING_QUESTION]: 'Processing your question...', From 1650f112a4ca029d0ab5ea833b8afa8e02465cbe Mon Sep 17 00:00:00 2001 From: Shubhangi-Microsoft Date: Fri, 13 Mar 2026 13:11:54 +0530 Subject: [PATCH 11/72] fix: wrap MessageBubble copy handler in useCallback --- .../src/app/frontend/src/components/MessageBubble.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/content-gen/src/app/frontend/src/components/MessageBubble.tsx b/content-gen/src/app/frontend/src/components/MessageBubble.tsx index 52340ba50..d4509a0c5 100644 --- a/content-gen/src/app/frontend/src/components/MessageBubble.tsx +++ b/content-gen/src/app/frontend/src/components/MessageBubble.tsx @@ -1,4 +1,4 @@ -import { memo } from 'react'; +import { memo, useCallback } from 'react'; import { Text, Badge, @@ -26,6 +26,7 @@ export interface MessageBubbleProps { export const MessageBubble = memo(function MessageBubble({ message }: MessageBubbleProps) { const isUser = message.role === 'user'; const { copied, copy } = useCopyToClipboard(); + const handleCopy = useCallback(() => copy(message.content), [copy, message.content]); return (
    } size="small" - onClick={() => copy(message.content)} + onClick={handleCopy} style={{ minWidth: '28px', height: '28px', From 10ede7ec41f925e5b680e1f4d2c4beafebd0b21a Mon Sep 17 00:00:00 2001 From: Shubhangi-Microsoft Date: Fri, 13 Mar 2026 17:12:00 +0530 Subject: [PATCH 12/72] fix: resolve 6 Copilot review comments on PR #768 - httpClient: replace require() with ESM dynamic import() for store access - useConversationActions: pass explicit undefined to resetChat() - parseBrief: route to /chat endpoint (no /brief/parse backend route) - streamChat: use httpClient.post + yield instead of SSE (backend returns JSON) - selectProducts: route to /chat endpoint (no /products/select backend route) - streamRegenerateImage: use /chat + polling instead of non-existent /regenerate SSE - remove unused readSSEResponse helper and parseSSEStream import --- .../src/app/frontend/optimization-report.html | 1244 +++++++++++++++++ .../src/app/frontend/src/api/httpClient.ts | 203 +++ content-gen/src/app/frontend/src/api/index.ts | 258 ++++ .../src/hooks/useConversationActions.ts | 2 +- src/app/frontend/package-lock.json | 8 +- src/app/frontend/package.json | 2 +- src/app/frontend/src/api/httpClient.ts | 8 +- src/app/frontend/src/api/index.ts | 121 +- .../src/hooks/useConversationActions.ts | 2 +- 9 files changed, 1788 insertions(+), 60 deletions(-) create mode 100644 content-gen/src/app/frontend/optimization-report.html create mode 100644 content-gen/src/app/frontend/src/api/httpClient.ts create mode 100644 content-gen/src/app/frontend/src/api/index.ts diff --git a/content-gen/src/app/frontend/optimization-report.html b/content-gen/src/app/frontend/optimization-report.html new file mode 100644 index 000000000..710c00e38 --- /dev/null +++ b/content-gen/src/app/frontend/optimization-report.html @@ -0,0 +1,1244 @@ + + + + + + Content Generation Frontend UI Refactorization KPI Report (dev vs psl-ui-refractoring) + + + +
    + +
    +

    🚀 Content Generation Frontend UI Refactorization KPI Report

    +

    Complete Technical Analysis & Implementation Details

    +

    Generated: July 2025 | Project: Content Generation Solution Accelerator

    + +
    +
    + 100% + Original UI Files Impacted +
    +
    + 4 + Redux Toolkit Slices Added +
    +
    + -48.29% + Original Components LOC Reduction +
    +
    + +29.87% + Net UI Delta Rate +
    +
    +
    + + +
    +

    📊 Executive Summary

    +

    Through comparison of origin/dev...psl-ui-refractoring in content-gen/src/app/frontend/src, this report captures measured UI refactorization impact with consistent diff-based KPIs.

    +
      +
    • 48 UI files changed (100% of original dev UI files touched)
    • +
    • 3,740 additions / 2,453 deletions (6,193 total churn)
    • +
    • +1,091 net lines in UI scope (+29.87% vs dev baseline)
    • +
    • Original components reduced by 1,130 lines (-48.29%)
    • +
    • 4 Redux slices added with full store modularization
    • +
    • 100% changed files are TypeScript (48/48 .ts/.tsx)
    • +
    + +
    +
    + Comparison Coverage Complete ✅ +
    +
    +
    + + +
    +

    🔄 Architecture: Before vs After

    + +

    Before: Monolithic Component Pattern

    +
    +
    Tightly Coupled UI Logic
    + +
    Large Monolith Components
    + +
    Mixed API + UI + State
    + +
    Harder Maintenance
    +
    + +

    After: Redux Toolkit + Custom Hooks + Modular Components

    +
    +
    Redux Store
    + +
    4 Typed Slices
    + +
    21 Granular Selectors
    + +
    7 Custom Hooks
    + +
    18 Memoized Components
    +
    + +

    Redux Slice Architecture

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    SliceFileResponsibilityKey Exports
    appSlicestore/appSlice.tsGlobal app-level UI stateTheme, layout, global flags
    chatSlicestore/chatSlice.tsChat state and async chat operationsMessages, streaming, SSE
    contentSlicestore/contentSlice.tsContent preview and generation statePreviews, generation stages
    chatHistorySlicestore/chatHistorySlice.tsConversation history and session dataHistory CRUD, persistence
    +
    + + +
    +

    📈 UI Refactoring Implementation

    + +
    +
    + + + +
    + +
    +

    Phase 1: Component Decomposition

    +

    Monolithic components (App.tsx at 846 lines, ChatHistory at 616 lines) were decomposed into focused, single-responsibility components. 10 new granular components were extracted.

    +
    +
    // Deleted
    +content-gen/src/app/frontend/src/api/index.ts (replaced by httpClient)
    +
    +// Major reductions
    +App.tsx:                846 → 72  lines (-91.5%)
    +ChatHistory.tsx:        616 → 327 lines (-46.9%)
    +InlineContentPreview:   528 → 196 lines (-62.9%)
    +ChatPanel.tsx:          425 → 159 lines (-62.6%)
    +
    +// New granular components (10 files, 1,220 lines)
    +components/AppHeader.tsx
    +components/ChatInput.tsx
    +components/ComplianceSection.tsx
    +components/ConversationItem.tsx
    +components/ImagePreviewCard.tsx
    +components/MessageBubble.tsx
    +components/ProductCard.tsx
    +components/SuggestionCard.tsx
    +components/TypingIndicator.tsx
    +components/ViolationCard.tsx
    +
    +
    + + + + +
    +
    + + +
    +

    📊 KPI 1: Codebase Overview

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    MetricBefore (dev)After (local)Delta
    Total UI source files1347+34 files
    Total source lines3,6524,743+1,091 (+29.87%)
    Files added35Added
    Files modified12Modified
    Files deleted1Legacy API file removed
    Line additions3,740+3,740
    Line deletions2,453−2,453
    +
    + + +
    +

    📊 KPI 2: Component Complexity (Lines of Code)

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    ComponentBefore (lines)After (lines)Reduction
    App.tsx84672−774 (−91.49%)
    ChatHistory.tsx616327−289 (−46.92%)
    InlineContentPreview.tsx528196−332 (−62.88%)
    ChatPanel.tsx425159−266 (−62.59%)
    api/index.ts321222−99 (−30.84%)
    ProductReview.tsx217128−89 (−41.01%)
    BriefReview.tsx177157−20 (−11.30%)
    WelcomeCard.tsx154103−51 (−33.12%)
    SelectedProductView.tsx13560−75 (−55.56%)
    ConfirmedBriefView.tsx8880−8 (−9.09%)
    + +

    Top Reduction Progress

    +
    +
    +

    App.tsx Reduction

    +
    +
    −91.49%
    +
    +
    +
    +

    InlineContentPreview.tsx Reduction

    +
    +
    −62.88%
    +
    +
    +
    +
    +
    +

    ChatPanel.tsx Reduction

    +
    +
    −62.59%
    +
    +
    +
    +

    SelectedProductView.tsx Reduction

    +
    +
    −55.56%
    +
    +
    +
    +
    +
    +

    ChatHistory.tsx Reduction

    +
    +
    −46.92%
    +
    +
    +
    +

    ProductReview.tsx Reduction

    +
    +
    −41.01%
    +
    +
    +
    +
    + + +
    +

    📊 KPI 3: Architecture & Modularity

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    MetricBeforeAfterDelta
    Custom hooks0 files7 files (797 lines)+7 hook modules
    State management files08 files (501 lines)+8 store modules
    Utility modules09 files (423 lines)+9 under utils/
    New components010 files (1,220 lines)+10 granular components
    Typed selectors021+21 centralized in selectors.ts
    + +

    New Files Created

    +
    +
    +

    State Management (8 files)

    +
      +
    • ✅ store/index.ts
    • +
    • ✅ store/store.ts
    • +
    • ✅ store/hooks.ts
    • +
    • ✅ store/selectors.ts
    • +
    • ✅ store/appSlice.ts
    • +
    • ✅ store/chatSlice.ts
    • +
    • ✅ store/contentSlice.ts
    • +
    • ✅ store/chatHistorySlice.ts
    • +
    +
    +
    +

    Custom Hooks (7 files)

    +
      +
    • ✅ hooks/index.ts
    • +
    • ✅ hooks/useAutoScroll.ts
    • +
    • ✅ hooks/useChatOrchestrator.ts
    • +
    • ✅ hooks/useContentGeneration.ts
    • +
    • ✅ hooks/useConversationActions.ts
    • +
    • ✅ hooks/useCopyToClipboard.ts
    • +
    • ✅ hooks/useWindowSize.ts
    • +
    +
    +
    +
    +
    +

    Utility Modules (9 files)

    +
      +
    • ✅ utils/index.ts
    • +
    • ✅ utils/briefFields.ts
    • +
    • ✅ utils/contentErrors.ts
    • +
    • ✅ utils/contentParsing.ts
    • +
    • ✅ utils/downloadImage.ts
    • +
    • ✅ utils/generationStages.ts
    • +
    • ✅ utils/messageUtils.ts
    • +
    • ✅ utils/sseParser.ts
    • +
    • ✅ utils/stringUtils.ts
    • +
    +
    +
    +

    New Components (10 files)

    +
      +
    • ✅ components/AppHeader.tsx (65 lines)
    • +
    • ✅ components/ChatInput.tsx (139 lines)
    • +
    • ✅ components/ComplianceSection.tsx (187 lines)
    • +
    • ✅ components/ConversationItem.tsx (276 lines)
    • +
    • ✅ components/ImagePreviewCard.tsx (95 lines)
    • +
    • ✅ components/MessageBubble.tsx (113 lines)
    • +
    • ✅ components/ProductCard.tsx (132 lines)
    • +
    • ✅ components/SuggestionCard.tsx (81 lines)
    • +
    • ✅ components/TypingIndicator.tsx (71 lines)
    • +
    • ✅ components/ViolationCard.tsx (61 lines)
    • +
    +
    +
    +
    + + +
    +

    📊 KPI 4: Bundle Size (Production Build)

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    ChunkBeforeAfterDelta
    Bundle KPINot measuredNot measuredBuild benchmark required
    Comparison basisgit diff origin/dev...HEAD in content-gen/src/app/frontend/srcDiff-based report
    Measured churn6,193 lines3,740 add / 2,453 del
    Net UI delta+1,091 lines+29.87% vs dev UI baseline
    StatusBundle size not included in this KPI setUse build artifacts for bundle KPI
    + +
    +

    💡 Note: This report is strictly based on branch diff metrics. Bundle-size numbers require production build output from both refs and are intentionally excluded here.

    +
    +
    + + +
    +

    📊 KPI 5: Code Quality Patterns

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    PatternBeforeAfterDelta
    memo( component wrappers018+18 memoized component wrappers
    useCallback(1326+13 callback memoization
    useMemo(09+9 derived value memoization
    createSlice (Redux Toolkit)04+4 typed state modules
    createAsyncThunk06+6 standardized async
    useAppDispatch013+13 typed dispatch hooks
    useAppSelector048+48 typed selector hooks
    displayName018+18 DevTools identifiers
    +
    + + +
    +

    🐛 KPI 6: Structural Risk Reductions

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    #Refactor OutcomeEvidence from DiffStatus
    1Monolith App.tsx decomposedApp.tsx reduced from 846 to 72 lines (−91.5%); logic extracted to hooks, store, and componentsCompleted
    2State flow centralizedAdded 4 Redux slices, 21 selectors, and typed hooks under store/Completed
    3Reusable utility layer introducedAdded 9 utility modules under utils/ (SSE parsing, content parsing, string utils)Completed
    4Chat rendering decomposedChatHistory (616→327), ChatPanel (425→159), extracted MessageBubble, ConversationItem, ChatInputCompleted
    5Content preview modularizedInlineContentPreview (528→196), extracted ComplianceSection, ViolationCard, ImagePreviewCardCompleted
    6API layer refactoredapi/index.ts reduced (321→222), SSE/HTTP client logic extracted to utils/sseParser.tsCompleted
    7Product flow decomposedProductReview (217→128), extracted ProductCard, SuggestionCard sub-componentsCompleted
    8Custom hooks extracted7 domain hooks created: useChatOrchestrator, useContentGeneration, useConversationActions, etc.Completed
    9Performance patterns applied18 memo() wrappers, 18 displayName identifiers, 26 useCallback, 9 useMemoCompleted
    10Type safety enforced0 TypeScript errors; typed Redux hooks (useAppSelector: 48 usages, useAppDispatch: 13 usages)Completed
    +
    + + +
    +

    ⚡ KPI 7: Refactorization and Quality Outcomes

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    OptimizationBeforeAfter
    Changed files in TypeScriptN/A48/48 (.ts/.tsx)
    Memoized components0 memo( patterns18 memo( patterns
    Callback stability13 useCallback(26 useCallback(
    Derived memoization0 useMemo(9 useMemo(
    Typed Redux hooks0 usages61 usages (13 dispatch + 48 selector)
    DevTools traceability0 displayName18 displayName identifiers
    Async state managementManual fetch + state6 createAsyncThunk actions
    + +

    Refactorization Delta Snapshot

    +
    +
    git diff --shortstat origin/dev...HEAD -- content-gen/src/app/frontend/src/
    +48 files changed, 3740 insertions(+), 2453 deletions(-)
    +
    +Change mix:
    +A=35 (72.92%)
    +M=12 (25.00%)
    +D=1  (2.08%)
    +
    +
    + + +
    +

    🎯 Summary Scorecard

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    KPIBeforeAfterVerdict
    UI files impacted13 files48 changed100% of original files
    State managementNo Redux slicesRedux Toolkit (4 slices)Centralized
    Typed selectors021New capability
    Render optimization0 memo( patterns18 memo( patternsProduction-grade
    Net UI delta rate+29.87%Growth from modularization
    Original components LOC2,3401,210-48.29%
    TypeScript changed-file footprint48/48100%
    TypeScript build errors00Clean build
    + +
    +
    + 48 + Files Changed +
    +
    + 35/12/1 + Added / Modified / Deleted +
    +
    + +1,091 + Net UI Lines +
    +
    + 100% + Changed Files in TS/TSX +
    +
    +
    + + +
    +

    ✨ Best Practices Implemented

    + +
    +
    +

    State Management

    +
      +
    • Redux Toolkit (official)
    • +
    • 4 typed slices
    • +
    • 6 createAsyncThunk actions
    • +
    • 21 centralized selectors
    • +
    • Type-safe hooks
    • +
    +
    + +
    +

    Performance

    +
      +
    • 18x memo() wrappers
    • +
    • 26x useCallback()
    • +
    • 9x useMemo()
    • +
    • 48x useAppSelector
    • +
    • Original LOC −48.29%
    • +
    +
    + +
    +

    Code Quality

    +
      +
    • Single Responsibility
    • +
    • DRY Principle
    • +
    • 100% TypeScript
    • +
    • 18 displayName identifiers
    • +
    • 0 build errors
    • +
    +
    + +
    +

    Architecture

    +
      +
    • 7 custom hooks
    • +
    • 9 utility modules
    • +
    • 10 new components
    • +
    • Store/hooks/utils layers
    • +
    • Typed selector & thunk usage
    • +
    +
    +
    +
    + + + +
    + + + + diff --git a/content-gen/src/app/frontend/src/api/httpClient.ts b/content-gen/src/app/frontend/src/api/httpClient.ts new file mode 100644 index 000000000..6aaf83b48 --- /dev/null +++ b/content-gen/src/app/frontend/src/api/httpClient.ts @@ -0,0 +1,203 @@ +/** + * Centralized HTTP client with interceptors. + * + * - Singleton — use the default `httpClient` export everywhere. + * - Request interceptors automatically attach auth headers + * (X-Ms-Client-Principal-Id) so callers never need to remember. + * - Response interceptors provide uniform error handling. + * - Built-in query-param serialization, configurable timeout, and base URL. + */ + +/* ------------------------------------------------------------------ */ +/* Types */ +/* ------------------------------------------------------------------ */ + +/** Options accepted by every request method. */ +export interface RequestOptions extends Omit { + /** Query parameters – appended to the URL automatically. */ + params?: Record; + /** Per-request timeout in ms (default: client-level `timeout`). */ + timeout?: number; +} + +type RequestInterceptor = (url: string, init: RequestInit) => RequestInit | Promise; +type ResponseInterceptor = (response: Response) => Response | Promise; + +/* ------------------------------------------------------------------ */ +/* HttpClient */ +/* ------------------------------------------------------------------ */ + +export class HttpClient { + private baseUrl: string; + private defaultTimeout: number; + private requestInterceptors: RequestInterceptor[] = []; + private responseInterceptors: ResponseInterceptor[] = []; + + constructor(baseUrl = '', timeout = 60_000) { + this.baseUrl = baseUrl; + this.defaultTimeout = timeout; + } + + /* ---------- interceptor registration ---------- */ + + onRequest(fn: RequestInterceptor): void { + this.requestInterceptors.push(fn); + } + + onResponse(fn: ResponseInterceptor): void { + this.responseInterceptors.push(fn); + } + + /* ---------- public request helpers ---------- */ + + async get(path: string, opts: RequestOptions = {}): Promise { + const res = await this.request(path, { ...opts, method: 'GET' }); + return res.json() as Promise; + } + + async post(path: string, body?: unknown, opts: RequestOptions = {}): Promise { + const res = await this.request(path, { + ...opts, + method: 'POST', + body: body != null ? JSON.stringify(body) : undefined, + headers: { + ...(body != null ? { 'Content-Type': 'application/json' } : {}), + ...opts.headers, + }, + }); + return res.json() as Promise; + } + + async put(path: string, body?: unknown, opts: RequestOptions = {}): Promise { + const res = await this.request(path, { + ...opts, + method: 'PUT', + body: body != null ? JSON.stringify(body) : undefined, + headers: { + ...(body != null ? { 'Content-Type': 'application/json' } : {}), + ...opts.headers, + }, + }); + return res.json() as Promise; + } + + async delete(path: string, opts: RequestOptions = {}): Promise { + const res = await this.request(path, { ...opts, method: 'DELETE' }); + return res.json() as Promise; + } + + /** + * Low-level request that returns the raw `Response`. + * Useful for streaming (SSE) endpoints where the caller needs `response.body`. + */ + async raw(path: string, opts: RequestOptions & { method?: string; body?: BodyInit | null } = {}): Promise { + return this.request(path, opts); + } + + /* ---------- internal plumbing ---------- */ + + private buildUrl(path: string, params?: Record): string { + const url = `${this.baseUrl}${path}`; + if (!params) return url; + + const qs = new URLSearchParams(); + for (const [key, value] of Object.entries(params)) { + if (value !== undefined) { + qs.set(key, String(value)); + } + } + const queryString = qs.toString(); + return queryString ? `${url}?${queryString}` : url; + } + + private async request(path: string, opts: RequestOptions & { method?: string; body?: BodyInit | null } = {}): Promise { + const { params, timeout, ...fetchOpts } = opts; + const url = this.buildUrl(path, params); + const effectiveTimeout = timeout ?? this.defaultTimeout; + + // Build the init object + let init: RequestInit = { ...fetchOpts }; + + // Run request interceptors + for (const interceptor of this.requestInterceptors) { + init = await interceptor(url, init); + } + + // Timeout via AbortController (merged with caller-supplied signal) + const timeoutCtrl = new AbortController(); + const callerSignal = init.signal; + + // If caller already passed a signal, listen for its abort + if (callerSignal) { + if (callerSignal.aborted) { + timeoutCtrl.abort(callerSignal.reason); + } else { + callerSignal.addEventListener('abort', () => timeoutCtrl.abort(callerSignal.reason), { once: true }); + } + } + + const timer = effectiveTimeout > 0 + ? setTimeout(() => timeoutCtrl.abort(new DOMException('Request timed out', 'TimeoutError')), effectiveTimeout) + : undefined; + + init.signal = timeoutCtrl.signal; + + try { + let response = await fetch(url, init); + + // Run response interceptors + for (const interceptor of this.responseInterceptors) { + response = await interceptor(response); + } + + return response; + } finally { + if (timer !== undefined) clearTimeout(timer); + } + } +} + +/* ------------------------------------------------------------------ */ +/* Singleton instance with default interceptors */ +/* ------------------------------------------------------------------ */ + +const httpClient = new HttpClient('/api'); + +/** + * Client for Azure platform endpoints (/.auth/me, etc.) — no base URL prefix. + * Shares the same interceptor pattern but targets the host root. + */ +export const platformClient = new HttpClient('', 10_000); + +// ---- request interceptor: auth headers ---- +httpClient.onRequest(async (_url, init) => { + const headers = new Headers(init.headers); + + // Attach userId from Redux store (lazy import to avoid circular deps). + // Falls back to 'anonymous' if store isn't ready yet. + try { + const { store } = await import('../store/store'); + const state = store?.getState?.(); + const userId: string = state?.app?.userId ?? 'anonymous'; + headers.set('X-Ms-Client-Principal-Id', userId); + } catch { + headers.set('X-Ms-Client-Principal-Id', 'anonymous'); + } + + return { ...init, headers }; +}); + +// ---- response interceptor: uniform error handling ---- +httpClient.onResponse((response) => { + if (!response.ok) { + // Don't throw for streaming endpoints — callers handle those manually. + // Clone so the body remains readable for callers that want custom handling. + const cloned = response.clone(); + console.error( + `[httpClient] ${response.status} ${response.statusText} – ${cloned.url}`, + ); + } + return response; +}); + +export default httpClient; diff --git a/content-gen/src/app/frontend/src/api/index.ts b/content-gen/src/app/frontend/src/api/index.ts new file mode 100644 index 000000000..b0c229ce9 --- /dev/null +++ b/content-gen/src/app/frontend/src/api/index.ts @@ -0,0 +1,258 @@ +/** + * API service for interacting with the Content Generation backend + */ + +import type { + CreativeBrief, + Product, + AgentResponse, + ParsedBriefResponse, + AppConfig, +} from '../types'; +import httpClient from './httpClient'; +export { default as httpClient } from './httpClient'; +import { getGenerationStage } from '../utils'; + +/** Normalize optional userId to a safe fallback. */ +function normalizeUserId(userId?: string): string { + return userId || 'anonymous'; +} + +/** + * Get application configuration including feature flags + */ +export async function getAppConfig(): Promise { + return httpClient.get('/config'); +} + +/** + * Parse a free-text creative brief into structured format + */ +export async function parseBrief( + briefText: string, + conversationId?: string, + userId?: string, + signal?: AbortSignal +): Promise { + return httpClient.post('/chat', { + message: briefText, + conversation_id: conversationId, + user_id: normalizeUserId(userId), + }, { signal }); +} + +/** + * Confirm a parsed creative brief + */ +export async function confirmBrief( + brief: CreativeBrief, + conversationId?: string, + userId?: string +): Promise<{ status: string; conversation_id: string; brief: CreativeBrief }> { + return httpClient.post('/brief/confirm', { + brief, + conversation_id: conversationId, + user_id: normalizeUserId(userId), + }); +} + +/** + * Select or modify products via natural language + */ +export async function selectProducts( + request: string, + currentProducts: Product[], + conversationId?: string, + userId?: string, + signal?: AbortSignal +): Promise<{ products: Product[]; action: string; message: string; conversation_id: string }> { + return httpClient.post('/chat', { + message: request, + current_products: currentProducts, + conversation_id: conversationId, + user_id: normalizeUserId(userId), + }, { signal }); +} + +/** + * Stream chat messages from the agent orchestration. + * + * Note: The /chat endpoint returns JSON (not SSE), so we perform a standard + * POST request and yield the single AgentResponse result. + */ +export async function* streamChat( + message: string, + conversationId?: string, + userId?: string, + signal?: AbortSignal +): AsyncGenerator { + const result = await httpClient.post( + '/chat', + { + message, + conversation_id: conversationId, + user_id: normalizeUserId(userId), + }, + { signal }, + ); + + // Preserve async-iterator interface by yielding the single JSON response. + yield result; +} + +/** + * Generate content from a confirmed brief + */ +export async function* streamGenerateContent( + brief: CreativeBrief, + products?: Product[], + generateImages: boolean = true, + conversationId?: string, + userId?: string, + signal?: AbortSignal +): AsyncGenerator { + // Use polling-based approach for reliability with long-running tasks + const startData = await httpClient.post<{ task_id: string }>('/generate/start', { + brief, + products: products || [], + generate_images: generateImages, + conversation_id: conversationId, + user_id: normalizeUserId(userId), + }, { signal }); + const taskId = startData.task_id; + + // Yield initial status + yield { + type: 'status', + content: 'Generation started...', + is_final: false, + } as AgentResponse; + + // Poll for completion + let attempts = 0; + const maxAttempts = 600; // 10 minutes max with 1-second polling (image generation can take 3-5 min) + const pollInterval = 1000; // 1 second + + while (attempts < maxAttempts) { + // Check if cancelled before waiting + if (signal?.aborted) { + throw new DOMException('Generation cancelled by user', 'AbortError'); + } + + await new Promise(resolve => setTimeout(resolve, pollInterval)); + attempts++; + + // Check if cancelled after waiting + if (signal?.aborted) { + throw new DOMException('Generation cancelled by user', 'AbortError'); + } + + try { + const statusData = await httpClient.get<{ status: string; result?: unknown; error?: string }>( + `/generate/status/${taskId}`, + { signal }, + ); + + if (statusData.status === 'completed') { + // Yield the final result + yield { + type: 'agent_response', + content: JSON.stringify(statusData.result), + is_final: true, + } as AgentResponse; + return; + } else if (statusData.status === 'failed') { + throw new Error(statusData.error || 'Generation failed'); + } else if (statusData.status === 'running') { + const elapsedSeconds = attempts; + const { stage, message: stageMessage } = getGenerationStage(elapsedSeconds); + + // Send status update every second for smoother progress + yield { + type: 'heartbeat', + content: stageMessage, + count: stage, + elapsed: elapsedSeconds, + is_final: false, + } as AgentResponse; + } + } catch (error) { + // Continue polling on transient errors + if (attempts >= maxAttempts) { + throw error; + } + } + } + + throw new Error('Generation timed out after 10 minutes'); +} +/** + * Regenerate image with a modification request + * Used when user wants to change the generated image after initial content generation + */ +export async function* streamRegenerateImage( + modificationRequest: string, + _brief: CreativeBrief, + products?: Product[], + _previousImagePrompt?: string, + conversationId?: string, + userId?: string, + signal?: AbortSignal +): AsyncGenerator { + // Image regeneration uses the unified /chat endpoint with MODIFY_IMAGE intent, + // which returns a task_id for polling via /generate/status. + const startData = await httpClient.post<{ action_type: string; data: { task_id: string; poll_url: string }; conversation_id: string }>( + '/chat', + { + message: modificationRequest, + conversation_id: conversationId, + user_id: normalizeUserId(userId), + selected_products: products || [], + has_generated_content: true, + }, + { signal }, + ); + + const taskId = startData.data?.task_id; + if (!taskId) { + // If no task_id, the response is the final result itself + yield { type: 'agent_response', content: JSON.stringify(startData), is_final: true } as AgentResponse; + return; + } + + yield { type: 'status', content: 'Regeneration started...', is_final: false } as AgentResponse; + + let attempts = 0; + const maxAttempts = 600; + const pollInterval = 1000; + + while (attempts < maxAttempts) { + if (signal?.aborted) { + throw new DOMException('Regeneration cancelled by user', 'AbortError'); + } + + await new Promise(resolve => setTimeout(resolve, pollInterval)); + attempts++; + + if (signal?.aborted) { + throw new DOMException('Regeneration cancelled by user', 'AbortError'); + } + + const statusData = await httpClient.get<{ status: string; result?: unknown; error?: string }>( + `/generate/status/${taskId}`, + { signal }, + ); + + if (statusData.status === 'completed') { + yield { type: 'agent_response', content: JSON.stringify(statusData.result), is_final: true } as AgentResponse; + return; + } else if (statusData.status === 'failed') { + throw new Error(statusData.error || 'Regeneration failed'); + } else { + const { stage, message: stageMessage } = getGenerationStage(attempts); + yield { type: 'heartbeat', content: stageMessage, count: stage, elapsed: attempts, is_final: false } as AgentResponse; + } + } + + throw new Error('Regeneration timed out after 10 minutes'); +} \ No newline at end of file diff --git a/content-gen/src/app/frontend/src/hooks/useConversationActions.ts b/content-gen/src/app/frontend/src/hooks/useConversationActions.ts index 48346600d..5dd8e87b9 100644 --- a/content-gen/src/app/frontend/src/hooks/useConversationActions.ts +++ b/content-gen/src/app/frontend/src/hooks/useConversationActions.ts @@ -130,7 +130,7 @@ export function useConversationActions() { /* Start a new conversation */ /* ------------------------------------------------------------ */ const newConversation = useCallback(() => { - dispatch(resetChat()); + dispatch(resetChat(undefined)); dispatch(resetContent()); }, [dispatch]); diff --git a/src/app/frontend/package-lock.json b/src/app/frontend/package-lock.json index 75cb9c13b..392884c07 100644 --- a/src/app/frontend/package-lock.json +++ b/src/app/frontend/package-lock.json @@ -9,7 +9,7 @@ "version": "1.0.0", "dependencies": { "@fluentui/react-components": "^9.54.0", - "@fluentui/react-icons": "^2.0.245", + "@fluentui/react-icons": "^2.0.320", "@reduxjs/toolkit": "^2.11.2", "react": "^18.3.1", "react-dom": "^18.3.1", @@ -1423,9 +1423,9 @@ } }, "node_modules/@fluentui/react-icons": { - "version": "2.0.315", - "resolved": "https://registry.npmjs.org/@fluentui/react-icons/-/react-icons-2.0.315.tgz", - "integrity": "sha512-IITWAQGgU7I32eHPDHi+TUCUF6malP27wZLUV3bqjGVF/x/lfxvTIx8yqv/cxuwF3+ITGFDpl+278ZYJtOI7ww==", + "version": "2.0.320", + "resolved": "https://registry.npmjs.org/@fluentui/react-icons/-/react-icons-2.0.320.tgz", + "integrity": "sha512-NU4gErPeaTD/T6Z9g3Uvp898lIFS6fDLr3++vpT8pcI4Ds0fZqQdrwNi3dF0R/SVws8DXQaRYiGlPHxszo4J4g==", "license": "MIT", "dependencies": { "@griffel/react": "^1.0.0", diff --git a/src/app/frontend/package.json b/src/app/frontend/package.json index d7b11ff63..d70638e23 100644 --- a/src/app/frontend/package.json +++ b/src/app/frontend/package.json @@ -11,7 +11,7 @@ }, "dependencies": { "@fluentui/react-components": "^9.54.0", - "@fluentui/react-icons": "^2.0.245", + "@fluentui/react-icons": "^2.0.320", "@reduxjs/toolkit": "^2.11.2", "react": "^18.3.1", "react-dom": "^18.3.1", diff --git a/src/app/frontend/src/api/httpClient.ts b/src/app/frontend/src/api/httpClient.ts index 4689a6d98..6aaf83b48 100644 --- a/src/app/frontend/src/api/httpClient.ts +++ b/src/app/frontend/src/api/httpClient.ts @@ -170,15 +170,15 @@ const httpClient = new HttpClient('/api'); export const platformClient = new HttpClient('', 10_000); // ---- request interceptor: auth headers ---- -httpClient.onRequest((_url, init) => { +httpClient.onRequest(async (_url, init) => { const headers = new Headers(init.headers); // Attach userId from Redux store (lazy import to avoid circular deps). // Falls back to 'anonymous' if store isn't ready yet. try { - // eslint-disable-next-line @typescript-eslint/no-require-imports - const { store } = require('../store/store'); - const userId: string = store.getState().app.userId || 'anonymous'; + const { store } = await import('../store/store'); + const state = store?.getState?.(); + const userId: string = state?.app?.userId ?? 'anonymous'; headers.set('X-Ms-Client-Principal-Id', userId); } catch { headers.set('X-Ms-Client-Principal-Id', 'anonymous'); diff --git a/src/app/frontend/src/api/index.ts b/src/app/frontend/src/api/index.ts index 5bb5d4017..b0c229ce9 100644 --- a/src/app/frontend/src/api/index.ts +++ b/src/app/frontend/src/api/index.ts @@ -11,34 +11,13 @@ import type { } from '../types'; import httpClient from './httpClient'; export { default as httpClient } from './httpClient'; -import { parseSSEStream, getGenerationStage } from '../utils'; +import { getGenerationStage } from '../utils'; /** Normalize optional userId to a safe fallback. */ function normalizeUserId(userId?: string): string { return userId || 'anonymous'; } -/** - * Validate an SSE response, extract its body reader, and yield parsed events. - * Consolidates the duplicated response → reader → parseSSEStream pipeline - * used by streamChat and streamRegenerateImage. - */ -async function* readSSEResponse( - response: Response, - context: string, -): AsyncGenerator { - if (!response.ok) { - throw new Error(`${context}: ${response.statusText}`); - } - - const reader = response.body?.getReader(); - if (!reader) { - throw new Error('No response body'); - } - - yield* parseSSEStream(reader); -} - /** * Get application configuration including feature flags */ @@ -55,8 +34,8 @@ export async function parseBrief( userId?: string, signal?: AbortSignal ): Promise { - return httpClient.post('/brief/parse', { - brief_text: briefText, + return httpClient.post('/chat', { + message: briefText, conversation_id: conversationId, user_id: normalizeUserId(userId), }, { signal }); @@ -87,8 +66,8 @@ export async function selectProducts( userId?: string, signal?: AbortSignal ): Promise<{ products: Product[]; action: string; message: string; conversation_id: string }> { - return httpClient.post('/products/select', { - request, + return httpClient.post('/chat', { + message: request, current_products: currentProducts, conversation_id: conversationId, user_id: normalizeUserId(userId), @@ -96,7 +75,10 @@ export async function selectProducts( } /** - * Stream chat messages from the agent orchestration + * Stream chat messages from the agent orchestration. + * + * Note: The /chat endpoint returns JSON (not SSE), so we perform a standard + * POST request and yield the single AgentResponse result. */ export async function* streamChat( message: string, @@ -104,18 +86,18 @@ export async function* streamChat( userId?: string, signal?: AbortSignal ): AsyncGenerator { - const response = await httpClient.raw('/chat', { - method: 'POST', - signal, - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ + const result = await httpClient.post( + '/chat', + { message, conversation_id: conversationId, user_id: normalizeUserId(userId), - }), - }); + }, + { signal }, + ); - yield* readSSEResponse(response, 'Chat request failed'); + // Preserve async-iterator interface by yielding the single JSON response. + yield result; } /** @@ -210,26 +192,67 @@ export async function* streamGenerateContent( */ export async function* streamRegenerateImage( modificationRequest: string, - brief: CreativeBrief, + _brief: CreativeBrief, products?: Product[], - previousImagePrompt?: string, + _previousImagePrompt?: string, conversationId?: string, userId?: string, signal?: AbortSignal ): AsyncGenerator { - const response = await httpClient.raw('/regenerate', { - method: 'POST', - signal, - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - modification_request: modificationRequest, - brief, - products: products || [], - previous_image_prompt: previousImagePrompt, + // Image regeneration uses the unified /chat endpoint with MODIFY_IMAGE intent, + // which returns a task_id for polling via /generate/status. + const startData = await httpClient.post<{ action_type: string; data: { task_id: string; poll_url: string }; conversation_id: string }>( + '/chat', + { + message: modificationRequest, conversation_id: conversationId, user_id: normalizeUserId(userId), - }), - }); + selected_products: products || [], + has_generated_content: true, + }, + { signal }, + ); + + const taskId = startData.data?.task_id; + if (!taskId) { + // If no task_id, the response is the final result itself + yield { type: 'agent_response', content: JSON.stringify(startData), is_final: true } as AgentResponse; + return; + } + + yield { type: 'status', content: 'Regeneration started...', is_final: false } as AgentResponse; + + let attempts = 0; + const maxAttempts = 600; + const pollInterval = 1000; + + while (attempts < maxAttempts) { + if (signal?.aborted) { + throw new DOMException('Regeneration cancelled by user', 'AbortError'); + } + + await new Promise(resolve => setTimeout(resolve, pollInterval)); + attempts++; + + if (signal?.aborted) { + throw new DOMException('Regeneration cancelled by user', 'AbortError'); + } + + const statusData = await httpClient.get<{ status: string; result?: unknown; error?: string }>( + `/generate/status/${taskId}`, + { signal }, + ); + + if (statusData.status === 'completed') { + yield { type: 'agent_response', content: JSON.stringify(statusData.result), is_final: true } as AgentResponse; + return; + } else if (statusData.status === 'failed') { + throw new Error(statusData.error || 'Regeneration failed'); + } else { + const { stage, message: stageMessage } = getGenerationStage(attempts); + yield { type: 'heartbeat', content: stageMessage, count: stage, elapsed: attempts, is_final: false } as AgentResponse; + } + } - yield* readSSEResponse(response, 'Regeneration request failed'); + throw new Error('Regeneration timed out after 10 minutes'); } \ No newline at end of file diff --git a/src/app/frontend/src/hooks/useConversationActions.ts b/src/app/frontend/src/hooks/useConversationActions.ts index 48346600d..5dd8e87b9 100644 --- a/src/app/frontend/src/hooks/useConversationActions.ts +++ b/src/app/frontend/src/hooks/useConversationActions.ts @@ -130,7 +130,7 @@ export function useConversationActions() { /* Start a new conversation */ /* ------------------------------------------------------------ */ const newConversation = useCallback(() => { - dispatch(resetChat()); + dispatch(resetChat(undefined)); dispatch(resetContent()); }, [dispatch]); From 1252f6f0a115c677c1a3418f8a938d296f907a7b Mon Sep 17 00:00:00 2001 From: v-maddukuriy Date: Tue, 17 Mar 2026 17:21:38 +0530 Subject: [PATCH 13/72] docs: add VM size recommendations for Bastion and Jumpbox subnets in virtualNetwork.bicep --- docs/CustomizingAzdParameters.md | 3 + infra/main.bicep | 58 + infra/main.json | 8926 +++++++++++++++++++++++++++- infra/modules/virtualNetwork.bicep | 8 + 4 files changed, 8976 insertions(+), 19 deletions(-) diff --git a/docs/CustomizingAzdParameters.md b/docs/CustomizingAzdParameters.md index f2adcca3a..f9a7f729d 100644 --- a/docs/CustomizingAzdParameters.md +++ b/docs/CustomizingAzdParameters.md @@ -21,6 +21,9 @@ By default this template will use the environment name as the prefix to prevent | `AZURE_ENV_OPENAI_LOCATION` | string | `` | Sets the Azure region for OpenAI resource deployment. | | `AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID` | string | `""` | Reuses an existing Log Analytics Workspace instead of creating a new one. | | `AZURE_EXISTING_AI_PROJECT_RESOURCE_ID`| string | `""` | Reuses an existing AI Foundry Project instead of creating a new one. | +| `AZURE_ENV_VM_SIZE` | string | `Standard_D2s_v5` | Overrides the jumpbox VM size (private networking only). Must support accelerated networking. | +| `AZURE_ENV_VM_ADMIN_USERNAME` | string | `JumpboxAdminUser` | Sets the jumpbox VM admin username (private networking only). | +| `AZURE_ENV_VM_ADMIN_PASSWORD` | string | *(generated)* | Sets the jumpbox VM admin password (private networking only). | | `ACR_NAME` | string | `contentgencontainerreg` | Sets the existing Azure Container Registry name (without `.azurecr.io`). | | `IMAGE_TAG` | string | `latest` | Sets the container image tag (e.g., `latest`, `dev`, `hotfix`). | diff --git a/infra/main.bicep b/infra/main.bicep index 6c8de7860..d2df507bd 100644 --- a/infra/main.bicep +++ b/infra/main.bicep @@ -110,6 +110,16 @@ param azureExistingAIProjectResourceId string = '' @description('Optional. Deploy Azure Bastion and Jumpbox VM for private network administration.') param deployBastionAndJumpbox bool = false +@description('Optional. Jumpbox VM size. Must support accelerated networking and Premium SSD.') +param vmSize string = '' + +@description('Optional. Jumpbox VM admin username.') +param vmAdminUsername string = '' + +@description('Optional. Jumpbox VM admin password.') +@secure() +param vmAdminPassword string = '' + @description('Optional. The tags to apply to all deployed Azure resources.') param tags object = {} @@ -873,6 +883,54 @@ module containerInstance 'modules/container-instance.bicep' = { } } +// ========== Jumpbox VM ========== // +var jumpboxVmName = 'vm-jumpbox-${solutionSuffix}' +module jumpboxVM 'br/public:avm/res/compute/virtual-machine:0.20.0' = if (enablePrivateNetworking && deployBastionAndJumpbox) { + name: take('avm.res.compute.virtual-machine.${jumpboxVmName}', 64) + params: { + name: take(jumpboxVmName, 15) + enableTelemetry: enableTelemetry + computerName: take(jumpboxVmName, 15) + osType: 'Windows' + vmSize: empty(vmSize) ? 'Standard_D2s_v5' : vmSize + adminUsername: empty(vmAdminUsername) ? 'JumpboxAdminUser' : vmAdminUsername + adminPassword: empty(vmAdminPassword) ? 'JumpboxAdminP@ssw0rd1234!' : vmAdminPassword + managedIdentities: { + userAssignedResourceIds: [ + userAssignedIdentity.outputs.resourceId + ] + } + availabilityZone: 1 + imageReference: { + publisher: 'MicrosoftWindowsDesktop' + offer: 'windows-11' + sku: 'win11-24h2-pro' + version: 'latest' + } + nicConfigurations: [ + { + name: 'nic-${jumpboxVmName}' + enableAcceleratedNetworking: true + ipConfigurations: [ + { + name: 'ipconfig01' + subnetResourceId: virtualNetwork!.outputs.jumpboxSubnetResourceId + } + ] + } + ] + osDisk: { + caching: 'ReadWrite' + diskSizeGB: 128 + managedDisk: { + storageAccountType: 'Premium_LRS' + } + } + location: solutionLocation + tags: tags + } +} + // ========== Outputs ========== // @description('Contains App Service Name') output APP_SERVICE_NAME string = webSite.outputs.name diff --git a/infra/main.json b/infra/main.json index 1132011bf..9cb2487b1 100644 --- a/infra/main.json +++ b/infra/main.json @@ -5,8 +5,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.41.2.15936", - "templateHash": "15058520269193157842" + "version": "0.39.26.7824", + "templateHash": "16548727235263926299" }, "name": "Intelligent Content Generation Accelerator", "description": "Solution Accelerator for multimodal marketing content generation using Microsoft Agent Framework.\n" @@ -176,6 +176,27 @@ "description": "Optional. Deploy Azure Bastion and Jumpbox VM for private network administration." } }, + "vmSize": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "Optional. Jumpbox VM size. Must support accelerated networking and Premium SSD." + } + }, + "vmAdminUsername": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "Optional. Jumpbox VM admin username." + } + }, + "vmAdminPassword": { + "type": "securestring", + "defaultValue": "", + "metadata": { + "description": "Optional. Jumpbox VM admin password." + } + }, "tags": { "type": "object", "defaultValue": {}, @@ -350,7 +371,8 @@ "aciPrivateIpFallback": "10.0.4.4", "aciPublicFqdnFallback": "[format('{0}.{1}.azurecontainer.io', variables('containerInstanceName'), variables('solutionLocation'))]", "aciBackendUrl": "[if(parameters('enablePrivateNetworking'), format('http://{0}:8000', variables('aciPrivateIpFallback')), format('http://{0}:8000', variables('aciPublicFqdnFallback')))]", - "containerInstanceName": "[format('aci-{0}', variables('solutionSuffix'))]" + "containerInstanceName": "[format('aci-{0}', variables('solutionSuffix'))]", + "jumpboxVmName": "[format('vm-jumpbox-{0}', variables('solutionSuffix'))]" }, "resources": { "avmTelemetry": { @@ -4829,8 +4851,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.41.2.15936", - "templateHash": "13822613177861506290" + "version": "0.39.26.7824", + "templateHash": "16294706585455769047" } }, "parameters": { @@ -14033,8 +14055,8 @@ }, "dependsOn": [ "aiFoundryAiServices", - "[format('avmPrivateDnsZones[{0}]', variables('dnsZoneIndex').cognitiveServices)]", "[format('avmPrivateDnsZones[{0}]', variables('dnsZoneIndex').openAI)]", + "[format('avmPrivateDnsZones[{0}]', variables('dnsZoneIndex').cognitiveServices)]", "virtualNetwork" ] }, @@ -14074,8 +14096,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.41.2.15936", - "templateHash": "14729448306021818204" + "version": "0.39.26.7824", + "templateHash": "12336056765515184474" } }, "parameters": { @@ -14212,8 +14234,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.41.2.15936", - "templateHash": "9237732984597233866" + "version": "0.39.26.7824", + "templateHash": "618751761593210210" } }, "parameters": { @@ -14255,7 +14277,7 @@ { "type": "Microsoft.Authorization/roleAssignments", "apiVersion": "2022-04-01", - "scope": "[resourceId('Microsoft.CognitiveServices/accounts', parameters('aiServicesName'))]", + "scope": "[format('Microsoft.CognitiveServices/accounts/{0}', parameters('aiServicesName'))]", "name": "[guid(resourceId('Microsoft.CognitiveServices/accounts', parameters('aiServicesName')), parameters('principalId'), resourceId('Microsoft.Authorization/roleDefinitions', '53ca6127-db72-4b80-b1b0-d745d6d5456d'))]", "properties": { "roleDefinitionId": "[resourceId('Microsoft.Authorization/roleDefinitions', '53ca6127-db72-4b80-b1b0-d745d6d5456d')]", @@ -14266,7 +14288,7 @@ { "type": "Microsoft.Authorization/roleAssignments", "apiVersion": "2022-04-01", - "scope": "[resourceId('Microsoft.CognitiveServices/accounts', parameters('aiServicesName'))]", + "scope": "[format('Microsoft.CognitiveServices/accounts/{0}', parameters('aiServicesName'))]", "name": "[guid(resourceId('Microsoft.CognitiveServices/accounts', parameters('aiServicesName')), parameters('principalId'), resourceId('Microsoft.Authorization/roleDefinitions', '5e0bd9bd-7b93-4f28-af87-19fc36ad61bd'))]", "properties": { "roleDefinitionId": "[resourceId('Microsoft.Authorization/roleDefinitions', '5e0bd9bd-7b93-4f28-af87-19fc36ad61bd')]", @@ -31229,8 +31251,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.41.2.15936", - "templateHash": "17215033518306564965" + "version": "0.39.26.7824", + "templateHash": "11911473200605315360" } }, "definitions": { @@ -32175,7 +32197,7 @@ }, "type": "Microsoft.Insights/diagnosticSettings", "apiVersion": "2021-05-01-preview", - "scope": "[resourceId('Microsoft.Web/sites', parameters('name'))]", + "scope": "[format('Microsoft.Web/sites/{0}', parameters('name'))]", "name": "[coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'name'), format('{0}-diagnosticSettings', parameters('name')))]", "properties": { "copy": [ @@ -32253,8 +32275,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.41.2.15936", - "templateHash": "26032048033705967" + "version": "0.39.26.7824", + "templateHash": "13592577410661714505" }, "name": "Site App Settings", "description": "This module deploys a Site App Setting." @@ -33374,8 +33396,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.41.2.15936", - "templateHash": "5828006773867864982" + "version": "0.39.26.7824", + "templateHash": "11247487291315089538" } }, "parameters": { @@ -33564,6 +33586,8872 @@ "userAssignedIdentity", "virtualNetwork" ] + }, + "jumpboxVM": { + "condition": "[and(parameters('enablePrivateNetworking'), parameters('deployBastionAndJumpbox'))]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "[take(format('avm.res.compute.virtual-machine.{0}', variables('jumpboxVmName')), 64)]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "name": { + "value": "[take(variables('jumpboxVmName'), 15)]" + }, + "enableTelemetry": { + "value": "[parameters('enableTelemetry')]" + }, + "computerName": { + "value": "[take(variables('jumpboxVmName'), 15)]" + }, + "osType": { + "value": "Windows" + }, + "vmSize": "[if(empty(parameters('vmSize')), createObject('value', 'Standard_D2s_v5'), createObject('value', parameters('vmSize')))]", + "adminUsername": "[if(empty(parameters('vmAdminUsername')), createObject('value', 'JumpboxAdminUser'), createObject('value', parameters('vmAdminUsername')))]", + "adminPassword": "[if(empty(parameters('vmAdminPassword')), createObject('value', 'JumpboxAdminP@ssw0rd1234!'), createObject('value', parameters('vmAdminPassword')))]", + "managedIdentities": { + "value": { + "userAssignedResourceIds": [ + "[reference('userAssignedIdentity').outputs.resourceId.value]" + ] + } + }, + "availabilityZone": { + "value": 1 + }, + "imageReference": { + "value": { + "publisher": "MicrosoftWindowsDesktop", + "offer": "windows-11", + "sku": "win11-24h2-pro", + "version": "latest" + } + }, + "nicConfigurations": { + "value": [ + { + "name": "[format('nic-{0}', variables('jumpboxVmName'))]", + "enableAcceleratedNetworking": true, + "ipConfigurations": [ + { + "name": "ipconfig01", + "subnetResourceId": "[reference('virtualNetwork').outputs.jumpboxSubnetResourceId.value]" + } + ] + } + ] + }, + "osDisk": { + "value": { + "caching": "ReadWrite", + "diskSizeGB": 128, + "managedDisk": { + "storageAccountType": "Premium_LRS" + } + } + }, + "location": { + "value": "[variables('solutionLocation')]" + }, + "tags": { + "value": "[parameters('tags')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "languageVersion": "2.0", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.37.4.10188", + "templateHash": "10754907249846822047" + }, + "name": "Virtual Machines", + "description": "This module deploys a Virtual Machine with one or multiple NICs and optionally one or multiple public IPs." + }, + "definitions": { + "osDiskType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The disk name." + } + }, + "diskSizeGB": { + "type": "int", + "nullable": true, + "metadata": { + "description": "Optional. Specifies the size of an empty data disk in gigabytes." + } + }, + "createOption": { + "type": "string", + "allowedValues": [ + "Attach", + "Empty", + "FromImage" + ], + "nullable": true, + "metadata": { + "description": "Optional. Specifies how the virtual machine should be created." + } + }, + "deleteOption": { + "type": "string", + "allowedValues": [ + "Delete", + "Detach" + ], + "nullable": true, + "metadata": { + "description": "Optional. Specifies whether data disk should be deleted or detached upon VM deletion." + } + }, + "caching": { + "type": "string", + "allowedValues": [ + "None", + "ReadOnly", + "ReadWrite" + ], + "nullable": true, + "metadata": { + "description": "Optional. Specifies the caching requirements." + } + }, + "diffDiskSettings": { + "type": "object", + "properties": { + "placement": { + "type": "string", + "allowedValues": [ + "CacheDisk", + "NvmeDisk", + "ResourceDisk" + ], + "metadata": { + "description": "Required. Specifies the ephemeral disk placement for the operating system disk." + } + } + }, + "nullable": true, + "metadata": { + "description": "Optional. Specifies the ephemeral Disk Settings for the operating system disk." + } + }, + "managedDisk": { + "type": "object", + "properties": { + "storageAccountType": { + "type": "string", + "allowedValues": [ + "PremiumV2_LRS", + "Premium_LRS", + "Premium_ZRS", + "StandardSSD_LRS", + "StandardSSD_ZRS", + "Standard_LRS", + "UltraSSD_LRS" + ], + "nullable": true, + "metadata": { + "description": "Optional. Specifies the storage account type for the managed disk." + } + }, + "diskEncryptionSetResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Specifies the customer managed disk encryption set resource id for the managed disk." + } + } + }, + "metadata": { + "description": "Required. The managed disk parameters." + } + } + }, + "metadata": { + "__bicep_export!": true, + "description": "The type describing an OS disk." + } + }, + "dataDiskType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The disk name. When attaching a pre-existing disk, this name is ignored and the name of the existing disk is used." + } + }, + "lun": { + "type": "int", + "nullable": true, + "metadata": { + "description": "Optional. Specifies the logical unit number of the data disk." + } + }, + "diskSizeGB": { + "type": "int", + "nullable": true, + "metadata": { + "description": "Optional. Specifies the size of an empty data disk in gigabytes. This property is ignored when attaching a pre-existing disk." + } + }, + "createOption": { + "type": "string", + "allowedValues": [ + "Attach", + "Empty", + "FromImage" + ], + "nullable": true, + "metadata": { + "description": "Optional. Specifies how the virtual machine should be created. This property is automatically set to 'Attach' when attaching a pre-existing disk." + } + }, + "deleteOption": { + "type": "string", + "allowedValues": [ + "Delete", + "Detach" + ], + "nullable": true, + "metadata": { + "description": "Optional. Specifies whether data disk should be deleted or detached upon VM deletion. This property is automatically set to 'Detach' when attaching a pre-existing disk." + } + }, + "caching": { + "type": "string", + "allowedValues": [ + "None", + "ReadOnly", + "ReadWrite" + ], + "nullable": true, + "metadata": { + "description": "Optional. Specifies the caching requirements. This property is automatically set to 'None' when attaching a pre-existing disk." + } + }, + "diskIOPSReadWrite": { + "type": "int", + "nullable": true, + "metadata": { + "description": "Optional. The number of IOPS allowed for this disk; only settable for UltraSSD disks. One operation can transfer between 4k and 256k bytes. Ignored when attaching a pre-existing disk." + } + }, + "diskMBpsReadWrite": { + "type": "int", + "nullable": true, + "metadata": { + "description": "Optional. The bandwidth allowed for this disk; only settable for UltraSSD disks. MBps means millions of bytes per second - MB here uses the ISO notation, of powers of 10. Ignored when attaching a pre-existing disk." + } + }, + "managedDisk": { + "type": "object", + "properties": { + "storageAccountType": { + "type": "string", + "allowedValues": [ + "PremiumV2_LRS", + "Premium_LRS", + "Premium_ZRS", + "StandardSSD_LRS", + "StandardSSD_ZRS", + "Standard_LRS", + "UltraSSD_LRS" + ], + "nullable": true, + "metadata": { + "description": "Optional. Specifies the storage account type for the managed disk. Ignored when attaching a pre-existing disk." + } + }, + "diskEncryptionSetResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Specifies the customer managed disk encryption set resource id for the managed disk." + } + }, + "id": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Specifies the resource id of a pre-existing managed disk. If the disk should be created, this property should be empty." + } + } + }, + "metadata": { + "description": "Required. The managed disk parameters." + } + }, + "tags": { + "type": "object", + "nullable": true, + "metadata": { + "description": "Optional. The tags of the public IP address. Valid only when creating a new managed disk." + } + } + }, + "metadata": { + "__bicep_export!": true, + "description": "The type describing a data disk." + } + }, + "publicKeyType": { + "type": "object", + "properties": { + "keyData": { + "type": "string", + "metadata": { + "description": "Required. Specifies the SSH public key data used to authenticate through ssh." + } + }, + "path": { + "type": "string", + "metadata": { + "description": "Required. Specifies the full path on the created VM where ssh public key is stored. If the file already exists, the specified key is appended to the file." + } + } + } + }, + "nicConfigurationType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The name of the NIC configuration." + } + }, + "nicSuffix": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The suffix to append to the NIC name." + } + }, + "enableIPForwarding": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. Indicates whether IP forwarding is enabled on this network interface." + } + }, + "enableAcceleratedNetworking": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. If the network interface is accelerated networking enabled." + } + }, + "deleteOption": { + "type": "string", + "allowedValues": [ + "Delete", + "Detach" + ], + "nullable": true, + "metadata": { + "description": "Optional. Specify what happens to the network interface when the VM is deleted." + } + }, + "dnsServers": { + "type": "array", + "items": { + "type": "string" + }, + "nullable": true, + "metadata": { + "description": "Optional. List of DNS servers IP addresses. Use 'AzureProvidedDNS' to switch to azure provided DNS resolution. 'AzureProvidedDNS' value cannot be combined with other IPs, it must be the only value in dnsServers collection." + } + }, + "networkSecurityGroupResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The network security group (NSG) to attach to the network interface." + } + }, + "ipConfigurations": { + "type": "array", + "items": { + "$ref": "#/definitions/ipConfigurationType" + }, + "metadata": { + "description": "Required. The IP configurations of the network interface." + } + }, + "lock": { + "$ref": "#/definitions/lockType", + "nullable": true, + "metadata": { + "description": "Optional. The lock settings of the service." + } + }, + "tags": { + "type": "object", + "nullable": true, + "metadata": { + "description": "Optional. The tags of the public IP address." + } + }, + "enableTelemetry": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. Enable/Disable usage telemetry for the module." + } + }, + "diagnosticSettings": { + "type": "array", + "items": { + "$ref": "#/definitions/diagnosticSettingFullType" + }, + "nullable": true, + "metadata": { + "description": "Optional. The diagnostic settings of the IP configuration." + } + }, + "roleAssignments": { + "type": "array", + "items": { + "$ref": "#/definitions/roleAssignmentType" + }, + "nullable": true, + "metadata": { + "description": "Optional. Array of role assignments to create." + } + } + }, + "metadata": { + "__bicep_export!": true, + "description": "The type for the NIC configuration." + } + }, + "imageReferenceType": { + "type": "object", + "properties": { + "communityGalleryImageId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Specified the community gallery image unique id for vm deployment. This can be fetched from community gallery image GET call." + } + }, + "id": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The resource Id of the image reference." + } + }, + "offer": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Specifies the offer of the platform image or marketplace image used to create the virtual machine." + } + }, + "publisher": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The image publisher." + } + }, + "sku": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The SKU of the image." + } + }, + "version": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Specifies the version of the platform image or marketplace image used to create the virtual machine. The allowed formats are Major.Minor.Build or 'latest'. Even if you use 'latest', the VM image will not automatically update after deploy time even if a new version becomes available." + } + }, + "sharedGalleryImageId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Specified the shared gallery image unique id for vm deployment. This can be fetched from shared gallery image GET call." + } + } + }, + "metadata": { + "__bicep_export!": true, + "description": "The type describing the image reference." + } + }, + "planType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The name of the plan." + } + }, + "product": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Specifies the product of the image from the marketplace." + } + }, + "publisher": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The publisher ID." + } + }, + "promotionCode": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The promotion code." + } + } + }, + "metadata": { + "__bicep_export!": true, + "description": "Specifies information about the marketplace image used to create the virtual machine." + } + }, + "autoShutDownConfigType": { + "type": "object", + "properties": { + "status": { + "type": "string", + "allowedValues": [ + "Disabled", + "Enabled" + ], + "nullable": true, + "metadata": { + "description": "Optional. The status of the auto shutdown configuration." + } + }, + "timeZone": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The time zone ID (e.g. China Standard Time, Greenland Standard Time, Pacific Standard time, etc.)." + } + }, + "dailyRecurrenceTime": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The time of day the schedule will occur." + } + }, + "notificationSettings": { + "type": "object", + "properties": { + "status": { + "type": "string", + "allowedValues": [ + "Disabled", + "Enabled" + ], + "nullable": true, + "metadata": { + "description": "Optional. The status of the notification settings." + } + }, + "emailRecipient": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The email address to send notifications to (can be a list of semi-colon separated email addresses)." + } + }, + "notificationLocale": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The locale to use when sending a notification (fallback for unsupported languages is EN)." + } + }, + "webhookUrl": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The webhook URL to which the notification will be sent." + } + }, + "timeInMinutes": { + "type": "int", + "nullable": true, + "metadata": { + "description": "Optional. The time in minutes before shutdown to send notifications." + } + } + }, + "nullable": true, + "metadata": { + "description": "Optional. The resource ID of the schedule." + } + } + }, + "metadata": { + "__bicep_export!": true, + "description": "The type describing the configuration profile." + } + }, + "vaultSecretGroupType": { + "type": "object", + "properties": { + "sourceVault": { + "$ref": "#/definitions/subResourceType", + "nullable": true, + "metadata": { + "description": "Optional. The relative URL of the Key Vault containing all of the certificates in VaultCertificates." + } + }, + "vaultCertificates": { + "type": "array", + "items": { + "type": "object", + "properties": { + "certificateStore": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. For Windows VMs, specifies the certificate store on the Virtual Machine to which the certificate should be added. The specified certificate store is implicitly in the LocalMachine account. For Linux VMs, the certificate file is placed under the /var/lib/waagent directory, with the file name .crt for the X509 certificate file and .prv for private key. Both of these files are .pem formatted." + } + }, + "certificateUrl": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. This is the URL of a certificate that has been uploaded to Key Vault as a secret." + } + } + } + }, + "nullable": true, + "metadata": { + "description": "Optional. The list of key vault references in SourceVault which contain certificates." + } + } + }, + "metadata": { + "__bicep_export!": true, + "description": "The type describing the set of certificates that should be installed onto the virtual machine." + } + }, + "vmGalleryApplicationType": { + "type": "object", + "properties": { + "packageReferenceId": { + "type": "string", + "metadata": { + "description": "Required. Specifies the GalleryApplicationVersion resource id on the form of /subscriptions/{SubscriptionId}/resourceGroups/{ResourceGroupName}/providers/Microsoft.Compute/galleries/{galleryName}/applications/{application}/versions/{version}." + } + }, + "configurationReference": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Specifies the uri to an azure blob that will replace the default configuration for the package if provided." + } + }, + "enableAutomaticUpgrade": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. If set to true, when a new Gallery Application version is available in PIR/SIG, it will be automatically updated for the VM/VMSS." + } + }, + "order": { + "type": "int", + "nullable": true, + "metadata": { + "description": "Optional. Specifies the order in which the packages have to be installed." + } + }, + "tags": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Specifies a passthrough value for more generic context." + } + }, + "treatFailureAsDeploymentFailure": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. If true, any failure for any operation in the VmApplication will fail the deployment." + } + } + }, + "metadata": { + "__bicep_export!": true, + "description": "The type describing the gallery application that should be made available to the VM/VMSS." + } + }, + "additionalUnattendContentType": { + "type": "object", + "properties": { + "settingName": { + "type": "string", + "allowedValues": [ + "AutoLogon", + "FirstLogonCommands" + ], + "nullable": true, + "metadata": { + "description": "Optional. Specifies the name of the setting to which the content applies." + } + }, + "content": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Specifies the XML formatted content that is added to the unattend.xml file for the specified path and component. The XML must be less than 4KB and must include the root element for the setting or feature that is being inserted." + } + } + }, + "metadata": { + "__bicep_export!": true, + "description": "The type describing additional base-64 encoded XML formatted information that can be included in the Unattend.xml file, which is used by Windows Setup." + } + }, + "winRMListenerType": { + "type": "object", + "properties": { + "certificateUrl": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The URL of a certificate that has been uploaded to Key Vault as a secret." + } + }, + "protocol": { + "type": "string", + "allowedValues": [ + "Http", + "Https" + ], + "nullable": true, + "metadata": { + "description": "Optional. Specifies the protocol of WinRM listener." + } + } + }, + "metadata": { + "__bicep_export!": true, + "description": "The type describing a Windows Remote Management listener." + } + }, + "nicConfigurationOutputType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "metadata": { + "description": "Required. The name of the NIC configuration." + } + }, + "ipConfigurations": { + "type": "array", + "items": { + "$ref": "#/definitions/networkInterfaceIPConfigurationOutputType" + }, + "metadata": { + "description": "Required. List of IP configurations of the NIC configuration." + } + } + }, + "metadata": { + "__bicep_export!": true, + "description": "The type describing the network interface configuration output." + } + }, + "extensionCustomScriptConfigType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The name of the virtual machine extension. Defaults to `CustomScriptExtension`." + } + }, + "typeHandlerVersion": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Specifies the version of the script handler. Defaults to `1.10` for Windows and `2.1` for Linux." + } + }, + "autoUpgradeMinorVersion": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. Indicates whether the extension should use a newer minor version if one is available at deployment time. Once deployed, however, the extension will not upgrade minor versions unless redeployed, even with this property set to true. Defaults to `true`." + } + }, + "forceUpdateTag": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. How the extension handler should be forced to update even if the extension configuration has not changed." + } + }, + "settings": { + "type": "object", + "properties": { + "commandToExecute": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Conditional. The entry point script to run. If the command contains any credentials, use the same property of the `protectedSettings` instead. Required if `protectedSettings.commandToExecute` is not provided." + } + }, + "fileUris": { + "type": "array", + "items": { + "type": "string" + }, + "nullable": true, + "metadata": { + "description": "Optional. URLs for files to be downloaded. If URLs are sensitive, for example, if they contain keys, this field should be specified in `protectedSettings`." + } + } + }, + "nullable": true, + "metadata": { + "description": "Optional. The configuration of the custom script extension. Note: You can provide any property either in the `settings` or `protectedSettings` but not both. If your property contains secrets, use `protectedSettings`." + } + }, + "protectedSettings": { + "type": "secureObject", + "properties": { + "commandToExecute": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Conditional. The entry point script to run. Use this property if your command contains secrets such as passwords or if your file URIs are sensitive. Required if `settings.commandToExecute` is not provided." + } + }, + "storageAccountName": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The name of storage account. If you specify storage credentials, all fileUris values must be URLs for Azure blobs.." + } + }, + "storageAccountKey": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The access key of the storage account." + } + }, + "managedIdentityResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The managed identity for downloading files. Must not be used in conjunction with the `storageAccountName` or `storageAccountKey` property. If you want to use the VM's system assigned identity, set the `value` to an empty string." + } + }, + "fileUris": { + "type": "array", + "items": { + "type": "string" + }, + "nullable": true, + "metadata": { + "description": "Optional. URLs for files to be downloaded." + } + } + }, + "nullable": true, + "metadata": { + "description": "Optional. The configuration of the custom script extension. Note: You can provide any property either in the `settings` or `protectedSettings` but not both. If your property contains secrets, use `protectedSettings`." + } + }, + "supressFailures": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. Indicates whether failures stemming from the extension will be suppressed (Operational failures such as not connecting to the VM will not be suppressed regardless of this value). Defaults to `false`." + } + }, + "enableAutomaticUpgrade": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. Indicates whether the extension should be automatically upgraded by the platform if there is a newer version of the extension available. Defaults to `false`." + } + }, + "tags": { + "type": "object", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.Compute/virtualMachines/extensions@2024-11-01#properties/tags" + }, + "description": "Optional. Tags of the resource." + }, + "nullable": true + }, + "protectedSettingsFromKeyVault": { + "type": "object", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.Compute/virtualMachines/extensions@2024-11-01#properties/properties/properties/protectedSettingsFromKeyVault" + }, + "description": "Optional. The extensions protected settings that are passed by reference, and consumed from key vault." + }, + "nullable": true + }, + "provisionAfterExtensions": { + "type": "array", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.Compute/virtualMachines/extensions@2024-11-01#properties/properties/properties/provisionAfterExtensions" + }, + "description": "Optional. Collection of extension names after which this extension needs to be provisioned." + }, + "nullable": true + } + }, + "metadata": { + "__bicep_export!": true, + "description": "The type of a 'CustomScriptExtension' extension." + } + }, + "_1.applicationGatewayBackendAddressPoolsType": { + "type": "object", + "properties": { + "id": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Resource ID of the backend address pool." + } + }, + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Name of the backend address pool that is unique within an Application Gateway." + } + }, + "properties": { + "type": "object", + "properties": { + "backendAddresses": { + "type": "array", + "items": { + "type": "object", + "properties": { + "ipAddress": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. IP address of the backend address." + } + }, + "fqdn": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. FQDN of the backend address." + } + } + } + }, + "nullable": true, + "metadata": { + "description": "Optional. Backend addresses." + } + } + }, + "nullable": true, + "metadata": { + "description": "Optional. Properties of the application gateway backend address pool." + } + } + }, + "metadata": { + "description": "The type for the application gateway backend address pool.", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/res/network/network-interface:0.5.1" + } + } + }, + "_1.applicationSecurityGroupType": { + "type": "object", + "properties": { + "id": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Resource ID of the application security group." + } + }, + "location": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Location of the application security group." + } + }, + "properties": { + "type": "object", + "nullable": true, + "metadata": { + "description": "Optional. Properties of the application security group." + } + }, + "tags": { + "type": "object", + "nullable": true, + "metadata": { + "description": "Optional. Tags of the application security group." + } + } + }, + "metadata": { + "description": "The type for the application security group.", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/res/network/network-interface:0.5.1" + } + } + }, + "_1.backendAddressPoolType": { + "type": "object", + "properties": { + "id": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The resource ID of the backend address pool." + } + }, + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The name of the backend address pool." + } + }, + "properties": { + "type": "object", + "nullable": true, + "metadata": { + "description": "Optional. The properties of the backend address pool." + } + } + }, + "metadata": { + "description": "The type for a backend address pool.", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/res/network/network-interface:0.5.1" + } + } + }, + "_1.inboundNatRuleType": { + "type": "object", + "properties": { + "id": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Resource ID of the inbound NAT rule." + } + }, + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Name of the resource that is unique within the set of inbound NAT rules used by the load balancer. This name can be used to access the resource." + } + }, + "properties": { + "type": "object", + "properties": { + "backendAddressPool": { + "$ref": "#/definitions/subResourceType", + "nullable": true, + "metadata": { + "description": "Optional. A reference to backendAddressPool resource." + } + }, + "backendPort": { + "type": "int", + "nullable": true, + "metadata": { + "description": "Optional. The port used for the internal endpoint. Acceptable values range from 1 to 65535." + } + }, + "enableFloatingIP": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. Configures a virtual machine's endpoint for the floating IP capability required to configure a SQL AlwaysOn Availability Group. This setting is required when using the SQL AlwaysOn Availability Groups in SQL server. This setting can't be changed after you create the endpoint." + } + }, + "enableTcpReset": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. Receive bidirectional TCP Reset on TCP flow idle timeout or unexpected connection termination. This element is only used when the protocol is set to TCP." + } + }, + "frontendIPConfiguration": { + "$ref": "#/definitions/subResourceType", + "nullable": true, + "metadata": { + "description": "Optional. A reference to frontend IP addresses." + } + }, + "frontendPort": { + "type": "int", + "nullable": true, + "metadata": { + "description": "Optional. The port for the external endpoint. Port numbers for each rule must be unique within the Load Balancer. Acceptable values range from 1 to 65534." + } + }, + "frontendPortRangeStart": { + "type": "int", + "nullable": true, + "metadata": { + "description": "Optional. The port range start for the external endpoint. This property is used together with BackendAddressPool and FrontendPortRangeEnd. Individual inbound NAT rule port mappings will be created for each backend address from BackendAddressPool. Acceptable values range from 1 to 65534." + } + }, + "frontendPortRangeEnd": { + "type": "int", + "nullable": true, + "metadata": { + "description": "Optional. The port range end for the external endpoint. This property is used together with BackendAddressPool and FrontendPortRangeStart. Individual inbound NAT rule port mappings will be created for each backend address from BackendAddressPool. Acceptable values range from 1 to 65534." + } + }, + "protocol": { + "type": "string", + "allowedValues": [ + "All", + "Tcp", + "Udp" + ], + "nullable": true, + "metadata": { + "description": "Optional. The reference to the transport protocol used by the load balancing rule." + } + } + }, + "nullable": true, + "metadata": { + "description": "Optional. Properties of the inbound NAT rule." + } + } + }, + "metadata": { + "description": "The type for the inbound NAT rule.", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/res/network/network-interface:0.5.1" + } + } + }, + "_1.virtualNetworkTapType": { + "type": "object", + "properties": { + "id": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Resource ID of the virtual network tap." + } + }, + "location": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Location of the virtual network tap." + } + }, + "properties": { + "type": "object", + "nullable": true, + "metadata": { + "description": "Optional. Properties of the virtual network tap." + } + }, + "tags": { + "type": "object", + "nullable": true, + "metadata": { + "description": "Optional. Tags of the virtual network tap." + } + } + }, + "metadata": { + "description": "The type for the virtual network tap.", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/res/network/network-interface:0.5.1" + } + } + }, + "_2.ddosSettingsType": { + "type": "object", + "properties": { + "ddosProtectionPlan": { + "type": "object", + "properties": { + "id": { + "type": "string", + "metadata": { + "description": "Required. The resource ID of the DDOS protection plan associated with the public IP address." + } + } + }, + "nullable": true, + "metadata": { + "description": "Optional. The DDoS protection plan associated with the public IP address." + } + }, + "protectionMode": { + "type": "string", + "allowedValues": [ + "Enabled" + ], + "metadata": { + "description": "Required. The DDoS protection policy customizations." + } + } + }, + "metadata": { + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/res/network/public-ip-address:0.8.0" + } + } + }, + "_2.dnsSettingsType": { + "type": "object", + "properties": { + "domainNameLabel": { + "type": "string", + "metadata": { + "description": "Required. The domain name label. The concatenation of the domain name label and the regionalized DNS zone make up the fully qualified domain name associated with the public IP address. If a domain name label is specified, an A DNS record is created for the public IP in the Microsoft Azure DNS system." + } + }, + "domainNameLabelScope": { + "type": "string", + "allowedValues": [ + "NoReuse", + "ResourceGroupReuse", + "SubscriptionReuse", + "TenantReuse" + ], + "nullable": true, + "metadata": { + "description": "Optional. The domain name label scope. If a domain name label and a domain name label scope are specified, an A DNS record is created for the public IP in the Microsoft Azure DNS system with a hashed value includes in FQDN." + } + }, + "fqdn": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The Fully Qualified Domain Name of the A DNS record associated with the public IP. This is the concatenation of the domainNameLabel and the regionalized DNS zone." + } + }, + "reverseFqdn": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The reverse FQDN. A user-visible, fully qualified domain name that resolves to this public IP address. If the reverseFqdn is specified, then a PTR DNS record is created pointing from the IP address in the in-addr.arpa domain to the reverse FQDN." + } + } + }, + "metadata": { + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/res/network/public-ip-address:0.8.0" + } + } + }, + "_2.ipTagType": { + "type": "object", + "properties": { + "ipTagType": { + "type": "string", + "metadata": { + "description": "Required. The IP tag type." + } + }, + "tag": { + "type": "string", + "metadata": { + "description": "Required. The IP tag." + } + } + }, + "metadata": { + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/res/network/public-ip-address:0.8.0" + } + } + }, + "_3.publicIPConfigurationType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The name of the Public IP Address." + } + }, + "publicIPAddressResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The resource ID of the public IP address." + } + }, + "diagnosticSettings": { + "type": "array", + "items": { + "$ref": "#/definitions/diagnosticSettingFullType" + }, + "nullable": true, + "metadata": { + "description": "Optional. Diagnostic settings for the public IP address." + } + }, + "location": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The idle timeout in minutes." + } + }, + "lock": { + "$ref": "#/definitions/lockType", + "nullable": true, + "metadata": { + "description": "Optional. The lock settings of the public IP address." + } + }, + "idleTimeoutInMinutes": { + "type": "int", + "nullable": true, + "metadata": { + "description": "Optional. The idle timeout of the public IP address." + } + }, + "ddosSettings": { + "$ref": "#/definitions/_2.ddosSettingsType", + "nullable": true, + "metadata": { + "description": "Optional. The DDoS protection plan configuration associated with the public IP address." + } + }, + "dnsSettings": { + "$ref": "#/definitions/_2.dnsSettingsType", + "nullable": true, + "metadata": { + "description": "Optional. The DNS settings of the public IP address." + } + }, + "publicIPAddressVersion": { + "type": "string", + "allowedValues": [ + "IPv4", + "IPv6" + ], + "nullable": true, + "metadata": { + "description": "Optional. The public IP address version." + } + }, + "publicIPAllocationMethod": { + "type": "string", + "allowedValues": [ + "Dynamic", + "Static" + ], + "nullable": true, + "metadata": { + "description": "Optional. The public IP address allocation method." + } + }, + "publicIpPrefixResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Resource ID of the Public IP Prefix object. This is only needed if you want your Public IPs created in a PIP Prefix." + } + }, + "publicIpNameSuffix": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The name suffix of the public IP address resource." + } + }, + "roleAssignments": { + "type": "array", + "items": { + "$ref": "#/definitions/roleAssignmentType" + }, + "nullable": true, + "metadata": { + "description": "Optional. Array of role assignments to create." + } + }, + "skuName": { + "type": "string", + "allowedValues": [ + "Basic", + "Standard" + ], + "nullable": true, + "metadata": { + "description": "Optional. The SKU name of the public IP address." + } + }, + "skuTier": { + "type": "string", + "allowedValues": [ + "Global", + "Regional" + ], + "nullable": true, + "metadata": { + "description": "Optional. The SKU tier of the public IP address." + } + }, + "tags": { + "type": "object", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.Network/publicIPAddresses@2024-07-01#properties/tags" + }, + "description": "Optional. The tags of the public IP address." + }, + "nullable": true + }, + "availabilityZones": { + "type": "array", + "allowedValues": [ + 1, + 2, + 3 + ], + "nullable": true, + "metadata": { + "description": "Optional. The zones of the public IP address." + } + }, + "ipTags": { + "type": "array", + "items": { + "$ref": "#/definitions/_2.ipTagType" + }, + "nullable": true, + "metadata": { + "description": "Optional. The list of tags associated with the public IP address." + } + }, + "enableTelemetry": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. Enable/Disable usage telemetry for the module." + } + } + }, + "metadata": { + "description": "The type for the public IP address configuration.", + "__bicep_imported_from!": { + "sourceTemplate": "modules/nic-configuration.bicep" + } + } + }, + "diagnosticSettingFullType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The name of the diagnostic setting." + } + }, + "logCategoriesAndGroups": { + "type": "array", + "items": { + "type": "object", + "properties": { + "category": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Name of a Diagnostic Log category for a resource type this setting is applied to. Set the specific logs to collect here." + } + }, + "categoryGroup": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Name of a Diagnostic Log category group for a resource type this setting is applied to. Set to `allLogs` to collect all logs." + } + }, + "enabled": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. Enable or disable the category explicitly. Default is `true`." + } + } + } + }, + "nullable": true, + "metadata": { + "description": "Optional. The name of logs that will be streamed. \"allLogs\" includes all possible logs for the resource. Set to `[]` to disable log collection." + } + }, + "metricCategories": { + "type": "array", + "items": { + "type": "object", + "properties": { + "category": { + "type": "string", + "metadata": { + "description": "Required. Name of a Diagnostic Metric category for a resource type this setting is applied to. Set to `AllMetrics` to collect all metrics." + } + }, + "enabled": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. Enable or disable the category explicitly. Default is `true`." + } + } + } + }, + "nullable": true, + "metadata": { + "description": "Optional. The name of metrics that will be streamed. \"allMetrics\" includes all possible metrics for the resource. Set to `[]` to disable metric collection." + } + }, + "logAnalyticsDestinationType": { + "type": "string", + "allowedValues": [ + "AzureDiagnostics", + "Dedicated" + ], + "nullable": true, + "metadata": { + "description": "Optional. A string indicating whether the export to Log Analytics should use the default destination type, i.e. AzureDiagnostics, or use a destination type." + } + }, + "workspaceResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Resource ID of the diagnostic log analytics workspace. For security reasons, it is recommended to set diagnostic settings to send data to either storage account, log analytics workspace or event hub." + } + }, + "storageAccountResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Resource ID of the diagnostic storage account. For security reasons, it is recommended to set diagnostic settings to send data to either storage account, log analytics workspace or event hub." + } + }, + "eventHubAuthorizationRuleResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Resource ID of the diagnostic event hub authorization rule for the Event Hubs namespace in which the event hub should be created or streamed to." + } + }, + "eventHubName": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Name of the diagnostic event hub within the namespace to which logs are streamed. Without this, an event hub is created for each log category. For security reasons, it is recommended to set diagnostic settings to send data to either storage account, log analytics workspace or event hub." + } + }, + "marketplacePartnerResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The full ARM resource ID of the Marketplace resource to which you would like to send Diagnostic Logs." + } + } + }, + "metadata": { + "description": "An AVM-aligned type for a diagnostic setting. To be used if both logs & metrics are supported by the resource provider.", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.5.1" + } + } + }, + "ipConfigurationType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The name of the IP configuration." + } + }, + "privateIPAllocationMethod": { + "type": "string", + "allowedValues": [ + "Dynamic", + "Static" + ], + "nullable": true, + "metadata": { + "description": "Optional. The private IP address allocation method." + } + }, + "privateIPAddress": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The private IP address." + } + }, + "subnetResourceId": { + "type": "string", + "metadata": { + "description": "Required. The resource ID of the subnet." + } + }, + "loadBalancerBackendAddressPools": { + "type": "array", + "items": { + "$ref": "#/definitions/_1.backendAddressPoolType" + }, + "nullable": true, + "metadata": { + "description": "Optional. The load balancer backend address pools." + } + }, + "applicationSecurityGroups": { + "type": "array", + "items": { + "$ref": "#/definitions/_1.applicationSecurityGroupType" + }, + "nullable": true, + "metadata": { + "description": "Optional. The application security groups." + } + }, + "applicationGatewayBackendAddressPools": { + "type": "array", + "items": { + "$ref": "#/definitions/_1.applicationGatewayBackendAddressPoolsType" + }, + "nullable": true, + "metadata": { + "description": "Optional. The application gateway backend address pools." + } + }, + "gatewayLoadBalancer": { + "$ref": "#/definitions/subResourceType", + "nullable": true, + "metadata": { + "description": "Optional. The gateway load balancer settings." + } + }, + "loadBalancerInboundNatRules": { + "type": "array", + "items": { + "$ref": "#/definitions/_1.inboundNatRuleType" + }, + "nullable": true, + "metadata": { + "description": "Optional. The load balancer inbound NAT rules." + } + }, + "privateIPAddressVersion": { + "type": "string", + "allowedValues": [ + "IPv4", + "IPv6" + ], + "nullable": true, + "metadata": { + "description": "Optional. The private IP address version." + } + }, + "virtualNetworkTaps": { + "type": "array", + "items": { + "$ref": "#/definitions/_1.virtualNetworkTapType" + }, + "nullable": true, + "metadata": { + "description": "Optional. The virtual network taps." + } + }, + "pipConfiguration": { + "$ref": "#/definitions/_3.publicIPConfigurationType", + "nullable": true, + "metadata": { + "description": "Optional. The public IP address configuration." + } + }, + "diagnosticSettings": { + "type": "array", + "items": { + "$ref": "#/definitions/diagnosticSettingFullType" + }, + "nullable": true, + "metadata": { + "description": "Optional. The diagnostic settings of the IP configuration." + } + }, + "tags": { + "type": "object", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.Network/networkInterfaces@2024-07-01#properties/tags" + }, + "description": "Optional. The tags of the public IP address." + }, + "nullable": true + }, + "enableTelemetry": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. Enable/Disable usage telemetry for the module." + } + } + }, + "metadata": { + "description": "The type for the IP configuration.", + "__bicep_imported_from!": { + "sourceTemplate": "modules/nic-configuration.bicep" + } + } + }, + "lockType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Specify the name of lock." + } + }, + "kind": { + "type": "string", + "allowedValues": [ + "CanNotDelete", + "None", + "ReadOnly" + ], + "nullable": true, + "metadata": { + "description": "Optional. Specify the type of lock." + } + }, + "notes": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Specify the notes of the lock." + } + } + }, + "metadata": { + "description": "An AVM-aligned type for a lock.", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.0" + } + } + }, + "managedIdentityAllType": { + "type": "object", + "properties": { + "systemAssigned": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. Enables system assigned managed identity on the resource." + } + }, + "userAssignedResourceIds": { + "type": "array", + "items": { + "type": "string" + }, + "nullable": true, + "metadata": { + "description": "Optional. The resource ID(s) to assign to the resource. Required if a user assigned identity is used for encryption." + } + } + }, + "metadata": { + "description": "An AVM-aligned type for a managed identity configuration. To be used if both a system-assigned & user-assigned identities are supported by the resource provider.", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.5.1" + } + } + }, + "networkInterfaceIPConfigurationOutputType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "metadata": { + "description": "The name of the IP configuration." + } + }, + "privateIP": { + "type": "string", + "nullable": true, + "metadata": { + "description": "The private IP address." + } + }, + "publicIP": { + "type": "string", + "nullable": true, + "metadata": { + "description": "The public IP address." + } + } + }, + "metadata": { + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/res/network/network-interface:0.5.1" + } + } + }, + "roleAssignmentType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The name (as GUID) of the role assignment. If not provided, a GUID will be generated." + } + }, + "roleDefinitionIdOrName": { + "type": "string", + "metadata": { + "description": "Required. The role to assign. You can provide either the display name of the role definition, the role definition GUID, or its fully qualified ID in the following format: '/providers/Microsoft.Authorization/roleDefinitions/c2f4ef07-c644-48eb-af81-4b1b4947fb11'." + } + }, + "principalId": { + "type": "string", + "metadata": { + "description": "Required. The principal ID of the principal (user/group/identity) to assign the role to." + } + }, + "principalType": { + "type": "string", + "allowedValues": [ + "Device", + "ForeignGroup", + "Group", + "ServicePrincipal", + "User" + ], + "nullable": true, + "metadata": { + "description": "Optional. The principal type of the assigned principal ID." + } + }, + "description": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The description of the role assignment." + } + }, + "condition": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The conditions on the role assignment. This limits the resources it can be assigned to. e.g.: @Resource[Microsoft.Storage/storageAccounts/blobServices/containers:ContainerName] StringEqualsIgnoreCase \"foo_storage_container\"." + } + }, + "conditionVersion": { + "type": "string", + "allowedValues": [ + "2.0" + ], + "nullable": true, + "metadata": { + "description": "Optional. Version of the condition." + } + }, + "delegatedManagedIdentityResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The Resource Id of the delegated managed identity resource." + } + } + }, + "metadata": { + "description": "An AVM-aligned type for a role assignment.", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.5.1" + } + } + }, + "subResourceType": { + "type": "object", + "properties": { + "id": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Resource ID of the sub resource." + } + } + }, + "metadata": { + "description": "The type for the sub resource.", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/res/network/network-interface:0.5.1" + } + } + } + }, + "parameters": { + "name": { + "type": "string", + "metadata": { + "description": "Required. The name of the virtual machine to be created. You should use a unique prefix to reduce name collisions in Active Directory." + } + }, + "computerName": { + "type": "string", + "defaultValue": "[parameters('name')]", + "metadata": { + "description": "Optional. Can be used if the computer name needs to be different from the Azure VM resource name. If not used, the resource name will be used as computer name." + } + }, + "vmSize": { + "type": "string", + "metadata": { + "description": "Required. Specifies the size for the VMs." + } + }, + "encryptionAtHost": { + "type": "bool", + "defaultValue": false, + "metadata": { + "description": "Optional. This property can be used by user in the request to enable or disable the Host Encryption for the virtual machine. This will enable the encryption for all the disks including Resource/Temp disk at host itself. For security reasons, it is recommended to set encryptionAtHost to True. Restrictions: Cannot be enabled if Azure Disk Encryption (guest-VM encryption using bitlocker/DM-Crypt) is enabled on your VMs." + } + }, + "securityType": { + "type": "string", + "defaultValue": "", + "allowedValues": [ + "", + "ConfidentialVM", + "TrustedLaunch" + ], + "metadata": { + "description": "Optional. Specifies the SecurityType of the virtual machine. It has to be set to any specified value to enable UefiSettings. The default behavior is: UefiSettings will not be enabled unless this property is set." + } + }, + "secureBootEnabled": { + "type": "bool", + "defaultValue": false, + "metadata": { + "description": "Optional. Specifies whether secure boot should be enabled on the virtual machine. This parameter is part of the UefiSettings. SecurityType should be set to TrustedLaunch to enable UefiSettings." + } + }, + "vTpmEnabled": { + "type": "bool", + "defaultValue": false, + "metadata": { + "description": "Optional. Specifies whether vTPM should be enabled on the virtual machine. This parameter is part of the UefiSettings. SecurityType should be set to TrustedLaunch to enable UefiSettings." + } + }, + "imageReference": { + "$ref": "#/definitions/imageReferenceType", + "metadata": { + "description": "Required. OS image reference. In case of marketplace images, it's the combination of the publisher, offer, sku, version attributes. In case of custom images it's the resource ID of the custom image." + } + }, + "plan": { + "$ref": "#/definitions/planType", + "nullable": true, + "metadata": { + "description": "Optional. Specifies information about the marketplace image used to create the virtual machine. This element is only used for marketplace images. Before you can use a marketplace image from an API, you must enable the image for programmatic use." + } + }, + "osDisk": { + "$ref": "#/definitions/osDiskType", + "metadata": { + "description": "Required. Specifies the OS disk. For security reasons, it is recommended to specify DiskEncryptionSet into the osDisk object. Restrictions: DiskEncryptionSet cannot be enabled if Azure Disk Encryption (guest-VM encryption using bitlocker/DM-Crypt) is enabled on your VMs." + } + }, + "dataDisks": { + "type": "array", + "items": { + "$ref": "#/definitions/dataDiskType" + }, + "nullable": true, + "metadata": { + "description": "Optional. Specifies the data disks. For security reasons, it is recommended to specify DiskEncryptionSet into the dataDisk object. Restrictions: DiskEncryptionSet cannot be enabled if Azure Disk Encryption (guest-VM encryption using bitlocker/DM-Crypt) is enabled on your VMs." + } + }, + "ultraSSDEnabled": { + "type": "bool", + "defaultValue": false, + "metadata": { + "description": "Optional. The flag that enables or disables a capability to have one or more managed data disks with UltraSSD_LRS storage account type on the VM or VMSS. Managed disks with storage account type UltraSSD_LRS can be added to a virtual machine or virtual machine scale set only if this property is enabled." + } + }, + "hibernationEnabled": { + "type": "bool", + "defaultValue": false, + "metadata": { + "description": "Optional. The flag that enables or disables hibernation capability on the VM." + } + }, + "adminUsername": { + "type": "securestring", + "metadata": { + "description": "Required. Administrator username." + } + }, + "adminPassword": { + "type": "securestring", + "defaultValue": "", + "metadata": { + "description": "Optional. When specifying a Windows Virtual Machine, this value should be passed." + } + }, + "userData": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "Optional. UserData for the VM, which must be base-64 encoded. Customer should not pass any secrets in here." + } + }, + "customData": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "Optional. Custom data associated to the VM, this value will be automatically converted into base64 to account for the expected VM format." + } + }, + "certificatesToBeInstalled": { + "type": "array", + "items": { + "$ref": "#/definitions/vaultSecretGroupType" + }, + "nullable": true, + "metadata": { + "description": "Optional. Specifies set of certificates that should be installed onto the virtual machine." + } + }, + "priority": { + "type": "string", + "nullable": true, + "allowedValues": [ + "Regular", + "Low", + "Spot" + ], + "metadata": { + "description": "Optional. Specifies the priority for the virtual machine." + } + }, + "evictionPolicy": { + "type": "string", + "defaultValue": "Deallocate", + "allowedValues": [ + "Deallocate", + "Delete" + ], + "metadata": { + "description": "Optional. Specifies the eviction policy for the low priority virtual machine." + } + }, + "maxPriceForLowPriorityVm": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "Optional. Specifies the maximum price you are willing to pay for a low priority VM/VMSS. This price is in US Dollars." + } + }, + "dedicatedHostResourceId": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "Optional. Specifies resource ID about the dedicated host that the virtual machine resides in." + } + }, + "licenseType": { + "type": "string", + "nullable": true, + "allowedValues": [ + "RHEL_BYOS", + "SLES_BYOS", + "Windows_Client", + "Windows_Server" + ], + "metadata": { + "description": "Optional. Specifies that the image or disk that is being used was licensed on-premises." + } + }, + "publicKeys": { + "type": "array", + "items": { + "$ref": "#/definitions/publicKeyType" + }, + "defaultValue": [], + "metadata": { + "description": "Optional. The list of SSH public keys used to authenticate with linux based VMs." + } + }, + "managedIdentities": { + "$ref": "#/definitions/managedIdentityAllType", + "nullable": true, + "metadata": { + "description": "Optional. The managed identity definition for this resource. The system-assigned managed identity will automatically be enabled if extensionAadJoinConfig.enabled = \"True\"." + } + }, + "bootDiagnostics": { + "type": "bool", + "defaultValue": false, + "metadata": { + "description": "Optional. Whether boot diagnostics should be enabled on the Virtual Machine. Boot diagnostics will be enabled with a managed storage account if no bootDiagnosticsStorageAccountName value is provided. If bootDiagnostics and bootDiagnosticsStorageAccountName values are not provided, boot diagnostics will be disabled." + } + }, + "bootDiagnosticStorageAccountName": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "Optional. Custom storage account used to store boot diagnostic information. Boot diagnostics will be enabled with a custom storage account if a value is provided." + } + }, + "bootDiagnosticStorageAccountUri": { + "type": "string", + "defaultValue": "[format('.blob.{0}/', environment().suffixes.storage)]", + "metadata": { + "description": "Optional. Storage account boot diagnostic base URI." + } + }, + "proximityPlacementGroupResourceId": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "Optional. Resource ID of a proximity placement group." + } + }, + "virtualMachineScaleSetResourceId": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "Optional. Resource ID of a virtual machine scale set, where the VM should be added." + } + }, + "availabilitySetResourceId": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "Optional. Resource ID of an availability set. Cannot be used in combination with availability zone nor scale set." + } + }, + "galleryApplications": { + "type": "array", + "items": { + "$ref": "#/definitions/vmGalleryApplicationType" + }, + "nullable": true, + "metadata": { + "description": "Optional. Specifies the gallery applications that should be made available to the VM/VMSS." + } + }, + "availabilityZone": { + "type": "int", + "allowedValues": [ + -1, + 1, + 2, + 3 + ], + "metadata": { + "description": "Required. If set to 1, 2 or 3, the availability zone is hardcoded to that value. If set to -1, no zone is defined. Note that the availability zone numbers here are the logical availability zone in your Azure subscription. Different subscriptions might have a different mapping of the physical zone and logical zone. To understand more, please refer to [Physical and logical availability zones](https://learn.microsoft.com/en-us/azure/reliability/availability-zones-overview?tabs=azure-cli#physical-and-logical-availability-zones)." + } + }, + "nicConfigurations": { + "type": "array", + "items": { + "$ref": "#/definitions/nicConfigurationType" + }, + "metadata": { + "description": "Required. Configures NICs and PIPs." + } + }, + "backupVaultName": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "Optional. Recovery service vault name to add VMs to backup." + } + }, + "backupVaultResourceGroup": { + "type": "string", + "defaultValue": "[resourceGroup().name]", + "metadata": { + "description": "Optional. Resource group of the backup recovery service vault. If not provided the current resource group name is considered by default." + } + }, + "backupPolicyName": { + "type": "string", + "defaultValue": "DefaultPolicy", + "metadata": { + "description": "Optional. Backup policy the VMs should be using for backup. If not provided, it will use the DefaultPolicy from the backup recovery service vault." + } + }, + "autoShutdownConfig": { + "$ref": "#/definitions/autoShutDownConfigType", + "defaultValue": {}, + "metadata": { + "description": "Optional. The configuration for auto-shutdown." + } + }, + "maintenanceConfigurationResourceId": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "Optional. The resource Id of a maintenance configuration for this VM." + } + }, + "allowExtensionOperations": { + "type": "bool", + "defaultValue": true, + "metadata": { + "description": "Optional. Specifies whether extension operations should be allowed on the virtual machine. This may only be set to False when no extensions are present on the virtual machine." + } + }, + "extensionDomainJoinPassword": { + "type": "securestring", + "defaultValue": "", + "metadata": { + "description": "Optional. Required if name is specified. Password of the user specified in user parameter." + } + }, + "extensionDomainJoinConfig": { + "type": "secureObject", + "defaultValue": {}, + "metadata": { + "description": "Optional. The configuration for the [Domain Join] extension. Must at least contain the [\"enabled\": true] property to be executed." + } + }, + "extensionAadJoinConfig": { + "type": "object", + "defaultValue": { + "enabled": false + }, + "metadata": { + "description": "Optional. The configuration for the [AAD Join] extension. Must at least contain the [\"enabled\": true] property to be executed. To enroll in Intune, add the setting mdmId: \"0000000a-0000-0000-c000-000000000000\"." + } + }, + "extensionAntiMalwareConfig": { + "type": "object", + "defaultValue": "[if(equals(parameters('osType'), 'Windows'), createObject('enabled', true()), createObject('enabled', false()))]", + "metadata": { + "description": "Optional. The configuration for the [Anti Malware] extension. Must at least contain the [\"enabled\": true] property to be executed." + } + }, + "extensionMonitoringAgentConfig": { + "type": "object", + "defaultValue": { + "enabled": false, + "dataCollectionRuleAssociations": [] + }, + "metadata": { + "description": "Optional. The configuration for the [Monitoring Agent] extension. Must at least contain the [\"enabled\": true] property to be executed." + } + }, + "extensionDependencyAgentConfig": { + "type": "object", + "defaultValue": { + "enabled": false + }, + "metadata": { + "description": "Optional. The configuration for the [Dependency Agent] extension. Must at least contain the [\"enabled\": true] property to be executed." + } + }, + "extensionNetworkWatcherAgentConfig": { + "type": "object", + "defaultValue": { + "enabled": false + }, + "metadata": { + "description": "Optional. The configuration for the [Network Watcher Agent] extension. Must at least contain the [\"enabled\": true] property to be executed." + } + }, + "extensionAzureDiskEncryptionConfig": { + "type": "object", + "defaultValue": { + "enabled": false + }, + "metadata": { + "description": "Optional. The configuration for the [Azure Disk Encryption] extension. Must at least contain the [\"enabled\": true] property to be executed. Restrictions: Cannot be enabled on disks that have encryption at host enabled. Managed disks encrypted using Azure Disk Encryption cannot be encrypted using customer-managed keys." + } + }, + "extensionDSCConfig": { + "type": "object", + "defaultValue": { + "enabled": false + }, + "metadata": { + "description": "Optional. The configuration for the [Desired State Configuration] extension. Must at least contain the [\"enabled\": true] property to be executed." + } + }, + "extensionCustomScriptConfig": { + "$ref": "#/definitions/extensionCustomScriptConfigType", + "nullable": true, + "metadata": { + "description": "Optional. The configuration for the [Custom Script] extension." + } + }, + "extensionNvidiaGpuDriverWindows": { + "type": "object", + "defaultValue": { + "enabled": false + }, + "metadata": { + "description": "Optional. The configuration for the [Nvidia Gpu Driver Windows] extension. Must at least contain the [\"enabled\": true] property to be executed." + } + }, + "extensionHostPoolRegistration": { + "type": "secureObject", + "defaultValue": { + "enabled": false + }, + "metadata": { + "description": "Optional. The configuration for the [Host Pool Registration] extension. Must at least contain the [\"enabled\": true] property to be executed. Needs a managed identity." + } + }, + "extensionGuestConfigurationExtension": { + "type": "object", + "defaultValue": { + "enabled": false + }, + "metadata": { + "description": "Optional. The configuration for the [Guest Configuration] extension. Must at least contain the [\"enabled\": true] property to be executed. Needs a managed identity." + } + }, + "guestConfiguration": { + "type": "object", + "defaultValue": {}, + "metadata": { + "description": "Optional. The guest configuration for the virtual machine. Needs the Guest Configuration extension to be enabled." + } + }, + "extensionGuestConfigurationExtensionProtectedSettings": { + "type": "secureObject", + "defaultValue": {}, + "metadata": { + "description": "Optional. An object that contains the extension specific protected settings." + } + }, + "location": { + "type": "string", + "defaultValue": "[resourceGroup().location]", + "metadata": { + "description": "Optional. Location for all resources." + } + }, + "lock": { + "$ref": "#/definitions/lockType", + "nullable": true, + "metadata": { + "description": "Optional. The lock settings of the service." + } + }, + "roleAssignments": { + "type": "array", + "items": { + "$ref": "#/definitions/roleAssignmentType" + }, + "nullable": true, + "metadata": { + "description": "Optional. Array of role assignments to create." + } + }, + "tags": { + "type": "object", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.Compute/virtualMachines@2024-11-01#properties/tags" + }, + "description": "Optional. Tags of the resource." + }, + "nullable": true + }, + "enableTelemetry": { + "type": "bool", + "defaultValue": true, + "metadata": { + "description": "Optional. Enable/Disable usage telemetry for module." + } + }, + "osType": { + "type": "string", + "allowedValues": [ + "Windows", + "Linux" + ], + "metadata": { + "description": "Required. The chosen OS type." + } + }, + "disablePasswordAuthentication": { + "type": "bool", + "defaultValue": false, + "metadata": { + "description": "Optional. Specifies whether password authentication should be disabled." + } + }, + "provisionVMAgent": { + "type": "bool", + "defaultValue": true, + "metadata": { + "description": "Optional. Indicates whether virtual machine agent should be provisioned on the virtual machine. When this property is not specified in the request body, default behavior is to set it to true. This will ensure that VM Agent is installed on the VM so that extensions can be added to the VM later." + } + }, + "enableAutomaticUpdates": { + "type": "bool", + "defaultValue": true, + "metadata": { + "description": "Optional. Indicates whether Automatic Updates is enabled for the Windows virtual machine. Default value is true. When patchMode is set to Manual, this parameter must be set to false. For virtual machine scale sets, this property can be updated and updates will take effect on OS reprovisioning." + } + }, + "patchMode": { + "type": "string", + "defaultValue": "", + "allowedValues": [ + "AutomaticByPlatform", + "AutomaticByOS", + "Manual", + "ImageDefault", + "" + ], + "metadata": { + "description": "Optional. VM guest patching orchestration mode. 'AutomaticByOS' & 'Manual' are for Windows only, 'ImageDefault' for Linux only. Refer to 'https://learn.microsoft.com/en-us/azure/virtual-machines/automatic-vm-guest-patching'." + } + }, + "bypassPlatformSafetyChecksOnUserSchedule": { + "type": "bool", + "defaultValue": true, + "metadata": { + "description": "Optional. Enables customer to schedule patching without accidental upgrades." + } + }, + "rebootSetting": { + "type": "string", + "defaultValue": "IfRequired", + "allowedValues": [ + "Always", + "IfRequired", + "Never", + "Unknown" + ], + "metadata": { + "description": "Optional. Specifies the reboot setting for all AutomaticByPlatform patch installation operations." + } + }, + "patchAssessmentMode": { + "type": "string", + "defaultValue": "ImageDefault", + "allowedValues": [ + "AutomaticByPlatform", + "ImageDefault" + ], + "metadata": { + "description": "Optional. VM guest patching assessment mode. Set it to 'AutomaticByPlatform' to enable automatically check for updates every 24 hours." + } + }, + "enableHotpatching": { + "type": "bool", + "defaultValue": false, + "metadata": { + "description": "Optional. Enables customers to patch their Azure VMs without requiring a reboot. For enableHotpatching, the 'provisionVMAgent' must be set to true and 'patchMode' must be set to 'AutomaticByPlatform'." + } + }, + "timeZone": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "Optional. Specifies the time zone of the virtual machine. e.g. 'Pacific Standard Time'. Possible values can be `TimeZoneInfo.id` value from time zones returned by `TimeZoneInfo.GetSystemTimeZones`." + } + }, + "additionalUnattendContent": { + "type": "array", + "items": { + "$ref": "#/definitions/additionalUnattendContentType" + }, + "nullable": true, + "metadata": { + "description": "Optional. Specifies additional XML formatted information that can be included in the Unattend.xml file, which is used by Windows Setup. Contents are defined by setting name, component name, and the pass in which the content is applied." + } + }, + "winRMListeners": { + "type": "array", + "items": { + "$ref": "#/definitions/winRMListenerType" + }, + "nullable": true, + "metadata": { + "description": "Optional. Specifies the Windows Remote Management listeners. This enables remote Windows PowerShell." + } + }, + "configurationProfile": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "Optional. The configuration profile of automanage. Either '/providers/Microsoft.Automanage/bestPractices/AzureBestPracticesProduction', 'providers/Microsoft.Automanage/bestPractices/AzureBestPracticesDevTest' or the resource Id of custom profile." + } + }, + "capacityReservationGroupResourceId": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "Optional. Capacity reservation group resource id that should be used for allocating the virtual machine vm instances provided enough capacity has been reserved." + } + }, + "networkAccessPolicy": { + "type": "string", + "defaultValue": "DenyAll", + "allowedValues": [ + "AllowAll", + "AllowPrivate", + "DenyAll" + ], + "metadata": { + "description": "Optional. Policy for accessing the disk via network." + } + }, + "publicNetworkAccess": { + "type": "string", + "defaultValue": "Disabled", + "allowedValues": [ + "Disabled", + "Enabled" + ], + "metadata": { + "description": "Optional. Policy for controlling export on the disk." + } + } + }, + "variables": { + "copy": [ + { + "name": "publicKeysFormatted", + "count": "[length(parameters('publicKeys'))]", + "input": { + "path": "[parameters('publicKeys')[copyIndex('publicKeysFormatted')].path]", + "keyData": "[parameters('publicKeys')[copyIndex('publicKeysFormatted')].keyData]" + } + }, + { + "name": "additionalUnattendContentFormatted", + "count": "[length(coalesce(parameters('additionalUnattendContent'), createArray()))]", + "input": { + "settingName": "[coalesce(parameters('additionalUnattendContent'), createArray())[copyIndex('additionalUnattendContentFormatted')].settingName]", + "content": "[coalesce(parameters('additionalUnattendContent'), createArray())[copyIndex('additionalUnattendContentFormatted')].content]", + "componentName": "Microsoft-Windows-Shell-Setup", + "passName": "OobeSystem" + } + }, + { + "name": "formattedRoleAssignments", + "count": "[length(coalesce(parameters('roleAssignments'), createArray()))]", + "input": "[union(coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')], createObject('roleDefinitionId', coalesce(tryGet(variables('builtInRoleNames'), coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName), if(contains(coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName, '/providers/Microsoft.Authorization/roleDefinitions/'), coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName, subscriptionResourceId('Microsoft.Authorization/roleDefinitions', coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName)))))]" + } + ], + "enableReferencedModulesTelemetry": false, + "linuxConfiguration": { + "disablePasswordAuthentication": "[parameters('disablePasswordAuthentication')]", + "ssh": { + "publicKeys": "[variables('publicKeysFormatted')]" + }, + "provisionVMAgent": "[parameters('provisionVMAgent')]", + "patchSettings": "[if(and(parameters('provisionVMAgent'), or(equals(toLower(parameters('patchMode')), toLower('AutomaticByPlatform')), equals(toLower(parameters('patchMode')), toLower('ImageDefault')))), createObject('patchMode', parameters('patchMode'), 'assessmentMode', parameters('patchAssessmentMode'), 'automaticByPlatformSettings', if(equals(toLower(parameters('patchMode')), toLower('AutomaticByPlatform')), createObject('bypassPlatformSafetyChecksOnUserSchedule', parameters('bypassPlatformSafetyChecksOnUserSchedule'), 'rebootSetting', parameters('rebootSetting')), null())), null())]" + }, + "windowsConfiguration": { + "provisionVMAgent": "[parameters('provisionVMAgent')]", + "enableAutomaticUpdates": "[parameters('enableAutomaticUpdates')]", + "patchSettings": "[if(and(parameters('provisionVMAgent'), or(or(equals(toLower(parameters('patchMode')), toLower('AutomaticByPlatform')), equals(toLower(parameters('patchMode')), toLower('AutomaticByOS'))), equals(toLower(parameters('patchMode')), toLower('Manual')))), createObject('patchMode', parameters('patchMode'), 'assessmentMode', parameters('patchAssessmentMode'), 'enableHotpatching', if(equals(toLower(parameters('patchMode')), toLower('AutomaticByPlatform')), parameters('enableHotpatching'), false()), 'automaticByPlatformSettings', if(equals(toLower(parameters('patchMode')), toLower('AutomaticByPlatform')), createObject('bypassPlatformSafetyChecksOnUserSchedule', parameters('bypassPlatformSafetyChecksOnUserSchedule'), 'rebootSetting', parameters('rebootSetting')), null())), null())]", + "timeZone": "[if(empty(parameters('timeZone')), null(), parameters('timeZone'))]", + "additionalUnattendContent": "[if(empty(parameters('additionalUnattendContent')), null(), variables('additionalUnattendContentFormatted'))]", + "winRM": "[if(not(empty(parameters('winRMListeners'))), createObject('listeners', parameters('winRMListeners')), null())]" + }, + "formattedUserAssignedIdentities": "[reduce(map(coalesce(tryGet(parameters('managedIdentities'), 'userAssignedResourceIds'), createArray()), lambda('id', createObject(format('{0}', lambdaVariables('id')), createObject()))), createObject(), lambda('cur', 'next', union(lambdaVariables('cur'), lambdaVariables('next'))))]", + "identity": "[if(not(empty(parameters('managedIdentities'))), createObject('type', if(if(parameters('extensionAadJoinConfig').enabled, true(), coalesce(tryGet(parameters('managedIdentities'), 'systemAssigned'), false())), if(not(empty(coalesce(tryGet(parameters('managedIdentities'), 'userAssignedResourceIds'), createObject()))), 'SystemAssigned, UserAssigned', 'SystemAssigned'), if(not(empty(coalesce(tryGet(parameters('managedIdentities'), 'userAssignedResourceIds'), createObject()))), 'UserAssigned', null())), 'userAssignedIdentities', if(not(empty(variables('formattedUserAssignedIdentities'))), variables('formattedUserAssignedIdentities'), null())), null())]", + "builtInRoleNames": { + "Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'b24988ac-6180-42a0-ab88-20f7382dd24c')]", + "Data Operator for Managed Disks": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '959f8984-c045-4866-89c7-12bf9737be2e')]", + "Desktop Virtualization Power On Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '489581de-a3bd-480d-9518-53dea7416b33')]", + "Desktop Virtualization Power On Off Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '40c5ff49-9181-41f8-ae61-143b0e78555e')]", + "Desktop Virtualization Virtual Machine Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'a959dbd1-f747-45e3-8ba6-dd80f235f97c')]", + "DevTest Labs User": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '76283e04-6283-4c54-8f91-bcf1374a3c64')]", + "Disk Backup Reader": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '3e5e47e6-65f7-47ef-90b5-e5dd4d455f24')]", + "Disk Pool Operator": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '60fc6e62-5479-42d4-8bf4-67625fcc2840')]", + "Disk Restore Operator": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'b50d9833-a0cb-478e-945f-707fcc997c13')]", + "Disk Snapshot Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '7efff54f-a5b4-42b5-a1c5-5411624893ce')]", + "Owner": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '8e3af657-a8ff-443c-a75c-2fe8c4bcb635')]", + "Reader": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'acdd72a7-3385-48ef-bd42-f606fba81ae7')]", + "Role Based Access Control Administrator": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'f58310d9-a9f6-439a-9e8d-f62e7b41a168')]", + "User Access Administrator": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '18d7d88d-d35e-4fb5-a5c3-7773c20a72d9')]", + "Virtual Machine Administrator Login": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '1c0163c0-47e6-4577-8991-ea5c82e286e4')]", + "Virtual Machine Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '9980e02c-c2be-4d73-94e8-173b1dc7cf3c')]", + "Virtual Machine User Login": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'fb879df8-f326-4884-b1cf-06f3ad86be52')]", + "VM Scanner Operator": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'd24ecba3-c1f4-40fa-a7bb-4588a071e8fd')]" + } + }, + "resources": { + "avmTelemetry": { + "condition": "[parameters('enableTelemetry')]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2024-03-01", + "name": "[format('46d3xbcp.res.compute-virtualmachine.{0}.{1}', replace('0.20.0', '.', '-'), substring(uniqueString(deployment().name, parameters('location')), 0, 4))]", + "properties": { + "mode": "Incremental", + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "resources": [], + "outputs": { + "telemetry": { + "type": "String", + "value": "For more information, see https://aka.ms/avm/TelemetryInfo" + } + } + } + } + }, + "managedDataDisks": { + "copy": { + "name": "managedDataDisks", + "count": "[length(coalesce(parameters('dataDisks'), createArray()))]" + }, + "condition": "[empty(tryGet(coalesce(parameters('dataDisks'), createArray())[copyIndex()].managedDisk, 'id'))]", + "type": "Microsoft.Compute/disks", + "apiVersion": "2024-03-02", + "name": "[coalesce(tryGet(coalesce(parameters('dataDisks'), createArray())[copyIndex()], 'name'), format('{0}-disk-data-{1}', parameters('name'), padLeft(add(copyIndex(), 1), 2, '0')))]", + "location": "[parameters('location')]", + "sku": { + "name": "[tryGet(coalesce(parameters('dataDisks'), createArray())[copyIndex()].managedDisk, 'storageAccountType')]" + }, + "properties": { + "diskSizeGB": "[coalesce(parameters('dataDisks'), createArray())[copyIndex()].diskSizeGB]", + "creationData": { + "createOption": "[coalesce(tryGet(coalesce(parameters('dataDisks'), createArray())[copyIndex()], 'createoption'), 'Empty')]" + }, + "diskIOPSReadWrite": "[tryGet(coalesce(parameters('dataDisks'), createArray())[copyIndex()], 'diskIOPSReadWrite')]", + "diskMBpsReadWrite": "[tryGet(coalesce(parameters('dataDisks'), createArray())[copyIndex()], 'diskMBpsReadWrite')]", + "publicNetworkAccess": "[parameters('publicNetworkAccess')]", + "networkAccessPolicy": "[parameters('networkAccessPolicy')]" + }, + "zones": "[if(and(not(equals(parameters('availabilityZone'), -1)), not(contains(tryGet(coalesce(parameters('dataDisks'), createArray())[copyIndex()].managedDisk, 'storageAccountType'), 'ZRS'))), array(string(parameters('availabilityZone'))), null())]", + "tags": "[coalesce(tryGet(coalesce(parameters('dataDisks'), createArray())[copyIndex()], 'tags'), parameters('tags'))]" + }, + "vm": { + "type": "Microsoft.Compute/virtualMachines", + "apiVersion": "2024-07-01", + "name": "[parameters('name')]", + "location": "[parameters('location')]", + "identity": "[variables('identity')]", + "tags": "[parameters('tags')]", + "zones": "[if(not(equals(parameters('availabilityZone'), -1)), array(string(parameters('availabilityZone'))), null())]", + "plan": "[parameters('plan')]", + "properties": { + "hardwareProfile": { + "vmSize": "[parameters('vmSize')]" + }, + "securityProfile": "[shallowMerge(createArray(if(parameters('encryptionAtHost'), createObject('encryptionAtHost', parameters('encryptionAtHost')), createObject()), createObject('securityType', parameters('securityType'), 'uefiSettings', if(equals(parameters('securityType'), 'TrustedLaunch'), createObject('secureBootEnabled', parameters('secureBootEnabled'), 'vTpmEnabled', parameters('vTpmEnabled')), null()))))]", + "storageProfile": { + "copy": [ + { + "name": "dataDisks", + "count": "[length(coalesce(parameters('dataDisks'), createArray()))]", + "input": { + "lun": "[coalesce(tryGet(coalesce(parameters('dataDisks'), createArray())[copyIndex('dataDisks')], 'lun'), copyIndex('dataDisks'))]", + "name": "[if(not(empty(tryGet(coalesce(parameters('dataDisks'), createArray())[copyIndex('dataDisks')].managedDisk, 'id'))), last(split(coalesce(coalesce(parameters('dataDisks'), createArray())[copyIndex('dataDisks')].managedDisk.id, ''), '/')), coalesce(tryGet(coalesce(parameters('dataDisks'), createArray())[copyIndex('dataDisks')], 'name'), format('{0}-disk-data-{1}', parameters('name'), padLeft(add(copyIndex('dataDisks'), 1), 2, '0'))))]", + "createOption": "[if(or(not(equals(if(empty(tryGet(coalesce(parameters('dataDisks'), createArray())[copyIndex('dataDisks')].managedDisk, 'id')), resourceId('Microsoft.Compute/disks', coalesce(tryGet(coalesce(parameters('dataDisks'), createArray())[copyIndex('dataDisks')], 'name'), format('{0}-disk-data-{1}', parameters('name'), padLeft(add(copyIndex('dataDisks'), 1), 2, '0')))), null()), null())), not(empty(tryGet(coalesce(parameters('dataDisks'), createArray())[copyIndex('dataDisks')].managedDisk, 'id')))), 'Attach', coalesce(tryGet(coalesce(parameters('dataDisks'), createArray())[copyIndex('dataDisks')], 'createoption'), 'Empty'))]", + "deleteOption": "[if(not(empty(tryGet(coalesce(parameters('dataDisks'), createArray())[copyIndex('dataDisks')].managedDisk, 'id'))), 'Detach', coalesce(tryGet(coalesce(parameters('dataDisks'), createArray())[copyIndex('dataDisks')], 'deleteOption'), 'Delete'))]", + "caching": "[if(not(empty(tryGet(coalesce(parameters('dataDisks'), createArray())[copyIndex('dataDisks')].managedDisk, 'id'))), 'None', coalesce(tryGet(coalesce(parameters('dataDisks'), createArray())[copyIndex('dataDisks')], 'caching'), 'ReadOnly'))]", + "managedDisk": { + "id": "[coalesce(tryGet(coalesce(parameters('dataDisks'), createArray())[copyIndex('dataDisks')].managedDisk, 'id'), if(empty(tryGet(coalesce(parameters('dataDisks'), createArray())[copyIndex('dataDisks')].managedDisk, 'id')), resourceId('Microsoft.Compute/disks', coalesce(tryGet(coalesce(parameters('dataDisks'), createArray())[copyIndex('dataDisks')], 'name'), format('{0}-disk-data-{1}', parameters('name'), padLeft(add(copyIndex('dataDisks'), 1), 2, '0')))), null()))]", + "diskEncryptionSet": "[if(contains(coalesce(parameters('dataDisks'), createArray())[copyIndex('dataDisks')].managedDisk, 'diskEncryptionSet'), createObject('id', coalesce(parameters('dataDisks'), createArray())[copyIndex('dataDisks')].managedDisk.diskEncryptionSet.id), null())]" + } + } + } + ], + "imageReference": "[parameters('imageReference')]", + "osDisk": { + "name": "[coalesce(tryGet(parameters('osDisk'), 'name'), format('{0}-disk-os-01', parameters('name')))]", + "createOption": "[coalesce(tryGet(parameters('osDisk'), 'createOption'), 'FromImage')]", + "deleteOption": "[coalesce(tryGet(parameters('osDisk'), 'deleteOption'), 'Delete')]", + "diffDiskSettings": "[if(empty(coalesce(tryGet(parameters('osDisk'), 'diffDiskSettings'), createObject())), null(), createObject('option', 'Local', 'placement', parameters('osDisk').diffDiskSettings.placement))]", + "diskSizeGB": "[tryGet(parameters('osDisk'), 'diskSizeGB')]", + "caching": "[coalesce(tryGet(parameters('osDisk'), 'caching'), 'ReadOnly')]", + "managedDisk": { + "storageAccountType": "[tryGet(parameters('osDisk').managedDisk, 'storageAccountType')]", + "diskEncryptionSet": { + "id": "[tryGet(parameters('osDisk').managedDisk, 'diskEncryptionSetResourceId')]" + } + } + } + }, + "additionalCapabilities": { + "ultraSSDEnabled": "[parameters('ultraSSDEnabled')]", + "hibernationEnabled": "[parameters('hibernationEnabled')]" + }, + "osProfile": { + "computerName": "[parameters('computerName')]", + "adminUsername": "[parameters('adminUsername')]", + "adminPassword": "[parameters('adminPassword')]", + "customData": "[if(not(empty(parameters('customData'))), base64(parameters('customData')), null())]", + "windowsConfiguration": "[if(equals(parameters('osType'), 'Windows'), variables('windowsConfiguration'), null())]", + "linuxConfiguration": "[if(equals(parameters('osType'), 'Linux'), variables('linuxConfiguration'), null())]", + "secrets": "[parameters('certificatesToBeInstalled')]", + "allowExtensionOperations": "[parameters('allowExtensionOperations')]" + }, + "networkProfile": { + "copy": [ + { + "name": "networkInterfaces", + "count": "[length(parameters('nicConfigurations'))]", + "input": { + "properties": { + "deleteOption": "[coalesce(tryGet(parameters('nicConfigurations')[copyIndex('networkInterfaces')], 'deleteOption'), 'Delete')]", + "primary": "[if(equals(copyIndex('networkInterfaces'), 0), true(), false())]" + }, + "id": "[resourceId('Microsoft.Network/networkInterfaces', coalesce(tryGet(parameters('nicConfigurations')[copyIndex('networkInterfaces')], 'name'), format('{0}{1}', parameters('name'), tryGet(parameters('nicConfigurations')[copyIndex('networkInterfaces')], 'nicSuffix'))))]" + } + } + ] + }, + "capacityReservation": "[if(not(empty(parameters('capacityReservationGroupResourceId'))), createObject('capacityReservationGroup', createObject('id', parameters('capacityReservationGroupResourceId'))), null())]", + "diagnosticsProfile": { + "bootDiagnostics": { + "enabled": "[if(not(empty(parameters('bootDiagnosticStorageAccountName'))), true(), parameters('bootDiagnostics'))]", + "storageUri": "[if(not(empty(parameters('bootDiagnosticStorageAccountName'))), format('https://{0}{1}', parameters('bootDiagnosticStorageAccountName'), parameters('bootDiagnosticStorageAccountUri')), null())]" + } + }, + "applicationProfile": "[if(not(empty(parameters('galleryApplications'))), createObject('galleryApplications', parameters('galleryApplications')), null())]", + "availabilitySet": "[if(not(empty(parameters('availabilitySetResourceId'))), createObject('id', parameters('availabilitySetResourceId')), null())]", + "proximityPlacementGroup": "[if(not(empty(parameters('proximityPlacementGroupResourceId'))), createObject('id', parameters('proximityPlacementGroupResourceId')), null())]", + "virtualMachineScaleSet": "[if(not(empty(parameters('virtualMachineScaleSetResourceId'))), createObject('id', parameters('virtualMachineScaleSetResourceId')), null())]", + "priority": "[parameters('priority')]", + "evictionPolicy": "[if(and(not(empty(parameters('priority'))), not(equals(parameters('priority'), 'Regular'))), parameters('evictionPolicy'), null())]", + "billingProfile": "[if(and(not(empty(parameters('priority'))), not(empty(parameters('maxPriceForLowPriorityVm')))), createObject('maxPrice', json(parameters('maxPriceForLowPriorityVm'))), null())]", + "host": "[if(not(empty(parameters('dedicatedHostResourceId'))), createObject('id', parameters('dedicatedHostResourceId')), null())]", + "licenseType": "[parameters('licenseType')]", + "userData": "[if(not(empty(parameters('userData'))), base64(parameters('userData')), null())]" + }, + "dependsOn": [ + "managedDataDisks", + "vm_nic" + ] + }, + "vm_configurationAssignment": { + "condition": "[not(empty(parameters('maintenanceConfigurationResourceId')))]", + "type": "Microsoft.Maintenance/configurationAssignments", + "apiVersion": "2023-04-01", + "scope": "[format('Microsoft.Compute/virtualMachines/{0}', parameters('name'))]", + "name": "[format('{0}assignment', parameters('name'))]", + "location": "[parameters('location')]", + "properties": { + "maintenanceConfigurationId": "[parameters('maintenanceConfigurationResourceId')]", + "resourceId": "[resourceId('Microsoft.Compute/virtualMachines', parameters('name'))]" + }, + "dependsOn": [ + "vm" + ] + }, + "vm_configurationProfileAssignment": { + "condition": "[not(empty(parameters('configurationProfile')))]", + "type": "Microsoft.Automanage/configurationProfileAssignments", + "apiVersion": "2022-05-04", + "scope": "[format('Microsoft.Compute/virtualMachines/{0}', parameters('name'))]", + "name": "default", + "properties": { + "configurationProfile": "[parameters('configurationProfile')]" + }, + "dependsOn": [ + "vm" + ] + }, + "vm_autoShutdownConfiguration": { + "condition": "[not(empty(parameters('autoShutdownConfig')))]", + "type": "Microsoft.DevTestLab/schedules", + "apiVersion": "2018-09-15", + "name": "[format('shutdown-computevm-{0}', parameters('name'))]", + "location": "[parameters('location')]", + "properties": { + "status": "[coalesce(tryGet(parameters('autoShutdownConfig'), 'status'), 'Disabled')]", + "targetResourceId": "[resourceId('Microsoft.Compute/virtualMachines', parameters('name'))]", + "taskType": "ComputeVmShutdownTask", + "dailyRecurrence": { + "time": "[coalesce(tryGet(parameters('autoShutdownConfig'), 'dailyRecurrenceTime'), '19:00')]" + }, + "timeZoneId": "[coalesce(tryGet(parameters('autoShutdownConfig'), 'timeZone'), 'UTC')]", + "notificationSettings": "[if(contains(parameters('autoShutdownConfig'), 'notificationSettings'), createObject('status', coalesce(tryGet(parameters('autoShutdownConfig'), 'status'), 'Disabled'), 'emailRecipient', coalesce(tryGet(tryGet(parameters('autoShutdownConfig'), 'notificationSettings'), 'emailRecipient'), ''), 'notificationLocale', coalesce(tryGet(tryGet(parameters('autoShutdownConfig'), 'notificationSettings'), 'notificationLocale'), 'en'), 'webhookUrl', coalesce(tryGet(tryGet(parameters('autoShutdownConfig'), 'notificationSettings'), 'webhookUrl'), ''), 'timeInMinutes', coalesce(tryGet(tryGet(parameters('autoShutdownConfig'), 'notificationSettings'), 'timeInMinutes'), 30)), null())]" + }, + "dependsOn": [ + "vm" + ] + }, + "vm_dataCollectionRuleAssociations": { + "copy": { + "name": "vm_dataCollectionRuleAssociations", + "count": "[length(parameters('extensionMonitoringAgentConfig').dataCollectionRuleAssociations)]" + }, + "condition": "[parameters('extensionMonitoringAgentConfig').enabled]", + "type": "Microsoft.Insights/dataCollectionRuleAssociations", + "apiVersion": "2023-03-11", + "scope": "[format('Microsoft.Compute/virtualMachines/{0}', parameters('name'))]", + "name": "[parameters('extensionMonitoringAgentConfig').dataCollectionRuleAssociations[copyIndex()].name]", + "properties": { + "dataCollectionRuleId": "[parameters('extensionMonitoringAgentConfig').dataCollectionRuleAssociations[copyIndex()].dataCollectionRuleResourceId]" + }, + "dependsOn": [ + "vm", + "vm_azureMonitorAgentExtension" + ] + }, + "cseIdentity": { + "condition": "[not(empty(tryGet(tryGet(parameters('extensionCustomScriptConfig'), 'protectedSettings'), 'managedIdentityResourceId')))]", + "existing": true, + "type": "Microsoft.ManagedIdentity/userAssignedIdentities", + "apiVersion": "2024-11-30", + "subscriptionId": "[split(parameters('extensionCustomScriptConfig').protectedSettings.managedIdentityResourceId, '/')[2]]", + "resourceGroup": "[split(parameters('extensionCustomScriptConfig').protectedSettings.managedIdentityResourceId, '/')[4]]", + "name": "[last(split(parameters('extensionCustomScriptConfig').protectedSettings.managedIdentityResourceId, '/'))]" + }, + "AzureWindowsBaseline": { + "condition": "[not(empty(parameters('guestConfiguration')))]", + "type": "Microsoft.GuestConfiguration/guestConfigurationAssignments", + "apiVersion": "2024-04-05", + "scope": "[format('Microsoft.Compute/virtualMachines/{0}', parameters('name'))]", + "name": "[coalesce(tryGet(parameters('guestConfiguration'), 'name'), 'AzureWindowsBaseline')]", + "location": "[parameters('location')]", + "properties": { + "guestConfiguration": "[parameters('guestConfiguration')]" + }, + "dependsOn": [ + "vm", + "vm_azureGuestConfigurationExtension" + ] + }, + "vm_lock": { + "condition": "[and(not(empty(coalesce(parameters('lock'), createObject()))), not(equals(tryGet(parameters('lock'), 'kind'), 'None')))]", + "type": "Microsoft.Authorization/locks", + "apiVersion": "2020-05-01", + "scope": "[format('Microsoft.Compute/virtualMachines/{0}', parameters('name'))]", + "name": "[coalesce(tryGet(parameters('lock'), 'name'), format('lock-{0}', parameters('name')))]", + "properties": { + "level": "[coalesce(tryGet(parameters('lock'), 'kind'), '')]", + "notes": "[coalesce(tryGet(parameters('lock'), 'notes'), if(equals(tryGet(parameters('lock'), 'kind'), 'CanNotDelete'), 'Cannot delete resource or child resources.', 'Cannot delete or modify the resource or child resources.'))]" + }, + "dependsOn": [ + "vm" + ] + }, + "vm_roleAssignments": { + "copy": { + "name": "vm_roleAssignments", + "count": "[length(coalesce(variables('formattedRoleAssignments'), createArray()))]" + }, + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2022-04-01", + "scope": "[format('Microsoft.Compute/virtualMachines/{0}', parameters('name'))]", + "name": "[coalesce(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'name'), guid(resourceId('Microsoft.Compute/virtualMachines', parameters('name')), coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].principalId, coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].roleDefinitionId))]", + "properties": { + "roleDefinitionId": "[coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].roleDefinitionId]", + "principalId": "[coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].principalId]", + "description": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'description')]", + "principalType": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'principalType')]", + "condition": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'condition')]", + "conditionVersion": "[if(not(empty(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'condition'))), coalesce(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'conditionVersion'), '2.0'), null())]", + "delegatedManagedIdentityResourceId": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'delegatedManagedIdentityResourceId')]" + }, + "dependsOn": [ + "vm" + ] + }, + "vm_nic": { + "copy": { + "name": "vm_nic", + "count": "[length(parameters('nicConfigurations'))]" + }, + "type": "Microsoft.Resources/deployments", + "apiVersion": "2022-09-01", + "name": "[format('{0}-VM-Nic-{1}', uniqueString(deployment().name, parameters('location')), copyIndex())]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "networkInterfaceName": { + "value": "[coalesce(tryGet(parameters('nicConfigurations')[copyIndex()], 'name'), format('{0}{1}', parameters('name'), tryGet(parameters('nicConfigurations')[copyIndex()], 'nicSuffix')))]" + }, + "virtualMachineName": { + "value": "[parameters('name')]" + }, + "location": { + "value": "[parameters('location')]" + }, + "enableIPForwarding": { + "value": "[coalesce(tryGet(parameters('nicConfigurations')[copyIndex()], 'enableIPForwarding'), false())]" + }, + "enableAcceleratedNetworking": { + "value": "[coalesce(tryGet(parameters('nicConfigurations')[copyIndex()], 'enableAcceleratedNetworking'), true())]" + }, + "dnsServers": "[if(contains(parameters('nicConfigurations')[copyIndex()], 'dnsServers'), if(not(empty(tryGet(parameters('nicConfigurations')[copyIndex()], 'dnsServers'))), createObject('value', tryGet(parameters('nicConfigurations')[copyIndex()], 'dnsServers')), createObject('value', createArray())), createObject('value', createArray()))]", + "networkSecurityGroupResourceId": { + "value": "[coalesce(tryGet(parameters('nicConfigurations')[copyIndex()], 'networkSecurityGroupResourceId'), '')]" + }, + "ipConfigurations": { + "value": "[parameters('nicConfigurations')[copyIndex()].ipConfigurations]" + }, + "lock": { + "value": "[coalesce(tryGet(parameters('nicConfigurations')[copyIndex()], 'lock'), parameters('lock'))]" + }, + "tags": { + "value": "[coalesce(tryGet(parameters('nicConfigurations')[copyIndex()], 'tags'), parameters('tags'))]" + }, + "diagnosticSettings": { + "value": "[tryGet(parameters('nicConfigurations')[copyIndex()], 'diagnosticSettings')]" + }, + "roleAssignments": { + "value": "[tryGet(parameters('nicConfigurations')[copyIndex()], 'roleAssignments')]" + }, + "enableTelemetry": { + "value": "[variables('enableReferencedModulesTelemetry')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "languageVersion": "2.0", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.37.4.10188", + "templateHash": "774019590280042559" + } + }, + "definitions": { + "publicIPConfigurationType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The name of the Public IP Address." + } + }, + "publicIPAddressResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The resource ID of the public IP address." + } + }, + "diagnosticSettings": { + "type": "array", + "items": { + "$ref": "#/definitions/diagnosticSettingFullType" + }, + "nullable": true, + "metadata": { + "description": "Optional. Diagnostic settings for the public IP address." + } + }, + "location": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The idle timeout in minutes." + } + }, + "lock": { + "$ref": "#/definitions/lockType", + "nullable": true, + "metadata": { + "description": "Optional. The lock settings of the public IP address." + } + }, + "idleTimeoutInMinutes": { + "type": "int", + "nullable": true, + "metadata": { + "description": "Optional. The idle timeout of the public IP address." + } + }, + "ddosSettings": { + "$ref": "#/definitions/ddosSettingsType", + "nullable": true, + "metadata": { + "description": "Optional. The DDoS protection plan configuration associated with the public IP address." + } + }, + "dnsSettings": { + "$ref": "#/definitions/dnsSettingsType", + "nullable": true, + "metadata": { + "description": "Optional. The DNS settings of the public IP address." + } + }, + "publicIPAddressVersion": { + "type": "string", + "allowedValues": [ + "IPv4", + "IPv6" + ], + "nullable": true, + "metadata": { + "description": "Optional. The public IP address version." + } + }, + "publicIPAllocationMethod": { + "type": "string", + "allowedValues": [ + "Dynamic", + "Static" + ], + "nullable": true, + "metadata": { + "description": "Optional. The public IP address allocation method." + } + }, + "publicIpPrefixResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Resource ID of the Public IP Prefix object. This is only needed if you want your Public IPs created in a PIP Prefix." + } + }, + "publicIpNameSuffix": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The name suffix of the public IP address resource." + } + }, + "roleAssignments": { + "type": "array", + "items": { + "$ref": "#/definitions/roleAssignmentType" + }, + "nullable": true, + "metadata": { + "description": "Optional. Array of role assignments to create." + } + }, + "skuName": { + "type": "string", + "allowedValues": [ + "Basic", + "Standard" + ], + "nullable": true, + "metadata": { + "description": "Optional. The SKU name of the public IP address." + } + }, + "skuTier": { + "type": "string", + "allowedValues": [ + "Global", + "Regional" + ], + "nullable": true, + "metadata": { + "description": "Optional. The SKU tier of the public IP address." + } + }, + "tags": { + "type": "object", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.Network/publicIPAddresses@2024-07-01#properties/tags" + }, + "description": "Optional. The tags of the public IP address." + }, + "nullable": true + }, + "availabilityZones": { + "type": "array", + "allowedValues": [ + 1, + 2, + 3 + ], + "nullable": true, + "metadata": { + "description": "Optional. The zones of the public IP address." + } + }, + "ipTags": { + "type": "array", + "items": { + "$ref": "#/definitions/ipTagType" + }, + "nullable": true, + "metadata": { + "description": "Optional. The list of tags associated with the public IP address." + } + }, + "enableTelemetry": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. Enable/Disable usage telemetry for the module." + } + } + }, + "metadata": { + "__bicep_export!": true, + "description": "The type for the public IP address configuration." + } + }, + "ipConfigurationType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The name of the IP configuration." + } + }, + "privateIPAllocationMethod": { + "type": "string", + "allowedValues": [ + "Dynamic", + "Static" + ], + "nullable": true, + "metadata": { + "description": "Optional. The private IP address allocation method." + } + }, + "privateIPAddress": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The private IP address." + } + }, + "subnetResourceId": { + "type": "string", + "metadata": { + "description": "Required. The resource ID of the subnet." + } + }, + "loadBalancerBackendAddressPools": { + "type": "array", + "items": { + "$ref": "#/definitions/backendAddressPoolType" + }, + "nullable": true, + "metadata": { + "description": "Optional. The load balancer backend address pools." + } + }, + "applicationSecurityGroups": { + "type": "array", + "items": { + "$ref": "#/definitions/applicationSecurityGroupType" + }, + "nullable": true, + "metadata": { + "description": "Optional. The application security groups." + } + }, + "applicationGatewayBackendAddressPools": { + "type": "array", + "items": { + "$ref": "#/definitions/applicationGatewayBackendAddressPoolsType" + }, + "nullable": true, + "metadata": { + "description": "Optional. The application gateway backend address pools." + } + }, + "gatewayLoadBalancer": { + "$ref": "#/definitions/subResourceType", + "nullable": true, + "metadata": { + "description": "Optional. The gateway load balancer settings." + } + }, + "loadBalancerInboundNatRules": { + "type": "array", + "items": { + "$ref": "#/definitions/inboundNatRuleType" + }, + "nullable": true, + "metadata": { + "description": "Optional. The load balancer inbound NAT rules." + } + }, + "privateIPAddressVersion": { + "type": "string", + "allowedValues": [ + "IPv4", + "IPv6" + ], + "nullable": true, + "metadata": { + "description": "Optional. The private IP address version." + } + }, + "virtualNetworkTaps": { + "type": "array", + "items": { + "$ref": "#/definitions/virtualNetworkTapType" + }, + "nullable": true, + "metadata": { + "description": "Optional. The virtual network taps." + } + }, + "pipConfiguration": { + "$ref": "#/definitions/publicIPConfigurationType", + "nullable": true, + "metadata": { + "description": "Optional. The public IP address configuration." + } + }, + "diagnosticSettings": { + "type": "array", + "items": { + "$ref": "#/definitions/diagnosticSettingFullType" + }, + "nullable": true, + "metadata": { + "description": "Optional. The diagnostic settings of the IP configuration." + } + }, + "tags": { + "type": "object", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.Network/networkInterfaces@2024-07-01#properties/tags" + }, + "description": "Optional. The tags of the public IP address." + }, + "nullable": true + }, + "enableTelemetry": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. Enable/Disable usage telemetry for the module." + } + } + }, + "metadata": { + "__bicep_export!": true, + "description": "The type for the IP configuration." + } + }, + "applicationGatewayBackendAddressPoolsType": { + "type": "object", + "properties": { + "id": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Resource ID of the backend address pool." + } + }, + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Name of the backend address pool that is unique within an Application Gateway." + } + }, + "properties": { + "type": "object", + "properties": { + "backendAddresses": { + "type": "array", + "items": { + "type": "object", + "properties": { + "ipAddress": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. IP address of the backend address." + } + }, + "fqdn": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. FQDN of the backend address." + } + } + } + }, + "nullable": true, + "metadata": { + "description": "Optional. Backend addresses." + } + } + }, + "nullable": true, + "metadata": { + "description": "Optional. Properties of the application gateway backend address pool." + } + } + }, + "metadata": { + "description": "The type for the application gateway backend address pool.", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/res/network/network-interface:0.5.1" + } + } + }, + "applicationSecurityGroupType": { + "type": "object", + "properties": { + "id": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Resource ID of the application security group." + } + }, + "location": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Location of the application security group." + } + }, + "properties": { + "type": "object", + "nullable": true, + "metadata": { + "description": "Optional. Properties of the application security group." + } + }, + "tags": { + "type": "object", + "nullable": true, + "metadata": { + "description": "Optional. Tags of the application security group." + } + } + }, + "metadata": { + "description": "The type for the application security group.", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/res/network/network-interface:0.5.1" + } + } + }, + "backendAddressPoolType": { + "type": "object", + "properties": { + "id": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The resource ID of the backend address pool." + } + }, + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The name of the backend address pool." + } + }, + "properties": { + "type": "object", + "nullable": true, + "metadata": { + "description": "Optional. The properties of the backend address pool." + } + } + }, + "metadata": { + "description": "The type for a backend address pool.", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/res/network/network-interface:0.5.1" + } + } + }, + "ddosSettingsType": { + "type": "object", + "properties": { + "ddosProtectionPlan": { + "type": "object", + "properties": { + "id": { + "type": "string", + "metadata": { + "description": "Required. The resource ID of the DDOS protection plan associated with the public IP address." + } + } + }, + "nullable": true, + "metadata": { + "description": "Optional. The DDoS protection plan associated with the public IP address." + } + }, + "protectionMode": { + "type": "string", + "allowedValues": [ + "Enabled" + ], + "metadata": { + "description": "Required. The DDoS protection policy customizations." + } + } + }, + "metadata": { + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/res/network/public-ip-address:0.8.0" + } + } + }, + "diagnosticSettingFullType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The name of the diagnostic setting." + } + }, + "logCategoriesAndGroups": { + "type": "array", + "items": { + "type": "object", + "properties": { + "category": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Name of a Diagnostic Log category for a resource type this setting is applied to. Set the specific logs to collect here." + } + }, + "categoryGroup": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Name of a Diagnostic Log category group for a resource type this setting is applied to. Set to `allLogs` to collect all logs." + } + }, + "enabled": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. Enable or disable the category explicitly. Default is `true`." + } + } + } + }, + "nullable": true, + "metadata": { + "description": "Optional. The name of logs that will be streamed. \"allLogs\" includes all possible logs for the resource. Set to `[]` to disable log collection." + } + }, + "metricCategories": { + "type": "array", + "items": { + "type": "object", + "properties": { + "category": { + "type": "string", + "metadata": { + "description": "Required. Name of a Diagnostic Metric category for a resource type this setting is applied to. Set to `AllMetrics` to collect all metrics." + } + }, + "enabled": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. Enable or disable the category explicitly. Default is `true`." + } + } + } + }, + "nullable": true, + "metadata": { + "description": "Optional. The name of metrics that will be streamed. \"allMetrics\" includes all possible metrics for the resource. Set to `[]` to disable metric collection." + } + }, + "logAnalyticsDestinationType": { + "type": "string", + "allowedValues": [ + "AzureDiagnostics", + "Dedicated" + ], + "nullable": true, + "metadata": { + "description": "Optional. A string indicating whether the export to Log Analytics should use the default destination type, i.e. AzureDiagnostics, or use a destination type." + } + }, + "workspaceResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Resource ID of the diagnostic log analytics workspace. For security reasons, it is recommended to set diagnostic settings to send data to either storage account, log analytics workspace or event hub." + } + }, + "storageAccountResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Resource ID of the diagnostic storage account. For security reasons, it is recommended to set diagnostic settings to send data to either storage account, log analytics workspace or event hub." + } + }, + "eventHubAuthorizationRuleResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Resource ID of the diagnostic event hub authorization rule for the Event Hubs namespace in which the event hub should be created or streamed to." + } + }, + "eventHubName": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Name of the diagnostic event hub within the namespace to which logs are streamed. Without this, an event hub is created for each log category. For security reasons, it is recommended to set diagnostic settings to send data to either storage account, log analytics workspace or event hub." + } + }, + "marketplacePartnerResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The full ARM resource ID of the Marketplace resource to which you would like to send Diagnostic Logs." + } + } + }, + "metadata": { + "description": "An AVM-aligned type for a diagnostic setting. To be used if both logs & metrics are supported by the resource provider.", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.5.1" + } + } + }, + "dnsSettingsType": { + "type": "object", + "properties": { + "domainNameLabel": { + "type": "string", + "metadata": { + "description": "Required. The domain name label. The concatenation of the domain name label and the regionalized DNS zone make up the fully qualified domain name associated with the public IP address. If a domain name label is specified, an A DNS record is created for the public IP in the Microsoft Azure DNS system." + } + }, + "domainNameLabelScope": { + "type": "string", + "allowedValues": [ + "NoReuse", + "ResourceGroupReuse", + "SubscriptionReuse", + "TenantReuse" + ], + "nullable": true, + "metadata": { + "description": "Optional. The domain name label scope. If a domain name label and a domain name label scope are specified, an A DNS record is created for the public IP in the Microsoft Azure DNS system with a hashed value includes in FQDN." + } + }, + "fqdn": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The Fully Qualified Domain Name of the A DNS record associated with the public IP. This is the concatenation of the domainNameLabel and the regionalized DNS zone." + } + }, + "reverseFqdn": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The reverse FQDN. A user-visible, fully qualified domain name that resolves to this public IP address. If the reverseFqdn is specified, then a PTR DNS record is created pointing from the IP address in the in-addr.arpa domain to the reverse FQDN." + } + } + }, + "metadata": { + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/res/network/public-ip-address:0.8.0" + } + } + }, + "inboundNatRuleType": { + "type": "object", + "properties": { + "id": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Resource ID of the inbound NAT rule." + } + }, + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Name of the resource that is unique within the set of inbound NAT rules used by the load balancer. This name can be used to access the resource." + } + }, + "properties": { + "type": "object", + "properties": { + "backendAddressPool": { + "$ref": "#/definitions/subResourceType", + "nullable": true, + "metadata": { + "description": "Optional. A reference to backendAddressPool resource." + } + }, + "backendPort": { + "type": "int", + "nullable": true, + "metadata": { + "description": "Optional. The port used for the internal endpoint. Acceptable values range from 1 to 65535." + } + }, + "enableFloatingIP": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. Configures a virtual machine's endpoint for the floating IP capability required to configure a SQL AlwaysOn Availability Group. This setting is required when using the SQL AlwaysOn Availability Groups in SQL server. This setting can't be changed after you create the endpoint." + } + }, + "enableTcpReset": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. Receive bidirectional TCP Reset on TCP flow idle timeout or unexpected connection termination. This element is only used when the protocol is set to TCP." + } + }, + "frontendIPConfiguration": { + "$ref": "#/definitions/subResourceType", + "nullable": true, + "metadata": { + "description": "Optional. A reference to frontend IP addresses." + } + }, + "frontendPort": { + "type": "int", + "nullable": true, + "metadata": { + "description": "Optional. The port for the external endpoint. Port numbers for each rule must be unique within the Load Balancer. Acceptable values range from 1 to 65534." + } + }, + "frontendPortRangeStart": { + "type": "int", + "nullable": true, + "metadata": { + "description": "Optional. The port range start for the external endpoint. This property is used together with BackendAddressPool and FrontendPortRangeEnd. Individual inbound NAT rule port mappings will be created for each backend address from BackendAddressPool. Acceptable values range from 1 to 65534." + } + }, + "frontendPortRangeEnd": { + "type": "int", + "nullable": true, + "metadata": { + "description": "Optional. The port range end for the external endpoint. This property is used together with BackendAddressPool and FrontendPortRangeStart. Individual inbound NAT rule port mappings will be created for each backend address from BackendAddressPool. Acceptable values range from 1 to 65534." + } + }, + "protocol": { + "type": "string", + "allowedValues": [ + "All", + "Tcp", + "Udp" + ], + "nullable": true, + "metadata": { + "description": "Optional. The reference to the transport protocol used by the load balancing rule." + } + } + }, + "nullable": true, + "metadata": { + "description": "Optional. Properties of the inbound NAT rule." + } + } + }, + "metadata": { + "description": "The type for the inbound NAT rule.", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/res/network/network-interface:0.5.1" + } + } + }, + "ipTagType": { + "type": "object", + "properties": { + "ipTagType": { + "type": "string", + "metadata": { + "description": "Required. The IP tag type." + } + }, + "tag": { + "type": "string", + "metadata": { + "description": "Required. The IP tag." + } + } + }, + "metadata": { + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/res/network/public-ip-address:0.8.0" + } + } + }, + "lockType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Specify the name of lock." + } + }, + "kind": { + "type": "string", + "allowedValues": [ + "CanNotDelete", + "None", + "ReadOnly" + ], + "nullable": true, + "metadata": { + "description": "Optional. Specify the type of lock." + } + }, + "notes": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Specify the notes of the lock." + } + } + }, + "metadata": { + "description": "An AVM-aligned type for a lock.", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.0" + } + } + }, + "networkInterfaceIPConfigurationOutputType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "metadata": { + "description": "The name of the IP configuration." + } + }, + "privateIP": { + "type": "string", + "nullable": true, + "metadata": { + "description": "The private IP address." + } + }, + "publicIP": { + "type": "string", + "nullable": true, + "metadata": { + "description": "The public IP address." + } + } + }, + "metadata": { + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/res/network/network-interface:0.5.1" + } + } + }, + "roleAssignmentType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The name (as GUID) of the role assignment. If not provided, a GUID will be generated." + } + }, + "roleDefinitionIdOrName": { + "type": "string", + "metadata": { + "description": "Required. The role to assign. You can provide either the display name of the role definition, the role definition GUID, or its fully qualified ID in the following format: '/providers/Microsoft.Authorization/roleDefinitions/c2f4ef07-c644-48eb-af81-4b1b4947fb11'." + } + }, + "principalId": { + "type": "string", + "metadata": { + "description": "Required. The principal ID of the principal (user/group/identity) to assign the role to." + } + }, + "principalType": { + "type": "string", + "allowedValues": [ + "Device", + "ForeignGroup", + "Group", + "ServicePrincipal", + "User" + ], + "nullable": true, + "metadata": { + "description": "Optional. The principal type of the assigned principal ID." + } + }, + "description": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The description of the role assignment." + } + }, + "condition": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The conditions on the role assignment. This limits the resources it can be assigned to. e.g.: @Resource[Microsoft.Storage/storageAccounts/blobServices/containers:ContainerName] StringEqualsIgnoreCase \"foo_storage_container\"." + } + }, + "conditionVersion": { + "type": "string", + "allowedValues": [ + "2.0" + ], + "nullable": true, + "metadata": { + "description": "Optional. Version of the condition." + } + }, + "delegatedManagedIdentityResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The Resource Id of the delegated managed identity resource." + } + } + }, + "metadata": { + "description": "An AVM-aligned type for a role assignment.", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.5.1" + } + } + }, + "subResourceType": { + "type": "object", + "properties": { + "id": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Resource ID of the sub resource." + } + } + }, + "metadata": { + "description": "The type for the sub resource.", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/res/network/network-interface:0.5.1" + } + } + }, + "virtualNetworkTapType": { + "type": "object", + "properties": { + "id": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Resource ID of the virtual network tap." + } + }, + "location": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Location of the virtual network tap." + } + }, + "properties": { + "type": "object", + "nullable": true, + "metadata": { + "description": "Optional. Properties of the virtual network tap." + } + }, + "tags": { + "type": "object", + "nullable": true, + "metadata": { + "description": "Optional. Tags of the virtual network tap." + } + } + }, + "metadata": { + "description": "The type for the virtual network tap.", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/res/network/network-interface:0.5.1" + } + } + } + }, + "parameters": { + "networkInterfaceName": { + "type": "string" + }, + "virtualMachineName": { + "type": "string" + }, + "ipConfigurations": { + "type": "array", + "items": { + "$ref": "#/definitions/ipConfigurationType" + } + }, + "location": { + "type": "string", + "metadata": { + "description": "Optional. Location for all resources." + } + }, + "tags": { + "type": "object", + "nullable": true, + "metadata": { + "description": "Optional. Tags of the resource." + } + }, + "enableIPForwarding": { + "type": "bool", + "defaultValue": false + }, + "enableAcceleratedNetworking": { + "type": "bool", + "defaultValue": false + }, + "dnsServers": { + "type": "array", + "items": { + "type": "string" + }, + "defaultValue": [] + }, + "enableTelemetry": { + "type": "bool", + "metadata": { + "description": "Required. Enable telemetry via a Globally Unique Identifier (GUID)." + } + }, + "networkSecurityGroupResourceId": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "Optional. The network security group (NSG) to attach to the network interface." + } + }, + "lock": { + "$ref": "#/definitions/lockType", + "nullable": true, + "metadata": { + "description": "Optional. The lock settings of the service." + } + }, + "diagnosticSettings": { + "type": "array", + "items": { + "$ref": "#/definitions/diagnosticSettingFullType" + }, + "nullable": true, + "metadata": { + "description": "Optional. The diagnostic settings of the service." + } + }, + "roleAssignments": { + "type": "array", + "items": { + "$ref": "#/definitions/roleAssignmentType" + }, + "nullable": true, + "metadata": { + "description": "Optional. Array of role assignments to create." + } + } + }, + "resources": { + "networkInterface_publicIPAddresses": { + "copy": { + "name": "networkInterface_publicIPAddresses", + "count": "[length(parameters('ipConfigurations'))]" + }, + "condition": "[and(not(empty(tryGet(parameters('ipConfigurations')[copyIndex()], 'pipConfiguration'))), empty(tryGet(tryGet(parameters('ipConfigurations')[copyIndex()], 'pipConfiguration'), 'publicIPAddressResourceId')))]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2022-09-01", + "name": "[format('{0}-publicIP-{1}', deployment().name, copyIndex())]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "name": { + "value": "[coalesce(tryGet(tryGet(parameters('ipConfigurations')[copyIndex()], 'pipConfiguration'), 'name'), format('{0}{1}', parameters('virtualMachineName'), tryGet(tryGet(parameters('ipConfigurations')[copyIndex()], 'pipConfiguration'), 'publicIpNameSuffix')))]" + }, + "diagnosticSettings": { + "value": "[coalesce(tryGet(tryGet(parameters('ipConfigurations')[copyIndex()], 'pipConfiguration'), 'diagnosticSettings'), tryGet(parameters('ipConfigurations')[copyIndex()], 'diagnosticSettings'))]" + }, + "location": { + "value": "[parameters('location')]" + }, + "lock": { + "value": "[parameters('lock')]" + }, + "idleTimeoutInMinutes": { + "value": "[tryGet(tryGet(parameters('ipConfigurations')[copyIndex()], 'pipConfiguration'), 'idleTimeoutInMinutes')]" + }, + "ddosSettings": { + "value": "[tryGet(tryGet(parameters('ipConfigurations')[copyIndex()], 'pipConfiguration'), 'ddosSettings')]" + }, + "dnsSettings": { + "value": "[tryGet(tryGet(parameters('ipConfigurations')[copyIndex()], 'pipConfiguration'), 'dnsSettings')]" + }, + "publicIPAddressVersion": { + "value": "[tryGet(tryGet(parameters('ipConfigurations')[copyIndex()], 'pipConfiguration'), 'publicIPAddressVersion')]" + }, + "publicIPAllocationMethod": { + "value": "[tryGet(tryGet(parameters('ipConfigurations')[copyIndex()], 'pipConfiguration'), 'publicIPAllocationMethod')]" + }, + "publicIpPrefixResourceId": { + "value": "[tryGet(tryGet(parameters('ipConfigurations')[copyIndex()], 'pipConfiguration'), 'publicIpPrefixResourceId')]" + }, + "roleAssignments": { + "value": "[tryGet(tryGet(parameters('ipConfigurations')[copyIndex()], 'pipConfiguration'), 'roleAssignments')]" + }, + "skuName": { + "value": "[tryGet(tryGet(parameters('ipConfigurations')[copyIndex()], 'pipConfiguration'), 'skuName')]" + }, + "skuTier": { + "value": "[tryGet(tryGet(parameters('ipConfigurations')[copyIndex()], 'pipConfiguration'), 'skuTier')]" + }, + "tags": { + "value": "[coalesce(tryGet(parameters('ipConfigurations')[copyIndex()], 'tags'), parameters('tags'))]" + }, + "availabilityZones": { + "value": "[tryGet(tryGet(parameters('ipConfigurations')[copyIndex()], 'pipConfiguration'), 'availabilityZones')]" + }, + "enableTelemetry": { + "value": "[coalesce(coalesce(tryGet(tryGet(parameters('ipConfigurations')[copyIndex()], 'pipConfiguration'), 'enableTelemetry'), tryGet(parameters('ipConfigurations')[copyIndex()], 'enableTelemetry')), parameters('enableTelemetry'))]" + }, + "ipTags": { + "value": "[tryGet(tryGet(parameters('ipConfigurations')[copyIndex()], 'pipConfiguration'), 'ipTags')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "languageVersion": "2.0", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.36.177.2456", + "templateHash": "14921988046704902194" + }, + "name": "Public IP Addresses", + "description": "This module deploys a Public IP Address." + }, + "definitions": { + "dnsSettingsType": { + "type": "object", + "properties": { + "domainNameLabel": { + "type": "string", + "metadata": { + "description": "Required. The domain name label. The concatenation of the domain name label and the regionalized DNS zone make up the fully qualified domain name associated with the public IP address. If a domain name label is specified, an A DNS record is created for the public IP in the Microsoft Azure DNS system." + } + }, + "domainNameLabelScope": { + "type": "string", + "allowedValues": [ + "NoReuse", + "ResourceGroupReuse", + "SubscriptionReuse", + "TenantReuse" + ], + "nullable": true, + "metadata": { + "description": "Optional. The domain name label scope. If a domain name label and a domain name label scope are specified, an A DNS record is created for the public IP in the Microsoft Azure DNS system with a hashed value includes in FQDN." + } + }, + "fqdn": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The Fully Qualified Domain Name of the A DNS record associated with the public IP. This is the concatenation of the domainNameLabel and the regionalized DNS zone." + } + }, + "reverseFqdn": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The reverse FQDN. A user-visible, fully qualified domain name that resolves to this public IP address. If the reverseFqdn is specified, then a PTR DNS record is created pointing from the IP address in the in-addr.arpa domain to the reverse FQDN." + } + } + }, + "metadata": { + "__bicep_export!": true + } + }, + "ddosSettingsType": { + "type": "object", + "properties": { + "ddosProtectionPlan": { + "type": "object", + "properties": { + "id": { + "type": "string", + "metadata": { + "description": "Required. The resource ID of the DDOS protection plan associated with the public IP address." + } + } + }, + "nullable": true, + "metadata": { + "description": "Optional. The DDoS protection plan associated with the public IP address." + } + }, + "protectionMode": { + "type": "string", + "allowedValues": [ + "Enabled" + ], + "metadata": { + "description": "Required. The DDoS protection policy customizations." + } + } + }, + "metadata": { + "__bicep_export!": true + } + }, + "ipTagType": { + "type": "object", + "properties": { + "ipTagType": { + "type": "string", + "metadata": { + "description": "Required. The IP tag type." + } + }, + "tag": { + "type": "string", + "metadata": { + "description": "Required. The IP tag." + } + } + }, + "metadata": { + "__bicep_export!": true + } + }, + "diagnosticSettingFullType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The name of the diagnostic setting." + } + }, + "logCategoriesAndGroups": { + "type": "array", + "items": { + "type": "object", + "properties": { + "category": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Name of a Diagnostic Log category for a resource type this setting is applied to. Set the specific logs to collect here." + } + }, + "categoryGroup": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Name of a Diagnostic Log category group for a resource type this setting is applied to. Set to `allLogs` to collect all logs." + } + }, + "enabled": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. Enable or disable the category explicitly. Default is `true`." + } + } + } + }, + "nullable": true, + "metadata": { + "description": "Optional. The name of logs that will be streamed. \"allLogs\" includes all possible logs for the resource. Set to `[]` to disable log collection." + } + }, + "metricCategories": { + "type": "array", + "items": { + "type": "object", + "properties": { + "category": { + "type": "string", + "metadata": { + "description": "Required. Name of a Diagnostic Metric category for a resource type this setting is applied to. Set to `AllMetrics` to collect all metrics." + } + }, + "enabled": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. Enable or disable the category explicitly. Default is `true`." + } + } + } + }, + "nullable": true, + "metadata": { + "description": "Optional. The name of metrics that will be streamed. \"allMetrics\" includes all possible metrics for the resource. Set to `[]` to disable metric collection." + } + }, + "logAnalyticsDestinationType": { + "type": "string", + "allowedValues": [ + "AzureDiagnostics", + "Dedicated" + ], + "nullable": true, + "metadata": { + "description": "Optional. A string indicating whether the export to Log Analytics should use the default destination type, i.e. AzureDiagnostics, or use a destination type." + } + }, + "workspaceResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Resource ID of the diagnostic log analytics workspace. For security reasons, it is recommended to set diagnostic settings to send data to either storage account, log analytics workspace or event hub." + } + }, + "storageAccountResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Resource ID of the diagnostic storage account. For security reasons, it is recommended to set diagnostic settings to send data to either storage account, log analytics workspace or event hub." + } + }, + "eventHubAuthorizationRuleResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Resource ID of the diagnostic event hub authorization rule for the Event Hubs namespace in which the event hub should be created or streamed to." + } + }, + "eventHubName": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Name of the diagnostic event hub within the namespace to which logs are streamed. Without this, an event hub is created for each log category. For security reasons, it is recommended to set diagnostic settings to send data to either storage account, log analytics workspace or event hub." + } + }, + "marketplacePartnerResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The full ARM resource ID of the Marketplace resource to which you would like to send Diagnostic Logs." + } + } + }, + "metadata": { + "description": "An AVM-aligned type for a diagnostic setting. To be used if both logs & metrics are supported by the resource provider.", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.2.1" + } + } + }, + "lockType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Specify the name of lock." + } + }, + "kind": { + "type": "string", + "allowedValues": [ + "CanNotDelete", + "None", + "ReadOnly" + ], + "nullable": true, + "metadata": { + "description": "Optional. Specify the type of lock." + } + } + }, + "metadata": { + "description": "An AVM-aligned type for a lock.", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.2.1" + } + } + }, + "roleAssignmentType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The name (as GUID) of the role assignment. If not provided, a GUID will be generated." + } + }, + "roleDefinitionIdOrName": { + "type": "string", + "metadata": { + "description": "Required. The role to assign. You can provide either the display name of the role definition, the role definition GUID, or its fully qualified ID in the following format: '/providers/Microsoft.Authorization/roleDefinitions/c2f4ef07-c644-48eb-af81-4b1b4947fb11'." + } + }, + "principalId": { + "type": "string", + "metadata": { + "description": "Required. The principal ID of the principal (user/group/identity) to assign the role to." + } + }, + "principalType": { + "type": "string", + "allowedValues": [ + "Device", + "ForeignGroup", + "Group", + "ServicePrincipal", + "User" + ], + "nullable": true, + "metadata": { + "description": "Optional. The principal type of the assigned principal ID." + } + }, + "description": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The description of the role assignment." + } + }, + "condition": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The conditions on the role assignment. This limits the resources it can be assigned to. e.g.: @Resource[Microsoft.Storage/storageAccounts/blobServices/containers:ContainerName] StringEqualsIgnoreCase \"foo_storage_container\"." + } + }, + "conditionVersion": { + "type": "string", + "allowedValues": [ + "2.0" + ], + "nullable": true, + "metadata": { + "description": "Optional. Version of the condition." + } + }, + "delegatedManagedIdentityResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The Resource Id of the delegated managed identity resource." + } + } + }, + "metadata": { + "description": "An AVM-aligned type for a role assignment.", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.2.1" + } + } + } + }, + "parameters": { + "name": { + "type": "string", + "metadata": { + "description": "Required. The name of the Public IP Address." + } + }, + "publicIpPrefixResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Resource ID of the Public IP Prefix object. This is only needed if you want your Public IPs created in a PIP Prefix." + } + }, + "publicIPAllocationMethod": { + "type": "string", + "defaultValue": "Static", + "allowedValues": [ + "Dynamic", + "Static" + ], + "metadata": { + "description": "Optional. The public IP address allocation method." + } + }, + "availabilityZones": { + "type": "array", + "items": { + "type": "int" + }, + "defaultValue": [ + 1, + 2, + 3 + ], + "allowedValues": [ + 1, + 2, + 3 + ], + "metadata": { + "description": "Optional. A list of availability zones denoting the IP allocated for the resource needs to come from." + } + }, + "publicIPAddressVersion": { + "type": "string", + "defaultValue": "IPv4", + "allowedValues": [ + "IPv4", + "IPv6" + ], + "metadata": { + "description": "Optional. IP address version." + } + }, + "dnsSettings": { + "$ref": "#/definitions/dnsSettingsType", + "nullable": true, + "metadata": { + "description": "Optional. The DNS settings of the public IP address." + } + }, + "ipTags": { + "type": "array", + "items": { + "$ref": "#/definitions/ipTagType" + }, + "nullable": true, + "metadata": { + "description": "Optional. The list of tags associated with the public IP address." + } + }, + "lock": { + "$ref": "#/definitions/lockType", + "nullable": true, + "metadata": { + "description": "Optional. The lock settings of the service." + } + }, + "skuName": { + "type": "string", + "defaultValue": "Standard", + "allowedValues": [ + "Basic", + "Standard" + ], + "metadata": { + "description": "Optional. Name of a public IP address SKU." + } + }, + "skuTier": { + "type": "string", + "defaultValue": "Regional", + "allowedValues": [ + "Global", + "Regional" + ], + "metadata": { + "description": "Optional. Tier of a public IP address SKU." + } + }, + "ddosSettings": { + "$ref": "#/definitions/ddosSettingsType", + "nullable": true, + "metadata": { + "description": "Optional. The DDoS protection plan configuration associated with the public IP address." + } + }, + "location": { + "type": "string", + "defaultValue": "[resourceGroup().location]", + "metadata": { + "description": "Optional. Location for all resources." + } + }, + "roleAssignments": { + "type": "array", + "items": { + "$ref": "#/definitions/roleAssignmentType" + }, + "nullable": true, + "metadata": { + "description": "Optional. Array of role assignments to create." + } + }, + "enableTelemetry": { + "type": "bool", + "defaultValue": true, + "metadata": { + "description": "Optional. Enable/Disable usage telemetry for module." + } + }, + "idleTimeoutInMinutes": { + "type": "int", + "defaultValue": 4, + "metadata": { + "description": "Optional. The idle timeout of the public IP address." + } + }, + "tags": { + "type": "object", + "nullable": true, + "metadata": { + "description": "Optional. Tags of the resource." + } + }, + "diagnosticSettings": { + "type": "array", + "items": { + "$ref": "#/definitions/diagnosticSettingFullType" + }, + "nullable": true, + "metadata": { + "description": "Optional. The diagnostic settings of the service." + } + } + }, + "variables": { + "copy": [ + { + "name": "formattedRoleAssignments", + "count": "[length(coalesce(parameters('roleAssignments'), createArray()))]", + "input": "[union(coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')], createObject('roleDefinitionId', coalesce(tryGet(variables('builtInRoleNames'), coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName), if(contains(coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName, '/providers/Microsoft.Authorization/roleDefinitions/'), coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName, subscriptionResourceId('Microsoft.Authorization/roleDefinitions', coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName)))))]" + } + ], + "builtInRoleNames": { + "Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'b24988ac-6180-42a0-ab88-20f7382dd24c')]", + "DNS Resolver Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '0f2ebee7-ffd4-4fc0-b3b7-664099fdad5d')]", + "DNS Zone Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'befefa01-2a29-4197-83a8-272ff33ce314')]", + "Domain Services Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'eeaeda52-9324-47f6-8069-5d5bade478b2')]", + "Domain Services Reader": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '361898ef-9ed1-48c2-849c-a832951106bb')]", + "Network Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '4d97b98b-1d4f-4787-a291-c67834d212e7')]", + "Owner": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '8e3af657-a8ff-443c-a75c-2fe8c4bcb635')]", + "Private DNS Zone Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'b12aa53e-6015-4669-85d0-8515ebb3ae7f')]", + "Reader": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'acdd72a7-3385-48ef-bd42-f606fba81ae7')]", + "Role Based Access Control Administrator": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'f58310d9-a9f6-439a-9e8d-f62e7b41a168')]" + } + }, + "resources": { + "avmTelemetry": { + "condition": "[parameters('enableTelemetry')]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2024-03-01", + "name": "[format('46d3xbcp.res.network-publicipaddress.{0}.{1}', replace('0.9.0', '.', '-'), substring(uniqueString(deployment().name, parameters('location')), 0, 4))]", + "properties": { + "mode": "Incremental", + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "resources": [], + "outputs": { + "telemetry": { + "type": "String", + "value": "For more information, see https://aka.ms/avm/TelemetryInfo" + } + } + } + } + }, + "publicIpAddress": { + "type": "Microsoft.Network/publicIPAddresses", + "apiVersion": "2024-05-01", + "name": "[parameters('name')]", + "location": "[parameters('location')]", + "tags": "[parameters('tags')]", + "sku": { + "name": "[parameters('skuName')]", + "tier": "[parameters('skuTier')]" + }, + "zones": "[map(parameters('availabilityZones'), lambda('zone', string(lambdaVariables('zone'))))]", + "properties": { + "ddosSettings": "[parameters('ddosSettings')]", + "dnsSettings": "[parameters('dnsSettings')]", + "publicIPAddressVersion": "[parameters('publicIPAddressVersion')]", + "publicIPAllocationMethod": "[parameters('publicIPAllocationMethod')]", + "publicIPPrefix": "[if(not(empty(parameters('publicIpPrefixResourceId'))), createObject('id', parameters('publicIpPrefixResourceId')), null())]", + "idleTimeoutInMinutes": "[parameters('idleTimeoutInMinutes')]", + "ipTags": "[parameters('ipTags')]" + } + }, + "publicIpAddress_lock": { + "condition": "[and(not(empty(coalesce(parameters('lock'), createObject()))), not(equals(tryGet(parameters('lock'), 'kind'), 'None')))]", + "type": "Microsoft.Authorization/locks", + "apiVersion": "2020-05-01", + "scope": "[format('Microsoft.Network/publicIPAddresses/{0}', parameters('name'))]", + "name": "[coalesce(tryGet(parameters('lock'), 'name'), format('lock-{0}', parameters('name')))]", + "properties": { + "level": "[coalesce(tryGet(parameters('lock'), 'kind'), '')]", + "notes": "[if(equals(tryGet(parameters('lock'), 'kind'), 'CanNotDelete'), 'Cannot delete resource or child resources.', 'Cannot delete or modify the resource or child resources.')]" + }, + "dependsOn": [ + "publicIpAddress" + ] + }, + "publicIpAddress_roleAssignments": { + "copy": { + "name": "publicIpAddress_roleAssignments", + "count": "[length(coalesce(variables('formattedRoleAssignments'), createArray()))]" + }, + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2022-04-01", + "scope": "[format('Microsoft.Network/publicIPAddresses/{0}', parameters('name'))]", + "name": "[coalesce(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'name'), guid(resourceId('Microsoft.Network/publicIPAddresses', parameters('name')), coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].principalId, coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].roleDefinitionId))]", + "properties": { + "roleDefinitionId": "[coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].roleDefinitionId]", + "principalId": "[coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].principalId]", + "description": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'description')]", + "principalType": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'principalType')]", + "condition": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'condition')]", + "conditionVersion": "[if(not(empty(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'condition'))), coalesce(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'conditionVersion'), '2.0'), null())]", + "delegatedManagedIdentityResourceId": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'delegatedManagedIdentityResourceId')]" + }, + "dependsOn": [ + "publicIpAddress" + ] + }, + "publicIpAddress_diagnosticSettings": { + "copy": { + "name": "publicIpAddress_diagnosticSettings", + "count": "[length(coalesce(parameters('diagnosticSettings'), createArray()))]" + }, + "type": "Microsoft.Insights/diagnosticSettings", + "apiVersion": "2021-05-01-preview", + "scope": "[format('Microsoft.Network/publicIPAddresses/{0}', parameters('name'))]", + "name": "[coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'name'), format('{0}-diagnosticSettings', parameters('name')))]", + "properties": { + "copy": [ + { + "name": "metrics", + "count": "[length(coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'metricCategories'), createArray(createObject('category', 'AllMetrics'))))]", + "input": { + "category": "[coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'metricCategories'), createArray(createObject('category', 'AllMetrics')))[copyIndex('metrics')].category]", + "enabled": "[coalesce(tryGet(coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'metricCategories'), createArray(createObject('category', 'AllMetrics')))[copyIndex('metrics')], 'enabled'), true())]", + "timeGrain": null + } + }, + { + "name": "logs", + "count": "[length(coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'logCategoriesAndGroups'), createArray(createObject('categoryGroup', 'allLogs'))))]", + "input": { + "categoryGroup": "[tryGet(coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'logCategoriesAndGroups'), createArray(createObject('categoryGroup', 'allLogs')))[copyIndex('logs')], 'categoryGroup')]", + "category": "[tryGet(coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'logCategoriesAndGroups'), createArray(createObject('categoryGroup', 'allLogs')))[copyIndex('logs')], 'category')]", + "enabled": "[coalesce(tryGet(coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'logCategoriesAndGroups'), createArray(createObject('categoryGroup', 'allLogs')))[copyIndex('logs')], 'enabled'), true())]" + } + } + ], + "storageAccountId": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'storageAccountResourceId')]", + "workspaceId": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'workspaceResourceId')]", + "eventHubAuthorizationRuleId": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'eventHubAuthorizationRuleResourceId')]", + "eventHubName": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'eventHubName')]", + "marketplacePartnerId": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'marketplacePartnerResourceId')]", + "logAnalyticsDestinationType": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'logAnalyticsDestinationType')]" + }, + "dependsOn": [ + "publicIpAddress" + ] + } + }, + "outputs": { + "resourceGroupName": { + "type": "string", + "metadata": { + "description": "The resource group the public IP address was deployed into." + }, + "value": "[resourceGroup().name]" + }, + "name": { + "type": "string", + "metadata": { + "description": "The name of the public IP address." + }, + "value": "[parameters('name')]" + }, + "resourceId": { + "type": "string", + "metadata": { + "description": "The resource ID of the public IP address." + }, + "value": "[resourceId('Microsoft.Network/publicIPAddresses', parameters('name'))]" + }, + "ipAddress": { + "type": "string", + "metadata": { + "description": "The public IP address of the public IP address resource." + }, + "value": "[coalesce(tryGet(reference('publicIpAddress'), 'ipAddress'), '')]" + }, + "location": { + "type": "string", + "metadata": { + "description": "The location the resource was deployed into." + }, + "value": "[reference('publicIpAddress', '2024-05-01', 'full').location]" + } + } + } + } + }, + "networkInterface": { + "type": "Microsoft.Resources/deployments", + "apiVersion": "2022-09-01", + "name": "[format('{0}-NetworkInterface', deployment().name)]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "name": { + "value": "[parameters('networkInterfaceName')]" + }, + "ipConfigurations": { + "copy": [ + { + "name": "value", + "count": "[length(parameters('ipConfigurations'))]", + "input": "[createObject('name', tryGet(parameters('ipConfigurations')[copyIndex('value')], 'name'), 'privateIPAllocationMethod', tryGet(parameters('ipConfigurations')[copyIndex('value')], 'privateIPAllocationMethod'), 'privateIPAddress', tryGet(parameters('ipConfigurations')[copyIndex('value')], 'privateIPAddress'), 'publicIPAddressResourceId', if(not(empty(tryGet(parameters('ipConfigurations')[copyIndex('value')], 'pipConfiguration'))), if(not(contains(coalesce(tryGet(parameters('ipConfigurations')[copyIndex('value')], 'pipConfiguration'), createObject()), 'publicIPAddressResourceId')), resourceId('Microsoft.Network/publicIPAddresses', coalesce(tryGet(tryGet(parameters('ipConfigurations')[copyIndex('value')], 'pipConfiguration'), 'name'), format('{0}{1}', parameters('virtualMachineName'), tryGet(tryGet(parameters('ipConfigurations')[copyIndex('value')], 'pipConfiguration'), 'publicIpNameSuffix')))), tryGet(parameters('ipConfigurations')[copyIndex('value')], 'pipConfiguration', 'publicIPAddressResourceId')), null()), 'subnetResourceId', parameters('ipConfigurations')[copyIndex('value')].subnetResourceId, 'loadBalancerBackendAddressPools', tryGet(parameters('ipConfigurations')[copyIndex('value')], 'loadBalancerBackendAddressPools'), 'applicationSecurityGroups', tryGet(parameters('ipConfigurations')[copyIndex('value')], 'applicationSecurityGroups'), 'applicationGatewayBackendAddressPools', tryGet(parameters('ipConfigurations')[copyIndex('value')], 'applicationGatewayBackendAddressPools'), 'gatewayLoadBalancer', tryGet(parameters('ipConfigurations')[copyIndex('value')], 'gatewayLoadBalancer'), 'loadBalancerInboundNatRules', tryGet(parameters('ipConfigurations')[copyIndex('value')], 'loadBalancerInboundNatRules'), 'privateIPAddressVersion', tryGet(parameters('ipConfigurations')[copyIndex('value')], 'privateIPAddressVersion'), 'virtualNetworkTaps', tryGet(parameters('ipConfigurations')[copyIndex('value')], 'virtualNetworkTaps'))]" + } + ] + }, + "location": { + "value": "[parameters('location')]" + }, + "tags": { + "value": "[parameters('tags')]" + }, + "diagnosticSettings": { + "value": "[parameters('diagnosticSettings')]" + }, + "dnsServers": { + "value": "[parameters('dnsServers')]" + }, + "enableAcceleratedNetworking": { + "value": "[parameters('enableAcceleratedNetworking')]" + }, + "enableTelemetry": { + "value": "[parameters('enableTelemetry')]" + }, + "enableIPForwarding": { + "value": "[parameters('enableIPForwarding')]" + }, + "lock": { + "value": "[parameters('lock')]" + }, + "networkSecurityGroupResourceId": "[if(not(empty(parameters('networkSecurityGroupResourceId'))), createObject('value', parameters('networkSecurityGroupResourceId')), createObject('value', ''))]", + "roleAssignments": { + "value": "[parameters('roleAssignments')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "languageVersion": "2.0", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.34.44.8038", + "templateHash": "10218370167882238860" + }, + "name": "Network Interface", + "description": "This module deploys a Network Interface." + }, + "definitions": { + "networkInterfaceIPConfigurationType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The name of the IP configuration." + } + }, + "privateIPAllocationMethod": { + "type": "string", + "allowedValues": [ + "Dynamic", + "Static" + ], + "nullable": true, + "metadata": { + "description": "Optional. The private IP address allocation method." + } + }, + "privateIPAddress": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The private IP address." + } + }, + "publicIPAddressResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The resource ID of the public IP address." + } + }, + "subnetResourceId": { + "type": "string", + "metadata": { + "description": "Required. The resource ID of the subnet." + } + }, + "loadBalancerBackendAddressPools": { + "type": "array", + "items": { + "$ref": "#/definitions/backendAddressPoolType" + }, + "nullable": true, + "metadata": { + "description": "Optional. Array of load balancer backend address pools." + } + }, + "loadBalancerInboundNatRules": { + "type": "array", + "items": { + "$ref": "#/definitions/inboundNatRuleType" + }, + "nullable": true, + "metadata": { + "description": "Optional. A list of references of LoadBalancerInboundNatRules." + } + }, + "applicationSecurityGroups": { + "type": "array", + "items": { + "$ref": "#/definitions/applicationSecurityGroupType" + }, + "nullable": true, + "metadata": { + "description": "Optional. Application security groups in which the IP configuration is included." + } + }, + "applicationGatewayBackendAddressPools": { + "type": "array", + "items": { + "$ref": "#/definitions/applicationGatewayBackendAddressPoolsType" + }, + "nullable": true, + "metadata": { + "description": "Optional. The reference to Application Gateway Backend Address Pools." + } + }, + "gatewayLoadBalancer": { + "$ref": "#/definitions/subResourceType", + "nullable": true, + "metadata": { + "description": "Optional. The reference to gateway load balancer frontend IP." + } + }, + "privateIPAddressVersion": { + "type": "string", + "allowedValues": [ + "IPv4", + "IPv6" + ], + "nullable": true, + "metadata": { + "description": "Optional. Whether the specific IP configuration is IPv4 or IPv6." + } + }, + "virtualNetworkTaps": { + "type": "array", + "items": { + "$ref": "#/definitions/virtualNetworkTapType" + }, + "nullable": true, + "metadata": { + "description": "Optional. The reference to Virtual Network Taps." + } + } + }, + "metadata": { + "__bicep_export!": true, + "description": "The resource ID of the deployed resource." + } + }, + "backendAddressPoolType": { + "type": "object", + "properties": { + "id": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The resource ID of the backend address pool." + } + }, + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The name of the backend address pool." + } + }, + "properties": { + "type": "object", + "nullable": true, + "metadata": { + "description": "Optional. The properties of the backend address pool." + } + } + }, + "metadata": { + "__bicep_export!": true, + "description": "The type for a backend address pool." + } + }, + "applicationSecurityGroupType": { + "type": "object", + "properties": { + "id": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Resource ID of the application security group." + } + }, + "location": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Location of the application security group." + } + }, + "properties": { + "type": "object", + "nullable": true, + "metadata": { + "description": "Optional. Properties of the application security group." + } + }, + "tags": { + "type": "object", + "nullable": true, + "metadata": { + "description": "Optional. Tags of the application security group." + } + } + }, + "metadata": { + "__bicep_export!": true, + "description": "The type for the application security group." + } + }, + "applicationGatewayBackendAddressPoolsType": { + "type": "object", + "properties": { + "id": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Resource ID of the backend address pool." + } + }, + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Name of the backend address pool that is unique within an Application Gateway." + } + }, + "properties": { + "type": "object", + "properties": { + "backendAddresses": { + "type": "array", + "items": { + "type": "object", + "properties": { + "ipAddress": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. IP address of the backend address." + } + }, + "fqdn": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. FQDN of the backend address." + } + } + } + }, + "nullable": true, + "metadata": { + "description": "Optional. Backend addresses." + } + } + }, + "nullable": true, + "metadata": { + "description": "Optional. Properties of the application gateway backend address pool." + } + } + }, + "metadata": { + "__bicep_export!": true, + "description": "The type for the application gateway backend address pool." + } + }, + "subResourceType": { + "type": "object", + "properties": { + "id": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Resource ID of the sub resource." + } + } + }, + "metadata": { + "__bicep_export!": true, + "description": "The type for the sub resource." + } + }, + "inboundNatRuleType": { + "type": "object", + "properties": { + "id": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Resource ID of the inbound NAT rule." + } + }, + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Name of the resource that is unique within the set of inbound NAT rules used by the load balancer. This name can be used to access the resource." + } + }, + "properties": { + "type": "object", + "properties": { + "backendAddressPool": { + "$ref": "#/definitions/subResourceType", + "nullable": true, + "metadata": { + "description": "Optional. A reference to backendAddressPool resource." + } + }, + "backendPort": { + "type": "int", + "nullable": true, + "metadata": { + "description": "Optional. The port used for the internal endpoint. Acceptable values range from 1 to 65535." + } + }, + "enableFloatingIP": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. Configures a virtual machine's endpoint for the floating IP capability required to configure a SQL AlwaysOn Availability Group. This setting is required when using the SQL AlwaysOn Availability Groups in SQL server. This setting can't be changed after you create the endpoint." + } + }, + "enableTcpReset": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. Receive bidirectional TCP Reset on TCP flow idle timeout or unexpected connection termination. This element is only used when the protocol is set to TCP." + } + }, + "frontendIPConfiguration": { + "$ref": "#/definitions/subResourceType", + "nullable": true, + "metadata": { + "description": "Optional. A reference to frontend IP addresses." + } + }, + "frontendPort": { + "type": "int", + "nullable": true, + "metadata": { + "description": "Optional. The port for the external endpoint. Port numbers for each rule must be unique within the Load Balancer. Acceptable values range from 1 to 65534." + } + }, + "frontendPortRangeStart": { + "type": "int", + "nullable": true, + "metadata": { + "description": "Optional. The port range start for the external endpoint. This property is used together with BackendAddressPool and FrontendPortRangeEnd. Individual inbound NAT rule port mappings will be created for each backend address from BackendAddressPool. Acceptable values range from 1 to 65534." + } + }, + "frontendPortRangeEnd": { + "type": "int", + "nullable": true, + "metadata": { + "description": "Optional. The port range end for the external endpoint. This property is used together with BackendAddressPool and FrontendPortRangeStart. Individual inbound NAT rule port mappings will be created for each backend address from BackendAddressPool. Acceptable values range from 1 to 65534." + } + }, + "protocol": { + "type": "string", + "allowedValues": [ + "All", + "Tcp", + "Udp" + ], + "nullable": true, + "metadata": { + "description": "Optional. The reference to the transport protocol used by the load balancing rule." + } + } + }, + "nullable": true, + "metadata": { + "description": "Optional. Properties of the inbound NAT rule." + } + } + }, + "metadata": { + "__bicep_export!": true, + "description": "The type for the inbound NAT rule." + } + }, + "virtualNetworkTapType": { + "type": "object", + "properties": { + "id": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Resource ID of the virtual network tap." + } + }, + "location": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Location of the virtual network tap." + } + }, + "properties": { + "type": "object", + "nullable": true, + "metadata": { + "description": "Optional. Properties of the virtual network tap." + } + }, + "tags": { + "type": "object", + "nullable": true, + "metadata": { + "description": "Optional. Tags of the virtual network tap." + } + } + }, + "metadata": { + "__bicep_export!": true, + "description": "The type for the virtual network tap." + } + }, + "networkInterfaceIPConfigurationOutputType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "metadata": { + "description": "The name of the IP configuration." + } + }, + "privateIP": { + "type": "string", + "nullable": true, + "metadata": { + "description": "The private IP address." + } + }, + "publicIP": { + "type": "string", + "nullable": true, + "metadata": { + "description": "The public IP address." + } + } + }, + "metadata": { + "__bicep_export!": true, + "description": "The type for the network interface IP configuration output." + } + }, + "diagnosticSettingFullType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The name of the diagnostic setting." + } + }, + "logCategoriesAndGroups": { + "type": "array", + "items": { + "type": "object", + "properties": { + "category": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Name of a Diagnostic Log category for a resource type this setting is applied to. Set the specific logs to collect here." + } + }, + "categoryGroup": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Name of a Diagnostic Log category group for a resource type this setting is applied to. Set to `allLogs` to collect all logs." + } + }, + "enabled": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. Enable or disable the category explicitly. Default is `true`." + } + } + } + }, + "nullable": true, + "metadata": { + "description": "Optional. The name of logs that will be streamed. \"allLogs\" includes all possible logs for the resource. Set to `[]` to disable log collection." + } + }, + "metricCategories": { + "type": "array", + "items": { + "type": "object", + "properties": { + "category": { + "type": "string", + "metadata": { + "description": "Required. Name of a Diagnostic Metric category for a resource type this setting is applied to. Set to `AllMetrics` to collect all metrics." + } + }, + "enabled": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. Enable or disable the category explicitly. Default is `true`." + } + } + } + }, + "nullable": true, + "metadata": { + "description": "Optional. The name of metrics that will be streamed. \"allMetrics\" includes all possible metrics for the resource. Set to `[]` to disable metric collection." + } + }, + "logAnalyticsDestinationType": { + "type": "string", + "allowedValues": [ + "AzureDiagnostics", + "Dedicated" + ], + "nullable": true, + "metadata": { + "description": "Optional. A string indicating whether the export to Log Analytics should use the default destination type, i.e. AzureDiagnostics, or use a destination type." + } + }, + "workspaceResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Resource ID of the diagnostic log analytics workspace. For security reasons, it is recommended to set diagnostic settings to send data to either storage account, log analytics workspace or event hub." + } + }, + "storageAccountResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Resource ID of the diagnostic storage account. For security reasons, it is recommended to set diagnostic settings to send data to either storage account, log analytics workspace or event hub." + } + }, + "eventHubAuthorizationRuleResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Resource ID of the diagnostic event hub authorization rule for the Event Hubs namespace in which the event hub should be created or streamed to." + } + }, + "eventHubName": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Name of the diagnostic event hub within the namespace to which logs are streamed. Without this, an event hub is created for each log category. For security reasons, it is recommended to set diagnostic settings to send data to either storage account, log analytics workspace or event hub." + } + }, + "marketplacePartnerResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The full ARM resource ID of the Marketplace resource to which you would like to send Diagnostic Logs." + } + } + }, + "metadata": { + "description": "An AVM-aligned type for a diagnostic setting. To be used if both logs & metrics are supported by the resource provider.", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.5.1" + } + } + }, + "lockType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Specify the name of lock." + } + }, + "kind": { + "type": "string", + "allowedValues": [ + "CanNotDelete", + "None", + "ReadOnly" + ], + "nullable": true, + "metadata": { + "description": "Optional. Specify the type of lock." + } + } + }, + "metadata": { + "description": "An AVM-aligned type for a lock.", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.5.1" + } + } + }, + "roleAssignmentType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The name (as GUID) of the role assignment. If not provided, a GUID will be generated." + } + }, + "roleDefinitionIdOrName": { + "type": "string", + "metadata": { + "description": "Required. The role to assign. You can provide either the display name of the role definition, the role definition GUID, or its fully qualified ID in the following format: '/providers/Microsoft.Authorization/roleDefinitions/c2f4ef07-c644-48eb-af81-4b1b4947fb11'." + } + }, + "principalId": { + "type": "string", + "metadata": { + "description": "Required. The principal ID of the principal (user/group/identity) to assign the role to." + } + }, + "principalType": { + "type": "string", + "allowedValues": [ + "Device", + "ForeignGroup", + "Group", + "ServicePrincipal", + "User" + ], + "nullable": true, + "metadata": { + "description": "Optional. The principal type of the assigned principal ID." + } + }, + "description": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The description of the role assignment." + } + }, + "condition": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The conditions on the role assignment. This limits the resources it can be assigned to. e.g.: @Resource[Microsoft.Storage/storageAccounts/blobServices/containers:ContainerName] StringEqualsIgnoreCase \"foo_storage_container\"." + } + }, + "conditionVersion": { + "type": "string", + "allowedValues": [ + "2.0" + ], + "nullable": true, + "metadata": { + "description": "Optional. Version of the condition." + } + }, + "delegatedManagedIdentityResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The Resource Id of the delegated managed identity resource." + } + } + }, + "metadata": { + "description": "An AVM-aligned type for a role assignment.", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.5.1" + } + } + } + }, + "parameters": { + "name": { + "type": "string", + "metadata": { + "description": "Required. The name of the network interface." + } + }, + "location": { + "type": "string", + "defaultValue": "[resourceGroup().location]", + "metadata": { + "description": "Optional. Location for all resources." + } + }, + "tags": { + "type": "object", + "nullable": true, + "metadata": { + "description": "Optional. Resource tags." + } + }, + "enableTelemetry": { + "type": "bool", + "defaultValue": true, + "metadata": { + "description": "Optional. Enable/Disable usage telemetry for module." + } + }, + "enableIPForwarding": { + "type": "bool", + "defaultValue": false, + "metadata": { + "description": "Optional. Indicates whether IP forwarding is enabled on this network interface." + } + }, + "enableAcceleratedNetworking": { + "type": "bool", + "defaultValue": false, + "metadata": { + "description": "Optional. If the network interface is accelerated networking enabled." + } + }, + "dnsServers": { + "type": "array", + "items": { + "type": "string" + }, + "defaultValue": [], + "metadata": { + "description": "Optional. List of DNS servers IP addresses. Use 'AzureProvidedDNS' to switch to azure provided DNS resolution. 'AzureProvidedDNS' value cannot be combined with other IPs, it must be the only value in dnsServers collection." + } + }, + "networkSecurityGroupResourceId": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "Optional. The network security group (NSG) to attach to the network interface." + } + }, + "auxiliaryMode": { + "type": "string", + "defaultValue": "None", + "allowedValues": [ + "Floating", + "MaxConnections", + "None" + ], + "metadata": { + "description": "Optional. Auxiliary mode of Network Interface resource. Not all regions are enabled for Auxiliary Mode Nic." + } + }, + "auxiliarySku": { + "type": "string", + "defaultValue": "None", + "allowedValues": [ + "A1", + "A2", + "A4", + "A8", + "None" + ], + "metadata": { + "description": "Optional. Auxiliary sku of Network Interface resource. Not all regions are enabled for Auxiliary Mode Nic." + } + }, + "disableTcpStateTracking": { + "type": "bool", + "defaultValue": false, + "metadata": { + "description": "Optional. Indicates whether to disable tcp state tracking. Subscription must be registered for the Microsoft.Network/AllowDisableTcpStateTracking feature before this property can be set to true." + } + }, + "ipConfigurations": { + "type": "array", + "items": { + "$ref": "#/definitions/networkInterfaceIPConfigurationType" + }, + "metadata": { + "description": "Required. A list of IPConfigurations of the network interface." + } + }, + "lock": { + "$ref": "#/definitions/lockType", + "nullable": true, + "metadata": { + "description": "Optional. The lock settings of the service." + } + }, + "roleAssignments": { + "type": "array", + "items": { + "$ref": "#/definitions/roleAssignmentType" + }, + "nullable": true, + "metadata": { + "description": "Optional. Array of role assignments to create." + } + }, + "diagnosticSettings": { + "type": "array", + "items": { + "$ref": "#/definitions/diagnosticSettingFullType" + }, + "nullable": true, + "metadata": { + "description": "Optional. The diagnostic settings of the service." + } + } + }, + "variables": { + "copy": [ + { + "name": "formattedRoleAssignments", + "count": "[length(coalesce(parameters('roleAssignments'), createArray()))]", + "input": "[union(coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')], createObject('roleDefinitionId', coalesce(tryGet(variables('builtInRoleNames'), coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName), if(contains(coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName, '/providers/Microsoft.Authorization/roleDefinitions/'), coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName, subscriptionResourceId('Microsoft.Authorization/roleDefinitions', coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName)))))]" + } + ], + "builtInRoleNames": { + "Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'b24988ac-6180-42a0-ab88-20f7382dd24c')]", + "DNS Resolver Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '0f2ebee7-ffd4-4fc0-b3b7-664099fdad5d')]", + "DNS Zone Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'befefa01-2a29-4197-83a8-272ff33ce314')]", + "Network Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '4d97b98b-1d4f-4787-a291-c67834d212e7')]", + "Owner": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '8e3af657-a8ff-443c-a75c-2fe8c4bcb635')]", + "Private DNS Zone Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'b12aa53e-6015-4669-85d0-8515ebb3ae7f')]", + "Reader": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'acdd72a7-3385-48ef-bd42-f606fba81ae7')]", + "Role Based Access Control Administrator": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'f58310d9-a9f6-439a-9e8d-f62e7b41a168')]" + } + }, + "resources": { + "publicIp": { + "copy": { + "name": "publicIp", + "count": "[length(parameters('ipConfigurations'))]" + }, + "condition": "[and(contains(parameters('ipConfigurations')[copyIndex()], 'publicIPAddressResourceId'), not(equals(tryGet(parameters('ipConfigurations')[copyIndex()], 'publicIPAddressResourceId'), null())))]", + "existing": true, + "type": "Microsoft.Network/publicIPAddresses", + "apiVersion": "2024-05-01", + "resourceGroup": "[split(coalesce(tryGet(parameters('ipConfigurations')[copyIndex()], 'publicIPAddressResourceId'), ''), '/')[4]]", + "name": "[last(split(coalesce(tryGet(parameters('ipConfigurations')[copyIndex()], 'publicIPAddressResourceId'), ''), '/'))]" + }, + "avmTelemetry": { + "condition": "[parameters('enableTelemetry')]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2024-03-01", + "name": "[format('46d3xbcp.res.network-networkinterface.{0}.{1}', replace('0.5.2', '.', '-'), substring(uniqueString(deployment().name, parameters('location')), 0, 4))]", + "properties": { + "mode": "Incremental", + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "resources": [], + "outputs": { + "telemetry": { + "type": "String", + "value": "For more information, see https://aka.ms/avm/TelemetryInfo" + } + } + } + } + }, + "networkInterface": { + "type": "Microsoft.Network/networkInterfaces", + "apiVersion": "2024-05-01", + "name": "[parameters('name')]", + "location": "[parameters('location')]", + "tags": "[parameters('tags')]", + "properties": { + "copy": [ + { + "name": "ipConfigurations", + "count": "[length(parameters('ipConfigurations'))]", + "input": { + "name": "[coalesce(tryGet(parameters('ipConfigurations')[copyIndex('ipConfigurations')], 'name'), format('ipconfig{0}', padLeft(add(copyIndex('ipConfigurations'), 1), 2, '0')))]", + "properties": { + "primary": "[if(equals(copyIndex('ipConfigurations'), 0), true(), false())]", + "privateIPAllocationMethod": "[tryGet(parameters('ipConfigurations')[copyIndex('ipConfigurations')], 'privateIPAllocationMethod')]", + "privateIPAddress": "[tryGet(parameters('ipConfigurations')[copyIndex('ipConfigurations')], 'privateIPAddress')]", + "publicIPAddress": "[if(contains(parameters('ipConfigurations')[copyIndex('ipConfigurations')], 'publicIPAddressResourceId'), if(not(equals(tryGet(parameters('ipConfigurations')[copyIndex('ipConfigurations')], 'publicIPAddressResourceId'), null())), createObject('id', tryGet(parameters('ipConfigurations')[copyIndex('ipConfigurations')], 'publicIPAddressResourceId')), null()), null())]", + "subnet": { + "id": "[parameters('ipConfigurations')[copyIndex('ipConfigurations')].subnetResourceId]" + }, + "loadBalancerBackendAddressPools": "[tryGet(parameters('ipConfigurations')[copyIndex('ipConfigurations')], 'loadBalancerBackendAddressPools')]", + "applicationSecurityGroups": "[tryGet(parameters('ipConfigurations')[copyIndex('ipConfigurations')], 'applicationSecurityGroups')]", + "applicationGatewayBackendAddressPools": "[tryGet(parameters('ipConfigurations')[copyIndex('ipConfigurations')], 'applicationGatewayBackendAddressPools')]", + "gatewayLoadBalancer": "[tryGet(parameters('ipConfigurations')[copyIndex('ipConfigurations')], 'gatewayLoadBalancer')]", + "loadBalancerInboundNatRules": "[tryGet(parameters('ipConfigurations')[copyIndex('ipConfigurations')], 'loadBalancerInboundNatRules')]", + "privateIPAddressVersion": "[tryGet(parameters('ipConfigurations')[copyIndex('ipConfigurations')], 'privateIPAddressVersion')]", + "virtualNetworkTaps": "[tryGet(parameters('ipConfigurations')[copyIndex('ipConfigurations')], 'virtualNetworkTaps')]" + } + } + } + ], + "auxiliaryMode": "[parameters('auxiliaryMode')]", + "auxiliarySku": "[parameters('auxiliarySku')]", + "disableTcpStateTracking": "[parameters('disableTcpStateTracking')]", + "dnsSettings": "[if(not(empty(parameters('dnsServers'))), createObject('dnsServers', parameters('dnsServers')), null())]", + "enableAcceleratedNetworking": "[parameters('enableAcceleratedNetworking')]", + "enableIPForwarding": "[parameters('enableIPForwarding')]", + "networkSecurityGroup": "[if(not(empty(parameters('networkSecurityGroupResourceId'))), createObject('id', parameters('networkSecurityGroupResourceId')), null())]" + } + }, + "networkInterface_lock": { + "condition": "[and(not(empty(coalesce(parameters('lock'), createObject()))), not(equals(tryGet(parameters('lock'), 'kind'), 'None')))]", + "type": "Microsoft.Authorization/locks", + "apiVersion": "2020-05-01", + "scope": "[format('Microsoft.Network/networkInterfaces/{0}', parameters('name'))]", + "name": "[coalesce(tryGet(parameters('lock'), 'name'), format('lock-{0}', parameters('name')))]", + "properties": { + "level": "[coalesce(tryGet(parameters('lock'), 'kind'), '')]", + "notes": "[if(equals(tryGet(parameters('lock'), 'kind'), 'CanNotDelete'), 'Cannot delete resource or child resources.', 'Cannot delete or modify the resource or child resources.')]" + }, + "dependsOn": [ + "networkInterface" + ] + }, + "networkInterface_diagnosticSettings": { + "copy": { + "name": "networkInterface_diagnosticSettings", + "count": "[length(coalesce(parameters('diagnosticSettings'), createArray()))]" + }, + "type": "Microsoft.Insights/diagnosticSettings", + "apiVersion": "2021-05-01-preview", + "scope": "[format('Microsoft.Network/networkInterfaces/{0}', parameters('name'))]", + "name": "[coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'name'), format('{0}-diagnosticSettings', parameters('name')))]", + "properties": { + "copy": [ + { + "name": "metrics", + "count": "[length(coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'metricCategories'), createArray(createObject('category', 'AllMetrics'))))]", + "input": { + "category": "[coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'metricCategories'), createArray(createObject('category', 'AllMetrics')))[copyIndex('metrics')].category]", + "enabled": "[coalesce(tryGet(coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'metricCategories'), createArray(createObject('category', 'AllMetrics')))[copyIndex('metrics')], 'enabled'), true())]", + "timeGrain": null + } + } + ], + "storageAccountId": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'storageAccountResourceId')]", + "workspaceId": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'workspaceResourceId')]", + "eventHubAuthorizationRuleId": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'eventHubAuthorizationRuleResourceId')]", + "eventHubName": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'eventHubName')]", + "marketplacePartnerId": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'marketplacePartnerResourceId')]", + "logAnalyticsDestinationType": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'logAnalyticsDestinationType')]" + }, + "dependsOn": [ + "networkInterface" + ] + }, + "networkInterface_roleAssignments": { + "copy": { + "name": "networkInterface_roleAssignments", + "count": "[length(coalesce(variables('formattedRoleAssignments'), createArray()))]" + }, + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2022-04-01", + "scope": "[format('Microsoft.Network/networkInterfaces/{0}', parameters('name'))]", + "name": "[coalesce(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'name'), guid(resourceId('Microsoft.Network/networkInterfaces', parameters('name')), coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].principalId, coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].roleDefinitionId))]", + "properties": { + "roleDefinitionId": "[coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].roleDefinitionId]", + "principalId": "[coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].principalId]", + "description": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'description')]", + "principalType": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'principalType')]", + "condition": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'condition')]", + "conditionVersion": "[if(not(empty(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'condition'))), coalesce(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'conditionVersion'), '2.0'), null())]", + "delegatedManagedIdentityResourceId": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'delegatedManagedIdentityResourceId')]" + }, + "dependsOn": [ + "networkInterface" + ] + } + }, + "outputs": { + "name": { + "type": "string", + "metadata": { + "description": "The name of the deployed resource." + }, + "value": "[parameters('name')]" + }, + "resourceId": { + "type": "string", + "metadata": { + "description": "The resource ID of the deployed resource." + }, + "value": "[resourceId('Microsoft.Network/networkInterfaces', parameters('name'))]" + }, + "resourceGroupName": { + "type": "string", + "metadata": { + "description": "The resource group of the deployed resource." + }, + "value": "[resourceGroup().name]" + }, + "location": { + "type": "string", + "metadata": { + "description": "The location the resource was deployed into." + }, + "value": "[reference('networkInterface', '2024-05-01', 'full').location]" + }, + "ipConfigurations": { + "type": "array", + "items": { + "$ref": "#/definitions/networkInterfaceIPConfigurationOutputType" + }, + "metadata": { + "description": "The list of IP configurations of the network interface." + }, + "copy": { + "count": "[length(parameters('ipConfigurations'))]", + "input": { + "name": "[reference('networkInterface').ipConfigurations[copyIndex()].name]", + "privateIP": "[coalesce(tryGet(reference('networkInterface').ipConfigurations[copyIndex()].properties, 'privateIPAddress'), '')]", + "publicIP": "[if(and(contains(parameters('ipConfigurations')[copyIndex()], 'publicIPAddressResourceId'), not(equals(tryGet(parameters('ipConfigurations')[copyIndex()], 'publicIPAddressResourceId'), null()))), coalesce(reference(format('publicIp[{0}]', copyIndex())).ipAddress, ''), '')]" + } + } + } + } + } + }, + "dependsOn": [ + "networkInterface_publicIPAddresses" + ] + } + }, + "outputs": { + "name": { + "type": "string", + "metadata": { + "description": "The name of the network interface." + }, + "value": "[reference('networkInterface').outputs.name.value]" + }, + "ipConfigurations": { + "type": "array", + "items": { + "$ref": "#/definitions/networkInterfaceIPConfigurationOutputType" + }, + "metadata": { + "description": "The list of IP configurations of the network interface." + }, + "value": "[reference('networkInterface').outputs.ipConfigurations.value]" + } + } + } + } + }, + "vm_domainJoinExtension": { + "condition": "[and(contains(parameters('extensionDomainJoinConfig'), 'enabled'), parameters('extensionDomainJoinConfig').enabled)]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2022-09-01", + "name": "[format('{0}-VM-DomainJoin', uniqueString(deployment().name, parameters('location')))]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "virtualMachineName": { + "value": "[parameters('name')]" + }, + "name": { + "value": "[coalesce(tryGet(parameters('extensionDomainJoinConfig'), 'name'), 'DomainJoin')]" + }, + "location": { + "value": "[parameters('location')]" + }, + "publisher": { + "value": "Microsoft.Compute" + }, + "type": { + "value": "JsonADDomainExtension" + }, + "typeHandlerVersion": { + "value": "[coalesce(tryGet(parameters('extensionDomainJoinConfig'), 'typeHandlerVersion'), '1.3')]" + }, + "autoUpgradeMinorVersion": { + "value": "[coalesce(tryGet(parameters('extensionDomainJoinConfig'), 'autoUpgradeMinorVersion'), true())]" + }, + "enableAutomaticUpgrade": { + "value": "[coalesce(tryGet(parameters('extensionDomainJoinConfig'), 'enableAutomaticUpgrade'), false())]" + }, + "settings": { + "value": "[parameters('extensionDomainJoinConfig').settings]" + }, + "supressFailures": { + "value": "[coalesce(tryGet(parameters('extensionDomainJoinConfig'), 'supressFailures'), false())]" + }, + "tags": { + "value": "[coalesce(tryGet(parameters('extensionDomainJoinConfig'), 'tags'), parameters('tags'))]" + }, + "protectedSettings": { + "value": { + "Password": "[parameters('extensionDomainJoinPassword')]" + } + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "languageVersion": "2.0", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.37.4.10188", + "templateHash": "13125609748815648088" + }, + "name": "Virtual Machine Extensions", + "description": "This module deploys a Virtual Machine Extension." + }, + "parameters": { + "virtualMachineName": { + "type": "string", + "metadata": { + "description": "Conditional. The name of the parent virtual machine that extension is provisioned for. Required if the template is used in a standalone deployment." + } + }, + "name": { + "type": "string", + "metadata": { + "description": "Required. The name of the virtual machine extension." + } + }, + "location": { + "type": "string", + "defaultValue": "[resourceGroup().location]", + "metadata": { + "description": "Optional. The location the extension is deployed to." + } + }, + "publisher": { + "type": "string", + "metadata": { + "description": "Required. The name of the extension handler publisher." + } + }, + "type": { + "type": "string", + "metadata": { + "description": "Required. Specifies the type of the extension; an example is \"CustomScriptExtension\"." + } + }, + "typeHandlerVersion": { + "type": "string", + "metadata": { + "description": "Required. Specifies the version of the script handler." + } + }, + "autoUpgradeMinorVersion": { + "type": "bool", + "metadata": { + "description": "Required. Indicates whether the extension should use a newer minor version if one is available at deployment time. Once deployed, however, the extension will not upgrade minor versions unless redeployed, even with this property set to true." + } + }, + "forceUpdateTag": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. How the extension handler should be forced to update even if the extension configuration has not changed." + } + }, + "settings": { + "type": "object", + "nullable": true, + "metadata": { + "description": "Optional. Any object that contains the extension specific settings." + } + }, + "protectedSettings": { + "type": "secureObject", + "nullable": true, + "metadata": { + "description": "Optional. Any object that contains the extension specific protected settings." + } + }, + "supressFailures": { + "type": "bool", + "defaultValue": false, + "metadata": { + "description": "Optional. Indicates whether failures stemming from the extension will be suppressed (Operational failures such as not connecting to the VM will not be suppressed regardless of this value). The default is false." + } + }, + "enableAutomaticUpgrade": { + "type": "bool", + "metadata": { + "description": "Required. Indicates whether the extension should be automatically upgraded by the platform if there is a newer version of the extension available." + } + }, + "tags": { + "type": "object", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.Compute/virtualMachines/extensions@2024-11-01#properties/tags" + }, + "description": "Optional. Tags of the resource." + }, + "nullable": true + }, + "protectedSettingsFromKeyVault": { + "type": "object", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.Compute/virtualMachines/extensions@2024-11-01#properties/properties/properties/protectedSettingsFromKeyVault" + }, + "description": "Optional. The extensions protected settings that are passed by reference, and consumed from key vault." + }, + "nullable": true + }, + "provisionAfterExtensions": { + "type": "array", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.Compute/virtualMachines/extensions@2024-11-01#properties/properties/properties/provisionAfterExtensions" + }, + "description": "Optional. Collection of extension names after which this extension needs to be provisioned." + }, + "nullable": true + } + }, + "resources": { + "virtualMachine": { + "existing": true, + "type": "Microsoft.Compute/virtualMachines", + "apiVersion": "2024-11-01", + "name": "[parameters('virtualMachineName')]" + }, + "extension": { + "type": "Microsoft.Compute/virtualMachines/extensions", + "apiVersion": "2024-11-01", + "name": "[format('{0}/{1}', parameters('virtualMachineName'), parameters('name'))]", + "location": "[parameters('location')]", + "tags": "[parameters('tags')]", + "properties": { + "publisher": "[parameters('publisher')]", + "type": "[parameters('type')]", + "typeHandlerVersion": "[parameters('typeHandlerVersion')]", + "autoUpgradeMinorVersion": "[parameters('autoUpgradeMinorVersion')]", + "enableAutomaticUpgrade": "[parameters('enableAutomaticUpgrade')]", + "forceUpdateTag": "[parameters('forceUpdateTag')]", + "settings": "[parameters('settings')]", + "protectedSettings": "[parameters('protectedSettings')]", + "suppressFailures": "[parameters('supressFailures')]", + "protectedSettingsFromKeyVault": "[parameters('protectedSettingsFromKeyVault')]", + "provisionAfterExtensions": "[parameters('provisionAfterExtensions')]" + } + } + }, + "outputs": { + "name": { + "type": "string", + "metadata": { + "description": "The name of the extension." + }, + "value": "[parameters('name')]" + }, + "resourceId": { + "type": "string", + "metadata": { + "description": "The resource ID of the extension." + }, + "value": "[resourceId('Microsoft.Compute/virtualMachines/extensions', parameters('virtualMachineName'), parameters('name'))]" + }, + "resourceGroupName": { + "type": "string", + "metadata": { + "description": "The name of the Resource Group the extension was created in." + }, + "value": "[resourceGroup().name]" + }, + "location": { + "type": "string", + "metadata": { + "description": "The location the resource was deployed into." + }, + "value": "[reference('extension', '2024-11-01', 'full').location]" + } + } + } + }, + "dependsOn": [ + "vm" + ] + }, + "vm_aadJoinExtension": { + "condition": "[parameters('extensionAadJoinConfig').enabled]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2022-09-01", + "name": "[format('{0}-VM-AADLogin', uniqueString(deployment().name, parameters('location')))]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "virtualMachineName": { + "value": "[parameters('name')]" + }, + "name": { + "value": "[coalesce(tryGet(parameters('extensionAadJoinConfig'), 'name'), 'AADLogin')]" + }, + "location": { + "value": "[parameters('location')]" + }, + "publisher": { + "value": "Microsoft.Azure.ActiveDirectory" + }, + "type": "[if(equals(parameters('osType'), 'Windows'), createObject('value', 'AADLoginForWindows'), createObject('value', 'AADSSHLoginforLinux'))]", + "typeHandlerVersion": { + "value": "[coalesce(tryGet(parameters('extensionAadJoinConfig'), 'typeHandlerVersion'), if(equals(parameters('osType'), 'Windows'), '2.0', '1.0'))]" + }, + "autoUpgradeMinorVersion": { + "value": "[coalesce(tryGet(parameters('extensionAadJoinConfig'), 'autoUpgradeMinorVersion'), true())]" + }, + "enableAutomaticUpgrade": { + "value": "[coalesce(tryGet(parameters('extensionAadJoinConfig'), 'enableAutomaticUpgrade'), false())]" + }, + "settings": { + "value": "[coalesce(tryGet(parameters('extensionAadJoinConfig'), 'settings'), createObject())]" + }, + "supressFailures": { + "value": "[coalesce(tryGet(parameters('extensionAadJoinConfig'), 'supressFailures'), false())]" + }, + "tags": { + "value": "[coalesce(tryGet(parameters('extensionAadJoinConfig'), 'tags'), parameters('tags'))]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "languageVersion": "2.0", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.37.4.10188", + "templateHash": "13125609748815648088" + }, + "name": "Virtual Machine Extensions", + "description": "This module deploys a Virtual Machine Extension." + }, + "parameters": { + "virtualMachineName": { + "type": "string", + "metadata": { + "description": "Conditional. The name of the parent virtual machine that extension is provisioned for. Required if the template is used in a standalone deployment." + } + }, + "name": { + "type": "string", + "metadata": { + "description": "Required. The name of the virtual machine extension." + } + }, + "location": { + "type": "string", + "defaultValue": "[resourceGroup().location]", + "metadata": { + "description": "Optional. The location the extension is deployed to." + } + }, + "publisher": { + "type": "string", + "metadata": { + "description": "Required. The name of the extension handler publisher." + } + }, + "type": { + "type": "string", + "metadata": { + "description": "Required. Specifies the type of the extension; an example is \"CustomScriptExtension\"." + } + }, + "typeHandlerVersion": { + "type": "string", + "metadata": { + "description": "Required. Specifies the version of the script handler." + } + }, + "autoUpgradeMinorVersion": { + "type": "bool", + "metadata": { + "description": "Required. Indicates whether the extension should use a newer minor version if one is available at deployment time. Once deployed, however, the extension will not upgrade minor versions unless redeployed, even with this property set to true." + } + }, + "forceUpdateTag": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. How the extension handler should be forced to update even if the extension configuration has not changed." + } + }, + "settings": { + "type": "object", + "nullable": true, + "metadata": { + "description": "Optional. Any object that contains the extension specific settings." + } + }, + "protectedSettings": { + "type": "secureObject", + "nullable": true, + "metadata": { + "description": "Optional. Any object that contains the extension specific protected settings." + } + }, + "supressFailures": { + "type": "bool", + "defaultValue": false, + "metadata": { + "description": "Optional. Indicates whether failures stemming from the extension will be suppressed (Operational failures such as not connecting to the VM will not be suppressed regardless of this value). The default is false." + } + }, + "enableAutomaticUpgrade": { + "type": "bool", + "metadata": { + "description": "Required. Indicates whether the extension should be automatically upgraded by the platform if there is a newer version of the extension available." + } + }, + "tags": { + "type": "object", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.Compute/virtualMachines/extensions@2024-11-01#properties/tags" + }, + "description": "Optional. Tags of the resource." + }, + "nullable": true + }, + "protectedSettingsFromKeyVault": { + "type": "object", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.Compute/virtualMachines/extensions@2024-11-01#properties/properties/properties/protectedSettingsFromKeyVault" + }, + "description": "Optional. The extensions protected settings that are passed by reference, and consumed from key vault." + }, + "nullable": true + }, + "provisionAfterExtensions": { + "type": "array", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.Compute/virtualMachines/extensions@2024-11-01#properties/properties/properties/provisionAfterExtensions" + }, + "description": "Optional. Collection of extension names after which this extension needs to be provisioned." + }, + "nullable": true + } + }, + "resources": { + "virtualMachine": { + "existing": true, + "type": "Microsoft.Compute/virtualMachines", + "apiVersion": "2024-11-01", + "name": "[parameters('virtualMachineName')]" + }, + "extension": { + "type": "Microsoft.Compute/virtualMachines/extensions", + "apiVersion": "2024-11-01", + "name": "[format('{0}/{1}', parameters('virtualMachineName'), parameters('name'))]", + "location": "[parameters('location')]", + "tags": "[parameters('tags')]", + "properties": { + "publisher": "[parameters('publisher')]", + "type": "[parameters('type')]", + "typeHandlerVersion": "[parameters('typeHandlerVersion')]", + "autoUpgradeMinorVersion": "[parameters('autoUpgradeMinorVersion')]", + "enableAutomaticUpgrade": "[parameters('enableAutomaticUpgrade')]", + "forceUpdateTag": "[parameters('forceUpdateTag')]", + "settings": "[parameters('settings')]", + "protectedSettings": "[parameters('protectedSettings')]", + "suppressFailures": "[parameters('supressFailures')]", + "protectedSettingsFromKeyVault": "[parameters('protectedSettingsFromKeyVault')]", + "provisionAfterExtensions": "[parameters('provisionAfterExtensions')]" + } + } + }, + "outputs": { + "name": { + "type": "string", + "metadata": { + "description": "The name of the extension." + }, + "value": "[parameters('name')]" + }, + "resourceId": { + "type": "string", + "metadata": { + "description": "The resource ID of the extension." + }, + "value": "[resourceId('Microsoft.Compute/virtualMachines/extensions', parameters('virtualMachineName'), parameters('name'))]" + }, + "resourceGroupName": { + "type": "string", + "metadata": { + "description": "The name of the Resource Group the extension was created in." + }, + "value": "[resourceGroup().name]" + }, + "location": { + "type": "string", + "metadata": { + "description": "The location the resource was deployed into." + }, + "value": "[reference('extension', '2024-11-01', 'full').location]" + } + } + } + }, + "dependsOn": [ + "vm", + "vm_domainJoinExtension" + ] + }, + "vm_microsoftAntiMalwareExtension": { + "condition": "[parameters('extensionAntiMalwareConfig').enabled]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2022-09-01", + "name": "[format('{0}-VM-MicrosoftAntiMalware', uniqueString(deployment().name, parameters('location')))]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "virtualMachineName": { + "value": "[parameters('name')]" + }, + "name": { + "value": "[coalesce(tryGet(parameters('extensionAntiMalwareConfig'), 'name'), 'MicrosoftAntiMalware')]" + }, + "location": { + "value": "[parameters('location')]" + }, + "publisher": { + "value": "Microsoft.Azure.Security" + }, + "type": { + "value": "IaaSAntimalware" + }, + "typeHandlerVersion": { + "value": "[coalesce(tryGet(parameters('extensionAntiMalwareConfig'), 'typeHandlerVersion'), '1.3')]" + }, + "autoUpgradeMinorVersion": { + "value": "[coalesce(tryGet(parameters('extensionAntiMalwareConfig'), 'autoUpgradeMinorVersion'), true())]" + }, + "enableAutomaticUpgrade": { + "value": "[coalesce(tryGet(parameters('extensionAntiMalwareConfig'), 'enableAutomaticUpgrade'), false())]" + }, + "settings": { + "value": "[coalesce(tryGet(parameters('extensionAntiMalwareConfig'), 'settings'), createObject('AntimalwareEnabled', 'true', 'Exclusions', createObject(), 'RealtimeProtectionEnabled', 'true', 'ScheduledScanSettings', createObject('day', '7', 'isEnabled', 'true', 'scanType', 'Quick', 'time', '120')))]" + }, + "supressFailures": { + "value": "[coalesce(tryGet(parameters('extensionAntiMalwareConfig'), 'supressFailures'), false())]" + }, + "tags": { + "value": "[coalesce(tryGet(parameters('extensionAntiMalwareConfig'), 'tags'), parameters('tags'))]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "languageVersion": "2.0", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.37.4.10188", + "templateHash": "13125609748815648088" + }, + "name": "Virtual Machine Extensions", + "description": "This module deploys a Virtual Machine Extension." + }, + "parameters": { + "virtualMachineName": { + "type": "string", + "metadata": { + "description": "Conditional. The name of the parent virtual machine that extension is provisioned for. Required if the template is used in a standalone deployment." + } + }, + "name": { + "type": "string", + "metadata": { + "description": "Required. The name of the virtual machine extension." + } + }, + "location": { + "type": "string", + "defaultValue": "[resourceGroup().location]", + "metadata": { + "description": "Optional. The location the extension is deployed to." + } + }, + "publisher": { + "type": "string", + "metadata": { + "description": "Required. The name of the extension handler publisher." + } + }, + "type": { + "type": "string", + "metadata": { + "description": "Required. Specifies the type of the extension; an example is \"CustomScriptExtension\"." + } + }, + "typeHandlerVersion": { + "type": "string", + "metadata": { + "description": "Required. Specifies the version of the script handler." + } + }, + "autoUpgradeMinorVersion": { + "type": "bool", + "metadata": { + "description": "Required. Indicates whether the extension should use a newer minor version if one is available at deployment time. Once deployed, however, the extension will not upgrade minor versions unless redeployed, even with this property set to true." + } + }, + "forceUpdateTag": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. How the extension handler should be forced to update even if the extension configuration has not changed." + } + }, + "settings": { + "type": "object", + "nullable": true, + "metadata": { + "description": "Optional. Any object that contains the extension specific settings." + } + }, + "protectedSettings": { + "type": "secureObject", + "nullable": true, + "metadata": { + "description": "Optional. Any object that contains the extension specific protected settings." + } + }, + "supressFailures": { + "type": "bool", + "defaultValue": false, + "metadata": { + "description": "Optional. Indicates whether failures stemming from the extension will be suppressed (Operational failures such as not connecting to the VM will not be suppressed regardless of this value). The default is false." + } + }, + "enableAutomaticUpgrade": { + "type": "bool", + "metadata": { + "description": "Required. Indicates whether the extension should be automatically upgraded by the platform if there is a newer version of the extension available." + } + }, + "tags": { + "type": "object", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.Compute/virtualMachines/extensions@2024-11-01#properties/tags" + }, + "description": "Optional. Tags of the resource." + }, + "nullable": true + }, + "protectedSettingsFromKeyVault": { + "type": "object", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.Compute/virtualMachines/extensions@2024-11-01#properties/properties/properties/protectedSettingsFromKeyVault" + }, + "description": "Optional. The extensions protected settings that are passed by reference, and consumed from key vault." + }, + "nullable": true + }, + "provisionAfterExtensions": { + "type": "array", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.Compute/virtualMachines/extensions@2024-11-01#properties/properties/properties/provisionAfterExtensions" + }, + "description": "Optional. Collection of extension names after which this extension needs to be provisioned." + }, + "nullable": true + } + }, + "resources": { + "virtualMachine": { + "existing": true, + "type": "Microsoft.Compute/virtualMachines", + "apiVersion": "2024-11-01", + "name": "[parameters('virtualMachineName')]" + }, + "extension": { + "type": "Microsoft.Compute/virtualMachines/extensions", + "apiVersion": "2024-11-01", + "name": "[format('{0}/{1}', parameters('virtualMachineName'), parameters('name'))]", + "location": "[parameters('location')]", + "tags": "[parameters('tags')]", + "properties": { + "publisher": "[parameters('publisher')]", + "type": "[parameters('type')]", + "typeHandlerVersion": "[parameters('typeHandlerVersion')]", + "autoUpgradeMinorVersion": "[parameters('autoUpgradeMinorVersion')]", + "enableAutomaticUpgrade": "[parameters('enableAutomaticUpgrade')]", + "forceUpdateTag": "[parameters('forceUpdateTag')]", + "settings": "[parameters('settings')]", + "protectedSettings": "[parameters('protectedSettings')]", + "suppressFailures": "[parameters('supressFailures')]", + "protectedSettingsFromKeyVault": "[parameters('protectedSettingsFromKeyVault')]", + "provisionAfterExtensions": "[parameters('provisionAfterExtensions')]" + } + } + }, + "outputs": { + "name": { + "type": "string", + "metadata": { + "description": "The name of the extension." + }, + "value": "[parameters('name')]" + }, + "resourceId": { + "type": "string", + "metadata": { + "description": "The resource ID of the extension." + }, + "value": "[resourceId('Microsoft.Compute/virtualMachines/extensions', parameters('virtualMachineName'), parameters('name'))]" + }, + "resourceGroupName": { + "type": "string", + "metadata": { + "description": "The name of the Resource Group the extension was created in." + }, + "value": "[resourceGroup().name]" + }, + "location": { + "type": "string", + "metadata": { + "description": "The location the resource was deployed into." + }, + "value": "[reference('extension', '2024-11-01', 'full').location]" + } + } + } + }, + "dependsOn": [ + "vm", + "vm_aadJoinExtension" + ] + }, + "vm_azureMonitorAgentExtension": { + "condition": "[parameters('extensionMonitoringAgentConfig').enabled]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2022-09-01", + "name": "[format('{0}-VM-AzureMonitorAgent', uniqueString(deployment().name, parameters('location')))]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "virtualMachineName": { + "value": "[parameters('name')]" + }, + "name": { + "value": "[coalesce(tryGet(parameters('extensionMonitoringAgentConfig'), 'name'), 'AzureMonitorAgent')]" + }, + "location": { + "value": "[parameters('location')]" + }, + "publisher": { + "value": "Microsoft.Azure.Monitor" + }, + "type": "[if(equals(parameters('osType'), 'Windows'), createObject('value', 'AzureMonitorWindowsAgent'), createObject('value', 'AzureMonitorLinuxAgent'))]", + "typeHandlerVersion": { + "value": "[coalesce(tryGet(parameters('extensionMonitoringAgentConfig'), 'typeHandlerVersion'), if(equals(parameters('osType'), 'Windows'), '1.22', '1.29'))]" + }, + "autoUpgradeMinorVersion": { + "value": "[coalesce(tryGet(parameters('extensionMonitoringAgentConfig'), 'autoUpgradeMinorVersion'), true())]" + }, + "enableAutomaticUpgrade": { + "value": "[coalesce(tryGet(parameters('extensionMonitoringAgentConfig'), 'enableAutomaticUpgrade'), false())]" + }, + "supressFailures": { + "value": "[coalesce(tryGet(parameters('extensionMonitoringAgentConfig'), 'supressFailures'), false())]" + }, + "tags": { + "value": "[coalesce(tryGet(parameters('extensionMonitoringAgentConfig'), 'tags'), parameters('tags'))]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "languageVersion": "2.0", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.37.4.10188", + "templateHash": "13125609748815648088" + }, + "name": "Virtual Machine Extensions", + "description": "This module deploys a Virtual Machine Extension." + }, + "parameters": { + "virtualMachineName": { + "type": "string", + "metadata": { + "description": "Conditional. The name of the parent virtual machine that extension is provisioned for. Required if the template is used in a standalone deployment." + } + }, + "name": { + "type": "string", + "metadata": { + "description": "Required. The name of the virtual machine extension." + } + }, + "location": { + "type": "string", + "defaultValue": "[resourceGroup().location]", + "metadata": { + "description": "Optional. The location the extension is deployed to." + } + }, + "publisher": { + "type": "string", + "metadata": { + "description": "Required. The name of the extension handler publisher." + } + }, + "type": { + "type": "string", + "metadata": { + "description": "Required. Specifies the type of the extension; an example is \"CustomScriptExtension\"." + } + }, + "typeHandlerVersion": { + "type": "string", + "metadata": { + "description": "Required. Specifies the version of the script handler." + } + }, + "autoUpgradeMinorVersion": { + "type": "bool", + "metadata": { + "description": "Required. Indicates whether the extension should use a newer minor version if one is available at deployment time. Once deployed, however, the extension will not upgrade minor versions unless redeployed, even with this property set to true." + } + }, + "forceUpdateTag": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. How the extension handler should be forced to update even if the extension configuration has not changed." + } + }, + "settings": { + "type": "object", + "nullable": true, + "metadata": { + "description": "Optional. Any object that contains the extension specific settings." + } + }, + "protectedSettings": { + "type": "secureObject", + "nullable": true, + "metadata": { + "description": "Optional. Any object that contains the extension specific protected settings." + } + }, + "supressFailures": { + "type": "bool", + "defaultValue": false, + "metadata": { + "description": "Optional. Indicates whether failures stemming from the extension will be suppressed (Operational failures such as not connecting to the VM will not be suppressed regardless of this value). The default is false." + } + }, + "enableAutomaticUpgrade": { + "type": "bool", + "metadata": { + "description": "Required. Indicates whether the extension should be automatically upgraded by the platform if there is a newer version of the extension available." + } + }, + "tags": { + "type": "object", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.Compute/virtualMachines/extensions@2024-11-01#properties/tags" + }, + "description": "Optional. Tags of the resource." + }, + "nullable": true + }, + "protectedSettingsFromKeyVault": { + "type": "object", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.Compute/virtualMachines/extensions@2024-11-01#properties/properties/properties/protectedSettingsFromKeyVault" + }, + "description": "Optional. The extensions protected settings that are passed by reference, and consumed from key vault." + }, + "nullable": true + }, + "provisionAfterExtensions": { + "type": "array", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.Compute/virtualMachines/extensions@2024-11-01#properties/properties/properties/provisionAfterExtensions" + }, + "description": "Optional. Collection of extension names after which this extension needs to be provisioned." + }, + "nullable": true + } + }, + "resources": { + "virtualMachine": { + "existing": true, + "type": "Microsoft.Compute/virtualMachines", + "apiVersion": "2024-11-01", + "name": "[parameters('virtualMachineName')]" + }, + "extension": { + "type": "Microsoft.Compute/virtualMachines/extensions", + "apiVersion": "2024-11-01", + "name": "[format('{0}/{1}', parameters('virtualMachineName'), parameters('name'))]", + "location": "[parameters('location')]", + "tags": "[parameters('tags')]", + "properties": { + "publisher": "[parameters('publisher')]", + "type": "[parameters('type')]", + "typeHandlerVersion": "[parameters('typeHandlerVersion')]", + "autoUpgradeMinorVersion": "[parameters('autoUpgradeMinorVersion')]", + "enableAutomaticUpgrade": "[parameters('enableAutomaticUpgrade')]", + "forceUpdateTag": "[parameters('forceUpdateTag')]", + "settings": "[parameters('settings')]", + "protectedSettings": "[parameters('protectedSettings')]", + "suppressFailures": "[parameters('supressFailures')]", + "protectedSettingsFromKeyVault": "[parameters('protectedSettingsFromKeyVault')]", + "provisionAfterExtensions": "[parameters('provisionAfterExtensions')]" + } + } + }, + "outputs": { + "name": { + "type": "string", + "metadata": { + "description": "The name of the extension." + }, + "value": "[parameters('name')]" + }, + "resourceId": { + "type": "string", + "metadata": { + "description": "The resource ID of the extension." + }, + "value": "[resourceId('Microsoft.Compute/virtualMachines/extensions', parameters('virtualMachineName'), parameters('name'))]" + }, + "resourceGroupName": { + "type": "string", + "metadata": { + "description": "The name of the Resource Group the extension was created in." + }, + "value": "[resourceGroup().name]" + }, + "location": { + "type": "string", + "metadata": { + "description": "The location the resource was deployed into." + }, + "value": "[reference('extension', '2024-11-01', 'full').location]" + } + } + } + }, + "dependsOn": [ + "vm", + "vm_microsoftAntiMalwareExtension" + ] + }, + "vm_dependencyAgentExtension": { + "condition": "[parameters('extensionDependencyAgentConfig').enabled]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2022-09-01", + "name": "[format('{0}-VM-DependencyAgent', uniqueString(deployment().name, parameters('location')))]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "virtualMachineName": { + "value": "[parameters('name')]" + }, + "name": { + "value": "[coalesce(tryGet(parameters('extensionDependencyAgentConfig'), 'name'), 'DependencyAgent')]" + }, + "location": { + "value": "[parameters('location')]" + }, + "publisher": { + "value": "Microsoft.Azure.Monitoring.DependencyAgent" + }, + "type": "[if(equals(parameters('osType'), 'Windows'), createObject('value', 'DependencyAgentWindows'), createObject('value', 'DependencyAgentLinux'))]", + "typeHandlerVersion": { + "value": "[coalesce(tryGet(parameters('extensionDependencyAgentConfig'), 'typeHandlerVersion'), '9.10')]" + }, + "autoUpgradeMinorVersion": { + "value": "[coalesce(tryGet(parameters('extensionDependencyAgentConfig'), 'autoUpgradeMinorVersion'), true())]" + }, + "enableAutomaticUpgrade": { + "value": "[coalesce(tryGet(parameters('extensionDependencyAgentConfig'), 'enableAutomaticUpgrade'), true())]" + }, + "settings": { + "value": { + "enableAMA": "[coalesce(tryGet(parameters('extensionDependencyAgentConfig'), 'enableAMA'), true())]" + } + }, + "supressFailures": { + "value": "[coalesce(tryGet(parameters('extensionDependencyAgentConfig'), 'supressFailures'), false())]" + }, + "tags": { + "value": "[coalesce(tryGet(parameters('extensionDependencyAgentConfig'), 'tags'), parameters('tags'))]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "languageVersion": "2.0", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.37.4.10188", + "templateHash": "13125609748815648088" + }, + "name": "Virtual Machine Extensions", + "description": "This module deploys a Virtual Machine Extension." + }, + "parameters": { + "virtualMachineName": { + "type": "string", + "metadata": { + "description": "Conditional. The name of the parent virtual machine that extension is provisioned for. Required if the template is used in a standalone deployment." + } + }, + "name": { + "type": "string", + "metadata": { + "description": "Required. The name of the virtual machine extension." + } + }, + "location": { + "type": "string", + "defaultValue": "[resourceGroup().location]", + "metadata": { + "description": "Optional. The location the extension is deployed to." + } + }, + "publisher": { + "type": "string", + "metadata": { + "description": "Required. The name of the extension handler publisher." + } + }, + "type": { + "type": "string", + "metadata": { + "description": "Required. Specifies the type of the extension; an example is \"CustomScriptExtension\"." + } + }, + "typeHandlerVersion": { + "type": "string", + "metadata": { + "description": "Required. Specifies the version of the script handler." + } + }, + "autoUpgradeMinorVersion": { + "type": "bool", + "metadata": { + "description": "Required. Indicates whether the extension should use a newer minor version if one is available at deployment time. Once deployed, however, the extension will not upgrade minor versions unless redeployed, even with this property set to true." + } + }, + "forceUpdateTag": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. How the extension handler should be forced to update even if the extension configuration has not changed." + } + }, + "settings": { + "type": "object", + "nullable": true, + "metadata": { + "description": "Optional. Any object that contains the extension specific settings." + } + }, + "protectedSettings": { + "type": "secureObject", + "nullable": true, + "metadata": { + "description": "Optional. Any object that contains the extension specific protected settings." + } + }, + "supressFailures": { + "type": "bool", + "defaultValue": false, + "metadata": { + "description": "Optional. Indicates whether failures stemming from the extension will be suppressed (Operational failures such as not connecting to the VM will not be suppressed regardless of this value). The default is false." + } + }, + "enableAutomaticUpgrade": { + "type": "bool", + "metadata": { + "description": "Required. Indicates whether the extension should be automatically upgraded by the platform if there is a newer version of the extension available." + } + }, + "tags": { + "type": "object", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.Compute/virtualMachines/extensions@2024-11-01#properties/tags" + }, + "description": "Optional. Tags of the resource." + }, + "nullable": true + }, + "protectedSettingsFromKeyVault": { + "type": "object", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.Compute/virtualMachines/extensions@2024-11-01#properties/properties/properties/protectedSettingsFromKeyVault" + }, + "description": "Optional. The extensions protected settings that are passed by reference, and consumed from key vault." + }, + "nullable": true + }, + "provisionAfterExtensions": { + "type": "array", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.Compute/virtualMachines/extensions@2024-11-01#properties/properties/properties/provisionAfterExtensions" + }, + "description": "Optional. Collection of extension names after which this extension needs to be provisioned." + }, + "nullable": true + } + }, + "resources": { + "virtualMachine": { + "existing": true, + "type": "Microsoft.Compute/virtualMachines", + "apiVersion": "2024-11-01", + "name": "[parameters('virtualMachineName')]" + }, + "extension": { + "type": "Microsoft.Compute/virtualMachines/extensions", + "apiVersion": "2024-11-01", + "name": "[format('{0}/{1}', parameters('virtualMachineName'), parameters('name'))]", + "location": "[parameters('location')]", + "tags": "[parameters('tags')]", + "properties": { + "publisher": "[parameters('publisher')]", + "type": "[parameters('type')]", + "typeHandlerVersion": "[parameters('typeHandlerVersion')]", + "autoUpgradeMinorVersion": "[parameters('autoUpgradeMinorVersion')]", + "enableAutomaticUpgrade": "[parameters('enableAutomaticUpgrade')]", + "forceUpdateTag": "[parameters('forceUpdateTag')]", + "settings": "[parameters('settings')]", + "protectedSettings": "[parameters('protectedSettings')]", + "suppressFailures": "[parameters('supressFailures')]", + "protectedSettingsFromKeyVault": "[parameters('protectedSettingsFromKeyVault')]", + "provisionAfterExtensions": "[parameters('provisionAfterExtensions')]" + } + } + }, + "outputs": { + "name": { + "type": "string", + "metadata": { + "description": "The name of the extension." + }, + "value": "[parameters('name')]" + }, + "resourceId": { + "type": "string", + "metadata": { + "description": "The resource ID of the extension." + }, + "value": "[resourceId('Microsoft.Compute/virtualMachines/extensions', parameters('virtualMachineName'), parameters('name'))]" + }, + "resourceGroupName": { + "type": "string", + "metadata": { + "description": "The name of the Resource Group the extension was created in." + }, + "value": "[resourceGroup().name]" + }, + "location": { + "type": "string", + "metadata": { + "description": "The location the resource was deployed into." + }, + "value": "[reference('extension', '2024-11-01', 'full').location]" + } + } + } + }, + "dependsOn": [ + "vm", + "vm_azureMonitorAgentExtension" + ] + }, + "vm_networkWatcherAgentExtension": { + "condition": "[parameters('extensionNetworkWatcherAgentConfig').enabled]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2022-09-01", + "name": "[format('{0}-VM-NetworkWatcherAgent', uniqueString(deployment().name, parameters('location')))]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "virtualMachineName": { + "value": "[parameters('name')]" + }, + "name": { + "value": "[coalesce(tryGet(parameters('extensionNetworkWatcherAgentConfig'), 'name'), 'NetworkWatcherAgent')]" + }, + "location": { + "value": "[parameters('location')]" + }, + "publisher": { + "value": "Microsoft.Azure.NetworkWatcher" + }, + "type": "[if(equals(parameters('osType'), 'Windows'), createObject('value', 'NetworkWatcherAgentWindows'), createObject('value', 'NetworkWatcherAgentLinux'))]", + "typeHandlerVersion": { + "value": "[coalesce(tryGet(parameters('extensionNetworkWatcherAgentConfig'), 'typeHandlerVersion'), '1.4')]" + }, + "autoUpgradeMinorVersion": { + "value": "[coalesce(tryGet(parameters('extensionNetworkWatcherAgentConfig'), 'autoUpgradeMinorVersion'), true())]" + }, + "enableAutomaticUpgrade": { + "value": "[coalesce(tryGet(parameters('extensionNetworkWatcherAgentConfig'), 'enableAutomaticUpgrade'), false())]" + }, + "supressFailures": { + "value": "[coalesce(tryGet(parameters('extensionNetworkWatcherAgentConfig'), 'supressFailures'), false())]" + }, + "tags": { + "value": "[coalesce(tryGet(parameters('extensionNetworkWatcherAgentConfig'), 'tags'), parameters('tags'))]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "languageVersion": "2.0", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.37.4.10188", + "templateHash": "13125609748815648088" + }, + "name": "Virtual Machine Extensions", + "description": "This module deploys a Virtual Machine Extension." + }, + "parameters": { + "virtualMachineName": { + "type": "string", + "metadata": { + "description": "Conditional. The name of the parent virtual machine that extension is provisioned for. Required if the template is used in a standalone deployment." + } + }, + "name": { + "type": "string", + "metadata": { + "description": "Required. The name of the virtual machine extension." + } + }, + "location": { + "type": "string", + "defaultValue": "[resourceGroup().location]", + "metadata": { + "description": "Optional. The location the extension is deployed to." + } + }, + "publisher": { + "type": "string", + "metadata": { + "description": "Required. The name of the extension handler publisher." + } + }, + "type": { + "type": "string", + "metadata": { + "description": "Required. Specifies the type of the extension; an example is \"CustomScriptExtension\"." + } + }, + "typeHandlerVersion": { + "type": "string", + "metadata": { + "description": "Required. Specifies the version of the script handler." + } + }, + "autoUpgradeMinorVersion": { + "type": "bool", + "metadata": { + "description": "Required. Indicates whether the extension should use a newer minor version if one is available at deployment time. Once deployed, however, the extension will not upgrade minor versions unless redeployed, even with this property set to true." + } + }, + "forceUpdateTag": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. How the extension handler should be forced to update even if the extension configuration has not changed." + } + }, + "settings": { + "type": "object", + "nullable": true, + "metadata": { + "description": "Optional. Any object that contains the extension specific settings." + } + }, + "protectedSettings": { + "type": "secureObject", + "nullable": true, + "metadata": { + "description": "Optional. Any object that contains the extension specific protected settings." + } + }, + "supressFailures": { + "type": "bool", + "defaultValue": false, + "metadata": { + "description": "Optional. Indicates whether failures stemming from the extension will be suppressed (Operational failures such as not connecting to the VM will not be suppressed regardless of this value). The default is false." + } + }, + "enableAutomaticUpgrade": { + "type": "bool", + "metadata": { + "description": "Required. Indicates whether the extension should be automatically upgraded by the platform if there is a newer version of the extension available." + } + }, + "tags": { + "type": "object", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.Compute/virtualMachines/extensions@2024-11-01#properties/tags" + }, + "description": "Optional. Tags of the resource." + }, + "nullable": true + }, + "protectedSettingsFromKeyVault": { + "type": "object", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.Compute/virtualMachines/extensions@2024-11-01#properties/properties/properties/protectedSettingsFromKeyVault" + }, + "description": "Optional. The extensions protected settings that are passed by reference, and consumed from key vault." + }, + "nullable": true + }, + "provisionAfterExtensions": { + "type": "array", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.Compute/virtualMachines/extensions@2024-11-01#properties/properties/properties/provisionAfterExtensions" + }, + "description": "Optional. Collection of extension names after which this extension needs to be provisioned." + }, + "nullable": true + } + }, + "resources": { + "virtualMachine": { + "existing": true, + "type": "Microsoft.Compute/virtualMachines", + "apiVersion": "2024-11-01", + "name": "[parameters('virtualMachineName')]" + }, + "extension": { + "type": "Microsoft.Compute/virtualMachines/extensions", + "apiVersion": "2024-11-01", + "name": "[format('{0}/{1}', parameters('virtualMachineName'), parameters('name'))]", + "location": "[parameters('location')]", + "tags": "[parameters('tags')]", + "properties": { + "publisher": "[parameters('publisher')]", + "type": "[parameters('type')]", + "typeHandlerVersion": "[parameters('typeHandlerVersion')]", + "autoUpgradeMinorVersion": "[parameters('autoUpgradeMinorVersion')]", + "enableAutomaticUpgrade": "[parameters('enableAutomaticUpgrade')]", + "forceUpdateTag": "[parameters('forceUpdateTag')]", + "settings": "[parameters('settings')]", + "protectedSettings": "[parameters('protectedSettings')]", + "suppressFailures": "[parameters('supressFailures')]", + "protectedSettingsFromKeyVault": "[parameters('protectedSettingsFromKeyVault')]", + "provisionAfterExtensions": "[parameters('provisionAfterExtensions')]" + } + } + }, + "outputs": { + "name": { + "type": "string", + "metadata": { + "description": "The name of the extension." + }, + "value": "[parameters('name')]" + }, + "resourceId": { + "type": "string", + "metadata": { + "description": "The resource ID of the extension." + }, + "value": "[resourceId('Microsoft.Compute/virtualMachines/extensions', parameters('virtualMachineName'), parameters('name'))]" + }, + "resourceGroupName": { + "type": "string", + "metadata": { + "description": "The name of the Resource Group the extension was created in." + }, + "value": "[resourceGroup().name]" + }, + "location": { + "type": "string", + "metadata": { + "description": "The location the resource was deployed into." + }, + "value": "[reference('extension', '2024-11-01', 'full').location]" + } + } + } + }, + "dependsOn": [ + "vm", + "vm_dependencyAgentExtension" + ] + }, + "vm_desiredStateConfigurationExtension": { + "condition": "[parameters('extensionDSCConfig').enabled]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2022-09-01", + "name": "[format('{0}-VM-DesiredStateConfiguration', uniqueString(deployment().name, parameters('location')))]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "virtualMachineName": { + "value": "[parameters('name')]" + }, + "name": { + "value": "[coalesce(tryGet(parameters('extensionDSCConfig'), 'name'), 'DesiredStateConfiguration')]" + }, + "location": { + "value": "[parameters('location')]" + }, + "publisher": { + "value": "Microsoft.Powershell" + }, + "type": { + "value": "DSC" + }, + "typeHandlerVersion": { + "value": "[coalesce(tryGet(parameters('extensionDSCConfig'), 'typeHandlerVersion'), '2.77')]" + }, + "autoUpgradeMinorVersion": { + "value": "[coalesce(tryGet(parameters('extensionDSCConfig'), 'autoUpgradeMinorVersion'), true())]" + }, + "enableAutomaticUpgrade": { + "value": "[coalesce(tryGet(parameters('extensionDSCConfig'), 'enableAutomaticUpgrade'), false())]" + }, + "settings": { + "value": "[coalesce(tryGet(parameters('extensionDSCConfig'), 'settings'), createObject())]" + }, + "supressFailures": { + "value": "[coalesce(tryGet(parameters('extensionDSCConfig'), 'supressFailures'), false())]" + }, + "tags": { + "value": "[coalesce(tryGet(parameters('extensionDSCConfig'), 'tags'), parameters('tags'))]" + }, + "protectedSettings": { + "value": "[coalesce(tryGet(parameters('extensionDSCConfig'), 'protectedSettings'), createObject())]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "languageVersion": "2.0", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.37.4.10188", + "templateHash": "13125609748815648088" + }, + "name": "Virtual Machine Extensions", + "description": "This module deploys a Virtual Machine Extension." + }, + "parameters": { + "virtualMachineName": { + "type": "string", + "metadata": { + "description": "Conditional. The name of the parent virtual machine that extension is provisioned for. Required if the template is used in a standalone deployment." + } + }, + "name": { + "type": "string", + "metadata": { + "description": "Required. The name of the virtual machine extension." + } + }, + "location": { + "type": "string", + "defaultValue": "[resourceGroup().location]", + "metadata": { + "description": "Optional. The location the extension is deployed to." + } + }, + "publisher": { + "type": "string", + "metadata": { + "description": "Required. The name of the extension handler publisher." + } + }, + "type": { + "type": "string", + "metadata": { + "description": "Required. Specifies the type of the extension; an example is \"CustomScriptExtension\"." + } + }, + "typeHandlerVersion": { + "type": "string", + "metadata": { + "description": "Required. Specifies the version of the script handler." + } + }, + "autoUpgradeMinorVersion": { + "type": "bool", + "metadata": { + "description": "Required. Indicates whether the extension should use a newer minor version if one is available at deployment time. Once deployed, however, the extension will not upgrade minor versions unless redeployed, even with this property set to true." + } + }, + "forceUpdateTag": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. How the extension handler should be forced to update even if the extension configuration has not changed." + } + }, + "settings": { + "type": "object", + "nullable": true, + "metadata": { + "description": "Optional. Any object that contains the extension specific settings." + } + }, + "protectedSettings": { + "type": "secureObject", + "nullable": true, + "metadata": { + "description": "Optional. Any object that contains the extension specific protected settings." + } + }, + "supressFailures": { + "type": "bool", + "defaultValue": false, + "metadata": { + "description": "Optional. Indicates whether failures stemming from the extension will be suppressed (Operational failures such as not connecting to the VM will not be suppressed regardless of this value). The default is false." + } + }, + "enableAutomaticUpgrade": { + "type": "bool", + "metadata": { + "description": "Required. Indicates whether the extension should be automatically upgraded by the platform if there is a newer version of the extension available." + } + }, + "tags": { + "type": "object", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.Compute/virtualMachines/extensions@2024-11-01#properties/tags" + }, + "description": "Optional. Tags of the resource." + }, + "nullable": true + }, + "protectedSettingsFromKeyVault": { + "type": "object", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.Compute/virtualMachines/extensions@2024-11-01#properties/properties/properties/protectedSettingsFromKeyVault" + }, + "description": "Optional. The extensions protected settings that are passed by reference, and consumed from key vault." + }, + "nullable": true + }, + "provisionAfterExtensions": { + "type": "array", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.Compute/virtualMachines/extensions@2024-11-01#properties/properties/properties/provisionAfterExtensions" + }, + "description": "Optional. Collection of extension names after which this extension needs to be provisioned." + }, + "nullable": true + } + }, + "resources": { + "virtualMachine": { + "existing": true, + "type": "Microsoft.Compute/virtualMachines", + "apiVersion": "2024-11-01", + "name": "[parameters('virtualMachineName')]" + }, + "extension": { + "type": "Microsoft.Compute/virtualMachines/extensions", + "apiVersion": "2024-11-01", + "name": "[format('{0}/{1}', parameters('virtualMachineName'), parameters('name'))]", + "location": "[parameters('location')]", + "tags": "[parameters('tags')]", + "properties": { + "publisher": "[parameters('publisher')]", + "type": "[parameters('type')]", + "typeHandlerVersion": "[parameters('typeHandlerVersion')]", + "autoUpgradeMinorVersion": "[parameters('autoUpgradeMinorVersion')]", + "enableAutomaticUpgrade": "[parameters('enableAutomaticUpgrade')]", + "forceUpdateTag": "[parameters('forceUpdateTag')]", + "settings": "[parameters('settings')]", + "protectedSettings": "[parameters('protectedSettings')]", + "suppressFailures": "[parameters('supressFailures')]", + "protectedSettingsFromKeyVault": "[parameters('protectedSettingsFromKeyVault')]", + "provisionAfterExtensions": "[parameters('provisionAfterExtensions')]" + } + } + }, + "outputs": { + "name": { + "type": "string", + "metadata": { + "description": "The name of the extension." + }, + "value": "[parameters('name')]" + }, + "resourceId": { + "type": "string", + "metadata": { + "description": "The resource ID of the extension." + }, + "value": "[resourceId('Microsoft.Compute/virtualMachines/extensions', parameters('virtualMachineName'), parameters('name'))]" + }, + "resourceGroupName": { + "type": "string", + "metadata": { + "description": "The name of the Resource Group the extension was created in." + }, + "value": "[resourceGroup().name]" + }, + "location": { + "type": "string", + "metadata": { + "description": "The location the resource was deployed into." + }, + "value": "[reference('extension', '2024-11-01', 'full').location]" + } + } + } + }, + "dependsOn": [ + "vm", + "vm_networkWatcherAgentExtension" + ] + }, + "vm_customScriptExtension": { + "condition": "[not(empty(parameters('extensionCustomScriptConfig')))]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2022-09-01", + "name": "[format('{0}-VM-CustomScriptExtension', uniqueString(deployment().name, parameters('location')))]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "virtualMachineName": { + "value": "[parameters('name')]" + }, + "name": { + "value": "[coalesce(tryGet(parameters('extensionCustomScriptConfig'), 'name'), 'CustomScriptExtension')]" + }, + "location": { + "value": "[parameters('location')]" + }, + "publisher": "[if(equals(parameters('osType'), 'Windows'), createObject('value', 'Microsoft.Compute'), createObject('value', 'Microsoft.Azure.Extensions'))]", + "type": "[if(equals(parameters('osType'), 'Windows'), createObject('value', 'CustomScriptExtension'), createObject('value', 'CustomScript'))]", + "typeHandlerVersion": { + "value": "[coalesce(tryGet(parameters('extensionCustomScriptConfig'), 'typeHandlerVersion'), if(equals(parameters('osType'), 'Windows'), '1.10', '2.1'))]" + }, + "autoUpgradeMinorVersion": { + "value": "[coalesce(tryGet(parameters('extensionCustomScriptConfig'), 'autoUpgradeMinorVersion'), true())]" + }, + "enableAutomaticUpgrade": { + "value": "[coalesce(tryGet(parameters('extensionCustomScriptConfig'), 'enableAutomaticUpgrade'), false())]" + }, + "forceUpdateTag": { + "value": "[tryGet(parameters('extensionCustomScriptConfig'), 'forceUpdateTag')]" + }, + "provisionAfterExtensions": { + "value": "[tryGet(parameters('extensionCustomScriptConfig'), 'provisionAfterExtensions')]" + }, + "supressFailures": { + "value": "[coalesce(tryGet(parameters('extensionCustomScriptConfig'), 'supressFailures'), false())]" + }, + "tags": { + "value": "[coalesce(tryGet(parameters('extensionCustomScriptConfig'), 'tags'), parameters('tags'))]" + }, + "protectedSettingsFromKeyVault": { + "value": "[tryGet(parameters('extensionCustomScriptConfig'), 'protectedSettingsFromKeyVault')]" + }, + "settings": { + "value": "[shallowMerge(createArray(if(not(empty(tryGet(tryGet(parameters('extensionCustomScriptConfig'), 'settings'), 'commandToExecute'))), createObject('commandToExecute', tryGet(tryGet(parameters('extensionCustomScriptConfig'), 'settings'), 'commandToExecute')), createObject()), if(not(empty(tryGet(tryGet(parameters('extensionCustomScriptConfig'), 'settings'), 'fileUris'))), createObject('fileUris', tryGet(parameters('extensionCustomScriptConfig'), 'settings', 'fileUris')), createObject())))]" + }, + "protectedSettings": { + "value": "[shallowMerge(createArray(if(not(empty(tryGet(tryGet(parameters('extensionCustomScriptConfig'), 'protectedSettings'), 'commandToExecute'))), createObject('commandToExecute', tryGet(parameters('extensionCustomScriptConfig').protectedSettings, 'commandToExecute')), createObject()), if(not(empty(tryGet(tryGet(parameters('extensionCustomScriptConfig'), 'protectedSettings'), 'storageAccountName'))), createObject('storageAccountName', parameters('extensionCustomScriptConfig').protectedSettings.storageAccountName), createObject()), if(not(empty(tryGet(tryGet(parameters('extensionCustomScriptConfig'), 'protectedSettings'), 'storageAccountKey'))), createObject('storageAccountKey', parameters('extensionCustomScriptConfig').protectedSettings.storageAccountKey), createObject()), if(not(empty(tryGet(tryGet(parameters('extensionCustomScriptConfig'), 'protectedSettings'), 'fileUris'))), createObject('fileUris', parameters('extensionCustomScriptConfig').protectedSettings.fileUris), createObject()), if(not(equals(tryGet(tryGet(parameters('extensionCustomScriptConfig'), 'protectedSettings'), 'managedIdentityResourceId'), null())), createObject('managedIdentity', if(not(empty(tryGet(parameters('extensionCustomScriptConfig').protectedSettings, 'managedIdentityResourceId'))), createObject('clientId', reference('cseIdentity').clientId), createObject())), createObject())))]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "languageVersion": "2.0", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.37.4.10188", + "templateHash": "13125609748815648088" + }, + "name": "Virtual Machine Extensions", + "description": "This module deploys a Virtual Machine Extension." + }, + "parameters": { + "virtualMachineName": { + "type": "string", + "metadata": { + "description": "Conditional. The name of the parent virtual machine that extension is provisioned for. Required if the template is used in a standalone deployment." + } + }, + "name": { + "type": "string", + "metadata": { + "description": "Required. The name of the virtual machine extension." + } + }, + "location": { + "type": "string", + "defaultValue": "[resourceGroup().location]", + "metadata": { + "description": "Optional. The location the extension is deployed to." + } + }, + "publisher": { + "type": "string", + "metadata": { + "description": "Required. The name of the extension handler publisher." + } + }, + "type": { + "type": "string", + "metadata": { + "description": "Required. Specifies the type of the extension; an example is \"CustomScriptExtension\"." + } + }, + "typeHandlerVersion": { + "type": "string", + "metadata": { + "description": "Required. Specifies the version of the script handler." + } + }, + "autoUpgradeMinorVersion": { + "type": "bool", + "metadata": { + "description": "Required. Indicates whether the extension should use a newer minor version if one is available at deployment time. Once deployed, however, the extension will not upgrade minor versions unless redeployed, even with this property set to true." + } + }, + "forceUpdateTag": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. How the extension handler should be forced to update even if the extension configuration has not changed." + } + }, + "settings": { + "type": "object", + "nullable": true, + "metadata": { + "description": "Optional. Any object that contains the extension specific settings." + } + }, + "protectedSettings": { + "type": "secureObject", + "nullable": true, + "metadata": { + "description": "Optional. Any object that contains the extension specific protected settings." + } + }, + "supressFailures": { + "type": "bool", + "defaultValue": false, + "metadata": { + "description": "Optional. Indicates whether failures stemming from the extension will be suppressed (Operational failures such as not connecting to the VM will not be suppressed regardless of this value). The default is false." + } + }, + "enableAutomaticUpgrade": { + "type": "bool", + "metadata": { + "description": "Required. Indicates whether the extension should be automatically upgraded by the platform if there is a newer version of the extension available." + } + }, + "tags": { + "type": "object", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.Compute/virtualMachines/extensions@2024-11-01#properties/tags" + }, + "description": "Optional. Tags of the resource." + }, + "nullable": true + }, + "protectedSettingsFromKeyVault": { + "type": "object", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.Compute/virtualMachines/extensions@2024-11-01#properties/properties/properties/protectedSettingsFromKeyVault" + }, + "description": "Optional. The extensions protected settings that are passed by reference, and consumed from key vault." + }, + "nullable": true + }, + "provisionAfterExtensions": { + "type": "array", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.Compute/virtualMachines/extensions@2024-11-01#properties/properties/properties/provisionAfterExtensions" + }, + "description": "Optional. Collection of extension names after which this extension needs to be provisioned." + }, + "nullable": true + } + }, + "resources": { + "virtualMachine": { + "existing": true, + "type": "Microsoft.Compute/virtualMachines", + "apiVersion": "2024-11-01", + "name": "[parameters('virtualMachineName')]" + }, + "extension": { + "type": "Microsoft.Compute/virtualMachines/extensions", + "apiVersion": "2024-11-01", + "name": "[format('{0}/{1}', parameters('virtualMachineName'), parameters('name'))]", + "location": "[parameters('location')]", + "tags": "[parameters('tags')]", + "properties": { + "publisher": "[parameters('publisher')]", + "type": "[parameters('type')]", + "typeHandlerVersion": "[parameters('typeHandlerVersion')]", + "autoUpgradeMinorVersion": "[parameters('autoUpgradeMinorVersion')]", + "enableAutomaticUpgrade": "[parameters('enableAutomaticUpgrade')]", + "forceUpdateTag": "[parameters('forceUpdateTag')]", + "settings": "[parameters('settings')]", + "protectedSettings": "[parameters('protectedSettings')]", + "suppressFailures": "[parameters('supressFailures')]", + "protectedSettingsFromKeyVault": "[parameters('protectedSettingsFromKeyVault')]", + "provisionAfterExtensions": "[parameters('provisionAfterExtensions')]" + } + } + }, + "outputs": { + "name": { + "type": "string", + "metadata": { + "description": "The name of the extension." + }, + "value": "[parameters('name')]" + }, + "resourceId": { + "type": "string", + "metadata": { + "description": "The resource ID of the extension." + }, + "value": "[resourceId('Microsoft.Compute/virtualMachines/extensions', parameters('virtualMachineName'), parameters('name'))]" + }, + "resourceGroupName": { + "type": "string", + "metadata": { + "description": "The name of the Resource Group the extension was created in." + }, + "value": "[resourceGroup().name]" + }, + "location": { + "type": "string", + "metadata": { + "description": "The location the resource was deployed into." + }, + "value": "[reference('extension', '2024-11-01', 'full').location]" + } + } + } + }, + "dependsOn": [ + "cseIdentity", + "vm", + "vm_desiredStateConfigurationExtension" + ] + }, + "vm_azureDiskEncryptionExtension": { + "condition": "[parameters('extensionAzureDiskEncryptionConfig').enabled]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2022-09-01", + "name": "[format('{0}-VM-AzureDiskEncryption', uniqueString(deployment().name, parameters('location')))]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "virtualMachineName": { + "value": "[parameters('name')]" + }, + "name": { + "value": "[coalesce(tryGet(parameters('extensionAzureDiskEncryptionConfig'), 'name'), 'AzureDiskEncryption')]" + }, + "location": { + "value": "[parameters('location')]" + }, + "publisher": { + "value": "Microsoft.Azure.Security" + }, + "type": "[if(equals(parameters('osType'), 'Windows'), createObject('value', 'AzureDiskEncryption'), createObject('value', 'AzureDiskEncryptionForLinux'))]", + "typeHandlerVersion": { + "value": "[coalesce(tryGet(parameters('extensionAzureDiskEncryptionConfig'), 'typeHandlerVersion'), if(equals(parameters('osType'), 'Windows'), '2.2', '1.1'))]" + }, + "autoUpgradeMinorVersion": { + "value": "[coalesce(tryGet(parameters('extensionAzureDiskEncryptionConfig'), 'autoUpgradeMinorVersion'), true())]" + }, + "enableAutomaticUpgrade": { + "value": "[coalesce(tryGet(parameters('extensionAzureDiskEncryptionConfig'), 'enableAutomaticUpgrade'), false())]" + }, + "forceUpdateTag": { + "value": "[coalesce(tryGet(parameters('extensionAzureDiskEncryptionConfig'), 'forceUpdateTag'), '1.0')]" + }, + "settings": { + "value": "[coalesce(tryGet(parameters('extensionAzureDiskEncryptionConfig'), 'settings'), createObject())]" + }, + "supressFailures": { + "value": "[coalesce(tryGet(parameters('extensionAzureDiskEncryptionConfig'), 'supressFailures'), false())]" + }, + "tags": { + "value": "[coalesce(tryGet(parameters('extensionAzureDiskEncryptionConfig'), 'tags'), parameters('tags'))]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "languageVersion": "2.0", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.37.4.10188", + "templateHash": "13125609748815648088" + }, + "name": "Virtual Machine Extensions", + "description": "This module deploys a Virtual Machine Extension." + }, + "parameters": { + "virtualMachineName": { + "type": "string", + "metadata": { + "description": "Conditional. The name of the parent virtual machine that extension is provisioned for. Required if the template is used in a standalone deployment." + } + }, + "name": { + "type": "string", + "metadata": { + "description": "Required. The name of the virtual machine extension." + } + }, + "location": { + "type": "string", + "defaultValue": "[resourceGroup().location]", + "metadata": { + "description": "Optional. The location the extension is deployed to." + } + }, + "publisher": { + "type": "string", + "metadata": { + "description": "Required. The name of the extension handler publisher." + } + }, + "type": { + "type": "string", + "metadata": { + "description": "Required. Specifies the type of the extension; an example is \"CustomScriptExtension\"." + } + }, + "typeHandlerVersion": { + "type": "string", + "metadata": { + "description": "Required. Specifies the version of the script handler." + } + }, + "autoUpgradeMinorVersion": { + "type": "bool", + "metadata": { + "description": "Required. Indicates whether the extension should use a newer minor version if one is available at deployment time. Once deployed, however, the extension will not upgrade minor versions unless redeployed, even with this property set to true." + } + }, + "forceUpdateTag": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. How the extension handler should be forced to update even if the extension configuration has not changed." + } + }, + "settings": { + "type": "object", + "nullable": true, + "metadata": { + "description": "Optional. Any object that contains the extension specific settings." + } + }, + "protectedSettings": { + "type": "secureObject", + "nullable": true, + "metadata": { + "description": "Optional. Any object that contains the extension specific protected settings." + } + }, + "supressFailures": { + "type": "bool", + "defaultValue": false, + "metadata": { + "description": "Optional. Indicates whether failures stemming from the extension will be suppressed (Operational failures such as not connecting to the VM will not be suppressed regardless of this value). The default is false." + } + }, + "enableAutomaticUpgrade": { + "type": "bool", + "metadata": { + "description": "Required. Indicates whether the extension should be automatically upgraded by the platform if there is a newer version of the extension available." + } + }, + "tags": { + "type": "object", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.Compute/virtualMachines/extensions@2024-11-01#properties/tags" + }, + "description": "Optional. Tags of the resource." + }, + "nullable": true + }, + "protectedSettingsFromKeyVault": { + "type": "object", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.Compute/virtualMachines/extensions@2024-11-01#properties/properties/properties/protectedSettingsFromKeyVault" + }, + "description": "Optional. The extensions protected settings that are passed by reference, and consumed from key vault." + }, + "nullable": true + }, + "provisionAfterExtensions": { + "type": "array", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.Compute/virtualMachines/extensions@2024-11-01#properties/properties/properties/provisionAfterExtensions" + }, + "description": "Optional. Collection of extension names after which this extension needs to be provisioned." + }, + "nullable": true + } + }, + "resources": { + "virtualMachine": { + "existing": true, + "type": "Microsoft.Compute/virtualMachines", + "apiVersion": "2024-11-01", + "name": "[parameters('virtualMachineName')]" + }, + "extension": { + "type": "Microsoft.Compute/virtualMachines/extensions", + "apiVersion": "2024-11-01", + "name": "[format('{0}/{1}', parameters('virtualMachineName'), parameters('name'))]", + "location": "[parameters('location')]", + "tags": "[parameters('tags')]", + "properties": { + "publisher": "[parameters('publisher')]", + "type": "[parameters('type')]", + "typeHandlerVersion": "[parameters('typeHandlerVersion')]", + "autoUpgradeMinorVersion": "[parameters('autoUpgradeMinorVersion')]", + "enableAutomaticUpgrade": "[parameters('enableAutomaticUpgrade')]", + "forceUpdateTag": "[parameters('forceUpdateTag')]", + "settings": "[parameters('settings')]", + "protectedSettings": "[parameters('protectedSettings')]", + "suppressFailures": "[parameters('supressFailures')]", + "protectedSettingsFromKeyVault": "[parameters('protectedSettingsFromKeyVault')]", + "provisionAfterExtensions": "[parameters('provisionAfterExtensions')]" + } + } + }, + "outputs": { + "name": { + "type": "string", + "metadata": { + "description": "The name of the extension." + }, + "value": "[parameters('name')]" + }, + "resourceId": { + "type": "string", + "metadata": { + "description": "The resource ID of the extension." + }, + "value": "[resourceId('Microsoft.Compute/virtualMachines/extensions', parameters('virtualMachineName'), parameters('name'))]" + }, + "resourceGroupName": { + "type": "string", + "metadata": { + "description": "The name of the Resource Group the extension was created in." + }, + "value": "[resourceGroup().name]" + }, + "location": { + "type": "string", + "metadata": { + "description": "The location the resource was deployed into." + }, + "value": "[reference('extension', '2024-11-01', 'full').location]" + } + } + } + }, + "dependsOn": [ + "vm", + "vm_customScriptExtension" + ] + }, + "vm_nvidiaGpuDriverWindowsExtension": { + "condition": "[parameters('extensionNvidiaGpuDriverWindows').enabled]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2022-09-01", + "name": "[format('{0}-VM-NvidiaGpuDriverWindows', uniqueString(deployment().name, parameters('location')))]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "virtualMachineName": { + "value": "[parameters('name')]" + }, + "name": { + "value": "[coalesce(tryGet(parameters('extensionNvidiaGpuDriverWindows'), 'name'), 'NvidiaGpuDriverWindows')]" + }, + "location": { + "value": "[parameters('location')]" + }, + "publisher": { + "value": "Microsoft.HpcCompute" + }, + "type": { + "value": "NvidiaGpuDriverWindows" + }, + "typeHandlerVersion": { + "value": "[coalesce(tryGet(parameters('extensionNvidiaGpuDriverWindows'), 'typeHandlerVersion'), '1.4')]" + }, + "autoUpgradeMinorVersion": { + "value": "[coalesce(tryGet(parameters('extensionNvidiaGpuDriverWindows'), 'autoUpgradeMinorVersion'), true())]" + }, + "enableAutomaticUpgrade": { + "value": "[coalesce(tryGet(parameters('extensionNvidiaGpuDriverWindows'), 'enableAutomaticUpgrade'), false())]" + }, + "supressFailures": { + "value": "[coalesce(tryGet(parameters('extensionNvidiaGpuDriverWindows'), 'supressFailures'), false())]" + }, + "tags": { + "value": "[coalesce(tryGet(parameters('extensionNvidiaGpuDriverWindows'), 'tags'), parameters('tags'))]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "languageVersion": "2.0", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.37.4.10188", + "templateHash": "13125609748815648088" + }, + "name": "Virtual Machine Extensions", + "description": "This module deploys a Virtual Machine Extension." + }, + "parameters": { + "virtualMachineName": { + "type": "string", + "metadata": { + "description": "Conditional. The name of the parent virtual machine that extension is provisioned for. Required if the template is used in a standalone deployment." + } + }, + "name": { + "type": "string", + "metadata": { + "description": "Required. The name of the virtual machine extension." + } + }, + "location": { + "type": "string", + "defaultValue": "[resourceGroup().location]", + "metadata": { + "description": "Optional. The location the extension is deployed to." + } + }, + "publisher": { + "type": "string", + "metadata": { + "description": "Required. The name of the extension handler publisher." + } + }, + "type": { + "type": "string", + "metadata": { + "description": "Required. Specifies the type of the extension; an example is \"CustomScriptExtension\"." + } + }, + "typeHandlerVersion": { + "type": "string", + "metadata": { + "description": "Required. Specifies the version of the script handler." + } + }, + "autoUpgradeMinorVersion": { + "type": "bool", + "metadata": { + "description": "Required. Indicates whether the extension should use a newer minor version if one is available at deployment time. Once deployed, however, the extension will not upgrade minor versions unless redeployed, even with this property set to true." + } + }, + "forceUpdateTag": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. How the extension handler should be forced to update even if the extension configuration has not changed." + } + }, + "settings": { + "type": "object", + "nullable": true, + "metadata": { + "description": "Optional. Any object that contains the extension specific settings." + } + }, + "protectedSettings": { + "type": "secureObject", + "nullable": true, + "metadata": { + "description": "Optional. Any object that contains the extension specific protected settings." + } + }, + "supressFailures": { + "type": "bool", + "defaultValue": false, + "metadata": { + "description": "Optional. Indicates whether failures stemming from the extension will be suppressed (Operational failures such as not connecting to the VM will not be suppressed regardless of this value). The default is false." + } + }, + "enableAutomaticUpgrade": { + "type": "bool", + "metadata": { + "description": "Required. Indicates whether the extension should be automatically upgraded by the platform if there is a newer version of the extension available." + } + }, + "tags": { + "type": "object", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.Compute/virtualMachines/extensions@2024-11-01#properties/tags" + }, + "description": "Optional. Tags of the resource." + }, + "nullable": true + }, + "protectedSettingsFromKeyVault": { + "type": "object", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.Compute/virtualMachines/extensions@2024-11-01#properties/properties/properties/protectedSettingsFromKeyVault" + }, + "description": "Optional. The extensions protected settings that are passed by reference, and consumed from key vault." + }, + "nullable": true + }, + "provisionAfterExtensions": { + "type": "array", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.Compute/virtualMachines/extensions@2024-11-01#properties/properties/properties/provisionAfterExtensions" + }, + "description": "Optional. Collection of extension names after which this extension needs to be provisioned." + }, + "nullable": true + } + }, + "resources": { + "virtualMachine": { + "existing": true, + "type": "Microsoft.Compute/virtualMachines", + "apiVersion": "2024-11-01", + "name": "[parameters('virtualMachineName')]" + }, + "extension": { + "type": "Microsoft.Compute/virtualMachines/extensions", + "apiVersion": "2024-11-01", + "name": "[format('{0}/{1}', parameters('virtualMachineName'), parameters('name'))]", + "location": "[parameters('location')]", + "tags": "[parameters('tags')]", + "properties": { + "publisher": "[parameters('publisher')]", + "type": "[parameters('type')]", + "typeHandlerVersion": "[parameters('typeHandlerVersion')]", + "autoUpgradeMinorVersion": "[parameters('autoUpgradeMinorVersion')]", + "enableAutomaticUpgrade": "[parameters('enableAutomaticUpgrade')]", + "forceUpdateTag": "[parameters('forceUpdateTag')]", + "settings": "[parameters('settings')]", + "protectedSettings": "[parameters('protectedSettings')]", + "suppressFailures": "[parameters('supressFailures')]", + "protectedSettingsFromKeyVault": "[parameters('protectedSettingsFromKeyVault')]", + "provisionAfterExtensions": "[parameters('provisionAfterExtensions')]" + } + } + }, + "outputs": { + "name": { + "type": "string", + "metadata": { + "description": "The name of the extension." + }, + "value": "[parameters('name')]" + }, + "resourceId": { + "type": "string", + "metadata": { + "description": "The resource ID of the extension." + }, + "value": "[resourceId('Microsoft.Compute/virtualMachines/extensions', parameters('virtualMachineName'), parameters('name'))]" + }, + "resourceGroupName": { + "type": "string", + "metadata": { + "description": "The name of the Resource Group the extension was created in." + }, + "value": "[resourceGroup().name]" + }, + "location": { + "type": "string", + "metadata": { + "description": "The location the resource was deployed into." + }, + "value": "[reference('extension', '2024-11-01', 'full').location]" + } + } + } + }, + "dependsOn": [ + "vm", + "vm_azureDiskEncryptionExtension" + ] + }, + "vm_hostPoolRegistrationExtension": { + "condition": "[parameters('extensionHostPoolRegistration').enabled]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2022-09-01", + "name": "[format('{0}-VM-HostPoolRegistration', uniqueString(deployment().name, parameters('location')))]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "virtualMachineName": { + "value": "[parameters('name')]" + }, + "name": { + "value": "[coalesce(tryGet(parameters('extensionHostPoolRegistration'), 'name'), 'HostPoolRegistration')]" + }, + "location": { + "value": "[parameters('location')]" + }, + "publisher": { + "value": "Microsoft.PowerShell" + }, + "type": { + "value": "DSC" + }, + "typeHandlerVersion": { + "value": "[coalesce(tryGet(parameters('extensionHostPoolRegistration'), 'typeHandlerVersion'), '2.77')]" + }, + "autoUpgradeMinorVersion": { + "value": "[coalesce(tryGet(parameters('extensionHostPoolRegistration'), 'autoUpgradeMinorVersion'), true())]" + }, + "enableAutomaticUpgrade": { + "value": "[coalesce(tryGet(parameters('extensionHostPoolRegistration'), 'enableAutomaticUpgrade'), false())]" + }, + "settings": { + "value": { + "modulesUrl": "[parameters('extensionHostPoolRegistration').modulesUrl]", + "configurationFunction": "[parameters('extensionHostPoolRegistration').configurationFunction]", + "properties": { + "hostPoolName": "[parameters('extensionHostPoolRegistration').hostPoolName]", + "registrationInfoToken": "[parameters('extensionHostPoolRegistration').registrationInfoToken]", + "aadJoin": true + }, + "supressFailures": "[coalesce(tryGet(parameters('extensionHostPoolRegistration'), 'supressFailures'), false())]" + } + }, + "tags": { + "value": "[coalesce(tryGet(parameters('extensionHostPoolRegistration'), 'tags'), parameters('tags'))]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "languageVersion": "2.0", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.37.4.10188", + "templateHash": "13125609748815648088" + }, + "name": "Virtual Machine Extensions", + "description": "This module deploys a Virtual Machine Extension." + }, + "parameters": { + "virtualMachineName": { + "type": "string", + "metadata": { + "description": "Conditional. The name of the parent virtual machine that extension is provisioned for. Required if the template is used in a standalone deployment." + } + }, + "name": { + "type": "string", + "metadata": { + "description": "Required. The name of the virtual machine extension." + } + }, + "location": { + "type": "string", + "defaultValue": "[resourceGroup().location]", + "metadata": { + "description": "Optional. The location the extension is deployed to." + } + }, + "publisher": { + "type": "string", + "metadata": { + "description": "Required. The name of the extension handler publisher." + } + }, + "type": { + "type": "string", + "metadata": { + "description": "Required. Specifies the type of the extension; an example is \"CustomScriptExtension\"." + } + }, + "typeHandlerVersion": { + "type": "string", + "metadata": { + "description": "Required. Specifies the version of the script handler." + } + }, + "autoUpgradeMinorVersion": { + "type": "bool", + "metadata": { + "description": "Required. Indicates whether the extension should use a newer minor version if one is available at deployment time. Once deployed, however, the extension will not upgrade minor versions unless redeployed, even with this property set to true." + } + }, + "forceUpdateTag": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. How the extension handler should be forced to update even if the extension configuration has not changed." + } + }, + "settings": { + "type": "object", + "nullable": true, + "metadata": { + "description": "Optional. Any object that contains the extension specific settings." + } + }, + "protectedSettings": { + "type": "secureObject", + "nullable": true, + "metadata": { + "description": "Optional. Any object that contains the extension specific protected settings." + } + }, + "supressFailures": { + "type": "bool", + "defaultValue": false, + "metadata": { + "description": "Optional. Indicates whether failures stemming from the extension will be suppressed (Operational failures such as not connecting to the VM will not be suppressed regardless of this value). The default is false." + } + }, + "enableAutomaticUpgrade": { + "type": "bool", + "metadata": { + "description": "Required. Indicates whether the extension should be automatically upgraded by the platform if there is a newer version of the extension available." + } + }, + "tags": { + "type": "object", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.Compute/virtualMachines/extensions@2024-11-01#properties/tags" + }, + "description": "Optional. Tags of the resource." + }, + "nullable": true + }, + "protectedSettingsFromKeyVault": { + "type": "object", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.Compute/virtualMachines/extensions@2024-11-01#properties/properties/properties/protectedSettingsFromKeyVault" + }, + "description": "Optional. The extensions protected settings that are passed by reference, and consumed from key vault." + }, + "nullable": true + }, + "provisionAfterExtensions": { + "type": "array", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.Compute/virtualMachines/extensions@2024-11-01#properties/properties/properties/provisionAfterExtensions" + }, + "description": "Optional. Collection of extension names after which this extension needs to be provisioned." + }, + "nullable": true + } + }, + "resources": { + "virtualMachine": { + "existing": true, + "type": "Microsoft.Compute/virtualMachines", + "apiVersion": "2024-11-01", + "name": "[parameters('virtualMachineName')]" + }, + "extension": { + "type": "Microsoft.Compute/virtualMachines/extensions", + "apiVersion": "2024-11-01", + "name": "[format('{0}/{1}', parameters('virtualMachineName'), parameters('name'))]", + "location": "[parameters('location')]", + "tags": "[parameters('tags')]", + "properties": { + "publisher": "[parameters('publisher')]", + "type": "[parameters('type')]", + "typeHandlerVersion": "[parameters('typeHandlerVersion')]", + "autoUpgradeMinorVersion": "[parameters('autoUpgradeMinorVersion')]", + "enableAutomaticUpgrade": "[parameters('enableAutomaticUpgrade')]", + "forceUpdateTag": "[parameters('forceUpdateTag')]", + "settings": "[parameters('settings')]", + "protectedSettings": "[parameters('protectedSettings')]", + "suppressFailures": "[parameters('supressFailures')]", + "protectedSettingsFromKeyVault": "[parameters('protectedSettingsFromKeyVault')]", + "provisionAfterExtensions": "[parameters('provisionAfterExtensions')]" + } + } + }, + "outputs": { + "name": { + "type": "string", + "metadata": { + "description": "The name of the extension." + }, + "value": "[parameters('name')]" + }, + "resourceId": { + "type": "string", + "metadata": { + "description": "The resource ID of the extension." + }, + "value": "[resourceId('Microsoft.Compute/virtualMachines/extensions', parameters('virtualMachineName'), parameters('name'))]" + }, + "resourceGroupName": { + "type": "string", + "metadata": { + "description": "The name of the Resource Group the extension was created in." + }, + "value": "[resourceGroup().name]" + }, + "location": { + "type": "string", + "metadata": { + "description": "The location the resource was deployed into." + }, + "value": "[reference('extension', '2024-11-01', 'full').location]" + } + } + } + }, + "dependsOn": [ + "vm", + "vm_nvidiaGpuDriverWindowsExtension" + ] + }, + "vm_azureGuestConfigurationExtension": { + "condition": "[parameters('extensionGuestConfigurationExtension').enabled]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2022-09-01", + "name": "[format('{0}-VM-GuestConfiguration', uniqueString(deployment().name, parameters('location')))]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "virtualMachineName": { + "value": "[parameters('name')]" + }, + "name": "[if(coalesce(tryGet(parameters('extensionGuestConfigurationExtension'), 'name'), equals(parameters('osType'), 'Windows')), createObject('value', 'AzurePolicyforWindows'), createObject('value', 'AzurePolicyforLinux'))]", + "location": { + "value": "[parameters('location')]" + }, + "publisher": { + "value": "Microsoft.GuestConfiguration" + }, + "type": "[if(equals(parameters('osType'), 'Windows'), createObject('value', 'ConfigurationforWindows'), createObject('value', 'ConfigurationForLinux'))]", + "typeHandlerVersion": { + "value": "[coalesce(tryGet(parameters('extensionGuestConfigurationExtension'), 'typeHandlerVersion'), if(equals(parameters('osType'), 'Windows'), '1.0', '1.0'))]" + }, + "autoUpgradeMinorVersion": { + "value": "[coalesce(tryGet(parameters('extensionGuestConfigurationExtension'), 'autoUpgradeMinorVersion'), true())]" + }, + "enableAutomaticUpgrade": { + "value": "[coalesce(tryGet(parameters('extensionGuestConfigurationExtension'), 'enableAutomaticUpgrade'), true())]" + }, + "forceUpdateTag": { + "value": "[coalesce(tryGet(parameters('extensionGuestConfigurationExtension'), 'forceUpdateTag'), '1.0')]" + }, + "settings": { + "value": "[coalesce(tryGet(parameters('extensionGuestConfigurationExtension'), 'settings'), createObject())]" + }, + "supressFailures": { + "value": "[coalesce(tryGet(parameters('extensionGuestConfigurationExtension'), 'supressFailures'), false())]" + }, + "protectedSettings": { + "value": "[parameters('extensionGuestConfigurationExtensionProtectedSettings')]" + }, + "tags": { + "value": "[coalesce(tryGet(parameters('extensionGuestConfigurationExtension'), 'tags'), parameters('tags'))]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "languageVersion": "2.0", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.37.4.10188", + "templateHash": "13125609748815648088" + }, + "name": "Virtual Machine Extensions", + "description": "This module deploys a Virtual Machine Extension." + }, + "parameters": { + "virtualMachineName": { + "type": "string", + "metadata": { + "description": "Conditional. The name of the parent virtual machine that extension is provisioned for. Required if the template is used in a standalone deployment." + } + }, + "name": { + "type": "string", + "metadata": { + "description": "Required. The name of the virtual machine extension." + } + }, + "location": { + "type": "string", + "defaultValue": "[resourceGroup().location]", + "metadata": { + "description": "Optional. The location the extension is deployed to." + } + }, + "publisher": { + "type": "string", + "metadata": { + "description": "Required. The name of the extension handler publisher." + } + }, + "type": { + "type": "string", + "metadata": { + "description": "Required. Specifies the type of the extension; an example is \"CustomScriptExtension\"." + } + }, + "typeHandlerVersion": { + "type": "string", + "metadata": { + "description": "Required. Specifies the version of the script handler." + } + }, + "autoUpgradeMinorVersion": { + "type": "bool", + "metadata": { + "description": "Required. Indicates whether the extension should use a newer minor version if one is available at deployment time. Once deployed, however, the extension will not upgrade minor versions unless redeployed, even with this property set to true." + } + }, + "forceUpdateTag": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. How the extension handler should be forced to update even if the extension configuration has not changed." + } + }, + "settings": { + "type": "object", + "nullable": true, + "metadata": { + "description": "Optional. Any object that contains the extension specific settings." + } + }, + "protectedSettings": { + "type": "secureObject", + "nullable": true, + "metadata": { + "description": "Optional. Any object that contains the extension specific protected settings." + } + }, + "supressFailures": { + "type": "bool", + "defaultValue": false, + "metadata": { + "description": "Optional. Indicates whether failures stemming from the extension will be suppressed (Operational failures such as not connecting to the VM will not be suppressed regardless of this value). The default is false." + } + }, + "enableAutomaticUpgrade": { + "type": "bool", + "metadata": { + "description": "Required. Indicates whether the extension should be automatically upgraded by the platform if there is a newer version of the extension available." + } + }, + "tags": { + "type": "object", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.Compute/virtualMachines/extensions@2024-11-01#properties/tags" + }, + "description": "Optional. Tags of the resource." + }, + "nullable": true + }, + "protectedSettingsFromKeyVault": { + "type": "object", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.Compute/virtualMachines/extensions@2024-11-01#properties/properties/properties/protectedSettingsFromKeyVault" + }, + "description": "Optional. The extensions protected settings that are passed by reference, and consumed from key vault." + }, + "nullable": true + }, + "provisionAfterExtensions": { + "type": "array", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.Compute/virtualMachines/extensions@2024-11-01#properties/properties/properties/provisionAfterExtensions" + }, + "description": "Optional. Collection of extension names after which this extension needs to be provisioned." + }, + "nullable": true + } + }, + "resources": { + "virtualMachine": { + "existing": true, + "type": "Microsoft.Compute/virtualMachines", + "apiVersion": "2024-11-01", + "name": "[parameters('virtualMachineName')]" + }, + "extension": { + "type": "Microsoft.Compute/virtualMachines/extensions", + "apiVersion": "2024-11-01", + "name": "[format('{0}/{1}', parameters('virtualMachineName'), parameters('name'))]", + "location": "[parameters('location')]", + "tags": "[parameters('tags')]", + "properties": { + "publisher": "[parameters('publisher')]", + "type": "[parameters('type')]", + "typeHandlerVersion": "[parameters('typeHandlerVersion')]", + "autoUpgradeMinorVersion": "[parameters('autoUpgradeMinorVersion')]", + "enableAutomaticUpgrade": "[parameters('enableAutomaticUpgrade')]", + "forceUpdateTag": "[parameters('forceUpdateTag')]", + "settings": "[parameters('settings')]", + "protectedSettings": "[parameters('protectedSettings')]", + "suppressFailures": "[parameters('supressFailures')]", + "protectedSettingsFromKeyVault": "[parameters('protectedSettingsFromKeyVault')]", + "provisionAfterExtensions": "[parameters('provisionAfterExtensions')]" + } + } + }, + "outputs": { + "name": { + "type": "string", + "metadata": { + "description": "The name of the extension." + }, + "value": "[parameters('name')]" + }, + "resourceId": { + "type": "string", + "metadata": { + "description": "The resource ID of the extension." + }, + "value": "[resourceId('Microsoft.Compute/virtualMachines/extensions', parameters('virtualMachineName'), parameters('name'))]" + }, + "resourceGroupName": { + "type": "string", + "metadata": { + "description": "The name of the Resource Group the extension was created in." + }, + "value": "[resourceGroup().name]" + }, + "location": { + "type": "string", + "metadata": { + "description": "The location the resource was deployed into." + }, + "value": "[reference('extension', '2024-11-01', 'full').location]" + } + } + } + }, + "dependsOn": [ + "vm", + "vm_hostPoolRegistrationExtension" + ] + }, + "vm_backup": { + "condition": "[not(empty(parameters('backupVaultName')))]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2022-09-01", + "name": "[format('{0}-VM-Backup', uniqueString(deployment().name, parameters('location')))]", + "resourceGroup": "[parameters('backupVaultResourceGroup')]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "name": { + "value": "[format('vm;iaasvmcontainerv2;{0};{1}', resourceGroup().name, parameters('name'))]" + }, + "location": { + "value": "[parameters('location')]" + }, + "policyId": { + "value": "[resourceId(parameters('backupVaultResourceGroup'), 'Microsoft.RecoveryServices/vaults/backupPolicies', parameters('backupVaultName'), parameters('backupPolicyName'))]" + }, + "protectedItemType": { + "value": "Microsoft.Compute/virtualMachines" + }, + "protectionContainerName": { + "value": "[format('iaasvmcontainer;iaasvmcontainerv2;{0};{1}', resourceGroup().name, parameters('name'))]" + }, + "recoveryVaultName": { + "value": "[parameters('backupVaultName')]" + }, + "sourceResourceId": { + "value": "[resourceId('Microsoft.Compute/virtualMachines', parameters('name'))]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.37.4.10188", + "templateHash": "13700395772485726477" + }, + "name": "Recovery Service Vaults Protection Container Protected Item", + "description": "This module deploys a Recovery Services Vault Protection Container Protected Item." + }, + "parameters": { + "name": { + "type": "string", + "metadata": { + "description": "Required. Name of the resource." + } + }, + "protectionContainerName": { + "type": "string", + "metadata": { + "description": "Conditional. Name of the Azure Recovery Service Vault Protection Container. Required if the template is used in a standalone deployment." + } + }, + "recoveryVaultName": { + "type": "string", + "metadata": { + "description": "Conditional. The name of the parent Azure Recovery Service Vault. Required if the template is used in a standalone deployment." + } + }, + "location": { + "type": "string", + "defaultValue": "[resourceGroup().location]", + "metadata": { + "description": "Optional. Location for all resources." + } + }, + "protectedItemType": { + "type": "string", + "allowedValues": [ + "AzureFileShareProtectedItem", + "AzureVmWorkloadSAPAseDatabase", + "AzureVmWorkloadSAPHanaDatabase", + "AzureVmWorkloadSQLDatabase", + "DPMProtectedItem", + "GenericProtectedItem", + "MabFileFolderProtectedItem", + "Microsoft.ClassicCompute/virtualMachines", + "Microsoft.Compute/virtualMachines", + "Microsoft.Sql/servers/databases" + ], + "metadata": { + "description": "Required. The backup item type." + } + }, + "policyId": { + "type": "string", + "metadata": { + "description": "Required. ID of the backup policy with which this item is backed up." + } + }, + "sourceResourceId": { + "type": "string", + "metadata": { + "description": "Required. Resource ID of the resource to back up." + } + } + }, + "resources": [ + { + "type": "Microsoft.RecoveryServices/vaults/backupFabrics/protectionContainers/protectedItems", + "apiVersion": "2025-02-01", + "name": "[format('{0}/Azure/{1}/{2}', parameters('recoveryVaultName'), parameters('protectionContainerName'), parameters('name'))]", + "location": "[parameters('location')]", + "properties": { + "protectedItemType": "[parameters('protectedItemType')]", + "policyId": "[parameters('policyId')]", + "sourceResourceId": "[parameters('sourceResourceId')]" + } + } + ], + "outputs": { + "resourceGroupName": { + "type": "string", + "metadata": { + "description": "The name of the Resource Group the protected item was created in." + }, + "value": "[resourceGroup().name]" + }, + "resourceId": { + "type": "string", + "metadata": { + "description": "The resource ID of the protected item." + }, + "value": "[resourceId('Microsoft.RecoveryServices/vaults/backupFabrics/protectionContainers/protectedItems', split(format('{0}/Azure/{1}/{2}', parameters('recoveryVaultName'), parameters('protectionContainerName'), parameters('name')), '/')[0], split(format('{0}/Azure/{1}/{2}', parameters('recoveryVaultName'), parameters('protectionContainerName'), parameters('name')), '/')[1], split(format('{0}/Azure/{1}/{2}', parameters('recoveryVaultName'), parameters('protectionContainerName'), parameters('name')), '/')[2], split(format('{0}/Azure/{1}/{2}', parameters('recoveryVaultName'), parameters('protectionContainerName'), parameters('name')), '/')[3])]" + }, + "name": { + "type": "string", + "metadata": { + "description": "The Name of the protected item." + }, + "value": "[format('{0}/Azure/{1}/{2}', parameters('recoveryVaultName'), parameters('protectionContainerName'), parameters('name'))]" + } + } + } + }, + "dependsOn": [ + "vm", + "vm_azureGuestConfigurationExtension" + ] + } + }, + "outputs": { + "name": { + "type": "string", + "metadata": { + "description": "The name of the VM." + }, + "value": "[parameters('name')]" + }, + "resourceId": { + "type": "string", + "metadata": { + "description": "The resource ID of the VM." + }, + "value": "[resourceId('Microsoft.Compute/virtualMachines', parameters('name'))]" + }, + "resourceGroupName": { + "type": "string", + "metadata": { + "description": "The name of the resource group the VM was created in." + }, + "value": "[resourceGroup().name]" + }, + "systemAssignedMIPrincipalId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "The principal ID of the system assigned identity." + }, + "value": "[tryGet(tryGet(reference('vm', '2024-07-01', 'full'), 'identity'), 'principalId')]" + }, + "location": { + "type": "string", + "metadata": { + "description": "The location the resource was deployed into." + }, + "value": "[reference('vm', '2024-07-01', 'full').location]" + }, + "nicConfigurations": { + "type": "array", + "items": { + "$ref": "#/definitions/nicConfigurationOutputType" + }, + "metadata": { + "description": "The list of NIC configurations of the virtual machine." + }, + "copy": { + "count": "[length(parameters('nicConfigurations'))]", + "input": { + "name": "[reference(format('vm_nic[{0}]', copyIndex())).outputs.name.value]", + "ipConfigurations": "[reference(format('vm_nic[{0}]', copyIndex())).outputs.ipConfigurations.value]" + } + } + } + } + } + }, + "dependsOn": [ + "userAssignedIdentity", + "virtualNetwork" + ] } }, "outputs": { diff --git a/infra/modules/virtualNetwork.bicep b/infra/modules/virtualNetwork.bicep index d4ec64795..b11a6fb61 100644 --- a/infra/modules/virtualNetwork.bicep +++ b/infra/modules/virtualNetwork.bicep @@ -102,6 +102,14 @@ var coreSubnets = [ ] // Optional Bastion and Jumpbox subnets (only deployed when needed for VM administration) +// VM Size Notes: +// 1 B-series VMs (like Standard_B2ms) do not support accelerated networking. +// 2 Pick a VM size that supports accelerated networking + Premium SSD (the usual jump-box candidates): +// Standard_D2s_v5 (2 vCPU, 8 GiB RAM, Premium SSD/v2/Ultra) // DEFAULT - current-gen Intel, broad regional availability. +// Standard_D2as_v5 (2 vCPU, 8 GiB RAM, Premium SSD/Ultra) // AMD alternative, typically ~15% cheaper. +// Standard_D2s_v4 (2 vCPU, 8 GiB RAM, Premium SSD) // Previous gen, also broadly available. +// Standard_DS2_v2 (2 vCPU, 7 GiB RAM, Premium SSD) // Legacy SKU, being retired from some regions - avoid for new deployments. +// 3 A-series (Av2) is NOT suitable: no Premium SSD support, no accelerated networking. var bastionSubnets = deployBastionAndJumpbox ? [ { name: 'AzureBastionSubnet' From 7b83c0c1272cf915150213b0a5d84d2da7f5240b Mon Sep 17 00:00:00 2001 From: v-maddukuriy Date: Fri, 20 Mar 2026 18:50:24 +0530 Subject: [PATCH 14/72] Add Bastion and Jumpbox VM using existing VNet configuration. --- docs/CustomizingAzdParameters.md | 12 +- infra/main.bicep | 139 +- infra/main.json | 58430 ++++++++++++++------------- infra/main.parameters.json | 6 - infra/main.waf.parameters.json | 15 +- infra/modules/virtualNetwork.bicep | 31 +- 6 files changed, 30334 insertions(+), 28299 deletions(-) diff --git a/docs/CustomizingAzdParameters.md b/docs/CustomizingAzdParameters.md index f9a7f729d..80ea0d8f1 100644 --- a/docs/CustomizingAzdParameters.md +++ b/docs/CustomizingAzdParameters.md @@ -18,12 +18,16 @@ By default this template will use the environment name as the prefix to prevent | `imageModelChoice` | string | `gpt-image-1-mini` | Image model to deploy (allowed: `gpt-image-1-mini`, `gpt-image-1.5`, `none`). | | `imageModelCapacity` | integer | `1` | Sets the image model deployment capacity in RPM (minimum: `1`). | | `azureOpenaiAPIVersion` | string | `2025-01-01-preview` | Specifies the API version for Azure OpenAI service. | -| `AZURE_ENV_OPENAI_LOCATION` | string | `` | Sets the Azure region for OpenAI resource deployment. | +| `AZURE_ENV_OPENAI_LOCATION` | string | `` | Sets the Azure region for OpenAI resource deployment. Allowed: `australiaeast`, `canadaeast`, `eastus2`, `japaneast`, `koreacentral`, `polandcentral`, `swedencentral`, `switzerlandnorth`, `uaenorth`, `uksouth`, `westus3`. | | `AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID` | string | `""` | Reuses an existing Log Analytics Workspace instead of creating a new one. | | `AZURE_EXISTING_AI_PROJECT_RESOURCE_ID`| string | `""` | Reuses an existing AI Foundry Project instead of creating a new one. | -| `AZURE_ENV_VM_SIZE` | string | `Standard_D2s_v5` | Overrides the jumpbox VM size (private networking only). Must support accelerated networking. | -| `AZURE_ENV_VM_ADMIN_USERNAME` | string | `JumpboxAdminUser` | Sets the jumpbox VM admin username (private networking only). | -| `AZURE_ENV_VM_ADMIN_PASSWORD` | string | *(generated)* | Sets the jumpbox VM admin password (private networking only). | +| `enableMonitoring` | boolean | `false` | Enable Log Analytics and Application Insights (WAF-aligned). | +| `enableScalability` | boolean | `false` | Enable auto-scaling and higher SKUs (WAF-aligned). | +| `enableRedundancy` | boolean | `false` | Enable zone redundancy and geo-replication (WAF-aligned). | +| `enablePrivateNetworking` | boolean | `false` | Enable VNet integration and private endpoints (WAF-aligned). | +| `AZURE_ENV_VM_SIZE` | string | `""` | Overrides the jumpbox VM size (private networking only). Must support accelerated networking and Premium SSD. | +| `AZURE_ENV_VM_ADMIN_USERNAME` | string | `""` | Sets the jumpbox VM admin username (private networking only). | +| `AZURE_ENV_VM_ADMIN_PASSWORD` | string | `""` | Sets the jumpbox VM admin password (private networking only). | | `ACR_NAME` | string | `contentgencontainerreg` | Sets the existing Azure Container Registry name (without `.azurecr.io`). | | `IMAGE_TAG` | string | `latest` | Sets the container image tag (e.g., `latest`, `dev`, `hotfix`). | diff --git a/infra/main.bicep b/infra/main.bicep index d2df507bd..dec2e9bf1 100644 --- a/infra/main.bicep +++ b/infra/main.bicep @@ -107,9 +107,6 @@ param existingLogAnalyticsWorkspaceId string = '' @description('Optional. Resource ID of an existing Foundry project.') param azureExistingAIProjectResourceId string = '' -@description('Optional. Deploy Azure Bastion and Jumpbox VM for private network administration.') -param deployBastionAndJumpbox bool = false - @description('Optional. Jumpbox VM size. Must support accelerated networking and Premium SSD.') param vmSize string = '' @@ -374,16 +371,92 @@ module userAssignedIdentity 'br/public:avm/res/managed-identity/user-assigned-id module virtualNetwork 'modules/virtualNetwork.bicep' = if (enablePrivateNetworking) { name: take('module.virtualNetwork.${solutionSuffix}', 64) params: { - vnetName: 'vnet-${solutionSuffix}' - vnetLocation: solutionLocation - vnetAddressPrefixes: ['10.0.0.0/20'] + name: 'vnet-${solutionSuffix}' + addressPrefixes: ['10.0.0.0/20'] // 4096 addresses (enough for 8 /23 subnets or 16 /24) + location: location tags: tags logAnalyticsWorkspaceId: logAnalyticsWorkspaceResourceId - enableTelemetry: enableTelemetry resourceSuffix: solutionSuffix - deployBastionAndJumpbox: deployBastionAndJumpbox + enableTelemetry: enableTelemetry + } +} + +// Azure Bastion Host +var bastionHostName = 'bas-${solutionSuffix}' +module bastionHost 'br/public:avm/res/network/bastion-host:0.8.2' = if (enablePrivateNetworking) { + name: take('avm.res.network.bastion-host.${bastionHostName}', 64) + params: { + name: bastionHostName + skuName: 'Standard' + location: location + virtualNetworkResourceId: virtualNetwork!.outputs.resourceId + diagnosticSettings: [ + { + name: 'bastionDiagnostics' + workspaceResourceId: logAnalyticsWorkspaceResourceId + logCategoriesAndGroups: [ + { + categoryGroup: 'allLogs' + enabled: true + } + ] + } + ] + tags: tags + enableTelemetry: enableTelemetry + publicIPAddressObject: { + name: 'pip-${bastionHostName}' + } + } +} + +// Jumpbox Virtual Machine +var jumpboxVmName = take('vm-jumpbox-${solutionSuffix}', 15) +module jumpboxVM 'br/public:avm/res/compute/virtual-machine:0.21.0' = if (enablePrivateNetworking) { + name: take('avm.res.compute.virtual-machine.${jumpboxVmName}', 64) + params: { + name: take(jumpboxVmName, 15) + enableTelemetry: enableTelemetry + computerName: take(jumpboxVmName, 15) + osType: 'Windows' + vmSize: empty(vmSize) ? 'Standard_D2s_v5' : vmSize + adminUsername: empty(vmAdminUsername) ? 'JumpboxAdminUser' : vmAdminUsername + adminPassword: empty(vmAdminPassword) ? 'JumpboxAdminP@ssw0rd1234!' : vmAdminPassword + managedIdentities: { + userAssignedResourceIds: [ + userAssignedIdentity.outputs.resourceId + ] + } + availabilityZone: 1 + imageReference: { + publisher: 'microsoft-dsvm' + offer: 'dsvm-win-2022' + sku: 'winserver-2022' + version: 'latest' + } + nicConfigurations: [ + { + name: 'nic-${jumpboxVmName}' + enableAcceleratedNetworking: true + ipConfigurations: [ + { + name: 'ipconfig01' + subnetResourceId: virtualNetwork!.outputs.jumpboxSubnetResourceId + } + ] + } + ] + osDisk: { + caching: 'ReadWrite' + diskSizeGB: 128 + managedDisk: { + storageAccountType: 'Premium_LRS' + } + } + encryptionAtHost: false // Some Azure subscriptions do not support encryption at host + location: solutionLocation + tags: tags } - dependsOn: enableMonitoring ? [logAnalyticsWorkspace] : [] } // ========== Private DNS Zones ========== // @@ -883,54 +956,6 @@ module containerInstance 'modules/container-instance.bicep' = { } } -// ========== Jumpbox VM ========== // -var jumpboxVmName = 'vm-jumpbox-${solutionSuffix}' -module jumpboxVM 'br/public:avm/res/compute/virtual-machine:0.20.0' = if (enablePrivateNetworking && deployBastionAndJumpbox) { - name: take('avm.res.compute.virtual-machine.${jumpboxVmName}', 64) - params: { - name: take(jumpboxVmName, 15) - enableTelemetry: enableTelemetry - computerName: take(jumpboxVmName, 15) - osType: 'Windows' - vmSize: empty(vmSize) ? 'Standard_D2s_v5' : vmSize - adminUsername: empty(vmAdminUsername) ? 'JumpboxAdminUser' : vmAdminUsername - adminPassword: empty(vmAdminPassword) ? 'JumpboxAdminP@ssw0rd1234!' : vmAdminPassword - managedIdentities: { - userAssignedResourceIds: [ - userAssignedIdentity.outputs.resourceId - ] - } - availabilityZone: 1 - imageReference: { - publisher: 'MicrosoftWindowsDesktop' - offer: 'windows-11' - sku: 'win11-24h2-pro' - version: 'latest' - } - nicConfigurations: [ - { - name: 'nic-${jumpboxVmName}' - enableAcceleratedNetworking: true - ipConfigurations: [ - { - name: 'ipconfig01' - subnetResourceId: virtualNetwork!.outputs.jumpboxSubnetResourceId - } - ] - } - ] - osDisk: { - caching: 'ReadWrite' - diskSizeGB: 128 - managedDisk: { - storageAccountType: 'Premium_LRS' - } - } - location: solutionLocation - tags: tags - } -} - // ========== Outputs ========== // @description('Contains App Service Name') output APP_SERVICE_NAME string = webSite.outputs.name diff --git a/infra/main.json b/infra/main.json index 9cb2487b1..1eede9c3f 100644 --- a/infra/main.json +++ b/infra/main.json @@ -6,7 +6,7 @@ "_generator": { "name": "bicep", "version": "0.39.26.7824", - "templateHash": "16548727235263926299" + "templateHash": "13582527567885599968" }, "name": "Intelligent Content Generation Accelerator", "description": "Solution Accelerator for multimodal marketing content generation using Microsoft Agent Framework.\n" @@ -346,6 +346,8 @@ "logAnalyticsWorkspaceResourceName": "[format('log-{0}', variables('solutionSuffix'))]", "applicationInsightsResourceName": "[format('appi-{0}', variables('solutionSuffix'))]", "userAssignedIdentityResourceName": "[format('id-{0}', variables('solutionSuffix'))]", + "bastionHostName": "[format('bas-{0}', variables('solutionSuffix'))]", + "jumpboxVmName": "[take(format('vm-jumpbox-{0}', variables('solutionSuffix')), 15)]", "privateDnsZones": [ "privatelink.cognitiveservices.azure.com", "privatelink.openai.azure.com", @@ -371,8 +373,7 @@ "aciPrivateIpFallback": "10.0.4.4", "aciPublicFqdnFallback": "[format('{0}.{1}.azurecontainer.io', variables('containerInstanceName'), variables('solutionLocation'))]", "aciBackendUrl": "[if(parameters('enablePrivateNetworking'), format('http://{0}:8000', variables('aciPrivateIpFallback')), format('http://{0}:8000', variables('aciPublicFqdnFallback')))]", - "containerInstanceName": "[format('aci-{0}', variables('solutionSuffix'))]", - "jumpboxVmName": "[format('vm-jumpbox-{0}', variables('solutionSuffix'))]" + "containerInstanceName": "[format('aci-{0}', variables('solutionSuffix'))]" }, "resources": { "avmTelemetry": { @@ -7398,17 +7399,11 @@ "logAnalyticsWorkspace" ] }, - "avmPrivateDnsZones": { - "copy": { - "name": "avmPrivateDnsZones", - "count": "[length(variables('privateDnsZones'))]", - "mode": "serial", - "batchSize": 5 - }, + "bastionHost": { "condition": "[parameters('enablePrivateNetworking')]", "type": "Microsoft.Resources/deployments", "apiVersion": "2025-04-01", - "name": "[take(format('avm.res.network.private-dns-zone.{0}', replace(variables('privateDnsZones')[copyIndex()], '.', '-')), 64)]", + "name": "[take(format('avm.res.network.bastion-host.{0}', variables('bastionHostName')), 64)]", "properties": { "expressionEvaluationOptions": { "scope": "inner" @@ -7416,21 +7411,41 @@ "mode": "Incremental", "parameters": { "name": { - "value": "[variables('privateDnsZones')[copyIndex()]]" + "value": "[variables('bastionHostName')]" }, - "tags": { - "value": "[parameters('tags')]" + "skuName": { + "value": "Standard" }, - "enableTelemetry": { - "value": "[parameters('enableTelemetry')]" + "location": { + "value": "[parameters('location')]" }, - "virtualNetworkLinks": { + "virtualNetworkResourceId": { + "value": "[reference('virtualNetwork').outputs.resourceId.value]" + }, + "diagnosticSettings": { "value": [ { - "virtualNetworkResourceId": "[if(parameters('enablePrivateNetworking'), reference('virtualNetwork').outputs.resourceId.value, '')]", - "registrationEnabled": false + "name": "bastionDiagnostics", + "workspaceResourceId": "[if(variables('useExistingLogAnalytics'), parameters('existingLogAnalyticsWorkspaceId'), if(parameters('enableMonitoring'), reference('logAnalyticsWorkspace').outputs.resourceId.value, ''))]", + "logCategoriesAndGroups": [ + { + "categoryGroup": "allLogs", + "enabled": true + } + ] } ] + }, + "tags": { + "value": "[parameters('tags')]" + }, + "enableTelemetry": { + "value": "[parameters('enableTelemetry')]" + }, + "publicIPAddressObject": { + "value": { + "name": "[format('pip-{0}', variables('bastionHostName'))]" + } } }, "template": { @@ -7440,245 +7455,119 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.37.4.10188", - "templateHash": "17921343070314002065" + "version": "0.39.26.7824", + "templateHash": "7741601918225805390" }, - "name": "Private DNS Zones", - "description": "This module deploys a Private DNS zone." + "name": "Bastion Hosts", + "description": "This module deploys a Bastion Host." }, "definitions": { - "aType": { + "publicIPAddressObjectType": { "type": "object", "properties": { "name": { "type": "string", "metadata": { - "description": "Required. The name of the record." + "description": "Required. The name of the Public IP Address." } }, - "metadata": { - "type": "object", + "publicIpPrefixResourceId": { + "type": "string", + "nullable": true, "metadata": { - "__bicep_resource_derived_type!": { - "source": "Microsoft.Network/privateDnsZones/A@2024-06-01#properties/properties/properties/metadata" - }, - "description": "Optional. The metadata of the record." - }, - "nullable": true + "description": "Optional. Resource ID of the Public IP Prefix object. This is only needed if you want your Public IPs created in a PIP Prefix." + } }, - "ttl": { - "type": "int", + "publicIPAllocationMethod": { + "type": "string", + "allowedValues": [ + "Dynamic", + "Static" + ], "nullable": true, "metadata": { - "description": "Optional. The TTL of the record." + "description": "Optional. The public IP address allocation method." } }, - "roleAssignments": { + "availabilityZones": { "type": "array", "items": { - "$ref": "#/definitions/roleAssignmentType" + "type": "int" }, "nullable": true, "metadata": { - "description": "Optional. Array of role assignments to create." + "description": "Optional. A list of availability zones denoting the IP allocated for the resource needs to come from." } }, - "aRecords": { - "type": "array", - "metadata": { - "__bicep_resource_derived_type!": { - "source": "Microsoft.Network/privateDnsZones/A@2024-06-01#properties/properties/properties/aRecords" - }, - "description": "Optional. The list of A records in the record set." - }, - "nullable": true - } - }, - "metadata": { - "__bicep_export!": true, - "description": "The type for the A record." - } - }, - "aaaaType": { - "type": "object", - "properties": { - "name": { + "publicIPAddressVersion": { "type": "string", + "allowedValues": [ + "IPv4", + "IPv6" + ], + "nullable": true, "metadata": { - "description": "Required. The name of the record." + "description": "Optional. IP address version." } }, - "metadata": { - "type": "object", - "metadata": { - "__bicep_resource_derived_type!": { - "source": "Microsoft.Network/privateDnsZones/AAAA@2024-06-01#properties/properties/properties/metadata" - }, - "description": "Optional. The metadata of the record." - }, - "nullable": true - }, - "ttl": { - "type": "int", + "dnsSettings": { + "$ref": "#/definitions/dnsSettingsType", "nullable": true, "metadata": { - "description": "Optional. The TTL of the record." + "description": "Optional. The DNS settings of the public IP address." } }, - "roleAssignments": { + "ipTags": { "type": "array", "items": { - "$ref": "#/definitions/roleAssignmentType" + "$ref": "#/definitions/ipTagType" }, "nullable": true, "metadata": { - "description": "Optional. Array of role assignments to create." - } - }, - "aaaaRecords": { - "type": "array", - "metadata": { - "__bicep_resource_derived_type!": { - "source": "Microsoft.Network/privateDnsZones/AAAA@2024-06-01#properties/properties/properties/aaaaRecords" - }, - "description": "Optional. The list of AAAA records in the record set." - }, - "nullable": true - } - }, - "metadata": { - "__bicep_export!": true, - "description": "The type for the AAAA record." - } - }, - "cnameType": { - "type": "object", - "properties": { - "name": { - "type": "string", - "metadata": { - "description": "Required. The name of the record." + "description": "Optional. The list of tags associated with the public IP address." } }, - "metadata": { - "type": "object", - "metadata": { - "__bicep_resource_derived_type!": { - "source": "Microsoft.Network/privateDnsZones/CNAME@2024-06-01#properties/properties/properties/metadata" - }, - "description": "Optional. The metadata of the record." - }, - "nullable": true - }, - "ttl": { - "type": "int", + "lock": { + "$ref": "#/definitions/lockType", "nullable": true, "metadata": { - "description": "Optional. The TTL of the record." + "description": "Optional. The lock settings of the service." } }, - "roleAssignments": { - "type": "array", - "items": { - "$ref": "#/definitions/roleAssignmentType" - }, + "skuName": { + "type": "string", + "allowedValues": [ + "Basic", + "Standard" + ], "nullable": true, "metadata": { - "description": "Optional. Array of role assignments to create." + "description": "Optional. Name of a public IP address SKU." } }, - "cnameRecord": { - "type": "object", - "metadata": { - "__bicep_resource_derived_type!": { - "source": "Microsoft.Network/privateDnsZones/CNAME@2024-06-01#properties/properties/properties/cnameRecord" - }, - "description": "Optional. The CNAME record in the record set." - }, - "nullable": true - } - }, - "metadata": { - "__bicep_export!": true, - "description": "The type for the CNAME record." - } - }, - "mxType": { - "type": "object", - "properties": { - "name": { + "skuTier": { "type": "string", - "metadata": { - "description": "Required. The name of the record." - } - }, - "metadata": { - "type": "object", - "metadata": { - "__bicep_resource_derived_type!": { - "source": "Microsoft.Network/privateDnsZones/MX@2024-06-01#properties/properties/properties/metadata" - }, - "description": "Optional. The metadata of the record." - }, - "nullable": true - }, - "ttl": { - "type": "int", + "allowedValues": [ + "Global", + "Regional" + ], "nullable": true, "metadata": { - "description": "Optional. The TTL of the record." + "description": "Optional. Tier of a public IP address SKU." } }, - "roleAssignments": { - "type": "array", - "items": { - "$ref": "#/definitions/roleAssignmentType" - }, + "ddosSettings": { + "$ref": "#/definitions/ddosSettingsType", "nullable": true, "metadata": { - "description": "Optional. Array of role assignments to create." + "description": "Optional. The DDoS protection plan configuration associated with the public IP address." } }, - "mxRecords": { - "type": "array", - "metadata": { - "__bicep_resource_derived_type!": { - "source": "Microsoft.Network/privateDnsZones/MX@2024-06-01#properties/properties/properties/mxRecords" - }, - "description": "Optional. The list of MX records in the record set." - }, - "nullable": true - } - }, - "metadata": { - "__bicep_export!": true, - "description": "The type for the MX record." - } - }, - "ptrType": { - "type": "object", - "properties": { - "name": { + "location": { "type": "string", - "metadata": { - "description": "Required. The name of the record." - } - }, - "metadata": { - "type": "object", - "metadata": { - "__bicep_resource_derived_type!": { - "source": "Microsoft.Network/privateDnsZones/PTR@2024-06-01#properties/properties/properties/metadata" - }, - "description": "Optional. The metadata of the record." - }, - "nullable": true - }, - "ttl": { - "type": "int", "nullable": true, "metadata": { - "description": "Optional. The TTL of the record." + "description": "Optional. Location for the Public IP resource." } }, "roleAssignments": { @@ -7688,256 +7577,383 @@ }, "nullable": true, "metadata": { - "description": "Optional. Array of role assignments to create." + "description": "Optional. Array of role assignments to create for the Public IP resource." } }, - "ptrRecords": { - "type": "array", + "enableTelemetry": { + "type": "bool", + "nullable": true, "metadata": { - "__bicep_resource_derived_type!": { - "source": "Microsoft.Network/privateDnsZones/PTR@2024-06-01#properties/properties/properties/ptrRecords" - }, - "description": "Optional. The list of PTR records in the record set." - }, - "nullable": true - } - }, - "metadata": { - "__bicep_export!": true, - "description": "The type for the PTR record." - } - }, - "soaType": { - "type": "object", - "properties": { - "name": { - "type": "string", + "description": "Optional. Enable or disable usage telemetry for the Public IP module." + } + }, + "idleTimeoutInMinutes": { + "type": "int", + "nullable": true, "metadata": { - "description": "Required. The name of the record." + "description": "Optional. Idle timeout in minutes for the Public IP resource." } }, - "metadata": { + "tags": { "type": "object", "metadata": { "__bicep_resource_derived_type!": { - "source": "Microsoft.Network/privateDnsZones/SOA@2024-06-01#properties/properties/properties/metadata" + "source": "Microsoft.Network/publicIPAddresses@2024-07-01#properties/tags" }, - "description": "Optional. The metadata of the record." + "description": "Optional. Tags to apply to the Public IP resource." }, "nullable": true }, - "ttl": { - "type": "int", - "nullable": true, - "metadata": { - "description": "Optional. The TTL of the record." - } - }, - "roleAssignments": { + "diagnosticSettings": { "type": "array", "items": { - "$ref": "#/definitions/roleAssignmentType" + "$ref": "#/definitions/diagnosticSettingFullType" }, "nullable": true, "metadata": { - "description": "Optional. Array of role assignments to create." + "description": "Optional. Diagnostic settings for the Public IP resource." } - }, - "soaRecord": { - "type": "object", - "metadata": { - "__bicep_resource_derived_type!": { - "source": "Microsoft.Network/privateDnsZones/SOA@2024-06-01#properties/properties/properties/soaRecord" - }, - "description": "Optional. The SOA record in the record set." - }, - "nullable": true } }, "metadata": { "__bicep_export!": true, - "description": "The type for the SOA record." + "description": "The type for the properties of the Public IP to create and be used by Azure Bastion, if no existing public IP was provided." } }, - "srvType": { + "ddosSettingsType": { "type": "object", "properties": { - "name": { - "type": "string", - "metadata": { - "description": "Required. The name of the record." - } - }, - "metadata": { + "ddosProtectionPlan": { "type": "object", - "metadata": { - "__bicep_resource_derived_type!": { - "source": "Microsoft.Network/privateDnsZones/SRV@2024-06-01#properties/properties/properties/metadata" - }, - "description": "Optional. The metadata of the record." + "properties": { + "id": { + "type": "string", + "metadata": { + "description": "Required. The resource ID of the DDOS protection plan associated with the public IP address." + } + } }, - "nullable": true - }, - "ttl": { - "type": "int", "nullable": true, "metadata": { - "description": "Optional. The TTL of the record." + "description": "Optional. The DDoS protection plan associated with the public IP address." } }, - "roleAssignments": { - "type": "array", - "items": { - "$ref": "#/definitions/roleAssignmentType" - }, - "nullable": true, + "protectionMode": { + "type": "string", + "allowedValues": [ + "Enabled" + ], "metadata": { - "description": "Optional. Array of role assignments to create." + "description": "Required. The DDoS protection policy customizations." } - }, - "srvRecords": { - "type": "array", - "metadata": { - "__bicep_resource_derived_type!": { - "source": "Microsoft.Network/privateDnsZones/SRV@2024-06-01#properties/properties/properties/srvRecords" - }, - "description": "Optional. The list of SRV records in the record set." - }, - "nullable": true } }, "metadata": { - "__bicep_export!": true, - "description": "The type for the SRV record." + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/res/network/public-ip-address:0.10.0" + } } }, - "txtType": { + "diagnosticSettingFullType": { "type": "object", "properties": { "name": { "type": "string", + "nullable": true, "metadata": { - "description": "Required. The name of the record." + "description": "Optional. The name of the diagnostic setting." } }, - "metadata": { - "type": "object", - "metadata": { - "__bicep_resource_derived_type!": { - "source": "Microsoft.Network/privateDnsZones/TXT@2024-06-01#properties/properties/properties/metadata" - }, - "description": "Optional. The metadata of the record." + "logCategoriesAndGroups": { + "type": "array", + "items": { + "type": "object", + "properties": { + "category": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Name of a Diagnostic Log category for a resource type this setting is applied to. Set the specific logs to collect here." + } + }, + "categoryGroup": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Name of a Diagnostic Log category group for a resource type this setting is applied to. Set to `allLogs` to collect all logs." + } + }, + "enabled": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. Enable or disable the category explicitly. Default is `true`." + } + } + } }, - "nullable": true - }, - "ttl": { - "type": "int", "nullable": true, "metadata": { - "description": "Optional. The TTL of the record." + "description": "Optional. The name of logs that will be streamed. \"allLogs\" includes all possible logs for the resource. Set to `[]` to disable log collection." } }, - "roleAssignments": { + "metricCategories": { "type": "array", "items": { - "$ref": "#/definitions/roleAssignmentType" + "type": "object", + "properties": { + "category": { + "type": "string", + "metadata": { + "description": "Required. Name of a Diagnostic Metric category for a resource type this setting is applied to. Set to `AllMetrics` to collect all metrics." + } + }, + "enabled": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. Enable or disable the category explicitly. Default is `true`." + } + } + } }, "nullable": true, "metadata": { - "description": "Optional. Array of role assignments to create." + "description": "Optional. The name of metrics that will be streamed. \"allMetrics\" includes all possible metrics for the resource. Set to `[]` to disable metric collection." } }, - "txtRecords": { - "type": "array", - "metadata": { - "__bicep_resource_derived_type!": { - "source": "Microsoft.Network/privateDnsZones/TXT@2024-06-01#properties/properties/properties/txtRecords" - }, - "description": "Optional. The list of TXT records in the record set." - }, - "nullable": true - } - }, - "metadata": { - "__bicep_export!": true, - "description": "The type for the TXT record." - } - }, - "virtualNetworkLinkType": { - "type": "object", - "properties": { - "name": { + "logAnalyticsDestinationType": { "type": "string", + "allowedValues": [ + "AzureDiagnostics", + "Dedicated" + ], "nullable": true, - "minLength": 1, - "maxLength": 80, "metadata": { - "description": "Optional. The resource name." + "description": "Optional. A string indicating whether the export to Log Analytics should use the default destination type, i.e. AzureDiagnostics, or use a destination type." } }, - "virtualNetworkResourceId": { + "workspaceResourceId": { "type": "string", + "nullable": true, "metadata": { - "description": "Required. The resource ID of the virtual network to link." + "description": "Optional. Resource ID of the diagnostic log analytics workspace. For security reasons, it is recommended to set diagnostic settings to send data to either storage account, log analytics workspace or event hub." } }, - "location": { + "storageAccountResourceId": { "type": "string", "nullable": true, "metadata": { - "description": "Optional. The Azure Region where the resource lives." + "description": "Optional. Resource ID of the diagnostic storage account. For security reasons, it is recommended to set diagnostic settings to send data to either storage account, log analytics workspace or event hub." } }, - "registrationEnabled": { - "type": "bool", + "eventHubAuthorizationRuleResourceId": { + "type": "string", "nullable": true, "metadata": { - "description": "Optional. Is auto-registration of virtual machine records in the virtual network in the Private DNS zone enabled?." + "description": "Optional. Resource ID of the diagnostic event hub authorization rule for the Event Hubs namespace in which the event hub should be created or streamed to." } }, - "tags": { - "type": "object", + "eventHubName": { + "type": "string", + "nullable": true, "metadata": { - "__bicep_resource_derived_type!": { - "source": "Microsoft.Network/privateDnsZones/virtualNetworkLinks@2024-06-01#properties/tags" - }, - "description": "Optional. Resource tags." - }, - "nullable": true + "description": "Optional. Name of the diagnostic event hub within the namespace to which logs are streamed. Without this, an event hub is created for each log category. For security reasons, it is recommended to set diagnostic settings to send data to either storage account, log analytics workspace or event hub." + } }, - "resolutionPolicy": { + "marketplacePartnerResourceId": { "type": "string", - "allowedValues": [ - "Default", - "NxDomainRedirect" - ], "nullable": true, "metadata": { - "description": "Optional. The resolution type of the private-dns-zone fallback machanism." + "description": "Optional. The full ARM resource ID of the Marketplace resource to which you would like to send Diagnostic Logs." } } }, "metadata": { - "__bicep_export!": true, - "description": "The type for the virtual network link." + "description": "An AVM-aligned type for a diagnostic setting. To be used if both logs & metrics are supported by the resource provider.", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.1" + } } }, - "lockType": { + "diagnosticSettingLogsOnlyType": { "type": "object", "properties": { "name": { "type": "string", "nullable": true, "metadata": { - "description": "Optional. Specify the name of lock." + "description": "Optional. The name of diagnostic setting." } }, - "kind": { - "type": "string", - "allowedValues": [ - "CanNotDelete", - "None", - "ReadOnly" + "logCategoriesAndGroups": { + "type": "array", + "items": { + "type": "object", + "properties": { + "category": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Name of a Diagnostic Log category for a resource type this setting is applied to. Set the specific logs to collect here." + } + }, + "categoryGroup": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Name of a Diagnostic Log category group for a resource type this setting is applied to. Set to `allLogs` to collect all logs." + } + }, + "enabled": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. Enable or disable the category explicitly. Default is `true`." + } + } + } + }, + "nullable": true, + "metadata": { + "description": "Optional. The name of logs that will be streamed. \"allLogs\" includes all possible logs for the resource. Set to `[]` to disable log collection." + } + }, + "logAnalyticsDestinationType": { + "type": "string", + "allowedValues": [ + "AzureDiagnostics", + "Dedicated" + ], + "nullable": true, + "metadata": { + "description": "Optional. A string indicating whether the export to Log Analytics should use the default destination type, i.e. AzureDiagnostics, or use a destination type." + } + }, + "workspaceResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Resource ID of the diagnostic log analytics workspace. For security reasons, it is recommended to set diagnostic settings to send data to either storage account, log analytics workspace or event hub." + } + }, + "storageAccountResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Resource ID of the diagnostic storage account. For security reasons, it is recommended to set diagnostic settings to send data to either storage account, log analytics workspace or event hub." + } + }, + "eventHubAuthorizationRuleResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Resource ID of the diagnostic event hub authorization rule for the Event Hubs namespace in which the event hub should be created or streamed to." + } + }, + "eventHubName": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Name of the diagnostic event hub within the namespace to which logs are streamed. Without this, an event hub is created for each log category. For security reasons, it is recommended to set diagnostic settings to send data to either storage account, log analytics workspace or event hub." + } + }, + "marketplacePartnerResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The full ARM resource ID of the Marketplace resource to which you would like to send Diagnostic Logs." + } + } + }, + "metadata": { + "description": "An AVM-aligned type for a diagnostic setting. To be used if only logs are supported by the resource provider.", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.1" + } + } + }, + "dnsSettingsType": { + "type": "object", + "properties": { + "domainNameLabel": { + "type": "string", + "metadata": { + "description": "Required. The domain name label. The concatenation of the domain name label and the regionalized DNS zone make up the fully qualified domain name associated with the public IP address. If a domain name label is specified, an A DNS record is created for the public IP in the Microsoft Azure DNS system." + } + }, + "domainNameLabelScope": { + "type": "string", + "allowedValues": [ + "NoReuse", + "ResourceGroupReuse", + "SubscriptionReuse", + "TenantReuse" + ], + "nullable": true, + "metadata": { + "description": "Optional. The domain name label scope. If a domain name label and a domain name label scope are specified, an A DNS record is created for the public IP in the Microsoft Azure DNS system with a hashed value includes in FQDN." + } + }, + "fqdn": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The Fully Qualified Domain Name of the A DNS record associated with the public IP. This is the concatenation of the domainNameLabel and the regionalized DNS zone." + } + }, + "reverseFqdn": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The reverse FQDN. A user-visible, fully qualified domain name that resolves to this public IP address. If the reverseFqdn is specified, then a PTR DNS record is created pointing from the IP address in the in-addr.arpa domain to the reverse FQDN." + } + } + }, + "metadata": { + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/res/network/public-ip-address:0.10.0" + } + } + }, + "ipTagType": { + "type": "object", + "properties": { + "ipTagType": { + "type": "string", + "metadata": { + "description": "Required. The IP tag type." + } + }, + "tag": { + "type": "string", + "metadata": { + "description": "Required. The IP tag." + } + } + }, + "metadata": { + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/res/network/public-ip-address:0.10.0" + } + } + }, + "lockType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Specify the name of lock." + } + }, + "kind": { + "type": "string", + "allowedValues": [ + "CanNotDelete", + "None", + "ReadOnly" ], "nullable": true, "metadata": { @@ -7955,7 +7971,7 @@ "metadata": { "description": "An AVM-aligned type for a lock.", "__bicep_imported_from!": { - "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.0" + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.1" } } }, @@ -8030,7 +8046,7 @@ "metadata": { "description": "An AVM-aligned type for a role assignment.", "__bicep_imported_from!": { - "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.5.1" + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.1" } } } @@ -8039,104 +8055,122 @@ "name": { "type": "string", "metadata": { - "description": "Required. Private DNS zone name." + "description": "Required. Name of the Azure Bastion resource." } }, - "a": { - "type": "array", - "items": { - "$ref": "#/definitions/aType" - }, - "nullable": true, + "location": { + "type": "string", + "defaultValue": "[resourceGroup().location]", "metadata": { - "description": "Optional. Array of A records." + "description": "Optional. Location for all resources." } }, - "aaaa": { - "type": "array", - "items": { - "$ref": "#/definitions/aaaaType" - }, - "nullable": true, + "virtualNetworkResourceId": { + "type": "string", "metadata": { - "description": "Optional. Array of AAAA records." + "description": "Required. Shared services Virtual Network resource Id." } }, - "cname": { - "type": "array", - "items": { - "$ref": "#/definitions/cnameType" - }, - "nullable": true, + "bastionSubnetPublicIpResourceId": { + "type": "string", + "defaultValue": "", "metadata": { - "description": "Optional. Array of CNAME records." + "description": "Optional. The Public IP resource ID to associate to the azureBastionSubnet. If empty, then the Public IP that is created as part of this module will be applied to the azureBastionSubnet. This parameter is ignored when enablePrivateOnlyBastion is true." } }, - "mx": { - "type": "array", - "items": { - "$ref": "#/definitions/mxType" + "publicIPAddressObject": { + "$ref": "#/definitions/publicIPAddressObjectType", + "defaultValue": { + "name": "[format('{0}-pip', parameters('name'))]" }, - "nullable": true, "metadata": { - "description": "Optional. Array of MX records." + "description": "Optional. Specifies the properties of the Public IP to create and be used by Azure Bastion, if no existing public IP was provided. This parameter is ignored when enablePrivateOnlyBastion is true." } }, - "ptr": { + "diagnosticSettings": { "type": "array", "items": { - "$ref": "#/definitions/ptrType" + "$ref": "#/definitions/diagnosticSettingLogsOnlyType" }, "nullable": true, "metadata": { - "description": "Optional. Array of PTR records." + "description": "Optional. The diagnostic settings of the service." } }, - "soa": { - "type": "array", - "items": { - "$ref": "#/definitions/soaType" - }, + "lock": { + "$ref": "#/definitions/lockType", "nullable": true, "metadata": { - "description": "Optional. Array of SOA records." + "description": "Optional. The lock settings of the service." } }, - "srv": { - "type": "array", - "items": { - "$ref": "#/definitions/srvType" - }, - "nullable": true, + "skuName": { + "type": "string", + "defaultValue": "Basic", + "allowedValues": [ + "Basic", + "Developer", + "Premium", + "Standard" + ], "metadata": { - "description": "Optional. Array of SRV records." + "description": "Optional. The SKU of this Bastion Host." } }, - "txt": { - "type": "array", - "items": { - "$ref": "#/definitions/txtType" - }, - "nullable": true, + "disableCopyPaste": { + "type": "bool", + "defaultValue": false, "metadata": { - "description": "Optional. Array of TXT records." + "description": "Optional. Choose to disable or enable Copy Paste. For Basic and Developer SKU Copy/Paste is always enabled." } }, - "virtualNetworkLinks": { - "type": "array", - "items": { - "$ref": "#/definitions/virtualNetworkLinkType" - }, - "nullable": true, + "enableFileCopy": { + "type": "bool", + "defaultValue": true, "metadata": { - "description": "Optional. Array of custom objects describing vNet links of the DNS zone. Each object should contain properties 'virtualNetworkResourceId' and 'registrationEnabled'. The 'vnetResourceId' is a resource ID of a vNet to link, 'registrationEnabled' (bool) enables automatic DNS registration in the zone for the linked vNet." + "description": "Optional. Choose to disable or enable File Copy. Not supported for Basic and Developer SKU." } }, - "location": { - "type": "string", - "defaultValue": "global", + "enableIpConnect": { + "type": "bool", + "defaultValue": false, "metadata": { - "description": "Optional. The location of the PrivateDNSZone. Should be global." + "description": "Optional. Choose to disable or enable IP Connect. Not supported for Basic and Developer SKU." + } + }, + "enableKerberos": { + "type": "bool", + "defaultValue": false, + "metadata": { + "description": "Optional. Choose to disable or enable Kerberos authentication. Not supported for Developer SKU." + } + }, + "enableShareableLink": { + "type": "bool", + "defaultValue": false, + "metadata": { + "description": "Optional. Choose to disable or enable Shareable Link. Not supported for Basic and Developer SKU." + } + }, + "enableSessionRecording": { + "type": "bool", + "defaultValue": false, + "metadata": { + "description": "Optional. Choose to disable or enable Session Recording feature. The Premium SKU is required for this feature. If Session Recording is enabled, the Native client support will be disabled." + } + }, + "enablePrivateOnlyBastion": { + "type": "bool", + "defaultValue": false, + "metadata": { + "description": "Optional. Choose to disable or enable Private-only Bastion deployment. The Premium SKU is required for this feature." + } + }, + "scaleUnits": { + "type": "int", + "defaultValue": 2, + "metadata": { + "description": "Optional. The scale units for the Bastion Host resource. The Basic and Developer SKU only support 2 scale units." } }, "roleAssignments": { @@ -8151,17 +8185,13 @@ }, "tags": { "type": "object", - "nullable": true, "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.Network/bastionHosts@2024-07-01#properties/tags" + }, "description": "Optional. Tags of the resource." - } - }, - "lock": { - "$ref": "#/definitions/lockType", - "nullable": true, - "metadata": { - "description": "Optional. The lock settings of the service." - } + }, + "nullable": true }, "enableTelemetry": { "type": "bool", @@ -8169,6 +8199,21 @@ "metadata": { "description": "Optional. Enable/Disable usage telemetry for module." } + }, + "availabilityZones": { + "type": "array", + "items": { + "type": "int" + }, + "defaultValue": [], + "allowedValues": [ + 1, + 2, + 3 + ], + "metadata": { + "description": "Optional. The list of Availability zones to use for the zone-redundant resources." + } } }, "variables": { @@ -8179,22 +8224,21 @@ "input": "[union(coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')], createObject('roleDefinitionId', coalesce(tryGet(variables('builtInRoleNames'), coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName), if(contains(coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName, '/providers/Microsoft.Authorization/roleDefinitions/'), coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName, subscriptionResourceId('Microsoft.Authorization/roleDefinitions', coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName)))))]" } ], + "enableReferencedModulesTelemetry": false, "builtInRoleNames": { "Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'b24988ac-6180-42a0-ab88-20f7382dd24c')]", - "Network Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '4d97b98b-1d4f-4787-a291-c67834d212e7')]", "Owner": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '8e3af657-a8ff-443c-a75c-2fe8c4bcb635')]", - "Private DNS Zone Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'b12aa53e-6015-4669-85d0-8515ebb3ae7f')]", "Reader": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'acdd72a7-3385-48ef-bd42-f606fba81ae7')]", - "Role Based Access Control Administrator": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'f58310d9-a9f6-439a-9e8d-f62e7b41a168')]" - }, - "enableReferencedModulesTelemetry": false + "Role Based Access Control Administrator": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'f58310d9-a9f6-439a-9e8d-f62e7b41a168')]", + "User Access Administrator": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '18d7d88d-d35e-4fb5-a5c3-7773c20a72d9')]" + } }, "resources": { "avmTelemetry": { "condition": "[parameters('enableTelemetry')]", "type": "Microsoft.Resources/deployments", "apiVersion": "2024-03-01", - "name": "[format('46d3xbcp.res.network-privatednszone.{0}.{1}', replace('0.8.0', '.', '-'), substring(uniqueString(deployment().name, parameters('location')), 0, 4))]", + "name": "[format('46d3xbcp.res.network-bastionhost.{0}.{1}', replace('0.8.2', '.', '-'), substring(uniqueString(deployment().name, parameters('location')), 0, 4))]", "properties": { "mode": "Incremental", "template": { @@ -8210,36 +8254,76 @@ } } }, - "privateDnsZone": { - "type": "Microsoft.Network/privateDnsZones", - "apiVersion": "2020-06-01", + "azureBastion": { + "type": "Microsoft.Network/bastionHosts", + "apiVersion": "2025-01-01", "name": "[parameters('name')]", "location": "[parameters('location')]", - "tags": "[parameters('tags')]" + "tags": "[coalesce(parameters('tags'), createObject())]", + "sku": { + "name": "[parameters('skuName')]" + }, + "zones": "[if(equals(parameters('skuName'), 'Developer'), createArray(), map(parameters('availabilityZones'), lambda('zone', format('{0}', lambdaVariables('zone')))))]", + "properties": "[union(createObject('scaleUnits', if(or(equals(parameters('skuName'), 'Basic'), equals(parameters('skuName'), 'Developer')), 2, parameters('scaleUnits')), 'ipConfigurations', if(equals(parameters('skuName'), 'Developer'), createArray(), createArray(createObject('name', 'IpConfAzureBastionSubnet', 'properties', union(createObject('subnet', createObject('id', format('{0}/subnets/AzureBastionSubnet', parameters('virtualNetworkResourceId')))), if(not(parameters('enablePrivateOnlyBastion')), createObject('publicIPAddress', createObject('id', if(not(empty(parameters('bastionSubnetPublicIpResourceId'))), parameters('bastionSubnetPublicIpResourceId'), reference('publicIPAddress').outputs.resourceId.value))), createObject())))))), if(equals(parameters('skuName'), 'Developer'), createObject('virtualNetwork', createObject('id', parameters('virtualNetworkResourceId'))), createObject()), if(or(or(equals(parameters('skuName'), 'Basic'), equals(parameters('skuName'), 'Standard')), equals(parameters('skuName'), 'Premium')), createObject('enableKerberos', parameters('enableKerberos')), createObject()), if(or(equals(parameters('skuName'), 'Standard'), equals(parameters('skuName'), 'Premium')), createObject('enableTunneling', if(equals(parameters('skuName'), 'Standard'), true(), if(parameters('enableSessionRecording'), false(), true())), 'disableCopyPaste', parameters('disableCopyPaste'), 'enableFileCopy', parameters('enableFileCopy'), 'enableIpConnect', parameters('enableIpConnect'), 'enableShareableLink', parameters('enableShareableLink')), createObject()), if(equals(parameters('skuName'), 'Premium'), createObject('enableSessionRecording', parameters('enableSessionRecording'), 'enablePrivateOnlyBastion', parameters('enablePrivateOnlyBastion')), createObject()))]", + "dependsOn": [ + "publicIPAddress" + ] }, - "privateDnsZone_lock": { + "azureBastion_lock": { "condition": "[and(not(empty(coalesce(parameters('lock'), createObject()))), not(equals(tryGet(parameters('lock'), 'kind'), 'None')))]", "type": "Microsoft.Authorization/locks", "apiVersion": "2020-05-01", - "scope": "[format('Microsoft.Network/privateDnsZones/{0}', parameters('name'))]", + "scope": "[format('Microsoft.Network/bastionHosts/{0}', parameters('name'))]", "name": "[coalesce(tryGet(parameters('lock'), 'name'), format('lock-{0}', parameters('name')))]", "properties": { "level": "[coalesce(tryGet(parameters('lock'), 'kind'), '')]", "notes": "[coalesce(tryGet(parameters('lock'), 'notes'), if(equals(tryGet(parameters('lock'), 'kind'), 'CanNotDelete'), 'Cannot delete resource or child resources.', 'Cannot delete or modify the resource or child resources.'))]" }, "dependsOn": [ - "privateDnsZone" + "azureBastion" ] }, - "privateDnsZone_roleAssignments": { + "azureBastion_diagnosticSettings": { "copy": { - "name": "privateDnsZone_roleAssignments", + "name": "azureBastion_diagnosticSettings", + "count": "[length(coalesce(parameters('diagnosticSettings'), createArray()))]" + }, + "type": "Microsoft.Insights/diagnosticSettings", + "apiVersion": "2021-05-01-preview", + "scope": "[format('Microsoft.Network/bastionHosts/{0}', parameters('name'))]", + "name": "[coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'name'), format('{0}-diagnosticSettings', parameters('name')))]", + "properties": { + "copy": [ + { + "name": "logs", + "count": "[length(coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'logCategoriesAndGroups'), createArray(createObject('categoryGroup', 'allLogs'))))]", + "input": { + "categoryGroup": "[tryGet(coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'logCategoriesAndGroups'), createArray(createObject('categoryGroup', 'allLogs')))[copyIndex('logs')], 'categoryGroup')]", + "category": "[tryGet(coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'logCategoriesAndGroups'), createArray(createObject('categoryGroup', 'allLogs')))[copyIndex('logs')], 'category')]", + "enabled": "[coalesce(tryGet(coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'logCategoriesAndGroups'), createArray(createObject('categoryGroup', 'allLogs')))[copyIndex('logs')], 'enabled'), true())]" + } + } + ], + "storageAccountId": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'storageAccountResourceId')]", + "workspaceId": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'workspaceResourceId')]", + "eventHubAuthorizationRuleId": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'eventHubAuthorizationRuleResourceId')]", + "eventHubName": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'eventHubName')]", + "marketplacePartnerId": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'marketplacePartnerResourceId')]", + "logAnalyticsDestinationType": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'logAnalyticsDestinationType')]" + }, + "dependsOn": [ + "azureBastion" + ] + }, + "azureBastion_roleAssignments": { + "copy": { + "name": "azureBastion_roleAssignments", "count": "[length(coalesce(variables('formattedRoleAssignments'), createArray()))]" }, "type": "Microsoft.Authorization/roleAssignments", "apiVersion": "2022-04-01", - "scope": "[format('Microsoft.Network/privateDnsZones/{0}', parameters('name'))]", - "name": "[coalesce(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'name'), guid(resourceId('Microsoft.Network/privateDnsZones', parameters('name')), coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].principalId, coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].roleDefinitionId))]", + "scope": "[format('Microsoft.Network/bastionHosts/{0}', parameters('name'))]", + "name": "[coalesce(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'name'), guid(resourceId('Microsoft.Network/bastionHosts', parameters('name')), coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].principalId, coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].roleDefinitionId))]", "properties": { "roleDefinitionId": "[coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].roleDefinitionId]", "principalId": "[coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].principalId]", @@ -8250,43 +8334,70 @@ "delegatedManagedIdentityResourceId": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'delegatedManagedIdentityResourceId')]" }, "dependsOn": [ - "privateDnsZone" + "azureBastion" ] }, - "privateDnsZone_A": { - "copy": { - "name": "privateDnsZone_A", - "count": "[length(coalesce(parameters('a'), createArray()))]" - }, + "publicIPAddress": { + "condition": "[and(and(empty(parameters('bastionSubnetPublicIpResourceId')), not(equals(parameters('skuName'), 'Developer'))), not(parameters('enablePrivateOnlyBastion')))]", "type": "Microsoft.Resources/deployments", - "apiVersion": "2022-09-01", - "name": "[format('{0}-PrivateDnsZone-ARecord-{1}', uniqueString(deployment().name, parameters('location')), copyIndex())]", + "apiVersion": "2025-04-01", + "name": "[format('{0}-Bastion-PIP', uniqueString(subscription().id, resourceGroup().id, parameters('location')))]", "properties": { "expressionEvaluationOptions": { "scope": "inner" }, "mode": "Incremental", "parameters": { - "privateDnsZoneName": { - "value": "[parameters('name')]" - }, "name": { - "value": "[coalesce(parameters('a'), createArray())[copyIndex()].name]" + "value": "[parameters('publicIPAddressObject').name]" }, - "aRecords": { - "value": "[tryGet(coalesce(parameters('a'), createArray())[copyIndex()], 'aRecords')]" + "enableTelemetry": { + "value": "[variables('enableReferencedModulesTelemetry')]" }, - "metadata": { - "value": "[tryGet(coalesce(parameters('a'), createArray())[copyIndex()], 'metadata')]" + "location": { + "value": "[parameters('location')]" }, - "ttl": { - "value": "[coalesce(tryGet(coalesce(parameters('a'), createArray())[copyIndex()], 'ttl'), 3600)]" + "lock": { + "value": "[parameters('lock')]" + }, + "diagnosticSettings": { + "value": "[tryGet(parameters('publicIPAddressObject'), 'diagnosticSettings')]" + }, + "ddosSettings": { + "value": "[tryGet(parameters('publicIPAddressObject'), 'ddosSettings')]" + }, + "dnsSettings": { + "value": "[tryGet(parameters('publicIPAddressObject'), 'dnsSettings')]" + }, + "idleTimeoutInMinutes": { + "value": "[tryGet(parameters('publicIPAddressObject'), 'idleTimeoutInMinutes')]" + }, + "ipTags": { + "value": "[tryGet(parameters('publicIPAddressObject'), 'ipTags')]" + }, + "publicIPAddressVersion": { + "value": "[tryGet(parameters('publicIPAddressObject'), 'publicIPAddressVersion')]" + }, + "publicIPAllocationMethod": { + "value": "[tryGet(parameters('publicIPAddressObject'), 'publicIPAllocationMethod')]" + }, + "publicIpPrefixResourceId": { + "value": "[tryGet(parameters('publicIPAddressObject'), 'publicIpPrefixResourceId')]" }, "roleAssignments": { - "value": "[tryGet(coalesce(parameters('a'), createArray())[copyIndex()], 'roleAssignments')]" + "value": "[tryGet(parameters('publicIPAddressObject'), 'roleAssignments')]" }, - "enableTelemetry": { - "value": "[variables('enableReferencedModulesTelemetry')]" + "skuName": { + "value": "[tryGet(parameters('publicIPAddressObject'), 'skuName')]" + }, + "skuTier": { + "value": "[tryGet(parameters('publicIPAddressObject'), 'skuTier')]" + }, + "tags": { + "value": "[coalesce(tryGet(parameters('publicIPAddressObject'), 'tags'), parameters('tags'))]" + }, + "availabilityZones": { + "value": "[coalesce(tryGet(parameters('publicIPAddressObject'), 'availabilityZones'), if(not(empty(parameters('availabilityZones'))), parameters('availabilityZones'), null()))]" } }, "template": { @@ -8296,304 +8407,265 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.37.4.10188", - "templateHash": "12608084563401365743" + "version": "0.39.26.7824", + "templateHash": "16564959277054027786" }, - "name": "Private DNS Zone A record", - "description": "This module deploys a Private DNS Zone A record." + "name": "Public IP Addresses", + "description": "This module deploys a Public IP Address." }, "definitions": { - "roleAssignmentType": { + "dnsSettingsType": { "type": "object", "properties": { - "name": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The name (as GUID) of the role assignment. If not provided, a GUID will be generated." - } - }, - "roleDefinitionIdOrName": { - "type": "string", - "metadata": { - "description": "Required. The role to assign. You can provide either the display name of the role definition, the role definition GUID, or its fully qualified ID in the following format: '/providers/Microsoft.Authorization/roleDefinitions/c2f4ef07-c644-48eb-af81-4b1b4947fb11'." - } - }, - "principalId": { + "domainNameLabel": { "type": "string", "metadata": { - "description": "Required. The principal ID of the principal (user/group/identity) to assign the role to." + "description": "Required. The domain name label. The concatenation of the domain name label and the regionalized DNS zone make up the fully qualified domain name associated with the public IP address. If a domain name label is specified, an A DNS record is created for the public IP in the Microsoft Azure DNS system." } }, - "principalType": { + "domainNameLabelScope": { "type": "string", "allowedValues": [ - "Device", - "ForeignGroup", - "Group", - "ServicePrincipal", - "User" + "NoReuse", + "ResourceGroupReuse", + "SubscriptionReuse", + "TenantReuse" ], "nullable": true, "metadata": { - "description": "Optional. The principal type of the assigned principal ID." + "description": "Optional. The domain name label scope. If a domain name label and a domain name label scope are specified, an A DNS record is created for the public IP in the Microsoft Azure DNS system with a hashed value includes in FQDN." } }, - "description": { + "fqdn": { "type": "string", "nullable": true, "metadata": { - "description": "Optional. The description of the role assignment." + "description": "Optional. The Fully Qualified Domain Name of the A DNS record associated with the public IP. This is the concatenation of the domainNameLabel and the regionalized DNS zone." } }, - "condition": { + "reverseFqdn": { "type": "string", "nullable": true, "metadata": { - "description": "Optional. The conditions on the role assignment. This limits the resources it can be assigned to. e.g.: @Resource[Microsoft.Storage/storageAccounts/blobServices/containers:ContainerName] StringEqualsIgnoreCase \"foo_storage_container\"." + "description": "Optional. The reverse FQDN. A user-visible, fully qualified domain name that resolves to this public IP address. If the reverseFqdn is specified, then a PTR DNS record is created pointing from the IP address in the in-addr.arpa domain to the reverse FQDN." } - }, - "conditionVersion": { - "type": "string", - "allowedValues": [ - "2.0" - ], + } + }, + "metadata": { + "__bicep_export!": true + } + }, + "ddosSettingsType": { + "type": "object", + "properties": { + "ddosProtectionPlan": { + "type": "object", + "properties": { + "id": { + "type": "string", + "metadata": { + "description": "Required. The resource ID of the DDOS protection plan associated with the public IP address." + } + } + }, "nullable": true, "metadata": { - "description": "Optional. Version of the condition." + "description": "Optional. The DDoS protection plan associated with the public IP address." } }, - "delegatedManagedIdentityResourceId": { + "protectionMode": { "type": "string", - "nullable": true, + "allowedValues": [ + "Enabled" + ], "metadata": { - "description": "Optional. The Resource Id of the delegated managed identity resource." + "description": "Required. The DDoS protection policy customizations." } } }, "metadata": { - "description": "An AVM-aligned type for a role assignment.", - "__bicep_imported_from!": { - "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.5.1" - } - } - } - }, - "parameters": { - "privateDnsZoneName": { - "type": "string", - "metadata": { - "description": "Conditional. The name of the parent Private DNS zone. Required if the template is used in a standalone deployment." + "__bicep_export!": true } }, - "name": { - "type": "string", + "ipTagType": { + "type": "object", + "properties": { + "ipTagType": { + "type": "string", + "metadata": { + "description": "Required. The IP tag type." + } + }, + "tag": { + "type": "string", + "metadata": { + "description": "Required. The IP tag." + } + } + }, "metadata": { - "description": "Required. The name of the A record." + "__bicep_export!": true } }, - "aRecords": { - "type": "array", - "metadata": { - "__bicep_resource_derived_type!": { - "source": "Microsoft.Network/privateDnsZones/A@2024-06-01#properties/properties/properties/aRecords" - }, - "description": "Optional. The list of A records in the record set." - }, - "nullable": true - }, - "metadata": { + "diagnosticSettingFullType": { "type": "object", - "metadata": { - "__bicep_resource_derived_type!": { - "source": "Microsoft.Network/privateDnsZones/A@2024-06-01#properties/properties/properties/metadata" + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The name of the diagnostic setting." + } }, - "description": "Optional. The metadata attached to the record set." + "logCategoriesAndGroups": { + "type": "array", + "items": { + "type": "object", + "properties": { + "category": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Name of a Diagnostic Log category for a resource type this setting is applied to. Set the specific logs to collect here." + } + }, + "categoryGroup": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Name of a Diagnostic Log category group for a resource type this setting is applied to. Set to `allLogs` to collect all logs." + } + }, + "enabled": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. Enable or disable the category explicitly. Default is `true`." + } + } + } + }, + "nullable": true, + "metadata": { + "description": "Optional. The name of logs that will be streamed. \"allLogs\" includes all possible logs for the resource. Set to `[]` to disable log collection." + } + }, + "metricCategories": { + "type": "array", + "items": { + "type": "object", + "properties": { + "category": { + "type": "string", + "metadata": { + "description": "Required. Name of a Diagnostic Metric category for a resource type this setting is applied to. Set to `AllMetrics` to collect all metrics." + } + }, + "enabled": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. Enable or disable the category explicitly. Default is `true`." + } + } + } + }, + "nullable": true, + "metadata": { + "description": "Optional. The name of metrics that will be streamed. \"allMetrics\" includes all possible metrics for the resource. Set to `[]` to disable metric collection." + } + }, + "logAnalyticsDestinationType": { + "type": "string", + "allowedValues": [ + "AzureDiagnostics", + "Dedicated" + ], + "nullable": true, + "metadata": { + "description": "Optional. A string indicating whether the export to Log Analytics should use the default destination type, i.e. AzureDiagnostics, or use a destination type." + } + }, + "workspaceResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Resource ID of the diagnostic log analytics workspace. For security reasons, it is recommended to set diagnostic settings to send data to either storage account, log analytics workspace or event hub." + } + }, + "storageAccountResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Resource ID of the diagnostic storage account. For security reasons, it is recommended to set diagnostic settings to send data to either storage account, log analytics workspace or event hub." + } + }, + "eventHubAuthorizationRuleResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Resource ID of the diagnostic event hub authorization rule for the Event Hubs namespace in which the event hub should be created or streamed to." + } + }, + "eventHubName": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Name of the diagnostic event hub within the namespace to which logs are streamed. Without this, an event hub is created for each log category. For security reasons, it is recommended to set diagnostic settings to send data to either storage account, log analytics workspace or event hub." + } + }, + "marketplacePartnerResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The full ARM resource ID of the Marketplace resource to which you would like to send Diagnostic Logs." + } + } }, - "nullable": true - }, - "ttl": { - "type": "int", - "defaultValue": 3600, - "metadata": { - "description": "Optional. The TTL (time-to-live) of the records in the record set." - } - }, - "enableTelemetry": { - "type": "bool", - "defaultValue": true, "metadata": { - "description": "Optional. Enable/Disable usage telemetry for module." + "description": "An AVM-aligned type for a diagnostic setting. To be used if both logs & metrics are supported by the resource provider.", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.1" + } } }, - "roleAssignments": { - "type": "array", - "items": { - "$ref": "#/definitions/roleAssignmentType" - }, - "nullable": true, - "metadata": { - "description": "Optional. Array of role assignments to create." - } - } - }, - "variables": { - "copy": [ - { - "name": "formattedRoleAssignments", - "count": "[length(coalesce(parameters('roleAssignments'), createArray()))]", - "input": "[union(coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')], createObject('roleDefinitionId', coalesce(tryGet(variables('builtInRoleNames'), coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName), if(contains(coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName, '/providers/Microsoft.Authorization/roleDefinitions/'), coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName, subscriptionResourceId('Microsoft.Authorization/roleDefinitions', coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName)))))]" - } - ], - "builtInRoleNames": { - "Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'b24988ac-6180-42a0-ab88-20f7382dd24c')]", - "Private DNS Zone Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'b12aa53e-6015-4669-85d0-8515ebb3ae7f')]", - "Network Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '4d97b98b-1d4f-4787-a291-c67834d212e7')]", - "Owner": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '8e3af657-a8ff-443c-a75c-2fe8c4bcb635')]", - "Reader": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'acdd72a7-3385-48ef-bd42-f606fba81ae7')]", - "Role Based Access Control Administrator": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'f58310d9-a9f6-439a-9e8d-f62e7b41a168')]", - "User Access Administrator": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '18d7d88d-d35e-4fb5-a5c3-7773c20a72d9')]" - } - }, - "resources": { - "avmTelemetry": { - "condition": "[parameters('enableTelemetry')]", - "type": "Microsoft.Resources/deployments", - "apiVersion": "2024-03-01", - "name": "[format('46d3xbcp.res.nw-privdnszonea.{0}.{1}', replace('0.1.0', '.', '-'), substring(uniqueString(deployment().name), 0, 4))]", + "lockType": { + "type": "object", "properties": { - "mode": "Incremental", - "template": { - "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", - "contentVersion": "1.0.0.0", - "resources": [], - "outputs": { - "telemetry": { - "type": "String", - "value": "For more information, see https://aka.ms/avm/TelemetryInfo" - } + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Specify the name of lock." + } + }, + "kind": { + "type": "string", + "allowedValues": [ + "CanNotDelete", + "None", + "ReadOnly" + ], + "nullable": true, + "metadata": { + "description": "Optional. Specify the type of lock." + } + }, + "notes": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Specify the notes of the lock." } } - } - }, - "privateDnsZone": { - "existing": true, - "type": "Microsoft.Network/privateDnsZones", - "apiVersion": "2020-06-01", - "name": "[parameters('privateDnsZoneName')]" - }, - "A": { - "type": "Microsoft.Network/privateDnsZones/A", - "apiVersion": "2020-06-01", - "name": "[format('{0}/{1}', parameters('privateDnsZoneName'), parameters('name'))]", - "properties": { - "aRecords": "[parameters('aRecords')]", - "metadata": "[parameters('metadata')]", - "ttl": "[parameters('ttl')]" - } - }, - "A_roleAssignments": { - "copy": { - "name": "A_roleAssignments", - "count": "[length(coalesce(variables('formattedRoleAssignments'), createArray()))]" - }, - "type": "Microsoft.Authorization/roleAssignments", - "apiVersion": "2022-04-01", - "scope": "[format('Microsoft.Network/privateDnsZones/{0}/A/{1}', parameters('privateDnsZoneName'), parameters('name'))]", - "name": "[coalesce(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'name'), guid(resourceId('Microsoft.Network/privateDnsZones/A', parameters('privateDnsZoneName'), parameters('name')), coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].principalId, coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].roleDefinitionId))]", - "properties": { - "roleDefinitionId": "[coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].roleDefinitionId]", - "principalId": "[coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].principalId]", - "description": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'description')]", - "principalType": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'principalType')]", - "condition": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'condition')]", - "conditionVersion": "[if(not(empty(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'condition'))), coalesce(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'conditionVersion'), '2.0'), null())]", - "delegatedManagedIdentityResourceId": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'delegatedManagedIdentityResourceId')]" - }, - "dependsOn": [ - "A" - ] - } - }, - "outputs": { - "name": { - "type": "string", - "metadata": { - "description": "The name of the deployed A record." - }, - "value": "[parameters('name')]" - }, - "resourceId": { - "type": "string", - "metadata": { - "description": "The resource ID of the deployed A record." }, - "value": "[resourceId('Microsoft.Network/privateDnsZones/A', parameters('privateDnsZoneName'), parameters('name'))]" - }, - "resourceGroupName": { - "type": "string", "metadata": { - "description": "The resource group of the deployed A record." - }, - "value": "[resourceGroup().name]" - } - } - } - }, - "dependsOn": [ - "privateDnsZone" - ] - }, - "privateDnsZone_AAAA": { - "copy": { - "name": "privateDnsZone_AAAA", - "count": "[length(coalesce(parameters('aaaa'), createArray()))]" - }, - "type": "Microsoft.Resources/deployments", - "apiVersion": "2022-09-01", - "name": "[format('{0}-PrivateDnsZone-AAAARecord-{1}', uniqueString(deployment().name, parameters('location')), copyIndex())]", - "properties": { - "expressionEvaluationOptions": { - "scope": "inner" - }, - "mode": "Incremental", - "parameters": { - "privateDnsZoneName": { - "value": "[parameters('name')]" - }, - "name": { - "value": "[coalesce(parameters('aaaa'), createArray())[copyIndex()].name]" - }, - "aaaaRecords": { - "value": "[tryGet(coalesce(parameters('aaaa'), createArray())[copyIndex()], 'aaaaRecords')]" - }, - "metadata": { - "value": "[tryGet(coalesce(parameters('aaaa'), createArray())[copyIndex()], 'metadata')]" - }, - "ttl": { - "value": "[coalesce(tryGet(coalesce(parameters('aaaa'), createArray())[copyIndex()], 'ttl'), 3600)]" - }, - "roleAssignments": { - "value": "[tryGet(coalesce(parameters('aaaa'), createArray())[copyIndex()], 'roleAssignments')]" - }, - "enableTelemetry": { - "value": "[variables('enableReferencedModulesTelemetry')]" - } - }, - "template": { - "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", - "languageVersion": "2.0", - "contentVersion": "1.0.0.0", - "metadata": { - "_generator": { - "name": "bicep", - "version": "0.37.4.10188", - "templateHash": "4881696097088567452" + "description": "An AVM-aligned type for a lock.", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.1" + } + } }, - "name": "Private DNS Zone AAAA record", - "description": "This module deploys a Private DNS Zone AAAA record." - }, - "definitions": { "roleAssignmentType": { "type": "object", "properties": { @@ -8665,49 +8737,145 @@ "metadata": { "description": "An AVM-aligned type for a role assignment.", "__bicep_imported_from!": { - "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.5.1" + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.1" } } } }, "parameters": { - "privateDnsZoneName": { + "name": { "type": "string", "metadata": { - "description": "Conditional. The name of the parent Private DNS zone. Required if the template is used in a standalone deployment." + "description": "Required. The name of the Public IP Address." } }, - "name": { + "publicIpPrefixResourceId": { "type": "string", + "nullable": true, "metadata": { - "description": "Required. The name of the AAAA record." + "description": "Optional. Resource ID of the Public IP Prefix object. This is only needed if you want your Public IPs created in a PIP Prefix." } }, - "aaaaRecords": { - "type": "array", + "publicIPAllocationMethod": { + "type": "string", + "defaultValue": "Static", + "allowedValues": [ + "Dynamic", + "Static" + ], "metadata": { - "__bicep_resource_derived_type!": { - "source": "Microsoft.Network/privateDnsZones/AAAA@2024-06-01#properties/properties/properties/aaaaRecords" - }, - "description": "Optional. The list of AAAA records in the record set." + "description": "Optional. The public IP address allocation method." + } + }, + "availabilityZones": { + "type": "array", + "items": { + "type": "int" }, - "nullable": true + "defaultValue": [ + 1, + 2, + 3 + ], + "allowedValues": [ + 1, + 2, + 3 + ], + "metadata": { + "description": "Optional. A list of availability zones denoting the IP allocated for the resource needs to come from." + } }, - "metadata": { - "type": "object", + "publicIPAddressVersion": { + "type": "string", + "defaultValue": "IPv4", + "allowedValues": [ + "IPv4", + "IPv6" + ], "metadata": { - "__bicep_resource_derived_type!": { - "source": "Microsoft.Network/privateDnsZones/AAAA@2024-06-01#properties/properties/properties/metadata" - }, - "description": "Optional. The metadata attached to the record set." + "description": "Optional. IP address version." + } + }, + "dnsSettings": { + "$ref": "#/definitions/dnsSettingsType", + "nullable": true, + "metadata": { + "description": "Optional. The DNS settings of the public IP address." + } + }, + "ipTags": { + "type": "array", + "items": { + "$ref": "#/definitions/ipTagType" }, - "nullable": true + "nullable": true, + "metadata": { + "description": "Optional. The list of tags associated with the public IP address." + } }, - "ttl": { - "type": "int", - "defaultValue": 3600, + "lock": { + "$ref": "#/definitions/lockType", + "nullable": true, "metadata": { - "description": "Optional. The TTL (time-to-live) of the records in the record set." + "description": "Optional. The lock settings of the service." + } + }, + "skuName": { + "type": "string", + "defaultValue": "Standard", + "allowedValues": [ + "Basic", + "Standard" + ], + "metadata": { + "description": "Optional. Name of a public IP address SKU." + } + }, + "skuTier": { + "type": "string", + "defaultValue": "Regional", + "allowedValues": [ + "Global", + "Regional" + ], + "metadata": { + "description": "Optional. Tier of a public IP address SKU." + } + }, + "ddosSettings": { + "$ref": "#/definitions/ddosSettingsType", + "nullable": true, + "metadata": { + "description": "Optional. The DDoS protection plan configuration associated with the public IP address." + } + }, + "deleteOption": { + "type": "string", + "nullable": true, + "allowedValues": [ + "Delete", + "Detach" + ], + "metadata": { + "description": "Optional. The delete option for the public IP address." + } + }, + "location": { + "type": "string", + "defaultValue": "[resourceGroup().location]", + "metadata": { + "description": "Optional. Location for all resources." + } + }, + "roleAssignments": { + "type": "array", + "items": { + "$ref": "#/definitions/roleAssignmentType" + }, + "nullable": true, + "metadata": { + "description": "Optional. Array of role assignments to create." } }, "enableTelemetry": { @@ -8717,14 +8885,31 @@ "description": "Optional. Enable/Disable usage telemetry for module." } }, - "roleAssignments": { + "idleTimeoutInMinutes": { + "type": "int", + "defaultValue": 4, + "metadata": { + "description": "Optional. The idle timeout of the public IP address." + } + }, + "tags": { + "type": "object", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.Network/publicIPAddresses@2025-01-01#properties/tags" + }, + "description": "Optional. Tags of the resource." + }, + "nullable": true + }, + "diagnosticSettings": { "type": "array", "items": { - "$ref": "#/definitions/roleAssignmentType" + "$ref": "#/definitions/diagnosticSettingFullType" }, "nullable": true, "metadata": { - "description": "Optional. Array of role assignments to create." + "description": "Optional. The diagnostic settings of the service." } } }, @@ -8738,12 +8923,15 @@ ], "builtInRoleNames": { "Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'b24988ac-6180-42a0-ab88-20f7382dd24c')]", - "Private DNS Zone Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'b12aa53e-6015-4669-85d0-8515ebb3ae7f')]", + "DNS Resolver Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '0f2ebee7-ffd4-4fc0-b3b7-664099fdad5d')]", + "DNS Zone Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'befefa01-2a29-4197-83a8-272ff33ce314')]", + "Domain Services Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'eeaeda52-9324-47f6-8069-5d5bade478b2')]", + "Domain Services Reader": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '361898ef-9ed1-48c2-849c-a832951106bb')]", "Network Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '4d97b98b-1d4f-4787-a291-c67834d212e7')]", "Owner": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '8e3af657-a8ff-443c-a75c-2fe8c4bcb635')]", + "Private DNS Zone Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'b12aa53e-6015-4669-85d0-8515ebb3ae7f')]", "Reader": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'acdd72a7-3385-48ef-bd42-f606fba81ae7')]", - "Role Based Access Control Administrator": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'f58310d9-a9f6-439a-9e8d-f62e7b41a168')]", - "User Access Administrator": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '18d7d88d-d35e-4fb5-a5c3-7773c20a72d9')]" + "Role Based Access Control Administrator": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'f58310d9-a9f6-439a-9e8d-f62e7b41a168')]" } }, "resources": { @@ -8751,7 +8939,7 @@ "condition": "[parameters('enableTelemetry')]", "type": "Microsoft.Resources/deployments", "apiVersion": "2024-03-01", - "name": "[format('46d3xbcp.res.nw-privdnszoneaaaa.{0}.{1}', replace('0.1.0', '.', '-'), substring(uniqueString(deployment().name), 0, 4))]", + "name": "[format('46d3xbcp.res.network-publicipaddress.{0}.{1}', replace('0.10.0', '.', '-'), substring(uniqueString(deployment().name, parameters('location')), 0, 4))]", "properties": { "mode": "Incremental", "template": { @@ -8767,31 +8955,51 @@ } } }, - "privateDnsZone": { - "existing": true, - "type": "Microsoft.Network/privateDnsZones", - "apiVersion": "2020-06-01", - "name": "[parameters('privateDnsZoneName')]" - }, - "AAAA": { - "type": "Microsoft.Network/privateDnsZones/AAAA", - "apiVersion": "2020-06-01", - "name": "[format('{0}/{1}', parameters('privateDnsZoneName'), parameters('name'))]", + "publicIpAddress": { + "type": "Microsoft.Network/publicIPAddresses", + "apiVersion": "2025-01-01", + "name": "[parameters('name')]", + "location": "[parameters('location')]", + "tags": "[parameters('tags')]", + "sku": { + "name": "[parameters('skuName')]", + "tier": "[parameters('skuTier')]" + }, + "zones": "[map(parameters('availabilityZones'), lambda('zone', string(lambdaVariables('zone'))))]", "properties": { - "aaaaRecords": "[parameters('aaaaRecords')]", - "metadata": "[parameters('metadata')]", - "ttl": "[parameters('ttl')]" + "ddosSettings": "[parameters('ddosSettings')]", + "dnsSettings": "[parameters('dnsSettings')]", + "publicIPAddressVersion": "[parameters('publicIPAddressVersion')]", + "publicIPAllocationMethod": "[parameters('publicIPAllocationMethod')]", + "publicIPPrefix": "[if(not(empty(parameters('publicIpPrefixResourceId'))), createObject('id', parameters('publicIpPrefixResourceId')), null())]", + "idleTimeoutInMinutes": "[parameters('idleTimeoutInMinutes')]", + "ipTags": "[parameters('ipTags')]", + "deleteOption": "[parameters('deleteOption')]" } }, - "AAAA_roleAssignments": { + "publicIpAddress_lock": { + "condition": "[and(not(empty(coalesce(parameters('lock'), createObject()))), not(equals(tryGet(parameters('lock'), 'kind'), 'None')))]", + "type": "Microsoft.Authorization/locks", + "apiVersion": "2020-05-01", + "scope": "[format('Microsoft.Network/publicIPAddresses/{0}', parameters('name'))]", + "name": "[coalesce(tryGet(parameters('lock'), 'name'), format('lock-{0}', parameters('name')))]", + "properties": { + "level": "[coalesce(tryGet(parameters('lock'), 'kind'), '')]", + "notes": "[coalesce(tryGet(parameters('lock'), 'notes'), if(equals(tryGet(parameters('lock'), 'kind'), 'CanNotDelete'), 'Cannot delete resource or child resources.', 'Cannot delete or modify the resource or child resources.'))]" + }, + "dependsOn": [ + "publicIpAddress" + ] + }, + "publicIpAddress_roleAssignments": { "copy": { - "name": "AAAA_roleAssignments", + "name": "publicIpAddress_roleAssignments", "count": "[length(coalesce(variables('formattedRoleAssignments'), createArray()))]" }, "type": "Microsoft.Authorization/roleAssignments", "apiVersion": "2022-04-01", - "scope": "[format('Microsoft.Network/privateDnsZones/{0}/AAAA/{1}', parameters('privateDnsZoneName'), parameters('name'))]", - "name": "[coalesce(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'name'), guid(resourceId('Microsoft.Network/privateDnsZones/AAAA', parameters('privateDnsZoneName'), parameters('name')), coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].principalId, coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].roleDefinitionId))]", + "scope": "[format('Microsoft.Network/publicIPAddresses/{0}', parameters('name'))]", + "name": "[coalesce(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'name'), guid(resourceId('Microsoft.Network/publicIPAddresses', parameters('name')), coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].principalId, coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].roleDefinitionId))]", "properties": { "roleDefinitionId": "[coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].roleDefinitionId]", "principalId": "[coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].principalId]", @@ -8802,2434 +9010,1609 @@ "delegatedManagedIdentityResourceId": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'delegatedManagedIdentityResourceId')]" }, "dependsOn": [ - "AAAA" + "publicIpAddress" ] - } - }, - "outputs": { - "name": { - "type": "string", - "metadata": { - "description": "The name of the deployed AAAA record." - }, - "value": "[parameters('name')]" }, - "resourceId": { - "type": "string", - "metadata": { - "description": "The resource ID of the deployed AAAA record." + "publicIpAddress_diagnosticSettings": { + "copy": { + "name": "publicIpAddress_diagnosticSettings", + "count": "[length(coalesce(parameters('diagnosticSettings'), createArray()))]" }, - "value": "[resourceId('Microsoft.Network/privateDnsZones/AAAA', parameters('privateDnsZoneName'), parameters('name'))]" - }, - "resourceGroupName": { - "type": "string", - "metadata": { - "description": "The resource group of the deployed AAAA record." - }, - "value": "[resourceGroup().name]" - } - } - } - }, - "dependsOn": [ - "privateDnsZone" - ] - }, - "privateDnsZone_CNAME": { - "copy": { - "name": "privateDnsZone_CNAME", - "count": "[length(coalesce(parameters('cname'), createArray()))]" - }, - "type": "Microsoft.Resources/deployments", - "apiVersion": "2022-09-01", - "name": "[format('{0}-PrivateDnsZone-CNAMERecord-{1}', uniqueString(deployment().name, parameters('location')), copyIndex())]", - "properties": { - "expressionEvaluationOptions": { - "scope": "inner" - }, - "mode": "Incremental", - "parameters": { - "privateDnsZoneName": { - "value": "[parameters('name')]" - }, - "name": { - "value": "[coalesce(parameters('cname'), createArray())[copyIndex()].name]" - }, - "cnameRecord": { - "value": "[tryGet(coalesce(parameters('cname'), createArray())[copyIndex()], 'cnameRecord')]" - }, - "metadata": { - "value": "[tryGet(coalesce(parameters('cname'), createArray())[copyIndex()], 'metadata')]" - }, - "ttl": { - "value": "[coalesce(tryGet(coalesce(parameters('cname'), createArray())[copyIndex()], 'ttl'), 3600)]" - }, - "roleAssignments": { - "value": "[tryGet(coalesce(parameters('cname'), createArray())[copyIndex()], 'roleAssignments')]" - }, - "enableTelemetry": { - "value": "[variables('enableReferencedModulesTelemetry')]" - } - }, - "template": { - "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", - "languageVersion": "2.0", - "contentVersion": "1.0.0.0", - "metadata": { - "_generator": { - "name": "bicep", - "version": "0.37.4.10188", - "templateHash": "13307906270868460967" - }, - "name": "Private DNS Zone CNAME record", - "description": "This module deploys a Private DNS Zone CNAME record." - }, - "definitions": { - "roleAssignmentType": { - "type": "object", + "type": "Microsoft.Insights/diagnosticSettings", + "apiVersion": "2021-05-01-preview", + "scope": "[format('Microsoft.Network/publicIPAddresses/{0}', parameters('name'))]", + "name": "[coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'name'), format('{0}-diagnosticSettings', parameters('name')))]", "properties": { - "name": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The name (as GUID) of the role assignment. If not provided, a GUID will be generated." - } - }, - "roleDefinitionIdOrName": { - "type": "string", - "metadata": { - "description": "Required. The role to assign. You can provide either the display name of the role definition, the role definition GUID, or its fully qualified ID in the following format: '/providers/Microsoft.Authorization/roleDefinitions/c2f4ef07-c644-48eb-af81-4b1b4947fb11'." - } - }, - "principalId": { - "type": "string", - "metadata": { - "description": "Required. The principal ID of the principal (user/group/identity) to assign the role to." - } - }, - "principalType": { - "type": "string", - "allowedValues": [ - "Device", - "ForeignGroup", - "Group", - "ServicePrincipal", - "User" - ], - "nullable": true, - "metadata": { - "description": "Optional. The principal type of the assigned principal ID." - } - }, - "description": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The description of the role assignment." - } - }, - "condition": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The conditions on the role assignment. This limits the resources it can be assigned to. e.g.: @Resource[Microsoft.Storage/storageAccounts/blobServices/containers:ContainerName] StringEqualsIgnoreCase \"foo_storage_container\"." - } - }, - "conditionVersion": { - "type": "string", - "allowedValues": [ - "2.0" - ], - "nullable": true, - "metadata": { - "description": "Optional. Version of the condition." - } - }, - "delegatedManagedIdentityResourceId": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The Resource Id of the delegated managed identity resource." + "copy": [ + { + "name": "metrics", + "count": "[length(coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'metricCategories'), createArray(createObject('category', 'AllMetrics'))))]", + "input": { + "category": "[coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'metricCategories'), createArray(createObject('category', 'AllMetrics')))[copyIndex('metrics')].category]", + "enabled": "[coalesce(tryGet(coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'metricCategories'), createArray(createObject('category', 'AllMetrics')))[copyIndex('metrics')], 'enabled'), true())]", + "timeGrain": null + } + }, + { + "name": "logs", + "count": "[length(coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'logCategoriesAndGroups'), createArray(createObject('categoryGroup', 'allLogs'))))]", + "input": { + "categoryGroup": "[tryGet(coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'logCategoriesAndGroups'), createArray(createObject('categoryGroup', 'allLogs')))[copyIndex('logs')], 'categoryGroup')]", + "category": "[tryGet(coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'logCategoriesAndGroups'), createArray(createObject('categoryGroup', 'allLogs')))[copyIndex('logs')], 'category')]", + "enabled": "[coalesce(tryGet(coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'logCategoriesAndGroups'), createArray(createObject('categoryGroup', 'allLogs')))[copyIndex('logs')], 'enabled'), true())]" + } } - } + ], + "storageAccountId": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'storageAccountResourceId')]", + "workspaceId": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'workspaceResourceId')]", + "eventHubAuthorizationRuleId": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'eventHubAuthorizationRuleResourceId')]", + "eventHubName": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'eventHubName')]", + "marketplacePartnerId": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'marketplacePartnerResourceId')]", + "logAnalyticsDestinationType": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'logAnalyticsDestinationType')]" }, - "metadata": { - "description": "An AVM-aligned type for a role assignment.", - "__bicep_imported_from!": { - "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.5.1" - } - } + "dependsOn": [ + "publicIpAddress" + ] } }, - "parameters": { - "privateDnsZoneName": { + "outputs": { + "resourceGroupName": { "type": "string", "metadata": { - "description": "Conditional. The name of the parent Private DNS zone. Required if the template is used in a standalone deployment." - } + "description": "The resource group the public IP address was deployed into." + }, + "value": "[resourceGroup().name]" }, "name": { "type": "string", "metadata": { - "description": "Required. The name of the CNAME record." - } - }, - "cnameRecord": { - "type": "object", - "metadata": { - "__bicep_resource_derived_type!": { - "source": "Microsoft.Network/privateDnsZones/CNAME@2024-06-01#properties/properties/properties/cnameRecord" - }, - "description": "Optional. A CNAME record." - }, - "nullable": true - }, - "metadata": { - "type": "object", - "metadata": { - "__bicep_resource_derived_type!": { - "source": "Microsoft.Network/privateDnsZones/CNAME@2024-06-01#properties/properties/properties/metadata" - }, - "description": "Optional. The metadata attached to the record set." - }, - "nullable": true - }, - "ttl": { - "type": "int", - "defaultValue": 3600, - "metadata": { - "description": "Optional. The TTL (time-to-live) of the records in the record set." - } - }, - "enableTelemetry": { - "type": "bool", - "defaultValue": true, - "metadata": { - "description": "Optional. Enable/Disable usage telemetry for module." - } - }, - "roleAssignments": { - "type": "array", - "items": { - "$ref": "#/definitions/roleAssignmentType" + "description": "The name of the public IP address." }, - "nullable": true, - "metadata": { - "description": "Optional. Array of role assignments to create." - } - } - }, - "variables": { - "copy": [ - { - "name": "formattedRoleAssignments", - "count": "[length(coalesce(parameters('roleAssignments'), createArray()))]", - "input": "[union(coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')], createObject('roleDefinitionId', coalesce(tryGet(variables('builtInRoleNames'), coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName), if(contains(coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName, '/providers/Microsoft.Authorization/roleDefinitions/'), coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName, subscriptionResourceId('Microsoft.Authorization/roleDefinitions', coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName)))))]" - } - ], - "builtInRoleNames": { - "Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'b24988ac-6180-42a0-ab88-20f7382dd24c')]", - "Private DNS Zone Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'b12aa53e-6015-4669-85d0-8515ebb3ae7f')]", - "Network Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '4d97b98b-1d4f-4787-a291-c67834d212e7')]", - "Owner": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '8e3af657-a8ff-443c-a75c-2fe8c4bcb635')]", - "Reader": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'acdd72a7-3385-48ef-bd42-f606fba81ae7')]", - "Role Based Access Control Administrator": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'f58310d9-a9f6-439a-9e8d-f62e7b41a168')]", - "User Access Administrator": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '18d7d88d-d35e-4fb5-a5c3-7773c20a72d9')]" - } - }, - "resources": { - "avmTelemetry": { - "condition": "[parameters('enableTelemetry')]", - "type": "Microsoft.Resources/deployments", - "apiVersion": "2024-03-01", - "name": "[format('46d3xbcp.res.nw-privdnszonecname.{0}.{1}', replace('0.1.0', '.', '-'), substring(uniqueString(deployment().name), 0, 4))]", - "properties": { - "mode": "Incremental", - "template": { - "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", - "contentVersion": "1.0.0.0", - "resources": [], - "outputs": { - "telemetry": { - "type": "String", - "value": "For more information, see https://aka.ms/avm/TelemetryInfo" - } - } - } - } - }, - "privateDnsZone": { - "existing": true, - "type": "Microsoft.Network/privateDnsZones", - "apiVersion": "2020-06-01", - "name": "[parameters('privateDnsZoneName')]" - }, - "CNAME": { - "type": "Microsoft.Network/privateDnsZones/CNAME", - "apiVersion": "2020-06-01", - "name": "[format('{0}/{1}', parameters('privateDnsZoneName'), parameters('name'))]", - "properties": { - "cnameRecord": "[parameters('cnameRecord')]", - "metadata": "[parameters('metadata')]", - "ttl": "[parameters('ttl')]" - } + "value": "[parameters('name')]" }, - "CNAME_roleAssignments": { - "copy": { - "name": "CNAME_roleAssignments", - "count": "[length(coalesce(variables('formattedRoleAssignments'), createArray()))]" - }, - "type": "Microsoft.Authorization/roleAssignments", - "apiVersion": "2022-04-01", - "scope": "[format('Microsoft.Network/privateDnsZones/{0}/CNAME/{1}', parameters('privateDnsZoneName'), parameters('name'))]", - "name": "[coalesce(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'name'), guid(resourceId('Microsoft.Network/privateDnsZones/CNAME', parameters('privateDnsZoneName'), parameters('name')), coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].principalId, coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].roleDefinitionId))]", - "properties": { - "roleDefinitionId": "[coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].roleDefinitionId]", - "principalId": "[coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].principalId]", - "description": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'description')]", - "principalType": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'principalType')]", - "condition": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'condition')]", - "conditionVersion": "[if(not(empty(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'condition'))), coalesce(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'conditionVersion'), '2.0'), null())]", - "delegatedManagedIdentityResourceId": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'delegatedManagedIdentityResourceId')]" - }, - "dependsOn": [ - "CNAME" - ] - } - }, - "outputs": { - "name": { + "resourceId": { "type": "string", "metadata": { - "description": "The name of the deployed CNAME record." + "description": "The resource ID of the public IP address." }, - "value": "[parameters('name')]" + "value": "[resourceId('Microsoft.Network/publicIPAddresses', parameters('name'))]" }, - "resourceId": { + "ipAddress": { "type": "string", "metadata": { - "description": "The resource ID of the deployed CNAME record." + "description": "The public IP address of the public IP address resource." }, - "value": "[resourceId('Microsoft.Network/privateDnsZones/CNAME', parameters('privateDnsZoneName'), parameters('name'))]" + "value": "[coalesce(tryGet(reference('publicIpAddress'), 'ipAddress'), '')]" }, - "resourceGroupName": { + "location": { "type": "string", "metadata": { - "description": "The resource group of the deployed CNAME record." + "description": "The location the resource was deployed into." }, - "value": "[resourceGroup().name]" + "value": "[reference('publicIpAddress', '2025-01-01', 'full').location]" } } } + } + } + }, + "outputs": { + "resourceGroupName": { + "type": "string", + "metadata": { + "description": "The resource group the Azure Bastion was deployed into." }, - "dependsOn": [ - "privateDnsZone" - ] + "value": "[resourceGroup().name]" }, - "privateDnsZone_MX": { - "copy": { - "name": "privateDnsZone_MX", - "count": "[length(coalesce(parameters('mx'), createArray()))]" + "name": { + "type": "string", + "metadata": { + "description": "The name the Azure Bastion." }, - "type": "Microsoft.Resources/deployments", - "apiVersion": "2022-09-01", - "name": "[format('{0}-PrivateDnsZone-MXRecord-{1}', uniqueString(deployment().name, parameters('location')), copyIndex())]", + "value": "[parameters('name')]" + }, + "resourceId": { + "type": "string", + "metadata": { + "description": "The resource ID the Azure Bastion." + }, + "value": "[resourceId('Microsoft.Network/bastionHosts', parameters('name'))]" + }, + "location": { + "type": "string", + "metadata": { + "description": "The location the resource was deployed into." + }, + "value": "[reference('azureBastion', '2025-01-01', 'full').location]" + }, + "ipConfAzureBastionSubnet": { + "type": "object", + "metadata": { + "description": "The Public IPconfiguration object for the AzureBastionSubnet." + }, + "value": "[if(equals(parameters('skuName'), 'Developer'), createObject(), reference('azureBastion').ipConfigurations[0])]" + } + } + } + }, + "dependsOn": [ + "logAnalyticsWorkspace", + "virtualNetwork" + ] + }, + "jumpboxVM": { + "condition": "[parameters('enablePrivateNetworking')]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "[take(format('avm.res.compute.virtual-machine.{0}', variables('jumpboxVmName')), 64)]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "name": { + "value": "[take(variables('jumpboxVmName'), 15)]" + }, + "vmSize": { + "value": "[coalesce(parameters('vmSize'), 'Standard_D2s_v5')]" + }, + "location": { + "value": "[parameters('location')]" + }, + "adminUsername": { + "value": "[coalesce(parameters('vmAdminUsername'), 'JumpboxAdminUser')]" + }, + "adminPassword": { + "value": "[coalesce(parameters('vmAdminPassword'), 'JumpboxAdminP@ssw0rd1234!')]" + }, + "tags": { + "value": "[parameters('tags')]" + }, + "availabilityZone": { + "value": -1 + }, + "imageReference": { + "value": { + "publisher": "microsoft-dsvm", + "offer": "dsvm-win-2022", + "sku": "winserver-2022", + "version": "latest" + } + }, + "osType": { + "value": "Windows" + }, + "osDisk": { + "value": { + "name": "[format('osdisk-{0}', variables('jumpboxVmName'))]", + "managedDisk": { + "storageAccountType": "Standard_LRS" + } + } + }, + "encryptionAtHost": { + "value": false + }, + "nicConfigurations": { + "value": [ + { + "name": "[format('nic-{0}', variables('jumpboxVmName'))]", + "ipConfigurations": [ + { + "name": "ipconfig1", + "subnetResourceId": "[reference('virtualNetwork').outputs.jumpboxSubnetResourceId.value]" + } + ], + "diagnosticSettings": [ + { + "name": "jumpboxDiagnostics", + "workspaceResourceId": "[if(variables('useExistingLogAnalytics'), parameters('existingLogAnalyticsWorkspaceId'), if(parameters('enableMonitoring'), reference('logAnalyticsWorkspace').outputs.resourceId.value, ''))]", + "logCategoriesAndGroups": [ + { + "categoryGroup": "allLogs", + "enabled": true + } + ], + "metricCategories": [ + { + "category": "AllMetrics", + "enabled": true + } + ] + } + ] + } + ] + }, + "enableTelemetry": { + "value": "[parameters('enableTelemetry')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "languageVersion": "2.0", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.39.26.7824", + "templateHash": "11442373542874951910" + }, + "name": "Virtual Machines", + "description": "This module deploys a Virtual Machine with one or multiple NICs and optionally one or multiple public IPs." + }, + "definitions": { + "osDiskType": { + "type": "object", "properties": { - "expressionEvaluationOptions": { - "scope": "inner" + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The disk name." + } }, - "mode": "Incremental", - "parameters": { - "privateDnsZoneName": { - "value": "[parameters('name')]" - }, - "name": { - "value": "[coalesce(parameters('mx'), createArray())[copyIndex()].name]" - }, + "diskSizeGB": { + "type": "int", + "nullable": true, "metadata": { - "value": "[tryGet(coalesce(parameters('mx'), createArray())[copyIndex()], 'metadata')]" - }, - "mxRecords": { - "value": "[tryGet(coalesce(parameters('mx'), createArray())[copyIndex()], 'mxRecords')]" - }, - "ttl": { - "value": "[coalesce(tryGet(coalesce(parameters('mx'), createArray())[copyIndex()], 'ttl'), 3600)]" - }, - "roleAssignments": { - "value": "[tryGet(coalesce(parameters('mx'), createArray())[copyIndex()], 'roleAssignments')]" - }, - "enableTelemetry": { - "value": "[variables('enableReferencedModulesTelemetry')]" + "description": "Optional. Specifies the size of an empty data disk in gigabytes." } }, - "template": { - "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", - "languageVersion": "2.0", - "contentVersion": "1.0.0.0", + "createOption": { + "type": "string", + "allowedValues": [ + "Attach", + "Empty", + "FromImage" + ], + "nullable": true, "metadata": { - "_generator": { - "name": "bicep", - "version": "0.37.4.10188", - "templateHash": "7946896598573056688" - }, - "name": "Private DNS Zone MX record", - "description": "This module deploys a Private DNS Zone MX record." - }, - "definitions": { - "roleAssignmentType": { - "type": "object", - "properties": { - "name": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The name (as GUID) of the role assignment. If not provided, a GUID will be generated." - } - }, - "roleDefinitionIdOrName": { - "type": "string", - "metadata": { - "description": "Required. The role to assign. You can provide either the display name of the role definition, the role definition GUID, or its fully qualified ID in the following format: '/providers/Microsoft.Authorization/roleDefinitions/c2f4ef07-c644-48eb-af81-4b1b4947fb11'." - } - }, - "principalId": { - "type": "string", - "metadata": { - "description": "Required. The principal ID of the principal (user/group/identity) to assign the role to." - } - }, - "principalType": { - "type": "string", - "allowedValues": [ - "Device", - "ForeignGroup", - "Group", - "ServicePrincipal", - "User" - ], - "nullable": true, - "metadata": { - "description": "Optional. The principal type of the assigned principal ID." - } - }, - "description": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The description of the role assignment." - } - }, - "condition": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The conditions on the role assignment. This limits the resources it can be assigned to. e.g.: @Resource[Microsoft.Storage/storageAccounts/blobServices/containers:ContainerName] StringEqualsIgnoreCase \"foo_storage_container\"." - } - }, - "conditionVersion": { - "type": "string", - "allowedValues": [ - "2.0" - ], - "nullable": true, - "metadata": { - "description": "Optional. Version of the condition." - } - }, - "delegatedManagedIdentityResourceId": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The Resource Id of the delegated managed identity resource." - } - } - }, + "description": "Optional. Specifies how the virtual machine should be created." + } + }, + "deleteOption": { + "type": "string", + "allowedValues": [ + "Delete", + "Detach" + ], + "nullable": true, + "metadata": { + "description": "Optional. Specifies whether data disk should be deleted or detached upon VM deletion." + } + }, + "caching": { + "type": "string", + "allowedValues": [ + "None", + "ReadOnly", + "ReadWrite" + ], + "nullable": true, + "metadata": { + "description": "Optional. Specifies the caching requirements." + } + }, + "diffDiskSettings": { + "type": "object", + "properties": { + "placement": { + "type": "string", + "allowedValues": [ + "CacheDisk", + "NvmeDisk", + "ResourceDisk" + ], "metadata": { - "description": "An AVM-aligned type for a role assignment.", - "__bicep_imported_from!": { - "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.5.1" - } + "description": "Required. Specifies the ephemeral disk placement for the operating system disk." } } }, - "parameters": { - "privateDnsZoneName": { + "nullable": true, + "metadata": { + "description": "Optional. Specifies the ephemeral Disk Settings for the operating system disk." + } + }, + "managedDisk": { + "type": "object", + "properties": { + "storageAccountType": { "type": "string", + "allowedValues": [ + "PremiumV2_LRS", + "Premium_LRS", + "Premium_ZRS", + "StandardSSD_LRS", + "StandardSSD_ZRS", + "Standard_LRS", + "UltraSSD_LRS" + ], + "nullable": true, "metadata": { - "description": "Conditional. The name of the parent Private DNS zone. Required if the template is used in a standalone deployment." + "description": "Optional. Specifies the storage account type for the managed disk." } }, - "name": { + "diskEncryptionSetResourceId": { "type": "string", + "nullable": true, "metadata": { - "description": "Required. The name of the MX record." - } - }, - "metadata": { - "type": "object", - "metadata": { - "__bicep_resource_derived_type!": { - "source": "Microsoft.Network/privateDnsZones/MX@2024-06-01#properties/properties/properties/metadata" - }, - "description": "Optional. The metadata attached to the record set." - }, - "nullable": true - }, - "mxRecords": { - "type": "array", - "metadata": { - "__bicep_resource_derived_type!": { - "source": "Microsoft.Network/privateDnsZones/MX@2024-06-01#properties/properties/properties/mxRecords" - }, - "description": "Optional. The list of MX records in the record set." - }, - "nullable": true - }, - "ttl": { - "type": "int", - "defaultValue": 3600, - "metadata": { - "description": "Optional. The TTL (time-to-live) of the records in the record set." - } - }, - "enableTelemetry": { - "type": "bool", - "defaultValue": true, - "metadata": { - "description": "Optional. Enable/Disable usage telemetry for module." + "description": "Optional. Specifies the customer managed disk encryption set resource id for the managed disk." } }, - "roleAssignments": { - "type": "array", - "items": { - "$ref": "#/definitions/roleAssignmentType" - }, + "resourceId": { + "type": "string", "nullable": true, "metadata": { - "description": "Optional. Array of role assignments to create." - } - } - }, - "variables": { - "copy": [ - { - "name": "formattedRoleAssignments", - "count": "[length(coalesce(parameters('roleAssignments'), createArray()))]", - "input": "[union(coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')], createObject('roleDefinitionId', coalesce(tryGet(variables('builtInRoleNames'), coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName), if(contains(coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName, '/providers/Microsoft.Authorization/roleDefinitions/'), coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName, subscriptionResourceId('Microsoft.Authorization/roleDefinitions', coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName)))))]" - } - ], - "builtInRoleNames": { - "Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'b24988ac-6180-42a0-ab88-20f7382dd24c')]", - "Private DNS Zone Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'b12aa53e-6015-4669-85d0-8515ebb3ae7f')]", - "Network Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '4d97b98b-1d4f-4787-a291-c67834d212e7')]", - "Owner": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '8e3af657-a8ff-443c-a75c-2fe8c4bcb635')]", - "Reader": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'acdd72a7-3385-48ef-bd42-f606fba81ae7')]", - "Role Based Access Control Administrator": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'f58310d9-a9f6-439a-9e8d-f62e7b41a168')]", - "User Access Administrator": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '18d7d88d-d35e-4fb5-a5c3-7773c20a72d9')]" - } - }, - "resources": { - "avmTelemetry": { - "condition": "[parameters('enableTelemetry')]", - "type": "Microsoft.Resources/deployments", - "apiVersion": "2024-03-01", - "name": "[format('46d3xbcp.res.nw-privdnszonemx.{0}.{1}', replace('0.1.0', '.', '-'), substring(uniqueString(deployment().name), 0, 4))]", - "properties": { - "mode": "Incremental", - "template": { - "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", - "contentVersion": "1.0.0.0", - "resources": [], - "outputs": { - "telemetry": { - "type": "String", - "value": "For more information, see https://aka.ms/avm/TelemetryInfo" - } - } - } - } - }, - "privateDnsZone": { - "existing": true, - "type": "Microsoft.Network/privateDnsZones", - "apiVersion": "2020-06-01", - "name": "[parameters('privateDnsZoneName')]" - }, - "MX": { - "type": "Microsoft.Network/privateDnsZones/MX", - "apiVersion": "2020-06-01", - "name": "[format('{0}/{1}', parameters('privateDnsZoneName'), parameters('name'))]", - "properties": { - "metadata": "[parameters('metadata')]", - "mxRecords": "[parameters('mxRecords')]", - "ttl": "[parameters('ttl')]" + "description": "Optional. Specifies the resource id of a pre-existing managed disk. If the disk should be created, this property should be empty." } - }, - "MX_roleAssignments": { - "copy": { - "name": "MX_roleAssignments", - "count": "[length(coalesce(variables('formattedRoleAssignments'), createArray()))]" - }, - "type": "Microsoft.Authorization/roleAssignments", - "apiVersion": "2022-04-01", - "scope": "[format('Microsoft.Network/privateDnsZones/{0}/MX/{1}', parameters('privateDnsZoneName'), parameters('name'))]", - "name": "[coalesce(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'name'), guid(resourceId('Microsoft.Network/privateDnsZones/MX', parameters('privateDnsZoneName'), parameters('name')), coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].principalId, coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].roleDefinitionId))]", - "properties": { - "roleDefinitionId": "[coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].roleDefinitionId]", - "principalId": "[coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].principalId]", - "description": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'description')]", - "principalType": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'principalType')]", - "condition": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'condition')]", - "conditionVersion": "[if(not(empty(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'condition'))), coalesce(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'conditionVersion'), '2.0'), null())]", - "delegatedManagedIdentityResourceId": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'delegatedManagedIdentityResourceId')]" - }, - "dependsOn": [ - "MX" - ] } }, - "outputs": { - "name": { + "metadata": { + "description": "Required. The managed disk parameters." + } + } + }, + "metadata": { + "__bicep_export!": true, + "description": "The type describing an OS disk." + } + }, + "dataDiskType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The disk name. When attaching a pre-existing disk, this name is ignored and the name of the existing disk is used." + } + }, + "lun": { + "type": "int", + "nullable": true, + "metadata": { + "description": "Optional. Specifies the logical unit number of the data disk." + } + }, + "diskSizeGB": { + "type": "int", + "nullable": true, + "metadata": { + "description": "Optional. Specifies the size of an empty data disk in gigabytes. This property is ignored when attaching a pre-existing disk." + } + }, + "createOption": { + "type": "string", + "allowedValues": [ + "Attach", + "Empty", + "FromImage" + ], + "nullable": true, + "metadata": { + "description": "Optional. Specifies how the virtual machine should be created. This property is automatically set to 'Attach' when attaching a pre-existing disk." + } + }, + "deleteOption": { + "type": "string", + "allowedValues": [ + "Delete", + "Detach" + ], + "nullable": true, + "metadata": { + "description": "Optional. Specifies whether data disk should be deleted or detached upon VM deletion. This property is automatically set to 'Detach' when attaching a pre-existing disk." + } + }, + "caching": { + "type": "string", + "allowedValues": [ + "None", + "ReadOnly", + "ReadWrite" + ], + "nullable": true, + "metadata": { + "description": "Optional. Specifies the caching requirements. This property is automatically set to 'None' when attaching a pre-existing disk." + } + }, + "diskIOPSReadWrite": { + "type": "int", + "nullable": true, + "metadata": { + "description": "Optional. The number of IOPS allowed for this disk; only settable for UltraSSD disks. One operation can transfer between 4k and 256k bytes. Ignored when attaching a pre-existing disk." + } + }, + "diskMBpsReadWrite": { + "type": "int", + "nullable": true, + "metadata": { + "description": "Optional. The bandwidth allowed for this disk; only settable for UltraSSD disks. MBps means millions of bytes per second - MB here uses the ISO notation, of powers of 10. Ignored when attaching a pre-existing disk." + } + }, + "managedDisk": { + "type": "object", + "properties": { + "storageAccountType": { "type": "string", + "allowedValues": [ + "PremiumV2_LRS", + "Premium_LRS", + "Premium_ZRS", + "StandardSSD_LRS", + "StandardSSD_ZRS", + "Standard_LRS", + "UltraSSD_LRS" + ], + "nullable": true, "metadata": { - "description": "The name of the deployed MX record." - }, - "value": "[parameters('name')]" + "description": "Optional. Specifies the storage account type for the managed disk. Ignored when attaching a pre-existing disk." + } }, - "resourceId": { + "diskEncryptionSetResourceId": { "type": "string", + "nullable": true, "metadata": { - "description": "The resource ID of the deployed MX record." - }, - "value": "[resourceId('Microsoft.Network/privateDnsZones/MX', parameters('privateDnsZoneName'), parameters('name'))]" + "description": "Optional. Specifies the customer managed disk encryption set resource id for the managed disk." + } }, - "resourceGroupName": { + "resourceId": { "type": "string", + "nullable": true, "metadata": { - "description": "The resource group of the deployed MX record." - }, - "value": "[resourceGroup().name]" + "description": "Optional. Specifies the resource id of a pre-existing managed disk. If the disk should be created, this property should be empty." + } } + }, + "metadata": { + "description": "Required. The managed disk parameters." } + }, + "tags": { + "type": "object", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.Compute/disks@2025-01-02#properties/tags" + }, + "description": "Optional. The tags of the public IP address. Valid only when creating a new managed disk." + }, + "nullable": true } }, - "dependsOn": [ - "privateDnsZone" - ] + "metadata": { + "__bicep_export!": true, + "description": "The type describing a data disk." + } }, - "privateDnsZone_PTR": { - "copy": { - "name": "privateDnsZone_PTR", - "count": "[length(coalesce(parameters('ptr'), createArray()))]" - }, - "type": "Microsoft.Resources/deployments", - "apiVersion": "2022-09-01", - "name": "[format('{0}-PrivateDnsZone-PTRRecord-{1}', uniqueString(deployment().name, parameters('location')), copyIndex())]", + "publicKeyType": { + "type": "object", "properties": { - "expressionEvaluationOptions": { - "scope": "inner" + "keyData": { + "type": "string", + "metadata": { + "description": "Required. Specifies the SSH public key data used to authenticate through ssh." + } }, - "mode": "Incremental", - "parameters": { - "privateDnsZoneName": { - "value": "[parameters('name')]" - }, - "name": { - "value": "[coalesce(parameters('ptr'), createArray())[copyIndex()].name]" - }, + "path": { + "type": "string", "metadata": { - "value": "[tryGet(coalesce(parameters('ptr'), createArray())[copyIndex()], 'metadata')]" - }, - "ptrRecords": { - "value": "[tryGet(coalesce(parameters('ptr'), createArray())[copyIndex()], 'ptrRecords')]" - }, - "ttl": { - "value": "[coalesce(tryGet(coalesce(parameters('ptr'), createArray())[copyIndex()], 'ttl'), 3600)]" - }, - "roleAssignments": { - "value": "[tryGet(coalesce(parameters('ptr'), createArray())[copyIndex()], 'roleAssignments')]" + "description": "Required. Specifies the full path on the created VM where ssh public key is stored. If the file already exists, the specified key is appended to the file." + } + } + } + }, + "nicConfigurationType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The name of the NIC configuration." + } + }, + "nicSuffix": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The suffix to append to the NIC name." + } + }, + "enableIPForwarding": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. Indicates whether IP forwarding is enabled on this network interface." + } + }, + "enableAcceleratedNetworking": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. If the network interface is accelerated networking enabled." + } + }, + "deleteOption": { + "type": "string", + "allowedValues": [ + "Delete", + "Detach" + ], + "nullable": true, + "metadata": { + "description": "Optional. Specify what happens to the network interface when the VM is deleted." + } + }, + "dnsServers": { + "type": "array", + "items": { + "type": "string" }, - "enableTelemetry": { - "value": "[variables('enableReferencedModulesTelemetry')]" + "nullable": true, + "metadata": { + "description": "Optional. List of DNS servers IP addresses. Use 'AzureProvidedDNS' to switch to azure provided DNS resolution. 'AzureProvidedDNS' value cannot be combined with other IPs, it must be the only value in dnsServers collection." } }, - "template": { - "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", - "languageVersion": "2.0", - "contentVersion": "1.0.0.0", + "networkSecurityGroupResourceId": { + "type": "string", + "nullable": true, "metadata": { - "_generator": { - "name": "bicep", - "version": "0.37.4.10188", - "templateHash": "7627375510490151870" - }, - "name": "Private DNS Zone PTR record", - "description": "This module deploys a Private DNS Zone PTR record." + "description": "Optional. The network security group (NSG) to attach to the network interface." + } + }, + "ipConfigurations": { + "type": "array", + "items": { + "$ref": "#/definitions/ipConfigurationType" }, - "definitions": { - "roleAssignmentType": { - "type": "object", - "properties": { - "name": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The name (as GUID) of the role assignment. If not provided, a GUID will be generated." - } - }, - "roleDefinitionIdOrName": { - "type": "string", - "metadata": { - "description": "Required. The role to assign. You can provide either the display name of the role definition, the role definition GUID, or its fully qualified ID in the following format: '/providers/Microsoft.Authorization/roleDefinitions/c2f4ef07-c644-48eb-af81-4b1b4947fb11'." - } - }, - "principalId": { - "type": "string", - "metadata": { - "description": "Required. The principal ID of the principal (user/group/identity) to assign the role to." - } - }, - "principalType": { - "type": "string", - "allowedValues": [ - "Device", - "ForeignGroup", - "Group", - "ServicePrincipal", - "User" - ], - "nullable": true, - "metadata": { - "description": "Optional. The principal type of the assigned principal ID." - } - }, - "description": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The description of the role assignment." - } - }, - "condition": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The conditions on the role assignment. This limits the resources it can be assigned to. e.g.: @Resource[Microsoft.Storage/storageAccounts/blobServices/containers:ContainerName] StringEqualsIgnoreCase \"foo_storage_container\"." - } - }, - "conditionVersion": { - "type": "string", - "allowedValues": [ - "2.0" - ], - "nullable": true, - "metadata": { - "description": "Optional. Version of the condition." - } - }, - "delegatedManagedIdentityResourceId": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The Resource Id of the delegated managed identity resource." - } - } - }, - "metadata": { - "description": "An AVM-aligned type for a role assignment.", - "__bicep_imported_from!": { - "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.5.1" - } - } - } + "metadata": { + "description": "Required. The IP configurations of the network interface." + } + }, + "lock": { + "$ref": "#/definitions/lockType", + "nullable": true, + "metadata": { + "description": "Optional. The lock settings of the service." + } + }, + "tags": { + "type": "object", + "nullable": true, + "metadata": { + "description": "Optional. The tags of the public IP address." + } + }, + "enableTelemetry": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. Enable/Disable usage telemetry for the module." + } + }, + "diagnosticSettings": { + "type": "array", + "items": { + "$ref": "#/definitions/diagnosticSettingFullType" }, - "parameters": { - "privateDnsZoneName": { + "nullable": true, + "metadata": { + "description": "Optional. The diagnostic settings of the IP configuration." + } + }, + "roleAssignments": { + "type": "array", + "items": { + "$ref": "#/definitions/roleAssignmentType" + }, + "nullable": true, + "metadata": { + "description": "Optional. Array of role assignments to create." + } + } + }, + "metadata": { + "__bicep_export!": true, + "description": "The type for the NIC configuration." + } + }, + "imageReferenceType": { + "type": "object", + "properties": { + "communityGalleryImageId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Specified the community gallery image unique id for vm deployment. This can be fetched from community gallery image GET call." + } + }, + "id": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The resource Id of the image reference." + } + }, + "offer": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Specifies the offer of the platform image or marketplace image used to create the virtual machine." + } + }, + "publisher": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The image publisher." + } + }, + "sku": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The SKU of the image." + } + }, + "version": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Specifies the version of the platform image or marketplace image used to create the virtual machine. The allowed formats are Major.Minor.Build or 'latest'. Even if you use 'latest', the VM image will not automatically update after deploy time even if a new version becomes available." + } + }, + "sharedGalleryImageId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Specified the shared gallery image unique id for vm deployment. This can be fetched from shared gallery image GET call." + } + } + }, + "metadata": { + "__bicep_export!": true, + "description": "The type describing the image reference." + } + }, + "planType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The name of the plan." + } + }, + "product": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Specifies the product of the image from the marketplace." + } + }, + "publisher": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The publisher ID." + } + }, + "promotionCode": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The promotion code." + } + } + }, + "metadata": { + "__bicep_export!": true, + "description": "Specifies information about the marketplace image used to create the virtual machine." + } + }, + "autoShutDownConfigType": { + "type": "object", + "properties": { + "status": { + "type": "string", + "allowedValues": [ + "Disabled", + "Enabled" + ], + "nullable": true, + "metadata": { + "description": "Optional. The status of the auto shutdown configuration." + } + }, + "timeZone": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The time zone ID (e.g. China Standard Time, Greenland Standard Time, Pacific Standard time, etc.)." + } + }, + "dailyRecurrenceTime": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The time of day the schedule will occur." + } + }, + "notificationSettings": { + "type": "object", + "properties": { + "status": { "type": "string", + "allowedValues": [ + "Disabled", + "Enabled" + ], + "nullable": true, "metadata": { - "description": "Conditional. The name of the parent Private DNS zone. Required if the template is used in a standalone deployment." + "description": "Optional. The status of the notification settings." } }, - "name": { + "emailRecipient": { "type": "string", + "nullable": true, "metadata": { - "description": "Required. The name of the PTR record." + "description": "Optional. The email address to send notifications to (can be a list of semi-colon separated email addresses)." } }, - "metadata": { - "type": "object", - "metadata": { - "__bicep_resource_derived_type!": { - "source": "Microsoft.Network/privateDnsZones/PTR@2024-06-01#properties/properties/properties/metadata" - }, - "description": "Optional. The metadata attached to the record set." - }, - "nullable": true - }, - "ptrRecords": { - "type": "array", - "metadata": { - "__bicep_resource_derived_type!": { - "source": "Microsoft.Network/privateDnsZones/PTR@2024-06-01#properties/properties/properties/ptrRecords" - }, - "description": "Optional. The list of PTR records in the record set." - }, - "nullable": true - }, - "ttl": { - "type": "int", - "defaultValue": 3600, + "notificationLocale": { + "type": "string", + "nullable": true, "metadata": { - "description": "Optional. The TTL (time-to-live) of the records in the record set." + "description": "Optional. The locale to use when sending a notification (fallback for unsupported languages is EN)." } }, - "enableTelemetry": { - "type": "bool", - "defaultValue": true, + "webhookUrl": { + "type": "string", + "nullable": true, "metadata": { - "description": "Optional. Enable/Disable usage telemetry for module." + "description": "Optional. The webhook URL to which the notification will be sent." } }, - "roleAssignments": { - "type": "array", - "items": { - "$ref": "#/definitions/roleAssignmentType" - }, + "timeInMinutes": { + "type": "int", "nullable": true, "metadata": { - "description": "Optional. Array of role assignments to create." - } - } - }, - "variables": { - "copy": [ - { - "name": "formattedRoleAssignments", - "count": "[length(coalesce(parameters('roleAssignments'), createArray()))]", - "input": "[union(coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')], createObject('roleDefinitionId', coalesce(tryGet(variables('builtInRoleNames'), coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName), if(contains(coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName, '/providers/Microsoft.Authorization/roleDefinitions/'), coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName, subscriptionResourceId('Microsoft.Authorization/roleDefinitions', coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName)))))]" + "description": "Optional. The time in minutes before shutdown to send notifications." } - ], - "builtInRoleNames": { - "Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'b24988ac-6180-42a0-ab88-20f7382dd24c')]", - "Private DNS Zone Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'b12aa53e-6015-4669-85d0-8515ebb3ae7f')]", - "Network Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '4d97b98b-1d4f-4787-a291-c67834d212e7')]", - "Owner": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '8e3af657-a8ff-443c-a75c-2fe8c4bcb635')]", - "Reader": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'acdd72a7-3385-48ef-bd42-f606fba81ae7')]", - "Role Based Access Control Administrator": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'f58310d9-a9f6-439a-9e8d-f62e7b41a168')]", - "User Access Administrator": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '18d7d88d-d35e-4fb5-a5c3-7773c20a72d9')]" } }, - "resources": { - "avmTelemetry": { - "condition": "[parameters('enableTelemetry')]", - "type": "Microsoft.Resources/deployments", - "apiVersion": "2024-03-01", - "name": "[format('46d3xbcp.res.nw-privdnszoneptr.{0}.{1}', replace('0.1.0', '.', '-'), substring(uniqueString(deployment().name), 0, 4))]", - "properties": { - "mode": "Incremental", - "template": { - "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", - "contentVersion": "1.0.0.0", - "resources": [], - "outputs": { - "telemetry": { - "type": "String", - "value": "For more information, see https://aka.ms/avm/TelemetryInfo" - } - } + "nullable": true, + "metadata": { + "description": "Optional. The resource ID of the schedule." + } + } + }, + "metadata": { + "__bicep_export!": true, + "description": "The type describing the configuration profile." + } + }, + "vaultSecretGroupType": { + "type": "object", + "properties": { + "sourceVault": { + "$ref": "#/definitions/subResourceType", + "nullable": true, + "metadata": { + "description": "Optional. The relative URL of the Key Vault containing all of the certificates in VaultCertificates." + } + }, + "vaultCertificates": { + "type": "array", + "items": { + "type": "object", + "properties": { + "certificateStore": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. For Windows VMs, specifies the certificate store on the Virtual Machine to which the certificate should be added. The specified certificate store is implicitly in the LocalMachine account. For Linux VMs, the certificate file is placed under the /var/lib/waagent directory, with the file name .crt for the X509 certificate file and .prv for private key. Both of these files are .pem formatted." } - } - }, - "privateDnsZone": { - "existing": true, - "type": "Microsoft.Network/privateDnsZones", - "apiVersion": "2020-06-01", - "name": "[parameters('privateDnsZoneName')]" - }, - "PTR": { - "type": "Microsoft.Network/privateDnsZones/PTR", - "apiVersion": "2020-06-01", - "name": "[format('{0}/{1}', parameters('privateDnsZoneName'), parameters('name'))]", - "properties": { - "metadata": "[parameters('metadata')]", - "ptrRecords": "[parameters('ptrRecords')]", - "ttl": "[parameters('ttl')]" - } - }, - "PTR_roleAssignments": { - "copy": { - "name": "PTR_roleAssignments", - "count": "[length(coalesce(variables('formattedRoleAssignments'), createArray()))]" - }, - "type": "Microsoft.Authorization/roleAssignments", - "apiVersion": "2022-04-01", - "scope": "[format('Microsoft.Network/privateDnsZones/{0}/PTR/{1}', parameters('privateDnsZoneName'), parameters('name'))]", - "name": "[coalesce(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'name'), guid(resourceId('Microsoft.Network/privateDnsZones/PTR', parameters('privateDnsZoneName'), parameters('name')), coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].principalId, coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].roleDefinitionId))]", - "properties": { - "roleDefinitionId": "[coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].roleDefinitionId]", - "principalId": "[coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].principalId]", - "description": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'description')]", - "principalType": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'principalType')]", - "condition": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'condition')]", - "conditionVersion": "[if(not(empty(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'condition'))), coalesce(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'conditionVersion'), '2.0'), null())]", - "delegatedManagedIdentityResourceId": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'delegatedManagedIdentityResourceId')]" }, - "dependsOn": [ - "PTR" - ] + "certificateUrl": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. This is the URL of a certificate that has been uploaded to Key Vault as a secret." + } + } } }, - "outputs": { - "name": { - "type": "string", - "metadata": { - "description": "The name of the deployed PTR record." - }, - "value": "[parameters('name')]" - }, - "resourceId": { - "type": "string", - "metadata": { - "description": "The resource ID of the deployed PTR record." - }, - "value": "[resourceId('Microsoft.Network/privateDnsZones/PTR', parameters('privateDnsZoneName'), parameters('name'))]" - }, - "resourceGroupName": { - "type": "string", - "metadata": { - "description": "The resource group of the deployed PTR record." - }, - "value": "[resourceGroup().name]" - } + "nullable": true, + "metadata": { + "description": "Optional. The list of key vault references in SourceVault which contain certificates." } } }, - "dependsOn": [ - "privateDnsZone" - ] + "metadata": { + "__bicep_export!": true, + "description": "The type describing the set of certificates that should be installed onto the virtual machine." + } }, - "privateDnsZone_SOA": { - "copy": { - "name": "privateDnsZone_SOA", - "count": "[length(coalesce(parameters('soa'), createArray()))]" - }, - "type": "Microsoft.Resources/deployments", - "apiVersion": "2022-09-01", - "name": "[format('{0}-PrivateDnsZone-SOARecord-{1}', uniqueString(deployment().name, parameters('location')), copyIndex())]", + "vmGalleryApplicationType": { + "type": "object", "properties": { - "expressionEvaluationOptions": { - "scope": "inner" + "packageReferenceId": { + "type": "string", + "metadata": { + "description": "Required. Specifies the GalleryApplicationVersion resource id on the form of /subscriptions/{SubscriptionId}/resourceGroups/{ResourceGroupName}/providers/Microsoft.Compute/galleries/{galleryName}/applications/{application}/versions/{version}." + } }, - "mode": "Incremental", - "parameters": { - "privateDnsZoneName": { - "value": "[parameters('name')]" - }, - "name": { - "value": "[coalesce(parameters('soa'), createArray())[copyIndex()].name]" - }, + "configurationReference": { + "type": "string", + "nullable": true, "metadata": { - "value": "[tryGet(coalesce(parameters('soa'), createArray())[copyIndex()], 'metadata')]" - }, - "soaRecord": { - "value": "[tryGet(coalesce(parameters('soa'), createArray())[copyIndex()], 'soaRecord')]" - }, - "ttl": { - "value": "[coalesce(tryGet(coalesce(parameters('soa'), createArray())[copyIndex()], 'ttl'), 3600)]" - }, - "roleAssignments": { - "value": "[tryGet(coalesce(parameters('soa'), createArray())[copyIndex()], 'roleAssignments')]" - }, - "enableTelemetry": { - "value": "[variables('enableReferencedModulesTelemetry')]" + "description": "Optional. Specifies the uri to an azure blob that will replace the default configuration for the package if provided." } }, - "template": { - "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", - "languageVersion": "2.0", - "contentVersion": "1.0.0.0", + "enableAutomaticUpgrade": { + "type": "bool", + "nullable": true, "metadata": { - "_generator": { - "name": "bicep", - "version": "0.37.4.10188", - "templateHash": "16709883266329935583" - }, - "name": "Private DNS Zone SOA record", - "description": "This module deploys a Private DNS Zone SOA record." - }, - "definitions": { - "roleAssignmentType": { - "type": "object", - "properties": { - "name": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The name (as GUID) of the role assignment. If not provided, a GUID will be generated." - } - }, - "roleDefinitionIdOrName": { - "type": "string", - "metadata": { - "description": "Required. The role to assign. You can provide either the display name of the role definition, the role definition GUID, or its fully qualified ID in the following format: '/providers/Microsoft.Authorization/roleDefinitions/c2f4ef07-c644-48eb-af81-4b1b4947fb11'." - } - }, - "principalId": { - "type": "string", - "metadata": { - "description": "Required. The principal ID of the principal (user/group/identity) to assign the role to." - } - }, - "principalType": { - "type": "string", - "allowedValues": [ - "Device", - "ForeignGroup", - "Group", - "ServicePrincipal", - "User" - ], - "nullable": true, - "metadata": { - "description": "Optional. The principal type of the assigned principal ID." - } - }, - "description": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The description of the role assignment." - } - }, - "condition": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The conditions on the role assignment. This limits the resources it can be assigned to. e.g.: @Resource[Microsoft.Storage/storageAccounts/blobServices/containers:ContainerName] StringEqualsIgnoreCase \"foo_storage_container\"." - } - }, - "conditionVersion": { - "type": "string", - "allowedValues": [ - "2.0" - ], - "nullable": true, - "metadata": { - "description": "Optional. Version of the condition." - } - }, - "delegatedManagedIdentityResourceId": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The Resource Id of the delegated managed identity resource." - } - } + "description": "Optional. If set to true, when a new Gallery Application version is available in PIR/SIG, it will be automatically updated for the VM/VMSS." + } + }, + "order": { + "type": "int", + "nullable": true, + "metadata": { + "description": "Optional. Specifies the order in which the packages have to be installed." + } + }, + "tags": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Specifies a passthrough value for more generic context." + } + }, + "treatFailureAsDeploymentFailure": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. If true, any failure for any operation in the VmApplication will fail the deployment." + } + } + }, + "metadata": { + "__bicep_export!": true, + "description": "The type describing the gallery application that should be made available to the VM/VMSS." + } + }, + "additionalUnattendContentType": { + "type": "object", + "properties": { + "settingName": { + "type": "string", + "allowedValues": [ + "AutoLogon", + "FirstLogonCommands" + ], + "nullable": true, + "metadata": { + "description": "Optional. Specifies the name of the setting to which the content applies." + } + }, + "content": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Specifies the XML formatted content that is added to the unattend.xml file for the specified path and component. The XML must be less than 4KB and must include the root element for the setting or feature that is being inserted." + } + } + }, + "metadata": { + "__bicep_export!": true, + "description": "The type describing additional base-64 encoded XML formatted information that can be included in the Unattend.xml file, which is used by Windows Setup." + } + }, + "winRMListenerType": { + "type": "object", + "properties": { + "certificateUrl": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The URL of a certificate that has been uploaded to Key Vault as a secret." + } + }, + "protocol": { + "type": "string", + "allowedValues": [ + "Http", + "Https" + ], + "nullable": true, + "metadata": { + "description": "Optional. Specifies the protocol of WinRM listener." + } + } + }, + "metadata": { + "__bicep_export!": true, + "description": "The type describing a Windows Remote Management listener." + } + }, + "nicConfigurationOutputType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "metadata": { + "description": "Required. The name of the NIC configuration." + } + }, + "ipConfigurations": { + "type": "array", + "items": { + "$ref": "#/definitions/networkInterfaceIPConfigurationOutputType" + }, + "metadata": { + "description": "Required. List of IP configurations of the NIC configuration." + } + } + }, + "metadata": { + "__bicep_export!": true, + "description": "The type describing the network interface configuration output." + } + }, + "extensionCustomScriptConfigType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The name of the virtual machine extension. Defaults to `CustomScriptExtension`." + } + }, + "typeHandlerVersion": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Specifies the version of the script handler. Defaults to `1.10` for Windows and `2.1` for Linux." + } + }, + "autoUpgradeMinorVersion": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. Indicates whether the extension should use a newer minor version if one is available at deployment time. Once deployed, however, the extension will not upgrade minor versions unless redeployed, even with this property set to true. Defaults to `true`." + } + }, + "forceUpdateTag": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. How the extension handler should be forced to update even if the extension configuration has not changed." + } + }, + "settings": { + "type": "object", + "properties": { + "commandToExecute": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Conditional. The entry point script to run. If the command contains any credentials, use the same property of the `protectedSettings` instead. Required if `protectedSettings.commandToExecute` is not provided." + } + }, + "fileUris": { + "type": "array", + "items": { + "type": "string" }, + "nullable": true, "metadata": { - "description": "An AVM-aligned type for a role assignment.", - "__bicep_imported_from!": { - "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.5.1" - } + "description": "Optional. URLs for files to be downloaded. If URLs are sensitive, for example, if they contain keys, this field should be specified in `protectedSettings`." } } }, - "parameters": { - "privateDnsZoneName": { + "nullable": true, + "metadata": { + "description": "Optional. The configuration of the custom script extension. Note: You can provide any property either in the `settings` or `protectedSettings` but not both. If your property contains secrets, use `protectedSettings`." + } + }, + "protectedSettings": { + "type": "secureObject", + "properties": { + "commandToExecute": { "type": "string", + "nullable": true, "metadata": { - "description": "Conditional. The name of the parent Private DNS zone. Required if the template is used in a standalone deployment." + "description": "Conditional. The entry point script to run. Use this property if your command contains secrets such as passwords or if your file URIs are sensitive. Required if `settings.commandToExecute` is not provided." } }, - "name": { + "storageAccountName": { "type": "string", + "nullable": true, "metadata": { - "description": "Required. The name of the SOA record." + "description": "Optional. The name of storage account. If you specify storage credentials, all fileUris values must be URLs for Azure blobs.." } }, - "metadata": { - "type": "object", - "metadata": { - "__bicep_resource_derived_type!": { - "source": "Microsoft.Network/privateDnsZones/SOA@2024-06-01#properties/properties/properties/metadata" - }, - "description": "Optional. The metadata attached to the record set." - }, - "nullable": true - }, - "soaRecord": { - "type": "object", - "metadata": { - "__bicep_resource_derived_type!": { - "source": "Microsoft.Network/privateDnsZones/SOA@2024-06-01#properties/properties/properties/soaRecord" - }, - "description": "Optional. A SOA record." - }, - "nullable": true - }, - "ttl": { - "type": "int", - "defaultValue": 3600, + "storageAccountKey": { + "type": "string", + "nullable": true, "metadata": { - "description": "Optional. The TTL (time-to-live) of the records in the record set." + "description": "Optional. The access key of the storage account." } }, - "enableTelemetry": { - "type": "bool", - "defaultValue": true, + "managedIdentityResourceId": { + "type": "string", + "nullable": true, "metadata": { - "description": "Optional. Enable/Disable usage telemetry for module." + "description": "Optional. The managed identity for downloading files. Must not be used in conjunction with the `storageAccountName` or `storageAccountKey` property. If you want to use the VM's system assigned identity, set the `value` to an empty string." } }, - "roleAssignments": { + "fileUris": { "type": "array", "items": { - "$ref": "#/definitions/roleAssignmentType" + "type": "string" }, "nullable": true, "metadata": { - "description": "Optional. Array of role assignments to create." - } - } - }, - "variables": { - "copy": [ - { - "name": "formattedRoleAssignments", - "count": "[length(coalesce(parameters('roleAssignments'), createArray()))]", - "input": "[union(coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')], createObject('roleDefinitionId', coalesce(tryGet(variables('builtInRoleNames'), coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName), if(contains(coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName, '/providers/Microsoft.Authorization/roleDefinitions/'), coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName, subscriptionResourceId('Microsoft.Authorization/roleDefinitions', coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName)))))]" + "description": "Optional. URLs for files to be downloaded." } - ], - "builtInRoleNames": { - "Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'b24988ac-6180-42a0-ab88-20f7382dd24c')]", - "Private DNS Zone Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'b12aa53e-6015-4669-85d0-8515ebb3ae7f')]", - "Network Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '4d97b98b-1d4f-4787-a291-c67834d212e7')]", - "Owner": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '8e3af657-a8ff-443c-a75c-2fe8c4bcb635')]", - "Reader": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'acdd72a7-3385-48ef-bd42-f606fba81ae7')]", - "Role Based Access Control Administrator": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'f58310d9-a9f6-439a-9e8d-f62e7b41a168')]", - "User Access Administrator": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '18d7d88d-d35e-4fb5-a5c3-7773c20a72d9')]" } }, - "resources": { - "avmTelemetry": { - "condition": "[parameters('enableTelemetry')]", - "type": "Microsoft.Resources/deployments", - "apiVersion": "2024-03-01", - "name": "[format('46d3xbcp.res.nw-privdnszonesoa.{0}.{1}', replace('0.1.0', '.', '-'), substring(uniqueString(deployment().name), 0, 4))]", - "properties": { - "mode": "Incremental", - "template": { - "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", - "contentVersion": "1.0.0.0", - "resources": [], - "outputs": { - "telemetry": { - "type": "String", - "value": "For more information, see https://aka.ms/avm/TelemetryInfo" - } - } - } - } - }, - "privateDnsZone": { - "existing": true, - "type": "Microsoft.Network/privateDnsZones", - "apiVersion": "2020-06-01", - "name": "[parameters('privateDnsZoneName')]" + "nullable": true, + "metadata": { + "description": "Optional. The configuration of the custom script extension. Note: You can provide any property either in the `settings` or `protectedSettings` but not both. If your property contains secrets, use `protectedSettings`." + } + }, + "supressFailures": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. Indicates whether failures stemming from the extension will be suppressed (Operational failures such as not connecting to the VM will not be suppressed regardless of this value). Defaults to `false`." + } + }, + "enableAutomaticUpgrade": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. Indicates whether the extension should be automatically upgraded by the platform if there is a newer version of the extension available. Defaults to `false`." + } + }, + "tags": { + "type": "object", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.Compute/virtualMachines/extensions@2024-11-01#properties/tags" }, - "SOA": { - "type": "Microsoft.Network/privateDnsZones/SOA", - "apiVersion": "2020-06-01", - "name": "[format('{0}/{1}', parameters('privateDnsZoneName'), parameters('name'))]", - "properties": { - "metadata": "[parameters('metadata')]", - "soaRecord": "[parameters('soaRecord')]", - "ttl": "[parameters('ttl')]" - } + "description": "Optional. Tags of the resource." + }, + "nullable": true + }, + "protectedSettingsFromKeyVault": { + "type": "object", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.Compute/virtualMachines/extensions@2024-11-01#properties/properties/properties/protectedSettingsFromKeyVault" }, - "SOA_roleAssignments": { - "copy": { - "name": "SOA_roleAssignments", - "count": "[length(coalesce(variables('formattedRoleAssignments'), createArray()))]" - }, - "type": "Microsoft.Authorization/roleAssignments", - "apiVersion": "2022-04-01", - "scope": "[format('Microsoft.Network/privateDnsZones/{0}/SOA/{1}', parameters('privateDnsZoneName'), parameters('name'))]", - "name": "[coalesce(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'name'), guid(resourceId('Microsoft.Network/privateDnsZones/SOA', parameters('privateDnsZoneName'), parameters('name')), coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].principalId, coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].roleDefinitionId))]", - "properties": { - "roleDefinitionId": "[coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].roleDefinitionId]", - "principalId": "[coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].principalId]", - "description": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'description')]", - "principalType": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'principalType')]", - "condition": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'condition')]", - "conditionVersion": "[if(not(empty(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'condition'))), coalesce(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'conditionVersion'), '2.0'), null())]", - "delegatedManagedIdentityResourceId": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'delegatedManagedIdentityResourceId')]" - }, - "dependsOn": [ - "SOA" - ] - } + "description": "Optional. The extensions protected settings that are passed by reference, and consumed from key vault." }, - "outputs": { - "name": { - "type": "string", - "metadata": { - "description": "The name of the deployed SOA record." - }, - "value": "[parameters('name')]" + "nullable": true + }, + "provisionAfterExtensions": { + "type": "array", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.Compute/virtualMachines/extensions@2024-11-01#properties/properties/properties/provisionAfterExtensions" }, - "resourceId": { - "type": "string", - "metadata": { - "description": "The resource ID of the deployed SOA record." + "description": "Optional. Collection of extension names after which this extension needs to be provisioned." + }, + "nullable": true + } + }, + "metadata": { + "__bicep_export!": true, + "description": "The type of a 'CustomScriptExtension' extension." + } + }, + "_1.applicationGatewayBackendAddressPoolsType": { + "type": "object", + "properties": { + "id": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Resource ID of the backend address pool." + } + }, + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Name of the backend address pool that is unique within an Application Gateway." + } + }, + "properties": { + "type": "object", + "properties": { + "backendAddresses": { + "type": "array", + "items": { + "type": "object", + "properties": { + "ipAddress": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. IP address of the backend address." + } + }, + "fqdn": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. FQDN of the backend address." + } + } + } }, - "value": "[resourceId('Microsoft.Network/privateDnsZones/SOA', parameters('privateDnsZoneName'), parameters('name'))]" - }, - "resourceGroupName": { - "type": "string", + "nullable": true, "metadata": { - "description": "The resource group of the deployed SOA record." - }, - "value": "[resourceGroup().name]" + "description": "Optional. Backend addresses." + } } + }, + "nullable": true, + "metadata": { + "description": "Optional. Properties of the application gateway backend address pool." } } }, - "dependsOn": [ - "privateDnsZone" - ] + "metadata": { + "description": "The type for the application gateway backend address pool.", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/res/network/network-interface:0.5.1" + } + } }, - "privateDnsZone_SRV": { - "copy": { - "name": "privateDnsZone_SRV", - "count": "[length(coalesce(parameters('srv'), createArray()))]" - }, - "type": "Microsoft.Resources/deployments", - "apiVersion": "2022-09-01", - "name": "[format('{0}-PrivateDnsZone-SRVRecord-{1}', uniqueString(deployment().name, parameters('location')), copyIndex())]", + "_1.applicationSecurityGroupType": { + "type": "object", "properties": { - "expressionEvaluationOptions": { - "scope": "inner" + "id": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Resource ID of the application security group." + } }, - "mode": "Incremental", - "parameters": { - "privateDnsZoneName": { - "value": "[parameters('name')]" - }, - "name": { - "value": "[coalesce(parameters('srv'), createArray())[copyIndex()].name]" - }, + "location": { + "type": "string", + "nullable": true, "metadata": { - "value": "[tryGet(coalesce(parameters('srv'), createArray())[copyIndex()], 'metadata')]" - }, - "srvRecords": { - "value": "[tryGet(coalesce(parameters('srv'), createArray())[copyIndex()], 'srvRecords')]" - }, - "ttl": { - "value": "[coalesce(tryGet(coalesce(parameters('srv'), createArray())[copyIndex()], 'ttl'), 3600)]" - }, - "roleAssignments": { - "value": "[tryGet(coalesce(parameters('srv'), createArray())[copyIndex()], 'roleAssignments')]" - }, - "enableTelemetry": { - "value": "[variables('enableReferencedModulesTelemetry')]" + "description": "Optional. Location of the application security group." } }, - "template": { - "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", - "languageVersion": "2.0", - "contentVersion": "1.0.0.0", + "properties": { + "type": "object", + "nullable": true, "metadata": { - "_generator": { - "name": "bicep", - "version": "0.37.4.10188", - "templateHash": "8123422724272920495" - }, - "name": "Private DNS Zone SRV record", - "description": "This module deploys a Private DNS Zone SRV record." - }, - "definitions": { - "roleAssignmentType": { - "type": "object", - "properties": { - "name": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The name (as GUID) of the role assignment. If not provided, a GUID will be generated." - } - }, - "roleDefinitionIdOrName": { - "type": "string", - "metadata": { - "description": "Required. The role to assign. You can provide either the display name of the role definition, the role definition GUID, or its fully qualified ID in the following format: '/providers/Microsoft.Authorization/roleDefinitions/c2f4ef07-c644-48eb-af81-4b1b4947fb11'." - } - }, - "principalId": { - "type": "string", - "metadata": { - "description": "Required. The principal ID of the principal (user/group/identity) to assign the role to." - } - }, - "principalType": { - "type": "string", - "allowedValues": [ - "Device", - "ForeignGroup", - "Group", - "ServicePrincipal", - "User" - ], - "nullable": true, - "metadata": { - "description": "Optional. The principal type of the assigned principal ID." - } - }, - "description": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The description of the role assignment." - } - }, - "condition": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The conditions on the role assignment. This limits the resources it can be assigned to. e.g.: @Resource[Microsoft.Storage/storageAccounts/blobServices/containers:ContainerName] StringEqualsIgnoreCase \"foo_storage_container\"." - } - }, - "conditionVersion": { - "type": "string", - "allowedValues": [ - "2.0" - ], - "nullable": true, - "metadata": { - "description": "Optional. Version of the condition." - } - }, - "delegatedManagedIdentityResourceId": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The Resource Id of the delegated managed identity resource." - } - } - }, - "metadata": { - "description": "An AVM-aligned type for a role assignment.", - "__bicep_imported_from!": { - "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.5.1" - } - } - } - }, - "parameters": { - "privateDnsZoneName": { - "type": "string", + "description": "Optional. Properties of the application security group." + } + }, + "tags": { + "type": "object", + "nullable": true, + "metadata": { + "description": "Optional. Tags of the application security group." + } + } + }, + "metadata": { + "description": "The type for the application security group.", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/res/network/network-interface:0.5.1" + } + } + }, + "_1.backendAddressPoolType": { + "type": "object", + "properties": { + "id": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The resource ID of the backend address pool." + } + }, + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The name of the backend address pool." + } + }, + "properties": { + "type": "object", + "nullable": true, + "metadata": { + "description": "Optional. The properties of the backend address pool." + } + } + }, + "metadata": { + "description": "The type for a backend address pool.", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/res/network/network-interface:0.5.1" + } + } + }, + "_1.inboundNatRuleType": { + "type": "object", + "properties": { + "id": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Resource ID of the inbound NAT rule." + } + }, + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Name of the resource that is unique within the set of inbound NAT rules used by the load balancer. This name can be used to access the resource." + } + }, + "properties": { + "type": "object", + "properties": { + "backendAddressPool": { + "$ref": "#/definitions/subResourceType", + "nullable": true, "metadata": { - "description": "Conditional. The name of the parent Private DNS zone. Required if the template is used in a standalone deployment." + "description": "Optional. A reference to backendAddressPool resource." } }, - "name": { - "type": "string", + "backendPort": { + "type": "int", + "nullable": true, "metadata": { - "description": "Required. The name of the SRV record." + "description": "Optional. The port used for the internal endpoint. Acceptable values range from 1 to 65535." } }, - "metadata": { - "type": "object", - "metadata": { - "__bicep_resource_derived_type!": { - "source": "Microsoft.Network/privateDnsZones/SRV@2024-06-01#properties/properties/properties/metadata" - }, - "description": "Optional. The metadata attached to the record set." - }, - "nullable": true - }, - "srvRecords": { - "type": "array", - "metadata": { - "__bicep_resource_derived_type!": { - "source": "Microsoft.Network/privateDnsZones/SRV@2024-06-01#properties/properties/properties/srvRecords" - }, - "description": "Optional. The list of SRV records in the record set." - }, - "nullable": true - }, - "ttl": { - "type": "int", - "defaultValue": 3600, + "enableFloatingIP": { + "type": "bool", + "nullable": true, "metadata": { - "description": "Optional. The TTL (time-to-live) of the records in the record set." + "description": "Optional. Configures a virtual machine's endpoint for the floating IP capability required to configure a SQL AlwaysOn Availability Group. This setting is required when using the SQL AlwaysOn Availability Groups in SQL server. This setting can't be changed after you create the endpoint." } }, - "enableTelemetry": { + "enableTcpReset": { "type": "bool", - "defaultValue": true, + "nullable": true, "metadata": { - "description": "Optional. Enable/Disable usage telemetry for module." + "description": "Optional. Receive bidirectional TCP Reset on TCP flow idle timeout or unexpected connection termination. This element is only used when the protocol is set to TCP." } }, - "roleAssignments": { - "type": "array", - "items": { - "$ref": "#/definitions/roleAssignmentType" - }, + "frontendIPConfiguration": { + "$ref": "#/definitions/subResourceType", "nullable": true, "metadata": { - "description": "Optional. Array of role assignments to create." - } - } - }, - "variables": { - "copy": [ - { - "name": "formattedRoleAssignments", - "count": "[length(coalesce(parameters('roleAssignments'), createArray()))]", - "input": "[union(coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')], createObject('roleDefinitionId', coalesce(tryGet(variables('builtInRoleNames'), coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName), if(contains(coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName, '/providers/Microsoft.Authorization/roleDefinitions/'), coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName, subscriptionResourceId('Microsoft.Authorization/roleDefinitions', coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName)))))]" - } - ], - "builtInRoleNames": { - "Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'b24988ac-6180-42a0-ab88-20f7382dd24c')]", - "Private DNS Zone Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'b12aa53e-6015-4669-85d0-8515ebb3ae7f')]", - "Network Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '4d97b98b-1d4f-4787-a291-c67834d212e7')]", - "Owner": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '8e3af657-a8ff-443c-a75c-2fe8c4bcb635')]", - "Reader": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'acdd72a7-3385-48ef-bd42-f606fba81ae7')]", - "Role Based Access Control Administrator": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'f58310d9-a9f6-439a-9e8d-f62e7b41a168')]", - "User Access Administrator": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '18d7d88d-d35e-4fb5-a5c3-7773c20a72d9')]" - } - }, - "resources": { - "avmTelemetry": { - "condition": "[parameters('enableTelemetry')]", - "type": "Microsoft.Resources/deployments", - "apiVersion": "2024-03-01", - "name": "[format('46d3xbcp.res.nw-privdnszonesrv.{0}.{1}', replace('0.1.0', '.', '-'), substring(uniqueString(deployment().name), 0, 4))]", - "properties": { - "mode": "Incremental", - "template": { - "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", - "contentVersion": "1.0.0.0", - "resources": [], - "outputs": { - "telemetry": { - "type": "String", - "value": "For more information, see https://aka.ms/avm/TelemetryInfo" - } - } - } + "description": "Optional. A reference to frontend IP addresses." } }, - "privateDnsZone": { - "existing": true, - "type": "Microsoft.Network/privateDnsZones", - "apiVersion": "2020-06-01", - "name": "[parameters('privateDnsZoneName')]" - }, - "SRV": { - "type": "Microsoft.Network/privateDnsZones/SRV", - "apiVersion": "2020-06-01", - "name": "[format('{0}/{1}', parameters('privateDnsZoneName'), parameters('name'))]", - "properties": { - "metadata": "[parameters('metadata')]", - "srvRecords": "[parameters('srvRecords')]", - "ttl": "[parameters('ttl')]" + "frontendPort": { + "type": "int", + "nullable": true, + "metadata": { + "description": "Optional. The port for the external endpoint. Port numbers for each rule must be unique within the Load Balancer. Acceptable values range from 1 to 65534." } }, - "SRV_roleAssignments": { - "copy": { - "name": "SRV_roleAssignments", - "count": "[length(coalesce(variables('formattedRoleAssignments'), createArray()))]" - }, - "type": "Microsoft.Authorization/roleAssignments", - "apiVersion": "2022-04-01", - "scope": "[format('Microsoft.Network/privateDnsZones/{0}/SRV/{1}', parameters('privateDnsZoneName'), parameters('name'))]", - "name": "[coalesce(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'name'), guid(resourceId('Microsoft.Network/privateDnsZones/SRV', parameters('privateDnsZoneName'), parameters('name')), coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].principalId, coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].roleDefinitionId))]", - "properties": { - "roleDefinitionId": "[coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].roleDefinitionId]", - "principalId": "[coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].principalId]", - "description": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'description')]", - "principalType": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'principalType')]", - "condition": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'condition')]", - "conditionVersion": "[if(not(empty(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'condition'))), coalesce(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'conditionVersion'), '2.0'), null())]", - "delegatedManagedIdentityResourceId": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'delegatedManagedIdentityResourceId')]" - }, - "dependsOn": [ - "SRV" - ] - } - }, - "outputs": { - "name": { - "type": "string", + "frontendPortRangeStart": { + "type": "int", + "nullable": true, "metadata": { - "description": "The name of the deployed SRV record." - }, - "value": "[parameters('name')]" + "description": "Optional. The port range start for the external endpoint. This property is used together with BackendAddressPool and FrontendPortRangeEnd. Individual inbound NAT rule port mappings will be created for each backend address from BackendAddressPool. Acceptable values range from 1 to 65534." + } }, - "resourceId": { - "type": "string", + "frontendPortRangeEnd": { + "type": "int", + "nullable": true, "metadata": { - "description": "The resource ID of the deployed SRV record." - }, - "value": "[resourceId('Microsoft.Network/privateDnsZones/SRV', parameters('privateDnsZoneName'), parameters('name'))]" + "description": "Optional. The port range end for the external endpoint. This property is used together with BackendAddressPool and FrontendPortRangeStart. Individual inbound NAT rule port mappings will be created for each backend address from BackendAddressPool. Acceptable values range from 1 to 65534." + } }, - "resourceGroupName": { + "protocol": { "type": "string", + "allowedValues": [ + "All", + "Tcp", + "Udp" + ], + "nullable": true, "metadata": { - "description": "The resource group of the deployed SRV record." - }, - "value": "[resourceGroup().name]" + "description": "Optional. The reference to the transport protocol used by the load balancing rule." + } } + }, + "nullable": true, + "metadata": { + "description": "Optional. Properties of the inbound NAT rule." } } }, - "dependsOn": [ - "privateDnsZone" - ] + "metadata": { + "description": "The type for the inbound NAT rule.", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/res/network/network-interface:0.5.1" + } + } }, - "privateDnsZone_TXT": { - "copy": { - "name": "privateDnsZone_TXT", - "count": "[length(coalesce(parameters('txt'), createArray()))]" - }, - "type": "Microsoft.Resources/deployments", - "apiVersion": "2022-09-01", - "name": "[format('{0}-PrivateDnsZone-TXTRecord-{1}', uniqueString(deployment().name, parameters('location')), copyIndex())]", + "_1.virtualNetworkTapType": { + "type": "object", "properties": { - "expressionEvaluationOptions": { - "scope": "inner" + "id": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Resource ID of the virtual network tap." + } }, - "mode": "Incremental", - "parameters": { - "privateDnsZoneName": { - "value": "[parameters('name')]" - }, - "name": { - "value": "[coalesce(parameters('txt'), createArray())[copyIndex()].name]" - }, + "location": { + "type": "string", + "nullable": true, "metadata": { - "value": "[tryGet(coalesce(parameters('txt'), createArray())[copyIndex()], 'metadata')]" - }, - "txtRecords": { - "value": "[tryGet(coalesce(parameters('txt'), createArray())[copyIndex()], 'txtRecords')]" - }, - "ttl": { - "value": "[coalesce(tryGet(coalesce(parameters('txt'), createArray())[copyIndex()], 'ttl'), 3600)]" - }, - "roleAssignments": { - "value": "[tryGet(coalesce(parameters('txt'), createArray())[copyIndex()], 'roleAssignments')]" - }, - "enableTelemetry": { - "value": "[variables('enableReferencedModulesTelemetry')]" + "description": "Optional. Location of the virtual network tap." } }, - "template": { - "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", - "languageVersion": "2.0", - "contentVersion": "1.0.0.0", + "properties": { + "type": "object", + "nullable": true, "metadata": { - "_generator": { - "name": "bicep", - "version": "0.37.4.10188", - "templateHash": "17170531000135004092" - }, - "name": "Private DNS Zone TXT record", - "description": "This module deploys a Private DNS Zone TXT record." - }, - "definitions": { - "roleAssignmentType": { - "type": "object", - "properties": { - "name": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The name (as GUID) of the role assignment. If not provided, a GUID will be generated." - } - }, - "roleDefinitionIdOrName": { - "type": "string", - "metadata": { - "description": "Required. The role to assign. You can provide either the display name of the role definition, the role definition GUID, or its fully qualified ID in the following format: '/providers/Microsoft.Authorization/roleDefinitions/c2f4ef07-c644-48eb-af81-4b1b4947fb11'." - } - }, - "principalId": { - "type": "string", - "metadata": { - "description": "Required. The principal ID of the principal (user/group/identity) to assign the role to." - } - }, - "principalType": { - "type": "string", - "allowedValues": [ - "Device", - "ForeignGroup", - "Group", - "ServicePrincipal", - "User" - ], - "nullable": true, - "metadata": { - "description": "Optional. The principal type of the assigned principal ID." - } - }, - "description": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The description of the role assignment." - } - }, - "condition": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The conditions on the role assignment. This limits the resources it can be assigned to. e.g.: @Resource[Microsoft.Storage/storageAccounts/blobServices/containers:ContainerName] StringEqualsIgnoreCase \"foo_storage_container\"." - } - }, - "conditionVersion": { - "type": "string", - "allowedValues": [ - "2.0" - ], - "nullable": true, - "metadata": { - "description": "Optional. Version of the condition." - } - }, - "delegatedManagedIdentityResourceId": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The Resource Id of the delegated managed identity resource." - } - } - }, + "description": "Optional. Properties of the virtual network tap." + } + }, + "tags": { + "type": "object", + "nullable": true, + "metadata": { + "description": "Optional. Tags of the virtual network tap." + } + } + }, + "metadata": { + "description": "The type for the virtual network tap.", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/res/network/network-interface:0.5.1" + } + } + }, + "_2.ddosSettingsType": { + "type": "object", + "properties": { + "ddosProtectionPlan": { + "type": "object", + "properties": { + "id": { + "type": "string", "metadata": { - "description": "An AVM-aligned type for a role assignment.", - "__bicep_imported_from!": { - "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.5.1" - } + "description": "Required. The resource ID of the DDOS protection plan associated with the public IP address." } } }, - "parameters": { - "privateDnsZoneName": { - "type": "string", - "metadata": { - "description": "Conditional. The name of the parent Private DNS zone. Required if the template is used in a standalone deployment." - } - }, - "name": { - "type": "string", - "metadata": { - "description": "Required. The name of the TXT record." - } - }, - "metadata": { - "type": "object", - "metadata": { - "__bicep_resource_derived_type!": { - "source": "Microsoft.Network/privateDnsZones/TXT@2024-06-01#properties/properties/properties/metadata" - }, - "description": "Optional. The metadata attached to the record set." - }, - "nullable": true - }, - "ttl": { - "type": "int", - "defaultValue": 3600, - "metadata": { - "description": "Optional. The TTL (time-to-live) of the records in the record set." - } - }, - "txtRecords": { - "type": "array", - "metadata": { - "__bicep_resource_derived_type!": { - "source": "Microsoft.Network/privateDnsZones/TXT@2024-06-01#properties/properties/properties/txtRecords" - }, - "description": "Optional. The list of TXT records in the record set." - }, - "nullable": true - }, - "enableTelemetry": { - "type": "bool", - "defaultValue": true, - "metadata": { - "description": "Optional. Enable/Disable usage telemetry for module." - } - }, - "roleAssignments": { - "type": "array", - "items": { - "$ref": "#/definitions/roleAssignmentType" - }, - "nullable": true, - "metadata": { - "description": "Optional. Array of role assignments to create." - } - } - }, - "variables": { - "copy": [ - { - "name": "formattedRoleAssignments", - "count": "[length(coalesce(parameters('roleAssignments'), createArray()))]", - "input": "[union(coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')], createObject('roleDefinitionId', coalesce(tryGet(variables('builtInRoleNames'), coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName), if(contains(coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName, '/providers/Microsoft.Authorization/roleDefinitions/'), coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName, subscriptionResourceId('Microsoft.Authorization/roleDefinitions', coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName)))))]" - } - ], - "builtInRoleNames": { - "Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'b24988ac-6180-42a0-ab88-20f7382dd24c')]", - "Private DNS Zone Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'b12aa53e-6015-4669-85d0-8515ebb3ae7f')]", - "Network Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '4d97b98b-1d4f-4787-a291-c67834d212e7')]", - "Owner": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '8e3af657-a8ff-443c-a75c-2fe8c4bcb635')]", - "Reader": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'acdd72a7-3385-48ef-bd42-f606fba81ae7')]", - "Role Based Access Control Administrator": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'f58310d9-a9f6-439a-9e8d-f62e7b41a168')]", - "User Access Administrator": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '18d7d88d-d35e-4fb5-a5c3-7773c20a72d9')]" - } - }, - "resources": { - "avmTelemetry": { - "condition": "[parameters('enableTelemetry')]", - "type": "Microsoft.Resources/deployments", - "apiVersion": "2024-03-01", - "name": "[format('46d3xbcp.res.nw-privdnszonetxt.{0}.{1}', replace('0.1.0', '.', '-'), substring(uniqueString(deployment().name), 0, 4))]", - "properties": { - "mode": "Incremental", - "template": { - "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", - "contentVersion": "1.0.0.0", - "resources": [], - "outputs": { - "telemetry": { - "type": "String", - "value": "For more information, see https://aka.ms/avm/TelemetryInfo" - } - } - } - } - }, - "privateDnsZone": { - "existing": true, - "type": "Microsoft.Network/privateDnsZones", - "apiVersion": "2020-06-01", - "name": "[parameters('privateDnsZoneName')]" - }, - "TXT": { - "type": "Microsoft.Network/privateDnsZones/TXT", - "apiVersion": "2020-06-01", - "name": "[format('{0}/{1}', parameters('privateDnsZoneName'), parameters('name'))]", - "properties": { - "metadata": "[parameters('metadata')]", - "ttl": "[parameters('ttl')]", - "txtRecords": "[parameters('txtRecords')]" - } - }, - "TXT_roleAssignments": { - "copy": { - "name": "TXT_roleAssignments", - "count": "[length(coalesce(variables('formattedRoleAssignments'), createArray()))]" - }, - "type": "Microsoft.Authorization/roleAssignments", - "apiVersion": "2022-04-01", - "scope": "[format('Microsoft.Network/privateDnsZones/{0}/TXT/{1}', parameters('privateDnsZoneName'), parameters('name'))]", - "name": "[coalesce(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'name'), guid(resourceId('Microsoft.Network/privateDnsZones/TXT', parameters('privateDnsZoneName'), parameters('name')), coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].principalId, coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].roleDefinitionId))]", - "properties": { - "roleDefinitionId": "[coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].roleDefinitionId]", - "principalId": "[coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].principalId]", - "description": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'description')]", - "principalType": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'principalType')]", - "condition": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'condition')]", - "conditionVersion": "[if(not(empty(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'condition'))), coalesce(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'conditionVersion'), '2.0'), null())]", - "delegatedManagedIdentityResourceId": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'delegatedManagedIdentityResourceId')]" - }, - "dependsOn": [ - "TXT" - ] - } - }, - "outputs": { - "name": { - "type": "string", - "metadata": { - "description": "The name of the deployed TXT record." - }, - "value": "[parameters('name')]" - }, - "resourceId": { - "type": "string", - "metadata": { - "description": "The resource ID of the deployed TXT record." - }, - "value": "[resourceId('Microsoft.Network/privateDnsZones/TXT', parameters('privateDnsZoneName'), parameters('name'))]" - }, - "resourceGroupName": { - "type": "string", - "metadata": { - "description": "The resource group of the deployed TXT record." - }, - "value": "[resourceGroup().name]" - } - } - } - }, - "dependsOn": [ - "privateDnsZone" - ] - }, - "privateDnsZone_virtualNetworkLinks": { - "copy": { - "name": "privateDnsZone_virtualNetworkLinks", - "count": "[length(coalesce(parameters('virtualNetworkLinks'), createArray()))]" - }, - "type": "Microsoft.Resources/deployments", - "apiVersion": "2022-09-01", - "name": "[format('{0}-PrivateDnsZone-VNetLink-{1}', uniqueString(deployment().name, parameters('location')), copyIndex())]", - "properties": { - "expressionEvaluationOptions": { - "scope": "inner" - }, - "mode": "Incremental", - "parameters": { - "privateDnsZoneName": { - "value": "[parameters('name')]" - }, - "name": { - "value": "[coalesce(tryGet(coalesce(parameters('virtualNetworkLinks'), createArray())[copyIndex()], 'name'), format('{0}-vnetlink', last(split(coalesce(parameters('virtualNetworkLinks'), createArray())[copyIndex()].virtualNetworkResourceId, '/'))))]" - }, - "virtualNetworkResourceId": { - "value": "[coalesce(parameters('virtualNetworkLinks'), createArray())[copyIndex()].virtualNetworkResourceId]" - }, - "location": { - "value": "[coalesce(tryGet(coalesce(parameters('virtualNetworkLinks'), createArray())[copyIndex()], 'location'), 'global')]" - }, - "registrationEnabled": { - "value": "[coalesce(tryGet(coalesce(parameters('virtualNetworkLinks'), createArray())[copyIndex()], 'registrationEnabled'), false())]" - }, - "tags": { - "value": "[coalesce(tryGet(coalesce(parameters('virtualNetworkLinks'), createArray())[copyIndex()], 'tags'), parameters('tags'))]" - }, - "resolutionPolicy": { - "value": "[tryGet(coalesce(parameters('virtualNetworkLinks'), createArray())[copyIndex()], 'resolutionPolicy')]" + "nullable": true, + "metadata": { + "description": "Optional. The DDoS protection plan associated with the public IP address." } }, - "template": { - "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", - "languageVersion": "2.0", - "contentVersion": "1.0.0.0", + "protectionMode": { + "type": "string", + "allowedValues": [ + "Enabled" + ], "metadata": { - "_generator": { - "name": "bicep", - "version": "0.37.4.10188", - "templateHash": "517173107480898390" - }, - "name": "Private DNS Zone Virtual Network Link", - "description": "This module deploys a Private DNS Zone Virtual Network Link." - }, - "parameters": { - "privateDnsZoneName": { - "type": "string", - "metadata": { - "description": "Conditional. The name of the parent Private DNS zone. Required if the template is used in a standalone deployment." - } - }, - "name": { - "type": "string", - "defaultValue": "[format('{0}-vnetlink', last(split(parameters('virtualNetworkResourceId'), '/')))]", - "metadata": { - "description": "Optional. The name of the virtual network link." - } - }, - "location": { - "type": "string", - "defaultValue": "global", - "metadata": { - "description": "Optional. The location of the PrivateDNSZone. Should be global." - } - }, - "tags": { - "type": "object", - "metadata": { - "__bicep_resource_derived_type!": { - "source": "Microsoft.Network/privateDnsZones/virtualNetworkLinks@2024-06-01#properties/tags" - }, - "description": "Optional. Tags of the resource." - }, - "nullable": true - }, - "registrationEnabled": { - "type": "bool", - "defaultValue": false, - "metadata": { - "description": "Optional. Is auto-registration of virtual machine records in the virtual network in the Private DNS zone enabled?." - } - }, - "virtualNetworkResourceId": { - "type": "string", - "metadata": { - "description": "Required. Link to another virtual network resource ID." - } - }, - "resolutionPolicy": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The resolution policy on the virtual network link. Only applicable for virtual network links to privatelink zones, and for A,AAAA,CNAME queries. When set to `NxDomainRedirect`, Azure DNS resolver falls back to public resolution if private dns query resolution results in non-existent domain response. `Default` is configured as the default option." - } - } - }, - "resources": { - "privateDnsZone": { - "existing": true, - "type": "Microsoft.Network/privateDnsZones", - "apiVersion": "2020-06-01", - "name": "[parameters('privateDnsZoneName')]" - }, - "virtualNetworkLink": { - "type": "Microsoft.Network/privateDnsZones/virtualNetworkLinks", - "apiVersion": "2024-06-01", - "name": "[format('{0}/{1}', parameters('privateDnsZoneName'), parameters('name'))]", - "location": "[parameters('location')]", - "tags": "[parameters('tags')]", - "properties": { - "registrationEnabled": "[parameters('registrationEnabled')]", - "virtualNetwork": { - "id": "[parameters('virtualNetworkResourceId')]" - }, - "resolutionPolicy": "[parameters('resolutionPolicy')]" - } - } - }, - "outputs": { - "name": { - "type": "string", - "metadata": { - "description": "The name of the deployed virtual network link." - }, - "value": "[parameters('name')]" - }, - "resourceId": { - "type": "string", - "metadata": { - "description": "The resource ID of the deployed virtual network link." - }, - "value": "[resourceId('Microsoft.Network/privateDnsZones/virtualNetworkLinks', parameters('privateDnsZoneName'), parameters('name'))]" - }, - "resourceGroupName": { - "type": "string", - "metadata": { - "description": "The resource group of the deployed virtual network link." - }, - "value": "[resourceGroup().name]" - }, - "location": { - "type": "string", - "metadata": { - "description": "The location the resource was deployed into." - }, - "value": "[reference('virtualNetworkLink', '2024-06-01', 'full').location]" - } + "description": "Required. The DDoS protection policy customizations." } } }, - "dependsOn": [ - "privateDnsZone" - ] - } - }, - "outputs": { - "resourceGroupName": { - "type": "string", - "metadata": { - "description": "The resource group the private DNS zone was deployed into." - }, - "value": "[resourceGroup().name]" - }, - "name": { - "type": "string", - "metadata": { - "description": "The name of the private DNS zone." - }, - "value": "[parameters('name')]" - }, - "resourceId": { - "type": "string", - "metadata": { - "description": "The resource ID of the private DNS zone." - }, - "value": "[resourceId('Microsoft.Network/privateDnsZones', parameters('name'))]" - }, - "location": { - "type": "string", "metadata": { - "description": "The location the resource was deployed into." - }, - "value": "[reference('privateDnsZone', '2020-06-01', 'full').location]" - } - } - } - }, - "dependsOn": [ - "virtualNetwork" - ] - }, - "aiFoundryAiServices": { - "condition": "[not(variables('useExistingAiFoundryAiProject'))]", - "type": "Microsoft.Resources/deployments", - "apiVersion": "2025-04-01", - "name": "[take(format('avm.res.cognitive-services.account.{0}', variables('aiFoundryAiServicesResourceName')), 64)]", - "properties": { - "expressionEvaluationOptions": { - "scope": "inner" - }, - "mode": "Incremental", - "parameters": { - "name": { - "value": "[variables('aiFoundryAiServicesResourceName')]" - }, - "location": { - "value": "[parameters('azureAiServiceLocation')]" - }, - "tags": { - "value": "[parameters('tags')]" - }, - "sku": { - "value": "S0" - }, - "kind": { - "value": "AIServices" - }, - "disableLocalAuth": { - "value": true - }, - "allowProjectManagement": { - "value": true - }, - "customSubDomainName": { - "value": "[variables('aiFoundryAiServicesResourceName')]" - }, - "restrictOutboundNetworkAccess": { - "value": false - }, - "deployments": { - "copy": [ - { - "name": "value", - "count": "[length(variables('aiFoundryAiServicesModelDeployment'))]", - "input": "[createObject('name', variables('aiFoundryAiServicesModelDeployment')[copyIndex('value')].name, 'model', createObject('format', variables('aiFoundryAiServicesModelDeployment')[copyIndex('value')].format, 'name', variables('aiFoundryAiServicesModelDeployment')[copyIndex('value')].name, 'version', variables('aiFoundryAiServicesModelDeployment')[copyIndex('value')].version), 'raiPolicyName', variables('aiFoundryAiServicesModelDeployment')[copyIndex('value')].raiPolicyName, 'sku', createObject('name', variables('aiFoundryAiServicesModelDeployment')[copyIndex('value')].sku.name, 'capacity', variables('aiFoundryAiServicesModelDeployment')[copyIndex('value')].sku.capacity))]" - } - ] - }, - "networkAcls": { - "value": { - "defaultAction": "Allow", - "virtualNetworkRules": [], - "ipRules": [] - } - }, - "managedIdentities": { - "value": { - "userAssignedResourceIds": [ - "[reference('userAssignedIdentity').outputs.resourceId.value]" - ] - } - }, - "roleAssignments": { - "value": [ - { - "roleDefinitionIdOrName": "53ca6127-db72-4b80-b1b0-d745d6d5456d", - "principalId": "[reference('userAssignedIdentity').outputs.principalId.value]", - "principalType": "ServicePrincipal" - }, - { - "roleDefinitionIdOrName": "64702f94-c441-49e6-a78b-ef80e0188fee", - "principalId": "[reference('userAssignedIdentity').outputs.principalId.value]", - "principalType": "ServicePrincipal" - }, - { - "roleDefinitionIdOrName": "5e0bd9bd-7b93-4f28-af87-19fc36ad61bd", - "principalId": "[reference('userAssignedIdentity').outputs.principalId.value]", - "principalType": "ServicePrincipal" - }, - { - "roleDefinitionIdOrName": "53ca6127-db72-4b80-b1b0-d745d6d5456d", - "principalId": "[deployer().objectId]" + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/res/network/public-ip-address:0.8.0" + } } - ] - }, - "diagnosticSettings": "[if(parameters('enableMonitoring'), createObject('value', createArray(createObject('workspaceResourceId', if(variables('useExistingLogAnalytics'), parameters('existingLogAnalyticsWorkspaceId'), if(parameters('enableMonitoring'), reference('logAnalyticsWorkspace').outputs.resourceId.value, ''))))), createObject('value', null()))]", - "publicNetworkAccess": "[if(parameters('enablePrivateNetworking'), createObject('value', 'Disabled'), createObject('value', 'Enabled'))]" - }, - "template": { - "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", - "languageVersion": "2.0", - "contentVersion": "1.0.0.0", - "metadata": { - "_generator": { - "name": "bicep", - "version": "0.38.33.27573", - "templateHash": "7191594406492701501" }, - "name": "Cognitive Services", - "description": "This module deploys a Cognitive Service." - }, - "definitions": { - "privateEndpointOutputType": { + "_2.dnsSettingsType": { "type": "object", "properties": { - "name": { + "domainNameLabel": { "type": "string", "metadata": { - "description": "The name of the private endpoint." + "description": "Required. The domain name label. The concatenation of the domain name label and the regionalized DNS zone make up the fully qualified domain name associated with the public IP address. If a domain name label is specified, an A DNS record is created for the public IP in the Microsoft Azure DNS system." } }, - "resourceId": { + "domainNameLabelScope": { "type": "string", + "allowedValues": [ + "NoReuse", + "ResourceGroupReuse", + "SubscriptionReuse", + "TenantReuse" + ], + "nullable": true, "metadata": { - "description": "The resource ID of the private endpoint." + "description": "Optional. The domain name label scope. If a domain name label and a domain name label scope are specified, an A DNS record is created for the public IP in the Microsoft Azure DNS system with a hashed value includes in FQDN." } }, - "groupId": { + "fqdn": { "type": "string", "nullable": true, "metadata": { - "description": "The group Id for the private endpoint Group." + "description": "Optional. The Fully Qualified Domain Name of the A DNS record associated with the public IP. This is the concatenation of the domainNameLabel and the regionalized DNS zone." } }, - "customDnsConfigs": { - "type": "array", - "items": { - "type": "object", - "properties": { - "fqdn": { - "type": "string", - "nullable": true, - "metadata": { - "description": "FQDN that resolves to private endpoint IP address." - } - }, - "ipAddresses": { - "type": "array", - "items": { - "type": "string" - }, - "metadata": { - "description": "A list of private IP addresses of the private endpoint." - } - } - } - }, + "reverseFqdn": { + "type": "string", + "nullable": true, "metadata": { - "description": "The custom DNS configurations of the private endpoint." - } - }, - "networkInterfaceResourceIds": { - "type": "array", - "items": { - "type": "string" - }, - "metadata": { - "description": "The IDs of the network interfaces associated with the private endpoint." + "description": "Optional. The reverse FQDN. A user-visible, fully qualified domain name that resolves to this public IP address. If the reverseFqdn is specified, then a PTR DNS record is created pointing from the IP address in the in-addr.arpa domain to the reverse FQDN." } } }, "metadata": { - "__bicep_export!": true, - "description": "The type for the private endpoint output." - } - }, - "deploymentType": { - "type": "object", - "properties": { - "name": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. Specify the name of cognitive service account deployment." - } - }, - "model": { - "type": "object", - "properties": { - "name": { - "type": "string", - "metadata": { - "description": "Required. The name of Cognitive Services account deployment model." - } - }, - "format": { - "type": "string", - "metadata": { - "description": "Required. The format of Cognitive Services account deployment model." - } - }, - "version": { - "type": "string", - "metadata": { - "description": "Required. The version of Cognitive Services account deployment model." - } - } - }, - "metadata": { - "description": "Required. Properties of Cognitive Services account deployment model." - } - }, - "sku": { - "type": "object", - "properties": { - "name": { - "type": "string", - "metadata": { - "description": "Required. The name of the resource model definition representing SKU." - } - }, - "capacity": { - "type": "int", - "nullable": true, - "metadata": { - "description": "Optional. The capacity of the resource model definition representing SKU." - } - }, - "tier": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The tier of the resource model definition representing SKU." - } - }, - "size": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The size of the resource model definition representing SKU." - } - }, - "family": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The family of the resource model definition representing SKU." - } - } - }, - "nullable": true, - "metadata": { - "description": "Optional. The resource model definition representing SKU." - } - }, - "raiPolicyName": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The name of RAI policy." - } - }, - "versionUpgradeOption": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The version upgrade option." - } + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/res/network/public-ip-address:0.8.0" } - }, - "metadata": { - "__bicep_export!": true, - "description": "The type for a cognitive services account deployment." } }, - "endpointType": { + "_2.ipTagType": { "type": "object", "properties": { - "name": { + "ipTagType": { "type": "string", - "nullable": true, "metadata": { - "description": "Type of the endpoint." + "description": "Required. The IP tag type." } }, - "endpoint": { + "tag": { "type": "string", - "nullable": true, "metadata": { - "description": "The endpoint URI." + "description": "Required. The IP tag." } } }, "metadata": { - "__bicep_export!": true, - "description": "The type for a cognitive services account endpoint." + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/res/network/public-ip-address:0.8.0" + } } }, - "secretsExportConfigurationType": { + "_3.diagnosticSettingFullType": { "type": "object", "properties": { - "keyVaultResourceId": { - "type": "string", - "metadata": { - "description": "Required. The key vault name where to store the keys and connection strings generated by the modules." - } - }, - "accessKey1Name": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The name for the accessKey1 secret to create." - } - }, - "accessKey2Name": { + "name": { "type": "string", "nullable": true, "metadata": { - "description": "Optional. The name for the accessKey2 secret to create." - } - } - }, - "metadata": { - "__bicep_export!": true, - "description": "The type of the secrets exported to the provided Key Vault." - } - }, - "commitmentPlanType": { - "type": "object", - "properties": { - "autoRenew": { - "type": "bool", - "metadata": { - "description": "Required. Whether the plan should auto-renew at the end of the current commitment period." + "description": "Optional. The name of the diagnostic setting." } }, - "current": { - "type": "object", - "properties": { - "count": { - "type": "int", - "metadata": { - "description": "Required. The number of committed instances (e.g., number of containers or cores)." - } - }, - "tier": { - "type": "string", - "metadata": { - "description": "Required. The tier of the commitment plan (e.g., T1, T2)." + "logCategoriesAndGroups": { + "type": "array", + "items": { + "type": "object", + "properties": { + "category": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Name of a Diagnostic Log category for a resource type this setting is applied to. Set the specific logs to collect here." + } + }, + "categoryGroup": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Name of a Diagnostic Log category group for a resource type this setting is applied to. Set to `allLogs` to collect all logs." + } + }, + "enabled": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. Enable or disable the category explicitly. Default is `true`." + } } } }, - "metadata": { - "description": "Required. The current commitment configuration." - } - }, - "hostingModel": { - "type": "string", - "metadata": { - "description": "Required. The hosting model for the commitment plan. (e.g., DisconnectedContainer, ConnectedContainer, ProvisionedWeb, Web)." - } - }, - "planType": { - "type": "string", - "metadata": { - "description": "Required. The plan type indicating which capability the plan applies to (e.g., NTTS, STT, CUSTOMSTT, ADDON)." - } - }, - "commitmentPlanGuid": { - "type": "string", "nullable": true, "metadata": { - "description": "Optional. The unique identifier of an existing commitment plan to update. Set to null to create a new plan." + "description": "Optional. The name of logs that will be streamed. \"allLogs\" includes all possible logs for the resource. Set to `[]` to disable log collection." } }, - "next": { - "type": "object", - "properties": { - "count": { - "type": "int", - "metadata": { - "description": "Required. The number of committed instances for the next period." - } - }, - "tier": { - "type": "string", - "metadata": { - "description": "Required. The tier for the next commitment period." + "metricCategories": { + "type": "array", + "items": { + "type": "object", + "properties": { + "category": { + "type": "string", + "metadata": { + "description": "Required. Name of a Diagnostic Metric category for a resource type this setting is applied to. Set to `AllMetrics` to collect all metrics." + } + }, + "enabled": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. Enable or disable the category explicitly. Default is `true`." + } } } }, "nullable": true, "metadata": { - "description": "Optional. The configuration of the next commitment period, if scheduled." + "description": "Optional. The name of metrics that will be streamed. \"allMetrics\" includes all possible metrics for the resource. Set to `[]` to disable metric collection." } - } - }, - "metadata": { - "__bicep_export!": true, - "description": "The type for a disconnected container commitment plan." - } - }, - "networkInjectionType": { - "type": "object", - "properties": { - "scenario": { + }, + "logAnalyticsDestinationType": { "type": "string", "allowedValues": [ - "agent", - "none" + "AzureDiagnostics", + "Dedicated" ], + "nullable": true, "metadata": { - "description": "Required. The scenario for the network injection." + "description": "Optional. A string indicating whether the export to Log Analytics should use the default destination type, i.e. AzureDiagnostics, or use a destination type." } }, - "subnetResourceId": { + "workspaceResourceId": { "type": "string", + "nullable": true, "metadata": { - "description": "Required. The Resource ID of the subnet on the Virtual Network on which to inject." + "description": "Optional. Resource ID of the diagnostic log analytics workspace. For security reasons, it is recommended to set diagnostic settings to send data to either storage account, log analytics workspace or event hub." } }, - "useMicrosoftManagedNetwork": { - "type": "bool", + "storageAccountResourceId": { + "type": "string", "nullable": true, "metadata": { - "description": "Optional. Whether to use Microsoft Managed Network. Defaults to false." + "description": "Optional. Resource ID of the diagnostic storage account. For security reasons, it is recommended to set diagnostic settings to send data to either storage account, log analytics workspace or event hub." } - } - }, - "metadata": { - "__bicep_export!": true, - "description": "Type for network configuration in AI Foundry where virtual network injection occurs to secure scenarios like Agents entirely within a private network." - } - }, - "_1.secretSetOutputType": { - "type": "object", - "properties": { - "secretResourceId": { + }, + "eventHubAuthorizationRuleResourceId": { "type": "string", + "nullable": true, "metadata": { - "description": "The resourceId of the exported secret." + "description": "Optional. Resource ID of the diagnostic event hub authorization rule for the Event Hubs namespace in which the event hub should be created or streamed to." } }, - "secretUri": { + "eventHubName": { "type": "string", + "nullable": true, "metadata": { - "description": "The secret URI of the exported secret." + "description": "Optional. Name of the diagnostic event hub within the namespace to which logs are streamed. Without this, an event hub is created for each log category. For security reasons, it is recommended to set diagnostic settings to send data to either storage account, log analytics workspace or event hub." } }, - "secretUriWithVersion": { + "marketplacePartnerResourceId": { "type": "string", + "nullable": true, "metadata": { - "description": "The secret URI with version of the exported secret." + "description": "Optional. The full ARM resource ID of the Marketplace resource to which you would like to send Diagnostic Logs." } } }, "metadata": { - "description": "An AVM-aligned type for the output of the secret set via the secrets export feature.", + "description": "An AVM-aligned type for a diagnostic setting. To be used if both logs & metrics are supported by the resource provider.", "__bicep_imported_from!": { - "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.0" + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.1" } } }, - "_2.lockType": { + "_3.lockType": { "type": "object", "properties": { "name": { @@ -11266,116 +10649,7 @@ } } }, - "_2.privateEndpointCustomDnsConfigType": { - "type": "object", - "properties": { - "fqdn": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. FQDN that resolves to private endpoint IP address." - } - }, - "ipAddresses": { - "type": "array", - "items": { - "type": "string" - }, - "metadata": { - "description": "Required. A list of private IP addresses of the private endpoint." - } - } - }, - "metadata": { - "__bicep_imported_from!": { - "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.1" - } - } - }, - "_2.privateEndpointIpConfigurationType": { - "type": "object", - "properties": { - "name": { - "type": "string", - "metadata": { - "description": "Required. The name of the resource that is unique within a resource group." - } - }, - "properties": { - "type": "object", - "properties": { - "groupId": { - "type": "string", - "metadata": { - "description": "Required. The ID of a group obtained from the remote resource that this private endpoint should connect to." - } - }, - "memberName": { - "type": "string", - "metadata": { - "description": "Required. The member name of a group obtained from the remote resource that this private endpoint should connect to." - } - }, - "privateIPAddress": { - "type": "string", - "metadata": { - "description": "Required. A private IP address obtained from the private endpoint's subnet." - } - } - }, - "metadata": { - "description": "Required. Properties of private endpoint IP configurations." - } - } - }, - "metadata": { - "__bicep_imported_from!": { - "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.1" - } - } - }, - "_2.privateEndpointPrivateDnsZoneGroupType": { - "type": "object", - "properties": { - "name": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The name of the Private DNS Zone Group." - } - }, - "privateDnsZoneGroupConfigs": { - "type": "array", - "items": { - "type": "object", - "properties": { - "name": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The name of the private DNS Zone Group config." - } - }, - "privateDnsZoneResourceId": { - "type": "string", - "metadata": { - "description": "Required. The resource id of the private DNS zone." - } - } - } - }, - "metadata": { - "description": "Required. The private DNS Zone Groups to associate the Private Endpoint. A DNS Zone Group can support up to 5 DNS zones." - } - } - }, - "metadata": { - "__bicep_imported_from!": { - "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.1" - } - } - }, - "_2.roleAssignmentType": { + "_3.roleAssignmentType": { "type": "object", "properties": { "name": { @@ -11450,372 +10724,541 @@ } } }, - "customerManagedKeyType": { + "_4.publicIPConfigurationType": { "type": "object", "properties": { - "keyVaultResourceId": { + "name": { "type": "string", + "nullable": true, "metadata": { - "description": "Required. The resource ID of a key vault to reference a customer managed key for encryption from." + "description": "Optional. The name of the Public IP Address." } }, - "keyName": { + "publicIPAddressResourceId": { "type": "string", + "nullable": true, "metadata": { - "description": "Required. The name of the customer managed key to use for encryption." + "description": "Optional. The resource ID of the public IP address." } }, - "keyVersion": { - "type": "string", + "diagnosticSettings": { + "type": "array", + "items": { + "$ref": "#/definitions/_3.diagnosticSettingFullType" + }, "nullable": true, "metadata": { - "description": "Optional. The version of the customer managed key to reference for encryption. If not provided, the deployment will use the latest version available at deployment time." + "description": "Optional. Diagnostic settings for the public IP address." } }, - "userAssignedIdentityResourceId": { + "location": { "type": "string", "nullable": true, "metadata": { - "description": "Optional. User assigned identity to use when fetching the customer managed key. Required if no system assigned identity is available for use." + "description": "Optional. The idle timeout in minutes." } - } - }, - "metadata": { - "description": "An AVM-aligned type for a customer-managed key. To be used if the resource type does not support auto-rotation of the customer-managed key.", - "__bicep_imported_from!": { - "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.0" - } - } - }, - "diagnosticSettingFullType": { - "type": "object", - "properties": { - "name": { - "type": "string", + }, + "lock": { + "$ref": "#/definitions/_3.lockType", "nullable": true, "metadata": { - "description": "Optional. The name of the diagnostic setting." + "description": "Optional. The lock settings of the public IP address." } }, - "logCategoriesAndGroups": { - "type": "array", - "items": { - "type": "object", - "properties": { - "category": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. Name of a Diagnostic Log category for a resource type this setting is applied to. Set the specific logs to collect here." - } - }, - "categoryGroup": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. Name of a Diagnostic Log category group for a resource type this setting is applied to. Set to `allLogs` to collect all logs." - } - }, - "enabled": { - "type": "bool", - "nullable": true, - "metadata": { - "description": "Optional. Enable or disable the category explicitly. Default is `true`." - } - } - } - }, + "idleTimeoutInMinutes": { + "type": "int", "nullable": true, "metadata": { - "description": "Optional. The name of logs that will be streamed. \"allLogs\" includes all possible logs for the resource. Set to `[]` to disable log collection." + "description": "Optional. The idle timeout of the public IP address." } }, - "metricCategories": { - "type": "array", - "items": { - "type": "object", - "properties": { - "category": { - "type": "string", - "metadata": { - "description": "Required. Name of a Diagnostic Metric category for a resource type this setting is applied to. Set to `AllMetrics` to collect all metrics." - } - }, - "enabled": { - "type": "bool", - "nullable": true, - "metadata": { - "description": "Optional. Enable or disable the category explicitly. Default is `true`." - } - } - } - }, + "ddosSettings": { + "$ref": "#/definitions/_2.ddosSettingsType", "nullable": true, "metadata": { - "description": "Optional. The name of metrics that will be streamed. \"allMetrics\" includes all possible metrics for the resource. Set to `[]` to disable metric collection." + "description": "Optional. The DDoS protection plan configuration associated with the public IP address." } }, - "logAnalyticsDestinationType": { + "dnsSettings": { + "$ref": "#/definitions/_2.dnsSettingsType", + "nullable": true, + "metadata": { + "description": "Optional. The DNS settings of the public IP address." + } + }, + "publicIPAddressVersion": { "type": "string", "allowedValues": [ - "AzureDiagnostics", - "Dedicated" + "IPv4", + "IPv6" ], "nullable": true, "metadata": { - "description": "Optional. A string indicating whether the export to Log Analytics should use the default destination type, i.e. AzureDiagnostics, or use a destination type." + "description": "Optional. The public IP address version." } }, - "workspaceResourceId": { + "publicIPAllocationMethod": { "type": "string", + "allowedValues": [ + "Dynamic", + "Static" + ], "nullable": true, "metadata": { - "description": "Optional. Resource ID of the diagnostic log analytics workspace. For security reasons, it is recommended to set diagnostic settings to send data to either storage account, log analytics workspace or event hub." + "description": "Optional. The public IP address allocation method." } }, - "storageAccountResourceId": { + "publicIpPrefixResourceId": { "type": "string", "nullable": true, "metadata": { - "description": "Optional. Resource ID of the diagnostic storage account. For security reasons, it is recommended to set diagnostic settings to send data to either storage account, log analytics workspace or event hub." + "description": "Optional. Resource ID of the Public IP Prefix object. This is only needed if you want your Public IPs created in a PIP Prefix." } }, - "eventHubAuthorizationRuleResourceId": { + "publicIpNameSuffix": { "type": "string", "nullable": true, "metadata": { - "description": "Optional. Resource ID of the diagnostic event hub authorization rule for the Event Hubs namespace in which the event hub should be created or streamed to." + "description": "Optional. The name suffix of the public IP address resource." } }, - "eventHubName": { + "roleAssignments": { + "type": "array", + "items": { + "$ref": "#/definitions/_3.roleAssignmentType" + }, + "nullable": true, + "metadata": { + "description": "Optional. Array of role assignments to create." + } + }, + "skuName": { "type": "string", + "allowedValues": [ + "Basic", + "Standard" + ], "nullable": true, "metadata": { - "description": "Optional. Name of the diagnostic event hub within the namespace to which logs are streamed. Without this, an event hub is created for each log category. For security reasons, it is recommended to set diagnostic settings to send data to either storage account, log analytics workspace or event hub." + "description": "Optional. The SKU name of the public IP address." } }, - "marketplacePartnerResourceId": { + "skuTier": { "type": "string", + "allowedValues": [ + "Global", + "Regional" + ], "nullable": true, "metadata": { - "description": "Optional. The full ARM resource ID of the Marketplace resource to which you would like to send Diagnostic Logs." + "description": "Optional. The SKU tier of the public IP address." + } + }, + "tags": { + "type": "object", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.Network/publicIPAddresses@2024-07-01#properties/tags" + }, + "description": "Optional. The tags of the public IP address." + }, + "nullable": true + }, + "availabilityZones": { + "type": "array", + "allowedValues": [ + 1, + 2, + 3 + ], + "nullable": true, + "metadata": { + "description": "Optional. The zones of the public IP address." + } + }, + "ipTags": { + "type": "array", + "items": { + "$ref": "#/definitions/_2.ipTagType" + }, + "nullable": true, + "metadata": { + "description": "Optional. The list of tags associated with the public IP address." + } + }, + "enableTelemetry": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. Enable/Disable usage telemetry for the module." } } }, "metadata": { - "description": "An AVM-aligned type for a diagnostic setting. To be used if both logs & metrics are supported by the resource provider.", + "description": "The type for the public IP address configuration.", "__bicep_imported_from!": { - "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.0" + "sourceTemplate": "modules/nic-configuration.bicep" } } }, - "lockType": { + "diagnosticSettingFullType": { "type": "object", "properties": { "name": { "type": "string", "nullable": true, "metadata": { - "description": "Optional. Specify the name of lock." + "description": "Optional. The name of the diagnostic setting." } }, - "kind": { + "logCategoriesAndGroups": { + "type": "array", + "items": { + "type": "object", + "properties": { + "category": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Name of a Diagnostic Log category for a resource type this setting is applied to. Set the specific logs to collect here." + } + }, + "categoryGroup": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Name of a Diagnostic Log category group for a resource type this setting is applied to. Set to `allLogs` to collect all logs." + } + }, + "enabled": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. Enable or disable the category explicitly. Default is `true`." + } + } + } + }, + "nullable": true, + "metadata": { + "description": "Optional. The name of logs that will be streamed. \"allLogs\" includes all possible logs for the resource. Set to `[]` to disable log collection." + } + }, + "metricCategories": { + "type": "array", + "items": { + "type": "object", + "properties": { + "category": { + "type": "string", + "metadata": { + "description": "Required. Name of a Diagnostic Metric category for a resource type this setting is applied to. Set to `AllMetrics` to collect all metrics." + } + }, + "enabled": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. Enable or disable the category explicitly. Default is `true`." + } + } + } + }, + "nullable": true, + "metadata": { + "description": "Optional. The name of metrics that will be streamed. \"allMetrics\" includes all possible metrics for the resource. Set to `[]` to disable metric collection." + } + }, + "logAnalyticsDestinationType": { "type": "string", "allowedValues": [ - "CanNotDelete", - "None", - "ReadOnly" + "AzureDiagnostics", + "Dedicated" ], "nullable": true, "metadata": { - "description": "Optional. Specify the type of lock." + "description": "Optional. A string indicating whether the export to Log Analytics should use the default destination type, i.e. AzureDiagnostics, or use a destination type." } }, - "notes": { + "workspaceResourceId": { "type": "string", "nullable": true, "metadata": { - "description": "Optional. Specify the notes of the lock." + "description": "Optional. Resource ID of the diagnostic log analytics workspace. For security reasons, it is recommended to set diagnostic settings to send data to either storage account, log analytics workspace or event hub." } - } - }, - "metadata": { - "description": "An AVM-aligned type for a lock.", - "__bicep_imported_from!": { - "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.0" - } - } - }, - "managedIdentityAllType": { - "type": "object", - "properties": { - "systemAssigned": { - "type": "bool", + }, + "storageAccountResourceId": { + "type": "string", "nullable": true, "metadata": { - "description": "Optional. Enables system assigned managed identity on the resource." + "description": "Optional. Resource ID of the diagnostic storage account. For security reasons, it is recommended to set diagnostic settings to send data to either storage account, log analytics workspace or event hub." } }, - "userAssignedResourceIds": { - "type": "array", - "items": { - "type": "string" - }, + "eventHubAuthorizationRuleResourceId": { + "type": "string", "nullable": true, "metadata": { - "description": "Optional. The resource ID(s) to assign to the resource. Required if a user assigned identity is used for encryption." + "description": "Optional. Resource ID of the diagnostic event hub authorization rule for the Event Hubs namespace in which the event hub should be created or streamed to." + } + }, + "eventHubName": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Name of the diagnostic event hub within the namespace to which logs are streamed. Without this, an event hub is created for each log category. For security reasons, it is recommended to set diagnostic settings to send data to either storage account, log analytics workspace or event hub." + } + }, + "marketplacePartnerResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The full ARM resource ID of the Marketplace resource to which you would like to send Diagnostic Logs." } } }, "metadata": { - "description": "An AVM-aligned type for a managed identity configuration. To be used if both a system-assigned & user-assigned identities are supported by the resource provider.", + "description": "An AVM-aligned type for a diagnostic setting. To be used if both logs & metrics are supported by the resource provider.", "__bicep_imported_from!": { - "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.0" + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.5.1" } } }, - "privateEndpointSingleServiceType": { + "ipConfigurationType": { "type": "object", "properties": { "name": { "type": "string", "nullable": true, "metadata": { - "description": "Optional. The name of the Private Endpoint." + "description": "Optional. The name of the IP configuration." } }, - "location": { + "privateIPAllocationMethod": { "type": "string", + "allowedValues": [ + "Dynamic", + "Static" + ], "nullable": true, "metadata": { - "description": "Optional. The location to deploy the Private Endpoint to." + "description": "Optional. The private IP address allocation method." } }, - "privateLinkServiceConnectionName": { + "privateIPAddress": { "type": "string", "nullable": true, "metadata": { - "description": "Optional. The name of the private link connection to create." + "description": "Optional. The private IP address." } }, - "service": { + "subnetResourceId": { "type": "string", + "metadata": { + "description": "Required. The resource ID of the subnet." + } + }, + "loadBalancerBackendAddressPools": { + "type": "array", + "items": { + "$ref": "#/definitions/_1.backendAddressPoolType" + }, "nullable": true, "metadata": { - "description": "Optional. The subresource to deploy the Private Endpoint for. For example \"vault\" for a Key Vault Private Endpoint." + "description": "Optional. The load balancer backend address pools." } }, - "subnetResourceId": { - "type": "string", + "applicationSecurityGroups": { + "type": "array", + "items": { + "$ref": "#/definitions/_1.applicationSecurityGroupType" + }, + "nullable": true, "metadata": { - "description": "Required. Resource ID of the subnet where the endpoint needs to be created." + "description": "Optional. The application security groups." } }, - "resourceGroupResourceId": { - "type": "string", + "applicationGatewayBackendAddressPools": { + "type": "array", + "items": { + "$ref": "#/definitions/_1.applicationGatewayBackendAddressPoolsType" + }, "nullable": true, "metadata": { - "description": "Optional. The resource ID of the Resource Group the Private Endpoint will be created in. If not specified, the Resource Group of the provided Virtual Network Subnet is used." + "description": "Optional. The application gateway backend address pools." } }, - "privateDnsZoneGroup": { - "$ref": "#/definitions/_2.privateEndpointPrivateDnsZoneGroupType", + "gatewayLoadBalancer": { + "$ref": "#/definitions/subResourceType", "nullable": true, "metadata": { - "description": "Optional. The private DNS Zone Group to configure for the Private Endpoint." + "description": "Optional. The gateway load balancer settings." } }, - "isManualConnection": { - "type": "bool", + "loadBalancerInboundNatRules": { + "type": "array", + "items": { + "$ref": "#/definitions/_1.inboundNatRuleType" + }, "nullable": true, "metadata": { - "description": "Optional. If Manual Private Link Connection is required." + "description": "Optional. The load balancer inbound NAT rules." } }, - "manualConnectionRequestMessage": { + "privateIPAddressVersion": { "type": "string", + "allowedValues": [ + "IPv4", + "IPv6" + ], "nullable": true, - "maxLength": 140, "metadata": { - "description": "Optional. A message passed to the owner of the remote resource with the manual connection request." + "description": "Optional. The private IP address version." } }, - "customDnsConfigs": { + "virtualNetworkTaps": { "type": "array", "items": { - "$ref": "#/definitions/_2.privateEndpointCustomDnsConfigType" + "$ref": "#/definitions/_1.virtualNetworkTapType" }, "nullable": true, "metadata": { - "description": "Optional. Custom DNS configurations." + "description": "Optional. The virtual network taps." } }, - "ipConfigurations": { - "type": "array", - "items": { - "$ref": "#/definitions/_2.privateEndpointIpConfigurationType" - }, + "pipConfiguration": { + "$ref": "#/definitions/_4.publicIPConfigurationType", "nullable": true, "metadata": { - "description": "Optional. A list of IP configurations of the Private Endpoint. This will be used to map to the first-party Service endpoints." + "description": "Optional. The public IP address configuration." } }, - "applicationSecurityGroupResourceIds": { + "diagnosticSettings": { "type": "array", "items": { - "type": "string" + "$ref": "#/definitions/_3.diagnosticSettingFullType" }, "nullable": true, "metadata": { - "description": "Optional. Application security groups in which the Private Endpoint IP configuration is included." + "description": "Optional. The diagnostic settings of the IP configuration." } }, - "customNetworkInterfaceName": { + "tags": { + "type": "object", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.Network/networkInterfaces@2024-07-01#properties/tags" + }, + "description": "Optional. The tags of the public IP address." + }, + "nullable": true + }, + "enableTelemetry": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. Enable/Disable usage telemetry for the module." + } + } + }, + "metadata": { + "description": "The type for the IP configuration.", + "__bicep_imported_from!": { + "sourceTemplate": "modules/nic-configuration.bicep" + } + } + }, + "lockType": { + "type": "object", + "properties": { + "name": { "type": "string", "nullable": true, "metadata": { - "description": "Optional. The custom name of the network interface attached to the Private Endpoint." + "description": "Optional. Specify the name of lock." } }, - "lock": { - "$ref": "#/definitions/_2.lockType", + "kind": { + "type": "string", + "allowedValues": [ + "CanNotDelete", + "None", + "ReadOnly" + ], "nullable": true, "metadata": { "description": "Optional. Specify the type of lock." } }, - "roleAssignments": { + "notes": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Specify the notes of the lock." + } + } + }, + "metadata": { + "description": "An AVM-aligned type for a lock.", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.0" + } + } + }, + "managedIdentityAllType": { + "type": "object", + "properties": { + "systemAssigned": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. Enables system assigned managed identity on the resource." + } + }, + "userAssignedResourceIds": { "type": "array", "items": { - "$ref": "#/definitions/_2.roleAssignmentType" + "type": "string" }, "nullable": true, "metadata": { - "description": "Optional. Array of role assignments to create." + "description": "Optional. The resource ID(s) to assign to the resource. Required if a user assigned identity is used for encryption." + } + } + }, + "metadata": { + "description": "An AVM-aligned type for a managed identity configuration. To be used if both a system-assigned & user-assigned identities are supported by the resource provider.", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.5.1" + } + } + }, + "networkInterfaceIPConfigurationOutputType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "metadata": { + "description": "The name of the IP configuration." } }, - "tags": { - "type": "object", + "privateIP": { + "type": "string", "nullable": true, "metadata": { - "__bicep_resource_derived_type!": { - "source": "Microsoft.Network/privateEndpoints@2024-07-01#properties/tags" - }, - "description": "Optional. Tags to be applied on all resources/Resource Groups in this deployment." + "description": "The private IP address." } }, - "enableTelemetry": { - "type": "bool", + "publicIP": { + "type": "string", "nullable": true, "metadata": { - "description": "Optional. Enable/Disable usage telemetry for module." + "description": "The public IP address." } } }, "metadata": { - "description": "An AVM-aligned type for a private endpoint. To be used if the private endpoint's default service / groupId can be assumed (i.e., for services that only have one Private Endpoint type like 'vault' for key vault).", "__bicep_imported_from!": { - "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.1" + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/res/network/network-interface:0.5.1" } } }, @@ -11890,23 +11333,25 @@ "metadata": { "description": "An AVM-aligned type for a role assignment.", "__bicep_imported_from!": { - "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.0" + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.5.1" } } }, - "secretsOutputType": { + "subResourceType": { "type": "object", - "properties": {}, - "additionalProperties": { - "$ref": "#/definitions/_1.secretSetOutputType", - "metadata": { - "description": "An exported secret's references." + "properties": { + "id": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Resource ID of the sub resource." + } } }, "metadata": { - "description": "A map of the exported secrets", + "description": "The type for the sub resource.", "__bicep_imported_from!": { - "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.0" + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/res/network/network-interface:0.5.1" } } } @@ -11915,555 +11360,1010 @@ "name": { "type": "string", "metadata": { - "description": "Required. The name of Cognitive Services account." + "description": "Required. The name of the virtual machine to be created. You should use a unique prefix to reduce name collisions in Active Directory." } }, - "kind": { + "computerName": { "type": "string", - "allowedValues": [ - "AIServices", - "AnomalyDetector", - "CognitiveServices", - "ComputerVision", - "ContentModerator", - "ContentSafety", - "ConversationalLanguageUnderstanding", - "CustomVision.Prediction", - "CustomVision.Training", - "Face", - "FormRecognizer", - "HealthInsights", - "ImmersiveReader", - "Internal.AllInOne", - "LUIS", - "LUIS.Authoring", - "LanguageAuthoring", - "MetricsAdvisor", - "OpenAI", - "Personalizer", - "QnAMaker.v2", - "SpeechServices", - "TextAnalytics", - "TextTranslation" - ], + "defaultValue": "[parameters('name')]", "metadata": { - "description": "Required. Kind of the Cognitive Services account. Use 'Get-AzCognitiveServicesAccountSku' to determine a valid combinations of 'kind' and 'SKU' for your Azure region." + "description": "Optional. Can be used if the computer name needs to be different from the Azure VM resource name. If not used, the resource name will be used as computer name." } }, - "sku": { + "vmSize": { "type": "string", - "defaultValue": "S0", - "allowedValues": [ - "C2", - "C3", - "C4", - "F0", - "F1", - "S", - "S0", - "S1", - "S10", - "S2", - "S3", - "S4", - "S5", - "S6", - "S7", - "S8", - "S9", - "DC0" - ], "metadata": { - "description": "Optional. SKU of the Cognitive Services account. Use 'Get-AzCognitiveServicesAccountSku' to determine a valid combinations of 'kind' and 'SKU' for your Azure region." + "description": "Required. Specifies the size for the VMs." } }, - "location": { - "type": "string", - "defaultValue": "[resourceGroup().location]", + "encryptionAtHost": { + "type": "bool", + "defaultValue": false, "metadata": { - "description": "Optional. Location for all Resources." + "description": "Optional. This property can be used by user in the request to enable or disable the Host Encryption for the virtual machine. This will enable the encryption for all the disks including Resource/Temp disk at host itself. For security reasons, it is recommended to set encryptionAtHost to True. Restrictions: Cannot be enabled if Azure Disk Encryption (guest-VM encryption using bitlocker/DM-Crypt) is enabled on your VMs." } }, - "diagnosticSettings": { - "type": "array", - "items": { - "$ref": "#/definitions/diagnosticSettingFullType" + "securityType": { + "type": "string", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.Compute/virtualMachines@2025-04-01#properties/properties/properties/securityProfile/properties/securityType" + }, + "description": "Optional. Specifies the SecurityType of the virtual machine. It has to be set to any specified value to enable UefiSettings. The default behavior is: UefiSettings will not be enabled unless this property is set." }, - "nullable": true, + "nullable": true + }, + "secureBootEnabled": { + "type": "bool", + "defaultValue": false, "metadata": { - "description": "Optional. The diagnostic settings of the service." + "description": "Optional. Specifies whether secure boot should be enabled on the virtual machine. This parameter is part of the UefiSettings. SecurityType should be set to TrustedLaunch to enable UefiSettings." } }, - "publicNetworkAccess": { - "type": "string", - "nullable": true, - "allowedValues": [ - "Enabled", - "Disabled" - ], + "vTpmEnabled": { + "type": "bool", + "defaultValue": false, "metadata": { - "description": "Optional. Whether or not public network access is allowed for this resource. For security reasons it should be disabled. If not specified, it will be disabled by default if private endpoints are set and networkAcls are not set." + "description": "Optional. Specifies whether vTPM should be enabled on the virtual machine. This parameter is part of the UefiSettings. SecurityType should be set to TrustedLaunch to enable UefiSettings." } }, - "customSubDomainName": { - "type": "string", + "imageReference": { + "$ref": "#/definitions/imageReferenceType", "nullable": true, "metadata": { - "description": "Conditional. Subdomain name used for token-based authentication. Required if 'networkAcls' or 'privateEndpoints' are set." + "description": "Conditional. OS image reference. In case of marketplace images, it's the combination of the publisher, offer, sku, version attributes. In case of custom images it's the resource ID of the custom image. Required if not creating the VM from an existing os-disk via the `osDisk.managedDisk.resourceId` parameter." } }, - "networkAcls": { - "type": "object", + "plan": { + "$ref": "#/definitions/planType", "nullable": true, "metadata": { - "description": "Optional. A collection of rules governing the accessibility from specific network locations." + "description": "Optional. Specifies information about the marketplace image used to create the virtual machine. This element is only used for marketplace images. Before you can use a marketplace image from an API, you must enable the image for programmatic use." } }, - "networkInjections": { - "$ref": "#/definitions/networkInjectionType", - "nullable": true, + "osDisk": { + "$ref": "#/definitions/osDiskType", "metadata": { - "description": "Optional. Specifies in AI Foundry where virtual network injection occurs to secure scenarios like Agents entirely within a private network." + "description": "Required. Specifies the OS disk. For security reasons, it is recommended to specify DiskEncryptionSet into the osDisk object. Restrictions: DiskEncryptionSet cannot be enabled if Azure Disk Encryption (guest-VM encryption using bitlocker/DM-Crypt) is enabled on your VMs." } }, - "privateEndpoints": { + "dataDisks": { "type": "array", "items": { - "$ref": "#/definitions/privateEndpointSingleServiceType" + "$ref": "#/definitions/dataDiskType" }, "nullable": true, "metadata": { - "description": "Optional. Configuration details for private endpoints. For security reasons, it is recommended to use private endpoints whenever possible." + "description": "Optional. Specifies the data disks. For security reasons, it is recommended to specify DiskEncryptionSet into the dataDisk object. Restrictions: DiskEncryptionSet cannot be enabled if Azure Disk Encryption (guest-VM encryption using bitlocker/DM-Crypt) is enabled on your VMs." } }, - "lock": { - "$ref": "#/definitions/lockType", + "ultraSSDEnabled": { + "type": "bool", + "defaultValue": false, + "metadata": { + "description": "Optional. The flag that enables or disables a capability to have one or more managed data disks with UltraSSD_LRS storage account type on the VM or VMSS. Managed disks with storage account type UltraSSD_LRS can be added to a virtual machine or virtual machine scale set only if this property is enabled." + } + }, + "hibernationEnabled": { + "type": "bool", + "defaultValue": false, + "metadata": { + "description": "Optional. The flag that enables or disables hibernation capability on the VM." + } + }, + "adminUsername": { + "type": "securestring", "nullable": true, "metadata": { - "description": "Optional. The lock settings of the service." + "description": "Conditional. Administrator username. Required if no pre-existing OS-Disk is provided (osDisk.managedDisk.resourceId is not empty)." } }, - "roleAssignments": { + "adminPassword": { + "type": "securestring", + "defaultValue": "", + "metadata": { + "description": "Optional. When specifying a Windows Virtual Machine, and no pre-existing OS-Disk is provided (osDisk.managedDisk.resourceId is not empty), this value should be passed." + } + }, + "userData": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "Optional. UserData for the VM, which must be base-64 encoded. Customer should not pass any secrets in here." + } + }, + "customData": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "Optional. Custom data associated to the VM, this value will be automatically converted into base64 to account for the expected VM format." + } + }, + "certificatesToBeInstalled": { "type": "array", "items": { - "$ref": "#/definitions/roleAssignmentType" + "$ref": "#/definitions/vaultSecretGroupType" }, "nullable": true, "metadata": { - "description": "Optional. Array of role assignments to create." + "description": "Optional. Specifies set of certificates that should be installed onto the virtual machine." } }, - "tags": { - "type": "object", + "priority": { + "type": "string", "nullable": true, + "allowedValues": [ + "Regular", + "Low", + "Spot" + ], "metadata": { - "description": "Optional. Tags of the resource." + "description": "Optional. Specifies the priority for the virtual machine." } }, - "allowedFqdnList": { - "type": "array", - "nullable": true, + "evictionPolicy": { + "type": "string", + "defaultValue": "Deallocate", + "allowedValues": [ + "Deallocate", + "Delete" + ], "metadata": { - "description": "Optional. List of allowed FQDN." + "description": "Optional. Specifies the eviction policy for the low priority virtual machine." } }, - "apiProperties": { - "type": "object", - "nullable": true, + "maxPriceForLowPriorityVm": { + "type": "string", + "defaultValue": "", "metadata": { - "description": "Optional. The API properties for special APIs." + "description": "Optional. Specifies the maximum price you are willing to pay for a low priority VM/VMSS. This price is in US Dollars." } }, - "disableLocalAuth": { - "type": "bool", - "defaultValue": true, + "dedicatedHostResourceId": { + "type": "string", + "defaultValue": "", "metadata": { - "description": "Optional. Allow only Azure AD authentication. Should be enabled for security reasons." + "description": "Optional. Specifies resource ID about the dedicated host that the virtual machine resides in." } }, - "customerManagedKey": { - "$ref": "#/definitions/customerManagedKeyType", + "licenseType": { + "type": "string", "nullable": true, + "allowedValues": [ + "RHEL_BYOS", + "SLES_BYOS", + "Windows_Client", + "Windows_Server" + ], "metadata": { - "description": "Optional. The customer managed key definition." + "description": "Optional. Specifies that the image or disk that is being used was licensed on-premises." } }, - "dynamicThrottlingEnabled": { - "type": "bool", - "defaultValue": false, + "publicKeys": { + "type": "array", + "items": { + "$ref": "#/definitions/publicKeyType" + }, + "defaultValue": [], "metadata": { - "description": "Optional. The flag to enable dynamic throttling." + "description": "Optional. The list of SSH public keys used to authenticate with linux based VMs." } }, - "migrationToken": { - "type": "securestring", + "managedIdentities": { + "$ref": "#/definitions/managedIdentityAllType", "nullable": true, "metadata": { - "description": "Optional. Resource migration token." + "description": "Optional. The managed identity definition for this resource. The system-assigned managed identity will automatically be enabled if extensionAadJoinConfig.enabled = \"True\"." } }, - "restore": { + "bootDiagnostics": { "type": "bool", "defaultValue": false, "metadata": { - "description": "Optional. Restore a soft-deleted cognitive service at deployment time. Will fail if no such soft-deleted resource exists." + "description": "Optional. Whether boot diagnostics should be enabled on the Virtual Machine. Boot diagnostics will be enabled with a managed storage account if no bootDiagnosticsStorageAccountName value is provided. If bootDiagnostics and bootDiagnosticsStorageAccountName values are not provided, boot diagnostics will be disabled." } }, - "restrictOutboundNetworkAccess": { - "type": "bool", - "defaultValue": true, + "bootDiagnosticStorageAccountName": { + "type": "string", + "defaultValue": "", "metadata": { - "description": "Optional. Restrict outbound network access." + "description": "Optional. Custom storage account used to store boot diagnostic information. Boot diagnostics will be enabled with a custom storage account if a value is provided." } }, - "userOwnedStorage": { - "type": "array", + "bootDiagnosticStorageAccountUri": { + "type": "string", + "defaultValue": "[format('.blob.{0}/', environment().suffixes.storage)]", "metadata": { - "__bicep_resource_derived_type!": { - "source": "Microsoft.CognitiveServices/accounts@2025-04-01-preview#properties/properties/properties/userOwnedStorage" - }, - "description": "Optional. The storage accounts for this resource." - }, - "nullable": true + "description": "Optional. Storage account boot diagnostic base URI." + } }, - "managedIdentities": { - "$ref": "#/definitions/managedIdentityAllType", - "nullable": true, + "proximityPlacementGroupResourceId": { + "type": "string", + "defaultValue": "", "metadata": { - "description": "Optional. The managed identity definition for this resource." + "description": "Optional. Resource ID of a proximity placement group." } }, - "enableTelemetry": { - "type": "bool", - "defaultValue": true, + "virtualMachineScaleSetResourceId": { + "type": "string", + "defaultValue": "", "metadata": { - "description": "Optional. Enable/Disable usage telemetry for module." + "description": "Optional. Resource ID of a virtual machine scale set, where the VM should be added." } }, - "deployments": { - "type": "array", - "items": { - "$ref": "#/definitions/deploymentType" - }, - "nullable": true, + "availabilitySetResourceId": { + "type": "string", + "defaultValue": "", "metadata": { - "description": "Optional. Array of deployments about cognitive service accounts to create." + "description": "Optional. Resource ID of an availability set. Cannot be used in combination with availability zone nor scale set." } }, - "secretsExportConfiguration": { - "$ref": "#/definitions/secretsExportConfigurationType", + "galleryApplications": { + "type": "array", + "items": { + "$ref": "#/definitions/vmGalleryApplicationType" + }, "nullable": true, "metadata": { - "description": "Optional. Key vault reference and secret settings for the module's secrets export." + "description": "Optional. Specifies the gallery applications that should be made available to the VM/VMSS." } }, - "allowProjectManagement": { - "type": "bool", - "nullable": true, + "availabilityZone": { + "type": "int", + "allowedValues": [ + -1, + 1, + 2, + 3 + ], "metadata": { - "description": "Optional. Enable/Disable project management feature for AI Foundry." + "description": "Required. If set to 1, 2 or 3, the availability zone is hardcoded to that value. If set to -1, no zone is defined. Note that the availability zone numbers here are the logical availability zone in your Azure subscription. Different subscriptions might have a different mapping of the physical zone and logical zone. To understand more, please refer to [Physical and logical availability zones](https://learn.microsoft.com/en-us/azure/reliability/availability-zones-overview?tabs=azure-cli#physical-and-logical-availability-zones)." } }, - "commitmentPlans": { + "nicConfigurations": { "type": "array", "items": { - "$ref": "#/definitions/commitmentPlanType" + "$ref": "#/definitions/nicConfigurationType" }, - "nullable": true, "metadata": { - "description": "Optional. Commitment plans to deploy for the cognitive services account." + "description": "Required. Configures NICs and PIPs." } - } - }, - "variables": { - "copy": [ - { - "name": "formattedRoleAssignments", - "count": "[length(coalesce(parameters('roleAssignments'), createArray()))]", - "input": "[union(coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')], createObject('roleDefinitionId', coalesce(tryGet(variables('builtInRoleNames'), coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName), if(contains(coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName, '/providers/Microsoft.Authorization/roleDefinitions/'), coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName, subscriptionResourceId('Microsoft.Authorization/roleDefinitions', coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName)))))]" + }, + "backupVaultName": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "Optional. Recovery service vault name to add VMs to backup." } - ], - "enableReferencedModulesTelemetry": false, - "formattedUserAssignedIdentities": "[reduce(map(coalesce(tryGet(parameters('managedIdentities'), 'userAssignedResourceIds'), createArray()), lambda('id', createObject(format('{0}', lambdaVariables('id')), createObject()))), createObject(), lambda('cur', 'next', union(lambdaVariables('cur'), lambdaVariables('next'))))]", - "identity": "[if(not(empty(parameters('managedIdentities'))), createObject('type', if(coalesce(tryGet(parameters('managedIdentities'), 'systemAssigned'), false()), if(not(empty(coalesce(tryGet(parameters('managedIdentities'), 'userAssignedResourceIds'), createObject()))), 'SystemAssigned, UserAssigned', 'SystemAssigned'), if(not(empty(coalesce(tryGet(parameters('managedIdentities'), 'userAssignedResourceIds'), createObject()))), 'UserAssigned', null())), 'userAssignedIdentities', if(not(empty(variables('formattedUserAssignedIdentities'))), variables('formattedUserAssignedIdentities'), null())), null())]", - "builtInRoleNames": { - "Cognitive Services Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '25fbc0a9-bd7c-42a3-aa1a-3b75d497ee68')]", - "Cognitive Services Custom Vision Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'c1ff6cc2-c111-46fe-8896-e0ef812ad9f3')]", - "Cognitive Services Custom Vision Deployment": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '5c4089e1-6d96-4d2f-b296-c1bc7137275f')]", - "Cognitive Services Custom Vision Labeler": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '88424f51-ebe7-446f-bc41-7fa16989e96c')]", - "Cognitive Services Custom Vision Reader": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '93586559-c37d-4a6b-ba08-b9f0940c2d73')]", - "Cognitive Services Custom Vision Trainer": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '0a5ae4ab-0d65-4eeb-be61-29fc9b54394b')]", - "Cognitive Services Data Reader (Preview)": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'b59867f0-fa02-499b-be73-45a86b5b3e1c')]", - "Cognitive Services Face Recognizer": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '9894cab4-e18a-44aa-828b-cb588cd6f2d7')]", - "Cognitive Services Immersive Reader User": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'b2de6794-95db-4659-8781-7e080d3f2b9d')]", - "Cognitive Services Language Owner": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'f07febfe-79bc-46b1-8b37-790e26e6e498')]", - "Cognitive Services Language Reader": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '7628b7b8-a8b2-4cdc-b46f-e9b35248918e')]", - "Cognitive Services Language Writer": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'f2310ca1-dc64-4889-bb49-c8e0fa3d47a8')]", - "Cognitive Services LUIS Owner": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'f72c8140-2111-481c-87ff-72b910f6e3f8')]", - "Cognitive Services LUIS Reader": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '18e81cdc-4e98-4e29-a639-e7d10c5a6226')]", - "Cognitive Services LUIS Writer": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '6322a993-d5c9-4bed-b113-e49bbea25b27')]", - "Cognitive Services Metrics Advisor Administrator": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'cb43c632-a144-4ec5-977c-e80c4affc34a')]", - "Cognitive Services Metrics Advisor User": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '3b20f47b-3825-43cb-8114-4bd2201156a8')]", - "Cognitive Services OpenAI Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'a001fd3d-188f-4b5d-821b-7da978bf7442')]", - "Cognitive Services OpenAI User": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '5e0bd9bd-7b93-4f28-af87-19fc36ad61bd')]", - "Cognitive Services QnA Maker Editor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'f4cc2bf9-21be-47a1-bdf1-5c5804381025')]", - "Cognitive Services QnA Maker Reader": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '466ccd10-b268-4a11-b098-b4849f024126')]", - "Cognitive Services Speech Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '0e75ca1e-0464-4b4d-8b93-68208a576181')]", - "Cognitive Services Speech User": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'f2dc8367-1007-4938-bd23-fe263f013447')]", - "Cognitive Services User": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'a97b65f3-24c7-4388-baec-2e87135dc908')]", - "Azure AI Developer": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '64702f94-c441-49e6-a78b-ef80e0188fee')]", - "Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'b24988ac-6180-42a0-ab88-20f7382dd24c')]", - "Owner": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '8e3af657-a8ff-443c-a75c-2fe8c4bcb635')]", - "Reader": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'acdd72a7-3385-48ef-bd42-f606fba81ae7')]", - "Role Based Access Control Administrator": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'f58310d9-a9f6-439a-9e8d-f62e7b41a168')]", - "User Access Administrator": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '18d7d88d-d35e-4fb5-a5c3-7773c20a72d9')]" }, - "isHSMManagedCMK": "[equals(tryGet(split(coalesce(tryGet(parameters('customerManagedKey'), 'keyVaultResourceId'), ''), '/'), 7), 'managedHSMs')]" - }, - "resources": { - "cMKKeyVault::cMKKey": { - "condition": "[and(and(not(empty(parameters('customerManagedKey'))), not(variables('isHSMManagedCMK'))), and(not(empty(parameters('customerManagedKey'))), not(variables('isHSMManagedCMK'))))]", - "existing": true, - "type": "Microsoft.KeyVault/vaults/keys", - "apiVersion": "2025-05-01", - "subscriptionId": "[split(tryGet(parameters('customerManagedKey'), 'keyVaultResourceId'), '/')[2]]", - "resourceGroup": "[split(tryGet(parameters('customerManagedKey'), 'keyVaultResourceId'), '/')[4]]", - "name": "[format('{0}/{1}', last(split(tryGet(parameters('customerManagedKey'), 'keyVaultResourceId'), '/')), tryGet(parameters('customerManagedKey'), 'keyName'))]" + "backupVaultResourceGroup": { + "type": "string", + "defaultValue": "[resourceGroup().name]", + "metadata": { + "description": "Optional. Resource group of the backup recovery service vault. If not provided the current resource group name is considered by default." + } }, - "avmTelemetry": { - "condition": "[parameters('enableTelemetry')]", - "type": "Microsoft.Resources/deployments", - "apiVersion": "2024-03-01", - "name": "[format('46d3xbcp.res.cognitiveservices-account.{0}.{1}', replace('0.14.0', '.', '-'), substring(uniqueString(deployment().name, parameters('location')), 0, 4))]", - "properties": { - "mode": "Incremental", - "template": { - "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", - "contentVersion": "1.0.0.0", - "resources": [], - "outputs": { - "telemetry": { - "type": "String", - "value": "For more information, see https://aka.ms/avm/TelemetryInfo" - } - } - } + "backupPolicyName": { + "type": "string", + "defaultValue": "DefaultPolicy", + "metadata": { + "description": "Optional. Backup policy the VMs should be using for backup. If not provided, it will use the DefaultPolicy from the backup recovery service vault." } }, - "cMKKeyVault": { - "condition": "[and(not(empty(parameters('customerManagedKey'))), not(variables('isHSMManagedCMK')))]", - "existing": true, - "type": "Microsoft.KeyVault/vaults", - "apiVersion": "2025-05-01", - "subscriptionId": "[split(tryGet(parameters('customerManagedKey'), 'keyVaultResourceId'), '/')[2]]", - "resourceGroup": "[split(tryGet(parameters('customerManagedKey'), 'keyVaultResourceId'), '/')[4]]", - "name": "[last(split(tryGet(parameters('customerManagedKey'), 'keyVaultResourceId'), '/'))]" + "autoShutdownConfig": { + "$ref": "#/definitions/autoShutDownConfigType", + "defaultValue": {}, + "metadata": { + "description": "Optional. The configuration for auto-shutdown." + } }, - "cMKUserAssignedIdentity": { - "condition": "[not(empty(tryGet(parameters('customerManagedKey'), 'userAssignedIdentityResourceId')))]", - "existing": true, - "type": "Microsoft.ManagedIdentity/userAssignedIdentities", - "apiVersion": "2025-01-31-preview", - "subscriptionId": "[split(tryGet(parameters('customerManagedKey'), 'userAssignedIdentityResourceId'), '/')[2]]", - "resourceGroup": "[split(tryGet(parameters('customerManagedKey'), 'userAssignedIdentityResourceId'), '/')[4]]", - "name": "[last(split(tryGet(parameters('customerManagedKey'), 'userAssignedIdentityResourceId'), '/'))]" + "maintenanceConfigurationResourceId": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "Optional. The resource Id of a maintenance configuration for this VM." + } }, - "cognitiveService": { - "type": "Microsoft.CognitiveServices/accounts", - "apiVersion": "2025-06-01", - "name": "[parameters('name')]", - "kind": "[parameters('kind')]", - "identity": "[variables('identity')]", - "location": "[parameters('location')]", - "tags": "[parameters('tags')]", - "sku": { - "name": "[parameters('sku')]" - }, - "properties": { - "allowProjectManagement": "[parameters('allowProjectManagement')]", - "customSubDomainName": "[parameters('customSubDomainName')]", - "networkAcls": "[if(not(empty(coalesce(parameters('networkAcls'), createObject()))), createObject('defaultAction', tryGet(parameters('networkAcls'), 'defaultAction'), 'virtualNetworkRules', coalesce(tryGet(parameters('networkAcls'), 'virtualNetworkRules'), createArray()), 'ipRules', coalesce(tryGet(parameters('networkAcls'), 'ipRules'), createArray())), null())]", - "networkInjections": "[if(not(empty(parameters('networkInjections'))), createArray(createObject('scenario', tryGet(parameters('networkInjections'), 'scenario'), 'subnetArmId', tryGet(parameters('networkInjections'), 'subnetResourceId'), 'useMicrosoftManagedNetwork', coalesce(tryGet(parameters('networkInjections'), 'useMicrosoftManagedNetwork'), false()))), null())]", - "publicNetworkAccess": "[if(not(equals(parameters('publicNetworkAccess'), null())), parameters('publicNetworkAccess'), if(not(empty(parameters('networkAcls'))), 'Enabled', 'Disabled'))]", - "allowedFqdnList": "[parameters('allowedFqdnList')]", - "apiProperties": "[parameters('apiProperties')]", - "disableLocalAuth": "[parameters('disableLocalAuth')]", - "encryption": "[if(not(empty(parameters('customerManagedKey'))), createObject('keySource', 'Microsoft.KeyVault', 'keyVaultProperties', createObject('identityClientId', if(not(empty(coalesce(tryGet(parameters('customerManagedKey'), 'userAssignedIdentityResourceId'), ''))), reference('cMKUserAssignedIdentity').clientId, null()), 'keyVaultUri', if(not(variables('isHSMManagedCMK')), reference('cMKKeyVault').vaultUri, format('https://{0}.managedhsm.azure.net/', last(split(parameters('customerManagedKey').keyVaultResourceId, '/')))), 'keyName', parameters('customerManagedKey').keyName, 'keyVersion', if(not(empty(tryGet(parameters('customerManagedKey'), 'keyVersion'))), parameters('customerManagedKey').keyVersion, if(not(variables('isHSMManagedCMK')), last(split(reference('cMKKeyVault::cMKKey').keyUriWithVersion, '/')), fail('Managed HSM CMK encryption requires specifying the ''keyVersion''.'))))), null())]", - "migrationToken": "[parameters('migrationToken')]", - "restore": "[parameters('restore')]", - "restrictOutboundNetworkAccess": "[parameters('restrictOutboundNetworkAccess')]", - "userOwnedStorage": "[if(not(empty(parameters('userOwnedStorage'))), parameters('userOwnedStorage'), null())]", - "dynamicThrottlingEnabled": "[parameters('dynamicThrottlingEnabled')]" + "allowExtensionOperations": { + "type": "bool", + "defaultValue": true, + "metadata": { + "description": "Optional. Specifies whether extension operations should be allowed on the virtual machine. This may only be set to False when no extensions are present on the virtual machine." + } + }, + "extensionDomainJoinPassword": { + "type": "securestring", + "defaultValue": "", + "metadata": { + "description": "Optional. Required if name is specified. Password of the user specified in user parameter." + } + }, + "extensionDomainJoinConfig": { + "type": "secureObject", + "defaultValue": {}, + "metadata": { + "description": "Optional. The configuration for the [Domain Join] extension. Must at least contain the [\"enabled\": true] property to be executed." + } + }, + "extensionAadJoinConfig": { + "type": "object", + "defaultValue": { + "enabled": false }, - "dependsOn": [ - "cMKKeyVault", - "cMKKeyVault::cMKKey", - "cMKUserAssignedIdentity" - ] + "metadata": { + "description": "Optional. The configuration for the [AAD Join] extension. Must at least contain the [\"enabled\": true] property to be executed. To enroll in Intune, add the setting mdmId: \"0000000a-0000-0000-c000-000000000000\"." + } }, - "cognitiveService_deployments": { - "copy": { - "name": "cognitiveService_deployments", - "count": "[length(coalesce(parameters('deployments'), createArray()))]", - "mode": "serial", - "batchSize": 1 + "extensionAntiMalwareConfig": { + "type": "object", + "defaultValue": "[if(equals(parameters('osType'), 'Windows'), createObject('enabled', true()), createObject('enabled', false()))]", + "metadata": { + "description": "Optional. The configuration for the [Anti Malware] extension. Must at least contain the [\"enabled\": true] property to be executed." + } + }, + "extensionMonitoringAgentConfig": { + "type": "object", + "defaultValue": { + "enabled": false, + "dataCollectionRuleAssociations": [] }, - "type": "Microsoft.CognitiveServices/accounts/deployments", - "apiVersion": "2025-06-01", - "name": "[format('{0}/{1}', parameters('name'), coalesce(tryGet(coalesce(parameters('deployments'), createArray())[copyIndex()], 'name'), format('{0}-deployments', parameters('name'))))]", - "properties": { - "model": "[coalesce(parameters('deployments'), createArray())[copyIndex()].model]", - "raiPolicyName": "[tryGet(coalesce(parameters('deployments'), createArray())[copyIndex()], 'raiPolicyName')]", - "versionUpgradeOption": "[tryGet(coalesce(parameters('deployments'), createArray())[copyIndex()], 'versionUpgradeOption')]" + "metadata": { + "description": "Optional. The configuration for the [Monitoring Agent] extension. Must at least contain the [\"enabled\": true] property to be executed." + } + }, + "extensionDependencyAgentConfig": { + "type": "object", + "defaultValue": { + "enabled": false }, - "sku": "[coalesce(tryGet(coalesce(parameters('deployments'), createArray())[copyIndex()], 'sku'), createObject('name', parameters('sku'), 'capacity', tryGet(parameters('sku'), 'capacity'), 'tier', tryGet(parameters('sku'), 'tier'), 'size', tryGet(parameters('sku'), 'size'), 'family', tryGet(parameters('sku'), 'family')))]", - "dependsOn": [ - "cognitiveService" - ] + "metadata": { + "description": "Optional. The configuration for the [Dependency Agent] extension. Must at least contain the [\"enabled\": true] property to be executed." + } }, - "cognitiveService_lock": { - "condition": "[and(not(empty(coalesce(parameters('lock'), createObject()))), not(equals(tryGet(parameters('lock'), 'kind'), 'None')))]", - "type": "Microsoft.Authorization/locks", - "apiVersion": "2020-05-01", - "scope": "[format('Microsoft.CognitiveServices/accounts/{0}', parameters('name'))]", - "name": "[coalesce(tryGet(parameters('lock'), 'name'), format('lock-{0}', parameters('name')))]", - "properties": { - "level": "[coalesce(tryGet(parameters('lock'), 'kind'), '')]", - "notes": "[coalesce(tryGet(parameters('lock'), 'notes'), if(equals(tryGet(parameters('lock'), 'kind'), 'CanNotDelete'), 'Cannot delete resource or child resources.', 'Cannot delete or modify the resource or child resources.'))]" + "extensionNetworkWatcherAgentConfig": { + "type": "object", + "defaultValue": { + "enabled": false }, - "dependsOn": [ - "cognitiveService" - ] + "metadata": { + "description": "Optional. The configuration for the [Network Watcher Agent] extension. Must at least contain the [\"enabled\": true] property to be executed." + } }, - "cognitiveService_commitmentPlans": { - "copy": { - "name": "cognitiveService_commitmentPlans", - "count": "[length(coalesce(parameters('commitmentPlans'), createArray()))]" + "extensionAzureDiskEncryptionConfig": { + "type": "object", + "defaultValue": { + "enabled": false }, - "type": "Microsoft.CognitiveServices/accounts/commitmentPlans", - "apiVersion": "2025-06-01", - "name": "[format('{0}/{1}', parameters('name'), format('{0}-{1}', coalesce(parameters('commitmentPlans'), createArray())[copyIndex()].hostingModel, coalesce(parameters('commitmentPlans'), createArray())[copyIndex()].planType))]", - "properties": "[coalesce(parameters('commitmentPlans'), createArray())[copyIndex()]]", - "dependsOn": [ - "cognitiveService" - ] + "metadata": { + "description": "Optional. The configuration for the [Azure Disk Encryption] extension. Must at least contain the [\"enabled\": true] property to be executed. Restrictions: Cannot be enabled on disks that have encryption at host enabled. Managed disks encrypted using Azure Disk Encryption cannot be encrypted using customer-managed keys." + } }, - "cognitiveService_diagnosticSettings": { - "copy": { - "name": "cognitiveService_diagnosticSettings", - "count": "[length(coalesce(parameters('diagnosticSettings'), createArray()))]" + "extensionDSCConfig": { + "type": "object", + "defaultValue": { + "enabled": false }, - "type": "Microsoft.Insights/diagnosticSettings", - "apiVersion": "2021-05-01-preview", - "scope": "[format('Microsoft.CognitiveServices/accounts/{0}', parameters('name'))]", - "name": "[coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'name'), format('{0}-diagnosticSettings', parameters('name')))]", - "properties": { - "copy": [ - { - "name": "metrics", - "count": "[length(coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'metricCategories'), createArray(createObject('category', 'AllMetrics'))))]", - "input": { - "category": "[coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'metricCategories'), createArray(createObject('category', 'AllMetrics')))[copyIndex('metrics')].category]", - "enabled": "[coalesce(tryGet(coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'metricCategories'), createArray(createObject('category', 'AllMetrics')))[copyIndex('metrics')], 'enabled'), true())]", - "timeGrain": null + "metadata": { + "description": "Optional. The configuration for the [Desired State Configuration] extension. Must at least contain the [\"enabled\": true] property to be executed." + } + }, + "extensionCustomScriptConfig": { + "$ref": "#/definitions/extensionCustomScriptConfigType", + "nullable": true, + "metadata": { + "description": "Optional. The configuration for the [Custom Script] extension." + } + }, + "extensionNvidiaGpuDriverWindows": { + "type": "object", + "defaultValue": { + "enabled": false + }, + "metadata": { + "description": "Optional. The configuration for the [Nvidia Gpu Driver Windows] extension. Must at least contain the [\"enabled\": true] property to be executed." + } + }, + "extensionHostPoolRegistration": { + "type": "secureObject", + "defaultValue": { + "enabled": false + }, + "metadata": { + "description": "Optional. The configuration for the [Host Pool Registration] extension. Must at least contain the [\"enabled\": true] property to be executed. Needs a managed identity." + } + }, + "extensionGuestConfigurationExtension": { + "type": "object", + "defaultValue": { + "enabled": false + }, + "metadata": { + "description": "Optional. The configuration for the [Guest Configuration] extension. Must at least contain the [\"enabled\": true] property to be executed. Needs a managed identity." + } + }, + "guestConfiguration": { + "type": "object", + "defaultValue": {}, + "metadata": { + "description": "Optional. The guest configuration for the virtual machine. Needs the Guest Configuration extension to be enabled." + } + }, + "extensionGuestConfigurationExtensionProtectedSettings": { + "type": "secureObject", + "defaultValue": {}, + "metadata": { + "description": "Optional. An object that contains the extension specific protected settings." + } + }, + "location": { + "type": "string", + "defaultValue": "[resourceGroup().location]", + "metadata": { + "description": "Optional. Location for all resources." + } + }, + "lock": { + "$ref": "#/definitions/lockType", + "nullable": true, + "metadata": { + "description": "Optional. The lock settings of the service." + } + }, + "roleAssignments": { + "type": "array", + "items": { + "$ref": "#/definitions/roleAssignmentType" + }, + "nullable": true, + "metadata": { + "description": "Optional. Array of role assignments to create." + } + }, + "tags": { + "type": "object", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.Compute/virtualMachines@2024-11-01#properties/tags" + }, + "description": "Optional. Tags of the resource." + }, + "nullable": true + }, + "enableTelemetry": { + "type": "bool", + "defaultValue": true, + "metadata": { + "description": "Optional. Enable/Disable usage telemetry for module." + } + }, + "osType": { + "type": "string", + "allowedValues": [ + "Windows", + "Linux" + ], + "metadata": { + "description": "Required. The chosen OS type." + } + }, + "disablePasswordAuthentication": { + "type": "bool", + "defaultValue": false, + "metadata": { + "description": "Optional. Specifies whether password authentication should be disabled." + } + }, + "provisionVMAgent": { + "type": "bool", + "defaultValue": true, + "metadata": { + "description": "Optional. Indicates whether virtual machine agent should be provisioned on the virtual machine. When this property is not specified in the request body, default behavior is to set it to true. This will ensure that VM Agent is installed on the VM so that extensions can be added to the VM later." + } + }, + "enableAutomaticUpdates": { + "type": "bool", + "defaultValue": true, + "metadata": { + "description": "Optional. Indicates whether Automatic Updates is enabled for the Windows virtual machine. Default value is true. When patchMode is set to Manual, this parameter must be set to false. For virtual machine scale sets, this property can be updated and updates will take effect on OS reprovisioning." + } + }, + "patchMode": { + "type": "string", + "defaultValue": "", + "allowedValues": [ + "AutomaticByPlatform", + "AutomaticByOS", + "Manual", + "ImageDefault", + "" + ], + "metadata": { + "description": "Optional. VM guest patching orchestration mode. 'AutomaticByOS' & 'Manual' are for Windows only, 'ImageDefault' for Linux only. Refer to 'https://learn.microsoft.com/en-us/azure/virtual-machines/automatic-vm-guest-patching'." + } + }, + "bypassPlatformSafetyChecksOnUserSchedule": { + "type": "bool", + "defaultValue": true, + "metadata": { + "description": "Optional. Enables customer to schedule patching without accidental upgrades." + } + }, + "rebootSetting": { + "type": "string", + "defaultValue": "IfRequired", + "allowedValues": [ + "Always", + "IfRequired", + "Never", + "Unknown" + ], + "metadata": { + "description": "Optional. Specifies the reboot setting for all AutomaticByPlatform patch installation operations." + } + }, + "patchAssessmentMode": { + "type": "string", + "defaultValue": "ImageDefault", + "allowedValues": [ + "AutomaticByPlatform", + "ImageDefault" + ], + "metadata": { + "description": "Optional. VM guest patching assessment mode. Set it to 'AutomaticByPlatform' to enable automatically check for updates every 24 hours." + } + }, + "enableHotpatching": { + "type": "bool", + "defaultValue": false, + "metadata": { + "description": "Optional. Enables customers to patch their Azure VMs without requiring a reboot. For enableHotpatching, the 'provisionVMAgent' must be set to true and 'patchMode' must be set to 'AutomaticByPlatform'." + } + }, + "timeZone": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "Optional. Specifies the time zone of the virtual machine. e.g. 'Pacific Standard Time'. Possible values can be `TimeZoneInfo.id` value from time zones returned by `TimeZoneInfo.GetSystemTimeZones`." + } + }, + "additionalUnattendContent": { + "type": "array", + "items": { + "$ref": "#/definitions/additionalUnattendContentType" + }, + "nullable": true, + "metadata": { + "description": "Optional. Specifies additional XML formatted information that can be included in the Unattend.xml file, which is used by Windows Setup. Contents are defined by setting name, component name, and the pass in which the content is applied." + } + }, + "winRMListeners": { + "type": "array", + "items": { + "$ref": "#/definitions/winRMListenerType" + }, + "nullable": true, + "metadata": { + "description": "Optional. Specifies the Windows Remote Management listeners. This enables remote Windows PowerShell." + } + }, + "configurationProfile": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "Optional. The configuration profile of automanage. Either '/providers/Microsoft.Automanage/bestPractices/AzureBestPracticesProduction', 'providers/Microsoft.Automanage/bestPractices/AzureBestPracticesDevTest' or the resource Id of custom profile." + } + }, + "capacityReservationGroupResourceId": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "Optional. Capacity reservation group resource id that should be used for allocating the virtual machine vm instances provided enough capacity has been reserved." + } + }, + "networkAccessPolicy": { + "type": "string", + "defaultValue": "DenyAll", + "allowedValues": [ + "AllowAll", + "AllowPrivate", + "DenyAll" + ], + "metadata": { + "description": "Optional. Policy for accessing the disk via network." + } + }, + "publicNetworkAccess": { + "type": "string", + "defaultValue": "Disabled", + "allowedValues": [ + "Disabled", + "Enabled" + ], + "metadata": { + "description": "Optional. Policy for controlling export on the disk." + } + } + }, + "variables": { + "copy": [ + { + "name": "publicKeysFormatted", + "count": "[length(parameters('publicKeys'))]", + "input": { + "path": "[parameters('publicKeys')[copyIndex('publicKeysFormatted')].path]", + "keyData": "[parameters('publicKeys')[copyIndex('publicKeysFormatted')].keyData]" + } + }, + { + "name": "additionalUnattendContentFormatted", + "count": "[length(coalesce(parameters('additionalUnattendContent'), createArray()))]", + "input": { + "settingName": "[coalesce(parameters('additionalUnattendContent'), createArray())[copyIndex('additionalUnattendContentFormatted')].settingName]", + "content": "[coalesce(parameters('additionalUnattendContent'), createArray())[copyIndex('additionalUnattendContentFormatted')].content]", + "componentName": "Microsoft-Windows-Shell-Setup", + "passName": "OobeSystem" + } + }, + { + "name": "formattedRoleAssignments", + "count": "[length(coalesce(parameters('roleAssignments'), createArray()))]", + "input": "[union(coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')], createObject('roleDefinitionId', coalesce(tryGet(variables('builtInRoleNames'), coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName), if(contains(coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName, '/providers/Microsoft.Authorization/roleDefinitions/'), coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName, subscriptionResourceId('Microsoft.Authorization/roleDefinitions', coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName)))))]" + } + ], + "enableReferencedModulesTelemetry": false, + "linuxConfiguration": { + "disablePasswordAuthentication": "[parameters('disablePasswordAuthentication')]", + "ssh": { + "publicKeys": "[variables('publicKeysFormatted')]" + }, + "provisionVMAgent": "[parameters('provisionVMAgent')]", + "patchSettings": "[if(and(parameters('provisionVMAgent'), or(equals(toLower(parameters('patchMode')), toLower('AutomaticByPlatform')), equals(toLower(parameters('patchMode')), toLower('ImageDefault')))), createObject('patchMode', parameters('patchMode'), 'assessmentMode', parameters('patchAssessmentMode'), 'automaticByPlatformSettings', if(equals(toLower(parameters('patchMode')), toLower('AutomaticByPlatform')), createObject('bypassPlatformSafetyChecksOnUserSchedule', parameters('bypassPlatformSafetyChecksOnUserSchedule'), 'rebootSetting', parameters('rebootSetting')), null())), null())]" + }, + "windowsConfiguration": { + "provisionVMAgent": "[parameters('provisionVMAgent')]", + "enableAutomaticUpdates": "[parameters('enableAutomaticUpdates')]", + "patchSettings": "[if(and(parameters('provisionVMAgent'), or(or(equals(toLower(parameters('patchMode')), toLower('AutomaticByPlatform')), equals(toLower(parameters('patchMode')), toLower('AutomaticByOS'))), equals(toLower(parameters('patchMode')), toLower('Manual')))), createObject('patchMode', parameters('patchMode'), 'assessmentMode', parameters('patchAssessmentMode'), 'enableHotpatching', if(equals(toLower(parameters('patchMode')), toLower('AutomaticByPlatform')), parameters('enableHotpatching'), false()), 'automaticByPlatformSettings', if(equals(toLower(parameters('patchMode')), toLower('AutomaticByPlatform')), createObject('bypassPlatformSafetyChecksOnUserSchedule', parameters('bypassPlatformSafetyChecksOnUserSchedule'), 'rebootSetting', parameters('rebootSetting')), null())), null())]", + "timeZone": "[if(empty(parameters('timeZone')), null(), parameters('timeZone'))]", + "additionalUnattendContent": "[if(empty(parameters('additionalUnattendContent')), null(), variables('additionalUnattendContentFormatted'))]", + "winRM": "[if(not(empty(parameters('winRMListeners'))), createObject('listeners', parameters('winRMListeners')), null())]" + }, + "formattedUserAssignedIdentities": "[reduce(map(coalesce(tryGet(parameters('managedIdentities'), 'userAssignedResourceIds'), createArray()), lambda('id', createObject(format('{0}', lambdaVariables('id')), createObject()))), createObject(), lambda('cur', 'next', union(lambdaVariables('cur'), lambdaVariables('next'))))]", + "identity": "[if(not(empty(parameters('managedIdentities'))), createObject('type', if(if(parameters('extensionAadJoinConfig').enabled, true(), coalesce(tryGet(parameters('managedIdentities'), 'systemAssigned'), false())), if(not(empty(coalesce(tryGet(parameters('managedIdentities'), 'userAssignedResourceIds'), createObject()))), 'SystemAssigned, UserAssigned', 'SystemAssigned'), if(not(empty(coalesce(tryGet(parameters('managedIdentities'), 'userAssignedResourceIds'), createObject()))), 'UserAssigned', null())), 'userAssignedIdentities', if(not(empty(variables('formattedUserAssignedIdentities'))), variables('formattedUserAssignedIdentities'), null())), null())]", + "builtInRoleNames": { + "Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'b24988ac-6180-42a0-ab88-20f7382dd24c')]", + "Data Operator for Managed Disks": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '959f8984-c045-4866-89c7-12bf9737be2e')]", + "Desktop Virtualization Power On Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '489581de-a3bd-480d-9518-53dea7416b33')]", + "Desktop Virtualization Power On Off Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '40c5ff49-9181-41f8-ae61-143b0e78555e')]", + "Desktop Virtualization Virtual Machine Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'a959dbd1-f747-45e3-8ba6-dd80f235f97c')]", + "DevTest Labs User": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '76283e04-6283-4c54-8f91-bcf1374a3c64')]", + "Disk Backup Reader": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '3e5e47e6-65f7-47ef-90b5-e5dd4d455f24')]", + "Disk Pool Operator": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '60fc6e62-5479-42d4-8bf4-67625fcc2840')]", + "Disk Restore Operator": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'b50d9833-a0cb-478e-945f-707fcc997c13')]", + "Disk Snapshot Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '7efff54f-a5b4-42b5-a1c5-5411624893ce')]", + "Owner": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '8e3af657-a8ff-443c-a75c-2fe8c4bcb635')]", + "Reader": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'acdd72a7-3385-48ef-bd42-f606fba81ae7')]", + "Role Based Access Control Administrator": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'f58310d9-a9f6-439a-9e8d-f62e7b41a168')]", + "User Access Administrator": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '18d7d88d-d35e-4fb5-a5c3-7773c20a72d9')]", + "Virtual Machine Administrator Login": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '1c0163c0-47e6-4577-8991-ea5c82e286e4')]", + "Virtual Machine Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '9980e02c-c2be-4d73-94e8-173b1dc7cf3c')]", + "Virtual Machine User Login": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'fb879df8-f326-4884-b1cf-06f3ad86be52')]", + "VM Scanner Operator": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'd24ecba3-c1f4-40fa-a7bb-4588a071e8fd')]" + } + }, + "resources": { + "avmTelemetry": { + "condition": "[parameters('enableTelemetry')]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2024-03-01", + "name": "[format('46d3xbcp.res.compute-virtualmachine.{0}.{1}', replace('0.21.0', '.', '-'), substring(uniqueString(deployment().name, parameters('location')), 0, 4))]", + "properties": { + "mode": "Incremental", + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "resources": [], + "outputs": { + "telemetry": { + "type": "String", + "value": "For more information, see https://aka.ms/avm/TelemetryInfo" } - }, - { - "name": "logs", - "count": "[length(coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'logCategoriesAndGroups'), createArray(createObject('categoryGroup', 'allLogs'))))]", - "input": { - "categoryGroup": "[tryGet(coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'logCategoriesAndGroups'), createArray(createObject('categoryGroup', 'allLogs')))[copyIndex('logs')], 'categoryGroup')]", - "category": "[tryGet(coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'logCategoriesAndGroups'), createArray(createObject('categoryGroup', 'allLogs')))[copyIndex('logs')], 'category')]", - "enabled": "[coalesce(tryGet(coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'logCategoriesAndGroups'), createArray(createObject('categoryGroup', 'allLogs')))[copyIndex('logs')], 'enabled'), true())]" + } + } + } + }, + "managedDataDisks": { + "copy": { + "name": "managedDataDisks", + "count": "[length(coalesce(parameters('dataDisks'), createArray()))]" + }, + "condition": "[empty(tryGet(coalesce(parameters('dataDisks'), createArray())[copyIndex()].managedDisk, 'resourceId'))]", + "type": "Microsoft.Compute/disks", + "apiVersion": "2024-03-02", + "name": "[coalesce(tryGet(coalesce(parameters('dataDisks'), createArray())[copyIndex()], 'name'), format('{0}-disk-data-{1}', parameters('name'), padLeft(add(copyIndex(), 1), 2, '0')))]", + "location": "[parameters('location')]", + "sku": { + "name": "[tryGet(coalesce(parameters('dataDisks'), createArray())[copyIndex()].managedDisk, 'storageAccountType')]" + }, + "properties": { + "diskSizeGB": "[tryGet(coalesce(parameters('dataDisks'), createArray())[copyIndex()], 'diskSizeGB')]", + "creationData": { + "createOption": "[coalesce(tryGet(coalesce(parameters('dataDisks'), createArray())[copyIndex()], 'createoption'), 'Empty')]" + }, + "diskIOPSReadWrite": "[tryGet(coalesce(parameters('dataDisks'), createArray())[copyIndex()], 'diskIOPSReadWrite')]", + "diskMBpsReadWrite": "[tryGet(coalesce(parameters('dataDisks'), createArray())[copyIndex()], 'diskMBpsReadWrite')]", + "publicNetworkAccess": "[parameters('publicNetworkAccess')]", + "networkAccessPolicy": "[parameters('networkAccessPolicy')]" + }, + "zones": "[if(and(not(equals(parameters('availabilityZone'), -1)), not(contains(tryGet(coalesce(parameters('dataDisks'), createArray())[copyIndex()].managedDisk, 'storageAccountType'), 'ZRS'))), array(string(parameters('availabilityZone'))), null())]", + "tags": "[coalesce(tryGet(coalesce(parameters('dataDisks'), createArray())[copyIndex()], 'tags'), parameters('tags'))]" + }, + "vm": { + "type": "Microsoft.Compute/virtualMachines", + "apiVersion": "2024-07-01", + "name": "[parameters('name')]", + "location": "[parameters('location')]", + "identity": "[variables('identity')]", + "tags": "[parameters('tags')]", + "zones": "[if(not(equals(parameters('availabilityZone'), -1)), array(string(parameters('availabilityZone'))), null())]", + "plan": "[parameters('plan')]", + "properties": { + "hardwareProfile": { + "vmSize": "[parameters('vmSize')]" + }, + "securityProfile": "[shallowMerge(createArray(if(parameters('encryptionAtHost'), createObject('encryptionAtHost', parameters('encryptionAtHost')), createObject()), createObject('securityType', parameters('securityType'), 'uefiSettings', if(equals(parameters('securityType'), 'TrustedLaunch'), createObject('secureBootEnabled', parameters('secureBootEnabled'), 'vTpmEnabled', parameters('vTpmEnabled')), null()))))]", + "storageProfile": { + "copy": [ + { + "name": "dataDisks", + "count": "[length(coalesce(parameters('dataDisks'), createArray()))]", + "input": { + "lun": "[coalesce(tryGet(coalesce(parameters('dataDisks'), createArray())[copyIndex('dataDisks')], 'lun'), copyIndex('dataDisks'))]", + "name": "[if(not(empty(tryGet(coalesce(parameters('dataDisks'), createArray())[copyIndex('dataDisks')].managedDisk, 'resourceId'))), last(split(coalesce(parameters('dataDisks'), createArray())[copyIndex('dataDisks')].managedDisk.resourceId, '/')), coalesce(tryGet(coalesce(parameters('dataDisks'), createArray())[copyIndex('dataDisks')], 'name'), format('{0}-disk-data-{1}', parameters('name'), padLeft(add(copyIndex('dataDisks'), 1), 2, '0'))))]", + "createOption": "[if(or(not(equals(if(empty(tryGet(coalesce(parameters('dataDisks'), createArray())[copyIndex('dataDisks')].managedDisk, 'resourceId')), resourceId('Microsoft.Compute/disks', coalesce(tryGet(coalesce(parameters('dataDisks'), createArray())[copyIndex('dataDisks')], 'name'), format('{0}-disk-data-{1}', parameters('name'), padLeft(add(copyIndex('dataDisks'), 1), 2, '0')))), null()), null())), not(empty(tryGet(coalesce(parameters('dataDisks'), createArray())[copyIndex('dataDisks')].managedDisk, 'resourceId')))), 'Attach', coalesce(tryGet(coalesce(parameters('dataDisks'), createArray())[copyIndex('dataDisks')], 'createoption'), 'Empty'))]", + "deleteOption": "[if(not(empty(tryGet(coalesce(parameters('dataDisks'), createArray())[copyIndex('dataDisks')].managedDisk, 'resourceId'))), 'Detach', coalesce(tryGet(coalesce(parameters('dataDisks'), createArray())[copyIndex('dataDisks')], 'deleteOption'), 'Delete'))]", + "caching": "[if(not(empty(tryGet(coalesce(parameters('dataDisks'), createArray())[copyIndex('dataDisks')].managedDisk, 'resourceId'))), 'None', coalesce(tryGet(coalesce(parameters('dataDisks'), createArray())[copyIndex('dataDisks')], 'caching'), 'ReadOnly'))]", + "managedDisk": { + "id": "[coalesce(tryGet(coalesce(parameters('dataDisks'), createArray())[copyIndex('dataDisks')].managedDisk, 'resourceId'), if(empty(tryGet(coalesce(parameters('dataDisks'), createArray())[copyIndex('dataDisks')].managedDisk, 'resourceId')), resourceId('Microsoft.Compute/disks', coalesce(tryGet(coalesce(parameters('dataDisks'), createArray())[copyIndex('dataDisks')], 'name'), format('{0}-disk-data-{1}', parameters('name'), padLeft(add(copyIndex('dataDisks'), 1), 2, '0')))), null()))]", + "diskEncryptionSet": "[if(not(empty(tryGet(coalesce(parameters('dataDisks'), createArray())[copyIndex('dataDisks')].managedDisk, 'diskEncryptionSetResourceId'))), createObject('id', coalesce(parameters('dataDisks'), createArray())[copyIndex('dataDisks')].managedDisk.diskEncryptionSetResourceId), null())]" + } + } + } + ], + "imageReference": "[parameters('imageReference')]", + "osDisk": { + "name": "[if(not(empty(tryGet(parameters('osDisk').managedDisk, 'resourceId'))), last(split(parameters('osDisk').managedDisk.resourceId, '/')), coalesce(tryGet(parameters('osDisk'), 'name'), format('{0}-disk-os-01', parameters('name'))))]", + "createOption": "[if(not(empty(tryGet(parameters('osDisk').managedDisk, 'resourceId'))), 'Attach', coalesce(tryGet(parameters('osDisk'), 'createOption'), 'FromImage'))]", + "osType": "[parameters('osType')]", + "deleteOption": "[if(not(empty(tryGet(parameters('osDisk').managedDisk, 'resourceId'))), 'Detach', coalesce(tryGet(parameters('osDisk'), 'deleteOption'), 'Delete'))]", + "diffDiskSettings": "[if(empty(coalesce(tryGet(parameters('osDisk'), 'diffDiskSettings'), createObject())), null(), createObject('option', 'Local', 'placement', parameters('osDisk').diffDiskSettings.placement))]", + "diskSizeGB": "[tryGet(parameters('osDisk'), 'diskSizeGB')]", + "caching": "[if(not(empty(tryGet(parameters('osDisk').managedDisk, 'resourceId'))), 'None', coalesce(tryGet(parameters('osDisk'), 'caching'), 'ReadOnly'))]", + "managedDisk": { + "storageAccountType": "[tryGet(parameters('osDisk').managedDisk, 'storageAccountType')]", + "diskEncryptionSet": "[if(not(empty(tryGet(parameters('osDisk').managedDisk, 'diskEncryptionSetResourceId'))), createObject('id', tryGet(parameters('osDisk').managedDisk, 'diskEncryptionSetResourceId')), null())]", + "id": "[tryGet(parameters('osDisk').managedDisk, 'resourceId')]" } } - ], - "storageAccountId": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'storageAccountResourceId')]", - "workspaceId": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'workspaceResourceId')]", - "eventHubAuthorizationRuleId": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'eventHubAuthorizationRuleResourceId')]", - "eventHubName": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'eventHubName')]", - "marketplacePartnerId": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'marketplacePartnerResourceId')]", - "logAnalyticsDestinationType": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'logAnalyticsDestinationType')]" + }, + "additionalCapabilities": { + "ultraSSDEnabled": "[parameters('ultraSSDEnabled')]", + "hibernationEnabled": "[parameters('hibernationEnabled')]" + }, + "osProfile": "[if(empty(tryGet(parameters('osDisk').managedDisk, 'resourceId')), createObject('computerName', parameters('computerName'), 'adminUsername', parameters('adminUsername'), 'adminPassword', parameters('adminPassword'), 'customData', if(not(empty(parameters('customData'))), base64(parameters('customData')), null()), 'windowsConfiguration', if(equals(parameters('osType'), 'Windows'), variables('windowsConfiguration'), null()), 'linuxConfiguration', if(equals(parameters('osType'), 'Linux'), variables('linuxConfiguration'), null()), 'secrets', parameters('certificatesToBeInstalled'), 'allowExtensionOperations', parameters('allowExtensionOperations')), null())]", + "networkProfile": { + "copy": [ + { + "name": "networkInterfaces", + "count": "[length(parameters('nicConfigurations'))]", + "input": { + "properties": { + "deleteOption": "[coalesce(tryGet(parameters('nicConfigurations')[copyIndex('networkInterfaces')], 'deleteOption'), 'Delete')]", + "primary": "[if(equals(copyIndex('networkInterfaces'), 0), true(), false())]" + }, + "id": "[resourceId('Microsoft.Network/networkInterfaces', coalesce(tryGet(parameters('nicConfigurations')[copyIndex('networkInterfaces')], 'name'), format('{0}{1}', parameters('name'), tryGet(parameters('nicConfigurations')[copyIndex('networkInterfaces')], 'nicSuffix'))))]" + } + } + ] + }, + "capacityReservation": "[if(not(empty(parameters('capacityReservationGroupResourceId'))), createObject('capacityReservationGroup', createObject('id', parameters('capacityReservationGroupResourceId'))), null())]", + "diagnosticsProfile": { + "bootDiagnostics": { + "enabled": "[if(not(empty(parameters('bootDiagnosticStorageAccountName'))), true(), parameters('bootDiagnostics'))]", + "storageUri": "[if(not(empty(parameters('bootDiagnosticStorageAccountName'))), format('https://{0}{1}', parameters('bootDiagnosticStorageAccountName'), parameters('bootDiagnosticStorageAccountUri')), null())]" + } + }, + "applicationProfile": "[if(not(empty(parameters('galleryApplications'))), createObject('galleryApplications', parameters('galleryApplications')), null())]", + "availabilitySet": "[if(not(empty(parameters('availabilitySetResourceId'))), createObject('id', parameters('availabilitySetResourceId')), null())]", + "proximityPlacementGroup": "[if(not(empty(parameters('proximityPlacementGroupResourceId'))), createObject('id', parameters('proximityPlacementGroupResourceId')), null())]", + "virtualMachineScaleSet": "[if(not(empty(parameters('virtualMachineScaleSetResourceId'))), createObject('id', parameters('virtualMachineScaleSetResourceId')), null())]", + "priority": "[parameters('priority')]", + "evictionPolicy": "[if(and(not(empty(parameters('priority'))), not(equals(parameters('priority'), 'Regular'))), parameters('evictionPolicy'), null())]", + "billingProfile": "[if(and(not(empty(parameters('priority'))), not(empty(parameters('maxPriceForLowPriorityVm')))), createObject('maxPrice', json(parameters('maxPriceForLowPriorityVm'))), null())]", + "host": "[if(not(empty(parameters('dedicatedHostResourceId'))), createObject('id', parameters('dedicatedHostResourceId')), null())]", + "licenseType": "[parameters('licenseType')]", + "userData": "[if(not(empty(parameters('userData'))), base64(parameters('userData')), null())]" }, "dependsOn": [ - "cognitiveService" + "managedDataDisks", + "vm_nic" ] }, - "cognitiveService_roleAssignments": { - "copy": { - "name": "cognitiveService_roleAssignments", - "count": "[length(coalesce(variables('formattedRoleAssignments'), createArray()))]" - }, - "type": "Microsoft.Authorization/roleAssignments", - "apiVersion": "2022-04-01", - "scope": "[format('Microsoft.CognitiveServices/accounts/{0}', parameters('name'))]", - "name": "[coalesce(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'name'), guid(resourceId('Microsoft.CognitiveServices/accounts', parameters('name')), coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].principalId, coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].roleDefinitionId))]", + "vm_configurationAssignment": { + "condition": "[not(empty(parameters('maintenanceConfigurationResourceId')))]", + "type": "Microsoft.Maintenance/configurationAssignments", + "apiVersion": "2023-04-01", + "scope": "[format('Microsoft.Compute/virtualMachines/{0}', parameters('name'))]", + "name": "[format('{0}assignment', parameters('name'))]", + "location": "[parameters('location')]", "properties": { - "roleDefinitionId": "[coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].roleDefinitionId]", - "principalId": "[coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].principalId]", - "description": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'description')]", - "principalType": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'principalType')]", - "condition": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'condition')]", - "conditionVersion": "[if(not(empty(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'condition'))), coalesce(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'conditionVersion'), '2.0'), null())]", - "delegatedManagedIdentityResourceId": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'delegatedManagedIdentityResourceId')]" + "maintenanceConfigurationId": "[parameters('maintenanceConfigurationResourceId')]", + "resourceId": "[resourceId('Microsoft.Compute/virtualMachines', parameters('name'))]" }, "dependsOn": [ - "cognitiveService" + "vm" ] }, - "cognitiveService_privateEndpoints": { - "copy": { - "name": "cognitiveService_privateEndpoints", - "count": "[length(coalesce(parameters('privateEndpoints'), createArray()))]" + "vm_configurationProfileAssignment": { + "condition": "[not(empty(parameters('configurationProfile')))]", + "type": "Microsoft.Automanage/configurationProfileAssignments", + "apiVersion": "2022-05-04", + "scope": "[format('Microsoft.Compute/virtualMachines/{0}', parameters('name'))]", + "name": "default", + "properties": { + "configurationProfile": "[parameters('configurationProfile')]" }, - "type": "Microsoft.Resources/deployments", - "apiVersion": "2025-04-01", - "name": "[format('{0}-cognitiveService-PrivateEndpoint-{1}', uniqueString(deployment().name, parameters('location')), copyIndex())]", - "subscriptionId": "[split(coalesce(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'resourceGroupResourceId'), resourceGroup().id), '/')[2]]", - "resourceGroup": "[split(coalesce(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'resourceGroupResourceId'), resourceGroup().id), '/')[4]]", + "dependsOn": [ + "vm" + ] + }, + "vm_autoShutdownConfiguration": { + "condition": "[not(empty(parameters('autoShutdownConfig')))]", + "type": "Microsoft.DevTestLab/schedules", + "apiVersion": "2018-09-15", + "name": "[format('shutdown-computevm-{0}', parameters('name'))]", + "location": "[parameters('location')]", "properties": { - "expressionEvaluationOptions": { - "scope": "inner" + "status": "[coalesce(tryGet(parameters('autoShutdownConfig'), 'status'), 'Disabled')]", + "targetResourceId": "[resourceId('Microsoft.Compute/virtualMachines', parameters('name'))]", + "taskType": "ComputeVmShutdownTask", + "dailyRecurrence": { + "time": "[coalesce(tryGet(parameters('autoShutdownConfig'), 'dailyRecurrenceTime'), '19:00')]" }, - "mode": "Incremental", - "parameters": { - "name": { - "value": "[coalesce(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'name'), format('pep-{0}-{1}-{2}', last(split(resourceId('Microsoft.CognitiveServices/accounts', parameters('name')), '/')), coalesce(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'service'), 'account'), copyIndex()))]" - }, - "privateLinkServiceConnections": "[if(not(equals(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'isManualConnection'), true())), createObject('value', createArray(createObject('name', coalesce(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'privateLinkServiceConnectionName'), format('{0}-{1}-{2}', last(split(resourceId('Microsoft.CognitiveServices/accounts', parameters('name')), '/')), coalesce(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'service'), 'account'), copyIndex())), 'properties', createObject('privateLinkServiceId', resourceId('Microsoft.CognitiveServices/accounts', parameters('name')), 'groupIds', createArray(coalesce(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'service'), 'account')))))), createObject('value', null()))]", - "manualPrivateLinkServiceConnections": "[if(equals(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'isManualConnection'), true()), createObject('value', createArray(createObject('name', coalesce(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'privateLinkServiceConnectionName'), format('{0}-{1}-{2}', last(split(resourceId('Microsoft.CognitiveServices/accounts', parameters('name')), '/')), coalesce(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'service'), 'account'), copyIndex())), 'properties', createObject('privateLinkServiceId', resourceId('Microsoft.CognitiveServices/accounts', parameters('name')), 'groupIds', createArray(coalesce(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'service'), 'account')), 'requestMessage', coalesce(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'manualConnectionRequestMessage'), 'Manual approval required.'))))), createObject('value', null()))]", - "subnetResourceId": { - "value": "[coalesce(parameters('privateEndpoints'), createArray())[copyIndex()].subnetResourceId]" + "timeZoneId": "[coalesce(tryGet(parameters('autoShutdownConfig'), 'timeZone'), 'UTC')]", + "notificationSettings": "[if(contains(parameters('autoShutdownConfig'), 'notificationSettings'), createObject('status', coalesce(tryGet(parameters('autoShutdownConfig'), 'status'), 'Disabled'), 'emailRecipient', coalesce(tryGet(tryGet(parameters('autoShutdownConfig'), 'notificationSettings'), 'emailRecipient'), ''), 'notificationLocale', coalesce(tryGet(tryGet(parameters('autoShutdownConfig'), 'notificationSettings'), 'notificationLocale'), 'en'), 'webhookUrl', coalesce(tryGet(tryGet(parameters('autoShutdownConfig'), 'notificationSettings'), 'webhookUrl'), ''), 'timeInMinutes', coalesce(tryGet(tryGet(parameters('autoShutdownConfig'), 'notificationSettings'), 'timeInMinutes'), 30)), null())]" + }, + "dependsOn": [ + "vm" + ] + }, + "vm_dataCollectionRuleAssociations": { + "copy": { + "name": "vm_dataCollectionRuleAssociations", + "count": "[length(parameters('extensionMonitoringAgentConfig').dataCollectionRuleAssociations)]" + }, + "condition": "[parameters('extensionMonitoringAgentConfig').enabled]", + "type": "Microsoft.Insights/dataCollectionRuleAssociations", + "apiVersion": "2024-03-11", + "scope": "[format('Microsoft.Compute/virtualMachines/{0}', parameters('name'))]", + "name": "[parameters('extensionMonitoringAgentConfig').dataCollectionRuleAssociations[copyIndex()].name]", + "properties": { + "dataCollectionRuleId": "[parameters('extensionMonitoringAgentConfig').dataCollectionRuleAssociations[copyIndex()].dataCollectionRuleResourceId]" + }, + "dependsOn": [ + "vm", + "vm_azureMonitorAgentExtension" + ] + }, + "cseIdentity": { + "condition": "[not(empty(tryGet(tryGet(parameters('extensionCustomScriptConfig'), 'protectedSettings'), 'managedIdentityResourceId')))]", + "existing": true, + "type": "Microsoft.ManagedIdentity/userAssignedIdentities", + "apiVersion": "2024-11-30", + "subscriptionId": "[split(parameters('extensionCustomScriptConfig').protectedSettings.managedIdentityResourceId, '/')[2]]", + "resourceGroup": "[split(parameters('extensionCustomScriptConfig').protectedSettings.managedIdentityResourceId, '/')[4]]", + "name": "[last(split(parameters('extensionCustomScriptConfig').protectedSettings.managedIdentityResourceId, '/'))]" + }, + "AzureWindowsBaseline": { + "condition": "[not(empty(parameters('guestConfiguration')))]", + "type": "Microsoft.GuestConfiguration/guestConfigurationAssignments", + "apiVersion": "2024-04-05", + "scope": "[format('Microsoft.Compute/virtualMachines/{0}', parameters('name'))]", + "name": "[coalesce(tryGet(parameters('guestConfiguration'), 'name'), 'AzureWindowsBaseline')]", + "location": "[parameters('location')]", + "properties": { + "guestConfiguration": "[parameters('guestConfiguration')]" + }, + "dependsOn": [ + "vm", + "vm_azureGuestConfigurationExtension" + ] + }, + "vm_lock": { + "condition": "[and(not(empty(coalesce(parameters('lock'), createObject()))), not(equals(tryGet(parameters('lock'), 'kind'), 'None')))]", + "type": "Microsoft.Authorization/locks", + "apiVersion": "2020-05-01", + "scope": "[format('Microsoft.Compute/virtualMachines/{0}', parameters('name'))]", + "name": "[coalesce(tryGet(parameters('lock'), 'name'), format('lock-{0}', parameters('name')))]", + "properties": { + "level": "[coalesce(tryGet(parameters('lock'), 'kind'), '')]", + "notes": "[coalesce(tryGet(parameters('lock'), 'notes'), if(equals(tryGet(parameters('lock'), 'kind'), 'CanNotDelete'), 'Cannot delete resource or child resources.', 'Cannot delete or modify the resource or child resources.'))]" + }, + "dependsOn": [ + "vm" + ] + }, + "vm_roleAssignments": { + "copy": { + "name": "vm_roleAssignments", + "count": "[length(coalesce(variables('formattedRoleAssignments'), createArray()))]" + }, + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2022-04-01", + "scope": "[format('Microsoft.Compute/virtualMachines/{0}', parameters('name'))]", + "name": "[coalesce(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'name'), guid(resourceId('Microsoft.Compute/virtualMachines', parameters('name')), coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].principalId, coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].roleDefinitionId))]", + "properties": { + "roleDefinitionId": "[coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].roleDefinitionId]", + "principalId": "[coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].principalId]", + "description": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'description')]", + "principalType": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'principalType')]", + "condition": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'condition')]", + "conditionVersion": "[if(not(empty(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'condition'))), coalesce(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'conditionVersion'), '2.0'), null())]", + "delegatedManagedIdentityResourceId": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'delegatedManagedIdentityResourceId')]" + }, + "dependsOn": [ + "vm" + ] + }, + "vm_nic": { + "copy": { + "name": "vm_nic", + "count": "[length(parameters('nicConfigurations'))]" + }, + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "[format('{0}-VM-Nic-{1}', uniqueString(deployment().name, parameters('location')), copyIndex())]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "networkInterfaceName": { + "value": "[coalesce(tryGet(parameters('nicConfigurations')[copyIndex()], 'name'), format('{0}{1}', parameters('name'), tryGet(parameters('nicConfigurations')[copyIndex()], 'nicSuffix')))]" }, - "enableTelemetry": { - "value": "[variables('enableReferencedModulesTelemetry')]" + "virtualMachineName": { + "value": "[parameters('name')]" }, "location": { - "value": "[coalesce(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'location'), reference(split(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()].subnetResourceId, '/subnets/')[0], '2020-06-01', 'Full').location)]" + "value": "[parameters('location')]" }, - "lock": { - "value": "[coalesce(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'lock'), parameters('lock'))]" + "enableIPForwarding": { + "value": "[coalesce(tryGet(parameters('nicConfigurations')[copyIndex()], 'enableIPForwarding'), false())]" }, - "privateDnsZoneGroup": { - "value": "[tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'privateDnsZoneGroup')]" + "enableAcceleratedNetworking": { + "value": "[coalesce(tryGet(parameters('nicConfigurations')[copyIndex()], 'enableAcceleratedNetworking'), true())]" }, - "roleAssignments": { - "value": "[tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'roleAssignments')]" + "dnsServers": "[if(contains(parameters('nicConfigurations')[copyIndex()], 'dnsServers'), if(not(empty(tryGet(parameters('nicConfigurations')[copyIndex()], 'dnsServers'))), createObject('value', tryGet(parameters('nicConfigurations')[copyIndex()], 'dnsServers')), createObject('value', createArray())), createObject('value', createArray()))]", + "networkSecurityGroupResourceId": { + "value": "[coalesce(tryGet(parameters('nicConfigurations')[copyIndex()], 'networkSecurityGroupResourceId'), '')]" }, - "tags": { - "value": "[coalesce(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'tags'), parameters('tags'))]" + "ipConfigurations": { + "value": "[parameters('nicConfigurations')[copyIndex()].ipConfigurations]" }, - "customDnsConfigs": { - "value": "[tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'customDnsConfigs')]" + "lock": { + "value": "[coalesce(tryGet(parameters('nicConfigurations')[copyIndex()], 'lock'), parameters('lock'))]" }, - "ipConfigurations": { - "value": "[tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'ipConfigurations')]" + "tags": { + "value": "[coalesce(tryGet(parameters('nicConfigurations')[copyIndex()], 'tags'), parameters('tags'))]" }, - "applicationSecurityGroupResourceIds": { - "value": "[tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'applicationSecurityGroupResourceIds')]" + "diagnosticSettings": { + "value": "[tryGet(parameters('nicConfigurations')[copyIndex()], 'diagnosticSettings')]" }, - "customNetworkInterfaceName": { - "value": "[tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'customNetworkInterfaceName')]" + "roleAssignments": { + "value": "[tryGet(parameters('nicConfigurations')[copyIndex()], 'roleAssignments')]" + }, + "enableTelemetry": { + "value": "[variables('enableReferencedModulesTelemetry')]" } }, "template": { @@ -12473,6819 +12373,3643 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.38.5.1644", - "templateHash": "16604612898799598358" - }, - "name": "Private Endpoints", - "description": "This module deploys a Private Endpoint." + "version": "0.39.26.7824", + "templateHash": "716745708639313461" + } }, "definitions": { - "privateDnsZoneGroupType": { + "publicIPConfigurationType": { "type": "object", "properties": { "name": { "type": "string", "nullable": true, "metadata": { - "description": "Optional. The name of the Private DNS Zone Group." + "description": "Optional. The name of the Public IP Address." } }, - "privateDnsZoneGroupConfigs": { + "publicIPAddressResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The resource ID of the public IP address." + } + }, + "diagnosticSettings": { "type": "array", "items": { - "$ref": "#/definitions/privateDnsZoneGroupConfigType" + "$ref": "#/definitions/diagnosticSettingFullType" }, + "nullable": true, "metadata": { - "description": "Required. The private DNS zone groups to associate the private endpoint. A DNS zone group can support up to 5 DNS zones." + "description": "Optional. Diagnostic settings for the public IP address." } - } - }, - "metadata": { - "__bicep_export!": true, - "description": "The type of a private dns zone group." - } - }, - "lockType": { - "type": "object", - "properties": { - "name": { + }, + "location": { "type": "string", "nullable": true, "metadata": { - "description": "Optional. Specify the name of lock." + "description": "Optional. The idle timeout in minutes." } }, - "kind": { + "lock": { + "$ref": "#/definitions/lockType", + "nullable": true, + "metadata": { + "description": "Optional. The lock settings of the public IP address." + } + }, + "idleTimeoutInMinutes": { + "type": "int", + "nullable": true, + "metadata": { + "description": "Optional. The idle timeout of the public IP address." + } + }, + "ddosSettings": { + "$ref": "#/definitions/ddosSettingsType", + "nullable": true, + "metadata": { + "description": "Optional. The DDoS protection plan configuration associated with the public IP address." + } + }, + "dnsSettings": { + "$ref": "#/definitions/dnsSettingsType", + "nullable": true, + "metadata": { + "description": "Optional. The DNS settings of the public IP address." + } + }, + "publicIPAddressVersion": { "type": "string", "allowedValues": [ - "CanNotDelete", - "None", - "ReadOnly" + "IPv4", + "IPv6" ], "nullable": true, "metadata": { - "description": "Optional. Specify the type of lock." + "description": "Optional. The public IP address version." } }, - "notes": { + "publicIPAllocationMethod": { "type": "string", + "allowedValues": [ + "Dynamic", + "Static" + ], "nullable": true, "metadata": { - "description": "Optional. Specify the notes of the lock." + "description": "Optional. The public IP address allocation method." } - } - }, - "metadata": { - "description": "An AVM-aligned type for a lock.", - "__bicep_imported_from!": { - "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.1" - } - } - }, - "privateDnsZoneGroupConfigType": { - "type": "object", - "properties": { - "name": { + }, + "publicIpPrefixResourceId": { "type": "string", "nullable": true, "metadata": { - "description": "Optional. The name of the private DNS zone group config." + "description": "Optional. Resource ID of the Public IP Prefix object. This is only needed if you want your Public IPs created in a PIP Prefix." } }, - "privateDnsZoneResourceId": { + "publicIpNameSuffix": { "type": "string", + "nullable": true, "metadata": { - "description": "Required. The resource id of the private DNS zone." + "description": "Optional. The name suffix of the public IP address resource." + } + }, + "roleAssignments": { + "type": "array", + "items": { + "$ref": "#/definitions/roleAssignmentType" + }, + "nullable": true, + "metadata": { + "description": "Optional. Array of role assignments to create." + } + }, + "skuName": { + "type": "string", + "allowedValues": [ + "Basic", + "Standard" + ], + "nullable": true, + "metadata": { + "description": "Optional. The SKU name of the public IP address." + } + }, + "skuTier": { + "type": "string", + "allowedValues": [ + "Global", + "Regional" + ], + "nullable": true, + "metadata": { + "description": "Optional. The SKU tier of the public IP address." + } + }, + "tags": { + "type": "object", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.Network/publicIPAddresses@2024-07-01#properties/tags" + }, + "description": "Optional. The tags of the public IP address." + }, + "nullable": true + }, + "availabilityZones": { + "type": "array", + "allowedValues": [ + 1, + 2, + 3 + ], + "nullable": true, + "metadata": { + "description": "Optional. The zones of the public IP address." + } + }, + "ipTags": { + "type": "array", + "items": { + "$ref": "#/definitions/ipTagType" + }, + "nullable": true, + "metadata": { + "description": "Optional. The list of tags associated with the public IP address." + } + }, + "enableTelemetry": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. Enable/Disable usage telemetry for the module." } } }, "metadata": { - "description": "The type of a private DNS zone group configuration.", - "__bicep_imported_from!": { - "sourceTemplate": "private-dns-zone-group/main.bicep" - } + "__bicep_export!": true, + "description": "The type for the public IP address configuration." } }, - "roleAssignmentType": { + "ipConfigurationType": { "type": "object", "properties": { "name": { "type": "string", "nullable": true, "metadata": { - "description": "Optional. The name (as GUID) of the role assignment. If not provided, a GUID will be generated." + "description": "Optional. The name of the IP configuration." } }, - "roleDefinitionIdOrName": { + "privateIPAllocationMethod": { "type": "string", + "allowedValues": [ + "Dynamic", + "Static" + ], + "nullable": true, "metadata": { - "description": "Required. The role to assign. You can provide either the display name of the role definition, the role definition GUID, or its fully qualified ID in the following format: '/providers/Microsoft.Authorization/roleDefinitions/c2f4ef07-c644-48eb-af81-4b1b4947fb11'." + "description": "Optional. The private IP address allocation method." } }, - "principalId": { + "privateIPAddress": { "type": "string", + "nullable": true, "metadata": { - "description": "Required. The principal ID of the principal (user/group/identity) to assign the role to." + "description": "Optional. The private IP address." } }, - "principalType": { + "subnetResourceId": { "type": "string", - "allowedValues": [ - "Device", - "ForeignGroup", - "Group", - "ServicePrincipal", - "User" - ], + "metadata": { + "description": "Required. The resource ID of the subnet." + } + }, + "loadBalancerBackendAddressPools": { + "type": "array", + "items": { + "$ref": "#/definitions/backendAddressPoolType" + }, "nullable": true, "metadata": { - "description": "Optional. The principal type of the assigned principal ID." + "description": "Optional. The load balancer backend address pools." } }, - "description": { - "type": "string", + "applicationSecurityGroups": { + "type": "array", + "items": { + "$ref": "#/definitions/applicationSecurityGroupType" + }, "nullable": true, "metadata": { - "description": "Optional. The description of the role assignment." + "description": "Optional. The application security groups." } }, - "condition": { - "type": "string", + "applicationGatewayBackendAddressPools": { + "type": "array", + "items": { + "$ref": "#/definitions/applicationGatewayBackendAddressPoolsType" + }, "nullable": true, "metadata": { - "description": "Optional. The conditions on the role assignment. This limits the resources it can be assigned to. e.g.: @Resource[Microsoft.Storage/storageAccounts/blobServices/containers:ContainerName] StringEqualsIgnoreCase \"foo_storage_container\"." + "description": "Optional. The application gateway backend address pools." } }, - "conditionVersion": { + "gatewayLoadBalancer": { + "$ref": "#/definitions/subResourceType", + "nullable": true, + "metadata": { + "description": "Optional. The gateway load balancer settings." + } + }, + "loadBalancerInboundNatRules": { + "type": "array", + "items": { + "$ref": "#/definitions/inboundNatRuleType" + }, + "nullable": true, + "metadata": { + "description": "Optional. The load balancer inbound NAT rules." + } + }, + "privateIPAddressVersion": { "type": "string", "allowedValues": [ - "2.0" + "IPv4", + "IPv6" ], "nullable": true, "metadata": { - "description": "Optional. Version of the condition." + "description": "Optional. The private IP address version." } }, - "delegatedManagedIdentityResourceId": { + "virtualNetworkTaps": { + "type": "array", + "items": { + "$ref": "#/definitions/virtualNetworkTapType" + }, + "nullable": true, + "metadata": { + "description": "Optional. The virtual network taps." + } + }, + "pipConfiguration": { + "$ref": "#/definitions/publicIPConfigurationType", + "nullable": true, + "metadata": { + "description": "Optional. The public IP address configuration." + } + }, + "diagnosticSettings": { + "type": "array", + "items": { + "$ref": "#/definitions/diagnosticSettingFullType" + }, + "nullable": true, + "metadata": { + "description": "Optional. The diagnostic settings of the IP configuration." + } + }, + "tags": { + "type": "object", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.Network/networkInterfaces@2024-07-01#properties/tags" + }, + "description": "Optional. The tags of the public IP address." + }, + "nullable": true + }, + "enableTelemetry": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. Enable/Disable usage telemetry for the module." + } + } + }, + "metadata": { + "__bicep_export!": true, + "description": "The type for the IP configuration." + } + }, + "applicationGatewayBackendAddressPoolsType": { + "type": "object", + "properties": { + "id": { "type": "string", "nullable": true, "metadata": { - "description": "Optional. The Resource Id of the delegated managed identity resource." + "description": "Optional. Resource ID of the backend address pool." + } + }, + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Name of the backend address pool that is unique within an Application Gateway." + } + }, + "properties": { + "type": "object", + "properties": { + "backendAddresses": { + "type": "array", + "items": { + "type": "object", + "properties": { + "ipAddress": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. IP address of the backend address." + } + }, + "fqdn": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. FQDN of the backend address." + } + } + } + }, + "nullable": true, + "metadata": { + "description": "Optional. Backend addresses." + } + } + }, + "nullable": true, + "metadata": { + "description": "Optional. Properties of the application gateway backend address pool." } } }, "metadata": { - "description": "An AVM-aligned type for a role assignment.", + "description": "The type for the application gateway backend address pool.", "__bicep_imported_from!": { - "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.1" + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/res/network/network-interface:0.5.1" } } - } - }, - "parameters": { - "name": { - "type": "string", - "metadata": { - "description": "Required. Name of the private endpoint resource to create." - } - }, - "subnetResourceId": { - "type": "string", - "metadata": { - "description": "Required. Resource ID of the subnet where the endpoint needs to be created." - } - }, - "applicationSecurityGroupResourceIds": { - "type": "array", - "items": { - "type": "string" - }, - "nullable": true, - "metadata": { - "description": "Optional. Application security groups in which the private endpoint IP configuration is included." - } - }, - "customNetworkInterfaceName": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The custom name of the network interface attached to the private endpoint." - } - }, - "ipConfigurations": { - "type": "array", - "metadata": { - "__bicep_resource_derived_type!": { - "source": "Microsoft.Network/privateEndpoints@2024-01-01#properties/properties/properties/ipConfigurations" - }, - "description": "Optional. A list of IP configurations of the private endpoint. This will be used to map to the First Party Service endpoints." - }, - "nullable": true - }, - "privateDnsZoneGroup": { - "$ref": "#/definitions/privateDnsZoneGroupType", - "nullable": true, - "metadata": { - "description": "Optional. The private DNS zone group to configure for the private endpoint." - } - }, - "location": { - "type": "string", - "defaultValue": "[resourceGroup().location]", - "metadata": { - "description": "Optional. Location for all Resources." - } - }, - "lock": { - "$ref": "#/definitions/lockType", - "nullable": true, - "metadata": { - "description": "Optional. The lock settings of the service." - } - }, - "roleAssignments": { - "type": "array", - "items": { - "$ref": "#/definitions/roleAssignmentType" - }, - "nullable": true, - "metadata": { - "description": "Optional. Array of role assignments to create." - } }, - "tags": { + "applicationSecurityGroupType": { "type": "object", - "metadata": { - "__bicep_resource_derived_type!": { - "source": "Microsoft.Network/privateEndpoints@2024-01-01#properties/tags" - }, - "description": "Optional. Tags to be applied on all resources/resource groups in this deployment." - }, - "nullable": true - }, - "customDnsConfigs": { - "type": "array", - "metadata": { - "__bicep_resource_derived_type!": { - "source": "Microsoft.Network/privateEndpoints@2024-01-01#properties/properties/properties/customDnsConfigs" - }, - "description": "Optional. Custom DNS configurations." - }, - "nullable": true - }, - "manualPrivateLinkServiceConnections": { - "type": "array", - "metadata": { - "__bicep_resource_derived_type!": { - "source": "Microsoft.Network/privateEndpoints@2024-01-01#properties/properties/properties/manualPrivateLinkServiceConnections" - }, - "description": "Conditional. A grouping of information about the connection to the remote resource. Used when the network admin does not have access to approve connections to the remote resource. Required if `privateLinkServiceConnections` is empty." - }, - "nullable": true - }, - "privateLinkServiceConnections": { - "type": "array", - "metadata": { - "__bicep_resource_derived_type!": { - "source": "Microsoft.Network/privateEndpoints@2024-01-01#properties/properties/properties/privateLinkServiceConnections" - }, - "description": "Conditional. A grouping of information about the connection to the remote resource. Required if `manualPrivateLinkServiceConnections` is empty." - }, - "nullable": true - }, - "enableTelemetry": { - "type": "bool", - "defaultValue": true, - "metadata": { - "description": "Optional. Enable/Disable usage telemetry for module." - } - } - }, - "variables": { - "copy": [ - { - "name": "formattedRoleAssignments", - "count": "[length(coalesce(parameters('roleAssignments'), createArray()))]", - "input": "[union(coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')], createObject('roleDefinitionId', coalesce(tryGet(variables('builtInRoleNames'), coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName), if(contains(coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName, '/providers/Microsoft.Authorization/roleDefinitions/'), coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName, subscriptionResourceId('Microsoft.Authorization/roleDefinitions', coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName)))))]" - } - ], - "builtInRoleNames": { - "Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'b24988ac-6180-42a0-ab88-20f7382dd24c')]", - "DNS Resolver Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '0f2ebee7-ffd4-4fc0-b3b7-664099fdad5d')]", - "DNS Zone Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'befefa01-2a29-4197-83a8-272ff33ce314')]", - "Domain Services Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'eeaeda52-9324-47f6-8069-5d5bade478b2')]", - "Domain Services Reader": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '361898ef-9ed1-48c2-849c-a832951106bb')]", - "Network Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '4d97b98b-1d4f-4787-a291-c67834d212e7')]", - "Owner": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '8e3af657-a8ff-443c-a75c-2fe8c4bcb635')]", - "Private DNS Zone Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'b12aa53e-6015-4669-85d0-8515ebb3ae7f')]", - "Reader": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'acdd72a7-3385-48ef-bd42-f606fba81ae7')]", - "Role Based Access Control Administrator": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'f58310d9-a9f6-439a-9e8d-f62e7b41a168')]" - } - }, - "resources": { - "avmTelemetry": { - "condition": "[parameters('enableTelemetry')]", - "type": "Microsoft.Resources/deployments", - "apiVersion": "2025-04-01", - "name": "[format('46d3xbcp.res.network-privateendpoint.{0}.{1}', replace('0.11.1', '.', '-'), substring(uniqueString(deployment().name, parameters('location')), 0, 4))]", - "properties": { - "mode": "Incremental", - "template": { - "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", - "contentVersion": "1.0.0.0", - "resources": [], - "outputs": { - "telemetry": { - "type": "String", - "value": "For more information, see https://aka.ms/avm/TelemetryInfo" - } - } - } - } - }, - "privateEndpoint": { - "type": "Microsoft.Network/privateEndpoints", - "apiVersion": "2024-10-01", - "name": "[parameters('name')]", - "location": "[parameters('location')]", - "tags": "[parameters('tags')]", - "properties": { - "copy": [ - { - "name": "applicationSecurityGroups", - "count": "[length(coalesce(parameters('applicationSecurityGroupResourceIds'), createArray()))]", - "input": { - "id": "[coalesce(parameters('applicationSecurityGroupResourceIds'), createArray())[copyIndex('applicationSecurityGroups')]]" - } - } - ], - "customDnsConfigs": "[coalesce(parameters('customDnsConfigs'), createArray())]", - "customNetworkInterfaceName": "[coalesce(parameters('customNetworkInterfaceName'), '')]", - "ipConfigurations": "[coalesce(parameters('ipConfigurations'), createArray())]", - "manualPrivateLinkServiceConnections": "[coalesce(parameters('manualPrivateLinkServiceConnections'), createArray())]", - "privateLinkServiceConnections": "[coalesce(parameters('privateLinkServiceConnections'), createArray())]", - "subnet": { - "id": "[parameters('subnetResourceId')]" - } - } - }, - "privateEndpoint_lock": { - "condition": "[and(not(empty(coalesce(parameters('lock'), createObject()))), not(equals(tryGet(parameters('lock'), 'kind'), 'None')))]", - "type": "Microsoft.Authorization/locks", - "apiVersion": "2020-05-01", - "scope": "[format('Microsoft.Network/privateEndpoints/{0}', parameters('name'))]", - "name": "[coalesce(tryGet(parameters('lock'), 'name'), format('lock-{0}', parameters('name')))]", - "properties": { - "level": "[coalesce(tryGet(parameters('lock'), 'kind'), '')]", - "notes": "[coalesce(tryGet(parameters('lock'), 'notes'), if(equals(tryGet(parameters('lock'), 'kind'), 'CanNotDelete'), 'Cannot delete resource or child resources.', 'Cannot delete or modify the resource or child resources.'))]" - }, - "dependsOn": [ - "privateEndpoint" - ] - }, - "privateEndpoint_roleAssignments": { - "copy": { - "name": "privateEndpoint_roleAssignments", - "count": "[length(coalesce(variables('formattedRoleAssignments'), createArray()))]" - }, - "type": "Microsoft.Authorization/roleAssignments", - "apiVersion": "2022-04-01", - "scope": "[format('Microsoft.Network/privateEndpoints/{0}', parameters('name'))]", - "name": "[coalesce(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'name'), guid(resourceId('Microsoft.Network/privateEndpoints', parameters('name')), coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].principalId, coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].roleDefinitionId))]", - "properties": { - "roleDefinitionId": "[coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].roleDefinitionId]", - "principalId": "[coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].principalId]", - "description": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'description')]", - "principalType": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'principalType')]", - "condition": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'condition')]", - "conditionVersion": "[if(not(empty(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'condition'))), coalesce(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'conditionVersion'), '2.0'), null())]", - "delegatedManagedIdentityResourceId": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'delegatedManagedIdentityResourceId')]" - }, - "dependsOn": [ - "privateEndpoint" - ] - }, - "privateEndpoint_privateDnsZoneGroup": { - "condition": "[not(empty(parameters('privateDnsZoneGroup')))]", - "type": "Microsoft.Resources/deployments", - "apiVersion": "2025-04-01", - "name": "[format('{0}-PrivateEndpoint-PrivateDnsZoneGroup', uniqueString(deployment().name))]", "properties": { - "expressionEvaluationOptions": { - "scope": "inner" - }, - "mode": "Incremental", - "parameters": { - "name": { - "value": "[tryGet(parameters('privateDnsZoneGroup'), 'name')]" - }, - "privateEndpointName": { - "value": "[parameters('name')]" - }, - "privateDnsZoneConfigs": { - "value": "[parameters('privateDnsZoneGroup').privateDnsZoneGroupConfigs]" - } - }, - "template": { - "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", - "languageVersion": "2.0", - "contentVersion": "1.0.0.0", + "id": { + "type": "string", + "nullable": true, "metadata": { - "_generator": { - "name": "bicep", - "version": "0.38.5.1644", - "templateHash": "24141742673128945" - }, - "name": "Private Endpoint Private DNS Zone Groups", - "description": "This module deploys a Private Endpoint Private DNS Zone Group." - }, - "definitions": { - "privateDnsZoneGroupConfigType": { - "type": "object", - "properties": { - "name": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The name of the private DNS zone group config." - } - }, - "privateDnsZoneResourceId": { - "type": "string", - "metadata": { - "description": "Required. The resource id of the private DNS zone." - } - } - }, - "metadata": { - "__bicep_export!": true, - "description": "The type of a private DNS zone group configuration." - } - } - }, - "parameters": { - "privateEndpointName": { - "type": "string", - "metadata": { - "description": "Conditional. The name of the parent private endpoint. Required if the template is used in a standalone deployment." - } - }, - "privateDnsZoneConfigs": { - "type": "array", - "items": { - "$ref": "#/definitions/privateDnsZoneGroupConfigType" - }, - "minLength": 1, - "maxLength": 5, - "metadata": { - "description": "Required. Array of private DNS zone configurations of the private DNS zone group. A DNS zone group can support up to 5 DNS zones." - } - }, - "name": { - "type": "string", - "defaultValue": "default", - "metadata": { - "description": "Optional. The name of the private DNS zone group." - } - } - }, - "resources": { - "privateEndpoint": { - "existing": true, - "type": "Microsoft.Network/privateEndpoints", - "apiVersion": "2024-10-01", - "name": "[parameters('privateEndpointName')]" - }, - "privateDnsZoneGroup": { - "type": "Microsoft.Network/privateEndpoints/privateDnsZoneGroups", - "apiVersion": "2024-10-01", - "name": "[format('{0}/{1}', parameters('privateEndpointName'), parameters('name'))]", - "properties": { - "copy": [ - { - "name": "privateDnsZoneConfigs", - "count": "[length(parameters('privateDnsZoneConfigs'))]", - "input": { - "name": "[coalesce(tryGet(parameters('privateDnsZoneConfigs')[copyIndex('privateDnsZoneConfigs')], 'name'), last(split(parameters('privateDnsZoneConfigs')[copyIndex('privateDnsZoneConfigs')].privateDnsZoneResourceId, '/')))]", - "properties": { - "privateDnsZoneId": "[parameters('privateDnsZoneConfigs')[copyIndex('privateDnsZoneConfigs')].privateDnsZoneResourceId]" - } - } - } - ] - } - } - }, - "outputs": { - "name": { - "type": "string", - "metadata": { - "description": "The name of the private endpoint DNS zone group." - }, - "value": "[parameters('name')]" - }, - "resourceId": { - "type": "string", - "metadata": { - "description": "The resource ID of the private endpoint DNS zone group." - }, - "value": "[resourceId('Microsoft.Network/privateEndpoints/privateDnsZoneGroups', parameters('privateEndpointName'), parameters('name'))]" - }, - "resourceGroupName": { - "type": "string", - "metadata": { - "description": "The resource group the private endpoint DNS zone group was deployed into." - }, - "value": "[resourceGroup().name]" - } + "description": "Optional. Resource ID of the application security group." } - } - }, - "dependsOn": [ - "privateEndpoint" - ] - } - }, - "outputs": { - "resourceGroupName": { - "type": "string", - "metadata": { - "description": "The resource group the private endpoint was deployed into." - }, - "value": "[resourceGroup().name]" - }, - "resourceId": { - "type": "string", - "metadata": { - "description": "The resource ID of the private endpoint." - }, - "value": "[resourceId('Microsoft.Network/privateEndpoints', parameters('name'))]" - }, - "name": { - "type": "string", - "metadata": { - "description": "The name of the private endpoint." - }, - "value": "[parameters('name')]" - }, - "location": { - "type": "string", - "metadata": { - "description": "The location the resource was deployed into." - }, - "value": "[reference('privateEndpoint', '2024-10-01', 'full').location]" - }, - "customDnsConfigs": { - "type": "array", - "metadata": { - "__bicep_resource_derived_type!": { - "source": "Microsoft.Network/privateEndpoints@2024-01-01#properties/properties/properties/customDnsConfigs", - "output": true }, - "description": "The custom DNS configurations of the private endpoint." - }, - "value": "[reference('privateEndpoint').customDnsConfigs]" - }, - "networkInterfaceResourceIds": { - "type": "array", - "items": { - "type": "string" - }, - "metadata": { - "description": "The resource IDs of the network interfaces associated with the private endpoint." - }, - "value": "[map(reference('privateEndpoint').networkInterfaces, lambda('nic', lambdaVariables('nic').id))]" - }, - "groupId": { - "type": "string", - "nullable": true, - "metadata": { - "description": "The group Id for the private endpoint Group." - }, - "value": "[coalesce(tryGet(tryGet(tryGet(tryGet(reference('privateEndpoint'), 'manualPrivateLinkServiceConnections'), 0, 'properties'), 'groupIds'), 0), tryGet(tryGet(tryGet(tryGet(reference('privateEndpoint'), 'privateLinkServiceConnections'), 0, 'properties'), 'groupIds'), 0))]" - } - } - } - }, - "dependsOn": [ - "cognitiveService" - ] - }, - "secretsExport": { - "condition": "[not(equals(parameters('secretsExportConfiguration'), null()))]", - "type": "Microsoft.Resources/deployments", - "apiVersion": "2025-04-01", - "name": "[format('{0}-secrets-kv', uniqueString(deployment().name, parameters('location')))]", - "subscriptionId": "[split(tryGet(parameters('secretsExportConfiguration'), 'keyVaultResourceId'), '/')[2]]", - "resourceGroup": "[split(tryGet(parameters('secretsExportConfiguration'), 'keyVaultResourceId'), '/')[4]]", - "properties": { - "expressionEvaluationOptions": { - "scope": "inner" - }, - "mode": "Incremental", - "parameters": { - "keyVaultName": { - "value": "[last(split(tryGet(parameters('secretsExportConfiguration'), 'keyVaultResourceId'), '/'))]" - }, - "secretsToSet": { - "value": "[union(createArray(), if(contains(parameters('secretsExportConfiguration'), 'accessKey1Name'), createArray(createObject('name', tryGet(parameters('secretsExportConfiguration'), 'accessKey1Name'), 'value', listKeys('cognitiveService', '2025-06-01').key1)), createArray()), if(contains(parameters('secretsExportConfiguration'), 'accessKey2Name'), createArray(createObject('name', tryGet(parameters('secretsExportConfiguration'), 'accessKey2Name'), 'value', listKeys('cognitiveService', '2025-06-01').key2)), createArray()))]" - } - }, - "template": { - "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", - "languageVersion": "2.0", - "contentVersion": "1.0.0.0", - "metadata": { - "_generator": { - "name": "bicep", - "version": "0.38.33.27573", - "templateHash": "1394089926798493893" - } - }, - "definitions": { - "secretSetOutputType": { - "type": "object", - "properties": { - "secretResourceId": { + "location": { "type": "string", + "nullable": true, "metadata": { - "description": "The resourceId of the exported secret." + "description": "Optional. Location of the application security group." } }, - "secretUri": { - "type": "string", + "properties": { + "type": "object", + "nullable": true, "metadata": { - "description": "The secret URI of the exported secret." + "description": "Optional. Properties of the application security group." } }, - "secretUriWithVersion": { - "type": "string", + "tags": { + "type": "object", + "nullable": true, "metadata": { - "description": "The secret URI with version of the exported secret." + "description": "Optional. Tags of the application security group." } } }, "metadata": { - "description": "An AVM-aligned type for the output of the secret set via the secrets export feature.", + "description": "The type for the application security group.", "__bicep_imported_from!": { - "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.1" + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/res/network/network-interface:0.5.1" } } }, - "secretToSetType": { + "backendAddressPoolType": { "type": "object", "properties": { + "id": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The resource ID of the backend address pool." + } + }, "name": { "type": "string", + "nullable": true, "metadata": { - "description": "Required. The name of the secret to set." + "description": "Optional. The name of the backend address pool." } }, - "value": { - "type": "securestring", + "properties": { + "type": "object", + "nullable": true, "metadata": { - "description": "Required. The value of the secret to set." + "description": "Optional. The properties of the backend address pool." } } }, "metadata": { - "description": "An AVM-aligned type for the secret to set via the secrets export feature.", + "description": "The type for a backend address pool.", "__bicep_imported_from!": { - "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.1" + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/res/network/network-interface:0.5.1" } } - } - }, - "parameters": { - "keyVaultName": { - "type": "string", - "metadata": { - "description": "Required. The name of the Key Vault to set the ecrets in." - } }, - "secretsToSet": { - "type": "array", - "items": { - "$ref": "#/definitions/secretToSetType" + "ddosSettingsType": { + "type": "object", + "properties": { + "ddosProtectionPlan": { + "type": "object", + "properties": { + "id": { + "type": "string", + "metadata": { + "description": "Required. The resource ID of the DDOS protection plan associated with the public IP address." + } + } + }, + "nullable": true, + "metadata": { + "description": "Optional. The DDoS protection plan associated with the public IP address." + } + }, + "protectionMode": { + "type": "string", + "allowedValues": [ + "Enabled" + ], + "metadata": { + "description": "Required. The DDoS protection policy customizations." + } + } }, "metadata": { - "description": "Required. The secrets to set in the Key Vault." + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/res/network/public-ip-address:0.8.0" + } } - } - }, - "resources": { - "keyVault": { - "existing": true, - "type": "Microsoft.KeyVault/vaults", - "apiVersion": "2025-05-01", - "name": "[parameters('keyVaultName')]" }, - "secrets": { - "copy": { - "name": "secrets", - "count": "[length(parameters('secretsToSet'))]" - }, - "type": "Microsoft.KeyVault/vaults/secrets", - "apiVersion": "2025-05-01", - "name": "[format('{0}/{1}', parameters('keyVaultName'), parameters('secretsToSet')[copyIndex()].name)]", + "diagnosticSettingFullType": { + "type": "object", "properties": { - "value": "[parameters('secretsToSet')[copyIndex()].value]" - } - } - }, - "outputs": { - "secretsSet": { - "type": "array", - "items": { - "$ref": "#/definitions/secretSetOutputType" + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The name of the diagnostic setting." + } + }, + "logCategoriesAndGroups": { + "type": "array", + "items": { + "type": "object", + "properties": { + "category": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Name of a Diagnostic Log category for a resource type this setting is applied to. Set the specific logs to collect here." + } + }, + "categoryGroup": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Name of a Diagnostic Log category group for a resource type this setting is applied to. Set to `allLogs` to collect all logs." + } + }, + "enabled": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. Enable or disable the category explicitly. Default is `true`." + } + } + } + }, + "nullable": true, + "metadata": { + "description": "Optional. The name of logs that will be streamed. \"allLogs\" includes all possible logs for the resource. Set to `[]` to disable log collection." + } + }, + "metricCategories": { + "type": "array", + "items": { + "type": "object", + "properties": { + "category": { + "type": "string", + "metadata": { + "description": "Required. Name of a Diagnostic Metric category for a resource type this setting is applied to. Set to `AllMetrics` to collect all metrics." + } + }, + "enabled": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. Enable or disable the category explicitly. Default is `true`." + } + } + } + }, + "nullable": true, + "metadata": { + "description": "Optional. The name of metrics that will be streamed. \"allMetrics\" includes all possible metrics for the resource. Set to `[]` to disable metric collection." + } + }, + "logAnalyticsDestinationType": { + "type": "string", + "allowedValues": [ + "AzureDiagnostics", + "Dedicated" + ], + "nullable": true, + "metadata": { + "description": "Optional. A string indicating whether the export to Log Analytics should use the default destination type, i.e. AzureDiagnostics, or use a destination type." + } + }, + "workspaceResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Resource ID of the diagnostic log analytics workspace. For security reasons, it is recommended to set diagnostic settings to send data to either storage account, log analytics workspace or event hub." + } + }, + "storageAccountResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Resource ID of the diagnostic storage account. For security reasons, it is recommended to set diagnostic settings to send data to either storage account, log analytics workspace or event hub." + } + }, + "eventHubAuthorizationRuleResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Resource ID of the diagnostic event hub authorization rule for the Event Hubs namespace in which the event hub should be created or streamed to." + } + }, + "eventHubName": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Name of the diagnostic event hub within the namespace to which logs are streamed. Without this, an event hub is created for each log category. For security reasons, it is recommended to set diagnostic settings to send data to either storage account, log analytics workspace or event hub." + } + }, + "marketplacePartnerResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The full ARM resource ID of the Marketplace resource to which you would like to send Diagnostic Logs." + } + } }, "metadata": { - "description": "The references to the secrets exported to the provided Key Vault." - }, - "copy": { - "count": "[length(range(0, length(coalesce(parameters('secretsToSet'), createArray()))))]", - "input": { - "secretResourceId": "[resourceId('Microsoft.KeyVault/vaults/secrets', parameters('keyVaultName'), parameters('secretsToSet')[range(0, length(coalesce(parameters('secretsToSet'), createArray())))[copyIndex()]].name)]", - "secretUri": "[reference(format('secrets[{0}]', range(0, length(coalesce(parameters('secretsToSet'), createArray())))[copyIndex()])).secretUri]", - "secretUriWithVersion": "[reference(format('secrets[{0}]', range(0, length(coalesce(parameters('secretsToSet'), createArray())))[copyIndex()])).secretUriWithVersion]" + "description": "An AVM-aligned type for a diagnostic setting. To be used if both logs & metrics are supported by the resource provider.", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.1" } } - } - } - } - }, - "dependsOn": [ - "cognitiveService" - ] - } - }, - "outputs": { - "name": { - "type": "string", - "metadata": { - "description": "The name of the cognitive services account." - }, - "value": "[parameters('name')]" - }, - "resourceId": { - "type": "string", - "metadata": { - "description": "The resource ID of the cognitive services account." - }, - "value": "[resourceId('Microsoft.CognitiveServices/accounts', parameters('name'))]" - }, - "resourceGroupName": { - "type": "string", - "metadata": { - "description": "The resource group the cognitive services account was deployed into." - }, - "value": "[resourceGroup().name]" - }, - "endpoint": { - "type": "string", - "metadata": { - "description": "The service endpoint of the cognitive services account." - }, - "value": "[reference('cognitiveService').endpoint]" - }, - "endpoints": { - "$ref": "#/definitions/endpointType", - "metadata": { - "description": "All endpoints available for the cognitive services account, types depends on the cognitive service kind." - }, - "value": "[reference('cognitiveService').endpoints]" - }, - "systemAssignedMIPrincipalId": { - "type": "string", - "nullable": true, - "metadata": { - "description": "The principal ID of the system assigned identity." - }, - "value": "[tryGet(tryGet(reference('cognitiveService', '2025-06-01', 'full'), 'identity'), 'principalId')]" - }, - "location": { - "type": "string", - "metadata": { - "description": "The location the resource was deployed into." - }, - "value": "[reference('cognitiveService', '2025-06-01', 'full').location]" - }, - "exportedSecrets": { - "$ref": "#/definitions/secretsOutputType", - "metadata": { - "description": "A hashtable of references to the secrets exported to the provided Key Vault. The key of each reference is each secret's name." - }, - "value": "[if(not(equals(parameters('secretsExportConfiguration'), null())), toObject(reference('secretsExport').outputs.secretsSet.value, lambda('secret', last(split(lambdaVariables('secret').secretResourceId, '/'))), lambda('secret', lambdaVariables('secret'))), createObject())]" - }, - "privateEndpoints": { - "type": "array", - "items": { - "$ref": "#/definitions/privateEndpointOutputType" - }, - "metadata": { - "description": "The private endpoints of the congitive services account." - }, - "copy": { - "count": "[length(coalesce(parameters('privateEndpoints'), createArray()))]", - "input": { - "name": "[reference(format('cognitiveService_privateEndpoints[{0}]', copyIndex())).outputs.name.value]", - "resourceId": "[reference(format('cognitiveService_privateEndpoints[{0}]', copyIndex())).outputs.resourceId.value]", - "groupId": "[tryGet(tryGet(reference(format('cognitiveService_privateEndpoints[{0}]', copyIndex())).outputs, 'groupId'), 'value')]", - "customDnsConfigs": "[reference(format('cognitiveService_privateEndpoints[{0}]', copyIndex())).outputs.customDnsConfigs.value]", - "networkInterfaceResourceIds": "[reference(format('cognitiveService_privateEndpoints[{0}]', copyIndex())).outputs.networkInterfaceResourceIds.value]" - } - } - } - } - } - }, - "dependsOn": [ - "logAnalyticsWorkspace", - "userAssignedIdentity" - ] - }, - "aiServicesPrivateEndpoint": { - "condition": "[and(not(variables('useExistingAiFoundryAiProject')), parameters('enablePrivateNetworking'))]", - "type": "Microsoft.Resources/deployments", - "apiVersion": "2025-04-01", - "name": "[take(format('pep-ai-services-{0}', variables('aiFoundryAiServicesResourceName')), 64)]", - "properties": { - "expressionEvaluationOptions": { - "scope": "inner" - }, - "mode": "Incremental", - "parameters": { - "name": { - "value": "[format('pep-{0}', variables('aiFoundryAiServicesResourceName'))]" - }, - "location": { - "value": "[variables('solutionLocation')]" - }, - "tags": { - "value": "[parameters('tags')]" - }, - "subnetResourceId": { - "value": "[reference('virtualNetwork').outputs.pepsSubnetResourceId.value]" - }, - "privateLinkServiceConnections": { - "value": [ - { - "name": "[format('pep-{0}', variables('aiFoundryAiServicesResourceName'))]", - "properties": { - "privateLinkServiceId": "[reference('aiFoundryAiServices').outputs.resourceId.value]", - "groupIds": [ - "account" - ] - } - } - ] - }, - "privateDnsZoneGroup": { - "value": { - "privateDnsZoneGroupConfigs": [ - { - "name": "cognitiveservices", - "privateDnsZoneResourceId": "[reference(format('avmPrivateDnsZones[{0}]', variables('dnsZoneIndex').cognitiveServices)).outputs.resourceId.value]" - }, - { - "name": "openai", - "privateDnsZoneResourceId": "[reference(format('avmPrivateDnsZones[{0}]', variables('dnsZoneIndex').openAI)).outputs.resourceId.value]" - } - ] - } - } - }, - "template": { - "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", - "languageVersion": "2.0", - "contentVersion": "1.0.0.0", - "metadata": { - "_generator": { - "name": "bicep", - "version": "0.34.44.8038", - "templateHash": "12389807800450456797" - }, - "name": "Private Endpoints", - "description": "This module deploys a Private Endpoint." - }, - "definitions": { - "privateDnsZoneGroupType": { - "type": "object", - "properties": { - "name": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The name of the Private DNS Zone Group." - } - }, - "privateDnsZoneGroupConfigs": { - "type": "array", - "items": { - "$ref": "#/definitions/privateDnsZoneGroupConfigType" - }, - "metadata": { - "description": "Required. The private DNS zone groups to associate the private endpoint. A DNS zone group can support up to 5 DNS zones." - } - } - }, - "metadata": { - "__bicep_export!": true - } - }, - "ipConfigurationType": { - "type": "object", - "properties": { - "name": { - "type": "string", - "metadata": { - "description": "Required. The name of the resource that is unique within a resource group." - } - }, - "properties": { - "type": "object", - "properties": { - "groupId": { - "type": "string", - "metadata": { - "description": "Required. The ID of a group obtained from the remote resource that this private endpoint should connect to. If used with private link service connection, this property must be defined as empty string." - } - }, - "memberName": { - "type": "string", - "metadata": { - "description": "Required. The member name of a group obtained from the remote resource that this private endpoint should connect to. If used with private link service connection, this property must be defined as empty string." - } }, - "privateIPAddress": { - "type": "string", - "metadata": { - "description": "Required. A private IP address obtained from the private endpoint's subnet." - } - } - }, - "metadata": { - "description": "Required. Properties of private endpoint IP configurations." - } - } - }, - "metadata": { - "__bicep_export!": true - } - }, - "privateLinkServiceConnectionType": { - "type": "object", - "properties": { - "name": { - "type": "string", - "metadata": { - "description": "Required. The name of the private link service connection." - } - }, - "properties": { - "type": "object", - "properties": { - "groupIds": { - "type": "array", - "items": { - "type": "string" + "dnsSettingsType": { + "type": "object", + "properties": { + "domainNameLabel": { + "type": "string", + "metadata": { + "description": "Required. The domain name label. The concatenation of the domain name label and the regionalized DNS zone make up the fully qualified domain name associated with the public IP address. If a domain name label is specified, an A DNS record is created for the public IP in the Microsoft Azure DNS system." + } + }, + "domainNameLabelScope": { + "type": "string", + "allowedValues": [ + "NoReuse", + "ResourceGroupReuse", + "SubscriptionReuse", + "TenantReuse" + ], + "nullable": true, + "metadata": { + "description": "Optional. The domain name label scope. If a domain name label and a domain name label scope are specified, an A DNS record is created for the public IP in the Microsoft Azure DNS system with a hashed value includes in FQDN." + } + }, + "fqdn": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The Fully Qualified Domain Name of the A DNS record associated with the public IP. This is the concatenation of the domainNameLabel and the regionalized DNS zone." + } + }, + "reverseFqdn": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The reverse FQDN. A user-visible, fully qualified domain name that resolves to this public IP address. If the reverseFqdn is specified, then a PTR DNS record is created pointing from the IP address in the in-addr.arpa domain to the reverse FQDN." + } + } }, "metadata": { - "description": "Required. The ID of a group obtained from the remote resource that this private endpoint should connect to. If used with private link service connection, this property must be defined as empty string array `[]`." - } - }, - "privateLinkServiceId": { - "type": "string", - "metadata": { - "description": "Required. The resource id of private link service." + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/res/network/public-ip-address:0.8.0" + } } }, - "requestMessage": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. A message passed to the owner of the remote resource with this connection request. Restricted to 140 chars." - } - } - }, - "metadata": { - "description": "Required. Properties of private link service connection." - } - } - }, - "metadata": { - "__bicep_export!": true - } - }, - "customDnsConfigType": { - "type": "object", - "properties": { - "fqdn": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. FQDN that resolves to private endpoint IP address." - } - }, - "ipAddresses": { - "type": "array", - "items": { - "type": "string" - }, - "metadata": { - "description": "Required. A list of private IP addresses of the private endpoint." - } - } - }, - "metadata": { - "__bicep_export!": true - } - }, - "lockType": { - "type": "object", - "properties": { - "name": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. Specify the name of lock." - } - }, - "kind": { - "type": "string", - "allowedValues": [ - "CanNotDelete", - "None", - "ReadOnly" - ], - "nullable": true, - "metadata": { - "description": "Optional. Specify the type of lock." - } - } - }, - "metadata": { - "description": "An AVM-aligned type for a lock.", - "__bicep_imported_from!": { - "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.5.1" - } - } - }, - "privateDnsZoneGroupConfigType": { - "type": "object", - "properties": { - "name": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The name of the private DNS zone group config." - } - }, - "privateDnsZoneResourceId": { - "type": "string", - "metadata": { - "description": "Required. The resource id of the private DNS zone." - } - } - }, - "metadata": { - "__bicep_imported_from!": { - "sourceTemplate": "private-dns-zone-group/main.bicep" - } - } - }, - "roleAssignmentType": { - "type": "object", - "properties": { - "name": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The name (as GUID) of the role assignment. If not provided, a GUID will be generated." - } - }, - "roleDefinitionIdOrName": { - "type": "string", - "metadata": { - "description": "Required. The role to assign. You can provide either the display name of the role definition, the role definition GUID, or its fully qualified ID in the following format: '/providers/Microsoft.Authorization/roleDefinitions/c2f4ef07-c644-48eb-af81-4b1b4947fb11'." - } - }, - "principalId": { - "type": "string", - "metadata": { - "description": "Required. The principal ID of the principal (user/group/identity) to assign the role to." - } - }, - "principalType": { - "type": "string", - "allowedValues": [ - "Device", - "ForeignGroup", - "Group", - "ServicePrincipal", - "User" - ], - "nullable": true, - "metadata": { - "description": "Optional. The principal type of the assigned principal ID." - } - }, - "description": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The description of the role assignment." - } - }, - "condition": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The conditions on the role assignment. This limits the resources it can be assigned to. e.g.: @Resource[Microsoft.Storage/storageAccounts/blobServices/containers:ContainerName] StringEqualsIgnoreCase \"foo_storage_container\"." - } - }, - "conditionVersion": { - "type": "string", - "allowedValues": [ - "2.0" - ], - "nullable": true, - "metadata": { - "description": "Optional. Version of the condition." - } - }, - "delegatedManagedIdentityResourceId": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The Resource Id of the delegated managed identity resource." - } - } - }, - "metadata": { - "description": "An AVM-aligned type for a role assignment.", - "__bicep_imported_from!": { - "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.5.1" - } - } - } - }, - "parameters": { - "name": { - "type": "string", - "metadata": { - "description": "Required. Name of the private endpoint resource to create." - } - }, - "subnetResourceId": { - "type": "string", - "metadata": { - "description": "Required. Resource ID of the subnet where the endpoint needs to be created." - } - }, - "applicationSecurityGroupResourceIds": { - "type": "array", - "items": { - "type": "string" - }, - "nullable": true, - "metadata": { - "description": "Optional. Application security groups in which the private endpoint IP configuration is included." - } - }, - "customNetworkInterfaceName": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The custom name of the network interface attached to the private endpoint." - } - }, - "ipConfigurations": { - "type": "array", - "items": { - "$ref": "#/definitions/ipConfigurationType" - }, - "nullable": true, - "metadata": { - "description": "Optional. A list of IP configurations of the private endpoint. This will be used to map to the First Party Service endpoints." - } - }, - "privateDnsZoneGroup": { - "$ref": "#/definitions/privateDnsZoneGroupType", - "nullable": true, - "metadata": { - "description": "Optional. The private DNS zone group to configure for the private endpoint." - } - }, - "location": { - "type": "string", - "defaultValue": "[resourceGroup().location]", - "metadata": { - "description": "Optional. Location for all Resources." - } - }, - "lock": { - "$ref": "#/definitions/lockType", - "nullable": true, - "metadata": { - "description": "Optional. The lock settings of the service." - } - }, - "roleAssignments": { - "type": "array", - "items": { - "$ref": "#/definitions/roleAssignmentType" - }, - "nullable": true, - "metadata": { - "description": "Optional. Array of role assignments to create." - } - }, - "tags": { - "type": "object", - "nullable": true, - "metadata": { - "description": "Optional. Tags to be applied on all resources/resource groups in this deployment." - } - }, - "customDnsConfigs": { - "type": "array", - "items": { - "$ref": "#/definitions/customDnsConfigType" - }, - "nullable": true, - "metadata": { - "description": "Optional. Custom DNS configurations." - } - }, - "manualPrivateLinkServiceConnections": { - "type": "array", - "items": { - "$ref": "#/definitions/privateLinkServiceConnectionType" - }, - "nullable": true, - "metadata": { - "description": "Conditional. A grouping of information about the connection to the remote resource. Used when the network admin does not have access to approve connections to the remote resource. Required if `privateLinkServiceConnections` is empty." - } - }, - "privateLinkServiceConnections": { - "type": "array", - "items": { - "$ref": "#/definitions/privateLinkServiceConnectionType" - }, - "nullable": true, - "metadata": { - "description": "Conditional. A grouping of information about the connection to the remote resource. Required if `manualPrivateLinkServiceConnections` is empty." - } - }, - "enableTelemetry": { - "type": "bool", - "defaultValue": true, - "metadata": { - "description": "Optional. Enable/Disable usage telemetry for module." - } - } - }, - "variables": { - "copy": [ - { - "name": "formattedRoleAssignments", - "count": "[length(coalesce(parameters('roleAssignments'), createArray()))]", - "input": "[union(coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')], createObject('roleDefinitionId', coalesce(tryGet(variables('builtInRoleNames'), coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName), if(contains(coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName, '/providers/Microsoft.Authorization/roleDefinitions/'), coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName, subscriptionResourceId('Microsoft.Authorization/roleDefinitions', coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName)))))]" - } - ], - "builtInRoleNames": { - "Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'b24988ac-6180-42a0-ab88-20f7382dd24c')]", - "DNS Resolver Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '0f2ebee7-ffd4-4fc0-b3b7-664099fdad5d')]", - "DNS Zone Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'befefa01-2a29-4197-83a8-272ff33ce314')]", - "Domain Services Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'eeaeda52-9324-47f6-8069-5d5bade478b2')]", - "Domain Services Reader": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '361898ef-9ed1-48c2-849c-a832951106bb')]", - "Network Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '4d97b98b-1d4f-4787-a291-c67834d212e7')]", - "Owner": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '8e3af657-a8ff-443c-a75c-2fe8c4bcb635')]", - "Private DNS Zone Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'b12aa53e-6015-4669-85d0-8515ebb3ae7f')]", - "Reader": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'acdd72a7-3385-48ef-bd42-f606fba81ae7')]", - "Role Based Access Control Administrator": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'f58310d9-a9f6-439a-9e8d-f62e7b41a168')]" - } - }, - "resources": { - "avmTelemetry": { - "condition": "[parameters('enableTelemetry')]", - "type": "Microsoft.Resources/deployments", - "apiVersion": "2024-03-01", - "name": "[format('46d3xbcp.res.network-privateendpoint.{0}.{1}', replace('0.11.0', '.', '-'), substring(uniqueString(deployment().name, parameters('location')), 0, 4))]", - "properties": { - "mode": "Incremental", - "template": { - "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", - "contentVersion": "1.0.0.0", - "resources": [], - "outputs": { - "telemetry": { - "type": "String", - "value": "For more information, see https://aka.ms/avm/TelemetryInfo" - } - } - } - } - }, - "privateEndpoint": { - "type": "Microsoft.Network/privateEndpoints", - "apiVersion": "2024-05-01", - "name": "[parameters('name')]", - "location": "[parameters('location')]", - "tags": "[parameters('tags')]", - "properties": { - "copy": [ - { - "name": "applicationSecurityGroups", - "count": "[length(coalesce(parameters('applicationSecurityGroupResourceIds'), createArray()))]", - "input": { - "id": "[coalesce(parameters('applicationSecurityGroupResourceIds'), createArray())[copyIndex('applicationSecurityGroups')]]" - } - } - ], - "customDnsConfigs": "[coalesce(parameters('customDnsConfigs'), createArray())]", - "customNetworkInterfaceName": "[coalesce(parameters('customNetworkInterfaceName'), '')]", - "ipConfigurations": "[coalesce(parameters('ipConfigurations'), createArray())]", - "manualPrivateLinkServiceConnections": "[coalesce(parameters('manualPrivateLinkServiceConnections'), createArray())]", - "privateLinkServiceConnections": "[coalesce(parameters('privateLinkServiceConnections'), createArray())]", - "subnet": { - "id": "[parameters('subnetResourceId')]" - } - } - }, - "privateEndpoint_lock": { - "condition": "[and(not(empty(coalesce(parameters('lock'), createObject()))), not(equals(tryGet(parameters('lock'), 'kind'), 'None')))]", - "type": "Microsoft.Authorization/locks", - "apiVersion": "2020-05-01", - "scope": "[format('Microsoft.Network/privateEndpoints/{0}', parameters('name'))]", - "name": "[coalesce(tryGet(parameters('lock'), 'name'), format('lock-{0}', parameters('name')))]", - "properties": { - "level": "[coalesce(tryGet(parameters('lock'), 'kind'), '')]", - "notes": "[if(equals(tryGet(parameters('lock'), 'kind'), 'CanNotDelete'), 'Cannot delete resource or child resources.', 'Cannot delete or modify the resource or child resources.')]" - }, - "dependsOn": [ - "privateEndpoint" - ] - }, - "privateEndpoint_roleAssignments": { - "copy": { - "name": "privateEndpoint_roleAssignments", - "count": "[length(coalesce(variables('formattedRoleAssignments'), createArray()))]" - }, - "type": "Microsoft.Authorization/roleAssignments", - "apiVersion": "2022-04-01", - "scope": "[format('Microsoft.Network/privateEndpoints/{0}', parameters('name'))]", - "name": "[coalesce(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'name'), guid(resourceId('Microsoft.Network/privateEndpoints', parameters('name')), coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].principalId, coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].roleDefinitionId))]", - "properties": { - "roleDefinitionId": "[coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].roleDefinitionId]", - "principalId": "[coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].principalId]", - "description": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'description')]", - "principalType": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'principalType')]", - "condition": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'condition')]", - "conditionVersion": "[if(not(empty(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'condition'))), coalesce(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'conditionVersion'), '2.0'), null())]", - "delegatedManagedIdentityResourceId": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'delegatedManagedIdentityResourceId')]" - }, - "dependsOn": [ - "privateEndpoint" - ] - }, - "privateEndpoint_privateDnsZoneGroup": { - "condition": "[not(empty(parameters('privateDnsZoneGroup')))]", - "type": "Microsoft.Resources/deployments", - "apiVersion": "2022-09-01", - "name": "[format('{0}-PrivateEndpoint-PrivateDnsZoneGroup', uniqueString(deployment().name))]", - "properties": { - "expressionEvaluationOptions": { - "scope": "inner" - }, - "mode": "Incremental", - "parameters": { - "name": { - "value": "[tryGet(parameters('privateDnsZoneGroup'), 'name')]" - }, - "privateEndpointName": { - "value": "[parameters('name')]" - }, - "privateDnsZoneConfigs": { - "value": "[parameters('privateDnsZoneGroup').privateDnsZoneGroupConfigs]" - } - }, - "template": { - "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", - "languageVersion": "2.0", - "contentVersion": "1.0.0.0", - "metadata": { - "_generator": { - "name": "bicep", - "version": "0.34.44.8038", - "templateHash": "13997305779829540948" - }, - "name": "Private Endpoint Private DNS Zone Groups", - "description": "This module deploys a Private Endpoint Private DNS Zone Group." - }, - "definitions": { - "privateDnsZoneGroupConfigType": { + "inboundNatRuleType": { "type": "object", "properties": { - "name": { + "id": { "type": "string", "nullable": true, "metadata": { - "description": "Optional. The name of the private DNS zone group config." + "description": "Optional. Resource ID of the inbound NAT rule." } }, - "privateDnsZoneResourceId": { + "name": { "type": "string", + "nullable": true, "metadata": { - "description": "Required. The resource id of the private DNS zone." + "description": "Optional. Name of the resource that is unique within the set of inbound NAT rules used by the load balancer. This name can be used to access the resource." + } + }, + "properties": { + "type": "object", + "properties": { + "backendAddressPool": { + "$ref": "#/definitions/subResourceType", + "nullable": true, + "metadata": { + "description": "Optional. A reference to backendAddressPool resource." + } + }, + "backendPort": { + "type": "int", + "nullable": true, + "metadata": { + "description": "Optional. The port used for the internal endpoint. Acceptable values range from 1 to 65535." + } + }, + "enableFloatingIP": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. Configures a virtual machine's endpoint for the floating IP capability required to configure a SQL AlwaysOn Availability Group. This setting is required when using the SQL AlwaysOn Availability Groups in SQL server. This setting can't be changed after you create the endpoint." + } + }, + "enableTcpReset": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. Receive bidirectional TCP Reset on TCP flow idle timeout or unexpected connection termination. This element is only used when the protocol is set to TCP." + } + }, + "frontendIPConfiguration": { + "$ref": "#/definitions/subResourceType", + "nullable": true, + "metadata": { + "description": "Optional. A reference to frontend IP addresses." + } + }, + "frontendPort": { + "type": "int", + "nullable": true, + "metadata": { + "description": "Optional. The port for the external endpoint. Port numbers for each rule must be unique within the Load Balancer. Acceptable values range from 1 to 65534." + } + }, + "frontendPortRangeStart": { + "type": "int", + "nullable": true, + "metadata": { + "description": "Optional. The port range start for the external endpoint. This property is used together with BackendAddressPool and FrontendPortRangeEnd. Individual inbound NAT rule port mappings will be created for each backend address from BackendAddressPool. Acceptable values range from 1 to 65534." + } + }, + "frontendPortRangeEnd": { + "type": "int", + "nullable": true, + "metadata": { + "description": "Optional. The port range end for the external endpoint. This property is used together with BackendAddressPool and FrontendPortRangeStart. Individual inbound NAT rule port mappings will be created for each backend address from BackendAddressPool. Acceptable values range from 1 to 65534." + } + }, + "protocol": { + "type": "string", + "allowedValues": [ + "All", + "Tcp", + "Udp" + ], + "nullable": true, + "metadata": { + "description": "Optional. The reference to the transport protocol used by the load balancing rule." + } + } + }, + "nullable": true, + "metadata": { + "description": "Optional. Properties of the inbound NAT rule." } } }, "metadata": { - "__bicep_export!": true - } - } - }, - "parameters": { - "privateEndpointName": { - "type": "string", - "metadata": { - "description": "Conditional. The name of the parent private endpoint. Required if the template is used in a standalone deployment." + "description": "The type for the inbound NAT rule.", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/res/network/network-interface:0.5.1" + } } }, - "privateDnsZoneConfigs": { - "type": "array", - "items": { - "$ref": "#/definitions/privateDnsZoneGroupConfigType" + "ipTagType": { + "type": "object", + "properties": { + "ipTagType": { + "type": "string", + "metadata": { + "description": "Required. The IP tag type." + } + }, + "tag": { + "type": "string", + "metadata": { + "description": "Required. The IP tag." + } + } }, - "minLength": 1, - "maxLength": 5, "metadata": { - "description": "Required. Array of private DNS zone configurations of the private DNS zone group. A DNS zone group can support up to 5 DNS zones." - } - }, - "name": { - "type": "string", - "defaultValue": "default", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/res/network/public-ip-address:0.8.0" + } + } + }, + "lockType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Specify the name of lock." + } + }, + "kind": { + "type": "string", + "allowedValues": [ + "CanNotDelete", + "None", + "ReadOnly" + ], + "nullable": true, + "metadata": { + "description": "Optional. Specify the type of lock." + } + }, + "notes": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Specify the notes of the lock." + } + } + }, "metadata": { - "description": "Optional. The name of the private DNS zone group." + "description": "An AVM-aligned type for a lock.", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.1" + } } - } - }, - "variables": { - "copy": [ - { - "name": "privateDnsZoneConfigsVar", - "count": "[length(parameters('privateDnsZoneConfigs'))]", - "input": { - "name": "[coalesce(tryGet(parameters('privateDnsZoneConfigs')[copyIndex('privateDnsZoneConfigsVar')], 'name'), last(split(parameters('privateDnsZoneConfigs')[copyIndex('privateDnsZoneConfigsVar')].privateDnsZoneResourceId, '/')))]", - "properties": { - "privateDnsZoneId": "[parameters('privateDnsZoneConfigs')[copyIndex('privateDnsZoneConfigsVar')].privateDnsZoneResourceId]" + }, + "networkInterfaceIPConfigurationOutputType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "metadata": { + "description": "The name of the IP configuration." + } + }, + "privateIP": { + "type": "string", + "nullable": true, + "metadata": { + "description": "The private IP address." + } + }, + "publicIP": { + "type": "string", + "nullable": true, + "metadata": { + "description": "The public IP address." } } + }, + "metadata": { + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/res/network/network-interface:0.5.1" + } } - ] - }, - "resources": { - "privateEndpoint": { - "existing": true, - "type": "Microsoft.Network/privateEndpoints", - "apiVersion": "2024-05-01", - "name": "[parameters('privateEndpointName')]" }, - "privateDnsZoneGroup": { - "type": "Microsoft.Network/privateEndpoints/privateDnsZoneGroups", - "apiVersion": "2024-05-01", - "name": "[format('{0}/{1}', parameters('privateEndpointName'), parameters('name'))]", + "roleAssignmentType": { + "type": "object", "properties": { - "privateDnsZoneConfigs": "[variables('privateDnsZoneConfigsVar')]" + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The name (as GUID) of the role assignment. If not provided, a GUID will be generated." + } + }, + "roleDefinitionIdOrName": { + "type": "string", + "metadata": { + "description": "Required. The role to assign. You can provide either the display name of the role definition, the role definition GUID, or its fully qualified ID in the following format: '/providers/Microsoft.Authorization/roleDefinitions/c2f4ef07-c644-48eb-af81-4b1b4947fb11'." + } + }, + "principalId": { + "type": "string", + "metadata": { + "description": "Required. The principal ID of the principal (user/group/identity) to assign the role to." + } + }, + "principalType": { + "type": "string", + "allowedValues": [ + "Device", + "ForeignGroup", + "Group", + "ServicePrincipal", + "User" + ], + "nullable": true, + "metadata": { + "description": "Optional. The principal type of the assigned principal ID." + } + }, + "description": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The description of the role assignment." + } + }, + "condition": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The conditions on the role assignment. This limits the resources it can be assigned to. e.g.: @Resource[Microsoft.Storage/storageAccounts/blobServices/containers:ContainerName] StringEqualsIgnoreCase \"foo_storage_container\"." + } + }, + "conditionVersion": { + "type": "string", + "allowedValues": [ + "2.0" + ], + "nullable": true, + "metadata": { + "description": "Optional. Version of the condition." + } + }, + "delegatedManagedIdentityResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The Resource Id of the delegated managed identity resource." + } + } + }, + "metadata": { + "description": "An AVM-aligned type for a role assignment.", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.1" + } + } + }, + "subResourceType": { + "type": "object", + "properties": { + "id": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Resource ID of the sub resource." + } + } + }, + "metadata": { + "description": "The type for the sub resource.", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/res/network/network-interface:0.5.1" + } + } + }, + "virtualNetworkTapType": { + "type": "object", + "properties": { + "id": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Resource ID of the virtual network tap." + } + }, + "location": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Location of the virtual network tap." + } + }, + "properties": { + "type": "object", + "nullable": true, + "metadata": { + "description": "Optional. Properties of the virtual network tap." + } + }, + "tags": { + "type": "object", + "nullable": true, + "metadata": { + "description": "Optional. Tags of the virtual network tap." + } + } + }, + "metadata": { + "description": "The type for the virtual network tap.", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/res/network/network-interface:0.5.1" + } } } }, - "outputs": { - "name": { + "parameters": { + "networkInterfaceName": { + "type": "string" + }, + "virtualMachineName": { + "type": "string" + }, + "ipConfigurations": { + "type": "array", + "items": { + "$ref": "#/definitions/ipConfigurationType" + } + }, + "location": { "type": "string", "metadata": { - "description": "The name of the private endpoint DNS zone group." - }, - "value": "[parameters('name')]" + "description": "Optional. Location for all resources." + } }, - "resourceId": { - "type": "string", + "tags": { + "type": "object", + "nullable": true, "metadata": { - "description": "The resource ID of the private endpoint DNS zone group." + "description": "Optional. Tags of the resource." + } + }, + "enableIPForwarding": { + "type": "bool", + "defaultValue": false + }, + "enableAcceleratedNetworking": { + "type": "bool", + "defaultValue": false + }, + "dnsServers": { + "type": "array", + "items": { + "type": "string" }, - "value": "[resourceId('Microsoft.Network/privateEndpoints/privateDnsZoneGroups', parameters('privateEndpointName'), parameters('name'))]" + "defaultValue": [] }, - "resourceGroupName": { + "enableTelemetry": { + "type": "bool", + "metadata": { + "description": "Required. Enable telemetry via a Globally Unique Identifier (GUID)." + } + }, + "networkSecurityGroupResourceId": { "type": "string", + "defaultValue": "", "metadata": { - "description": "The resource group the private endpoint DNS zone group was deployed into." + "description": "Optional. The network security group (NSG) to attach to the network interface." + } + }, + "lock": { + "$ref": "#/definitions/lockType", + "nullable": true, + "metadata": { + "description": "Optional. The lock settings of the service." + } + }, + "diagnosticSettings": { + "type": "array", + "items": { + "$ref": "#/definitions/diagnosticSettingFullType" }, - "value": "[resourceGroup().name]" + "nullable": true, + "metadata": { + "description": "Optional. The diagnostic settings of the service." + } + }, + "roleAssignments": { + "type": "array", + "items": { + "$ref": "#/definitions/roleAssignmentType" + }, + "nullable": true, + "metadata": { + "description": "Optional. Array of role assignments to create." + } } - } - } - }, - "dependsOn": [ - "privateEndpoint" - ] - } - }, - "outputs": { - "resourceGroupName": { - "type": "string", - "metadata": { - "description": "The resource group the private endpoint was deployed into." - }, - "value": "[resourceGroup().name]" - }, - "resourceId": { - "type": "string", - "metadata": { - "description": "The resource ID of the private endpoint." - }, - "value": "[resourceId('Microsoft.Network/privateEndpoints', parameters('name'))]" - }, - "name": { - "type": "string", - "metadata": { - "description": "The name of the private endpoint." - }, - "value": "[parameters('name')]" - }, - "location": { - "type": "string", - "metadata": { - "description": "The location the resource was deployed into." - }, - "value": "[reference('privateEndpoint', '2024-05-01', 'full').location]" - }, - "customDnsConfigs": { - "type": "array", - "items": { - "$ref": "#/definitions/customDnsConfigType" - }, - "metadata": { - "description": "The custom DNS configurations of the private endpoint." - }, - "value": "[reference('privateEndpoint').customDnsConfigs]" - }, - "networkInterfaceResourceIds": { - "type": "array", - "items": { - "type": "string" - }, - "metadata": { - "description": "The resource IDs of the network interfaces associated with the private endpoint." - }, - "value": "[map(reference('privateEndpoint').networkInterfaces, lambda('nic', lambdaVariables('nic').id))]" - }, - "groupId": { - "type": "string", - "nullable": true, - "metadata": { - "description": "The group Id for the private endpoint Group." - }, - "value": "[coalesce(tryGet(tryGet(tryGet(tryGet(reference('privateEndpoint'), 'manualPrivateLinkServiceConnections'), 0, 'properties'), 'groupIds'), 0), tryGet(tryGet(tryGet(tryGet(reference('privateEndpoint'), 'privateLinkServiceConnections'), 0, 'properties'), 'groupIds'), 0))]" - } - } - } - }, - "dependsOn": [ - "aiFoundryAiServices", - "[format('avmPrivateDnsZones[{0}]', variables('dnsZoneIndex').openAI)]", - "[format('avmPrivateDnsZones[{0}]', variables('dnsZoneIndex').cognitiveServices)]", - "virtualNetwork" - ] - }, - "aiFoundryAiServicesProject": { - "condition": "[not(variables('useExistingAiFoundryAiProject'))]", - "type": "Microsoft.Resources/deployments", - "apiVersion": "2025-04-01", - "name": "[take(format('module.ai-project.{0}', variables('aiFoundryAiProjectResourceName')), 64)]", - "properties": { - "expressionEvaluationOptions": { - "scope": "inner" - }, - "mode": "Incremental", - "parameters": { - "name": { - "value": "[variables('aiFoundryAiProjectResourceName')]" - }, - "location": { - "value": "[parameters('azureAiServiceLocation')]" - }, - "tags": { - "value": "[parameters('tags')]" - }, - "desc": { - "value": "[variables('aiFoundryAiProjectDescription')]" - }, - "aiServicesName": { - "value": "[variables('aiFoundryAiServicesResourceName')]" - }, - "azureExistingAIProjectResourceId": { - "value": "[parameters('azureExistingAIProjectResourceId')]" - } - }, - "template": { - "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", - "contentVersion": "1.0.0.0", - "metadata": { - "_generator": { - "name": "bicep", - "version": "0.39.26.7824", - "templateHash": "12336056765515184474" - } - }, - "parameters": { - "name": { - "type": "string", - "metadata": { - "description": "Required. Name of the AI Services project." - } - }, - "location": { - "type": "string", - "defaultValue": "[resourceGroup().location]", - "metadata": { - "description": "Required. The location of the Project resource." - } - }, - "desc": { - "type": "string", - "defaultValue": "[parameters('name')]", - "metadata": { - "description": "Optional. The description of the AI Foundry project to create. Defaults to the project name." - } - }, - "aiServicesName": { - "type": "string", - "metadata": { - "description": "Required. Name of the existing Cognitive Services resource to create the AI Foundry project in." - } - }, - "azureExistingAIProjectResourceId": { - "type": "string", - "defaultValue": "", - "metadata": { - "description": "Required. Azure Existing AI Project ResourceID." - } - }, - "tags": { - "type": "object", - "defaultValue": {}, - "metadata": { - "description": "Optional. Tags to be applied to the resources." - } - } - }, - "variables": { - "useExistingAiFoundryAiProject": "[not(empty(parameters('azureExistingAIProjectResourceId')))]", - "existingOpenAIEndpoint": "[if(variables('useExistingAiFoundryAiProject'), format('https://{0}.openai.azure.com/', split(parameters('azureExistingAIProjectResourceId'), '/')[8]), '')]" - }, - "resources": [ - { - "type": "Microsoft.CognitiveServices/accounts/projects", - "apiVersion": "2025-06-01", - "name": "[format('{0}/{1}', parameters('aiServicesName'), parameters('name'))]", - "tags": "[parameters('tags')]", - "location": "[parameters('location')]", - "identity": { - "type": "SystemAssigned" - }, - "properties": { - "description": "[parameters('desc')]", - "displayName": "[parameters('name')]" - } - } - ], - "outputs": { - "name": { - "type": "string", - "metadata": { - "description": "Required. Name of the AI project." - }, - "value": "[parameters('name')]" - }, - "resourceId": { - "type": "string", - "metadata": { - "description": "Required. Resource ID of the AI project." - }, - "value": "[resourceId('Microsoft.CognitiveServices/accounts/projects', parameters('aiServicesName'), parameters('name'))]" - }, - "apiEndpoint": { - "type": "string", - "metadata": { - "description": "Required. API endpoint for the AI project." - }, - "value": "[reference(resourceId('Microsoft.CognitiveServices/accounts/projects', parameters('aiServicesName'), parameters('name')), '2025-06-01').endpoints['AI Foundry API']]" - }, - "aoaiEndpoint": { - "type": "string", - "metadata": { - "description": "Contains AI Endpoint." - }, - "value": "[if(not(empty(variables('existingOpenAIEndpoint'))), variables('existingOpenAIEndpoint'), reference(resourceId('Microsoft.CognitiveServices/accounts', parameters('aiServicesName')), '2025-06-01').endpoints['OpenAI Language Model Instance API'])]" - }, - "systemAssignedMIPrincipalId": { - "type": "string", - "metadata": { - "description": "Required. Principal ID of the AI project system-assigned managed identity." - }, - "value": "[reference(resourceId('Microsoft.CognitiveServices/accounts/projects', parameters('aiServicesName'), parameters('name')), '2025-06-01', 'full').identity.principalId]" - } - } - } - }, - "dependsOn": [ - "aiFoundryAiServices" - ] - }, - "existingAiServicesRoleAssignments": { - "condition": "[variables('useExistingAiFoundryAiProject')]", - "type": "Microsoft.Resources/deployments", - "apiVersion": "2025-04-01", - "name": "[take(format('module.foundry-role-assignment.{0}', variables('aiFoundryAiServicesResourceName')), 64)]", - "subscriptionId": "[variables('aiFoundryAiServicesSubscriptionId')]", - "resourceGroup": "[variables('aiFoundryAiServicesResourceGroupName')]", - "properties": { - "expressionEvaluationOptions": { - "scope": "inner" - }, - "mode": "Incremental", - "parameters": { - "aiServicesName": { - "value": "[variables('aiFoundryAiServicesResourceName')]" - }, - "principalId": { - "value": "[reference('userAssignedIdentity').outputs.principalId.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.39.26.7824", - "templateHash": "618751761593210210" - } - }, - "parameters": { - "principalId": { - "type": "string", - "metadata": { - "description": "Required. The principal ID of the managed identity to grant access." - } - }, - "aiServicesName": { - "type": "string", - "metadata": { - "description": "Required. The name of the existing AI Services account." - } - }, - "aiProjectName": { - "type": "string", - "defaultValue": "", - "metadata": { - "description": "Optional. The name of the existing AI Project." - } - }, - "principalType": { - "type": "string", - "defaultValue": "ServicePrincipal", - "allowedValues": [ - "Device", - "ForeignGroup", - "Group", - "ServicePrincipal", - "User" - ], - "metadata": { - "description": "Optional. The principal type of the identity." - } - } - }, - "resources": [ - { - "type": "Microsoft.Authorization/roleAssignments", - "apiVersion": "2022-04-01", - "scope": "[format('Microsoft.CognitiveServices/accounts/{0}', parameters('aiServicesName'))]", - "name": "[guid(resourceId('Microsoft.CognitiveServices/accounts', parameters('aiServicesName')), parameters('principalId'), resourceId('Microsoft.Authorization/roleDefinitions', '53ca6127-db72-4b80-b1b0-d745d6d5456d'))]", - "properties": { - "roleDefinitionId": "[resourceId('Microsoft.Authorization/roleDefinitions', '53ca6127-db72-4b80-b1b0-d745d6d5456d')]", - "principalId": "[parameters('principalId')]", - "principalType": "[parameters('principalType')]" - } - }, - { - "type": "Microsoft.Authorization/roleAssignments", - "apiVersion": "2022-04-01", - "scope": "[format('Microsoft.CognitiveServices/accounts/{0}', parameters('aiServicesName'))]", - "name": "[guid(resourceId('Microsoft.CognitiveServices/accounts', parameters('aiServicesName')), parameters('principalId'), resourceId('Microsoft.Authorization/roleDefinitions', '5e0bd9bd-7b93-4f28-af87-19fc36ad61bd'))]", - "properties": { - "roleDefinitionId": "[resourceId('Microsoft.Authorization/roleDefinitions', '5e0bd9bd-7b93-4f28-af87-19fc36ad61bd')]", - "principalId": "[parameters('principalId')]", - "principalType": "[parameters('principalType')]" - } - } - ], - "outputs": { - "aiServicesResourceId": { - "type": "string", - "metadata": { - "description": "The resource ID of the existing AI Services account." - }, - "value": "[resourceId('Microsoft.CognitiveServices/accounts', parameters('aiServicesName'))]" - }, - "aiServicesEndpoint": { - "type": "string", - "metadata": { - "description": "The endpoint of the existing AI Services account." - }, - "value": "[reference(resourceId('Microsoft.CognitiveServices/accounts', parameters('aiServicesName')), '2025-04-01-preview').endpoint]" - }, - "aiProjectPrincipalId": { - "type": "string", - "metadata": { - "description": "The principal ID of the existing AI Project (if provided)." - }, - "value": "[if(not(empty(parameters('aiProjectName'))), reference(resourceId('Microsoft.CognitiveServices/accounts/projects', parameters('aiServicesName'), parameters('aiProjectName')), '2025-04-01-preview', 'full').identity.principalId, '')]" - } - } - } - }, - "dependsOn": [ - "userAssignedIdentity" - ] - }, - "aiSearch": { - "type": "Microsoft.Resources/deployments", - "apiVersion": "2025-04-01", - "name": "[take(format('avm.res.search.search-service.{0}', variables('aiSearchName')), 64)]", - "properties": { - "expressionEvaluationOptions": { - "scope": "inner" - }, - "mode": "Incremental", - "parameters": { - "name": { - "value": "[variables('aiSearchName')]" - }, - "location": { - "value": "[variables('solutionLocation')]" - }, - "tags": { - "value": "[parameters('tags')]" - }, - "enableTelemetry": { - "value": "[parameters('enableTelemetry')]" - }, - "sku": "[if(parameters('enableScalability'), createObject('value', 'standard'), createObject('value', 'basic'))]", - "replicaCount": "[if(parameters('enableRedundancy'), createObject('value', 2), createObject('value', 1))]", - "partitionCount": { - "value": 1 - }, - "hostingMode": { - "value": "default" - }, - "semanticSearch": { - "value": "free" - }, - "authOptions": { - "value": { - "aadOrApiKey": { - "aadAuthFailureMode": "http401WithBearerChallenge" - } - } - }, - "disableLocalAuth": { - "value": false - }, - "roleAssignments": { - "value": [ - { - "principalId": "[reference('userAssignedIdentity').outputs.principalId.value]", - "roleDefinitionIdOrName": "Search Index Data Contributor", - "principalType": "ServicePrincipal" - }, - { - "principalId": "[reference('userAssignedIdentity').outputs.principalId.value]", - "roleDefinitionIdOrName": "Search Service Contributor", - "principalType": "ServicePrincipal" - } - ] - }, - "diagnosticSettings": "[if(parameters('enableMonitoring'), createObject('value', createArray(createObject('workspaceResourceId', if(variables('useExistingLogAnalytics'), parameters('existingLogAnalyticsWorkspaceId'), if(parameters('enableMonitoring'), reference('logAnalyticsWorkspace').outputs.resourceId.value, ''))))), createObject('value', null()))]", - "publicNetworkAccess": { - "value": "Enabled" - } - }, - "template": { - "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", - "languageVersion": "2.0", - "contentVersion": "1.0.0.0", - "metadata": { - "_generator": { - "name": "bicep", - "version": "0.37.4.10188", - "templateHash": "10902281417196168235" - }, - "name": "Search Services", - "description": "This module deploys a Search Service." - }, - "definitions": { - "privateEndpointOutputType": { - "type": "object", - "properties": { - "name": { - "type": "string", - "metadata": { - "description": "The name of the private endpoint." - } - }, - "resourceId": { - "type": "string", - "metadata": { - "description": "The resource ID of the private endpoint." - } - }, - "groupId": { - "type": "string", - "nullable": true, - "metadata": { - "description": "The group Id for the private endpoint Group." - } - }, - "customDnsConfigs": { - "type": "array", - "items": { - "type": "object", - "properties": { - "fqdn": { - "type": "string", - "nullable": true, - "metadata": { - "description": "FQDN that resolves to private endpoint IP address." - } + }, + "resources": { + "networkInterface_publicIPAddresses": { + "copy": { + "name": "networkInterface_publicIPAddresses", + "count": "[length(parameters('ipConfigurations'))]" }, - "ipAddresses": { - "type": "array", - "items": { - "type": "string" + "condition": "[and(not(empty(tryGet(parameters('ipConfigurations')[copyIndex()], 'pipConfiguration'))), empty(tryGet(tryGet(parameters('ipConfigurations')[copyIndex()], 'pipConfiguration'), 'publicIPAddressResourceId')))]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "[format('{0}-publicIP-{1}', deployment().name, copyIndex())]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" }, - "metadata": { - "description": "A list of private IP addresses of the private endpoint." - } - } - } - }, - "metadata": { - "description": "The custom DNS configurations of the private endpoint." - } - }, - "networkInterfaceResourceIds": { - "type": "array", - "items": { - "type": "string" - }, - "metadata": { - "description": "The IDs of the network interfaces associated with the private endpoint." - } - } - }, - "metadata": { - "__bicep_export!": true - } - }, - "secretsExportConfigurationType": { - "type": "object", - "properties": { - "keyVaultResourceId": { - "type": "string", - "metadata": { - "description": "Required. The key vault name where to store the API Admin keys generated by the modules." - } - }, - "primaryAdminKeyName": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The primaryAdminKey secret name to create." - } - }, - "secondaryAdminKeyName": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The secondaryAdminKey secret name to create." - } - } - } - }, - "secretsOutputType": { - "type": "object", - "properties": {}, - "additionalProperties": { - "$ref": "#/definitions/secretSetType", - "metadata": { - "description": "An exported secret's references." - } - } - }, - "authOptionsType": { - "type": "object", - "properties": { - "aadOrApiKey": { - "type": "object", - "properties": { - "aadAuthFailureMode": { - "type": "string", - "allowedValues": [ - "http401WithBearerChallenge", - "http403" - ], - "nullable": true, - "metadata": { - "description": "Optional. Describes what response the data plane API of a search service would send for requests that failed authentication." - } - } - }, - "nullable": true, - "metadata": { - "description": "Optional. Indicates that either the API key or an access token from a Microsoft Entra ID tenant can be used for authentication." - } - }, - "apiKeyOnly": { - "type": "object", - "nullable": true, - "metadata": { - "description": "Optional. Indicates that only the API key can be used for authentication." - } - } - }, - "metadata": { - "__bicep_export!": true - } - }, - "networkRuleSetType": { - "type": "object", - "properties": { - "bypass": { - "type": "string", - "allowedValues": [ - "AzurePortal", - "AzureServices", - "None" - ], - "nullable": true, - "metadata": { - "description": "Optional. Network specific rules that determine how the Azure AI Search service may be reached." - } - }, - "ipRules": { - "type": "array", - "items": { - "$ref": "#/definitions/ipRuleType" - }, - "nullable": true, - "metadata": { - "description": "Optional. A list of IP restriction rules that defines the inbound network(s) with allowing access to the search service endpoint. At the meantime, all other public IP networks are blocked by the firewall. These restriction rules are applied only when the 'publicNetworkAccess' of the search service is 'enabled'; otherwise, traffic over public interface is not allowed even with any public IP rules, and private endpoint connections would be the exclusive access method." - } - } - }, - "metadata": { - "__bicep_export!": true - } - }, - "ipRuleType": { - "type": "object", - "properties": { - "value": { - "type": "string", - "metadata": { - "description": "Required. Value corresponding to a single IPv4 address (eg., 123.1.2.3) or an IP range in CIDR format (eg., 123.1.2.3/24) to be allowed." - } - } - }, - "metadata": { - "__bicep_export!": true - } - }, - "_1.lockType": { - "type": "object", - "properties": { - "name": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. Specify the name of lock." - } - }, - "kind": { - "type": "string", - "allowedValues": [ - "CanNotDelete", - "None", - "ReadOnly" - ], - "nullable": true, - "metadata": { - "description": "Optional. Specify the type of lock." - } - }, - "notes": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. Specify the notes of the lock." - } - } - }, - "metadata": { - "description": "An AVM-aligned type for a lock.", - "__bicep_imported_from!": { - "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.1" - } - } - }, - "_1.privateEndpointCustomDnsConfigType": { - "type": "object", - "properties": { - "fqdn": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. FQDN that resolves to private endpoint IP address." - } - }, - "ipAddresses": { - "type": "array", - "items": { - "type": "string" - }, - "metadata": { - "description": "Required. A list of private IP addresses of the private endpoint." - } - } - }, - "metadata": { - "__bicep_imported_from!": { - "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.1" - } - } - }, - "_1.privateEndpointIpConfigurationType": { - "type": "object", - "properties": { - "name": { - "type": "string", - "metadata": { - "description": "Required. The name of the resource that is unique within a resource group." - } - }, - "properties": { - "type": "object", - "properties": { - "groupId": { - "type": "string", - "metadata": { - "description": "Required. The ID of a group obtained from the remote resource that this private endpoint should connect to." - } - }, - "memberName": { - "type": "string", - "metadata": { - "description": "Required. The member name of a group obtained from the remote resource that this private endpoint should connect to." - } - }, - "privateIPAddress": { - "type": "string", - "metadata": { - "description": "Required. A private IP address obtained from the private endpoint's subnet." - } - } - }, - "metadata": { - "description": "Required. Properties of private endpoint IP configurations." - } - } - }, - "metadata": { - "__bicep_imported_from!": { - "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.1" - } - } - }, - "_1.privateEndpointPrivateDnsZoneGroupType": { - "type": "object", - "properties": { - "name": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The name of the Private DNS Zone Group." - } - }, - "privateDnsZoneGroupConfigs": { - "type": "array", - "items": { - "type": "object", - "properties": { - "name": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The name of the private DNS Zone Group config." - } - }, - "privateDnsZoneResourceId": { - "type": "string", - "metadata": { - "description": "Required. The resource id of the private DNS zone." - } - } - } - }, - "metadata": { - "description": "Required. The private DNS Zone Groups to associate the Private Endpoint. A DNS Zone Group can support up to 5 DNS zones." - } - } - }, - "metadata": { - "__bicep_imported_from!": { - "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.1" - } - } - }, - "_1.roleAssignmentType": { - "type": "object", - "properties": { - "name": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The name (as GUID) of the role assignment. If not provided, a GUID will be generated." - } - }, - "roleDefinitionIdOrName": { - "type": "string", - "metadata": { - "description": "Required. The role to assign. You can provide either the display name of the role definition, the role definition GUID, or its fully qualified ID in the following format: '/providers/Microsoft.Authorization/roleDefinitions/c2f4ef07-c644-48eb-af81-4b1b4947fb11'." - } - }, - "principalId": { - "type": "string", - "metadata": { - "description": "Required. The principal ID of the principal (user/group/identity) to assign the role to." - } - }, - "principalType": { - "type": "string", - "allowedValues": [ - "Device", - "ForeignGroup", - "Group", - "ServicePrincipal", - "User" - ], - "nullable": true, - "metadata": { - "description": "Optional. The principal type of the assigned principal ID." - } - }, - "description": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The description of the role assignment." - } - }, - "condition": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The conditions on the role assignment. This limits the resources it can be assigned to. e.g.: @Resource[Microsoft.Storage/storageAccounts/blobServices/containers:ContainerName] StringEqualsIgnoreCase \"foo_storage_container\"." - } - }, - "conditionVersion": { - "type": "string", - "allowedValues": [ - "2.0" - ], - "nullable": true, - "metadata": { - "description": "Optional. Version of the condition." - } - }, - "delegatedManagedIdentityResourceId": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The Resource Id of the delegated managed identity resource." - } - } - }, - "metadata": { - "description": "An AVM-aligned type for a role assignment.", - "__bicep_imported_from!": { - "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.1" - } - } - }, - "diagnosticSettingFullType": { - "type": "object", - "properties": { - "name": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The name of the diagnostic setting." - } - }, - "logCategoriesAndGroups": { - "type": "array", - "items": { - "type": "object", - "properties": { - "category": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. Name of a Diagnostic Log category for a resource type this setting is applied to. Set the specific logs to collect here." - } - }, - "categoryGroup": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. Name of a Diagnostic Log category group for a resource type this setting is applied to. Set to `allLogs` to collect all logs." - } - }, - "enabled": { - "type": "bool", - "nullable": true, - "metadata": { - "description": "Optional. Enable or disable the category explicitly. Default is `true`." - } - } - } - }, - "nullable": true, - "metadata": { - "description": "Optional. The name of logs that will be streamed. \"allLogs\" includes all possible logs for the resource. Set to `[]` to disable log collection." - } - }, - "metricCategories": { - "type": "array", - "items": { - "type": "object", - "properties": { - "category": { - "type": "string", - "metadata": { - "description": "Required. Name of a Diagnostic Metric category for a resource type this setting is applied to. Set to `AllMetrics` to collect all metrics." - } - }, - "enabled": { - "type": "bool", - "nullable": true, - "metadata": { - "description": "Optional. Enable or disable the category explicitly. Default is `true`." - } - } - } - }, - "nullable": true, - "metadata": { - "description": "Optional. The name of metrics that will be streamed. \"allMetrics\" includes all possible metrics for the resource. Set to `[]` to disable metric collection." - } - }, - "logAnalyticsDestinationType": { - "type": "string", - "allowedValues": [ - "AzureDiagnostics", - "Dedicated" - ], - "nullable": true, - "metadata": { - "description": "Optional. A string indicating whether the export to Log Analytics should use the default destination type, i.e. AzureDiagnostics, or use a destination type." - } - }, - "workspaceResourceId": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. Resource ID of the diagnostic log analytics workspace. For security reasons, it is recommended to set diagnostic settings to send data to either storage account, log analytics workspace or event hub." - } - }, - "storageAccountResourceId": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. Resource ID of the diagnostic storage account. For security reasons, it is recommended to set diagnostic settings to send data to either storage account, log analytics workspace or event hub." - } - }, - "eventHubAuthorizationRuleResourceId": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. Resource ID of the diagnostic event hub authorization rule for the Event Hubs namespace in which the event hub should be created or streamed to." - } - }, - "eventHubName": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. Name of the diagnostic event hub within the namespace to which logs are streamed. Without this, an event hub is created for each log category. For security reasons, it is recommended to set diagnostic settings to send data to either storage account, log analytics workspace or event hub." - } - }, - "marketplacePartnerResourceId": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The full ARM resource ID of the Marketplace resource to which you would like to send Diagnostic Logs." - } - } - }, - "metadata": { - "description": "An AVM-aligned type for a diagnostic setting. To be used if both logs & metrics are supported by the resource provider.", - "__bicep_imported_from!": { - "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.5.1" - } - } - }, - "lockType": { - "type": "object", - "properties": { - "name": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. Specify the name of lock." - } - }, - "kind": { - "type": "string", - "allowedValues": [ - "CanNotDelete", - "None", - "ReadOnly" - ], - "nullable": true, - "metadata": { - "description": "Optional. Specify the type of lock." - } - }, - "notes": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. Specify the notes of the lock." - } - } - }, - "metadata": { - "description": "An AVM-aligned type for a lock.", - "__bicep_imported_from!": { - "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.0" - } - } - }, - "managedIdentityAllType": { - "type": "object", - "properties": { - "systemAssigned": { - "type": "bool", - "nullable": true, - "metadata": { - "description": "Optional. Enables system assigned managed identity on the resource." - } - }, - "userAssignedResourceIds": { - "type": "array", - "items": { - "type": "string" - }, - "nullable": true, - "metadata": { - "description": "Optional. The resource ID(s) to assign to the resource. Required if a user assigned identity is used for encryption." - } - } - }, - "metadata": { - "description": "An AVM-aligned type for a managed identity configuration. To be used if both a system-assigned & user-assigned identities are supported by the resource provider.", - "__bicep_imported_from!": { - "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.5.1" - } - } - }, - "privateEndpointSingleServiceType": { - "type": "object", - "properties": { - "name": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The name of the Private Endpoint." - } - }, - "location": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The location to deploy the Private Endpoint to." - } - }, - "privateLinkServiceConnectionName": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The name of the private link connection to create." - } - }, - "service": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The subresource to deploy the Private Endpoint for. For example \"vault\" for a Key Vault Private Endpoint." - } - }, - "subnetResourceId": { - "type": "string", - "metadata": { - "description": "Required. Resource ID of the subnet where the endpoint needs to be created." - } - }, - "resourceGroupResourceId": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The resource ID of the Resource Group the Private Endpoint will be created in. If not specified, the Resource Group of the provided Virtual Network Subnet is used." - } - }, - "privateDnsZoneGroup": { - "$ref": "#/definitions/_1.privateEndpointPrivateDnsZoneGroupType", - "nullable": true, - "metadata": { - "description": "Optional. The private DNS Zone Group to configure for the Private Endpoint." - } - }, - "isManualConnection": { - "type": "bool", - "nullable": true, - "metadata": { - "description": "Optional. If Manual Private Link Connection is required." - } - }, - "manualConnectionRequestMessage": { - "type": "string", - "nullable": true, - "maxLength": 140, - "metadata": { - "description": "Optional. A message passed to the owner of the remote resource with the manual connection request." - } - }, - "customDnsConfigs": { - "type": "array", - "items": { - "$ref": "#/definitions/_1.privateEndpointCustomDnsConfigType" - }, - "nullable": true, - "metadata": { - "description": "Optional. Custom DNS configurations." - } - }, - "ipConfigurations": { - "type": "array", - "items": { - "$ref": "#/definitions/_1.privateEndpointIpConfigurationType" - }, - "nullable": true, - "metadata": { - "description": "Optional. A list of IP configurations of the Private Endpoint. This will be used to map to the first-party Service endpoints." - } - }, - "applicationSecurityGroupResourceIds": { - "type": "array", - "items": { - "type": "string" - }, - "nullable": true, - "metadata": { - "description": "Optional. Application security groups in which the Private Endpoint IP configuration is included." - } - }, - "customNetworkInterfaceName": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The custom name of the network interface attached to the Private Endpoint." - } - }, - "lock": { - "$ref": "#/definitions/_1.lockType", - "nullable": true, - "metadata": { - "description": "Optional. Specify the type of lock." - } - }, - "roleAssignments": { - "type": "array", - "items": { - "$ref": "#/definitions/_1.roleAssignmentType" - }, - "nullable": true, - "metadata": { - "description": "Optional. Array of role assignments to create." - } - }, - "tags": { - "type": "object", - "nullable": true, - "metadata": { - "__bicep_resource_derived_type!": { - "source": "Microsoft.Network/privateEndpoints@2024-07-01#properties/tags" - }, - "description": "Optional. Tags to be applied on all resources/Resource Groups in this deployment." - } - }, - "enableTelemetry": { - "type": "bool", - "nullable": true, - "metadata": { - "description": "Optional. Enable/Disable usage telemetry for module." - } - } - }, - "metadata": { - "description": "An AVM-aligned type for a private endpoint. To be used if the private endpoint's default service / groupId can be assumed (i.e., for services that only have one Private Endpoint type like 'vault' for key vault).", - "__bicep_imported_from!": { - "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.1" - } - } - }, - "roleAssignmentType": { - "type": "object", - "properties": { - "name": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The name (as GUID) of the role assignment. If not provided, a GUID will be generated." - } - }, - "roleDefinitionIdOrName": { - "type": "string", - "metadata": { - "description": "Required. The role to assign. You can provide either the display name of the role definition, the role definition GUID, or its fully qualified ID in the following format: '/providers/Microsoft.Authorization/roleDefinitions/c2f4ef07-c644-48eb-af81-4b1b4947fb11'." - } - }, - "principalId": { - "type": "string", - "metadata": { - "description": "Required. The principal ID of the principal (user/group/identity) to assign the role to." - } - }, - "principalType": { - "type": "string", - "allowedValues": [ - "Device", - "ForeignGroup", - "Group", - "ServicePrincipal", - "User" - ], - "nullable": true, - "metadata": { - "description": "Optional. The principal type of the assigned principal ID." - } - }, - "description": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The description of the role assignment." - } - }, - "condition": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The conditions on the role assignment. This limits the resources it can be assigned to. e.g.: @Resource[Microsoft.Storage/storageAccounts/blobServices/containers:ContainerName] StringEqualsIgnoreCase \"foo_storage_container\"." - } - }, - "conditionVersion": { - "type": "string", - "allowedValues": [ - "2.0" - ], - "nullable": true, - "metadata": { - "description": "Optional. Version of the condition." - } - }, - "delegatedManagedIdentityResourceId": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The Resource Id of the delegated managed identity resource." - } - } - }, - "metadata": { - "description": "An AVM-aligned type for a role assignment.", - "__bicep_imported_from!": { - "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.5.1" - } - } - }, - "secretSetType": { - "type": "object", - "properties": { - "secretResourceId": { - "type": "string", - "metadata": { - "description": "The resourceId of the exported secret." - } - }, - "secretUri": { - "type": "string", - "metadata": { - "description": "The secret URI of the exported secret." - } - } - }, - "metadata": { - "__bicep_imported_from!": { - "sourceTemplate": "modules/keyVaultExport.bicep" - } - } - } - }, - "parameters": { - "name": { - "type": "string", - "metadata": { - "description": "Required. The name of the Azure Cognitive Search service to create or update. Search service names must only contain lowercase letters, digits or dashes, cannot use dash as the first two or last one characters, cannot contain consecutive dashes, and must be between 2 and 60 characters in length. Search service names must be globally unique since they are part of the service URI (https://.search.windows.net). You cannot change the service name after the service is created." - } - }, - "authOptions": { - "$ref": "#/definitions/authOptionsType", - "nullable": true, - "metadata": { - "description": "Optional. Defines the options for how the data plane API of a Search service authenticates requests. Must remain an empty object {} if 'disableLocalAuth' is set to true." - } - }, - "disableLocalAuth": { - "type": "bool", - "defaultValue": true, - "metadata": { - "description": "Optional. When set to true, calls to the search service will not be permitted to utilize API keys for authentication. This cannot be set to true if 'authOptions' are defined." - } - }, - "enableTelemetry": { - "type": "bool", - "defaultValue": true, - "metadata": { - "description": "Optional. Enable/Disable usage telemetry for module." - } - }, - "cmkEnforcement": { - "type": "string", - "defaultValue": "Unspecified", - "allowedValues": [ - "Disabled", - "Enabled", - "Unspecified" - ], - "metadata": { - "description": "Optional. Describes a policy that determines how resources within the search service are to be encrypted with Customer Managed Keys." - } - }, - "hostingMode": { - "type": "string", - "defaultValue": "default", - "allowedValues": [ - "default", - "highDensity" - ], - "metadata": { - "description": "Optional. Applicable only for the standard3 SKU. You can set this property to enable up to 3 high density partitions that allow up to 1000 indexes, which is much higher than the maximum indexes allowed for any other SKU. For the standard3 SKU, the value is either 'default' or 'highDensity'. For all other SKUs, this value must be 'default'." - } - }, - "location": { - "type": "string", - "defaultValue": "[resourceGroup().location]", - "metadata": { - "description": "Optional. Location for all Resources." - } - }, - "lock": { - "$ref": "#/definitions/lockType", - "nullable": true, - "metadata": { - "description": "Optional. The lock settings for all Resources in the solution." - } - }, - "networkRuleSet": { - "$ref": "#/definitions/networkRuleSetType", - "nullable": true, - "metadata": { - "description": "Optional. Network specific rules that determine how the Azure Cognitive Search service may be reached." - } - }, - "partitionCount": { - "type": "int", - "defaultValue": 1, - "minValue": 1, - "maxValue": 12, - "metadata": { - "description": "Optional. The number of partitions in the search service; if specified, it can be 1, 2, 3, 4, 6, or 12. Values greater than 1 are only valid for standard SKUs. For 'standard3' services with hostingMode set to 'highDensity', the allowed values are between 1 and 3." - } - }, - "privateEndpoints": { - "type": "array", - "items": { - "$ref": "#/definitions/privateEndpointSingleServiceType" - }, - "nullable": true, - "metadata": { - "description": "Optional. Configuration details for private endpoints. For security reasons, it is recommended to use private endpoints whenever possible." - } - }, - "sharedPrivateLinkResources": { - "type": "array", - "defaultValue": [], - "metadata": { - "description": "Optional. The sharedPrivateLinkResources to create as part of the search Service." - } - }, - "publicNetworkAccess": { - "type": "string", - "defaultValue": "Enabled", - "allowedValues": [ - "Enabled", - "Disabled" - ], - "metadata": { - "description": "Optional. This value can be set to 'Enabled' to avoid breaking changes on existing customer resources and templates. If set to 'Disabled', traffic over public interface is not allowed, and private endpoint connections would be the exclusive access method." - } - }, - "secretsExportConfiguration": { - "$ref": "#/definitions/secretsExportConfigurationType", - "nullable": true, - "metadata": { - "description": "Optional. Key vault reference and secret settings for the module's secrets export." - } - }, - "replicaCount": { - "type": "int", - "defaultValue": 3, - "minValue": 1, - "maxValue": 12, - "metadata": { - "description": "Optional. The number of replicas in the search service. If specified, it must be a value between 1 and 12 inclusive for standard SKUs or between 1 and 3 inclusive for basic SKU." - } - }, - "roleAssignments": { - "type": "array", - "items": { - "$ref": "#/definitions/roleAssignmentType" - }, - "nullable": true, - "metadata": { - "description": "Optional. Array of role assignments to create." - } - }, - "semanticSearch": { - "type": "string", - "nullable": true, - "allowedValues": [ - "disabled", - "free", - "standard" - ], - "metadata": { - "description": "Optional. Sets options that control the availability of semantic search. This configuration is only possible for certain search SKUs in certain locations." - } - }, - "sku": { - "type": "string", - "defaultValue": "standard", - "allowedValues": [ - "basic", - "free", - "standard", - "standard2", - "standard3", - "storage_optimized_l1", - "storage_optimized_l2" - ], - "metadata": { - "description": "Optional. Defines the SKU of an Azure Cognitive Search Service, which determines price tier and capacity limits." - } - }, - "managedIdentities": { - "$ref": "#/definitions/managedIdentityAllType", - "nullable": true, - "metadata": { - "description": "Optional. The managed identity definition for this resource." - } - }, - "diagnosticSettings": { - "type": "array", - "items": { - "$ref": "#/definitions/diagnosticSettingFullType" - }, - "nullable": true, - "metadata": { - "description": "Optional. The diagnostic settings of the service." - } - }, - "tags": { - "type": "object", - "metadata": { - "__bicep_resource_derived_type!": { - "source": "Microsoft.Search/searchServices@2025-02-01-preview#properties/tags" - }, - "description": "Optional. Tags to help categorize the resource in the Azure portal." - }, - "nullable": true - } - }, - "variables": { - "copy": [ - { - "name": "formattedRoleAssignments", - "count": "[length(coalesce(parameters('roleAssignments'), createArray()))]", - "input": "[union(coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')], createObject('roleDefinitionId', coalesce(tryGet(variables('builtInRoleNames'), coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName), if(contains(coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName, '/providers/Microsoft.Authorization/roleDefinitions/'), coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName, subscriptionResourceId('Microsoft.Authorization/roleDefinitions', coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName)))))]" - } - ], - "enableReferencedModulesTelemetry": false, - "formattedUserAssignedIdentities": "[reduce(map(coalesce(tryGet(parameters('managedIdentities'), 'userAssignedResourceIds'), createArray()), lambda('id', createObject(format('{0}', lambdaVariables('id')), createObject()))), createObject(), lambda('cur', 'next', union(lambdaVariables('cur'), lambdaVariables('next'))))]", - "identity": "[if(not(empty(parameters('managedIdentities'))), createObject('type', if(coalesce(tryGet(parameters('managedIdentities'), 'systemAssigned'), false()), if(not(empty(coalesce(tryGet(parameters('managedIdentities'), 'userAssignedResourceIds'), createObject()))), 'SystemAssigned,UserAssigned', 'SystemAssigned'), if(not(empty(coalesce(tryGet(parameters('managedIdentities'), 'userAssignedResourceIds'), createObject()))), 'UserAssigned', '')), 'userAssignedIdentities', if(not(empty(variables('formattedUserAssignedIdentities'))), variables('formattedUserAssignedIdentities'), null())), null())]", - "builtInRoleNames": { - "Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'b24988ac-6180-42a0-ab88-20f7382dd24c')]", - "Owner": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '8e3af657-a8ff-443c-a75c-2fe8c4bcb635')]", - "Reader": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'acdd72a7-3385-48ef-bd42-f606fba81ae7')]", - "Role Based Access Control Administrator": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'f58310d9-a9f6-439a-9e8d-f62e7b41a168')]", - "Search Index Data Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '8ebe5a00-799e-43f5-93ac-243d3dce84a7')]", - "Search Index Data Reader": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '1407120a-92aa-4202-b7e9-c0e197c71c8f')]", - "Search Service Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '7ca78c08-252a-4471-8644-bb5ff32d4ba0')]", - "User Access Administrator": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '18d7d88d-d35e-4fb5-a5c3-7773c20a72d9')]" - } - }, - "resources": { - "avmTelemetry": { - "condition": "[parameters('enableTelemetry')]", - "type": "Microsoft.Resources/deployments", - "apiVersion": "2024-03-01", - "name": "[format('46d3xbcp.res.search-searchservice.{0}.{1}', replace('0.11.1', '.', '-'), substring(uniqueString(deployment().name, parameters('location')), 0, 4))]", - "properties": { - "mode": "Incremental", - "template": { - "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", - "contentVersion": "1.0.0.0", - "resources": [], - "outputs": { - "telemetry": { - "type": "String", - "value": "For more information, see https://aka.ms/avm/TelemetryInfo" - } - } - } - } - }, - "searchService": { - "type": "Microsoft.Search/searchServices", - "apiVersion": "2025-02-01-preview", - "name": "[parameters('name')]", - "location": "[parameters('location')]", - "sku": { - "name": "[parameters('sku')]" - }, - "tags": "[parameters('tags')]", - "identity": "[variables('identity')]", - "properties": { - "authOptions": "[parameters('authOptions')]", - "disableLocalAuth": "[parameters('disableLocalAuth')]", - "encryptionWithCmk": { - "enforcement": "[parameters('cmkEnforcement')]" - }, - "hostingMode": "[parameters('hostingMode')]", - "networkRuleSet": "[parameters('networkRuleSet')]", - "partitionCount": "[parameters('partitionCount')]", - "replicaCount": "[parameters('replicaCount')]", - "publicNetworkAccess": "[toLower(parameters('publicNetworkAccess'))]", - "semanticSearch": "[parameters('semanticSearch')]" - } - }, - "searchService_diagnosticSettings": { - "copy": { - "name": "searchService_diagnosticSettings", - "count": "[length(coalesce(parameters('diagnosticSettings'), createArray()))]" - }, - "type": "Microsoft.Insights/diagnosticSettings", - "apiVersion": "2021-05-01-preview", - "scope": "[format('Microsoft.Search/searchServices/{0}', parameters('name'))]", - "name": "[coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'name'), format('{0}-diagnosticSettings', parameters('name')))]", - "properties": { - "copy": [ - { - "name": "metrics", - "count": "[length(coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'metricCategories'), createArray(createObject('category', 'AllMetrics'))))]", - "input": { - "category": "[coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'metricCategories'), createArray(createObject('category', 'AllMetrics')))[copyIndex('metrics')].category]", - "enabled": "[coalesce(tryGet(coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'metricCategories'), createArray(createObject('category', 'AllMetrics')))[copyIndex('metrics')], 'enabled'), true())]", - "timeGrain": null - } - }, - { - "name": "logs", - "count": "[length(coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'logCategoriesAndGroups'), createArray(createObject('categoryGroup', 'allLogs'))))]", - "input": { - "categoryGroup": "[tryGet(coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'logCategoriesAndGroups'), createArray(createObject('categoryGroup', 'allLogs')))[copyIndex('logs')], 'categoryGroup')]", - "category": "[tryGet(coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'logCategoriesAndGroups'), createArray(createObject('categoryGroup', 'allLogs')))[copyIndex('logs')], 'category')]", - "enabled": "[coalesce(tryGet(coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'logCategoriesAndGroups'), createArray(createObject('categoryGroup', 'allLogs')))[copyIndex('logs')], 'enabled'), true())]" - } - } - ], - "storageAccountId": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'storageAccountResourceId')]", - "workspaceId": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'workspaceResourceId')]", - "eventHubAuthorizationRuleId": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'eventHubAuthorizationRuleResourceId')]", - "eventHubName": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'eventHubName')]", - "marketplacePartnerId": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'marketplacePartnerResourceId')]", - "logAnalyticsDestinationType": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'logAnalyticsDestinationType')]" - }, - "dependsOn": [ - "searchService" - ] - }, - "searchService_lock": { - "condition": "[and(not(empty(coalesce(parameters('lock'), createObject()))), not(equals(tryGet(parameters('lock'), 'kind'), 'None')))]", - "type": "Microsoft.Authorization/locks", - "apiVersion": "2020-05-01", - "scope": "[format('Microsoft.Search/searchServices/{0}', parameters('name'))]", - "name": "[coalesce(tryGet(parameters('lock'), 'name'), format('lock-{0}', parameters('name')))]", - "properties": { - "level": "[coalesce(tryGet(parameters('lock'), 'kind'), '')]", - "notes": "[coalesce(tryGet(parameters('lock'), 'notes'), if(equals(tryGet(parameters('lock'), 'kind'), 'CanNotDelete'), 'Cannot delete resource or child resources.', 'Cannot delete or modify the resource or child resources.'))]" - }, - "dependsOn": [ - "searchService" - ] - }, - "searchService_roleAssignments": { - "copy": { - "name": "searchService_roleAssignments", - "count": "[length(coalesce(variables('formattedRoleAssignments'), createArray()))]" - }, - "type": "Microsoft.Authorization/roleAssignments", - "apiVersion": "2022-04-01", - "scope": "[format('Microsoft.Search/searchServices/{0}', parameters('name'))]", - "name": "[coalesce(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'name'), guid(resourceId('Microsoft.Search/searchServices', parameters('name')), coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].principalId, coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].roleDefinitionId))]", - "properties": { - "roleDefinitionId": "[coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].roleDefinitionId]", - "principalId": "[coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].principalId]", - "description": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'description')]", - "principalType": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'principalType')]", - "condition": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'condition')]", - "conditionVersion": "[if(not(empty(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'condition'))), coalesce(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'conditionVersion'), '2.0'), null())]", - "delegatedManagedIdentityResourceId": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'delegatedManagedIdentityResourceId')]" - }, - "dependsOn": [ - "searchService" - ] - }, - "searchService_privateEndpoints": { - "copy": { - "name": "searchService_privateEndpoints", - "count": "[length(coalesce(parameters('privateEndpoints'), createArray()))]" - }, - "type": "Microsoft.Resources/deployments", - "apiVersion": "2022-09-01", - "name": "[format('{0}-searchService-PrivateEndpoint-{1}', uniqueString(deployment().name, parameters('location')), copyIndex())]", - "subscriptionId": "[split(coalesce(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'resourceGroupResourceId'), resourceGroup().id), '/')[2]]", - "resourceGroup": "[split(coalesce(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'resourceGroupResourceId'), resourceGroup().id), '/')[4]]", - "properties": { - "expressionEvaluationOptions": { - "scope": "inner" - }, - "mode": "Incremental", - "parameters": { - "name": { - "value": "[coalesce(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'name'), format('pep-{0}-{1}-{2}', last(split(resourceId('Microsoft.Search/searchServices', parameters('name')), '/')), coalesce(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'service'), 'searchService'), copyIndex()))]" - }, - "privateLinkServiceConnections": "[if(not(equals(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'isManualConnection'), true())), createObject('value', createArray(createObject('name', coalesce(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'privateLinkServiceConnectionName'), format('{0}-{1}-{2}', last(split(resourceId('Microsoft.Search/searchServices', parameters('name')), '/')), coalesce(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'service'), 'searchService'), copyIndex())), 'properties', createObject('privateLinkServiceId', resourceId('Microsoft.Search/searchServices', parameters('name')), 'groupIds', createArray(coalesce(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'service'), 'searchService')))))), createObject('value', null()))]", - "manualPrivateLinkServiceConnections": "[if(equals(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'isManualConnection'), true()), createObject('value', createArray(createObject('name', coalesce(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'privateLinkServiceConnectionName'), format('{0}-{1}-{2}', last(split(resourceId('Microsoft.Search/searchServices', parameters('name')), '/')), coalesce(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'service'), 'searchService'), copyIndex())), 'properties', createObject('privateLinkServiceId', resourceId('Microsoft.Search/searchServices', parameters('name')), 'groupIds', createArray(coalesce(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'service'), 'searchService')), 'requestMessage', coalesce(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'manualConnectionRequestMessage'), 'Manual approval required.'))))), createObject('value', null()))]", - "subnetResourceId": { - "value": "[coalesce(parameters('privateEndpoints'), createArray())[copyIndex()].subnetResourceId]" - }, - "enableTelemetry": { - "value": "[variables('enableReferencedModulesTelemetry')]" - }, - "location": { - "value": "[coalesce(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'location'), reference(split(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()].subnetResourceId, '/subnets/')[0], '2020-06-01', 'Full').location)]" - }, - "lock": { - "value": "[coalesce(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'lock'), parameters('lock'))]" - }, - "privateDnsZoneGroup": { - "value": "[tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'privateDnsZoneGroup')]" - }, - "roleAssignments": { - "value": "[tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'roleAssignments')]" - }, - "tags": { - "value": "[coalesce(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'tags'), parameters('tags'))]" - }, - "customDnsConfigs": { - "value": "[tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'customDnsConfigs')]" - }, - "ipConfigurations": { - "value": "[tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'ipConfigurations')]" - }, - "applicationSecurityGroupResourceIds": { - "value": "[tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'applicationSecurityGroupResourceIds')]" - }, - "customNetworkInterfaceName": { - "value": "[tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'customNetworkInterfaceName')]" - } - }, - "template": { - "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", - "languageVersion": "2.0", - "contentVersion": "1.0.0.0", - "metadata": { - "_generator": { - "name": "bicep", - "version": "0.34.44.8038", - "templateHash": "12389807800450456797" - }, - "name": "Private Endpoints", - "description": "This module deploys a Private Endpoint." - }, - "definitions": { - "privateDnsZoneGroupType": { - "type": "object", - "properties": { - "name": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The name of the Private DNS Zone Group." - } - }, - "privateDnsZoneGroupConfigs": { - "type": "array", - "items": { - "$ref": "#/definitions/privateDnsZoneGroupConfigType" - }, - "metadata": { - "description": "Required. The private DNS zone groups to associate the private endpoint. A DNS zone group can support up to 5 DNS zones." - } - } - }, - "metadata": { - "__bicep_export!": true - } - }, - "ipConfigurationType": { - "type": "object", - "properties": { - "name": { - "type": "string", - "metadata": { - "description": "Required. The name of the resource that is unique within a resource group." - } - }, - "properties": { - "type": "object", - "properties": { - "groupId": { - "type": "string", - "metadata": { - "description": "Required. The ID of a group obtained from the remote resource that this private endpoint should connect to. If used with private link service connection, this property must be defined as empty string." - } - }, - "memberName": { - "type": "string", - "metadata": { - "description": "Required. The member name of a group obtained from the remote resource that this private endpoint should connect to. If used with private link service connection, this property must be defined as empty string." - } - }, - "privateIPAddress": { - "type": "string", - "metadata": { - "description": "Required. A private IP address obtained from the private endpoint's subnet." - } - } - }, - "metadata": { - "description": "Required. Properties of private endpoint IP configurations." - } - } - }, - "metadata": { - "__bicep_export!": true - } - }, - "privateLinkServiceConnectionType": { - "type": "object", - "properties": { - "name": { - "type": "string", - "metadata": { - "description": "Required. The name of the private link service connection." - } - }, - "properties": { - "type": "object", - "properties": { - "groupIds": { - "type": "array", - "items": { - "type": "string" - }, - "metadata": { - "description": "Required. The ID of a group obtained from the remote resource that this private endpoint should connect to. If used with private link service connection, this property must be defined as empty string array `[]`." - } - }, - "privateLinkServiceId": { - "type": "string", - "metadata": { - "description": "Required. The resource id of private link service." - } - }, - "requestMessage": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. A message passed to the owner of the remote resource with this connection request. Restricted to 140 chars." - } - } - }, - "metadata": { - "description": "Required. Properties of private link service connection." - } - } - }, - "metadata": { - "__bicep_export!": true - } - }, - "customDnsConfigType": { - "type": "object", - "properties": { - "fqdn": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. FQDN that resolves to private endpoint IP address." - } - }, - "ipAddresses": { - "type": "array", - "items": { - "type": "string" - }, - "metadata": { - "description": "Required. A list of private IP addresses of the private endpoint." - } - } - }, - "metadata": { - "__bicep_export!": true - } - }, - "lockType": { - "type": "object", - "properties": { - "name": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. Specify the name of lock." - } - }, - "kind": { - "type": "string", - "allowedValues": [ - "CanNotDelete", - "None", - "ReadOnly" - ], - "nullable": true, - "metadata": { - "description": "Optional. Specify the type of lock." - } - } - }, - "metadata": { - "description": "An AVM-aligned type for a lock.", - "__bicep_imported_from!": { - "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.5.1" - } - } - }, - "privateDnsZoneGroupConfigType": { - "type": "object", - "properties": { - "name": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The name of the private DNS zone group config." - } - }, - "privateDnsZoneResourceId": { - "type": "string", - "metadata": { - "description": "Required. The resource id of the private DNS zone." - } - } - }, - "metadata": { - "__bicep_imported_from!": { - "sourceTemplate": "private-dns-zone-group/main.bicep" - } - } - }, - "roleAssignmentType": { - "type": "object", - "properties": { - "name": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The name (as GUID) of the role assignment. If not provided, a GUID will be generated." - } - }, - "roleDefinitionIdOrName": { - "type": "string", - "metadata": { - "description": "Required. The role to assign. You can provide either the display name of the role definition, the role definition GUID, or its fully qualified ID in the following format: '/providers/Microsoft.Authorization/roleDefinitions/c2f4ef07-c644-48eb-af81-4b1b4947fb11'." - } - }, - "principalId": { - "type": "string", - "metadata": { - "description": "Required. The principal ID of the principal (user/group/identity) to assign the role to." - } - }, - "principalType": { - "type": "string", - "allowedValues": [ - "Device", - "ForeignGroup", - "Group", - "ServicePrincipal", - "User" - ], - "nullable": true, - "metadata": { - "description": "Optional. The principal type of the assigned principal ID." - } - }, - "description": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The description of the role assignment." - } - }, - "condition": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The conditions on the role assignment. This limits the resources it can be assigned to. e.g.: @Resource[Microsoft.Storage/storageAccounts/blobServices/containers:ContainerName] StringEqualsIgnoreCase \"foo_storage_container\"." - } - }, - "conditionVersion": { - "type": "string", - "allowedValues": [ - "2.0" - ], - "nullable": true, - "metadata": { - "description": "Optional. Version of the condition." - } - }, - "delegatedManagedIdentityResourceId": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The Resource Id of the delegated managed identity resource." - } - } - }, - "metadata": { - "description": "An AVM-aligned type for a role assignment.", - "__bicep_imported_from!": { - "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.5.1" - } - } - } - }, - "parameters": { - "name": { - "type": "string", - "metadata": { - "description": "Required. Name of the private endpoint resource to create." - } - }, - "subnetResourceId": { - "type": "string", - "metadata": { - "description": "Required. Resource ID of the subnet where the endpoint needs to be created." - } - }, - "applicationSecurityGroupResourceIds": { - "type": "array", - "items": { - "type": "string" - }, - "nullable": true, - "metadata": { - "description": "Optional. Application security groups in which the private endpoint IP configuration is included." - } - }, - "customNetworkInterfaceName": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The custom name of the network interface attached to the private endpoint." - } - }, - "ipConfigurations": { - "type": "array", - "items": { - "$ref": "#/definitions/ipConfigurationType" - }, - "nullable": true, - "metadata": { - "description": "Optional. A list of IP configurations of the private endpoint. This will be used to map to the First Party Service endpoints." - } - }, - "privateDnsZoneGroup": { - "$ref": "#/definitions/privateDnsZoneGroupType", - "nullable": true, - "metadata": { - "description": "Optional. The private DNS zone group to configure for the private endpoint." - } - }, - "location": { - "type": "string", - "defaultValue": "[resourceGroup().location]", - "metadata": { - "description": "Optional. Location for all Resources." - } - }, - "lock": { - "$ref": "#/definitions/lockType", - "nullable": true, - "metadata": { - "description": "Optional. The lock settings of the service." - } - }, - "roleAssignments": { - "type": "array", - "items": { - "$ref": "#/definitions/roleAssignmentType" - }, - "nullable": true, - "metadata": { - "description": "Optional. Array of role assignments to create." - } - }, - "tags": { - "type": "object", - "nullable": true, - "metadata": { - "description": "Optional. Tags to be applied on all resources/resource groups in this deployment." - } - }, - "customDnsConfigs": { - "type": "array", - "items": { - "$ref": "#/definitions/customDnsConfigType" - }, - "nullable": true, - "metadata": { - "description": "Optional. Custom DNS configurations." - } - }, - "manualPrivateLinkServiceConnections": { - "type": "array", - "items": { - "$ref": "#/definitions/privateLinkServiceConnectionType" - }, - "nullable": true, - "metadata": { - "description": "Conditional. A grouping of information about the connection to the remote resource. Used when the network admin does not have access to approve connections to the remote resource. Required if `privateLinkServiceConnections` is empty." - } - }, - "privateLinkServiceConnections": { - "type": "array", - "items": { - "$ref": "#/definitions/privateLinkServiceConnectionType" - }, - "nullable": true, - "metadata": { - "description": "Conditional. A grouping of information about the connection to the remote resource. Required if `manualPrivateLinkServiceConnections` is empty." - } - }, - "enableTelemetry": { - "type": "bool", - "defaultValue": true, - "metadata": { - "description": "Optional. Enable/Disable usage telemetry for module." - } - } - }, - "variables": { - "copy": [ - { - "name": "formattedRoleAssignments", - "count": "[length(coalesce(parameters('roleAssignments'), createArray()))]", - "input": "[union(coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')], createObject('roleDefinitionId', coalesce(tryGet(variables('builtInRoleNames'), coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName), if(contains(coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName, '/providers/Microsoft.Authorization/roleDefinitions/'), coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName, subscriptionResourceId('Microsoft.Authorization/roleDefinitions', coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName)))))]" - } - ], - "builtInRoleNames": { - "Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'b24988ac-6180-42a0-ab88-20f7382dd24c')]", - "DNS Resolver Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '0f2ebee7-ffd4-4fc0-b3b7-664099fdad5d')]", - "DNS Zone Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'befefa01-2a29-4197-83a8-272ff33ce314')]", - "Domain Services Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'eeaeda52-9324-47f6-8069-5d5bade478b2')]", - "Domain Services Reader": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '361898ef-9ed1-48c2-849c-a832951106bb')]", - "Network Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '4d97b98b-1d4f-4787-a291-c67834d212e7')]", - "Owner": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '8e3af657-a8ff-443c-a75c-2fe8c4bcb635')]", - "Private DNS Zone Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'b12aa53e-6015-4669-85d0-8515ebb3ae7f')]", - "Reader": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'acdd72a7-3385-48ef-bd42-f606fba81ae7')]", - "Role Based Access Control Administrator": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'f58310d9-a9f6-439a-9e8d-f62e7b41a168')]" - } - }, - "resources": { - "avmTelemetry": { - "condition": "[parameters('enableTelemetry')]", - "type": "Microsoft.Resources/deployments", - "apiVersion": "2024-03-01", - "name": "[format('46d3xbcp.res.network-privateendpoint.{0}.{1}', replace('0.11.0', '.', '-'), substring(uniqueString(deployment().name, parameters('location')), 0, 4))]", - "properties": { - "mode": "Incremental", - "template": { - "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", - "contentVersion": "1.0.0.0", - "resources": [], - "outputs": { - "telemetry": { - "type": "String", - "value": "For more information, see https://aka.ms/avm/TelemetryInfo" - } - } - } - } - }, - "privateEndpoint": { - "type": "Microsoft.Network/privateEndpoints", - "apiVersion": "2024-05-01", - "name": "[parameters('name')]", - "location": "[parameters('location')]", - "tags": "[parameters('tags')]", - "properties": { - "copy": [ - { - "name": "applicationSecurityGroups", - "count": "[length(coalesce(parameters('applicationSecurityGroupResourceIds'), createArray()))]", - "input": { - "id": "[coalesce(parameters('applicationSecurityGroupResourceIds'), createArray())[copyIndex('applicationSecurityGroups')]]" - } - } - ], - "customDnsConfigs": "[coalesce(parameters('customDnsConfigs'), createArray())]", - "customNetworkInterfaceName": "[coalesce(parameters('customNetworkInterfaceName'), '')]", - "ipConfigurations": "[coalesce(parameters('ipConfigurations'), createArray())]", - "manualPrivateLinkServiceConnections": "[coalesce(parameters('manualPrivateLinkServiceConnections'), createArray())]", - "privateLinkServiceConnections": "[coalesce(parameters('privateLinkServiceConnections'), createArray())]", - "subnet": { - "id": "[parameters('subnetResourceId')]" - } - } - }, - "privateEndpoint_lock": { - "condition": "[and(not(empty(coalesce(parameters('lock'), createObject()))), not(equals(tryGet(parameters('lock'), 'kind'), 'None')))]", - "type": "Microsoft.Authorization/locks", - "apiVersion": "2020-05-01", - "scope": "[format('Microsoft.Network/privateEndpoints/{0}', parameters('name'))]", - "name": "[coalesce(tryGet(parameters('lock'), 'name'), format('lock-{0}', parameters('name')))]", - "properties": { - "level": "[coalesce(tryGet(parameters('lock'), 'kind'), '')]", - "notes": "[if(equals(tryGet(parameters('lock'), 'kind'), 'CanNotDelete'), 'Cannot delete resource or child resources.', 'Cannot delete or modify the resource or child resources.')]" - }, - "dependsOn": [ - "privateEndpoint" - ] - }, - "privateEndpoint_roleAssignments": { - "copy": { - "name": "privateEndpoint_roleAssignments", - "count": "[length(coalesce(variables('formattedRoleAssignments'), createArray()))]" - }, - "type": "Microsoft.Authorization/roleAssignments", - "apiVersion": "2022-04-01", - "scope": "[format('Microsoft.Network/privateEndpoints/{0}', parameters('name'))]", - "name": "[coalesce(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'name'), guid(resourceId('Microsoft.Network/privateEndpoints', parameters('name')), coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].principalId, coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].roleDefinitionId))]", - "properties": { - "roleDefinitionId": "[coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].roleDefinitionId]", - "principalId": "[coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].principalId]", - "description": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'description')]", - "principalType": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'principalType')]", - "condition": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'condition')]", - "conditionVersion": "[if(not(empty(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'condition'))), coalesce(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'conditionVersion'), '2.0'), null())]", - "delegatedManagedIdentityResourceId": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'delegatedManagedIdentityResourceId')]" - }, - "dependsOn": [ - "privateEndpoint" - ] - }, - "privateEndpoint_privateDnsZoneGroup": { - "condition": "[not(empty(parameters('privateDnsZoneGroup')))]", - "type": "Microsoft.Resources/deployments", - "apiVersion": "2022-09-01", - "name": "[format('{0}-PrivateEndpoint-PrivateDnsZoneGroup', uniqueString(deployment().name))]", - "properties": { - "expressionEvaluationOptions": { - "scope": "inner" - }, - "mode": "Incremental", - "parameters": { - "name": { - "value": "[tryGet(parameters('privateDnsZoneGroup'), 'name')]" - }, - "privateEndpointName": { - "value": "[parameters('name')]" - }, - "privateDnsZoneConfigs": { - "value": "[parameters('privateDnsZoneGroup').privateDnsZoneGroupConfigs]" - } - }, - "template": { - "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", - "languageVersion": "2.0", - "contentVersion": "1.0.0.0", - "metadata": { - "_generator": { - "name": "bicep", - "version": "0.34.44.8038", - "templateHash": "13997305779829540948" - }, - "name": "Private Endpoint Private DNS Zone Groups", - "description": "This module deploys a Private Endpoint Private DNS Zone Group." - }, - "definitions": { - "privateDnsZoneGroupConfigType": { - "type": "object", - "properties": { - "name": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The name of the private DNS zone group config." - } - }, - "privateDnsZoneResourceId": { - "type": "string", - "metadata": { - "description": "Required. The resource id of the private DNS zone." - } - } - }, - "metadata": { - "__bicep_export!": true - } - } - }, - "parameters": { - "privateEndpointName": { - "type": "string", - "metadata": { - "description": "Conditional. The name of the parent private endpoint. Required if the template is used in a standalone deployment." - } - }, - "privateDnsZoneConfigs": { - "type": "array", - "items": { - "$ref": "#/definitions/privateDnsZoneGroupConfigType" - }, - "minLength": 1, - "maxLength": 5, - "metadata": { - "description": "Required. Array of private DNS zone configurations of the private DNS zone group. A DNS zone group can support up to 5 DNS zones." - } - }, - "name": { - "type": "string", - "defaultValue": "default", - "metadata": { - "description": "Optional. The name of the private DNS zone group." - } - } - }, - "variables": { - "copy": [ - { - "name": "privateDnsZoneConfigsVar", - "count": "[length(parameters('privateDnsZoneConfigs'))]", - "input": { - "name": "[coalesce(tryGet(parameters('privateDnsZoneConfigs')[copyIndex('privateDnsZoneConfigsVar')], 'name'), last(split(parameters('privateDnsZoneConfigs')[copyIndex('privateDnsZoneConfigsVar')].privateDnsZoneResourceId, '/')))]", - "properties": { - "privateDnsZoneId": "[parameters('privateDnsZoneConfigs')[copyIndex('privateDnsZoneConfigsVar')].privateDnsZoneResourceId]" - } - } - } - ] - }, - "resources": { - "privateEndpoint": { - "existing": true, - "type": "Microsoft.Network/privateEndpoints", - "apiVersion": "2024-05-01", - "name": "[parameters('privateEndpointName')]" - }, - "privateDnsZoneGroup": { - "type": "Microsoft.Network/privateEndpoints/privateDnsZoneGroups", - "apiVersion": "2024-05-01", - "name": "[format('{0}/{1}', parameters('privateEndpointName'), parameters('name'))]", - "properties": { - "privateDnsZoneConfigs": "[variables('privateDnsZoneConfigsVar')]" - } - } - }, - "outputs": { - "name": { - "type": "string", - "metadata": { - "description": "The name of the private endpoint DNS zone group." - }, - "value": "[parameters('name')]" - }, - "resourceId": { - "type": "string", - "metadata": { - "description": "The resource ID of the private endpoint DNS zone group." - }, - "value": "[resourceId('Microsoft.Network/privateEndpoints/privateDnsZoneGroups', parameters('privateEndpointName'), parameters('name'))]" - }, - "resourceGroupName": { - "type": "string", - "metadata": { - "description": "The resource group the private endpoint DNS zone group was deployed into." - }, - "value": "[resourceGroup().name]" - } - } - } - }, - "dependsOn": [ - "privateEndpoint" - ] - } - }, - "outputs": { - "resourceGroupName": { - "type": "string", - "metadata": { - "description": "The resource group the private endpoint was deployed into." - }, - "value": "[resourceGroup().name]" - }, - "resourceId": { - "type": "string", - "metadata": { - "description": "The resource ID of the private endpoint." - }, - "value": "[resourceId('Microsoft.Network/privateEndpoints', parameters('name'))]" - }, - "name": { - "type": "string", - "metadata": { - "description": "The name of the private endpoint." - }, - "value": "[parameters('name')]" - }, - "location": { - "type": "string", - "metadata": { - "description": "The location the resource was deployed into." - }, - "value": "[reference('privateEndpoint', '2024-05-01', 'full').location]" - }, - "customDnsConfigs": { - "type": "array", - "items": { - "$ref": "#/definitions/customDnsConfigType" - }, - "metadata": { - "description": "The custom DNS configurations of the private endpoint." - }, - "value": "[reference('privateEndpoint').customDnsConfigs]" - }, - "networkInterfaceResourceIds": { - "type": "array", - "items": { - "type": "string" - }, - "metadata": { - "description": "The resource IDs of the network interfaces associated with the private endpoint." - }, - "value": "[map(reference('privateEndpoint').networkInterfaces, lambda('nic', lambdaVariables('nic').id))]" - }, - "groupId": { - "type": "string", - "nullable": true, - "metadata": { - "description": "The group Id for the private endpoint Group." - }, - "value": "[coalesce(tryGet(tryGet(tryGet(tryGet(reference('privateEndpoint'), 'manualPrivateLinkServiceConnections'), 0, 'properties'), 'groupIds'), 0), tryGet(tryGet(tryGet(tryGet(reference('privateEndpoint'), 'privateLinkServiceConnections'), 0, 'properties'), 'groupIds'), 0))]" - } - } - } - }, - "dependsOn": [ - "searchService" - ] - }, - "searchService_sharedPrivateLinkResources": { - "copy": { - "name": "searchService_sharedPrivateLinkResources", - "count": "[length(parameters('sharedPrivateLinkResources'))]", - "mode": "serial", - "batchSize": 1 - }, - "type": "Microsoft.Resources/deployments", - "apiVersion": "2022-09-01", - "name": "[format('{0}-searchService-SharedPrvLink-{1}', uniqueString(deployment().name, parameters('location')), copyIndex())]", - "properties": { - "expressionEvaluationOptions": { - "scope": "inner" - }, - "mode": "Incremental", - "parameters": { - "name": { - "value": "[coalesce(tryGet(parameters('sharedPrivateLinkResources')[copyIndex()], 'name'), format('spl-{0}-{1}-{2}', last(split(resourceId('Microsoft.Search/searchServices', parameters('name')), '/')), parameters('sharedPrivateLinkResources')[copyIndex()].groupId, copyIndex()))]" - }, - "searchServiceName": { - "value": "[parameters('name')]" - }, - "privateLinkResourceId": { - "value": "[parameters('sharedPrivateLinkResources')[copyIndex()].privateLinkResourceId]" - }, - "groupId": { - "value": "[parameters('sharedPrivateLinkResources')[copyIndex()].groupId]" - }, - "requestMessage": { - "value": "[parameters('sharedPrivateLinkResources')[copyIndex()].requestMessage]" - }, - "resourceRegion": { - "value": "[tryGet(parameters('sharedPrivateLinkResources')[copyIndex()], 'resourceRegion')]" - } - }, - "template": { - "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", - "languageVersion": "2.0", - "contentVersion": "1.0.0.0", - "metadata": { - "_generator": { - "name": "bicep", - "version": "0.37.4.10188", - "templateHash": "557730297583881254" - }, - "name": "Search Services Private Link Resources", - "description": "This module deploys a Search Service Private Link Resource." - }, - "parameters": { - "searchServiceName": { - "type": "string", - "metadata": { - "description": "Conditional. The name of the parent searchServices. Required if the template is used in a standalone deployment." - } - }, - "name": { - "type": "string", - "metadata": { - "description": "Required. The name of the shared private link resource managed by the Azure Cognitive Search service within the specified resource group." - } - }, - "privateLinkResourceId": { - "type": "string", - "metadata": { - "description": "Required. The resource ID of the resource the shared private link resource is for." - } - }, - "groupId": { - "type": "string", - "metadata": { - "description": "Required. The group ID from the provider of resource the shared private link resource is for." - } - }, - "requestMessage": { - "type": "string", - "metadata": { - "description": "Required. The request message for requesting approval of the shared private link resource." - } - }, - "resourceRegion": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. Can be used to specify the Azure Resource Manager location of the resource to which a shared private link is to be created. This is only required for those resources whose DNS configuration are regional (such as Azure Kubernetes Service)." - } - } - }, - "resources": { - "searchService": { - "existing": true, - "type": "Microsoft.Search/searchServices", - "apiVersion": "2025-02-01-preview", - "name": "[parameters('searchServiceName')]" - }, - "sharedPrivateLinkResource": { - "type": "Microsoft.Search/searchServices/sharedPrivateLinkResources", - "apiVersion": "2025-02-01-preview", - "name": "[format('{0}/{1}', parameters('searchServiceName'), parameters('name'))]", - "properties": { - "privateLinkResourceId": "[parameters('privateLinkResourceId')]", - "groupId": "[parameters('groupId')]", - "requestMessage": "[parameters('requestMessage')]", - "resourceRegion": "[parameters('resourceRegion')]" - } - } - }, - "outputs": { - "name": { - "type": "string", - "metadata": { - "description": "The name of the shared private link resource." - }, - "value": "[parameters('name')]" - }, - "resourceId": { - "type": "string", - "metadata": { - "description": "The resource ID of the shared private link resource." - }, - "value": "[resourceId('Microsoft.Search/searchServices/sharedPrivateLinkResources', parameters('searchServiceName'), parameters('name'))]" - }, - "resourceGroupName": { - "type": "string", - "metadata": { - "description": "The name of the resource group the shared private link resource was created in." - }, - "value": "[resourceGroup().name]" - } - } - } - }, - "dependsOn": [ - "searchService" - ] - }, - "secretsExport": { - "condition": "[not(equals(parameters('secretsExportConfiguration'), null()))]", - "type": "Microsoft.Resources/deployments", - "apiVersion": "2022-09-01", - "name": "[format('{0}-secrets-kv', uniqueString(deployment().name, parameters('location')))]", - "subscriptionId": "[split(tryGet(parameters('secretsExportConfiguration'), 'keyVaultResourceId'), '/')[2]]", - "resourceGroup": "[split(tryGet(parameters('secretsExportConfiguration'), 'keyVaultResourceId'), '/')[4]]", - "properties": { - "expressionEvaluationOptions": { - "scope": "inner" - }, - "mode": "Incremental", - "parameters": { - "keyVaultName": { - "value": "[last(split(tryGet(parameters('secretsExportConfiguration'), 'keyVaultResourceId'), '/'))]" - }, - "secretsToSet": { - "value": "[union(createArray(), if(contains(parameters('secretsExportConfiguration'), 'primaryAdminKeyName'), createArray(createObject('name', tryGet(parameters('secretsExportConfiguration'), 'primaryAdminKeyName'), 'value', listAdminKeys('searchService', '2025-02-01-preview').primaryKey)), createArray()), if(contains(parameters('secretsExportConfiguration'), 'secondaryAdminKeyName'), createArray(createObject('name', tryGet(parameters('secretsExportConfiguration'), 'secondaryAdminKeyName'), 'value', listAdminKeys('searchService', '2025-02-01-preview').secondaryKey)), createArray()))]" - } - }, - "template": { - "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", - "languageVersion": "2.0", - "contentVersion": "1.0.0.0", - "metadata": { - "_generator": { - "name": "bicep", - "version": "0.37.4.10188", - "templateHash": "7634110751636246703" - } - }, - "definitions": { - "secretSetType": { - "type": "object", - "properties": { - "secretResourceId": { - "type": "string", - "metadata": { - "description": "The resourceId of the exported secret." - } - }, - "secretUri": { - "type": "string", - "metadata": { - "description": "The secret URI of the exported secret." - } - } - }, - "metadata": { - "__bicep_export!": true - } - }, - "secretToSetType": { - "type": "object", - "properties": { - "name": { - "type": "string", - "metadata": { - "description": "Required. The name of the secret to set." - } - }, - "value": { - "type": "securestring", - "metadata": { - "description": "Required. The value of the secret to set." - } - } - } - } - }, - "parameters": { - "keyVaultName": { - "type": "string", - "metadata": { - "description": "Required. The name of the Key Vault to set the ecrets in." - } - }, - "secretsToSet": { - "type": "array", - "items": { - "$ref": "#/definitions/secretToSetType" - }, - "metadata": { - "description": "Required. The secrets to set in the Key Vault." - } - } - }, - "resources": { - "keyVault": { - "existing": true, - "type": "Microsoft.KeyVault/vaults", - "apiVersion": "2024-11-01", - "name": "[parameters('keyVaultName')]" - }, - "secrets": { - "copy": { - "name": "secrets", - "count": "[length(parameters('secretsToSet'))]" - }, - "type": "Microsoft.KeyVault/vaults/secrets", - "apiVersion": "2024-11-01", - "name": "[format('{0}/{1}', parameters('keyVaultName'), parameters('secretsToSet')[copyIndex()].name)]", - "properties": { - "value": "[parameters('secretsToSet')[copyIndex()].value]" - } - } - }, - "outputs": { - "secretsSet": { - "type": "array", - "items": { - "$ref": "#/definitions/secretSetType" - }, - "metadata": { - "description": "The references to the secrets exported to the provided Key Vault." - }, - "copy": { - "count": "[length(range(0, length(coalesce(parameters('secretsToSet'), createArray()))))]", - "input": { - "secretResourceId": "[resourceId('Microsoft.KeyVault/vaults/secrets', parameters('keyVaultName'), parameters('secretsToSet')[range(0, length(coalesce(parameters('secretsToSet'), createArray())))[copyIndex()]].name)]", - "secretUri": "[reference(format('secrets[{0}]', range(0, length(coalesce(parameters('secretsToSet'), createArray())))[copyIndex()])).secretUri]" - } - } - } - } - } - }, - "dependsOn": [ - "searchService" - ] - } - }, - "outputs": { - "name": { - "type": "string", - "metadata": { - "description": "The name of the search service." - }, - "value": "[parameters('name')]" - }, - "resourceId": { - "type": "string", - "metadata": { - "description": "The resource ID of the search service." - }, - "value": "[resourceId('Microsoft.Search/searchServices', parameters('name'))]" - }, - "resourceGroupName": { - "type": "string", - "metadata": { - "description": "The name of the resource group the search service was created in." - }, - "value": "[resourceGroup().name]" - }, - "systemAssignedMIPrincipalId": { - "type": "string", - "nullable": true, - "metadata": { - "description": "The principal ID of the system assigned identity." - }, - "value": "[tryGet(tryGet(reference('searchService', '2025-02-01-preview', 'full'), 'identity'), 'principalId')]" - }, - "location": { - "type": "string", - "metadata": { - "description": "The location the resource was deployed into." - }, - "value": "[reference('searchService', '2025-02-01-preview', 'full').location]" - }, - "endpoint": { - "type": "string", - "metadata": { - "description": "The endpoint of the search service." - }, - "value": "[reference('searchService').endpoint]" - }, - "privateEndpoints": { - "type": "array", - "items": { - "$ref": "#/definitions/privateEndpointOutputType" - }, - "metadata": { - "description": "The private endpoints of the search service." - }, - "copy": { - "count": "[length(coalesce(parameters('privateEndpoints'), createArray()))]", - "input": { - "name": "[reference(format('searchService_privateEndpoints[{0}]', copyIndex())).outputs.name.value]", - "resourceId": "[reference(format('searchService_privateEndpoints[{0}]', copyIndex())).outputs.resourceId.value]", - "groupId": "[tryGet(tryGet(reference(format('searchService_privateEndpoints[{0}]', copyIndex())).outputs, 'groupId'), 'value')]", - "customDnsConfigs": "[reference(format('searchService_privateEndpoints[{0}]', copyIndex())).outputs.customDnsConfigs.value]", - "networkInterfaceResourceIds": "[reference(format('searchService_privateEndpoints[{0}]', copyIndex())).outputs.networkInterfaceResourceIds.value]" - } - } - }, - "exportedSecrets": { - "$ref": "#/definitions/secretsOutputType", - "metadata": { - "description": "A hashtable of references to the secrets exported to the provided Key Vault. The key of each reference is each secret's name." - }, - "value": "[if(not(equals(parameters('secretsExportConfiguration'), null())), toObject(reference('secretsExport').outputs.secretsSet.value, lambda('secret', last(split(lambdaVariables('secret').secretResourceId, '/'))), lambda('secret', lambdaVariables('secret'))), createObject())]" - }, - "primaryKey": { - "type": "securestring", - "metadata": { - "description": "The primary admin API key of the search service." - }, - "value": "[listAdminKeys('searchService', '2025-02-01-preview').primaryKey]" - }, - "secondaryKey": { - "type": "securestring", - "metadata": { - "description": "The secondaryKey admin API key of the search service." - }, - "value": "[listAdminKeys('searchService', '2025-02-01-preview').secondaryKey]" - } - } - } - }, - "dependsOn": [ - "logAnalyticsWorkspace", - "userAssignedIdentity" - ] - }, - "storageAccount": { - "type": "Microsoft.Resources/deployments", - "apiVersion": "2025-04-01", - "name": "[take(format('avm.res.storage.storage-account.{0}', variables('storageAccountName')), 64)]", - "properties": { - "expressionEvaluationOptions": { - "scope": "inner" - }, - "mode": "Incremental", - "parameters": { - "name": { - "value": "[variables('storageAccountName')]" - }, - "location": { - "value": "[variables('solutionLocation')]" - }, - "skuName": "[if(parameters('enableRedundancy'), createObject('value', 'Standard_ZRS'), createObject('value', 'Standard_LRS'))]", - "managedIdentities": { - "value": { - "systemAssigned": true - } - }, - "minimumTlsVersion": { - "value": "TLS1_2" - }, - "enableTelemetry": { - "value": "[parameters('enableTelemetry')]" - }, - "tags": { - "value": "[parameters('tags')]" - }, - "accessTier": { - "value": "Hot" - }, - "supportsHttpsTrafficOnly": { - "value": true - }, - "blobServices": { - "value": { - "containerDeleteRetentionPolicyEnabled": true, - "containerDeleteRetentionPolicyDays": 7, - "deleteRetentionPolicyEnabled": true, - "deleteRetentionPolicyDays": 7, - "containers": [ - { - "name": "[variables('productImagesContainer')]", - "publicAccess": "None" - }, - { - "name": "[variables('generatedImagesContainer')]", - "publicAccess": "None" - }, - { - "name": "[variables('dataContainer')]", - "publicAccess": "None" - } - ] - } - }, - "roleAssignments": { - "value": [ - { - "principalId": "[reference('userAssignedIdentity').outputs.principalId.value]", - "roleDefinitionIdOrName": "Storage Blob Data Contributor", - "principalType": "ServicePrincipal" - } - ] - }, - "networkAcls": { - "value": { - "bypass": "AzureServices", - "defaultAction": "[if(parameters('enablePrivateNetworking'), 'Deny', 'Allow')]" - } - }, - "allowBlobPublicAccess": { - "value": false - }, - "publicNetworkAccess": "[if(parameters('enablePrivateNetworking'), createObject('value', 'Disabled'), createObject('value', 'Enabled'))]", - "privateEndpoints": "[if(parameters('enablePrivateNetworking'), createObject('value', createArray(createObject('service', 'blob', 'subnetResourceId', reference('virtualNetwork').outputs.pepsSubnetResourceId.value, 'privateDnsZoneGroup', createObject('privateDnsZoneGroupConfigs', createArray(createObject('privateDnsZoneResourceId', reference(format('avmPrivateDnsZones[{0}]', variables('dnsZoneIndex').storageBlob)).outputs.resourceId.value)))))), createObject('value', null()))]", - "diagnosticSettings": "[if(parameters('enableMonitoring'), createObject('value', createArray(createObject('workspaceResourceId', if(variables('useExistingLogAnalytics'), parameters('existingLogAnalyticsWorkspaceId'), if(parameters('enableMonitoring'), reference('logAnalyticsWorkspace').outputs.resourceId.value, ''))))), createObject('value', null()))]" - }, - "template": { - "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", - "languageVersion": "2.0", - "contentVersion": "1.0.0.0", - "metadata": { - "_generator": { - "name": "bicep", - "version": "0.39.26.7824", - "templateHash": "8444048237705693390" - }, - "name": "Storage Accounts", - "description": "This module deploys a Storage Account." - }, - "definitions": { - "privateEndpointOutputType": { - "type": "object", - "properties": { - "name": { - "type": "string", - "metadata": { - "description": "The name of the private endpoint." - } - }, - "resourceId": { - "type": "string", - "metadata": { - "description": "The resource ID of the private endpoint." - } - }, - "groupId": { - "type": "string", - "nullable": true, - "metadata": { - "description": "The group Id for the private endpoint Group." - } - }, - "customDnsConfigs": { - "type": "array", - "items": { - "type": "object", - "properties": { - "fqdn": { - "type": "string", - "nullable": true, - "metadata": { - "description": "FQDN that resolves to private endpoint IP address." - } - }, - "ipAddresses": { - "type": "array", - "items": { - "type": "string" - }, - "metadata": { - "description": "A list of private IP addresses of the private endpoint." - } - } - } - }, - "metadata": { - "description": "The custom DNS configurations of the private endpoint." - } - }, - "networkInterfaceResourceIds": { - "type": "array", - "items": { - "type": "string" - }, - "metadata": { - "description": "The IDs of the network interfaces associated with the private endpoint." - } - } - }, - "metadata": { - "__bicep_export!": true, - "description": "The type for the private endpoints output." - } - }, - "networkAclsType": { - "type": "object", - "properties": { - "resourceAccessRules": { - "type": "array", - "items": { - "type": "object", - "properties": { - "tenantId": { - "type": "string", - "metadata": { - "description": "Required. The ID of the tenant in which the resource resides in." - } - }, - "resourceId": { - "type": "string", - "metadata": { - "description": "Required. The resource ID of the target service. Can also contain a wildcard, if multiple services e.g. in a resource group should be included." - } - } - } - }, - "nullable": true, - "metadata": { - "description": "Optional. Sets the resource access rules. Array entries must consist of \"tenantId\" and \"resourceId\" fields only." - } - }, - "bypass": { - "type": "string", - "allowedValues": [ - "AzureServices", - "AzureServices, Logging", - "AzureServices, Logging, Metrics", - "AzureServices, Metrics", - "Logging", - "Logging, Metrics", - "Metrics", - "None" - ], - "nullable": true, - "metadata": { - "description": "Optional. Specifies whether traffic is bypassed for Logging/Metrics/AzureServices. Possible values are any combination of Logging,Metrics,AzureServices (For example, \"Logging, Metrics\"), or None to bypass none of those traffics." - } - }, - "virtualNetworkRules": { - "type": "array", - "nullable": true, - "metadata": { - "description": "Optional. Sets the virtual network rules." - } - }, - "ipRules": { - "type": "array", - "nullable": true, - "metadata": { - "description": "Optional. Sets the IP ACL rules." - } - }, - "defaultAction": { - "type": "string", - "allowedValues": [ - "Allow", - "Deny" - ], - "nullable": true, - "metadata": { - "description": "Optional. Specifies the default action of allow or deny when no other rules match." - } - } - }, - "metadata": { - "__bicep_export!": true, - "description": "The type for the network configuration." - } - }, - "secretsExportConfigurationType": { - "type": "object", - "properties": { - "keyVaultResourceId": { - "type": "string", - "metadata": { - "description": "Required. The key vault name where to store the keys and connection strings generated by the modules." - } - }, - "accessKey1Name": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The accessKey1 secret name to create." - } - }, - "connectionString1Name": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The connectionString1 secret name to create." - } - }, - "accessKey2Name": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The accessKey2 secret name to create." - } - }, - "connectionString2Name": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The connectionString2 secret name to create." - } - } - }, - "metadata": { - "__bicep_export!": true, - "description": "The type of the exported secrets." - } - }, - "localUserType": { - "type": "object", - "properties": { - "name": { - "type": "string", - "metadata": { - "description": "Required. The name of the local user used for SFTP Authentication." - } - }, - "hasSharedKey": { - "type": "bool", - "nullable": true, - "metadata": { - "description": "Optional. Indicates whether shared key exists. Set it to false to remove existing shared key." - } - }, - "hasSshKey": { - "type": "bool", - "metadata": { - "description": "Required. Indicates whether SSH key exists. Set it to false to remove existing SSH key." - } - }, - "hasSshPassword": { - "type": "bool", - "metadata": { - "description": "Required. Indicates whether SSH password exists. Set it to false to remove existing SSH password." - } - }, - "homeDirectory": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The local user home directory." - } - }, - "permissionScopes": { - "type": "array", - "items": { - "$ref": "#/definitions/permissionScopeType" - }, - "metadata": { - "description": "Required. The permission scopes of the local user." - } - }, - "sshAuthorizedKeys": { - "type": "array", - "items": { - "$ref": "#/definitions/sshAuthorizedKeyType" - }, - "nullable": true, - "metadata": { - "description": "Optional. The local user SSH authorized keys for SFTP." - } - } - }, - "metadata": { - "__bicep_export!": true, - "description": "The type of a local user." - } - }, - "blobServiceType": { - "type": "object", - "properties": { - "automaticSnapshotPolicyEnabled": { - "type": "bool", - "nullable": true, - "metadata": { - "description": "Optional. Automatic Snapshot is enabled if set to true." - } - }, - "changeFeedEnabled": { - "type": "bool", - "nullable": true, - "metadata": { - "description": "Optional. The blob service properties for change feed events. Indicates whether change feed event logging is enabled for the Blob service." - } - }, - "changeFeedRetentionInDays": { - "type": "int", - "nullable": true, - "minValue": 1, - "maxValue": 146000, - "metadata": { - "description": "Optional. Indicates whether change feed event logging is enabled for the Blob service. Indicates the duration of changeFeed retention in days. If left blank, it indicates an infinite retention of the change feed." - } - }, - "containerDeleteRetentionPolicyEnabled": { - "type": "bool", - "nullable": true, - "metadata": { - "description": "Optional. The blob service properties for container soft delete. Indicates whether DeleteRetentionPolicy is enabled." - } - }, - "containerDeleteRetentionPolicyDays": { - "type": "int", - "nullable": true, - "minValue": 1, - "maxValue": 365, - "metadata": { - "description": "Optional. Indicates the number of days that the deleted item should be retained." - } - }, - "containerDeleteRetentionPolicyAllowPermanentDelete": { - "type": "bool", - "nullable": true, - "metadata": { - "description": "Optional. This property when set to true allows deletion of the soft deleted blob versions and snapshots. This property cannot be used with blob restore policy. This property only applies to blob service and does not apply to containers or file share." - } - }, - "corsRules": { - "type": "array", - "items": { - "$ref": "#/definitions/blobCorsRuleType" - }, - "nullable": true, - "metadata": { - "description": "Optional. The List of CORS rules. You can include up to five CorsRule elements in the request." - } - }, - "defaultServiceVersion": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. Indicates the default version to use for requests to the Blob service if an incoming request's version is not specified. Possible values include version 2008-10-27 and all more recent versions." - } - }, - "deleteRetentionPolicyEnabled": { - "type": "bool", - "nullable": true, - "metadata": { - "description": "Optional. The blob service properties for blob soft delete." - } - }, - "deleteRetentionPolicyDays": { - "type": "int", - "nullable": true, - "minValue": 1, - "maxValue": 365, - "metadata": { - "description": "Optional. Indicates the number of days that the deleted blob should be retained." - } - }, - "deleteRetentionPolicyAllowPermanentDelete": { - "type": "bool", - "nullable": true, - "metadata": { - "description": "Optional. This property when set to true allows deletion of the soft deleted blob versions and snapshots. This property cannot be used with blob restore policy. This property only applies to blob service and does not apply to containers or file share." - } - }, - "isVersioningEnabled": { - "type": "bool", - "nullable": true, - "metadata": { - "description": "Optional. Use versioning to automatically maintain previous versions of your blobs. Cannot be enabled for ADLS Gen2 storage accounts." - } - }, - "versionDeletePolicyDays": { - "type": "int", - "nullable": true, - "metadata": { - "description": "Optional. Number of days to keep a version before deleting. If set, a lifecycle management policy will be created to handle deleting previous versions." - } - }, - "lastAccessTimeTrackingPolicyEnabled": { - "type": "bool", - "nullable": true, - "metadata": { - "description": "Optional. The blob service property to configure last access time based tracking policy. When set to true last access time based tracking is enabled." - } - }, - "restorePolicyEnabled": { - "type": "bool", - "nullable": true, - "metadata": { - "description": "Optional. The blob service properties for blob restore policy. If point-in-time restore is enabled, then versioning, change feed, and blob soft delete must also be enabled." - } - }, - "restorePolicyDays": { - "type": "int", - "nullable": true, - "minValue": 1, - "metadata": { - "description": "Optional. How long this blob can be restored. It should be less than DeleteRetentionPolicy days." - } - }, - "containers": { - "type": "array", - "items": { - "$ref": "#/definitions/containerType" - }, - "nullable": true, - "metadata": { - "description": "Optional. Blob containers to create." - } - }, - "diagnosticSettings": { - "type": "array", - "items": { - "$ref": "#/definitions/diagnosticSettingFullType" - }, - "nullable": true, - "metadata": { - "description": "Optional. The diagnostic settings of the service." - } - } - }, - "metadata": { - "__bicep_export!": true, - "description": "The type of a blob service." - } - }, - "fileServiceType": { - "type": "object", - "properties": { - "protocolSettings": { - "type": "object", - "metadata": { - "__bicep_resource_derived_type!": { - "source": "Microsoft.Storage/storageAccounts/fileServices@2024-01-01#properties/properties/properties/protocolSettings" - }, - "description": "Optional. Protocol settings for file service." - }, - "nullable": true - }, - "shareDeleteRetentionPolicy": { - "type": "object", - "metadata": { - "__bicep_resource_derived_type!": { - "source": "Microsoft.Storage/storageAccounts/fileServices@2024-01-01#properties/properties/properties/shareDeleteRetentionPolicy" - }, - "description": "Optional. The service properties for soft delete." - }, - "nullable": true - }, - "shares": { - "type": "array", - "items": { - "$ref": "#/definitions/fileShareType" - }, - "nullable": true, - "metadata": { - "description": "Optional. File shares to create." - } - }, - "corsRules": { - "type": "array", - "items": { - "$ref": "#/definitions/fileCorsRuleType" - }, - "nullable": true, - "metadata": { - "description": "Optional. The List of CORS rules. You can include up to five CorsRule elements in the request." - } - }, - "diagnosticSettings": { - "type": "array", - "items": { - "$ref": "#/definitions/diagnosticSettingFullType" - }, - "nullable": true, - "metadata": { - "description": "Optional. The diagnostic settings of the service." - } - } - }, - "metadata": { - "__bicep_export!": true, - "description": "The type of a file service." - } - }, - "queueServiceType": { - "type": "object", - "properties": { - "queues": { - "type": "array", - "items": { - "$ref": "#/definitions/queueType" - }, - "nullable": true, - "metadata": { - "description": "Optional. Queues to create." - } - }, - "corsRules": { - "type": "array", - "items": { - "$ref": "#/definitions/queueCorsRuleType" - }, - "nullable": true, - "metadata": { - "description": "Optional. The List of CORS rules. You can include up to five CorsRule elements in the request." - } - }, - "diagnosticSettings": { - "type": "array", - "items": { - "$ref": "#/definitions/diagnosticSettingFullType" - }, - "nullable": true, - "metadata": { - "description": "Optional. The diagnostic settings of the service." - } - } - }, - "metadata": { - "__bicep_export!": true, - "description": "The type of a queue service." - } - }, - "tableServiceType": { - "type": "object", - "properties": { - "tables": { - "type": "array", - "items": { - "$ref": "#/definitions/tableType" - }, - "nullable": true, - "metadata": { - "description": "Optional. Tables to create." - } - }, - "corsRules": { - "type": "array", - "items": { - "$ref": "#/definitions/tableCorsRuleType" - }, - "nullable": true, - "metadata": { - "description": "Optional. The List of CORS rules. You can include up to five CorsRule elements in the request." - } - }, - "diagnosticSettings": { - "type": "array", - "items": { - "$ref": "#/definitions/diagnosticSettingFullType" - }, - "nullable": true, - "metadata": { - "description": "Optional. The diagnostic settings of the service." - } - } - }, - "metadata": { - "__bicep_export!": true, - "description": "The type of a table service." - } - }, - "objectReplicationPolicyType": { - "type": "object", - "properties": { - "name": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The name of the object replication policy. If not provided, a GUID will be generated." - } - }, - "destinationStorageAccountResourceId": { - "type": "string", - "metadata": { - "description": "Required. The resource ID of the destination storage account." - } - }, - "enableMetrics": { - "type": "bool", - "nullable": true, - "metadata": { - "description": "Optional. Indicates whether metrics are enabled for the object replication policy." - } - }, - "rules": { - "type": "array", - "items": { - "$ref": "#/definitions/objectReplicationPolicyRuleType" - }, - "metadata": { - "description": "Required. The storage account object replication rules." - } - } - }, - "metadata": { - "__bicep_export!": true, - "description": "The type of an object replication policy." - } - }, - "_1.immutabilityPolicyType": { - "type": "object", - "properties": { - "immutabilityPeriodSinceCreationInDays": { - "type": "int", - "nullable": true, - "metadata": { - "description": "Optional. The immutability period for the blobs in the container since the policy creation, in days." - } - }, - "allowProtectedAppendWrites": { - "type": "bool", - "nullable": true, - "metadata": { - "description": "Optional. This property can only be changed for unlocked time-based retention policies. When enabled, new blocks can be written to an append blob while maintaining immutability protection and compliance. Only new blocks can be added and any existing blocks cannot be modified or deleted. This property cannot be changed with ExtendImmutabilityPolicy API." - } - }, - "allowProtectedAppendWritesAll": { - "type": "bool", - "nullable": true, - "metadata": { - "description": "Optional. This property can only be changed for unlocked time-based retention policies. When enabled, new blocks can be written to both \"Append and Block Blobs\" while maintaining immutability protection and compliance. Only new blocks can be added and any existing blocks cannot be modified or deleted. This property cannot be changed with ExtendImmutabilityPolicy API. The \"allowProtectedAppendWrites\" and \"allowProtectedAppendWritesAll\" properties are mutually exclusive." - } - } - }, - "metadata": { - "description": "The type for an immutability policy.", - "__bicep_imported_from!": { - "sourceTemplate": "blob-service/container/main.bicep" - } - } - }, - "_2.privateEndpointCustomDnsConfigType": { - "type": "object", - "properties": { - "fqdn": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. FQDN that resolves to private endpoint IP address." - } - }, - "ipAddresses": { - "type": "array", - "items": { - "type": "string" - }, - "metadata": { - "description": "Required. A list of private IP addresses of the private endpoint." - } - } - }, - "metadata": { - "__bicep_imported_from!": { - "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.1" - } - } - }, - "_2.privateEndpointIpConfigurationType": { - "type": "object", - "properties": { - "name": { - "type": "string", - "metadata": { - "description": "Required. The name of the resource that is unique within a resource group." - } - }, - "properties": { - "type": "object", - "properties": { - "groupId": { - "type": "string", - "metadata": { - "description": "Required. The ID of a group obtained from the remote resource that this private endpoint should connect to." - } - }, - "memberName": { - "type": "string", - "metadata": { - "description": "Required. The member name of a group obtained from the remote resource that this private endpoint should connect to." - } - }, - "privateIPAddress": { - "type": "string", - "metadata": { - "description": "Required. A private IP address obtained from the private endpoint's subnet." - } - } - }, - "metadata": { - "description": "Required. Properties of private endpoint IP configurations." - } - } - }, - "metadata": { - "__bicep_imported_from!": { - "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.1" - } - } - }, - "_2.privateEndpointPrivateDnsZoneGroupType": { - "type": "object", - "properties": { - "name": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The name of the Private DNS Zone Group." - } - }, - "privateDnsZoneGroupConfigs": { - "type": "array", - "items": { - "type": "object", - "properties": { - "name": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The name of the private DNS Zone Group config." - } - }, - "privateDnsZoneResourceId": { - "type": "string", - "metadata": { - "description": "Required. The resource id of the private DNS zone." - } - } - } - }, - "metadata": { - "description": "Required. The private DNS Zone Groups to associate the Private Endpoint. A DNS Zone Group can support up to 5 DNS zones." - } - } - }, - "metadata": { - "__bicep_imported_from!": { - "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.1" - } - } - }, - "_2.secretSetOutputType": { - "type": "object", - "properties": { - "secretResourceId": { - "type": "string", - "metadata": { - "description": "The resourceId of the exported secret." - } - }, - "secretUri": { - "type": "string", - "metadata": { - "description": "The secret URI of the exported secret." - } - }, - "secretUriWithVersion": { - "type": "string", - "metadata": { - "description": "The secret URI with version of the exported secret." - } - } - }, - "metadata": { - "description": "An AVM-aligned type for the output of the secret set via the secrets export feature.", - "__bicep_imported_from!": { - "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.1" - } - } - }, - "blobCorsRuleType": { - "type": "object", - "properties": { - "allowedHeaders": { - "type": "array", - "items": { - "type": "string" - }, - "metadata": { - "description": "Required. A list of headers allowed to be part of the cross-origin request." - } - }, - "allowedMethods": { - "type": "array", - "allowedValues": [ - "CONNECT", - "DELETE", - "GET", - "HEAD", - "MERGE", - "OPTIONS", - "PATCH", - "POST", - "PUT", - "TRACE" - ], - "metadata": { - "description": "Required. A list of HTTP methods that are allowed to be executed by the origin." - } - }, - "allowedOrigins": { - "type": "array", - "items": { - "type": "string" - }, - "metadata": { - "description": "Required. A list of origin domains that will be allowed via CORS, or \"*\" to allow all domains." - } - }, - "exposedHeaders": { - "type": "array", - "items": { - "type": "string" - }, - "metadata": { - "description": "Required. A list of response headers to expose to CORS clients." - } - }, - "maxAgeInSeconds": { - "type": "int", - "metadata": { - "description": "Required. The number of seconds that the client/browser should cache a preflight response." - } - } - }, - "metadata": { - "description": "The type for a cors rule.", - "__bicep_imported_from!": { - "sourceTemplate": "blob-service/main.bicep", - "originalIdentifier": "corsRuleType" - } - } - }, - "containerType": { - "type": "object", - "properties": { - "name": { - "type": "string", - "metadata": { - "description": "Required. The name of the Storage Container to deploy." - } - }, - "defaultEncryptionScope": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. Default the container to use specified encryption scope for all writes." - } - }, - "denyEncryptionScopeOverride": { - "type": "bool", - "nullable": true, - "metadata": { - "description": "Optional. Block override of encryption scope from the container default." - } - }, - "enableNfsV3AllSquash": { - "type": "bool", - "nullable": true, - "metadata": { - "description": "Optional. Enable NFSv3 all squash on blob container." - } - }, - "enableNfsV3RootSquash": { - "type": "bool", - "nullable": true, - "metadata": { - "description": "Optional. Enable NFSv3 root squash on blob container." - } - }, - "immutableStorageWithVersioningEnabled": { - "type": "bool", - "nullable": true, - "metadata": { - "description": "Optional. This is an immutable property, when set to true it enables object level immutability at the container level. The property is immutable and can only be set to true at the container creation time. Existing containers must undergo a migration process." - } - }, - "immutabilityPolicy": { - "$ref": "#/definitions/_1.immutabilityPolicyType", - "nullable": true, - "metadata": { - "description": "Optional. Configure immutability policy." - } - }, - "metadata": { - "type": "object", - "metadata": { - "__bicep_resource_derived_type!": { - "source": "Microsoft.Storage/storageAccounts/blobServices/containers@2024-01-01#properties/properties/properties/metadata" - }, - "description": "Optional. A name-value pair to associate with the container as metadata." - }, - "nullable": true - }, - "publicAccess": { - "type": "string", - "allowedValues": [ - "Blob", - "Container", - "None" - ], - "nullable": true, - "metadata": { - "description": "Optional. Specifies whether data in the container may be accessed publicly and the level of access." - } - }, - "roleAssignments": { - "type": "array", - "items": { - "$ref": "#/definitions/roleAssignmentType" - }, - "nullable": true, - "metadata": { - "description": "Optional. Array of role assignments to create." - } - } - }, - "metadata": { - "description": "The type of a storage container.", - "__bicep_imported_from!": { - "sourceTemplate": "blob-service/main.bicep" - } - } - }, - "customerManagedKeyWithAutoRotateType": { - "type": "object", - "properties": { - "keyVaultResourceId": { - "type": "string", - "metadata": { - "description": "Required. The resource ID of a key vault to reference a customer managed key for encryption from." - } - }, - "keyName": { - "type": "string", - "metadata": { - "description": "Required. The name of the customer managed key to use for encryption." - } - }, - "keyVersion": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The version of the customer managed key to reference for encryption. If not provided, using version as per 'autoRotationEnabled' setting." - } - }, - "autoRotationEnabled": { - "type": "bool", - "nullable": true, - "metadata": { - "description": "Optional. Enable or disable auto-rotating to the latest key version. Default is `true`. If set to `false`, the latest key version at the time of the deployment is used." - } - }, - "userAssignedIdentityResourceId": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. User assigned identity to use when fetching the customer managed key. Required if no system assigned identity is available for use." - } - } - }, - "metadata": { - "description": "An AVM-aligned type for a customer-managed key. To be used if the resource type supports auto-rotation of the customer-managed key.", - "__bicep_imported_from!": { - "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.1" - } - } - }, - "diagnosticSettingFullType": { - "type": "object", - "properties": { - "name": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The name of the diagnostic setting." - } - }, - "logCategoriesAndGroups": { - "type": "array", - "items": { - "type": "object", - "properties": { - "category": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. Name of a Diagnostic Log category for a resource type this setting is applied to. Set the specific logs to collect here." - } - }, - "categoryGroup": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. Name of a Diagnostic Log category group for a resource type this setting is applied to. Set to `allLogs` to collect all logs." - } - }, - "enabled": { - "type": "bool", - "nullable": true, - "metadata": { - "description": "Optional. Enable or disable the category explicitly. Default is `true`." - } - } - } - }, - "nullable": true, - "metadata": { - "description": "Optional. The name of logs that will be streamed. \"allLogs\" includes all possible logs for the resource. Set to `[]` to disable log collection." - } - }, - "metricCategories": { - "type": "array", - "items": { - "type": "object", - "properties": { - "category": { - "type": "string", - "metadata": { - "description": "Required. Name of a Diagnostic Metric category for a resource type this setting is applied to. Set to `AllMetrics` to collect all metrics." - } - }, - "enabled": { - "type": "bool", - "nullable": true, - "metadata": { - "description": "Optional. Enable or disable the category explicitly. Default is `true`." - } - } - } - }, - "nullable": true, - "metadata": { - "description": "Optional. The name of metrics that will be streamed. \"allMetrics\" includes all possible metrics for the resource. Set to `[]` to disable metric collection." - } - }, - "logAnalyticsDestinationType": { - "type": "string", - "allowedValues": [ - "AzureDiagnostics", - "Dedicated" - ], - "nullable": true, - "metadata": { - "description": "Optional. A string indicating whether the export to Log Analytics should use the default destination type, i.e. AzureDiagnostics, or use a destination type." - } - }, - "workspaceResourceId": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. Resource ID of the diagnostic log analytics workspace. For security reasons, it is recommended to set diagnostic settings to send data to either storage account, log analytics workspace or event hub." - } - }, - "storageAccountResourceId": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. Resource ID of the diagnostic storage account. For security reasons, it is recommended to set diagnostic settings to send data to either storage account, log analytics workspace or event hub." - } - }, - "eventHubAuthorizationRuleResourceId": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. Resource ID of the diagnostic event hub authorization rule for the Event Hubs namespace in which the event hub should be created or streamed to." - } - }, - "eventHubName": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. Name of the diagnostic event hub within the namespace to which logs are streamed. Without this, an event hub is created for each log category. For security reasons, it is recommended to set diagnostic settings to send data to either storage account, log analytics workspace or event hub." - } - }, - "marketplacePartnerResourceId": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The full ARM resource ID of the Marketplace resource to which you would like to send Diagnostic Logs." - } - } - }, - "metadata": { - "description": "An AVM-aligned type for a diagnostic setting. To be used if both logs & metrics are supported by the resource provider.", - "__bicep_imported_from!": { - "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.1" - } - } - }, - "diagnosticSettingMetricsOnlyType": { - "type": "object", - "properties": { - "name": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The name of diagnostic setting." - } - }, - "metricCategories": { - "type": "array", - "items": { - "type": "object", - "properties": { - "category": { - "type": "string", - "metadata": { - "description": "Required. Name of a Diagnostic Metric category for a resource type this setting is applied to. Set to `AllMetrics` to collect all metrics." - } - }, - "enabled": { - "type": "bool", - "nullable": true, - "metadata": { - "description": "Optional. Enable or disable the category explicitly. Default is `true`." - } - } - } - }, - "nullable": true, - "metadata": { - "description": "Optional. The name of metrics that will be streamed. \"allMetrics\" includes all possible metrics for the resource. Set to `[]` to disable metric collection." - } - }, - "logAnalyticsDestinationType": { - "type": "string", - "allowedValues": [ - "AzureDiagnostics", - "Dedicated" - ], - "nullable": true, - "metadata": { - "description": "Optional. A string indicating whether the export to Log Analytics should use the default destination type, i.e. AzureDiagnostics, or use a destination type." - } - }, - "workspaceResourceId": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. Resource ID of the diagnostic log analytics workspace. For security reasons, it is recommended to set diagnostic settings to send data to either storage account, log analytics workspace or event hub." - } - }, - "storageAccountResourceId": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. Resource ID of the diagnostic storage account. For security reasons, it is recommended to set diagnostic settings to send data to either storage account, log analytics workspace or event hub." - } - }, - "eventHubAuthorizationRuleResourceId": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. Resource ID of the diagnostic event hub authorization rule for the Event Hubs namespace in which the event hub should be created or streamed to." - } - }, - "eventHubName": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. Name of the diagnostic event hub within the namespace to which logs are streamed. Without this, an event hub is created for each log category. For security reasons, it is recommended to set diagnostic settings to send data to either storage account, log analytics workspace or event hub." - } - }, - "marketplacePartnerResourceId": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The full ARM resource ID of the Marketplace resource to which you would like to send Diagnostic Logs." - } - } - }, - "metadata": { - "description": "An AVM-aligned type for a diagnostic setting. To be used if only metrics are supported by the resource provider.", - "__bicep_imported_from!": { - "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.1" - } - } - }, - "fileCorsRuleType": { - "type": "object", - "properties": { - "allowedHeaders": { - "type": "array", - "items": { - "type": "string" - }, - "metadata": { - "description": "Required. A list of headers allowed to be part of the cross-origin request." - } - }, - "allowedMethods": { - "type": "array", - "allowedValues": [ - "CONNECT", - "DELETE", - "GET", - "HEAD", - "MERGE", - "OPTIONS", - "PATCH", - "POST", - "PUT", - "TRACE" - ], - "metadata": { - "description": "Required. A list of HTTP methods that are allowed to be executed by the origin." - } - }, - "allowedOrigins": { - "type": "array", - "items": { - "type": "string" - }, - "metadata": { - "description": "Required. A list of origin domains that will be allowed via CORS, or \"*\" to allow all domains." - } - }, - "exposedHeaders": { - "type": "array", - "items": { - "type": "string" - }, - "metadata": { - "description": "Required. A list of response headers to expose to CORS clients." - } - }, - "maxAgeInSeconds": { - "type": "int", - "metadata": { - "description": "Required. The number of seconds that the client/browser should cache a preflight response." - } - } - }, - "metadata": { - "description": "The type for a cors rule.", - "__bicep_imported_from!": { - "sourceTemplate": "file-service/main.bicep", - "originalIdentifier": "corsRuleType" - } - } - }, - "fileShareType": { - "type": "object", - "properties": { - "name": { - "type": "string", - "metadata": { - "description": "Required. The name of the file share." - } - }, - "accessTier": { - "type": "string", - "allowedValues": [ - "Cool", - "Hot", - "Premium", - "TransactionOptimized" - ], - "nullable": true, - "metadata": { - "description": "Optional. Access tier for specific share. Required if the Storage Account kind is set to FileStorage (should be set to \"Premium\"). GpV2 account can choose between TransactionOptimized (default), Hot, and Cool." - } - }, - "enabledProtocols": { - "type": "string", - "allowedValues": [ - "NFS", - "SMB" - ], - "nullable": true, - "metadata": { - "description": "Optional. The authentication protocol that is used for the file share. Can only be specified when creating a share." - } - }, - "rootSquash": { - "type": "string", - "allowedValues": [ - "AllSquash", - "NoRootSquash", - "RootSquash" - ], - "nullable": true, - "metadata": { - "description": "Optional. Permissions for NFS file shares are enforced by the client OS rather than the Azure Files service. Toggling the root squash behavior reduces the rights of the root user for NFS shares." - } - }, - "shareQuota": { - "type": "int", - "nullable": true, - "metadata": { - "description": "Optional. The maximum size of the share, in gigabytes. Must be greater than 0, and less than or equal to 5120 (5TB). For Large File Shares, the maximum size is 102400 (100TB)." - } - }, - "roleAssignments": { - "type": "array", - "items": { - "$ref": "#/definitions/roleAssignmentType" - }, - "nullable": true, - "metadata": { - "description": "Optional. Array of role assignments to create." - } - } - }, - "metadata": { - "description": "The type for a file share.", - "__bicep_imported_from!": { - "sourceTemplate": "file-service/main.bicep" - } - } - }, - "lockType": { - "type": "object", - "properties": { - "name": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. Specify the name of lock." - } - }, - "kind": { - "type": "string", - "allowedValues": [ - "CanNotDelete", - "None", - "ReadOnly" - ], - "nullable": true, - "metadata": { - "description": "Optional. Specify the type of lock." - } - }, - "notes": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. Specify the notes of the lock." - } - } - }, - "metadata": { - "description": "An AVM-aligned type for a lock.", - "__bicep_imported_from!": { - "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.1" - } - } - }, - "managedIdentityAllType": { - "type": "object", - "properties": { - "systemAssigned": { - "type": "bool", - "nullable": true, - "metadata": { - "description": "Optional. Enables system assigned managed identity on the resource." - } - }, - "userAssignedResourceIds": { - "type": "array", - "items": { - "type": "string" - }, - "nullable": true, - "metadata": { - "description": "Optional. The resource ID(s) to assign to the resource. Required if a user assigned identity is used for encryption." - } - } - }, - "metadata": { - "description": "An AVM-aligned type for a managed identity configuration. To be used if both a system-assigned & user-assigned identities are supported by the resource provider.", - "__bicep_imported_from!": { - "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.1" - } - } - }, - "objectReplicationPolicyRuleType": { - "type": "object", - "properties": { - "ruleId": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The ID of the rule. Auto-generated on destination account. Required for source account." - } - }, - "containerName": { - "type": "string", - "metadata": { - "description": "Required. The name of the source container." - } - }, - "destinationContainerName": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The name of the destination container. If not provided, the same name as the source container will be used." - } - }, - "filters": { - "type": "object", - "properties": { - "prefixMatch": { - "type": "array", - "items": { - "type": "string" - }, - "nullable": true, - "metadata": { - "description": "Optional. The prefix to match for the replication policy rule." - } - }, - "minCreationTime": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The minimum creation time to match for the replication policy rule." - } - } - }, - "nullable": true, - "metadata": { - "description": "Optional. The filters for the object replication policy rule." - } - } - }, - "metadata": { - "description": "The type of an object replication policy rule.", - "__bicep_imported_from!": { - "sourceTemplate": "object-replication-policy/policy/main.bicep" - } - } - }, - "permissionScopeType": { - "type": "object", - "properties": { - "permissions": { - "type": "string", - "metadata": { - "description": "Required. The permissions for the local user. Possible values include: Read (r), Write (w), Delete (d), List (l), and Create (c)." - } - }, - "resourceName": { - "type": "string", - "metadata": { - "description": "Required. The name of resource, normally the container name or the file share name, used by the local user." - } - }, - "service": { - "type": "string", - "metadata": { - "description": "Required. The service used by the local user, e.g. blob, file." - } - } - }, - "metadata": { - "__bicep_imported_from!": { - "sourceTemplate": "local-user/main.bicep" - } - } - }, - "privateEndpointMultiServiceType": { - "type": "object", - "properties": { - "name": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The name of the private endpoint." - } - }, - "location": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The location to deploy the private endpoint to." - } - }, - "privateLinkServiceConnectionName": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The name of the private link connection to create." - } - }, - "service": { - "type": "string", - "metadata": { - "description": "Required. The subresource to deploy the private endpoint for. For example \"blob\", \"table\", \"queue\" or \"file\" for a Storage Account's Private Endpoints." - } - }, - "subnetResourceId": { - "type": "string", - "metadata": { - "description": "Required. Resource ID of the subnet where the endpoint needs to be created." - } - }, - "resourceGroupResourceId": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The resource ID of the Resource Group the Private Endpoint will be created in. If not specified, the Resource Group of the provided Virtual Network Subnet is used." - } - }, - "privateDnsZoneGroup": { - "$ref": "#/definitions/_2.privateEndpointPrivateDnsZoneGroupType", - "nullable": true, - "metadata": { - "description": "Optional. The private DNS zone group to configure for the private endpoint." - } - }, - "isManualConnection": { - "type": "bool", - "nullable": true, - "metadata": { - "description": "Optional. If Manual Private Link Connection is required." - } - }, - "manualConnectionRequestMessage": { - "type": "string", - "nullable": true, - "maxLength": 140, - "metadata": { - "description": "Optional. A message passed to the owner of the remote resource with the manual connection request." - } - }, - "customDnsConfigs": { - "type": "array", - "items": { - "$ref": "#/definitions/_2.privateEndpointCustomDnsConfigType" - }, - "nullable": true, - "metadata": { - "description": "Optional. Custom DNS configurations." - } - }, - "ipConfigurations": { - "type": "array", - "items": { - "$ref": "#/definitions/_2.privateEndpointIpConfigurationType" - }, - "nullable": true, - "metadata": { - "description": "Optional. A list of IP configurations of the private endpoint. This will be used to map to the First Party Service endpoints." - } - }, - "applicationSecurityGroupResourceIds": { - "type": "array", - "items": { - "type": "string" - }, - "nullable": true, - "metadata": { - "description": "Optional. Application security groups in which the private endpoint IP configuration is included." - } - }, - "customNetworkInterfaceName": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The custom name of the network interface attached to the private endpoint." - } - }, - "lock": { - "$ref": "#/definitions/lockType", - "nullable": true, - "metadata": { - "description": "Optional. Specify the type of lock." - } - }, - "roleAssignments": { - "type": "array", - "items": { - "$ref": "#/definitions/roleAssignmentType" - }, - "nullable": true, - "metadata": { - "description": "Optional. Array of role assignments to create." - } - }, - "tags": { - "type": "object", - "nullable": true, - "metadata": { - "__bicep_resource_derived_type!": { - "source": "Microsoft.Network/privateEndpoints@2024-07-01#properties/tags" - }, - "description": "Optional. Tags to be applied on all resources/resource groups in this deployment." - } - }, - "enableTelemetry": { - "type": "bool", - "nullable": true, - "metadata": { - "description": "Optional. Enable/Disable usage telemetry for module." - } - } - }, - "metadata": { - "description": "An AVM-aligned type for a private endpoint. To be used if the private endpoint's default service / groupId can NOT be assumed (i.e., for services that have more than one subresource, like Storage Account with Blob (blob, table, queue, file, ...).", - "__bicep_imported_from!": { - "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.1" - } - } - }, - "queueCorsRuleType": { - "type": "object", - "properties": { - "allowedHeaders": { - "type": "array", - "items": { - "type": "string" - }, - "metadata": { - "description": "Required. A list of headers allowed to be part of the cross-origin request." - } - }, - "allowedMethods": { - "type": "array", - "allowedValues": [ - "CONNECT", - "DELETE", - "GET", - "HEAD", - "MERGE", - "OPTIONS", - "PATCH", - "POST", - "PUT", - "TRACE" - ], - "metadata": { - "description": "Required. A list of HTTP methods that are allowed to be executed by the origin." - } - }, - "allowedOrigins": { - "type": "array", - "items": { - "type": "string" - }, - "metadata": { - "description": "Required. A list of origin domains that will be allowed via CORS, or \"*\" to allow all domains." - } - }, - "exposedHeaders": { - "type": "array", - "items": { - "type": "string" - }, - "metadata": { - "description": "Required. A list of response headers to expose to CORS clients." - } - }, - "maxAgeInSeconds": { - "type": "int", - "metadata": { - "description": "Required. The number of seconds that the client/browser should cache a preflight response." - } - } - }, - "metadata": { - "description": "The type for a cors rule.", - "__bicep_imported_from!": { - "sourceTemplate": "queue-service/main.bicep", - "originalIdentifier": "corsRuleType" - } - } - }, - "queueType": { - "type": "object", - "properties": { - "name": { - "type": "string", - "metadata": { - "description": "Required. The name of the queue." - } - }, - "metadata": { - "type": "object", - "metadata": { - "__bicep_resource_derived_type!": { - "source": "Microsoft.Storage/storageAccounts/queueServices/queues@2024-01-01#properties/properties/properties/metadata" - }, - "description": "Optional. Metadata to set on the queue." - }, - "nullable": true - }, - "roleAssignments": { - "type": "array", - "items": { - "$ref": "#/definitions/roleAssignmentType" - }, - "nullable": true, - "metadata": { - "description": "Optional. Array of role assignments to create." - } - } - }, - "metadata": { - "description": "The type for a queue.", - "__bicep_imported_from!": { - "sourceTemplate": "queue-service/main.bicep" - } - } - }, - "roleAssignmentType": { - "type": "object", - "properties": { - "name": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The name (as GUID) of the role assignment. If not provided, a GUID will be generated." - } - }, - "roleDefinitionIdOrName": { - "type": "string", - "metadata": { - "description": "Required. The role to assign. You can provide either the display name of the role definition, the role definition GUID, or its fully qualified ID in the following format: '/providers/Microsoft.Authorization/roleDefinitions/c2f4ef07-c644-48eb-af81-4b1b4947fb11'." - } - }, - "principalId": { - "type": "string", - "metadata": { - "description": "Required. The principal ID of the principal (user/group/identity) to assign the role to." - } - }, - "principalType": { - "type": "string", - "allowedValues": [ - "Device", - "ForeignGroup", - "Group", - "ServicePrincipal", - "User" - ], - "nullable": true, - "metadata": { - "description": "Optional. The principal type of the assigned principal ID." - } - }, - "description": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The description of the role assignment." - } - }, - "condition": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The conditions on the role assignment. This limits the resources it can be assigned to. e.g.: @Resource[Microsoft.Storage/storageAccounts/blobServices/containers:ContainerName] StringEqualsIgnoreCase \"foo_storage_container\"." - } - }, - "conditionVersion": { - "type": "string", - "allowedValues": [ - "2.0" - ], - "nullable": true, - "metadata": { - "description": "Optional. Version of the condition." - } - }, - "delegatedManagedIdentityResourceId": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The Resource Id of the delegated managed identity resource." - } - } - }, - "metadata": { - "description": "An AVM-aligned type for a role assignment.", - "__bicep_imported_from!": { - "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.1" - } - } - }, - "secretsOutputType": { - "type": "object", - "properties": {}, - "additionalProperties": { - "$ref": "#/definitions/_2.secretSetOutputType", - "metadata": { - "description": "An exported secret's references." - } - }, - "metadata": { - "description": "A map of the exported secrets", - "__bicep_imported_from!": { - "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.1" - } - } - }, - "sshAuthorizedKeyType": { - "type": "object", - "properties": { - "description": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. Description used to store the function/usage of the key." - } - }, - "key": { - "type": "securestring", - "metadata": { - "description": "Required. SSH public key base64 encoded. The format should be: '{keyType} {keyData}', e.g. ssh-rsa AAAABBBB." - } - } - }, - "metadata": { - "__bicep_imported_from!": { - "sourceTemplate": "local-user/main.bicep" - } - } - }, - "tableCorsRuleType": { - "type": "object", - "properties": { - "allowedHeaders": { - "type": "array", - "items": { - "type": "string" - }, - "metadata": { - "description": "Required. A list of headers allowed to be part of the cross-origin request." - } - }, - "allowedMethods": { - "type": "array", - "allowedValues": [ - "CONNECT", - "DELETE", - "GET", - "HEAD", - "MERGE", - "OPTIONS", - "PATCH", - "POST", - "PUT", - "TRACE" - ], - "metadata": { - "description": "Required. A list of HTTP methods that are allowed to be executed by the origin." - } - }, - "allowedOrigins": { - "type": "array", - "items": { - "type": "string" - }, - "metadata": { - "description": "Required. A list of origin domains that will be allowed via CORS, or \"*\" to allow all domains." - } - }, - "exposedHeaders": { - "type": "array", - "items": { - "type": "string" - }, - "metadata": { - "description": "Required. A list of response headers to expose to CORS clients." - } - }, - "maxAgeInSeconds": { - "type": "int", - "metadata": { - "description": "Required. The number of seconds that the client/browser should cache a preflight response." - } - } - }, - "metadata": { - "description": "The type for a cors rule.", - "__bicep_imported_from!": { - "sourceTemplate": "table-service/main.bicep", - "originalIdentifier": "corsRuleType" - } - } - }, - "tableType": { - "type": "object", - "properties": { - "name": { - "type": "string", - "metadata": { - "description": "Required. The name of the table." - } - }, - "roleAssignments": { - "type": "array", - "items": { - "$ref": "#/definitions/roleAssignmentType" - }, - "nullable": true, - "metadata": { - "description": "Optional. Array of role assignments to create." - } - } - }, - "metadata": { - "description": "The type for a table.", - "__bicep_imported_from!": { - "sourceTemplate": "table-service/main.bicep" - } - } - } - }, - "parameters": { - "name": { - "type": "string", - "maxLength": 24, - "metadata": { - "description": "Required. Name of the Storage Account. Must be lower-case." - } - }, - "location": { - "type": "string", - "defaultValue": "[resourceGroup().location]", - "metadata": { - "description": "Optional. Location for all resources." - } - }, - "extendedLocationZone": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. Extended Zone location (ex 'losangeles'). When supplied, the storage account will be created in the specified zone under the parent location. The extended zone must be available in the supplied parent location." - } - }, - "roleAssignments": { - "type": "array", - "items": { - "$ref": "#/definitions/roleAssignmentType" - }, - "nullable": true, - "metadata": { - "description": "Optional. Array of role assignments to create." - } - }, - "managedIdentities": { - "$ref": "#/definitions/managedIdentityAllType", - "nullable": true, - "metadata": { - "description": "Optional. The managed identity definition for this resource." - } - }, - "kind": { - "type": "string", - "defaultValue": "StorageV2", - "allowedValues": [ - "Storage", - "StorageV2", - "BlobStorage", - "FileStorage", - "BlockBlobStorage" - ], - "metadata": { - "description": "Optional. Type of Storage Account to create." - } - }, - "skuName": { - "type": "string", - "defaultValue": "Standard_GRS", - "allowedValues": [ - "Standard_LRS", - "Standard_ZRS", - "Standard_GRS", - "Standard_GZRS", - "Standard_RAGRS", - "Standard_RAGZRS", - "StandardV2_LRS", - "StandardV2_ZRS", - "StandardV2_GRS", - "StandardV2_GZRS", - "Premium_LRS", - "Premium_ZRS", - "PremiumV2_LRS", - "PremiumV2_ZRS" - ], - "metadata": { - "description": "Optional. Storage Account Sku Name - note: certain V2 SKUs require the use of: kind = FileStorage." - } - }, - "accessTier": { - "type": "string", - "defaultValue": "Hot", - "allowedValues": [ - "Premium", - "Hot", - "Cool", - "Cold" - ], - "metadata": { - "description": "Conditional. Required if the Storage Account kind is set to BlobStorage. The access tier is used for billing. The \"Premium\" access tier is the default value for premium block blobs storage account type and it cannot be changed for the premium block blobs storage account type." - } - }, - "largeFileSharesState": { - "type": "string", - "defaultValue": "Disabled", - "allowedValues": [ - "Disabled", - "Enabled" - ], - "metadata": { - "description": "Optional. Allow large file shares if set to 'Enabled'. It cannot be disabled once it is enabled. Only supported on locally redundant and zone redundant file shares. It cannot be set on FileStorage storage accounts (storage accounts for premium file shares)." - } - }, - "azureFilesIdentityBasedAuthentication": { - "type": "object", - "metadata": { - "__bicep_resource_derived_type!": { - "source": "Microsoft.Storage/storageAccounts@2025-01-01#properties/properties/properties/azureFilesIdentityBasedAuthentication" - }, - "description": "Optional. Provides the identity based authentication settings for Azure Files." - }, - "nullable": true - }, - "defaultToOAuthAuthentication": { - "type": "bool", - "defaultValue": false, - "metadata": { - "description": "Optional. A boolean flag which indicates whether the default authentication is OAuth or not." - } - }, - "allowSharedKeyAccess": { - "type": "bool", - "defaultValue": true, - "metadata": { - "description": "Optional. Indicates whether the storage account permits requests to be authorized with the account access key via Shared Key. If false, then all requests, including shared access signatures, must be authorized with Azure Active Directory (Azure AD). The default value is null, which is equivalent to true." - } - }, - "privateEndpoints": { - "type": "array", - "items": { - "$ref": "#/definitions/privateEndpointMultiServiceType" - }, - "nullable": true, - "metadata": { - "description": "Optional. Configuration details for private endpoints. For security reasons, it is recommended to use private endpoints whenever possible." - } - }, - "managementPolicyRules": { - "type": "array", - "metadata": { - "__bicep_resource_derived_type!": { - "source": "Microsoft.Storage/storageAccounts/managementPolicies@2025-01-01#properties/properties/properties/policy/properties/rules" - }, - "description": "Optional. The Storage Account ManagementPolicies Rules." - }, - "nullable": true - }, - "networkAcls": { - "$ref": "#/definitions/networkAclsType", - "nullable": true, - "metadata": { - "description": "Optional. Networks ACLs, this value contains IPs to whitelist and/or Subnet information. If in use, bypass needs to be supplied. For security reasons, it is recommended to set the DefaultAction Deny." - } - }, - "requireInfrastructureEncryption": { - "type": "bool", - "defaultValue": true, - "metadata": { - "description": "Optional. A Boolean indicating whether or not the service applies a secondary layer of encryption with platform managed keys for data at rest. For security reasons, it is recommended to set it to true." - } - }, - "allowCrossTenantReplication": { - "type": "bool", - "defaultValue": false, - "metadata": { - "description": "Optional. Allow or disallow cross AAD tenant object replication." - } - }, - "customDomainName": { - "type": "string", - "defaultValue": "", - "metadata": { - "description": "Optional. Sets the custom domain name assigned to the storage account. Name is the CNAME source." - } - }, - "customDomainUseSubDomainName": { - "type": "bool", - "defaultValue": false, - "metadata": { - "description": "Optional. Indicates whether indirect CName validation is enabled. This should only be set on updates." - } - }, - "dnsEndpointType": { - "type": "string", - "nullable": true, - "allowedValues": [ - "AzureDnsZone", - "Standard" - ], - "metadata": { - "description": "Optional. Allows you to specify the type of endpoint. Set this to AzureDNSZone to create a large number of accounts in a single subscription, which creates accounts in an Azure DNS Zone and the endpoint URL will have an alphanumeric DNS Zone identifier." - } - }, - "blobServices": { - "$ref": "#/definitions/blobServiceType", - "defaultValue": "[if(not(equals(parameters('kind'), 'FileStorage')), createObject('containerDeleteRetentionPolicyEnabled', true(), 'containerDeleteRetentionPolicyDays', 7, 'deleteRetentionPolicyEnabled', true(), 'deleteRetentionPolicyDays', 6), createObject())]", - "metadata": { - "description": "Optional. Blob service and containers to deploy." - } - }, - "fileServices": { - "$ref": "#/definitions/fileServiceType", - "defaultValue": {}, - "metadata": { - "description": "Optional. File service and shares to deploy." - } - }, - "queueServices": { - "$ref": "#/definitions/queueServiceType", - "defaultValue": {}, - "metadata": { - "description": "Optional. Queue service and queues to create." - } - }, - "tableServices": { - "$ref": "#/definitions/tableServiceType", - "defaultValue": {}, - "metadata": { - "description": "Optional. Table service and tables to create." - } - }, - "allowBlobPublicAccess": { - "type": "bool", - "defaultValue": false, - "metadata": { - "description": "Optional. Indicates whether public access is enabled for all blobs or containers in the storage account. For security reasons, it is recommended to set it to false." - } - }, - "minimumTlsVersion": { - "type": "string", - "defaultValue": "TLS1_2", - "allowedValues": [ - "TLS1_2" - ], - "metadata": { - "description": "Optional. Set the minimum TLS version on request to storage. The TLS versions 1.0 and 1.1 are deprecated and not supported anymore." - } - }, - "enableHierarchicalNamespace": { - "type": "bool", - "nullable": true, - "metadata": { - "description": "Conditional. If true, enables Hierarchical Namespace for the storage account. Required if enableSftp or enableNfsV3 is set to true." - } - }, - "enableSftp": { - "type": "bool", - "defaultValue": false, - "metadata": { - "description": "Optional. If true, enables Secure File Transfer Protocol for the storage account. Requires enableHierarchicalNamespace to be true." - } - }, - "localUsers": { - "type": "array", - "items": { - "$ref": "#/definitions/localUserType" - }, - "nullable": true, - "metadata": { - "description": "Optional. Local users to deploy for SFTP authentication." - } - }, - "isLocalUserEnabled": { - "type": "bool", - "defaultValue": false, - "metadata": { - "description": "Optional. Enables local users feature, if set to true." - } - }, - "enableNfsV3": { - "type": "bool", - "defaultValue": false, - "metadata": { - "description": "Optional. If true, enables NFS 3.0 support for the storage account. Requires enableHierarchicalNamespace to be true." - } - }, - "diagnosticSettings": { - "type": "array", - "items": { - "$ref": "#/definitions/diagnosticSettingMetricsOnlyType" - }, - "nullable": true, - "metadata": { - "description": "Optional. The diagnostic settings of the service." - } - }, - "lock": { - "$ref": "#/definitions/lockType", - "nullable": true, - "metadata": { - "description": "Optional. The lock settings of the service." + "mode": "Incremental", + "parameters": { + "name": { + "value": "[coalesce(tryGet(tryGet(parameters('ipConfigurations')[copyIndex()], 'pipConfiguration'), 'name'), format('{0}{1}', parameters('virtualMachineName'), tryGet(tryGet(parameters('ipConfigurations')[copyIndex()], 'pipConfiguration'), 'publicIpNameSuffix')))]" + }, + "diagnosticSettings": { + "value": "[coalesce(tryGet(tryGet(parameters('ipConfigurations')[copyIndex()], 'pipConfiguration'), 'diagnosticSettings'), tryGet(parameters('ipConfigurations')[copyIndex()], 'diagnosticSettings'))]" + }, + "location": { + "value": "[parameters('location')]" + }, + "lock": { + "value": "[parameters('lock')]" + }, + "idleTimeoutInMinutes": { + "value": "[tryGet(tryGet(parameters('ipConfigurations')[copyIndex()], 'pipConfiguration'), 'idleTimeoutInMinutes')]" + }, + "ddosSettings": { + "value": "[tryGet(tryGet(parameters('ipConfigurations')[copyIndex()], 'pipConfiguration'), 'ddosSettings')]" + }, + "dnsSettings": { + "value": "[tryGet(tryGet(parameters('ipConfigurations')[copyIndex()], 'pipConfiguration'), 'dnsSettings')]" + }, + "publicIPAddressVersion": { + "value": "[tryGet(tryGet(parameters('ipConfigurations')[copyIndex()], 'pipConfiguration'), 'publicIPAddressVersion')]" + }, + "publicIPAllocationMethod": { + "value": "[tryGet(tryGet(parameters('ipConfigurations')[copyIndex()], 'pipConfiguration'), 'publicIPAllocationMethod')]" + }, + "publicIpPrefixResourceId": { + "value": "[tryGet(tryGet(parameters('ipConfigurations')[copyIndex()], 'pipConfiguration'), 'publicIpPrefixResourceId')]" + }, + "roleAssignments": { + "value": "[tryGet(tryGet(parameters('ipConfigurations')[copyIndex()], 'pipConfiguration'), 'roleAssignments')]" + }, + "skuName": { + "value": "[tryGet(tryGet(parameters('ipConfigurations')[copyIndex()], 'pipConfiguration'), 'skuName')]" + }, + "skuTier": { + "value": "[tryGet(tryGet(parameters('ipConfigurations')[copyIndex()], 'pipConfiguration'), 'skuTier')]" + }, + "tags": { + "value": "[coalesce(tryGet(parameters('ipConfigurations')[copyIndex()], 'tags'), parameters('tags'))]" + }, + "availabilityZones": { + "value": "[tryGet(tryGet(parameters('ipConfigurations')[copyIndex()], 'pipConfiguration'), 'availabilityZones')]" + }, + "enableTelemetry": { + "value": "[coalesce(coalesce(tryGet(tryGet(parameters('ipConfigurations')[copyIndex()], 'pipConfiguration'), 'enableTelemetry'), tryGet(parameters('ipConfigurations')[copyIndex()], 'enableTelemetry')), parameters('enableTelemetry'))]" + }, + "ipTags": { + "value": "[tryGet(tryGet(parameters('ipConfigurations')[copyIndex()], 'pipConfiguration'), 'ipTags')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "languageVersion": "2.0", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.38.5.1644", + "templateHash": "7550528442771433353" + }, + "name": "Public IP Addresses", + "description": "This module deploys a Public IP Address." + }, + "definitions": { + "dnsSettingsType": { + "type": "object", + "properties": { + "domainNameLabel": { + "type": "string", + "metadata": { + "description": "Required. The domain name label. The concatenation of the domain name label and the regionalized DNS zone make up the fully qualified domain name associated with the public IP address. If a domain name label is specified, an A DNS record is created for the public IP in the Microsoft Azure DNS system." + } + }, + "domainNameLabelScope": { + "type": "string", + "allowedValues": [ + "NoReuse", + "ResourceGroupReuse", + "SubscriptionReuse", + "TenantReuse" + ], + "nullable": true, + "metadata": { + "description": "Optional. The domain name label scope. If a domain name label and a domain name label scope are specified, an A DNS record is created for the public IP in the Microsoft Azure DNS system with a hashed value includes in FQDN." + } + }, + "fqdn": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The Fully Qualified Domain Name of the A DNS record associated with the public IP. This is the concatenation of the domainNameLabel and the regionalized DNS zone." + } + }, + "reverseFqdn": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The reverse FQDN. A user-visible, fully qualified domain name that resolves to this public IP address. If the reverseFqdn is specified, then a PTR DNS record is created pointing from the IP address in the in-addr.arpa domain to the reverse FQDN." + } + } + }, + "metadata": { + "__bicep_export!": true + } + }, + "ddosSettingsType": { + "type": "object", + "properties": { + "ddosProtectionPlan": { + "type": "object", + "properties": { + "id": { + "type": "string", + "metadata": { + "description": "Required. The resource ID of the DDOS protection plan associated with the public IP address." + } + } + }, + "nullable": true, + "metadata": { + "description": "Optional. The DDoS protection plan associated with the public IP address." + } + }, + "protectionMode": { + "type": "string", + "allowedValues": [ + "Enabled" + ], + "metadata": { + "description": "Required. The DDoS protection policy customizations." + } + } + }, + "metadata": { + "__bicep_export!": true + } + }, + "ipTagType": { + "type": "object", + "properties": { + "ipTagType": { + "type": "string", + "metadata": { + "description": "Required. The IP tag type." + } + }, + "tag": { + "type": "string", + "metadata": { + "description": "Required. The IP tag." + } + } + }, + "metadata": { + "__bicep_export!": true + } + }, + "diagnosticSettingFullType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The name of the diagnostic setting." + } + }, + "logCategoriesAndGroups": { + "type": "array", + "items": { + "type": "object", + "properties": { + "category": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Name of a Diagnostic Log category for a resource type this setting is applied to. Set the specific logs to collect here." + } + }, + "categoryGroup": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Name of a Diagnostic Log category group for a resource type this setting is applied to. Set to `allLogs` to collect all logs." + } + }, + "enabled": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. Enable or disable the category explicitly. Default is `true`." + } + } + } + }, + "nullable": true, + "metadata": { + "description": "Optional. The name of logs that will be streamed. \"allLogs\" includes all possible logs for the resource. Set to `[]` to disable log collection." + } + }, + "metricCategories": { + "type": "array", + "items": { + "type": "object", + "properties": { + "category": { + "type": "string", + "metadata": { + "description": "Required. Name of a Diagnostic Metric category for a resource type this setting is applied to. Set to `AllMetrics` to collect all metrics." + } + }, + "enabled": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. Enable or disable the category explicitly. Default is `true`." + } + } + } + }, + "nullable": true, + "metadata": { + "description": "Optional. The name of metrics that will be streamed. \"allMetrics\" includes all possible metrics for the resource. Set to `[]` to disable metric collection." + } + }, + "logAnalyticsDestinationType": { + "type": "string", + "allowedValues": [ + "AzureDiagnostics", + "Dedicated" + ], + "nullable": true, + "metadata": { + "description": "Optional. A string indicating whether the export to Log Analytics should use the default destination type, i.e. AzureDiagnostics, or use a destination type." + } + }, + "workspaceResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Resource ID of the diagnostic log analytics workspace. For security reasons, it is recommended to set diagnostic settings to send data to either storage account, log analytics workspace or event hub." + } + }, + "storageAccountResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Resource ID of the diagnostic storage account. For security reasons, it is recommended to set diagnostic settings to send data to either storage account, log analytics workspace or event hub." + } + }, + "eventHubAuthorizationRuleResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Resource ID of the diagnostic event hub authorization rule for the Event Hubs namespace in which the event hub should be created or streamed to." + } + }, + "eventHubName": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Name of the diagnostic event hub within the namespace to which logs are streamed. Without this, an event hub is created for each log category. For security reasons, it is recommended to set diagnostic settings to send data to either storage account, log analytics workspace or event hub." + } + }, + "marketplacePartnerResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The full ARM resource ID of the Marketplace resource to which you would like to send Diagnostic Logs." + } + } + }, + "metadata": { + "description": "An AVM-aligned type for a diagnostic setting. To be used if both logs & metrics are supported by the resource provider.", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.2.1" + } + } + }, + "lockType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Specify the name of lock." + } + }, + "kind": { + "type": "string", + "allowedValues": [ + "CanNotDelete", + "None", + "ReadOnly" + ], + "nullable": true, + "metadata": { + "description": "Optional. Specify the type of lock." + } + }, + "notes": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Specify the notes of the lock." + } + } + }, + "metadata": { + "description": "An AVM-aligned type for a lock.", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.0" + } + } + }, + "roleAssignmentType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The name (as GUID) of the role assignment. If not provided, a GUID will be generated." + } + }, + "roleDefinitionIdOrName": { + "type": "string", + "metadata": { + "description": "Required. The role to assign. You can provide either the display name of the role definition, the role definition GUID, or its fully qualified ID in the following format: '/providers/Microsoft.Authorization/roleDefinitions/c2f4ef07-c644-48eb-af81-4b1b4947fb11'." + } + }, + "principalId": { + "type": "string", + "metadata": { + "description": "Required. The principal ID of the principal (user/group/identity) to assign the role to." + } + }, + "principalType": { + "type": "string", + "allowedValues": [ + "Device", + "ForeignGroup", + "Group", + "ServicePrincipal", + "User" + ], + "nullable": true, + "metadata": { + "description": "Optional. The principal type of the assigned principal ID." + } + }, + "description": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The description of the role assignment." + } + }, + "condition": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The conditions on the role assignment. This limits the resources it can be assigned to. e.g.: @Resource[Microsoft.Storage/storageAccounts/blobServices/containers:ContainerName] StringEqualsIgnoreCase \"foo_storage_container\"." + } + }, + "conditionVersion": { + "type": "string", + "allowedValues": [ + "2.0" + ], + "nullable": true, + "metadata": { + "description": "Optional. Version of the condition." + } + }, + "delegatedManagedIdentityResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The Resource Id of the delegated managed identity resource." + } + } + }, + "metadata": { + "description": "An AVM-aligned type for a role assignment.", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.2.1" + } + } + } + }, + "parameters": { + "name": { + "type": "string", + "metadata": { + "description": "Required. The name of the Public IP Address." + } + }, + "publicIpPrefixResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Resource ID of the Public IP Prefix object. This is only needed if you want your Public IPs created in a PIP Prefix." + } + }, + "publicIPAllocationMethod": { + "type": "string", + "defaultValue": "Static", + "allowedValues": [ + "Dynamic", + "Static" + ], + "metadata": { + "description": "Optional. The public IP address allocation method." + } + }, + "availabilityZones": { + "type": "array", + "items": { + "type": "int" + }, + "defaultValue": [ + 1, + 2, + 3 + ], + "allowedValues": [ + 1, + 2, + 3 + ], + "metadata": { + "description": "Optional. A list of availability zones denoting the IP allocated for the resource needs to come from." + } + }, + "publicIPAddressVersion": { + "type": "string", + "defaultValue": "IPv4", + "allowedValues": [ + "IPv4", + "IPv6" + ], + "metadata": { + "description": "Optional. IP address version." + } + }, + "dnsSettings": { + "$ref": "#/definitions/dnsSettingsType", + "nullable": true, + "metadata": { + "description": "Optional. The DNS settings of the public IP address." + } + }, + "ipTags": { + "type": "array", + "items": { + "$ref": "#/definitions/ipTagType" + }, + "nullable": true, + "metadata": { + "description": "Optional. The list of tags associated with the public IP address." + } + }, + "lock": { + "$ref": "#/definitions/lockType", + "nullable": true, + "metadata": { + "description": "Optional. The lock settings of the service." + } + }, + "skuName": { + "type": "string", + "defaultValue": "Standard", + "allowedValues": [ + "Basic", + "Standard" + ], + "metadata": { + "description": "Optional. Name of a public IP address SKU." + } + }, + "skuTier": { + "type": "string", + "defaultValue": "Regional", + "allowedValues": [ + "Global", + "Regional" + ], + "metadata": { + "description": "Optional. Tier of a public IP address SKU." + } + }, + "ddosSettings": { + "$ref": "#/definitions/ddosSettingsType", + "nullable": true, + "metadata": { + "description": "Optional. The DDoS protection plan configuration associated with the public IP address." + } + }, + "location": { + "type": "string", + "defaultValue": "[resourceGroup().location]", + "metadata": { + "description": "Optional. Location for all resources." + } + }, + "roleAssignments": { + "type": "array", + "items": { + "$ref": "#/definitions/roleAssignmentType" + }, + "nullable": true, + "metadata": { + "description": "Optional. Array of role assignments to create." + } + }, + "enableTelemetry": { + "type": "bool", + "defaultValue": true, + "metadata": { + "description": "Optional. Enable/Disable usage telemetry for module." + } + }, + "idleTimeoutInMinutes": { + "type": "int", + "defaultValue": 4, + "metadata": { + "description": "Optional. The idle timeout of the public IP address." + } + }, + "tags": { + "type": "object", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.Network/publicIPAddresses@2024-10-01#properties/tags" + }, + "description": "Optional. Tags of the resource." + }, + "nullable": true + }, + "diagnosticSettings": { + "type": "array", + "items": { + "$ref": "#/definitions/diagnosticSettingFullType" + }, + "nullable": true, + "metadata": { + "description": "Optional. The diagnostic settings of the service." + } + } + }, + "variables": { + "copy": [ + { + "name": "formattedRoleAssignments", + "count": "[length(coalesce(parameters('roleAssignments'), createArray()))]", + "input": "[union(coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')], createObject('roleDefinitionId', coalesce(tryGet(variables('builtInRoleNames'), coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName), if(contains(coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName, '/providers/Microsoft.Authorization/roleDefinitions/'), coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName, subscriptionResourceId('Microsoft.Authorization/roleDefinitions', coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName)))))]" + } + ], + "builtInRoleNames": { + "Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'b24988ac-6180-42a0-ab88-20f7382dd24c')]", + "DNS Resolver Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '0f2ebee7-ffd4-4fc0-b3b7-664099fdad5d')]", + "DNS Zone Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'befefa01-2a29-4197-83a8-272ff33ce314')]", + "Domain Services Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'eeaeda52-9324-47f6-8069-5d5bade478b2')]", + "Domain Services Reader": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '361898ef-9ed1-48c2-849c-a832951106bb')]", + "Network Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '4d97b98b-1d4f-4787-a291-c67834d212e7')]", + "Owner": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '8e3af657-a8ff-443c-a75c-2fe8c4bcb635')]", + "Private DNS Zone Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'b12aa53e-6015-4669-85d0-8515ebb3ae7f')]", + "Reader": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'acdd72a7-3385-48ef-bd42-f606fba81ae7')]", + "Role Based Access Control Administrator": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'f58310d9-a9f6-439a-9e8d-f62e7b41a168')]" + } + }, + "resources": { + "avmTelemetry": { + "condition": "[parameters('enableTelemetry')]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2024-03-01", + "name": "[format('46d3xbcp.res.network-publicipaddress.{0}.{1}', replace('0.9.1', '.', '-'), substring(uniqueString(deployment().name, parameters('location')), 0, 4))]", + "properties": { + "mode": "Incremental", + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "resources": [], + "outputs": { + "telemetry": { + "type": "String", + "value": "For more information, see https://aka.ms/avm/TelemetryInfo" + } + } + } + } + }, + "publicIpAddress": { + "type": "Microsoft.Network/publicIPAddresses", + "apiVersion": "2024-05-01", + "name": "[parameters('name')]", + "location": "[parameters('location')]", + "tags": "[parameters('tags')]", + "sku": { + "name": "[parameters('skuName')]", + "tier": "[parameters('skuTier')]" + }, + "zones": "[map(parameters('availabilityZones'), lambda('zone', string(lambdaVariables('zone'))))]", + "properties": { + "ddosSettings": "[parameters('ddosSettings')]", + "dnsSettings": "[parameters('dnsSettings')]", + "publicIPAddressVersion": "[parameters('publicIPAddressVersion')]", + "publicIPAllocationMethod": "[parameters('publicIPAllocationMethod')]", + "publicIPPrefix": "[if(not(empty(parameters('publicIpPrefixResourceId'))), createObject('id', parameters('publicIpPrefixResourceId')), null())]", + "idleTimeoutInMinutes": "[parameters('idleTimeoutInMinutes')]", + "ipTags": "[parameters('ipTags')]" + } + }, + "publicIpAddress_lock": { + "condition": "[and(not(empty(coalesce(parameters('lock'), createObject()))), not(equals(tryGet(parameters('lock'), 'kind'), 'None')))]", + "type": "Microsoft.Authorization/locks", + "apiVersion": "2020-05-01", + "scope": "[format('Microsoft.Network/publicIPAddresses/{0}', parameters('name'))]", + "name": "[coalesce(tryGet(parameters('lock'), 'name'), format('lock-{0}', parameters('name')))]", + "properties": { + "level": "[coalesce(tryGet(parameters('lock'), 'kind'), '')]", + "notes": "[coalesce(tryGet(parameters('lock'), 'notes'), if(equals(tryGet(parameters('lock'), 'kind'), 'CanNotDelete'), 'Cannot delete resource or child resources.', 'Cannot delete or modify the resource or child resources.'))]" + }, + "dependsOn": [ + "publicIpAddress" + ] + }, + "publicIpAddress_roleAssignments": { + "copy": { + "name": "publicIpAddress_roleAssignments", + "count": "[length(coalesce(variables('formattedRoleAssignments'), createArray()))]" + }, + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2022-04-01", + "scope": "[format('Microsoft.Network/publicIPAddresses/{0}', parameters('name'))]", + "name": "[coalesce(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'name'), guid(resourceId('Microsoft.Network/publicIPAddresses', parameters('name')), coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].principalId, coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].roleDefinitionId))]", + "properties": { + "roleDefinitionId": "[coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].roleDefinitionId]", + "principalId": "[coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].principalId]", + "description": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'description')]", + "principalType": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'principalType')]", + "condition": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'condition')]", + "conditionVersion": "[if(not(empty(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'condition'))), coalesce(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'conditionVersion'), '2.0'), null())]", + "delegatedManagedIdentityResourceId": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'delegatedManagedIdentityResourceId')]" + }, + "dependsOn": [ + "publicIpAddress" + ] + }, + "publicIpAddress_diagnosticSettings": { + "copy": { + "name": "publicIpAddress_diagnosticSettings", + "count": "[length(coalesce(parameters('diagnosticSettings'), createArray()))]" + }, + "type": "Microsoft.Insights/diagnosticSettings", + "apiVersion": "2021-05-01-preview", + "scope": "[format('Microsoft.Network/publicIPAddresses/{0}', parameters('name'))]", + "name": "[coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'name'), format('{0}-diagnosticSettings', parameters('name')))]", + "properties": { + "copy": [ + { + "name": "metrics", + "count": "[length(coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'metricCategories'), createArray(createObject('category', 'AllMetrics'))))]", + "input": { + "category": "[coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'metricCategories'), createArray(createObject('category', 'AllMetrics')))[copyIndex('metrics')].category]", + "enabled": "[coalesce(tryGet(coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'metricCategories'), createArray(createObject('category', 'AllMetrics')))[copyIndex('metrics')], 'enabled'), true())]", + "timeGrain": null + } + }, + { + "name": "logs", + "count": "[length(coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'logCategoriesAndGroups'), createArray(createObject('categoryGroup', 'allLogs'))))]", + "input": { + "categoryGroup": "[tryGet(coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'logCategoriesAndGroups'), createArray(createObject('categoryGroup', 'allLogs')))[copyIndex('logs')], 'categoryGroup')]", + "category": "[tryGet(coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'logCategoriesAndGroups'), createArray(createObject('categoryGroup', 'allLogs')))[copyIndex('logs')], 'category')]", + "enabled": "[coalesce(tryGet(coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'logCategoriesAndGroups'), createArray(createObject('categoryGroup', 'allLogs')))[copyIndex('logs')], 'enabled'), true())]" + } + } + ], + "storageAccountId": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'storageAccountResourceId')]", + "workspaceId": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'workspaceResourceId')]", + "eventHubAuthorizationRuleId": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'eventHubAuthorizationRuleResourceId')]", + "eventHubName": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'eventHubName')]", + "marketplacePartnerId": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'marketplacePartnerResourceId')]", + "logAnalyticsDestinationType": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'logAnalyticsDestinationType')]" + }, + "dependsOn": [ + "publicIpAddress" + ] + } + }, + "outputs": { + "resourceGroupName": { + "type": "string", + "metadata": { + "description": "The resource group the public IP address was deployed into." + }, + "value": "[resourceGroup().name]" + }, + "name": { + "type": "string", + "metadata": { + "description": "The name of the public IP address." + }, + "value": "[parameters('name')]" + }, + "resourceId": { + "type": "string", + "metadata": { + "description": "The resource ID of the public IP address." + }, + "value": "[resourceId('Microsoft.Network/publicIPAddresses', parameters('name'))]" + }, + "ipAddress": { + "type": "string", + "metadata": { + "description": "The public IP address of the public IP address resource." + }, + "value": "[coalesce(tryGet(reference('publicIpAddress'), 'ipAddress'), '')]" + }, + "location": { + "type": "string", + "metadata": { + "description": "The location the resource was deployed into." + }, + "value": "[reference('publicIpAddress', '2024-05-01', 'full').location]" + } + } + } + } + }, + "networkInterface": { + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "[format('{0}-NetworkInterface', deployment().name)]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "name": { + "value": "[parameters('networkInterfaceName')]" + }, + "ipConfigurations": { + "copy": [ + { + "name": "value", + "count": "[length(parameters('ipConfigurations'))]", + "input": "[createObject('name', tryGet(parameters('ipConfigurations')[copyIndex('value')], 'name'), 'privateIPAllocationMethod', tryGet(parameters('ipConfigurations')[copyIndex('value')], 'privateIPAllocationMethod'), 'privateIPAddress', tryGet(parameters('ipConfigurations')[copyIndex('value')], 'privateIPAddress'), 'publicIPAddressResourceId', if(not(empty(tryGet(parameters('ipConfigurations')[copyIndex('value')], 'pipConfiguration'))), if(not(contains(coalesce(tryGet(parameters('ipConfigurations')[copyIndex('value')], 'pipConfiguration'), createObject()), 'publicIPAddressResourceId')), resourceId('Microsoft.Network/publicIPAddresses', coalesce(tryGet(tryGet(parameters('ipConfigurations')[copyIndex('value')], 'pipConfiguration'), 'name'), format('{0}{1}', parameters('virtualMachineName'), tryGet(tryGet(parameters('ipConfigurations')[copyIndex('value')], 'pipConfiguration'), 'publicIpNameSuffix')))), tryGet(parameters('ipConfigurations')[copyIndex('value')], 'pipConfiguration', 'publicIPAddressResourceId')), null()), 'subnetResourceId', parameters('ipConfigurations')[copyIndex('value')].subnetResourceId, 'loadBalancerBackendAddressPools', tryGet(parameters('ipConfigurations')[copyIndex('value')], 'loadBalancerBackendAddressPools'), 'applicationSecurityGroups', tryGet(parameters('ipConfigurations')[copyIndex('value')], 'applicationSecurityGroups'), 'applicationGatewayBackendAddressPools', tryGet(parameters('ipConfigurations')[copyIndex('value')], 'applicationGatewayBackendAddressPools'), 'gatewayLoadBalancer', tryGet(parameters('ipConfigurations')[copyIndex('value')], 'gatewayLoadBalancer'), 'loadBalancerInboundNatRules', tryGet(parameters('ipConfigurations')[copyIndex('value')], 'loadBalancerInboundNatRules'), 'privateIPAddressVersion', tryGet(parameters('ipConfigurations')[copyIndex('value')], 'privateIPAddressVersion'), 'virtualNetworkTaps', tryGet(parameters('ipConfigurations')[copyIndex('value')], 'virtualNetworkTaps'))]" + } + ] + }, + "location": { + "value": "[parameters('location')]" + }, + "tags": { + "value": "[parameters('tags')]" + }, + "diagnosticSettings": { + "value": "[parameters('diagnosticSettings')]" + }, + "dnsServers": { + "value": "[parameters('dnsServers')]" + }, + "enableAcceleratedNetworking": { + "value": "[parameters('enableAcceleratedNetworking')]" + }, + "enableTelemetry": { + "value": "[parameters('enableTelemetry')]" + }, + "enableIPForwarding": { + "value": "[parameters('enableIPForwarding')]" + }, + "lock": { + "value": "[parameters('lock')]" + }, + "networkSecurityGroupResourceId": "[if(not(empty(parameters('networkSecurityGroupResourceId'))), createObject('value', parameters('networkSecurityGroupResourceId')), createObject('value', ''))]", + "roleAssignments": { + "value": "[parameters('roleAssignments')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "languageVersion": "2.0", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.38.5.1644", + "templateHash": "272838238520810437" + }, + "name": "Network Interface", + "description": "This module deploys a Network Interface." + }, + "definitions": { + "networkInterfaceIPConfigurationType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The name of the IP configuration." + } + }, + "privateIPAllocationMethod": { + "type": "string", + "allowedValues": [ + "Dynamic", + "Static" + ], + "nullable": true, + "metadata": { + "description": "Optional. The private IP address allocation method." + } + }, + "privateIPAddress": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The private IP address." + } + }, + "publicIPAddressResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The resource ID of the public IP address." + } + }, + "subnetResourceId": { + "type": "string", + "metadata": { + "description": "Required. The resource ID of the subnet." + } + }, + "loadBalancerBackendAddressPools": { + "type": "array", + "items": { + "$ref": "#/definitions/backendAddressPoolType" + }, + "nullable": true, + "metadata": { + "description": "Optional. Array of load balancer backend address pools." + } + }, + "loadBalancerInboundNatRules": { + "type": "array", + "items": { + "$ref": "#/definitions/inboundNatRuleType" + }, + "nullable": true, + "metadata": { + "description": "Optional. A list of references of LoadBalancerInboundNatRules." + } + }, + "applicationSecurityGroups": { + "type": "array", + "items": { + "$ref": "#/definitions/applicationSecurityGroupType" + }, + "nullable": true, + "metadata": { + "description": "Optional. Application security groups in which the IP configuration is included." + } + }, + "applicationGatewayBackendAddressPools": { + "type": "array", + "items": { + "$ref": "#/definitions/applicationGatewayBackendAddressPoolsType" + }, + "nullable": true, + "metadata": { + "description": "Optional. The reference to Application Gateway Backend Address Pools." + } + }, + "gatewayLoadBalancer": { + "$ref": "#/definitions/subResourceType", + "nullable": true, + "metadata": { + "description": "Optional. The reference to gateway load balancer frontend IP." + } + }, + "privateIPAddressVersion": { + "type": "string", + "allowedValues": [ + "IPv4", + "IPv6" + ], + "nullable": true, + "metadata": { + "description": "Optional. Whether the specific IP configuration is IPv4 or IPv6." + } + }, + "virtualNetworkTaps": { + "type": "array", + "items": { + "$ref": "#/definitions/virtualNetworkTapType" + }, + "nullable": true, + "metadata": { + "description": "Optional. The reference to Virtual Network Taps." + } + } + }, + "metadata": { + "__bicep_export!": true, + "description": "The resource ID of the deployed resource." + } + }, + "backendAddressPoolType": { + "type": "object", + "properties": { + "id": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The resource ID of the backend address pool." + } + }, + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The name of the backend address pool." + } + }, + "properties": { + "type": "object", + "nullable": true, + "metadata": { + "description": "Optional. The properties of the backend address pool." + } + } + }, + "metadata": { + "__bicep_export!": true, + "description": "The type for a backend address pool." + } + }, + "applicationSecurityGroupType": { + "type": "object", + "properties": { + "id": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Resource ID of the application security group." + } + }, + "location": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Location of the application security group." + } + }, + "properties": { + "type": "object", + "nullable": true, + "metadata": { + "description": "Optional. Properties of the application security group." + } + }, + "tags": { + "type": "object", + "nullable": true, + "metadata": { + "description": "Optional. Tags of the application security group." + } + } + }, + "metadata": { + "__bicep_export!": true, + "description": "The type for the application security group." + } + }, + "applicationGatewayBackendAddressPoolsType": { + "type": "object", + "properties": { + "id": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Resource ID of the backend address pool." + } + }, + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Name of the backend address pool that is unique within an Application Gateway." + } + }, + "properties": { + "type": "object", + "properties": { + "backendAddresses": { + "type": "array", + "items": { + "type": "object", + "properties": { + "ipAddress": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. IP address of the backend address." + } + }, + "fqdn": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. FQDN of the backend address." + } + } + } + }, + "nullable": true, + "metadata": { + "description": "Optional. Backend addresses." + } + } + }, + "nullable": true, + "metadata": { + "description": "Optional. Properties of the application gateway backend address pool." + } + } + }, + "metadata": { + "__bicep_export!": true, + "description": "The type for the application gateway backend address pool." + } + }, + "subResourceType": { + "type": "object", + "properties": { + "id": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Resource ID of the sub resource." + } + } + }, + "metadata": { + "__bicep_export!": true, + "description": "The type for the sub resource." + } + }, + "inboundNatRuleType": { + "type": "object", + "properties": { + "id": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Resource ID of the inbound NAT rule." + } + }, + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Name of the resource that is unique within the set of inbound NAT rules used by the load balancer. This name can be used to access the resource." + } + }, + "properties": { + "type": "object", + "properties": { + "backendAddressPool": { + "$ref": "#/definitions/subResourceType", + "nullable": true, + "metadata": { + "description": "Optional. A reference to backendAddressPool resource." + } + }, + "backendPort": { + "type": "int", + "nullable": true, + "metadata": { + "description": "Optional. The port used for the internal endpoint. Acceptable values range from 1 to 65535." + } + }, + "enableFloatingIP": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. Configures a virtual machine's endpoint for the floating IP capability required to configure a SQL AlwaysOn Availability Group. This setting is required when using the SQL AlwaysOn Availability Groups in SQL server. This setting can't be changed after you create the endpoint." + } + }, + "enableTcpReset": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. Receive bidirectional TCP Reset on TCP flow idle timeout or unexpected connection termination. This element is only used when the protocol is set to TCP." + } + }, + "frontendIPConfiguration": { + "$ref": "#/definitions/subResourceType", + "nullable": true, + "metadata": { + "description": "Optional. A reference to frontend IP addresses." + } + }, + "frontendPort": { + "type": "int", + "nullable": true, + "metadata": { + "description": "Optional. The port for the external endpoint. Port numbers for each rule must be unique within the Load Balancer. Acceptable values range from 1 to 65534." + } + }, + "frontendPortRangeStart": { + "type": "int", + "nullable": true, + "metadata": { + "description": "Optional. The port range start for the external endpoint. This property is used together with BackendAddressPool and FrontendPortRangeEnd. Individual inbound NAT rule port mappings will be created for each backend address from BackendAddressPool. Acceptable values range from 1 to 65534." + } + }, + "frontendPortRangeEnd": { + "type": "int", + "nullable": true, + "metadata": { + "description": "Optional. The port range end for the external endpoint. This property is used together with BackendAddressPool and FrontendPortRangeStart. Individual inbound NAT rule port mappings will be created for each backend address from BackendAddressPool. Acceptable values range from 1 to 65534." + } + }, + "protocol": { + "type": "string", + "allowedValues": [ + "All", + "Tcp", + "Udp" + ], + "nullable": true, + "metadata": { + "description": "Optional. The reference to the transport protocol used by the load balancing rule." + } + } + }, + "nullable": true, + "metadata": { + "description": "Optional. Properties of the inbound NAT rule." + } + } + }, + "metadata": { + "__bicep_export!": true, + "description": "The type for the inbound NAT rule." + } + }, + "virtualNetworkTapType": { + "type": "object", + "properties": { + "id": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Resource ID of the virtual network tap." + } + }, + "location": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Location of the virtual network tap." + } + }, + "properties": { + "type": "object", + "nullable": true, + "metadata": { + "description": "Optional. Properties of the virtual network tap." + } + }, + "tags": { + "type": "object", + "nullable": true, + "metadata": { + "description": "Optional. Tags of the virtual network tap." + } + } + }, + "metadata": { + "__bicep_export!": true, + "description": "The type for the virtual network tap." + } + }, + "networkInterfaceIPConfigurationOutputType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "metadata": { + "description": "The name of the IP configuration." + } + }, + "privateIP": { + "type": "string", + "nullable": true, + "metadata": { + "description": "The private IP address." + } + }, + "publicIP": { + "type": "string", + "nullable": true, + "metadata": { + "description": "The public IP address." + } + } + }, + "metadata": { + "__bicep_export!": true, + "description": "The type for the network interface IP configuration output." + } + }, + "diagnosticSettingFullType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The name of the diagnostic setting." + } + }, + "logCategoriesAndGroups": { + "type": "array", + "items": { + "type": "object", + "properties": { + "category": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Name of a Diagnostic Log category for a resource type this setting is applied to. Set the specific logs to collect here." + } + }, + "categoryGroup": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Name of a Diagnostic Log category group for a resource type this setting is applied to. Set to `allLogs` to collect all logs." + } + }, + "enabled": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. Enable or disable the category explicitly. Default is `true`." + } + } + } + }, + "nullable": true, + "metadata": { + "description": "Optional. The name of logs that will be streamed. \"allLogs\" includes all possible logs for the resource. Set to `[]` to disable log collection." + } + }, + "metricCategories": { + "type": "array", + "items": { + "type": "object", + "properties": { + "category": { + "type": "string", + "metadata": { + "description": "Required. Name of a Diagnostic Metric category for a resource type this setting is applied to. Set to `AllMetrics` to collect all metrics." + } + }, + "enabled": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. Enable or disable the category explicitly. Default is `true`." + } + } + } + }, + "nullable": true, + "metadata": { + "description": "Optional. The name of metrics that will be streamed. \"allMetrics\" includes all possible metrics for the resource. Set to `[]` to disable metric collection." + } + }, + "logAnalyticsDestinationType": { + "type": "string", + "allowedValues": [ + "AzureDiagnostics", + "Dedicated" + ], + "nullable": true, + "metadata": { + "description": "Optional. A string indicating whether the export to Log Analytics should use the default destination type, i.e. AzureDiagnostics, or use a destination type." + } + }, + "workspaceResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Resource ID of the diagnostic log analytics workspace. For security reasons, it is recommended to set diagnostic settings to send data to either storage account, log analytics workspace or event hub." + } + }, + "storageAccountResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Resource ID of the diagnostic storage account. For security reasons, it is recommended to set diagnostic settings to send data to either storage account, log analytics workspace or event hub." + } + }, + "eventHubAuthorizationRuleResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Resource ID of the diagnostic event hub authorization rule for the Event Hubs namespace in which the event hub should be created or streamed to." + } + }, + "eventHubName": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Name of the diagnostic event hub within the namespace to which logs are streamed. Without this, an event hub is created for each log category. For security reasons, it is recommended to set diagnostic settings to send data to either storage account, log analytics workspace or event hub." + } + }, + "marketplacePartnerResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The full ARM resource ID of the Marketplace resource to which you would like to send Diagnostic Logs." + } + } + }, + "metadata": { + "description": "An AVM-aligned type for a diagnostic setting. To be used if both logs & metrics are supported by the resource provider.", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.5.1" + } + } + }, + "lockType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Specify the name of lock." + } + }, + "kind": { + "type": "string", + "allowedValues": [ + "CanNotDelete", + "None", + "ReadOnly" + ], + "nullable": true, + "metadata": { + "description": "Optional. Specify the type of lock." + } + }, + "notes": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Specify the notes of the lock." + } + } + }, + "metadata": { + "description": "An AVM-aligned type for a lock.", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.0" + } + } + }, + "roleAssignmentType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The name (as GUID) of the role assignment. If not provided, a GUID will be generated." + } + }, + "roleDefinitionIdOrName": { + "type": "string", + "metadata": { + "description": "Required. The role to assign. You can provide either the display name of the role definition, the role definition GUID, or its fully qualified ID in the following format: '/providers/Microsoft.Authorization/roleDefinitions/c2f4ef07-c644-48eb-af81-4b1b4947fb11'." + } + }, + "principalId": { + "type": "string", + "metadata": { + "description": "Required. The principal ID of the principal (user/group/identity) to assign the role to." + } + }, + "principalType": { + "type": "string", + "allowedValues": [ + "Device", + "ForeignGroup", + "Group", + "ServicePrincipal", + "User" + ], + "nullable": true, + "metadata": { + "description": "Optional. The principal type of the assigned principal ID." + } + }, + "description": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The description of the role assignment." + } + }, + "condition": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The conditions on the role assignment. This limits the resources it can be assigned to. e.g.: @Resource[Microsoft.Storage/storageAccounts/blobServices/containers:ContainerName] StringEqualsIgnoreCase \"foo_storage_container\"." + } + }, + "conditionVersion": { + "type": "string", + "allowedValues": [ + "2.0" + ], + "nullable": true, + "metadata": { + "description": "Optional. Version of the condition." + } + }, + "delegatedManagedIdentityResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The Resource Id of the delegated managed identity resource." + } + } + }, + "metadata": { + "description": "An AVM-aligned type for a role assignment.", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.5.1" + } + } + } + }, + "parameters": { + "name": { + "type": "string", + "metadata": { + "description": "Required. The name of the network interface." + } + }, + "location": { + "type": "string", + "defaultValue": "[resourceGroup().location]", + "metadata": { + "description": "Optional. Location for all resources." + } + }, + "tags": { + "type": "object", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.Network/networkInterfaces@2024-07-01#properties/tags" + }, + "description": "Optional. Resource tags." + }, + "nullable": true + }, + "enableTelemetry": { + "type": "bool", + "defaultValue": true, + "metadata": { + "description": "Optional. Enable/Disable usage telemetry for module." + } + }, + "enableIPForwarding": { + "type": "bool", + "defaultValue": false, + "metadata": { + "description": "Optional. Indicates whether IP forwarding is enabled on this network interface." + } + }, + "enableAcceleratedNetworking": { + "type": "bool", + "defaultValue": false, + "metadata": { + "description": "Optional. If the network interface is accelerated networking enabled." + } + }, + "dnsServers": { + "type": "array", + "items": { + "type": "string" + }, + "defaultValue": [], + "metadata": { + "description": "Optional. List of DNS servers IP addresses. Use 'AzureProvidedDNS' to switch to azure provided DNS resolution. 'AzureProvidedDNS' value cannot be combined with other IPs, it must be the only value in dnsServers collection." + } + }, + "networkSecurityGroupResourceId": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "Optional. The network security group (NSG) to attach to the network interface." + } + }, + "auxiliaryMode": { + "type": "string", + "defaultValue": "None", + "allowedValues": [ + "Floating", + "MaxConnections", + "None" + ], + "metadata": { + "description": "Optional. Auxiliary mode of Network Interface resource. Not all regions are enabled for Auxiliary Mode Nic." + } + }, + "auxiliarySku": { + "type": "string", + "defaultValue": "None", + "allowedValues": [ + "A1", + "A2", + "A4", + "A8", + "None" + ], + "metadata": { + "description": "Optional. Auxiliary sku of Network Interface resource. Not all regions are enabled for Auxiliary Mode Nic." + } + }, + "disableTcpStateTracking": { + "type": "bool", + "defaultValue": false, + "metadata": { + "description": "Optional. Indicates whether to disable tcp state tracking. Subscription must be registered for the Microsoft.Network/AllowDisableTcpStateTracking feature before this property can be set to true." + } + }, + "ipConfigurations": { + "type": "array", + "items": { + "$ref": "#/definitions/networkInterfaceIPConfigurationType" + }, + "metadata": { + "description": "Required. A list of IPConfigurations of the network interface." + } + }, + "lock": { + "$ref": "#/definitions/lockType", + "nullable": true, + "metadata": { + "description": "Optional. The lock settings of the service." + } + }, + "roleAssignments": { + "type": "array", + "items": { + "$ref": "#/definitions/roleAssignmentType" + }, + "nullable": true, + "metadata": { + "description": "Optional. Array of role assignments to create." + } + }, + "diagnosticSettings": { + "type": "array", + "items": { + "$ref": "#/definitions/diagnosticSettingFullType" + }, + "nullable": true, + "metadata": { + "description": "Optional. The diagnostic settings of the service." + } + } + }, + "variables": { + "copy": [ + { + "name": "formattedRoleAssignments", + "count": "[length(coalesce(parameters('roleAssignments'), createArray()))]", + "input": "[union(coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')], createObject('roleDefinitionId', coalesce(tryGet(variables('builtInRoleNames'), coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName), if(contains(coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName, '/providers/Microsoft.Authorization/roleDefinitions/'), coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName, subscriptionResourceId('Microsoft.Authorization/roleDefinitions', coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName)))))]" + } + ], + "builtInRoleNames": { + "Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'b24988ac-6180-42a0-ab88-20f7382dd24c')]", + "DNS Resolver Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '0f2ebee7-ffd4-4fc0-b3b7-664099fdad5d')]", + "DNS Zone Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'befefa01-2a29-4197-83a8-272ff33ce314')]", + "Network Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '4d97b98b-1d4f-4787-a291-c67834d212e7')]", + "Owner": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '8e3af657-a8ff-443c-a75c-2fe8c4bcb635')]", + "Private DNS Zone Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'b12aa53e-6015-4669-85d0-8515ebb3ae7f')]", + "Reader": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'acdd72a7-3385-48ef-bd42-f606fba81ae7')]", + "Role Based Access Control Administrator": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'f58310d9-a9f6-439a-9e8d-f62e7b41a168')]" + } + }, + "resources": { + "publicIp": { + "copy": { + "name": "publicIp", + "count": "[length(parameters('ipConfigurations'))]" + }, + "condition": "[and(contains(parameters('ipConfigurations')[copyIndex()], 'publicIPAddressResourceId'), not(equals(tryGet(parameters('ipConfigurations')[copyIndex()], 'publicIPAddressResourceId'), null())))]", + "existing": true, + "type": "Microsoft.Network/publicIPAddresses", + "apiVersion": "2024-05-01", + "resourceGroup": "[split(coalesce(tryGet(parameters('ipConfigurations')[copyIndex()], 'publicIPAddressResourceId'), ''), '/')[4]]", + "name": "[last(split(coalesce(tryGet(parameters('ipConfigurations')[copyIndex()], 'publicIPAddressResourceId'), ''), '/'))]" + }, + "avmTelemetry": { + "condition": "[parameters('enableTelemetry')]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2024-03-01", + "name": "[format('46d3xbcp.res.network-networkinterface.{0}.{1}', replace('0.5.3', '.', '-'), substring(uniqueString(deployment().name, parameters('location')), 0, 4))]", + "properties": { + "mode": "Incremental", + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "resources": [], + "outputs": { + "telemetry": { + "type": "String", + "value": "For more information, see https://aka.ms/avm/TelemetryInfo" + } + } + } + } + }, + "networkInterface": { + "type": "Microsoft.Network/networkInterfaces", + "apiVersion": "2024-05-01", + "name": "[parameters('name')]", + "location": "[parameters('location')]", + "tags": "[parameters('tags')]", + "properties": { + "copy": [ + { + "name": "ipConfigurations", + "count": "[length(parameters('ipConfigurations'))]", + "input": { + "name": "[coalesce(tryGet(parameters('ipConfigurations')[copyIndex('ipConfigurations')], 'name'), format('ipconfig{0}', padLeft(add(copyIndex('ipConfigurations'), 1), 2, '0')))]", + "properties": { + "primary": "[if(equals(copyIndex('ipConfigurations'), 0), true(), false())]", + "privateIPAllocationMethod": "[tryGet(parameters('ipConfigurations')[copyIndex('ipConfigurations')], 'privateIPAllocationMethod')]", + "privateIPAddress": "[tryGet(parameters('ipConfigurations')[copyIndex('ipConfigurations')], 'privateIPAddress')]", + "publicIPAddress": "[if(contains(parameters('ipConfigurations')[copyIndex('ipConfigurations')], 'publicIPAddressResourceId'), if(not(equals(tryGet(parameters('ipConfigurations')[copyIndex('ipConfigurations')], 'publicIPAddressResourceId'), null())), createObject('id', tryGet(parameters('ipConfigurations')[copyIndex('ipConfigurations')], 'publicIPAddressResourceId')), null()), null())]", + "subnet": { + "id": "[parameters('ipConfigurations')[copyIndex('ipConfigurations')].subnetResourceId]" + }, + "loadBalancerBackendAddressPools": "[tryGet(parameters('ipConfigurations')[copyIndex('ipConfigurations')], 'loadBalancerBackendAddressPools')]", + "applicationSecurityGroups": "[tryGet(parameters('ipConfigurations')[copyIndex('ipConfigurations')], 'applicationSecurityGroups')]", + "applicationGatewayBackendAddressPools": "[tryGet(parameters('ipConfigurations')[copyIndex('ipConfigurations')], 'applicationGatewayBackendAddressPools')]", + "gatewayLoadBalancer": "[tryGet(parameters('ipConfigurations')[copyIndex('ipConfigurations')], 'gatewayLoadBalancer')]", + "loadBalancerInboundNatRules": "[tryGet(parameters('ipConfigurations')[copyIndex('ipConfigurations')], 'loadBalancerInboundNatRules')]", + "privateIPAddressVersion": "[tryGet(parameters('ipConfigurations')[copyIndex('ipConfigurations')], 'privateIPAddressVersion')]", + "virtualNetworkTaps": "[tryGet(parameters('ipConfigurations')[copyIndex('ipConfigurations')], 'virtualNetworkTaps')]" + } + } + } + ], + "auxiliaryMode": "[parameters('auxiliaryMode')]", + "auxiliarySku": "[parameters('auxiliarySku')]", + "disableTcpStateTracking": "[parameters('disableTcpStateTracking')]", + "dnsSettings": "[if(not(empty(parameters('dnsServers'))), createObject('dnsServers', parameters('dnsServers')), null())]", + "enableAcceleratedNetworking": "[parameters('enableAcceleratedNetworking')]", + "enableIPForwarding": "[parameters('enableIPForwarding')]", + "networkSecurityGroup": "[if(not(empty(parameters('networkSecurityGroupResourceId'))), createObject('id', parameters('networkSecurityGroupResourceId')), null())]" + } + }, + "networkInterface_lock": { + "condition": "[and(not(empty(coalesce(parameters('lock'), createObject()))), not(equals(tryGet(parameters('lock'), 'kind'), 'None')))]", + "type": "Microsoft.Authorization/locks", + "apiVersion": "2020-05-01", + "scope": "[format('Microsoft.Network/networkInterfaces/{0}', parameters('name'))]", + "name": "[coalesce(tryGet(parameters('lock'), 'name'), format('lock-{0}', parameters('name')))]", + "properties": { + "level": "[coalesce(tryGet(parameters('lock'), 'kind'), '')]", + "notes": "[coalesce(tryGet(parameters('lock'), 'notes'), if(equals(tryGet(parameters('lock'), 'kind'), 'CanNotDelete'), 'Cannot delete resource or child resources.', 'Cannot delete or modify the resource or child resources.'))]" + }, + "dependsOn": [ + "networkInterface" + ] + }, + "networkInterface_diagnosticSettings": { + "copy": { + "name": "networkInterface_diagnosticSettings", + "count": "[length(coalesce(parameters('diagnosticSettings'), createArray()))]" + }, + "type": "Microsoft.Insights/diagnosticSettings", + "apiVersion": "2021-05-01-preview", + "scope": "[format('Microsoft.Network/networkInterfaces/{0}', parameters('name'))]", + "name": "[coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'name'), format('{0}-diagnosticSettings', parameters('name')))]", + "properties": { + "copy": [ + { + "name": "metrics", + "count": "[length(coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'metricCategories'), createArray(createObject('category', 'AllMetrics'))))]", + "input": { + "category": "[coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'metricCategories'), createArray(createObject('category', 'AllMetrics')))[copyIndex('metrics')].category]", + "enabled": "[coalesce(tryGet(coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'metricCategories'), createArray(createObject('category', 'AllMetrics')))[copyIndex('metrics')], 'enabled'), true())]", + "timeGrain": null + } + } + ], + "storageAccountId": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'storageAccountResourceId')]", + "workspaceId": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'workspaceResourceId')]", + "eventHubAuthorizationRuleId": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'eventHubAuthorizationRuleResourceId')]", + "eventHubName": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'eventHubName')]", + "marketplacePartnerId": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'marketplacePartnerResourceId')]", + "logAnalyticsDestinationType": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'logAnalyticsDestinationType')]" + }, + "dependsOn": [ + "networkInterface" + ] + }, + "networkInterface_roleAssignments": { + "copy": { + "name": "networkInterface_roleAssignments", + "count": "[length(coalesce(variables('formattedRoleAssignments'), createArray()))]" + }, + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2022-04-01", + "scope": "[format('Microsoft.Network/networkInterfaces/{0}', parameters('name'))]", + "name": "[coalesce(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'name'), guid(resourceId('Microsoft.Network/networkInterfaces', parameters('name')), coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].principalId, coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].roleDefinitionId))]", + "properties": { + "roleDefinitionId": "[coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].roleDefinitionId]", + "principalId": "[coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].principalId]", + "description": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'description')]", + "principalType": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'principalType')]", + "condition": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'condition')]", + "conditionVersion": "[if(not(empty(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'condition'))), coalesce(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'conditionVersion'), '2.0'), null())]", + "delegatedManagedIdentityResourceId": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'delegatedManagedIdentityResourceId')]" + }, + "dependsOn": [ + "networkInterface" + ] + } + }, + "outputs": { + "name": { + "type": "string", + "metadata": { + "description": "The name of the deployed resource." + }, + "value": "[parameters('name')]" + }, + "resourceId": { + "type": "string", + "metadata": { + "description": "The resource ID of the deployed resource." + }, + "value": "[resourceId('Microsoft.Network/networkInterfaces', parameters('name'))]" + }, + "resourceGroupName": { + "type": "string", + "metadata": { + "description": "The resource group of the deployed resource." + }, + "value": "[resourceGroup().name]" + }, + "location": { + "type": "string", + "metadata": { + "description": "The location the resource was deployed into." + }, + "value": "[reference('networkInterface', '2024-05-01', 'full').location]" + }, + "ipConfigurations": { + "type": "array", + "items": { + "$ref": "#/definitions/networkInterfaceIPConfigurationOutputType" + }, + "metadata": { + "description": "The list of IP configurations of the network interface." + }, + "copy": { + "count": "[length(parameters('ipConfigurations'))]", + "input": { + "name": "[reference('networkInterface').ipConfigurations[copyIndex()].name]", + "privateIP": "[coalesce(tryGet(reference('networkInterface').ipConfigurations[copyIndex()].properties, 'privateIPAddress'), '')]", + "publicIP": "[if(and(contains(parameters('ipConfigurations')[copyIndex()], 'publicIPAddressResourceId'), not(equals(tryGet(parameters('ipConfigurations')[copyIndex()], 'publicIPAddressResourceId'), null()))), coalesce(reference(format('publicIp[{0}]', copyIndex())).ipAddress, ''), '')]" + } + } + } + } + } + }, + "dependsOn": [ + "networkInterface_publicIPAddresses" + ] + } + }, + "outputs": { + "name": { + "type": "string", + "metadata": { + "description": "The name of the network interface." + }, + "value": "[reference('networkInterface').outputs.name.value]" + }, + "ipConfigurations": { + "type": "array", + "items": { + "$ref": "#/definitions/networkInterfaceIPConfigurationOutputType" + }, + "metadata": { + "description": "The list of IP configurations of the network interface." + }, + "value": "[reference('networkInterface').outputs.ipConfigurations.value]" + } + } + } } }, - "tags": { - "type": "object", - "metadata": { - "__bicep_resource_derived_type!": { - "source": "Microsoft.Storage/storageAccounts@2025-01-01#properties/tags" + "vm_domainJoinExtension": { + "condition": "[and(contains(parameters('extensionDomainJoinConfig'), 'enabled'), parameters('extensionDomainJoinConfig').enabled)]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "[format('{0}-VM-DomainJoin', uniqueString(deployment().name, parameters('location')))]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" }, - "description": "Optional. Tags of the resource." - }, - "nullable": true - }, - "enableTelemetry": { - "type": "bool", - "defaultValue": true, - "metadata": { - "description": "Optional. Enable/Disable usage telemetry for module." - } - }, - "allowedCopyScope": { - "type": "string", - "nullable": true, - "allowedValues": [ - "AAD", - "PrivateLink" - ], - "metadata": { - "description": "Optional. Restrict copy to and from Storage Accounts within an AAD tenant or with Private Links to the same VNet." - } - }, - "publicNetworkAccess": { - "type": "string", - "nullable": true, - "allowedValues": [ - "Enabled", - "Disabled", - "SecuredByPerimeter" - ], - "metadata": { - "description": "Optional. Whether or not public network access is allowed for this resource. For security reasons it should be disabled. If not specified, it will be disabled by default if private endpoints are set and networkAcls are not set." - } - }, - "supportsHttpsTrafficOnly": { - "type": "bool", - "defaultValue": true, - "metadata": { - "description": "Optional. Allows HTTPS traffic only to storage service if sets to true." - } - }, - "customerManagedKey": { - "$ref": "#/definitions/customerManagedKeyWithAutoRotateType", - "nullable": true, - "metadata": { - "description": "Optional. The customer managed key definition." - } - }, - "sasExpirationPeriod": { - "type": "string", - "defaultValue": "", - "metadata": { - "description": "Optional. The SAS expiration period. DD.HH:MM:SS." - } - }, - "sasExpirationAction": { - "type": "string", - "defaultValue": "Log", - "allowedValues": [ - "Block", - "Log" - ], - "metadata": { - "description": "Optional. The SAS expiration action. Allowed values are Block and Log." - } - }, - "keyType": { - "type": "string", - "nullable": true, - "allowedValues": [ - "Account", - "Service" - ], - "metadata": { - "description": "Optional. The keyType to use with Queue & Table services." - } - }, - "secretsExportConfiguration": { - "$ref": "#/definitions/secretsExportConfigurationType", - "nullable": true, - "metadata": { - "description": "Optional. Key vault reference and secret settings for the module's secrets export." - } - }, - "immutableStorageWithVersioning": { - "type": "object", - "metadata": { - "__bicep_resource_derived_type!": { - "source": "Microsoft.Storage/storageAccounts@2025-01-01#properties/properties/properties/immutableStorageWithVersioning" + "mode": "Incremental", + "parameters": { + "virtualMachineName": { + "value": "[parameters('name')]" + }, + "name": { + "value": "[coalesce(tryGet(parameters('extensionDomainJoinConfig'), 'name'), 'DomainJoin')]" + }, + "location": { + "value": "[parameters('location')]" + }, + "publisher": { + "value": "Microsoft.Compute" + }, + "type": { + "value": "JsonADDomainExtension" + }, + "typeHandlerVersion": { + "value": "[coalesce(tryGet(parameters('extensionDomainJoinConfig'), 'typeHandlerVersion'), '1.3')]" + }, + "autoUpgradeMinorVersion": { + "value": "[coalesce(tryGet(parameters('extensionDomainJoinConfig'), 'autoUpgradeMinorVersion'), true())]" + }, + "enableAutomaticUpgrade": { + "value": "[coalesce(tryGet(parameters('extensionDomainJoinConfig'), 'enableAutomaticUpgrade'), false())]" + }, + "settings": { + "value": "[parameters('extensionDomainJoinConfig').settings]" + }, + "supressFailures": { + "value": "[coalesce(tryGet(parameters('extensionDomainJoinConfig'), 'supressFailures'), false())]" + }, + "tags": { + "value": "[coalesce(tryGet(parameters('extensionDomainJoinConfig'), 'tags'), parameters('tags'))]" + }, + "protectedSettings": { + "value": { + "Password": "[parameters('extensionDomainJoinPassword')]" + } + } }, - "description": "Optional. The property is immutable and can only be set to true at the account creation time. When set to true, it enables object level immutability for all the new containers in the account by default. Cannot be enabled for ADLS Gen2 storage accounts." + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "languageVersion": "2.0", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.39.26.7824", + "templateHash": "8391598897118491777" + }, + "name": "Virtual Machine Extensions", + "description": "This module deploys a Virtual Machine Extension." + }, + "parameters": { + "virtualMachineName": { + "type": "string", + "metadata": { + "description": "Conditional. The name of the parent virtual machine that extension is provisioned for. Required if the template is used in a standalone deployment." + } + }, + "name": { + "type": "string", + "metadata": { + "description": "Required. The name of the virtual machine extension." + } + }, + "location": { + "type": "string", + "defaultValue": "[resourceGroup().location]", + "metadata": { + "description": "Optional. The location the extension is deployed to." + } + }, + "publisher": { + "type": "string", + "metadata": { + "description": "Required. The name of the extension handler publisher." + } + }, + "type": { + "type": "string", + "metadata": { + "description": "Required. Specifies the type of the extension; an example is \"CustomScriptExtension\"." + } + }, + "typeHandlerVersion": { + "type": "string", + "metadata": { + "description": "Required. Specifies the version of the script handler." + } + }, + "autoUpgradeMinorVersion": { + "type": "bool", + "metadata": { + "description": "Required. Indicates whether the extension should use a newer minor version if one is available at deployment time. Once deployed, however, the extension will not upgrade minor versions unless redeployed, even with this property set to true." + } + }, + "forceUpdateTag": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. How the extension handler should be forced to update even if the extension configuration has not changed." + } + }, + "settings": { + "type": "object", + "nullable": true, + "metadata": { + "description": "Optional. Any object that contains the extension specific settings." + } + }, + "protectedSettings": { + "type": "secureObject", + "nullable": true, + "metadata": { + "description": "Optional. Any object that contains the extension specific protected settings." + } + }, + "supressFailures": { + "type": "bool", + "defaultValue": false, + "metadata": { + "description": "Optional. Indicates whether failures stemming from the extension will be suppressed (Operational failures such as not connecting to the VM will not be suppressed regardless of this value). The default is false." + } + }, + "enableAutomaticUpgrade": { + "type": "bool", + "metadata": { + "description": "Required. Indicates whether the extension should be automatically upgraded by the platform if there is a newer version of the extension available." + } + }, + "tags": { + "type": "object", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.Compute/virtualMachines/extensions@2024-11-01#properties/tags" + }, + "description": "Optional. Tags of the resource." + }, + "nullable": true + }, + "protectedSettingsFromKeyVault": { + "type": "object", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.Compute/virtualMachines/extensions@2024-11-01#properties/properties/properties/protectedSettingsFromKeyVault" + }, + "description": "Optional. The extensions protected settings that are passed by reference, and consumed from key vault." + }, + "nullable": true + }, + "provisionAfterExtensions": { + "type": "array", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.Compute/virtualMachines/extensions@2024-11-01#properties/properties/properties/provisionAfterExtensions" + }, + "description": "Optional. Collection of extension names after which this extension needs to be provisioned." + }, + "nullable": true + } + }, + "resources": { + "virtualMachine": { + "existing": true, + "type": "Microsoft.Compute/virtualMachines", + "apiVersion": "2024-11-01", + "name": "[parameters('virtualMachineName')]" + }, + "extension": { + "type": "Microsoft.Compute/virtualMachines/extensions", + "apiVersion": "2024-11-01", + "name": "[format('{0}/{1}', parameters('virtualMachineName'), parameters('name'))]", + "location": "[parameters('location')]", + "tags": "[parameters('tags')]", + "properties": { + "publisher": "[parameters('publisher')]", + "type": "[parameters('type')]", + "typeHandlerVersion": "[parameters('typeHandlerVersion')]", + "autoUpgradeMinorVersion": "[parameters('autoUpgradeMinorVersion')]", + "enableAutomaticUpgrade": "[parameters('enableAutomaticUpgrade')]", + "forceUpdateTag": "[parameters('forceUpdateTag')]", + "settings": "[parameters('settings')]", + "protectedSettings": "[parameters('protectedSettings')]", + "suppressFailures": "[parameters('supressFailures')]", + "protectedSettingsFromKeyVault": "[parameters('protectedSettingsFromKeyVault')]", + "provisionAfterExtensions": "[parameters('provisionAfterExtensions')]" + } + } + }, + "outputs": { + "name": { + "type": "string", + "metadata": { + "description": "The name of the extension." + }, + "value": "[parameters('name')]" + }, + "resourceId": { + "type": "string", + "metadata": { + "description": "The resource ID of the extension." + }, + "value": "[resourceId('Microsoft.Compute/virtualMachines/extensions', parameters('virtualMachineName'), parameters('name'))]" + }, + "resourceGroupName": { + "type": "string", + "metadata": { + "description": "The name of the Resource Group the extension was created in." + }, + "value": "[resourceGroup().name]" + }, + "location": { + "type": "string", + "metadata": { + "description": "The location the resource was deployed into." + }, + "value": "[reference('extension', '2024-11-01', 'full').location]" + } + } + } }, - "nullable": true + "dependsOn": [ + "vm" + ] }, - "objectReplicationPolicies": { - "type": "array", - "items": { - "$ref": "#/definitions/objectReplicationPolicyType" + "vm_aadJoinExtension": { + "condition": "[parameters('extensionAadJoinConfig').enabled]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "[format('{0}-VM-AADLogin', uniqueString(deployment().name, parameters('location')))]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "virtualMachineName": { + "value": "[parameters('name')]" + }, + "name": { + "value": "[coalesce(tryGet(parameters('extensionAadJoinConfig'), 'name'), 'AADLogin')]" + }, + "location": { + "value": "[parameters('location')]" + }, + "publisher": { + "value": "Microsoft.Azure.ActiveDirectory" + }, + "type": "[if(equals(parameters('osType'), 'Windows'), createObject('value', 'AADLoginForWindows'), createObject('value', 'AADSSHLoginforLinux'))]", + "typeHandlerVersion": { + "value": "[coalesce(tryGet(parameters('extensionAadJoinConfig'), 'typeHandlerVersion'), if(equals(parameters('osType'), 'Windows'), '2.0', '1.0'))]" + }, + "autoUpgradeMinorVersion": { + "value": "[coalesce(tryGet(parameters('extensionAadJoinConfig'), 'autoUpgradeMinorVersion'), true())]" + }, + "enableAutomaticUpgrade": { + "value": "[coalesce(tryGet(parameters('extensionAadJoinConfig'), 'enableAutomaticUpgrade'), false())]" + }, + "settings": { + "value": "[coalesce(tryGet(parameters('extensionAadJoinConfig'), 'settings'), createObject())]" + }, + "supressFailures": { + "value": "[coalesce(tryGet(parameters('extensionAadJoinConfig'), 'supressFailures'), false())]" + }, + "tags": { + "value": "[coalesce(tryGet(parameters('extensionAadJoinConfig'), 'tags'), parameters('tags'))]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "languageVersion": "2.0", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.39.26.7824", + "templateHash": "8391598897118491777" + }, + "name": "Virtual Machine Extensions", + "description": "This module deploys a Virtual Machine Extension." + }, + "parameters": { + "virtualMachineName": { + "type": "string", + "metadata": { + "description": "Conditional. The name of the parent virtual machine that extension is provisioned for. Required if the template is used in a standalone deployment." + } + }, + "name": { + "type": "string", + "metadata": { + "description": "Required. The name of the virtual machine extension." + } + }, + "location": { + "type": "string", + "defaultValue": "[resourceGroup().location]", + "metadata": { + "description": "Optional. The location the extension is deployed to." + } + }, + "publisher": { + "type": "string", + "metadata": { + "description": "Required. The name of the extension handler publisher." + } + }, + "type": { + "type": "string", + "metadata": { + "description": "Required. Specifies the type of the extension; an example is \"CustomScriptExtension\"." + } + }, + "typeHandlerVersion": { + "type": "string", + "metadata": { + "description": "Required. Specifies the version of the script handler." + } + }, + "autoUpgradeMinorVersion": { + "type": "bool", + "metadata": { + "description": "Required. Indicates whether the extension should use a newer minor version if one is available at deployment time. Once deployed, however, the extension will not upgrade minor versions unless redeployed, even with this property set to true." + } + }, + "forceUpdateTag": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. How the extension handler should be forced to update even if the extension configuration has not changed." + } + }, + "settings": { + "type": "object", + "nullable": true, + "metadata": { + "description": "Optional. Any object that contains the extension specific settings." + } + }, + "protectedSettings": { + "type": "secureObject", + "nullable": true, + "metadata": { + "description": "Optional. Any object that contains the extension specific protected settings." + } + }, + "supressFailures": { + "type": "bool", + "defaultValue": false, + "metadata": { + "description": "Optional. Indicates whether failures stemming from the extension will be suppressed (Operational failures such as not connecting to the VM will not be suppressed regardless of this value). The default is false." + } + }, + "enableAutomaticUpgrade": { + "type": "bool", + "metadata": { + "description": "Required. Indicates whether the extension should be automatically upgraded by the platform if there is a newer version of the extension available." + } + }, + "tags": { + "type": "object", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.Compute/virtualMachines/extensions@2024-11-01#properties/tags" + }, + "description": "Optional. Tags of the resource." + }, + "nullable": true + }, + "protectedSettingsFromKeyVault": { + "type": "object", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.Compute/virtualMachines/extensions@2024-11-01#properties/properties/properties/protectedSettingsFromKeyVault" + }, + "description": "Optional. The extensions protected settings that are passed by reference, and consumed from key vault." + }, + "nullable": true + }, + "provisionAfterExtensions": { + "type": "array", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.Compute/virtualMachines/extensions@2024-11-01#properties/properties/properties/provisionAfterExtensions" + }, + "description": "Optional. Collection of extension names after which this extension needs to be provisioned." + }, + "nullable": true + } + }, + "resources": { + "virtualMachine": { + "existing": true, + "type": "Microsoft.Compute/virtualMachines", + "apiVersion": "2024-11-01", + "name": "[parameters('virtualMachineName')]" + }, + "extension": { + "type": "Microsoft.Compute/virtualMachines/extensions", + "apiVersion": "2024-11-01", + "name": "[format('{0}/{1}', parameters('virtualMachineName'), parameters('name'))]", + "location": "[parameters('location')]", + "tags": "[parameters('tags')]", + "properties": { + "publisher": "[parameters('publisher')]", + "type": "[parameters('type')]", + "typeHandlerVersion": "[parameters('typeHandlerVersion')]", + "autoUpgradeMinorVersion": "[parameters('autoUpgradeMinorVersion')]", + "enableAutomaticUpgrade": "[parameters('enableAutomaticUpgrade')]", + "forceUpdateTag": "[parameters('forceUpdateTag')]", + "settings": "[parameters('settings')]", + "protectedSettings": "[parameters('protectedSettings')]", + "suppressFailures": "[parameters('supressFailures')]", + "protectedSettingsFromKeyVault": "[parameters('protectedSettingsFromKeyVault')]", + "provisionAfterExtensions": "[parameters('provisionAfterExtensions')]" + } + } + }, + "outputs": { + "name": { + "type": "string", + "metadata": { + "description": "The name of the extension." + }, + "value": "[parameters('name')]" + }, + "resourceId": { + "type": "string", + "metadata": { + "description": "The resource ID of the extension." + }, + "value": "[resourceId('Microsoft.Compute/virtualMachines/extensions', parameters('virtualMachineName'), parameters('name'))]" + }, + "resourceGroupName": { + "type": "string", + "metadata": { + "description": "The name of the Resource Group the extension was created in." + }, + "value": "[resourceGroup().name]" + }, + "location": { + "type": "string", + "metadata": { + "description": "The location the resource was deployed into." + }, + "value": "[reference('extension', '2024-11-01', 'full').location]" + } + } + } }, - "nullable": true, - "metadata": { - "description": "Optional. Object replication policies for the storage account." - } - } - }, - "variables": { - "copy": [ - { - "name": "formattedRoleAssignments", - "count": "[length(coalesce(parameters('roleAssignments'), createArray()))]", - "input": "[union(coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')], createObject('roleDefinitionId', coalesce(tryGet(variables('builtInRoleNames'), coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName), if(contains(coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName, '/providers/Microsoft.Authorization/roleDefinitions/'), coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName, subscriptionResourceId('Microsoft.Authorization/roleDefinitions', coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName)))))]" - } - ], - "enableReferencedModulesTelemetry": false, - "immutabilityValidation": "[if(and(equals(parameters('enableHierarchicalNamespace'), true()), not(empty(parameters('immutableStorageWithVersioning')))), fail('Configuration error: Immutable storage with versioning cannot be enabled when hierarchical namespace is enabled.'), null())]", - "supportsBlobService": "[or(or(or(equals(parameters('kind'), 'BlockBlobStorage'), equals(parameters('kind'), 'BlobStorage')), equals(parameters('kind'), 'StorageV2')), equals(parameters('kind'), 'Storage'))]", - "supportsFileService": "[or(or(equals(parameters('kind'), 'FileStorage'), equals(parameters('kind'), 'StorageV2')), equals(parameters('kind'), 'Storage'))]", - "formattedUserAssignedIdentities": "[reduce(map(coalesce(tryGet(parameters('managedIdentities'), 'userAssignedResourceIds'), createArray()), lambda('id', createObject(format('{0}', lambdaVariables('id')), createObject()))), createObject(), lambda('cur', 'next', union(lambdaVariables('cur'), lambdaVariables('next'))))]", - "identity": "[if(not(empty(parameters('managedIdentities'))), createObject('type', if(coalesce(tryGet(parameters('managedIdentities'), 'systemAssigned'), false()), if(not(empty(coalesce(tryGet(parameters('managedIdentities'), 'userAssignedResourceIds'), createObject()))), 'SystemAssigned,UserAssigned', 'SystemAssigned'), if(not(empty(coalesce(tryGet(parameters('managedIdentities'), 'userAssignedResourceIds'), createObject()))), 'UserAssigned', 'None')), 'userAssignedIdentities', if(not(empty(variables('formattedUserAssignedIdentities'))), variables('formattedUserAssignedIdentities'), null())), null())]", - "builtInRoleNames": { - "Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'b24988ac-6180-42a0-ab88-20f7382dd24c')]", - "Owner": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '8e3af657-a8ff-443c-a75c-2fe8c4bcb635')]", - "Reader": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'acdd72a7-3385-48ef-bd42-f606fba81ae7')]", - "Reader and Data Access": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'c12c1c16-33a1-487b-954d-41c89c60f349')]", - "Role Based Access Control Administrator": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'f58310d9-a9f6-439a-9e8d-f62e7b41a168')]", - "Storage Account Backup Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'e5e2a7ff-d759-4cd2-bb51-3152d37e2eb1')]", - "Storage Account Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '17d1049b-9a84-46fb-8f53-869881c3d3ab')]", - "Storage Account Key Operator Service Role": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '81a9662b-bebf-436f-a333-f67b29880f12')]", - "Storage Blob Data Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'ba92f5b4-2d11-453d-a403-e96b0029c9fe')]", - "Storage Blob Data Owner": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'b7e6dc6d-f1e8-4753-8033-0f276bb0955b')]", - "Storage Blob Data Reader": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '2a2b9908-6ea1-4ae2-8e65-a410df84e7d1')]", - "Storage Blob Delegator": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'db58b8e5-c6ad-4a2a-8342-4190687cbf4a')]", - "Storage File Data Privileged Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '69566ab7-960f-475b-8e7c-b3118f30c6bd')]", - "Storage File Data Privileged Reader": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'b8eda974-7b85-4f76-af95-65846b26df6d')]", - "Storage File Data SMB Share Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '0c867c2a-1d8c-454a-a3db-ab2ea1bdc8bb')]", - "Storage File Data SMB Share Elevated Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'a7264617-510b-434b-a828-9731dc254ea7')]", - "Storage File Data SMB Share Reader": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'aba4ae5f-2193-4029-9191-0cb91df5e314')]", - "Storage Queue Data Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '974c5e8b-45b9-4653-ba55-5f855dd0fb88')]", - "Storage Queue Data Message Processor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '8a0f0c08-91a1-4084-bc3d-661d67233fed')]", - "Storage Queue Data Message Sender": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'c6a89b2d-59bc-44d0-9896-0f6e12d7b80a')]", - "Storage Queue Data Reader": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '19e7f393-937e-4f77-808e-94535e297925')]", - "Storage Table Data Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '0a9a7e1f-b9d0-4cc4-a60d-0319b160aaa3')]", - "Storage Table Data Reader": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '76199698-9eea-4c19-bc75-cec21354c6b6')]", - "User Access Administrator": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '18d7d88d-d35e-4fb5-a5c3-7773c20a72d9')]" - }, - "formattedManagementPolicies": "[union(coalesce(parameters('managementPolicyRules'), createArray()), if(and(and(not(empty(parameters('blobServices'))), coalesce(tryGet(parameters('blobServices'), 'isVersioningEnabled'), false())), not(equals(tryGet(parameters('blobServices'), 'versionDeletePolicyDays'), null()))), createArray(createObject('name', 'DeletePreviousVersions (auto-created)', 'enabled', true(), 'type', 'Lifecycle', 'definition', createObject('actions', createObject('version', createObject('delete', createObject('daysAfterCreationGreaterThan', parameters('blobServices').versionDeletePolicyDays))), 'filters', createObject('blobTypes', createArray('blockBlob', 'appendBlob'))))), createArray()))]", - "isHSMManagedCMK": "[equals(tryGet(split(coalesce(tryGet(parameters('customerManagedKey'), 'keyVaultResourceId'), ''), '/'), 7), 'managedHSMs')]" - }, - "resources": { - "cMKKeyVault::cMKKey": { - "condition": "[and(and(not(variables('isHSMManagedCMK')), not(empty(tryGet(parameters('customerManagedKey'), 'keyVaultResourceId')))), and(not(empty(tryGet(parameters('customerManagedKey'), 'keyVaultResourceId'))), not(empty(tryGet(parameters('customerManagedKey'), 'keyName')))))]", - "existing": true, - "type": "Microsoft.KeyVault/vaults/keys", - "apiVersion": "2024-11-01", - "subscriptionId": "[split(tryGet(parameters('customerManagedKey'), 'keyVaultResourceId'), '/')[2]]", - "resourceGroup": "[split(tryGet(parameters('customerManagedKey'), 'keyVaultResourceId'), '/')[4]]", - "name": "[format('{0}/{1}', last(split(tryGet(parameters('customerManagedKey'), 'keyVaultResourceId'), '/')), tryGet(parameters('customerManagedKey'), 'keyName'))]" + "dependsOn": [ + "vm", + "vm_domainJoinExtension" + ] }, - "avmTelemetry": { - "condition": "[parameters('enableTelemetry')]", + "vm_microsoftAntiMalwareExtension": { + "condition": "[parameters('extensionAntiMalwareConfig').enabled]", "type": "Microsoft.Resources/deployments", "apiVersion": "2025-04-01", - "name": "[format('46d3xbcp.res.storage-storageaccount.{0}.{1}', replace('0.30.0', '.', '-'), substring(uniqueString(deployment().name, parameters('location')), 0, 4))]", + "name": "[format('{0}-VM-MicrosoftAntiMalware', uniqueString(deployment().name, parameters('location')))]", "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, "mode": "Incremental", + "parameters": { + "virtualMachineName": { + "value": "[parameters('name')]" + }, + "name": { + "value": "[coalesce(tryGet(parameters('extensionAntiMalwareConfig'), 'name'), 'MicrosoftAntiMalware')]" + }, + "location": { + "value": "[parameters('location')]" + }, + "publisher": { + "value": "Microsoft.Azure.Security" + }, + "type": { + "value": "IaaSAntimalware" + }, + "typeHandlerVersion": { + "value": "[coalesce(tryGet(parameters('extensionAntiMalwareConfig'), 'typeHandlerVersion'), '1.3')]" + }, + "autoUpgradeMinorVersion": { + "value": "[coalesce(tryGet(parameters('extensionAntiMalwareConfig'), 'autoUpgradeMinorVersion'), true())]" + }, + "enableAutomaticUpgrade": { + "value": "[coalesce(tryGet(parameters('extensionAntiMalwareConfig'), 'enableAutomaticUpgrade'), false())]" + }, + "settings": { + "value": "[coalesce(tryGet(parameters('extensionAntiMalwareConfig'), 'settings'), createObject('AntimalwareEnabled', 'true', 'Exclusions', createObject(), 'RealtimeProtectionEnabled', 'true', 'ScheduledScanSettings', createObject('day', '7', 'isEnabled', 'true', 'scanType', 'Quick', 'time', '120')))]" + }, + "supressFailures": { + "value": "[coalesce(tryGet(parameters('extensionAntiMalwareConfig'), 'supressFailures'), false())]" + }, + "tags": { + "value": "[coalesce(tryGet(parameters('extensionAntiMalwareConfig'), 'tags'), parameters('tags'))]" + } + }, "template": { "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "languageVersion": "2.0", "contentVersion": "1.0.0.0", - "resources": [], + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.39.26.7824", + "templateHash": "8391598897118491777" + }, + "name": "Virtual Machine Extensions", + "description": "This module deploys a Virtual Machine Extension." + }, + "parameters": { + "virtualMachineName": { + "type": "string", + "metadata": { + "description": "Conditional. The name of the parent virtual machine that extension is provisioned for. Required if the template is used in a standalone deployment." + } + }, + "name": { + "type": "string", + "metadata": { + "description": "Required. The name of the virtual machine extension." + } + }, + "location": { + "type": "string", + "defaultValue": "[resourceGroup().location]", + "metadata": { + "description": "Optional. The location the extension is deployed to." + } + }, + "publisher": { + "type": "string", + "metadata": { + "description": "Required. The name of the extension handler publisher." + } + }, + "type": { + "type": "string", + "metadata": { + "description": "Required. Specifies the type of the extension; an example is \"CustomScriptExtension\"." + } + }, + "typeHandlerVersion": { + "type": "string", + "metadata": { + "description": "Required. Specifies the version of the script handler." + } + }, + "autoUpgradeMinorVersion": { + "type": "bool", + "metadata": { + "description": "Required. Indicates whether the extension should use a newer minor version if one is available at deployment time. Once deployed, however, the extension will not upgrade minor versions unless redeployed, even with this property set to true." + } + }, + "forceUpdateTag": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. How the extension handler should be forced to update even if the extension configuration has not changed." + } + }, + "settings": { + "type": "object", + "nullable": true, + "metadata": { + "description": "Optional. Any object that contains the extension specific settings." + } + }, + "protectedSettings": { + "type": "secureObject", + "nullable": true, + "metadata": { + "description": "Optional. Any object that contains the extension specific protected settings." + } + }, + "supressFailures": { + "type": "bool", + "defaultValue": false, + "metadata": { + "description": "Optional. Indicates whether failures stemming from the extension will be suppressed (Operational failures such as not connecting to the VM will not be suppressed regardless of this value). The default is false." + } + }, + "enableAutomaticUpgrade": { + "type": "bool", + "metadata": { + "description": "Required. Indicates whether the extension should be automatically upgraded by the platform if there is a newer version of the extension available." + } + }, + "tags": { + "type": "object", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.Compute/virtualMachines/extensions@2024-11-01#properties/tags" + }, + "description": "Optional. Tags of the resource." + }, + "nullable": true + }, + "protectedSettingsFromKeyVault": { + "type": "object", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.Compute/virtualMachines/extensions@2024-11-01#properties/properties/properties/protectedSettingsFromKeyVault" + }, + "description": "Optional. The extensions protected settings that are passed by reference, and consumed from key vault." + }, + "nullable": true + }, + "provisionAfterExtensions": { + "type": "array", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.Compute/virtualMachines/extensions@2024-11-01#properties/properties/properties/provisionAfterExtensions" + }, + "description": "Optional. Collection of extension names after which this extension needs to be provisioned." + }, + "nullable": true + } + }, + "resources": { + "virtualMachine": { + "existing": true, + "type": "Microsoft.Compute/virtualMachines", + "apiVersion": "2024-11-01", + "name": "[parameters('virtualMachineName')]" + }, + "extension": { + "type": "Microsoft.Compute/virtualMachines/extensions", + "apiVersion": "2024-11-01", + "name": "[format('{0}/{1}', parameters('virtualMachineName'), parameters('name'))]", + "location": "[parameters('location')]", + "tags": "[parameters('tags')]", + "properties": { + "publisher": "[parameters('publisher')]", + "type": "[parameters('type')]", + "typeHandlerVersion": "[parameters('typeHandlerVersion')]", + "autoUpgradeMinorVersion": "[parameters('autoUpgradeMinorVersion')]", + "enableAutomaticUpgrade": "[parameters('enableAutomaticUpgrade')]", + "forceUpdateTag": "[parameters('forceUpdateTag')]", + "settings": "[parameters('settings')]", + "protectedSettings": "[parameters('protectedSettings')]", + "suppressFailures": "[parameters('supressFailures')]", + "protectedSettingsFromKeyVault": "[parameters('protectedSettingsFromKeyVault')]", + "provisionAfterExtensions": "[parameters('provisionAfterExtensions')]" + } + } + }, "outputs": { - "telemetry": { - "type": "String", - "value": "For more information, see https://aka.ms/avm/TelemetryInfo" + "name": { + "type": "string", + "metadata": { + "description": "The name of the extension." + }, + "value": "[parameters('name')]" + }, + "resourceId": { + "type": "string", + "metadata": { + "description": "The resource ID of the extension." + }, + "value": "[resourceId('Microsoft.Compute/virtualMachines/extensions', parameters('virtualMachineName'), parameters('name'))]" + }, + "resourceGroupName": { + "type": "string", + "metadata": { + "description": "The name of the Resource Group the extension was created in." + }, + "value": "[resourceGroup().name]" + }, + "location": { + "type": "string", + "metadata": { + "description": "The location the resource was deployed into." + }, + "value": "[reference('extension', '2024-11-01', 'full').location]" } } } - } - }, - "cMKKeyVault": { - "condition": "[and(not(variables('isHSMManagedCMK')), not(empty(tryGet(parameters('customerManagedKey'), 'keyVaultResourceId'))))]", - "existing": true, - "type": "Microsoft.KeyVault/vaults", - "apiVersion": "2025-05-01", - "subscriptionId": "[split(tryGet(parameters('customerManagedKey'), 'keyVaultResourceId'), '/')[2]]", - "resourceGroup": "[split(tryGet(parameters('customerManagedKey'), 'keyVaultResourceId'), '/')[4]]", - "name": "[last(split(tryGet(parameters('customerManagedKey'), 'keyVaultResourceId'), '/'))]" - }, - "cMKUserAssignedIdentity": { - "condition": "[not(empty(tryGet(parameters('customerManagedKey'), 'userAssignedIdentityResourceId')))]", - "existing": true, - "type": "Microsoft.ManagedIdentity/userAssignedIdentities", - "apiVersion": "2024-11-30", - "subscriptionId": "[split(tryGet(parameters('customerManagedKey'), 'userAssignedIdentityResourceId'), '/')[2]]", - "resourceGroup": "[split(tryGet(parameters('customerManagedKey'), 'userAssignedIdentityResourceId'), '/')[4]]", - "name": "[last(split(tryGet(parameters('customerManagedKey'), 'userAssignedIdentityResourceId'), '/'))]" - }, - "storageAccount": { - "type": "Microsoft.Storage/storageAccounts", - "apiVersion": "2025-01-01", - "name": "[parameters('name')]", - "location": "[parameters('location')]", - "extendedLocation": "[if(not(empty(parameters('extendedLocationZone'))), createObject('name', parameters('extendedLocationZone'), 'type', 'EdgeZone'), null())]", - "kind": "[parameters('kind')]", - "sku": { - "name": "[parameters('skuName')]" - }, - "identity": "[variables('identity')]", - "tags": "[parameters('tags')]", - "properties": "[shallowMerge(createArray(createObject('allowSharedKeyAccess', parameters('allowSharedKeyAccess'), 'defaultToOAuthAuthentication', parameters('defaultToOAuthAuthentication'), 'allowCrossTenantReplication', parameters('allowCrossTenantReplication'), 'allowedCopyScope', parameters('allowedCopyScope'), 'customDomain', createObject('name', parameters('customDomainName'), 'useSubDomainName', parameters('customDomainUseSubDomainName')), 'dnsEndpointType', parameters('dnsEndpointType'), 'isLocalUserEnabled', parameters('isLocalUserEnabled'), 'encryption', union(createObject('keySource', if(not(empty(parameters('customerManagedKey'))), 'Microsoft.Keyvault', 'Microsoft.Storage'), 'services', createObject('blob', if(variables('supportsBlobService'), createObject('enabled', true()), null()), 'file', if(variables('supportsFileService'), createObject('enabled', true()), null()), 'table', createObject('enabled', true(), 'keyType', parameters('keyType')), 'queue', createObject('enabled', true(), 'keyType', parameters('keyType'))), 'keyvaultproperties', if(not(empty(parameters('customerManagedKey'))), createObject('keyname', parameters('customerManagedKey').keyName, 'keyvaulturi', if(not(variables('isHSMManagedCMK')), reference('cMKKeyVault').vaultUri, format('https://{0}.managedhsm.azure.net/', last(split(parameters('customerManagedKey').keyVaultResourceId, '/')))), 'keyversion', if(not(empty(tryGet(parameters('customerManagedKey'), 'keyVersion'))), parameters('customerManagedKey').keyVersion, if(coalesce(tryGet(parameters('customerManagedKey'), 'autoRotationEnabled'), true()), null(), if(not(variables('isHSMManagedCMK')), last(split(reference('cMKKeyVault::cMKKey').keyUriWithVersion, '/')), fail('Managed HSM CMK encryption requires either specifying the ''keyVersion'' or omitting the ''autoRotationEnabled'' property. Setting ''autoRotationEnabled'' to false without a ''keyVersion'' is not allowed.'))))), null()), 'identity', createObject('userAssignedIdentity', if(not(empty(tryGet(parameters('customerManagedKey'), 'userAssignedIdentityResourceId'))), extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', split(tryGet(parameters('customerManagedKey'), 'userAssignedIdentityResourceId'), '/')[2], split(tryGet(parameters('customerManagedKey'), 'userAssignedIdentityResourceId'), '/')[4]), 'Microsoft.ManagedIdentity/userAssignedIdentities', last(split(tryGet(parameters('customerManagedKey'), 'userAssignedIdentityResourceId'), '/'))), null()))), if(parameters('requireInfrastructureEncryption'), createObject('requireInfrastructureEncryption', if(not(equals(parameters('kind'), 'Storage')), parameters('requireInfrastructureEncryption'), null())), createObject())), 'accessTier', if(and(not(equals(parameters('kind'), 'Storage')), not(equals(parameters('kind'), 'BlockBlobStorage'))), parameters('accessTier'), null()), 'sasPolicy', if(not(empty(parameters('sasExpirationPeriod'))), createObject('expirationAction', parameters('sasExpirationAction'), 'sasExpirationPeriod', parameters('sasExpirationPeriod')), null()), 'supportsHttpsTrafficOnly', parameters('supportsHttpsTrafficOnly'), 'isSftpEnabled', parameters('enableSftp'), 'isNfsV3Enabled', if(parameters('enableNfsV3'), parameters('enableNfsV3'), ''), 'largeFileSharesState', if(or(equals(parameters('skuName'), 'Standard_LRS'), equals(parameters('skuName'), 'Standard_ZRS')), parameters('largeFileSharesState'), null()), 'minimumTlsVersion', parameters('minimumTlsVersion'), 'networkAcls', if(not(empty(parameters('networkAcls'))), union(createObject('resourceAccessRules', tryGet(parameters('networkAcls'), 'resourceAccessRules'), 'defaultAction', coalesce(tryGet(parameters('networkAcls'), 'defaultAction'), 'Deny'), 'virtualNetworkRules', tryGet(parameters('networkAcls'), 'virtualNetworkRules'), 'ipRules', tryGet(parameters('networkAcls'), 'ipRules')), if(contains(parameters('networkAcls'), 'bypass'), createObject('bypass', tryGet(parameters('networkAcls'), 'bypass')), createObject())), createObject('bypass', 'AzureServices', 'defaultAction', 'Deny')), 'allowBlobPublicAccess', parameters('allowBlobPublicAccess'), 'publicNetworkAccess', if(not(empty(parameters('publicNetworkAccess'))), parameters('publicNetworkAccess'), if(and(not(empty(parameters('privateEndpoints'))), empty(parameters('networkAcls'))), 'Disabled', null()))), if(not(empty(parameters('azureFilesIdentityBasedAuthentication'))), createObject('azureFilesIdentityBasedAuthentication', parameters('azureFilesIdentityBasedAuthentication')), createObject()), if(not(equals(parameters('enableHierarchicalNamespace'), null())), createObject('isHnsEnabled', parameters('enableHierarchicalNamespace')), createObject()), createObject('immutableStorageWithVersioning', parameters('immutableStorageWithVersioning'))))]", - "dependsOn": [ - "cMKKeyVault", - "cMKKeyVault::cMKKey" - ] - }, - "storageAccount_diagnosticSettings": { - "copy": { - "name": "storageAccount_diagnosticSettings", - "count": "[length(coalesce(parameters('diagnosticSettings'), createArray()))]" - }, - "type": "Microsoft.Insights/diagnosticSettings", - "apiVersion": "2021-05-01-preview", - "scope": "[format('Microsoft.Storage/storageAccounts/{0}', parameters('name'))]", - "name": "[coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'name'), format('{0}-diagnosticSettings', parameters('name')))]", - "properties": { - "copy": [ - { - "name": "metrics", - "count": "[length(coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'metricCategories'), createArray(createObject('category', 'AllMetrics'))))]", - "input": { - "category": "[coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'metricCategories'), createArray(createObject('category', 'AllMetrics')))[copyIndex('metrics')].category]", - "enabled": "[coalesce(tryGet(coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'metricCategories'), createArray(createObject('category', 'AllMetrics')))[copyIndex('metrics')], 'enabled'), true())]", - "timeGrain": null - } - } - ], - "storageAccountId": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'storageAccountResourceId')]", - "workspaceId": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'workspaceResourceId')]", - "eventHubAuthorizationRuleId": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'eventHubAuthorizationRuleResourceId')]", - "eventHubName": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'eventHubName')]", - "marketplacePartnerId": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'marketplacePartnerResourceId')]", - "logAnalyticsDestinationType": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'logAnalyticsDestinationType')]" - }, - "dependsOn": [ - "storageAccount" - ] - }, - "storageAccount_lock": { - "condition": "[and(not(empty(coalesce(parameters('lock'), createObject()))), not(equals(tryGet(parameters('lock'), 'kind'), 'None')))]", - "type": "Microsoft.Authorization/locks", - "apiVersion": "2020-05-01", - "scope": "[format('Microsoft.Storage/storageAccounts/{0}', parameters('name'))]", - "name": "[coalesce(tryGet(parameters('lock'), 'name'), format('lock-{0}', parameters('name')))]", - "properties": { - "level": "[coalesce(tryGet(parameters('lock'), 'kind'), '')]", - "notes": "[coalesce(tryGet(parameters('lock'), 'notes'), if(equals(tryGet(parameters('lock'), 'kind'), 'CanNotDelete'), 'Cannot delete resource or child resources.', 'Cannot delete or modify the resource or child resources.'))]" }, "dependsOn": [ - "storageAccount" - ] - }, - "storageAccount_roleAssignments": { - "copy": { - "name": "storageAccount_roleAssignments", - "count": "[length(coalesce(variables('formattedRoleAssignments'), createArray()))]" - }, - "type": "Microsoft.Authorization/roleAssignments", - "apiVersion": "2022-04-01", - "scope": "[format('Microsoft.Storage/storageAccounts/{0}', parameters('name'))]", - "name": "[coalesce(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'name'), guid(resourceId('Microsoft.Storage/storageAccounts', parameters('name')), coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].principalId, coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].roleDefinitionId))]", - "properties": { - "roleDefinitionId": "[coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].roleDefinitionId]", - "principalId": "[coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].principalId]", - "description": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'description')]", - "principalType": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'principalType')]", - "condition": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'condition')]", - "conditionVersion": "[if(not(empty(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'condition'))), coalesce(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'conditionVersion'), '2.0'), null())]", - "delegatedManagedIdentityResourceId": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'delegatedManagedIdentityResourceId')]" - }, - "dependsOn": [ - "storageAccount" + "vm", + "vm_aadJoinExtension" ] }, - "storageAccount_privateEndpoints": { - "copy": { - "name": "storageAccount_privateEndpoints", - "count": "[length(coalesce(parameters('privateEndpoints'), createArray()))]" - }, + "vm_azureMonitorAgentExtension": { + "condition": "[parameters('extensionMonitoringAgentConfig').enabled]", "type": "Microsoft.Resources/deployments", "apiVersion": "2025-04-01", - "name": "[format('{0}-sa-PrivateEndpoint-{1}', uniqueString(deployment().name, parameters('location')), copyIndex())]", - "subscriptionId": "[split(coalesce(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'resourceGroupResourceId'), resourceGroup().id), '/')[2]]", - "resourceGroup": "[split(coalesce(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'resourceGroupResourceId'), resourceGroup().id), '/')[4]]", + "name": "[format('{0}-VM-AzureMonitorAgent', uniqueString(deployment().name, parameters('location')))]", "properties": { "expressionEvaluationOptions": { "scope": "inner" }, "mode": "Incremental", "parameters": { - "name": { - "value": "[coalesce(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'name'), format('pep-{0}-{1}-{2}', last(split(resourceId('Microsoft.Storage/storageAccounts', parameters('name')), '/')), coalesce(parameters('privateEndpoints'), createArray())[copyIndex()].service, copyIndex()))]" - }, - "privateLinkServiceConnections": "[if(not(equals(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'isManualConnection'), true())), createObject('value', createArray(createObject('name', coalesce(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'privateLinkServiceConnectionName'), format('{0}-{1}-{2}', last(split(resourceId('Microsoft.Storage/storageAccounts', parameters('name')), '/')), coalesce(parameters('privateEndpoints'), createArray())[copyIndex()].service, copyIndex())), 'properties', createObject('privateLinkServiceId', resourceId('Microsoft.Storage/storageAccounts', parameters('name')), 'groupIds', createArray(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()].service))))), createObject('value', null()))]", - "manualPrivateLinkServiceConnections": "[if(equals(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'isManualConnection'), true()), createObject('value', createArray(createObject('name', coalesce(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'privateLinkServiceConnectionName'), format('{0}-{1}-{2}', last(split(resourceId('Microsoft.Storage/storageAccounts', parameters('name')), '/')), coalesce(parameters('privateEndpoints'), createArray())[copyIndex()].service, copyIndex())), 'properties', createObject('privateLinkServiceId', resourceId('Microsoft.Storage/storageAccounts', parameters('name')), 'groupIds', createArray(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()].service), 'requestMessage', coalesce(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'manualConnectionRequestMessage'), 'Manual approval required.'))))), createObject('value', null()))]", - "subnetResourceId": { - "value": "[coalesce(parameters('privateEndpoints'), createArray())[copyIndex()].subnetResourceId]" + "virtualMachineName": { + "value": "[parameters('name')]" }, - "enableTelemetry": { - "value": "[variables('enableReferencedModulesTelemetry')]" + "name": { + "value": "[coalesce(tryGet(parameters('extensionMonitoringAgentConfig'), 'name'), 'AzureMonitorAgent')]" }, "location": { - "value": "[coalesce(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'location'), reference(split(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()].subnetResourceId, '/subnets/')[0], '2020-06-01', 'Full').location)]" - }, - "lock": { - "value": "[coalesce(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'lock'), parameters('lock'))]" - }, - "privateDnsZoneGroup": { - "value": "[tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'privateDnsZoneGroup')]" + "value": "[parameters('location')]" }, - "roleAssignments": { - "value": "[tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'roleAssignments')]" + "publisher": { + "value": "Microsoft.Azure.Monitor" }, - "tags": { - "value": "[coalesce(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'tags'), parameters('tags'))]" + "type": "[if(equals(parameters('osType'), 'Windows'), createObject('value', 'AzureMonitorWindowsAgent'), createObject('value', 'AzureMonitorLinuxAgent'))]", + "typeHandlerVersion": { + "value": "[coalesce(tryGet(parameters('extensionMonitoringAgentConfig'), 'typeHandlerVersion'), if(equals(parameters('osType'), 'Windows'), '1.22', '1.29'))]" }, - "customDnsConfigs": { - "value": "[tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'customDnsConfigs')]" + "autoUpgradeMinorVersion": { + "value": "[coalesce(tryGet(parameters('extensionMonitoringAgentConfig'), 'autoUpgradeMinorVersion'), true())]" }, - "ipConfigurations": { - "value": "[tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'ipConfigurations')]" + "enableAutomaticUpgrade": { + "value": "[coalesce(tryGet(parameters('extensionMonitoringAgentConfig'), 'enableAutomaticUpgrade'), false())]" }, - "applicationSecurityGroupResourceIds": { - "value": "[tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'applicationSecurityGroupResourceIds')]" + "supressFailures": { + "value": "[coalesce(tryGet(parameters('extensionMonitoringAgentConfig'), 'supressFailures'), false())]" }, - "customNetworkInterfaceName": { - "value": "[tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'customNetworkInterfaceName')]" + "tags": { + "value": "[coalesce(tryGet(parameters('extensionMonitoringAgentConfig'), 'tags'), parameters('tags'))]" } }, "template": { @@ -19295,731 +16019,931 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.38.5.1644", - "templateHash": "16604612898799598358" + "version": "0.39.26.7824", + "templateHash": "8391598897118491777" }, - "name": "Private Endpoints", - "description": "This module deploys a Private Endpoint." + "name": "Virtual Machine Extensions", + "description": "This module deploys a Virtual Machine Extension." }, - "definitions": { - "privateDnsZoneGroupType": { - "type": "object", - "properties": { - "name": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The name of the Private DNS Zone Group." - } - }, - "privateDnsZoneGroupConfigs": { - "type": "array", - "items": { - "$ref": "#/definitions/privateDnsZoneGroupConfigType" - }, - "metadata": { - "description": "Required. The private DNS zone groups to associate the private endpoint. A DNS zone group can support up to 5 DNS zones." - } - } - }, + "parameters": { + "virtualMachineName": { + "type": "string", "metadata": { - "__bicep_export!": true, - "description": "The type of a private dns zone group." + "description": "Conditional. The name of the parent virtual machine that extension is provisioned for. Required if the template is used in a standalone deployment." } }, - "lockType": { - "type": "object", - "properties": { - "name": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. Specify the name of lock." - } - }, - "kind": { - "type": "string", - "allowedValues": [ - "CanNotDelete", - "None", - "ReadOnly" - ], - "nullable": true, - "metadata": { - "description": "Optional. Specify the type of lock." - } - }, - "notes": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. Specify the notes of the lock." - } - } - }, + "name": { + "type": "string", "metadata": { - "description": "An AVM-aligned type for a lock.", - "__bicep_imported_from!": { - "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.1" - } + "description": "Required. The name of the virtual machine extension." } }, - "privateDnsZoneGroupConfigType": { + "location": { + "type": "string", + "defaultValue": "[resourceGroup().location]", + "metadata": { + "description": "Optional. The location the extension is deployed to." + } + }, + "publisher": { + "type": "string", + "metadata": { + "description": "Required. The name of the extension handler publisher." + } + }, + "type": { + "type": "string", + "metadata": { + "description": "Required. Specifies the type of the extension; an example is \"CustomScriptExtension\"." + } + }, + "typeHandlerVersion": { + "type": "string", + "metadata": { + "description": "Required. Specifies the version of the script handler." + } + }, + "autoUpgradeMinorVersion": { + "type": "bool", + "metadata": { + "description": "Required. Indicates whether the extension should use a newer minor version if one is available at deployment time. Once deployed, however, the extension will not upgrade minor versions unless redeployed, even with this property set to true." + } + }, + "forceUpdateTag": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. How the extension handler should be forced to update even if the extension configuration has not changed." + } + }, + "settings": { "type": "object", - "properties": { - "name": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The name of the private DNS zone group config." - } - }, - "privateDnsZoneResourceId": { - "type": "string", - "metadata": { - "description": "Required. The resource id of the private DNS zone." - } - } - }, + "nullable": true, "metadata": { - "description": "The type of a private DNS zone group configuration.", - "__bicep_imported_from!": { - "sourceTemplate": "private-dns-zone-group/main.bicep" - } + "description": "Optional. Any object that contains the extension specific settings." } }, - "roleAssignmentType": { + "protectedSettings": { + "type": "secureObject", + "nullable": true, + "metadata": { + "description": "Optional. Any object that contains the extension specific protected settings." + } + }, + "supressFailures": { + "type": "bool", + "defaultValue": false, + "metadata": { + "description": "Optional. Indicates whether failures stemming from the extension will be suppressed (Operational failures such as not connecting to the VM will not be suppressed regardless of this value). The default is false." + } + }, + "enableAutomaticUpgrade": { + "type": "bool", + "metadata": { + "description": "Required. Indicates whether the extension should be automatically upgraded by the platform if there is a newer version of the extension available." + } + }, + "tags": { "type": "object", - "properties": { - "name": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The name (as GUID) of the role assignment. If not provided, a GUID will be generated." - } - }, - "roleDefinitionIdOrName": { - "type": "string", - "metadata": { - "description": "Required. The role to assign. You can provide either the display name of the role definition, the role definition GUID, or its fully qualified ID in the following format: '/providers/Microsoft.Authorization/roleDefinitions/c2f4ef07-c644-48eb-af81-4b1b4947fb11'." - } - }, - "principalId": { - "type": "string", - "metadata": { - "description": "Required. The principal ID of the principal (user/group/identity) to assign the role to." - } - }, - "principalType": { - "type": "string", - "allowedValues": [ - "Device", - "ForeignGroup", - "Group", - "ServicePrincipal", - "User" - ], - "nullable": true, - "metadata": { - "description": "Optional. The principal type of the assigned principal ID." - } - }, - "description": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The description of the role assignment." - } - }, - "condition": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The conditions on the role assignment. This limits the resources it can be assigned to. e.g.: @Resource[Microsoft.Storage/storageAccounts/blobServices/containers:ContainerName] StringEqualsIgnoreCase \"foo_storage_container\"." - } + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.Compute/virtualMachines/extensions@2024-11-01#properties/tags" }, - "conditionVersion": { - "type": "string", - "allowedValues": [ - "2.0" - ], - "nullable": true, - "metadata": { - "description": "Optional. Version of the condition." - } + "description": "Optional. Tags of the resource." + }, + "nullable": true + }, + "protectedSettingsFromKeyVault": { + "type": "object", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.Compute/virtualMachines/extensions@2024-11-01#properties/properties/properties/protectedSettingsFromKeyVault" }, - "delegatedManagedIdentityResourceId": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The Resource Id of the delegated managed identity resource." - } - } + "description": "Optional. The extensions protected settings that are passed by reference, and consumed from key vault." }, + "nullable": true + }, + "provisionAfterExtensions": { + "type": "array", "metadata": { - "description": "An AVM-aligned type for a role assignment.", - "__bicep_imported_from!": { - "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.1" - } + "__bicep_resource_derived_type!": { + "source": "Microsoft.Compute/virtualMachines/extensions@2024-11-01#properties/properties/properties/provisionAfterExtensions" + }, + "description": "Optional. Collection of extension names after which this extension needs to be provisioned." + }, + "nullable": true + } + }, + "resources": { + "virtualMachine": { + "existing": true, + "type": "Microsoft.Compute/virtualMachines", + "apiVersion": "2024-11-01", + "name": "[parameters('virtualMachineName')]" + }, + "extension": { + "type": "Microsoft.Compute/virtualMachines/extensions", + "apiVersion": "2024-11-01", + "name": "[format('{0}/{1}', parameters('virtualMachineName'), parameters('name'))]", + "location": "[parameters('location')]", + "tags": "[parameters('tags')]", + "properties": { + "publisher": "[parameters('publisher')]", + "type": "[parameters('type')]", + "typeHandlerVersion": "[parameters('typeHandlerVersion')]", + "autoUpgradeMinorVersion": "[parameters('autoUpgradeMinorVersion')]", + "enableAutomaticUpgrade": "[parameters('enableAutomaticUpgrade')]", + "forceUpdateTag": "[parameters('forceUpdateTag')]", + "settings": "[parameters('settings')]", + "protectedSettings": "[parameters('protectedSettings')]", + "suppressFailures": "[parameters('supressFailures')]", + "protectedSettingsFromKeyVault": "[parameters('protectedSettingsFromKeyVault')]", + "provisionAfterExtensions": "[parameters('provisionAfterExtensions')]" } } }, + "outputs": { + "name": { + "type": "string", + "metadata": { + "description": "The name of the extension." + }, + "value": "[parameters('name')]" + }, + "resourceId": { + "type": "string", + "metadata": { + "description": "The resource ID of the extension." + }, + "value": "[resourceId('Microsoft.Compute/virtualMachines/extensions', parameters('virtualMachineName'), parameters('name'))]" + }, + "resourceGroupName": { + "type": "string", + "metadata": { + "description": "The name of the Resource Group the extension was created in." + }, + "value": "[resourceGroup().name]" + }, + "location": { + "type": "string", + "metadata": { + "description": "The location the resource was deployed into." + }, + "value": "[reference('extension', '2024-11-01', 'full').location]" + } + } + } + }, + "dependsOn": [ + "vm", + "vm_microsoftAntiMalwareExtension" + ] + }, + "vm_dependencyAgentExtension": { + "condition": "[parameters('extensionDependencyAgentConfig').enabled]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "[format('{0}-VM-DependencyAgent', uniqueString(deployment().name, parameters('location')))]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "virtualMachineName": { + "value": "[parameters('name')]" + }, + "name": { + "value": "[coalesce(tryGet(parameters('extensionDependencyAgentConfig'), 'name'), 'DependencyAgent')]" + }, + "location": { + "value": "[parameters('location')]" + }, + "publisher": { + "value": "Microsoft.Azure.Monitoring.DependencyAgent" + }, + "type": "[if(equals(parameters('osType'), 'Windows'), createObject('value', 'DependencyAgentWindows'), createObject('value', 'DependencyAgentLinux'))]", + "typeHandlerVersion": { + "value": "[coalesce(tryGet(parameters('extensionDependencyAgentConfig'), 'typeHandlerVersion'), '9.10')]" + }, + "autoUpgradeMinorVersion": { + "value": "[coalesce(tryGet(parameters('extensionDependencyAgentConfig'), 'autoUpgradeMinorVersion'), true())]" + }, + "enableAutomaticUpgrade": { + "value": "[coalesce(tryGet(parameters('extensionDependencyAgentConfig'), 'enableAutomaticUpgrade'), true())]" + }, + "settings": { + "value": { + "enableAMA": "[coalesce(tryGet(parameters('extensionDependencyAgentConfig'), 'enableAMA'), true())]" + } + }, + "supressFailures": { + "value": "[coalesce(tryGet(parameters('extensionDependencyAgentConfig'), 'supressFailures'), false())]" + }, + "tags": { + "value": "[coalesce(tryGet(parameters('extensionDependencyAgentConfig'), 'tags'), parameters('tags'))]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "languageVersion": "2.0", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.39.26.7824", + "templateHash": "8391598897118491777" + }, + "name": "Virtual Machine Extensions", + "description": "This module deploys a Virtual Machine Extension." + }, "parameters": { + "virtualMachineName": { + "type": "string", + "metadata": { + "description": "Conditional. The name of the parent virtual machine that extension is provisioned for. Required if the template is used in a standalone deployment." + } + }, "name": { "type": "string", "metadata": { - "description": "Required. Name of the private endpoint resource to create." + "description": "Required. The name of the virtual machine extension." } }, - "subnetResourceId": { + "location": { "type": "string", + "defaultValue": "[resourceGroup().location]", "metadata": { - "description": "Required. Resource ID of the subnet where the endpoint needs to be created." + "description": "Optional. The location the extension is deployed to." } }, - "applicationSecurityGroupResourceIds": { - "type": "array", - "items": { - "type": "string" - }, - "nullable": true, + "publisher": { + "type": "string", "metadata": { - "description": "Optional. Application security groups in which the private endpoint IP configuration is included." + "description": "Required. The name of the extension handler publisher." } }, - "customNetworkInterfaceName": { + "type": { "type": "string", - "nullable": true, "metadata": { - "description": "Optional. The custom name of the network interface attached to the private endpoint." + "description": "Required. Specifies the type of the extension; an example is \"CustomScriptExtension\"." } }, - "ipConfigurations": { - "type": "array", + "typeHandlerVersion": { + "type": "string", "metadata": { - "__bicep_resource_derived_type!": { - "source": "Microsoft.Network/privateEndpoints@2024-01-01#properties/properties/properties/ipConfigurations" - }, - "description": "Optional. A list of IP configurations of the private endpoint. This will be used to map to the First Party Service endpoints." - }, - "nullable": true + "description": "Required. Specifies the version of the script handler." + } }, - "privateDnsZoneGroup": { - "$ref": "#/definitions/privateDnsZoneGroupType", - "nullable": true, + "autoUpgradeMinorVersion": { + "type": "bool", "metadata": { - "description": "Optional. The private DNS zone group to configure for the private endpoint." + "description": "Required. Indicates whether the extension should use a newer minor version if one is available at deployment time. Once deployed, however, the extension will not upgrade minor versions unless redeployed, even with this property set to true." } }, - "location": { + "forceUpdateTag": { "type": "string", - "defaultValue": "[resourceGroup().location]", + "nullable": true, "metadata": { - "description": "Optional. Location for all Resources." + "description": "Optional. How the extension handler should be forced to update even if the extension configuration has not changed." } }, - "lock": { - "$ref": "#/definitions/lockType", + "settings": { + "type": "object", "nullable": true, "metadata": { - "description": "Optional. The lock settings of the service." + "description": "Optional. Any object that contains the extension specific settings." } }, - "roleAssignments": { - "type": "array", - "items": { - "$ref": "#/definitions/roleAssignmentType" - }, + "protectedSettings": { + "type": "secureObject", "nullable": true, "metadata": { - "description": "Optional. Array of role assignments to create." + "description": "Optional. Any object that contains the extension specific protected settings." } }, - "tags": { - "type": "object", + "supressFailures": { + "type": "bool", + "defaultValue": false, "metadata": { - "__bicep_resource_derived_type!": { - "source": "Microsoft.Network/privateEndpoints@2024-01-01#properties/tags" - }, - "description": "Optional. Tags to be applied on all resources/resource groups in this deployment." - }, - "nullable": true + "description": "Optional. Indicates whether failures stemming from the extension will be suppressed (Operational failures such as not connecting to the VM will not be suppressed regardless of this value). The default is false." + } }, - "customDnsConfigs": { - "type": "array", + "enableAutomaticUpgrade": { + "type": "bool", + "metadata": { + "description": "Required. Indicates whether the extension should be automatically upgraded by the platform if there is a newer version of the extension available." + } + }, + "tags": { + "type": "object", "metadata": { "__bicep_resource_derived_type!": { - "source": "Microsoft.Network/privateEndpoints@2024-01-01#properties/properties/properties/customDnsConfigs" + "source": "Microsoft.Compute/virtualMachines/extensions@2024-11-01#properties/tags" }, - "description": "Optional. Custom DNS configurations." + "description": "Optional. Tags of the resource." }, "nullable": true }, - "manualPrivateLinkServiceConnections": { - "type": "array", + "protectedSettingsFromKeyVault": { + "type": "object", "metadata": { "__bicep_resource_derived_type!": { - "source": "Microsoft.Network/privateEndpoints@2024-01-01#properties/properties/properties/manualPrivateLinkServiceConnections" + "source": "Microsoft.Compute/virtualMachines/extensions@2024-11-01#properties/properties/properties/protectedSettingsFromKeyVault" }, - "description": "Conditional. A grouping of information about the connection to the remote resource. Used when the network admin does not have access to approve connections to the remote resource. Required if `privateLinkServiceConnections` is empty." + "description": "Optional. The extensions protected settings that are passed by reference, and consumed from key vault." }, "nullable": true }, - "privateLinkServiceConnections": { + "provisionAfterExtensions": { "type": "array", "metadata": { "__bicep_resource_derived_type!": { - "source": "Microsoft.Network/privateEndpoints@2024-01-01#properties/properties/properties/privateLinkServiceConnections" + "source": "Microsoft.Compute/virtualMachines/extensions@2024-11-01#properties/properties/properties/provisionAfterExtensions" }, - "description": "Conditional. A grouping of information about the connection to the remote resource. Required if `manualPrivateLinkServiceConnections` is empty." + "description": "Optional. Collection of extension names after which this extension needs to be provisioned." }, "nullable": true - }, - "enableTelemetry": { - "type": "bool", - "defaultValue": true, - "metadata": { - "description": "Optional. Enable/Disable usage telemetry for module." - } - } - }, - "variables": { - "copy": [ - { - "name": "formattedRoleAssignments", - "count": "[length(coalesce(parameters('roleAssignments'), createArray()))]", - "input": "[union(coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')], createObject('roleDefinitionId', coalesce(tryGet(variables('builtInRoleNames'), coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName), if(contains(coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName, '/providers/Microsoft.Authorization/roleDefinitions/'), coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName, subscriptionResourceId('Microsoft.Authorization/roleDefinitions', coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName)))))]" - } - ], - "builtInRoleNames": { - "Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'b24988ac-6180-42a0-ab88-20f7382dd24c')]", - "DNS Resolver Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '0f2ebee7-ffd4-4fc0-b3b7-664099fdad5d')]", - "DNS Zone Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'befefa01-2a29-4197-83a8-272ff33ce314')]", - "Domain Services Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'eeaeda52-9324-47f6-8069-5d5bade478b2')]", - "Domain Services Reader": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '361898ef-9ed1-48c2-849c-a832951106bb')]", - "Network Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '4d97b98b-1d4f-4787-a291-c67834d212e7')]", - "Owner": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '8e3af657-a8ff-443c-a75c-2fe8c4bcb635')]", - "Private DNS Zone Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'b12aa53e-6015-4669-85d0-8515ebb3ae7f')]", - "Reader": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'acdd72a7-3385-48ef-bd42-f606fba81ae7')]", - "Role Based Access Control Administrator": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'f58310d9-a9f6-439a-9e8d-f62e7b41a168')]" } }, "resources": { - "avmTelemetry": { - "condition": "[parameters('enableTelemetry')]", - "type": "Microsoft.Resources/deployments", - "apiVersion": "2025-04-01", - "name": "[format('46d3xbcp.res.network-privateendpoint.{0}.{1}', replace('0.11.1', '.', '-'), substring(uniqueString(deployment().name, parameters('location')), 0, 4))]", - "properties": { - "mode": "Incremental", - "template": { - "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", - "contentVersion": "1.0.0.0", - "resources": [], - "outputs": { - "telemetry": { - "type": "String", - "value": "For more information, see https://aka.ms/avm/TelemetryInfo" - } - } - } - } + "virtualMachine": { + "existing": true, + "type": "Microsoft.Compute/virtualMachines", + "apiVersion": "2024-11-01", + "name": "[parameters('virtualMachineName')]" }, - "privateEndpoint": { - "type": "Microsoft.Network/privateEndpoints", - "apiVersion": "2024-10-01", - "name": "[parameters('name')]", + "extension": { + "type": "Microsoft.Compute/virtualMachines/extensions", + "apiVersion": "2024-11-01", + "name": "[format('{0}/{1}', parameters('virtualMachineName'), parameters('name'))]", "location": "[parameters('location')]", "tags": "[parameters('tags')]", "properties": { - "copy": [ - { - "name": "applicationSecurityGroups", - "count": "[length(coalesce(parameters('applicationSecurityGroupResourceIds'), createArray()))]", - "input": { - "id": "[coalesce(parameters('applicationSecurityGroupResourceIds'), createArray())[copyIndex('applicationSecurityGroups')]]" - } - } - ], - "customDnsConfigs": "[coalesce(parameters('customDnsConfigs'), createArray())]", - "customNetworkInterfaceName": "[coalesce(parameters('customNetworkInterfaceName'), '')]", - "ipConfigurations": "[coalesce(parameters('ipConfigurations'), createArray())]", - "manualPrivateLinkServiceConnections": "[coalesce(parameters('manualPrivateLinkServiceConnections'), createArray())]", - "privateLinkServiceConnections": "[coalesce(parameters('privateLinkServiceConnections'), createArray())]", - "subnet": { - "id": "[parameters('subnetResourceId')]" - } + "publisher": "[parameters('publisher')]", + "type": "[parameters('type')]", + "typeHandlerVersion": "[parameters('typeHandlerVersion')]", + "autoUpgradeMinorVersion": "[parameters('autoUpgradeMinorVersion')]", + "enableAutomaticUpgrade": "[parameters('enableAutomaticUpgrade')]", + "forceUpdateTag": "[parameters('forceUpdateTag')]", + "settings": "[parameters('settings')]", + "protectedSettings": "[parameters('protectedSettings')]", + "suppressFailures": "[parameters('supressFailures')]", + "protectedSettingsFromKeyVault": "[parameters('protectedSettingsFromKeyVault')]", + "provisionAfterExtensions": "[parameters('provisionAfterExtensions')]" } - }, - "privateEndpoint_lock": { - "condition": "[and(not(empty(coalesce(parameters('lock'), createObject()))), not(equals(tryGet(parameters('lock'), 'kind'), 'None')))]", - "type": "Microsoft.Authorization/locks", - "apiVersion": "2020-05-01", - "scope": "[format('Microsoft.Network/privateEndpoints/{0}', parameters('name'))]", - "name": "[coalesce(tryGet(parameters('lock'), 'name'), format('lock-{0}', parameters('name')))]", - "properties": { - "level": "[coalesce(tryGet(parameters('lock'), 'kind'), '')]", - "notes": "[coalesce(tryGet(parameters('lock'), 'notes'), if(equals(tryGet(parameters('lock'), 'kind'), 'CanNotDelete'), 'Cannot delete resource or child resources.', 'Cannot delete or modify the resource or child resources.'))]" + } + }, + "outputs": { + "name": { + "type": "string", + "metadata": { + "description": "The name of the extension." }, - "dependsOn": [ - "privateEndpoint" - ] + "value": "[parameters('name')]" }, - "privateEndpoint_roleAssignments": { - "copy": { - "name": "privateEndpoint_roleAssignments", - "count": "[length(coalesce(variables('formattedRoleAssignments'), createArray()))]" + "resourceId": { + "type": "string", + "metadata": { + "description": "The resource ID of the extension." }, - "type": "Microsoft.Authorization/roleAssignments", - "apiVersion": "2022-04-01", - "scope": "[format('Microsoft.Network/privateEndpoints/{0}', parameters('name'))]", - "name": "[coalesce(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'name'), guid(resourceId('Microsoft.Network/privateEndpoints', parameters('name')), coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].principalId, coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].roleDefinitionId))]", - "properties": { - "roleDefinitionId": "[coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].roleDefinitionId]", - "principalId": "[coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].principalId]", - "description": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'description')]", - "principalType": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'principalType')]", - "condition": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'condition')]", - "conditionVersion": "[if(not(empty(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'condition'))), coalesce(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'conditionVersion'), '2.0'), null())]", - "delegatedManagedIdentityResourceId": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'delegatedManagedIdentityResourceId')]" + "value": "[resourceId('Microsoft.Compute/virtualMachines/extensions', parameters('virtualMachineName'), parameters('name'))]" + }, + "resourceGroupName": { + "type": "string", + "metadata": { + "description": "The name of the Resource Group the extension was created in." }, - "dependsOn": [ - "privateEndpoint" - ] + "value": "[resourceGroup().name]" }, - "privateEndpoint_privateDnsZoneGroup": { - "condition": "[not(empty(parameters('privateDnsZoneGroup')))]", - "type": "Microsoft.Resources/deployments", - "apiVersion": "2025-04-01", - "name": "[format('{0}-PrivateEndpoint-PrivateDnsZoneGroup', uniqueString(deployment().name))]", - "properties": { - "expressionEvaluationOptions": { - "scope": "inner" - }, - "mode": "Incremental", - "parameters": { - "name": { - "value": "[tryGet(parameters('privateDnsZoneGroup'), 'name')]" - }, - "privateEndpointName": { - "value": "[parameters('name')]" - }, - "privateDnsZoneConfigs": { - "value": "[parameters('privateDnsZoneGroup').privateDnsZoneGroupConfigs]" - } - }, - "template": { - "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", - "languageVersion": "2.0", - "contentVersion": "1.0.0.0", - "metadata": { - "_generator": { - "name": "bicep", - "version": "0.38.5.1644", - "templateHash": "24141742673128945" - }, - "name": "Private Endpoint Private DNS Zone Groups", - "description": "This module deploys a Private Endpoint Private DNS Zone Group." - }, - "definitions": { - "privateDnsZoneGroupConfigType": { - "type": "object", - "properties": { - "name": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The name of the private DNS zone group config." - } - }, - "privateDnsZoneResourceId": { - "type": "string", - "metadata": { - "description": "Required. The resource id of the private DNS zone." - } - } - }, - "metadata": { - "__bicep_export!": true, - "description": "The type of a private DNS zone group configuration." - } - } - }, - "parameters": { - "privateEndpointName": { - "type": "string", - "metadata": { - "description": "Conditional. The name of the parent private endpoint. Required if the template is used in a standalone deployment." - } - }, - "privateDnsZoneConfigs": { - "type": "array", - "items": { - "$ref": "#/definitions/privateDnsZoneGroupConfigType" - }, - "minLength": 1, - "maxLength": 5, - "metadata": { - "description": "Required. Array of private DNS zone configurations of the private DNS zone group. A DNS zone group can support up to 5 DNS zones." - } - }, - "name": { - "type": "string", - "defaultValue": "default", - "metadata": { - "description": "Optional. The name of the private DNS zone group." - } - } - }, - "resources": { - "privateEndpoint": { - "existing": true, - "type": "Microsoft.Network/privateEndpoints", - "apiVersion": "2024-10-01", - "name": "[parameters('privateEndpointName')]" - }, - "privateDnsZoneGroup": { - "type": "Microsoft.Network/privateEndpoints/privateDnsZoneGroups", - "apiVersion": "2024-10-01", - "name": "[format('{0}/{1}', parameters('privateEndpointName'), parameters('name'))]", - "properties": { - "copy": [ - { - "name": "privateDnsZoneConfigs", - "count": "[length(parameters('privateDnsZoneConfigs'))]", - "input": { - "name": "[coalesce(tryGet(parameters('privateDnsZoneConfigs')[copyIndex('privateDnsZoneConfigs')], 'name'), last(split(parameters('privateDnsZoneConfigs')[copyIndex('privateDnsZoneConfigs')].privateDnsZoneResourceId, '/')))]", - "properties": { - "privateDnsZoneId": "[parameters('privateDnsZoneConfigs')[copyIndex('privateDnsZoneConfigs')].privateDnsZoneResourceId]" - } - } - } - ] - } - } - }, - "outputs": { - "name": { - "type": "string", - "metadata": { - "description": "The name of the private endpoint DNS zone group." - }, - "value": "[parameters('name')]" - }, - "resourceId": { - "type": "string", - "metadata": { - "description": "The resource ID of the private endpoint DNS zone group." - }, - "value": "[resourceId('Microsoft.Network/privateEndpoints/privateDnsZoneGroups', parameters('privateEndpointName'), parameters('name'))]" - }, - "resourceGroupName": { - "type": "string", - "metadata": { - "description": "The resource group the private endpoint DNS zone group was deployed into." - }, - "value": "[resourceGroup().name]" - } - } - } + "location": { + "type": "string", + "metadata": { + "description": "The location the resource was deployed into." }, - "dependsOn": [ - "privateEndpoint" - ] + "value": "[reference('extension', '2024-11-01', 'full').location]" } + } + } + }, + "dependsOn": [ + "vm", + "vm_azureMonitorAgentExtension" + ] + }, + "vm_networkWatcherAgentExtension": { + "condition": "[parameters('extensionNetworkWatcherAgentConfig').enabled]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "[format('{0}-VM-NetworkWatcherAgent', uniqueString(deployment().name, parameters('location')))]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "virtualMachineName": { + "value": "[parameters('name')]" + }, + "name": { + "value": "[coalesce(tryGet(parameters('extensionNetworkWatcherAgentConfig'), 'name'), 'NetworkWatcherAgent')]" + }, + "location": { + "value": "[parameters('location')]" + }, + "publisher": { + "value": "Microsoft.Azure.NetworkWatcher" + }, + "type": "[if(equals(parameters('osType'), 'Windows'), createObject('value', 'NetworkWatcherAgentWindows'), createObject('value', 'NetworkWatcherAgentLinux'))]", + "typeHandlerVersion": { + "value": "[coalesce(tryGet(parameters('extensionNetworkWatcherAgentConfig'), 'typeHandlerVersion'), '1.4')]" + }, + "autoUpgradeMinorVersion": { + "value": "[coalesce(tryGet(parameters('extensionNetworkWatcherAgentConfig'), 'autoUpgradeMinorVersion'), true())]" + }, + "enableAutomaticUpgrade": { + "value": "[coalesce(tryGet(parameters('extensionNetworkWatcherAgentConfig'), 'enableAutomaticUpgrade'), false())]" + }, + "supressFailures": { + "value": "[coalesce(tryGet(parameters('extensionNetworkWatcherAgentConfig'), 'supressFailures'), false())]" + }, + "tags": { + "value": "[coalesce(tryGet(parameters('extensionNetworkWatcherAgentConfig'), 'tags'), parameters('tags'))]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "languageVersion": "2.0", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.39.26.7824", + "templateHash": "8391598897118491777" + }, + "name": "Virtual Machine Extensions", + "description": "This module deploys a Virtual Machine Extension." }, - "outputs": { - "resourceGroupName": { + "parameters": { + "virtualMachineName": { "type": "string", "metadata": { - "description": "The resource group the private endpoint was deployed into." - }, - "value": "[resourceGroup().name]" + "description": "Conditional. The name of the parent virtual machine that extension is provisioned for. Required if the template is used in a standalone deployment." + } }, - "resourceId": { + "name": { "type": "string", "metadata": { - "description": "The resource ID of the private endpoint." - }, - "value": "[resourceId('Microsoft.Network/privateEndpoints', parameters('name'))]" + "description": "Required. The name of the virtual machine extension." + } }, - "name": { + "location": { "type": "string", + "defaultValue": "[resourceGroup().location]", "metadata": { - "description": "The name of the private endpoint." - }, - "value": "[parameters('name')]" + "description": "Optional. The location the extension is deployed to." + } }, - "location": { + "publisher": { "type": "string", "metadata": { - "description": "The location the resource was deployed into." + "description": "Required. The name of the extension handler publisher." + } + }, + "type": { + "type": "string", + "metadata": { + "description": "Required. Specifies the type of the extension; an example is \"CustomScriptExtension\"." + } + }, + "typeHandlerVersion": { + "type": "string", + "metadata": { + "description": "Required. Specifies the version of the script handler." + } + }, + "autoUpgradeMinorVersion": { + "type": "bool", + "metadata": { + "description": "Required. Indicates whether the extension should use a newer minor version if one is available at deployment time. Once deployed, however, the extension will not upgrade minor versions unless redeployed, even with this property set to true." + } + }, + "forceUpdateTag": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. How the extension handler should be forced to update even if the extension configuration has not changed." + } + }, + "settings": { + "type": "object", + "nullable": true, + "metadata": { + "description": "Optional. Any object that contains the extension specific settings." + } + }, + "protectedSettings": { + "type": "secureObject", + "nullable": true, + "metadata": { + "description": "Optional. Any object that contains the extension specific protected settings." + } + }, + "supressFailures": { + "type": "bool", + "defaultValue": false, + "metadata": { + "description": "Optional. Indicates whether failures stemming from the extension will be suppressed (Operational failures such as not connecting to the VM will not be suppressed regardless of this value). The default is false." + } + }, + "enableAutomaticUpgrade": { + "type": "bool", + "metadata": { + "description": "Required. Indicates whether the extension should be automatically upgraded by the platform if there is a newer version of the extension available." + } + }, + "tags": { + "type": "object", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.Compute/virtualMachines/extensions@2024-11-01#properties/tags" + }, + "description": "Optional. Tags of the resource." }, - "value": "[reference('privateEndpoint', '2024-10-01', 'full').location]" + "nullable": true }, - "customDnsConfigs": { - "type": "array", + "protectedSettingsFromKeyVault": { + "type": "object", "metadata": { "__bicep_resource_derived_type!": { - "source": "Microsoft.Network/privateEndpoints@2024-01-01#properties/properties/properties/customDnsConfigs", - "output": true + "source": "Microsoft.Compute/virtualMachines/extensions@2024-11-01#properties/properties/properties/protectedSettingsFromKeyVault" }, - "description": "The custom DNS configurations of the private endpoint." + "description": "Optional. The extensions protected settings that are passed by reference, and consumed from key vault." }, - "value": "[reference('privateEndpoint').customDnsConfigs]" + "nullable": true }, - "networkInterfaceResourceIds": { + "provisionAfterExtensions": { "type": "array", - "items": { - "type": "string" + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.Compute/virtualMachines/extensions@2024-11-01#properties/properties/properties/provisionAfterExtensions" + }, + "description": "Optional. Collection of extension names after which this extension needs to be provisioned." + }, + "nullable": true + } + }, + "resources": { + "virtualMachine": { + "existing": true, + "type": "Microsoft.Compute/virtualMachines", + "apiVersion": "2024-11-01", + "name": "[parameters('virtualMachineName')]" + }, + "extension": { + "type": "Microsoft.Compute/virtualMachines/extensions", + "apiVersion": "2024-11-01", + "name": "[format('{0}/{1}', parameters('virtualMachineName'), parameters('name'))]", + "location": "[parameters('location')]", + "tags": "[parameters('tags')]", + "properties": { + "publisher": "[parameters('publisher')]", + "type": "[parameters('type')]", + "typeHandlerVersion": "[parameters('typeHandlerVersion')]", + "autoUpgradeMinorVersion": "[parameters('autoUpgradeMinorVersion')]", + "enableAutomaticUpgrade": "[parameters('enableAutomaticUpgrade')]", + "forceUpdateTag": "[parameters('forceUpdateTag')]", + "settings": "[parameters('settings')]", + "protectedSettings": "[parameters('protectedSettings')]", + "suppressFailures": "[parameters('supressFailures')]", + "protectedSettingsFromKeyVault": "[parameters('protectedSettingsFromKeyVault')]", + "provisionAfterExtensions": "[parameters('provisionAfterExtensions')]" + } + } + }, + "outputs": { + "name": { + "type": "string", + "metadata": { + "description": "The name of the extension." }, + "value": "[parameters('name')]" + }, + "resourceId": { + "type": "string", "metadata": { - "description": "The resource IDs of the network interfaces associated with the private endpoint." + "description": "The resource ID of the extension." }, - "value": "[map(reference('privateEndpoint').networkInterfaces, lambda('nic', lambdaVariables('nic').id))]" + "value": "[resourceId('Microsoft.Compute/virtualMachines/extensions', parameters('virtualMachineName'), parameters('name'))]" }, - "groupId": { + "resourceGroupName": { "type": "string", - "nullable": true, "metadata": { - "description": "The group Id for the private endpoint Group." + "description": "The name of the Resource Group the extension was created in." }, - "value": "[coalesce(tryGet(tryGet(tryGet(tryGet(reference('privateEndpoint'), 'manualPrivateLinkServiceConnections'), 0, 'properties'), 'groupIds'), 0), tryGet(tryGet(tryGet(tryGet(reference('privateEndpoint'), 'privateLinkServiceConnections'), 0, 'properties'), 'groupIds'), 0))]" + "value": "[resourceGroup().name]" + }, + "location": { + "type": "string", + "metadata": { + "description": "The location the resource was deployed into." + }, + "value": "[reference('extension', '2024-11-01', 'full').location]" } } } }, "dependsOn": [ - "storageAccount" + "vm", + "vm_dependencyAgentExtension" ] }, - "storageAccount_managementPolicies": { - "condition": "[not(empty(coalesce(variables('formattedManagementPolicies'), createArray())))]", + "vm_desiredStateConfigurationExtension": { + "condition": "[parameters('extensionDSCConfig').enabled]", "type": "Microsoft.Resources/deployments", "apiVersion": "2025-04-01", - "name": "[format('{0}-Storage-ManagementPolicies', uniqueString(deployment().name, parameters('location')))]", + "name": "[format('{0}-VM-DesiredStateConfiguration', uniqueString(deployment().name, parameters('location')))]", "properties": { "expressionEvaluationOptions": { "scope": "inner" }, "mode": "Incremental", "parameters": { - "storageAccountName": { + "virtualMachineName": { "value": "[parameters('name')]" }, - "rules": { - "value": "[variables('formattedManagementPolicies')]" + "name": { + "value": "[coalesce(tryGet(parameters('extensionDSCConfig'), 'name'), 'DesiredStateConfiguration')]" + }, + "location": { + "value": "[parameters('location')]" + }, + "publisher": { + "value": "Microsoft.Powershell" + }, + "type": { + "value": "DSC" + }, + "typeHandlerVersion": { + "value": "[coalesce(tryGet(parameters('extensionDSCConfig'), 'typeHandlerVersion'), '2.77')]" + }, + "autoUpgradeMinorVersion": { + "value": "[coalesce(tryGet(parameters('extensionDSCConfig'), 'autoUpgradeMinorVersion'), true())]" + }, + "enableAutomaticUpgrade": { + "value": "[coalesce(tryGet(parameters('extensionDSCConfig'), 'enableAutomaticUpgrade'), false())]" + }, + "settings": { + "value": "[coalesce(tryGet(parameters('extensionDSCConfig'), 'settings'), createObject())]" + }, + "supressFailures": { + "value": "[coalesce(tryGet(parameters('extensionDSCConfig'), 'supressFailures'), false())]" + }, + "tags": { + "value": "[coalesce(tryGet(parameters('extensionDSCConfig'), 'tags'), parameters('tags'))]" + }, + "protectedSettings": { + "value": "[coalesce(tryGet(parameters('extensionDSCConfig'), 'protectedSettings'), createObject())]" } }, "template": { "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "languageVersion": "2.0", "contentVersion": "1.0.0.0", "metadata": { "_generator": { "name": "bicep", "version": "0.39.26.7824", - "templateHash": "4538661605890101674" + "templateHash": "8391598897118491777" }, - "name": "Storage Account Management Policies", - "description": "This module deploys a Storage Account Management Policy." + "name": "Virtual Machine Extensions", + "description": "This module deploys a Virtual Machine Extension." }, "parameters": { - "storageAccountName": { + "virtualMachineName": { "type": "string", - "maxLength": 24, "metadata": { - "description": "Conditional. The name of the parent Storage Account. Required if the template is used in a standalone deployment." + "description": "Conditional. The name of the parent virtual machine that extension is provisioned for. Required if the template is used in a standalone deployment." } }, - "rules": { + "name": { + "type": "string", + "metadata": { + "description": "Required. The name of the virtual machine extension." + } + }, + "location": { + "type": "string", + "defaultValue": "[resourceGroup().location]", + "metadata": { + "description": "Optional. The location the extension is deployed to." + } + }, + "publisher": { + "type": "string", + "metadata": { + "description": "Required. The name of the extension handler publisher." + } + }, + "type": { + "type": "string", + "metadata": { + "description": "Required. Specifies the type of the extension; an example is \"CustomScriptExtension\"." + } + }, + "typeHandlerVersion": { + "type": "string", + "metadata": { + "description": "Required. Specifies the version of the script handler." + } + }, + "autoUpgradeMinorVersion": { + "type": "bool", + "metadata": { + "description": "Required. Indicates whether the extension should use a newer minor version if one is available at deployment time. Once deployed, however, the extension will not upgrade minor versions unless redeployed, even with this property set to true." + } + }, + "forceUpdateTag": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. How the extension handler should be forced to update even if the extension configuration has not changed." + } + }, + "settings": { + "type": "object", + "nullable": true, + "metadata": { + "description": "Optional. Any object that contains the extension specific settings." + } + }, + "protectedSettings": { + "type": "secureObject", + "nullable": true, + "metadata": { + "description": "Optional. Any object that contains the extension specific protected settings." + } + }, + "supressFailures": { + "type": "bool", + "defaultValue": false, + "metadata": { + "description": "Optional. Indicates whether failures stemming from the extension will be suppressed (Operational failures such as not connecting to the VM will not be suppressed regardless of this value). The default is false." + } + }, + "enableAutomaticUpgrade": { + "type": "bool", + "metadata": { + "description": "Required. Indicates whether the extension should be automatically upgraded by the platform if there is a newer version of the extension available." + } + }, + "tags": { + "type": "object", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.Compute/virtualMachines/extensions@2024-11-01#properties/tags" + }, + "description": "Optional. Tags of the resource." + }, + "nullable": true + }, + "protectedSettingsFromKeyVault": { + "type": "object", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.Compute/virtualMachines/extensions@2024-11-01#properties/properties/properties/protectedSettingsFromKeyVault" + }, + "description": "Optional. The extensions protected settings that are passed by reference, and consumed from key vault." + }, + "nullable": true + }, + "provisionAfterExtensions": { "type": "array", "metadata": { "__bicep_resource_derived_type!": { - "source": "Microsoft.Storage/storageAccounts/managementPolicies@2024-01-01#properties/properties/properties/policy/properties/rules" + "source": "Microsoft.Compute/virtualMachines/extensions@2024-11-01#properties/properties/properties/provisionAfterExtensions" }, - "description": "Required. The Storage Account ManagementPolicies Rules." - } + "description": "Optional. Collection of extension names after which this extension needs to be provisioned." + }, + "nullable": true } }, - "resources": [ - { - "type": "Microsoft.Storage/storageAccounts/managementPolicies", - "apiVersion": "2024-01-01", - "name": "[format('{0}/{1}', parameters('storageAccountName'), 'default')]", + "resources": { + "virtualMachine": { + "existing": true, + "type": "Microsoft.Compute/virtualMachines", + "apiVersion": "2024-11-01", + "name": "[parameters('virtualMachineName')]" + }, + "extension": { + "type": "Microsoft.Compute/virtualMachines/extensions", + "apiVersion": "2024-11-01", + "name": "[format('{0}/{1}', parameters('virtualMachineName'), parameters('name'))]", + "location": "[parameters('location')]", + "tags": "[parameters('tags')]", "properties": { - "policy": { - "rules": "[parameters('rules')]" - } + "publisher": "[parameters('publisher')]", + "type": "[parameters('type')]", + "typeHandlerVersion": "[parameters('typeHandlerVersion')]", + "autoUpgradeMinorVersion": "[parameters('autoUpgradeMinorVersion')]", + "enableAutomaticUpgrade": "[parameters('enableAutomaticUpgrade')]", + "forceUpdateTag": "[parameters('forceUpdateTag')]", + "settings": "[parameters('settings')]", + "protectedSettings": "[parameters('protectedSettings')]", + "suppressFailures": "[parameters('supressFailures')]", + "protectedSettingsFromKeyVault": "[parameters('protectedSettingsFromKeyVault')]", + "provisionAfterExtensions": "[parameters('provisionAfterExtensions')]" } } - ], + }, "outputs": { - "resourceId": { + "name": { "type": "string", "metadata": { - "description": "The resource ID of the deployed management policy." + "description": "The name of the extension." }, - "value": "default" + "value": "[parameters('name')]" }, - "name": { + "resourceId": { "type": "string", "metadata": { - "description": "The name of the deployed management policy." + "description": "The resource ID of the extension." }, - "value": "default" + "value": "[resourceId('Microsoft.Compute/virtualMachines/extensions', parameters('virtualMachineName'), parameters('name'))]" }, "resourceGroupName": { "type": "string", "metadata": { - "description": "The resource group of the deployed management policy." + "description": "The name of the Resource Group the extension was created in." }, "value": "[resourceGroup().name]" + }, + "location": { + "type": "string", + "metadata": { + "description": "The location the resource was deployed into." + }, + "value": "[reference('extension', '2024-11-01', 'full').location]" } } } }, "dependsOn": [ - "storageAccount", - "storageAccount_blobServices" + "vm", + "vm_networkWatcherAgentExtension" ] }, - "storageAccount_localUsers": { - "copy": { - "name": "storageAccount_localUsers", - "count": "[length(coalesce(parameters('localUsers'), createArray()))]" - }, + "vm_customScriptExtension": { + "condition": "[not(empty(parameters('extensionCustomScriptConfig')))]", "type": "Microsoft.Resources/deployments", "apiVersion": "2025-04-01", - "name": "[format('{0}-Storage-LocalUsers-{1}', uniqueString(deployment().name, parameters('location')), copyIndex())]", + "name": "[format('{0}-VM-CustomScriptExtension', uniqueString(deployment().name, parameters('location')))]", "properties": { "expressionEvaluationOptions": { "scope": "inner" }, "mode": "Incremental", "parameters": { - "storageAccountName": { + "virtualMachineName": { "value": "[parameters('name')]" }, "name": { - "value": "[coalesce(parameters('localUsers'), createArray())[copyIndex()].name]" + "value": "[coalesce(tryGet(parameters('extensionCustomScriptConfig'), 'name'), 'CustomScriptExtension')]" }, - "hasSshKey": { - "value": "[coalesce(parameters('localUsers'), createArray())[copyIndex()].hasSshKey]" + "location": { + "value": "[parameters('location')]" }, - "hasSshPassword": { - "value": "[coalesce(parameters('localUsers'), createArray())[copyIndex()].hasSshPassword]" + "publisher": "[if(equals(parameters('osType'), 'Windows'), createObject('value', 'Microsoft.Compute'), createObject('value', 'Microsoft.Azure.Extensions'))]", + "type": "[if(equals(parameters('osType'), 'Windows'), createObject('value', 'CustomScriptExtension'), createObject('value', 'CustomScript'))]", + "typeHandlerVersion": { + "value": "[coalesce(tryGet(parameters('extensionCustomScriptConfig'), 'typeHandlerVersion'), if(equals(parameters('osType'), 'Windows'), '1.10', '2.1'))]" }, - "permissionScopes": { - "value": "[coalesce(parameters('localUsers'), createArray())[copyIndex()].permissionScopes]" + "autoUpgradeMinorVersion": { + "value": "[coalesce(tryGet(parameters('extensionCustomScriptConfig'), 'autoUpgradeMinorVersion'), true())]" }, - "hasSharedKey": { - "value": "[tryGet(coalesce(parameters('localUsers'), createArray())[copyIndex()], 'hasSharedKey')]" + "enableAutomaticUpgrade": { + "value": "[coalesce(tryGet(parameters('extensionCustomScriptConfig'), 'enableAutomaticUpgrade'), false())]" }, - "homeDirectory": { - "value": "[tryGet(coalesce(parameters('localUsers'), createArray())[copyIndex()], 'homeDirectory')]" + "forceUpdateTag": { + "value": "[tryGet(parameters('extensionCustomScriptConfig'), 'forceUpdateTag')]" }, - "sshAuthorizedKeys": { - "value": "[tryGet(coalesce(parameters('localUsers'), createArray())[copyIndex()], 'sshAuthorizedKeys')]" + "provisionAfterExtensions": { + "value": "[tryGet(parameters('extensionCustomScriptConfig'), 'provisionAfterExtensions')]" + }, + "supressFailures": { + "value": "[coalesce(tryGet(parameters('extensionCustomScriptConfig'), 'supressFailures'), false())]" + }, + "tags": { + "value": "[coalesce(tryGet(parameters('extensionCustomScriptConfig'), 'tags'), parameters('tags'))]" + }, + "protectedSettingsFromKeyVault": { + "value": "[tryGet(parameters('extensionCustomScriptConfig'), 'protectedSettingsFromKeyVault')]" + }, + "settings": { + "value": "[shallowMerge(createArray(if(not(empty(tryGet(tryGet(parameters('extensionCustomScriptConfig'), 'settings'), 'commandToExecute'))), createObject('commandToExecute', tryGet(tryGet(parameters('extensionCustomScriptConfig'), 'settings'), 'commandToExecute')), createObject()), if(not(empty(tryGet(tryGet(parameters('extensionCustomScriptConfig'), 'settings'), 'fileUris'))), createObject('fileUris', tryGet(parameters('extensionCustomScriptConfig'), 'settings', 'fileUris')), createObject())))]" + }, + "protectedSettings": { + "value": "[shallowMerge(createArray(if(not(empty(tryGet(tryGet(parameters('extensionCustomScriptConfig'), 'protectedSettings'), 'commandToExecute'))), createObject('commandToExecute', tryGet(parameters('extensionCustomScriptConfig').protectedSettings, 'commandToExecute')), createObject()), if(not(empty(tryGet(tryGet(parameters('extensionCustomScriptConfig'), 'protectedSettings'), 'storageAccountName'))), createObject('storageAccountName', parameters('extensionCustomScriptConfig').protectedSettings.storageAccountName), createObject()), if(not(empty(tryGet(tryGet(parameters('extensionCustomScriptConfig'), 'protectedSettings'), 'storageAccountKey'))), createObject('storageAccountKey', parameters('extensionCustomScriptConfig').protectedSettings.storageAccountKey), createObject()), if(not(empty(tryGet(tryGet(parameters('extensionCustomScriptConfig'), 'protectedSettings'), 'fileUris'))), createObject('fileUris', parameters('extensionCustomScriptConfig').protectedSettings.fileUris), createObject()), if(not(equals(tryGet(tryGet(parameters('extensionCustomScriptConfig'), 'protectedSettings'), 'managedIdentityResourceId'), null())), createObject('managedIdentity', if(not(empty(tryGet(parameters('extensionCustomScriptConfig').protectedSettings, 'managedIdentityResourceId'))), createObject('clientId', reference('cseIdentity').clientId), createObject())), createObject())))]" } }, "template": { @@ -20030,138 +16954,145 @@ "_generator": { "name": "bicep", "version": "0.39.26.7824", - "templateHash": "17421429164012186211" + "templateHash": "8391598897118491777" }, - "name": "Storage Account Local Users", - "description": "This module deploys a Storage Account Local User, which is used for SFTP authentication." + "name": "Virtual Machine Extensions", + "description": "This module deploys a Virtual Machine Extension." }, - "definitions": { - "sshAuthorizedKeyType": { - "type": "object", - "properties": { - "description": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. Description used to store the function/usage of the key." - } - }, - "key": { - "type": "securestring", - "metadata": { - "description": "Required. SSH public key base64 encoded. The format should be: '{keyType} {keyData}', e.g. ssh-rsa AAAABBBB." - } - } - }, + "parameters": { + "virtualMachineName": { + "type": "string", "metadata": { - "__bicep_export!": true + "description": "Conditional. The name of the parent virtual machine that extension is provisioned for. Required if the template is used in a standalone deployment." } }, - "permissionScopeType": { - "type": "object", - "properties": { - "permissions": { - "type": "string", - "metadata": { - "description": "Required. The permissions for the local user. Possible values include: Read (r), Write (w), Delete (d), List (l), and Create (c)." - } - }, - "resourceName": { - "type": "string", - "metadata": { - "description": "Required. The name of resource, normally the container name or the file share name, used by the local user." - } - }, - "service": { - "type": "string", - "metadata": { - "description": "Required. The service used by the local user, e.g. blob, file." - } - } - }, + "name": { + "type": "string", "metadata": { - "__bicep_export!": true + "description": "Required. The name of the virtual machine extension." } - } - }, - "parameters": { - "storageAccountName": { + }, + "location": { + "type": "string", + "defaultValue": "[resourceGroup().location]", + "metadata": { + "description": "Optional. The location the extension is deployed to." + } + }, + "publisher": { + "type": "string", + "metadata": { + "description": "Required. The name of the extension handler publisher." + } + }, + "type": { "type": "string", - "maxLength": 24, "metadata": { - "description": "Conditional. The name of the parent Storage Account. Required if the template is used in a standalone deployment." + "description": "Required. Specifies the type of the extension; an example is \"CustomScriptExtension\"." } }, - "name": { + "typeHandlerVersion": { "type": "string", "metadata": { - "description": "Required. The name of the local user used for SFTP Authentication." + "description": "Required. Specifies the version of the script handler." } }, - "hasSharedKey": { + "autoUpgradeMinorVersion": { "type": "bool", - "defaultValue": false, "metadata": { - "description": "Optional. Indicates whether shared key exists. Set it to false to remove existing shared key." + "description": "Required. Indicates whether the extension should use a newer minor version if one is available at deployment time. Once deployed, however, the extension will not upgrade minor versions unless redeployed, even with this property set to true." } }, - "hasSshKey": { - "type": "bool", + "forceUpdateTag": { + "type": "string", + "nullable": true, "metadata": { - "description": "Required. Indicates whether SSH key exists. Set it to false to remove existing SSH key." + "description": "Optional. How the extension handler should be forced to update even if the extension configuration has not changed." } }, - "hasSshPassword": { + "settings": { + "type": "object", + "nullable": true, + "metadata": { + "description": "Optional. Any object that contains the extension specific settings." + } + }, + "protectedSettings": { + "type": "secureObject", + "nullable": true, + "metadata": { + "description": "Optional. Any object that contains the extension specific protected settings." + } + }, + "supressFailures": { "type": "bool", + "defaultValue": false, "metadata": { - "description": "Required. Indicates whether SSH password exists. Set it to false to remove existing SSH password." + "description": "Optional. Indicates whether failures stemming from the extension will be suppressed (Operational failures such as not connecting to the VM will not be suppressed regardless of this value). The default is false." } }, - "homeDirectory": { - "type": "string", - "defaultValue": "", + "enableAutomaticUpgrade": { + "type": "bool", "metadata": { - "description": "Optional. The local user home directory." + "description": "Required. Indicates whether the extension should be automatically upgraded by the platform if there is a newer version of the extension available." } }, - "permissionScopes": { - "type": "array", - "items": { - "$ref": "#/definitions/permissionScopeType" + "tags": { + "type": "object", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.Compute/virtualMachines/extensions@2024-11-01#properties/tags" + }, + "description": "Optional. Tags of the resource." }, + "nullable": true + }, + "protectedSettingsFromKeyVault": { + "type": "object", "metadata": { - "description": "Required. The permission scopes of the local user." - } + "__bicep_resource_derived_type!": { + "source": "Microsoft.Compute/virtualMachines/extensions@2024-11-01#properties/properties/properties/protectedSettingsFromKeyVault" + }, + "description": "Optional. The extensions protected settings that are passed by reference, and consumed from key vault." + }, + "nullable": true }, - "sshAuthorizedKeys": { + "provisionAfterExtensions": { "type": "array", - "items": { - "$ref": "#/definitions/sshAuthorizedKeyType" - }, - "nullable": true, "metadata": { - "description": "Optional. The local user SSH authorized keys for SFTP." - } + "__bicep_resource_derived_type!": { + "source": "Microsoft.Compute/virtualMachines/extensions@2024-11-01#properties/properties/properties/provisionAfterExtensions" + }, + "description": "Optional. Collection of extension names after which this extension needs to be provisioned." + }, + "nullable": true } }, "resources": { - "storageAccount": { + "virtualMachine": { "existing": true, - "type": "Microsoft.Storage/storageAccounts", - "apiVersion": "2024-01-01", - "name": "[parameters('storageAccountName')]" + "type": "Microsoft.Compute/virtualMachines", + "apiVersion": "2024-11-01", + "name": "[parameters('virtualMachineName')]" }, - "localUsers": { - "type": "Microsoft.Storage/storageAccounts/localUsers", - "apiVersion": "2024-01-01", - "name": "[format('{0}/{1}', parameters('storageAccountName'), parameters('name'))]", + "extension": { + "type": "Microsoft.Compute/virtualMachines/extensions", + "apiVersion": "2024-11-01", + "name": "[format('{0}/{1}', parameters('virtualMachineName'), parameters('name'))]", + "location": "[parameters('location')]", + "tags": "[parameters('tags')]", "properties": { - "hasSharedKey": "[parameters('hasSharedKey')]", - "hasSshKey": "[parameters('hasSshKey')]", - "hasSshPassword": "[parameters('hasSshPassword')]", - "homeDirectory": "[parameters('homeDirectory')]", - "permissionScopes": "[parameters('permissionScopes')]", - "sshAuthorizedKeys": "[parameters('sshAuthorizedKeys')]" + "publisher": "[parameters('publisher')]", + "type": "[parameters('type')]", + "typeHandlerVersion": "[parameters('typeHandlerVersion')]", + "autoUpgradeMinorVersion": "[parameters('autoUpgradeMinorVersion')]", + "enableAutomaticUpgrade": "[parameters('enableAutomaticUpgrade')]", + "forceUpdateTag": "[parameters('forceUpdateTag')]", + "settings": "[parameters('settings')]", + "protectedSettings": "[parameters('protectedSettings')]", + "suppressFailures": "[parameters('supressFailures')]", + "protectedSettingsFromKeyVault": "[parameters('protectedSettingsFromKeyVault')]", + "provisionAfterExtensions": "[parameters('provisionAfterExtensions')]" } } }, @@ -20169,95 +17100,84 @@ "name": { "type": "string", "metadata": { - "description": "The name of the deployed local user." + "description": "The name of the extension." }, "value": "[parameters('name')]" }, + "resourceId": { + "type": "string", + "metadata": { + "description": "The resource ID of the extension." + }, + "value": "[resourceId('Microsoft.Compute/virtualMachines/extensions', parameters('virtualMachineName'), parameters('name'))]" + }, "resourceGroupName": { "type": "string", "metadata": { - "description": "The resource group of the deployed local user." + "description": "The name of the Resource Group the extension was created in." }, "value": "[resourceGroup().name]" }, - "resourceId": { + "location": { "type": "string", "metadata": { - "description": "The resource ID of the deployed local user." + "description": "The location the resource was deployed into." }, - "value": "[resourceId('Microsoft.Storage/storageAccounts/localUsers', parameters('storageAccountName'), parameters('name'))]" + "value": "[reference('extension', '2024-11-01', 'full').location]" } } } }, "dependsOn": [ - "storageAccount" + "cseIdentity", + "vm", + "vm_desiredStateConfigurationExtension" ] }, - "storageAccount_blobServices": { - "condition": "[not(empty(parameters('blobServices')))]", + "vm_azureDiskEncryptionExtension": { + "condition": "[parameters('extensionAzureDiskEncryptionConfig').enabled]", "type": "Microsoft.Resources/deployments", "apiVersion": "2025-04-01", - "name": "[format('{0}-Storage-BlobServices', uniqueString(deployment().name, parameters('location')))]", + "name": "[format('{0}-VM-AzureDiskEncryption', uniqueString(deployment().name, parameters('location')))]", "properties": { "expressionEvaluationOptions": { "scope": "inner" }, "mode": "Incremental", "parameters": { - "storageAccountName": { + "virtualMachineName": { "value": "[parameters('name')]" }, - "containers": { - "value": "[tryGet(parameters('blobServices'), 'containers')]" - }, - "automaticSnapshotPolicyEnabled": { - "value": "[tryGet(parameters('blobServices'), 'automaticSnapshotPolicyEnabled')]" - }, - "changeFeedEnabled": { - "value": "[tryGet(parameters('blobServices'), 'changeFeedEnabled')]" - }, - "changeFeedRetentionInDays": { - "value": "[tryGet(parameters('blobServices'), 'changeFeedRetentionInDays')]" - }, - "containerDeleteRetentionPolicyEnabled": { - "value": "[tryGet(parameters('blobServices'), 'containerDeleteRetentionPolicyEnabled')]" - }, - "containerDeleteRetentionPolicyDays": { - "value": "[tryGet(parameters('blobServices'), 'containerDeleteRetentionPolicyDays')]" - }, - "containerDeleteRetentionPolicyAllowPermanentDelete": { - "value": "[tryGet(parameters('blobServices'), 'containerDeleteRetentionPolicyAllowPermanentDelete')]" - }, - "corsRules": { - "value": "[tryGet(parameters('blobServices'), 'corsRules')]" + "name": { + "value": "[coalesce(tryGet(parameters('extensionAzureDiskEncryptionConfig'), 'name'), 'AzureDiskEncryption')]" }, - "defaultServiceVersion": { - "value": "[tryGet(parameters('blobServices'), 'defaultServiceVersion')]" + "location": { + "value": "[parameters('location')]" }, - "deleteRetentionPolicyAllowPermanentDelete": { - "value": "[tryGet(parameters('blobServices'), 'deleteRetentionPolicyAllowPermanentDelete')]" + "publisher": { + "value": "Microsoft.Azure.Security" }, - "deleteRetentionPolicyEnabled": { - "value": "[tryGet(parameters('blobServices'), 'deleteRetentionPolicyEnabled')]" + "type": "[if(equals(parameters('osType'), 'Windows'), createObject('value', 'AzureDiskEncryption'), createObject('value', 'AzureDiskEncryptionForLinux'))]", + "typeHandlerVersion": { + "value": "[coalesce(tryGet(parameters('extensionAzureDiskEncryptionConfig'), 'typeHandlerVersion'), if(equals(parameters('osType'), 'Windows'), '2.2', '1.1'))]" }, - "deleteRetentionPolicyDays": { - "value": "[tryGet(parameters('blobServices'), 'deleteRetentionPolicyDays')]" + "autoUpgradeMinorVersion": { + "value": "[coalesce(tryGet(parameters('extensionAzureDiskEncryptionConfig'), 'autoUpgradeMinorVersion'), true())]" }, - "isVersioningEnabled": { - "value": "[tryGet(parameters('blobServices'), 'isVersioningEnabled')]" + "enableAutomaticUpgrade": { + "value": "[coalesce(tryGet(parameters('extensionAzureDiskEncryptionConfig'), 'enableAutomaticUpgrade'), false())]" }, - "lastAccessTimeTrackingPolicyEnabled": { - "value": "[tryGet(parameters('blobServices'), 'lastAccessTimeTrackingPolicyEnabled')]" + "forceUpdateTag": { + "value": "[coalesce(tryGet(parameters('extensionAzureDiskEncryptionConfig'), 'forceUpdateTag'), '1.0')]" }, - "restorePolicyEnabled": { - "value": "[tryGet(parameters('blobServices'), 'restorePolicyEnabled')]" + "settings": { + "value": "[coalesce(tryGet(parameters('extensionAzureDiskEncryptionConfig'), 'settings'), createObject())]" }, - "restorePolicyDays": { - "value": "[tryGet(parameters('blobServices'), 'restorePolicyDays')]" + "supressFailures": { + "value": "[coalesce(tryGet(parameters('extensionAzureDiskEncryptionConfig'), 'supressFailures'), false())]" }, - "diagnosticSettings": { - "value": "[tryGet(parameters('blobServices'), 'diagnosticSettings')]" + "tags": { + "value": "[coalesce(tryGet(parameters('extensionAzureDiskEncryptionConfig'), 'tags'), parameters('tags'))]" } }, "template": { @@ -20268,2398 +17188,1999 @@ "_generator": { "name": "bicep", "version": "0.39.26.7824", - "templateHash": "4804748808287128942" + "templateHash": "8391598897118491777" }, - "name": "Storage Account blob Services", - "description": "This module deploys a Storage Account Blob Service." + "name": "Virtual Machine Extensions", + "description": "This module deploys a Virtual Machine Extension." }, - "definitions": { - "corsRuleType": { - "type": "object", - "properties": { - "allowedHeaders": { - "type": "array", - "items": { - "type": "string" - }, - "metadata": { - "description": "Required. A list of headers allowed to be part of the cross-origin request." - } - }, - "allowedMethods": { - "type": "array", - "allowedValues": [ - "CONNECT", - "DELETE", - "GET", - "HEAD", - "MERGE", - "OPTIONS", - "PATCH", - "POST", - "PUT", - "TRACE" - ], - "metadata": { - "description": "Required. A list of HTTP methods that are allowed to be executed by the origin." - } - }, - "allowedOrigins": { - "type": "array", - "items": { - "type": "string" - }, - "metadata": { - "description": "Required. A list of origin domains that will be allowed via CORS, or \"*\" to allow all domains." - } - }, - "exposedHeaders": { - "type": "array", - "items": { - "type": "string" - }, - "metadata": { - "description": "Required. A list of response headers to expose to CORS clients." - } - }, - "maxAgeInSeconds": { - "type": "int", - "metadata": { - "description": "Required. The number of seconds that the client/browser should cache a preflight response." - } - } - }, - "metadata": { - "__bicep_export!": true, - "description": "The type for a cors rule." - } - }, - "containerType": { - "type": "object", - "properties": { - "name": { - "type": "string", - "metadata": { - "description": "Required. The name of the Storage Container to deploy." - } - }, - "defaultEncryptionScope": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. Default the container to use specified encryption scope for all writes." - } - }, - "denyEncryptionScopeOverride": { - "type": "bool", - "nullable": true, - "metadata": { - "description": "Optional. Block override of encryption scope from the container default." - } - }, - "enableNfsV3AllSquash": { - "type": "bool", - "nullable": true, - "metadata": { - "description": "Optional. Enable NFSv3 all squash on blob container." - } - }, - "enableNfsV3RootSquash": { - "type": "bool", - "nullable": true, - "metadata": { - "description": "Optional. Enable NFSv3 root squash on blob container." - } - }, - "immutableStorageWithVersioningEnabled": { - "type": "bool", - "nullable": true, - "metadata": { - "description": "Optional. This is an immutable property, when set to true it enables object level immutability at the container level. The property is immutable and can only be set to true at the container creation time. Existing containers must undergo a migration process." - } - }, - "immutabilityPolicy": { - "$ref": "#/definitions/immutabilityPolicyType", - "nullable": true, - "metadata": { - "description": "Optional. Configure immutability policy." - } - }, - "metadata": { - "type": "object", - "metadata": { - "__bicep_resource_derived_type!": { - "source": "Microsoft.Storage/storageAccounts/blobServices/containers@2024-01-01#properties/properties/properties/metadata" - }, - "description": "Optional. A name-value pair to associate with the container as metadata." - }, - "nullable": true - }, - "publicAccess": { - "type": "string", - "allowedValues": [ - "Blob", - "Container", - "None" - ], - "nullable": true, - "metadata": { - "description": "Optional. Specifies whether data in the container may be accessed publicly and the level of access." - } - }, - "roleAssignments": { - "type": "array", - "items": { - "$ref": "#/definitions/roleAssignmentType" - }, - "nullable": true, - "metadata": { - "description": "Optional. Array of role assignments to create." - } - } - }, - "metadata": { - "__bicep_export!": true, - "description": "The type of a storage container." - } - }, - "diagnosticSettingFullType": { - "type": "object", - "properties": { - "name": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The name of the diagnostic setting." - } - }, - "logCategoriesAndGroups": { - "type": "array", - "items": { - "type": "object", - "properties": { - "category": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. Name of a Diagnostic Log category for a resource type this setting is applied to. Set the specific logs to collect here." - } - }, - "categoryGroup": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. Name of a Diagnostic Log category group for a resource type this setting is applied to. Set to `allLogs` to collect all logs." - } - }, - "enabled": { - "type": "bool", - "nullable": true, - "metadata": { - "description": "Optional. Enable or disable the category explicitly. Default is `true`." - } - } - } - }, - "nullable": true, - "metadata": { - "description": "Optional. The name of logs that will be streamed. \"allLogs\" includes all possible logs for the resource. Set to `[]` to disable log collection." - } - }, - "metricCategories": { - "type": "array", - "items": { - "type": "object", - "properties": { - "category": { - "type": "string", - "metadata": { - "description": "Required. Name of a Diagnostic Metric category for a resource type this setting is applied to. Set to `AllMetrics` to collect all metrics." - } - }, - "enabled": { - "type": "bool", - "nullable": true, - "metadata": { - "description": "Optional. Enable or disable the category explicitly. Default is `true`." - } - } - } - }, - "nullable": true, - "metadata": { - "description": "Optional. The name of metrics that will be streamed. \"allMetrics\" includes all possible metrics for the resource. Set to `[]` to disable metric collection." - } - }, - "logAnalyticsDestinationType": { - "type": "string", - "allowedValues": [ - "AzureDiagnostics", - "Dedicated" - ], - "nullable": true, - "metadata": { - "description": "Optional. A string indicating whether the export to Log Analytics should use the default destination type, i.e. AzureDiagnostics, or use a destination type." - } - }, - "workspaceResourceId": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. Resource ID of the diagnostic log analytics workspace. For security reasons, it is recommended to set diagnostic settings to send data to either storage account, log analytics workspace or event hub." - } - }, - "storageAccountResourceId": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. Resource ID of the diagnostic storage account. For security reasons, it is recommended to set diagnostic settings to send data to either storage account, log analytics workspace or event hub." - } - }, - "eventHubAuthorizationRuleResourceId": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. Resource ID of the diagnostic event hub authorization rule for the Event Hubs namespace in which the event hub should be created or streamed to." - } - }, - "eventHubName": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. Name of the diagnostic event hub within the namespace to which logs are streamed. Without this, an event hub is created for each log category. For security reasons, it is recommended to set diagnostic settings to send data to either storage account, log analytics workspace or event hub." - } - }, - "marketplacePartnerResourceId": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The full ARM resource ID of the Marketplace resource to which you would like to send Diagnostic Logs." - } - } - }, - "metadata": { - "description": "An AVM-aligned type for a diagnostic setting. To be used if both logs & metrics are supported by the resource provider.", - "__bicep_imported_from!": { - "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.1" - } - } - }, - "immutabilityPolicyType": { - "type": "object", - "properties": { - "immutabilityPeriodSinceCreationInDays": { - "type": "int", - "nullable": true, - "metadata": { - "description": "Optional. The immutability period for the blobs in the container since the policy creation, in days." - } - }, - "allowProtectedAppendWrites": { - "type": "bool", - "nullable": true, - "metadata": { - "description": "Optional. This property can only be changed for unlocked time-based retention policies. When enabled, new blocks can be written to an append blob while maintaining immutability protection and compliance. Only new blocks can be added and any existing blocks cannot be modified or deleted. This property cannot be changed with ExtendImmutabilityPolicy API." - } - }, - "allowProtectedAppendWritesAll": { - "type": "bool", - "nullable": true, - "metadata": { - "description": "Optional. This property can only be changed for unlocked time-based retention policies. When enabled, new blocks can be written to both \"Append and Block Blobs\" while maintaining immutability protection and compliance. Only new blocks can be added and any existing blocks cannot be modified or deleted. This property cannot be changed with ExtendImmutabilityPolicy API. The \"allowProtectedAppendWrites\" and \"allowProtectedAppendWritesAll\" properties are mutually exclusive." - } - } - }, + "parameters": { + "virtualMachineName": { + "type": "string", "metadata": { - "description": "The type for an immutability policy.", - "__bicep_imported_from!": { - "sourceTemplate": "container/main.bicep" - } + "description": "Conditional. The name of the parent virtual machine that extension is provisioned for. Required if the template is used in a standalone deployment." } }, - "roleAssignmentType": { - "type": "object", - "properties": { - "name": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The name (as GUID) of the role assignment. If not provided, a GUID will be generated." - } - }, - "roleDefinitionIdOrName": { - "type": "string", - "metadata": { - "description": "Required. The role to assign. You can provide either the display name of the role definition, the role definition GUID, or its fully qualified ID in the following format: '/providers/Microsoft.Authorization/roleDefinitions/c2f4ef07-c644-48eb-af81-4b1b4947fb11'." - } - }, - "principalId": { - "type": "string", - "metadata": { - "description": "Required. The principal ID of the principal (user/group/identity) to assign the role to." - } - }, - "principalType": { - "type": "string", - "allowedValues": [ - "Device", - "ForeignGroup", - "Group", - "ServicePrincipal", - "User" - ], - "nullable": true, - "metadata": { - "description": "Optional. The principal type of the assigned principal ID." - } - }, - "description": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The description of the role assignment." - } - }, - "condition": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The conditions on the role assignment. This limits the resources it can be assigned to. e.g.: @Resource[Microsoft.Storage/storageAccounts/blobServices/containers:ContainerName] StringEqualsIgnoreCase \"foo_storage_container\"." - } - }, - "conditionVersion": { - "type": "string", - "allowedValues": [ - "2.0" - ], - "nullable": true, - "metadata": { - "description": "Optional. Version of the condition." - } - }, - "delegatedManagedIdentityResourceId": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The Resource Id of the delegated managed identity resource." - } - } - }, + "name": { + "type": "string", "metadata": { - "description": "An AVM-aligned type for a role assignment.", - "__bicep_imported_from!": { - "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.1" - } + "description": "Required. The name of the virtual machine extension." } - } - }, - "parameters": { - "storageAccountName": { + }, + "location": { "type": "string", - "maxLength": 24, + "defaultValue": "[resourceGroup().location]", "metadata": { - "description": "Conditional. The name of the parent Storage Account. Required if the template is used in a standalone deployment." + "description": "Optional. The location the extension is deployed to." } }, - "automaticSnapshotPolicyEnabled": { - "type": "bool", - "defaultValue": false, + "publisher": { + "type": "string", "metadata": { - "description": "Optional. Automatic Snapshot is enabled if set to true." + "description": "Required. The name of the extension handler publisher." } }, - "changeFeedEnabled": { + "type": { + "type": "string", + "metadata": { + "description": "Required. Specifies the type of the extension; an example is \"CustomScriptExtension\"." + } + }, + "typeHandlerVersion": { + "type": "string", + "metadata": { + "description": "Required. Specifies the version of the script handler." + } + }, + "autoUpgradeMinorVersion": { "type": "bool", - "defaultValue": false, "metadata": { - "description": "Optional. The blob service properties for change feed events. Indicates whether change feed event logging is enabled for the Blob service." + "description": "Required. Indicates whether the extension should use a newer minor version if one is available at deployment time. Once deployed, however, the extension will not upgrade minor versions unless redeployed, even with this property set to true." } }, - "changeFeedRetentionInDays": { - "type": "int", + "forceUpdateTag": { + "type": "string", "nullable": true, - "minValue": 1, - "maxValue": 146000, "metadata": { - "description": "Optional. Indicates whether change feed event logging is enabled for the Blob service. Indicates the duration of changeFeed retention in days. If left blank, it indicates an infinite retention of the change feed." + "description": "Optional. How the extension handler should be forced to update even if the extension configuration has not changed." } }, - "containerDeleteRetentionPolicyEnabled": { - "type": "bool", - "defaultValue": true, + "settings": { + "type": "object", + "nullable": true, "metadata": { - "description": "Optional. The blob service properties for container soft delete. Indicates whether DeleteRetentionPolicy is enabled." + "description": "Optional. Any object that contains the extension specific settings." } }, - "containerDeleteRetentionPolicyDays": { - "type": "int", + "protectedSettings": { + "type": "secureObject", "nullable": true, - "minValue": 1, - "maxValue": 365, "metadata": { - "description": "Optional. Indicates the number of days that the deleted item should be retained." + "description": "Optional. Any object that contains the extension specific protected settings." } }, - "containerDeleteRetentionPolicyAllowPermanentDelete": { + "supressFailures": { "type": "bool", "defaultValue": false, "metadata": { - "description": "Optional. This property when set to true allows deletion of the soft deleted blob versions and snapshots. This property cannot be used with blob restore policy. This property only applies to blob service and does not apply to containers or file share." + "description": "Optional. Indicates whether failures stemming from the extension will be suppressed (Operational failures such as not connecting to the VM will not be suppressed regardless of this value). The default is false." } }, - "corsRules": { - "type": "array", - "items": { - "$ref": "#/definitions/corsRuleType" + "enableAutomaticUpgrade": { + "type": "bool", + "metadata": { + "description": "Required. Indicates whether the extension should be automatically upgraded by the platform if there is a newer version of the extension available." + } + }, + "tags": { + "type": "object", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.Compute/virtualMachines/extensions@2024-11-01#properties/tags" + }, + "description": "Optional. Tags of the resource." }, - "nullable": true, + "nullable": true + }, + "protectedSettingsFromKeyVault": { + "type": "object", "metadata": { - "description": "Optional. The List of CORS rules. You can include up to five CorsRule elements in the request." + "__bicep_resource_derived_type!": { + "source": "Microsoft.Compute/virtualMachines/extensions@2024-11-01#properties/properties/properties/protectedSettingsFromKeyVault" + }, + "description": "Optional. The extensions protected settings that are passed by reference, and consumed from key vault." + }, + "nullable": true + }, + "provisionAfterExtensions": { + "type": "array", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.Compute/virtualMachines/extensions@2024-11-01#properties/properties/properties/provisionAfterExtensions" + }, + "description": "Optional. Collection of extension names after which this extension needs to be provisioned." + }, + "nullable": true + } + }, + "resources": { + "virtualMachine": { + "existing": true, + "type": "Microsoft.Compute/virtualMachines", + "apiVersion": "2024-11-01", + "name": "[parameters('virtualMachineName')]" + }, + "extension": { + "type": "Microsoft.Compute/virtualMachines/extensions", + "apiVersion": "2024-11-01", + "name": "[format('{0}/{1}', parameters('virtualMachineName'), parameters('name'))]", + "location": "[parameters('location')]", + "tags": "[parameters('tags')]", + "properties": { + "publisher": "[parameters('publisher')]", + "type": "[parameters('type')]", + "typeHandlerVersion": "[parameters('typeHandlerVersion')]", + "autoUpgradeMinorVersion": "[parameters('autoUpgradeMinorVersion')]", + "enableAutomaticUpgrade": "[parameters('enableAutomaticUpgrade')]", + "forceUpdateTag": "[parameters('forceUpdateTag')]", + "settings": "[parameters('settings')]", + "protectedSettings": "[parameters('protectedSettings')]", + "suppressFailures": "[parameters('supressFailures')]", + "protectedSettingsFromKeyVault": "[parameters('protectedSettingsFromKeyVault')]", + "provisionAfterExtensions": "[parameters('provisionAfterExtensions')]" } + } + }, + "outputs": { + "name": { + "type": "string", + "metadata": { + "description": "The name of the extension." + }, + "value": "[parameters('name')]" }, - "defaultServiceVersion": { + "resourceId": { "type": "string", - "nullable": true, "metadata": { - "description": "Optional. Indicates the default version to use for requests to the Blob service if an incoming request's version is not specified. Possible values include version 2008-10-27 and all more recent versions." + "description": "The resource ID of the extension." + }, + "value": "[resourceId('Microsoft.Compute/virtualMachines/extensions', parameters('virtualMachineName'), parameters('name'))]" + }, + "resourceGroupName": { + "type": "string", + "metadata": { + "description": "The name of the Resource Group the extension was created in." + }, + "value": "[resourceGroup().name]" + }, + "location": { + "type": "string", + "metadata": { + "description": "The location the resource was deployed into." + }, + "value": "[reference('extension', '2024-11-01', 'full').location]" + } + } + } + }, + "dependsOn": [ + "vm", + "vm_customScriptExtension" + ] + }, + "vm_nvidiaGpuDriverWindowsExtension": { + "condition": "[parameters('extensionNvidiaGpuDriverWindows').enabled]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "[format('{0}-VM-NvidiaGpuDriverWindows', uniqueString(deployment().name, parameters('location')))]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "virtualMachineName": { + "value": "[parameters('name')]" + }, + "name": { + "value": "[coalesce(tryGet(parameters('extensionNvidiaGpuDriverWindows'), 'name'), 'NvidiaGpuDriverWindows')]" + }, + "location": { + "value": "[parameters('location')]" + }, + "publisher": { + "value": "Microsoft.HpcCompute" + }, + "type": { + "value": "NvidiaGpuDriverWindows" + }, + "typeHandlerVersion": { + "value": "[coalesce(tryGet(parameters('extensionNvidiaGpuDriverWindows'), 'typeHandlerVersion'), '1.4')]" + }, + "autoUpgradeMinorVersion": { + "value": "[coalesce(tryGet(parameters('extensionNvidiaGpuDriverWindows'), 'autoUpgradeMinorVersion'), true())]" + }, + "enableAutomaticUpgrade": { + "value": "[coalesce(tryGet(parameters('extensionNvidiaGpuDriverWindows'), 'enableAutomaticUpgrade'), false())]" + }, + "supressFailures": { + "value": "[coalesce(tryGet(parameters('extensionNvidiaGpuDriverWindows'), 'supressFailures'), false())]" + }, + "tags": { + "value": "[coalesce(tryGet(parameters('extensionNvidiaGpuDriverWindows'), 'tags'), parameters('tags'))]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "languageVersion": "2.0", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.39.26.7824", + "templateHash": "8391598897118491777" + }, + "name": "Virtual Machine Extensions", + "description": "This module deploys a Virtual Machine Extension." + }, + "parameters": { + "virtualMachineName": { + "type": "string", + "metadata": { + "description": "Conditional. The name of the parent virtual machine that extension is provisioned for. Required if the template is used in a standalone deployment." } }, - "deleteRetentionPolicyEnabled": { - "type": "bool", - "defaultValue": true, + "name": { + "type": "string", "metadata": { - "description": "Optional. The blob service properties for blob soft delete." + "description": "Required. The name of the virtual machine extension." } }, - "deleteRetentionPolicyDays": { - "type": "int", - "defaultValue": 7, - "minValue": 1, - "maxValue": 365, + "location": { + "type": "string", + "defaultValue": "[resourceGroup().location]", "metadata": { - "description": "Optional. Indicates the number of days that the deleted blob should be retained." + "description": "Optional. The location the extension is deployed to." } }, - "deleteRetentionPolicyAllowPermanentDelete": { - "type": "bool", - "defaultValue": false, + "publisher": { + "type": "string", "metadata": { - "description": "Optional. This property when set to true allows deletion of the soft deleted blob versions and snapshots. This property cannot be used with blob restore policy. This property only applies to blob service and does not apply to containers or file share." + "description": "Required. The name of the extension handler publisher." } }, - "isVersioningEnabled": { - "type": "bool", - "defaultValue": false, + "type": { + "type": "string", "metadata": { - "description": "Optional. Use versioning to automatically maintain previous versions of your blobs. Cannot be enabled for ADLS Gen2 storage accounts." + "description": "Required. Specifies the type of the extension; an example is \"CustomScriptExtension\"." } }, - "lastAccessTimeTrackingPolicyEnabled": { - "type": "bool", - "defaultValue": false, + "typeHandlerVersion": { + "type": "string", "metadata": { - "description": "Optional. The blob service property to configure last access time based tracking policy. When set to true last access time based tracking is enabled." + "description": "Required. Specifies the version of the script handler." } }, - "restorePolicyEnabled": { + "autoUpgradeMinorVersion": { "type": "bool", - "defaultValue": false, "metadata": { - "description": "Optional. The blob service properties for blob restore policy. If point-in-time restore is enabled, then versioning, change feed, and blob soft delete must also be enabled." + "description": "Required. Indicates whether the extension should use a newer minor version if one is available at deployment time. Once deployed, however, the extension will not upgrade minor versions unless redeployed, even with this property set to true." } }, - "restorePolicyDays": { - "type": "int", - "defaultValue": 7, - "minValue": 1, + "forceUpdateTag": { + "type": "string", + "nullable": true, "metadata": { - "description": "Optional. How long this blob can be restored. It should be less than DeleteRetentionPolicy days." + "description": "Optional. How the extension handler should be forced to update even if the extension configuration has not changed." } }, - "containers": { - "type": "array", - "items": { - "$ref": "#/definitions/containerType" - }, + "settings": { + "type": "object", "nullable": true, "metadata": { - "description": "Optional. Blob containers to create." + "description": "Optional. Any object that contains the extension specific settings." } }, - "diagnosticSettings": { - "type": "array", - "items": { - "$ref": "#/definitions/diagnosticSettingFullType" - }, + "protectedSettings": { + "type": "secureObject", "nullable": true, "metadata": { - "description": "Optional. The diagnostic settings of the service." + "description": "Optional. Any object that contains the extension specific protected settings." } - } - }, - "variables": { - "enableReferencedModulesTelemetry": false, - "name": "default" - }, - "resources": { - "storageAccount": { - "existing": true, - "type": "Microsoft.Storage/storageAccounts", - "apiVersion": "2025-01-01", - "name": "[parameters('storageAccountName')]" - }, - "blobServices": { - "type": "Microsoft.Storage/storageAccounts/blobServices", - "apiVersion": "2025-01-01", - "name": "[format('{0}/{1}', parameters('storageAccountName'), variables('name'))]", - "properties": { - "automaticSnapshotPolicyEnabled": "[parameters('automaticSnapshotPolicyEnabled')]", - "changeFeed": "[if(parameters('changeFeedEnabled'), createObject('enabled', true(), 'retentionInDays', parameters('changeFeedRetentionInDays')), null())]", - "containerDeleteRetentionPolicy": { - "enabled": "[parameters('containerDeleteRetentionPolicyEnabled')]", - "days": "[parameters('containerDeleteRetentionPolicyDays')]", - "allowPermanentDelete": "[if(equals(parameters('containerDeleteRetentionPolicyEnabled'), true()), parameters('containerDeleteRetentionPolicyAllowPermanentDelete'), null())]" - }, - "cors": "[if(not(equals(parameters('corsRules'), null())), createObject('corsRules', parameters('corsRules')), null())]", - "defaultServiceVersion": "[parameters('defaultServiceVersion')]", - "deleteRetentionPolicy": { - "enabled": "[parameters('deleteRetentionPolicyEnabled')]", - "days": "[parameters('deleteRetentionPolicyDays')]", - "allowPermanentDelete": "[if(and(parameters('deleteRetentionPolicyEnabled'), parameters('deleteRetentionPolicyAllowPermanentDelete')), true(), null())]" - }, - "isVersioningEnabled": "[parameters('isVersioningEnabled')]", - "lastAccessTimeTrackingPolicy": "[if(and(not(equals(reference('storageAccount', '2025-01-01', 'full').kind, 'Storage')), empty(tryGet(reference('storageAccount', '2025-01-01', 'full'), 'extendedLocation'))), createObject('enable', parameters('lastAccessTimeTrackingPolicyEnabled'), 'name', if(equals(parameters('lastAccessTimeTrackingPolicyEnabled'), true()), 'AccessTimeTracking', null()), 'trackingGranularityInDays', if(equals(parameters('lastAccessTimeTrackingPolicyEnabled'), true()), 1, null())), null())]", - "restorePolicy": "[if(parameters('restorePolicyEnabled'), createObject('enabled', true(), 'days', parameters('restorePolicyDays')), null())]" - }, - "dependsOn": [ - "storageAccount" - ] - }, - "blobServices_diagnosticSettings": { - "copy": { - "name": "blobServices_diagnosticSettings", - "count": "[length(coalesce(parameters('diagnosticSettings'), createArray()))]" - }, - "type": "Microsoft.Insights/diagnosticSettings", - "apiVersion": "2021-05-01-preview", - "scope": "[format('Microsoft.Storage/storageAccounts/{0}/blobServices/{1}', parameters('storageAccountName'), variables('name'))]", - "name": "[coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'name'), format('{0}-diagnosticSettings', variables('name')))]", - "properties": { - "copy": [ - { - "name": "metrics", - "count": "[length(coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'metricCategories'), createArray(createObject('category', 'AllMetrics'))))]", - "input": { - "category": "[coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'metricCategories'), createArray(createObject('category', 'AllMetrics')))[copyIndex('metrics')].category]", - "enabled": "[coalesce(tryGet(coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'metricCategories'), createArray(createObject('category', 'AllMetrics')))[copyIndex('metrics')], 'enabled'), true())]", - "timeGrain": null - } - }, - { - "name": "logs", - "count": "[length(coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'logCategoriesAndGroups'), createArray(createObject('categoryGroup', 'allLogs'))))]", - "input": { - "categoryGroup": "[tryGet(coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'logCategoriesAndGroups'), createArray(createObject('categoryGroup', 'allLogs')))[copyIndex('logs')], 'categoryGroup')]", - "category": "[tryGet(coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'logCategoriesAndGroups'), createArray(createObject('categoryGroup', 'allLogs')))[copyIndex('logs')], 'category')]", - "enabled": "[coalesce(tryGet(coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'logCategoriesAndGroups'), createArray(createObject('categoryGroup', 'allLogs')))[copyIndex('logs')], 'enabled'), true())]" - } - } - ], - "storageAccountId": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'storageAccountResourceId')]", - "workspaceId": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'workspaceResourceId')]", - "eventHubAuthorizationRuleId": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'eventHubAuthorizationRuleResourceId')]", - "eventHubName": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'eventHubName')]", - "marketplacePartnerId": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'marketplacePartnerResourceId')]", - "logAnalyticsDestinationType": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'logAnalyticsDestinationType')]" - }, - "dependsOn": [ - "blobServices" - ] - }, - "blobServices_container": { - "copy": { - "name": "blobServices_container", - "count": "[length(coalesce(parameters('containers'), createArray()))]" - }, - "type": "Microsoft.Resources/deployments", - "apiVersion": "2025-04-01", - "name": "[format('{0}-Container-{1}', deployment().name, copyIndex())]", - "properties": { - "expressionEvaluationOptions": { - "scope": "inner" - }, - "mode": "Incremental", - "parameters": { - "storageAccountName": { - "value": "[parameters('storageAccountName')]" - }, - "blobServiceName": { - "value": "[variables('name')]" - }, - "name": { - "value": "[coalesce(parameters('containers'), createArray())[copyIndex()].name]" - }, - "defaultEncryptionScope": { - "value": "[tryGet(coalesce(parameters('containers'), createArray())[copyIndex()], 'defaultEncryptionScope')]" - }, - "denyEncryptionScopeOverride": { - "value": "[tryGet(coalesce(parameters('containers'), createArray())[copyIndex()], 'denyEncryptionScopeOverride')]" - }, - "enableNfsV3AllSquash": { - "value": "[tryGet(coalesce(parameters('containers'), createArray())[copyIndex()], 'enableNfsV3AllSquash')]" - }, - "enableNfsV3RootSquash": { - "value": "[tryGet(coalesce(parameters('containers'), createArray())[copyIndex()], 'enableNfsV3RootSquash')]" - }, - "immutableStorageWithVersioningEnabled": { - "value": "[tryGet(coalesce(parameters('containers'), createArray())[copyIndex()], 'immutableStorageWithVersioningEnabled')]" - }, - "metadata": { - "value": "[tryGet(coalesce(parameters('containers'), createArray())[copyIndex()], 'metadata')]" - }, - "publicAccess": { - "value": "[tryGet(coalesce(parameters('containers'), createArray())[copyIndex()], 'publicAccess')]" - }, - "roleAssignments": { - "value": "[tryGet(coalesce(parameters('containers'), createArray())[copyIndex()], 'roleAssignments')]" - }, - "immutabilityPolicy": { - "value": "[tryGet(coalesce(parameters('containers'), createArray())[copyIndex()], 'immutabilityPolicy')]" - }, - "enableTelemetry": { - "value": "[variables('enableReferencedModulesTelemetry')]" - } + }, + "supressFailures": { + "type": "bool", + "defaultValue": false, + "metadata": { + "description": "Optional. Indicates whether failures stemming from the extension will be suppressed (Operational failures such as not connecting to the VM will not be suppressed regardless of this value). The default is false." + } + }, + "enableAutomaticUpgrade": { + "type": "bool", + "metadata": { + "description": "Required. Indicates whether the extension should be automatically upgraded by the platform if there is a newer version of the extension available." + } + }, + "tags": { + "type": "object", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.Compute/virtualMachines/extensions@2024-11-01#properties/tags" }, - "template": { - "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", - "languageVersion": "2.0", - "contentVersion": "1.0.0.0", - "metadata": { - "_generator": { - "name": "bicep", - "version": "0.39.26.7824", - "templateHash": "4737601268768949442" - }, - "name": "Storage Account Blob Containers", - "description": "This module deploys a Storage Account Blob Container." - }, - "definitions": { - "immutabilityPolicyType": { - "type": "object", - "properties": { - "immutabilityPeriodSinceCreationInDays": { - "type": "int", - "nullable": true, - "metadata": { - "description": "Optional. The immutability period for the blobs in the container since the policy creation, in days." - } - }, - "allowProtectedAppendWrites": { - "type": "bool", - "nullable": true, - "metadata": { - "description": "Optional. This property can only be changed for unlocked time-based retention policies. When enabled, new blocks can be written to an append blob while maintaining immutability protection and compliance. Only new blocks can be added and any existing blocks cannot be modified or deleted. This property cannot be changed with ExtendImmutabilityPolicy API." - } - }, - "allowProtectedAppendWritesAll": { - "type": "bool", - "nullable": true, - "metadata": { - "description": "Optional. This property can only be changed for unlocked time-based retention policies. When enabled, new blocks can be written to both \"Append and Block Blobs\" while maintaining immutability protection and compliance. Only new blocks can be added and any existing blocks cannot be modified or deleted. This property cannot be changed with ExtendImmutabilityPolicy API. The \"allowProtectedAppendWrites\" and \"allowProtectedAppendWritesAll\" properties are mutually exclusive." - } - } - }, - "metadata": { - "__bicep_export!": true, - "description": "The type for an immutability policy." - } - }, - "roleAssignmentType": { - "type": "object", - "properties": { - "name": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The name (as GUID) of the role assignment. If not provided, a GUID will be generated." - } - }, - "roleDefinitionIdOrName": { - "type": "string", - "metadata": { - "description": "Required. The role to assign. You can provide either the display name of the role definition, the role definition GUID, or its fully qualified ID in the following format: '/providers/Microsoft.Authorization/roleDefinitions/c2f4ef07-c644-48eb-af81-4b1b4947fb11'." - } - }, - "principalId": { - "type": "string", - "metadata": { - "description": "Required. The principal ID of the principal (user/group/identity) to assign the role to." - } - }, - "principalType": { - "type": "string", - "allowedValues": [ - "Device", - "ForeignGroup", - "Group", - "ServicePrincipal", - "User" - ], - "nullable": true, - "metadata": { - "description": "Optional. The principal type of the assigned principal ID." - } - }, - "description": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The description of the role assignment." - } - }, - "condition": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The conditions on the role assignment. This limits the resources it can be assigned to. e.g.: @Resource[Microsoft.Storage/storageAccounts/blobServices/containers:ContainerName] StringEqualsIgnoreCase \"foo_storage_container\"." - } - }, - "conditionVersion": { - "type": "string", - "allowedValues": [ - "2.0" - ], - "nullable": true, - "metadata": { - "description": "Optional. Version of the condition." - } - }, - "delegatedManagedIdentityResourceId": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The Resource Id of the delegated managed identity resource." - } - } - }, - "metadata": { - "description": "An AVM-aligned type for a role assignment.", - "__bicep_imported_from!": { - "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.1" - } - } - } - }, - "parameters": { - "storageAccountName": { - "type": "string", - "maxLength": 24, - "metadata": { - "description": "Conditional. The name of the parent Storage Account. Required if the template is used in a standalone deployment." - } - }, - "blobServiceName": { - "type": "string", - "defaultValue": "default", - "metadata": { - "description": "Optional. The name of the parent Blob Service. Required if the template is used in a standalone deployment." - } - }, - "name": { - "type": "string", - "metadata": { - "description": "Required. The name of the Storage Container to deploy." - } - }, - "defaultEncryptionScope": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. Default the container to use specified encryption scope for all writes." - } - }, - "denyEncryptionScopeOverride": { - "type": "bool", - "nullable": true, - "metadata": { - "description": "Optional. Block override of encryption scope from the container default." - } - }, - "enableNfsV3AllSquash": { - "type": "bool", - "defaultValue": false, - "metadata": { - "description": "Optional. Enable NFSv3 all squash on blob container." - } - }, - "enableNfsV3RootSquash": { - "type": "bool", - "defaultValue": false, - "metadata": { - "description": "Optional. Enable NFSv3 root squash on blob container." - } - }, - "immutableStorageWithVersioningEnabled": { - "type": "bool", - "defaultValue": false, - "metadata": { - "description": "Optional. This is an immutable property, when set to true it enables object level immutability at the container level. The property is immutable and can only be set to true at the container creation time. Existing containers must undergo a migration process." - } - }, - "immutabilityPolicy": { - "$ref": "#/definitions/immutabilityPolicyType", - "nullable": true, - "metadata": { - "description": "Optional. Configure immutability policy." - } - }, - "metadata": { - "type": "object", - "metadata": { - "__bicep_resource_derived_type!": { - "source": "Microsoft.Storage/storageAccounts/blobServices/containers@2024-01-01#properties/properties/properties/metadata" - }, - "description": "Optional. A name-value pair to associate with the container as metadata." - }, - "defaultValue": {} - }, - "publicAccess": { - "type": "string", - "defaultValue": "None", - "allowedValues": [ - "Container", - "Blob", - "None" - ], - "metadata": { - "description": "Optional. Specifies whether data in the container may be accessed publicly and the level of access." - } - }, - "enableTelemetry": { - "type": "bool", - "defaultValue": true, - "metadata": { - "description": "Optional. Enable/Disable usage telemetry for module." - } - }, - "roleAssignments": { - "type": "array", - "items": { - "$ref": "#/definitions/roleAssignmentType" - }, - "nullable": true, - "metadata": { - "description": "Optional. Array of role assignments to create." - } - } - }, - "variables": { - "copy": [ - { - "name": "formattedRoleAssignments", - "count": "[length(coalesce(parameters('roleAssignments'), createArray()))]", - "input": "[union(coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')], createObject('roleDefinitionId', coalesce(tryGet(variables('builtInRoleNames'), coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName), if(contains(coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName, '/providers/Microsoft.Authorization/roleDefinitions/'), coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName, subscriptionResourceId('Microsoft.Authorization/roleDefinitions', coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName)))))]" - } - ], - "builtInRoleNames": { - "Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'b24988ac-6180-42a0-ab88-20f7382dd24c')]", - "Owner": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '8e3af657-a8ff-443c-a75c-2fe8c4bcb635')]", - "Reader": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'acdd72a7-3385-48ef-bd42-f606fba81ae7')]", - "Reader and Data Access": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'c12c1c16-33a1-487b-954d-41c89c60f349')]", - "Role Based Access Control Administrator": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'f58310d9-a9f6-439a-9e8d-f62e7b41a168')]", - "Storage Account Backup Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'e5e2a7ff-d759-4cd2-bb51-3152d37e2eb1')]", - "Storage Account Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '17d1049b-9a84-46fb-8f53-869881c3d3ab')]", - "Storage Account Key Operator Service Role": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '81a9662b-bebf-436f-a333-f67b29880f12')]", - "Storage Blob Data Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'ba92f5b4-2d11-453d-a403-e96b0029c9fe')]", - "Storage Blob Data Owner": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'b7e6dc6d-f1e8-4753-8033-0f276bb0955b')]", - "Storage Blob Data Reader": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '2a2b9908-6ea1-4ae2-8e65-a410df84e7d1')]", - "Storage Blob Delegator": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'db58b8e5-c6ad-4a2a-8342-4190687cbf4a')]", - "User Access Administrator": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '18d7d88d-d35e-4fb5-a5c3-7773c20a72d9')]" - } - }, - "resources": { - "storageAccount::blobServices": { - "existing": true, - "type": "Microsoft.Storage/storageAccounts/blobServices", - "apiVersion": "2025-01-01", - "name": "[format('{0}/{1}', parameters('storageAccountName'), parameters('blobServiceName'))]" - }, - "avmTelemetry": { - "condition": "[parameters('enableTelemetry')]", - "type": "Microsoft.Resources/deployments", - "apiVersion": "2024-03-01", - "name": "[format('46d3xbcp.res.storage-blobcontainer.{0}.{1}', replace('-..--..-', '.', '-'), substring(uniqueString(deployment().name), 0, 4))]", - "properties": { - "mode": "Incremental", - "template": { - "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", - "contentVersion": "1.0.0.0", - "resources": [], - "outputs": { - "telemetry": { - "type": "String", - "value": "For more information, see https://aka.ms/avm/TelemetryInfo" - } - } - } - } - }, - "storageAccount": { - "existing": true, - "type": "Microsoft.Storage/storageAccounts", - "apiVersion": "2025-01-01", - "name": "[parameters('storageAccountName')]" - }, - "container": { - "type": "Microsoft.Storage/storageAccounts/blobServices/containers", - "apiVersion": "2025-01-01", - "name": "[format('{0}/{1}/{2}', parameters('storageAccountName'), parameters('blobServiceName'), parameters('name'))]", - "properties": { - "defaultEncryptionScope": "[parameters('defaultEncryptionScope')]", - "denyEncryptionScopeOverride": "[parameters('denyEncryptionScopeOverride')]", - "enableNfsV3AllSquash": "[if(equals(parameters('enableNfsV3AllSquash'), true()), parameters('enableNfsV3AllSquash'), null())]", - "enableNfsV3RootSquash": "[if(equals(parameters('enableNfsV3RootSquash'), true()), parameters('enableNfsV3RootSquash'), null())]", - "immutableStorageWithVersioning": "[if(parameters('immutableStorageWithVersioningEnabled'), createObject('enabled', parameters('immutableStorageWithVersioningEnabled')), null())]", - "metadata": "[parameters('metadata')]", - "publicAccess": "[parameters('publicAccess')]" - } - }, - "container_roleAssignments": { - "copy": { - "name": "container_roleAssignments", - "count": "[length(coalesce(variables('formattedRoleAssignments'), createArray()))]" - }, - "type": "Microsoft.Authorization/roleAssignments", - "apiVersion": "2022-04-01", - "scope": "[format('Microsoft.Storage/storageAccounts/{0}/blobServices/{1}/containers/{2}', parameters('storageAccountName'), parameters('blobServiceName'), parameters('name'))]", - "name": "[coalesce(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'name'), guid(resourceId('Microsoft.Storage/storageAccounts/blobServices/containers', parameters('storageAccountName'), parameters('blobServiceName'), parameters('name')), coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].principalId, coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].roleDefinitionId))]", - "properties": { - "roleDefinitionId": "[coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].roleDefinitionId]", - "principalId": "[coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].principalId]", - "description": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'description')]", - "principalType": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'principalType')]", - "condition": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'condition')]", - "conditionVersion": "[if(not(empty(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'condition'))), coalesce(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'conditionVersion'), '2.0'), null())]", - "delegatedManagedIdentityResourceId": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'delegatedManagedIdentityResourceId')]" - }, - "dependsOn": [ - "container" - ] - }, - "container_immutabilityPolicy": { - "condition": "[not(empty(coalesce(parameters('immutabilityPolicy'), createObject())))]", - "type": "Microsoft.Resources/deployments", - "apiVersion": "2025-04-01", - "name": "[take(format('{0}-ImmutPol', deployment().name), 64)]", - "properties": { - "expressionEvaluationOptions": { - "scope": "inner" - }, - "mode": "Incremental", - "parameters": { - "storageAccountName": { - "value": "[parameters('storageAccountName')]" - }, - "containerName": { - "value": "[parameters('name')]" - }, - "immutabilityPeriodSinceCreationInDays": { - "value": "[tryGet(parameters('immutabilityPolicy'), 'immutabilityPeriodSinceCreationInDays')]" - }, - "allowProtectedAppendWrites": { - "value": "[tryGet(parameters('immutabilityPolicy'), 'allowProtectedAppendWrites')]" - }, - "allowProtectedAppendWritesAll": { - "value": "[tryGet(parameters('immutabilityPolicy'), 'allowProtectedAppendWritesAll')]" - } - }, - "template": { - "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", - "contentVersion": "1.0.0.0", - "metadata": { - "_generator": { - "name": "bicep", - "version": "0.39.26.7824", - "templateHash": "9416359146780945405" - }, - "name": "Storage Account Blob Container Immutability Policies", - "description": "This module deploys a Storage Account Blob Container Immutability Policy." - }, - "parameters": { - "storageAccountName": { - "type": "string", - "maxLength": 24, - "metadata": { - "description": "Conditional. The name of the parent Storage Account. Required if the template is used in a standalone deployment." - } - }, - "containerName": { - "type": "string", - "metadata": { - "description": "Conditional. The name of the parent container to apply the policy to. Required if the template is used in a standalone deployment." - } - }, - "immutabilityPeriodSinceCreationInDays": { - "type": "int", - "defaultValue": 365, - "metadata": { - "description": "Optional. The immutability period for the blobs in the container since the policy creation, in days." - } - }, - "allowProtectedAppendWrites": { - "type": "bool", - "defaultValue": true, - "metadata": { - "description": "Optional. This property can only be changed for unlocked time-based retention policies. When enabled, new blocks can be written to an append blob while maintaining immutability protection and compliance. Only new blocks can be added and any existing blocks cannot be modified or deleted. This property cannot be changed with ExtendImmutabilityPolicy API. The \"allowProtectedAppendWrites\" and \"allowProtectedAppendWritesAll\" properties are mutually exclusive." - } - }, - "allowProtectedAppendWritesAll": { - "type": "bool", - "defaultValue": true, - "metadata": { - "description": "Optional. This property can only be changed for unlocked time-based retention policies. When enabled, new blocks can be written to both \"Append and Block Blobs\" while maintaining immutability protection and compliance. Only new blocks can be added and any existing blocks cannot be modified or deleted. This property cannot be changed with ExtendImmutabilityPolicy API. The \"allowProtectedAppendWrites\" and \"allowProtectedAppendWritesAll\" properties are mutually exclusive." - } - } - }, - "variables": { - "name": "default" - }, - "resources": [ - { - "type": "Microsoft.Storage/storageAccounts/blobServices/containers/immutabilityPolicies", - "apiVersion": "2025-01-01", - "name": "[format('{0}/{1}/{2}/{3}', parameters('storageAccountName'), 'default', parameters('containerName'), variables('name'))]", - "properties": { - "immutabilityPeriodSinceCreationInDays": "[parameters('immutabilityPeriodSinceCreationInDays')]", - "allowProtectedAppendWrites": "[parameters('allowProtectedAppendWrites')]", - "allowProtectedAppendWritesAll": "[parameters('allowProtectedAppendWritesAll')]" - } - } - ], - "outputs": { - "name": { - "type": "string", - "metadata": { - "description": "The name of the deployed immutability policy." - }, - "value": "[variables('name')]" - }, - "resourceId": { - "type": "string", - "metadata": { - "description": "The resource ID of the deployed immutability policy." - }, - "value": "[resourceId('Microsoft.Storage/storageAccounts/blobServices/containers/immutabilityPolicies', parameters('storageAccountName'), 'default', parameters('containerName'), variables('name'))]" - }, - "resourceGroupName": { - "type": "string", - "metadata": { - "description": "The resource group of the deployed immutability policy." - }, - "value": "[resourceGroup().name]" - } - } - } - }, - "dependsOn": [ - "container" - ] - } - }, - "outputs": { - "name": { - "type": "string", - "metadata": { - "description": "The name of the deployed container." - }, - "value": "[parameters('name')]" - }, - "resourceId": { - "type": "string", - "metadata": { - "description": "The resource ID of the deployed container." - }, - "value": "[resourceId('Microsoft.Storage/storageAccounts/blobServices/containers', parameters('storageAccountName'), parameters('blobServiceName'), parameters('name'))]" - }, - "resourceGroupName": { - "type": "string", - "metadata": { - "description": "The resource group of the deployed container." - }, - "value": "[resourceGroup().name]" - } - } - } + "description": "Optional. Tags of the resource." + }, + "nullable": true + }, + "protectedSettingsFromKeyVault": { + "type": "object", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.Compute/virtualMachines/extensions@2024-11-01#properties/properties/properties/protectedSettingsFromKeyVault" + }, + "description": "Optional. The extensions protected settings that are passed by reference, and consumed from key vault." + }, + "nullable": true + }, + "provisionAfterExtensions": { + "type": "array", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.Compute/virtualMachines/extensions@2024-11-01#properties/properties/properties/provisionAfterExtensions" + }, + "description": "Optional. Collection of extension names after which this extension needs to be provisioned." + }, + "nullable": true + } + }, + "resources": { + "virtualMachine": { + "existing": true, + "type": "Microsoft.Compute/virtualMachines", + "apiVersion": "2024-11-01", + "name": "[parameters('virtualMachineName')]" + }, + "extension": { + "type": "Microsoft.Compute/virtualMachines/extensions", + "apiVersion": "2024-11-01", + "name": "[format('{0}/{1}', parameters('virtualMachineName'), parameters('name'))]", + "location": "[parameters('location')]", + "tags": "[parameters('tags')]", + "properties": { + "publisher": "[parameters('publisher')]", + "type": "[parameters('type')]", + "typeHandlerVersion": "[parameters('typeHandlerVersion')]", + "autoUpgradeMinorVersion": "[parameters('autoUpgradeMinorVersion')]", + "enableAutomaticUpgrade": "[parameters('enableAutomaticUpgrade')]", + "forceUpdateTag": "[parameters('forceUpdateTag')]", + "settings": "[parameters('settings')]", + "protectedSettings": "[parameters('protectedSettings')]", + "suppressFailures": "[parameters('supressFailures')]", + "protectedSettingsFromKeyVault": "[parameters('protectedSettingsFromKeyVault')]", + "provisionAfterExtensions": "[parameters('provisionAfterExtensions')]" + } + } + }, + "outputs": { + "name": { + "type": "string", + "metadata": { + "description": "The name of the extension." }, - "dependsOn": [ - "blobServices" - ] + "value": "[parameters('name')]" + }, + "resourceId": { + "type": "string", + "metadata": { + "description": "The resource ID of the extension." + }, + "value": "[resourceId('Microsoft.Compute/virtualMachines/extensions', parameters('virtualMachineName'), parameters('name'))]" + }, + "resourceGroupName": { + "type": "string", + "metadata": { + "description": "The name of the Resource Group the extension was created in." + }, + "value": "[resourceGroup().name]" + }, + "location": { + "type": "string", + "metadata": { + "description": "The location the resource was deployed into." + }, + "value": "[reference('extension', '2024-11-01', 'full').location]" + } + } + } + }, + "dependsOn": [ + "vm", + "vm_azureDiskEncryptionExtension" + ] + }, + "vm_hostPoolRegistrationExtension": { + "condition": "[parameters('extensionHostPoolRegistration').enabled]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "[format('{0}-VM-HostPoolRegistration', uniqueString(deployment().name, parameters('location')))]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "virtualMachineName": { + "value": "[parameters('name')]" + }, + "name": { + "value": "[coalesce(tryGet(parameters('extensionHostPoolRegistration'), 'name'), 'HostPoolRegistration')]" + }, + "location": { + "value": "[parameters('location')]" + }, + "publisher": { + "value": "Microsoft.PowerShell" + }, + "type": { + "value": "DSC" + }, + "typeHandlerVersion": { + "value": "[coalesce(tryGet(parameters('extensionHostPoolRegistration'), 'typeHandlerVersion'), '2.77')]" + }, + "autoUpgradeMinorVersion": { + "value": "[coalesce(tryGet(parameters('extensionHostPoolRegistration'), 'autoUpgradeMinorVersion'), true())]" + }, + "enableAutomaticUpgrade": { + "value": "[coalesce(tryGet(parameters('extensionHostPoolRegistration'), 'enableAutomaticUpgrade'), false())]" + }, + "settings": { + "value": { + "modulesUrl": "[parameters('extensionHostPoolRegistration').modulesUrl]", + "configurationFunction": "[parameters('extensionHostPoolRegistration').configurationFunction]", + "properties": { + "hostPoolName": "[parameters('extensionHostPoolRegistration').hostPoolName]", + "registrationInfoToken": "[parameters('extensionHostPoolRegistration').registrationInfoToken]", + "aadJoin": true + }, + "supressFailures": "[coalesce(tryGet(parameters('extensionHostPoolRegistration'), 'supressFailures'), false())]" + } + }, + "tags": { + "value": "[coalesce(tryGet(parameters('extensionHostPoolRegistration'), 'tags'), parameters('tags'))]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "languageVersion": "2.0", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.39.26.7824", + "templateHash": "8391598897118491777" + }, + "name": "Virtual Machine Extensions", + "description": "This module deploys a Virtual Machine Extension." + }, + "parameters": { + "virtualMachineName": { + "type": "string", + "metadata": { + "description": "Conditional. The name of the parent virtual machine that extension is provisioned for. Required if the template is used in a standalone deployment." + } + }, + "name": { + "type": "string", + "metadata": { + "description": "Required. The name of the virtual machine extension." + } + }, + "location": { + "type": "string", + "defaultValue": "[resourceGroup().location]", + "metadata": { + "description": "Optional. The location the extension is deployed to." + } + }, + "publisher": { + "type": "string", + "metadata": { + "description": "Required. The name of the extension handler publisher." + } + }, + "type": { + "type": "string", + "metadata": { + "description": "Required. Specifies the type of the extension; an example is \"CustomScriptExtension\"." + } + }, + "typeHandlerVersion": { + "type": "string", + "metadata": { + "description": "Required. Specifies the version of the script handler." + } + }, + "autoUpgradeMinorVersion": { + "type": "bool", + "metadata": { + "description": "Required. Indicates whether the extension should use a newer minor version if one is available at deployment time. Once deployed, however, the extension will not upgrade minor versions unless redeployed, even with this property set to true." + } + }, + "forceUpdateTag": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. How the extension handler should be forced to update even if the extension configuration has not changed." + } + }, + "settings": { + "type": "object", + "nullable": true, + "metadata": { + "description": "Optional. Any object that contains the extension specific settings." + } + }, + "protectedSettings": { + "type": "secureObject", + "nullable": true, + "metadata": { + "description": "Optional. Any object that contains the extension specific protected settings." + } + }, + "supressFailures": { + "type": "bool", + "defaultValue": false, + "metadata": { + "description": "Optional. Indicates whether failures stemming from the extension will be suppressed (Operational failures such as not connecting to the VM will not be suppressed regardless of this value). The default is false." + } + }, + "enableAutomaticUpgrade": { + "type": "bool", + "metadata": { + "description": "Required. Indicates whether the extension should be automatically upgraded by the platform if there is a newer version of the extension available." + } + }, + "tags": { + "type": "object", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.Compute/virtualMachines/extensions@2024-11-01#properties/tags" + }, + "description": "Optional. Tags of the resource." + }, + "nullable": true + }, + "protectedSettingsFromKeyVault": { + "type": "object", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.Compute/virtualMachines/extensions@2024-11-01#properties/properties/properties/protectedSettingsFromKeyVault" + }, + "description": "Optional. The extensions protected settings that are passed by reference, and consumed from key vault." + }, + "nullable": true + }, + "provisionAfterExtensions": { + "type": "array", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.Compute/virtualMachines/extensions@2024-11-01#properties/properties/properties/provisionAfterExtensions" + }, + "description": "Optional. Collection of extension names after which this extension needs to be provisioned." + }, + "nullable": true + } + }, + "resources": { + "virtualMachine": { + "existing": true, + "type": "Microsoft.Compute/virtualMachines", + "apiVersion": "2024-11-01", + "name": "[parameters('virtualMachineName')]" + }, + "extension": { + "type": "Microsoft.Compute/virtualMachines/extensions", + "apiVersion": "2024-11-01", + "name": "[format('{0}/{1}', parameters('virtualMachineName'), parameters('name'))]", + "location": "[parameters('location')]", + "tags": "[parameters('tags')]", + "properties": { + "publisher": "[parameters('publisher')]", + "type": "[parameters('type')]", + "typeHandlerVersion": "[parameters('typeHandlerVersion')]", + "autoUpgradeMinorVersion": "[parameters('autoUpgradeMinorVersion')]", + "enableAutomaticUpgrade": "[parameters('enableAutomaticUpgrade')]", + "forceUpdateTag": "[parameters('forceUpdateTag')]", + "settings": "[parameters('settings')]", + "protectedSettings": "[parameters('protectedSettings')]", + "suppressFailures": "[parameters('supressFailures')]", + "protectedSettingsFromKeyVault": "[parameters('protectedSettingsFromKeyVault')]", + "provisionAfterExtensions": "[parameters('provisionAfterExtensions')]" + } } }, "outputs": { "name": { "type": "string", "metadata": { - "description": "The name of the deployed blob service." + "description": "The name of the extension." }, - "value": "[variables('name')]" + "value": "[parameters('name')]" }, "resourceId": { "type": "string", "metadata": { - "description": "The resource ID of the deployed blob service." + "description": "The resource ID of the extension." }, - "value": "[resourceId('Microsoft.Storage/storageAccounts/blobServices', parameters('storageAccountName'), variables('name'))]" + "value": "[resourceId('Microsoft.Compute/virtualMachines/extensions', parameters('virtualMachineName'), parameters('name'))]" }, "resourceGroupName": { "type": "string", "metadata": { - "description": "The name of the deployed blob service." + "description": "The name of the Resource Group the extension was created in." }, "value": "[resourceGroup().name]" + }, + "location": { + "type": "string", + "metadata": { + "description": "The location the resource was deployed into." + }, + "value": "[reference('extension', '2024-11-01', 'full').location]" } } } }, "dependsOn": [ - "storageAccount" + "vm", + "vm_nvidiaGpuDriverWindowsExtension" ] }, - "storageAccount_fileServices": { - "condition": "[not(empty(parameters('fileServices')))]", + "vm_azureGuestConfigurationExtension": { + "condition": "[parameters('extensionGuestConfigurationExtension').enabled]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "[format('{0}-VM-GuestConfiguration', uniqueString(deployment().name, parameters('location')))]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "virtualMachineName": { + "value": "[parameters('name')]" + }, + "name": "[if(coalesce(tryGet(parameters('extensionGuestConfigurationExtension'), 'name'), equals(parameters('osType'), 'Windows')), createObject('value', 'AzurePolicyforWindows'), createObject('value', 'AzurePolicyforLinux'))]", + "location": { + "value": "[parameters('location')]" + }, + "publisher": { + "value": "Microsoft.GuestConfiguration" + }, + "type": "[if(equals(parameters('osType'), 'Windows'), createObject('value', 'ConfigurationforWindows'), createObject('value', 'ConfigurationForLinux'))]", + "typeHandlerVersion": { + "value": "[coalesce(tryGet(parameters('extensionGuestConfigurationExtension'), 'typeHandlerVersion'), if(equals(parameters('osType'), 'Windows'), '1.0', '1.0'))]" + }, + "autoUpgradeMinorVersion": { + "value": "[coalesce(tryGet(parameters('extensionGuestConfigurationExtension'), 'autoUpgradeMinorVersion'), true())]" + }, + "enableAutomaticUpgrade": { + "value": "[coalesce(tryGet(parameters('extensionGuestConfigurationExtension'), 'enableAutomaticUpgrade'), true())]" + }, + "forceUpdateTag": { + "value": "[coalesce(tryGet(parameters('extensionGuestConfigurationExtension'), 'forceUpdateTag'), '1.0')]" + }, + "settings": { + "value": "[coalesce(tryGet(parameters('extensionGuestConfigurationExtension'), 'settings'), createObject())]" + }, + "supressFailures": { + "value": "[coalesce(tryGet(parameters('extensionGuestConfigurationExtension'), 'supressFailures'), false())]" + }, + "protectedSettings": { + "value": "[parameters('extensionGuestConfigurationExtensionProtectedSettings')]" + }, + "tags": { + "value": "[coalesce(tryGet(parameters('extensionGuestConfigurationExtension'), 'tags'), parameters('tags'))]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "languageVersion": "2.0", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.39.26.7824", + "templateHash": "8391598897118491777" + }, + "name": "Virtual Machine Extensions", + "description": "This module deploys a Virtual Machine Extension." + }, + "parameters": { + "virtualMachineName": { + "type": "string", + "metadata": { + "description": "Conditional. The name of the parent virtual machine that extension is provisioned for. Required if the template is used in a standalone deployment." + } + }, + "name": { + "type": "string", + "metadata": { + "description": "Required. The name of the virtual machine extension." + } + }, + "location": { + "type": "string", + "defaultValue": "[resourceGroup().location]", + "metadata": { + "description": "Optional. The location the extension is deployed to." + } + }, + "publisher": { + "type": "string", + "metadata": { + "description": "Required. The name of the extension handler publisher." + } + }, + "type": { + "type": "string", + "metadata": { + "description": "Required. Specifies the type of the extension; an example is \"CustomScriptExtension\"." + } + }, + "typeHandlerVersion": { + "type": "string", + "metadata": { + "description": "Required. Specifies the version of the script handler." + } + }, + "autoUpgradeMinorVersion": { + "type": "bool", + "metadata": { + "description": "Required. Indicates whether the extension should use a newer minor version if one is available at deployment time. Once deployed, however, the extension will not upgrade minor versions unless redeployed, even with this property set to true." + } + }, + "forceUpdateTag": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. How the extension handler should be forced to update even if the extension configuration has not changed." + } + }, + "settings": { + "type": "object", + "nullable": true, + "metadata": { + "description": "Optional. Any object that contains the extension specific settings." + } + }, + "protectedSettings": { + "type": "secureObject", + "nullable": true, + "metadata": { + "description": "Optional. Any object that contains the extension specific protected settings." + } + }, + "supressFailures": { + "type": "bool", + "defaultValue": false, + "metadata": { + "description": "Optional. Indicates whether failures stemming from the extension will be suppressed (Operational failures such as not connecting to the VM will not be suppressed regardless of this value). The default is false." + } + }, + "enableAutomaticUpgrade": { + "type": "bool", + "metadata": { + "description": "Required. Indicates whether the extension should be automatically upgraded by the platform if there is a newer version of the extension available." + } + }, + "tags": { + "type": "object", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.Compute/virtualMachines/extensions@2024-11-01#properties/tags" + }, + "description": "Optional. Tags of the resource." + }, + "nullable": true + }, + "protectedSettingsFromKeyVault": { + "type": "object", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.Compute/virtualMachines/extensions@2024-11-01#properties/properties/properties/protectedSettingsFromKeyVault" + }, + "description": "Optional. The extensions protected settings that are passed by reference, and consumed from key vault." + }, + "nullable": true + }, + "provisionAfterExtensions": { + "type": "array", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.Compute/virtualMachines/extensions@2024-11-01#properties/properties/properties/provisionAfterExtensions" + }, + "description": "Optional. Collection of extension names after which this extension needs to be provisioned." + }, + "nullable": true + } + }, + "resources": { + "virtualMachine": { + "existing": true, + "type": "Microsoft.Compute/virtualMachines", + "apiVersion": "2024-11-01", + "name": "[parameters('virtualMachineName')]" + }, + "extension": { + "type": "Microsoft.Compute/virtualMachines/extensions", + "apiVersion": "2024-11-01", + "name": "[format('{0}/{1}', parameters('virtualMachineName'), parameters('name'))]", + "location": "[parameters('location')]", + "tags": "[parameters('tags')]", + "properties": { + "publisher": "[parameters('publisher')]", + "type": "[parameters('type')]", + "typeHandlerVersion": "[parameters('typeHandlerVersion')]", + "autoUpgradeMinorVersion": "[parameters('autoUpgradeMinorVersion')]", + "enableAutomaticUpgrade": "[parameters('enableAutomaticUpgrade')]", + "forceUpdateTag": "[parameters('forceUpdateTag')]", + "settings": "[parameters('settings')]", + "protectedSettings": "[parameters('protectedSettings')]", + "suppressFailures": "[parameters('supressFailures')]", + "protectedSettingsFromKeyVault": "[parameters('protectedSettingsFromKeyVault')]", + "provisionAfterExtensions": "[parameters('provisionAfterExtensions')]" + } + } + }, + "outputs": { + "name": { + "type": "string", + "metadata": { + "description": "The name of the extension." + }, + "value": "[parameters('name')]" + }, + "resourceId": { + "type": "string", + "metadata": { + "description": "The resource ID of the extension." + }, + "value": "[resourceId('Microsoft.Compute/virtualMachines/extensions', parameters('virtualMachineName'), parameters('name'))]" + }, + "resourceGroupName": { + "type": "string", + "metadata": { + "description": "The name of the Resource Group the extension was created in." + }, + "value": "[resourceGroup().name]" + }, + "location": { + "type": "string", + "metadata": { + "description": "The location the resource was deployed into." + }, + "value": "[reference('extension', '2024-11-01', 'full').location]" + } + } + } + }, + "dependsOn": [ + "vm", + "vm_hostPoolRegistrationExtension" + ] + }, + "vm_backup": { + "condition": "[not(empty(parameters('backupVaultName')))]", "type": "Microsoft.Resources/deployments", "apiVersion": "2025-04-01", - "name": "[format('{0}-Storage-FileServices', uniqueString(deployment().name, parameters('location')))]", + "name": "[format('{0}-VM-Backup', uniqueString(deployment().name, parameters('location')))]", + "resourceGroup": "[parameters('backupVaultResourceGroup')]", "properties": { "expressionEvaluationOptions": { "scope": "inner" }, "mode": "Incremental", "parameters": { - "storageAccountName": { - "value": "[parameters('name')]" + "name": { + "value": "[format('vm;iaasvmcontainerv2;{0};{1}', resourceGroup().name, parameters('name'))]" }, - "diagnosticSettings": { - "value": "[tryGet(parameters('fileServices'), 'diagnosticSettings')]" + "location": { + "value": "[parameters('location')]" }, - "protocolSettings": { - "value": "[tryGet(parameters('fileServices'), 'protocolSettings')]" + "policyId": { + "value": "[resourceId(parameters('backupVaultResourceGroup'), 'Microsoft.RecoveryServices/vaults/backupPolicies', parameters('backupVaultName'), parameters('backupPolicyName'))]" }, - "shareDeleteRetentionPolicy": { - "value": "[tryGet(parameters('fileServices'), 'shareDeleteRetentionPolicy')]" + "protectedItemType": { + "value": "Microsoft.Compute/virtualMachines" }, - "shares": { - "value": "[tryGet(parameters('fileServices'), 'shares')]" + "protectionContainerName": { + "value": "[format('iaasvmcontainer;iaasvmcontainerv2;{0};{1}', resourceGroup().name, parameters('name'))]" }, - "corsRules": { - "value": "[tryGet(parameters('fileServices'), 'corsRules')]" + "recoveryVaultName": { + "value": "[parameters('backupVaultName')]" + }, + "sourceResourceId": { + "value": "[resourceId('Microsoft.Compute/virtualMachines', parameters('name'))]" } }, "template": { "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", - "languageVersion": "2.0", "contentVersion": "1.0.0.0", "metadata": { "_generator": { "name": "bicep", "version": "0.39.26.7824", - "templateHash": "8847095544204825048" - }, - "name": "Storage Account File Share Services", - "description": "This module deploys a Storage Account File Share Service." - }, - "definitions": { - "corsRuleType": { - "type": "object", - "properties": { - "allowedHeaders": { - "type": "array", - "items": { - "type": "string" - }, - "metadata": { - "description": "Required. A list of headers allowed to be part of the cross-origin request." - } - }, - "allowedMethods": { - "type": "array", - "allowedValues": [ - "CONNECT", - "DELETE", - "GET", - "HEAD", - "MERGE", - "OPTIONS", - "PATCH", - "POST", - "PUT", - "TRACE" - ], - "metadata": { - "description": "Required. A list of HTTP methods that are allowed to be executed by the origin." - } - }, - "allowedOrigins": { - "type": "array", - "items": { - "type": "string" - }, - "metadata": { - "description": "Required. A list of origin domains that will be allowed via CORS, or \"*\" to allow all domains." - } - }, - "exposedHeaders": { - "type": "array", - "items": { - "type": "string" - }, - "metadata": { - "description": "Required. A list of response headers to expose to CORS clients." - } - }, - "maxAgeInSeconds": { - "type": "int", - "metadata": { - "description": "Required. The number of seconds that the client/browser should cache a preflight response." - } - } - }, - "metadata": { - "__bicep_export!": true, - "description": "The type for a cors rule." - } - }, - "fileShareType": { - "type": "object", - "properties": { - "name": { - "type": "string", - "metadata": { - "description": "Required. The name of the file share." - } - }, - "accessTier": { - "type": "string", - "allowedValues": [ - "Cool", - "Hot", - "Premium", - "TransactionOptimized" - ], - "nullable": true, - "metadata": { - "description": "Optional. Access tier for specific share. Required if the Storage Account kind is set to FileStorage (should be set to \"Premium\"). GpV2 account can choose between TransactionOptimized (default), Hot, and Cool." - } - }, - "enabledProtocols": { - "type": "string", - "allowedValues": [ - "NFS", - "SMB" - ], - "nullable": true, - "metadata": { - "description": "Optional. The authentication protocol that is used for the file share. Can only be specified when creating a share." - } - }, - "rootSquash": { - "type": "string", - "allowedValues": [ - "AllSquash", - "NoRootSquash", - "RootSquash" - ], - "nullable": true, - "metadata": { - "description": "Optional. Permissions for NFS file shares are enforced by the client OS rather than the Azure Files service. Toggling the root squash behavior reduces the rights of the root user for NFS shares." - } - }, - "shareQuota": { - "type": "int", - "nullable": true, - "metadata": { - "description": "Optional. The maximum size of the share, in gigabytes. Must be greater than 0, and less than or equal to 5120 (5TB). For Large File Shares, the maximum size is 102400 (100TB)." - } - }, - "roleAssignments": { - "type": "array", - "items": { - "$ref": "#/definitions/roleAssignmentType" - }, - "nullable": true, - "metadata": { - "description": "Optional. Array of role assignments to create." - } - } - }, - "metadata": { - "__bicep_export!": true, - "description": "The type for a file share." - } - }, - "diagnosticSettingFullType": { - "type": "object", - "properties": { - "name": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The name of the diagnostic setting." - } - }, - "logCategoriesAndGroups": { - "type": "array", - "items": { - "type": "object", - "properties": { - "category": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. Name of a Diagnostic Log category for a resource type this setting is applied to. Set the specific logs to collect here." - } - }, - "categoryGroup": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. Name of a Diagnostic Log category group for a resource type this setting is applied to. Set to `allLogs` to collect all logs." - } - }, - "enabled": { - "type": "bool", - "nullable": true, - "metadata": { - "description": "Optional. Enable or disable the category explicitly. Default is `true`." - } - } - } - }, - "nullable": true, - "metadata": { - "description": "Optional. The name of logs that will be streamed. \"allLogs\" includes all possible logs for the resource. Set to `[]` to disable log collection." - } - }, - "metricCategories": { - "type": "array", - "items": { - "type": "object", - "properties": { - "category": { - "type": "string", - "metadata": { - "description": "Required. Name of a Diagnostic Metric category for a resource type this setting is applied to. Set to `AllMetrics` to collect all metrics." - } - }, - "enabled": { - "type": "bool", - "nullable": true, - "metadata": { - "description": "Optional. Enable or disable the category explicitly. Default is `true`." - } - } - } - }, - "nullable": true, - "metadata": { - "description": "Optional. The name of metrics that will be streamed. \"allMetrics\" includes all possible metrics for the resource. Set to `[]` to disable metric collection." - } - }, - "logAnalyticsDestinationType": { - "type": "string", - "allowedValues": [ - "AzureDiagnostics", - "Dedicated" - ], - "nullable": true, - "metadata": { - "description": "Optional. A string indicating whether the export to Log Analytics should use the default destination type, i.e. AzureDiagnostics, or use a destination type." - } - }, - "workspaceResourceId": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. Resource ID of the diagnostic log analytics workspace. For security reasons, it is recommended to set diagnostic settings to send data to either storage account, log analytics workspace or event hub." - } - }, - "storageAccountResourceId": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. Resource ID of the diagnostic storage account. For security reasons, it is recommended to set diagnostic settings to send data to either storage account, log analytics workspace or event hub." - } - }, - "eventHubAuthorizationRuleResourceId": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. Resource ID of the diagnostic event hub authorization rule for the Event Hubs namespace in which the event hub should be created or streamed to." - } - }, - "eventHubName": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. Name of the diagnostic event hub within the namespace to which logs are streamed. Without this, an event hub is created for each log category. For security reasons, it is recommended to set diagnostic settings to send data to either storage account, log analytics workspace or event hub." - } - }, - "marketplacePartnerResourceId": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The full ARM resource ID of the Marketplace resource to which you would like to send Diagnostic Logs." - } - } - }, - "metadata": { - "description": "An AVM-aligned type for a diagnostic setting. To be used if both logs & metrics are supported by the resource provider.", - "__bicep_imported_from!": { - "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.0" - } - } + "templateHash": "3866626825190424174" }, - "roleAssignmentType": { - "type": "object", - "properties": { - "name": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The name (as GUID) of the role assignment. If not provided, a GUID will be generated." - } - }, - "roleDefinitionIdOrName": { - "type": "string", - "metadata": { - "description": "Required. The role to assign. You can provide either the display name of the role definition, the role definition GUID, or its fully qualified ID in the following format: '/providers/Microsoft.Authorization/roleDefinitions/c2f4ef07-c644-48eb-af81-4b1b4947fb11'." - } - }, - "principalId": { - "type": "string", - "metadata": { - "description": "Required. The principal ID of the principal (user/group/identity) to assign the role to." - } - }, - "principalType": { - "type": "string", - "allowedValues": [ - "Device", - "ForeignGroup", - "Group", - "ServicePrincipal", - "User" - ], - "nullable": true, - "metadata": { - "description": "Optional. The principal type of the assigned principal ID." - } - }, - "description": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The description of the role assignment." - } - }, - "condition": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The conditions on the role assignment. This limits the resources it can be assigned to. e.g.: @Resource[Microsoft.Storage/storageAccounts/blobServices/containers:ContainerName] StringEqualsIgnoreCase \"foo_storage_container\"." - } - }, - "conditionVersion": { - "type": "string", - "allowedValues": [ - "2.0" - ], - "nullable": true, - "metadata": { - "description": "Optional. Version of the condition." - } - }, - "delegatedManagedIdentityResourceId": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The Resource Id of the delegated managed identity resource." - } - } - }, - "metadata": { - "description": "An AVM-aligned type for a role assignment.", - "__bicep_imported_from!": { - "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.1" - } - } - } + "name": "Recovery Service Vaults Protection Container Protected Item", + "description": "This module deploys a Recovery Services Vault Protection Container Protected Item." }, "parameters": { - "storageAccountName": { + "name": { "type": "string", - "maxLength": 24, "metadata": { - "description": "Conditional. The name of the parent Storage Account. Required if the template is used in a standalone deployment." + "description": "Required. Name of the resource." } }, - "name": { + "protectionContainerName": { "type": "string", - "defaultValue": "default", "metadata": { - "description": "Optional. The name of the file service." + "description": "Conditional. Name of the Azure Recovery Service Vault Protection Container. Required if the template is used in a standalone deployment." } }, - "protocolSettings": { - "type": "object", + "recoveryVaultName": { + "type": "string", "metadata": { - "__bicep_resource_derived_type!": { - "source": "Microsoft.Storage/storageAccounts/fileServices@2024-01-01#properties/properties/properties/protocolSettings" - }, - "description": "Optional. Protocol settings for file service." - }, - "defaultValue": {} + "description": "Conditional. The name of the parent Azure Recovery Service Vault. Required if the template is used in a standalone deployment." + } }, - "shareDeleteRetentionPolicy": { - "type": "object", + "location": { + "type": "string", + "defaultValue": "[resourceGroup().location]", "metadata": { - "__bicep_resource_derived_type!": { - "source": "Microsoft.Storage/storageAccounts/fileServices@2024-01-01#properties/properties/properties/shareDeleteRetentionPolicy" - }, - "description": "Optional. The service properties for soft delete." - }, - "defaultValue": { - "enabled": true, - "days": 7 + "description": "Optional. Location for all resources." } }, - "corsRules": { - "type": "array", - "items": { - "$ref": "#/definitions/corsRuleType" - }, - "nullable": true, + "protectedItemType": { + "type": "string", + "allowedValues": [ + "AzureFileShareProtectedItem", + "AzureVmWorkloadSAPAseDatabase", + "AzureVmWorkloadSAPHanaDatabase", + "AzureVmWorkloadSQLDatabase", + "DPMProtectedItem", + "GenericProtectedItem", + "MabFileFolderProtectedItem", + "Microsoft.ClassicCompute/virtualMachines", + "Microsoft.Compute/virtualMachines", + "Microsoft.Sql/servers/databases" + ], "metadata": { - "description": "Optional. The List of CORS rules. You can include up to five CorsRule elements in the request." + "description": "Required. The backup item type." } }, - "diagnosticSettings": { - "type": "array", - "items": { - "$ref": "#/definitions/diagnosticSettingFullType" - }, - "nullable": true, + "policyId": { + "type": "string", "metadata": { - "description": "Optional. The diagnostic settings of the service." + "description": "Required. ID of the backup policy with which this item is backed up." } }, - "shares": { - "type": "array", - "items": { - "$ref": "#/definitions/fileShareType" - }, - "nullable": true, + "sourceResourceId": { + "type": "string", "metadata": { - "description": "Optional. File shares to create." + "description": "Required. Resource ID of the resource to back up." } } }, - "variables": { - "enableReferencedModulesTelemetry": false - }, - "resources": { - "storageAccount": { - "existing": true, - "type": "Microsoft.Storage/storageAccounts", - "apiVersion": "2024-01-01", - "name": "[parameters('storageAccountName')]" - }, - "fileServices": { - "type": "Microsoft.Storage/storageAccounts/fileServices", - "apiVersion": "2024-01-01", - "name": "[format('{0}/{1}', parameters('storageAccountName'), parameters('name'))]", + "resources": [ + { + "type": "Microsoft.RecoveryServices/vaults/backupFabrics/protectionContainers/protectedItems", + "apiVersion": "2025-02-01", + "name": "[format('{0}/Azure/{1}/{2}', parameters('recoveryVaultName'), parameters('protectionContainerName'), parameters('name'))]", + "location": "[parameters('location')]", "properties": { - "cors": "[if(not(equals(parameters('corsRules'), null())), createObject('corsRules', parameters('corsRules')), null())]", - "protocolSettings": "[parameters('protocolSettings')]", - "shareDeleteRetentionPolicy": "[parameters('shareDeleteRetentionPolicy')]" + "protectedItemType": "[parameters('protectedItemType')]", + "policyId": "[parameters('policyId')]", + "sourceResourceId": "[parameters('sourceResourceId')]" } - }, - "fileServices_diagnosticSettings": { - "copy": { - "name": "fileServices_diagnosticSettings", - "count": "[length(coalesce(parameters('diagnosticSettings'), createArray()))]" - }, - "type": "Microsoft.Insights/diagnosticSettings", - "apiVersion": "2021-05-01-preview", - "scope": "[format('Microsoft.Storage/storageAccounts/{0}/fileServices/{1}', parameters('storageAccountName'), parameters('name'))]", - "name": "[coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'name'), format('{0}-diagnosticSettings', parameters('name')))]", - "properties": { - "copy": [ - { - "name": "metrics", - "count": "[length(coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'metricCategories'), createArray(createObject('category', 'AllMetrics'))))]", - "input": { - "category": "[coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'metricCategories'), createArray(createObject('category', 'AllMetrics')))[copyIndex('metrics')].category]", - "enabled": "[coalesce(tryGet(coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'metricCategories'), createArray(createObject('category', 'AllMetrics')))[copyIndex('metrics')], 'enabled'), true())]", - "timeGrain": null - } - }, - { - "name": "logs", - "count": "[length(coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'logCategoriesAndGroups'), createArray(createObject('categoryGroup', 'allLogs'))))]", - "input": { - "categoryGroup": "[tryGet(coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'logCategoriesAndGroups'), createArray(createObject('categoryGroup', 'allLogs')))[copyIndex('logs')], 'categoryGroup')]", - "category": "[tryGet(coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'logCategoriesAndGroups'), createArray(createObject('categoryGroup', 'allLogs')))[copyIndex('logs')], 'category')]", - "enabled": "[coalesce(tryGet(coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'logCategoriesAndGroups'), createArray(createObject('categoryGroup', 'allLogs')))[copyIndex('logs')], 'enabled'), true())]" - } - } - ], - "storageAccountId": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'storageAccountResourceId')]", - "workspaceId": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'workspaceResourceId')]", - "eventHubAuthorizationRuleId": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'eventHubAuthorizationRuleResourceId')]", - "eventHubName": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'eventHubName')]", - "marketplacePartnerId": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'marketplacePartnerResourceId')]", - "logAnalyticsDestinationType": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'logAnalyticsDestinationType')]" + } + ], + "outputs": { + "resourceGroupName": { + "type": "string", + "metadata": { + "description": "The name of the Resource Group the protected item was created in." }, - "dependsOn": [ - "fileServices" - ] + "value": "[resourceGroup().name]" }, - "fileServices_shares": { - "copy": { - "name": "fileServices_shares", - "count": "[length(coalesce(parameters('shares'), createArray()))]" + "resourceId": { + "type": "string", + "metadata": { + "description": "The resource ID of the protected item." }, - "type": "Microsoft.Resources/deployments", - "apiVersion": "2025-04-01", - "name": "[format('{0}-FileShare-{1}', deployment().name, copyIndex())]", - "properties": { - "expressionEvaluationOptions": { - "scope": "inner" - }, - "mode": "Incremental", - "parameters": { - "storageAccountName": { - "value": "[parameters('storageAccountName')]" - }, - "fileServicesName": { - "value": "[parameters('name')]" - }, - "name": { - "value": "[coalesce(parameters('shares'), createArray())[copyIndex()].name]" - }, - "accessTier": { - "value": "[coalesce(tryGet(coalesce(parameters('shares'), createArray())[copyIndex()], 'accessTier'), if(equals(reference('storageAccount', '2024-01-01', 'full').kind, 'FileStorage'), 'Premium', 'TransactionOptimized'))]" - }, - "enabledProtocols": { - "value": "[tryGet(coalesce(parameters('shares'), createArray())[copyIndex()], 'enabledProtocols')]" - }, - "rootSquash": { - "value": "[tryGet(coalesce(parameters('shares'), createArray())[copyIndex()], 'rootSquash')]" - }, - "shareQuota": { - "value": "[tryGet(coalesce(parameters('shares'), createArray())[copyIndex()], 'shareQuota')]" - }, - "roleAssignments": { - "value": "[tryGet(coalesce(parameters('shares'), createArray())[copyIndex()], 'roleAssignments')]" - }, - "enableTelemetry": { - "value": "[variables('enableReferencedModulesTelemetry')]" - } - }, - "template": { - "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", - "languageVersion": "2.0", - "contentVersion": "1.0.0.0", - "metadata": { - "_generator": { - "name": "bicep", - "version": "0.39.26.7824", - "templateHash": "1953115828549574279" - }, - "name": "Storage Account File Shares", - "description": "This module deploys a Storage Account File Share." - }, - "definitions": { - "roleAssignmentType": { - "type": "object", - "properties": { - "name": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The name (as GUID) of the role assignment. If not provided, a GUID will be generated." - } - }, - "roleDefinitionIdOrName": { - "type": "string", - "metadata": { - "description": "Required. The role to assign. You can provide either the display name of the role definition, the role definition GUID, or its fully qualified ID in the following format: '/providers/Microsoft.Authorization/roleDefinitions/c2f4ef07-c644-48eb-af81-4b1b4947fb11'." - } - }, - "principalId": { - "type": "string", - "metadata": { - "description": "Required. The principal ID of the principal (user/group/identity) to assign the role to." - } - }, - "principalType": { - "type": "string", - "allowedValues": [ - "Device", - "ForeignGroup", - "Group", - "ServicePrincipal", - "User" - ], - "nullable": true, - "metadata": { - "description": "Optional. The principal type of the assigned principal ID." - } - }, - "description": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The description of the role assignment." - } - }, - "condition": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The conditions on the role assignment. This limits the resources it can be assigned to. e.g.: @Resource[Microsoft.Storage/storageAccounts/blobServices/containers:ContainerName] StringEqualsIgnoreCase \"foo_storage_container\"." - } - }, - "conditionVersion": { - "type": "string", - "allowedValues": [ - "2.0" - ], - "nullable": true, - "metadata": { - "description": "Optional. Version of the condition." - } - }, - "delegatedManagedIdentityResourceId": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The Resource Id of the delegated managed identity resource." - } - } - }, - "metadata": { - "description": "An AVM-aligned type for a role assignment.", - "__bicep_imported_from!": { - "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.1" - } - } - } - }, - "parameters": { - "storageAccountName": { - "type": "string", - "maxLength": 24, - "metadata": { - "description": "Conditional. The name of the parent Storage Account. Required if the template is used in a standalone deployment." - } - }, - "fileServicesName": { - "type": "string", - "defaultValue": "default", - "metadata": { - "description": "Conditional. The name of the parent file service. Required if the template is used in a standalone deployment." - } - }, - "name": { - "type": "string", - "metadata": { - "description": "Required. The name of the file share to create." - } - }, - "accessTier": { - "type": "string", - "defaultValue": "TransactionOptimized", - "allowedValues": [ - "Premium", - "Hot", - "Cool", - "TransactionOptimized" - ], - "metadata": { - "description": "Conditional. Access tier for specific share. Required if the Storage Account kind is set to FileStorage (should be set to \"Premium\"). GpV2 account can choose between TransactionOptimized (default), Hot, and Cool." - } - }, - "shareQuota": { - "type": "int", - "defaultValue": 5120, - "metadata": { - "description": "Optional. The maximum size of the share, in gigabytes. Must be greater than 0, and less than or equal to 5120 (5TB). For Large File Shares, the maximum size is 102400 (100TB)." - } - }, - "enabledProtocols": { - "type": "string", - "defaultValue": "SMB", - "allowedValues": [ - "NFS", - "SMB" - ], - "metadata": { - "description": "Optional. The authentication protocol that is used for the file share. Can only be specified when creating a share." - } - }, - "rootSquash": { - "type": "string", - "defaultValue": "NoRootSquash", - "allowedValues": [ - "AllSquash", - "NoRootSquash", - "RootSquash" - ], - "metadata": { - "description": "Optional. Permissions for NFS file shares are enforced by the client OS rather than the Azure Files service. Toggling the root squash behavior reduces the rights of the root user for NFS shares." - } - }, - "enableTelemetry": { - "type": "bool", - "defaultValue": true, - "metadata": { - "description": "Optional. Enable/Disable usage telemetry for module." - } - }, - "roleAssignments": { - "type": "array", - "items": { - "$ref": "#/definitions/roleAssignmentType" - }, - "nullable": true, - "metadata": { - "description": "Optional. Array of role assignments to create." - } - } - }, - "variables": { - "copy": [ - { - "name": "formattedRoleAssignments", - "count": "[length(coalesce(parameters('roleAssignments'), createArray()))]", - "input": "[union(coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')], createObject('roleDefinitionId', coalesce(tryGet(variables('builtInRoleNames'), coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName), if(contains(coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName, '/providers/Microsoft.Authorization/roleDefinitions/'), coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName, subscriptionResourceId('Microsoft.Authorization/roleDefinitions', coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName)))))]" - } - ], - "builtInRoleNames": { - "Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'b24988ac-6180-42a0-ab88-20f7382dd24c')]", - "Owner": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '8e3af657-a8ff-443c-a75c-2fe8c4bcb635')]", - "Reader": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'acdd72a7-3385-48ef-bd42-f606fba81ae7')]", - "Reader and Data Access": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'c12c1c16-33a1-487b-954d-41c89c60f349')]", - "Role Based Access Control Administrator": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'f58310d9-a9f6-439a-9e8d-f62e7b41a168')]", - "Storage Account Backup Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'e5e2a7ff-d759-4cd2-bb51-3152d37e2eb1')]", - "Storage Account Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '17d1049b-9a84-46fb-8f53-869881c3d3ab')]", - "Storage Account Key Operator Service Role": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '81a9662b-bebf-436f-a333-f67b29880f12')]", - "Storage File Data SMB Share Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '0c867c2a-1d8c-454a-a3db-ab2ea1bdc8bb')]", - "Storage File Data SMB Share Elevated Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'a7264617-510b-434b-a828-9731dc254ea7')]", - "Storage File Data SMB Share Reader": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'aba4ae5f-2193-4029-9191-0cb91df5e314')]", - "User Access Administrator": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '18d7d88d-d35e-4fb5-a5c3-7773c20a72d9')]" - } - }, - "resources": { - "storageAccount::fileService": { - "existing": true, - "type": "Microsoft.Storage/storageAccounts/fileServices", - "apiVersion": "2024-01-01", - "name": "[format('{0}/{1}', parameters('storageAccountName'), parameters('fileServicesName'))]" - }, - "avmTelemetry": { - "condition": "[parameters('enableTelemetry')]", - "type": "Microsoft.Resources/deployments", - "apiVersion": "2024-03-01", - "name": "[format('46d3xbcp.res.storage-fileshare.{0}.{1}', replace('-..--..-', '.', '-'), substring(uniqueString(deployment().name), 0, 4))]", - "properties": { - "mode": "Incremental", - "template": { - "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", - "contentVersion": "1.0.0.0", - "resources": [], - "outputs": { - "telemetry": { - "type": "String", - "value": "For more information, see https://aka.ms/avm/TelemetryInfo" - } - } - } - } - }, - "storageAccount": { - "existing": true, - "type": "Microsoft.Storage/storageAccounts", - "apiVersion": "2024-01-01", - "name": "[parameters('storageAccountName')]" - }, - "fileShare": { - "type": "Microsoft.Storage/storageAccounts/fileServices/shares", - "apiVersion": "2024-01-01", - "name": "[format('{0}/{1}/{2}', parameters('storageAccountName'), parameters('fileServicesName'), parameters('name'))]", - "properties": { - "accessTier": "[parameters('accessTier')]", - "shareQuota": "[parameters('shareQuota')]", - "rootSquash": "[if(equals(parameters('enabledProtocols'), 'NFS'), parameters('rootSquash'), null())]", - "enabledProtocols": "[parameters('enabledProtocols')]" - } - }, - "fileShare_roleAssignments": { - "copy": { - "name": "fileShare_roleAssignments", - "count": "[length(coalesce(variables('formattedRoleAssignments'), createArray()))]" - }, - "type": "Microsoft.Resources/deployments", - "apiVersion": "2025-04-01", - "name": "[format('{0}-Share-Rbac-{1}', uniqueString(deployment().name), copyIndex())]", - "properties": { - "expressionEvaluationOptions": { - "scope": "inner" - }, - "mode": "Incremental", - "parameters": { - "scope": { - "value": "[replace(resourceId('Microsoft.Storage/storageAccounts/fileServices/shares', parameters('storageAccountName'), parameters('fileServicesName'), parameters('name')), '/shares/', '/fileshares/')]" - }, - "name": { - "value": "[coalesce(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'name'), guid(resourceId('Microsoft.Storage/storageAccounts/fileServices/shares', parameters('storageAccountName'), parameters('fileServicesName'), parameters('name')), coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].principalId, coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].roleDefinitionId))]" - }, - "roleDefinitionId": { - "value": "[coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].roleDefinitionId]" - }, - "principalId": { - "value": "[coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].principalId]" - }, - "principalType": { - "value": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'principalType')]" - }, - "condition": { - "value": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'condition')]" - }, - "conditionVersion": "[if(not(empty(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'condition'))), createObject('value', coalesce(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'conditionVersion'), '2.0')), createObject('value', null()))]", - "delegatedManagedIdentityResourceId": { - "value": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'delegatedManagedIdentityResourceId')]" - }, - "description": { - "value": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'description')]" - } - }, - "template": { - "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", - "contentVersion": "1.0.0.0", - "parameters": { - "scope": { - "type": "string", - "metadata": { - "description": "Required. The scope to deploy the role assignment to." - } - }, - "name": { - "type": "string", - "metadata": { - "description": "Required. The name of the role assignment." - } - }, - "roleDefinitionId": { - "type": "string", - "metadata": { - "description": "Required. The role definition Id to assign." - } - }, - "principalId": { - "type": "string", - "metadata": { - "description": "Required. The principal ID of the principal (user/group/identity) to assign the role to." - } - }, - "principalType": { - "type": "string", - "allowedValues": [ - "Device", - "ForeignGroup", - "Group", - "ServicePrincipal", - "User", - "" - ], - "defaultValue": "", - "metadata": { - "description": "Optional. The principal type of the assigned principal ID." - } - }, - "description": { - "type": "string", - "defaultValue": "", - "metadata": { - "description": "Optional. The description of the role assignment." - } - }, - "condition": { - "type": "string", - "defaultValue": "", - "metadata": { - "description": "Optional. The conditions on the role assignment. This limits the resources it can be assigned to. e.g.: @Resource[Microsoft.Storage/storageAccounts/blobServices/containers:ContainerName] StringEqualsIgnoreCase \"foo_storage_container\"" - } - }, - "conditionVersion": { - "type": "string", - "allowedValues": [ - "2.0" - ], - "defaultValue": "2.0", - "metadata": { - "description": "Optional. Version of the condition." - } - }, - "delegatedManagedIdentityResourceId": { - "type": "string", - "defaultValue": "", - "metadata": { - "description": "Optional. The Resource Id of the delegated managed identity resource." - } - } - }, - "resources": [ - { - "type": "Microsoft.Authorization/roleAssignments", - "apiVersion": "2022-04-01", - "scope": "[parameters('scope')]", - "name": "[parameters('name')]", - "properties": { - "roleDefinitionId": "[parameters('roleDefinitionId')]", - "principalId": "[parameters('principalId')]", - "description": "[parameters('description')]", - "principalType": "[if(not(empty(parameters('principalType'))), parameters('principalType'), null())]", - "condition": "[if(not(empty(parameters('condition'))), parameters('condition'), null())]", - "conditionVersion": "[if(and(not(empty(parameters('conditionVersion'))), not(empty(parameters('condition')))), parameters('conditionVersion'), null())]", - "delegatedManagedIdentityResourceId": "[if(not(empty(parameters('delegatedManagedIdentityResourceId'))), parameters('delegatedManagedIdentityResourceId'), null())]" - } - } - ] - } - }, - "dependsOn": [ - "fileShare" - ] - } - }, - "outputs": { - "name": { - "type": "string", - "metadata": { - "description": "The name of the deployed file share." - }, - "value": "[parameters('name')]" - }, - "resourceId": { - "type": "string", - "metadata": { - "description": "The resource ID of the deployed file share." - }, - "value": "[resourceId('Microsoft.Storage/storageAccounts/fileServices/shares', parameters('storageAccountName'), parameters('fileServicesName'), parameters('name'))]" - }, - "resourceGroupName": { - "type": "string", - "metadata": { - "description": "The resource group of the deployed file share." - }, - "value": "[resourceGroup().name]" - } - } - } + "value": "[resourceId('Microsoft.RecoveryServices/vaults/backupFabrics/protectionContainers/protectedItems', split(format('{0}/Azure/{1}/{2}', parameters('recoveryVaultName'), parameters('protectionContainerName'), parameters('name')), '/')[0], split(format('{0}/Azure/{1}/{2}', parameters('recoveryVaultName'), parameters('protectionContainerName'), parameters('name')), '/')[1], split(format('{0}/Azure/{1}/{2}', parameters('recoveryVaultName'), parameters('protectionContainerName'), parameters('name')), '/')[2], split(format('{0}/Azure/{1}/{2}', parameters('recoveryVaultName'), parameters('protectionContainerName'), parameters('name')), '/')[3])]" + }, + "name": { + "type": "string", + "metadata": { + "description": "The Name of the protected item." }, - "dependsOn": [ - "fileServices", - "storageAccount" - ] + "value": "[format('{0}/Azure/{1}/{2}', parameters('recoveryVaultName'), parameters('protectionContainerName'), parameters('name'))]" } + } + } + }, + "dependsOn": [ + "vm", + "vm_azureGuestConfigurationExtension" + ] + } + }, + "outputs": { + "name": { + "type": "string", + "metadata": { + "description": "The name of the VM." + }, + "value": "[parameters('name')]" + }, + "resourceId": { + "type": "string", + "metadata": { + "description": "The resource ID of the VM." + }, + "value": "[resourceId('Microsoft.Compute/virtualMachines', parameters('name'))]" + }, + "resourceGroupName": { + "type": "string", + "metadata": { + "description": "The name of the resource group the VM was created in." + }, + "value": "[resourceGroup().name]" + }, + "systemAssignedMIPrincipalId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "The principal ID of the system assigned identity." + }, + "value": "[tryGet(tryGet(reference('vm', '2024-07-01', 'full'), 'identity'), 'principalId')]" + }, + "location": { + "type": "string", + "metadata": { + "description": "The location the resource was deployed into." + }, + "value": "[reference('vm', '2024-07-01', 'full').location]" + }, + "nicConfigurations": { + "type": "array", + "items": { + "$ref": "#/definitions/nicConfigurationOutputType" + }, + "metadata": { + "description": "The list of NIC configurations of the virtual machine." + }, + "copy": { + "count": "[length(parameters('nicConfigurations'))]", + "input": { + "name": "[reference(format('vm_nic[{0}]', copyIndex())).outputs.name.value]", + "ipConfigurations": "[reference(format('vm_nic[{0}]', copyIndex())).outputs.ipConfigurations.value]" + } + } + } + } + } + }, + "dependsOn": [ + "logAnalyticsWorkspace", + "virtualNetwork" + ] + }, + "avmPrivateDnsZones": { + "copy": { + "name": "avmPrivateDnsZones", + "count": "[length(variables('privateDnsZones'))]", + "mode": "serial", + "batchSize": 5 + }, + "condition": "[parameters('enablePrivateNetworking')]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "[take(format('avm.res.network.private-dns-zone.{0}', replace(variables('privateDnsZones')[copyIndex()], '.', '-')), 64)]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "name": { + "value": "[variables('privateDnsZones')[copyIndex()]]" + }, + "tags": { + "value": "[parameters('tags')]" + }, + "enableTelemetry": { + "value": "[parameters('enableTelemetry')]" + }, + "virtualNetworkLinks": { + "value": [ + { + "virtualNetworkResourceId": "[if(parameters('enablePrivateNetworking'), reference('virtualNetwork').outputs.resourceId.value, '')]", + "registrationEnabled": false + } + ] + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "languageVersion": "2.0", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.37.4.10188", + "templateHash": "17921343070314002065" + }, + "name": "Private DNS Zones", + "description": "This module deploys a Private DNS zone." + }, + "definitions": { + "aType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "metadata": { + "description": "Required. The name of the record." + } + }, + "metadata": { + "type": "object", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.Network/privateDnsZones/A@2024-06-01#properties/properties/properties/metadata" + }, + "description": "Optional. The metadata of the record." }, - "outputs": { - "name": { - "type": "string", - "metadata": { - "description": "The name of the deployed file share service." - }, - "value": "[parameters('name')]" + "nullable": true + }, + "ttl": { + "type": "int", + "nullable": true, + "metadata": { + "description": "Optional. The TTL of the record." + } + }, + "roleAssignments": { + "type": "array", + "items": { + "$ref": "#/definitions/roleAssignmentType" + }, + "nullable": true, + "metadata": { + "description": "Optional. Array of role assignments to create." + } + }, + "aRecords": { + "type": "array", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.Network/privateDnsZones/A@2024-06-01#properties/properties/properties/aRecords" }, - "resourceId": { - "type": "string", - "metadata": { - "description": "The resource ID of the deployed file share service." - }, - "value": "[resourceId('Microsoft.Storage/storageAccounts/fileServices', parameters('storageAccountName'), parameters('name'))]" + "description": "Optional. The list of A records in the record set." + }, + "nullable": true + } + }, + "metadata": { + "__bicep_export!": true, + "description": "The type for the A record." + } + }, + "aaaaType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "metadata": { + "description": "Required. The name of the record." + } + }, + "metadata": { + "type": "object", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.Network/privateDnsZones/AAAA@2024-06-01#properties/properties/properties/metadata" + }, + "description": "Optional. The metadata of the record." + }, + "nullable": true + }, + "ttl": { + "type": "int", + "nullable": true, + "metadata": { + "description": "Optional. The TTL of the record." + } + }, + "roleAssignments": { + "type": "array", + "items": { + "$ref": "#/definitions/roleAssignmentType" + }, + "nullable": true, + "metadata": { + "description": "Optional. Array of role assignments to create." + } + }, + "aaaaRecords": { + "type": "array", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.Network/privateDnsZones/AAAA@2024-06-01#properties/properties/properties/aaaaRecords" + }, + "description": "Optional. The list of AAAA records in the record set." + }, + "nullable": true + } + }, + "metadata": { + "__bicep_export!": true, + "description": "The type for the AAAA record." + } + }, + "cnameType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "metadata": { + "description": "Required. The name of the record." + } + }, + "metadata": { + "type": "object", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.Network/privateDnsZones/CNAME@2024-06-01#properties/properties/properties/metadata" + }, + "description": "Optional. The metadata of the record." + }, + "nullable": true + }, + "ttl": { + "type": "int", + "nullable": true, + "metadata": { + "description": "Optional. The TTL of the record." + } + }, + "roleAssignments": { + "type": "array", + "items": { + "$ref": "#/definitions/roleAssignmentType" + }, + "nullable": true, + "metadata": { + "description": "Optional. Array of role assignments to create." + } + }, + "cnameRecord": { + "type": "object", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.Network/privateDnsZones/CNAME@2024-06-01#properties/properties/properties/cnameRecord" + }, + "description": "Optional. The CNAME record in the record set." + }, + "nullable": true + } + }, + "metadata": { + "__bicep_export!": true, + "description": "The type for the CNAME record." + } + }, + "mxType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "metadata": { + "description": "Required. The name of the record." + } + }, + "metadata": { + "type": "object", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.Network/privateDnsZones/MX@2024-06-01#properties/properties/properties/metadata" + }, + "description": "Optional. The metadata of the record." + }, + "nullable": true + }, + "ttl": { + "type": "int", + "nullable": true, + "metadata": { + "description": "Optional. The TTL of the record." + } + }, + "roleAssignments": { + "type": "array", + "items": { + "$ref": "#/definitions/roleAssignmentType" + }, + "nullable": true, + "metadata": { + "description": "Optional. Array of role assignments to create." + } + }, + "mxRecords": { + "type": "array", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.Network/privateDnsZones/MX@2024-06-01#properties/properties/properties/mxRecords" + }, + "description": "Optional. The list of MX records in the record set." + }, + "nullable": true + } + }, + "metadata": { + "__bicep_export!": true, + "description": "The type for the MX record." + } + }, + "ptrType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "metadata": { + "description": "Required. The name of the record." + } + }, + "metadata": { + "type": "object", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.Network/privateDnsZones/PTR@2024-06-01#properties/properties/properties/metadata" + }, + "description": "Optional. The metadata of the record." + }, + "nullable": true + }, + "ttl": { + "type": "int", + "nullable": true, + "metadata": { + "description": "Optional. The TTL of the record." + } + }, + "roleAssignments": { + "type": "array", + "items": { + "$ref": "#/definitions/roleAssignmentType" + }, + "nullable": true, + "metadata": { + "description": "Optional. Array of role assignments to create." + } + }, + "ptrRecords": { + "type": "array", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.Network/privateDnsZones/PTR@2024-06-01#properties/properties/properties/ptrRecords" + }, + "description": "Optional. The list of PTR records in the record set." + }, + "nullable": true + } + }, + "metadata": { + "__bicep_export!": true, + "description": "The type for the PTR record." + } + }, + "soaType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "metadata": { + "description": "Required. The name of the record." + } + }, + "metadata": { + "type": "object", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.Network/privateDnsZones/SOA@2024-06-01#properties/properties/properties/metadata" + }, + "description": "Optional. The metadata of the record." + }, + "nullable": true + }, + "ttl": { + "type": "int", + "nullable": true, + "metadata": { + "description": "Optional. The TTL of the record." + } + }, + "roleAssignments": { + "type": "array", + "items": { + "$ref": "#/definitions/roleAssignmentType" + }, + "nullable": true, + "metadata": { + "description": "Optional. Array of role assignments to create." + } + }, + "soaRecord": { + "type": "object", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.Network/privateDnsZones/SOA@2024-06-01#properties/properties/properties/soaRecord" + }, + "description": "Optional. The SOA record in the record set." + }, + "nullable": true + } + }, + "metadata": { + "__bicep_export!": true, + "description": "The type for the SOA record." + } + }, + "srvType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "metadata": { + "description": "Required. The name of the record." + } + }, + "metadata": { + "type": "object", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.Network/privateDnsZones/SRV@2024-06-01#properties/properties/properties/metadata" + }, + "description": "Optional. The metadata of the record." + }, + "nullable": true + }, + "ttl": { + "type": "int", + "nullable": true, + "metadata": { + "description": "Optional. The TTL of the record." + } + }, + "roleAssignments": { + "type": "array", + "items": { + "$ref": "#/definitions/roleAssignmentType" + }, + "nullable": true, + "metadata": { + "description": "Optional. Array of role assignments to create." + } + }, + "srvRecords": { + "type": "array", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.Network/privateDnsZones/SRV@2024-06-01#properties/properties/properties/srvRecords" + }, + "description": "Optional. The list of SRV records in the record set." + }, + "nullable": true + } + }, + "metadata": { + "__bicep_export!": true, + "description": "The type for the SRV record." + } + }, + "txtType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "metadata": { + "description": "Required. The name of the record." + } + }, + "metadata": { + "type": "object", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.Network/privateDnsZones/TXT@2024-06-01#properties/properties/properties/metadata" + }, + "description": "Optional. The metadata of the record." + }, + "nullable": true + }, + "ttl": { + "type": "int", + "nullable": true, + "metadata": { + "description": "Optional. The TTL of the record." + } + }, + "roleAssignments": { + "type": "array", + "items": { + "$ref": "#/definitions/roleAssignmentType" + }, + "nullable": true, + "metadata": { + "description": "Optional. Array of role assignments to create." + } + }, + "txtRecords": { + "type": "array", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.Network/privateDnsZones/TXT@2024-06-01#properties/properties/properties/txtRecords" + }, + "description": "Optional. The list of TXT records in the record set." + }, + "nullable": true + } + }, + "metadata": { + "__bicep_export!": true, + "description": "The type for the TXT record." + } + }, + "virtualNetworkLinkType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "minLength": 1, + "maxLength": 80, + "metadata": { + "description": "Optional. The resource name." + } + }, + "virtualNetworkResourceId": { + "type": "string", + "metadata": { + "description": "Required. The resource ID of the virtual network to link." + } + }, + "location": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The Azure Region where the resource lives." + } + }, + "registrationEnabled": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. Is auto-registration of virtual machine records in the virtual network in the Private DNS zone enabled?." + } + }, + "tags": { + "type": "object", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.Network/privateDnsZones/virtualNetworkLinks@2024-06-01#properties/tags" }, - "resourceGroupName": { - "type": "string", - "metadata": { - "description": "The resource group of the deployed file share service." - }, - "value": "[resourceGroup().name]" - } + "description": "Optional. Resource tags." + }, + "nullable": true + }, + "resolutionPolicy": { + "type": "string", + "allowedValues": [ + "Default", + "NxDomainRedirect" + ], + "nullable": true, + "metadata": { + "description": "Optional. The resolution type of the private-dns-zone fallback machanism." } } }, - "dependsOn": [ - "storageAccount" - ] + "metadata": { + "__bicep_export!": true, + "description": "The type for the virtual network link." + } }, - "storageAccount_queueServices": { - "condition": "[not(empty(parameters('queueServices')))]", - "type": "Microsoft.Resources/deployments", - "apiVersion": "2025-04-01", - "name": "[format('{0}-Storage-QueueServices', uniqueString(deployment().name, parameters('location')))]", + "lockType": { + "type": "object", "properties": { - "expressionEvaluationOptions": { - "scope": "inner" + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Specify the name of lock." + } }, - "mode": "Incremental", - "parameters": { - "storageAccountName": { - "value": "[parameters('name')]" - }, - "diagnosticSettings": { - "value": "[tryGet(parameters('queueServices'), 'diagnosticSettings')]" - }, - "queues": { - "value": "[tryGet(parameters('queueServices'), 'queues')]" - }, - "corsRules": { - "value": "[tryGet(parameters('queueServices'), 'corsRules')]" + "kind": { + "type": "string", + "allowedValues": [ + "CanNotDelete", + "None", + "ReadOnly" + ], + "nullable": true, + "metadata": { + "description": "Optional. Specify the type of lock." } }, - "template": { - "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", - "languageVersion": "2.0", - "contentVersion": "1.0.0.0", + "notes": { + "type": "string", + "nullable": true, "metadata": { - "_generator": { - "name": "bicep", - "version": "0.39.26.7824", - "templateHash": "762865197442503763" - }, - "name": "Storage Account Queue Services", - "description": "This module deploys a Storage Account Queue Service." - }, - "definitions": { - "corsRuleType": { - "type": "object", - "properties": { - "allowedHeaders": { - "type": "array", - "items": { - "type": "string" - }, - "metadata": { - "description": "Required. A list of headers allowed to be part of the cross-origin request." - } - }, - "allowedMethods": { - "type": "array", - "allowedValues": [ - "CONNECT", - "DELETE", - "GET", - "HEAD", - "MERGE", - "OPTIONS", - "PATCH", - "POST", - "PUT", - "TRACE" - ], - "metadata": { - "description": "Required. A list of HTTP methods that are allowed to be executed by the origin." - } - }, - "allowedOrigins": { - "type": "array", - "items": { - "type": "string" - }, - "metadata": { - "description": "Required. A list of origin domains that will be allowed via CORS, or \"*\" to allow all domains." - } - }, - "exposedHeaders": { - "type": "array", - "items": { - "type": "string" - }, - "metadata": { - "description": "Required. A list of response headers to expose to CORS clients." - } - }, - "maxAgeInSeconds": { - "type": "int", - "metadata": { - "description": "Required. The number of seconds that the client/browser should cache a preflight response." - } - } - }, - "metadata": { - "__bicep_export!": true, - "description": "The type for a cors rule." - } - }, - "queueType": { - "type": "object", - "properties": { - "name": { - "type": "string", - "metadata": { - "description": "Required. The name of the queue." - } - }, - "metadata": { - "type": "object", - "metadata": { - "__bicep_resource_derived_type!": { - "source": "Microsoft.Storage/storageAccounts/queueServices/queues@2024-01-01#properties/properties/properties/metadata" - }, - "description": "Optional. Metadata to set on the queue." - }, - "nullable": true - }, - "roleAssignments": { - "type": "array", - "items": { - "$ref": "#/definitions/roleAssignmentType" - }, - "nullable": true, - "metadata": { - "description": "Optional. Array of role assignments to create." - } - } - }, - "metadata": { - "__bicep_export!": true, - "description": "The type for a queue." - } - }, - "diagnosticSettingFullType": { - "type": "object", - "properties": { - "name": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The name of the diagnostic setting." - } - }, - "logCategoriesAndGroups": { - "type": "array", - "items": { - "type": "object", - "properties": { - "category": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. Name of a Diagnostic Log category for a resource type this setting is applied to. Set the specific logs to collect here." - } - }, - "categoryGroup": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. Name of a Diagnostic Log category group for a resource type this setting is applied to. Set to `allLogs` to collect all logs." - } - }, - "enabled": { - "type": "bool", - "nullable": true, - "metadata": { - "description": "Optional. Enable or disable the category explicitly. Default is `true`." - } - } - } - }, - "nullable": true, - "metadata": { - "description": "Optional. The name of logs that will be streamed. \"allLogs\" includes all possible logs for the resource. Set to `[]` to disable log collection." - } - }, - "metricCategories": { - "type": "array", - "items": { - "type": "object", - "properties": { - "category": { - "type": "string", - "metadata": { - "description": "Required. Name of a Diagnostic Metric category for a resource type this setting is applied to. Set to `AllMetrics` to collect all metrics." - } - }, - "enabled": { - "type": "bool", - "nullable": true, - "metadata": { - "description": "Optional. Enable or disable the category explicitly. Default is `true`." - } - } - } - }, - "nullable": true, - "metadata": { - "description": "Optional. The name of metrics that will be streamed. \"allMetrics\" includes all possible metrics for the resource. Set to `[]` to disable metric collection." - } - }, - "logAnalyticsDestinationType": { - "type": "string", - "allowedValues": [ - "AzureDiagnostics", - "Dedicated" - ], - "nullable": true, - "metadata": { - "description": "Optional. A string indicating whether the export to Log Analytics should use the default destination type, i.e. AzureDiagnostics, or use a destination type." - } - }, - "workspaceResourceId": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. Resource ID of the diagnostic log analytics workspace. For security reasons, it is recommended to set diagnostic settings to send data to either storage account, log analytics workspace or event hub." - } - }, - "storageAccountResourceId": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. Resource ID of the diagnostic storage account. For security reasons, it is recommended to set diagnostic settings to send data to either storage account, log analytics workspace or event hub." - } - }, - "eventHubAuthorizationRuleResourceId": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. Resource ID of the diagnostic event hub authorization rule for the Event Hubs namespace in which the event hub should be created or streamed to." - } - }, - "eventHubName": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. Name of the diagnostic event hub within the namespace to which logs are streamed. Without this, an event hub is created for each log category. For security reasons, it is recommended to set diagnostic settings to send data to either storage account, log analytics workspace or event hub." - } - }, - "marketplacePartnerResourceId": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The full ARM resource ID of the Marketplace resource to which you would like to send Diagnostic Logs." - } - } - }, - "metadata": { - "description": "An AVM-aligned type for a diagnostic setting. To be used if both logs & metrics are supported by the resource provider.", - "__bicep_imported_from!": { - "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.1" - } - } + "description": "Optional. Specify the notes of the lock." + } + } + }, + "metadata": { + "description": "An AVM-aligned type for a lock.", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.0" + } + } + }, + "roleAssignmentType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The name (as GUID) of the role assignment. If not provided, a GUID will be generated." + } + }, + "roleDefinitionIdOrName": { + "type": "string", + "metadata": { + "description": "Required. The role to assign. You can provide either the display name of the role definition, the role definition GUID, or its fully qualified ID in the following format: '/providers/Microsoft.Authorization/roleDefinitions/c2f4ef07-c644-48eb-af81-4b1b4947fb11'." + } + }, + "principalId": { + "type": "string", + "metadata": { + "description": "Required. The principal ID of the principal (user/group/identity) to assign the role to." + } + }, + "principalType": { + "type": "string", + "allowedValues": [ + "Device", + "ForeignGroup", + "Group", + "ServicePrincipal", + "User" + ], + "nullable": true, + "metadata": { + "description": "Optional. The principal type of the assigned principal ID." + } + }, + "description": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The description of the role assignment." + } + }, + "condition": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The conditions on the role assignment. This limits the resources it can be assigned to. e.g.: @Resource[Microsoft.Storage/storageAccounts/blobServices/containers:ContainerName] StringEqualsIgnoreCase \"foo_storage_container\"." + } + }, + "conditionVersion": { + "type": "string", + "allowedValues": [ + "2.0" + ], + "nullable": true, + "metadata": { + "description": "Optional. Version of the condition." + } + }, + "delegatedManagedIdentityResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The Resource Id of the delegated managed identity resource." + } + } + }, + "metadata": { + "description": "An AVM-aligned type for a role assignment.", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.5.1" + } + } + } + }, + "parameters": { + "name": { + "type": "string", + "metadata": { + "description": "Required. Private DNS zone name." + } + }, + "a": { + "type": "array", + "items": { + "$ref": "#/definitions/aType" + }, + "nullable": true, + "metadata": { + "description": "Optional. Array of A records." + } + }, + "aaaa": { + "type": "array", + "items": { + "$ref": "#/definitions/aaaaType" + }, + "nullable": true, + "metadata": { + "description": "Optional. Array of AAAA records." + } + }, + "cname": { + "type": "array", + "items": { + "$ref": "#/definitions/cnameType" + }, + "nullable": true, + "metadata": { + "description": "Optional. Array of CNAME records." + } + }, + "mx": { + "type": "array", + "items": { + "$ref": "#/definitions/mxType" + }, + "nullable": true, + "metadata": { + "description": "Optional. Array of MX records." + } + }, + "ptr": { + "type": "array", + "items": { + "$ref": "#/definitions/ptrType" + }, + "nullable": true, + "metadata": { + "description": "Optional. Array of PTR records." + } + }, + "soa": { + "type": "array", + "items": { + "$ref": "#/definitions/soaType" + }, + "nullable": true, + "metadata": { + "description": "Optional. Array of SOA records." + } + }, + "srv": { + "type": "array", + "items": { + "$ref": "#/definitions/srvType" + }, + "nullable": true, + "metadata": { + "description": "Optional. Array of SRV records." + } + }, + "txt": { + "type": "array", + "items": { + "$ref": "#/definitions/txtType" + }, + "nullable": true, + "metadata": { + "description": "Optional. Array of TXT records." + } + }, + "virtualNetworkLinks": { + "type": "array", + "items": { + "$ref": "#/definitions/virtualNetworkLinkType" + }, + "nullable": true, + "metadata": { + "description": "Optional. Array of custom objects describing vNet links of the DNS zone. Each object should contain properties 'virtualNetworkResourceId' and 'registrationEnabled'. The 'vnetResourceId' is a resource ID of a vNet to link, 'registrationEnabled' (bool) enables automatic DNS registration in the zone for the linked vNet." + } + }, + "location": { + "type": "string", + "defaultValue": "global", + "metadata": { + "description": "Optional. The location of the PrivateDNSZone. Should be global." + } + }, + "roleAssignments": { + "type": "array", + "items": { + "$ref": "#/definitions/roleAssignmentType" + }, + "nullable": true, + "metadata": { + "description": "Optional. Array of role assignments to create." + } + }, + "tags": { + "type": "object", + "nullable": true, + "metadata": { + "description": "Optional. Tags of the resource." + } + }, + "lock": { + "$ref": "#/definitions/lockType", + "nullable": true, + "metadata": { + "description": "Optional. The lock settings of the service." + } + }, + "enableTelemetry": { + "type": "bool", + "defaultValue": true, + "metadata": { + "description": "Optional. Enable/Disable usage telemetry for module." + } + } + }, + "variables": { + "copy": [ + { + "name": "formattedRoleAssignments", + "count": "[length(coalesce(parameters('roleAssignments'), createArray()))]", + "input": "[union(coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')], createObject('roleDefinitionId', coalesce(tryGet(variables('builtInRoleNames'), coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName), if(contains(coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName, '/providers/Microsoft.Authorization/roleDefinitions/'), coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName, subscriptionResourceId('Microsoft.Authorization/roleDefinitions', coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName)))))]" + } + ], + "builtInRoleNames": { + "Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'b24988ac-6180-42a0-ab88-20f7382dd24c')]", + "Network Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '4d97b98b-1d4f-4787-a291-c67834d212e7')]", + "Owner": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '8e3af657-a8ff-443c-a75c-2fe8c4bcb635')]", + "Private DNS Zone Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'b12aa53e-6015-4669-85d0-8515ebb3ae7f')]", + "Reader": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'acdd72a7-3385-48ef-bd42-f606fba81ae7')]", + "Role Based Access Control Administrator": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'f58310d9-a9f6-439a-9e8d-f62e7b41a168')]" + }, + "enableReferencedModulesTelemetry": false + }, + "resources": { + "avmTelemetry": { + "condition": "[parameters('enableTelemetry')]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2024-03-01", + "name": "[format('46d3xbcp.res.network-privatednszone.{0}.{1}', replace('0.8.0', '.', '-'), substring(uniqueString(deployment().name, parameters('location')), 0, 4))]", + "properties": { + "mode": "Incremental", + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "resources": [], + "outputs": { + "telemetry": { + "type": "String", + "value": "For more information, see https://aka.ms/avm/TelemetryInfo" + } + } + } + } + }, + "privateDnsZone": { + "type": "Microsoft.Network/privateDnsZones", + "apiVersion": "2020-06-01", + "name": "[parameters('name')]", + "location": "[parameters('location')]", + "tags": "[parameters('tags')]" + }, + "privateDnsZone_lock": { + "condition": "[and(not(empty(coalesce(parameters('lock'), createObject()))), not(equals(tryGet(parameters('lock'), 'kind'), 'None')))]", + "type": "Microsoft.Authorization/locks", + "apiVersion": "2020-05-01", + "scope": "[format('Microsoft.Network/privateDnsZones/{0}', parameters('name'))]", + "name": "[coalesce(tryGet(parameters('lock'), 'name'), format('lock-{0}', parameters('name')))]", + "properties": { + "level": "[coalesce(tryGet(parameters('lock'), 'kind'), '')]", + "notes": "[coalesce(tryGet(parameters('lock'), 'notes'), if(equals(tryGet(parameters('lock'), 'kind'), 'CanNotDelete'), 'Cannot delete resource or child resources.', 'Cannot delete or modify the resource or child resources.'))]" + }, + "dependsOn": [ + "privateDnsZone" + ] + }, + "privateDnsZone_roleAssignments": { + "copy": { + "name": "privateDnsZone_roleAssignments", + "count": "[length(coalesce(variables('formattedRoleAssignments'), createArray()))]" + }, + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2022-04-01", + "scope": "[format('Microsoft.Network/privateDnsZones/{0}', parameters('name'))]", + "name": "[coalesce(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'name'), guid(resourceId('Microsoft.Network/privateDnsZones', parameters('name')), coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].principalId, coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].roleDefinitionId))]", + "properties": { + "roleDefinitionId": "[coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].roleDefinitionId]", + "principalId": "[coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].principalId]", + "description": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'description')]", + "principalType": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'principalType')]", + "condition": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'condition')]", + "conditionVersion": "[if(not(empty(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'condition'))), coalesce(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'conditionVersion'), '2.0'), null())]", + "delegatedManagedIdentityResourceId": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'delegatedManagedIdentityResourceId')]" + }, + "dependsOn": [ + "privateDnsZone" + ] + }, + "privateDnsZone_A": { + "copy": { + "name": "privateDnsZone_A", + "count": "[length(coalesce(parameters('a'), createArray()))]" + }, + "type": "Microsoft.Resources/deployments", + "apiVersion": "2022-09-01", + "name": "[format('{0}-PrivateDnsZone-ARecord-{1}', uniqueString(deployment().name, parameters('location')), copyIndex())]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "privateDnsZoneName": { + "value": "[parameters('name')]" + }, + "name": { + "value": "[coalesce(parameters('a'), createArray())[copyIndex()].name]" + }, + "aRecords": { + "value": "[tryGet(coalesce(parameters('a'), createArray())[copyIndex()], 'aRecords')]" + }, + "metadata": { + "value": "[tryGet(coalesce(parameters('a'), createArray())[copyIndex()], 'metadata')]" + }, + "ttl": { + "value": "[coalesce(tryGet(coalesce(parameters('a'), createArray())[copyIndex()], 'ttl'), 3600)]" + }, + "roleAssignments": { + "value": "[tryGet(coalesce(parameters('a'), createArray())[copyIndex()], 'roleAssignments')]" + }, + "enableTelemetry": { + "value": "[variables('enableReferencedModulesTelemetry')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "languageVersion": "2.0", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.37.4.10188", + "templateHash": "12608084563401365743" }, + "name": "Private DNS Zone A record", + "description": "This module deploys a Private DNS Zone A record." + }, + "definitions": { "roleAssignmentType": { "type": "object", "properties": { @@ -22731,375 +19252,166 @@ "metadata": { "description": "An AVM-aligned type for a role assignment.", "__bicep_imported_from!": { - "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.1" + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.5.1" } } } }, "parameters": { - "storageAccountName": { + "privateDnsZoneName": { "type": "string", - "maxLength": 24, "metadata": { - "description": "Conditional. The name of the parent Storage Account. Required if the template is used in a standalone deployment." + "description": "Conditional. The name of the parent Private DNS zone. Required if the template is used in a standalone deployment." } }, - "queues": { - "type": "array", - "items": { - "$ref": "#/definitions/queueType" - }, - "defaultValue": [], + "name": { + "type": "string", "metadata": { - "description": "Optional. Queues to create." + "description": "Required. The name of the A record." } }, - "corsRules": { + "aRecords": { "type": "array", - "items": { - "$ref": "#/definitions/corsRuleType" + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.Network/privateDnsZones/A@2024-06-01#properties/properties/properties/aRecords" + }, + "description": "Optional. The list of A records in the record set." }, - "nullable": true, + "nullable": true + }, + "metadata": { + "type": "object", "metadata": { - "description": "Optional. The List of CORS rules. You can include up to five CorsRule elements in the request." + "__bicep_resource_derived_type!": { + "source": "Microsoft.Network/privateDnsZones/A@2024-06-01#properties/properties/properties/metadata" + }, + "description": "Optional. The metadata attached to the record set." + }, + "nullable": true + }, + "ttl": { + "type": "int", + "defaultValue": 3600, + "metadata": { + "description": "Optional. The TTL (time-to-live) of the records in the record set." } }, - "diagnosticSettings": { + "enableTelemetry": { + "type": "bool", + "defaultValue": true, + "metadata": { + "description": "Optional. Enable/Disable usage telemetry for module." + } + }, + "roleAssignments": { "type": "array", "items": { - "$ref": "#/definitions/diagnosticSettingFullType" + "$ref": "#/definitions/roleAssignmentType" }, "nullable": true, "metadata": { - "description": "Optional. The diagnostic settings of the service." + "description": "Optional. Array of role assignments to create." } } }, "variables": { - "name": "default" + "copy": [ + { + "name": "formattedRoleAssignments", + "count": "[length(coalesce(parameters('roleAssignments'), createArray()))]", + "input": "[union(coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')], createObject('roleDefinitionId', coalesce(tryGet(variables('builtInRoleNames'), coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName), if(contains(coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName, '/providers/Microsoft.Authorization/roleDefinitions/'), coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName, subscriptionResourceId('Microsoft.Authorization/roleDefinitions', coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName)))))]" + } + ], + "builtInRoleNames": { + "Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'b24988ac-6180-42a0-ab88-20f7382dd24c')]", + "Private DNS Zone Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'b12aa53e-6015-4669-85d0-8515ebb3ae7f')]", + "Network Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '4d97b98b-1d4f-4787-a291-c67834d212e7')]", + "Owner": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '8e3af657-a8ff-443c-a75c-2fe8c4bcb635')]", + "Reader": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'acdd72a7-3385-48ef-bd42-f606fba81ae7')]", + "Role Based Access Control Administrator": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'f58310d9-a9f6-439a-9e8d-f62e7b41a168')]", + "User Access Administrator": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '18d7d88d-d35e-4fb5-a5c3-7773c20a72d9')]" + } }, "resources": { - "storageAccount": { - "existing": true, - "type": "Microsoft.Storage/storageAccounts", - "apiVersion": "2024-01-01", - "name": "[parameters('storageAccountName')]" - }, - "queueServices": { - "type": "Microsoft.Storage/storageAccounts/queueServices", - "apiVersion": "2024-01-01", - "name": "[format('{0}/{1}', parameters('storageAccountName'), variables('name'))]", - "properties": { - "cors": "[if(not(equals(parameters('corsRules'), null())), createObject('corsRules', parameters('corsRules')), null())]" - } - }, - "queueServices_diagnosticSettings": { - "copy": { - "name": "queueServices_diagnosticSettings", - "count": "[length(coalesce(parameters('diagnosticSettings'), createArray()))]" - }, - "type": "Microsoft.Insights/diagnosticSettings", - "apiVersion": "2021-05-01-preview", - "scope": "[format('Microsoft.Storage/storageAccounts/{0}/queueServices/{1}', parameters('storageAccountName'), variables('name'))]", - "name": "[coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'name'), format('{0}-diagnosticSettings', variables('name')))]", - "properties": { - "copy": [ - { - "name": "metrics", - "count": "[length(coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'metricCategories'), createArray(createObject('category', 'AllMetrics'))))]", - "input": { - "category": "[coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'metricCategories'), createArray(createObject('category', 'AllMetrics')))[copyIndex('metrics')].category]", - "enabled": "[coalesce(tryGet(coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'metricCategories'), createArray(createObject('category', 'AllMetrics')))[copyIndex('metrics')], 'enabled'), true())]", - "timeGrain": null - } - }, - { - "name": "logs", - "count": "[length(coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'logCategoriesAndGroups'), createArray(createObject('categoryGroup', 'allLogs'))))]", - "input": { - "categoryGroup": "[tryGet(coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'logCategoriesAndGroups'), createArray(createObject('categoryGroup', 'allLogs')))[copyIndex('logs')], 'categoryGroup')]", - "category": "[tryGet(coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'logCategoriesAndGroups'), createArray(createObject('categoryGroup', 'allLogs')))[copyIndex('logs')], 'category')]", - "enabled": "[coalesce(tryGet(coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'logCategoriesAndGroups'), createArray(createObject('categoryGroup', 'allLogs')))[copyIndex('logs')], 'enabled'), true())]" - } - } - ], - "storageAccountId": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'storageAccountResourceId')]", - "workspaceId": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'workspaceResourceId')]", - "eventHubAuthorizationRuleId": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'eventHubAuthorizationRuleResourceId')]", - "eventHubName": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'eventHubName')]", - "marketplacePartnerId": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'marketplacePartnerResourceId')]", - "logAnalyticsDestinationType": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'logAnalyticsDestinationType')]" - }, - "dependsOn": [ - "queueServices" - ] - }, - "queueServices_queues": { - "copy": { - "name": "queueServices_queues", - "count": "[length(coalesce(parameters('queues'), createArray()))]" - }, + "avmTelemetry": { + "condition": "[parameters('enableTelemetry')]", "type": "Microsoft.Resources/deployments", - "apiVersion": "2025-04-01", - "name": "[format('{0}-Queue-{1}', deployment().name, copyIndex())]", + "apiVersion": "2024-03-01", + "name": "[format('46d3xbcp.res.nw-privdnszonea.{0}.{1}', replace('0.1.0', '.', '-'), substring(uniqueString(deployment().name), 0, 4))]", "properties": { - "expressionEvaluationOptions": { - "scope": "inner" - }, - "mode": "Incremental", - "parameters": { - "storageAccountName": { - "value": "[parameters('storageAccountName')]" - }, - "name": { - "value": "[coalesce(parameters('queues'), createArray())[copyIndex()].name]" - }, - "metadata": { - "value": "[tryGet(coalesce(parameters('queues'), createArray())[copyIndex()], 'metadata')]" - }, - "roleAssignments": { - "value": "[tryGet(coalesce(parameters('queues'), createArray())[copyIndex()], 'roleAssignments')]" - } - }, - "template": { - "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", - "languageVersion": "2.0", - "contentVersion": "1.0.0.0", - "metadata": { - "_generator": { - "name": "bicep", - "version": "0.39.26.7824", - "templateHash": "2653192815476217627" - }, - "name": "Storage Account Queues", - "description": "This module deploys a Storage Account Queue." - }, - "definitions": { - "roleAssignmentType": { - "type": "object", - "properties": { - "name": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The name (as GUID) of the role assignment. If not provided, a GUID will be generated." - } - }, - "roleDefinitionIdOrName": { - "type": "string", - "metadata": { - "description": "Required. The role to assign. You can provide either the display name of the role definition, the role definition GUID, or its fully qualified ID in the following format: '/providers/Microsoft.Authorization/roleDefinitions/c2f4ef07-c644-48eb-af81-4b1b4947fb11'." - } - }, - "principalId": { - "type": "string", - "metadata": { - "description": "Required. The principal ID of the principal (user/group/identity) to assign the role to." - } - }, - "principalType": { - "type": "string", - "allowedValues": [ - "Device", - "ForeignGroup", - "Group", - "ServicePrincipal", - "User" - ], - "nullable": true, - "metadata": { - "description": "Optional. The principal type of the assigned principal ID." - } - }, - "description": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The description of the role assignment." - } - }, - "condition": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The conditions on the role assignment. This limits the resources it can be assigned to. e.g.: @Resource[Microsoft.Storage/storageAccounts/blobServices/containers:ContainerName] StringEqualsIgnoreCase \"foo_storage_container\"." - } - }, - "conditionVersion": { - "type": "string", - "allowedValues": [ - "2.0" - ], - "nullable": true, - "metadata": { - "description": "Optional. Version of the condition." - } - }, - "delegatedManagedIdentityResourceId": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The Resource Id of the delegated managed identity resource." - } - } - }, - "metadata": { - "description": "An AVM-aligned type for a role assignment.", - "__bicep_imported_from!": { - "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.1" - } - } - } - }, - "parameters": { - "storageAccountName": { - "type": "string", - "maxLength": 24, - "metadata": { - "description": "Conditional. The name of the parent Storage Account. Required if the template is used in a standalone deployment." - } - }, - "name": { - "type": "string", - "metadata": { - "description": "Required. The name of the storage queue to deploy." - } - }, - "metadata": { - "type": "object", - "metadata": { - "__bicep_resource_derived_type!": { - "source": "Microsoft.Storage/storageAccounts/queueServices/queues@2024-01-01#properties/properties/properties/metadata" - }, - "description": "Optional. A name-value pair that represents queue metadata." - }, - "defaultValue": {} - }, - "roleAssignments": { - "type": "array", - "items": { - "$ref": "#/definitions/roleAssignmentType" - }, - "nullable": true, - "metadata": { - "description": "Optional. Array of role assignments to create." - } - } - }, - "variables": { - "copy": [ - { - "name": "formattedRoleAssignments", - "count": "[length(coalesce(parameters('roleAssignments'), createArray()))]", - "input": "[union(coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')], createObject('roleDefinitionId', coalesce(tryGet(variables('builtInRoleNames'), coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName), if(contains(coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName, '/providers/Microsoft.Authorization/roleDefinitions/'), coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName, subscriptionResourceId('Microsoft.Authorization/roleDefinitions', coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName)))))]" - } - ], - "builtInRoleNames": { - "Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'b24988ac-6180-42a0-ab88-20f7382dd24c')]", - "Owner": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '8e3af657-a8ff-443c-a75c-2fe8c4bcb635')]", - "Reader": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'acdd72a7-3385-48ef-bd42-f606fba81ae7')]", - "Reader and Data Access": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'c12c1c16-33a1-487b-954d-41c89c60f349')]", - "Role Based Access Control Administrator": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'f58310d9-a9f6-439a-9e8d-f62e7b41a168')]", - "Storage Account Backup Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'e5e2a7ff-d759-4cd2-bb51-3152d37e2eb1')]", - "Storage Account Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '17d1049b-9a84-46fb-8f53-869881c3d3ab')]", - "Storage Account Key Operator Service Role": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '81a9662b-bebf-436f-a333-f67b29880f12')]", - "Storage Queue Data Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '974c5e8b-45b9-4653-ba55-5f855dd0fb88')]", - "Storage Queue Data Message Processor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '8a0f0c08-91a1-4084-bc3d-661d67233fed')]", - "Storage Queue Data Message Sender": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'c6a89b2d-59bc-44d0-9896-0f6e12d7b80a')]", - "Storage Queue Data Reader": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '19e7f393-937e-4f77-808e-94535e297925')]", - "User Access Administrator": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '18d7d88d-d35e-4fb5-a5c3-7773c20a72d9')]" - } - }, - "resources": { - "storageAccount::queueServices": { - "existing": true, - "type": "Microsoft.Storage/storageAccounts/queueServices", - "apiVersion": "2024-01-01", - "name": "[format('{0}/{1}', parameters('storageAccountName'), 'default')]" - }, - "storageAccount": { - "existing": true, - "type": "Microsoft.Storage/storageAccounts", - "apiVersion": "2024-01-01", - "name": "[parameters('storageAccountName')]" - }, - "queue": { - "type": "Microsoft.Storage/storageAccounts/queueServices/queues", - "apiVersion": "2024-01-01", - "name": "[format('{0}/{1}/{2}', parameters('storageAccountName'), 'default', parameters('name'))]", - "properties": { - "metadata": "[parameters('metadata')]" - } - }, - "queue_roleAssignments": { - "copy": { - "name": "queue_roleAssignments", - "count": "[length(coalesce(variables('formattedRoleAssignments'), createArray()))]" - }, - "type": "Microsoft.Authorization/roleAssignments", - "apiVersion": "2022-04-01", - "scope": "[format('Microsoft.Storage/storageAccounts/{0}/queueServices/{1}/queues/{2}', parameters('storageAccountName'), 'default', parameters('name'))]", - "name": "[coalesce(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'name'), guid(resourceId('Microsoft.Storage/storageAccounts/queueServices/queues', parameters('storageAccountName'), 'default', parameters('name')), coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].principalId, coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].roleDefinitionId))]", - "properties": { - "roleDefinitionId": "[coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].roleDefinitionId]", - "principalId": "[coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].principalId]", - "description": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'description')]", - "principalType": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'principalType')]", - "condition": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'condition')]", - "conditionVersion": "[if(not(empty(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'condition'))), coalesce(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'conditionVersion'), '2.0'), null())]", - "delegatedManagedIdentityResourceId": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'delegatedManagedIdentityResourceId')]" - }, - "dependsOn": [ - "queue" - ] - } - }, + "mode": "Incremental", + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "resources": [], "outputs": { - "name": { - "type": "string", - "metadata": { - "description": "The name of the deployed queue." - }, - "value": "[parameters('name')]" - }, - "resourceId": { - "type": "string", - "metadata": { - "description": "The resource ID of the deployed queue." - }, - "value": "[resourceId('Microsoft.Storage/storageAccounts/queueServices/queues', parameters('storageAccountName'), 'default', parameters('name'))]" - }, - "resourceGroupName": { - "type": "string", - "metadata": { - "description": "The resource group of the deployed queue." - }, - "value": "[resourceGroup().name]" + "telemetry": { + "type": "String", + "value": "For more information, see https://aka.ms/avm/TelemetryInfo" } } } } + }, + "privateDnsZone": { + "existing": true, + "type": "Microsoft.Network/privateDnsZones", + "apiVersion": "2020-06-01", + "name": "[parameters('privateDnsZoneName')]" + }, + "A": { + "type": "Microsoft.Network/privateDnsZones/A", + "apiVersion": "2020-06-01", + "name": "[format('{0}/{1}', parameters('privateDnsZoneName'), parameters('name'))]", + "properties": { + "aRecords": "[parameters('aRecords')]", + "metadata": "[parameters('metadata')]", + "ttl": "[parameters('ttl')]" + } + }, + "A_roleAssignments": { + "copy": { + "name": "A_roleAssignments", + "count": "[length(coalesce(variables('formattedRoleAssignments'), createArray()))]" + }, + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2022-04-01", + "scope": "[format('Microsoft.Network/privateDnsZones/{0}/A/{1}', parameters('privateDnsZoneName'), parameters('name'))]", + "name": "[coalesce(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'name'), guid(resourceId('Microsoft.Network/privateDnsZones/A', parameters('privateDnsZoneName'), parameters('name')), coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].principalId, coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].roleDefinitionId))]", + "properties": { + "roleDefinitionId": "[coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].roleDefinitionId]", + "principalId": "[coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].principalId]", + "description": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'description')]", + "principalType": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'principalType')]", + "condition": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'condition')]", + "conditionVersion": "[if(not(empty(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'condition'))), coalesce(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'conditionVersion'), '2.0'), null())]", + "delegatedManagedIdentityResourceId": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'delegatedManagedIdentityResourceId')]" + }, + "dependsOn": [ + "A" + ] } }, "outputs": { "name": { "type": "string", "metadata": { - "description": "The name of the deployed queue service." + "description": "The name of the deployed A record." }, - "value": "[variables('name')]" + "value": "[parameters('name')]" }, "resourceId": { "type": "string", "metadata": { - "description": "The resource ID of the deployed queue service." + "description": "The resource ID of the deployed A record." }, - "value": "[resourceId('Microsoft.Storage/storageAccounts/queueServices', parameters('storageAccountName'), variables('name'))]" + "value": "[resourceId('Microsoft.Network/privateDnsZones/A', parameters('privateDnsZoneName'), parameters('name'))]" }, "resourceGroupName": { "type": "string", "metadata": { - "description": "The resource group of the deployed queue service." + "description": "The resource group of the deployed A record." }, "value": "[resourceGroup().name]" } @@ -23107,31 +19419,43 @@ } }, "dependsOn": [ - "storageAccount" + "privateDnsZone" ] }, - "storageAccount_tableServices": { - "condition": "[not(empty(parameters('tableServices')))]", + "privateDnsZone_AAAA": { + "copy": { + "name": "privateDnsZone_AAAA", + "count": "[length(coalesce(parameters('aaaa'), createArray()))]" + }, "type": "Microsoft.Resources/deployments", - "apiVersion": "2025-04-01", - "name": "[format('{0}-Storage-TableServices', uniqueString(deployment().name, parameters('location')))]", + "apiVersion": "2022-09-01", + "name": "[format('{0}-PrivateDnsZone-AAAARecord-{1}', uniqueString(deployment().name, parameters('location')), copyIndex())]", "properties": { "expressionEvaluationOptions": { "scope": "inner" }, "mode": "Incremental", "parameters": { - "storageAccountName": { + "privateDnsZoneName": { "value": "[parameters('name')]" }, - "diagnosticSettings": { - "value": "[tryGet(parameters('tableServices'), 'diagnosticSettings')]" + "name": { + "value": "[coalesce(parameters('aaaa'), createArray())[copyIndex()].name]" }, - "tables": { - "value": "[tryGet(parameters('tableServices'), 'tables')]" + "aaaaRecords": { + "value": "[tryGet(coalesce(parameters('aaaa'), createArray())[copyIndex()], 'aaaaRecords')]" }, - "corsRules": { - "value": "[tryGet(parameters('tableServices'), 'corsRules')]" + "metadata": { + "value": "[tryGet(coalesce(parameters('aaaa'), createArray())[copyIndex()], 'metadata')]" + }, + "ttl": { + "value": "[coalesce(tryGet(coalesce(parameters('aaaa'), createArray())[copyIndex()], 'ttl'), 3600)]" + }, + "roleAssignments": { + "value": "[tryGet(coalesce(parameters('aaaa'), createArray())[copyIndex()], 'roleAssignments')]" + }, + "enableTelemetry": { + "value": "[variables('enableReferencedModulesTelemetry')]" } }, "template": { @@ -23141,220 +19465,304 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.39.26.7824", - "templateHash": "17140438874562378925" + "version": "0.37.4.10188", + "templateHash": "4881696097088567452" }, - "name": "Storage Account Table Services", - "description": "This module deploys a Storage Account Table Service." + "name": "Private DNS Zone AAAA record", + "description": "This module deploys a Private DNS Zone AAAA record." }, "definitions": { - "corsRuleType": { - "type": "object", - "properties": { - "allowedHeaders": { - "type": "array", - "items": { - "type": "string" - }, - "metadata": { - "description": "Required. A list of headers allowed to be part of the cross-origin request." - } - }, - "allowedMethods": { - "type": "array", - "allowedValues": [ - "CONNECT", - "DELETE", - "GET", - "HEAD", - "MERGE", - "OPTIONS", - "PATCH", - "POST", - "PUT", - "TRACE" - ], - "metadata": { - "description": "Required. A list of HTTP methods that are allowed to be executed by the origin." - } - }, - "allowedOrigins": { - "type": "array", - "items": { - "type": "string" - }, - "metadata": { - "description": "Required. A list of origin domains that will be allowed via CORS, or \"*\" to allow all domains." - } - }, - "exposedHeaders": { - "type": "array", - "items": { - "type": "string" - }, - "metadata": { - "description": "Required. A list of response headers to expose to CORS clients." - } - }, - "maxAgeInSeconds": { - "type": "int", - "metadata": { - "description": "Required. The number of seconds that the client/browser should cache a preflight response." - } - } - }, - "metadata": { - "__bicep_export!": true, - "description": "The type for a cors rule." - } - }, - "tableType": { - "type": "object", - "properties": { - "name": { - "type": "string", - "metadata": { - "description": "Required. The name of the table." - } - }, - "roleAssignments": { - "type": "array", - "items": { - "$ref": "#/definitions/roleAssignmentType" - }, - "nullable": true, - "metadata": { - "description": "Optional. Array of role assignments to create." - } - } - }, - "metadata": { - "__bicep_export!": true, - "description": "The type for a table." - } - }, - "diagnosticSettingFullType": { + "roleAssignmentType": { "type": "object", "properties": { "name": { "type": "string", "nullable": true, "metadata": { - "description": "Optional. The name of the diagnostic setting." + "description": "Optional. The name (as GUID) of the role assignment. If not provided, a GUID will be generated." } }, - "logCategoriesAndGroups": { - "type": "array", - "items": { - "type": "object", - "properties": { - "category": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. Name of a Diagnostic Log category for a resource type this setting is applied to. Set the specific logs to collect here." - } - }, - "categoryGroup": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. Name of a Diagnostic Log category group for a resource type this setting is applied to. Set to `allLogs` to collect all logs." - } - }, - "enabled": { - "type": "bool", - "nullable": true, - "metadata": { - "description": "Optional. Enable or disable the category explicitly. Default is `true`." - } - } - } - }, - "nullable": true, + "roleDefinitionIdOrName": { + "type": "string", "metadata": { - "description": "Optional. The name of logs that will be streamed. \"allLogs\" includes all possible logs for the resource. Set to `[]` to disable log collection." + "description": "Required. The role to assign. You can provide either the display name of the role definition, the role definition GUID, or its fully qualified ID in the following format: '/providers/Microsoft.Authorization/roleDefinitions/c2f4ef07-c644-48eb-af81-4b1b4947fb11'." } }, - "metricCategories": { - "type": "array", - "items": { - "type": "object", - "properties": { - "category": { - "type": "string", - "metadata": { - "description": "Required. Name of a Diagnostic Metric category for a resource type this setting is applied to. Set to `AllMetrics` to collect all metrics." - } - }, - "enabled": { - "type": "bool", - "nullable": true, - "metadata": { - "description": "Optional. Enable or disable the category explicitly. Default is `true`." - } - } - } - }, - "nullable": true, + "principalId": { + "type": "string", "metadata": { - "description": "Optional. The name of metrics that will be streamed. \"allMetrics\" includes all possible metrics for the resource. Set to `[]` to disable metric collection." + "description": "Required. The principal ID of the principal (user/group/identity) to assign the role to." } }, - "logAnalyticsDestinationType": { + "principalType": { "type": "string", "allowedValues": [ - "AzureDiagnostics", - "Dedicated" + "Device", + "ForeignGroup", + "Group", + "ServicePrincipal", + "User" ], "nullable": true, "metadata": { - "description": "Optional. A string indicating whether the export to Log Analytics should use the default destination type, i.e. AzureDiagnostics, or use a destination type." - } - }, - "workspaceResourceId": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. Resource ID of the diagnostic log analytics workspace. For security reasons, it is recommended to set diagnostic settings to send data to either storage account, log analytics workspace or event hub." + "description": "Optional. The principal type of the assigned principal ID." } }, - "storageAccountResourceId": { + "description": { "type": "string", "nullable": true, "metadata": { - "description": "Optional. Resource ID of the diagnostic storage account. For security reasons, it is recommended to set diagnostic settings to send data to either storage account, log analytics workspace or event hub." + "description": "Optional. The description of the role assignment." } }, - "eventHubAuthorizationRuleResourceId": { + "condition": { "type": "string", "nullable": true, "metadata": { - "description": "Optional. Resource ID of the diagnostic event hub authorization rule for the Event Hubs namespace in which the event hub should be created or streamed to." + "description": "Optional. The conditions on the role assignment. This limits the resources it can be assigned to. e.g.: @Resource[Microsoft.Storage/storageAccounts/blobServices/containers:ContainerName] StringEqualsIgnoreCase \"foo_storage_container\"." } }, - "eventHubName": { + "conditionVersion": { "type": "string", + "allowedValues": [ + "2.0" + ], "nullable": true, "metadata": { - "description": "Optional. Name of the diagnostic event hub within the namespace to which logs are streamed. Without this, an event hub is created for each log category. For security reasons, it is recommended to set diagnostic settings to send data to either storage account, log analytics workspace or event hub." + "description": "Optional. Version of the condition." } }, - "marketplacePartnerResourceId": { + "delegatedManagedIdentityResourceId": { "type": "string", "nullable": true, "metadata": { - "description": "Optional. The full ARM resource ID of the Marketplace resource to which you would like to send Diagnostic Logs." + "description": "Optional. The Resource Id of the delegated managed identity resource." } } }, "metadata": { - "description": "An AVM-aligned type for a diagnostic setting. To be used if both logs & metrics are supported by the resource provider.", + "description": "An AVM-aligned type for a role assignment.", "__bicep_imported_from!": { - "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.1" + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.5.1" + } + } + } + }, + "parameters": { + "privateDnsZoneName": { + "type": "string", + "metadata": { + "description": "Conditional. The name of the parent Private DNS zone. Required if the template is used in a standalone deployment." + } + }, + "name": { + "type": "string", + "metadata": { + "description": "Required. The name of the AAAA record." + } + }, + "aaaaRecords": { + "type": "array", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.Network/privateDnsZones/AAAA@2024-06-01#properties/properties/properties/aaaaRecords" + }, + "description": "Optional. The list of AAAA records in the record set." + }, + "nullable": true + }, + "metadata": { + "type": "object", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.Network/privateDnsZones/AAAA@2024-06-01#properties/properties/properties/metadata" + }, + "description": "Optional. The metadata attached to the record set." + }, + "nullable": true + }, + "ttl": { + "type": "int", + "defaultValue": 3600, + "metadata": { + "description": "Optional. The TTL (time-to-live) of the records in the record set." + } + }, + "enableTelemetry": { + "type": "bool", + "defaultValue": true, + "metadata": { + "description": "Optional. Enable/Disable usage telemetry for module." + } + }, + "roleAssignments": { + "type": "array", + "items": { + "$ref": "#/definitions/roleAssignmentType" + }, + "nullable": true, + "metadata": { + "description": "Optional. Array of role assignments to create." + } + } + }, + "variables": { + "copy": [ + { + "name": "formattedRoleAssignments", + "count": "[length(coalesce(parameters('roleAssignments'), createArray()))]", + "input": "[union(coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')], createObject('roleDefinitionId', coalesce(tryGet(variables('builtInRoleNames'), coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName), if(contains(coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName, '/providers/Microsoft.Authorization/roleDefinitions/'), coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName, subscriptionResourceId('Microsoft.Authorization/roleDefinitions', coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName)))))]" + } + ], + "builtInRoleNames": { + "Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'b24988ac-6180-42a0-ab88-20f7382dd24c')]", + "Private DNS Zone Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'b12aa53e-6015-4669-85d0-8515ebb3ae7f')]", + "Network Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '4d97b98b-1d4f-4787-a291-c67834d212e7')]", + "Owner": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '8e3af657-a8ff-443c-a75c-2fe8c4bcb635')]", + "Reader": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'acdd72a7-3385-48ef-bd42-f606fba81ae7')]", + "Role Based Access Control Administrator": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'f58310d9-a9f6-439a-9e8d-f62e7b41a168')]", + "User Access Administrator": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '18d7d88d-d35e-4fb5-a5c3-7773c20a72d9')]" + } + }, + "resources": { + "avmTelemetry": { + "condition": "[parameters('enableTelemetry')]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2024-03-01", + "name": "[format('46d3xbcp.res.nw-privdnszoneaaaa.{0}.{1}', replace('0.1.0', '.', '-'), substring(uniqueString(deployment().name), 0, 4))]", + "properties": { + "mode": "Incremental", + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "resources": [], + "outputs": { + "telemetry": { + "type": "String", + "value": "For more information, see https://aka.ms/avm/TelemetryInfo" + } + } } } }, + "privateDnsZone": { + "existing": true, + "type": "Microsoft.Network/privateDnsZones", + "apiVersion": "2020-06-01", + "name": "[parameters('privateDnsZoneName')]" + }, + "AAAA": { + "type": "Microsoft.Network/privateDnsZones/AAAA", + "apiVersion": "2020-06-01", + "name": "[format('{0}/{1}', parameters('privateDnsZoneName'), parameters('name'))]", + "properties": { + "aaaaRecords": "[parameters('aaaaRecords')]", + "metadata": "[parameters('metadata')]", + "ttl": "[parameters('ttl')]" + } + }, + "AAAA_roleAssignments": { + "copy": { + "name": "AAAA_roleAssignments", + "count": "[length(coalesce(variables('formattedRoleAssignments'), createArray()))]" + }, + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2022-04-01", + "scope": "[format('Microsoft.Network/privateDnsZones/{0}/AAAA/{1}', parameters('privateDnsZoneName'), parameters('name'))]", + "name": "[coalesce(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'name'), guid(resourceId('Microsoft.Network/privateDnsZones/AAAA', parameters('privateDnsZoneName'), parameters('name')), coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].principalId, coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].roleDefinitionId))]", + "properties": { + "roleDefinitionId": "[coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].roleDefinitionId]", + "principalId": "[coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].principalId]", + "description": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'description')]", + "principalType": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'principalType')]", + "condition": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'condition')]", + "conditionVersion": "[if(not(empty(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'condition'))), coalesce(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'conditionVersion'), '2.0'), null())]", + "delegatedManagedIdentityResourceId": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'delegatedManagedIdentityResourceId')]" + }, + "dependsOn": [ + "AAAA" + ] + } + }, + "outputs": { + "name": { + "type": "string", + "metadata": { + "description": "The name of the deployed AAAA record." + }, + "value": "[parameters('name')]" + }, + "resourceId": { + "type": "string", + "metadata": { + "description": "The resource ID of the deployed AAAA record." + }, + "value": "[resourceId('Microsoft.Network/privateDnsZones/AAAA', parameters('privateDnsZoneName'), parameters('name'))]" + }, + "resourceGroupName": { + "type": "string", + "metadata": { + "description": "The resource group of the deployed AAAA record." + }, + "value": "[resourceGroup().name]" + } + } + } + }, + "dependsOn": [ + "privateDnsZone" + ] + }, + "privateDnsZone_CNAME": { + "copy": { + "name": "privateDnsZone_CNAME", + "count": "[length(coalesce(parameters('cname'), createArray()))]" + }, + "type": "Microsoft.Resources/deployments", + "apiVersion": "2022-09-01", + "name": "[format('{0}-PrivateDnsZone-CNAMERecord-{1}', uniqueString(deployment().name, parameters('location')), copyIndex())]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "privateDnsZoneName": { + "value": "[parameters('name')]" + }, + "name": { + "value": "[coalesce(parameters('cname'), createArray())[copyIndex()].name]" + }, + "cnameRecord": { + "value": "[tryGet(coalesce(parameters('cname'), createArray())[copyIndex()], 'cnameRecord')]" + }, + "metadata": { + "value": "[tryGet(coalesce(parameters('cname'), createArray())[copyIndex()], 'metadata')]" + }, + "ttl": { + "value": "[coalesce(tryGet(coalesce(parameters('cname'), createArray())[copyIndex()], 'ttl'), 3600)]" + }, + "roleAssignments": { + "value": "[tryGet(coalesce(parameters('cname'), createArray())[copyIndex()], 'roleAssignments')]" + }, + "enableTelemetry": { + "value": "[variables('enableReferencedModulesTelemetry')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "languageVersion": "2.0", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.37.4.10188", + "templateHash": "13307906270868460967" + }, + "name": "Private DNS Zone CNAME record", + "description": "This module deploys a Private DNS Zone CNAME record." + }, + "definitions": { "roleAssignmentType": { "type": "object", "properties": { @@ -23426,357 +19834,457 @@ "metadata": { "description": "An AVM-aligned type for a role assignment.", "__bicep_imported_from!": { - "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.1" + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.5.1" } } } }, "parameters": { - "storageAccountName": { + "privateDnsZoneName": { "type": "string", - "maxLength": 24, "metadata": { - "description": "Conditional. The name of the parent Storage Account. Required if the template is used in a standalone deployment." + "description": "Conditional. The name of the parent Private DNS zone. Required if the template is used in a standalone deployment." } }, - "tables": { - "type": "array", - "items": { - "$ref": "#/definitions/tableType" - }, - "nullable": true, + "name": { + "type": "string", "metadata": { - "description": "Optional. Tables to create." + "description": "Required. The name of the CNAME record." } }, - "corsRules": { - "type": "array", - "items": { - "$ref": "#/definitions/corsRuleType" + "cnameRecord": { + "type": "object", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.Network/privateDnsZones/CNAME@2024-06-01#properties/properties/properties/cnameRecord" + }, + "description": "Optional. A CNAME record." }, - "nullable": true, + "nullable": true + }, + "metadata": { + "type": "object", "metadata": { - "description": "Optional. The List of CORS rules. You can include up to five CorsRule elements in the request." + "__bicep_resource_derived_type!": { + "source": "Microsoft.Network/privateDnsZones/CNAME@2024-06-01#properties/properties/properties/metadata" + }, + "description": "Optional. The metadata attached to the record set." + }, + "nullable": true + }, + "ttl": { + "type": "int", + "defaultValue": 3600, + "metadata": { + "description": "Optional. The TTL (time-to-live) of the records in the record set." } }, - "diagnosticSettings": { + "enableTelemetry": { + "type": "bool", + "defaultValue": true, + "metadata": { + "description": "Optional. Enable/Disable usage telemetry for module." + } + }, + "roleAssignments": { "type": "array", "items": { - "$ref": "#/definitions/diagnosticSettingFullType" + "$ref": "#/definitions/roleAssignmentType" }, "nullable": true, "metadata": { - "description": "Optional. The diagnostic settings of the service." + "description": "Optional. Array of role assignments to create." } } }, "variables": { - "name": "default" + "copy": [ + { + "name": "formattedRoleAssignments", + "count": "[length(coalesce(parameters('roleAssignments'), createArray()))]", + "input": "[union(coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')], createObject('roleDefinitionId', coalesce(tryGet(variables('builtInRoleNames'), coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName), if(contains(coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName, '/providers/Microsoft.Authorization/roleDefinitions/'), coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName, subscriptionResourceId('Microsoft.Authorization/roleDefinitions', coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName)))))]" + } + ], + "builtInRoleNames": { + "Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'b24988ac-6180-42a0-ab88-20f7382dd24c')]", + "Private DNS Zone Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'b12aa53e-6015-4669-85d0-8515ebb3ae7f')]", + "Network Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '4d97b98b-1d4f-4787-a291-c67834d212e7')]", + "Owner": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '8e3af657-a8ff-443c-a75c-2fe8c4bcb635')]", + "Reader": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'acdd72a7-3385-48ef-bd42-f606fba81ae7')]", + "Role Based Access Control Administrator": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'f58310d9-a9f6-439a-9e8d-f62e7b41a168')]", + "User Access Administrator": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '18d7d88d-d35e-4fb5-a5c3-7773c20a72d9')]" + } }, "resources": { - "storageAccount": { + "avmTelemetry": { + "condition": "[parameters('enableTelemetry')]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2024-03-01", + "name": "[format('46d3xbcp.res.nw-privdnszonecname.{0}.{1}', replace('0.1.0', '.', '-'), substring(uniqueString(deployment().name), 0, 4))]", + "properties": { + "mode": "Incremental", + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "resources": [], + "outputs": { + "telemetry": { + "type": "String", + "value": "For more information, see https://aka.ms/avm/TelemetryInfo" + } + } + } + } + }, + "privateDnsZone": { "existing": true, - "type": "Microsoft.Storage/storageAccounts", - "apiVersion": "2024-01-01", - "name": "[parameters('storageAccountName')]" + "type": "Microsoft.Network/privateDnsZones", + "apiVersion": "2020-06-01", + "name": "[parameters('privateDnsZoneName')]" }, - "tableServices": { - "type": "Microsoft.Storage/storageAccounts/tableServices", - "apiVersion": "2024-01-01", - "name": "[format('{0}/{1}', parameters('storageAccountName'), variables('name'))]", + "CNAME": { + "type": "Microsoft.Network/privateDnsZones/CNAME", + "apiVersion": "2020-06-01", + "name": "[format('{0}/{1}', parameters('privateDnsZoneName'), parameters('name'))]", "properties": { - "cors": "[if(not(equals(parameters('corsRules'), null())), createObject('corsRules', parameters('corsRules')), null())]" + "cnameRecord": "[parameters('cnameRecord')]", + "metadata": "[parameters('metadata')]", + "ttl": "[parameters('ttl')]" } }, - "tableServices_diagnosticSettings": { + "CNAME_roleAssignments": { "copy": { - "name": "tableServices_diagnosticSettings", - "count": "[length(coalesce(parameters('diagnosticSettings'), createArray()))]" + "name": "CNAME_roleAssignments", + "count": "[length(coalesce(variables('formattedRoleAssignments'), createArray()))]" }, - "type": "Microsoft.Insights/diagnosticSettings", - "apiVersion": "2021-05-01-preview", - "scope": "[format('Microsoft.Storage/storageAccounts/{0}/tableServices/{1}', parameters('storageAccountName'), variables('name'))]", - "name": "[coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'name'), format('{0}-diagnosticSettings', variables('name')))]", + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2022-04-01", + "scope": "[format('Microsoft.Network/privateDnsZones/{0}/CNAME/{1}', parameters('privateDnsZoneName'), parameters('name'))]", + "name": "[coalesce(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'name'), guid(resourceId('Microsoft.Network/privateDnsZones/CNAME', parameters('privateDnsZoneName'), parameters('name')), coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].principalId, coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].roleDefinitionId))]", "properties": { - "copy": [ - { - "name": "metrics", - "count": "[length(coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'metricCategories'), createArray(createObject('category', 'AllMetrics'))))]", - "input": { - "category": "[coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'metricCategories'), createArray(createObject('category', 'AllMetrics')))[copyIndex('metrics')].category]", - "enabled": "[coalesce(tryGet(coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'metricCategories'), createArray(createObject('category', 'AllMetrics')))[copyIndex('metrics')], 'enabled'), true())]", - "timeGrain": null - } - }, - { - "name": "logs", - "count": "[length(coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'logCategoriesAndGroups'), createArray(createObject('categoryGroup', 'allLogs'))))]", - "input": { - "categoryGroup": "[tryGet(coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'logCategoriesAndGroups'), createArray(createObject('categoryGroup', 'allLogs')))[copyIndex('logs')], 'categoryGroup')]", - "category": "[tryGet(coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'logCategoriesAndGroups'), createArray(createObject('categoryGroup', 'allLogs')))[copyIndex('logs')], 'category')]", - "enabled": "[coalesce(tryGet(coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'logCategoriesAndGroups'), createArray(createObject('categoryGroup', 'allLogs')))[copyIndex('logs')], 'enabled'), true())]" - } - } - ], - "storageAccountId": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'storageAccountResourceId')]", - "workspaceId": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'workspaceResourceId')]", - "eventHubAuthorizationRuleId": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'eventHubAuthorizationRuleResourceId')]", - "eventHubName": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'eventHubName')]", - "marketplacePartnerId": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'marketplacePartnerResourceId')]", - "logAnalyticsDestinationType": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'logAnalyticsDestinationType')]" + "roleDefinitionId": "[coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].roleDefinitionId]", + "principalId": "[coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].principalId]", + "description": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'description')]", + "principalType": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'principalType')]", + "condition": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'condition')]", + "conditionVersion": "[if(not(empty(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'condition'))), coalesce(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'conditionVersion'), '2.0'), null())]", + "delegatedManagedIdentityResourceId": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'delegatedManagedIdentityResourceId')]" }, "dependsOn": [ - "tableServices" + "CNAME" ] + } + }, + "outputs": { + "name": { + "type": "string", + "metadata": { + "description": "The name of the deployed CNAME record." + }, + "value": "[parameters('name')]" }, - "tableServices_tables": { - "copy": { - "name": "tableServices_tables", - "count": "[length(coalesce(parameters('tables'), createArray()))]" + "resourceId": { + "type": "string", + "metadata": { + "description": "The resource ID of the deployed CNAME record." }, - "type": "Microsoft.Resources/deployments", - "apiVersion": "2025-04-01", - "name": "[format('{0}-Table-{1}', deployment().name, copyIndex())]", + "value": "[resourceId('Microsoft.Network/privateDnsZones/CNAME', parameters('privateDnsZoneName'), parameters('name'))]" + }, + "resourceGroupName": { + "type": "string", + "metadata": { + "description": "The resource group of the deployed CNAME record." + }, + "value": "[resourceGroup().name]" + } + } + } + }, + "dependsOn": [ + "privateDnsZone" + ] + }, + "privateDnsZone_MX": { + "copy": { + "name": "privateDnsZone_MX", + "count": "[length(coalesce(parameters('mx'), createArray()))]" + }, + "type": "Microsoft.Resources/deployments", + "apiVersion": "2022-09-01", + "name": "[format('{0}-PrivateDnsZone-MXRecord-{1}', uniqueString(deployment().name, parameters('location')), copyIndex())]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "privateDnsZoneName": { + "value": "[parameters('name')]" + }, + "name": { + "value": "[coalesce(parameters('mx'), createArray())[copyIndex()].name]" + }, + "metadata": { + "value": "[tryGet(coalesce(parameters('mx'), createArray())[copyIndex()], 'metadata')]" + }, + "mxRecords": { + "value": "[tryGet(coalesce(parameters('mx'), createArray())[copyIndex()], 'mxRecords')]" + }, + "ttl": { + "value": "[coalesce(tryGet(coalesce(parameters('mx'), createArray())[copyIndex()], 'ttl'), 3600)]" + }, + "roleAssignments": { + "value": "[tryGet(coalesce(parameters('mx'), createArray())[copyIndex()], 'roleAssignments')]" + }, + "enableTelemetry": { + "value": "[variables('enableReferencedModulesTelemetry')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "languageVersion": "2.0", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.37.4.10188", + "templateHash": "7946896598573056688" + }, + "name": "Private DNS Zone MX record", + "description": "This module deploys a Private DNS Zone MX record." + }, + "definitions": { + "roleAssignmentType": { + "type": "object", "properties": { - "expressionEvaluationOptions": { - "scope": "inner" + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The name (as GUID) of the role assignment. If not provided, a GUID will be generated." + } }, - "mode": "Incremental", - "parameters": { - "name": { - "value": "[coalesce(parameters('tables'), createArray())[copyIndex()].name]" - }, - "storageAccountName": { - "value": "[parameters('storageAccountName')]" - }, - "roleAssignments": { - "value": "[tryGet(coalesce(parameters('tables'), createArray())[copyIndex()], 'roleAssignments')]" + "roleDefinitionIdOrName": { + "type": "string", + "metadata": { + "description": "Required. The role to assign. You can provide either the display name of the role definition, the role definition GUID, or its fully qualified ID in the following format: '/providers/Microsoft.Authorization/roleDefinitions/c2f4ef07-c644-48eb-af81-4b1b4947fb11'." } }, - "template": { - "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", - "languageVersion": "2.0", - "contentVersion": "1.0.0.0", + "principalId": { + "type": "string", "metadata": { - "_generator": { - "name": "bicep", - "version": "0.39.26.7824", - "templateHash": "11466809443516053137" - }, - "name": "Storage Account Table", - "description": "This module deploys a Storage Account Table." - }, - "definitions": { - "roleAssignmentType": { - "type": "object", - "properties": { - "name": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The name (as GUID) of the role assignment. If not provided, a GUID will be generated." - } - }, - "roleDefinitionIdOrName": { - "type": "string", - "metadata": { - "description": "Required. The role to assign. You can provide either the display name of the role definition, the role definition GUID, or its fully qualified ID in the following format: '/providers/Microsoft.Authorization/roleDefinitions/c2f4ef07-c644-48eb-af81-4b1b4947fb11'." - } - }, - "principalId": { - "type": "string", - "metadata": { - "description": "Required. The principal ID of the principal (user/group/identity) to assign the role to." - } - }, - "principalType": { - "type": "string", - "allowedValues": [ - "Device", - "ForeignGroup", - "Group", - "ServicePrincipal", - "User" - ], - "nullable": true, - "metadata": { - "description": "Optional. The principal type of the assigned principal ID." - } - }, - "description": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The description of the role assignment." - } - }, - "condition": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The conditions on the role assignment. This limits the resources it can be assigned to. e.g.: @Resource[Microsoft.Storage/storageAccounts/blobServices/containers:ContainerName] StringEqualsIgnoreCase \"foo_storage_container\"." - } - }, - "conditionVersion": { - "type": "string", - "allowedValues": [ - "2.0" - ], - "nullable": true, - "metadata": { - "description": "Optional. Version of the condition." - } - }, - "delegatedManagedIdentityResourceId": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The Resource Id of the delegated managed identity resource." - } - } - }, - "metadata": { - "description": "An AVM-aligned type for a role assignment.", - "__bicep_imported_from!": { - "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.1" - } - } - } - }, - "parameters": { - "storageAccountName": { - "type": "string", - "maxLength": 24, - "metadata": { - "description": "Conditional. The name of the parent Storage Account. Required if the template is used in a standalone deployment." - } - }, - "roleAssignments": { - "type": "array", - "items": { - "$ref": "#/definitions/roleAssignmentType" - }, - "nullable": true, - "metadata": { - "description": "Optional. Array of role assignments to create." - } - }, - "name": { - "type": "string", - "metadata": { - "description": "Required. Name of the table." - } - } - }, - "variables": { - "copy": [ - { - "name": "formattedRoleAssignments", - "count": "[length(coalesce(parameters('roleAssignments'), createArray()))]", - "input": "[union(coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')], createObject('roleDefinitionId', coalesce(tryGet(variables('builtInRoleNames'), coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName), if(contains(coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName, '/providers/Microsoft.Authorization/roleDefinitions/'), coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName, subscriptionResourceId('Microsoft.Authorization/roleDefinitions', coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName)))))]" - } - ], - "builtInRoleNames": { - "Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'b24988ac-6180-42a0-ab88-20f7382dd24c')]", - "Owner": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '8e3af657-a8ff-443c-a75c-2fe8c4bcb635')]", - "Reader": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'acdd72a7-3385-48ef-bd42-f606fba81ae7')]", - "Reader and Data Access": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'c12c1c16-33a1-487b-954d-41c89c60f349')]", - "Role Based Access Control Administrator": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'f58310d9-a9f6-439a-9e8d-f62e7b41a168')]", - "Storage Account Backup Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'e5e2a7ff-d759-4cd2-bb51-3152d37e2eb1')]", - "Storage Account Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '17d1049b-9a84-46fb-8f53-869881c3d3ab')]", - "Storage Account Key Operator Service Role": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '81a9662b-bebf-436f-a333-f67b29880f12')]", - "Storage Table Data Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '0a9a7e1f-b9d0-4cc4-a60d-0319b160aaa3')]", - "Storage Table Data Reader": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '76199698-9eea-4c19-bc75-cec21354c6b6')]", - "User Access Administrator": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '18d7d88d-d35e-4fb5-a5c3-7773c20a72d9')]" - } - }, - "resources": { - "storageAccount::tableServices": { - "existing": true, - "type": "Microsoft.Storage/storageAccounts/tableServices", - "apiVersion": "2024-01-01", - "name": "[format('{0}/{1}', parameters('storageAccountName'), 'default')]" - }, - "storageAccount": { - "existing": true, - "type": "Microsoft.Storage/storageAccounts", - "apiVersion": "2024-01-01", - "name": "[parameters('storageAccountName')]" - }, - "table": { - "type": "Microsoft.Storage/storageAccounts/tableServices/tables", - "apiVersion": "2024-01-01", - "name": "[format('{0}/{1}/{2}', parameters('storageAccountName'), 'default', parameters('name'))]" - }, - "table_roleAssignments": { - "copy": { - "name": "table_roleAssignments", - "count": "[length(coalesce(variables('formattedRoleAssignments'), createArray()))]" - }, - "type": "Microsoft.Authorization/roleAssignments", - "apiVersion": "2022-04-01", - "scope": "[format('Microsoft.Storage/storageAccounts/{0}/tableServices/{1}/tables/{2}', parameters('storageAccountName'), 'default', parameters('name'))]", - "name": "[coalesce(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'name'), guid(resourceId('Microsoft.Storage/storageAccounts/tableServices/tables', parameters('storageAccountName'), 'default', parameters('name')), coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].principalId, coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].roleDefinitionId))]", - "properties": { - "roleDefinitionId": "[coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].roleDefinitionId]", - "principalId": "[coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].principalId]", - "description": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'description')]", - "principalType": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'principalType')]", - "condition": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'condition')]", - "conditionVersion": "[if(not(empty(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'condition'))), coalesce(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'conditionVersion'), '2.0'), null())]", - "delegatedManagedIdentityResourceId": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'delegatedManagedIdentityResourceId')]" - }, - "dependsOn": [ - "table" - ] - } - }, + "description": "Required. The principal ID of the principal (user/group/identity) to assign the role to." + } + }, + "principalType": { + "type": "string", + "allowedValues": [ + "Device", + "ForeignGroup", + "Group", + "ServicePrincipal", + "User" + ], + "nullable": true, + "metadata": { + "description": "Optional. The principal type of the assigned principal ID." + } + }, + "description": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The description of the role assignment." + } + }, + "condition": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The conditions on the role assignment. This limits the resources it can be assigned to. e.g.: @Resource[Microsoft.Storage/storageAccounts/blobServices/containers:ContainerName] StringEqualsIgnoreCase \"foo_storage_container\"." + } + }, + "conditionVersion": { + "type": "string", + "allowedValues": [ + "2.0" + ], + "nullable": true, + "metadata": { + "description": "Optional. Version of the condition." + } + }, + "delegatedManagedIdentityResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The Resource Id of the delegated managed identity resource." + } + } + }, + "metadata": { + "description": "An AVM-aligned type for a role assignment.", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.5.1" + } + } + } + }, + "parameters": { + "privateDnsZoneName": { + "type": "string", + "metadata": { + "description": "Conditional. The name of the parent Private DNS zone. Required if the template is used in a standalone deployment." + } + }, + "name": { + "type": "string", + "metadata": { + "description": "Required. The name of the MX record." + } + }, + "metadata": { + "type": "object", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.Network/privateDnsZones/MX@2024-06-01#properties/properties/properties/metadata" + }, + "description": "Optional. The metadata attached to the record set." + }, + "nullable": true + }, + "mxRecords": { + "type": "array", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.Network/privateDnsZones/MX@2024-06-01#properties/properties/properties/mxRecords" + }, + "description": "Optional. The list of MX records in the record set." + }, + "nullable": true + }, + "ttl": { + "type": "int", + "defaultValue": 3600, + "metadata": { + "description": "Optional. The TTL (time-to-live) of the records in the record set." + } + }, + "enableTelemetry": { + "type": "bool", + "defaultValue": true, + "metadata": { + "description": "Optional. Enable/Disable usage telemetry for module." + } + }, + "roleAssignments": { + "type": "array", + "items": { + "$ref": "#/definitions/roleAssignmentType" + }, + "nullable": true, + "metadata": { + "description": "Optional. Array of role assignments to create." + } + } + }, + "variables": { + "copy": [ + { + "name": "formattedRoleAssignments", + "count": "[length(coalesce(parameters('roleAssignments'), createArray()))]", + "input": "[union(coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')], createObject('roleDefinitionId', coalesce(tryGet(variables('builtInRoleNames'), coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName), if(contains(coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName, '/providers/Microsoft.Authorization/roleDefinitions/'), coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName, subscriptionResourceId('Microsoft.Authorization/roleDefinitions', coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName)))))]" + } + ], + "builtInRoleNames": { + "Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'b24988ac-6180-42a0-ab88-20f7382dd24c')]", + "Private DNS Zone Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'b12aa53e-6015-4669-85d0-8515ebb3ae7f')]", + "Network Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '4d97b98b-1d4f-4787-a291-c67834d212e7')]", + "Owner": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '8e3af657-a8ff-443c-a75c-2fe8c4bcb635')]", + "Reader": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'acdd72a7-3385-48ef-bd42-f606fba81ae7')]", + "Role Based Access Control Administrator": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'f58310d9-a9f6-439a-9e8d-f62e7b41a168')]", + "User Access Administrator": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '18d7d88d-d35e-4fb5-a5c3-7773c20a72d9')]" + } + }, + "resources": { + "avmTelemetry": { + "condition": "[parameters('enableTelemetry')]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2024-03-01", + "name": "[format('46d3xbcp.res.nw-privdnszonemx.{0}.{1}', replace('0.1.0', '.', '-'), substring(uniqueString(deployment().name), 0, 4))]", + "properties": { + "mode": "Incremental", + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "resources": [], "outputs": { - "name": { - "type": "string", - "metadata": { - "description": "The name of the deployed table." - }, - "value": "[parameters('name')]" - }, - "resourceId": { - "type": "string", - "metadata": { - "description": "The resource ID of the deployed table." - }, - "value": "[resourceId('Microsoft.Storage/storageAccounts/tableServices/tables', parameters('storageAccountName'), 'default', parameters('name'))]" - }, - "resourceGroupName": { - "type": "string", - "metadata": { - "description": "The resource group of the deployed table." - }, - "value": "[resourceGroup().name]" + "telemetry": { + "type": "String", + "value": "For more information, see https://aka.ms/avm/TelemetryInfo" } } } } + }, + "privateDnsZone": { + "existing": true, + "type": "Microsoft.Network/privateDnsZones", + "apiVersion": "2020-06-01", + "name": "[parameters('privateDnsZoneName')]" + }, + "MX": { + "type": "Microsoft.Network/privateDnsZones/MX", + "apiVersion": "2020-06-01", + "name": "[format('{0}/{1}', parameters('privateDnsZoneName'), parameters('name'))]", + "properties": { + "metadata": "[parameters('metadata')]", + "mxRecords": "[parameters('mxRecords')]", + "ttl": "[parameters('ttl')]" + } + }, + "MX_roleAssignments": { + "copy": { + "name": "MX_roleAssignments", + "count": "[length(coalesce(variables('formattedRoleAssignments'), createArray()))]" + }, + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2022-04-01", + "scope": "[format('Microsoft.Network/privateDnsZones/{0}/MX/{1}', parameters('privateDnsZoneName'), parameters('name'))]", + "name": "[coalesce(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'name'), guid(resourceId('Microsoft.Network/privateDnsZones/MX', parameters('privateDnsZoneName'), parameters('name')), coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].principalId, coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].roleDefinitionId))]", + "properties": { + "roleDefinitionId": "[coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].roleDefinitionId]", + "principalId": "[coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].principalId]", + "description": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'description')]", + "principalType": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'principalType')]", + "condition": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'condition')]", + "conditionVersion": "[if(not(empty(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'condition'))), coalesce(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'conditionVersion'), '2.0'), null())]", + "delegatedManagedIdentityResourceId": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'delegatedManagedIdentityResourceId')]" + }, + "dependsOn": [ + "MX" + ] } }, "outputs": { "name": { "type": "string", "metadata": { - "description": "The name of the deployed table service." + "description": "The name of the deployed MX record." }, - "value": "[variables('name')]" + "value": "[parameters('name')]" }, "resourceId": { "type": "string", "metadata": { - "description": "The resource ID of the deployed table service." + "description": "The resource ID of the deployed MX record." }, - "value": "[resourceId('Microsoft.Storage/storageAccounts/tableServices', parameters('storageAccountName'), variables('name'))]" + "value": "[resourceId('Microsoft.Network/privateDnsZones/MX', parameters('privateDnsZoneName'), parameters('name'))]" }, "resourceGroupName": { "type": "string", "metadata": { - "description": "The resource group of the deployed table service." + "description": "The resource group of the deployed MX record." }, "value": "[resourceGroup().name]" } @@ -23784,27 +20292,43 @@ } }, "dependsOn": [ - "storageAccount" + "privateDnsZone" ] }, - "secretsExport": { - "condition": "[not(equals(parameters('secretsExportConfiguration'), null()))]", + "privateDnsZone_PTR": { + "copy": { + "name": "privateDnsZone_PTR", + "count": "[length(coalesce(parameters('ptr'), createArray()))]" + }, "type": "Microsoft.Resources/deployments", - "apiVersion": "2025-04-01", - "name": "[format('{0}-secrets-kv', uniqueString(deployment().name, parameters('location')))]", - "subscriptionId": "[split(tryGet(parameters('secretsExportConfiguration'), 'keyVaultResourceId'), '/')[2]]", - "resourceGroup": "[split(tryGet(parameters('secretsExportConfiguration'), 'keyVaultResourceId'), '/')[4]]", + "apiVersion": "2022-09-01", + "name": "[format('{0}-PrivateDnsZone-PTRRecord-{1}', uniqueString(deployment().name, parameters('location')), copyIndex())]", "properties": { "expressionEvaluationOptions": { "scope": "inner" }, "mode": "Incremental", "parameters": { - "keyVaultName": { - "value": "[last(split(tryGet(parameters('secretsExportConfiguration'), 'keyVaultResourceId'), '/'))]" + "privateDnsZoneName": { + "value": "[parameters('name')]" }, - "secretsToSet": { - "value": "[union(createArray(), if(contains(parameters('secretsExportConfiguration'), 'accessKey1Name'), createArray(createObject('name', tryGet(parameters('secretsExportConfiguration'), 'accessKey1Name'), 'value', listKeys('storageAccount', '2025-01-01').keys[0].value)), createArray()), if(contains(parameters('secretsExportConfiguration'), 'connectionString1Name'), createArray(createObject('name', tryGet(parameters('secretsExportConfiguration'), 'connectionString1Name'), 'value', format('DefaultEndpointsProtocol=https;AccountName={0};AccountKey={1};EndpointSuffix={2}', parameters('name'), listKeys('storageAccount', '2025-01-01').keys[0].value, environment().suffixes.storage))), createArray()), if(contains(parameters('secretsExportConfiguration'), 'accessKey2Name'), createArray(createObject('name', tryGet(parameters('secretsExportConfiguration'), 'accessKey2Name'), 'value', listKeys('storageAccount', '2025-01-01').keys[1].value)), createArray()), if(contains(parameters('secretsExportConfiguration'), 'connectionString2Name'), createArray(createObject('name', tryGet(parameters('secretsExportConfiguration'), 'connectionString2Name'), 'value', format('DefaultEndpointsProtocol=https;AccountName={0};AccountKey={1};EndpointSuffix={2}', parameters('name'), listKeys('storageAccount', '2025-01-01').keys[1].value, environment().suffixes.storage))), createArray()))]" + "name": { + "value": "[coalesce(parameters('ptr'), createArray())[copyIndex()].name]" + }, + "metadata": { + "value": "[tryGet(coalesce(parameters('ptr'), createArray())[copyIndex()], 'metadata')]" + }, + "ptrRecords": { + "value": "[tryGet(coalesce(parameters('ptr'), createArray())[copyIndex()], 'ptrRecords')]" + }, + "ttl": { + "value": "[coalesce(tryGet(coalesce(parameters('ptr'), createArray())[copyIndex()], 'ttl'), 3600)]" + }, + "roleAssignments": { + "value": "[tryGet(coalesce(parameters('ptr'), createArray())[copyIndex()], 'roleAssignments')]" + }, + "enableTelemetry": { + "value": "[variables('enableReferencedModulesTelemetry')]" } }, "template": { @@ -23814,151 +20338,288 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.39.26.7824", - "templateHash": "13614544361780789643" - } + "version": "0.37.4.10188", + "templateHash": "7627375510490151870" + }, + "name": "Private DNS Zone PTR record", + "description": "This module deploys a Private DNS Zone PTR record." }, "definitions": { - "secretSetOutputType": { + "roleAssignmentType": { "type": "object", "properties": { - "secretResourceId": { + "name": { "type": "string", + "nullable": true, "metadata": { - "description": "The resourceId of the exported secret." + "description": "Optional. The name (as GUID) of the role assignment. If not provided, a GUID will be generated." } }, - "secretUri": { + "roleDefinitionIdOrName": { "type": "string", "metadata": { - "description": "The secret URI of the exported secret." + "description": "Required. The role to assign. You can provide either the display name of the role definition, the role definition GUID, or its fully qualified ID in the following format: '/providers/Microsoft.Authorization/roleDefinitions/c2f4ef07-c644-48eb-af81-4b1b4947fb11'." } }, - "secretUriWithVersion": { + "principalId": { "type": "string", "metadata": { - "description": "The secret URI with version of the exported secret." + "description": "Required. The principal ID of the principal (user/group/identity) to assign the role to." } - } - }, - "metadata": { - "description": "An AVM-aligned type for the output of the secret set via the secrets export feature.", - "__bicep_imported_from!": { - "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.0" - } - } - }, - "secretToSetType": { - "type": "object", - "properties": { - "name": { + }, + "principalType": { "type": "string", + "allowedValues": [ + "Device", + "ForeignGroup", + "Group", + "ServicePrincipal", + "User" + ], + "nullable": true, "metadata": { - "description": "Required. The name of the secret to set." + "description": "Optional. The principal type of the assigned principal ID." } }, - "value": { - "type": "securestring", + "description": { + "type": "string", + "nullable": true, "metadata": { - "description": "Required. The value of the secret to set." + "description": "Optional. The description of the role assignment." + } + }, + "condition": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The conditions on the role assignment. This limits the resources it can be assigned to. e.g.: @Resource[Microsoft.Storage/storageAccounts/blobServices/containers:ContainerName] StringEqualsIgnoreCase \"foo_storage_container\"." + } + }, + "conditionVersion": { + "type": "string", + "allowedValues": [ + "2.0" + ], + "nullable": true, + "metadata": { + "description": "Optional. Version of the condition." + } + }, + "delegatedManagedIdentityResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The Resource Id of the delegated managed identity resource." } } }, "metadata": { - "description": "An AVM-aligned type for the secret to set via the secrets export feature.", + "description": "An AVM-aligned type for a role assignment.", "__bicep_imported_from!": { - "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.0" + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.5.1" } } } }, "parameters": { - "keyVaultName": { + "privateDnsZoneName": { "type": "string", "metadata": { - "description": "Required. The name of the Key Vault to set the ecrets in." + "description": "Conditional. The name of the parent Private DNS zone. Required if the template is used in a standalone deployment." } }, - "secretsToSet": { + "name": { + "type": "string", + "metadata": { + "description": "Required. The name of the PTR record." + } + }, + "metadata": { + "type": "object", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.Network/privateDnsZones/PTR@2024-06-01#properties/properties/properties/metadata" + }, + "description": "Optional. The metadata attached to the record set." + }, + "nullable": true + }, + "ptrRecords": { + "type": "array", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.Network/privateDnsZones/PTR@2024-06-01#properties/properties/properties/ptrRecords" + }, + "description": "Optional. The list of PTR records in the record set." + }, + "nullable": true + }, + "ttl": { + "type": "int", + "defaultValue": 3600, + "metadata": { + "description": "Optional. The TTL (time-to-live) of the records in the record set." + } + }, + "enableTelemetry": { + "type": "bool", + "defaultValue": true, + "metadata": { + "description": "Optional. Enable/Disable usage telemetry for module." + } + }, + "roleAssignments": { "type": "array", "items": { - "$ref": "#/definitions/secretToSetType" + "$ref": "#/definitions/roleAssignmentType" }, + "nullable": true, "metadata": { - "description": "Required. The secrets to set in the Key Vault." + "description": "Optional. Array of role assignments to create." + } + } + }, + "variables": { + "copy": [ + { + "name": "formattedRoleAssignments", + "count": "[length(coalesce(parameters('roleAssignments'), createArray()))]", + "input": "[union(coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')], createObject('roleDefinitionId', coalesce(tryGet(variables('builtInRoleNames'), coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName), if(contains(coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName, '/providers/Microsoft.Authorization/roleDefinitions/'), coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName, subscriptionResourceId('Microsoft.Authorization/roleDefinitions', coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName)))))]" } + ], + "builtInRoleNames": { + "Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'b24988ac-6180-42a0-ab88-20f7382dd24c')]", + "Private DNS Zone Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'b12aa53e-6015-4669-85d0-8515ebb3ae7f')]", + "Network Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '4d97b98b-1d4f-4787-a291-c67834d212e7')]", + "Owner": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '8e3af657-a8ff-443c-a75c-2fe8c4bcb635')]", + "Reader": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'acdd72a7-3385-48ef-bd42-f606fba81ae7')]", + "Role Based Access Control Administrator": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'f58310d9-a9f6-439a-9e8d-f62e7b41a168')]", + "User Access Administrator": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '18d7d88d-d35e-4fb5-a5c3-7773c20a72d9')]" } }, "resources": { - "keyVault": { + "avmTelemetry": { + "condition": "[parameters('enableTelemetry')]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2024-03-01", + "name": "[format('46d3xbcp.res.nw-privdnszoneptr.{0}.{1}', replace('0.1.0', '.', '-'), substring(uniqueString(deployment().name), 0, 4))]", + "properties": { + "mode": "Incremental", + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "resources": [], + "outputs": { + "telemetry": { + "type": "String", + "value": "For more information, see https://aka.ms/avm/TelemetryInfo" + } + } + } + } + }, + "privateDnsZone": { "existing": true, - "type": "Microsoft.KeyVault/vaults", - "apiVersion": "2024-11-01", - "name": "[parameters('keyVaultName')]" + "type": "Microsoft.Network/privateDnsZones", + "apiVersion": "2020-06-01", + "name": "[parameters('privateDnsZoneName')]" }, - "secrets": { + "PTR": { + "type": "Microsoft.Network/privateDnsZones/PTR", + "apiVersion": "2020-06-01", + "name": "[format('{0}/{1}', parameters('privateDnsZoneName'), parameters('name'))]", + "properties": { + "metadata": "[parameters('metadata')]", + "ptrRecords": "[parameters('ptrRecords')]", + "ttl": "[parameters('ttl')]" + } + }, + "PTR_roleAssignments": { "copy": { - "name": "secrets", - "count": "[length(parameters('secretsToSet'))]" + "name": "PTR_roleAssignments", + "count": "[length(coalesce(variables('formattedRoleAssignments'), createArray()))]" }, - "type": "Microsoft.KeyVault/vaults/secrets", - "apiVersion": "2024-11-01", - "name": "[format('{0}/{1}', parameters('keyVaultName'), parameters('secretsToSet')[copyIndex()].name)]", + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2022-04-01", + "scope": "[format('Microsoft.Network/privateDnsZones/{0}/PTR/{1}', parameters('privateDnsZoneName'), parameters('name'))]", + "name": "[coalesce(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'name'), guid(resourceId('Microsoft.Network/privateDnsZones/PTR', parameters('privateDnsZoneName'), parameters('name')), coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].principalId, coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].roleDefinitionId))]", "properties": { - "value": "[parameters('secretsToSet')[copyIndex()].value]" - } + "roleDefinitionId": "[coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].roleDefinitionId]", + "principalId": "[coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].principalId]", + "description": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'description')]", + "principalType": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'principalType')]", + "condition": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'condition')]", + "conditionVersion": "[if(not(empty(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'condition'))), coalesce(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'conditionVersion'), '2.0'), null())]", + "delegatedManagedIdentityResourceId": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'delegatedManagedIdentityResourceId')]" + }, + "dependsOn": [ + "PTR" + ] } }, "outputs": { - "secretsSet": { - "type": "array", - "items": { - "$ref": "#/definitions/secretSetOutputType" + "name": { + "type": "string", + "metadata": { + "description": "The name of the deployed PTR record." }, + "value": "[parameters('name')]" + }, + "resourceId": { + "type": "string", "metadata": { - "description": "The references to the secrets exported to the provided Key Vault." + "description": "The resource ID of the deployed PTR record." }, - "copy": { - "count": "[length(range(0, length(coalesce(parameters('secretsToSet'), createArray()))))]", - "input": { - "secretResourceId": "[resourceId('Microsoft.KeyVault/vaults/secrets', parameters('keyVaultName'), parameters('secretsToSet')[range(0, length(coalesce(parameters('secretsToSet'), createArray())))[copyIndex()]].name)]", - "secretUri": "[reference(format('secrets[{0}]', range(0, length(coalesce(parameters('secretsToSet'), createArray())))[copyIndex()])).secretUri]", - "secretUriWithVersion": "[reference(format('secrets[{0}]', range(0, length(coalesce(parameters('secretsToSet'), createArray())))[copyIndex()])).secretUriWithVersion]" - } - } + "value": "[resourceId('Microsoft.Network/privateDnsZones/PTR', parameters('privateDnsZoneName'), parameters('name'))]" + }, + "resourceGroupName": { + "type": "string", + "metadata": { + "description": "The resource group of the deployed PTR record." + }, + "value": "[resourceGroup().name]" } } } }, "dependsOn": [ - "storageAccount" + "privateDnsZone" ] }, - "storageAccount_objectReplicationPolicies": { + "privateDnsZone_SOA": { "copy": { - "name": "storageAccount_objectReplicationPolicies", - "count": "[length(coalesce(parameters('objectReplicationPolicies'), createArray()))]" + "name": "privateDnsZone_SOA", + "count": "[length(coalesce(parameters('soa'), createArray()))]" }, "type": "Microsoft.Resources/deployments", - "apiVersion": "2025-04-01", - "name": "[format('{0}-Storage-ObjRepPolicy-{1}', uniqueString(deployment().name, parameters('location')), copyIndex())]", + "apiVersion": "2022-09-01", + "name": "[format('{0}-PrivateDnsZone-SOARecord-{1}', uniqueString(deployment().name, parameters('location')), copyIndex())]", "properties": { "expressionEvaluationOptions": { "scope": "inner" }, "mode": "Incremental", "parameters": { - "storageAccountName": { + "privateDnsZoneName": { "value": "[parameters('name')]" }, - "destinationAccountResourceId": { - "value": "[coalesce(parameters('objectReplicationPolicies'), createArray())[copyIndex()].destinationStorageAccountResourceId]" + "name": { + "value": "[coalesce(parameters('soa'), createArray())[copyIndex()].name]" }, - "enableMetrics": { - "value": "[coalesce(tryGet(coalesce(parameters('objectReplicationPolicies'), createArray())[copyIndex()], 'enableMetrics'), false())]" + "metadata": { + "value": "[tryGet(coalesce(parameters('soa'), createArray())[copyIndex()], 'metadata')]" }, - "rules": { - "value": "[tryGet(coalesce(parameters('objectReplicationPolicies'), createArray())[copyIndex()], 'rules')]" + "soaRecord": { + "value": "[tryGet(coalesce(parameters('soa'), createArray())[copyIndex()], 'soaRecord')]" + }, + "ttl": { + "value": "[coalesce(tryGet(coalesce(parameters('soa'), createArray())[copyIndex()], 'ttl'), 3600)]" + }, + "roleAssignments": { + "value": "[tryGet(coalesce(parameters('soa'), createArray())[copyIndex()], 'roleAssignments')]" + }, + "enableTelemetry": { + "value": "[variables('enableReferencedModulesTelemetry')]" } }, "template": { @@ -23968,1874 +20629,1739 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.39.26.7824", - "templateHash": "3528396711847214833" + "version": "0.37.4.10188", + "templateHash": "16709883266329935583" }, - "name": "Storage Account Object Replication Policy", - "description": "This module deploys a Storage Account Object Replication Policy for both the source account and destination account." + "name": "Private DNS Zone SOA record", + "description": "This module deploys a Private DNS Zone SOA record." }, "definitions": { - "objectReplicationPolicyRuleType": { + "roleAssignmentType": { "type": "object", "properties": { - "ruleId": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The name (as GUID) of the role assignment. If not provided, a GUID will be generated." + } + }, + "roleDefinitionIdOrName": { + "type": "string", + "metadata": { + "description": "Required. The role to assign. You can provide either the display name of the role definition, the role definition GUID, or its fully qualified ID in the following format: '/providers/Microsoft.Authorization/roleDefinitions/c2f4ef07-c644-48eb-af81-4b1b4947fb11'." + } + }, + "principalId": { + "type": "string", + "metadata": { + "description": "Required. The principal ID of the principal (user/group/identity) to assign the role to." + } + }, + "principalType": { "type": "string", + "allowedValues": [ + "Device", + "ForeignGroup", + "Group", + "ServicePrincipal", + "User" + ], "nullable": true, "metadata": { - "description": "Optional. The ID of the rule. Auto-generated on destination account. Required for source account." + "description": "Optional. The principal type of the assigned principal ID." } }, - "containerName": { + "description": { "type": "string", + "nullable": true, "metadata": { - "description": "Required. The name of the source container." + "description": "Optional. The description of the role assignment." } }, - "destinationContainerName": { + "condition": { "type": "string", "nullable": true, "metadata": { - "description": "Optional. The name of the destination container. If not provided, the same name as the source container will be used." + "description": "Optional. The conditions on the role assignment. This limits the resources it can be assigned to. e.g.: @Resource[Microsoft.Storage/storageAccounts/blobServices/containers:ContainerName] StringEqualsIgnoreCase \"foo_storage_container\"." } }, - "filters": { - "type": "object", - "properties": { - "prefixMatch": { - "type": "array", - "items": { - "type": "string" - }, - "nullable": true, - "metadata": { - "description": "Optional. The prefix to match for the replication policy rule." - } - }, - "minCreationTime": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The minimum creation time to match for the replication policy rule." - } - } - }, + "conditionVersion": { + "type": "string", + "allowedValues": [ + "2.0" + ], "nullable": true, "metadata": { - "description": "Optional. The filters for the object replication policy rule." + "description": "Optional. Version of the condition." + } + }, + "delegatedManagedIdentityResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The Resource Id of the delegated managed identity resource." } } }, "metadata": { - "description": "The type of an object replication policy rule.", + "description": "An AVM-aligned type for a role assignment.", "__bicep_imported_from!": { - "sourceTemplate": "policy/main.bicep" + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.5.1" } } } }, "parameters": { - "name": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. Name of the policy." - } - }, - "storageAccountName": { + "privateDnsZoneName": { "type": "string", - "maxLength": 24, "metadata": { - "description": "Required. The name of the parent Storage Account." + "description": "Conditional. The name of the parent Private DNS zone. Required if the template is used in a standalone deployment." } }, - "destinationAccountResourceId": { + "name": { "type": "string", "metadata": { - "description": "Required. Resource ID of the destination storage account for replication." - } - }, - "enableMetrics": { - "type": "bool", - "nullable": true, - "metadata": { - "description": "Optional. Whether metrics are enabled for the object replication policy." + "description": "Required. The name of the SOA record." } }, - "rules": { - "type": "array", - "items": { - "$ref": "#/definitions/objectReplicationPolicyRuleType" - }, + "metadata": { + "type": "object", "metadata": { - "description": "Required. Rules for the object replication policy." - } - } - }, - "variables": { - "destAccountResourceIdParts": "[split(parameters('destinationAccountResourceId'), '/')]", - "destAccountName": "[if(not(empty(variables('destAccountResourceIdParts'))), last(variables('destAccountResourceIdParts')), parameters('destinationAccountResourceId'))]", - "destAccountSubscription": "[if(greater(length(variables('destAccountResourceIdParts')), 2), variables('destAccountResourceIdParts')[2], subscription().subscriptionId)]", - "destAccountResourceGroupName": "[if(greater(length(variables('destAccountResourceIdParts')), 4), variables('destAccountResourceIdParts')[4], resourceGroup().name)]" - }, - "resources": { - "storageAccount": { - "existing": true, - "type": "Microsoft.Storage/storageAccounts", - "apiVersion": "2025-01-01", - "name": "[parameters('storageAccountName')]" - }, - "destinationPolicy": { - "type": "Microsoft.Resources/deployments", - "apiVersion": "2025-04-01", - "name": "[take(format('{0}-ObjRep-Policy-dest-{1}', deployment().name, variables('destAccountName')), 64)]", - "subscriptionId": "[variables('destAccountSubscription')]", - "resourceGroup": "[variables('destAccountResourceGroupName')]", - "properties": { - "expressionEvaluationOptions": { - "scope": "inner" - }, - "mode": "Incremental", - "parameters": { - "name": { - "value": "[coalesce(parameters('name'), 'default')]" - }, - "storageAccountName": { - "value": "[variables('destAccountName')]" - }, - "sourceStorageAccountResourceId": { - "value": "[resourceId('Microsoft.Storage/storageAccounts', parameters('storageAccountName'))]" - }, - "destinationAccountResourceId": { - "value": "[parameters('destinationAccountResourceId')]" - }, - "enableMetrics": { - "value": "[parameters('enableMetrics')]" - }, - "rules": { - "value": "[parameters('rules')]" - } + "__bicep_resource_derived_type!": { + "source": "Microsoft.Network/privateDnsZones/SOA@2024-06-01#properties/properties/properties/metadata" }, - "template": { - "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", - "languageVersion": "2.0", - "contentVersion": "1.0.0.0", - "metadata": { - "_generator": { - "name": "bicep", - "version": "0.39.26.7824", - "templateHash": "4325417308313318683" - }, - "name": "Storage Account Object Replication Policy", - "description": "This module deploys a Storage Account Object Replication Policy for a provided storage account." - }, - "definitions": { - "objectReplicationPolicyRuleType": { - "type": "object", - "properties": { - "ruleId": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The ID of the rule. Auto-generated on destination account. Required for source account." - } - }, - "containerName": { - "type": "string", - "metadata": { - "description": "Required. The name of the source container." - } - }, - "destinationContainerName": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The name of the destination container. If not provided, the same name as the source container will be used." - } - }, - "filters": { - "type": "object", - "properties": { - "prefixMatch": { - "type": "array", - "items": { - "type": "string" - }, - "nullable": true, - "metadata": { - "description": "Optional. The prefix to match for the replication policy rule." - } - }, - "minCreationTime": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The minimum creation time to match for the replication policy rule." - } - } - }, - "nullable": true, - "metadata": { - "description": "Optional. The filters for the object replication policy rule." - } - } - }, - "metadata": { - "__bicep_export!": true, - "description": "The type of an object replication policy rule." - } - } - }, - "parameters": { - "name": { - "type": "string", - "metadata": { - "description": "Required. Name of the policy." - } - }, - "storageAccountName": { - "type": "string", - "maxLength": 24, - "metadata": { - "description": "Required. The name of the Storage Account on which to create the policy." - } - }, - "sourceStorageAccountResourceId": { - "type": "string", - "metadata": { - "description": "Required. Resource ID of the source storage account for replication." - } - }, - "destinationAccountResourceId": { - "type": "string", - "metadata": { - "description": "Required. Resource ID of the destination storage account for replication." - } - }, - "enableMetrics": { - "type": "bool", - "nullable": true, - "metadata": { - "description": "Optional. Whether metrics are enabled for the object replication policy." - } - }, - "rules": { - "type": "array", - "items": { - "$ref": "#/definitions/objectReplicationPolicyRuleType" - }, - "metadata": { - "description": "Required. Rules for the object replication policy." - } - } - }, - "resources": { - "storageAccount": { - "existing": true, - "type": "Microsoft.Storage/storageAccounts", - "apiVersion": "2025-01-01", - "name": "[parameters('storageAccountName')]" - }, - "objectReplicationPolicy": { - "type": "Microsoft.Storage/storageAccounts/objectReplicationPolicies", - "apiVersion": "2025-01-01", - "name": "[format('{0}/{1}', parameters('storageAccountName'), parameters('name'))]", - "properties": { - "copy": [ - { - "name": "rules", - "count": "[length(parameters('rules'))]", - "input": { - "ruleId": "[tryGet(parameters('rules')[copyIndex('rules')], 'ruleId')]", - "sourceContainer": "[parameters('rules')[copyIndex('rules')].containerName]", - "destinationContainer": "[coalesce(tryGet(parameters('rules')[copyIndex('rules')], 'destinationContainerName'), parameters('rules')[copyIndex('rules')].containerName)]", - "filters": "[if(not(equals(tryGet(parameters('rules')[copyIndex('rules')], 'filters'), null())), createObject('prefixMatch', tryGet(tryGet(parameters('rules')[copyIndex('rules')], 'filters'), 'prefixMatch'), 'minCreationTime', tryGet(tryGet(parameters('rules')[copyIndex('rules')], 'filters'), 'minCreationTime')), null())]" - } - } - ], - "destinationAccount": "[parameters('destinationAccountResourceId')]", - "metrics": { - "enabled": "[coalesce(parameters('enableMetrics'), false())]" - }, - "sourceAccount": "[parameters('sourceStorageAccountResourceId')]" - } - } - }, - "outputs": { - "resourceGroupName": { - "type": "string", - "metadata": { - "description": "Resource group name of the provisioned resources." - }, - "value": "[resourceGroup().name]" - }, - "objectReplicationPolicyId": { - "type": "string", - "metadata": { - "description": "Resource ID of the created Object Replication Policy." - }, - "value": "[resourceId('Microsoft.Storage/storageAccounts/objectReplicationPolicies', parameters('storageAccountName'), parameters('name'))]" - }, - "policyId": { - "type": "string", - "metadata": { - "description": "Policy ID of the created Object Replication Policy." - }, - "value": "[reference('objectReplicationPolicy').policyId]" - }, - "rules": { - "type": "array", - "metadata": { - "__bicep_resource_derived_type!": { - "source": "Microsoft.Storage/storageAccounts/objectReplicationPolicies@2025-01-01#properties/properties/properties/rules", - "output": true - }, - "description": "Rules created Object Replication Policy." - }, - "value": "[reference('objectReplicationPolicy').rules]" - } - } - } - } + "description": "Optional. The metadata attached to the record set." + }, + "nullable": true }, - "sourcePolicy": { - "type": "Microsoft.Resources/deployments", - "apiVersion": "2025-04-01", - "name": "[take(format('{0}-ObjRep-Policy-source-{1}', deployment().name, parameters('storageAccountName')), 64)]", - "properties": { - "expressionEvaluationOptions": { - "scope": "inner" - }, - "mode": "Incremental", - "parameters": { - "name": { - "value": "[reference('destinationPolicy').outputs.policyId.value]" - }, - "storageAccountName": { - "value": "[parameters('storageAccountName')]" - }, - "sourceStorageAccountResourceId": { - "value": "[resourceId('Microsoft.Storage/storageAccounts', parameters('storageAccountName'))]" - }, - "destinationAccountResourceId": { - "value": "[parameters('destinationAccountResourceId')]" - }, - "enableMetrics": { - "value": "[parameters('enableMetrics')]" - }, - "rules": { - "copy": [ - { - "name": "value", - "count": "[length(parameters('rules'))]", - "input": "[union(parameters('rules')[copyIndex('value')], createObject('ruleId', reference('destinationPolicy').outputs.rules.value[copyIndex('value')].ruleId))]" - } - ] - } - }, - "template": { - "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", - "languageVersion": "2.0", - "contentVersion": "1.0.0.0", - "metadata": { - "_generator": { - "name": "bicep", - "version": "0.39.26.7824", - "templateHash": "4325417308313318683" - }, - "name": "Storage Account Object Replication Policy", - "description": "This module deploys a Storage Account Object Replication Policy for a provided storage account." - }, - "definitions": { - "objectReplicationPolicyRuleType": { - "type": "object", - "properties": { - "ruleId": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The ID of the rule. Auto-generated on destination account. Required for source account." - } - }, - "containerName": { - "type": "string", - "metadata": { - "description": "Required. The name of the source container." - } - }, - "destinationContainerName": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The name of the destination container. If not provided, the same name as the source container will be used." - } - }, - "filters": { - "type": "object", - "properties": { - "prefixMatch": { - "type": "array", - "items": { - "type": "string" - }, - "nullable": true, - "metadata": { - "description": "Optional. The prefix to match for the replication policy rule." - } - }, - "minCreationTime": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The minimum creation time to match for the replication policy rule." - } - } - }, - "nullable": true, - "metadata": { - "description": "Optional. The filters for the object replication policy rule." - } - } - }, - "metadata": { - "__bicep_export!": true, - "description": "The type of an object replication policy rule." - } - } - }, - "parameters": { - "name": { - "type": "string", - "metadata": { - "description": "Required. Name of the policy." - } - }, - "storageAccountName": { - "type": "string", - "maxLength": 24, - "metadata": { - "description": "Required. The name of the Storage Account on which to create the policy." - } - }, - "sourceStorageAccountResourceId": { - "type": "string", - "metadata": { - "description": "Required. Resource ID of the source storage account for replication." - } - }, - "destinationAccountResourceId": { - "type": "string", - "metadata": { - "description": "Required. Resource ID of the destination storage account for replication." - } - }, - "enableMetrics": { - "type": "bool", - "nullable": true, - "metadata": { - "description": "Optional. Whether metrics are enabled for the object replication policy." - } - }, - "rules": { - "type": "array", - "items": { - "$ref": "#/definitions/objectReplicationPolicyRuleType" - }, - "metadata": { - "description": "Required. Rules for the object replication policy." - } - } - }, - "resources": { - "storageAccount": { - "existing": true, - "type": "Microsoft.Storage/storageAccounts", - "apiVersion": "2025-01-01", - "name": "[parameters('storageAccountName')]" - }, - "objectReplicationPolicy": { - "type": "Microsoft.Storage/storageAccounts/objectReplicationPolicies", - "apiVersion": "2025-01-01", - "name": "[format('{0}/{1}', parameters('storageAccountName'), parameters('name'))]", - "properties": { - "copy": [ - { - "name": "rules", - "count": "[length(parameters('rules'))]", - "input": { - "ruleId": "[tryGet(parameters('rules')[copyIndex('rules')], 'ruleId')]", - "sourceContainer": "[parameters('rules')[copyIndex('rules')].containerName]", - "destinationContainer": "[coalesce(tryGet(parameters('rules')[copyIndex('rules')], 'destinationContainerName'), parameters('rules')[copyIndex('rules')].containerName)]", - "filters": "[if(not(equals(tryGet(parameters('rules')[copyIndex('rules')], 'filters'), null())), createObject('prefixMatch', tryGet(tryGet(parameters('rules')[copyIndex('rules')], 'filters'), 'prefixMatch'), 'minCreationTime', tryGet(tryGet(parameters('rules')[copyIndex('rules')], 'filters'), 'minCreationTime')), null())]" - } - } - ], - "destinationAccount": "[parameters('destinationAccountResourceId')]", - "metrics": { - "enabled": "[coalesce(parameters('enableMetrics'), false())]" - }, - "sourceAccount": "[parameters('sourceStorageAccountResourceId')]" - } - } - }, + "soaRecord": { + "type": "object", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.Network/privateDnsZones/SOA@2024-06-01#properties/properties/properties/soaRecord" + }, + "description": "Optional. A SOA record." + }, + "nullable": true + }, + "ttl": { + "type": "int", + "defaultValue": 3600, + "metadata": { + "description": "Optional. The TTL (time-to-live) of the records in the record set." + } + }, + "enableTelemetry": { + "type": "bool", + "defaultValue": true, + "metadata": { + "description": "Optional. Enable/Disable usage telemetry for module." + } + }, + "roleAssignments": { + "type": "array", + "items": { + "$ref": "#/definitions/roleAssignmentType" + }, + "nullable": true, + "metadata": { + "description": "Optional. Array of role assignments to create." + } + } + }, + "variables": { + "copy": [ + { + "name": "formattedRoleAssignments", + "count": "[length(coalesce(parameters('roleAssignments'), createArray()))]", + "input": "[union(coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')], createObject('roleDefinitionId', coalesce(tryGet(variables('builtInRoleNames'), coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName), if(contains(coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName, '/providers/Microsoft.Authorization/roleDefinitions/'), coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName, subscriptionResourceId('Microsoft.Authorization/roleDefinitions', coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName)))))]" + } + ], + "builtInRoleNames": { + "Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'b24988ac-6180-42a0-ab88-20f7382dd24c')]", + "Private DNS Zone Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'b12aa53e-6015-4669-85d0-8515ebb3ae7f')]", + "Network Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '4d97b98b-1d4f-4787-a291-c67834d212e7')]", + "Owner": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '8e3af657-a8ff-443c-a75c-2fe8c4bcb635')]", + "Reader": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'acdd72a7-3385-48ef-bd42-f606fba81ae7')]", + "Role Based Access Control Administrator": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'f58310d9-a9f6-439a-9e8d-f62e7b41a168')]", + "User Access Administrator": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '18d7d88d-d35e-4fb5-a5c3-7773c20a72d9')]" + } + }, + "resources": { + "avmTelemetry": { + "condition": "[parameters('enableTelemetry')]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2024-03-01", + "name": "[format('46d3xbcp.res.nw-privdnszonesoa.{0}.{1}', replace('0.1.0', '.', '-'), substring(uniqueString(deployment().name), 0, 4))]", + "properties": { + "mode": "Incremental", + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "resources": [], "outputs": { - "resourceGroupName": { - "type": "string", - "metadata": { - "description": "Resource group name of the provisioned resources." - }, - "value": "[resourceGroup().name]" - }, - "objectReplicationPolicyId": { - "type": "string", - "metadata": { - "description": "Resource ID of the created Object Replication Policy." - }, - "value": "[resourceId('Microsoft.Storage/storageAccounts/objectReplicationPolicies', parameters('storageAccountName'), parameters('name'))]" - }, - "policyId": { - "type": "string", - "metadata": { - "description": "Policy ID of the created Object Replication Policy." - }, - "value": "[reference('objectReplicationPolicy').policyId]" - }, - "rules": { - "type": "array", - "metadata": { - "__bicep_resource_derived_type!": { - "source": "Microsoft.Storage/storageAccounts/objectReplicationPolicies@2025-01-01#properties/properties/properties/rules", - "output": true - }, - "description": "Rules created Object Replication Policy." - }, - "value": "[reference('objectReplicationPolicy').rules]" + "telemetry": { + "type": "String", + "value": "For more information, see https://aka.ms/avm/TelemetryInfo" } } } + } + }, + "privateDnsZone": { + "existing": true, + "type": "Microsoft.Network/privateDnsZones", + "apiVersion": "2020-06-01", + "name": "[parameters('privateDnsZoneName')]" + }, + "SOA": { + "type": "Microsoft.Network/privateDnsZones/SOA", + "apiVersion": "2020-06-01", + "name": "[format('{0}/{1}', parameters('privateDnsZoneName'), parameters('name'))]", + "properties": { + "metadata": "[parameters('metadata')]", + "soaRecord": "[parameters('soaRecord')]", + "ttl": "[parameters('ttl')]" + } + }, + "SOA_roleAssignments": { + "copy": { + "name": "SOA_roleAssignments", + "count": "[length(coalesce(variables('formattedRoleAssignments'), createArray()))]" + }, + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2022-04-01", + "scope": "[format('Microsoft.Network/privateDnsZones/{0}/SOA/{1}', parameters('privateDnsZoneName'), parameters('name'))]", + "name": "[coalesce(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'name'), guid(resourceId('Microsoft.Network/privateDnsZones/SOA', parameters('privateDnsZoneName'), parameters('name')), coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].principalId, coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].roleDefinitionId))]", + "properties": { + "roleDefinitionId": "[coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].roleDefinitionId]", + "principalId": "[coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].principalId]", + "description": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'description')]", + "principalType": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'principalType')]", + "condition": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'condition')]", + "conditionVersion": "[if(not(empty(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'condition'))), coalesce(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'conditionVersion'), '2.0'), null())]", + "delegatedManagedIdentityResourceId": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'delegatedManagedIdentityResourceId')]" }, "dependsOn": [ - "destinationPolicy" + "SOA" ] } }, "outputs": { - "resourceGroupName": { + "name": { "type": "string", "metadata": { - "description": "Resource group name of the provisioned resources." + "description": "The name of the deployed SOA record." }, - "value": "[resourceGroup().name]" + "value": "[parameters('name')]" }, - "objectReplicationPolicyId": { + "resourceId": { "type": "string", "metadata": { - "description": "Resource ID of the created Object Replication Policy in the source account." + "description": "The resource ID of the deployed SOA record." }, - "value": "[reference('sourcePolicy').outputs.objectReplicationPolicyId.value]" + "value": "[resourceId('Microsoft.Network/privateDnsZones/SOA', parameters('privateDnsZoneName'), parameters('name'))]" }, - "policyId": { + "resourceGroupName": { "type": "string", "metadata": { - "description": "Policy ID of the created Object Replication Policy in the source account." + "description": "The resource group of the deployed SOA record." }, - "value": "[reference('sourcePolicy').outputs.policyId.value]" + "value": "[resourceGroup().name]" } } } }, "dependsOn": [ - "storageAccount", - "storageAccount_blobServices" + "privateDnsZone" ] - } - }, - "outputs": { - "resourceId": { - "type": "string", - "metadata": { - "description": "The resource ID of the deployed storage account." - }, - "value": "[resourceId('Microsoft.Storage/storageAccounts', parameters('name'))]" - }, - "name": { - "type": "string", - "metadata": { - "description": "The name of the deployed storage account." - }, - "value": "[parameters('name')]" - }, - "resourceGroupName": { - "type": "string", - "metadata": { - "description": "The resource group of the deployed storage account." - }, - "value": "[resourceGroup().name]" - }, - "primaryBlobEndpoint": { - "type": "string", - "metadata": { - "description": "The primary blob endpoint reference if blob services are deployed." - }, - "value": "[if(and(not(empty(parameters('blobServices'))), contains(parameters('blobServices'), 'containers')), reference(format('Microsoft.Storage/storageAccounts/{0}', parameters('name')), '2019-04-01').primaryEndpoints.blob, '')]" - }, - "systemAssignedMIPrincipalId": { - "type": "string", - "nullable": true, - "metadata": { - "description": "The principal ID of the system assigned identity." - }, - "value": "[tryGet(tryGet(reference('storageAccount', '2025-01-01', 'full'), 'identity'), 'principalId')]" - }, - "location": { - "type": "string", - "metadata": { - "description": "The location the resource was deployed into." - }, - "value": "[reference('storageAccount', '2025-01-01', 'full').location]" - }, - "serviceEndpoints": { - "type": "object", - "metadata": { - "description": "All service endpoints of the deployed storage account, Note Standard_LRS and Standard_ZRS accounts only have a blob service endpoint." - }, - "value": "[reference('storageAccount').primaryEndpoints]" }, - "privateEndpoints": { - "type": "array", - "items": { - "$ref": "#/definitions/privateEndpointOutputType" - }, - "metadata": { - "description": "The private endpoints of the Storage Account." - }, + "privateDnsZone_SRV": { "copy": { - "count": "[length(coalesce(parameters('privateEndpoints'), createArray()))]", - "input": { - "name": "[reference(format('storageAccount_privateEndpoints[{0}]', copyIndex())).outputs.name.value]", - "resourceId": "[reference(format('storageAccount_privateEndpoints[{0}]', copyIndex())).outputs.resourceId.value]", - "groupId": "[tryGet(tryGet(reference(format('storageAccount_privateEndpoints[{0}]', copyIndex())).outputs, 'groupId'), 'value')]", - "customDnsConfigs": "[reference(format('storageAccount_privateEndpoints[{0}]', copyIndex())).outputs.customDnsConfigs.value]", - "networkInterfaceResourceIds": "[reference(format('storageAccount_privateEndpoints[{0}]', copyIndex())).outputs.networkInterfaceResourceIds.value]" - } - } - }, - "exportedSecrets": { - "$ref": "#/definitions/secretsOutputType", - "metadata": { - "description": "A hashtable of references to the secrets exported to the provided Key Vault. The key of each reference is each secret's name." - }, - "value": "[if(not(equals(parameters('secretsExportConfiguration'), null())), toObject(reference('secretsExport').outputs.secretsSet.value, lambda('secret', last(split(lambdaVariables('secret').secretResourceId, '/'))), lambda('secret', lambdaVariables('secret'))), createObject())]" - }, - "primaryAccessKey": { - "type": "securestring", - "metadata": { - "description": "The primary access key of the storage account." - }, - "value": "[listKeys('storageAccount', '2025-01-01').keys[0].value]" - }, - "secondaryAccessKey": { - "type": "securestring", - "metadata": { - "description": "The secondary access key of the storage account." - }, - "value": "[listKeys('storageAccount', '2025-01-01').keys[1].value]" - }, - "primaryConnectionString": { - "type": "securestring", - "metadata": { - "description": "The primary connection string of the storage account." - }, - "value": "[format('DefaultEndpointsProtocol=https;AccountName={0};AccountKey={1};EndpointSuffix={2}', parameters('name'), listKeys('storageAccount', '2025-01-01').keys[0].value, environment().suffixes.storage)]" - }, - "secondaryConnectionString": { - "type": "securestring", - "metadata": { - "description": "The secondary connection string of the storage account." - }, - "value": "[format('DefaultEndpointsProtocol=https;AccountName={0};AccountKey={1};EndpointSuffix={2}', parameters('name'), listKeys('storageAccount', '2025-01-01').keys[1].value, environment().suffixes.storage)]" - } - } - } - }, - "dependsOn": [ - "[format('avmPrivateDnsZones[{0}]', variables('dnsZoneIndex').storageBlob)]", - "logAnalyticsWorkspace", - "userAssignedIdentity", - "virtualNetwork" - ] - }, - "cosmosDB": { - "type": "Microsoft.Resources/deployments", - "apiVersion": "2025-04-01", - "name": "[take(format('avm.res.document-db.database-account.{0}', variables('cosmosDBResourceName')), 64)]", - "properties": { - "expressionEvaluationOptions": { - "scope": "inner" - }, - "mode": "Incremental", - "parameters": { - "name": { - "value": "[format('cosmos-{0}', variables('solutionSuffix'))]" - }, - "location": { - "value": "[parameters('secondaryLocation')]" - }, - "tags": { - "value": "[parameters('tags')]" - }, - "enableTelemetry": { - "value": "[parameters('enableTelemetry')]" - }, - "sqlDatabases": { - "value": [ - { - "name": "[variables('cosmosDBDatabaseName')]", - "containers": [ - { - "name": "[variables('cosmosDBConversationsContainer')]", - "paths": [ - "/userId" - ] - }, - { - "name": "[variables('cosmosDBProductsContainer')]", - "paths": [ - "/category" - ] - } - ] - } - ] - }, - "sqlRoleDefinitions": { - "value": [ - { - "roleName": "contentgen-data-contributor", - "dataActions": [ - "Microsoft.DocumentDB/databaseAccounts/readMetadata", - "Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers/*", - "Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers/items/*" - ] - } - ] - }, - "sqlRoleAssignments": { - "value": [ - { - "principalId": "[reference('userAssignedIdentity').outputs.principalId.value]", - "roleDefinitionId": "00000000-0000-0000-0000-000000000002" + "name": "privateDnsZone_SRV", + "count": "[length(coalesce(parameters('srv'), createArray()))]" }, - { - "principalId": "[deployer().objectId]", - "roleDefinitionId": "00000000-0000-0000-0000-000000000002" - } - ] - }, - "diagnosticSettings": "[if(parameters('enableMonitoring'), createObject('value', createArray(createObject('workspaceResourceId', if(variables('useExistingLogAnalytics'), parameters('existingLogAnalyticsWorkspaceId'), if(parameters('enableMonitoring'), reference('logAnalyticsWorkspace').outputs.resourceId.value, ''))))), createObject('value', null()))]", - "networkRestrictions": { - "value": { - "networkAclBypass": "AzureServices", - "publicNetworkAccess": "[if(parameters('enablePrivateNetworking'), 'Disabled', 'Enabled')]" - } - }, - "zoneRedundant": { - "value": "[parameters('enableRedundancy')]" - }, - "capabilitiesToAdd": "[if(parameters('enableRedundancy'), createObject('value', null()), createObject('value', createArray('EnableServerless')))]", - "enableAutomaticFailover": { - "value": "[parameters('enableRedundancy')]" - }, - "failoverLocations": "[if(parameters('enableRedundancy'), createObject('value', createArray(createObject('failoverPriority', 0, 'isZoneRedundant', true(), 'locationName', parameters('secondaryLocation')), createObject('failoverPriority', 1, 'isZoneRedundant', true(), 'locationName', variables('cosmosDbHaLocation')))), createObject('value', createArray(createObject('locationName', parameters('secondaryLocation'), 'failoverPriority', 0, 'isZoneRedundant', false()))))]", - "privateEndpoints": "[if(parameters('enablePrivateNetworking'), createObject('value', createArray(createObject('service', 'Sql', 'subnetResourceId', reference('virtualNetwork').outputs.pepsSubnetResourceId.value, 'privateDnsZoneGroup', createObject('privateDnsZoneGroupConfigs', createArray(createObject('privateDnsZoneResourceId', reference(format('avmPrivateDnsZones[{0}]', variables('dnsZoneIndex').cosmosDB)).outputs.resourceId.value)))))), createObject('value', null()))]" - }, - "template": { - "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", - "languageVersion": "2.0", - "contentVersion": "1.0.0.0", - "metadata": { - "_generator": { - "name": "bicep", - "version": "0.38.33.27573", - "templateHash": "11889744396543212232" - }, - "name": "Azure Cosmos DB account", - "description": "This module deploys an Azure Cosmos DB account. The API used for the account is determined by the child resources that are deployed." - }, - "definitions": { - "privateEndpointOutputType": { - "type": "object", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2022-09-01", + "name": "[format('{0}-PrivateDnsZone-SRVRecord-{1}', uniqueString(deployment().name, parameters('location')), copyIndex())]", "properties": { - "name": { - "type": "string", - "metadata": { - "description": "The name of the private endpoint." - } + "expressionEvaluationOptions": { + "scope": "inner" }, - "resourceId": { - "type": "string", + "mode": "Incremental", + "parameters": { + "privateDnsZoneName": { + "value": "[parameters('name')]" + }, + "name": { + "value": "[coalesce(parameters('srv'), createArray())[copyIndex()].name]" + }, "metadata": { - "description": "The resource ID of the private endpoint." + "value": "[tryGet(coalesce(parameters('srv'), createArray())[copyIndex()], 'metadata')]" + }, + "srvRecords": { + "value": "[tryGet(coalesce(parameters('srv'), createArray())[copyIndex()], 'srvRecords')]" + }, + "ttl": { + "value": "[coalesce(tryGet(coalesce(parameters('srv'), createArray())[copyIndex()], 'ttl'), 3600)]" + }, + "roleAssignments": { + "value": "[tryGet(coalesce(parameters('srv'), createArray())[copyIndex()], 'roleAssignments')]" + }, + "enableTelemetry": { + "value": "[variables('enableReferencedModulesTelemetry')]" } }, - "groupId": { - "type": "string", - "nullable": true, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "languageVersion": "2.0", + "contentVersion": "1.0.0.0", "metadata": { - "description": "The group ID for the private endpoint group." - } - }, - "customDnsConfigs": { - "type": "array", - "items": { - "type": "object", - "properties": { - "fqdn": { - "type": "string", - "nullable": true, - "metadata": { - "description": "fully-qualified domain name (FQDN) that resolves to private endpoint IP address." + "_generator": { + "name": "bicep", + "version": "0.37.4.10188", + "templateHash": "8123422724272920495" + }, + "name": "Private DNS Zone SRV record", + "description": "This module deploys a Private DNS Zone SRV record." + }, + "definitions": { + "roleAssignmentType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The name (as GUID) of the role assignment. If not provided, a GUID will be generated." + } + }, + "roleDefinitionIdOrName": { + "type": "string", + "metadata": { + "description": "Required. The role to assign. You can provide either the display name of the role definition, the role definition GUID, or its fully qualified ID in the following format: '/providers/Microsoft.Authorization/roleDefinitions/c2f4ef07-c644-48eb-af81-4b1b4947fb11'." + } + }, + "principalId": { + "type": "string", + "metadata": { + "description": "Required. The principal ID of the principal (user/group/identity) to assign the role to." + } + }, + "principalType": { + "type": "string", + "allowedValues": [ + "Device", + "ForeignGroup", + "Group", + "ServicePrincipal", + "User" + ], + "nullable": true, + "metadata": { + "description": "Optional. The principal type of the assigned principal ID." + } + }, + "description": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The description of the role assignment." + } + }, + "condition": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The conditions on the role assignment. This limits the resources it can be assigned to. e.g.: @Resource[Microsoft.Storage/storageAccounts/blobServices/containers:ContainerName] StringEqualsIgnoreCase \"foo_storage_container\"." + } + }, + "conditionVersion": { + "type": "string", + "allowedValues": [ + "2.0" + ], + "nullable": true, + "metadata": { + "description": "Optional. Version of the condition." + } + }, + "delegatedManagedIdentityResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The Resource Id of the delegated managed identity resource." + } + } + }, + "metadata": { + "description": "An AVM-aligned type for a role assignment.", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.5.1" } + } + } + }, + "parameters": { + "privateDnsZoneName": { + "type": "string", + "metadata": { + "description": "Conditional. The name of the parent Private DNS zone. Required if the template is used in a standalone deployment." + } + }, + "name": { + "type": "string", + "metadata": { + "description": "Required. The name of the SRV record." + } + }, + "metadata": { + "type": "object", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.Network/privateDnsZones/SRV@2024-06-01#properties/properties/properties/metadata" + }, + "description": "Optional. The metadata attached to the record set." }, - "ipAddresses": { - "type": "array", - "items": { - "type": "string" + "nullable": true + }, + "srvRecords": { + "type": "array", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.Network/privateDnsZones/SRV@2024-06-01#properties/properties/properties/srvRecords" }, - "metadata": { - "description": "A list of private IP addresses for the private endpoint." - } + "description": "Optional. The list of SRV records in the record set." + }, + "nullable": true + }, + "ttl": { + "type": "int", + "defaultValue": 3600, + "metadata": { + "description": "Optional. The TTL (time-to-live) of the records in the record set." + } + }, + "enableTelemetry": { + "type": "bool", + "defaultValue": true, + "metadata": { + "description": "Optional. Enable/Disable usage telemetry for module." + } + }, + "roleAssignments": { + "type": "array", + "items": { + "$ref": "#/definitions/roleAssignmentType" + }, + "nullable": true, + "metadata": { + "description": "Optional. Array of role assignments to create." } } }, - "metadata": { - "description": "The custom DNS configurations of the private endpoint." - } - }, - "networkInterfaceResourceIds": { - "type": "array", - "items": { - "type": "string" + "variables": { + "copy": [ + { + "name": "formattedRoleAssignments", + "count": "[length(coalesce(parameters('roleAssignments'), createArray()))]", + "input": "[union(coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')], createObject('roleDefinitionId', coalesce(tryGet(variables('builtInRoleNames'), coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName), if(contains(coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName, '/providers/Microsoft.Authorization/roleDefinitions/'), coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName, subscriptionResourceId('Microsoft.Authorization/roleDefinitions', coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName)))))]" + } + ], + "builtInRoleNames": { + "Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'b24988ac-6180-42a0-ab88-20f7382dd24c')]", + "Private DNS Zone Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'b12aa53e-6015-4669-85d0-8515ebb3ae7f')]", + "Network Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '4d97b98b-1d4f-4787-a291-c67834d212e7')]", + "Owner": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '8e3af657-a8ff-443c-a75c-2fe8c4bcb635')]", + "Reader": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'acdd72a7-3385-48ef-bd42-f606fba81ae7')]", + "Role Based Access Control Administrator": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'f58310d9-a9f6-439a-9e8d-f62e7b41a168')]", + "User Access Administrator": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '18d7d88d-d35e-4fb5-a5c3-7773c20a72d9')]" + } }, - "metadata": { - "description": "The IDs of the network interfaces associated with the private endpoint." - } - } - }, - "metadata": { - "__bicep_export!": true, - "description": "The type for the private endpoint output." - } - }, - "failoverLocationType": { - "type": "object", - "properties": { - "failoverPriority": { - "type": "int", - "metadata": { - "description": "Required. The failover priority of the region. A failover priority of 0 indicates a write region. The maximum value for a failover priority = (total number of regions - 1). Failover priority values must be unique for each of the regions in which the database account exists." - } - }, - "isZoneRedundant": { - "type": "bool", - "nullable": true, - "metadata": { - "description": "Optional. Flag to indicate whether or not this region is an AvailabilityZone region. Defaults to true." - } - }, - "locationName": { - "type": "string", - "metadata": { - "description": "Required. The name of the region." + "resources": { + "avmTelemetry": { + "condition": "[parameters('enableTelemetry')]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2024-03-01", + "name": "[format('46d3xbcp.res.nw-privdnszonesrv.{0}.{1}', replace('0.1.0', '.', '-'), substring(uniqueString(deployment().name), 0, 4))]", + "properties": { + "mode": "Incremental", + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "resources": [], + "outputs": { + "telemetry": { + "type": "String", + "value": "For more information, see https://aka.ms/avm/TelemetryInfo" + } + } + } + } + }, + "privateDnsZone": { + "existing": true, + "type": "Microsoft.Network/privateDnsZones", + "apiVersion": "2020-06-01", + "name": "[parameters('privateDnsZoneName')]" + }, + "SRV": { + "type": "Microsoft.Network/privateDnsZones/SRV", + "apiVersion": "2020-06-01", + "name": "[format('{0}/{1}', parameters('privateDnsZoneName'), parameters('name'))]", + "properties": { + "metadata": "[parameters('metadata')]", + "srvRecords": "[parameters('srvRecords')]", + "ttl": "[parameters('ttl')]" + } + }, + "SRV_roleAssignments": { + "copy": { + "name": "SRV_roleAssignments", + "count": "[length(coalesce(variables('formattedRoleAssignments'), createArray()))]" + }, + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2022-04-01", + "scope": "[format('Microsoft.Network/privateDnsZones/{0}/SRV/{1}', parameters('privateDnsZoneName'), parameters('name'))]", + "name": "[coalesce(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'name'), guid(resourceId('Microsoft.Network/privateDnsZones/SRV', parameters('privateDnsZoneName'), parameters('name')), coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].principalId, coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].roleDefinitionId))]", + "properties": { + "roleDefinitionId": "[coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].roleDefinitionId]", + "principalId": "[coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].principalId]", + "description": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'description')]", + "principalType": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'principalType')]", + "condition": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'condition')]", + "conditionVersion": "[if(not(empty(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'condition'))), coalesce(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'conditionVersion'), '2.0'), null())]", + "delegatedManagedIdentityResourceId": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'delegatedManagedIdentityResourceId')]" + }, + "dependsOn": [ + "SRV" + ] + } + }, + "outputs": { + "name": { + "type": "string", + "metadata": { + "description": "The name of the deployed SRV record." + }, + "value": "[parameters('name')]" + }, + "resourceId": { + "type": "string", + "metadata": { + "description": "The resource ID of the deployed SRV record." + }, + "value": "[resourceId('Microsoft.Network/privateDnsZones/SRV', parameters('privateDnsZoneName'), parameters('name'))]" + }, + "resourceGroupName": { + "type": "string", + "metadata": { + "description": "The resource group of the deployed SRV record." + }, + "value": "[resourceGroup().name]" + } } } }, - "metadata": { - "__bicep_export!": true, - "description": "The type for the failover location." - } + "dependsOn": [ + "privateDnsZone" + ] }, - "sqlRoleAssignmentType": { - "type": "object", - "properties": { - "name": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The unique name of the role assignment." - } - }, - "roleDefinitionId": { - "type": "string", - "metadata": { - "description": "Required. The unique identifier of the Azure Cosmos DB for NoSQL native role-based access control definition." - } - }, - "principalId": { - "type": "string", - "metadata": { - "description": "Required. The unique identifier for the associated Microsoft Entra ID principal to which access is being granted through this role-based access control assignment. The tenant ID for the principal is inferred using the tenant associated with the subscription." - } - }, - "scope": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The data plane resource id for which access is being granted through this Role Assignment. Defaults to the root of the database account, but can also be scoped to e.g., the container and database level." - } - } + "privateDnsZone_TXT": { + "copy": { + "name": "privateDnsZone_TXT", + "count": "[length(coalesce(parameters('txt'), createArray()))]" }, - "metadata": { - "__bicep_export!": true, - "description": "The type for an Azure Cosmos DB for NoSQL native role-based access control assignment." - } - }, - "sqlRoleDefinitionType": { - "type": "object", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2022-09-01", + "name": "[format('{0}-PrivateDnsZone-TXTRecord-{1}', uniqueString(deployment().name, parameters('location')), copyIndex())]", "properties": { - "name": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The unique identifier of the role-based access control definition." - } - }, - "roleName": { - "type": "string", - "metadata": { - "description": "Required. A user-friendly name for the role-based access control definition. This must be unique within the database account." - } + "expressionEvaluationOptions": { + "scope": "inner" }, - "dataActions": { - "type": "array", - "items": { - "type": "string" + "mode": "Incremental", + "parameters": { + "privateDnsZoneName": { + "value": "[parameters('name')]" }, - "minLength": 1, - "metadata": { - "description": "Required. An array of data actions that are allowed." - } - }, - "assignableScopes": { - "type": "array", - "items": { - "type": "string" + "name": { + "value": "[coalesce(parameters('txt'), createArray())[copyIndex()].name]" }, - "nullable": true, "metadata": { - "description": "Optional. A set of fully-qualified scopes at or below which role-based access control assignments may be created using this definition. This setting allows application of this definition on the entire account or any underlying resource. This setting must have at least one element. Scopes higher than the account level are not enforceable as assignable scopes. Resources referenced in assignable scopes do not need to exist at creation. Defaults to the current account scope." - } - }, - "assignments": { - "type": "array", - "items": { - "$ref": "#/definitions/nestedSqlRoleAssignmentType" + "value": "[tryGet(coalesce(parameters('txt'), createArray())[copyIndex()], 'metadata')]" }, - "nullable": true, - "metadata": { - "description": "Optional. An array of role-based access control assignments to be created for the definition." - } - } - }, - "metadata": { - "__bicep_export!": true, - "description": "The type for an Azure Cosmos DB for NoSQL or Table native role-based access control definition." - } - }, - "networkRestrictionType": { - "type": "object", - "properties": { - "ipRules": { - "type": "array", - "items": { - "type": "string" + "txtRecords": { + "value": "[tryGet(coalesce(parameters('txt'), createArray())[copyIndex()], 'txtRecords')]" }, - "nullable": true, - "metadata": { - "description": "Optional. A single IPv4 address or a single IPv4 address range in Classless Inter-Domain Routing (CIDR) format. Provided IPs must be well-formatted and cannot be contained in one of the following ranges: `10.0.0.0/8`, `100.64.0.0/10`, `172.16.0.0/12`, `192.168.0.0/16`, since these are not enforceable by the IP address filter. Example of valid inputs: `23.40.210.245` or `23.40.210.0/8`." - } - }, - "networkAclBypass": { - "type": "string", - "allowedValues": [ - "AzureServices", - "None" - ], - "nullable": true, - "metadata": { - "description": "Optional. Specifies the network ACL bypass for Azure services. Default to \"None\"." + "ttl": { + "value": "[coalesce(tryGet(coalesce(parameters('txt'), createArray())[copyIndex()], 'ttl'), 3600)]" + }, + "roleAssignments": { + "value": "[tryGet(coalesce(parameters('txt'), createArray())[copyIndex()], 'roleAssignments')]" + }, + "enableTelemetry": { + "value": "[variables('enableReferencedModulesTelemetry')]" } }, - "publicNetworkAccess": { - "type": "string", - "allowedValues": [ - "Disabled", - "Enabled" - ], - "nullable": true, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "languageVersion": "2.0", + "contentVersion": "1.0.0.0", "metadata": { - "description": "Optional. Whether requests from the public network are allowed. Default to \"Disabled\"." - } - }, - "virtualNetworkRules": { - "type": "array", - "items": { - "type": "object", - "properties": { - "subnetResourceId": { - "type": "string", - "metadata": { - "description": "Required. Resource ID of a subnet." + "_generator": { + "name": "bicep", + "version": "0.37.4.10188", + "templateHash": "17170531000135004092" + }, + "name": "Private DNS Zone TXT record", + "description": "This module deploys a Private DNS Zone TXT record." + }, + "definitions": { + "roleAssignmentType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The name (as GUID) of the role assignment. If not provided, a GUID will be generated." + } + }, + "roleDefinitionIdOrName": { + "type": "string", + "metadata": { + "description": "Required. The role to assign. You can provide either the display name of the role definition, the role definition GUID, or its fully qualified ID in the following format: '/providers/Microsoft.Authorization/roleDefinitions/c2f4ef07-c644-48eb-af81-4b1b4947fb11'." + } + }, + "principalId": { + "type": "string", + "metadata": { + "description": "Required. The principal ID of the principal (user/group/identity) to assign the role to." + } + }, + "principalType": { + "type": "string", + "allowedValues": [ + "Device", + "ForeignGroup", + "Group", + "ServicePrincipal", + "User" + ], + "nullable": true, + "metadata": { + "description": "Optional. The principal type of the assigned principal ID." + } + }, + "description": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The description of the role assignment." + } + }, + "condition": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The conditions on the role assignment. This limits the resources it can be assigned to. e.g.: @Resource[Microsoft.Storage/storageAccounts/blobServices/containers:ContainerName] StringEqualsIgnoreCase \"foo_storage_container\"." + } + }, + "conditionVersion": { + "type": "string", + "allowedValues": [ + "2.0" + ], + "nullable": true, + "metadata": { + "description": "Optional. Version of the condition." + } + }, + "delegatedManagedIdentityResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The Resource Id of the delegated managed identity resource." + } + } + }, + "metadata": { + "description": "An AVM-aligned type for a role assignment.", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.5.1" + } + } + } + }, + "parameters": { + "privateDnsZoneName": { + "type": "string", + "metadata": { + "description": "Conditional. The name of the parent Private DNS zone. Required if the template is used in a standalone deployment." + } + }, + "name": { + "type": "string", + "metadata": { + "description": "Required. The name of the TXT record." + } + }, + "metadata": { + "type": "object", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.Network/privateDnsZones/TXT@2024-06-01#properties/properties/properties/metadata" + }, + "description": "Optional. The metadata attached to the record set." + }, + "nullable": true + }, + "ttl": { + "type": "int", + "defaultValue": 3600, + "metadata": { + "description": "Optional. The TTL (time-to-live) of the records in the record set." + } + }, + "txtRecords": { + "type": "array", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.Network/privateDnsZones/TXT@2024-06-01#properties/properties/properties/txtRecords" + }, + "description": "Optional. The list of TXT records in the record set." + }, + "nullable": true + }, + "enableTelemetry": { + "type": "bool", + "defaultValue": true, + "metadata": { + "description": "Optional. Enable/Disable usage telemetry for module." + } + }, + "roleAssignments": { + "type": "array", + "items": { + "$ref": "#/definitions/roleAssignmentType" + }, + "nullable": true, + "metadata": { + "description": "Optional. Array of role assignments to create." + } + } + }, + "variables": { + "copy": [ + { + "name": "formattedRoleAssignments", + "count": "[length(coalesce(parameters('roleAssignments'), createArray()))]", + "input": "[union(coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')], createObject('roleDefinitionId', coalesce(tryGet(variables('builtInRoleNames'), coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName), if(contains(coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName, '/providers/Microsoft.Authorization/roleDefinitions/'), coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName, subscriptionResourceId('Microsoft.Authorization/roleDefinitions', coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName)))))]" + } + ], + "builtInRoleNames": { + "Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'b24988ac-6180-42a0-ab88-20f7382dd24c')]", + "Private DNS Zone Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'b12aa53e-6015-4669-85d0-8515ebb3ae7f')]", + "Network Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '4d97b98b-1d4f-4787-a291-c67834d212e7')]", + "Owner": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '8e3af657-a8ff-443c-a75c-2fe8c4bcb635')]", + "Reader": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'acdd72a7-3385-48ef-bd42-f606fba81ae7')]", + "Role Based Access Control Administrator": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'f58310d9-a9f6-439a-9e8d-f62e7b41a168')]", + "User Access Administrator": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '18d7d88d-d35e-4fb5-a5c3-7773c20a72d9')]" + } + }, + "resources": { + "avmTelemetry": { + "condition": "[parameters('enableTelemetry')]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2024-03-01", + "name": "[format('46d3xbcp.res.nw-privdnszonetxt.{0}.{1}', replace('0.1.0', '.', '-'), substring(uniqueString(deployment().name), 0, 4))]", + "properties": { + "mode": "Incremental", + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "resources": [], + "outputs": { + "telemetry": { + "type": "String", + "value": "For more information, see https://aka.ms/avm/TelemetryInfo" + } + } } } + }, + "privateDnsZone": { + "existing": true, + "type": "Microsoft.Network/privateDnsZones", + "apiVersion": "2020-06-01", + "name": "[parameters('privateDnsZoneName')]" + }, + "TXT": { + "type": "Microsoft.Network/privateDnsZones/TXT", + "apiVersion": "2020-06-01", + "name": "[format('{0}/{1}', parameters('privateDnsZoneName'), parameters('name'))]", + "properties": { + "metadata": "[parameters('metadata')]", + "ttl": "[parameters('ttl')]", + "txtRecords": "[parameters('txtRecords')]" + } + }, + "TXT_roleAssignments": { + "copy": { + "name": "TXT_roleAssignments", + "count": "[length(coalesce(variables('formattedRoleAssignments'), createArray()))]" + }, + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2022-04-01", + "scope": "[format('Microsoft.Network/privateDnsZones/{0}/TXT/{1}', parameters('privateDnsZoneName'), parameters('name'))]", + "name": "[coalesce(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'name'), guid(resourceId('Microsoft.Network/privateDnsZones/TXT', parameters('privateDnsZoneName'), parameters('name')), coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].principalId, coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].roleDefinitionId))]", + "properties": { + "roleDefinitionId": "[coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].roleDefinitionId]", + "principalId": "[coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].principalId]", + "description": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'description')]", + "principalType": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'principalType')]", + "condition": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'condition')]", + "conditionVersion": "[if(not(empty(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'condition'))), coalesce(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'conditionVersion'), '2.0'), null())]", + "delegatedManagedIdentityResourceId": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'delegatedManagedIdentityResourceId')]" + }, + "dependsOn": [ + "TXT" + ] } }, - "nullable": true, - "metadata": { - "description": "Optional. List of virtual network access control list (ACL) rules configured for the account." - } - }, - "networkAclBypassResourceIds": { - "type": "array", - "items": { - "type": "string" - }, - "nullable": true, - "metadata": { - "description": "Optional. An array that contains the Resource Ids for Network Acl Bypass for the Cosmos DB account." + "outputs": { + "name": { + "type": "string", + "metadata": { + "description": "The name of the deployed TXT record." + }, + "value": "[parameters('name')]" + }, + "resourceId": { + "type": "string", + "metadata": { + "description": "The resource ID of the deployed TXT record." + }, + "value": "[resourceId('Microsoft.Network/privateDnsZones/TXT', parameters('privateDnsZoneName'), parameters('name'))]" + }, + "resourceGroupName": { + "type": "string", + "metadata": { + "description": "The resource group of the deployed TXT record." + }, + "value": "[resourceGroup().name]" + } } } }, - "metadata": { - "__bicep_export!": true, - "description": "The type for the network restriction." - } + "dependsOn": [ + "privateDnsZone" + ] }, - "gremlinDatabaseType": { - "type": "object", + "privateDnsZone_virtualNetworkLinks": { + "copy": { + "name": "privateDnsZone_virtualNetworkLinks", + "count": "[length(coalesce(parameters('virtualNetworkLinks'), createArray()))]" + }, + "type": "Microsoft.Resources/deployments", + "apiVersion": "2022-09-01", + "name": "[format('{0}-PrivateDnsZone-VNetLink-{1}', uniqueString(deployment().name, parameters('location')), copyIndex())]", "properties": { - "name": { - "type": "string", - "metadata": { - "description": "Required. Name of the Gremlin database." - } + "expressionEvaluationOptions": { + "scope": "inner" }, - "tags": { - "type": "object", - "metadata": { - "__bicep_resource_derived_type!": { - "source": "Microsoft.DocumentDB/databaseAccounts/gremlinDatabases@2024-11-15#properties/tags" - }, - "description": "Optional. Tags of the Gremlin database resource." + "mode": "Incremental", + "parameters": { + "privateDnsZoneName": { + "value": "[parameters('name')]" }, - "nullable": true - }, - "graphs": { - "type": "array", - "items": { - "$ref": "#/definitions/graphType" + "name": { + "value": "[coalesce(tryGet(coalesce(parameters('virtualNetworkLinks'), createArray())[copyIndex()], 'name'), format('{0}-vnetlink', last(split(coalesce(parameters('virtualNetworkLinks'), createArray())[copyIndex()].virtualNetworkResourceId, '/'))))]" }, - "nullable": true, - "metadata": { - "description": "Optional. Array of graphs to deploy in the Gremlin database." - } - }, - "maxThroughput": { - "type": "int", - "nullable": true, - "metadata": { - "description": "Optional. Represents maximum throughput, the resource can scale up to. Cannot be set together with `throughput`. If `throughput` is set to something else than -1, this autoscale setting is ignored. Setting throughput at the database level is only recommended for development/test or when workload across all graphs in the shared throughput database is uniform. For best performance for large production workloads, it is recommended to set dedicated throughput (autoscale or manual) at the graph level and not at the database level." - } - }, - "throughput": { - "type": "int", - "nullable": true, - "metadata": { - "description": "Optional. Request Units per second (for example 10000). Cannot be set together with `maxThroughput`. Setting throughput at the database level is only recommended for development/test or when workload across all graphs in the shared throughput database is uniform. For best performance for large production workloads, it is recommended to set dedicated throughput (autoscale or manual) at the graph level and not at the database level." - } - } - }, - "metadata": { - "__bicep_export!": true, - "description": "The type for a gremlin databae." - } - }, - "mongoDbType": { - "type": "object", - "properties": { - "name": { - "type": "string", - "metadata": { - "description": "Required. Name of the mongodb database." - } - }, - "throughput": { - "type": "int", - "nullable": true, - "metadata": { - "description": "Optional. Request Units per second. Setting throughput at the database level is only recommended for development/test or when workload across all collections in the shared throughput database is uniform. For best performance for large production workloads, it is recommended to set dedicated throughput (autoscale or manual) at the collection level and not at the database level." - } - }, - "collections": { - "type": "array", - "items": { - "$ref": "#/definitions/collectionType" + "virtualNetworkResourceId": { + "value": "[coalesce(parameters('virtualNetworkLinks'), createArray())[copyIndex()].virtualNetworkResourceId]" }, - "nullable": true, - "metadata": { - "description": "Optional. Collections in the mongodb database." + "location": { + "value": "[coalesce(tryGet(coalesce(parameters('virtualNetworkLinks'), createArray())[copyIndex()], 'location'), 'global')]" + }, + "registrationEnabled": { + "value": "[coalesce(tryGet(coalesce(parameters('virtualNetworkLinks'), createArray())[copyIndex()], 'registrationEnabled'), false())]" + }, + "tags": { + "value": "[coalesce(tryGet(coalesce(parameters('virtualNetworkLinks'), createArray())[copyIndex()], 'tags'), parameters('tags'))]" + }, + "resolutionPolicy": { + "value": "[tryGet(coalesce(parameters('virtualNetworkLinks'), createArray())[copyIndex()], 'resolutionPolicy')]" } }, - "autoscaleSettings": { - "type": "object", + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "languageVersion": "2.0", + "contentVersion": "1.0.0.0", "metadata": { - "__bicep_resource_derived_type!": { - "source": "Microsoft.DocumentDB/databaseAccounts/mongodbDatabases@2025-04-15#properties/properties/properties/options/properties/autoscaleSettings" + "_generator": { + "name": "bicep", + "version": "0.37.4.10188", + "templateHash": "517173107480898390" }, - "description": "Optional. Specifies the Autoscale settings. Note: Either throughput or autoscaleSettings is required, but not both." + "name": "Private DNS Zone Virtual Network Link", + "description": "This module deploys a Private DNS Zone Virtual Network Link." }, - "nullable": true - }, - "tags": { - "type": "object", - "metadata": { - "__bicep_resource_derived_type!": { - "source": "Microsoft.DocumentDB/databaseAccounts/mongodbDatabases@2025-04-15#properties/tags" + "parameters": { + "privateDnsZoneName": { + "type": "string", + "metadata": { + "description": "Conditional. The name of the parent Private DNS zone. Required if the template is used in a standalone deployment." + } }, - "description": "Optional. Tags of the resource." + "name": { + "type": "string", + "defaultValue": "[format('{0}-vnetlink', last(split(parameters('virtualNetworkResourceId'), '/')))]", + "metadata": { + "description": "Optional. The name of the virtual network link." + } + }, + "location": { + "type": "string", + "defaultValue": "global", + "metadata": { + "description": "Optional. The location of the PrivateDNSZone. Should be global." + } + }, + "tags": { + "type": "object", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.Network/privateDnsZones/virtualNetworkLinks@2024-06-01#properties/tags" + }, + "description": "Optional. Tags of the resource." + }, + "nullable": true + }, + "registrationEnabled": { + "type": "bool", + "defaultValue": false, + "metadata": { + "description": "Optional. Is auto-registration of virtual machine records in the virtual network in the Private DNS zone enabled?." + } + }, + "virtualNetworkResourceId": { + "type": "string", + "metadata": { + "description": "Required. Link to another virtual network resource ID." + } + }, + "resolutionPolicy": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The resolution policy on the virtual network link. Only applicable for virtual network links to privatelink zones, and for A,AAAA,CNAME queries. When set to `NxDomainRedirect`, Azure DNS resolver falls back to public resolution if private dns query resolution results in non-existent domain response. `Default` is configured as the default option." + } + } }, - "nullable": true + "resources": { + "privateDnsZone": { + "existing": true, + "type": "Microsoft.Network/privateDnsZones", + "apiVersion": "2020-06-01", + "name": "[parameters('privateDnsZoneName')]" + }, + "virtualNetworkLink": { + "type": "Microsoft.Network/privateDnsZones/virtualNetworkLinks", + "apiVersion": "2024-06-01", + "name": "[format('{0}/{1}', parameters('privateDnsZoneName'), parameters('name'))]", + "location": "[parameters('location')]", + "tags": "[parameters('tags')]", + "properties": { + "registrationEnabled": "[parameters('registrationEnabled')]", + "virtualNetwork": { + "id": "[parameters('virtualNetworkResourceId')]" + }, + "resolutionPolicy": "[parameters('resolutionPolicy')]" + } + } + }, + "outputs": { + "name": { + "type": "string", + "metadata": { + "description": "The name of the deployed virtual network link." + }, + "value": "[parameters('name')]" + }, + "resourceId": { + "type": "string", + "metadata": { + "description": "The resource ID of the deployed virtual network link." + }, + "value": "[resourceId('Microsoft.Network/privateDnsZones/virtualNetworkLinks', parameters('privateDnsZoneName'), parameters('name'))]" + }, + "resourceGroupName": { + "type": "string", + "metadata": { + "description": "The resource group of the deployed virtual network link." + }, + "value": "[resourceGroup().name]" + }, + "location": { + "type": "string", + "metadata": { + "description": "The location the resource was deployed into." + }, + "value": "[reference('virtualNetworkLink', '2024-06-01', 'full').location]" + } + } } }, + "dependsOn": [ + "privateDnsZone" + ] + } + }, + "outputs": { + "resourceGroupName": { + "type": "string", "metadata": { - "__bicep_export!": true, - "description": "The type for a mongo databae." - } + "description": "The resource group the private DNS zone was deployed into." + }, + "value": "[resourceGroup().name]" }, - "sqlDatabaseType": { - "type": "object", - "properties": { - "name": { - "type": "string", - "metadata": { - "description": "Required. Name of the SQL database ." - } - }, - "containers": { - "type": "array", - "items": { - "$ref": "#/definitions/containerType" - }, - "nullable": true, - "metadata": { - "description": "Optional. Array of containers to deploy in the SQL database." - } - }, - "throughput": { - "type": "int", - "nullable": true, - "metadata": { - "description": "Optional. Request units per second. Will be ignored if autoscaleSettingsMaxThroughput is used. Setting throughput at the database level is only recommended for development/test or when workload across all containers in the shared throughput database is uniform. For best performance for large production workloads, it is recommended to set dedicated throughput (autoscale or manual) at the container level and not at the database level." - } - }, - "autoscaleSettingsMaxThroughput": { - "type": "int", - "nullable": true, - "metadata": { - "description": "Optional. Specifies the Autoscale settings and represents maximum throughput, the resource can scale up to. The autoscale throughput should have valid throughput values between 1000 and 1000000 inclusive in increments of 1000. If value is set to null, then autoscale will be disabled. Setting throughput at the database level is only recommended for development/test or when workload across all containers in the shared throughput database is uniform. For best performance for large production workloads, it is recommended to set dedicated throughput (autoscale or manual) at the container level and not at the database level." - } - }, - "tags": { - "type": "object", - "metadata": { - "__bicep_resource_derived_type!": { - "source": "Microsoft.DocumentDB/databaseAccounts/sqlDatabases@2025-04-15#properties/tags" - }, - "description": "Optional. Tags of the SQL database resource." - }, - "nullable": true - } + "name": { + "type": "string", + "metadata": { + "description": "The name of the private DNS zone." + }, + "value": "[parameters('name')]" + }, + "resourceId": { + "type": "string", + "metadata": { + "description": "The resource ID of the private DNS zone." }, + "value": "[resourceId('Microsoft.Network/privateDnsZones', parameters('name'))]" + }, + "location": { + "type": "string", "metadata": { - "__bicep_export!": true, - "description": "The type for a sql database." + "description": "The location the resource was deployed into." + }, + "value": "[reference('privateDnsZone', '2020-06-01', 'full').location]" + } + } + } + }, + "dependsOn": [ + "virtualNetwork" + ] + }, + "aiFoundryAiServices": { + "condition": "[not(variables('useExistingAiFoundryAiProject'))]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "[take(format('avm.res.cognitive-services.account.{0}', variables('aiFoundryAiServicesResourceName')), 64)]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "name": { + "value": "[variables('aiFoundryAiServicesResourceName')]" + }, + "location": { + "value": "[parameters('azureAiServiceLocation')]" + }, + "tags": { + "value": "[parameters('tags')]" + }, + "sku": { + "value": "S0" + }, + "kind": { + "value": "AIServices" + }, + "disableLocalAuth": { + "value": true + }, + "allowProjectManagement": { + "value": true + }, + "customSubDomainName": { + "value": "[variables('aiFoundryAiServicesResourceName')]" + }, + "restrictOutboundNetworkAccess": { + "value": false + }, + "deployments": { + "copy": [ + { + "name": "value", + "count": "[length(variables('aiFoundryAiServicesModelDeployment'))]", + "input": "[createObject('name', variables('aiFoundryAiServicesModelDeployment')[copyIndex('value')].name, 'model', createObject('format', variables('aiFoundryAiServicesModelDeployment')[copyIndex('value')].format, 'name', variables('aiFoundryAiServicesModelDeployment')[copyIndex('value')].name, 'version', variables('aiFoundryAiServicesModelDeployment')[copyIndex('value')].version), 'raiPolicyName', variables('aiFoundryAiServicesModelDeployment')[copyIndex('value')].raiPolicyName, 'sku', createObject('name', variables('aiFoundryAiServicesModelDeployment')[copyIndex('value')].sku.name, 'capacity', variables('aiFoundryAiServicesModelDeployment')[copyIndex('value')].sku.capacity))]" } - }, - "tableType": { - "type": "object", - "properties": { - "name": { - "type": "string", - "metadata": { - "description": "Required. Name of the table." - } - }, - "tags": { - "type": "object", - "metadata": { - "__bicep_resource_derived_type!": { - "source": "Microsoft.DocumentDB/databaseAccounts/tables@2025-04-15#properties/tags" - }, - "description": "Optional. Tags for the table." - }, - "nullable": true - }, - "maxThroughput": { - "type": "int", - "nullable": true, - "metadata": { - "description": "Optional. Represents maximum throughput, the resource can scale up to. Cannot be set together with `throughput`. If `throughput` is set to something else than -1, this autoscale setting is ignored." - } - }, - "throughput": { - "type": "int", - "nullable": true, - "metadata": { - "description": "Optional. Request Units per second (for example 10000). Cannot be set together with `maxThroughput`." - } - } + ] + }, + "networkAcls": { + "value": { + "defaultAction": "Allow", + "virtualNetworkRules": [], + "ipRules": [] + } + }, + "managedIdentities": { + "value": { + "userAssignedResourceIds": [ + "[reference('userAssignedIdentity').outputs.resourceId.value]" + ] + } + }, + "roleAssignments": { + "value": [ + { + "roleDefinitionIdOrName": "53ca6127-db72-4b80-b1b0-d745d6d5456d", + "principalId": "[reference('userAssignedIdentity').outputs.principalId.value]", + "principalType": "ServicePrincipal" + }, + { + "roleDefinitionIdOrName": "64702f94-c441-49e6-a78b-ef80e0188fee", + "principalId": "[reference('userAssignedIdentity').outputs.principalId.value]", + "principalType": "ServicePrincipal" + }, + { + "roleDefinitionIdOrName": "5e0bd9bd-7b93-4f28-af87-19fc36ad61bd", + "principalId": "[reference('userAssignedIdentity').outputs.principalId.value]", + "principalType": "ServicePrincipal" }, - "metadata": { - "__bicep_export!": true, - "description": "The type for a table." + { + "roleDefinitionIdOrName": "53ca6127-db72-4b80-b1b0-d745d6d5456d", + "principalId": "[deployer().objectId]" } + ] + }, + "diagnosticSettings": "[if(parameters('enableMonitoring'), createObject('value', createArray(createObject('workspaceResourceId', if(variables('useExistingLogAnalytics'), parameters('existingLogAnalyticsWorkspaceId'), if(parameters('enableMonitoring'), reference('logAnalyticsWorkspace').outputs.resourceId.value, ''))))), createObject('value', null()))]", + "publicNetworkAccess": "[if(parameters('enablePrivateNetworking'), createObject('value', 'Disabled'), createObject('value', 'Enabled'))]" + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "languageVersion": "2.0", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.38.33.27573", + "templateHash": "7191594406492701501" }, - "cassandraStandaloneRoleAssignmentType": { + "name": "Cognitive Services", + "description": "This module deploys a Cognitive Service." + }, + "definitions": { + "privateEndpointOutputType": { "type": "object", "properties": { "name": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The unique name of the role assignment." - } - }, - "roleDefinitionId": { - "type": "string", - "metadata": { - "description": "Required. The unique identifier of the Azure Cosmos DB for Apache Cassandra native role-based access control definition." - } - }, - "principalId": { "type": "string", "metadata": { - "description": "Required. The unique identifier for the associated Microsoft Entra ID principal to which access is being granted through this role-based access control assignment. The tenant ID for the principal is inferred using the tenant associated with the subscription." + "description": "The name of the private endpoint." } }, - "scope": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The data plane resource path for which access is being granted through this role-based access control assignment. Defaults to the current account." - } - } - }, - "metadata": { - "__bicep_export!": true, - "description": "The type for an Azure Cosmos DB for Apache Cassandra native role-based access control assignment." - } - }, - "cassandraRoleDefinitionType": { - "type": "object", - "properties": { - "name": { + "resourceId": { "type": "string", - "nullable": true, "metadata": { - "description": "Optional. The unique identifier of the role-based access control definition." + "description": "The resource ID of the private endpoint." } }, - "roleName": { + "groupId": { "type": "string", - "metadata": { - "description": "Required. A user-friendly name for the role-based access control definition. Must be unique for the database account." - } - }, - "dataActions": { - "type": "array", - "items": { - "type": "string" - }, "nullable": true, "metadata": { - "description": "Optional. An array of data actions that are allowed. Note: Valid data action strings are currently undocumented (API version 2025-05-01-preview). Expected to follow format similar to SQL RBAC once documented by Microsoft." + "description": "The group Id for the private endpoint Group." } }, - "notDataActions": { + "customDnsConfigs": { "type": "array", "items": { - "type": "string" + "type": "object", + "properties": { + "fqdn": { + "type": "string", + "nullable": true, + "metadata": { + "description": "FQDN that resolves to private endpoint IP address." + } + }, + "ipAddresses": { + "type": "array", + "items": { + "type": "string" + }, + "metadata": { + "description": "A list of private IP addresses of the private endpoint." + } + } + } }, - "nullable": true, "metadata": { - "description": "Optional. An array of data actions that are denied. Note: Unlike SQL RBAC, Cassandra supports deny rules for granular access control. Valid data action strings are currently undocumented (API version 2025-05-01-preview)." + "description": "The custom DNS configurations of the private endpoint." } }, - "assignableScopes": { + "networkInterfaceResourceIds": { "type": "array", "items": { "type": "string" }, - "nullable": true, - "metadata": { - "description": "Optional. A set of fully qualified Scopes at or below which Role Assignments may be created using this Role Definition." - } - }, - "assignments": { - "type": "array", - "items": { - "$ref": "#/definitions/cassandraRoleAssignmentType" - }, - "nullable": true, "metadata": { - "description": "Optional. An array of role-based access control assignments to be created for the definition." + "description": "The IDs of the network interfaces associated with the private endpoint." } } }, "metadata": { "__bicep_export!": true, - "description": "The type for an Azure Cosmos DB for Apache Cassandra native role-based access control definition." + "description": "The type for the private endpoint output." } }, - "cassandraKeyspaceType": { + "deploymentType": { "type": "object", "properties": { "name": { "type": "string", + "nullable": true, "metadata": { - "description": "Required. Name of the Cassandra keyspace." + "description": "Optional. Specify the name of cognitive service account deployment." } }, - "tables": { - "type": "array", - "items": { - "$ref": "#/definitions/cassandraTableType" + "model": { + "type": "object", + "properties": { + "name": { + "type": "string", + "metadata": { + "description": "Required. The name of Cognitive Services account deployment model." + } + }, + "format": { + "type": "string", + "metadata": { + "description": "Required. The format of Cognitive Services account deployment model." + } + }, + "version": { + "type": "string", + "metadata": { + "description": "Required. The version of Cognitive Services account deployment model." + } + } }, - "nullable": true, "metadata": { - "description": "Optional. Array of Cassandra tables to deploy in the keyspace." + "description": "Required. Properties of Cognitive Services account deployment model." } }, - "views": { - "type": "array", - "items": { - "$ref": "#/definitions/cassandraViewType" + "sku": { + "type": "object", + "properties": { + "name": { + "type": "string", + "metadata": { + "description": "Required. The name of the resource model definition representing SKU." + } + }, + "capacity": { + "type": "int", + "nullable": true, + "metadata": { + "description": "Optional. The capacity of the resource model definition representing SKU." + } + }, + "tier": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The tier of the resource model definition representing SKU." + } + }, + "size": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The size of the resource model definition representing SKU." + } + }, + "family": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The family of the resource model definition representing SKU." + } + } }, "nullable": true, "metadata": { - "description": "Optional. Array of Cassandra views (materialized views) to deploy in the keyspace." + "description": "Optional. The resource model definition representing SKU." } }, - "autoscaleSettingsMaxThroughput": { - "type": "int", + "raiPolicyName": { + "type": "string", "nullable": true, "metadata": { - "description": "Optional. Represents maximum throughput, the resource can scale up to. Cannot be set together with `throughput`. If `throughput` is set to something else than -1, this autoscale setting is ignored. Setting throughput at the keyspace level is only recommended for development/test or when workload across all tables in the shared throughput keyspace is uniform. For best performance for large production workloads, it is recommended to set dedicated throughput (autoscale or manual) at the table level and not at the keyspace level." + "description": "Optional. The name of RAI policy." } }, - "throughput": { - "type": "int", + "versionUpgradeOption": { + "type": "string", "nullable": true, "metadata": { - "description": "Optional. Request Units per second (for example 10000). Cannot be set together with `autoscaleSettingsMaxThroughput`. Setting throughput at the keyspace level is only recommended for development/test or when workload across all tables in the shared throughput keyspace is uniform. For best performance for large production workloads, it is recommended to set dedicated throughput (autoscale or manual) at the table level and not at the keyspace level." - } - }, - "tags": { - "type": "object", - "metadata": { - "__bicep_resource_derived_type!": { - "source": "Microsoft.DocumentDB/databaseAccounts/cassandraKeyspaces@2024-11-15#properties/tags" - }, - "description": "Optional. Tags of the Cassandra keyspace resource." - }, - "nullable": true - } - }, - "metadata": { - "__bicep_export!": true, - "description": "The type for an Azure Cosmos DB Cassandra keyspace." - } - }, - "defaultIdentityType": { - "type": "object", - "discriminator": { - "propertyName": "name", - "mapping": { - "FirstPartyIdentity": { - "$ref": "#/definitions/defaultIdentityFirstPartyType" - }, - "SystemAssignedIdentity": { - "$ref": "#/definitions/defaultIdentitySystemAssignedType" - }, - "UserAssignedIdentity": { - "$ref": "#/definitions/defaultIdentityUserAssignedType" + "description": "Optional. The version upgrade option." } } }, "metadata": { "__bicep_export!": true, - "description": "The type for the default identity." + "description": "The type for a cognitive services account deployment." } }, - "defaultIdentityFirstPartyType": { + "endpointType": { "type": "object", "properties": { "name": { "type": "string", - "allowedValues": [ - "FirstPartyIdentity" - ], + "nullable": true, "metadata": { - "description": "Required. The type of default identity to use." + "description": "Type of the endpoint." } - } - } - }, - "defaultIdentitySystemAssignedType": { - "type": "object", - "properties": { - "name": { + }, + "endpoint": { "type": "string", - "allowedValues": [ - "SystemAssignedIdentity" - ], + "nullable": true, "metadata": { - "description": "Required. The type of default identity to use." + "description": "The endpoint URI." } } + }, + "metadata": { + "__bicep_export!": true, + "description": "The type for a cognitive services account endpoint." } }, - "defaultIdentityUserAssignedType": { + "secretsExportConfigurationType": { "type": "object", "properties": { - "name": { + "keyVaultResourceId": { "type": "string", - "allowedValues": [ - "UserAssignedIdentity" - ], "metadata": { - "description": "Required. The type of default identity to use." + "description": "Required. The key vault name where to store the keys and connection strings generated by the modules." } }, - "resourceId": { - "type": "string", - "metadata": { - "description": "Required. The resource ID of the user assigned identity to use as the default identity." - } - } - } - }, - "_1.privateEndpointCustomDnsConfigType": { - "type": "object", - "properties": { - "fqdn": { + "accessKey1Name": { "type": "string", "nullable": true, "metadata": { - "description": "Optional. FQDN that resolves to private endpoint IP address." + "description": "Optional. The name for the accessKey1 secret to create." } }, - "ipAddresses": { - "type": "array", - "items": { - "type": "string" - }, + "accessKey2Name": { + "type": "string", + "nullable": true, "metadata": { - "description": "Required. A list of private IP addresses of the private endpoint." + "description": "Optional. The name for the accessKey2 secret to create." } } }, "metadata": { - "__bicep_imported_from!": { - "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.1" - } + "__bicep_export!": true, + "description": "The type of the secrets exported to the provided Key Vault." } }, - "_1.privateEndpointIpConfigurationType": { + "commitmentPlanType": { "type": "object", "properties": { - "name": { - "type": "string", + "autoRenew": { + "type": "bool", "metadata": { - "description": "Required. The name of the resource that is unique within a resource group." + "description": "Required. Whether the plan should auto-renew at the end of the current commitment period." } }, - "properties": { + "current": { "type": "object", "properties": { - "groupId": { - "type": "string", + "count": { + "type": "int", "metadata": { - "description": "Required. The ID of a group obtained from the remote resource that this private endpoint should connect to." + "description": "Required. The number of committed instances (e.g., number of containers or cores)." } }, - "memberName": { + "tier": { "type": "string", "metadata": { - "description": "Required. The member name of a group obtained from the remote resource that this private endpoint should connect to." + "description": "Required. The tier of the commitment plan (e.g., T1, T2)." + } + } + }, + "metadata": { + "description": "Required. The current commitment configuration." + } + }, + "hostingModel": { + "type": "string", + "metadata": { + "description": "Required. The hosting model for the commitment plan. (e.g., DisconnectedContainer, ConnectedContainer, ProvisionedWeb, Web)." + } + }, + "planType": { + "type": "string", + "metadata": { + "description": "Required. The plan type indicating which capability the plan applies to (e.g., NTTS, STT, CUSTOMSTT, ADDON)." + } + }, + "commitmentPlanGuid": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The unique identifier of an existing commitment plan to update. Set to null to create a new plan." + } + }, + "next": { + "type": "object", + "properties": { + "count": { + "type": "int", + "metadata": { + "description": "Required. The number of committed instances for the next period." } }, - "privateIPAddress": { + "tier": { "type": "string", "metadata": { - "description": "Required. A private IP address obtained from the private endpoint's subnet." + "description": "Required. The tier for the next commitment period." } } }, + "nullable": true, "metadata": { - "description": "Required. Properties of private endpoint IP configurations." + "description": "Optional. The configuration of the next commitment period, if scheduled." } } }, "metadata": { - "__bicep_imported_from!": { - "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.1" - } + "__bicep_export!": true, + "description": "The type for a disconnected container commitment plan." } }, - "_1.privateEndpointPrivateDnsZoneGroupType": { + "networkInjectionType": { "type": "object", "properties": { - "name": { + "scenario": { "type": "string", - "nullable": true, + "allowedValues": [ + "agent", + "none" + ], "metadata": { - "description": "Optional. The name of the Private DNS Zone Group." + "description": "Required. The scenario for the network injection." } }, - "privateDnsZoneGroupConfigs": { - "type": "array", - "items": { - "type": "object", - "properties": { - "name": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The name of the private DNS Zone Group config." - } - }, - "privateDnsZoneResourceId": { - "type": "string", - "metadata": { - "description": "Required. The resource id of the private DNS zone." - } - } - } - }, + "subnetResourceId": { + "type": "string", "metadata": { - "description": "Required. The private DNS Zone Groups to associate the Private Endpoint. A DNS Zone Group can support up to 5 DNS zones." + "description": "Required. The Resource ID of the subnet on the Virtual Network on which to inject." + } + }, + "useMicrosoftManagedNetwork": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. Whether to use Microsoft Managed Network. Defaults to false." } } }, "metadata": { - "__bicep_imported_from!": { - "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.1" - } + "__bicep_export!": true, + "description": "Type for network configuration in AI Foundry where virtual network injection occurs to secure scenarios like Agents entirely within a private network." } }, - "cassandraRoleAssignmentType": { + "_1.secretSetOutputType": { "type": "object", "properties": { - "name": { + "secretResourceId": { "type": "string", - "nullable": true, "metadata": { - "description": "Optional. The unique identifier of the role assignment." + "description": "The resourceId of the exported secret." } }, - "principalId": { + "secretUri": { "type": "string", "metadata": { - "description": "Required. The unique identifier for the associated AAD principal." + "description": "The secret URI of the exported secret." } }, - "scope": { + "secretUriWithVersion": { "type": "string", - "nullable": true, "metadata": { - "description": "Optional. The data plane resource path for which access is being granted. Defaults to the current account." + "description": "The secret URI with version of the exported secret." } } }, "metadata": { + "description": "An AVM-aligned type for the output of the secret set via the secrets export feature.", "__bicep_imported_from!": { - "sourceTemplate": "cassandra-role-definition/main.bicep" + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.0" } } }, - "cassandraTableType": { + "_2.lockType": { "type": "object", "properties": { "name": { "type": "string", + "nullable": true, "metadata": { - "description": "Required. Name of the table." - } - }, - "schema": { - "type": "object", - "metadata": { - "__bicep_resource_derived_type!": { - "source": "Microsoft.DocumentDB/databaseAccounts/cassandraKeyspaces/tables@2024-11-15#properties/properties/properties/resource/properties/schema" - }, - "description": "Required. Schema definition for the table." + "description": "Optional. Specify the name of lock." } }, - "tags": { - "type": "object", - "metadata": { - "__bicep_resource_derived_type!": { - "source": "Microsoft.DocumentDB/databaseAccounts/cassandraKeyspaces/tables@2024-11-15#properties/tags" - }, - "description": "Optional. Tags for the table." - }, - "nullable": true - }, - "defaultTtl": { - "type": "int", + "kind": { + "type": "string", + "allowedValues": [ + "CanNotDelete", + "None", + "ReadOnly" + ], "nullable": true, "metadata": { - "description": "Optional. Default TTL (Time To Live) in seconds for data in the table." + "description": "Optional. Specify the type of lock." } }, - "analyticalStorageTtl": { - "type": "int", + "notes": { + "type": "string", "nullable": true, "metadata": { - "description": "Optional. Analytical TTL for the table." + "description": "Optional. Specify the notes of the lock." } - }, - "throughput": { - "type": "int", + } + }, + "metadata": { + "description": "An AVM-aligned type for a lock.", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.1" + } + } + }, + "_2.privateEndpointCustomDnsConfigType": { + "type": "object", + "properties": { + "fqdn": { + "type": "string", "nullable": true, "metadata": { - "description": "Optional. Request units per second. Cannot be used with autoscaleSettingsMaxThroughput." + "description": "Optional. FQDN that resolves to private endpoint IP address." } }, - "autoscaleSettingsMaxThroughput": { - "type": "int", - "nullable": true, + "ipAddresses": { + "type": "array", + "items": { + "type": "string" + }, "metadata": { - "description": "Optional. Maximum autoscale throughput for the table. Cannot be used with throughput." + "description": "Required. A list of private IP addresses of the private endpoint." } } }, "metadata": { - "description": "The type of a Cassandra table.", - "__bicep_imported_from!": { - "sourceTemplate": "cassandra-keyspace/main.bicep", - "originalIdentifier": "tableType" + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.1" } } }, - "cassandraViewType": { + "_2.privateEndpointIpConfigurationType": { "type": "object", "properties": { "name": { "type": "string", "metadata": { - "description": "Required. Name of the view." - } - }, - "viewDefinition": { - "type": "string", - "metadata": { - "description": "Required. View definition (CQL statement)." + "description": "Required. The name of the resource that is unique within a resource group." } }, - "tags": { + "properties": { "type": "object", - "metadata": { - "__bicep_resource_derived_type!": { - "source": "Microsoft.DocumentDB/databaseAccounts/cassandraKeyspaces/views@2025-05-01-preview#properties/tags" + "properties": { + "groupId": { + "type": "string", + "metadata": { + "description": "Required. The ID of a group obtained from the remote resource that this private endpoint should connect to." + } }, - "description": "Optional. Tags for the view." + "memberName": { + "type": "string", + "metadata": { + "description": "Required. The member name of a group obtained from the remote resource that this private endpoint should connect to." + } + }, + "privateIPAddress": { + "type": "string", + "metadata": { + "description": "Required. A private IP address obtained from the private endpoint's subnet." + } + } }, - "nullable": true - }, - "throughput": { - "type": "int", - "nullable": true, - "metadata": { - "description": "Optional. Request units per second. Cannot be used with autoscaleSettingsMaxThroughput." - } - }, - "autoscaleSettingsMaxThroughput": { - "type": "int", - "nullable": true, "metadata": { - "description": "Optional. Maximum autoscale throughput for the view. Cannot be used with throughput." + "description": "Required. Properties of private endpoint IP configurations." } } }, "metadata": { - "description": "The type of a Cassandra view (materialized view).", "__bicep_imported_from!": { - "sourceTemplate": "cassandra-keyspace/main.bicep", - "originalIdentifier": "viewType" + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.1" } } }, - "collectionType": { + "_2.privateEndpointPrivateDnsZoneGroupType": { "type": "object", "properties": { "name": { "type": "string", - "metadata": { - "description": "Required. Name of the collection." - } - }, - "throughput": { - "type": "int", "nullable": true, "metadata": { - "description": "Optional. Request Units per second. For best performance for large production workloads, it is recommended to set dedicated throughput (autoscale or manual) at the collection level and not at the database level." + "description": "Optional. The name of the Private DNS Zone Group." } }, - "indexes": { + "privateDnsZoneGroupConfigs": { "type": "array", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The name of the private DNS Zone Group config." + } + }, + "privateDnsZoneResourceId": { + "type": "string", + "metadata": { + "description": "Required. The resource id of the private DNS zone." + } + } + } + }, "metadata": { - "__bicep_resource_derived_type!": { - "source": "Microsoft.DocumentDB/databaseAccounts/mongodbDatabases/collections@2025-04-15#properties/properties/properties/resource/properties/indexes" - }, - "description": "Required. Indexes for the collection." - } - }, - "shardKey": { - "type": "object", - "metadata": { - "__bicep_resource_derived_type!": { - "source": "Microsoft.DocumentDB/databaseAccounts/mongodbDatabases/collections@2025-04-15#properties/properties/properties/resource/properties/shardKey" - }, - "description": "Required. ShardKey for the collection." + "description": "Required. The private DNS Zone Groups to associate the Private Endpoint. A DNS Zone Group can support up to 5 DNS zones." } } }, "metadata": { - "description": "The type of a collection.", "__bicep_imported_from!": { - "sourceTemplate": "mongodb-database/main.bicep" + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.1" } } }, - "containerType": { + "_2.roleAssignmentType": { "type": "object", "properties": { "name": { "type": "string", + "nullable": true, "metadata": { - "description": "Required. Name of the container." + "description": "Optional. The name (as GUID) of the role assignment. If not provided, a GUID will be generated." } }, - "analyticalStorageTtl": { - "type": "int", - "nullable": true, + "roleDefinitionIdOrName": { + "type": "string", "metadata": { - "description": "Optional. Default to 0. Indicates how long data should be retained in the analytical store, for a container. Analytical store is enabled when ATTL is set with a value other than 0. If the value is set to -1, the analytical store retains all historical data, irrespective of the retention of the data in the transactional store." + "description": "Required. The role to assign. You can provide either the display name of the role definition, the role definition GUID, or its fully qualified ID in the following format: '/providers/Microsoft.Authorization/roleDefinitions/c2f4ef07-c644-48eb-af81-4b1b4947fb11'." } }, - "conflictResolutionPolicy": { - "type": "object", + "principalId": { + "type": "string", "metadata": { - "__bicep_resource_derived_type!": { - "source": "Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers@2025-04-15#properties/properties/properties/resource/properties/conflictResolutionPolicy" - }, - "description": "Optional. The conflict resolution policy for the container. Conflicts and conflict resolution policies are applicable if the Azure Cosmos DB account is configured with multiple write regions." - }, - "nullable": true + "description": "Required. The principal ID of the principal (user/group/identity) to assign the role to." + } }, - "defaultTtl": { - "type": "int", + "principalType": { + "type": "string", + "allowedValues": [ + "Device", + "ForeignGroup", + "Group", + "ServicePrincipal", + "User" + ], "nullable": true, - "minValue": -1, - "maxValue": 2147483647, "metadata": { - "description": "Optional. Default to -1. Default time to live (in seconds). With Time to Live or TTL, Azure Cosmos DB provides the ability to delete items automatically from a container after a certain time period. If the value is set to \"-1\", it is equal to infinity, and items don't expire by default." + "description": "Optional. The principal type of the assigned principal ID." } }, - "throughput": { - "type": "int", + "description": { + "type": "string", "nullable": true, "metadata": { - "description": "Optional. Default to 400. Request Units per second. Will be ignored if autoscaleSettingsMaxThroughput is used. For best performance for large production workloads, it is recommended to set dedicated throughput (autoscale or manual) at the container level and not at the database level." + "description": "Optional. The description of the role assignment." } }, - "autoscaleSettingsMaxThroughput": { - "type": "int", + "condition": { + "type": "string", "nullable": true, - "maxValue": 1000000, "metadata": { - "description": "Optional. Specifies the Autoscale settings and represents maximum throughput, the resource can scale up to. The autoscale throughput should have valid throughput values between 1000 and 1000000 inclusive in increments of 1000. If value is set to null, then autoscale will be disabled. For best performance for large production workloads, it is recommended to set dedicated throughput (autoscale or manual) at the container level and not at the database level." + "description": "Optional. The conditions on the role assignment. This limits the resources it can be assigned to. e.g.: @Resource[Microsoft.Storage/storageAccounts/blobServices/containers:ContainerName] StringEqualsIgnoreCase \"foo_storage_container\"." } }, - "tags": { - "type": "object", + "conditionVersion": { + "type": "string", + "allowedValues": [ + "2.0" + ], + "nullable": true, "metadata": { - "__bicep_resource_derived_type!": { - "source": "Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers@2025-04-15#properties/tags" - }, - "description": "Optional. Tags of the SQL Database resource." - }, - "nullable": true + "description": "Optional. Version of the condition." + } }, - "paths": { - "type": "array", - "items": { - "type": "string" - }, - "minLength": 1, - "maxLength": 3, + "delegatedManagedIdentityResourceId": { + "type": "string", + "nullable": true, "metadata": { - "description": "Required. List of paths using which data within the container can be partitioned. For kind=MultiHash it can be up to 3. For anything else it needs to be exactly 1." + "description": "Optional. The Resource Id of the delegated managed identity resource." } - }, - "indexingPolicy": { - "type": "object", + } + }, + "metadata": { + "description": "An AVM-aligned type for a role assignment.", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.1" + } + } + }, + "customerManagedKeyType": { + "type": "object", + "properties": { + "keyVaultResourceId": { + "type": "string", "metadata": { - "__bicep_resource_derived_type!": { - "source": "Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers@2025-04-15#properties/properties/properties/resource/properties/indexingPolicy" - }, - "description": "Optional. Indexing policy of the container." - }, - "nullable": true + "description": "Required. The resource ID of a key vault to reference a customer managed key for encryption from." + } }, - "uniqueKeyPolicyKeys": { - "type": "array", + "keyName": { + "type": "string", "metadata": { - "__bicep_resource_derived_type!": { - "source": "Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers@2025-04-15#properties/properties/properties/resource/properties/uniqueKeyPolicy/properties/uniqueKeys" - }, - "description": "Optional. The unique key policy configuration containing a list of unique keys that enforces uniqueness constraint on documents in the collection in the Azure Cosmos DB service." - }, - "nullable": true + "description": "Required. The name of the customer managed key to use for encryption." + } }, - "kind": { + "keyVersion": { "type": "string", - "allowedValues": [ - "Hash", - "MultiHash" - ], "nullable": true, "metadata": { - "description": "Optional. Default to Hash. Indicates the kind of algorithm used for partitioning." + "description": "Optional. The version of the customer managed key to reference for encryption. If not provided, the deployment will use the latest version available at deployment time." } }, - "version": { - "type": "int", - "allowedValues": [ - 1, - 2 - ], + "userAssignedIdentityResourceId": { + "type": "string", "nullable": true, "metadata": { - "description": "Optional. Default to 1 for Hash and 2 for MultiHash - 1 is not allowed for MultiHash. Version of the partition key definition." + "description": "Optional. User assigned identity to use when fetching the customer managed key. Required if no system assigned identity is available for use." } } }, "metadata": { - "description": "The type of a container.", + "description": "An AVM-aligned type for a customer-managed key. To be used if the resource type does not support auto-rotation of the customer-managed key.", "__bicep_imported_from!": { - "sourceTemplate": "sql-database/main.bicep" + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.0" } } }, @@ -25957,54 +22483,7 @@ "metadata": { "description": "An AVM-aligned type for a diagnostic setting. To be used if both logs & metrics are supported by the resource provider.", "__bicep_imported_from!": { - "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.1" - } - } - }, - "graphType": { - "type": "object", - "properties": { - "name": { - "type": "string", - "metadata": { - "description": "Required. Name of the graph." - } - }, - "tags": { - "type": "object", - "metadata": { - "__bicep_resource_derived_type!": { - "source": "Microsoft.DocumentDB/databaseAccounts/gremlinDatabases/graphs@2025-04-15#properties/tags" - }, - "description": "Optional. Tags of the Gremlin graph resource." - }, - "nullable": true - }, - "indexingPolicy": { - "type": "object", - "metadata": { - "__bicep_resource_derived_type!": { - "source": "Microsoft.DocumentDB/databaseAccounts/gremlinDatabases/graphs@2025-04-15#properties/properties/properties/resource/properties/indexingPolicy" - }, - "description": "Optional. Indexing policy of the graph." - }, - "nullable": true - }, - "partitionKeyPaths": { - "type": "array", - "metadata": { - "__bicep_resource_derived_type!": { - "source": "Microsoft.DocumentDB/databaseAccounts/gremlinDatabases/graphs@2025-04-15#properties/properties/properties/resource/properties/partitionKey/properties/paths" - }, - "description": "Optional. List of paths using which data within the container can be partitioned." - }, - "nullable": true - } - }, - "metadata": { - "description": "The type of a graph.", - "__bicep_imported_from!": { - "sourceTemplate": "gremlin-database/main.bicep" + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.0" } } }, @@ -26041,7 +22520,7 @@ "metadata": { "description": "An AVM-aligned type for a lock.", "__bicep_imported_from!": { - "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.1" + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.0" } } }, @@ -26069,57 +22548,25 @@ "metadata": { "description": "An AVM-aligned type for a managed identity configuration. To be used if both a system-assigned & user-assigned identities are supported by the resource provider.", "__bicep_imported_from!": { - "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.1" - } - } - }, - "nestedSqlRoleAssignmentType": { - "type": "object", - "properties": { - "name": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. Name unique identifier of the SQL Role Assignment." - } - }, - "principalId": { - "type": "string", - "metadata": { - "description": "Required. The unique identifier for the associated AAD principal in the AAD graph to which access is being granted through this Role Assignment. Tenant ID for the principal is inferred using the tenant associated with the subscription." - } - }, - "scope": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The data plane resource id for which access is being granted through this Role Assignment. Defaults to the root of the database account, but can also be scoped to e.g., the container and database level." - } - } - }, - "metadata": { - "description": "The type for the SQL Role Assignments.", - "__bicep_imported_from!": { - "sourceTemplate": "sql-role-definition/main.bicep", - "originalIdentifier": "sqlRoleAssignmentType" + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.0" } } }, - "privateEndpointMultiServiceType": { + "privateEndpointSingleServiceType": { "type": "object", "properties": { "name": { "type": "string", "nullable": true, "metadata": { - "description": "Optional. The name of the private endpoint." + "description": "Optional. The name of the Private Endpoint." } }, "location": { "type": "string", "nullable": true, "metadata": { - "description": "Optional. The location to deploy the private endpoint to." + "description": "Optional. The location to deploy the Private Endpoint to." } }, "privateLinkServiceConnectionName": { @@ -26131,8 +22578,9 @@ }, "service": { "type": "string", + "nullable": true, "metadata": { - "description": "Required. The subresource to deploy the private endpoint for. For example \"blob\", \"table\", \"queue\" or \"file\" for a Storage Account's Private Endpoints." + "description": "Optional. The subresource to deploy the Private Endpoint for. For example \"vault\" for a Key Vault Private Endpoint." } }, "subnetResourceId": { @@ -26149,10 +22597,10 @@ } }, "privateDnsZoneGroup": { - "$ref": "#/definitions/_1.privateEndpointPrivateDnsZoneGroupType", + "$ref": "#/definitions/_2.privateEndpointPrivateDnsZoneGroupType", "nullable": true, "metadata": { - "description": "Optional. The private DNS zone group to configure for the private endpoint." + "description": "Optional. The private DNS Zone Group to configure for the Private Endpoint." } }, "isManualConnection": { @@ -26173,7 +22621,7 @@ "customDnsConfigs": { "type": "array", "items": { - "$ref": "#/definitions/_1.privateEndpointCustomDnsConfigType" + "$ref": "#/definitions/_2.privateEndpointCustomDnsConfigType" }, "nullable": true, "metadata": { @@ -26183,11 +22631,11 @@ "ipConfigurations": { "type": "array", "items": { - "$ref": "#/definitions/_1.privateEndpointIpConfigurationType" + "$ref": "#/definitions/_2.privateEndpointIpConfigurationType" }, "nullable": true, "metadata": { - "description": "Optional. A list of IP configurations of the private endpoint. This will be used to map to the First Party Service endpoints." + "description": "Optional. A list of IP configurations of the Private Endpoint. This will be used to map to the first-party Service endpoints." } }, "applicationSecurityGroupResourceIds": { @@ -26197,18 +22645,18 @@ }, "nullable": true, "metadata": { - "description": "Optional. Application security groups in which the private endpoint IP configuration is included." + "description": "Optional. Application security groups in which the Private Endpoint IP configuration is included." } }, "customNetworkInterfaceName": { "type": "string", "nullable": true, "metadata": { - "description": "Optional. The custom name of the network interface attached to the private endpoint." + "description": "Optional. The custom name of the network interface attached to the Private Endpoint." } }, "lock": { - "$ref": "#/definitions/lockType", + "$ref": "#/definitions/_2.lockType", "nullable": true, "metadata": { "description": "Optional. Specify the type of lock." @@ -26217,7 +22665,7 @@ "roleAssignments": { "type": "array", "items": { - "$ref": "#/definitions/roleAssignmentType" + "$ref": "#/definitions/_2.roleAssignmentType" }, "nullable": true, "metadata": { @@ -26231,7 +22679,7 @@ "__bicep_resource_derived_type!": { "source": "Microsoft.Network/privateEndpoints@2024-07-01#properties/tags" }, - "description": "Optional. Tags to be applied on all resources/resource groups in this deployment." + "description": "Optional. Tags to be applied on all resources/Resource Groups in this deployment." } }, "enableTelemetry": { @@ -26243,7 +22691,7 @@ } }, "metadata": { - "description": "An AVM-aligned type for a private endpoint. To be used if the private endpoint's default service / groupId can NOT be assumed (i.e., for services that have more than one subresource, like Storage Account with Blob (blob, table, queue, file, ...).", + "description": "An AVM-aligned type for a private endpoint. To be used if the private endpoint's default service / groupId can be assumed (i.e., for services that only have one Private Endpoint type like 'vault' for key vault).", "__bicep_imported_from!": { "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.1" } @@ -26320,221 +22768,150 @@ "metadata": { "description": "An AVM-aligned type for a role assignment.", "__bicep_imported_from!": { - "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.1" + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.0" } } - } - }, - "parameters": { - "name": { - "type": "string", - "metadata": { - "description": "Required. The name of the account." - } - }, - "location": { - "type": "string", - "defaultValue": "[resourceGroup().location]", - "metadata": { - "description": "Optional. Defaults to the current resource group scope location. Location for all resources." - } }, - "tags": { + "secretsOutputType": { "type": "object", - "metadata": { - "__bicep_resource_derived_type!": { - "source": "Microsoft.DocumentDB/databaseAccounts@2024-11-15#properties/tags" - }, - "description": "Optional. Tags for the resource." + "properties": {}, + "additionalProperties": { + "$ref": "#/definitions/_1.secretSetOutputType", + "metadata": { + "description": "An exported secret's references." + } }, - "nullable": true - }, - "managedIdentities": { - "$ref": "#/definitions/managedIdentityAllType", - "nullable": true, "metadata": { - "description": "Optional. The managed identity definition for this resource." + "description": "A map of the exported secrets", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.0" + } } - }, - "databaseAccountOfferType": { + } + }, + "parameters": { + "name": { "type": "string", - "defaultValue": "Standard", - "allowedValues": [ - "Standard" - ], - "metadata": { - "description": "Optional. The offer type for the account. Defaults to \"Standard\"." - } - }, - "failoverLocations": { - "type": "array", - "items": { - "$ref": "#/definitions/failoverLocationType" - }, - "nullable": true, "metadata": { - "description": "Optional. The set of locations enabled for the account. Defaults to the location where the account is deployed." - } - }, - "zoneRedundant": { - "type": "bool", - "defaultValue": true, - "metadata": { - "description": "Optional. Indicates whether the single-region account is zone redundant. Defaults to true. This property is ignored for multi-region accounts." + "description": "Required. The name of Cognitive Services account." } }, - "defaultConsistencyLevel": { + "kind": { "type": "string", - "defaultValue": "Session", "allowedValues": [ - "Eventual", - "ConsistentPrefix", - "Session", - "BoundedStaleness", - "Strong" - ], - "metadata": { - "description": "Optional. The default consistency level of the account. Defaults to \"Session\"." - } - }, - "disableLocalAuthentication": { - "type": "bool", - "defaultValue": true, - "metadata": { - "description": "Optional. Opt-out of local authentication and ensure that only Microsoft Entra can be used exclusively for authentication. Defaults to true." - } - }, - "enableAnalyticalStorage": { - "type": "bool", - "defaultValue": false, - "metadata": { - "description": "Optional. Flag to indicate whether to enable storage analytics. Defaults to false." - } - }, - "enableAutomaticFailover": { - "type": "bool", - "defaultValue": true, - "metadata": { - "description": "Optional. Enable automatic failover for regions. Defaults to true." - } - }, - "enableFreeTier": { - "type": "bool", - "defaultValue": false, - "metadata": { - "description": "Optional. Flag to indicate whether \"Free Tier\" is enabled. Defaults to false." - } - }, - "enableMultipleWriteLocations": { - "type": "bool", - "defaultValue": false, - "metadata": { - "description": "Optional. Enables the account to write in multiple locations. Periodic backup must be used if enabled. Defaults to false." - } - }, - "disableKeyBasedMetadataWriteAccess": { - "type": "bool", - "defaultValue": true, - "metadata": { - "description": "Optional. Disable write operations on metadata resources (databases, containers, throughput) via account keys. Defaults to true." - } - }, - "maxStalenessPrefix": { - "type": "int", - "defaultValue": 100000, - "minValue": 1, - "maxValue": 2147483647, - "metadata": { - "description": "Optional. The maximum stale requests. Required for \"BoundedStaleness\" consistency level. Valid ranges, Single Region: 10 to 1000000. Multi Region: 100000 to 1000000. Defaults to 100000." - } - }, - "maxIntervalInSeconds": { - "type": "int", - "defaultValue": 300, - "minValue": 5, - "maxValue": 86400, + "AIServices", + "AnomalyDetector", + "CognitiveServices", + "ComputerVision", + "ContentModerator", + "ContentSafety", + "ConversationalLanguageUnderstanding", + "CustomVision.Prediction", + "CustomVision.Training", + "Face", + "FormRecognizer", + "HealthInsights", + "ImmersiveReader", + "Internal.AllInOne", + "LUIS", + "LUIS.Authoring", + "LanguageAuthoring", + "MetricsAdvisor", + "OpenAI", + "Personalizer", + "QnAMaker.v2", + "SpeechServices", + "TextAnalytics", + "TextTranslation" + ], "metadata": { - "description": "Optional. The maximum lag time in minutes. Required for \"BoundedStaleness\" consistency level. Valid ranges, Single Region: 5 to 84600. Multi Region: 300 to 86400. Defaults to 300." + "description": "Required. Kind of the Cognitive Services account. Use 'Get-AzCognitiveServicesAccountSku' to determine a valid combinations of 'kind' and 'SKU' for your Azure region." } }, - "serverVersion": { + "sku": { "type": "string", - "defaultValue": "4.2", + "defaultValue": "S0", "allowedValues": [ - "3.2", - "3.6", - "4.0", - "4.2", - "5.0", - "6.0", - "7.0" + "C2", + "C3", + "C4", + "F0", + "F1", + "S", + "S0", + "S1", + "S10", + "S2", + "S3", + "S4", + "S5", + "S6", + "S7", + "S8", + "S9", + "DC0" ], "metadata": { - "description": "Optional. Specifies the MongoDB server version to use if using Azure Cosmos DB for MongoDB RU. Defaults to \"4.2\"." + "description": "Optional. SKU of the Cognitive Services account. Use 'Get-AzCognitiveServicesAccountSku' to determine a valid combinations of 'kind' and 'SKU' for your Azure region." } }, - "sqlDatabases": { - "type": "array", - "items": { - "$ref": "#/definitions/sqlDatabaseType" - }, - "nullable": true, + "location": { + "type": "string", + "defaultValue": "[resourceGroup().location]", "metadata": { - "description": "Optional. Configuration for databases when using Azure Cosmos DB for NoSQL." + "description": "Optional. Location for all Resources." } }, - "mongodbDatabases": { + "diagnosticSettings": { "type": "array", "items": { - "$ref": "#/definitions/mongoDbType" + "$ref": "#/definitions/diagnosticSettingFullType" }, "nullable": true, "metadata": { - "description": "Optional. Configuration for databases when using Azure Cosmos DB for MongoDB RU." + "description": "Optional. The diagnostic settings of the service." } }, - "gremlinDatabases": { - "type": "array", - "items": { - "$ref": "#/definitions/gremlinDatabaseType" - }, + "publicNetworkAccess": { + "type": "string", "nullable": true, + "allowedValues": [ + "Enabled", + "Disabled" + ], "metadata": { - "description": "Optional. Configuration for databases when using Azure Cosmos DB for Apache Gremlin." + "description": "Optional. Whether or not public network access is allowed for this resource. For security reasons it should be disabled. If not specified, it will be disabled by default if private endpoints are set and networkAcls are not set." } }, - "tables": { - "type": "array", - "items": { - "$ref": "#/definitions/tableType" - }, + "customSubDomainName": { + "type": "string", "nullable": true, "metadata": { - "description": "Optional. Configuration for databases when using Azure Cosmos DB for Table." + "description": "Conditional. Subdomain name used for token-based authentication. Required if 'networkAcls' or 'privateEndpoints' are set." } }, - "cassandraKeyspaces": { - "type": "array", - "items": { - "$ref": "#/definitions/cassandraKeyspaceType" - }, + "networkAcls": { + "type": "object", "nullable": true, "metadata": { - "description": "Optional. Configuration for keyspaces when using Azure Cosmos DB for Apache Cassandra." + "description": "Optional. A collection of rules governing the accessibility from specific network locations." } }, - "enableTelemetry": { - "type": "bool", - "defaultValue": true, + "networkInjections": { + "$ref": "#/definitions/networkInjectionType", + "nullable": true, "metadata": { - "description": "Optional. Enable/Disable usage telemetry for module." + "description": "Optional. Specifies in AI Foundry where virtual network injection occurs to secure scenarios like Agents entirely within a private network." } }, - "totalThroughputLimit": { - "type": "int", - "defaultValue": -1, + "privateEndpoints": { + "type": "array", + "items": { + "$ref": "#/definitions/privateEndpointSingleServiceType" + }, + "nullable": true, "metadata": { - "description": "Optional. The total throughput limit imposed on this account in request units per second (RU/s). Default to unlimited throughput." + "description": "Optional. Configuration details for private endpoints. For security reasons, it is recommended to use private endpoints whenever possible." } }, "lock": { @@ -26551,219 +22928,128 @@ }, "nullable": true, "metadata": { - "description": "Optional. An array of control plane Azure role-based access control assignments." - } - }, - "sqlRoleDefinitions": { - "type": "array", - "items": { - "$ref": "#/definitions/sqlRoleDefinitionType" - }, - "nullable": true, - "metadata": { - "description": "Optional. Configurations for Azure Cosmos DB for NoSQL native role-based access control definitions. Allows the creations of custom role definitions." + "description": "Optional. Array of role assignments to create." } }, - "sqlRoleAssignments": { - "type": "array", - "items": { - "$ref": "#/definitions/sqlRoleAssignmentType" - }, + "tags": { + "type": "object", "nullable": true, "metadata": { - "description": "Optional. Configurations for Azure Cosmos DB for NoSQL native role-based access control assignments." + "description": "Optional. Tags of the resource." } }, - "cassandraRoleDefinitions": { + "allowedFqdnList": { "type": "array", - "items": { - "$ref": "#/definitions/cassandraRoleDefinitionType" - }, "nullable": true, "metadata": { - "description": "Optional. Configurations for Azure Cosmos DB for Apache Cassandra native role-based access control definitions. Allows the creations of custom role definitions." + "description": "Optional. List of allowed FQDN." } }, - "cassandraRoleAssignments": { - "type": "array", - "items": { - "$ref": "#/definitions/cassandraStandaloneRoleAssignmentType" - }, + "apiProperties": { + "type": "object", "nullable": true, "metadata": { - "description": "Optional. Azure Cosmos DB for Apache Cassandra native data plane role-based access control assignments. Each assignment references a role definition unique identifier and a principal identifier." + "description": "Optional. The API properties for special APIs." } }, - "diagnosticSettings": { - "type": "array", - "items": { - "$ref": "#/definitions/diagnosticSettingFullType" - }, - "nullable": true, + "disableLocalAuth": { + "type": "bool", + "defaultValue": true, "metadata": { - "description": "Optional. The diagnostic settings for the service." + "description": "Optional. Allow only Azure AD authentication. Should be enabled for security reasons." } }, - "capabilitiesToAdd": { - "type": "array", - "items": { - "type": "string" - }, + "customerManagedKey": { + "$ref": "#/definitions/customerManagedKeyType", "nullable": true, - "allowedValues": [ - "EnableCassandra", - "EnableTable", - "EnableGremlin", - "EnableMongo", - "DisableRateLimitingResponses", - "EnableServerless", - "EnableNoSQLVectorSearch", - "EnableNoSQLFullTextSearch", - "EnableMaterializedViews", - "DeleteAllItemsByPartitionKey" - ], - "metadata": { - "description": "Optional. A list of Azure Cosmos DB specific capabilities for the account." - } - }, - "backupPolicyType": { - "type": "string", - "defaultValue": "Continuous", - "allowedValues": [ - "Periodic", - "Continuous" - ], "metadata": { - "description": "Optional. Configures the backup mode. Periodic backup must be used if multiple write locations are used. Defaults to \"Continuous\"." + "description": "Optional. The customer managed key definition." } }, - "backupPolicyContinuousTier": { - "type": "string", - "defaultValue": "Continuous30Days", - "allowedValues": [ - "Continuous30Days", - "Continuous7Days" - ], + "dynamicThrottlingEnabled": { + "type": "bool", + "defaultValue": false, "metadata": { - "description": "Optional. Configuration values to specify the retention period for continuous mode backup. Default to \"Continuous30Days\"." + "description": "Optional. The flag to enable dynamic throttling." } }, - "backupIntervalInMinutes": { - "type": "int", - "defaultValue": 240, - "minValue": 60, - "maxValue": 1440, + "migrationToken": { + "type": "securestring", + "nullable": true, "metadata": { - "description": "Optional. An integer representing the interval in minutes between two backups. This setting only applies to the periodic backup type. Defaults to 240." + "description": "Optional. Resource migration token." } }, - "backupRetentionIntervalInHours": { - "type": "int", - "defaultValue": 8, - "minValue": 2, - "maxValue": 720, + "restore": { + "type": "bool", + "defaultValue": false, "metadata": { - "description": "Optional. An integer representing the time (in hours) that each backup is retained. This setting only applies to the periodic backup type. Defaults to 8." + "description": "Optional. Restore a soft-deleted cognitive service at deployment time. Will fail if no such soft-deleted resource exists." } }, - "backupStorageRedundancy": { - "type": "string", - "defaultValue": "Local", - "allowedValues": [ - "Geo", - "Local", - "Zone" - ], + "restrictOutboundNetworkAccess": { + "type": "bool", + "defaultValue": true, "metadata": { - "description": "Optional. Setting that indicates the type of backup residency. This setting only applies to the periodic backup type. Defaults to \"Local\"." + "description": "Optional. Restrict outbound network access." } }, - "privateEndpoints": { + "userOwnedStorage": { "type": "array", - "items": { - "$ref": "#/definitions/privateEndpointMultiServiceType" - }, - "nullable": true, "metadata": { - "description": "Optional. Configuration details for private endpoints. For security reasons, it is advised to use private endpoints whenever possible." - } - }, - "networkRestrictions": { - "$ref": "#/definitions/networkRestrictionType", - "defaultValue": { - "ipRules": [], - "virtualNetworkRules": [], - "publicNetworkAccess": "Disabled" + "__bicep_resource_derived_type!": { + "source": "Microsoft.CognitiveServices/accounts@2025-04-01-preview#properties/properties/properties/userOwnedStorage" + }, + "description": "Optional. The storage accounts for this resource." }, - "metadata": { - "description": "Optional. The network configuration of this module. Defaults to `{ ipRules: [], virtualNetworkRules: [], publicNetworkAccess: 'Disabled' }`." - } + "nullable": true }, - "minimumTlsVersion": { - "type": "string", - "defaultValue": "Tls12", - "allowedValues": [ - "Tls12" - ], + "managedIdentities": { + "$ref": "#/definitions/managedIdentityAllType", + "nullable": true, "metadata": { - "description": "Optional. Setting that indicates the minimum allowed TLS version. Azure Cosmos DB for MongoDB RU and Apache Cassandra only work with TLS 1.2 or later. Defaults to \"Tls12\" (TLS 1.2)." + "description": "Optional. The managed identity definition for this resource." } }, - "enableBurstCapacity": { + "enableTelemetry": { "type": "bool", "defaultValue": true, "metadata": { - "description": "Optional. Flag to indicate enabling/disabling of Burst Capacity feature on the account. Cannot be enabled for serverless accounts." + "description": "Optional. Enable/Disable usage telemetry for module." } }, - "enableCassandraConnector": { - "type": "bool", - "defaultValue": false, + "deployments": { + "type": "array", + "items": { + "$ref": "#/definitions/deploymentType" + }, + "nullable": true, "metadata": { - "description": "Optional. Enables the cassandra connector on the Cosmos DB C* account." + "description": "Optional. Array of deployments about cognitive service accounts to create." } }, - "enablePartitionMerge": { - "type": "bool", - "defaultValue": false, + "secretsExportConfiguration": { + "$ref": "#/definitions/secretsExportConfigurationType", + "nullable": true, "metadata": { - "description": "Optional. Flag to enable/disable the 'Partition Merge' feature on the account." + "description": "Optional. Key vault reference and secret settings for the module's secrets export." } }, - "enablePerRegionPerPartitionAutoscale": { + "allowProjectManagement": { "type": "bool", - "defaultValue": false, + "nullable": true, "metadata": { - "description": "Optional. Flag to enable/disable the 'PerRegionPerPartitionAutoscale' feature on the account." + "description": "Optional. Enable/Disable project management feature for AI Foundry." } }, - "analyticalStorageConfiguration": { - "type": "object", - "metadata": { - "__bicep_resource_derived_type!": { - "source": "Microsoft.DocumentDB/databaseAccounts@2025-04-15#properties/properties/properties/analyticalStorageConfiguration" - }, - "description": "Optional. Analytical storage specific properties." - }, - "nullable": true - }, - "cors": { + "commitmentPlans": { "type": "array", - "metadata": { - "__bicep_resource_derived_type!": { - "source": "Microsoft.DocumentDB/databaseAccounts@2025-04-15#properties/properties/properties/cors" - }, - "description": "Optional. The CORS policy for the Cosmos DB database account." - }, - "nullable": true - }, - "defaultIdentity": { - "$ref": "#/definitions/defaultIdentityType", - "defaultValue": { - "name": "FirstPartyIdentity" + "items": { + "$ref": "#/definitions/commitmentPlanType" }, + "nullable": true, "metadata": { - "description": "Optional. The default identity for accessing key vault used in features like customer managed keys. Use `FirstPartyIdentity` to use the tenant-level CosmosDB enterprise application. The default identity needs to be explicitly set by the users." + "description": "Optional. Commitment plans to deploy for the cognitive services account." } } }, @@ -26772,31 +23058,61 @@ { "name": "formattedRoleAssignments", "count": "[length(coalesce(parameters('roleAssignments'), createArray()))]", - "input": "[union(coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')], createObject('roleDefinitionId', coalesce(tryGet(variables('builtInControlPlaneRoleNames'), coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName), if(contains(coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName, '/providers/Microsoft.Authorization/roleDefinitions/'), coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName, subscriptionResourceId('Microsoft.Authorization/roleDefinitions', coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName)))))]" + "input": "[union(coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')], createObject('roleDefinitionId', coalesce(tryGet(variables('builtInRoleNames'), coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName), if(contains(coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName, '/providers/Microsoft.Authorization/roleDefinitions/'), coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName, subscriptionResourceId('Microsoft.Authorization/roleDefinitions', coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName)))))]" } ], "enableReferencedModulesTelemetry": false, "formattedUserAssignedIdentities": "[reduce(map(coalesce(tryGet(parameters('managedIdentities'), 'userAssignedResourceIds'), createArray()), lambda('id', createObject(format('{0}', lambdaVariables('id')), createObject()))), createObject(), lambda('cur', 'next', union(lambdaVariables('cur'), lambdaVariables('next'))))]", - "identity": "[if(not(empty(parameters('managedIdentities'))), createObject('type', if(coalesce(tryGet(parameters('managedIdentities'), 'systemAssigned'), false()), if(not(empty(coalesce(tryGet(parameters('managedIdentities'), 'userAssignedResourceIds'), createObject()))), 'SystemAssigned,UserAssigned', 'SystemAssigned'), if(not(empty(coalesce(tryGet(parameters('managedIdentities'), 'userAssignedResourceIds'), createObject()))), 'UserAssigned', null())), 'userAssignedIdentities', if(not(empty(variables('formattedUserAssignedIdentities'))), variables('formattedUserAssignedIdentities'), null())), null())]", - "builtInControlPlaneRoleNames": { + "identity": "[if(not(empty(parameters('managedIdentities'))), createObject('type', if(coalesce(tryGet(parameters('managedIdentities'), 'systemAssigned'), false()), if(not(empty(coalesce(tryGet(parameters('managedIdentities'), 'userAssignedResourceIds'), createObject()))), 'SystemAssigned, UserAssigned', 'SystemAssigned'), if(not(empty(coalesce(tryGet(parameters('managedIdentities'), 'userAssignedResourceIds'), createObject()))), 'UserAssigned', null())), 'userAssignedIdentities', if(not(empty(variables('formattedUserAssignedIdentities'))), variables('formattedUserAssignedIdentities'), null())), null())]", + "builtInRoleNames": { + "Cognitive Services Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '25fbc0a9-bd7c-42a3-aa1a-3b75d497ee68')]", + "Cognitive Services Custom Vision Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'c1ff6cc2-c111-46fe-8896-e0ef812ad9f3')]", + "Cognitive Services Custom Vision Deployment": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '5c4089e1-6d96-4d2f-b296-c1bc7137275f')]", + "Cognitive Services Custom Vision Labeler": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '88424f51-ebe7-446f-bc41-7fa16989e96c')]", + "Cognitive Services Custom Vision Reader": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '93586559-c37d-4a6b-ba08-b9f0940c2d73')]", + "Cognitive Services Custom Vision Trainer": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '0a5ae4ab-0d65-4eeb-be61-29fc9b54394b')]", + "Cognitive Services Data Reader (Preview)": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'b59867f0-fa02-499b-be73-45a86b5b3e1c')]", + "Cognitive Services Face Recognizer": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '9894cab4-e18a-44aa-828b-cb588cd6f2d7')]", + "Cognitive Services Immersive Reader User": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'b2de6794-95db-4659-8781-7e080d3f2b9d')]", + "Cognitive Services Language Owner": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'f07febfe-79bc-46b1-8b37-790e26e6e498')]", + "Cognitive Services Language Reader": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '7628b7b8-a8b2-4cdc-b46f-e9b35248918e')]", + "Cognitive Services Language Writer": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'f2310ca1-dc64-4889-bb49-c8e0fa3d47a8')]", + "Cognitive Services LUIS Owner": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'f72c8140-2111-481c-87ff-72b910f6e3f8')]", + "Cognitive Services LUIS Reader": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '18e81cdc-4e98-4e29-a639-e7d10c5a6226')]", + "Cognitive Services LUIS Writer": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '6322a993-d5c9-4bed-b113-e49bbea25b27')]", + "Cognitive Services Metrics Advisor Administrator": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'cb43c632-a144-4ec5-977c-e80c4affc34a')]", + "Cognitive Services Metrics Advisor User": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '3b20f47b-3825-43cb-8114-4bd2201156a8')]", + "Cognitive Services OpenAI Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'a001fd3d-188f-4b5d-821b-7da978bf7442')]", + "Cognitive Services OpenAI User": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '5e0bd9bd-7b93-4f28-af87-19fc36ad61bd')]", + "Cognitive Services QnA Maker Editor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'f4cc2bf9-21be-47a1-bdf1-5c5804381025')]", + "Cognitive Services QnA Maker Reader": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '466ccd10-b268-4a11-b098-b4849f024126')]", + "Cognitive Services Speech Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '0e75ca1e-0464-4b4d-8b93-68208a576181')]", + "Cognitive Services Speech User": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'f2dc8367-1007-4938-bd23-fe263f013447')]", + "Cognitive Services User": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'a97b65f3-24c7-4388-baec-2e87135dc908')]", + "Azure AI Developer": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '64702f94-c441-49e6-a78b-ef80e0188fee')]", "Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'b24988ac-6180-42a0-ab88-20f7382dd24c')]", - "Cosmos DB Account Reader Role": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'fbdf93bf-df7d-467e-a4d2-9458aa1360c8')]", - "Cosmos DB Operator": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '230815da-be43-4aae-9cb4-875f7bd000aa')]", - "CosmosBackupOperator": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'db7b14f2-5adf-42da-9f96-f2ee17bab5cb')]", - "CosmosRestoreOperator": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '5432c526-bc82-444a-b7ba-57c5b0b5b34f')]", - "DocumentDB Account Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '5bd9cd88-fe45-4216-938b-f97437e15450')]", "Owner": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '8e3af657-a8ff-443c-a75c-2fe8c4bcb635')]", "Reader": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'acdd72a7-3385-48ef-bd42-f606fba81ae7')]", "Role Based Access Control Administrator": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'f58310d9-a9f6-439a-9e8d-f62e7b41a168')]", "User Access Administrator": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '18d7d88d-d35e-4fb5-a5c3-7773c20a72d9')]" - } + }, + "isHSMManagedCMK": "[equals(tryGet(split(coalesce(tryGet(parameters('customerManagedKey'), 'keyVaultResourceId'), ''), '/'), 7), 'managedHSMs')]" }, "resources": { + "cMKKeyVault::cMKKey": { + "condition": "[and(and(not(empty(parameters('customerManagedKey'))), not(variables('isHSMManagedCMK'))), and(not(empty(parameters('customerManagedKey'))), not(variables('isHSMManagedCMK'))))]", + "existing": true, + "type": "Microsoft.KeyVault/vaults/keys", + "apiVersion": "2025-05-01", + "subscriptionId": "[split(tryGet(parameters('customerManagedKey'), 'keyVaultResourceId'), '/')[2]]", + "resourceGroup": "[split(tryGet(parameters('customerManagedKey'), 'keyVaultResourceId'), '/')[4]]", + "name": "[format('{0}/{1}', last(split(tryGet(parameters('customerManagedKey'), 'keyVaultResourceId'), '/')), tryGet(parameters('customerManagedKey'), 'keyName'))]" + }, "avmTelemetry": { "condition": "[parameters('enableTelemetry')]", "type": "Microsoft.Resources/deployments", - "apiVersion": "2024-07-01", - "name": "[format('46d3xbcp.res.documentdb-databaseaccount.{0}.{1}', replace('0.18.0', '.', '-'), substring(uniqueString(deployment().name, parameters('location')), 0, 4))]", + "apiVersion": "2024-03-01", + "name": "[format('46d3xbcp.res.cognitiveservices-account.{0}.{1}', replace('0.14.0', '.', '-'), substring(uniqueString(deployment().name, parameters('location')), 0, 4))]", "properties": { "mode": "Incremental", "template": { @@ -26812,38 +23128,112 @@ } } }, - "databaseAccount": { - "type": "Microsoft.DocumentDB/databaseAccounts", - "apiVersion": "2025-04-15", + "cMKKeyVault": { + "condition": "[and(not(empty(parameters('customerManagedKey'))), not(variables('isHSMManagedCMK')))]", + "existing": true, + "type": "Microsoft.KeyVault/vaults", + "apiVersion": "2025-05-01", + "subscriptionId": "[split(tryGet(parameters('customerManagedKey'), 'keyVaultResourceId'), '/')[2]]", + "resourceGroup": "[split(tryGet(parameters('customerManagedKey'), 'keyVaultResourceId'), '/')[4]]", + "name": "[last(split(tryGet(parameters('customerManagedKey'), 'keyVaultResourceId'), '/'))]" + }, + "cMKUserAssignedIdentity": { + "condition": "[not(empty(tryGet(parameters('customerManagedKey'), 'userAssignedIdentityResourceId')))]", + "existing": true, + "type": "Microsoft.ManagedIdentity/userAssignedIdentities", + "apiVersion": "2025-01-31-preview", + "subscriptionId": "[split(tryGet(parameters('customerManagedKey'), 'userAssignedIdentityResourceId'), '/')[2]]", + "resourceGroup": "[split(tryGet(parameters('customerManagedKey'), 'userAssignedIdentityResourceId'), '/')[4]]", + "name": "[last(split(tryGet(parameters('customerManagedKey'), 'userAssignedIdentityResourceId'), '/'))]" + }, + "cognitiveService": { + "type": "Microsoft.CognitiveServices/accounts", + "apiVersion": "2025-06-01", "name": "[parameters('name')]", + "kind": "[parameters('kind')]", + "identity": "[variables('identity')]", "location": "[parameters('location')]", "tags": "[parameters('tags')]", - "identity": "[variables('identity')]", - "kind": "[if(not(empty(parameters('mongodbDatabases'))), 'MongoDB', 'GlobalDocumentDB')]", - "properties": "[shallowMerge(createArray(createObject('enableBurstCapacity', if(not(contains(coalesce(parameters('capabilitiesToAdd'), createArray()), 'EnableServerless')), parameters('enableBurstCapacity'), false()), 'analyticalStorageConfiguration', parameters('analyticalStorageConfiguration'), 'defaultIdentity', if(and(not(empty(parameters('defaultIdentity'))), not(equals(tryGet(parameters('defaultIdentity'), 'name'), 'UserAssignedIdentity'))), parameters('defaultIdentity').name, format('UserAssignedIdentity={0}', tryGet(parameters('defaultIdentity'), 'resourceId'))), 'enablePartitionMerge', parameters('enablePartitionMerge'), 'enablePerRegionPerPartitionAutoscale', parameters('enablePerRegionPerPartitionAutoscale'), 'databaseAccountOfferType', parameters('databaseAccountOfferType'), 'backupPolicy', shallowMerge(createArray(createObject('type', parameters('backupPolicyType')), if(equals(parameters('backupPolicyType'), 'Continuous'), createObject('continuousModeProperties', createObject('tier', parameters('backupPolicyContinuousTier'))), createObject()), if(equals(parameters('backupPolicyType'), 'Periodic'), createObject('periodicModeProperties', createObject('backupIntervalInMinutes', parameters('backupIntervalInMinutes'), 'backupRetentionIntervalInHours', parameters('backupRetentionIntervalInHours'), 'backupStorageRedundancy', parameters('backupStorageRedundancy'))), createObject()))), 'capabilities', map(coalesce(parameters('capabilitiesToAdd'), createArray()), lambda('capability', createObject('name', lambdaVariables('capability'))))), if(not(empty(parameters('cors'))), createObject('cors', parameters('cors')), createObject()), if(contains(coalesce(parameters('capabilitiesToAdd'), createArray()), 'EnableCassandra'), createObject('connectorOffer', if(parameters('enableCassandraConnector'), 'Small', null()), 'enableCassandraConnector', parameters('enableCassandraConnector')), createObject()), createObject('minimalTlsVersion', parameters('minimumTlsVersion'), 'capacity', createObject('totalThroughputLimit', parameters('totalThroughputLimit')), 'publicNetworkAccess', coalesce(tryGet(parameters('networkRestrictions'), 'publicNetworkAccess'), 'Disabled')), if(or(or(or(or(not(empty(parameters('sqlDatabases'))), not(empty(parameters('mongodbDatabases')))), not(empty(parameters('gremlinDatabases')))), not(empty(parameters('tables')))), not(empty(parameters('cassandraKeyspaces')))), createObject('consistencyPolicy', shallowMerge(createArray(createObject('defaultConsistencyLevel', parameters('defaultConsistencyLevel')), if(equals(parameters('defaultConsistencyLevel'), 'BoundedStaleness'), createObject('maxStalenessPrefix', parameters('maxStalenessPrefix'), 'maxIntervalInSeconds', parameters('maxIntervalInSeconds')), createObject()))), 'enableMultipleWriteLocations', parameters('enableMultipleWriteLocations'), 'locations', if(not(empty(parameters('failoverLocations'))), map(parameters('failoverLocations'), lambda('failoverLocation', createObject('failoverPriority', lambdaVariables('failoverLocation').failoverPriority, 'locationName', lambdaVariables('failoverLocation').locationName, 'isZoneRedundant', coalesce(tryGet(lambdaVariables('failoverLocation'), 'isZoneRedundant'), true())))), createArray(createObject('failoverPriority', 0, 'locationName', parameters('location'), 'isZoneRedundant', parameters('zoneRedundant')))), 'ipRules', map(coalesce(tryGet(parameters('networkRestrictions'), 'ipRules'), createArray()), lambda('ipRule', createObject('ipAddressOrRange', lambdaVariables('ipRule')))), 'virtualNetworkRules', map(coalesce(tryGet(parameters('networkRestrictions'), 'virtualNetworkRules'), createArray()), lambda('rule', createObject('id', lambdaVariables('rule').subnetResourceId, 'ignoreMissingVNetServiceEndpoint', false()))), 'networkAclBypass', coalesce(tryGet(parameters('networkRestrictions'), 'networkAclBypass'), 'None'), 'networkAclBypassResourceIds', tryGet(parameters('networkRestrictions'), 'networkAclBypassResourceIds'), 'isVirtualNetworkFilterEnabled', or(not(empty(tryGet(parameters('networkRestrictions'), 'ipRules'))), not(empty(tryGet(parameters('networkRestrictions'), 'virtualNetworkRules')))), 'enableFreeTier', parameters('enableFreeTier'), 'enableAutomaticFailover', parameters('enableAutomaticFailover'), 'enableAnalyticalStorage', parameters('enableAnalyticalStorage')), createObject()), if(or(or(not(empty(parameters('mongodbDatabases'))), not(empty(parameters('gremlinDatabases')))), not(empty(parameters('cassandraKeyspaces')))), createObject('disableLocalAuth', false(), 'disableKeyBasedMetadataWriteAccess', false()), createObject('disableLocalAuth', parameters('disableLocalAuthentication'), 'disableKeyBasedMetadataWriteAccess', parameters('disableKeyBasedMetadataWriteAccess'))), if(not(empty(parameters('mongodbDatabases'))), createObject('apiProperties', createObject('serverVersion', parameters('serverVersion'))), createObject())))]" + "sku": { + "name": "[parameters('sku')]" + }, + "properties": { + "allowProjectManagement": "[parameters('allowProjectManagement')]", + "customSubDomainName": "[parameters('customSubDomainName')]", + "networkAcls": "[if(not(empty(coalesce(parameters('networkAcls'), createObject()))), createObject('defaultAction', tryGet(parameters('networkAcls'), 'defaultAction'), 'virtualNetworkRules', coalesce(tryGet(parameters('networkAcls'), 'virtualNetworkRules'), createArray()), 'ipRules', coalesce(tryGet(parameters('networkAcls'), 'ipRules'), createArray())), null())]", + "networkInjections": "[if(not(empty(parameters('networkInjections'))), createArray(createObject('scenario', tryGet(parameters('networkInjections'), 'scenario'), 'subnetArmId', tryGet(parameters('networkInjections'), 'subnetResourceId'), 'useMicrosoftManagedNetwork', coalesce(tryGet(parameters('networkInjections'), 'useMicrosoftManagedNetwork'), false()))), null())]", + "publicNetworkAccess": "[if(not(equals(parameters('publicNetworkAccess'), null())), parameters('publicNetworkAccess'), if(not(empty(parameters('networkAcls'))), 'Enabled', 'Disabled'))]", + "allowedFqdnList": "[parameters('allowedFqdnList')]", + "apiProperties": "[parameters('apiProperties')]", + "disableLocalAuth": "[parameters('disableLocalAuth')]", + "encryption": "[if(not(empty(parameters('customerManagedKey'))), createObject('keySource', 'Microsoft.KeyVault', 'keyVaultProperties', createObject('identityClientId', if(not(empty(coalesce(tryGet(parameters('customerManagedKey'), 'userAssignedIdentityResourceId'), ''))), reference('cMKUserAssignedIdentity').clientId, null()), 'keyVaultUri', if(not(variables('isHSMManagedCMK')), reference('cMKKeyVault').vaultUri, format('https://{0}.managedhsm.azure.net/', last(split(parameters('customerManagedKey').keyVaultResourceId, '/')))), 'keyName', parameters('customerManagedKey').keyName, 'keyVersion', if(not(empty(tryGet(parameters('customerManagedKey'), 'keyVersion'))), parameters('customerManagedKey').keyVersion, if(not(variables('isHSMManagedCMK')), last(split(reference('cMKKeyVault::cMKKey').keyUriWithVersion, '/')), fail('Managed HSM CMK encryption requires specifying the ''keyVersion''.'))))), null())]", + "migrationToken": "[parameters('migrationToken')]", + "restore": "[parameters('restore')]", + "restrictOutboundNetworkAccess": "[parameters('restrictOutboundNetworkAccess')]", + "userOwnedStorage": "[if(not(empty(parameters('userOwnedStorage'))), parameters('userOwnedStorage'), null())]", + "dynamicThrottlingEnabled": "[parameters('dynamicThrottlingEnabled')]" + }, + "dependsOn": [ + "cMKKeyVault", + "cMKKeyVault::cMKKey", + "cMKUserAssignedIdentity" + ] }, - "databaseAccount_lock": { + "cognitiveService_deployments": { + "copy": { + "name": "cognitiveService_deployments", + "count": "[length(coalesce(parameters('deployments'), createArray()))]", + "mode": "serial", + "batchSize": 1 + }, + "type": "Microsoft.CognitiveServices/accounts/deployments", + "apiVersion": "2025-06-01", + "name": "[format('{0}/{1}', parameters('name'), coalesce(tryGet(coalesce(parameters('deployments'), createArray())[copyIndex()], 'name'), format('{0}-deployments', parameters('name'))))]", + "properties": { + "model": "[coalesce(parameters('deployments'), createArray())[copyIndex()].model]", + "raiPolicyName": "[tryGet(coalesce(parameters('deployments'), createArray())[copyIndex()], 'raiPolicyName')]", + "versionUpgradeOption": "[tryGet(coalesce(parameters('deployments'), createArray())[copyIndex()], 'versionUpgradeOption')]" + }, + "sku": "[coalesce(tryGet(coalesce(parameters('deployments'), createArray())[copyIndex()], 'sku'), createObject('name', parameters('sku'), 'capacity', tryGet(parameters('sku'), 'capacity'), 'tier', tryGet(parameters('sku'), 'tier'), 'size', tryGet(parameters('sku'), 'size'), 'family', tryGet(parameters('sku'), 'family')))]", + "dependsOn": [ + "cognitiveService" + ] + }, + "cognitiveService_lock": { "condition": "[and(not(empty(coalesce(parameters('lock'), createObject()))), not(equals(tryGet(parameters('lock'), 'kind'), 'None')))]", "type": "Microsoft.Authorization/locks", "apiVersion": "2020-05-01", - "scope": "[format('Microsoft.DocumentDB/databaseAccounts/{0}', parameters('name'))]", + "scope": "[format('Microsoft.CognitiveServices/accounts/{0}', parameters('name'))]", "name": "[coalesce(tryGet(parameters('lock'), 'name'), format('lock-{0}', parameters('name')))]", "properties": { "level": "[coalesce(tryGet(parameters('lock'), 'kind'), '')]", "notes": "[coalesce(tryGet(parameters('lock'), 'notes'), if(equals(tryGet(parameters('lock'), 'kind'), 'CanNotDelete'), 'Cannot delete resource or child resources.', 'Cannot delete or modify the resource or child resources.'))]" }, "dependsOn": [ - "databaseAccount" + "cognitiveService" ] }, - "databaseAccount_diagnosticSettings": { + "cognitiveService_commitmentPlans": { "copy": { - "name": "databaseAccount_diagnosticSettings", + "name": "cognitiveService_commitmentPlans", + "count": "[length(coalesce(parameters('commitmentPlans'), createArray()))]" + }, + "type": "Microsoft.CognitiveServices/accounts/commitmentPlans", + "apiVersion": "2025-06-01", + "name": "[format('{0}/{1}', parameters('name'), format('{0}-{1}', coalesce(parameters('commitmentPlans'), createArray())[copyIndex()].hostingModel, coalesce(parameters('commitmentPlans'), createArray())[copyIndex()].planType))]", + "properties": "[coalesce(parameters('commitmentPlans'), createArray())[copyIndex()]]", + "dependsOn": [ + "cognitiveService" + ] + }, + "cognitiveService_diagnosticSettings": { + "copy": { + "name": "cognitiveService_diagnosticSettings", "count": "[length(coalesce(parameters('diagnosticSettings'), createArray()))]" }, "type": "Microsoft.Insights/diagnosticSettings", "apiVersion": "2021-05-01-preview", - "scope": "[format('Microsoft.DocumentDB/databaseAccounts/{0}', parameters('name'))]", + "scope": "[format('Microsoft.CognitiveServices/accounts/{0}', parameters('name'))]", "name": "[coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'name'), format('{0}-diagnosticSettings', parameters('name')))]", "properties": { "copy": [ @@ -26874,18 +23264,18 @@ "logAnalyticsDestinationType": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'logAnalyticsDestinationType')]" }, "dependsOn": [ - "databaseAccount" + "cognitiveService" ] }, - "databaseAccount_roleAssignments": { + "cognitiveService_roleAssignments": { "copy": { - "name": "databaseAccount_roleAssignments", + "name": "cognitiveService_roleAssignments", "count": "[length(coalesce(variables('formattedRoleAssignments'), createArray()))]" }, "type": "Microsoft.Authorization/roleAssignments", "apiVersion": "2022-04-01", - "scope": "[format('Microsoft.DocumentDB/databaseAccounts/{0}', parameters('name'))]", - "name": "[coalesce(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'name'), guid(resourceId('Microsoft.DocumentDB/databaseAccounts', parameters('name')), coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].principalId, coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].roleDefinitionId))]", + "scope": "[format('Microsoft.CognitiveServices/accounts/{0}', parameters('name'))]", + "name": "[coalesce(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'name'), guid(resourceId('Microsoft.CognitiveServices/accounts', parameters('name')), coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].principalId, coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].roleDefinitionId))]", "properties": { "roleDefinitionId": "[coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].roleDefinitionId]", "principalId": "[coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].principalId]", @@ -26896,17 +23286,19 @@ "delegatedManagedIdentityResourceId": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'delegatedManagedIdentityResourceId')]" }, "dependsOn": [ - "databaseAccount" + "cognitiveService" ] }, - "databaseAccount_sqlDatabases": { + "cognitiveService_privateEndpoints": { "copy": { - "name": "databaseAccount_sqlDatabases", - "count": "[length(coalesce(parameters('sqlDatabases'), createArray()))]" + "name": "cognitiveService_privateEndpoints", + "count": "[length(coalesce(parameters('privateEndpoints'), createArray()))]" }, "type": "Microsoft.Resources/deployments", "apiVersion": "2025-04-01", - "name": "[format('{0}-sqldb-{1}', uniqueString(deployment().name, parameters('location')), coalesce(parameters('sqlDatabases'), createArray())[copyIndex()].name)]", + "name": "[format('{0}-cognitiveService-PrivateEndpoint-{1}', uniqueString(deployment().name, parameters('location')), copyIndex())]", + "subscriptionId": "[split(coalesce(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'resourceGroupResourceId'), resourceGroup().id), '/')[2]]", + "resourceGroup": "[split(coalesce(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'resourceGroupResourceId'), resourceGroup().id), '/')[4]]", "properties": { "expressionEvaluationOptions": { "scope": "inner" @@ -26914,19 +23306,42 @@ "mode": "Incremental", "parameters": { "name": { - "value": "[coalesce(parameters('sqlDatabases'), createArray())[copyIndex()].name]" + "value": "[coalesce(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'name'), format('pep-{0}-{1}-{2}', last(split(resourceId('Microsoft.CognitiveServices/accounts', parameters('name')), '/')), coalesce(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'service'), 'account'), copyIndex()))]" + }, + "privateLinkServiceConnections": "[if(not(equals(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'isManualConnection'), true())), createObject('value', createArray(createObject('name', coalesce(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'privateLinkServiceConnectionName'), format('{0}-{1}-{2}', last(split(resourceId('Microsoft.CognitiveServices/accounts', parameters('name')), '/')), coalesce(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'service'), 'account'), copyIndex())), 'properties', createObject('privateLinkServiceId', resourceId('Microsoft.CognitiveServices/accounts', parameters('name')), 'groupIds', createArray(coalesce(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'service'), 'account')))))), createObject('value', null()))]", + "manualPrivateLinkServiceConnections": "[if(equals(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'isManualConnection'), true()), createObject('value', createArray(createObject('name', coalesce(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'privateLinkServiceConnectionName'), format('{0}-{1}-{2}', last(split(resourceId('Microsoft.CognitiveServices/accounts', parameters('name')), '/')), coalesce(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'service'), 'account'), copyIndex())), 'properties', createObject('privateLinkServiceId', resourceId('Microsoft.CognitiveServices/accounts', parameters('name')), 'groupIds', createArray(coalesce(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'service'), 'account')), 'requestMessage', coalesce(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'manualConnectionRequestMessage'), 'Manual approval required.'))))), createObject('value', null()))]", + "subnetResourceId": { + "value": "[coalesce(parameters('privateEndpoints'), createArray())[copyIndex()].subnetResourceId]" + }, + "enableTelemetry": { + "value": "[variables('enableReferencedModulesTelemetry')]" + }, + "location": { + "value": "[coalesce(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'location'), reference(split(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()].subnetResourceId, '/subnets/')[0], '2020-06-01', 'Full').location)]" + }, + "lock": { + "value": "[coalesce(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'lock'), parameters('lock'))]" }, - "containers": { - "value": "[tryGet(coalesce(parameters('sqlDatabases'), createArray())[copyIndex()], 'containers')]" + "privateDnsZoneGroup": { + "value": "[tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'privateDnsZoneGroup')]" }, - "throughput": { - "value": "[tryGet(coalesce(parameters('sqlDatabases'), createArray())[copyIndex()], 'throughput')]" + "roleAssignments": { + "value": "[tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'roleAssignments')]" }, - "databaseAccountName": { - "value": "[parameters('name')]" + "tags": { + "value": "[coalesce(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'tags'), parameters('tags'))]" }, - "autoscaleSettingsMaxThroughput": { - "value": "[tryGet(coalesce(parameters('sqlDatabases'), createArray())[copyIndex()], 'autoscaleSettingsMaxThroughput')]" + "customDnsConfigs": { + "value": "[tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'customDnsConfigs')]" + }, + "ipConfigurations": { + "value": "[tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'ipConfigurations')]" + }, + "applicationSecurityGroupResourceIds": { + "value": "[tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'applicationSecurityGroupResourceIds')]" + }, + "customNetworkInterfaceName": { + "value": "[tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'customNetworkInterfaceName')]" } }, "template": { @@ -26936,253 +23351,417 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.38.33.27573", - "templateHash": "1549250134356326406" + "version": "0.38.5.1644", + "templateHash": "16604612898799598358" }, - "name": "DocumentDB Database Account SQL Databases", - "description": "This module deploys a SQL Database in a CosmosDB Account." + "name": "Private Endpoints", + "description": "This module deploys a Private Endpoint." }, "definitions": { - "containerType": { + "privateDnsZoneGroupType": { "type": "object", "properties": { "name": { "type": "string", + "nullable": true, "metadata": { - "description": "Required. Name of the container." + "description": "Optional. The name of the Private DNS Zone Group." } }, - "analyticalStorageTtl": { - "type": "int", + "privateDnsZoneGroupConfigs": { + "type": "array", + "items": { + "$ref": "#/definitions/privateDnsZoneGroupConfigType" + }, + "metadata": { + "description": "Required. The private DNS zone groups to associate the private endpoint. A DNS zone group can support up to 5 DNS zones." + } + } + }, + "metadata": { + "__bicep_export!": true, + "description": "The type of a private dns zone group." + } + }, + "lockType": { + "type": "object", + "properties": { + "name": { + "type": "string", "nullable": true, "metadata": { - "description": "Optional. Default to 0. Indicates how long data should be retained in the analytical store, for a container. Analytical store is enabled when ATTL is set with a value other than 0. If the value is set to -1, the analytical store retains all historical data, irrespective of the retention of the data in the transactional store." + "description": "Optional. Specify the name of lock." } }, - "conflictResolutionPolicy": { - "type": "object", + "kind": { + "type": "string", + "allowedValues": [ + "CanNotDelete", + "None", + "ReadOnly" + ], + "nullable": true, "metadata": { - "__bicep_resource_derived_type!": { - "source": "Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers@2025-04-15#properties/properties/properties/resource/properties/conflictResolutionPolicy" - }, - "description": "Optional. The conflict resolution policy for the container. Conflicts and conflict resolution policies are applicable if the Azure Cosmos DB account is configured with multiple write regions." - }, - "nullable": true + "description": "Optional. Specify the type of lock." + } }, - "defaultTtl": { - "type": "int", + "notes": { + "type": "string", "nullable": true, - "minValue": -1, - "maxValue": 2147483647, "metadata": { - "description": "Optional. Default to -1. Default time to live (in seconds). With Time to Live or TTL, Azure Cosmos DB provides the ability to delete items automatically from a container after a certain time period. If the value is set to \"-1\", it is equal to infinity, and items don't expire by default." + "description": "Optional. Specify the notes of the lock." } - }, - "throughput": { - "type": "int", + } + }, + "metadata": { + "description": "An AVM-aligned type for a lock.", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.1" + } + } + }, + "privateDnsZoneGroupConfigType": { + "type": "object", + "properties": { + "name": { + "type": "string", "nullable": true, "metadata": { - "description": "Optional. Default to 400. Request Units per second. Will be ignored if autoscaleSettingsMaxThroughput is used. For best performance for large production workloads, it is recommended to set dedicated throughput (autoscale or manual) at the container level and not at the database level." + "description": "Optional. The name of the private DNS zone group config." } }, - "autoscaleSettingsMaxThroughput": { - "type": "int", + "privateDnsZoneResourceId": { + "type": "string", + "metadata": { + "description": "Required. The resource id of the private DNS zone." + } + } + }, + "metadata": { + "description": "The type of a private DNS zone group configuration.", + "__bicep_imported_from!": { + "sourceTemplate": "private-dns-zone-group/main.bicep" + } + } + }, + "roleAssignmentType": { + "type": "object", + "properties": { + "name": { + "type": "string", "nullable": true, - "maxValue": 1000000, "metadata": { - "description": "Optional. Specifies the Autoscale settings and represents maximum throughput, the resource can scale up to. The autoscale throughput should have valid throughput values between 1000 and 1000000 inclusive in increments of 1000. If value is set to null, then autoscale will be disabled. For best performance for large production workloads, it is recommended to set dedicated throughput (autoscale or manual) at the container level and not at the database level." + "description": "Optional. The name (as GUID) of the role assignment. If not provided, a GUID will be generated." } }, - "tags": { - "type": "object", + "roleDefinitionIdOrName": { + "type": "string", "metadata": { - "__bicep_resource_derived_type!": { - "source": "Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers@2025-04-15#properties/tags" - }, - "description": "Optional. Tags of the SQL Database resource." - }, - "nullable": true + "description": "Required. The role to assign. You can provide either the display name of the role definition, the role definition GUID, or its fully qualified ID in the following format: '/providers/Microsoft.Authorization/roleDefinitions/c2f4ef07-c644-48eb-af81-4b1b4947fb11'." + } }, - "paths": { - "type": "array", - "items": { - "type": "string" - }, - "minLength": 1, - "maxLength": 3, + "principalId": { + "type": "string", "metadata": { - "description": "Required. List of paths using which data within the container can be partitioned. For kind=MultiHash it can be up to 3. For anything else it needs to be exactly 1." + "description": "Required. The principal ID of the principal (user/group/identity) to assign the role to." } }, - "indexingPolicy": { - "type": "object", + "principalType": { + "type": "string", + "allowedValues": [ + "Device", + "ForeignGroup", + "Group", + "ServicePrincipal", + "User" + ], + "nullable": true, "metadata": { - "__bicep_resource_derived_type!": { - "source": "Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers@2025-04-15#properties/properties/properties/resource/properties/indexingPolicy" - }, - "description": "Optional. Indexing policy of the container." - }, - "nullable": true + "description": "Optional. The principal type of the assigned principal ID." + } }, - "uniqueKeyPolicyKeys": { - "type": "array", + "description": { + "type": "string", + "nullable": true, "metadata": { - "__bicep_resource_derived_type!": { - "source": "Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers@2025-04-15#properties/properties/properties/resource/properties/uniqueKeyPolicy/properties/uniqueKeys" - }, - "description": "Optional. The unique key policy configuration containing a list of unique keys that enforces uniqueness constraint on documents in the collection in the Azure Cosmos DB service." - }, - "nullable": true + "description": "Optional. The description of the role assignment." + } }, - "kind": { + "condition": { "type": "string", - "allowedValues": [ - "Hash", - "MultiHash" - ], "nullable": true, "metadata": { - "description": "Optional. Default to Hash. Indicates the kind of algorithm used for partitioning." + "description": "Optional. The conditions on the role assignment. This limits the resources it can be assigned to. e.g.: @Resource[Microsoft.Storage/storageAccounts/blobServices/containers:ContainerName] StringEqualsIgnoreCase \"foo_storage_container\"." } }, - "version": { - "type": "int", + "conditionVersion": { + "type": "string", "allowedValues": [ - 1, - 2 + "2.0" ], "nullable": true, "metadata": { - "description": "Optional. Default to 1 for Hash and 2 for MultiHash - 1 is not allowed for MultiHash. Version of the partition key definition." + "description": "Optional. Version of the condition." + } + }, + "delegatedManagedIdentityResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The Resource Id of the delegated managed identity resource." } } }, "metadata": { - "__bicep_export!": true, - "description": "The type of a container." + "description": "An AVM-aligned type for a role assignment.", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.1" + } } } }, "parameters": { - "databaseAccountName": { + "name": { "type": "string", "metadata": { - "description": "Conditional. The name of the parent Database Account. Required if the template is used in a standalone deployment." + "description": "Required. Name of the private endpoint resource to create." } }, - "name": { + "subnetResourceId": { "type": "string", "metadata": { - "description": "Required. Name of the SQL database ." + "description": "Required. Resource ID of the subnet where the endpoint needs to be created." } }, - "containers": { + "applicationSecurityGroupResourceIds": { "type": "array", "items": { - "$ref": "#/definitions/containerType" + "type": "string" }, "nullable": true, "metadata": { - "description": "Optional. Array of containers to deploy in the SQL database." + "description": "Optional. Application security groups in which the private endpoint IP configuration is included." } }, - "throughput": { - "type": "int", + "customNetworkInterfaceName": { + "type": "string", "nullable": true, "metadata": { - "description": "Optional. Request units per second. Will be ignored if autoscaleSettingsMaxThroughput is used. Setting throughput at the database level is only recommended for development/test or when workload across all containers in the shared throughput database is uniform. For best performance for large production workloads, it is recommended to set dedicated throughput (autoscale or manual) at the container level and not at the database level." + "description": "Optional. The custom name of the network interface attached to the private endpoint." } }, - "autoscaleSettingsMaxThroughput": { - "type": "int", + "ipConfigurations": { + "type": "array", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.Network/privateEndpoints@2024-01-01#properties/properties/properties/ipConfigurations" + }, + "description": "Optional. A list of IP configurations of the private endpoint. This will be used to map to the First Party Service endpoints." + }, + "nullable": true + }, + "privateDnsZoneGroup": { + "$ref": "#/definitions/privateDnsZoneGroupType", "nullable": true, "metadata": { - "description": "Optional. Specifies the Autoscale settings and represents maximum throughput, the resource can scale up to. The autoscale throughput should have valid throughput values between 1000 and 1000000 inclusive in increments of 1000. If value is set to null, then autoscale will be disabled. Setting throughput at the database level is only recommended for development/test or when workload across all containers in the shared throughput database is uniform. For best performance for large production workloads, it is recommended to set dedicated throughput (autoscale or manual) at the container level and not at the database level." + "description": "Optional. The private DNS zone group to configure for the private endpoint." + } + }, + "location": { + "type": "string", + "defaultValue": "[resourceGroup().location]", + "metadata": { + "description": "Optional. Location for all Resources." + } + }, + "lock": { + "$ref": "#/definitions/lockType", + "nullable": true, + "metadata": { + "description": "Optional. The lock settings of the service." + } + }, + "roleAssignments": { + "type": "array", + "items": { + "$ref": "#/definitions/roleAssignmentType" + }, + "nullable": true, + "metadata": { + "description": "Optional. Array of role assignments to create." } }, "tags": { "type": "object", "metadata": { "__bicep_resource_derived_type!": { - "source": "Microsoft.DocumentDB/databaseAccounts/sqlDatabases@2025-04-15#properties/tags" + "source": "Microsoft.Network/privateEndpoints@2024-01-01#properties/tags" }, - "description": "Optional. Tags of the SQL database resource." + "description": "Optional. Tags to be applied on all resources/resource groups in this deployment." + }, + "nullable": true + }, + "customDnsConfigs": { + "type": "array", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.Network/privateEndpoints@2024-01-01#properties/properties/properties/customDnsConfigs" + }, + "description": "Optional. Custom DNS configurations." + }, + "nullable": true + }, + "manualPrivateLinkServiceConnections": { + "type": "array", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.Network/privateEndpoints@2024-01-01#properties/properties/properties/manualPrivateLinkServiceConnections" + }, + "description": "Conditional. A grouping of information about the connection to the remote resource. Used when the network admin does not have access to approve connections to the remote resource. Required if `privateLinkServiceConnections` is empty." + }, + "nullable": true + }, + "privateLinkServiceConnections": { + "type": "array", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.Network/privateEndpoints@2024-01-01#properties/properties/properties/privateLinkServiceConnections" + }, + "description": "Conditional. A grouping of information about the connection to the remote resource. Required if `manualPrivateLinkServiceConnections` is empty." }, "nullable": true + }, + "enableTelemetry": { + "type": "bool", + "defaultValue": true, + "metadata": { + "description": "Optional. Enable/Disable usage telemetry for module." + } + } + }, + "variables": { + "copy": [ + { + "name": "formattedRoleAssignments", + "count": "[length(coalesce(parameters('roleAssignments'), createArray()))]", + "input": "[union(coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')], createObject('roleDefinitionId', coalesce(tryGet(variables('builtInRoleNames'), coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName), if(contains(coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName, '/providers/Microsoft.Authorization/roleDefinitions/'), coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName, subscriptionResourceId('Microsoft.Authorization/roleDefinitions', coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName)))))]" + } + ], + "builtInRoleNames": { + "Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'b24988ac-6180-42a0-ab88-20f7382dd24c')]", + "DNS Resolver Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '0f2ebee7-ffd4-4fc0-b3b7-664099fdad5d')]", + "DNS Zone Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'befefa01-2a29-4197-83a8-272ff33ce314')]", + "Domain Services Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'eeaeda52-9324-47f6-8069-5d5bade478b2')]", + "Domain Services Reader": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '361898ef-9ed1-48c2-849c-a832951106bb')]", + "Network Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '4d97b98b-1d4f-4787-a291-c67834d212e7')]", + "Owner": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '8e3af657-a8ff-443c-a75c-2fe8c4bcb635')]", + "Private DNS Zone Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'b12aa53e-6015-4669-85d0-8515ebb3ae7f')]", + "Reader": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'acdd72a7-3385-48ef-bd42-f606fba81ae7')]", + "Role Based Access Control Administrator": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'f58310d9-a9f6-439a-9e8d-f62e7b41a168')]" } }, "resources": { - "databaseAccount": { - "existing": true, - "type": "Microsoft.DocumentDB/databaseAccounts", - "apiVersion": "2025-04-15", - "name": "[parameters('databaseAccountName')]" + "avmTelemetry": { + "condition": "[parameters('enableTelemetry')]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "[format('46d3xbcp.res.network-privateendpoint.{0}.{1}', replace('0.11.1', '.', '-'), substring(uniqueString(deployment().name, parameters('location')), 0, 4))]", + "properties": { + "mode": "Incremental", + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "resources": [], + "outputs": { + "telemetry": { + "type": "String", + "value": "For more information, see https://aka.ms/avm/TelemetryInfo" + } + } + } + } }, - "sqlDatabase": { - "type": "Microsoft.DocumentDB/databaseAccounts/sqlDatabases", - "apiVersion": "2025-04-15", - "name": "[format('{0}/{1}', parameters('databaseAccountName'), parameters('name'))]", + "privateEndpoint": { + "type": "Microsoft.Network/privateEndpoints", + "apiVersion": "2024-10-01", + "name": "[parameters('name')]", + "location": "[parameters('location')]", "tags": "[parameters('tags')]", "properties": { - "resource": { - "id": "[parameters('name')]" - }, - "options": "[if(contains(reference('databaseAccount').capabilities, createObject('name', 'EnableServerless')), null(), createObject('throughput', if(equals(parameters('autoscaleSettingsMaxThroughput'), null()), parameters('throughput'), null()), 'autoscaleSettings', if(not(equals(parameters('autoscaleSettingsMaxThroughput'), null())), createObject('maxThroughput', parameters('autoscaleSettingsMaxThroughput')), null())))]" + "copy": [ + { + "name": "applicationSecurityGroups", + "count": "[length(coalesce(parameters('applicationSecurityGroupResourceIds'), createArray()))]", + "input": { + "id": "[coalesce(parameters('applicationSecurityGroupResourceIds'), createArray())[copyIndex('applicationSecurityGroups')]]" + } + } + ], + "customDnsConfigs": "[coalesce(parameters('customDnsConfigs'), createArray())]", + "customNetworkInterfaceName": "[coalesce(parameters('customNetworkInterfaceName'), '')]", + "ipConfigurations": "[coalesce(parameters('ipConfigurations'), createArray())]", + "manualPrivateLinkServiceConnections": "[coalesce(parameters('manualPrivateLinkServiceConnections'), createArray())]", + "privateLinkServiceConnections": "[coalesce(parameters('privateLinkServiceConnections'), createArray())]", + "subnet": { + "id": "[parameters('subnetResourceId')]" + } + } + }, + "privateEndpoint_lock": { + "condition": "[and(not(empty(coalesce(parameters('lock'), createObject()))), not(equals(tryGet(parameters('lock'), 'kind'), 'None')))]", + "type": "Microsoft.Authorization/locks", + "apiVersion": "2020-05-01", + "scope": "[format('Microsoft.Network/privateEndpoints/{0}', parameters('name'))]", + "name": "[coalesce(tryGet(parameters('lock'), 'name'), format('lock-{0}', parameters('name')))]", + "properties": { + "level": "[coalesce(tryGet(parameters('lock'), 'kind'), '')]", + "notes": "[coalesce(tryGet(parameters('lock'), 'notes'), if(equals(tryGet(parameters('lock'), 'kind'), 'CanNotDelete'), 'Cannot delete resource or child resources.', 'Cannot delete or modify the resource or child resources.'))]" }, "dependsOn": [ - "databaseAccount" + "privateEndpoint" ] }, - "container": { + "privateEndpoint_roleAssignments": { "copy": { - "name": "container", - "count": "[length(coalesce(parameters('containers'), createArray()))]" + "name": "privateEndpoint_roleAssignments", + "count": "[length(coalesce(variables('formattedRoleAssignments'), createArray()))]" + }, + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2022-04-01", + "scope": "[format('Microsoft.Network/privateEndpoints/{0}', parameters('name'))]", + "name": "[coalesce(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'name'), guid(resourceId('Microsoft.Network/privateEndpoints', parameters('name')), coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].principalId, coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].roleDefinitionId))]", + "properties": { + "roleDefinitionId": "[coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].roleDefinitionId]", + "principalId": "[coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].principalId]", + "description": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'description')]", + "principalType": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'principalType')]", + "condition": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'condition')]", + "conditionVersion": "[if(not(empty(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'condition'))), coalesce(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'conditionVersion'), '2.0'), null())]", + "delegatedManagedIdentityResourceId": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'delegatedManagedIdentityResourceId')]" }, + "dependsOn": [ + "privateEndpoint" + ] + }, + "privateEndpoint_privateDnsZoneGroup": { + "condition": "[not(empty(parameters('privateDnsZoneGroup')))]", "type": "Microsoft.Resources/deployments", "apiVersion": "2025-04-01", - "name": "[format('{0}-sqldb-{1}', uniqueString(deployment().name, parameters('name')), coalesce(parameters('containers'), createArray())[copyIndex()].name)]", + "name": "[format('{0}-PrivateEndpoint-PrivateDnsZoneGroup', uniqueString(deployment().name))]", "properties": { "expressionEvaluationOptions": { "scope": "inner" }, "mode": "Incremental", "parameters": { - "databaseAccountName": { - "value": "[parameters('databaseAccountName')]" - }, - "sqlDatabaseName": { - "value": "[parameters('name')]" - }, "name": { - "value": "[coalesce(parameters('containers'), createArray())[copyIndex()].name]" - }, - "analyticalStorageTtl": { - "value": "[tryGet(coalesce(parameters('containers'), createArray())[copyIndex()], 'analyticalStorageTtl')]" - }, - "autoscaleSettingsMaxThroughput": { - "value": "[tryGet(coalesce(parameters('containers'), createArray())[copyIndex()], 'autoscaleSettingsMaxThroughput')]" - }, - "conflictResolutionPolicy": { - "value": "[tryGet(coalesce(parameters('containers'), createArray())[copyIndex()], 'conflictResolutionPolicy')]" - }, - "defaultTtl": { - "value": "[tryGet(coalesce(parameters('containers'), createArray())[copyIndex()], 'defaultTtl')]" - }, - "indexingPolicy": { - "value": "[tryGet(coalesce(parameters('containers'), createArray())[copyIndex()], 'indexingPolicy')]" - }, - "kind": { - "value": "[tryGet(coalesce(parameters('containers'), createArray())[copyIndex()], 'kind')]" - }, - "version": { - "value": "[tryGet(coalesce(parameters('containers'), createArray())[copyIndex()], 'version')]" + "value": "[tryGet(parameters('privateDnsZoneGroup'), 'name')]" }, - "paths": { - "value": "[tryGet(coalesce(parameters('containers'), createArray())[copyIndex()], 'paths')]" + "privateEndpointName": { + "value": "[parameters('name')]" }, - "throughput": "[if(and(or(not(equals(parameters('throughput'), null())), not(equals(parameters('autoscaleSettingsMaxThroughput'), null()))), equals(tryGet(coalesce(parameters('containers'), createArray())[copyIndex()], 'throughput'), null())), createObject('value', -1), createObject('value', tryGet(coalesce(parameters('containers'), createArray())[copyIndex()], 'throughput')))]", - "uniqueKeyPolicyKeys": { - "value": "[tryGet(coalesce(parameters('containers'), createArray())[copyIndex()], 'uniqueKeyPolicyKeys')]" + "privateDnsZoneConfigs": { + "value": "[parameters('privateDnsZoneGroup').privateDnsZoneGroupConfigs]" } }, "template": { @@ -27192,191 +23771,108 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.38.33.27573", - "templateHash": "1005439058963058082" + "version": "0.38.5.1644", + "templateHash": "24141742673128945" }, - "name": "DocumentDB Database Account SQL Database Containers", - "description": "This module deploys a SQL Database Container in a CosmosDB Account." + "name": "Private Endpoint Private DNS Zone Groups", + "description": "This module deploys a Private Endpoint Private DNS Zone Group." }, - "parameters": { - "databaseAccountName": { - "type": "string", - "metadata": { - "description": "Conditional. The name of the parent Database Account. Required if the template is used in a standalone deployment." - } - }, - "sqlDatabaseName": { - "type": "string", - "metadata": { - "description": "Conditional. The name of the parent SQL Database. Required if the template is used in a standalone deployment." - } - }, - "name": { - "type": "string", - "metadata": { - "description": "Required. Name of the container." - } - }, - "analyticalStorageTtl": { - "type": "int", - "defaultValue": 0, - "metadata": { - "description": "Optional. Default to 0. Indicates how long data should be retained in the analytical store, for a container. Analytical store is enabled when ATTL is set with a value other than 0. If the value is set to -1, the analytical store retains all historical data, irrespective of the retention of the data in the transactional store." - } - }, - "conflictResolutionPolicy": { + "definitions": { + "privateDnsZoneGroupConfigType": { "type": "object", - "metadata": { - "__bicep_resource_derived_type!": { - "source": "Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers@2025-04-15#properties/properties/properties/resource/properties/conflictResolutionPolicy" + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The name of the private DNS zone group config." + } }, - "description": "Optional. The conflict resolution policy for the container. Conflicts and conflict resolution policies are applicable if the Azure Cosmos DB account is configured with multiple write regions." + "privateDnsZoneResourceId": { + "type": "string", + "metadata": { + "description": "Required. The resource id of the private DNS zone." + } + } }, - "nullable": true - }, - "defaultTtl": { - "type": "int", - "nullable": true, - "minValue": -1, - "maxValue": 2147483647, - "metadata": { - "description": "Optional. Default to -1. Default time to live (in seconds). With Time to Live or TTL, Azure Cosmos DB provides the ability to delete items automatically from a container after a certain time period. If the value is set to \"-1\", it is equal to infinity, and items don't expire by default." - } - }, - "throughput": { - "type": "int", - "defaultValue": 400, "metadata": { - "description": "Optional. Default to 400. Request Units per second. Will be ignored if autoscaleSettingsMaxThroughput is used. For best performance for large production workloads, it is recommended to set dedicated throughput (autoscale or manual) at the container level and not at the database level." + "__bicep_export!": true, + "description": "The type of a private DNS zone group configuration." } - }, - "autoscaleSettingsMaxThroughput": { - "type": "int", - "nullable": true, - "maxValue": 1000000, + } + }, + "parameters": { + "privateEndpointName": { + "type": "string", "metadata": { - "description": "Optional. Specifies the Autoscale settings and represents maximum throughput, the resource can scale up to. The autoscale throughput should have valid throughput values between 1000 and 1000000 inclusive in increments of 1000. If value is set to null, then autoscale will be disabled. For best performance for large production workloads, it is recommended to set dedicated throughput (autoscale or manual) at the container level and not at the database level." + "description": "Conditional. The name of the parent private endpoint. Required if the template is used in a standalone deployment." } }, - "tags": { - "type": "object", - "metadata": { - "__bicep_resource_derived_type!": { - "source": "Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers@2025-04-15#properties/tags" - }, - "description": "Optional. Tags of the SQL Database resource." - }, - "nullable": true - }, - "paths": { + "privateDnsZoneConfigs": { "type": "array", "items": { - "type": "string" + "$ref": "#/definitions/privateDnsZoneGroupConfigType" }, "minLength": 1, - "maxLength": 3, + "maxLength": 5, "metadata": { - "description": "Required. List of paths using which data within the container can be partitioned. For kind=MultiHash it can be up to 3. For anything else it needs to be exactly 1." + "description": "Required. Array of private DNS zone configurations of the private DNS zone group. A DNS zone group can support up to 5 DNS zones." } }, - "indexingPolicy": { - "type": "object", - "metadata": { - "__bicep_resource_derived_type!": { - "source": "Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers@2025-04-15#properties/properties/properties/resource/properties/indexingPolicy" - }, - "description": "Optional. Indexing policy of the container." - }, - "nullable": true - }, - "uniqueKeyPolicyKeys": { - "type": "array", - "metadata": { - "__bicep_resource_derived_type!": { - "source": "Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers@2025-04-15#properties/properties/properties/resource/properties/uniqueKeyPolicy/properties/uniqueKeys" - }, - "description": "Optional. The unique key policy configuration containing a list of unique keys that enforces uniqueness constraint on documents in the collection in the Azure Cosmos DB service." - }, - "nullable": true - }, - "kind": { + "name": { "type": "string", - "defaultValue": "Hash", - "allowedValues": [ - "Hash", - "MultiHash" - ], - "metadata": { - "description": "Optional. Default to Hash. Indicates the kind of algorithm used for partitioning." - } - }, - "version": { - "type": "int", - "defaultValue": 1, - "allowedValues": [ - 1, - 2 - ], + "defaultValue": "default", "metadata": { - "description": "Optional. Default to 1 for Hash and 2 for MultiHash - 1 is not allowed for MultiHash. Version of the partition key definition." + "description": "Optional. The name of the private DNS zone group." } } }, - "variables": { - "copy": [ - { - "name": "partitionKeyPaths", - "count": "[length(parameters('paths'))]", - "input": "[if(startsWith(parameters('paths')[copyIndex('partitionKeyPaths')], '/'), parameters('paths')[copyIndex('partitionKeyPaths')], format('/{0}', parameters('paths')[copyIndex('partitionKeyPaths')]))]" - } - ] - }, "resources": { - "databaseAccount::sqlDatabase": { - "existing": true, - "type": "Microsoft.DocumentDB/databaseAccounts/sqlDatabases", - "apiVersion": "2025-04-15", - "name": "[format('{0}/{1}', parameters('databaseAccountName'), parameters('sqlDatabaseName'))]" - }, - "databaseAccount": { + "privateEndpoint": { "existing": true, - "type": "Microsoft.DocumentDB/databaseAccounts", - "apiVersion": "2025-04-15", - "name": "[parameters('databaseAccountName')]" + "type": "Microsoft.Network/privateEndpoints", + "apiVersion": "2024-10-01", + "name": "[parameters('privateEndpointName')]" }, - "container": { - "type": "Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers", - "apiVersion": "2025-04-15", - "name": "[format('{0}/{1}/{2}', parameters('databaseAccountName'), parameters('sqlDatabaseName'), parameters('name'))]", - "tags": "[parameters('tags')]", + "privateDnsZoneGroup": { + "type": "Microsoft.Network/privateEndpoints/privateDnsZoneGroups", + "apiVersion": "2024-10-01", + "name": "[format('{0}/{1}', parameters('privateEndpointName'), parameters('name'))]", "properties": { - "resource": "[shallowMerge(createArray(createObject('conflictResolutionPolicy', parameters('conflictResolutionPolicy'), 'id', parameters('name'), 'indexingPolicy', parameters('indexingPolicy'), 'partitionKey', createObject('paths', variables('partitionKeyPaths'), 'kind', parameters('kind'), 'version', if(equals(parameters('kind'), 'MultiHash'), 2, parameters('version'))), 'uniqueKeyPolicy', if(not(empty(parameters('uniqueKeyPolicyKeys'))), createObject('uniqueKeys', parameters('uniqueKeyPolicyKeys')), null())), if(not(equals(parameters('analyticalStorageTtl'), 0)), createObject('analyticalStorageTtl', parameters('analyticalStorageTtl')), createObject()), if(not(equals(parameters('defaultTtl'), null())), createObject('defaultTtl', parameters('defaultTtl')), createObject())))]", - "options": "[if(contains(reference('databaseAccount').capabilities, createObject('name', 'EnableServerless')), null(), createObject('throughput', if(and(equals(parameters('autoscaleSettingsMaxThroughput'), null()), not(equals(parameters('throughput'), -1))), parameters('throughput'), null()), 'autoscaleSettings', if(not(equals(parameters('autoscaleSettingsMaxThroughput'), null())), createObject('maxThroughput', parameters('autoscaleSettingsMaxThroughput')), null())))]" - }, - "dependsOn": [ - "databaseAccount" - ] + "copy": [ + { + "name": "privateDnsZoneConfigs", + "count": "[length(parameters('privateDnsZoneConfigs'))]", + "input": { + "name": "[coalesce(tryGet(parameters('privateDnsZoneConfigs')[copyIndex('privateDnsZoneConfigs')], 'name'), last(split(parameters('privateDnsZoneConfigs')[copyIndex('privateDnsZoneConfigs')].privateDnsZoneResourceId, '/')))]", + "properties": { + "privateDnsZoneId": "[parameters('privateDnsZoneConfigs')[copyIndex('privateDnsZoneConfigs')].privateDnsZoneResourceId]" + } + } + } + ] + } } }, "outputs": { "name": { "type": "string", "metadata": { - "description": "The name of the container." + "description": "The name of the private endpoint DNS zone group." }, "value": "[parameters('name')]" }, "resourceId": { "type": "string", "metadata": { - "description": "The resource ID of the container." + "description": "The resource ID of the private endpoint DNS zone group." }, - "value": "[resourceId('Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers', parameters('databaseAccountName'), parameters('sqlDatabaseName'), parameters('name'))]" + "value": "[resourceId('Microsoft.Network/privateEndpoints/privateDnsZoneGroups', parameters('privateEndpointName'), parameters('name'))]" }, "resourceGroupName": { "type": "string", "metadata": { - "description": "The name of the resource group the container was created in." + "description": "The resource group the private endpoint DNS zone group was deployed into." }, "value": "[resourceGroup().name]" } @@ -27384,616 +23880,873 @@ } }, "dependsOn": [ - "sqlDatabase" + "privateEndpoint" ] } }, "outputs": { + "resourceGroupName": { + "type": "string", + "metadata": { + "description": "The resource group the private endpoint was deployed into." + }, + "value": "[resourceGroup().name]" + }, + "resourceId": { + "type": "string", + "metadata": { + "description": "The resource ID of the private endpoint." + }, + "value": "[resourceId('Microsoft.Network/privateEndpoints', parameters('name'))]" + }, "name": { "type": "string", "metadata": { - "description": "The name of the SQL database." + "description": "The name of the private endpoint." }, "value": "[parameters('name')]" }, - "resourceId": { + "location": { "type": "string", "metadata": { - "description": "The resource ID of the SQL database." + "description": "The location the resource was deployed into." }, - "value": "[resourceId('Microsoft.DocumentDB/databaseAccounts/sqlDatabases', parameters('databaseAccountName'), parameters('name'))]" + "value": "[reference('privateEndpoint', '2024-10-01', 'full').location]" }, - "resourceGroupName": { + "customDnsConfigs": { + "type": "array", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.Network/privateEndpoints@2024-01-01#properties/properties/properties/customDnsConfigs", + "output": true + }, + "description": "The custom DNS configurations of the private endpoint." + }, + "value": "[reference('privateEndpoint').customDnsConfigs]" + }, + "networkInterfaceResourceIds": { + "type": "array", + "items": { + "type": "string" + }, + "metadata": { + "description": "The resource IDs of the network interfaces associated with the private endpoint." + }, + "value": "[map(reference('privateEndpoint').networkInterfaces, lambda('nic', lambdaVariables('nic').id))]" + }, + "groupId": { "type": "string", + "nullable": true, "metadata": { - "description": "The name of the resource group the SQL database was created in." + "description": "The group Id for the private endpoint Group." }, - "value": "[resourceGroup().name]" + "value": "[coalesce(tryGet(tryGet(tryGet(tryGet(reference('privateEndpoint'), 'manualPrivateLinkServiceConnections'), 0, 'properties'), 'groupIds'), 0), tryGet(tryGet(tryGet(tryGet(reference('privateEndpoint'), 'privateLinkServiceConnections'), 0, 'properties'), 'groupIds'), 0))]" + } + } + } + }, + "dependsOn": [ + "cognitiveService" + ] + }, + "secretsExport": { + "condition": "[not(equals(parameters('secretsExportConfiguration'), null()))]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "[format('{0}-secrets-kv', uniqueString(deployment().name, parameters('location')))]", + "subscriptionId": "[split(tryGet(parameters('secretsExportConfiguration'), 'keyVaultResourceId'), '/')[2]]", + "resourceGroup": "[split(tryGet(parameters('secretsExportConfiguration'), 'keyVaultResourceId'), '/')[4]]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "keyVaultName": { + "value": "[last(split(tryGet(parameters('secretsExportConfiguration'), 'keyVaultResourceId'), '/'))]" + }, + "secretsToSet": { + "value": "[union(createArray(), if(contains(parameters('secretsExportConfiguration'), 'accessKey1Name'), createArray(createObject('name', tryGet(parameters('secretsExportConfiguration'), 'accessKey1Name'), 'value', listKeys('cognitiveService', '2025-06-01').key1)), createArray()), if(contains(parameters('secretsExportConfiguration'), 'accessKey2Name'), createArray(createObject('name', tryGet(parameters('secretsExportConfiguration'), 'accessKey2Name'), 'value', listKeys('cognitiveService', '2025-06-01').key2)), createArray()))]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "languageVersion": "2.0", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.38.33.27573", + "templateHash": "1394089926798493893" + } + }, + "definitions": { + "secretSetOutputType": { + "type": "object", + "properties": { + "secretResourceId": { + "type": "string", + "metadata": { + "description": "The resourceId of the exported secret." + } + }, + "secretUri": { + "type": "string", + "metadata": { + "description": "The secret URI of the exported secret." + } + }, + "secretUriWithVersion": { + "type": "string", + "metadata": { + "description": "The secret URI with version of the exported secret." + } + } + }, + "metadata": { + "description": "An AVM-aligned type for the output of the secret set via the secrets export feature.", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.1" + } + } + }, + "secretToSetType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "metadata": { + "description": "Required. The name of the secret to set." + } + }, + "value": { + "type": "securestring", + "metadata": { + "description": "Required. The value of the secret to set." + } + } + }, + "metadata": { + "description": "An AVM-aligned type for the secret to set via the secrets export feature.", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.1" + } + } + } + }, + "parameters": { + "keyVaultName": { + "type": "string", + "metadata": { + "description": "Required. The name of the Key Vault to set the ecrets in." + } + }, + "secretsToSet": { + "type": "array", + "items": { + "$ref": "#/definitions/secretToSetType" + }, + "metadata": { + "description": "Required. The secrets to set in the Key Vault." + } } + }, + "resources": { + "keyVault": { + "existing": true, + "type": "Microsoft.KeyVault/vaults", + "apiVersion": "2025-05-01", + "name": "[parameters('keyVaultName')]" + }, + "secrets": { + "copy": { + "name": "secrets", + "count": "[length(parameters('secretsToSet'))]" + }, + "type": "Microsoft.KeyVault/vaults/secrets", + "apiVersion": "2025-05-01", + "name": "[format('{0}/{1}', parameters('keyVaultName'), parameters('secretsToSet')[copyIndex()].name)]", + "properties": { + "value": "[parameters('secretsToSet')[copyIndex()].value]" + } + } + }, + "outputs": { + "secretsSet": { + "type": "array", + "items": { + "$ref": "#/definitions/secretSetOutputType" + }, + "metadata": { + "description": "The references to the secrets exported to the provided Key Vault." + }, + "copy": { + "count": "[length(range(0, length(coalesce(parameters('secretsToSet'), createArray()))))]", + "input": { + "secretResourceId": "[resourceId('Microsoft.KeyVault/vaults/secrets', parameters('keyVaultName'), parameters('secretsToSet')[range(0, length(coalesce(parameters('secretsToSet'), createArray())))[copyIndex()]].name)]", + "secretUri": "[reference(format('secrets[{0}]', range(0, length(coalesce(parameters('secretsToSet'), createArray())))[copyIndex()])).secretUri]", + "secretUriWithVersion": "[reference(format('secrets[{0}]', range(0, length(coalesce(parameters('secretsToSet'), createArray())))[copyIndex()])).secretUriWithVersion]" + } + } + } + } + } + }, + "dependsOn": [ + "cognitiveService" + ] + } + }, + "outputs": { + "name": { + "type": "string", + "metadata": { + "description": "The name of the cognitive services account." + }, + "value": "[parameters('name')]" + }, + "resourceId": { + "type": "string", + "metadata": { + "description": "The resource ID of the cognitive services account." + }, + "value": "[resourceId('Microsoft.CognitiveServices/accounts', parameters('name'))]" + }, + "resourceGroupName": { + "type": "string", + "metadata": { + "description": "The resource group the cognitive services account was deployed into." + }, + "value": "[resourceGroup().name]" + }, + "endpoint": { + "type": "string", + "metadata": { + "description": "The service endpoint of the cognitive services account." + }, + "value": "[reference('cognitiveService').endpoint]" + }, + "endpoints": { + "$ref": "#/definitions/endpointType", + "metadata": { + "description": "All endpoints available for the cognitive services account, types depends on the cognitive service kind." + }, + "value": "[reference('cognitiveService').endpoints]" + }, + "systemAssignedMIPrincipalId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "The principal ID of the system assigned identity." + }, + "value": "[tryGet(tryGet(reference('cognitiveService', '2025-06-01', 'full'), 'identity'), 'principalId')]" + }, + "location": { + "type": "string", + "metadata": { + "description": "The location the resource was deployed into." + }, + "value": "[reference('cognitiveService', '2025-06-01', 'full').location]" + }, + "exportedSecrets": { + "$ref": "#/definitions/secretsOutputType", + "metadata": { + "description": "A hashtable of references to the secrets exported to the provided Key Vault. The key of each reference is each secret's name." + }, + "value": "[if(not(equals(parameters('secretsExportConfiguration'), null())), toObject(reference('secretsExport').outputs.secretsSet.value, lambda('secret', last(split(lambdaVariables('secret').secretResourceId, '/'))), lambda('secret', lambdaVariables('secret'))), createObject())]" + }, + "privateEndpoints": { + "type": "array", + "items": { + "$ref": "#/definitions/privateEndpointOutputType" + }, + "metadata": { + "description": "The private endpoints of the congitive services account." + }, + "copy": { + "count": "[length(coalesce(parameters('privateEndpoints'), createArray()))]", + "input": { + "name": "[reference(format('cognitiveService_privateEndpoints[{0}]', copyIndex())).outputs.name.value]", + "resourceId": "[reference(format('cognitiveService_privateEndpoints[{0}]', copyIndex())).outputs.resourceId.value]", + "groupId": "[tryGet(tryGet(reference(format('cognitiveService_privateEndpoints[{0}]', copyIndex())).outputs, 'groupId'), 'value')]", + "customDnsConfigs": "[reference(format('cognitiveService_privateEndpoints[{0}]', copyIndex())).outputs.customDnsConfigs.value]", + "networkInterfaceResourceIds": "[reference(format('cognitiveService_privateEndpoints[{0}]', copyIndex())).outputs.networkInterfaceResourceIds.value]" + } + } + } + } + } + }, + "dependsOn": [ + "logAnalyticsWorkspace", + "userAssignedIdentity" + ] + }, + "aiServicesPrivateEndpoint": { + "condition": "[and(not(variables('useExistingAiFoundryAiProject')), parameters('enablePrivateNetworking'))]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "[take(format('pep-ai-services-{0}', variables('aiFoundryAiServicesResourceName')), 64)]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "name": { + "value": "[format('pep-{0}', variables('aiFoundryAiServicesResourceName'))]" + }, + "location": { + "value": "[variables('solutionLocation')]" + }, + "tags": { + "value": "[parameters('tags')]" + }, + "subnetResourceId": { + "value": "[reference('virtualNetwork').outputs.pepsSubnetResourceId.value]" + }, + "privateLinkServiceConnections": { + "value": [ + { + "name": "[format('pep-{0}', variables('aiFoundryAiServicesResourceName'))]", + "properties": { + "privateLinkServiceId": "[reference('aiFoundryAiServices').outputs.resourceId.value]", + "groupIds": [ + "account" + ] + } + } + ] + }, + "privateDnsZoneGroup": { + "value": { + "privateDnsZoneGroupConfigs": [ + { + "name": "cognitiveservices", + "privateDnsZoneResourceId": "[reference(format('avmPrivateDnsZones[{0}]', variables('dnsZoneIndex').cognitiveServices)).outputs.resourceId.value]" + }, + { + "name": "openai", + "privateDnsZoneResourceId": "[reference(format('avmPrivateDnsZones[{0}]', variables('dnsZoneIndex').openAI)).outputs.resourceId.value]" + } + ] + } + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "languageVersion": "2.0", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.34.44.8038", + "templateHash": "12389807800450456797" + }, + "name": "Private Endpoints", + "description": "This module deploys a Private Endpoint." + }, + "definitions": { + "privateDnsZoneGroupType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The name of the Private DNS Zone Group." + } + }, + "privateDnsZoneGroupConfigs": { + "type": "array", + "items": { + "$ref": "#/definitions/privateDnsZoneGroupConfigType" + }, + "metadata": { + "description": "Required. The private DNS zone groups to associate the private endpoint. A DNS zone group can support up to 5 DNS zones." } } }, - "dependsOn": [ - "databaseAccount" - ] + "metadata": { + "__bicep_export!": true + } }, - "databaseAccount_sqlRoleDefinitions": { - "copy": { - "name": "databaseAccount_sqlRoleDefinitions", - "count": "[length(coalesce(parameters('sqlRoleDefinitions'), createArray()))]" - }, - "type": "Microsoft.Resources/deployments", - "apiVersion": "2025-04-01", - "name": "[format('{0}-sqlrd-{1}', uniqueString(deployment().name, parameters('location')), copyIndex())]", + "ipConfigurationType": { + "type": "object", "properties": { - "expressionEvaluationOptions": { - "scope": "inner" - }, - "mode": "Incremental", - "parameters": { - "databaseAccountName": { - "value": "[parameters('name')]" - }, - "name": { - "value": "[tryGet(coalesce(parameters('sqlRoleDefinitions'), createArray())[copyIndex()], 'name')]" - }, - "dataActions": { - "value": "[coalesce(parameters('sqlRoleDefinitions'), createArray())[copyIndex()].dataActions]" - }, - "roleName": { - "value": "[coalesce(parameters('sqlRoleDefinitions'), createArray())[copyIndex()].roleName]" - }, - "assignableScopes": { - "value": "[tryGet(coalesce(parameters('sqlRoleDefinitions'), createArray())[copyIndex()], 'assignableScopes')]" - }, - "sqlRoleAssignments": { - "value": "[tryGet(coalesce(parameters('sqlRoleDefinitions'), createArray())[copyIndex()], 'assignments')]" - }, - "enableTelemetry": { - "value": "[variables('enableReferencedModulesTelemetry')]" + "name": { + "type": "string", + "metadata": { + "description": "Required. The name of the resource that is unique within a resource group." } }, - "template": { - "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", - "languageVersion": "2.0", - "contentVersion": "1.0.0.0", - "metadata": { - "_generator": { - "name": "bicep", - "version": "0.38.33.27573", - "templateHash": "8600771348637416058" - }, - "name": "DocumentDB Database Account SQL Role Definitions.", - "description": "This module deploys a SQL Role Definision in a CosmosDB Account." - }, - "definitions": { - "sqlRoleAssignmentType": { - "type": "object", - "properties": { - "name": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. Name unique identifier of the SQL Role Assignment." - } - }, - "principalId": { - "type": "string", - "metadata": { - "description": "Required. The unique identifier for the associated AAD principal in the AAD graph to which access is being granted through this Role Assignment. Tenant ID for the principal is inferred using the tenant associated with the subscription." - } - }, - "scope": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The data plane resource id for which access is being granted through this Role Assignment. Defaults to the root of the database account, but can also be scoped to e.g., the container and database level." - } - } - }, - "metadata": { - "__bicep_export!": true, - "description": "The type for the SQL Role Assignments." - } - } - }, - "parameters": { - "databaseAccountName": { + "properties": { + "type": "object", + "properties": { + "groupId": { "type": "string", "metadata": { - "description": "Conditional. The name of the parent Database Account. Required if the template is used in a standalone deployment." + "description": "Required. The ID of a group obtained from the remote resource that this private endpoint should connect to. If used with private link service connection, this property must be defined as empty string." } }, - "name": { + "memberName": { "type": "string", - "nullable": true, "metadata": { - "description": "Optional. The unique identifier of the Role Definition." + "description": "Required. The member name of a group obtained from the remote resource that this private endpoint should connect to. If used with private link service connection, this property must be defined as empty string." } }, - "roleName": { + "privateIPAddress": { "type": "string", "metadata": { - "description": "Required. A user-friendly name for the Role Definition. Must be unique for the database account." + "description": "Required. A private IP address obtained from the private endpoint's subnet." } - }, - "dataActions": { + } + }, + "metadata": { + "description": "Required. Properties of private endpoint IP configurations." + } + } + }, + "metadata": { + "__bicep_export!": true + } + }, + "privateLinkServiceConnectionType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "metadata": { + "description": "Required. The name of the private link service connection." + } + }, + "properties": { + "type": "object", + "properties": { + "groupIds": { "type": "array", "items": { "type": "string" }, - "minLength": 1, "metadata": { - "description": "Required. An array of data actions that are allowed." + "description": "Required. The ID of a group obtained from the remote resource that this private endpoint should connect to. If used with private link service connection, this property must be defined as empty string array `[]`." } }, - "assignableScopes": { - "type": "array", - "items": { - "type": "string" - }, - "nullable": true, + "privateLinkServiceId": { + "type": "string", "metadata": { - "description": "Optional. A set of fully qualified Scopes at or below which Role Assignments may be created using this Role Definition. This will allow application of this Role Definition on the entire database account or any underlying Database / Collection. Must have at least one element. Scopes higher than Database account are not enforceable as assignable Scopes. Note that resources referenced in assignable Scopes need not exist. Defaults to the current account." + "description": "Required. The resource id of private link service." } }, - "sqlRoleAssignments": { - "type": "array", - "items": { - "$ref": "#/definitions/sqlRoleAssignmentType" - }, + "requestMessage": { + "type": "string", "nullable": true, "metadata": { - "description": "Optional. An array of SQL Role Assignments to be created for the SQL Role Definition." - } - }, - "enableTelemetry": { - "type": "bool", - "defaultValue": true, - "metadata": { - "description": "Optional. Enable/Disable usage telemetry for module." + "description": "Optional. A message passed to the owner of the remote resource with this connection request. Restricted to 140 chars." } } }, - "variables": { - "enableReferencedModulesTelemetry": false - }, - "resources": { - "avmTelemetry": { - "condition": "[parameters('enableTelemetry')]", - "type": "Microsoft.Resources/deployments", - "apiVersion": "2024-03-01", - "name": "[format('46d3xbcp.res.doctdb-dbacct-sqlroledefinition.{0}.{1}', replace('-..--..-', '.', '-'), substring(uniqueString(deployment().name), 0, 4))]", - "properties": { - "mode": "Incremental", - "template": { - "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", - "contentVersion": "1.0.0.0", - "resources": [], - "outputs": { - "telemetry": { - "type": "String", - "value": "For more information, see https://aka.ms/avm/TelemetryInfo" - } - } - } - } - }, - "databaseAccount": { - "existing": true, - "type": "Microsoft.DocumentDB/databaseAccounts", - "apiVersion": "2024-11-15", - "name": "[parameters('databaseAccountName')]" - }, - "sqlRoleDefinition": { - "type": "Microsoft.DocumentDB/databaseAccounts/sqlRoleDefinitions", - "apiVersion": "2024-11-15", - "name": "[format('{0}/{1}', parameters('databaseAccountName'), coalesce(parameters('name'), guid(resourceId('Microsoft.DocumentDB/databaseAccounts', parameters('databaseAccountName')), parameters('databaseAccountName'), parameters('roleName'))))]", - "properties": { - "assignableScopes": "[coalesce(parameters('assignableScopes'), createArray(resourceId('Microsoft.DocumentDB/databaseAccounts', parameters('databaseAccountName'))))]", - "permissions": [ - { - "dataActions": "[parameters('dataActions')]" - } - ], - "roleName": "[parameters('roleName')]", - "type": "CustomRole" - } - }, - "databaseAccount_sqlRoleAssignments": { - "copy": { - "name": "databaseAccount_sqlRoleAssignments", - "count": "[length(coalesce(parameters('sqlRoleAssignments'), createArray()))]" - }, - "type": "Microsoft.Resources/deployments", - "apiVersion": "2025-04-01", - "name": "[format('{0}-sqlra-{1}', uniqueString(deployment().name), copyIndex())]", - "properties": { - "expressionEvaluationOptions": { - "scope": "inner" - }, - "mode": "Incremental", - "parameters": { - "databaseAccountName": { - "value": "[parameters('databaseAccountName')]" - }, - "roleDefinitionIdOrName": { - "value": "[resourceId('Microsoft.DocumentDB/databaseAccounts/sqlRoleDefinitions', parameters('databaseAccountName'), coalesce(parameters('name'), guid(resourceId('Microsoft.DocumentDB/databaseAccounts', parameters('databaseAccountName')), parameters('databaseAccountName'), parameters('roleName'))))]" - }, - "principalId": { - "value": "[coalesce(parameters('sqlRoleAssignments'), createArray())[copyIndex()].principalId]" - }, - "name": { - "value": "[tryGet(coalesce(parameters('sqlRoleAssignments'), createArray())[copyIndex()], 'name')]" - }, - "enableTelemetry": { - "value": "[variables('enableReferencedModulesTelemetry')]" - } - }, - "template": { - "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", - "languageVersion": "2.0", - "contentVersion": "1.0.0.0", - "metadata": { - "_generator": { - "name": "bicep", - "version": "0.38.33.27573", - "templateHash": "17007224102611744259" - }, - "name": "DocumentDB Database Account SQL Role Assignments.", - "description": "This module deploys a SQL Role Assignment in a CosmosDB Account." - }, - "parameters": { - "databaseAccountName": { - "type": "string", - "metadata": { - "description": "Conditional. The name of the parent Database Account. Required if the template is used in a standalone deployment." - } - }, - "name": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. Name unique identifier of the SQL Role Assignment." - } - }, - "principalId": { - "type": "string", - "metadata": { - "description": "Required. The unique identifier for the associated AAD principal in the AAD graph to which access is being granted through this Role Assignment. Tenant ID for the principal is inferred using the tenant associated with the subscription." - } - }, - "roleDefinitionIdOrName": { - "type": "string", - "metadata": { - "description": "Required. The unique identifier of the associated SQL Role Definition." - } - }, - "enableTelemetry": { - "type": "bool", - "defaultValue": true, - "metadata": { - "description": "Optional. Enable/Disable usage telemetry for module." - } - }, - "scope": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The data plane resource id for which access is being granted through this Role Assignment. Defaults to the root of the database account, but can also be scoped to e.g., the container and database level." - } - } - }, - "variables": { - "builtInDataPlaneRoleNames": { - "Cosmos DB Built-in Data Reader": "[format('{0}/sqlRoleDefinitions/00000000-0000-0000-0000-000000000001', resourceId('Microsoft.DocumentDB/databaseAccounts', parameters('databaseAccountName')))]", - "Cosmos DB Built-in Data Contributor": "[format('{0}/sqlRoleDefinitions/00000000-0000-0000-0000-000000000002', resourceId('Microsoft.DocumentDB/databaseAccounts', parameters('databaseAccountName')))]" - }, - "formattedRoleDefinition": "[coalesce(tryGet(variables('builtInDataPlaneRoleNames'), parameters('roleDefinitionIdOrName')), if(contains(parameters('roleDefinitionIdOrName'), '/sqlRoleDefinitions/'), parameters('roleDefinitionIdOrName'), format('{0}/sqlRoleDefinitions/{1}', resourceId('Microsoft.DocumentDB/databaseAccounts', parameters('databaseAccountName')), parameters('roleDefinitionIdOrName'))))]", - "formattedScope": "[replace(replace(coalesce(parameters('scope'), resourceId('Microsoft.DocumentDB/databaseAccounts', parameters('databaseAccountName'))), '/sqlDatabases/', '/dbs/'), '/containers/', '/colls/')]" - }, - "resources": { - "avmTelemetry": { - "condition": "[parameters('enableTelemetry')]", - "type": "Microsoft.Resources/deployments", - "apiVersion": "2024-03-01", - "name": "[format('46d3xbcp.res.doctdb-dbacct-sqlroleassignment.{0}.{1}', replace('-..--..-', '.', '-'), substring(uniqueString(deployment().name), 0, 4))]", - "properties": { - "mode": "Incremental", - "template": { - "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", - "contentVersion": "1.0.0.0", - "resources": [], - "outputs": { - "telemetry": { - "type": "String", - "value": "For more information, see https://aka.ms/avm/TelemetryInfo" - } - } - } - } - }, - "databaseAccount": { - "existing": true, - "type": "Microsoft.DocumentDB/databaseAccounts", - "apiVersion": "2024-11-15", - "name": "[parameters('databaseAccountName')]" - }, - "sqlRoleAssignment": { - "type": "Microsoft.DocumentDB/databaseAccounts/sqlRoleAssignments", - "apiVersion": "2024-11-15", - "name": "[format('{0}/{1}', parameters('databaseAccountName'), coalesce(parameters('name'), guid(variables('formattedRoleDefinition'), parameters('principalId'), variables('formattedScope'))))]", - "properties": { - "principalId": "[parameters('principalId')]", - "roleDefinitionId": "[variables('formattedRoleDefinition')]", - "scope": "[variables('formattedScope')]" - } - } - }, - "outputs": { - "name": { - "type": "string", - "metadata": { - "description": "The name of the SQL Role Assignment." - }, - "value": "[coalesce(parameters('name'), guid(variables('formattedRoleDefinition'), parameters('principalId'), variables('formattedScope')))]" - }, - "resourceId": { - "type": "string", - "metadata": { - "description": "The resource ID of the SQL Role Assignment." - }, - "value": "[resourceId('Microsoft.DocumentDB/databaseAccounts/sqlRoleAssignments', parameters('databaseAccountName'), coalesce(parameters('name'), guid(variables('formattedRoleDefinition'), parameters('principalId'), variables('formattedScope'))))]" - }, - "resourceGroupName": { - "type": "string", - "metadata": { - "description": "The name of the resource group the SQL Role Definition was created in." - }, - "value": "[resourceGroup().name]" - } - } - } - }, - "dependsOn": [ - "sqlRoleDefinition" - ] - } + "metadata": { + "description": "Required. Properties of private link service connection." + } + } + }, + "metadata": { + "__bicep_export!": true + } + }, + "customDnsConfigType": { + "type": "object", + "properties": { + "fqdn": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. FQDN that resolves to private endpoint IP address." + } + }, + "ipAddresses": { + "type": "array", + "items": { + "type": "string" }, - "outputs": { - "name": { - "type": "string", - "metadata": { - "description": "The name of the SQL Role Definition." - }, - "value": "[coalesce(parameters('name'), guid(resourceId('Microsoft.DocumentDB/databaseAccounts', parameters('databaseAccountName')), parameters('databaseAccountName'), parameters('roleName')))]" - }, - "resourceId": { - "type": "string", - "metadata": { - "description": "The resource ID of the SQL Role Definition." - }, - "value": "[resourceId('Microsoft.DocumentDB/databaseAccounts/sqlRoleDefinitions', parameters('databaseAccountName'), coalesce(parameters('name'), guid(resourceId('Microsoft.DocumentDB/databaseAccounts', parameters('databaseAccountName')), parameters('databaseAccountName'), parameters('roleName'))))]" - }, - "resourceGroupName": { - "type": "string", - "metadata": { - "description": "The name of the resource group the SQL Role Definition was created in." - }, - "value": "[resourceGroup().name]" - }, - "roleName": { - "type": "string", - "metadata": { - "description": "The role name of the SQL Role Definition." - }, - "value": "[reference('sqlRoleDefinition').roleName]" - } + "metadata": { + "description": "Required. A list of private IP addresses of the private endpoint." + } + } + }, + "metadata": { + "__bicep_export!": true + } + }, + "lockType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Specify the name of lock." + } + }, + "kind": { + "type": "string", + "allowedValues": [ + "CanNotDelete", + "None", + "ReadOnly" + ], + "nullable": true, + "metadata": { + "description": "Optional. Specify the type of lock." + } + } + }, + "metadata": { + "description": "An AVM-aligned type for a lock.", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.5.1" + } + } + }, + "privateDnsZoneGroupConfigType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The name of the private DNS zone group config." + } + }, + "privateDnsZoneResourceId": { + "type": "string", + "metadata": { + "description": "Required. The resource id of the private DNS zone." + } + } + }, + "metadata": { + "__bicep_imported_from!": { + "sourceTemplate": "private-dns-zone-group/main.bicep" + } + } + }, + "roleAssignmentType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The name (as GUID) of the role assignment. If not provided, a GUID will be generated." + } + }, + "roleDefinitionIdOrName": { + "type": "string", + "metadata": { + "description": "Required. The role to assign. You can provide either the display name of the role definition, the role definition GUID, or its fully qualified ID in the following format: '/providers/Microsoft.Authorization/roleDefinitions/c2f4ef07-c644-48eb-af81-4b1b4947fb11'." + } + }, + "principalId": { + "type": "string", + "metadata": { + "description": "Required. The principal ID of the principal (user/group/identity) to assign the role to." + } + }, + "principalType": { + "type": "string", + "allowedValues": [ + "Device", + "ForeignGroup", + "Group", + "ServicePrincipal", + "User" + ], + "nullable": true, + "metadata": { + "description": "Optional. The principal type of the assigned principal ID." + } + }, + "description": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The description of the role assignment." + } + }, + "condition": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The conditions on the role assignment. This limits the resources it can be assigned to. e.g.: @Resource[Microsoft.Storage/storageAccounts/blobServices/containers:ContainerName] StringEqualsIgnoreCase \"foo_storage_container\"." + } + }, + "conditionVersion": { + "type": "string", + "allowedValues": [ + "2.0" + ], + "nullable": true, + "metadata": { + "description": "Optional. Version of the condition." + } + }, + "delegatedManagedIdentityResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The Resource Id of the delegated managed identity resource." } } }, - "dependsOn": [ - "databaseAccount" - ] + "metadata": { + "description": "An AVM-aligned type for a role assignment.", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.5.1" + } + } + } + }, + "parameters": { + "name": { + "type": "string", + "metadata": { + "description": "Required. Name of the private endpoint resource to create." + } }, - "databaseAccount_sqlRoleAssignments": { - "copy": { - "name": "databaseAccount_sqlRoleAssignments", - "count": "[length(coalesce(parameters('sqlRoleAssignments'), createArray()))]" + "subnetResourceId": { + "type": "string", + "metadata": { + "description": "Required. Resource ID of the subnet where the endpoint needs to be created." + } + }, + "applicationSecurityGroupResourceIds": { + "type": "array", + "items": { + "type": "string" + }, + "nullable": true, + "metadata": { + "description": "Optional. Application security groups in which the private endpoint IP configuration is included." + } + }, + "customNetworkInterfaceName": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The custom name of the network interface attached to the private endpoint." + } + }, + "ipConfigurations": { + "type": "array", + "items": { + "$ref": "#/definitions/ipConfigurationType" + }, + "nullable": true, + "metadata": { + "description": "Optional. A list of IP configurations of the private endpoint. This will be used to map to the First Party Service endpoints." + } + }, + "privateDnsZoneGroup": { + "$ref": "#/definitions/privateDnsZoneGroupType", + "nullable": true, + "metadata": { + "description": "Optional. The private DNS zone group to configure for the private endpoint." + } + }, + "location": { + "type": "string", + "defaultValue": "[resourceGroup().location]", + "metadata": { + "description": "Optional. Location for all Resources." + } + }, + "lock": { + "$ref": "#/definitions/lockType", + "nullable": true, + "metadata": { + "description": "Optional. The lock settings of the service." + } + }, + "roleAssignments": { + "type": "array", + "items": { + "$ref": "#/definitions/roleAssignmentType" + }, + "nullable": true, + "metadata": { + "description": "Optional. Array of role assignments to create." + } + }, + "tags": { + "type": "object", + "nullable": true, + "metadata": { + "description": "Optional. Tags to be applied on all resources/resource groups in this deployment." + } + }, + "customDnsConfigs": { + "type": "array", + "items": { + "$ref": "#/definitions/customDnsConfigType" + }, + "nullable": true, + "metadata": { + "description": "Optional. Custom DNS configurations." + } + }, + "manualPrivateLinkServiceConnections": { + "type": "array", + "items": { + "$ref": "#/definitions/privateLinkServiceConnectionType" }, + "nullable": true, + "metadata": { + "description": "Conditional. A grouping of information about the connection to the remote resource. Used when the network admin does not have access to approve connections to the remote resource. Required if `privateLinkServiceConnections` is empty." + } + }, + "privateLinkServiceConnections": { + "type": "array", + "items": { + "$ref": "#/definitions/privateLinkServiceConnectionType" + }, + "nullable": true, + "metadata": { + "description": "Conditional. A grouping of information about the connection to the remote resource. Required if `manualPrivateLinkServiceConnections` is empty." + } + }, + "enableTelemetry": { + "type": "bool", + "defaultValue": true, + "metadata": { + "description": "Optional. Enable/Disable usage telemetry for module." + } + } + }, + "variables": { + "copy": [ + { + "name": "formattedRoleAssignments", + "count": "[length(coalesce(parameters('roleAssignments'), createArray()))]", + "input": "[union(coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')], createObject('roleDefinitionId', coalesce(tryGet(variables('builtInRoleNames'), coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName), if(contains(coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName, '/providers/Microsoft.Authorization/roleDefinitions/'), coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName, subscriptionResourceId('Microsoft.Authorization/roleDefinitions', coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName)))))]" + } + ], + "builtInRoleNames": { + "Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'b24988ac-6180-42a0-ab88-20f7382dd24c')]", + "DNS Resolver Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '0f2ebee7-ffd4-4fc0-b3b7-664099fdad5d')]", + "DNS Zone Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'befefa01-2a29-4197-83a8-272ff33ce314')]", + "Domain Services Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'eeaeda52-9324-47f6-8069-5d5bade478b2')]", + "Domain Services Reader": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '361898ef-9ed1-48c2-849c-a832951106bb')]", + "Network Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '4d97b98b-1d4f-4787-a291-c67834d212e7')]", + "Owner": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '8e3af657-a8ff-443c-a75c-2fe8c4bcb635')]", + "Private DNS Zone Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'b12aa53e-6015-4669-85d0-8515ebb3ae7f')]", + "Reader": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'acdd72a7-3385-48ef-bd42-f606fba81ae7')]", + "Role Based Access Control Administrator": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'f58310d9-a9f6-439a-9e8d-f62e7b41a168')]" + } + }, + "resources": { + "avmTelemetry": { + "condition": "[parameters('enableTelemetry')]", "type": "Microsoft.Resources/deployments", - "apiVersion": "2025-04-01", - "name": "[format('{0}-sqlra-{1}', uniqueString(deployment().name), copyIndex())]", + "apiVersion": "2024-03-01", + "name": "[format('46d3xbcp.res.network-privateendpoint.{0}.{1}', replace('0.11.0', '.', '-'), substring(uniqueString(deployment().name, parameters('location')), 0, 4))]", "properties": { - "expressionEvaluationOptions": { - "scope": "inner" - }, "mode": "Incremental", - "parameters": { - "databaseAccountName": { - "value": "[parameters('name')]" - }, - "roleDefinitionIdOrName": { - "value": "[coalesce(parameters('sqlRoleAssignments'), createArray())[copyIndex()].roleDefinitionId]" - }, - "principalId": { - "value": "[coalesce(parameters('sqlRoleAssignments'), createArray())[copyIndex()].principalId]" - }, - "name": { - "value": "[tryGet(coalesce(parameters('sqlRoleAssignments'), createArray())[copyIndex()], 'name')]" - }, - "scope": { - "value": "[tryGet(coalesce(parameters('sqlRoleAssignments'), createArray())[copyIndex()], 'scope')]" - }, - "enableTelemetry": { - "value": "[variables('enableReferencedModulesTelemetry')]" - } - }, "template": { "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", - "languageVersion": "2.0", "contentVersion": "1.0.0.0", - "metadata": { - "_generator": { - "name": "bicep", - "version": "0.38.33.27573", - "templateHash": "17007224102611744259" - }, - "name": "DocumentDB Database Account SQL Role Assignments.", - "description": "This module deploys a SQL Role Assignment in a CosmosDB Account." - }, - "parameters": { - "databaseAccountName": { - "type": "string", - "metadata": { - "description": "Conditional. The name of the parent Database Account. Required if the template is used in a standalone deployment." - } - }, - "name": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. Name unique identifier of the SQL Role Assignment." - } - }, - "principalId": { - "type": "string", - "metadata": { - "description": "Required. The unique identifier for the associated AAD principal in the AAD graph to which access is being granted through this Role Assignment. Tenant ID for the principal is inferred using the tenant associated with the subscription." - } - }, - "roleDefinitionIdOrName": { - "type": "string", - "metadata": { - "description": "Required. The unique identifier of the associated SQL Role Definition." - } - }, - "enableTelemetry": { - "type": "bool", - "defaultValue": true, - "metadata": { - "description": "Optional. Enable/Disable usage telemetry for module." - } - }, - "scope": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The data plane resource id for which access is being granted through this Role Assignment. Defaults to the root of the database account, but can also be scoped to e.g., the container and database level." - } - } - }, - "variables": { - "builtInDataPlaneRoleNames": { - "Cosmos DB Built-in Data Reader": "[format('{0}/sqlRoleDefinitions/00000000-0000-0000-0000-000000000001', resourceId('Microsoft.DocumentDB/databaseAccounts', parameters('databaseAccountName')))]", - "Cosmos DB Built-in Data Contributor": "[format('{0}/sqlRoleDefinitions/00000000-0000-0000-0000-000000000002', resourceId('Microsoft.DocumentDB/databaseAccounts', parameters('databaseAccountName')))]" - }, - "formattedRoleDefinition": "[coalesce(tryGet(variables('builtInDataPlaneRoleNames'), parameters('roleDefinitionIdOrName')), if(contains(parameters('roleDefinitionIdOrName'), '/sqlRoleDefinitions/'), parameters('roleDefinitionIdOrName'), format('{0}/sqlRoleDefinitions/{1}', resourceId('Microsoft.DocumentDB/databaseAccounts', parameters('databaseAccountName')), parameters('roleDefinitionIdOrName'))))]", - "formattedScope": "[replace(replace(coalesce(parameters('scope'), resourceId('Microsoft.DocumentDB/databaseAccounts', parameters('databaseAccountName'))), '/sqlDatabases/', '/dbs/'), '/containers/', '/colls/')]" - }, - "resources": { - "avmTelemetry": { - "condition": "[parameters('enableTelemetry')]", - "type": "Microsoft.Resources/deployments", - "apiVersion": "2024-03-01", - "name": "[format('46d3xbcp.res.doctdb-dbacct-sqlroleassignment.{0}.{1}', replace('-..--..-', '.', '-'), substring(uniqueString(deployment().name), 0, 4))]", - "properties": { - "mode": "Incremental", - "template": { - "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", - "contentVersion": "1.0.0.0", - "resources": [], - "outputs": { - "telemetry": { - "type": "String", - "value": "For more information, see https://aka.ms/avm/TelemetryInfo" - } - } - } - } - }, - "databaseAccount": { - "existing": true, - "type": "Microsoft.DocumentDB/databaseAccounts", - "apiVersion": "2024-11-15", - "name": "[parameters('databaseAccountName')]" - }, - "sqlRoleAssignment": { - "type": "Microsoft.DocumentDB/databaseAccounts/sqlRoleAssignments", - "apiVersion": "2024-11-15", - "name": "[format('{0}/{1}', parameters('databaseAccountName'), coalesce(parameters('name'), guid(variables('formattedRoleDefinition'), parameters('principalId'), variables('formattedScope'))))]", - "properties": { - "principalId": "[parameters('principalId')]", - "roleDefinitionId": "[variables('formattedRoleDefinition')]", - "scope": "[variables('formattedScope')]" - } - } - }, + "resources": [], "outputs": { - "name": { - "type": "string", - "metadata": { - "description": "The name of the SQL Role Assignment." - }, - "value": "[coalesce(parameters('name'), guid(variables('formattedRoleDefinition'), parameters('principalId'), variables('formattedScope')))]" - }, - "resourceId": { - "type": "string", - "metadata": { - "description": "The resource ID of the SQL Role Assignment." - }, - "value": "[resourceId('Microsoft.DocumentDB/databaseAccounts/sqlRoleAssignments', parameters('databaseAccountName'), coalesce(parameters('name'), guid(variables('formattedRoleDefinition'), parameters('principalId'), variables('formattedScope'))))]" - }, - "resourceGroupName": { - "type": "string", - "metadata": { - "description": "The name of the resource group the SQL Role Definition was created in." - }, - "value": "[resourceGroup().name]" + "telemetry": { + "type": "String", + "value": "For more information, see https://aka.ms/avm/TelemetryInfo" + } + } + } + } + }, + "privateEndpoint": { + "type": "Microsoft.Network/privateEndpoints", + "apiVersion": "2024-05-01", + "name": "[parameters('name')]", + "location": "[parameters('location')]", + "tags": "[parameters('tags')]", + "properties": { + "copy": [ + { + "name": "applicationSecurityGroups", + "count": "[length(coalesce(parameters('applicationSecurityGroupResourceIds'), createArray()))]", + "input": { + "id": "[coalesce(parameters('applicationSecurityGroupResourceIds'), createArray())[copyIndex('applicationSecurityGroups')]]" } } + ], + "customDnsConfigs": "[coalesce(parameters('customDnsConfigs'), createArray())]", + "customNetworkInterfaceName": "[coalesce(parameters('customNetworkInterfaceName'), '')]", + "ipConfigurations": "[coalesce(parameters('ipConfigurations'), createArray())]", + "manualPrivateLinkServiceConnections": "[coalesce(parameters('manualPrivateLinkServiceConnections'), createArray())]", + "privateLinkServiceConnections": "[coalesce(parameters('privateLinkServiceConnections'), createArray())]", + "subnet": { + "id": "[parameters('subnetResourceId')]" } + } + }, + "privateEndpoint_lock": { + "condition": "[and(not(empty(coalesce(parameters('lock'), createObject()))), not(equals(tryGet(parameters('lock'), 'kind'), 'None')))]", + "type": "Microsoft.Authorization/locks", + "apiVersion": "2020-05-01", + "scope": "[format('Microsoft.Network/privateEndpoints/{0}', parameters('name'))]", + "name": "[coalesce(tryGet(parameters('lock'), 'name'), format('lock-{0}', parameters('name')))]", + "properties": { + "level": "[coalesce(tryGet(parameters('lock'), 'kind'), '')]", + "notes": "[if(equals(tryGet(parameters('lock'), 'kind'), 'CanNotDelete'), 'Cannot delete resource or child resources.', 'Cannot delete or modify the resource or child resources.')]" }, "dependsOn": [ - "databaseAccount", - "databaseAccount_sqlDatabases", - "databaseAccount_sqlRoleDefinitions" + "privateEndpoint" ] }, - "databaseAccount_cassandraRoleDefinitions": { + "privateEndpoint_roleAssignments": { "copy": { - "name": "databaseAccount_cassandraRoleDefinitions", - "count": "[length(coalesce(parameters('cassandraRoleDefinitions'), createArray()))]" + "name": "privateEndpoint_roleAssignments", + "count": "[length(coalesce(variables('formattedRoleAssignments'), createArray()))]" + }, + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2022-04-01", + "scope": "[format('Microsoft.Network/privateEndpoints/{0}', parameters('name'))]", + "name": "[coalesce(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'name'), guid(resourceId('Microsoft.Network/privateEndpoints', parameters('name')), coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].principalId, coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].roleDefinitionId))]", + "properties": { + "roleDefinitionId": "[coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].roleDefinitionId]", + "principalId": "[coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].principalId]", + "description": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'description')]", + "principalType": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'principalType')]", + "condition": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'condition')]", + "conditionVersion": "[if(not(empty(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'condition'))), coalesce(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'conditionVersion'), '2.0'), null())]", + "delegatedManagedIdentityResourceId": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'delegatedManagedIdentityResourceId')]" }, + "dependsOn": [ + "privateEndpoint" + ] + }, + "privateEndpoint_privateDnsZoneGroup": { + "condition": "[not(empty(parameters('privateDnsZoneGroup')))]", "type": "Microsoft.Resources/deployments", - "apiVersion": "2025-04-01", - "name": "[format('{0}-cassandra-rd-{1}', uniqueString(deployment().name, parameters('location')), copyIndex())]", + "apiVersion": "2022-09-01", + "name": "[format('{0}-PrivateEndpoint-PrivateDnsZoneGroup', uniqueString(deployment().name))]", "properties": { "expressionEvaluationOptions": { "scope": "inner" }, "mode": "Incremental", "parameters": { - "databaseAccountName": { - "value": "[parameters('name')]" - }, "name": { - "value": "[tryGet(coalesce(parameters('cassandraRoleDefinitions'), createArray())[copyIndex()], 'name')]" - }, - "roleName": { - "value": "[coalesce(parameters('cassandraRoleDefinitions'), createArray())[copyIndex()].roleName]" - }, - "dataActions": { - "value": "[tryGet(coalesce(parameters('cassandraRoleDefinitions'), createArray())[copyIndex()], 'dataActions')]" - }, - "notDataActions": { - "value": "[tryGet(coalesce(parameters('cassandraRoleDefinitions'), createArray())[copyIndex()], 'notDataActions')]" + "value": "[tryGet(parameters('privateDnsZoneGroup'), 'name')]" }, - "assignableScopes": { - "value": "[tryGet(coalesce(parameters('cassandraRoleDefinitions'), createArray())[copyIndex()], 'assignableScopes')]" + "privateEndpointName": { + "value": "[parameters('name')]" }, - "cassandraRoleAssignments": { - "value": "[tryGet(coalesce(parameters('cassandraRoleDefinitions'), createArray())[copyIndex()], 'assignments')]" + "privateDnsZoneConfigs": { + "value": "[parameters('privateDnsZoneGroup').privateDnsZoneGroupConfigs]" } }, "template": { @@ -28003,1242 +24756,1746 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.38.33.27573", - "templateHash": "17859939500809924517" + "version": "0.34.44.8038", + "templateHash": "13997305779829540948" }, - "name": "DocumentDB Database Account Cassandra Role Definitions.", - "description": "This module deploys a Cassandra Role Definition in a CosmosDB Account." + "name": "Private Endpoint Private DNS Zone Groups", + "description": "This module deploys a Private Endpoint Private DNS Zone Group." }, "definitions": { - "cassandraRoleAssignmentType": { + "privateDnsZoneGroupConfigType": { "type": "object", "properties": { "name": { "type": "string", "nullable": true, "metadata": { - "description": "Optional. The unique identifier of the role assignment." - } - }, - "principalId": { - "type": "string", - "metadata": { - "description": "Required. The unique identifier for the associated AAD principal." + "description": "Optional. The name of the private DNS zone group config." } }, - "scope": { + "privateDnsZoneResourceId": { "type": "string", - "nullable": true, "metadata": { - "description": "Optional. The data plane resource path for which access is being granted. Defaults to the current account." - } - } - }, - "metadata": { - "__bicep_export!": true - } - } - }, - "parameters": { - "databaseAccountName": { - "type": "string", - "metadata": { - "description": "Conditional. The name of the parent Database Account. Required if the template is used in a standalone deployment." - } - }, - "name": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The unique identifier of the Role Definition." - } - }, - "roleName": { - "type": "string", - "metadata": { - "description": "Required. A user-friendly name for the Role Definition. Must be unique for the database account." - } - }, - "dataActions": { - "type": "array", - "items": { - "type": "string" - }, - "defaultValue": [], - "metadata": { - "description": "Optional. An array of data actions that are allowed. Note: Valid data action strings for Cassandra API are currently undocumented (as of API version 2025-05-01-preview). Please refer to official Azure documentation once available." - } - }, - "notDataActions": { - "type": "array", - "items": { - "type": "string" + "description": "Required. The resource id of the private DNS zone." + } + } }, - "defaultValue": [], "metadata": { - "description": "Optional. An array of data actions that are denied. Note: Unlike SQL RBAC, Cassandra RBAC supports deny rules (notDataActions) for granular access control. Valid data action strings are currently undocumented (as of API version 2025-05-01-preview)." + "__bicep_export!": true + } + } + }, + "parameters": { + "privateEndpointName": { + "type": "string", + "metadata": { + "description": "Conditional. The name of the parent private endpoint. Required if the template is used in a standalone deployment." } }, - "assignableScopes": { + "privateDnsZoneConfigs": { "type": "array", "items": { - "type": "string" + "$ref": "#/definitions/privateDnsZoneGroupConfigType" }, - "nullable": true, + "minLength": 1, + "maxLength": 5, "metadata": { - "description": "Optional. A set of fully qualified Scopes at or below which Role Assignments may be created using this Role Definition. This will allow application of this Role Definition on the entire database account or any underlying Database / Keyspace. Must have at least one element. Scopes higher than Database account are not enforceable as assignable Scopes. Note that resources referenced in assignable Scopes need not exist. Defaults to the current account." + "description": "Required. Array of private DNS zone configurations of the private DNS zone group. A DNS zone group can support up to 5 DNS zones." } }, - "cassandraRoleAssignments": { - "type": "array", - "items": { - "$ref": "#/definitions/cassandraRoleAssignmentType" - }, - "nullable": true, + "name": { + "type": "string", + "defaultValue": "default", "metadata": { - "description": "Optional. An array of Cassandra Role Assignments to be created for the Cassandra Role Definition." + "description": "Optional. The name of the private DNS zone group." } } }, + "variables": { + "copy": [ + { + "name": "privateDnsZoneConfigsVar", + "count": "[length(parameters('privateDnsZoneConfigs'))]", + "input": { + "name": "[coalesce(tryGet(parameters('privateDnsZoneConfigs')[copyIndex('privateDnsZoneConfigsVar')], 'name'), last(split(parameters('privateDnsZoneConfigs')[copyIndex('privateDnsZoneConfigsVar')].privateDnsZoneResourceId, '/')))]", + "properties": { + "privateDnsZoneId": "[parameters('privateDnsZoneConfigs')[copyIndex('privateDnsZoneConfigsVar')].privateDnsZoneResourceId]" + } + } + } + ] + }, "resources": { - "databaseAccount": { + "privateEndpoint": { "existing": true, - "type": "Microsoft.DocumentDB/databaseAccounts", - "apiVersion": "2024-11-15", - "name": "[parameters('databaseAccountName')]" + "type": "Microsoft.Network/privateEndpoints", + "apiVersion": "2024-05-01", + "name": "[parameters('privateEndpointName')]" }, - "cassandraRoleDefinition": { - "type": "Microsoft.DocumentDB/databaseAccounts/cassandraRoleDefinitions", - "apiVersion": "2025-05-01-preview", - "name": "[format('{0}/{1}', parameters('databaseAccountName'), coalesce(parameters('name'), guid(resourceId('Microsoft.DocumentDB/databaseAccounts', parameters('databaseAccountName')), parameters('databaseAccountName'), parameters('roleName'))))]", + "privateDnsZoneGroup": { + "type": "Microsoft.Network/privateEndpoints/privateDnsZoneGroups", + "apiVersion": "2024-05-01", + "name": "[format('{0}/{1}', parameters('privateEndpointName'), parameters('name'))]", "properties": { - "assignableScopes": "[coalesce(parameters('assignableScopes'), createArray(resourceId('Microsoft.DocumentDB/databaseAccounts', parameters('databaseAccountName'))))]", - "permissions": [ - { - "dataActions": "[parameters('dataActions')]", - "notDataActions": "[parameters('notDataActions')]" - } - ], - "roleName": "[parameters('roleName')]", - "type": "CustomRole" + "privateDnsZoneConfigs": "[variables('privateDnsZoneConfigsVar')]" } + } + }, + "outputs": { + "name": { + "type": "string", + "metadata": { + "description": "The name of the private endpoint DNS zone group." + }, + "value": "[parameters('name')]" }, - "databaseAccount_cassandraRoleAssignments": { - "copy": { - "name": "databaseAccount_cassandraRoleAssignments", - "count": "[length(coalesce(parameters('cassandraRoleAssignments'), createArray()))]" + "resourceId": { + "type": "string", + "metadata": { + "description": "The resource ID of the private endpoint DNS zone group." + }, + "value": "[resourceId('Microsoft.Network/privateEndpoints/privateDnsZoneGroups', parameters('privateEndpointName'), parameters('name'))]" + }, + "resourceGroupName": { + "type": "string", + "metadata": { + "description": "The resource group the private endpoint DNS zone group was deployed into." + }, + "value": "[resourceGroup().name]" + } + } + } + }, + "dependsOn": [ + "privateEndpoint" + ] + } + }, + "outputs": { + "resourceGroupName": { + "type": "string", + "metadata": { + "description": "The resource group the private endpoint was deployed into." + }, + "value": "[resourceGroup().name]" + }, + "resourceId": { + "type": "string", + "metadata": { + "description": "The resource ID of the private endpoint." + }, + "value": "[resourceId('Microsoft.Network/privateEndpoints', parameters('name'))]" + }, + "name": { + "type": "string", + "metadata": { + "description": "The name of the private endpoint." + }, + "value": "[parameters('name')]" + }, + "location": { + "type": "string", + "metadata": { + "description": "The location the resource was deployed into." + }, + "value": "[reference('privateEndpoint', '2024-05-01', 'full').location]" + }, + "customDnsConfigs": { + "type": "array", + "items": { + "$ref": "#/definitions/customDnsConfigType" + }, + "metadata": { + "description": "The custom DNS configurations of the private endpoint." + }, + "value": "[reference('privateEndpoint').customDnsConfigs]" + }, + "networkInterfaceResourceIds": { + "type": "array", + "items": { + "type": "string" + }, + "metadata": { + "description": "The resource IDs of the network interfaces associated with the private endpoint." + }, + "value": "[map(reference('privateEndpoint').networkInterfaces, lambda('nic', lambdaVariables('nic').id))]" + }, + "groupId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "The group Id for the private endpoint Group." + }, + "value": "[coalesce(tryGet(tryGet(tryGet(tryGet(reference('privateEndpoint'), 'manualPrivateLinkServiceConnections'), 0, 'properties'), 'groupIds'), 0), tryGet(tryGet(tryGet(tryGet(reference('privateEndpoint'), 'privateLinkServiceConnections'), 0, 'properties'), 'groupIds'), 0))]" + } + } + } + }, + "dependsOn": [ + "aiFoundryAiServices", + "[format('avmPrivateDnsZones[{0}]', variables('dnsZoneIndex').cognitiveServices)]", + "[format('avmPrivateDnsZones[{0}]', variables('dnsZoneIndex').openAI)]", + "virtualNetwork" + ] + }, + "aiFoundryAiServicesProject": { + "condition": "[not(variables('useExistingAiFoundryAiProject'))]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "[take(format('module.ai-project.{0}', variables('aiFoundryAiProjectResourceName')), 64)]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "name": { + "value": "[variables('aiFoundryAiProjectResourceName')]" + }, + "location": { + "value": "[parameters('azureAiServiceLocation')]" + }, + "tags": { + "value": "[parameters('tags')]" + }, + "desc": { + "value": "[variables('aiFoundryAiProjectDescription')]" + }, + "aiServicesName": { + "value": "[variables('aiFoundryAiServicesResourceName')]" + }, + "azureExistingAIProjectResourceId": { + "value": "[parameters('azureExistingAIProjectResourceId')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.39.26.7824", + "templateHash": "12336056765515184474" + } + }, + "parameters": { + "name": { + "type": "string", + "metadata": { + "description": "Required. Name of the AI Services project." + } + }, + "location": { + "type": "string", + "defaultValue": "[resourceGroup().location]", + "metadata": { + "description": "Required. The location of the Project resource." + } + }, + "desc": { + "type": "string", + "defaultValue": "[parameters('name')]", + "metadata": { + "description": "Optional. The description of the AI Foundry project to create. Defaults to the project name." + } + }, + "aiServicesName": { + "type": "string", + "metadata": { + "description": "Required. Name of the existing Cognitive Services resource to create the AI Foundry project in." + } + }, + "azureExistingAIProjectResourceId": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "Required. Azure Existing AI Project ResourceID." + } + }, + "tags": { + "type": "object", + "defaultValue": {}, + "metadata": { + "description": "Optional. Tags to be applied to the resources." + } + } + }, + "variables": { + "useExistingAiFoundryAiProject": "[not(empty(parameters('azureExistingAIProjectResourceId')))]", + "existingOpenAIEndpoint": "[if(variables('useExistingAiFoundryAiProject'), format('https://{0}.openai.azure.com/', split(parameters('azureExistingAIProjectResourceId'), '/')[8]), '')]" + }, + "resources": [ + { + "type": "Microsoft.CognitiveServices/accounts/projects", + "apiVersion": "2025-06-01", + "name": "[format('{0}/{1}', parameters('aiServicesName'), parameters('name'))]", + "tags": "[parameters('tags')]", + "location": "[parameters('location')]", + "identity": { + "type": "SystemAssigned" + }, + "properties": { + "description": "[parameters('desc')]", + "displayName": "[parameters('name')]" + } + } + ], + "outputs": { + "name": { + "type": "string", + "metadata": { + "description": "Required. Name of the AI project." + }, + "value": "[parameters('name')]" + }, + "resourceId": { + "type": "string", + "metadata": { + "description": "Required. Resource ID of the AI project." + }, + "value": "[resourceId('Microsoft.CognitiveServices/accounts/projects', parameters('aiServicesName'), parameters('name'))]" + }, + "apiEndpoint": { + "type": "string", + "metadata": { + "description": "Required. API endpoint for the AI project." + }, + "value": "[reference(resourceId('Microsoft.CognitiveServices/accounts/projects', parameters('aiServicesName'), parameters('name')), '2025-06-01').endpoints['AI Foundry API']]" + }, + "aoaiEndpoint": { + "type": "string", + "metadata": { + "description": "Contains AI Endpoint." + }, + "value": "[if(not(empty(variables('existingOpenAIEndpoint'))), variables('existingOpenAIEndpoint'), reference(resourceId('Microsoft.CognitiveServices/accounts', parameters('aiServicesName')), '2025-06-01').endpoints['OpenAI Language Model Instance API'])]" + }, + "systemAssignedMIPrincipalId": { + "type": "string", + "metadata": { + "description": "Required. Principal ID of the AI project system-assigned managed identity." + }, + "value": "[reference(resourceId('Microsoft.CognitiveServices/accounts/projects', parameters('aiServicesName'), parameters('name')), '2025-06-01', 'full').identity.principalId]" + } + } + } + }, + "dependsOn": [ + "aiFoundryAiServices" + ] + }, + "existingAiServicesRoleAssignments": { + "condition": "[variables('useExistingAiFoundryAiProject')]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "[take(format('module.foundry-role-assignment.{0}', variables('aiFoundryAiServicesResourceName')), 64)]", + "subscriptionId": "[variables('aiFoundryAiServicesSubscriptionId')]", + "resourceGroup": "[variables('aiFoundryAiServicesResourceGroupName')]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "aiServicesName": { + "value": "[variables('aiFoundryAiServicesResourceName')]" + }, + "principalId": { + "value": "[reference('userAssignedIdentity').outputs.principalId.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.39.26.7824", + "templateHash": "618751761593210210" + } + }, + "parameters": { + "principalId": { + "type": "string", + "metadata": { + "description": "Required. The principal ID of the managed identity to grant access." + } + }, + "aiServicesName": { + "type": "string", + "metadata": { + "description": "Required. The name of the existing AI Services account." + } + }, + "aiProjectName": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "Optional. The name of the existing AI Project." + } + }, + "principalType": { + "type": "string", + "defaultValue": "ServicePrincipal", + "allowedValues": [ + "Device", + "ForeignGroup", + "Group", + "ServicePrincipal", + "User" + ], + "metadata": { + "description": "Optional. The principal type of the identity." + } + } + }, + "resources": [ + { + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2022-04-01", + "scope": "[format('Microsoft.CognitiveServices/accounts/{0}', parameters('aiServicesName'))]", + "name": "[guid(resourceId('Microsoft.CognitiveServices/accounts', parameters('aiServicesName')), parameters('principalId'), resourceId('Microsoft.Authorization/roleDefinitions', '53ca6127-db72-4b80-b1b0-d745d6d5456d'))]", + "properties": { + "roleDefinitionId": "[resourceId('Microsoft.Authorization/roleDefinitions', '53ca6127-db72-4b80-b1b0-d745d6d5456d')]", + "principalId": "[parameters('principalId')]", + "principalType": "[parameters('principalType')]" + } + }, + { + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2022-04-01", + "scope": "[format('Microsoft.CognitiveServices/accounts/{0}', parameters('aiServicesName'))]", + "name": "[guid(resourceId('Microsoft.CognitiveServices/accounts', parameters('aiServicesName')), parameters('principalId'), resourceId('Microsoft.Authorization/roleDefinitions', '5e0bd9bd-7b93-4f28-af87-19fc36ad61bd'))]", + "properties": { + "roleDefinitionId": "[resourceId('Microsoft.Authorization/roleDefinitions', '5e0bd9bd-7b93-4f28-af87-19fc36ad61bd')]", + "principalId": "[parameters('principalId')]", + "principalType": "[parameters('principalType')]" + } + } + ], + "outputs": { + "aiServicesResourceId": { + "type": "string", + "metadata": { + "description": "The resource ID of the existing AI Services account." + }, + "value": "[resourceId('Microsoft.CognitiveServices/accounts', parameters('aiServicesName'))]" + }, + "aiServicesEndpoint": { + "type": "string", + "metadata": { + "description": "The endpoint of the existing AI Services account." + }, + "value": "[reference(resourceId('Microsoft.CognitiveServices/accounts', parameters('aiServicesName')), '2025-04-01-preview').endpoint]" + }, + "aiProjectPrincipalId": { + "type": "string", + "metadata": { + "description": "The principal ID of the existing AI Project (if provided)." + }, + "value": "[if(not(empty(parameters('aiProjectName'))), reference(resourceId('Microsoft.CognitiveServices/accounts/projects', parameters('aiServicesName'), parameters('aiProjectName')), '2025-04-01-preview', 'full').identity.principalId, '')]" + } + } + } + }, + "dependsOn": [ + "userAssignedIdentity" + ] + }, + "aiSearch": { + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "[take(format('avm.res.search.search-service.{0}', variables('aiSearchName')), 64)]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "name": { + "value": "[variables('aiSearchName')]" + }, + "location": { + "value": "[variables('solutionLocation')]" + }, + "tags": { + "value": "[parameters('tags')]" + }, + "enableTelemetry": { + "value": "[parameters('enableTelemetry')]" + }, + "sku": "[if(parameters('enableScalability'), createObject('value', 'standard'), createObject('value', 'basic'))]", + "replicaCount": "[if(parameters('enableRedundancy'), createObject('value', 2), createObject('value', 1))]", + "partitionCount": { + "value": 1 + }, + "hostingMode": { + "value": "default" + }, + "semanticSearch": { + "value": "free" + }, + "authOptions": { + "value": { + "aadOrApiKey": { + "aadAuthFailureMode": "http401WithBearerChallenge" + } + } + }, + "disableLocalAuth": { + "value": false + }, + "roleAssignments": { + "value": [ + { + "principalId": "[reference('userAssignedIdentity').outputs.principalId.value]", + "roleDefinitionIdOrName": "Search Index Data Contributor", + "principalType": "ServicePrincipal" + }, + { + "principalId": "[reference('userAssignedIdentity').outputs.principalId.value]", + "roleDefinitionIdOrName": "Search Service Contributor", + "principalType": "ServicePrincipal" + } + ] + }, + "diagnosticSettings": "[if(parameters('enableMonitoring'), createObject('value', createArray(createObject('workspaceResourceId', if(variables('useExistingLogAnalytics'), parameters('existingLogAnalyticsWorkspaceId'), if(parameters('enableMonitoring'), reference('logAnalyticsWorkspace').outputs.resourceId.value, ''))))), createObject('value', null()))]", + "publicNetworkAccess": { + "value": "Enabled" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "languageVersion": "2.0", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.37.4.10188", + "templateHash": "10902281417196168235" + }, + "name": "Search Services", + "description": "This module deploys a Search Service." + }, + "definitions": { + "privateEndpointOutputType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "metadata": { + "description": "The name of the private endpoint." + } + }, + "resourceId": { + "type": "string", + "metadata": { + "description": "The resource ID of the private endpoint." + } + }, + "groupId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "The group Id for the private endpoint Group." + } + }, + "customDnsConfigs": { + "type": "array", + "items": { + "type": "object", + "properties": { + "fqdn": { + "type": "string", + "nullable": true, + "metadata": { + "description": "FQDN that resolves to private endpoint IP address." + } }, - "type": "Microsoft.Resources/deployments", - "apiVersion": "2025-04-01", - "name": "[format('{0}-cassandra-ra-{1}', uniqueString(deployment().name), copyIndex())]", - "properties": { - "expressionEvaluationOptions": { - "scope": "inner" - }, - "mode": "Incremental", - "parameters": { - "databaseAccountName": { - "value": "[parameters('databaseAccountName')]" - }, - "roleDefinitionId": { - "value": "[resourceId('Microsoft.DocumentDB/databaseAccounts/cassandraRoleDefinitions', parameters('databaseAccountName'), coalesce(parameters('name'), guid(resourceId('Microsoft.DocumentDB/databaseAccounts', parameters('databaseAccountName')), parameters('databaseAccountName'), parameters('roleName'))))]" - }, - "principalId": { - "value": "[coalesce(parameters('cassandraRoleAssignments'), createArray())[copyIndex()].principalId]" - }, - "name": { - "value": "[tryGet(coalesce(parameters('cassandraRoleAssignments'), createArray())[copyIndex()], 'name')]" - }, - "scope": { - "value": "[tryGet(coalesce(parameters('cassandraRoleAssignments'), createArray())[copyIndex()], 'scope')]" - } + "ipAddresses": { + "type": "array", + "items": { + "type": "string" }, - "template": { - "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", - "languageVersion": "2.0", - "contentVersion": "1.0.0.0", - "metadata": { - "_generator": { - "name": "bicep", - "version": "0.38.33.27573", - "templateHash": "552115240340341941" - }, - "name": "DocumentDB Database Account Cassandra Role Assignments.", - "description": "This module deploys a Cassandra Role Assignment in a CosmosDB Account." - }, - "parameters": { - "databaseAccountName": { - "type": "string", - "metadata": { - "description": "Conditional. The name of the parent Database Account. Required if the template is used in a standalone deployment." - } - }, - "name": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. Name unique identifier of the Cassandra Role Assignment." - } - }, - "principalId": { - "type": "string", - "metadata": { - "description": "Required. The unique identifier for the associated AAD principal in the AAD graph to which access is being granted through this Role Assignment. Tenant ID for the principal is inferred using the tenant associated with the subscription." - } - }, - "roleDefinitionId": { - "type": "string", - "metadata": { - "description": "Required. The unique identifier of the associated Cassandra Role Definition." - } - }, - "scope": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The data plane resource path for which access is being granted through this Cassandra Role Assignment. Defaults to the current account." - } - } - }, - "resources": { - "databaseAccount": { - "existing": true, - "type": "Microsoft.DocumentDB/databaseAccounts", - "apiVersion": "2024-11-15", - "name": "[parameters('databaseAccountName')]" - }, - "cassandraRoleAssignment": { - "type": "Microsoft.DocumentDB/databaseAccounts/cassandraRoleAssignments", - "apiVersion": "2025-05-01-preview", - "name": "[format('{0}/{1}', parameters('databaseAccountName'), coalesce(parameters('name'), guid(parameters('roleDefinitionId'), parameters('principalId'), resourceId('Microsoft.DocumentDB/databaseAccounts', parameters('databaseAccountName')))))]", - "properties": { - "principalId": "[parameters('principalId')]", - "roleDefinitionId": "[parameters('roleDefinitionId')]", - "scope": "[coalesce(parameters('scope'), resourceId('Microsoft.DocumentDB/databaseAccounts', parameters('databaseAccountName')))]" - } - } - }, - "outputs": { - "name": { - "type": "string", - "metadata": { - "description": "The name of the Cassandra Role Assignment." - }, - "value": "[coalesce(parameters('name'), guid(parameters('roleDefinitionId'), parameters('principalId'), resourceId('Microsoft.DocumentDB/databaseAccounts', parameters('databaseAccountName'))))]" - }, - "resourceId": { - "type": "string", - "metadata": { - "description": "The resource ID of the Cassandra Role Assignment." - }, - "value": "[resourceId('Microsoft.DocumentDB/databaseAccounts/cassandraRoleAssignments', parameters('databaseAccountName'), coalesce(parameters('name'), guid(parameters('roleDefinitionId'), parameters('principalId'), resourceId('Microsoft.DocumentDB/databaseAccounts', parameters('databaseAccountName')))))]" - }, - "resourceGroupName": { - "type": "string", - "metadata": { - "description": "The name of the resource group the Cassandra Role Assignment was created in." - }, - "value": "[resourceGroup().name]" - } - } + "metadata": { + "description": "A list of private IP addresses of the private endpoint." } - }, - "dependsOn": [ - "cassandraRoleDefinition" - ] + } } }, - "outputs": { - "name": { - "type": "string", - "metadata": { - "description": "The name of the cassandra role definition." - }, - "value": "[coalesce(parameters('name'), guid(resourceId('Microsoft.DocumentDB/databaseAccounts', parameters('databaseAccountName')), parameters('databaseAccountName'), parameters('roleName')))]" - }, - "resourceId": { - "type": "string", - "metadata": { - "description": "The resource ID of the cassandra role definition." - }, - "value": "[resourceId('Microsoft.DocumentDB/databaseAccounts/cassandraRoleDefinitions', parameters('databaseAccountName'), coalesce(parameters('name'), guid(resourceId('Microsoft.DocumentDB/databaseAccounts', parameters('databaseAccountName')), parameters('databaseAccountName'), parameters('roleName'))))]" - }, - "resourceGroupName": { - "type": "string", - "metadata": { - "description": "The name of the resource group the cassandra role definition was created in." - }, - "value": "[resourceGroup().name]" - } + "metadata": { + "description": "The custom DNS configurations of the private endpoint." + } + }, + "networkInterfaceResourceIds": { + "type": "array", + "items": { + "type": "string" + }, + "metadata": { + "description": "The IDs of the network interfaces associated with the private endpoint." } } }, - "dependsOn": [ - "databaseAccount" - ] + "metadata": { + "__bicep_export!": true + } }, - "databaseAccount_cassandraRoleAssignments": { - "copy": { - "name": "databaseAccount_cassandraRoleAssignments", - "count": "[length(coalesce(parameters('cassandraRoleAssignments'), createArray()))]" - }, - "type": "Microsoft.Resources/deployments", - "apiVersion": "2025-04-01", - "name": "[format('{0}-cassandra-ra-{1}', uniqueString(deployment().name), copyIndex())]", + "secretsExportConfigurationType": { + "type": "object", "properties": { - "expressionEvaluationOptions": { - "scope": "inner" + "keyVaultResourceId": { + "type": "string", + "metadata": { + "description": "Required. The key vault name where to store the API Admin keys generated by the modules." + } }, - "mode": "Incremental", - "parameters": { - "databaseAccountName": { - "value": "[parameters('name')]" - }, - "roleDefinitionId": { - "value": "[coalesce(parameters('cassandraRoleAssignments'), createArray())[copyIndex()].roleDefinitionId]" - }, - "principalId": { - "value": "[coalesce(parameters('cassandraRoleAssignments'), createArray())[copyIndex()].principalId]" - }, - "name": { - "value": "[tryGet(coalesce(parameters('cassandraRoleAssignments'), createArray())[copyIndex()], 'name')]" - }, - "scope": { - "value": "[tryGet(coalesce(parameters('cassandraRoleAssignments'), createArray())[copyIndex()], 'scope')]" + "primaryAdminKeyName": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The primaryAdminKey secret name to create." } }, - "template": { - "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", - "languageVersion": "2.0", - "contentVersion": "1.0.0.0", + "secondaryAdminKeyName": { + "type": "string", + "nullable": true, "metadata": { - "_generator": { - "name": "bicep", - "version": "0.38.33.27573", - "templateHash": "552115240340341941" - }, - "name": "DocumentDB Database Account Cassandra Role Assignments.", - "description": "This module deploys a Cassandra Role Assignment in a CosmosDB Account." - }, - "parameters": { - "databaseAccountName": { - "type": "string", - "metadata": { - "description": "Conditional. The name of the parent Database Account. Required if the template is used in a standalone deployment." - } - }, - "name": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. Name unique identifier of the Cassandra Role Assignment." - } - }, - "principalId": { - "type": "string", - "metadata": { - "description": "Required. The unique identifier for the associated AAD principal in the AAD graph to which access is being granted through this Role Assignment. Tenant ID for the principal is inferred using the tenant associated with the subscription." - } - }, - "roleDefinitionId": { - "type": "string", - "metadata": { - "description": "Required. The unique identifier of the associated Cassandra Role Definition." - } - }, - "scope": { + "description": "Optional. The secondaryAdminKey secret name to create." + } + } + } + }, + "secretsOutputType": { + "type": "object", + "properties": {}, + "additionalProperties": { + "$ref": "#/definitions/secretSetType", + "metadata": { + "description": "An exported secret's references." + } + } + }, + "authOptionsType": { + "type": "object", + "properties": { + "aadOrApiKey": { + "type": "object", + "properties": { + "aadAuthFailureMode": { "type": "string", + "allowedValues": [ + "http401WithBearerChallenge", + "http403" + ], "nullable": true, "metadata": { - "description": "Optional. The data plane resource path for which access is being granted through this Cassandra Role Assignment. Defaults to the current account." + "description": "Optional. Describes what response the data plane API of a search service would send for requests that failed authentication." } } }, - "resources": { - "databaseAccount": { - "existing": true, - "type": "Microsoft.DocumentDB/databaseAccounts", - "apiVersion": "2024-11-15", - "name": "[parameters('databaseAccountName')]" - }, - "cassandraRoleAssignment": { - "type": "Microsoft.DocumentDB/databaseAccounts/cassandraRoleAssignments", - "apiVersion": "2025-05-01-preview", - "name": "[format('{0}/{1}', parameters('databaseAccountName'), coalesce(parameters('name'), guid(parameters('roleDefinitionId'), parameters('principalId'), resourceId('Microsoft.DocumentDB/databaseAccounts', parameters('databaseAccountName')))))]", - "properties": { - "principalId": "[parameters('principalId')]", - "roleDefinitionId": "[parameters('roleDefinitionId')]", - "scope": "[coalesce(parameters('scope'), resourceId('Microsoft.DocumentDB/databaseAccounts', parameters('databaseAccountName')))]" - } - } + "nullable": true, + "metadata": { + "description": "Optional. Indicates that either the API key or an access token from a Microsoft Entra ID tenant can be used for authentication." + } + }, + "apiKeyOnly": { + "type": "object", + "nullable": true, + "metadata": { + "description": "Optional. Indicates that only the API key can be used for authentication." + } + } + }, + "metadata": { + "__bicep_export!": true + } + }, + "networkRuleSetType": { + "type": "object", + "properties": { + "bypass": { + "type": "string", + "allowedValues": [ + "AzurePortal", + "AzureServices", + "None" + ], + "nullable": true, + "metadata": { + "description": "Optional. Network specific rules that determine how the Azure AI Search service may be reached." + } + }, + "ipRules": { + "type": "array", + "items": { + "$ref": "#/definitions/ipRuleType" }, - "outputs": { - "name": { - "type": "string", - "metadata": { - "description": "The name of the Cassandra Role Assignment." - }, - "value": "[coalesce(parameters('name'), guid(parameters('roleDefinitionId'), parameters('principalId'), resourceId('Microsoft.DocumentDB/databaseAccounts', parameters('databaseAccountName'))))]" - }, - "resourceId": { - "type": "string", - "metadata": { - "description": "The resource ID of the Cassandra Role Assignment." - }, - "value": "[resourceId('Microsoft.DocumentDB/databaseAccounts/cassandraRoleAssignments', parameters('databaseAccountName'), coalesce(parameters('name'), guid(parameters('roleDefinitionId'), parameters('principalId'), resourceId('Microsoft.DocumentDB/databaseAccounts', parameters('databaseAccountName')))))]" - }, - "resourceGroupName": { - "type": "string", - "metadata": { - "description": "The name of the resource group the Cassandra Role Assignment was created in." - }, - "value": "[resourceGroup().name]" - } + "nullable": true, + "metadata": { + "description": "Optional. A list of IP restriction rules that defines the inbound network(s) with allowing access to the search service endpoint. At the meantime, all other public IP networks are blocked by the firewall. These restriction rules are applied only when the 'publicNetworkAccess' of the search service is 'enabled'; otherwise, traffic over public interface is not allowed even with any public IP rules, and private endpoint connections would be the exclusive access method." } } }, - "dependsOn": [ - "databaseAccount", - "databaseAccount_cassandraKeyspaces", - "databaseAccount_cassandraRoleDefinitions" - ] + "metadata": { + "__bicep_export!": true + } }, - "databaseAccount_mongodbDatabases": { - "copy": { - "name": "databaseAccount_mongodbDatabases", - "count": "[length(coalesce(parameters('mongodbDatabases'), createArray()))]" + "ipRuleType": { + "type": "object", + "properties": { + "value": { + "type": "string", + "metadata": { + "description": "Required. Value corresponding to a single IPv4 address (eg., 123.1.2.3) or an IP range in CIDR format (eg., 123.1.2.3/24) to be allowed." + } + } }, - "type": "Microsoft.Resources/deployments", - "apiVersion": "2025-04-01", - "name": "[format('{0}-mongodb-{1}', uniqueString(deployment().name, parameters('location')), coalesce(parameters('mongodbDatabases'), createArray())[copyIndex()].name)]", + "metadata": { + "__bicep_export!": true + } + }, + "_1.lockType": { + "type": "object", "properties": { - "expressionEvaluationOptions": { - "scope": "inner" + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Specify the name of lock." + } }, - "mode": "Incremental", - "parameters": { - "databaseAccountName": { - "value": "[parameters('name')]" - }, - "name": { - "value": "[coalesce(parameters('mongodbDatabases'), createArray())[copyIndex()].name]" - }, - "tags": { - "value": "[coalesce(tryGet(coalesce(parameters('mongodbDatabases'), createArray())[copyIndex()], 'tags'), parameters('tags'))]" - }, - "collections": { - "value": "[tryGet(coalesce(parameters('mongodbDatabases'), createArray())[copyIndex()], 'collections')]" - }, - "throughput": { - "value": "[tryGet(coalesce(parameters('mongodbDatabases'), createArray())[copyIndex()], 'throughput')]" - }, - "autoscaleSettings": { - "value": "[tryGet(coalesce(parameters('mongodbDatabases'), createArray())[copyIndex()], 'autoscaleSettings')]" + "kind": { + "type": "string", + "allowedValues": [ + "CanNotDelete", + "None", + "ReadOnly" + ], + "nullable": true, + "metadata": { + "description": "Optional. Specify the type of lock." } }, - "template": { - "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", - "languageVersion": "2.0", - "contentVersion": "1.0.0.0", + "notes": { + "type": "string", + "nullable": true, "metadata": { - "_generator": { - "name": "bicep", - "version": "0.38.33.27573", - "templateHash": "7289795303297936310" - }, - "name": "DocumentDB Database Account MongoDB Databases", - "description": "This module deploys a MongoDB Database within a CosmosDB Account." - }, - "definitions": { - "collectionType": { - "type": "object", - "properties": { - "name": { - "type": "string", - "metadata": { - "description": "Required. Name of the collection." - } - }, - "throughput": { - "type": "int", - "nullable": true, - "metadata": { - "description": "Optional. Request Units per second. For best performance for large production workloads, it is recommended to set dedicated throughput (autoscale or manual) at the collection level and not at the database level." - } - }, - "indexes": { - "type": "array", - "metadata": { - "__bicep_resource_derived_type!": { - "source": "Microsoft.DocumentDB/databaseAccounts/mongodbDatabases/collections@2025-04-15#properties/properties/properties/resource/properties/indexes" - }, - "description": "Required. Indexes for the collection." - } - }, - "shardKey": { - "type": "object", - "metadata": { - "__bicep_resource_derived_type!": { - "source": "Microsoft.DocumentDB/databaseAccounts/mongodbDatabases/collections@2025-04-15#properties/properties/properties/resource/properties/shardKey" - }, - "description": "Required. ShardKey for the collection." - } - } - }, - "metadata": { - "__bicep_export!": true, - "description": "The type of a collection." - } - } + "description": "Optional. Specify the notes of the lock." + } + } + }, + "metadata": { + "description": "An AVM-aligned type for a lock.", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.1" + } + } + }, + "_1.privateEndpointCustomDnsConfigType": { + "type": "object", + "properties": { + "fqdn": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. FQDN that resolves to private endpoint IP address." + } + }, + "ipAddresses": { + "type": "array", + "items": { + "type": "string" }, - "parameters": { - "databaseAccountName": { + "metadata": { + "description": "Required. A list of private IP addresses of the private endpoint." + } + } + }, + "metadata": { + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.1" + } + } + }, + "_1.privateEndpointIpConfigurationType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "metadata": { + "description": "Required. The name of the resource that is unique within a resource group." + } + }, + "properties": { + "type": "object", + "properties": { + "groupId": { "type": "string", "metadata": { - "description": "Conditional. The name of the parent Cosmos DB database account. Required if the template is used in a standalone deployment." + "description": "Required. The ID of a group obtained from the remote resource that this private endpoint should connect to." } }, - "name": { + "memberName": { "type": "string", "metadata": { - "description": "Required. Name of the mongodb database." + "description": "Required. The member name of a group obtained from the remote resource that this private endpoint should connect to." } }, - "throughput": { - "type": "int", - "defaultValue": 400, + "privateIPAddress": { + "type": "string", "metadata": { - "description": "Optional. Request Units per second. Setting throughput at the database level is only recommended for development/test or when workload across all collections in the shared throughput database is uniform. For best performance for large production workloads, it is recommended to set dedicated throughput (autoscale or manual) at the collection level and not at the database level." + "description": "Required. A private IP address obtained from the private endpoint's subnet." } - }, - "collections": { - "type": "array", - "items": { - "$ref": "#/definitions/collectionType" + } + }, + "metadata": { + "description": "Required. Properties of private endpoint IP configurations." + } + } + }, + "metadata": { + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.1" + } + } + }, + "_1.privateEndpointPrivateDnsZoneGroupType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The name of the Private DNS Zone Group." + } + }, + "privateDnsZoneGroupConfigs": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The name of the private DNS Zone Group config." + } }, - "nullable": true, - "metadata": { - "description": "Optional. Collections in the mongodb database." + "privateDnsZoneResourceId": { + "type": "string", + "metadata": { + "description": "Required. The resource id of the private DNS zone." + } } - }, - "tags": { - "type": "object", - "metadata": { - "__bicep_resource_derived_type!": { - "source": "Microsoft.DocumentDB/databaseAccounts/mongodbDatabases@2025-04-15#properties/tags" - }, - "description": "Optional. Tags of the resource." - }, - "nullable": true - }, - "autoscaleSettings": { - "type": "object", - "metadata": { - "__bicep_resource_derived_type!": { - "source": "Microsoft.DocumentDB/databaseAccounts/mongodbDatabases@2025-04-15#properties/properties/properties/options/properties/autoscaleSettings" - }, - "description": "Optional. Specifies the Autoscale settings. Note: Either throughput or autoscaleSettings is required, but not both." - }, - "nullable": true } }, - "resources": { - "databaseAccount": { - "existing": true, - "type": "Microsoft.DocumentDB/databaseAccounts", - "apiVersion": "2025-04-15", - "name": "[parameters('databaseAccountName')]" - }, - "mongodbDatabase": { - "type": "Microsoft.DocumentDB/databaseAccounts/mongodbDatabases", - "apiVersion": "2025-04-15", - "name": "[format('{0}/{1}', parameters('databaseAccountName'), parameters('name'))]", - "tags": "[parameters('tags')]", - "properties": { - "resource": { - "id": "[parameters('name')]" - }, - "options": "[if(contains(reference('databaseAccount').capabilities, createObject('name', 'EnableServerless')), null(), createObject('throughput', parameters('throughput'), 'autoscaleSettings', parameters('autoscaleSettings')))]" - }, - "dependsOn": [ - "databaseAccount" - ] - }, - "mongodbDatabase_collections": { - "copy": { - "name": "mongodbDatabase_collections", - "count": "[length(coalesce(parameters('collections'), createArray()))]" + "metadata": { + "description": "Required. The private DNS Zone Groups to associate the Private Endpoint. A DNS Zone Group can support up to 5 DNS zones." + } + } + }, + "metadata": { + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.1" + } + } + }, + "_1.roleAssignmentType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The name (as GUID) of the role assignment. If not provided, a GUID will be generated." + } + }, + "roleDefinitionIdOrName": { + "type": "string", + "metadata": { + "description": "Required. The role to assign. You can provide either the display name of the role definition, the role definition GUID, or its fully qualified ID in the following format: '/providers/Microsoft.Authorization/roleDefinitions/c2f4ef07-c644-48eb-af81-4b1b4947fb11'." + } + }, + "principalId": { + "type": "string", + "metadata": { + "description": "Required. The principal ID of the principal (user/group/identity) to assign the role to." + } + }, + "principalType": { + "type": "string", + "allowedValues": [ + "Device", + "ForeignGroup", + "Group", + "ServicePrincipal", + "User" + ], + "nullable": true, + "metadata": { + "description": "Optional. The principal type of the assigned principal ID." + } + }, + "description": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The description of the role assignment." + } + }, + "condition": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The conditions on the role assignment. This limits the resources it can be assigned to. e.g.: @Resource[Microsoft.Storage/storageAccounts/blobServices/containers:ContainerName] StringEqualsIgnoreCase \"foo_storage_container\"." + } + }, + "conditionVersion": { + "type": "string", + "allowedValues": [ + "2.0" + ], + "nullable": true, + "metadata": { + "description": "Optional. Version of the condition." + } + }, + "delegatedManagedIdentityResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The Resource Id of the delegated managed identity resource." + } + } + }, + "metadata": { + "description": "An AVM-aligned type for a role assignment.", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.1" + } + } + }, + "diagnosticSettingFullType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The name of the diagnostic setting." + } + }, + "logCategoriesAndGroups": { + "type": "array", + "items": { + "type": "object", + "properties": { + "category": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Name of a Diagnostic Log category for a resource type this setting is applied to. Set the specific logs to collect here." + } }, - "type": "Microsoft.Resources/deployments", - "apiVersion": "2025-04-01", - "name": "[format('{0}-collection-{1}', uniqueString(deployment().name, parameters('name')), coalesce(parameters('collections'), createArray())[copyIndex()].name)]", - "properties": { - "expressionEvaluationOptions": { - "scope": "inner" - }, - "mode": "Incremental", - "parameters": { - "databaseAccountName": { - "value": "[parameters('databaseAccountName')]" - }, - "mongodbDatabaseName": { - "value": "[parameters('name')]" - }, - "name": { - "value": "[coalesce(parameters('collections'), createArray())[copyIndex()].name]" - }, - "indexes": { - "value": "[coalesce(parameters('collections'), createArray())[copyIndex()].indexes]" - }, - "shardKey": { - "value": "[coalesce(parameters('collections'), createArray())[copyIndex()].shardKey]" - }, - "throughput": { - "value": "[tryGet(coalesce(parameters('collections'), createArray())[copyIndex()], 'throughput')]" - } - }, - "template": { - "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", - "contentVersion": "1.0.0.0", - "metadata": { - "_generator": { - "name": "bicep", - "version": "0.38.33.27573", - "templateHash": "4317369978166598876" - }, - "name": "DocumentDB Database Account MongoDB Database Collections", - "description": "This module deploys a MongoDB Database Collection." - }, - "parameters": { - "databaseAccountName": { - "type": "string", - "metadata": { - "description": "Conditional. The name of the parent Cosmos DB database account. Required if the template is used in a standalone deployment." - } - }, - "mongodbDatabaseName": { - "type": "string", - "metadata": { - "description": "Conditional. The name of the parent mongodb database. Required if the template is used in a standalone deployment." - } - }, - "name": { - "type": "string", - "metadata": { - "description": "Required. Name of the collection." - } - }, - "throughput": { - "type": "int", - "defaultValue": 400, - "metadata": { - "description": "Optional. Request Units per second. For best performance for large production workloads, it is recommended to set dedicated throughput (autoscale or manual) at the collection level and not at the database level." - } - }, - "indexes": { - "type": "array", - "metadata": { - "__bicep_resource_derived_type!": { - "source": "Microsoft.DocumentDB/databaseAccounts/mongodbDatabases/collections@2025-04-15#properties/properties/properties/resource/properties/indexes" - }, - "description": "Required. Indexes for the collection." - } - }, - "shardKey": { - "type": "object", - "metadata": { - "__bicep_resource_derived_type!": { - "source": "Microsoft.DocumentDB/databaseAccounts/mongodbDatabases/collections@2025-04-15#properties/properties/properties/resource/properties/shardKey" - }, - "description": "Required. ShardKey for the collection." - } - } - }, - "resources": [ - { - "type": "Microsoft.DocumentDB/databaseAccounts/mongodbDatabases/collections", - "apiVersion": "2025-04-15", - "name": "[format('{0}/{1}/{2}', parameters('databaseAccountName'), parameters('mongodbDatabaseName'), parameters('name'))]", - "properties": { - "options": "[if(contains(reference(resourceId('Microsoft.DocumentDB/databaseAccounts', parameters('databaseAccountName')), '2025-04-15').capabilities, createObject('name', 'EnableServerless')), null(), createObject('throughput', parameters('throughput')))]", - "resource": { - "id": "[parameters('name')]", - "indexes": "[parameters('indexes')]", - "shardKey": "[parameters('shardKey')]" - } - } - } - ], - "outputs": { - "name": { - "type": "string", - "metadata": { - "description": "The name of the mongodb database collection." - }, - "value": "[parameters('name')]" - }, - "resourceId": { - "type": "string", - "metadata": { - "description": "The resource ID of the mongodb database collection." - }, - "value": "[resourceId('Microsoft.DocumentDB/databaseAccounts/mongodbDatabases/collections', parameters('databaseAccountName'), parameters('mongodbDatabaseName'), parameters('name'))]" - }, - "resourceGroupName": { - "type": "string", - "metadata": { - "description": "The name of the resource group the mongodb database collection was created in." - }, - "value": "[resourceGroup().name]" - } - } + "categoryGroup": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Name of a Diagnostic Log category group for a resource type this setting is applied to. Set to `allLogs` to collect all logs." } }, - "dependsOn": [ - "mongodbDatabase" - ] + "enabled": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. Enable or disable the category explicitly. Default is `true`." + } + } } }, - "outputs": { - "name": { - "type": "string", - "metadata": { - "description": "The name of the mongodb database." - }, - "value": "[parameters('name')]" - }, - "resourceId": { - "type": "string", - "metadata": { - "description": "The resource ID of the mongodb database." - }, - "value": "[resourceId('Microsoft.DocumentDB/databaseAccounts/mongodbDatabases', parameters('databaseAccountName'), parameters('name'))]" - }, - "resourceGroupName": { - "type": "string", - "metadata": { - "description": "The name of the resource group the mongodb database was created in." + "nullable": true, + "metadata": { + "description": "Optional. The name of logs that will be streamed. \"allLogs\" includes all possible logs for the resource. Set to `[]` to disable log collection." + } + }, + "metricCategories": { + "type": "array", + "items": { + "type": "object", + "properties": { + "category": { + "type": "string", + "metadata": { + "description": "Required. Name of a Diagnostic Metric category for a resource type this setting is applied to. Set to `AllMetrics` to collect all metrics." + } }, - "value": "[resourceGroup().name]" + "enabled": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. Enable or disable the category explicitly. Default is `true`." + } + } } + }, + "nullable": true, + "metadata": { + "description": "Optional. The name of metrics that will be streamed. \"allMetrics\" includes all possible metrics for the resource. Set to `[]` to disable metric collection." + } + }, + "logAnalyticsDestinationType": { + "type": "string", + "allowedValues": [ + "AzureDiagnostics", + "Dedicated" + ], + "nullable": true, + "metadata": { + "description": "Optional. A string indicating whether the export to Log Analytics should use the default destination type, i.e. AzureDiagnostics, or use a destination type." + } + }, + "workspaceResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Resource ID of the diagnostic log analytics workspace. For security reasons, it is recommended to set diagnostic settings to send data to either storage account, log analytics workspace or event hub." + } + }, + "storageAccountResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Resource ID of the diagnostic storage account. For security reasons, it is recommended to set diagnostic settings to send data to either storage account, log analytics workspace or event hub." + } + }, + "eventHubAuthorizationRuleResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Resource ID of the diagnostic event hub authorization rule for the Event Hubs namespace in which the event hub should be created or streamed to." + } + }, + "eventHubName": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Name of the diagnostic event hub within the namespace to which logs are streamed. Without this, an event hub is created for each log category. For security reasons, it is recommended to set diagnostic settings to send data to either storage account, log analytics workspace or event hub." + } + }, + "marketplacePartnerResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The full ARM resource ID of the Marketplace resource to which you would like to send Diagnostic Logs." + } + } + }, + "metadata": { + "description": "An AVM-aligned type for a diagnostic setting. To be used if both logs & metrics are supported by the resource provider.", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.5.1" + } + } + }, + "lockType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Specify the name of lock." + } + }, + "kind": { + "type": "string", + "allowedValues": [ + "CanNotDelete", + "None", + "ReadOnly" + ], + "nullable": true, + "metadata": { + "description": "Optional. Specify the type of lock." + } + }, + "notes": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Specify the notes of the lock." } } }, - "dependsOn": [ - "databaseAccount" - ] + "metadata": { + "description": "An AVM-aligned type for a lock.", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.0" + } + } }, - "databaseAccount_gremlinDatabases": { - "copy": { - "name": "databaseAccount_gremlinDatabases", - "count": "[length(coalesce(parameters('gremlinDatabases'), createArray()))]" - }, - "type": "Microsoft.Resources/deployments", - "apiVersion": "2025-04-01", - "name": "[format('{0}-gremlin-{1}', uniqueString(deployment().name, parameters('location')), coalesce(parameters('gremlinDatabases'), createArray())[copyIndex()].name)]", + "managedIdentityAllType": { + "type": "object", "properties": { - "expressionEvaluationOptions": { - "scope": "inner" + "systemAssigned": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. Enables system assigned managed identity on the resource." + } }, - "mode": "Incremental", - "parameters": { - "databaseAccountName": { - "value": "[parameters('name')]" - }, - "name": { - "value": "[coalesce(parameters('gremlinDatabases'), createArray())[copyIndex()].name]" - }, - "tags": { - "value": "[coalesce(tryGet(coalesce(parameters('gremlinDatabases'), createArray())[copyIndex()], 'tags'), parameters('tags'))]" - }, - "graphs": { - "value": "[tryGet(coalesce(parameters('gremlinDatabases'), createArray())[copyIndex()], 'graphs')]" - }, - "maxThroughput": { - "value": "[tryGet(coalesce(parameters('gremlinDatabases'), createArray())[copyIndex()], 'maxThroughput')]" + "userAssignedResourceIds": { + "type": "array", + "items": { + "type": "string" }, - "throughput": { - "value": "[tryGet(coalesce(parameters('gremlinDatabases'), createArray())[copyIndex()], 'throughput')]" + "nullable": true, + "metadata": { + "description": "Optional. The resource ID(s) to assign to the resource. Required if a user assigned identity is used for encryption." + } + } + }, + "metadata": { + "description": "An AVM-aligned type for a managed identity configuration. To be used if both a system-assigned & user-assigned identities are supported by the resource provider.", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.5.1" + } + } + }, + "privateEndpointSingleServiceType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The name of the Private Endpoint." } }, - "template": { - "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", - "languageVersion": "2.0", - "contentVersion": "1.0.0.0", + "location": { + "type": "string", + "nullable": true, "metadata": { - "_generator": { - "name": "bicep", - "version": "0.38.33.27573", - "templateHash": "14708982296215631776" - }, - "name": "DocumentDB Database Account Gremlin Databases", - "description": "This module deploys a Gremlin Database within a CosmosDB Account." + "description": "Optional. The location to deploy the Private Endpoint to." + } + }, + "privateLinkServiceConnectionName": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The name of the private link connection to create." + } + }, + "service": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The subresource to deploy the Private Endpoint for. For example \"vault\" for a Key Vault Private Endpoint." + } + }, + "subnetResourceId": { + "type": "string", + "metadata": { + "description": "Required. Resource ID of the subnet where the endpoint needs to be created." + } + }, + "resourceGroupResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The resource ID of the Resource Group the Private Endpoint will be created in. If not specified, the Resource Group of the provided Virtual Network Subnet is used." + } + }, + "privateDnsZoneGroup": { + "$ref": "#/definitions/_1.privateEndpointPrivateDnsZoneGroupType", + "nullable": true, + "metadata": { + "description": "Optional. The private DNS Zone Group to configure for the Private Endpoint." + } + }, + "isManualConnection": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. If Manual Private Link Connection is required." + } + }, + "manualConnectionRequestMessage": { + "type": "string", + "nullable": true, + "maxLength": 140, + "metadata": { + "description": "Optional. A message passed to the owner of the remote resource with the manual connection request." + } + }, + "customDnsConfigs": { + "type": "array", + "items": { + "$ref": "#/definitions/_1.privateEndpointCustomDnsConfigType" }, - "definitions": { - "graphType": { - "type": "object", - "properties": { - "name": { - "type": "string", - "metadata": { - "description": "Required. Name of the graph." - } - }, - "tags": { - "type": "object", - "metadata": { - "__bicep_resource_derived_type!": { - "source": "Microsoft.DocumentDB/databaseAccounts/gremlinDatabases/graphs@2025-04-15#properties/tags" - }, - "description": "Optional. Tags of the Gremlin graph resource." - }, - "nullable": true - }, - "indexingPolicy": { - "type": "object", - "metadata": { - "__bicep_resource_derived_type!": { - "source": "Microsoft.DocumentDB/databaseAccounts/gremlinDatabases/graphs@2025-04-15#properties/properties/properties/resource/properties/indexingPolicy" - }, - "description": "Optional. Indexing policy of the graph." - }, - "nullable": true - }, - "partitionKeyPaths": { - "type": "array", - "metadata": { - "__bicep_resource_derived_type!": { - "source": "Microsoft.DocumentDB/databaseAccounts/gremlinDatabases/graphs@2025-04-15#properties/properties/properties/resource/properties/partitionKey/properties/paths" - }, - "description": "Optional. List of paths using which data within the container can be partitioned." - }, - "nullable": true - } - }, - "metadata": { - "__bicep_export!": true, - "description": "The type of a graph." - } - } + "nullable": true, + "metadata": { + "description": "Optional. Custom DNS configurations." + } + }, + "ipConfigurations": { + "type": "array", + "items": { + "$ref": "#/definitions/_1.privateEndpointIpConfigurationType" }, - "parameters": { - "name": { - "type": "string", - "metadata": { - "description": "Required. Name of the Gremlin database." - } - }, - "tags": { - "type": "object", - "metadata": { - "__bicep_resource_derived_type!": { - "source": "Microsoft.DocumentDB/databaseAccounts/gremlinDatabases@2024-11-15#properties/tags" - }, - "description": "Optional. Tags of the Gremlin database resource." - }, - "nullable": true - }, - "databaseAccountName": { - "type": "string", - "metadata": { - "description": "Conditional. The name of the parent Gremlin database. Required if the template is used in a standalone deployment." - } - }, - "graphs": { - "type": "array", - "items": { - "$ref": "#/definitions/graphType" - }, - "nullable": true, - "metadata": { - "description": "Optional. Array of graphs to deploy in the Gremlin database." - } - }, - "maxThroughput": { - "type": "int", - "defaultValue": 4000, - "metadata": { - "description": "Optional. Represents maximum throughput, the resource can scale up to. Cannot be set together with `throughput`. If `throughput` is set to something else than -1, this autoscale setting is ignored. Setting throughput at the database level is only recommended for development/test or when workload across all graphs in the shared throughput database is uniform. For best performance for large production workloads, it is recommended to set dedicated throughput (autoscale or manual) at the graph level and not at the database level." - } - }, - "throughput": { - "type": "int", - "nullable": true, - "metadata": { - "description": "Optional. Request Units per second (for example 10000). Cannot be set together with `maxThroughput`. Setting throughput at the database level is only recommended for development/test or when workload across all graphs in the shared throughput database is uniform. For best performance for large production workloads, it is recommended to set dedicated throughput (autoscale or manual) at the graph level and not at the database level." - } - } + "nullable": true, + "metadata": { + "description": "Optional. A list of IP configurations of the Private Endpoint. This will be used to map to the first-party Service endpoints." + } + }, + "applicationSecurityGroupResourceIds": { + "type": "array", + "items": { + "type": "string" }, - "resources": { - "databaseAccount": { - "existing": true, - "type": "Microsoft.DocumentDB/databaseAccounts", - "apiVersion": "2025-04-15", - "name": "[parameters('databaseAccountName')]" - }, - "gremlinDatabase": { - "type": "Microsoft.DocumentDB/databaseAccounts/gremlinDatabases", - "apiVersion": "2025-04-15", - "name": "[format('{0}/{1}', parameters('databaseAccountName'), parameters('name'))]", - "tags": "[parameters('tags')]", - "properties": { - "options": "[if(contains(reference('databaseAccount').capabilities, createObject('name', 'EnableServerless')), createObject(), createObject('autoscaleSettings', if(equals(parameters('throughput'), null()), createObject('maxThroughput', parameters('maxThroughput')), null()), 'throughput', parameters('throughput')))]", - "resource": { - "id": "[parameters('name')]" - } - }, - "dependsOn": [ - "databaseAccount" - ] - }, - "gremlinDatabase_gremlinGraphs": { - "copy": { - "name": "gremlinDatabase_gremlinGraphs", - "count": "[length(coalesce(parameters('graphs'), createArray()))]" - }, - "type": "Microsoft.Resources/deployments", - "apiVersion": "2025-04-01", - "name": "[format('{0}-gremlindb-{1}', uniqueString(deployment().name, parameters('name')), coalesce(parameters('graphs'), createArray())[copyIndex()].name)]", - "properties": { - "expressionEvaluationOptions": { - "scope": "inner" - }, - "mode": "Incremental", - "parameters": { - "name": { - "value": "[coalesce(parameters('graphs'), createArray())[copyIndex()].name]" - }, - "gremlinDatabaseName": { - "value": "[parameters('name')]" - }, - "databaseAccountName": { - "value": "[parameters('databaseAccountName')]" - }, - "indexingPolicy": { - "value": "[tryGet(coalesce(parameters('graphs'), createArray())[copyIndex()], 'indexingPolicy')]" - }, - "partitionKeyPaths": { - "value": "[tryGet(coalesce(parameters('graphs'), createArray())[copyIndex()], 'partitionKeyPaths')]" - } - }, - "template": { - "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", - "languageVersion": "2.0", - "contentVersion": "1.0.0.0", - "metadata": { - "_generator": { - "name": "bicep", - "version": "0.38.33.27573", - "templateHash": "15097132107382000570" - }, - "name": "DocumentDB Database Accounts Gremlin Databases Graphs", - "description": "This module deploys a DocumentDB Database Accounts Gremlin Database Graph." - }, - "parameters": { - "name": { - "type": "string", - "metadata": { - "description": "Required. Name of the graph." - } - }, - "tags": { - "type": "object", - "metadata": { - "__bicep_resource_derived_type!": { - "source": "Microsoft.DocumentDB/databaseAccounts/gremlinDatabases/graphs@2025-04-15#properties/tags" - }, - "description": "Optional. Tags of the Gremlin graph resource." - }, - "nullable": true - }, - "databaseAccountName": { - "type": "string", - "metadata": { - "description": "Conditional. The name of the parent Database Account. Required if the template is used in a standalone deployment." - } - }, - "gremlinDatabaseName": { - "type": "string", - "metadata": { - "description": "Conditional. The name of the parent Gremlin Database. Required if the template is used in a standalone deployment." - } - }, - "indexingPolicy": { - "type": "object", - "metadata": { - "__bicep_resource_derived_type!": { - "source": "Microsoft.DocumentDB/databaseAccounts/gremlinDatabases/graphs@2025-04-15#properties/properties/properties/resource/properties/indexingPolicy" - }, - "description": "Optional. Indexing policy of the graph." - }, - "nullable": true - }, - "partitionKeyPaths": { - "type": "array", - "metadata": { - "__bicep_resource_derived_type!": { - "source": "Microsoft.DocumentDB/databaseAccounts/gremlinDatabases/graphs@2025-04-15#properties/properties/properties/resource/properties/partitionKey/properties/paths" - }, - "description": "Optional. List of paths using which data within the container can be partitioned." - }, - "nullable": true - } - }, - "resources": { - "databaseAccount::gremlinDatabase": { - "existing": true, - "type": "Microsoft.DocumentDB/databaseAccounts/gremlinDatabases", - "apiVersion": "2025-04-15", - "name": "[format('{0}/{1}', parameters('databaseAccountName'), parameters('gremlinDatabaseName'))]" - }, - "databaseAccount": { - "existing": true, - "type": "Microsoft.DocumentDB/databaseAccounts", - "apiVersion": "2025-04-15", - "name": "[parameters('databaseAccountName')]" - }, - "gremlinGraph": { - "type": "Microsoft.DocumentDB/databaseAccounts/gremlinDatabases/graphs", - "apiVersion": "2025-04-15", - "name": "[format('{0}/{1}/{2}', parameters('databaseAccountName'), parameters('gremlinDatabaseName'), parameters('name'))]", - "tags": "[parameters('tags')]", - "properties": { - "resource": { - "id": "[parameters('name')]", - "indexingPolicy": "[parameters('indexingPolicy')]", - "partitionKey": { - "paths": "[parameters('partitionKeyPaths')]" - } - } - } - } - }, - "outputs": { - "name": { - "type": "string", - "metadata": { - "description": "The name of the graph." - }, - "value": "[parameters('name')]" - }, - "resourceId": { - "type": "string", - "metadata": { - "description": "The resource ID of the graph." - }, - "value": "[resourceId('Microsoft.DocumentDB/databaseAccounts/gremlinDatabases/graphs', parameters('databaseAccountName'), parameters('gremlinDatabaseName'), parameters('name'))]" - }, - "resourceGroupName": { - "type": "string", - "metadata": { - "description": "The name of the resource group the graph was created in." - }, - "value": "[resourceGroup().name]" - } - } - } - }, - "dependsOn": [ - "gremlinDatabase" - ] - } + "nullable": true, + "metadata": { + "description": "Optional. Application security groups in which the Private Endpoint IP configuration is included." + } + }, + "customNetworkInterfaceName": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The custom name of the network interface attached to the Private Endpoint." + } + }, + "lock": { + "$ref": "#/definitions/_1.lockType", + "nullable": true, + "metadata": { + "description": "Optional. Specify the type of lock." + } + }, + "roleAssignments": { + "type": "array", + "items": { + "$ref": "#/definitions/_1.roleAssignmentType" }, - "outputs": { - "name": { - "type": "string", - "metadata": { - "description": "The name of the Gremlin database." - }, - "value": "[parameters('name')]" - }, - "resourceId": { - "type": "string", - "metadata": { - "description": "The resource ID of the Gremlin database." - }, - "value": "[resourceId('Microsoft.DocumentDB/databaseAccounts/gremlinDatabases', parameters('databaseAccountName'), parameters('name'))]" + "nullable": true, + "metadata": { + "description": "Optional. Array of role assignments to create." + } + }, + "tags": { + "type": "object", + "nullable": true, + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.Network/privateEndpoints@2024-07-01#properties/tags" }, - "resourceGroupName": { - "type": "string", - "metadata": { - "description": "The name of the resource group the Gremlin database was created in." - }, - "value": "[resourceGroup().name]" - } + "description": "Optional. Tags to be applied on all resources/Resource Groups in this deployment." + } + }, + "enableTelemetry": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. Enable/Disable usage telemetry for module." + } + } + }, + "metadata": { + "description": "An AVM-aligned type for a private endpoint. To be used if the private endpoint's default service / groupId can be assumed (i.e., for services that only have one Private Endpoint type like 'vault' for key vault).", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.1" + } + } + }, + "roleAssignmentType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The name (as GUID) of the role assignment. If not provided, a GUID will be generated." + } + }, + "roleDefinitionIdOrName": { + "type": "string", + "metadata": { + "description": "Required. The role to assign. You can provide either the display name of the role definition, the role definition GUID, or its fully qualified ID in the following format: '/providers/Microsoft.Authorization/roleDefinitions/c2f4ef07-c644-48eb-af81-4b1b4947fb11'." + } + }, + "principalId": { + "type": "string", + "metadata": { + "description": "Required. The principal ID of the principal (user/group/identity) to assign the role to." + } + }, + "principalType": { + "type": "string", + "allowedValues": [ + "Device", + "ForeignGroup", + "Group", + "ServicePrincipal", + "User" + ], + "nullable": true, + "metadata": { + "description": "Optional. The principal type of the assigned principal ID." + } + }, + "description": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The description of the role assignment." + } + }, + "condition": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The conditions on the role assignment. This limits the resources it can be assigned to. e.g.: @Resource[Microsoft.Storage/storageAccounts/blobServices/containers:ContainerName] StringEqualsIgnoreCase \"foo_storage_container\"." + } + }, + "conditionVersion": { + "type": "string", + "allowedValues": [ + "2.0" + ], + "nullable": true, + "metadata": { + "description": "Optional. Version of the condition." + } + }, + "delegatedManagedIdentityResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The Resource Id of the delegated managed identity resource." } } }, - "dependsOn": [ - "databaseAccount" - ] + "metadata": { + "description": "An AVM-aligned type for a role assignment.", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.5.1" + } + } }, - "databaseAccount_tables": { - "copy": { - "name": "databaseAccount_tables", - "count": "[length(coalesce(parameters('tables'), createArray()))]" - }, - "type": "Microsoft.Resources/deployments", - "apiVersion": "2025-04-01", - "name": "[format('{0}-table-{1}', uniqueString(deployment().name, parameters('location')), coalesce(parameters('tables'), createArray())[copyIndex()].name)]", + "secretSetType": { + "type": "object", "properties": { - "expressionEvaluationOptions": { - "scope": "inner" + "secretResourceId": { + "type": "string", + "metadata": { + "description": "The resourceId of the exported secret." + } }, - "mode": "Incremental", - "parameters": { - "databaseAccountName": { - "value": "[parameters('name')]" - }, - "name": { - "value": "[coalesce(parameters('tables'), createArray())[copyIndex()].name]" - }, - "tags": { - "value": "[coalesce(tryGet(coalesce(parameters('tables'), createArray())[copyIndex()], 'tags'), parameters('tags'))]" - }, - "maxThroughput": { - "value": "[tryGet(coalesce(parameters('tables'), createArray())[copyIndex()], 'maxThroughput')]" - }, - "throughput": { - "value": "[tryGet(coalesce(parameters('tables'), createArray())[copyIndex()], 'throughput')]" + "secretUri": { + "type": "string", + "metadata": { + "description": "The secret URI of the exported secret." } + } + }, + "metadata": { + "__bicep_imported_from!": { + "sourceTemplate": "modules/keyVaultExport.bicep" + } + } + } + }, + "parameters": { + "name": { + "type": "string", + "metadata": { + "description": "Required. The name of the Azure Cognitive Search service to create or update. Search service names must only contain lowercase letters, digits or dashes, cannot use dash as the first two or last one characters, cannot contain consecutive dashes, and must be between 2 and 60 characters in length. Search service names must be globally unique since they are part of the service URI (https://.search.windows.net). You cannot change the service name after the service is created." + } + }, + "authOptions": { + "$ref": "#/definitions/authOptionsType", + "nullable": true, + "metadata": { + "description": "Optional. Defines the options for how the data plane API of a Search service authenticates requests. Must remain an empty object {} if 'disableLocalAuth' is set to true." + } + }, + "disableLocalAuth": { + "type": "bool", + "defaultValue": true, + "metadata": { + "description": "Optional. When set to true, calls to the search service will not be permitted to utilize API keys for authentication. This cannot be set to true if 'authOptions' are defined." + } + }, + "enableTelemetry": { + "type": "bool", + "defaultValue": true, + "metadata": { + "description": "Optional. Enable/Disable usage telemetry for module." + } + }, + "cmkEnforcement": { + "type": "string", + "defaultValue": "Unspecified", + "allowedValues": [ + "Disabled", + "Enabled", + "Unspecified" + ], + "metadata": { + "description": "Optional. Describes a policy that determines how resources within the search service are to be encrypted with Customer Managed Keys." + } + }, + "hostingMode": { + "type": "string", + "defaultValue": "default", + "allowedValues": [ + "default", + "highDensity" + ], + "metadata": { + "description": "Optional. Applicable only for the standard3 SKU. You can set this property to enable up to 3 high density partitions that allow up to 1000 indexes, which is much higher than the maximum indexes allowed for any other SKU. For the standard3 SKU, the value is either 'default' or 'highDensity'. For all other SKUs, this value must be 'default'." + } + }, + "location": { + "type": "string", + "defaultValue": "[resourceGroup().location]", + "metadata": { + "description": "Optional. Location for all Resources." + } + }, + "lock": { + "$ref": "#/definitions/lockType", + "nullable": true, + "metadata": { + "description": "Optional. The lock settings for all Resources in the solution." + } + }, + "networkRuleSet": { + "$ref": "#/definitions/networkRuleSetType", + "nullable": true, + "metadata": { + "description": "Optional. Network specific rules that determine how the Azure Cognitive Search service may be reached." + } + }, + "partitionCount": { + "type": "int", + "defaultValue": 1, + "minValue": 1, + "maxValue": 12, + "metadata": { + "description": "Optional. The number of partitions in the search service; if specified, it can be 1, 2, 3, 4, 6, or 12. Values greater than 1 are only valid for standard SKUs. For 'standard3' services with hostingMode set to 'highDensity', the allowed values are between 1 and 3." + } + }, + "privateEndpoints": { + "type": "array", + "items": { + "$ref": "#/definitions/privateEndpointSingleServiceType" + }, + "nullable": true, + "metadata": { + "description": "Optional. Configuration details for private endpoints. For security reasons, it is recommended to use private endpoints whenever possible." + } + }, + "sharedPrivateLinkResources": { + "type": "array", + "defaultValue": [], + "metadata": { + "description": "Optional. The sharedPrivateLinkResources to create as part of the search Service." + } + }, + "publicNetworkAccess": { + "type": "string", + "defaultValue": "Enabled", + "allowedValues": [ + "Enabled", + "Disabled" + ], + "metadata": { + "description": "Optional. This value can be set to 'Enabled' to avoid breaking changes on existing customer resources and templates. If set to 'Disabled', traffic over public interface is not allowed, and private endpoint connections would be the exclusive access method." + } + }, + "secretsExportConfiguration": { + "$ref": "#/definitions/secretsExportConfigurationType", + "nullable": true, + "metadata": { + "description": "Optional. Key vault reference and secret settings for the module's secrets export." + } + }, + "replicaCount": { + "type": "int", + "defaultValue": 3, + "minValue": 1, + "maxValue": 12, + "metadata": { + "description": "Optional. The number of replicas in the search service. If specified, it must be a value between 1 and 12 inclusive for standard SKUs or between 1 and 3 inclusive for basic SKU." + } + }, + "roleAssignments": { + "type": "array", + "items": { + "$ref": "#/definitions/roleAssignmentType" + }, + "nullable": true, + "metadata": { + "description": "Optional. Array of role assignments to create." + } + }, + "semanticSearch": { + "type": "string", + "nullable": true, + "allowedValues": [ + "disabled", + "free", + "standard" + ], + "metadata": { + "description": "Optional. Sets options that control the availability of semantic search. This configuration is only possible for certain search SKUs in certain locations." + } + }, + "sku": { + "type": "string", + "defaultValue": "standard", + "allowedValues": [ + "basic", + "free", + "standard", + "standard2", + "standard3", + "storage_optimized_l1", + "storage_optimized_l2" + ], + "metadata": { + "description": "Optional. Defines the SKU of an Azure Cognitive Search Service, which determines price tier and capacity limits." + } + }, + "managedIdentities": { + "$ref": "#/definitions/managedIdentityAllType", + "nullable": true, + "metadata": { + "description": "Optional. The managed identity definition for this resource." + } + }, + "diagnosticSettings": { + "type": "array", + "items": { + "$ref": "#/definitions/diagnosticSettingFullType" + }, + "nullable": true, + "metadata": { + "description": "Optional. The diagnostic settings of the service." + } + }, + "tags": { + "type": "object", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.Search/searchServices@2025-02-01-preview#properties/tags" }, + "description": "Optional. Tags to help categorize the resource in the Azure portal." + }, + "nullable": true + } + }, + "variables": { + "copy": [ + { + "name": "formattedRoleAssignments", + "count": "[length(coalesce(parameters('roleAssignments'), createArray()))]", + "input": "[union(coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')], createObject('roleDefinitionId', coalesce(tryGet(variables('builtInRoleNames'), coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName), if(contains(coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName, '/providers/Microsoft.Authorization/roleDefinitions/'), coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName, subscriptionResourceId('Microsoft.Authorization/roleDefinitions', coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName)))))]" + } + ], + "enableReferencedModulesTelemetry": false, + "formattedUserAssignedIdentities": "[reduce(map(coalesce(tryGet(parameters('managedIdentities'), 'userAssignedResourceIds'), createArray()), lambda('id', createObject(format('{0}', lambdaVariables('id')), createObject()))), createObject(), lambda('cur', 'next', union(lambdaVariables('cur'), lambdaVariables('next'))))]", + "identity": "[if(not(empty(parameters('managedIdentities'))), createObject('type', if(coalesce(tryGet(parameters('managedIdentities'), 'systemAssigned'), false()), if(not(empty(coalesce(tryGet(parameters('managedIdentities'), 'userAssignedResourceIds'), createObject()))), 'SystemAssigned,UserAssigned', 'SystemAssigned'), if(not(empty(coalesce(tryGet(parameters('managedIdentities'), 'userAssignedResourceIds'), createObject()))), 'UserAssigned', '')), 'userAssignedIdentities', if(not(empty(variables('formattedUserAssignedIdentities'))), variables('formattedUserAssignedIdentities'), null())), null())]", + "builtInRoleNames": { + "Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'b24988ac-6180-42a0-ab88-20f7382dd24c')]", + "Owner": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '8e3af657-a8ff-443c-a75c-2fe8c4bcb635')]", + "Reader": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'acdd72a7-3385-48ef-bd42-f606fba81ae7')]", + "Role Based Access Control Administrator": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'f58310d9-a9f6-439a-9e8d-f62e7b41a168')]", + "Search Index Data Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '8ebe5a00-799e-43f5-93ac-243d3dce84a7')]", + "Search Index Data Reader": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '1407120a-92aa-4202-b7e9-c0e197c71c8f')]", + "Search Service Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '7ca78c08-252a-4471-8644-bb5ff32d4ba0')]", + "User Access Administrator": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '18d7d88d-d35e-4fb5-a5c3-7773c20a72d9')]" + } + }, + "resources": { + "avmTelemetry": { + "condition": "[parameters('enableTelemetry')]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2024-03-01", + "name": "[format('46d3xbcp.res.search-searchservice.{0}.{1}', replace('0.11.1', '.', '-'), substring(uniqueString(deployment().name, parameters('location')), 0, 4))]", + "properties": { + "mode": "Incremental", "template": { "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", - "languageVersion": "2.0", "contentVersion": "1.0.0.0", - "metadata": { - "_generator": { - "name": "bicep", - "version": "0.38.33.27573", - "templateHash": "11768488776074268398" - }, - "name": "Azure Cosmos DB account tables", - "description": "This module deploys a table within an Azure Cosmos DB Account." - }, - "parameters": { - "name": { - "type": "string", - "metadata": { - "description": "Required. Name of the table." - } - }, - "tags": { - "type": "object", - "metadata": { - "__bicep_resource_derived_type!": { - "source": "Microsoft.DocumentDB/databaseAccounts/tables@2025-04-15#properties/tags" - }, - "description": "Optional. Tags for the table." - }, - "nullable": true - }, - "databaseAccountName": { - "type": "string", - "metadata": { - "description": "Conditional. The name of the parent Azure Cosmos DB account. Required if the template is used in a standalone deployment." - } - }, - "maxThroughput": { - "type": "int", - "defaultValue": 4000, - "metadata": { - "description": "Optional. Represents maximum throughput, the resource can scale up to. Cannot be set together with `throughput`. If `throughput` is set to something else than -1, this autoscale setting is ignored." - } - }, - "throughput": { - "type": "int", - "nullable": true, - "metadata": { - "description": "Optional. Request Units per second (for example 10000). Cannot be set together with `maxThroughput`." - } + "resources": [], + "outputs": { + "telemetry": { + "type": "String", + "value": "For more information, see https://aka.ms/avm/TelemetryInfo" } - }, - "resources": { - "databaseAccount": { - "existing": true, - "type": "Microsoft.DocumentDB/databaseAccounts", - "apiVersion": "2025-04-15", - "name": "[parameters('databaseAccountName')]" - }, - "table": { - "type": "Microsoft.DocumentDB/databaseAccounts/tables", - "apiVersion": "2025-04-15", - "name": "[format('{0}/{1}', parameters('databaseAccountName'), parameters('name'))]", - "tags": "[parameters('tags')]", - "properties": { - "options": "[if(contains(reference('databaseAccount').capabilities, createObject('name', 'EnableServerless')), createObject(), createObject('autoscaleSettings', if(equals(parameters('throughput'), null()), createObject('maxThroughput', parameters('maxThroughput')), null()), 'throughput', parameters('throughput')))]", - "resource": { - "id": "[parameters('name')]" - } - }, - "dependsOn": [ - "databaseAccount" - ] + } + } + } + }, + "searchService": { + "type": "Microsoft.Search/searchServices", + "apiVersion": "2025-02-01-preview", + "name": "[parameters('name')]", + "location": "[parameters('location')]", + "sku": { + "name": "[parameters('sku')]" + }, + "tags": "[parameters('tags')]", + "identity": "[variables('identity')]", + "properties": { + "authOptions": "[parameters('authOptions')]", + "disableLocalAuth": "[parameters('disableLocalAuth')]", + "encryptionWithCmk": { + "enforcement": "[parameters('cmkEnforcement')]" + }, + "hostingMode": "[parameters('hostingMode')]", + "networkRuleSet": "[parameters('networkRuleSet')]", + "partitionCount": "[parameters('partitionCount')]", + "replicaCount": "[parameters('replicaCount')]", + "publicNetworkAccess": "[toLower(parameters('publicNetworkAccess'))]", + "semanticSearch": "[parameters('semanticSearch')]" + } + }, + "searchService_diagnosticSettings": { + "copy": { + "name": "searchService_diagnosticSettings", + "count": "[length(coalesce(parameters('diagnosticSettings'), createArray()))]" + }, + "type": "Microsoft.Insights/diagnosticSettings", + "apiVersion": "2021-05-01-preview", + "scope": "[format('Microsoft.Search/searchServices/{0}', parameters('name'))]", + "name": "[coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'name'), format('{0}-diagnosticSettings', parameters('name')))]", + "properties": { + "copy": [ + { + "name": "metrics", + "count": "[length(coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'metricCategories'), createArray(createObject('category', 'AllMetrics'))))]", + "input": { + "category": "[coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'metricCategories'), createArray(createObject('category', 'AllMetrics')))[copyIndex('metrics')].category]", + "enabled": "[coalesce(tryGet(coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'metricCategories'), createArray(createObject('category', 'AllMetrics')))[copyIndex('metrics')], 'enabled'), true())]", + "timeGrain": null } }, - "outputs": { - "name": { - "type": "string", - "metadata": { - "description": "The name of the table." - }, - "value": "[parameters('name')]" - }, - "resourceId": { - "type": "string", - "metadata": { - "description": "The resource ID of the table." - }, - "value": "[resourceId('Microsoft.DocumentDB/databaseAccounts/tables', parameters('databaseAccountName'), parameters('name'))]" - }, - "resourceGroupName": { - "type": "string", - "metadata": { - "description": "The name of the resource group the table was created in." - }, - "value": "[resourceGroup().name]" + { + "name": "logs", + "count": "[length(coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'logCategoriesAndGroups'), createArray(createObject('categoryGroup', 'allLogs'))))]", + "input": { + "categoryGroup": "[tryGet(coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'logCategoriesAndGroups'), createArray(createObject('categoryGroup', 'allLogs')))[copyIndex('logs')], 'categoryGroup')]", + "category": "[tryGet(coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'logCategoriesAndGroups'), createArray(createObject('categoryGroup', 'allLogs')))[copyIndex('logs')], 'category')]", + "enabled": "[coalesce(tryGet(coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'logCategoriesAndGroups'), createArray(createObject('categoryGroup', 'allLogs')))[copyIndex('logs')], 'enabled'), true())]" } } - } + ], + "storageAccountId": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'storageAccountResourceId')]", + "workspaceId": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'workspaceResourceId')]", + "eventHubAuthorizationRuleId": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'eventHubAuthorizationRuleResourceId')]", + "eventHubName": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'eventHubName')]", + "marketplacePartnerId": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'marketplacePartnerResourceId')]", + "logAnalyticsDestinationType": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'logAnalyticsDestinationType')]" + }, + "dependsOn": [ + "searchService" + ] + }, + "searchService_lock": { + "condition": "[and(not(empty(coalesce(parameters('lock'), createObject()))), not(equals(tryGet(parameters('lock'), 'kind'), 'None')))]", + "type": "Microsoft.Authorization/locks", + "apiVersion": "2020-05-01", + "scope": "[format('Microsoft.Search/searchServices/{0}', parameters('name'))]", + "name": "[coalesce(tryGet(parameters('lock'), 'name'), format('lock-{0}', parameters('name')))]", + "properties": { + "level": "[coalesce(tryGet(parameters('lock'), 'kind'), '')]", + "notes": "[coalesce(tryGet(parameters('lock'), 'notes'), if(equals(tryGet(parameters('lock'), 'kind'), 'CanNotDelete'), 'Cannot delete resource or child resources.', 'Cannot delete or modify the resource or child resources.'))]" + }, + "dependsOn": [ + "searchService" + ] + }, + "searchService_roleAssignments": { + "copy": { + "name": "searchService_roleAssignments", + "count": "[length(coalesce(variables('formattedRoleAssignments'), createArray()))]" + }, + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2022-04-01", + "scope": "[format('Microsoft.Search/searchServices/{0}', parameters('name'))]", + "name": "[coalesce(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'name'), guid(resourceId('Microsoft.Search/searchServices', parameters('name')), coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].principalId, coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].roleDefinitionId))]", + "properties": { + "roleDefinitionId": "[coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].roleDefinitionId]", + "principalId": "[coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].principalId]", + "description": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'description')]", + "principalType": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'principalType')]", + "condition": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'condition')]", + "conditionVersion": "[if(not(empty(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'condition'))), coalesce(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'conditionVersion'), '2.0'), null())]", + "delegatedManagedIdentityResourceId": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'delegatedManagedIdentityResourceId')]" }, "dependsOn": [ - "databaseAccount" + "searchService" ] }, - "databaseAccount_cassandraKeyspaces": { + "searchService_privateEndpoints": { "copy": { - "name": "databaseAccount_cassandraKeyspaces", - "count": "[length(coalesce(parameters('cassandraKeyspaces'), createArray()))]" + "name": "searchService_privateEndpoints", + "count": "[length(coalesce(parameters('privateEndpoints'), createArray()))]" }, "type": "Microsoft.Resources/deployments", - "apiVersion": "2025-04-01", - "name": "[format('{0}-cassandradb-{1}', uniqueString(deployment().name, parameters('location')), coalesce(parameters('cassandraKeyspaces'), createArray())[copyIndex()].name)]", + "apiVersion": "2022-09-01", + "name": "[format('{0}-searchService-PrivateEndpoint-{1}', uniqueString(deployment().name, parameters('location')), copyIndex())]", + "subscriptionId": "[split(coalesce(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'resourceGroupResourceId'), resourceGroup().id), '/')[2]]", + "resourceGroup": "[split(coalesce(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'resourceGroupResourceId'), resourceGroup().id), '/')[4]]", "properties": { "expressionEvaluationOptions": { "scope": "inner" }, "mode": "Incremental", "parameters": { - "databaseAccountName": { - "value": "[parameters('name')]" - }, "name": { - "value": "[coalesce(parameters('cassandraKeyspaces'), createArray())[copyIndex()].name]" + "value": "[coalesce(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'name'), format('pep-{0}-{1}-{2}', last(split(resourceId('Microsoft.Search/searchServices', parameters('name')), '/')), coalesce(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'service'), 'searchService'), copyIndex()))]" + }, + "privateLinkServiceConnections": "[if(not(equals(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'isManualConnection'), true())), createObject('value', createArray(createObject('name', coalesce(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'privateLinkServiceConnectionName'), format('{0}-{1}-{2}', last(split(resourceId('Microsoft.Search/searchServices', parameters('name')), '/')), coalesce(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'service'), 'searchService'), copyIndex())), 'properties', createObject('privateLinkServiceId', resourceId('Microsoft.Search/searchServices', parameters('name')), 'groupIds', createArray(coalesce(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'service'), 'searchService')))))), createObject('value', null()))]", + "manualPrivateLinkServiceConnections": "[if(equals(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'isManualConnection'), true()), createObject('value', createArray(createObject('name', coalesce(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'privateLinkServiceConnectionName'), format('{0}-{1}-{2}', last(split(resourceId('Microsoft.Search/searchServices', parameters('name')), '/')), coalesce(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'service'), 'searchService'), copyIndex())), 'properties', createObject('privateLinkServiceId', resourceId('Microsoft.Search/searchServices', parameters('name')), 'groupIds', createArray(coalesce(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'service'), 'searchService')), 'requestMessage', coalesce(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'manualConnectionRequestMessage'), 'Manual approval required.'))))), createObject('value', null()))]", + "subnetResourceId": { + "value": "[coalesce(parameters('privateEndpoints'), createArray())[copyIndex()].subnetResourceId]" + }, + "enableTelemetry": { + "value": "[variables('enableReferencedModulesTelemetry')]" + }, + "location": { + "value": "[coalesce(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'location'), reference(split(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()].subnetResourceId, '/subnets/')[0], '2020-06-01', 'Full').location)]" + }, + "lock": { + "value": "[coalesce(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'lock'), parameters('lock'))]" + }, + "privateDnsZoneGroup": { + "value": "[tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'privateDnsZoneGroup')]" + }, + "roleAssignments": { + "value": "[tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'roleAssignments')]" }, "tags": { - "value": "[coalesce(tryGet(coalesce(parameters('cassandraKeyspaces'), createArray())[copyIndex()], 'tags'), parameters('tags'))]" + "value": "[coalesce(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'tags'), parameters('tags'))]" }, - "tables": { - "value": "[tryGet(coalesce(parameters('cassandraKeyspaces'), createArray())[copyIndex()], 'tables')]" + "customDnsConfigs": { + "value": "[tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'customDnsConfigs')]" }, - "views": { - "value": "[tryGet(coalesce(parameters('cassandraKeyspaces'), createArray())[copyIndex()], 'views')]" + "ipConfigurations": { + "value": "[tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'ipConfigurations')]" }, - "autoscaleSettingsMaxThroughput": { - "value": "[tryGet(coalesce(parameters('cassandraKeyspaces'), createArray())[copyIndex()], 'autoscaleSettingsMaxThroughput')]" + "applicationSecurityGroupResourceIds": { + "value": "[tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'applicationSecurityGroupResourceIds')]" }, - "throughput": { - "value": "[tryGet(coalesce(parameters('cassandraKeyspaces'), createArray())[copyIndex()], 'throughput')]" + "customNetworkInterfaceName": { + "value": "[tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'customNetworkInterfaceName')]" } }, "template": { @@ -29248,118 +26505,271 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.38.33.27573", - "templateHash": "63327155428300562" + "version": "0.34.44.8038", + "templateHash": "12389807800450456797" }, - "name": "DocumentDB Database Account Cassandra Keyspaces", - "description": "This module deploys a Cassandra Keyspace within a CosmosDB Account." + "name": "Private Endpoints", + "description": "This module deploys a Private Endpoint." }, "definitions": { - "tableType": { + "privateDnsZoneGroupType": { "type": "object", "properties": { "name": { "type": "string", + "nullable": true, "metadata": { - "description": "Required. Name of the table." + "description": "Optional. The name of the Private DNS Zone Group." } }, - "schema": { - "type": "object", + "privateDnsZoneGroupConfigs": { + "type": "array", + "items": { + "$ref": "#/definitions/privateDnsZoneGroupConfigType" + }, "metadata": { - "__bicep_resource_derived_type!": { - "source": "Microsoft.DocumentDB/databaseAccounts/cassandraKeyspaces/tables@2024-11-15#properties/properties/properties/resource/properties/schema" - }, - "description": "Required. Schema definition for the table." + "description": "Required. The private DNS zone groups to associate the private endpoint. A DNS zone group can support up to 5 DNS zones." + } + } + }, + "metadata": { + "__bicep_export!": true + } + }, + "ipConfigurationType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "metadata": { + "description": "Required. The name of the resource that is unique within a resource group." } }, - "tags": { + "properties": { "type": "object", - "metadata": { - "__bicep_resource_derived_type!": { - "source": "Microsoft.DocumentDB/databaseAccounts/cassandraKeyspaces/tables@2024-11-15#properties/tags" + "properties": { + "groupId": { + "type": "string", + "metadata": { + "description": "Required. The ID of a group obtained from the remote resource that this private endpoint should connect to. If used with private link service connection, this property must be defined as empty string." + } }, - "description": "Optional. Tags for the table." + "memberName": { + "type": "string", + "metadata": { + "description": "Required. The member name of a group obtained from the remote resource that this private endpoint should connect to. If used with private link service connection, this property must be defined as empty string." + } + }, + "privateIPAddress": { + "type": "string", + "metadata": { + "description": "Required. A private IP address obtained from the private endpoint's subnet." + } + } }, - "nullable": true + "metadata": { + "description": "Required. Properties of private endpoint IP configurations." + } + } + }, + "metadata": { + "__bicep_export!": true + } + }, + "privateLinkServiceConnectionType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "metadata": { + "description": "Required. The name of the private link service connection." + } }, - "defaultTtl": { - "type": "int", + "properties": { + "type": "object", + "properties": { + "groupIds": { + "type": "array", + "items": { + "type": "string" + }, + "metadata": { + "description": "Required. The ID of a group obtained from the remote resource that this private endpoint should connect to. If used with private link service connection, this property must be defined as empty string array `[]`." + } + }, + "privateLinkServiceId": { + "type": "string", + "metadata": { + "description": "Required. The resource id of private link service." + } + }, + "requestMessage": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. A message passed to the owner of the remote resource with this connection request. Restricted to 140 chars." + } + } + }, + "metadata": { + "description": "Required. Properties of private link service connection." + } + } + }, + "metadata": { + "__bicep_export!": true + } + }, + "customDnsConfigType": { + "type": "object", + "properties": { + "fqdn": { + "type": "string", "nullable": true, "metadata": { - "description": "Optional. Default TTL (Time To Live) in seconds for data in the table." + "description": "Optional. FQDN that resolves to private endpoint IP address." } }, - "analyticalStorageTtl": { - "type": "int", + "ipAddresses": { + "type": "array", + "items": { + "type": "string" + }, + "metadata": { + "description": "Required. A list of private IP addresses of the private endpoint." + } + } + }, + "metadata": { + "__bicep_export!": true + } + }, + "lockType": { + "type": "object", + "properties": { + "name": { + "type": "string", "nullable": true, "metadata": { - "description": "Optional. Analytical TTL for the table." + "description": "Optional. Specify the name of lock." } }, - "throughput": { - "type": "int", + "kind": { + "type": "string", + "allowedValues": [ + "CanNotDelete", + "None", + "ReadOnly" + ], "nullable": true, "metadata": { - "description": "Optional. Request units per second. Cannot be used with autoscaleSettingsMaxThroughput." + "description": "Optional. Specify the type of lock." } - }, - "autoscaleSettingsMaxThroughput": { - "type": "int", + } + }, + "metadata": { + "description": "An AVM-aligned type for a lock.", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.5.1" + } + } + }, + "privateDnsZoneGroupConfigType": { + "type": "object", + "properties": { + "name": { + "type": "string", "nullable": true, "metadata": { - "description": "Optional. Maximum autoscale throughput for the table. Cannot be used with throughput." + "description": "Optional. The name of the private DNS zone group config." + } + }, + "privateDnsZoneResourceId": { + "type": "string", + "metadata": { + "description": "Required. The resource id of the private DNS zone." } } }, "metadata": { - "__bicep_export!": true, - "description": "The type of a Cassandra table." + "__bicep_imported_from!": { + "sourceTemplate": "private-dns-zone-group/main.bicep" + } } }, - "viewType": { + "roleAssignmentType": { "type": "object", "properties": { "name": { "type": "string", + "nullable": true, "metadata": { - "description": "Required. Name of the view." + "description": "Optional. The name (as GUID) of the role assignment. If not provided, a GUID will be generated." } }, - "viewDefinition": { + "roleDefinitionIdOrName": { "type": "string", "metadata": { - "description": "Required. View definition (CQL statement)." + "description": "Required. The role to assign. You can provide either the display name of the role definition, the role definition GUID, or its fully qualified ID in the following format: '/providers/Microsoft.Authorization/roleDefinitions/c2f4ef07-c644-48eb-af81-4b1b4947fb11'." } }, - "tags": { - "type": "object", + "principalId": { + "type": "string", "metadata": { - "__bicep_resource_derived_type!": { - "source": "Microsoft.DocumentDB/databaseAccounts/cassandraKeyspaces/views@2025-05-01-preview#properties/tags" - }, - "description": "Optional. Tags for the view." - }, - "nullable": true + "description": "Required. The principal ID of the principal (user/group/identity) to assign the role to." + } }, - "throughput": { - "type": "int", + "principalType": { + "type": "string", + "allowedValues": [ + "Device", + "ForeignGroup", + "Group", + "ServicePrincipal", + "User" + ], "nullable": true, "metadata": { - "description": "Optional. Request units per second. Cannot be used with autoscaleSettingsMaxThroughput." + "description": "Optional. The principal type of the assigned principal ID." } }, - "autoscaleSettingsMaxThroughput": { - "type": "int", + "description": { + "type": "string", "nullable": true, "metadata": { - "description": "Optional. Maximum autoscale throughput for the view. Cannot be used with throughput." + "description": "Optional. The description of the role assignment." + } + }, + "condition": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The conditions on the role assignment. This limits the resources it can be assigned to. e.g.: @Resource[Microsoft.Storage/storageAccounts/blobServices/containers:ContainerName] StringEqualsIgnoreCase \"foo_storage_container\"." + } + }, + "conditionVersion": { + "type": "string", + "allowedValues": [ + "2.0" + ], + "nullable": true, + "metadata": { + "description": "Optional. Version of the condition." + } + }, + "delegatedManagedIdentityResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The Resource Id of the delegated managed identity resource." } } }, "metadata": { - "__bicep_export!": true, - "description": "The type of a Cassandra view (materialized view)." + "description": "An AVM-aligned type for a role assignment.", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.5.1" + } } } }, @@ -29367,273 +26777,227 @@ "name": { "type": "string", "metadata": { - "description": "Required. Name of the Cassandra keyspace." + "description": "Required. Name of the private endpoint resource to create." } }, - "tags": { - "type": "object", + "subnetResourceId": { + "type": "string", "metadata": { - "__bicep_resource_derived_type!": { - "source": "Microsoft.DocumentDB/databaseAccounts/cassandraKeyspaces@2024-11-15#properties/tags" - }, - "description": "Optional. Tags of the Cassandra keyspace resource." + "description": "Required. Resource ID of the subnet where the endpoint needs to be created." + } + }, + "applicationSecurityGroupResourceIds": { + "type": "array", + "items": { + "type": "string" }, - "nullable": true + "nullable": true, + "metadata": { + "description": "Optional. Application security groups in which the private endpoint IP configuration is included." + } }, - "databaseAccountName": { + "customNetworkInterfaceName": { "type": "string", + "nullable": true, "metadata": { - "description": "Conditional. The name of the parent Cosmos DB account. Required if the template is used in a standalone deployment." + "description": "Optional. The custom name of the network interface attached to the private endpoint." } }, - "tables": { + "ipConfigurations": { "type": "array", "items": { - "$ref": "#/definitions/tableType" + "$ref": "#/definitions/ipConfigurationType" }, - "defaultValue": [], + "nullable": true, "metadata": { - "description": "Optional. Array of Cassandra tables to deploy in the keyspace." + "description": "Optional. A list of IP configurations of the private endpoint. This will be used to map to the First Party Service endpoints." } }, - "views": { + "privateDnsZoneGroup": { + "$ref": "#/definitions/privateDnsZoneGroupType", + "nullable": true, + "metadata": { + "description": "Optional. The private DNS zone group to configure for the private endpoint." + } + }, + "location": { + "type": "string", + "defaultValue": "[resourceGroup().location]", + "metadata": { + "description": "Optional. Location for all Resources." + } + }, + "lock": { + "$ref": "#/definitions/lockType", + "nullable": true, + "metadata": { + "description": "Optional. The lock settings of the service." + } + }, + "roleAssignments": { "type": "array", "items": { - "$ref": "#/definitions/viewType" + "$ref": "#/definitions/roleAssignmentType" }, - "defaultValue": [], + "nullable": true, "metadata": { - "description": "Optional. Array of Cassandra views (materialized views) to deploy in the keyspace." + "description": "Optional. Array of role assignments to create." } }, - "autoscaleSettingsMaxThroughput": { - "type": "int", - "defaultValue": 4000, + "tags": { + "type": "object", + "nullable": true, "metadata": { - "description": "Optional. Maximum autoscale throughput for the keyspace. If not set, autoscale will be disabled. Setting throughput at the keyspace level is only recommended for development/test or when workload across all tables in the shared throughput keyspace is uniform. For best performance for large production workloads, it is recommended to set dedicated throughput (autoscale or manual) at the table level." + "description": "Optional. Tags to be applied on all resources/resource groups in this deployment." } }, - "throughput": { - "type": "int", + "customDnsConfigs": { + "type": "array", + "items": { + "$ref": "#/definitions/customDnsConfigType" + }, "nullable": true, "metadata": { - "description": "Optional. Request units per second. Cannot be used with autoscaleSettingsMaxThroughput. Setting throughput at the keyspace level is only recommended for development/test or when workload across all tables in the shared throughput keyspace is uniform. For best performance for large production workloads, it is recommended to set dedicated throughput (autoscale or manual) at the table level." + "description": "Optional. Custom DNS configurations." } - } - }, - "resources": { - "databaseAccount": { - "existing": true, - "type": "Microsoft.DocumentDB/databaseAccounts", - "apiVersion": "2024-11-15", - "name": "[parameters('databaseAccountName')]" }, - "cassandraKeyspace": { - "type": "Microsoft.DocumentDB/databaseAccounts/cassandraKeyspaces", - "apiVersion": "2024-11-15", - "name": "[format('{0}/{1}', parameters('databaseAccountName'), parameters('name'))]", - "tags": "[parameters('tags')]", - "properties": { - "options": "[if(contains(reference('databaseAccount').capabilities, createObject('name', 'EnableServerless')), createObject(), createObject('autoscaleSettings', if(equals(parameters('throughput'), null()), createObject('maxThroughput', parameters('autoscaleSettingsMaxThroughput')), null()), 'throughput', parameters('throughput')))]", - "resource": { - "id": "[parameters('name')]" - } + "manualPrivateLinkServiceConnections": { + "type": "array", + "items": { + "$ref": "#/definitions/privateLinkServiceConnectionType" }, - "dependsOn": [ - "databaseAccount" - ] + "nullable": true, + "metadata": { + "description": "Conditional. A grouping of information about the connection to the remote resource. Used when the network admin does not have access to approve connections to the remote resource. Required if `privateLinkServiceConnections` is empty." + } }, - "cassandraKeyspace_tables": { - "copy": { - "name": "cassandraKeyspace_tables", - "count": "[length(parameters('tables'))]" + "privateLinkServiceConnections": { + "type": "array", + "items": { + "$ref": "#/definitions/privateLinkServiceConnectionType" }, - "type": "Microsoft.Resources/deployments", - "apiVersion": "2025-04-01", - "name": "[format('{0}-cassandradb-{1}', uniqueString(deployment().name, parameters('name')), parameters('tables')[copyIndex()].name)]", - "properties": { - "expressionEvaluationOptions": { - "scope": "inner" - }, - "mode": "Incremental", - "parameters": { - "name": { - "value": "[parameters('tables')[copyIndex()].name]" - }, - "cassandraKeyspaceName": { - "value": "[parameters('name')]" - }, - "databaseAccountName": { - "value": "[parameters('databaseAccountName')]" - }, - "schema": { - "value": "[parameters('tables')[copyIndex()].schema]" - }, - "analyticalStorageTtl": { - "value": "[tryGet(parameters('tables')[copyIndex()], 'analyticalStorageTtl')]" - }, - "throughput": { - "value": "[tryGet(parameters('tables')[copyIndex()], 'throughput')]" - }, - "autoscaleSettingsMaxThroughput": { - "value": "[tryGet(parameters('tables')[copyIndex()], 'autoscaleSettingsMaxThroughput')]" - }, - "defaultTtl": { - "value": "[tryGet(parameters('tables')[copyIndex()], 'defaultTtl')]" - }, - "tags": { - "value": "[coalesce(tryGet(parameters('tables')[copyIndex()], 'tags'), parameters('tags'))]" - } - }, + "nullable": true, + "metadata": { + "description": "Conditional. A grouping of information about the connection to the remote resource. Required if `manualPrivateLinkServiceConnections` is empty." + } + }, + "enableTelemetry": { + "type": "bool", + "defaultValue": true, + "metadata": { + "description": "Optional. Enable/Disable usage telemetry for module." + } + } + }, + "variables": { + "copy": [ + { + "name": "formattedRoleAssignments", + "count": "[length(coalesce(parameters('roleAssignments'), createArray()))]", + "input": "[union(coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')], createObject('roleDefinitionId', coalesce(tryGet(variables('builtInRoleNames'), coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName), if(contains(coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName, '/providers/Microsoft.Authorization/roleDefinitions/'), coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName, subscriptionResourceId('Microsoft.Authorization/roleDefinitions', coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName)))))]" + } + ], + "builtInRoleNames": { + "Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'b24988ac-6180-42a0-ab88-20f7382dd24c')]", + "DNS Resolver Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '0f2ebee7-ffd4-4fc0-b3b7-664099fdad5d')]", + "DNS Zone Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'befefa01-2a29-4197-83a8-272ff33ce314')]", + "Domain Services Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'eeaeda52-9324-47f6-8069-5d5bade478b2')]", + "Domain Services Reader": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '361898ef-9ed1-48c2-849c-a832951106bb')]", + "Network Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '4d97b98b-1d4f-4787-a291-c67834d212e7')]", + "Owner": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '8e3af657-a8ff-443c-a75c-2fe8c4bcb635')]", + "Private DNS Zone Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'b12aa53e-6015-4669-85d0-8515ebb3ae7f')]", + "Reader": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'acdd72a7-3385-48ef-bd42-f606fba81ae7')]", + "Role Based Access Control Administrator": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'f58310d9-a9f6-439a-9e8d-f62e7b41a168')]" + } + }, + "resources": { + "avmTelemetry": { + "condition": "[parameters('enableTelemetry')]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2024-03-01", + "name": "[format('46d3xbcp.res.network-privateendpoint.{0}.{1}', replace('0.11.0', '.', '-'), substring(uniqueString(deployment().name, parameters('location')), 0, 4))]", + "properties": { + "mode": "Incremental", "template": { "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", - "languageVersion": "2.0", "contentVersion": "1.0.0.0", - "metadata": { - "_generator": { - "name": "bicep", - "version": "0.38.33.27573", - "templateHash": "785607874724829202" - }, - "name": "DocumentDB Database Account Cassandra Keyspaces Tables", - "description": "This module deploys a Cassandra Table within a Cassandra Keyspace in a CosmosDB Account." - }, - "parameters": { - "name": { - "type": "string", - "metadata": { - "description": "Required. Name of the Cassandra table." - } - }, - "tags": { - "type": "object", - "metadata": { - "__bicep_resource_derived_type!": { - "source": "Microsoft.DocumentDB/databaseAccounts/cassandraKeyspaces/tables@2024-11-15#properties/tags" - }, - "description": "Optional. Tags of the Cassandra table resource." - }, - "nullable": true - }, - "databaseAccountName": { - "type": "string", - "metadata": { - "description": "Conditional. The name of the parent Database Account. Required if the template is used in a standalone deployment." - } - }, - "cassandraKeyspaceName": { - "type": "string", - "metadata": { - "description": "Conditional. The name of the parent Cassandra Keyspace. Required if the template is used in a standalone deployment." - } - }, - "schema": { - "type": "object", - "metadata": { - "__bicep_resource_derived_type!": { - "source": "Microsoft.DocumentDB/databaseAccounts/cassandraKeyspaces/tables@2024-11-15#properties/properties/properties/resource/properties/schema" - }, - "description": "Required. Schema definition for the Cassandra table." - } - }, - "analyticalStorageTtl": { - "type": "int", - "defaultValue": 0, - "metadata": { - "description": "Optional. Analytical TTL for the table. Default to 0 (disabled). Analytical store is enabled when set to a value other than 0. If set to -1, analytical store retains all historical data." - } - }, - "throughput": { - "type": "int", - "nullable": true, - "metadata": { - "description": "Optional. Request units per second. Cannot be used with autoscaleSettingsMaxThroughput. If not specified, the table will inherit throughput from the keyspace." - } - }, - "autoscaleSettingsMaxThroughput": { - "type": "int", - "nullable": true, - "metadata": { - "description": "Optional. Maximum autoscale throughput for the table. Cannot be used with throughput. If not specified, the table will inherit throughput from the keyspace." - } - }, - "defaultTtl": { - "type": "int", - "defaultValue": 0, - "metadata": { - "description": "Optional. Default time to live in seconds. Default to 0 (disabled). If set to -1, items do not expire." - } - } - }, - "resources": { - "databaseAccount::cassandraKeyspace": { - "existing": true, - "type": "Microsoft.DocumentDB/databaseAccounts/cassandraKeyspaces", - "apiVersion": "2024-11-15", - "name": "[format('{0}/{1}', parameters('databaseAccountName'), parameters('cassandraKeyspaceName'))]" - }, - "databaseAccount": { - "existing": true, - "type": "Microsoft.DocumentDB/databaseAccounts", - "apiVersion": "2024-11-15", - "name": "[parameters('databaseAccountName')]" - }, - "cassandraTable": { - "type": "Microsoft.DocumentDB/databaseAccounts/cassandraKeyspaces/tables", - "apiVersion": "2024-11-15", - "name": "[format('{0}/{1}/{2}', parameters('databaseAccountName'), parameters('cassandraKeyspaceName'), parameters('name'))]", - "tags": "[parameters('tags')]", - "properties": { - "resource": { - "id": "[parameters('name')]", - "schema": "[parameters('schema')]", - "defaultTtl": "[parameters('defaultTtl')]", - "analyticalStorageTtl": "[parameters('analyticalStorageTtl')]" - }, - "options": "[if(contains(reference('databaseAccount').capabilities, createObject('name', 'EnableServerless')), createObject(), createObject('autoscaleSettings', if(and(equals(parameters('throughput'), null()), not(equals(parameters('autoscaleSettingsMaxThroughput'), null()))), createObject('maxThroughput', parameters('autoscaleSettingsMaxThroughput')), null()), 'throughput', parameters('throughput')))]" - }, - "dependsOn": [ - "databaseAccount" - ] - } - }, + "resources": [], "outputs": { - "name": { - "type": "string", - "metadata": { - "description": "The name of the Cassandra table." - }, - "value": "[parameters('name')]" - }, - "resourceId": { - "type": "string", - "metadata": { - "description": "The resource ID of the Cassandra table." - }, - "value": "[resourceId('Microsoft.DocumentDB/databaseAccounts/cassandraKeyspaces/tables', parameters('databaseAccountName'), parameters('cassandraKeyspaceName'), parameters('name'))]" - }, - "resourceGroupName": { - "type": "string", - "metadata": { - "description": "The name of the resource group the Cassandra table was created in." - }, - "value": "[resourceGroup().name]" + "telemetry": { + "type": "String", + "value": "For more information, see https://aka.ms/avm/TelemetryInfo" + } + } + } + } + }, + "privateEndpoint": { + "type": "Microsoft.Network/privateEndpoints", + "apiVersion": "2024-05-01", + "name": "[parameters('name')]", + "location": "[parameters('location')]", + "tags": "[parameters('tags')]", + "properties": { + "copy": [ + { + "name": "applicationSecurityGroups", + "count": "[length(coalesce(parameters('applicationSecurityGroupResourceIds'), createArray()))]", + "input": { + "id": "[coalesce(parameters('applicationSecurityGroupResourceIds'), createArray())[copyIndex('applicationSecurityGroups')]]" } } + ], + "customDnsConfigs": "[coalesce(parameters('customDnsConfigs'), createArray())]", + "customNetworkInterfaceName": "[coalesce(parameters('customNetworkInterfaceName'), '')]", + "ipConfigurations": "[coalesce(parameters('ipConfigurations'), createArray())]", + "manualPrivateLinkServiceConnections": "[coalesce(parameters('manualPrivateLinkServiceConnections'), createArray())]", + "privateLinkServiceConnections": "[coalesce(parameters('privateLinkServiceConnections'), createArray())]", + "subnet": { + "id": "[parameters('subnetResourceId')]" } + } + }, + "privateEndpoint_lock": { + "condition": "[and(not(empty(coalesce(parameters('lock'), createObject()))), not(equals(tryGet(parameters('lock'), 'kind'), 'None')))]", + "type": "Microsoft.Authorization/locks", + "apiVersion": "2020-05-01", + "scope": "[format('Microsoft.Network/privateEndpoints/{0}', parameters('name'))]", + "name": "[coalesce(tryGet(parameters('lock'), 'name'), format('lock-{0}', parameters('name')))]", + "properties": { + "level": "[coalesce(tryGet(parameters('lock'), 'kind'), '')]", + "notes": "[if(equals(tryGet(parameters('lock'), 'kind'), 'CanNotDelete'), 'Cannot delete resource or child resources.', 'Cannot delete or modify the resource or child resources.')]" }, "dependsOn": [ - "cassandraKeyspace" + "privateEndpoint" ] }, - "cassandraKeyspace_views": { + "privateEndpoint_roleAssignments": { "copy": { - "name": "cassandraKeyspace_views", - "count": "[length(parameters('views'))]" + "name": "privateEndpoint_roleAssignments", + "count": "[length(coalesce(variables('formattedRoleAssignments'), createArray()))]" + }, + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2022-04-01", + "scope": "[format('Microsoft.Network/privateEndpoints/{0}', parameters('name'))]", + "name": "[coalesce(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'name'), guid(resourceId('Microsoft.Network/privateEndpoints', parameters('name')), coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].principalId, coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].roleDefinitionId))]", + "properties": { + "roleDefinitionId": "[coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].roleDefinitionId]", + "principalId": "[coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].principalId]", + "description": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'description')]", + "principalType": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'principalType')]", + "condition": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'condition')]", + "conditionVersion": "[if(not(empty(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'condition'))), coalesce(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'conditionVersion'), '2.0'), null())]", + "delegatedManagedIdentityResourceId": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'delegatedManagedIdentityResourceId')]" }, + "dependsOn": [ + "privateEndpoint" + ] + }, + "privateEndpoint_privateDnsZoneGroup": { + "condition": "[not(empty(parameters('privateDnsZoneGroup')))]", "type": "Microsoft.Resources/deployments", - "apiVersion": "2025-04-01", - "name": "[format('{0}-cassandraview-{1}', uniqueString(deployment().name, parameters('name')), parameters('views')[copyIndex()].name)]", + "apiVersion": "2022-09-01", + "name": "[format('{0}-PrivateEndpoint-PrivateDnsZoneGroup', uniqueString(deployment().name))]", "properties": { "expressionEvaluationOptions": { "scope": "inner" @@ -29641,25 +27005,13 @@ "mode": "Incremental", "parameters": { "name": { - "value": "[parameters('views')[copyIndex()].name]" + "value": "[tryGet(parameters('privateDnsZoneGroup'), 'name')]" }, - "cassandraKeyspaceName": { + "privateEndpointName": { "value": "[parameters('name')]" }, - "databaseAccountName": { - "value": "[parameters('databaseAccountName')]" - }, - "viewDefinition": { - "value": "[parameters('views')[copyIndex()].viewDefinition]" - }, - "throughput": { - "value": "[tryGet(parameters('views')[copyIndex()], 'throughput')]" - }, - "autoscaleSettingsMaxThroughput": { - "value": "[tryGet(parameters('views')[copyIndex()], 'autoscaleSettingsMaxThroughput')]" - }, - "tags": { - "value": "[coalesce(tryGet(parameters('views')[copyIndex()], 'tags'), parameters('tags'))]" + "privateDnsZoneConfigs": { + "value": "[parameters('privateDnsZoneGroup').privateDnsZoneGroupConfigs]" } }, "template": { @@ -29669,119 +27021,110 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.38.33.27573", - "templateHash": "14021794949328228224" + "version": "0.34.44.8038", + "templateHash": "13997305779829540948" }, - "name": "DocumentDB Database Account Cassandra Keyspaces Views", - "description": "This module deploys a Cassandra View (Materialized View) within a Cassandra Keyspace in a CosmosDB Account." + "name": "Private Endpoint Private DNS Zone Groups", + "description": "This module deploys a Private Endpoint Private DNS Zone Group." }, - "parameters": { - "name": { - "type": "string", - "metadata": { - "description": "Required. Name of the Cassandra view." - } - }, - "tags": { + "definitions": { + "privateDnsZoneGroupConfigType": { "type": "object", - "metadata": { - "__bicep_resource_derived_type!": { - "source": "Microsoft.DocumentDB/databaseAccounts/cassandraKeyspaces/views@2025-05-01-preview#properties/tags" + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The name of the private DNS zone group config." + } }, - "description": "Optional. Tags of the Cassandra view resource." + "privateDnsZoneResourceId": { + "type": "string", + "metadata": { + "description": "Required. The resource id of the private DNS zone." + } + } }, - "nullable": true - }, - "databaseAccountName": { - "type": "string", - "metadata": { - "description": "Conditional. The name of the parent Database Account. Required if the template is used in a standalone deployment." - } - }, - "cassandraKeyspaceName": { - "type": "string", "metadata": { - "description": "Conditional. The name of the parent Cassandra Keyspace. Required if the template is used in a standalone deployment." + "__bicep_export!": true } - }, - "viewDefinition": { + } + }, + "parameters": { + "privateEndpointName": { "type": "string", "metadata": { - "description": "Required. View definition of the Cassandra view. This is the CQL statement that defines the materialized view." - } - }, - "throughput": { - "type": "int", - "nullable": true, - "metadata": { - "description": "Optional. Request units per second. Cannot be used with autoscaleSettingsMaxThroughput." + "description": "Conditional. The name of the parent private endpoint. Required if the template is used in a standalone deployment." } }, - "autoscaleSettingsMaxThroughput": { - "type": "int", - "nullable": true, + "privateDnsZoneConfigs": { + "type": "array", + "items": { + "$ref": "#/definitions/privateDnsZoneGroupConfigType" + }, + "minLength": 1, + "maxLength": 5, "metadata": { - "description": "Optional. Maximum autoscale throughput for the view. Cannot be used with throughput." + "description": "Required. Array of private DNS zone configurations of the private DNS zone group. A DNS zone group can support up to 5 DNS zones." } }, - "location": { + "name": { "type": "string", - "defaultValue": "[resourceGroup().location]", + "defaultValue": "default", "metadata": { - "description": "Optional. Location for all resources." + "description": "Optional. The name of the private DNS zone group." } } }, + "variables": { + "copy": [ + { + "name": "privateDnsZoneConfigsVar", + "count": "[length(parameters('privateDnsZoneConfigs'))]", + "input": { + "name": "[coalesce(tryGet(parameters('privateDnsZoneConfigs')[copyIndex('privateDnsZoneConfigsVar')], 'name'), last(split(parameters('privateDnsZoneConfigs')[copyIndex('privateDnsZoneConfigsVar')].privateDnsZoneResourceId, '/')))]", + "properties": { + "privateDnsZoneId": "[parameters('privateDnsZoneConfigs')[copyIndex('privateDnsZoneConfigsVar')].privateDnsZoneResourceId]" + } + } + } + ] + }, "resources": { - "databaseAccount::cassandraKeyspace": { - "existing": true, - "type": "Microsoft.DocumentDB/databaseAccounts/cassandraKeyspaces", - "apiVersion": "2025-05-01-preview", - "name": "[format('{0}/{1}', parameters('databaseAccountName'), parameters('cassandraKeyspaceName'))]" - }, - "databaseAccount": { + "privateEndpoint": { "existing": true, - "type": "Microsoft.DocumentDB/databaseAccounts", - "apiVersion": "2025-05-01-preview", - "name": "[parameters('databaseAccountName')]" + "type": "Microsoft.Network/privateEndpoints", + "apiVersion": "2024-05-01", + "name": "[parameters('privateEndpointName')]" }, - "cassandraView": { - "type": "Microsoft.DocumentDB/databaseAccounts/cassandraKeyspaces/views", - "apiVersion": "2025-05-01-preview", - "name": "[format('{0}/{1}/{2}', parameters('databaseAccountName'), parameters('cassandraKeyspaceName'), parameters('name'))]", - "tags": "[parameters('tags')]", - "location": "[parameters('location')]", + "privateDnsZoneGroup": { + "type": "Microsoft.Network/privateEndpoints/privateDnsZoneGroups", + "apiVersion": "2024-05-01", + "name": "[format('{0}/{1}', parameters('privateEndpointName'), parameters('name'))]", "properties": { - "resource": { - "id": "[parameters('name')]", - "viewDefinition": "[parameters('viewDefinition')]" - }, - "options": "[if(contains(reference('databaseAccount').capabilities, createObject('name', 'EnableServerless')), createObject(), createObject('autoscaleSettings', if(and(equals(parameters('throughput'), null()), not(equals(parameters('autoscaleSettingsMaxThroughput'), null()))), createObject('maxThroughput', parameters('autoscaleSettingsMaxThroughput')), null()), 'throughput', parameters('throughput')))]" - }, - "dependsOn": [ - "databaseAccount" - ] + "privateDnsZoneConfigs": "[variables('privateDnsZoneConfigsVar')]" + } } }, "outputs": { "name": { "type": "string", "metadata": { - "description": "The name of the Cassandra view." + "description": "The name of the private endpoint DNS zone group." }, "value": "[parameters('name')]" }, "resourceId": { "type": "string", "metadata": { - "description": "The resource ID of the Cassandra view." + "description": "The resource ID of the private endpoint DNS zone group." }, - "value": "[resourceId('Microsoft.DocumentDB/databaseAccounts/cassandraKeyspaces/views', parameters('databaseAccountName'), parameters('cassandraKeyspaceName'), parameters('name'))]" + "value": "[resourceId('Microsoft.Network/privateEndpoints/privateDnsZoneGroups', parameters('privateEndpointName'), parameters('name'))]" }, "resourceGroupName": { "type": "string", "metadata": { - "description": "The name of the resource group the Cassandra view was created in." + "description": "The resource group the private endpoint DNS zone group was deployed into." }, "value": "[resourceGroup().name]" } @@ -29789,2483 +27132,6179 @@ } }, "dependsOn": [ - "cassandraKeyspace" + "privateEndpoint" ] } }, + "outputs": { + "resourceGroupName": { + "type": "string", + "metadata": { + "description": "The resource group the private endpoint was deployed into." + }, + "value": "[resourceGroup().name]" + }, + "resourceId": { + "type": "string", + "metadata": { + "description": "The resource ID of the private endpoint." + }, + "value": "[resourceId('Microsoft.Network/privateEndpoints', parameters('name'))]" + }, + "name": { + "type": "string", + "metadata": { + "description": "The name of the private endpoint." + }, + "value": "[parameters('name')]" + }, + "location": { + "type": "string", + "metadata": { + "description": "The location the resource was deployed into." + }, + "value": "[reference('privateEndpoint', '2024-05-01', 'full').location]" + }, + "customDnsConfigs": { + "type": "array", + "items": { + "$ref": "#/definitions/customDnsConfigType" + }, + "metadata": { + "description": "The custom DNS configurations of the private endpoint." + }, + "value": "[reference('privateEndpoint').customDnsConfigs]" + }, + "networkInterfaceResourceIds": { + "type": "array", + "items": { + "type": "string" + }, + "metadata": { + "description": "The resource IDs of the network interfaces associated with the private endpoint." + }, + "value": "[map(reference('privateEndpoint').networkInterfaces, lambda('nic', lambdaVariables('nic').id))]" + }, + "groupId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "The group Id for the private endpoint Group." + }, + "value": "[coalesce(tryGet(tryGet(tryGet(tryGet(reference('privateEndpoint'), 'manualPrivateLinkServiceConnections'), 0, 'properties'), 'groupIds'), 0), tryGet(tryGet(tryGet(tryGet(reference('privateEndpoint'), 'privateLinkServiceConnections'), 0, 'properties'), 'groupIds'), 0))]" + } + } + } + }, + "dependsOn": [ + "searchService" + ] + }, + "searchService_sharedPrivateLinkResources": { + "copy": { + "name": "searchService_sharedPrivateLinkResources", + "count": "[length(parameters('sharedPrivateLinkResources'))]", + "mode": "serial", + "batchSize": 1 + }, + "type": "Microsoft.Resources/deployments", + "apiVersion": "2022-09-01", + "name": "[format('{0}-searchService-SharedPrvLink-{1}', uniqueString(deployment().name, parameters('location')), copyIndex())]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "name": { + "value": "[coalesce(tryGet(parameters('sharedPrivateLinkResources')[copyIndex()], 'name'), format('spl-{0}-{1}-{2}', last(split(resourceId('Microsoft.Search/searchServices', parameters('name')), '/')), parameters('sharedPrivateLinkResources')[copyIndex()].groupId, copyIndex()))]" + }, + "searchServiceName": { + "value": "[parameters('name')]" + }, + "privateLinkResourceId": { + "value": "[parameters('sharedPrivateLinkResources')[copyIndex()].privateLinkResourceId]" + }, + "groupId": { + "value": "[parameters('sharedPrivateLinkResources')[copyIndex()].groupId]" + }, + "requestMessage": { + "value": "[parameters('sharedPrivateLinkResources')[copyIndex()].requestMessage]" + }, + "resourceRegion": { + "value": "[tryGet(parameters('sharedPrivateLinkResources')[copyIndex()], 'resourceRegion')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "languageVersion": "2.0", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.37.4.10188", + "templateHash": "557730297583881254" + }, + "name": "Search Services Private Link Resources", + "description": "This module deploys a Search Service Private Link Resource." + }, + "parameters": { + "searchServiceName": { + "type": "string", + "metadata": { + "description": "Conditional. The name of the parent searchServices. Required if the template is used in a standalone deployment." + } + }, + "name": { + "type": "string", + "metadata": { + "description": "Required. The name of the shared private link resource managed by the Azure Cognitive Search service within the specified resource group." + } + }, + "privateLinkResourceId": { + "type": "string", + "metadata": { + "description": "Required. The resource ID of the resource the shared private link resource is for." + } + }, + "groupId": { + "type": "string", + "metadata": { + "description": "Required. The group ID from the provider of resource the shared private link resource is for." + } + }, + "requestMessage": { + "type": "string", + "metadata": { + "description": "Required. The request message for requesting approval of the shared private link resource." + } + }, + "resourceRegion": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Can be used to specify the Azure Resource Manager location of the resource to which a shared private link is to be created. This is only required for those resources whose DNS configuration are regional (such as Azure Kubernetes Service)." + } + } + }, + "resources": { + "searchService": { + "existing": true, + "type": "Microsoft.Search/searchServices", + "apiVersion": "2025-02-01-preview", + "name": "[parameters('searchServiceName')]" + }, + "sharedPrivateLinkResource": { + "type": "Microsoft.Search/searchServices/sharedPrivateLinkResources", + "apiVersion": "2025-02-01-preview", + "name": "[format('{0}/{1}', parameters('searchServiceName'), parameters('name'))]", + "properties": { + "privateLinkResourceId": "[parameters('privateLinkResourceId')]", + "groupId": "[parameters('groupId')]", + "requestMessage": "[parameters('requestMessage')]", + "resourceRegion": "[parameters('resourceRegion')]" + } + } + }, "outputs": { "name": { "type": "string", "metadata": { - "description": "The name of the Cassandra keyspace." + "description": "The name of the shared private link resource." }, "value": "[parameters('name')]" }, "resourceId": { "type": "string", "metadata": { - "description": "The resource ID of the Cassandra keyspace." + "description": "The resource ID of the shared private link resource." }, - "value": "[resourceId('Microsoft.DocumentDB/databaseAccounts/cassandraKeyspaces', parameters('databaseAccountName'), parameters('name'))]" + "value": "[resourceId('Microsoft.Search/searchServices/sharedPrivateLinkResources', parameters('searchServiceName'), parameters('name'))]" }, "resourceGroupName": { "type": "string", "metadata": { - "description": "The name of the resource group the Cassandra keyspace was created in." + "description": "The name of the resource group the shared private link resource was created in." }, "value": "[resourceGroup().name]" } } } }, - "dependsOn": [ - "databaseAccount" - ] + "dependsOn": [ + "searchService" + ] + }, + "secretsExport": { + "condition": "[not(equals(parameters('secretsExportConfiguration'), null()))]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2022-09-01", + "name": "[format('{0}-secrets-kv', uniqueString(deployment().name, parameters('location')))]", + "subscriptionId": "[split(tryGet(parameters('secretsExportConfiguration'), 'keyVaultResourceId'), '/')[2]]", + "resourceGroup": "[split(tryGet(parameters('secretsExportConfiguration'), 'keyVaultResourceId'), '/')[4]]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "keyVaultName": { + "value": "[last(split(tryGet(parameters('secretsExportConfiguration'), 'keyVaultResourceId'), '/'))]" + }, + "secretsToSet": { + "value": "[union(createArray(), if(contains(parameters('secretsExportConfiguration'), 'primaryAdminKeyName'), createArray(createObject('name', tryGet(parameters('secretsExportConfiguration'), 'primaryAdminKeyName'), 'value', listAdminKeys('searchService', '2025-02-01-preview').primaryKey)), createArray()), if(contains(parameters('secretsExportConfiguration'), 'secondaryAdminKeyName'), createArray(createObject('name', tryGet(parameters('secretsExportConfiguration'), 'secondaryAdminKeyName'), 'value', listAdminKeys('searchService', '2025-02-01-preview').secondaryKey)), createArray()))]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "languageVersion": "2.0", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.37.4.10188", + "templateHash": "7634110751636246703" + } + }, + "definitions": { + "secretSetType": { + "type": "object", + "properties": { + "secretResourceId": { + "type": "string", + "metadata": { + "description": "The resourceId of the exported secret." + } + }, + "secretUri": { + "type": "string", + "metadata": { + "description": "The secret URI of the exported secret." + } + } + }, + "metadata": { + "__bicep_export!": true + } + }, + "secretToSetType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "metadata": { + "description": "Required. The name of the secret to set." + } + }, + "value": { + "type": "securestring", + "metadata": { + "description": "Required. The value of the secret to set." + } + } + } + } + }, + "parameters": { + "keyVaultName": { + "type": "string", + "metadata": { + "description": "Required. The name of the Key Vault to set the ecrets in." + } + }, + "secretsToSet": { + "type": "array", + "items": { + "$ref": "#/definitions/secretToSetType" + }, + "metadata": { + "description": "Required. The secrets to set in the Key Vault." + } + } + }, + "resources": { + "keyVault": { + "existing": true, + "type": "Microsoft.KeyVault/vaults", + "apiVersion": "2024-11-01", + "name": "[parameters('keyVaultName')]" + }, + "secrets": { + "copy": { + "name": "secrets", + "count": "[length(parameters('secretsToSet'))]" + }, + "type": "Microsoft.KeyVault/vaults/secrets", + "apiVersion": "2024-11-01", + "name": "[format('{0}/{1}', parameters('keyVaultName'), parameters('secretsToSet')[copyIndex()].name)]", + "properties": { + "value": "[parameters('secretsToSet')[copyIndex()].value]" + } + } + }, + "outputs": { + "secretsSet": { + "type": "array", + "items": { + "$ref": "#/definitions/secretSetType" + }, + "metadata": { + "description": "The references to the secrets exported to the provided Key Vault." + }, + "copy": { + "count": "[length(range(0, length(coalesce(parameters('secretsToSet'), createArray()))))]", + "input": { + "secretResourceId": "[resourceId('Microsoft.KeyVault/vaults/secrets', parameters('keyVaultName'), parameters('secretsToSet')[range(0, length(coalesce(parameters('secretsToSet'), createArray())))[copyIndex()]].name)]", + "secretUri": "[reference(format('secrets[{0}]', range(0, length(coalesce(parameters('secretsToSet'), createArray())))[copyIndex()])).secretUri]" + } + } + } + } + } + }, + "dependsOn": [ + "searchService" + ] + } + }, + "outputs": { + "name": { + "type": "string", + "metadata": { + "description": "The name of the search service." + }, + "value": "[parameters('name')]" + }, + "resourceId": { + "type": "string", + "metadata": { + "description": "The resource ID of the search service." + }, + "value": "[resourceId('Microsoft.Search/searchServices', parameters('name'))]" + }, + "resourceGroupName": { + "type": "string", + "metadata": { + "description": "The name of the resource group the search service was created in." + }, + "value": "[resourceGroup().name]" + }, + "systemAssignedMIPrincipalId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "The principal ID of the system assigned identity." + }, + "value": "[tryGet(tryGet(reference('searchService', '2025-02-01-preview', 'full'), 'identity'), 'principalId')]" + }, + "location": { + "type": "string", + "metadata": { + "description": "The location the resource was deployed into." + }, + "value": "[reference('searchService', '2025-02-01-preview', 'full').location]" + }, + "endpoint": { + "type": "string", + "metadata": { + "description": "The endpoint of the search service." + }, + "value": "[reference('searchService').endpoint]" + }, + "privateEndpoints": { + "type": "array", + "items": { + "$ref": "#/definitions/privateEndpointOutputType" + }, + "metadata": { + "description": "The private endpoints of the search service." + }, + "copy": { + "count": "[length(coalesce(parameters('privateEndpoints'), createArray()))]", + "input": { + "name": "[reference(format('searchService_privateEndpoints[{0}]', copyIndex())).outputs.name.value]", + "resourceId": "[reference(format('searchService_privateEndpoints[{0}]', copyIndex())).outputs.resourceId.value]", + "groupId": "[tryGet(tryGet(reference(format('searchService_privateEndpoints[{0}]', copyIndex())).outputs, 'groupId'), 'value')]", + "customDnsConfigs": "[reference(format('searchService_privateEndpoints[{0}]', copyIndex())).outputs.customDnsConfigs.value]", + "networkInterfaceResourceIds": "[reference(format('searchService_privateEndpoints[{0}]', copyIndex())).outputs.networkInterfaceResourceIds.value]" + } + } + }, + "exportedSecrets": { + "$ref": "#/definitions/secretsOutputType", + "metadata": { + "description": "A hashtable of references to the secrets exported to the provided Key Vault. The key of each reference is each secret's name." + }, + "value": "[if(not(equals(parameters('secretsExportConfiguration'), null())), toObject(reference('secretsExport').outputs.secretsSet.value, lambda('secret', last(split(lambdaVariables('secret').secretResourceId, '/'))), lambda('secret', lambdaVariables('secret'))), createObject())]" + }, + "primaryKey": { + "type": "securestring", + "metadata": { + "description": "The primary admin API key of the search service." + }, + "value": "[listAdminKeys('searchService', '2025-02-01-preview').primaryKey]" + }, + "secondaryKey": { + "type": "securestring", + "metadata": { + "description": "The secondaryKey admin API key of the search service." + }, + "value": "[listAdminKeys('searchService', '2025-02-01-preview').secondaryKey]" + } + } + } + }, + "dependsOn": [ + "logAnalyticsWorkspace", + "userAssignedIdentity" + ] + }, + "storageAccount": { + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "[take(format('avm.res.storage.storage-account.{0}', variables('storageAccountName')), 64)]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "name": { + "value": "[variables('storageAccountName')]" + }, + "location": { + "value": "[variables('solutionLocation')]" + }, + "skuName": "[if(parameters('enableRedundancy'), createObject('value', 'Standard_ZRS'), createObject('value', 'Standard_LRS'))]", + "managedIdentities": { + "value": { + "systemAssigned": true + } + }, + "minimumTlsVersion": { + "value": "TLS1_2" + }, + "enableTelemetry": { + "value": "[parameters('enableTelemetry')]" + }, + "tags": { + "value": "[parameters('tags')]" + }, + "accessTier": { + "value": "Hot" + }, + "supportsHttpsTrafficOnly": { + "value": true + }, + "blobServices": { + "value": { + "containerDeleteRetentionPolicyEnabled": true, + "containerDeleteRetentionPolicyDays": 7, + "deleteRetentionPolicyEnabled": true, + "deleteRetentionPolicyDays": 7, + "containers": [ + { + "name": "[variables('productImagesContainer')]", + "publicAccess": "None" + }, + { + "name": "[variables('generatedImagesContainer')]", + "publicAccess": "None" + }, + { + "name": "[variables('dataContainer')]", + "publicAccess": "None" + } + ] + } + }, + "roleAssignments": { + "value": [ + { + "principalId": "[reference('userAssignedIdentity').outputs.principalId.value]", + "roleDefinitionIdOrName": "Storage Blob Data Contributor", + "principalType": "ServicePrincipal" + } + ] + }, + "networkAcls": { + "value": { + "bypass": "AzureServices", + "defaultAction": "[if(parameters('enablePrivateNetworking'), 'Deny', 'Allow')]" + } + }, + "allowBlobPublicAccess": { + "value": false + }, + "publicNetworkAccess": "[if(parameters('enablePrivateNetworking'), createObject('value', 'Disabled'), createObject('value', 'Enabled'))]", + "privateEndpoints": "[if(parameters('enablePrivateNetworking'), createObject('value', createArray(createObject('service', 'blob', 'subnetResourceId', reference('virtualNetwork').outputs.pepsSubnetResourceId.value, 'privateDnsZoneGroup', createObject('privateDnsZoneGroupConfigs', createArray(createObject('privateDnsZoneResourceId', reference(format('avmPrivateDnsZones[{0}]', variables('dnsZoneIndex').storageBlob)).outputs.resourceId.value)))))), createObject('value', null()))]", + "diagnosticSettings": "[if(parameters('enableMonitoring'), createObject('value', createArray(createObject('workspaceResourceId', if(variables('useExistingLogAnalytics'), parameters('existingLogAnalyticsWorkspaceId'), if(parameters('enableMonitoring'), reference('logAnalyticsWorkspace').outputs.resourceId.value, ''))))), createObject('value', null()))]" + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "languageVersion": "2.0", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.39.26.7824", + "templateHash": "8444048237705693390" + }, + "name": "Storage Accounts", + "description": "This module deploys a Storage Account." + }, + "definitions": { + "privateEndpointOutputType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "metadata": { + "description": "The name of the private endpoint." + } + }, + "resourceId": { + "type": "string", + "metadata": { + "description": "The resource ID of the private endpoint." + } + }, + "groupId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "The group Id for the private endpoint Group." + } + }, + "customDnsConfigs": { + "type": "array", + "items": { + "type": "object", + "properties": { + "fqdn": { + "type": "string", + "nullable": true, + "metadata": { + "description": "FQDN that resolves to private endpoint IP address." + } + }, + "ipAddresses": { + "type": "array", + "items": { + "type": "string" + }, + "metadata": { + "description": "A list of private IP addresses of the private endpoint." + } + } + } + }, + "metadata": { + "description": "The custom DNS configurations of the private endpoint." + } + }, + "networkInterfaceResourceIds": { + "type": "array", + "items": { + "type": "string" + }, + "metadata": { + "description": "The IDs of the network interfaces associated with the private endpoint." + } + } + }, + "metadata": { + "__bicep_export!": true, + "description": "The type for the private endpoints output." + } + }, + "networkAclsType": { + "type": "object", + "properties": { + "resourceAccessRules": { + "type": "array", + "items": { + "type": "object", + "properties": { + "tenantId": { + "type": "string", + "metadata": { + "description": "Required. The ID of the tenant in which the resource resides in." + } + }, + "resourceId": { + "type": "string", + "metadata": { + "description": "Required. The resource ID of the target service. Can also contain a wildcard, if multiple services e.g. in a resource group should be included." + } + } + } + }, + "nullable": true, + "metadata": { + "description": "Optional. Sets the resource access rules. Array entries must consist of \"tenantId\" and \"resourceId\" fields only." + } + }, + "bypass": { + "type": "string", + "allowedValues": [ + "AzureServices", + "AzureServices, Logging", + "AzureServices, Logging, Metrics", + "AzureServices, Metrics", + "Logging", + "Logging, Metrics", + "Metrics", + "None" + ], + "nullable": true, + "metadata": { + "description": "Optional. Specifies whether traffic is bypassed for Logging/Metrics/AzureServices. Possible values are any combination of Logging,Metrics,AzureServices (For example, \"Logging, Metrics\"), or None to bypass none of those traffics." + } + }, + "virtualNetworkRules": { + "type": "array", + "nullable": true, + "metadata": { + "description": "Optional. Sets the virtual network rules." + } + }, + "ipRules": { + "type": "array", + "nullable": true, + "metadata": { + "description": "Optional. Sets the IP ACL rules." + } + }, + "defaultAction": { + "type": "string", + "allowedValues": [ + "Allow", + "Deny" + ], + "nullable": true, + "metadata": { + "description": "Optional. Specifies the default action of allow or deny when no other rules match." + } + } + }, + "metadata": { + "__bicep_export!": true, + "description": "The type for the network configuration." + } + }, + "secretsExportConfigurationType": { + "type": "object", + "properties": { + "keyVaultResourceId": { + "type": "string", + "metadata": { + "description": "Required. The key vault name where to store the keys and connection strings generated by the modules." + } + }, + "accessKey1Name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The accessKey1 secret name to create." + } + }, + "connectionString1Name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The connectionString1 secret name to create." + } + }, + "accessKey2Name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The accessKey2 secret name to create." + } + }, + "connectionString2Name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The connectionString2 secret name to create." + } + } + }, + "metadata": { + "__bicep_export!": true, + "description": "The type of the exported secrets." + } }, - "databaseAccount_privateEndpoints": { - "copy": { - "name": "databaseAccount_privateEndpoints", - "count": "[length(coalesce(parameters('privateEndpoints'), createArray()))]" - }, - "type": "Microsoft.Resources/deployments", - "apiVersion": "2025-04-01", - "name": "[format('{0}-dbAccount-PrivateEndpoint-{1}', uniqueString(deployment().name, parameters('location')), copyIndex())]", - "subscriptionId": "[split(coalesce(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'resourceGroupResourceId'), resourceGroup().id), '/')[2]]", - "resourceGroup": "[split(coalesce(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'resourceGroupResourceId'), resourceGroup().id), '/')[4]]", + "localUserType": { + "type": "object", "properties": { - "expressionEvaluationOptions": { - "scope": "inner" + "name": { + "type": "string", + "metadata": { + "description": "Required. The name of the local user used for SFTP Authentication." + } }, - "mode": "Incremental", - "parameters": { - "name": { - "value": "[coalesce(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'name'), format('pep-{0}-{1}-{2}', last(split(resourceId('Microsoft.DocumentDB/databaseAccounts', parameters('name')), '/')), coalesce(parameters('privateEndpoints'), createArray())[copyIndex()].service, copyIndex()))]" - }, - "privateLinkServiceConnections": "[if(not(equals(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'isManualConnection'), true())), createObject('value', createArray(createObject('name', coalesce(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'privateLinkServiceConnectionName'), format('{0}-{1}-{2}', last(split(resourceId('Microsoft.DocumentDB/databaseAccounts', parameters('name')), '/')), coalesce(parameters('privateEndpoints'), createArray())[copyIndex()].service, copyIndex())), 'properties', createObject('privateLinkServiceId', resourceId('Microsoft.DocumentDB/databaseAccounts', parameters('name')), 'groupIds', createArray(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()].service))))), createObject('value', null()))]", - "manualPrivateLinkServiceConnections": "[if(equals(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'isManualConnection'), true()), createObject('value', createArray(createObject('name', coalesce(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'privateLinkServiceConnectionName'), format('{0}-{1}-{2}', last(split(resourceId('Microsoft.DocumentDB/databaseAccounts', parameters('name')), '/')), coalesce(parameters('privateEndpoints'), createArray())[copyIndex()].service, copyIndex())), 'properties', createObject('privateLinkServiceId', resourceId('Microsoft.DocumentDB/databaseAccounts', parameters('name')), 'groupIds', createArray(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()].service), 'requestMessage', coalesce(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'manualConnectionRequestMessage'), 'Manual approval required.'))))), createObject('value', null()))]", - "subnetResourceId": { - "value": "[coalesce(parameters('privateEndpoints'), createArray())[copyIndex()].subnetResourceId]" - }, - "enableTelemetry": { - "value": "[variables('enableReferencedModulesTelemetry')]" - }, - "location": { - "value": "[coalesce(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'location'), reference(split(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()].subnetResourceId, '/subnets/')[0], '2020-06-01', 'Full').location)]" - }, - "lock": { - "value": "[coalesce(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'lock'), parameters('lock'))]" - }, - "privateDnsZoneGroup": { - "value": "[tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'privateDnsZoneGroup')]" - }, - "roleAssignments": { - "value": "[tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'roleAssignments')]" - }, - "tags": { - "value": "[coalesce(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'tags'), parameters('tags'))]" - }, - "customDnsConfigs": { - "value": "[tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'customDnsConfigs')]" - }, - "ipConfigurations": { - "value": "[tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'ipConfigurations')]" + "hasSharedKey": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. Indicates whether shared key exists. Set it to false to remove existing shared key." + } + }, + "hasSshKey": { + "type": "bool", + "metadata": { + "description": "Required. Indicates whether SSH key exists. Set it to false to remove existing SSH key." + } + }, + "hasSshPassword": { + "type": "bool", + "metadata": { + "description": "Required. Indicates whether SSH password exists. Set it to false to remove existing SSH password." + } + }, + "homeDirectory": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The local user home directory." + } + }, + "permissionScopes": { + "type": "array", + "items": { + "$ref": "#/definitions/permissionScopeType" }, - "applicationSecurityGroupResourceIds": { - "value": "[tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'applicationSecurityGroupResourceIds')]" + "metadata": { + "description": "Required. The permission scopes of the local user." + } + }, + "sshAuthorizedKeys": { + "type": "array", + "items": { + "$ref": "#/definitions/sshAuthorizedKeyType" }, - "customNetworkInterfaceName": { - "value": "[tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'customNetworkInterfaceName')]" + "nullable": true, + "metadata": { + "description": "Optional. The local user SSH authorized keys for SFTP." + } + } + }, + "metadata": { + "__bicep_export!": true, + "description": "The type of a local user." + } + }, + "blobServiceType": { + "type": "object", + "properties": { + "automaticSnapshotPolicyEnabled": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. Automatic Snapshot is enabled if set to true." } }, - "template": { - "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", - "languageVersion": "2.0", - "contentVersion": "1.0.0.0", + "changeFeedEnabled": { + "type": "bool", + "nullable": true, "metadata": { - "_generator": { - "name": "bicep", - "version": "0.38.5.1644", - "templateHash": "16604612898799598358" - }, - "name": "Private Endpoints", - "description": "This module deploys a Private Endpoint." - }, - "definitions": { - "privateDnsZoneGroupType": { - "type": "object", - "properties": { - "name": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The name of the Private DNS Zone Group." - } - }, - "privateDnsZoneGroupConfigs": { - "type": "array", - "items": { - "$ref": "#/definitions/privateDnsZoneGroupConfigType" - }, - "metadata": { - "description": "Required. The private DNS zone groups to associate the private endpoint. A DNS zone group can support up to 5 DNS zones." - } - } - }, - "metadata": { - "__bicep_export!": true, - "description": "The type of a private dns zone group." - } - }, - "lockType": { - "type": "object", - "properties": { - "name": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. Specify the name of lock." - } - }, - "kind": { - "type": "string", - "allowedValues": [ - "CanNotDelete", - "None", - "ReadOnly" - ], - "nullable": true, - "metadata": { - "description": "Optional. Specify the type of lock." - } - }, - "notes": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. Specify the notes of the lock." - } - } - }, - "metadata": { - "description": "An AVM-aligned type for a lock.", - "__bicep_imported_from!": { - "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.1" - } - } - }, - "privateDnsZoneGroupConfigType": { - "type": "object", - "properties": { - "name": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The name of the private DNS zone group config." - } - }, - "privateDnsZoneResourceId": { - "type": "string", - "metadata": { - "description": "Required. The resource id of the private DNS zone." - } - } - }, - "metadata": { - "description": "The type of a private DNS zone group configuration.", - "__bicep_imported_from!": { - "sourceTemplate": "private-dns-zone-group/main.bicep" - } - } - }, - "roleAssignmentType": { - "type": "object", - "properties": { - "name": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The name (as GUID) of the role assignment. If not provided, a GUID will be generated." - } - }, - "roleDefinitionIdOrName": { - "type": "string", - "metadata": { - "description": "Required. The role to assign. You can provide either the display name of the role definition, the role definition GUID, or its fully qualified ID in the following format: '/providers/Microsoft.Authorization/roleDefinitions/c2f4ef07-c644-48eb-af81-4b1b4947fb11'." - } - }, - "principalId": { - "type": "string", - "metadata": { - "description": "Required. The principal ID of the principal (user/group/identity) to assign the role to." - } - }, - "principalType": { - "type": "string", - "allowedValues": [ - "Device", - "ForeignGroup", - "Group", - "ServicePrincipal", - "User" - ], - "nullable": true, - "metadata": { - "description": "Optional. The principal type of the assigned principal ID." - } - }, - "description": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The description of the role assignment." - } - }, - "condition": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The conditions on the role assignment. This limits the resources it can be assigned to. e.g.: @Resource[Microsoft.Storage/storageAccounts/blobServices/containers:ContainerName] StringEqualsIgnoreCase \"foo_storage_container\"." - } - }, - "conditionVersion": { - "type": "string", - "allowedValues": [ - "2.0" - ], - "nullable": true, - "metadata": { - "description": "Optional. Version of the condition." - } - }, - "delegatedManagedIdentityResourceId": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The Resource Id of the delegated managed identity resource." - } - } - }, - "metadata": { - "description": "An AVM-aligned type for a role assignment.", - "__bicep_imported_from!": { - "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.1" - } - } - } + "description": "Optional. The blob service properties for change feed events. Indicates whether change feed event logging is enabled for the Blob service." + } + }, + "changeFeedRetentionInDays": { + "type": "int", + "nullable": true, + "minValue": 1, + "maxValue": 146000, + "metadata": { + "description": "Optional. Indicates whether change feed event logging is enabled for the Blob service. Indicates the duration of changeFeed retention in days. If left blank, it indicates an infinite retention of the change feed." + } + }, + "containerDeleteRetentionPolicyEnabled": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. The blob service properties for container soft delete. Indicates whether DeleteRetentionPolicy is enabled." + } + }, + "containerDeleteRetentionPolicyDays": { + "type": "int", + "nullable": true, + "minValue": 1, + "maxValue": 365, + "metadata": { + "description": "Optional. Indicates the number of days that the deleted item should be retained." + } + }, + "containerDeleteRetentionPolicyAllowPermanentDelete": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. This property when set to true allows deletion of the soft deleted blob versions and snapshots. This property cannot be used with blob restore policy. This property only applies to blob service and does not apply to containers or file share." + } + }, + "corsRules": { + "type": "array", + "items": { + "$ref": "#/definitions/blobCorsRuleType" }, - "parameters": { - "name": { - "type": "string", - "metadata": { - "description": "Required. Name of the private endpoint resource to create." - } - }, - "subnetResourceId": { - "type": "string", - "metadata": { - "description": "Required. Resource ID of the subnet where the endpoint needs to be created." - } - }, - "applicationSecurityGroupResourceIds": { - "type": "array", - "items": { - "type": "string" - }, - "nullable": true, - "metadata": { - "description": "Optional. Application security groups in which the private endpoint IP configuration is included." - } - }, - "customNetworkInterfaceName": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The custom name of the network interface attached to the private endpoint." - } - }, - "ipConfigurations": { - "type": "array", - "metadata": { - "__bicep_resource_derived_type!": { - "source": "Microsoft.Network/privateEndpoints@2024-01-01#properties/properties/properties/ipConfigurations" - }, - "description": "Optional. A list of IP configurations of the private endpoint. This will be used to map to the First Party Service endpoints." - }, - "nullable": true - }, - "privateDnsZoneGroup": { - "$ref": "#/definitions/privateDnsZoneGroupType", - "nullable": true, - "metadata": { - "description": "Optional. The private DNS zone group to configure for the private endpoint." - } - }, - "location": { - "type": "string", - "defaultValue": "[resourceGroup().location]", - "metadata": { - "description": "Optional. Location for all Resources." - } - }, - "lock": { - "$ref": "#/definitions/lockType", - "nullable": true, - "metadata": { - "description": "Optional. The lock settings of the service." - } - }, - "roleAssignments": { - "type": "array", - "items": { - "$ref": "#/definitions/roleAssignmentType" - }, - "nullable": true, - "metadata": { - "description": "Optional. Array of role assignments to create." - } - }, - "tags": { - "type": "object", - "metadata": { - "__bicep_resource_derived_type!": { - "source": "Microsoft.Network/privateEndpoints@2024-01-01#properties/tags" - }, - "description": "Optional. Tags to be applied on all resources/resource groups in this deployment." - }, - "nullable": true - }, - "customDnsConfigs": { - "type": "array", - "metadata": { - "__bicep_resource_derived_type!": { - "source": "Microsoft.Network/privateEndpoints@2024-01-01#properties/properties/properties/customDnsConfigs" - }, - "description": "Optional. Custom DNS configurations." - }, - "nullable": true - }, - "manualPrivateLinkServiceConnections": { - "type": "array", - "metadata": { - "__bicep_resource_derived_type!": { - "source": "Microsoft.Network/privateEndpoints@2024-01-01#properties/properties/properties/manualPrivateLinkServiceConnections" - }, - "description": "Conditional. A grouping of information about the connection to the remote resource. Used when the network admin does not have access to approve connections to the remote resource. Required if `privateLinkServiceConnections` is empty." - }, - "nullable": true - }, - "privateLinkServiceConnections": { - "type": "array", - "metadata": { - "__bicep_resource_derived_type!": { - "source": "Microsoft.Network/privateEndpoints@2024-01-01#properties/properties/properties/privateLinkServiceConnections" - }, - "description": "Conditional. A grouping of information about the connection to the remote resource. Required if `manualPrivateLinkServiceConnections` is empty." - }, - "nullable": true - }, - "enableTelemetry": { - "type": "bool", - "defaultValue": true, - "metadata": { - "description": "Optional. Enable/Disable usage telemetry for module." - } - } + "nullable": true, + "metadata": { + "description": "Optional. The List of CORS rules. You can include up to five CorsRule elements in the request." + } + }, + "defaultServiceVersion": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Indicates the default version to use for requests to the Blob service if an incoming request's version is not specified. Possible values include version 2008-10-27 and all more recent versions." + } + }, + "deleteRetentionPolicyEnabled": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. The blob service properties for blob soft delete." + } + }, + "deleteRetentionPolicyDays": { + "type": "int", + "nullable": true, + "minValue": 1, + "maxValue": 365, + "metadata": { + "description": "Optional. Indicates the number of days that the deleted blob should be retained." + } + }, + "deleteRetentionPolicyAllowPermanentDelete": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. This property when set to true allows deletion of the soft deleted blob versions and snapshots. This property cannot be used with blob restore policy. This property only applies to blob service and does not apply to containers or file share." + } + }, + "isVersioningEnabled": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. Use versioning to automatically maintain previous versions of your blobs. Cannot be enabled for ADLS Gen2 storage accounts." + } + }, + "versionDeletePolicyDays": { + "type": "int", + "nullable": true, + "metadata": { + "description": "Optional. Number of days to keep a version before deleting. If set, a lifecycle management policy will be created to handle deleting previous versions." + } + }, + "lastAccessTimeTrackingPolicyEnabled": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. The blob service property to configure last access time based tracking policy. When set to true last access time based tracking is enabled." + } + }, + "restorePolicyEnabled": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. The blob service properties for blob restore policy. If point-in-time restore is enabled, then versioning, change feed, and blob soft delete must also be enabled." + } + }, + "restorePolicyDays": { + "type": "int", + "nullable": true, + "minValue": 1, + "metadata": { + "description": "Optional. How long this blob can be restored. It should be less than DeleteRetentionPolicy days." + } + }, + "containers": { + "type": "array", + "items": { + "$ref": "#/definitions/containerType" }, - "variables": { - "copy": [ - { - "name": "formattedRoleAssignments", - "count": "[length(coalesce(parameters('roleAssignments'), createArray()))]", - "input": "[union(coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')], createObject('roleDefinitionId', coalesce(tryGet(variables('builtInRoleNames'), coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName), if(contains(coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName, '/providers/Microsoft.Authorization/roleDefinitions/'), coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName, subscriptionResourceId('Microsoft.Authorization/roleDefinitions', coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName)))))]" - } - ], - "builtInRoleNames": { - "Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'b24988ac-6180-42a0-ab88-20f7382dd24c')]", - "DNS Resolver Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '0f2ebee7-ffd4-4fc0-b3b7-664099fdad5d')]", - "DNS Zone Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'befefa01-2a29-4197-83a8-272ff33ce314')]", - "Domain Services Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'eeaeda52-9324-47f6-8069-5d5bade478b2')]", - "Domain Services Reader": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '361898ef-9ed1-48c2-849c-a832951106bb')]", - "Network Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '4d97b98b-1d4f-4787-a291-c67834d212e7')]", - "Owner": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '8e3af657-a8ff-443c-a75c-2fe8c4bcb635')]", - "Private DNS Zone Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'b12aa53e-6015-4669-85d0-8515ebb3ae7f')]", - "Reader": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'acdd72a7-3385-48ef-bd42-f606fba81ae7')]", - "Role Based Access Control Administrator": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'f58310d9-a9f6-439a-9e8d-f62e7b41a168')]" - } + "nullable": true, + "metadata": { + "description": "Optional. Blob containers to create." + } + }, + "diagnosticSettings": { + "type": "array", + "items": { + "$ref": "#/definitions/diagnosticSettingFullType" }, - "resources": { - "avmTelemetry": { - "condition": "[parameters('enableTelemetry')]", - "type": "Microsoft.Resources/deployments", - "apiVersion": "2025-04-01", - "name": "[format('46d3xbcp.res.network-privateendpoint.{0}.{1}', replace('0.11.1', '.', '-'), substring(uniqueString(deployment().name, parameters('location')), 0, 4))]", - "properties": { - "mode": "Incremental", - "template": { - "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", - "contentVersion": "1.0.0.0", - "resources": [], - "outputs": { - "telemetry": { - "type": "String", - "value": "For more information, see https://aka.ms/avm/TelemetryInfo" - } - } - } - } - }, - "privateEndpoint": { - "type": "Microsoft.Network/privateEndpoints", - "apiVersion": "2024-10-01", - "name": "[parameters('name')]", - "location": "[parameters('location')]", - "tags": "[parameters('tags')]", - "properties": { - "copy": [ - { - "name": "applicationSecurityGroups", - "count": "[length(coalesce(parameters('applicationSecurityGroupResourceIds'), createArray()))]", - "input": { - "id": "[coalesce(parameters('applicationSecurityGroupResourceIds'), createArray())[copyIndex('applicationSecurityGroups')]]" - } - } - ], - "customDnsConfigs": "[coalesce(parameters('customDnsConfigs'), createArray())]", - "customNetworkInterfaceName": "[coalesce(parameters('customNetworkInterfaceName'), '')]", - "ipConfigurations": "[coalesce(parameters('ipConfigurations'), createArray())]", - "manualPrivateLinkServiceConnections": "[coalesce(parameters('manualPrivateLinkServiceConnections'), createArray())]", - "privateLinkServiceConnections": "[coalesce(parameters('privateLinkServiceConnections'), createArray())]", - "subnet": { - "id": "[parameters('subnetResourceId')]" - } - } - }, - "privateEndpoint_lock": { - "condition": "[and(not(empty(coalesce(parameters('lock'), createObject()))), not(equals(tryGet(parameters('lock'), 'kind'), 'None')))]", - "type": "Microsoft.Authorization/locks", - "apiVersion": "2020-05-01", - "scope": "[format('Microsoft.Network/privateEndpoints/{0}', parameters('name'))]", - "name": "[coalesce(tryGet(parameters('lock'), 'name'), format('lock-{0}', parameters('name')))]", - "properties": { - "level": "[coalesce(tryGet(parameters('lock'), 'kind'), '')]", - "notes": "[coalesce(tryGet(parameters('lock'), 'notes'), if(equals(tryGet(parameters('lock'), 'kind'), 'CanNotDelete'), 'Cannot delete resource or child resources.', 'Cannot delete or modify the resource or child resources.'))]" - }, - "dependsOn": [ - "privateEndpoint" - ] + "nullable": true, + "metadata": { + "description": "Optional. The diagnostic settings of the service." + } + } + }, + "metadata": { + "__bicep_export!": true, + "description": "The type of a blob service." + } + }, + "fileServiceType": { + "type": "object", + "properties": { + "protocolSettings": { + "type": "object", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.Storage/storageAccounts/fileServices@2024-01-01#properties/properties/properties/protocolSettings" }, - "privateEndpoint_roleAssignments": { - "copy": { - "name": "privateEndpoint_roleAssignments", - "count": "[length(coalesce(variables('formattedRoleAssignments'), createArray()))]" - }, - "type": "Microsoft.Authorization/roleAssignments", - "apiVersion": "2022-04-01", - "scope": "[format('Microsoft.Network/privateEndpoints/{0}', parameters('name'))]", - "name": "[coalesce(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'name'), guid(resourceId('Microsoft.Network/privateEndpoints', parameters('name')), coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].principalId, coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].roleDefinitionId))]", - "properties": { - "roleDefinitionId": "[coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].roleDefinitionId]", - "principalId": "[coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].principalId]", - "description": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'description')]", - "principalType": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'principalType')]", - "condition": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'condition')]", - "conditionVersion": "[if(not(empty(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'condition'))), coalesce(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'conditionVersion'), '2.0'), null())]", - "delegatedManagedIdentityResourceId": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'delegatedManagedIdentityResourceId')]" - }, - "dependsOn": [ - "privateEndpoint" - ] + "description": "Optional. Protocol settings for file service." + }, + "nullable": true + }, + "shareDeleteRetentionPolicy": { + "type": "object", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.Storage/storageAccounts/fileServices@2024-01-01#properties/properties/properties/shareDeleteRetentionPolicy" }, - "privateEndpoint_privateDnsZoneGroup": { - "condition": "[not(empty(parameters('privateDnsZoneGroup')))]", - "type": "Microsoft.Resources/deployments", - "apiVersion": "2025-04-01", - "name": "[format('{0}-PrivateEndpoint-PrivateDnsZoneGroup', uniqueString(deployment().name))]", - "properties": { - "expressionEvaluationOptions": { - "scope": "inner" - }, - "mode": "Incremental", - "parameters": { - "name": { - "value": "[tryGet(parameters('privateDnsZoneGroup'), 'name')]" - }, - "privateEndpointName": { - "value": "[parameters('name')]" - }, - "privateDnsZoneConfigs": { - "value": "[parameters('privateDnsZoneGroup').privateDnsZoneGroupConfigs]" - } - }, - "template": { - "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", - "languageVersion": "2.0", - "contentVersion": "1.0.0.0", - "metadata": { - "_generator": { - "name": "bicep", - "version": "0.38.5.1644", - "templateHash": "24141742673128945" - }, - "name": "Private Endpoint Private DNS Zone Groups", - "description": "This module deploys a Private Endpoint Private DNS Zone Group." - }, - "definitions": { - "privateDnsZoneGroupConfigType": { - "type": "object", - "properties": { - "name": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The name of the private DNS zone group config." - } - }, - "privateDnsZoneResourceId": { - "type": "string", - "metadata": { - "description": "Required. The resource id of the private DNS zone." - } - } - }, - "metadata": { - "__bicep_export!": true, - "description": "The type of a private DNS zone group configuration." - } - } - }, - "parameters": { - "privateEndpointName": { - "type": "string", - "metadata": { - "description": "Conditional. The name of the parent private endpoint. Required if the template is used in a standalone deployment." - } - }, - "privateDnsZoneConfigs": { - "type": "array", - "items": { - "$ref": "#/definitions/privateDnsZoneGroupConfigType" - }, - "minLength": 1, - "maxLength": 5, - "metadata": { - "description": "Required. Array of private DNS zone configurations of the private DNS zone group. A DNS zone group can support up to 5 DNS zones." - } - }, - "name": { - "type": "string", - "defaultValue": "default", - "metadata": { - "description": "Optional. The name of the private DNS zone group." - } - } - }, - "resources": { - "privateEndpoint": { - "existing": true, - "type": "Microsoft.Network/privateEndpoints", - "apiVersion": "2024-10-01", - "name": "[parameters('privateEndpointName')]" - }, - "privateDnsZoneGroup": { - "type": "Microsoft.Network/privateEndpoints/privateDnsZoneGroups", - "apiVersion": "2024-10-01", - "name": "[format('{0}/{1}', parameters('privateEndpointName'), parameters('name'))]", - "properties": { - "copy": [ - { - "name": "privateDnsZoneConfigs", - "count": "[length(parameters('privateDnsZoneConfigs'))]", - "input": { - "name": "[coalesce(tryGet(parameters('privateDnsZoneConfigs')[copyIndex('privateDnsZoneConfigs')], 'name'), last(split(parameters('privateDnsZoneConfigs')[copyIndex('privateDnsZoneConfigs')].privateDnsZoneResourceId, '/')))]", - "properties": { - "privateDnsZoneId": "[parameters('privateDnsZoneConfigs')[copyIndex('privateDnsZoneConfigs')].privateDnsZoneResourceId]" - } - } - } - ] - } - } - }, - "outputs": { - "name": { - "type": "string", - "metadata": { - "description": "The name of the private endpoint DNS zone group." - }, - "value": "[parameters('name')]" - }, - "resourceId": { - "type": "string", - "metadata": { - "description": "The resource ID of the private endpoint DNS zone group." - }, - "value": "[resourceId('Microsoft.Network/privateEndpoints/privateDnsZoneGroups', parameters('privateEndpointName'), parameters('name'))]" - }, - "resourceGroupName": { - "type": "string", - "metadata": { - "description": "The resource group the private endpoint DNS zone group was deployed into." - }, - "value": "[resourceGroup().name]" - } - } - } - }, - "dependsOn": [ - "privateEndpoint" - ] - } + "description": "Optional. The service properties for soft delete." + }, + "nullable": true + }, + "shares": { + "type": "array", + "items": { + "$ref": "#/definitions/fileShareType" + }, + "nullable": true, + "metadata": { + "description": "Optional. File shares to create." + } + }, + "corsRules": { + "type": "array", + "items": { + "$ref": "#/definitions/fileCorsRuleType" + }, + "nullable": true, + "metadata": { + "description": "Optional. The List of CORS rules. You can include up to five CorsRule elements in the request." + } + }, + "diagnosticSettings": { + "type": "array", + "items": { + "$ref": "#/definitions/diagnosticSettingFullType" + }, + "nullable": true, + "metadata": { + "description": "Optional. The diagnostic settings of the service." + } + } + }, + "metadata": { + "__bicep_export!": true, + "description": "The type of a file service." + } + }, + "queueServiceType": { + "type": "object", + "properties": { + "queues": { + "type": "array", + "items": { + "$ref": "#/definitions/queueType" + }, + "nullable": true, + "metadata": { + "description": "Optional. Queues to create." + } + }, + "corsRules": { + "type": "array", + "items": { + "$ref": "#/definitions/queueCorsRuleType" + }, + "nullable": true, + "metadata": { + "description": "Optional. The List of CORS rules. You can include up to five CorsRule elements in the request." + } + }, + "diagnosticSettings": { + "type": "array", + "items": { + "$ref": "#/definitions/diagnosticSettingFullType" + }, + "nullable": true, + "metadata": { + "description": "Optional. The diagnostic settings of the service." + } + } + }, + "metadata": { + "__bicep_export!": true, + "description": "The type of a queue service." + } + }, + "tableServiceType": { + "type": "object", + "properties": { + "tables": { + "type": "array", + "items": { + "$ref": "#/definitions/tableType" + }, + "nullable": true, + "metadata": { + "description": "Optional. Tables to create." + } + }, + "corsRules": { + "type": "array", + "items": { + "$ref": "#/definitions/tableCorsRuleType" + }, + "nullable": true, + "metadata": { + "description": "Optional. The List of CORS rules. You can include up to five CorsRule elements in the request." + } + }, + "diagnosticSettings": { + "type": "array", + "items": { + "$ref": "#/definitions/diagnosticSettingFullType" + }, + "nullable": true, + "metadata": { + "description": "Optional. The diagnostic settings of the service." + } + } + }, + "metadata": { + "__bicep_export!": true, + "description": "The type of a table service." + } + }, + "objectReplicationPolicyType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The name of the object replication policy. If not provided, a GUID will be generated." + } + }, + "destinationStorageAccountResourceId": { + "type": "string", + "metadata": { + "description": "Required. The resource ID of the destination storage account." + } + }, + "enableMetrics": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. Indicates whether metrics are enabled for the object replication policy." + } + }, + "rules": { + "type": "array", + "items": { + "$ref": "#/definitions/objectReplicationPolicyRuleType" + }, + "metadata": { + "description": "Required. The storage account object replication rules." + } + } + }, + "metadata": { + "__bicep_export!": true, + "description": "The type of an object replication policy." + } + }, + "_1.immutabilityPolicyType": { + "type": "object", + "properties": { + "immutabilityPeriodSinceCreationInDays": { + "type": "int", + "nullable": true, + "metadata": { + "description": "Optional. The immutability period for the blobs in the container since the policy creation, in days." + } + }, + "allowProtectedAppendWrites": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. This property can only be changed for unlocked time-based retention policies. When enabled, new blocks can be written to an append blob while maintaining immutability protection and compliance. Only new blocks can be added and any existing blocks cannot be modified or deleted. This property cannot be changed with ExtendImmutabilityPolicy API." + } + }, + "allowProtectedAppendWritesAll": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. This property can only be changed for unlocked time-based retention policies. When enabled, new blocks can be written to both \"Append and Block Blobs\" while maintaining immutability protection and compliance. Only new blocks can be added and any existing blocks cannot be modified or deleted. This property cannot be changed with ExtendImmutabilityPolicy API. The \"allowProtectedAppendWrites\" and \"allowProtectedAppendWritesAll\" properties are mutually exclusive." + } + } + }, + "metadata": { + "description": "The type for an immutability policy.", + "__bicep_imported_from!": { + "sourceTemplate": "blob-service/container/main.bicep" + } + } + }, + "_2.privateEndpointCustomDnsConfigType": { + "type": "object", + "properties": { + "fqdn": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. FQDN that resolves to private endpoint IP address." + } + }, + "ipAddresses": { + "type": "array", + "items": { + "type": "string" }, - "outputs": { - "resourceGroupName": { - "type": "string", - "metadata": { - "description": "The resource group the private endpoint was deployed into." - }, - "value": "[resourceGroup().name]" - }, - "resourceId": { + "metadata": { + "description": "Required. A list of private IP addresses of the private endpoint." + } + } + }, + "metadata": { + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.1" + } + } + }, + "_2.privateEndpointIpConfigurationType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "metadata": { + "description": "Required. The name of the resource that is unique within a resource group." + } + }, + "properties": { + "type": "object", + "properties": { + "groupId": { "type": "string", "metadata": { - "description": "The resource ID of the private endpoint." - }, - "value": "[resourceId('Microsoft.Network/privateEndpoints', parameters('name'))]" + "description": "Required. The ID of a group obtained from the remote resource that this private endpoint should connect to." + } }, - "name": { + "memberName": { "type": "string", "metadata": { - "description": "The name of the private endpoint." - }, - "value": "[parameters('name')]" + "description": "Required. The member name of a group obtained from the remote resource that this private endpoint should connect to." + } }, - "location": { + "privateIPAddress": { "type": "string", "metadata": { - "description": "The location the resource was deployed into." + "description": "Required. A private IP address obtained from the private endpoint's subnet." + } + } + }, + "metadata": { + "description": "Required. Properties of private endpoint IP configurations." + } + } + }, + "metadata": { + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.1" + } + } + }, + "_2.privateEndpointPrivateDnsZoneGroupType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The name of the Private DNS Zone Group." + } + }, + "privateDnsZoneGroupConfigs": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The name of the private DNS Zone Group config." + } }, - "value": "[reference('privateEndpoint', '2024-10-01', 'full').location]" + "privateDnsZoneResourceId": { + "type": "string", + "metadata": { + "description": "Required. The resource id of the private DNS zone." + } + } + } + }, + "metadata": { + "description": "Required. The private DNS Zone Groups to associate the Private Endpoint. A DNS Zone Group can support up to 5 DNS zones." + } + } + }, + "metadata": { + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.1" + } + } + }, + "_2.secretSetOutputType": { + "type": "object", + "properties": { + "secretResourceId": { + "type": "string", + "metadata": { + "description": "The resourceId of the exported secret." + } + }, + "secretUri": { + "type": "string", + "metadata": { + "description": "The secret URI of the exported secret." + } + }, + "secretUriWithVersion": { + "type": "string", + "metadata": { + "description": "The secret URI with version of the exported secret." + } + } + }, + "metadata": { + "description": "An AVM-aligned type for the output of the secret set via the secrets export feature.", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.1" + } + } + }, + "blobCorsRuleType": { + "type": "object", + "properties": { + "allowedHeaders": { + "type": "array", + "items": { + "type": "string" + }, + "metadata": { + "description": "Required. A list of headers allowed to be part of the cross-origin request." + } + }, + "allowedMethods": { + "type": "array", + "allowedValues": [ + "CONNECT", + "DELETE", + "GET", + "HEAD", + "MERGE", + "OPTIONS", + "PATCH", + "POST", + "PUT", + "TRACE" + ], + "metadata": { + "description": "Required. A list of HTTP methods that are allowed to be executed by the origin." + } + }, + "allowedOrigins": { + "type": "array", + "items": { + "type": "string" + }, + "metadata": { + "description": "Required. A list of origin domains that will be allowed via CORS, or \"*\" to allow all domains." + } + }, + "exposedHeaders": { + "type": "array", + "items": { + "type": "string" + }, + "metadata": { + "description": "Required. A list of response headers to expose to CORS clients." + } + }, + "maxAgeInSeconds": { + "type": "int", + "metadata": { + "description": "Required. The number of seconds that the client/browser should cache a preflight response." + } + } + }, + "metadata": { + "description": "The type for a cors rule.", + "__bicep_imported_from!": { + "sourceTemplate": "blob-service/main.bicep", + "originalIdentifier": "corsRuleType" + } + } + }, + "containerType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "metadata": { + "description": "Required. The name of the Storage Container to deploy." + } + }, + "defaultEncryptionScope": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Default the container to use specified encryption scope for all writes." + } + }, + "denyEncryptionScopeOverride": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. Block override of encryption scope from the container default." + } + }, + "enableNfsV3AllSquash": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. Enable NFSv3 all squash on blob container." + } + }, + "enableNfsV3RootSquash": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. Enable NFSv3 root squash on blob container." + } + }, + "immutableStorageWithVersioningEnabled": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. This is an immutable property, when set to true it enables object level immutability at the container level. The property is immutable and can only be set to true at the container creation time. Existing containers must undergo a migration process." + } + }, + "immutabilityPolicy": { + "$ref": "#/definitions/_1.immutabilityPolicyType", + "nullable": true, + "metadata": { + "description": "Optional. Configure immutability policy." + } + }, + "metadata": { + "type": "object", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.Storage/storageAccounts/blobServices/containers@2024-01-01#properties/properties/properties/metadata" }, - "customDnsConfigs": { - "type": "array", - "metadata": { - "__bicep_resource_derived_type!": { - "source": "Microsoft.Network/privateEndpoints@2024-01-01#properties/properties/properties/customDnsConfigs", - "output": true - }, - "description": "The custom DNS configurations of the private endpoint." + "description": "Optional. A name-value pair to associate with the container as metadata." + }, + "nullable": true + }, + "publicAccess": { + "type": "string", + "allowedValues": [ + "Blob", + "Container", + "None" + ], + "nullable": true, + "metadata": { + "description": "Optional. Specifies whether data in the container may be accessed publicly and the level of access." + } + }, + "roleAssignments": { + "type": "array", + "items": { + "$ref": "#/definitions/roleAssignmentType" + }, + "nullable": true, + "metadata": { + "description": "Optional. Array of role assignments to create." + } + } + }, + "metadata": { + "description": "The type of a storage container.", + "__bicep_imported_from!": { + "sourceTemplate": "blob-service/main.bicep" + } + } + }, + "customerManagedKeyWithAutoRotateType": { + "type": "object", + "properties": { + "keyVaultResourceId": { + "type": "string", + "metadata": { + "description": "Required. The resource ID of a key vault to reference a customer managed key for encryption from." + } + }, + "keyName": { + "type": "string", + "metadata": { + "description": "Required. The name of the customer managed key to use for encryption." + } + }, + "keyVersion": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The version of the customer managed key to reference for encryption. If not provided, using version as per 'autoRotationEnabled' setting." + } + }, + "autoRotationEnabled": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. Enable or disable auto-rotating to the latest key version. Default is `true`. If set to `false`, the latest key version at the time of the deployment is used." + } + }, + "userAssignedIdentityResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. User assigned identity to use when fetching the customer managed key. Required if no system assigned identity is available for use." + } + } + }, + "metadata": { + "description": "An AVM-aligned type for a customer-managed key. To be used if the resource type supports auto-rotation of the customer-managed key.", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.1" + } + } + }, + "diagnosticSettingFullType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The name of the diagnostic setting." + } + }, + "logCategoriesAndGroups": { + "type": "array", + "items": { + "type": "object", + "properties": { + "category": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Name of a Diagnostic Log category for a resource type this setting is applied to. Set the specific logs to collect here." + } }, - "value": "[reference('privateEndpoint').customDnsConfigs]" - }, - "networkInterfaceResourceIds": { - "type": "array", - "items": { - "type": "string" + "categoryGroup": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Name of a Diagnostic Log category group for a resource type this setting is applied to. Set to `allLogs` to collect all logs." + } }, - "metadata": { - "description": "The resource IDs of the network interfaces associated with the private endpoint." + "enabled": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. Enable or disable the category explicitly. Default is `true`." + } + } + } + }, + "nullable": true, + "metadata": { + "description": "Optional. The name of logs that will be streamed. \"allLogs\" includes all possible logs for the resource. Set to `[]` to disable log collection." + } + }, + "metricCategories": { + "type": "array", + "items": { + "type": "object", + "properties": { + "category": { + "type": "string", + "metadata": { + "description": "Required. Name of a Diagnostic Metric category for a resource type this setting is applied to. Set to `AllMetrics` to collect all metrics." + } }, - "value": "[map(reference('privateEndpoint').networkInterfaces, lambda('nic', lambdaVariables('nic').id))]" - }, - "groupId": { - "type": "string", - "nullable": true, - "metadata": { - "description": "The group Id for the private endpoint Group." + "enabled": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. Enable or disable the category explicitly. Default is `true`." + } + } + } + }, + "nullable": true, + "metadata": { + "description": "Optional. The name of metrics that will be streamed. \"allMetrics\" includes all possible metrics for the resource. Set to `[]` to disable metric collection." + } + }, + "logAnalyticsDestinationType": { + "type": "string", + "allowedValues": [ + "AzureDiagnostics", + "Dedicated" + ], + "nullable": true, + "metadata": { + "description": "Optional. A string indicating whether the export to Log Analytics should use the default destination type, i.e. AzureDiagnostics, or use a destination type." + } + }, + "workspaceResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Resource ID of the diagnostic log analytics workspace. For security reasons, it is recommended to set diagnostic settings to send data to either storage account, log analytics workspace or event hub." + } + }, + "storageAccountResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Resource ID of the diagnostic storage account. For security reasons, it is recommended to set diagnostic settings to send data to either storage account, log analytics workspace or event hub." + } + }, + "eventHubAuthorizationRuleResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Resource ID of the diagnostic event hub authorization rule for the Event Hubs namespace in which the event hub should be created or streamed to." + } + }, + "eventHubName": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Name of the diagnostic event hub within the namespace to which logs are streamed. Without this, an event hub is created for each log category. For security reasons, it is recommended to set diagnostic settings to send data to either storage account, log analytics workspace or event hub." + } + }, + "marketplacePartnerResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The full ARM resource ID of the Marketplace resource to which you would like to send Diagnostic Logs." + } + } + }, + "metadata": { + "description": "An AVM-aligned type for a diagnostic setting. To be used if both logs & metrics are supported by the resource provider.", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.1" + } + } + }, + "diagnosticSettingMetricsOnlyType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The name of diagnostic setting." + } + }, + "metricCategories": { + "type": "array", + "items": { + "type": "object", + "properties": { + "category": { + "type": "string", + "metadata": { + "description": "Required. Name of a Diagnostic Metric category for a resource type this setting is applied to. Set to `AllMetrics` to collect all metrics." + } }, - "value": "[coalesce(tryGet(tryGet(tryGet(tryGet(reference('privateEndpoint'), 'manualPrivateLinkServiceConnections'), 0, 'properties'), 'groupIds'), 0), tryGet(tryGet(tryGet(tryGet(reference('privateEndpoint'), 'privateLinkServiceConnections'), 0, 'properties'), 'groupIds'), 0))]" + "enabled": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. Enable or disable the category explicitly. Default is `true`." + } + } } + }, + "nullable": true, + "metadata": { + "description": "Optional. The name of metrics that will be streamed. \"allMetrics\" includes all possible metrics for the resource. Set to `[]` to disable metric collection." + } + }, + "logAnalyticsDestinationType": { + "type": "string", + "allowedValues": [ + "AzureDiagnostics", + "Dedicated" + ], + "nullable": true, + "metadata": { + "description": "Optional. A string indicating whether the export to Log Analytics should use the default destination type, i.e. AzureDiagnostics, or use a destination type." + } + }, + "workspaceResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Resource ID of the diagnostic log analytics workspace. For security reasons, it is recommended to set diagnostic settings to send data to either storage account, log analytics workspace or event hub." + } + }, + "storageAccountResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Resource ID of the diagnostic storage account. For security reasons, it is recommended to set diagnostic settings to send data to either storage account, log analytics workspace or event hub." + } + }, + "eventHubAuthorizationRuleResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Resource ID of the diagnostic event hub authorization rule for the Event Hubs namespace in which the event hub should be created or streamed to." + } + }, + "eventHubName": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Name of the diagnostic event hub within the namespace to which logs are streamed. Without this, an event hub is created for each log category. For security reasons, it is recommended to set diagnostic settings to send data to either storage account, log analytics workspace or event hub." + } + }, + "marketplacePartnerResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The full ARM resource ID of the Marketplace resource to which you would like to send Diagnostic Logs." } } }, - "dependsOn": [ - "databaseAccount" - ] - } - }, - "outputs": { - "name": { - "type": "string", "metadata": { - "description": "The name of the database account." - }, - "value": "[parameters('name')]" + "description": "An AVM-aligned type for a diagnostic setting. To be used if only metrics are supported by the resource provider.", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.1" + } + } }, - "resourceId": { - "type": "string", - "metadata": { - "description": "The resource ID of the database account." + "fileCorsRuleType": { + "type": "object", + "properties": { + "allowedHeaders": { + "type": "array", + "items": { + "type": "string" + }, + "metadata": { + "description": "Required. A list of headers allowed to be part of the cross-origin request." + } + }, + "allowedMethods": { + "type": "array", + "allowedValues": [ + "CONNECT", + "DELETE", + "GET", + "HEAD", + "MERGE", + "OPTIONS", + "PATCH", + "POST", + "PUT", + "TRACE" + ], + "metadata": { + "description": "Required. A list of HTTP methods that are allowed to be executed by the origin." + } + }, + "allowedOrigins": { + "type": "array", + "items": { + "type": "string" + }, + "metadata": { + "description": "Required. A list of origin domains that will be allowed via CORS, or \"*\" to allow all domains." + } + }, + "exposedHeaders": { + "type": "array", + "items": { + "type": "string" + }, + "metadata": { + "description": "Required. A list of response headers to expose to CORS clients." + } + }, + "maxAgeInSeconds": { + "type": "int", + "metadata": { + "description": "Required. The number of seconds that the client/browser should cache a preflight response." + } + } }, - "value": "[resourceId('Microsoft.DocumentDB/databaseAccounts', parameters('name'))]" - }, - "resourceGroupName": { - "type": "string", "metadata": { - "description": "The name of the resource group the database account was created in." - }, - "value": "[resourceGroup().name]" + "description": "The type for a cors rule.", + "__bicep_imported_from!": { + "sourceTemplate": "file-service/main.bicep", + "originalIdentifier": "corsRuleType" + } + } }, - "systemAssignedMIPrincipalId": { - "type": "string", - "nullable": true, - "metadata": { - "description": "The principal ID of the system assigned identity." + "fileShareType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "metadata": { + "description": "Required. The name of the file share." + } + }, + "accessTier": { + "type": "string", + "allowedValues": [ + "Cool", + "Hot", + "Premium", + "TransactionOptimized" + ], + "nullable": true, + "metadata": { + "description": "Optional. Access tier for specific share. Required if the Storage Account kind is set to FileStorage (should be set to \"Premium\"). GpV2 account can choose between TransactionOptimized (default), Hot, and Cool." + } + }, + "enabledProtocols": { + "type": "string", + "allowedValues": [ + "NFS", + "SMB" + ], + "nullable": true, + "metadata": { + "description": "Optional. The authentication protocol that is used for the file share. Can only be specified when creating a share." + } + }, + "rootSquash": { + "type": "string", + "allowedValues": [ + "AllSquash", + "NoRootSquash", + "RootSquash" + ], + "nullable": true, + "metadata": { + "description": "Optional. Permissions for NFS file shares are enforced by the client OS rather than the Azure Files service. Toggling the root squash behavior reduces the rights of the root user for NFS shares." + } + }, + "shareQuota": { + "type": "int", + "nullable": true, + "metadata": { + "description": "Optional. The maximum size of the share, in gigabytes. Must be greater than 0, and less than or equal to 5120 (5TB). For Large File Shares, the maximum size is 102400 (100TB)." + } + }, + "roleAssignments": { + "type": "array", + "items": { + "$ref": "#/definitions/roleAssignmentType" + }, + "nullable": true, + "metadata": { + "description": "Optional. Array of role assignments to create." + } + } }, - "value": "[tryGet(tryGet(reference('databaseAccount', '2025-04-15', 'full'), 'identity'), 'principalId')]" - }, - "location": { - "type": "string", "metadata": { - "description": "The location the resource was deployed into." - }, - "value": "[reference('databaseAccount', '2025-04-15', 'full').location]" + "description": "The type for a file share.", + "__bicep_imported_from!": { + "sourceTemplate": "file-service/main.bicep" + } + } }, - "endpoint": { - "type": "string", - "metadata": { - "description": "The endpoint of the database account." + "lockType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Specify the name of lock." + } + }, + "kind": { + "type": "string", + "allowedValues": [ + "CanNotDelete", + "None", + "ReadOnly" + ], + "nullable": true, + "metadata": { + "description": "Optional. Specify the type of lock." + } + }, + "notes": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Specify the notes of the lock." + } + } }, - "value": "[reference('databaseAccount').documentEndpoint]" + "metadata": { + "description": "An AVM-aligned type for a lock.", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.1" + } + } }, - "privateEndpoints": { - "type": "array", - "items": { - "$ref": "#/definitions/privateEndpointOutputType" + "managedIdentityAllType": { + "type": "object", + "properties": { + "systemAssigned": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. Enables system assigned managed identity on the resource." + } + }, + "userAssignedResourceIds": { + "type": "array", + "items": { + "type": "string" + }, + "nullable": true, + "metadata": { + "description": "Optional. The resource ID(s) to assign to the resource. Required if a user assigned identity is used for encryption." + } + } }, "metadata": { - "description": "The private endpoints of the database account." - }, - "copy": { - "count": "[length(coalesce(parameters('privateEndpoints'), createArray()))]", - "input": { - "name": "[reference(format('databaseAccount_privateEndpoints[{0}]', copyIndex())).outputs.name.value]", - "resourceId": "[reference(format('databaseAccount_privateEndpoints[{0}]', copyIndex())).outputs.resourceId.value]", - "groupId": "[tryGet(tryGet(reference(format('databaseAccount_privateEndpoints[{0}]', copyIndex())).outputs, 'groupId'), 'value')]", - "customDnsConfigs": "[reference(format('databaseAccount_privateEndpoints[{0}]', copyIndex())).outputs.customDnsConfigs.value]", - "networkInterfaceResourceIds": "[reference(format('databaseAccount_privateEndpoints[{0}]', copyIndex())).outputs.networkInterfaceResourceIds.value]" + "description": "An AVM-aligned type for a managed identity configuration. To be used if both a system-assigned & user-assigned identities are supported by the resource provider.", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.1" } } }, - "primaryReadWriteKey": { - "type": "securestring", - "metadata": { - "description": "The primary read-write key." + "objectReplicationPolicyRuleType": { + "type": "object", + "properties": { + "ruleId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The ID of the rule. Auto-generated on destination account. Required for source account." + } + }, + "containerName": { + "type": "string", + "metadata": { + "description": "Required. The name of the source container." + } + }, + "destinationContainerName": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The name of the destination container. If not provided, the same name as the source container will be used." + } + }, + "filters": { + "type": "object", + "properties": { + "prefixMatch": { + "type": "array", + "items": { + "type": "string" + }, + "nullable": true, + "metadata": { + "description": "Optional. The prefix to match for the replication policy rule." + } + }, + "minCreationTime": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The minimum creation time to match for the replication policy rule." + } + } + }, + "nullable": true, + "metadata": { + "description": "Optional. The filters for the object replication policy rule." + } + } }, - "value": "[listKeys('databaseAccount', '2025-04-15').primaryMasterKey]" - }, - "primaryReadOnlyKey": { - "type": "securestring", "metadata": { - "description": "The primary read-only key." - }, - "value": "[listKeys('databaseAccount', '2025-04-15').primaryReadonlyMasterKey]" + "description": "The type of an object replication policy rule.", + "__bicep_imported_from!": { + "sourceTemplate": "object-replication-policy/policy/main.bicep" + } + } }, - "primaryReadWriteConnectionString": { - "type": "securestring", - "metadata": { - "description": "The primary read-write connection string." + "permissionScopeType": { + "type": "object", + "properties": { + "permissions": { + "type": "string", + "metadata": { + "description": "Required. The permissions for the local user. Possible values include: Read (r), Write (w), Delete (d), List (l), and Create (c)." + } + }, + "resourceName": { + "type": "string", + "metadata": { + "description": "Required. The name of resource, normally the container name or the file share name, used by the local user." + } + }, + "service": { + "type": "string", + "metadata": { + "description": "Required. The service used by the local user, e.g. blob, file." + } + } }, - "value": "[listConnectionStrings('databaseAccount', '2025-04-15').connectionStrings[0].connectionString]" - }, - "primaryReadOnlyConnectionString": { - "type": "securestring", "metadata": { - "description": "The primary read-only connection string." - }, - "value": "[listConnectionStrings('databaseAccount', '2025-04-15').connectionStrings[2].connectionString]" + "__bicep_imported_from!": { + "sourceTemplate": "local-user/main.bicep" + } + } }, - "secondaryReadWriteKey": { - "type": "securestring", - "metadata": { - "description": "The secondary read-write key." + "privateEndpointMultiServiceType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The name of the private endpoint." + } + }, + "location": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The location to deploy the private endpoint to." + } + }, + "privateLinkServiceConnectionName": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The name of the private link connection to create." + } + }, + "service": { + "type": "string", + "metadata": { + "description": "Required. The subresource to deploy the private endpoint for. For example \"blob\", \"table\", \"queue\" or \"file\" for a Storage Account's Private Endpoints." + } + }, + "subnetResourceId": { + "type": "string", + "metadata": { + "description": "Required. Resource ID of the subnet where the endpoint needs to be created." + } + }, + "resourceGroupResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The resource ID of the Resource Group the Private Endpoint will be created in. If not specified, the Resource Group of the provided Virtual Network Subnet is used." + } + }, + "privateDnsZoneGroup": { + "$ref": "#/definitions/_2.privateEndpointPrivateDnsZoneGroupType", + "nullable": true, + "metadata": { + "description": "Optional. The private DNS zone group to configure for the private endpoint." + } + }, + "isManualConnection": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. If Manual Private Link Connection is required." + } + }, + "manualConnectionRequestMessage": { + "type": "string", + "nullable": true, + "maxLength": 140, + "metadata": { + "description": "Optional. A message passed to the owner of the remote resource with the manual connection request." + } + }, + "customDnsConfigs": { + "type": "array", + "items": { + "$ref": "#/definitions/_2.privateEndpointCustomDnsConfigType" + }, + "nullable": true, + "metadata": { + "description": "Optional. Custom DNS configurations." + } + }, + "ipConfigurations": { + "type": "array", + "items": { + "$ref": "#/definitions/_2.privateEndpointIpConfigurationType" + }, + "nullable": true, + "metadata": { + "description": "Optional. A list of IP configurations of the private endpoint. This will be used to map to the First Party Service endpoints." + } + }, + "applicationSecurityGroupResourceIds": { + "type": "array", + "items": { + "type": "string" + }, + "nullable": true, + "metadata": { + "description": "Optional. Application security groups in which the private endpoint IP configuration is included." + } + }, + "customNetworkInterfaceName": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The custom name of the network interface attached to the private endpoint." + } + }, + "lock": { + "$ref": "#/definitions/lockType", + "nullable": true, + "metadata": { + "description": "Optional. Specify the type of lock." + } + }, + "roleAssignments": { + "type": "array", + "items": { + "$ref": "#/definitions/roleAssignmentType" + }, + "nullable": true, + "metadata": { + "description": "Optional. Array of role assignments to create." + } + }, + "tags": { + "type": "object", + "nullable": true, + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.Network/privateEndpoints@2024-07-01#properties/tags" + }, + "description": "Optional. Tags to be applied on all resources/resource groups in this deployment." + } + }, + "enableTelemetry": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. Enable/Disable usage telemetry for module." + } + } }, - "value": "[listKeys('databaseAccount', '2025-04-15').secondaryMasterKey]" - }, - "secondaryReadOnlyKey": { - "type": "securestring", "metadata": { - "description": "The secondary read-only key." - }, - "value": "[listKeys('databaseAccount', '2025-04-15').secondaryReadonlyMasterKey]" + "description": "An AVM-aligned type for a private endpoint. To be used if the private endpoint's default service / groupId can NOT be assumed (i.e., for services that have more than one subresource, like Storage Account with Blob (blob, table, queue, file, ...).", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.1" + } + } }, - "secondaryReadWriteConnectionString": { - "type": "securestring", - "metadata": { - "description": "The secondary read-write connection string." + "queueCorsRuleType": { + "type": "object", + "properties": { + "allowedHeaders": { + "type": "array", + "items": { + "type": "string" + }, + "metadata": { + "description": "Required. A list of headers allowed to be part of the cross-origin request." + } + }, + "allowedMethods": { + "type": "array", + "allowedValues": [ + "CONNECT", + "DELETE", + "GET", + "HEAD", + "MERGE", + "OPTIONS", + "PATCH", + "POST", + "PUT", + "TRACE" + ], + "metadata": { + "description": "Required. A list of HTTP methods that are allowed to be executed by the origin." + } + }, + "allowedOrigins": { + "type": "array", + "items": { + "type": "string" + }, + "metadata": { + "description": "Required. A list of origin domains that will be allowed via CORS, or \"*\" to allow all domains." + } + }, + "exposedHeaders": { + "type": "array", + "items": { + "type": "string" + }, + "metadata": { + "description": "Required. A list of response headers to expose to CORS clients." + } + }, + "maxAgeInSeconds": { + "type": "int", + "metadata": { + "description": "Required. The number of seconds that the client/browser should cache a preflight response." + } + } }, - "value": "[listConnectionStrings('databaseAccount', '2025-04-15').connectionStrings[1].connectionString]" - }, - "secondaryReadOnlyConnectionString": { - "type": "securestring", "metadata": { - "description": "The secondary read-only connection string." - }, - "value": "[listConnectionStrings('databaseAccount', '2025-04-15').connectionStrings[3].connectionString]" - } - } - } - }, - "dependsOn": [ - "[format('avmPrivateDnsZones[{0}]', variables('dnsZoneIndex').cosmosDB)]", - "logAnalyticsWorkspace", - "userAssignedIdentity", - "virtualNetwork" - ] - }, - "webServerFarm": { - "type": "Microsoft.Resources/deployments", - "apiVersion": "2025-04-01", - "name": "[take(format('avm.res.web.serverfarm.{0}', variables('webServerFarmResourceName')), 64)]", - "resourceGroup": "[resourceGroup().name]", - "properties": { - "expressionEvaluationOptions": { - "scope": "inner" - }, - "mode": "Incremental", - "parameters": { - "name": { - "value": "[variables('webServerFarmResourceName')]" - }, - "tags": { - "value": "[parameters('tags')]" - }, - "enableTelemetry": { - "value": "[parameters('enableTelemetry')]" - }, - "location": { - "value": "[variables('solutionLocation')]" - }, - "reserved": { - "value": true - }, - "kind": { - "value": "linux" - }, - "diagnosticSettings": "[if(parameters('enableMonitoring'), createObject('value', createArray(createObject('workspaceResourceId', if(variables('useExistingLogAnalytics'), parameters('existingLogAnalyticsWorkspaceId'), if(parameters('enableMonitoring'), reference('logAnalyticsWorkspace').outputs.resourceId.value, ''))))), createObject('value', null()))]", - "skuName": "[if(or(parameters('enableScalability'), parameters('enableRedundancy')), createObject('value', 'P1v3'), createObject('value', 'B1'))]", - "skuCapacity": { - "value": 1 - }, - "zoneRedundant": "[if(parameters('enableRedundancy'), createObject('value', true()), createObject('value', false()))]" - }, - "template": { - "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", - "languageVersion": "2.0", - "contentVersion": "1.0.0.0", - "metadata": { - "_generator": { - "name": "bicep", - "version": "0.36.177.2456", - "templateHash": "16945786131371363466" + "description": "The type for a cors rule.", + "__bicep_imported_from!": { + "sourceTemplate": "queue-service/main.bicep", + "originalIdentifier": "corsRuleType" + } + } }, - "name": "App Service Plan", - "description": "This module deploys an App Service Plan." - }, - "definitions": { - "diagnosticSettingMetricsOnlyType": { + "queueType": { "type": "object", "properties": { "name": { "type": "string", - "nullable": true, "metadata": { - "description": "Optional. The name of diagnostic setting." + "description": "Required. The name of the queue." } }, - "metricCategories": { + "metadata": { + "type": "object", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.Storage/storageAccounts/queueServices/queues@2024-01-01#properties/properties/properties/metadata" + }, + "description": "Optional. Metadata to set on the queue." + }, + "nullable": true + }, + "roleAssignments": { "type": "array", "items": { - "type": "object", - "properties": { - "category": { - "type": "string", - "metadata": { - "description": "Required. Name of a Diagnostic Metric category for a resource type this setting is applied to. Set to `AllMetrics` to collect all metrics." - } - }, - "enabled": { - "type": "bool", - "nullable": true, - "metadata": { - "description": "Optional. Enable or disable the category explicitly. Default is `true`." - } - } - } + "$ref": "#/definitions/roleAssignmentType" }, "nullable": true, "metadata": { - "description": "Optional. The name of metrics that will be streamed. \"allMetrics\" includes all possible metrics for the resource. Set to `[]` to disable metric collection." + "description": "Optional. Array of role assignments to create." } - }, - "logAnalyticsDestinationType": { + } + }, + "metadata": { + "description": "The type for a queue.", + "__bicep_imported_from!": { + "sourceTemplate": "queue-service/main.bicep" + } + } + }, + "roleAssignmentType": { + "type": "object", + "properties": { + "name": { "type": "string", - "allowedValues": [ - "AzureDiagnostics", - "Dedicated" - ], "nullable": true, "metadata": { - "description": "Optional. A string indicating whether the export to Log Analytics should use the default destination type, i.e. AzureDiagnostics, or use a destination type." + "description": "Optional. The name (as GUID) of the role assignment. If not provided, a GUID will be generated." } }, - "workspaceResourceId": { + "roleDefinitionIdOrName": { + "type": "string", + "metadata": { + "description": "Required. The role to assign. You can provide either the display name of the role definition, the role definition GUID, or its fully qualified ID in the following format: '/providers/Microsoft.Authorization/roleDefinitions/c2f4ef07-c644-48eb-af81-4b1b4947fb11'." + } + }, + "principalId": { + "type": "string", + "metadata": { + "description": "Required. The principal ID of the principal (user/group/identity) to assign the role to." + } + }, + "principalType": { "type": "string", + "allowedValues": [ + "Device", + "ForeignGroup", + "Group", + "ServicePrincipal", + "User" + ], "nullable": true, "metadata": { - "description": "Optional. Resource ID of the diagnostic log analytics workspace. For security reasons, it is recommended to set diagnostic settings to send data to either storage account, log analytics workspace or event hub." + "description": "Optional. The principal type of the assigned principal ID." } }, - "storageAccountResourceId": { + "description": { "type": "string", "nullable": true, "metadata": { - "description": "Optional. Resource ID of the diagnostic storage account. For security reasons, it is recommended to set diagnostic settings to send data to either storage account, log analytics workspace or event hub." + "description": "Optional. The description of the role assignment." } }, - "eventHubAuthorizationRuleResourceId": { + "condition": { "type": "string", "nullable": true, "metadata": { - "description": "Optional. Resource ID of the diagnostic event hub authorization rule for the Event Hubs namespace in which the event hub should be created or streamed to." + "description": "Optional. The conditions on the role assignment. This limits the resources it can be assigned to. e.g.: @Resource[Microsoft.Storage/storageAccounts/blobServices/containers:ContainerName] StringEqualsIgnoreCase \"foo_storage_container\"." } }, - "eventHubName": { + "conditionVersion": { "type": "string", + "allowedValues": [ + "2.0" + ], "nullable": true, "metadata": { - "description": "Optional. Name of the diagnostic event hub within the namespace to which logs are streamed. Without this, an event hub is created for each log category. For security reasons, it is recommended to set diagnostic settings to send data to either storage account, log analytics workspace or event hub." + "description": "Optional. Version of the condition." } }, - "marketplacePartnerResourceId": { + "delegatedManagedIdentityResourceId": { "type": "string", "nullable": true, "metadata": { - "description": "Optional. The full ARM resource ID of the Marketplace resource to which you would like to send Diagnostic Logs." + "description": "Optional. The Resource Id of the delegated managed identity resource." } } }, "metadata": { - "description": "An AVM-aligned type for a diagnostic setting. To be used if only metrics are supported by the resource provider.", + "description": "An AVM-aligned type for a role assignment.", "__bicep_imported_from!": { - "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.0" + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.1" } } }, - "lockType": { + "secretsOutputType": { + "type": "object", + "properties": {}, + "additionalProperties": { + "$ref": "#/definitions/_2.secretSetOutputType", + "metadata": { + "description": "An exported secret's references." + } + }, + "metadata": { + "description": "A map of the exported secrets", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.1" + } + } + }, + "sshAuthorizedKeyType": { "type": "object", "properties": { - "name": { + "description": { "type": "string", "nullable": true, "metadata": { - "description": "Optional. Specify the name of lock." + "description": "Optional. Description used to store the function/usage of the key." } }, - "kind": { - "type": "string", + "key": { + "type": "securestring", + "metadata": { + "description": "Required. SSH public key base64 encoded. The format should be: '{keyType} {keyData}', e.g. ssh-rsa AAAABBBB." + } + } + }, + "metadata": { + "__bicep_imported_from!": { + "sourceTemplate": "local-user/main.bicep" + } + } + }, + "tableCorsRuleType": { + "type": "object", + "properties": { + "allowedHeaders": { + "type": "array", + "items": { + "type": "string" + }, + "metadata": { + "description": "Required. A list of headers allowed to be part of the cross-origin request." + } + }, + "allowedMethods": { + "type": "array", "allowedValues": [ - "CanNotDelete", - "None", - "ReadOnly" + "CONNECT", + "DELETE", + "GET", + "HEAD", + "MERGE", + "OPTIONS", + "PATCH", + "POST", + "PUT", + "TRACE" ], - "nullable": true, "metadata": { - "description": "Optional. Specify the type of lock." + "description": "Required. A list of HTTP methods that are allowed to be executed by the origin." } }, - "notes": { + "allowedOrigins": { + "type": "array", + "items": { + "type": "string" + }, + "metadata": { + "description": "Required. A list of origin domains that will be allowed via CORS, or \"*\" to allow all domains." + } + }, + "exposedHeaders": { + "type": "array", + "items": { + "type": "string" + }, + "metadata": { + "description": "Required. A list of response headers to expose to CORS clients." + } + }, + "maxAgeInSeconds": { + "type": "int", + "metadata": { + "description": "Required. The number of seconds that the client/browser should cache a preflight response." + } + } + }, + "metadata": { + "description": "The type for a cors rule.", + "__bicep_imported_from!": { + "sourceTemplate": "table-service/main.bicep", + "originalIdentifier": "corsRuleType" + } + } + }, + "tableType": { + "type": "object", + "properties": { + "name": { "type": "string", + "metadata": { + "description": "Required. The name of the table." + } + }, + "roleAssignments": { + "type": "array", + "items": { + "$ref": "#/definitions/roleAssignmentType" + }, "nullable": true, "metadata": { - "description": "Optional. Specify the notes of the lock." + "description": "Optional. Array of role assignments to create." } } }, "metadata": { - "description": "An AVM-aligned type for a lock.", - "__bicep_imported_from!": { - "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.0" - } + "description": "The type for a table.", + "__bicep_imported_from!": { + "sourceTemplate": "table-service/main.bicep" + } + } + } + }, + "parameters": { + "name": { + "type": "string", + "maxLength": 24, + "metadata": { + "description": "Required. Name of the Storage Account. Must be lower-case." + } + }, + "location": { + "type": "string", + "defaultValue": "[resourceGroup().location]", + "metadata": { + "description": "Optional. Location for all resources." + } + }, + "extendedLocationZone": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Extended Zone location (ex 'losangeles'). When supplied, the storage account will be created in the specified zone under the parent location. The extended zone must be available in the supplied parent location." + } + }, + "roleAssignments": { + "type": "array", + "items": { + "$ref": "#/definitions/roleAssignmentType" + }, + "nullable": true, + "metadata": { + "description": "Optional. Array of role assignments to create." + } + }, + "managedIdentities": { + "$ref": "#/definitions/managedIdentityAllType", + "nullable": true, + "metadata": { + "description": "Optional. The managed identity definition for this resource." + } + }, + "kind": { + "type": "string", + "defaultValue": "StorageV2", + "allowedValues": [ + "Storage", + "StorageV2", + "BlobStorage", + "FileStorage", + "BlockBlobStorage" + ], + "metadata": { + "description": "Optional. Type of Storage Account to create." + } + }, + "skuName": { + "type": "string", + "defaultValue": "Standard_GRS", + "allowedValues": [ + "Standard_LRS", + "Standard_ZRS", + "Standard_GRS", + "Standard_GZRS", + "Standard_RAGRS", + "Standard_RAGZRS", + "StandardV2_LRS", + "StandardV2_ZRS", + "StandardV2_GRS", + "StandardV2_GZRS", + "Premium_LRS", + "Premium_ZRS", + "PremiumV2_LRS", + "PremiumV2_ZRS" + ], + "metadata": { + "description": "Optional. Storage Account Sku Name - note: certain V2 SKUs require the use of: kind = FileStorage." + } + }, + "accessTier": { + "type": "string", + "defaultValue": "Hot", + "allowedValues": [ + "Premium", + "Hot", + "Cool", + "Cold" + ], + "metadata": { + "description": "Conditional. Required if the Storage Account kind is set to BlobStorage. The access tier is used for billing. The \"Premium\" access tier is the default value for premium block blobs storage account type and it cannot be changed for the premium block blobs storage account type." + } + }, + "largeFileSharesState": { + "type": "string", + "defaultValue": "Disabled", + "allowedValues": [ + "Disabled", + "Enabled" + ], + "metadata": { + "description": "Optional. Allow large file shares if set to 'Enabled'. It cannot be disabled once it is enabled. Only supported on locally redundant and zone redundant file shares. It cannot be set on FileStorage storage accounts (storage accounts for premium file shares)." + } + }, + "azureFilesIdentityBasedAuthentication": { + "type": "object", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.Storage/storageAccounts@2025-01-01#properties/properties/properties/azureFilesIdentityBasedAuthentication" + }, + "description": "Optional. Provides the identity based authentication settings for Azure Files." + }, + "nullable": true + }, + "defaultToOAuthAuthentication": { + "type": "bool", + "defaultValue": false, + "metadata": { + "description": "Optional. A boolean flag which indicates whether the default authentication is OAuth or not." + } + }, + "allowSharedKeyAccess": { + "type": "bool", + "defaultValue": true, + "metadata": { + "description": "Optional. Indicates whether the storage account permits requests to be authorized with the account access key via Shared Key. If false, then all requests, including shared access signatures, must be authorized with Azure Active Directory (Azure AD). The default value is null, which is equivalent to true." + } + }, + "privateEndpoints": { + "type": "array", + "items": { + "$ref": "#/definitions/privateEndpointMultiServiceType" + }, + "nullable": true, + "metadata": { + "description": "Optional. Configuration details for private endpoints. For security reasons, it is recommended to use private endpoints whenever possible." } }, - "roleAssignmentType": { - "type": "object", - "properties": { - "name": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The name (as GUID) of the role assignment. If not provided, a GUID will be generated." - } - }, - "roleDefinitionIdOrName": { - "type": "string", - "metadata": { - "description": "Required. The role to assign. You can provide either the display name of the role definition, the role definition GUID, or its fully qualified ID in the following format: '/providers/Microsoft.Authorization/roleDefinitions/c2f4ef07-c644-48eb-af81-4b1b4947fb11'." - } - }, - "principalId": { - "type": "string", - "metadata": { - "description": "Required. The principal ID of the principal (user/group/identity) to assign the role to." - } - }, - "principalType": { - "type": "string", - "allowedValues": [ - "Device", - "ForeignGroup", - "Group", - "ServicePrincipal", - "User" - ], - "nullable": true, - "metadata": { - "description": "Optional. The principal type of the assigned principal ID." - } - }, - "description": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The description of the role assignment." - } - }, - "condition": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The conditions on the role assignment. This limits the resources it can be assigned to. e.g.: @Resource[Microsoft.Storage/storageAccounts/blobServices/containers:ContainerName] StringEqualsIgnoreCase \"foo_storage_container\"." - } - }, - "conditionVersion": { - "type": "string", - "allowedValues": [ - "2.0" - ], - "nullable": true, - "metadata": { - "description": "Optional. Version of the condition." - } + "managementPolicyRules": { + "type": "array", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.Storage/storageAccounts/managementPolicies@2025-01-01#properties/properties/properties/policy/properties/rules" }, - "delegatedManagedIdentityResourceId": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The Resource Id of the delegated managed identity resource." - } - } + "description": "Optional. The Storage Account ManagementPolicies Rules." }, + "nullable": true + }, + "networkAcls": { + "$ref": "#/definitions/networkAclsType", + "nullable": true, "metadata": { - "description": "An AVM-aligned type for a role assignment.", - "__bicep_imported_from!": { - "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.0" - } + "description": "Optional. Networks ACLs, this value contains IPs to whitelist and/or Subnet information. If in use, bypass needs to be supplied. For security reasons, it is recommended to set the DefaultAction Deny." } - } - }, - "parameters": { - "name": { - "type": "string", - "minLength": 1, - "maxLength": 60, + }, + "requireInfrastructureEncryption": { + "type": "bool", + "defaultValue": true, "metadata": { - "description": "Required. Name of the app service plan." + "description": "Optional. A Boolean indicating whether or not the service applies a secondary layer of encryption with platform managed keys for data at rest. For security reasons, it is recommended to set it to true." } }, - "skuName": { + "allowCrossTenantReplication": { + "type": "bool", + "defaultValue": false, + "metadata": { + "description": "Optional. Allow or disallow cross AAD tenant object replication." + } + }, + "customDomainName": { "type": "string", - "defaultValue": "P1v3", + "defaultValue": "", "metadata": { - "example": " 'F1'\n 'B1'\n 'P1v3'\n 'I1v2'\n 'FC1'\n ", - "description": "Optional. The name of the SKU will Determine the tier, size, family of the App Service Plan. This defaults to P1v3 to leverage availability zones." + "description": "Optional. Sets the custom domain name assigned to the storage account. Name is the CNAME source." } }, - "skuCapacity": { - "type": "int", - "defaultValue": 3, + "customDomainUseSubDomainName": { + "type": "bool", + "defaultValue": false, "metadata": { - "description": "Optional. Number of workers associated with the App Service Plan. This defaults to 3, to leverage availability zones." + "description": "Optional. Indicates whether indirect CName validation is enabled. This should only be set on updates." } }, - "location": { + "dnsEndpointType": { "type": "string", - "defaultValue": "[resourceGroup().location]", + "nullable": true, + "allowedValues": [ + "AzureDnsZone", + "Standard" + ], "metadata": { - "description": "Optional. Location for all resources." + "description": "Optional. Allows you to specify the type of endpoint. Set this to AzureDNSZone to create a large number of accounts in a single subscription, which creates accounts in an Azure DNS Zone and the endpoint URL will have an alphanumeric DNS Zone identifier." } }, - "kind": { + "blobServices": { + "$ref": "#/definitions/blobServiceType", + "defaultValue": "[if(not(equals(parameters('kind'), 'FileStorage')), createObject('containerDeleteRetentionPolicyEnabled', true(), 'containerDeleteRetentionPolicyDays', 7, 'deleteRetentionPolicyEnabled', true(), 'deleteRetentionPolicyDays', 6), createObject())]", + "metadata": { + "description": "Optional. Blob service and containers to deploy." + } + }, + "fileServices": { + "$ref": "#/definitions/fileServiceType", + "defaultValue": {}, + "metadata": { + "description": "Optional. File service and shares to deploy." + } + }, + "queueServices": { + "$ref": "#/definitions/queueServiceType", + "defaultValue": {}, + "metadata": { + "description": "Optional. Queue service and queues to create." + } + }, + "tableServices": { + "$ref": "#/definitions/tableServiceType", + "defaultValue": {}, + "metadata": { + "description": "Optional. Table service and tables to create." + } + }, + "allowBlobPublicAccess": { + "type": "bool", + "defaultValue": false, + "metadata": { + "description": "Optional. Indicates whether public access is enabled for all blobs or containers in the storage account. For security reasons, it is recommended to set it to false." + } + }, + "minimumTlsVersion": { "type": "string", - "defaultValue": "app", + "defaultValue": "TLS1_2", "allowedValues": [ - "app", - "elastic", - "functionapp", - "windows", - "linux" + "TLS1_2" ], "metadata": { - "description": "Optional. Kind of server OS." + "description": "Optional. Set the minimum TLS version on request to storage. The TLS versions 1.0 and 1.1 are deprecated and not supported anymore." } }, - "reserved": { + "enableHierarchicalNamespace": { "type": "bool", - "defaultValue": "[equals(parameters('kind'), 'linux')]", + "nullable": true, "metadata": { - "description": "Conditional. Defaults to false when creating Windows/app App Service Plan. Required if creating a Linux App Service Plan and must be set to true." + "description": "Conditional. If true, enables Hierarchical Namespace for the storage account. Required if enableSftp or enableNfsV3 is set to true." } }, - "appServiceEnvironmentResourceId": { - "type": "string", - "defaultValue": "", + "enableSftp": { + "type": "bool", + "defaultValue": false, "metadata": { - "description": "Optional. The Resource ID of the App Service Environment to use for the App Service Plan." + "description": "Optional. If true, enables Secure File Transfer Protocol for the storage account. Requires enableHierarchicalNamespace to be true." } }, - "workerTierName": { - "type": "string", - "defaultValue": "", + "localUsers": { + "type": "array", + "items": { + "$ref": "#/definitions/localUserType" + }, + "nullable": true, "metadata": { - "description": "Optional. Target worker tier assigned to the App Service plan." + "description": "Optional. Local users to deploy for SFTP authentication." } }, - "perSiteScaling": { + "isLocalUserEnabled": { "type": "bool", "defaultValue": false, "metadata": { - "description": "Optional. If true, apps assigned to this App Service plan can be scaled independently. If false, apps assigned to this App Service plan will scale to all instances of the plan." + "description": "Optional. Enables local users feature, if set to true." } }, - "elasticScaleEnabled": { + "enableNfsV3": { "type": "bool", - "defaultValue": "[greater(parameters('maximumElasticWorkerCount'), 1)]", + "defaultValue": false, "metadata": { - "description": "Optional. Enable/Disable ElasticScaleEnabled App Service Plan." + "description": "Optional. If true, enables NFS 3.0 support for the storage account. Requires enableHierarchicalNamespace to be true." } }, - "maximumElasticWorkerCount": { - "type": "int", - "defaultValue": 1, + "diagnosticSettings": { + "type": "array", + "items": { + "$ref": "#/definitions/diagnosticSettingMetricsOnlyType" + }, + "nullable": true, "metadata": { - "description": "Optional. Maximum number of total workers allowed for this ElasticScaleEnabled App Service Plan." + "description": "Optional. The diagnostic settings of the service." } }, - "targetWorkerCount": { - "type": "int", - "defaultValue": 0, + "lock": { + "$ref": "#/definitions/lockType", + "nullable": true, "metadata": { - "description": "Optional. Scaling worker count." + "description": "Optional. The lock settings of the service." } }, - "targetWorkerSize": { - "type": "int", - "defaultValue": 0, + "tags": { + "type": "object", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.Storage/storageAccounts@2025-01-01#properties/tags" + }, + "description": "Optional. Tags of the resource." + }, + "nullable": true + }, + "enableTelemetry": { + "type": "bool", + "defaultValue": true, + "metadata": { + "description": "Optional. Enable/Disable usage telemetry for module." + } + }, + "allowedCopyScope": { + "type": "string", + "nullable": true, "allowedValues": [ - 0, - 1, - 2 + "AAD", + "PrivateLink" ], "metadata": { - "description": "Optional. The instance size of the hosting plan (small, medium, or large)." + "description": "Optional. Restrict copy to and from Storage Accounts within an AAD tenant or with Private Links to the same VNet." } }, - "zoneRedundant": { + "publicNetworkAccess": { + "type": "string", + "nullable": true, + "allowedValues": [ + "Enabled", + "Disabled", + "SecuredByPerimeter" + ], + "metadata": { + "description": "Optional. Whether or not public network access is allowed for this resource. For security reasons it should be disabled. If not specified, it will be disabled by default if private endpoints are set and networkAcls are not set." + } + }, + "supportsHttpsTrafficOnly": { "type": "bool", - "defaultValue": "[if(or(startsWith(parameters('skuName'), 'P'), startsWith(parameters('skuName'), 'EP')), true(), false())]", + "defaultValue": true, "metadata": { - "description": "Optional. Zone Redundant server farms can only be used on Premium or ElasticPremium SKU tiers within ZRS Supported regions (https://learn.microsoft.com/en-us/azure/storage/common/redundancy-regions-zrs)." + "description": "Optional. Allows HTTPS traffic only to storage service if sets to true." } }, - "lock": { - "$ref": "#/definitions/lockType", + "customerManagedKey": { + "$ref": "#/definitions/customerManagedKeyWithAutoRotateType", "nullable": true, "metadata": { - "description": "Optional. The lock settings of the service." + "description": "Optional. The customer managed key definition." } }, - "roleAssignments": { - "type": "array", - "items": { - "$ref": "#/definitions/roleAssignmentType" - }, + "sasExpirationPeriod": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "Optional. The SAS expiration period. DD.HH:MM:SS." + } + }, + "sasExpirationAction": { + "type": "string", + "defaultValue": "Log", + "allowedValues": [ + "Block", + "Log" + ], + "metadata": { + "description": "Optional. The SAS expiration action. Allowed values are Block and Log." + } + }, + "keyType": { + "type": "string", "nullable": true, + "allowedValues": [ + "Account", + "Service" + ], "metadata": { - "description": "Optional. Array of role assignments to create." + "description": "Optional. The keyType to use with Queue & Table services." } }, - "tags": { + "secretsExportConfiguration": { + "$ref": "#/definitions/secretsExportConfigurationType", + "nullable": true, + "metadata": { + "description": "Optional. Key vault reference and secret settings for the module's secrets export." + } + }, + "immutableStorageWithVersioning": { "type": "object", "metadata": { "__bicep_resource_derived_type!": { - "source": "Microsoft.Web/serverfarms@2024-11-01#properties/tags" + "source": "Microsoft.Storage/storageAccounts@2025-01-01#properties/properties/properties/immutableStorageWithVersioning" }, - "description": "Optional. Tags of the resource." + "description": "Optional. The property is immutable and can only be set to true at the account creation time. When set to true, it enables object level immutability for all the new containers in the account by default. Cannot be enabled for ADLS Gen2 storage accounts." }, "nullable": true }, - "enableTelemetry": { - "type": "bool", - "defaultValue": true, - "metadata": { - "description": "Optional. Enable/Disable usage telemetry for module." - } + "objectReplicationPolicies": { + "type": "array", + "items": { + "$ref": "#/definitions/objectReplicationPolicyType" + }, + "nullable": true, + "metadata": { + "description": "Optional. Object replication policies for the storage account." + } + } + }, + "variables": { + "copy": [ + { + "name": "formattedRoleAssignments", + "count": "[length(coalesce(parameters('roleAssignments'), createArray()))]", + "input": "[union(coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')], createObject('roleDefinitionId', coalesce(tryGet(variables('builtInRoleNames'), coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName), if(contains(coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName, '/providers/Microsoft.Authorization/roleDefinitions/'), coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName, subscriptionResourceId('Microsoft.Authorization/roleDefinitions', coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName)))))]" + } + ], + "enableReferencedModulesTelemetry": false, + "immutabilityValidation": "[if(and(equals(parameters('enableHierarchicalNamespace'), true()), not(empty(parameters('immutableStorageWithVersioning')))), fail('Configuration error: Immutable storage with versioning cannot be enabled when hierarchical namespace is enabled.'), null())]", + "supportsBlobService": "[or(or(or(equals(parameters('kind'), 'BlockBlobStorage'), equals(parameters('kind'), 'BlobStorage')), equals(parameters('kind'), 'StorageV2')), equals(parameters('kind'), 'Storage'))]", + "supportsFileService": "[or(or(equals(parameters('kind'), 'FileStorage'), equals(parameters('kind'), 'StorageV2')), equals(parameters('kind'), 'Storage'))]", + "formattedUserAssignedIdentities": "[reduce(map(coalesce(tryGet(parameters('managedIdentities'), 'userAssignedResourceIds'), createArray()), lambda('id', createObject(format('{0}', lambdaVariables('id')), createObject()))), createObject(), lambda('cur', 'next', union(lambdaVariables('cur'), lambdaVariables('next'))))]", + "identity": "[if(not(empty(parameters('managedIdentities'))), createObject('type', if(coalesce(tryGet(parameters('managedIdentities'), 'systemAssigned'), false()), if(not(empty(coalesce(tryGet(parameters('managedIdentities'), 'userAssignedResourceIds'), createObject()))), 'SystemAssigned,UserAssigned', 'SystemAssigned'), if(not(empty(coalesce(tryGet(parameters('managedIdentities'), 'userAssignedResourceIds'), createObject()))), 'UserAssigned', 'None')), 'userAssignedIdentities', if(not(empty(variables('formattedUserAssignedIdentities'))), variables('formattedUserAssignedIdentities'), null())), null())]", + "builtInRoleNames": { + "Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'b24988ac-6180-42a0-ab88-20f7382dd24c')]", + "Owner": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '8e3af657-a8ff-443c-a75c-2fe8c4bcb635')]", + "Reader": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'acdd72a7-3385-48ef-bd42-f606fba81ae7')]", + "Reader and Data Access": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'c12c1c16-33a1-487b-954d-41c89c60f349')]", + "Role Based Access Control Administrator": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'f58310d9-a9f6-439a-9e8d-f62e7b41a168')]", + "Storage Account Backup Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'e5e2a7ff-d759-4cd2-bb51-3152d37e2eb1')]", + "Storage Account Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '17d1049b-9a84-46fb-8f53-869881c3d3ab')]", + "Storage Account Key Operator Service Role": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '81a9662b-bebf-436f-a333-f67b29880f12')]", + "Storage Blob Data Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'ba92f5b4-2d11-453d-a403-e96b0029c9fe')]", + "Storage Blob Data Owner": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'b7e6dc6d-f1e8-4753-8033-0f276bb0955b')]", + "Storage Blob Data Reader": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '2a2b9908-6ea1-4ae2-8e65-a410df84e7d1')]", + "Storage Blob Delegator": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'db58b8e5-c6ad-4a2a-8342-4190687cbf4a')]", + "Storage File Data Privileged Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '69566ab7-960f-475b-8e7c-b3118f30c6bd')]", + "Storage File Data Privileged Reader": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'b8eda974-7b85-4f76-af95-65846b26df6d')]", + "Storage File Data SMB Share Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '0c867c2a-1d8c-454a-a3db-ab2ea1bdc8bb')]", + "Storage File Data SMB Share Elevated Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'a7264617-510b-434b-a828-9731dc254ea7')]", + "Storage File Data SMB Share Reader": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'aba4ae5f-2193-4029-9191-0cb91df5e314')]", + "Storage Queue Data Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '974c5e8b-45b9-4653-ba55-5f855dd0fb88')]", + "Storage Queue Data Message Processor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '8a0f0c08-91a1-4084-bc3d-661d67233fed')]", + "Storage Queue Data Message Sender": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'c6a89b2d-59bc-44d0-9896-0f6e12d7b80a')]", + "Storage Queue Data Reader": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '19e7f393-937e-4f77-808e-94535e297925')]", + "Storage Table Data Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '0a9a7e1f-b9d0-4cc4-a60d-0319b160aaa3')]", + "Storage Table Data Reader": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '76199698-9eea-4c19-bc75-cec21354c6b6')]", + "User Access Administrator": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '18d7d88d-d35e-4fb5-a5c3-7773c20a72d9')]" + }, + "formattedManagementPolicies": "[union(coalesce(parameters('managementPolicyRules'), createArray()), if(and(and(not(empty(parameters('blobServices'))), coalesce(tryGet(parameters('blobServices'), 'isVersioningEnabled'), false())), not(equals(tryGet(parameters('blobServices'), 'versionDeletePolicyDays'), null()))), createArray(createObject('name', 'DeletePreviousVersions (auto-created)', 'enabled', true(), 'type', 'Lifecycle', 'definition', createObject('actions', createObject('version', createObject('delete', createObject('daysAfterCreationGreaterThan', parameters('blobServices').versionDeletePolicyDays))), 'filters', createObject('blobTypes', createArray('blockBlob', 'appendBlob'))))), createArray()))]", + "isHSMManagedCMK": "[equals(tryGet(split(coalesce(tryGet(parameters('customerManagedKey'), 'keyVaultResourceId'), ''), '/'), 7), 'managedHSMs')]" + }, + "resources": { + "cMKKeyVault::cMKKey": { + "condition": "[and(and(not(variables('isHSMManagedCMK')), not(empty(tryGet(parameters('customerManagedKey'), 'keyVaultResourceId')))), and(not(empty(tryGet(parameters('customerManagedKey'), 'keyVaultResourceId'))), not(empty(tryGet(parameters('customerManagedKey'), 'keyName')))))]", + "existing": true, + "type": "Microsoft.KeyVault/vaults/keys", + "apiVersion": "2024-11-01", + "subscriptionId": "[split(tryGet(parameters('customerManagedKey'), 'keyVaultResourceId'), '/')[2]]", + "resourceGroup": "[split(tryGet(parameters('customerManagedKey'), 'keyVaultResourceId'), '/')[4]]", + "name": "[format('{0}/{1}', last(split(tryGet(parameters('customerManagedKey'), 'keyVaultResourceId'), '/')), tryGet(parameters('customerManagedKey'), 'keyName'))]" + }, + "avmTelemetry": { + "condition": "[parameters('enableTelemetry')]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "[format('46d3xbcp.res.storage-storageaccount.{0}.{1}', replace('0.30.0', '.', '-'), substring(uniqueString(deployment().name, parameters('location')), 0, 4))]", + "properties": { + "mode": "Incremental", + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "resources": [], + "outputs": { + "telemetry": { + "type": "String", + "value": "For more information, see https://aka.ms/avm/TelemetryInfo" + } + } + } + } + }, + "cMKKeyVault": { + "condition": "[and(not(variables('isHSMManagedCMK')), not(empty(tryGet(parameters('customerManagedKey'), 'keyVaultResourceId'))))]", + "existing": true, + "type": "Microsoft.KeyVault/vaults", + "apiVersion": "2025-05-01", + "subscriptionId": "[split(tryGet(parameters('customerManagedKey'), 'keyVaultResourceId'), '/')[2]]", + "resourceGroup": "[split(tryGet(parameters('customerManagedKey'), 'keyVaultResourceId'), '/')[4]]", + "name": "[last(split(tryGet(parameters('customerManagedKey'), 'keyVaultResourceId'), '/'))]" + }, + "cMKUserAssignedIdentity": { + "condition": "[not(empty(tryGet(parameters('customerManagedKey'), 'userAssignedIdentityResourceId')))]", + "existing": true, + "type": "Microsoft.ManagedIdentity/userAssignedIdentities", + "apiVersion": "2024-11-30", + "subscriptionId": "[split(tryGet(parameters('customerManagedKey'), 'userAssignedIdentityResourceId'), '/')[2]]", + "resourceGroup": "[split(tryGet(parameters('customerManagedKey'), 'userAssignedIdentityResourceId'), '/')[4]]", + "name": "[last(split(tryGet(parameters('customerManagedKey'), 'userAssignedIdentityResourceId'), '/'))]" + }, + "storageAccount": { + "type": "Microsoft.Storage/storageAccounts", + "apiVersion": "2025-01-01", + "name": "[parameters('name')]", + "location": "[parameters('location')]", + "extendedLocation": "[if(not(empty(parameters('extendedLocationZone'))), createObject('name', parameters('extendedLocationZone'), 'type', 'EdgeZone'), null())]", + "kind": "[parameters('kind')]", + "sku": { + "name": "[parameters('skuName')]" + }, + "identity": "[variables('identity')]", + "tags": "[parameters('tags')]", + "properties": "[shallowMerge(createArray(createObject('allowSharedKeyAccess', parameters('allowSharedKeyAccess'), 'defaultToOAuthAuthentication', parameters('defaultToOAuthAuthentication'), 'allowCrossTenantReplication', parameters('allowCrossTenantReplication'), 'allowedCopyScope', parameters('allowedCopyScope'), 'customDomain', createObject('name', parameters('customDomainName'), 'useSubDomainName', parameters('customDomainUseSubDomainName')), 'dnsEndpointType', parameters('dnsEndpointType'), 'isLocalUserEnabled', parameters('isLocalUserEnabled'), 'encryption', union(createObject('keySource', if(not(empty(parameters('customerManagedKey'))), 'Microsoft.Keyvault', 'Microsoft.Storage'), 'services', createObject('blob', if(variables('supportsBlobService'), createObject('enabled', true()), null()), 'file', if(variables('supportsFileService'), createObject('enabled', true()), null()), 'table', createObject('enabled', true(), 'keyType', parameters('keyType')), 'queue', createObject('enabled', true(), 'keyType', parameters('keyType'))), 'keyvaultproperties', if(not(empty(parameters('customerManagedKey'))), createObject('keyname', parameters('customerManagedKey').keyName, 'keyvaulturi', if(not(variables('isHSMManagedCMK')), reference('cMKKeyVault').vaultUri, format('https://{0}.managedhsm.azure.net/', last(split(parameters('customerManagedKey').keyVaultResourceId, '/')))), 'keyversion', if(not(empty(tryGet(parameters('customerManagedKey'), 'keyVersion'))), parameters('customerManagedKey').keyVersion, if(coalesce(tryGet(parameters('customerManagedKey'), 'autoRotationEnabled'), true()), null(), if(not(variables('isHSMManagedCMK')), last(split(reference('cMKKeyVault::cMKKey').keyUriWithVersion, '/')), fail('Managed HSM CMK encryption requires either specifying the ''keyVersion'' or omitting the ''autoRotationEnabled'' property. Setting ''autoRotationEnabled'' to false without a ''keyVersion'' is not allowed.'))))), null()), 'identity', createObject('userAssignedIdentity', if(not(empty(tryGet(parameters('customerManagedKey'), 'userAssignedIdentityResourceId'))), extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', split(tryGet(parameters('customerManagedKey'), 'userAssignedIdentityResourceId'), '/')[2], split(tryGet(parameters('customerManagedKey'), 'userAssignedIdentityResourceId'), '/')[4]), 'Microsoft.ManagedIdentity/userAssignedIdentities', last(split(tryGet(parameters('customerManagedKey'), 'userAssignedIdentityResourceId'), '/'))), null()))), if(parameters('requireInfrastructureEncryption'), createObject('requireInfrastructureEncryption', if(not(equals(parameters('kind'), 'Storage')), parameters('requireInfrastructureEncryption'), null())), createObject())), 'accessTier', if(and(not(equals(parameters('kind'), 'Storage')), not(equals(parameters('kind'), 'BlockBlobStorage'))), parameters('accessTier'), null()), 'sasPolicy', if(not(empty(parameters('sasExpirationPeriod'))), createObject('expirationAction', parameters('sasExpirationAction'), 'sasExpirationPeriod', parameters('sasExpirationPeriod')), null()), 'supportsHttpsTrafficOnly', parameters('supportsHttpsTrafficOnly'), 'isSftpEnabled', parameters('enableSftp'), 'isNfsV3Enabled', if(parameters('enableNfsV3'), parameters('enableNfsV3'), ''), 'largeFileSharesState', if(or(equals(parameters('skuName'), 'Standard_LRS'), equals(parameters('skuName'), 'Standard_ZRS')), parameters('largeFileSharesState'), null()), 'minimumTlsVersion', parameters('minimumTlsVersion'), 'networkAcls', if(not(empty(parameters('networkAcls'))), union(createObject('resourceAccessRules', tryGet(parameters('networkAcls'), 'resourceAccessRules'), 'defaultAction', coalesce(tryGet(parameters('networkAcls'), 'defaultAction'), 'Deny'), 'virtualNetworkRules', tryGet(parameters('networkAcls'), 'virtualNetworkRules'), 'ipRules', tryGet(parameters('networkAcls'), 'ipRules')), if(contains(parameters('networkAcls'), 'bypass'), createObject('bypass', tryGet(parameters('networkAcls'), 'bypass')), createObject())), createObject('bypass', 'AzureServices', 'defaultAction', 'Deny')), 'allowBlobPublicAccess', parameters('allowBlobPublicAccess'), 'publicNetworkAccess', if(not(empty(parameters('publicNetworkAccess'))), parameters('publicNetworkAccess'), if(and(not(empty(parameters('privateEndpoints'))), empty(parameters('networkAcls'))), 'Disabled', null()))), if(not(empty(parameters('azureFilesIdentityBasedAuthentication'))), createObject('azureFilesIdentityBasedAuthentication', parameters('azureFilesIdentityBasedAuthentication')), createObject()), if(not(equals(parameters('enableHierarchicalNamespace'), null())), createObject('isHnsEnabled', parameters('enableHierarchicalNamespace')), createObject()), createObject('immutableStorageWithVersioning', parameters('immutableStorageWithVersioning'))))]", + "dependsOn": [ + "cMKKeyVault", + "cMKKeyVault::cMKKey" + ] + }, + "storageAccount_diagnosticSettings": { + "copy": { + "name": "storageAccount_diagnosticSettings", + "count": "[length(coalesce(parameters('diagnosticSettings'), createArray()))]" + }, + "type": "Microsoft.Insights/diagnosticSettings", + "apiVersion": "2021-05-01-preview", + "scope": "[format('Microsoft.Storage/storageAccounts/{0}', parameters('name'))]", + "name": "[coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'name'), format('{0}-diagnosticSettings', parameters('name')))]", + "properties": { + "copy": [ + { + "name": "metrics", + "count": "[length(coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'metricCategories'), createArray(createObject('category', 'AllMetrics'))))]", + "input": { + "category": "[coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'metricCategories'), createArray(createObject('category', 'AllMetrics')))[copyIndex('metrics')].category]", + "enabled": "[coalesce(tryGet(coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'metricCategories'), createArray(createObject('category', 'AllMetrics')))[copyIndex('metrics')], 'enabled'), true())]", + "timeGrain": null + } + } + ], + "storageAccountId": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'storageAccountResourceId')]", + "workspaceId": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'workspaceResourceId')]", + "eventHubAuthorizationRuleId": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'eventHubAuthorizationRuleResourceId')]", + "eventHubName": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'eventHubName')]", + "marketplacePartnerId": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'marketplacePartnerResourceId')]", + "logAnalyticsDestinationType": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'logAnalyticsDestinationType')]" + }, + "dependsOn": [ + "storageAccount" + ] + }, + "storageAccount_lock": { + "condition": "[and(not(empty(coalesce(parameters('lock'), createObject()))), not(equals(tryGet(parameters('lock'), 'kind'), 'None')))]", + "type": "Microsoft.Authorization/locks", + "apiVersion": "2020-05-01", + "scope": "[format('Microsoft.Storage/storageAccounts/{0}', parameters('name'))]", + "name": "[coalesce(tryGet(parameters('lock'), 'name'), format('lock-{0}', parameters('name')))]", + "properties": { + "level": "[coalesce(tryGet(parameters('lock'), 'kind'), '')]", + "notes": "[coalesce(tryGet(parameters('lock'), 'notes'), if(equals(tryGet(parameters('lock'), 'kind'), 'CanNotDelete'), 'Cannot delete resource or child resources.', 'Cannot delete or modify the resource or child resources.'))]" + }, + "dependsOn": [ + "storageAccount" + ] + }, + "storageAccount_roleAssignments": { + "copy": { + "name": "storageAccount_roleAssignments", + "count": "[length(coalesce(variables('formattedRoleAssignments'), createArray()))]" + }, + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2022-04-01", + "scope": "[format('Microsoft.Storage/storageAccounts/{0}', parameters('name'))]", + "name": "[coalesce(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'name'), guid(resourceId('Microsoft.Storage/storageAccounts', parameters('name')), coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].principalId, coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].roleDefinitionId))]", + "properties": { + "roleDefinitionId": "[coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].roleDefinitionId]", + "principalId": "[coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].principalId]", + "description": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'description')]", + "principalType": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'principalType')]", + "condition": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'condition')]", + "conditionVersion": "[if(not(empty(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'condition'))), coalesce(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'conditionVersion'), '2.0'), null())]", + "delegatedManagedIdentityResourceId": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'delegatedManagedIdentityResourceId')]" + }, + "dependsOn": [ + "storageAccount" + ] }, - "diagnosticSettings": { - "type": "array", - "items": { - "$ref": "#/definitions/diagnosticSettingMetricsOnlyType" + "storageAccount_privateEndpoints": { + "copy": { + "name": "storageAccount_privateEndpoints", + "count": "[length(coalesce(parameters('privateEndpoints'), createArray()))]" }, - "nullable": true, - "metadata": { - "description": "Optional. The diagnostic settings of the service." - } - } - }, - "variables": { - "copy": [ - { - "name": "formattedRoleAssignments", - "count": "[length(coalesce(parameters('roleAssignments'), createArray()))]", - "input": "[union(coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')], createObject('roleDefinitionId', coalesce(tryGet(variables('builtInRoleNames'), coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName), if(contains(coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName, '/providers/Microsoft.Authorization/roleDefinitions/'), coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName, subscriptionResourceId('Microsoft.Authorization/roleDefinitions', coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName)))))]" - } - ], - "builtInRoleNames": { - "Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'b24988ac-6180-42a0-ab88-20f7382dd24c')]", - "Owner": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '8e3af657-a8ff-443c-a75c-2fe8c4bcb635')]", - "Reader": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'acdd72a7-3385-48ef-bd42-f606fba81ae7')]", - "Role Based Access Control Administrator": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'f58310d9-a9f6-439a-9e8d-f62e7b41a168')]", - "User Access Administrator": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '18d7d88d-d35e-4fb5-a5c3-7773c20a72d9')]", - "Web Plan Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '2cc479cb-7b4d-49a8-b449-8c00fd0f0a4b')]", - "Website Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'de139f84-1756-47ae-9be6-808fbbe84772')]" - } - }, - "resources": { - "avmTelemetry": { - "condition": "[parameters('enableTelemetry')]", "type": "Microsoft.Resources/deployments", - "apiVersion": "2024-03-01", - "name": "[format('46d3xbcp.res.web-serverfarm.{0}.{1}', replace('0.5.0', '.', '-'), substring(uniqueString(deployment().name, parameters('location')), 0, 4))]", + "apiVersion": "2025-04-01", + "name": "[format('{0}-sa-PrivateEndpoint-{1}', uniqueString(deployment().name, parameters('location')), copyIndex())]", + "subscriptionId": "[split(coalesce(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'resourceGroupResourceId'), resourceGroup().id), '/')[2]]", + "resourceGroup": "[split(coalesce(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'resourceGroupResourceId'), resourceGroup().id), '/')[4]]", "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, "mode": "Incremental", + "parameters": { + "name": { + "value": "[coalesce(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'name'), format('pep-{0}-{1}-{2}', last(split(resourceId('Microsoft.Storage/storageAccounts', parameters('name')), '/')), coalesce(parameters('privateEndpoints'), createArray())[copyIndex()].service, copyIndex()))]" + }, + "privateLinkServiceConnections": "[if(not(equals(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'isManualConnection'), true())), createObject('value', createArray(createObject('name', coalesce(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'privateLinkServiceConnectionName'), format('{0}-{1}-{2}', last(split(resourceId('Microsoft.Storage/storageAccounts', parameters('name')), '/')), coalesce(parameters('privateEndpoints'), createArray())[copyIndex()].service, copyIndex())), 'properties', createObject('privateLinkServiceId', resourceId('Microsoft.Storage/storageAccounts', parameters('name')), 'groupIds', createArray(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()].service))))), createObject('value', null()))]", + "manualPrivateLinkServiceConnections": "[if(equals(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'isManualConnection'), true()), createObject('value', createArray(createObject('name', coalesce(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'privateLinkServiceConnectionName'), format('{0}-{1}-{2}', last(split(resourceId('Microsoft.Storage/storageAccounts', parameters('name')), '/')), coalesce(parameters('privateEndpoints'), createArray())[copyIndex()].service, copyIndex())), 'properties', createObject('privateLinkServiceId', resourceId('Microsoft.Storage/storageAccounts', parameters('name')), 'groupIds', createArray(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()].service), 'requestMessage', coalesce(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'manualConnectionRequestMessage'), 'Manual approval required.'))))), createObject('value', null()))]", + "subnetResourceId": { + "value": "[coalesce(parameters('privateEndpoints'), createArray())[copyIndex()].subnetResourceId]" + }, + "enableTelemetry": { + "value": "[variables('enableReferencedModulesTelemetry')]" + }, + "location": { + "value": "[coalesce(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'location'), reference(split(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()].subnetResourceId, '/subnets/')[0], '2020-06-01', 'Full').location)]" + }, + "lock": { + "value": "[coalesce(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'lock'), parameters('lock'))]" + }, + "privateDnsZoneGroup": { + "value": "[tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'privateDnsZoneGroup')]" + }, + "roleAssignments": { + "value": "[tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'roleAssignments')]" + }, + "tags": { + "value": "[coalesce(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'tags'), parameters('tags'))]" + }, + "customDnsConfigs": { + "value": "[tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'customDnsConfigs')]" + }, + "ipConfigurations": { + "value": "[tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'ipConfigurations')]" + }, + "applicationSecurityGroupResourceIds": { + "value": "[tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'applicationSecurityGroupResourceIds')]" + }, + "customNetworkInterfaceName": { + "value": "[tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'customNetworkInterfaceName')]" + } + }, "template": { "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "languageVersion": "2.0", "contentVersion": "1.0.0.0", - "resources": [], + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.38.5.1644", + "templateHash": "16604612898799598358" + }, + "name": "Private Endpoints", + "description": "This module deploys a Private Endpoint." + }, + "definitions": { + "privateDnsZoneGroupType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The name of the Private DNS Zone Group." + } + }, + "privateDnsZoneGroupConfigs": { + "type": "array", + "items": { + "$ref": "#/definitions/privateDnsZoneGroupConfigType" + }, + "metadata": { + "description": "Required. The private DNS zone groups to associate the private endpoint. A DNS zone group can support up to 5 DNS zones." + } + } + }, + "metadata": { + "__bicep_export!": true, + "description": "The type of a private dns zone group." + } + }, + "lockType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Specify the name of lock." + } + }, + "kind": { + "type": "string", + "allowedValues": [ + "CanNotDelete", + "None", + "ReadOnly" + ], + "nullable": true, + "metadata": { + "description": "Optional. Specify the type of lock." + } + }, + "notes": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Specify the notes of the lock." + } + } + }, + "metadata": { + "description": "An AVM-aligned type for a lock.", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.1" + } + } + }, + "privateDnsZoneGroupConfigType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The name of the private DNS zone group config." + } + }, + "privateDnsZoneResourceId": { + "type": "string", + "metadata": { + "description": "Required. The resource id of the private DNS zone." + } + } + }, + "metadata": { + "description": "The type of a private DNS zone group configuration.", + "__bicep_imported_from!": { + "sourceTemplate": "private-dns-zone-group/main.bicep" + } + } + }, + "roleAssignmentType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The name (as GUID) of the role assignment. If not provided, a GUID will be generated." + } + }, + "roleDefinitionIdOrName": { + "type": "string", + "metadata": { + "description": "Required. The role to assign. You can provide either the display name of the role definition, the role definition GUID, or its fully qualified ID in the following format: '/providers/Microsoft.Authorization/roleDefinitions/c2f4ef07-c644-48eb-af81-4b1b4947fb11'." + } + }, + "principalId": { + "type": "string", + "metadata": { + "description": "Required. The principal ID of the principal (user/group/identity) to assign the role to." + } + }, + "principalType": { + "type": "string", + "allowedValues": [ + "Device", + "ForeignGroup", + "Group", + "ServicePrincipal", + "User" + ], + "nullable": true, + "metadata": { + "description": "Optional. The principal type of the assigned principal ID." + } + }, + "description": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The description of the role assignment." + } + }, + "condition": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The conditions on the role assignment. This limits the resources it can be assigned to. e.g.: @Resource[Microsoft.Storage/storageAccounts/blobServices/containers:ContainerName] StringEqualsIgnoreCase \"foo_storage_container\"." + } + }, + "conditionVersion": { + "type": "string", + "allowedValues": [ + "2.0" + ], + "nullable": true, + "metadata": { + "description": "Optional. Version of the condition." + } + }, + "delegatedManagedIdentityResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The Resource Id of the delegated managed identity resource." + } + } + }, + "metadata": { + "description": "An AVM-aligned type for a role assignment.", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.1" + } + } + } + }, + "parameters": { + "name": { + "type": "string", + "metadata": { + "description": "Required. Name of the private endpoint resource to create." + } + }, + "subnetResourceId": { + "type": "string", + "metadata": { + "description": "Required. Resource ID of the subnet where the endpoint needs to be created." + } + }, + "applicationSecurityGroupResourceIds": { + "type": "array", + "items": { + "type": "string" + }, + "nullable": true, + "metadata": { + "description": "Optional. Application security groups in which the private endpoint IP configuration is included." + } + }, + "customNetworkInterfaceName": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The custom name of the network interface attached to the private endpoint." + } + }, + "ipConfigurations": { + "type": "array", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.Network/privateEndpoints@2024-01-01#properties/properties/properties/ipConfigurations" + }, + "description": "Optional. A list of IP configurations of the private endpoint. This will be used to map to the First Party Service endpoints." + }, + "nullable": true + }, + "privateDnsZoneGroup": { + "$ref": "#/definitions/privateDnsZoneGroupType", + "nullable": true, + "metadata": { + "description": "Optional. The private DNS zone group to configure for the private endpoint." + } + }, + "location": { + "type": "string", + "defaultValue": "[resourceGroup().location]", + "metadata": { + "description": "Optional. Location for all Resources." + } + }, + "lock": { + "$ref": "#/definitions/lockType", + "nullable": true, + "metadata": { + "description": "Optional. The lock settings of the service." + } + }, + "roleAssignments": { + "type": "array", + "items": { + "$ref": "#/definitions/roleAssignmentType" + }, + "nullable": true, + "metadata": { + "description": "Optional. Array of role assignments to create." + } + }, + "tags": { + "type": "object", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.Network/privateEndpoints@2024-01-01#properties/tags" + }, + "description": "Optional. Tags to be applied on all resources/resource groups in this deployment." + }, + "nullable": true + }, + "customDnsConfigs": { + "type": "array", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.Network/privateEndpoints@2024-01-01#properties/properties/properties/customDnsConfigs" + }, + "description": "Optional. Custom DNS configurations." + }, + "nullable": true + }, + "manualPrivateLinkServiceConnections": { + "type": "array", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.Network/privateEndpoints@2024-01-01#properties/properties/properties/manualPrivateLinkServiceConnections" + }, + "description": "Conditional. A grouping of information about the connection to the remote resource. Used when the network admin does not have access to approve connections to the remote resource. Required if `privateLinkServiceConnections` is empty." + }, + "nullable": true + }, + "privateLinkServiceConnections": { + "type": "array", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.Network/privateEndpoints@2024-01-01#properties/properties/properties/privateLinkServiceConnections" + }, + "description": "Conditional. A grouping of information about the connection to the remote resource. Required if `manualPrivateLinkServiceConnections` is empty." + }, + "nullable": true + }, + "enableTelemetry": { + "type": "bool", + "defaultValue": true, + "metadata": { + "description": "Optional. Enable/Disable usage telemetry for module." + } + } + }, + "variables": { + "copy": [ + { + "name": "formattedRoleAssignments", + "count": "[length(coalesce(parameters('roleAssignments'), createArray()))]", + "input": "[union(coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')], createObject('roleDefinitionId', coalesce(tryGet(variables('builtInRoleNames'), coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName), if(contains(coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName, '/providers/Microsoft.Authorization/roleDefinitions/'), coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName, subscriptionResourceId('Microsoft.Authorization/roleDefinitions', coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName)))))]" + } + ], + "builtInRoleNames": { + "Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'b24988ac-6180-42a0-ab88-20f7382dd24c')]", + "DNS Resolver Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '0f2ebee7-ffd4-4fc0-b3b7-664099fdad5d')]", + "DNS Zone Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'befefa01-2a29-4197-83a8-272ff33ce314')]", + "Domain Services Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'eeaeda52-9324-47f6-8069-5d5bade478b2')]", + "Domain Services Reader": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '361898ef-9ed1-48c2-849c-a832951106bb')]", + "Network Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '4d97b98b-1d4f-4787-a291-c67834d212e7')]", + "Owner": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '8e3af657-a8ff-443c-a75c-2fe8c4bcb635')]", + "Private DNS Zone Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'b12aa53e-6015-4669-85d0-8515ebb3ae7f')]", + "Reader": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'acdd72a7-3385-48ef-bd42-f606fba81ae7')]", + "Role Based Access Control Administrator": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'f58310d9-a9f6-439a-9e8d-f62e7b41a168')]" + } + }, + "resources": { + "avmTelemetry": { + "condition": "[parameters('enableTelemetry')]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "[format('46d3xbcp.res.network-privateendpoint.{0}.{1}', replace('0.11.1', '.', '-'), substring(uniqueString(deployment().name, parameters('location')), 0, 4))]", + "properties": { + "mode": "Incremental", + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "resources": [], + "outputs": { + "telemetry": { + "type": "String", + "value": "For more information, see https://aka.ms/avm/TelemetryInfo" + } + } + } + } + }, + "privateEndpoint": { + "type": "Microsoft.Network/privateEndpoints", + "apiVersion": "2024-10-01", + "name": "[parameters('name')]", + "location": "[parameters('location')]", + "tags": "[parameters('tags')]", + "properties": { + "copy": [ + { + "name": "applicationSecurityGroups", + "count": "[length(coalesce(parameters('applicationSecurityGroupResourceIds'), createArray()))]", + "input": { + "id": "[coalesce(parameters('applicationSecurityGroupResourceIds'), createArray())[copyIndex('applicationSecurityGroups')]]" + } + } + ], + "customDnsConfigs": "[coalesce(parameters('customDnsConfigs'), createArray())]", + "customNetworkInterfaceName": "[coalesce(parameters('customNetworkInterfaceName'), '')]", + "ipConfigurations": "[coalesce(parameters('ipConfigurations'), createArray())]", + "manualPrivateLinkServiceConnections": "[coalesce(parameters('manualPrivateLinkServiceConnections'), createArray())]", + "privateLinkServiceConnections": "[coalesce(parameters('privateLinkServiceConnections'), createArray())]", + "subnet": { + "id": "[parameters('subnetResourceId')]" + } + } + }, + "privateEndpoint_lock": { + "condition": "[and(not(empty(coalesce(parameters('lock'), createObject()))), not(equals(tryGet(parameters('lock'), 'kind'), 'None')))]", + "type": "Microsoft.Authorization/locks", + "apiVersion": "2020-05-01", + "scope": "[format('Microsoft.Network/privateEndpoints/{0}', parameters('name'))]", + "name": "[coalesce(tryGet(parameters('lock'), 'name'), format('lock-{0}', parameters('name')))]", + "properties": { + "level": "[coalesce(tryGet(parameters('lock'), 'kind'), '')]", + "notes": "[coalesce(tryGet(parameters('lock'), 'notes'), if(equals(tryGet(parameters('lock'), 'kind'), 'CanNotDelete'), 'Cannot delete resource or child resources.', 'Cannot delete or modify the resource or child resources.'))]" + }, + "dependsOn": [ + "privateEndpoint" + ] + }, + "privateEndpoint_roleAssignments": { + "copy": { + "name": "privateEndpoint_roleAssignments", + "count": "[length(coalesce(variables('formattedRoleAssignments'), createArray()))]" + }, + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2022-04-01", + "scope": "[format('Microsoft.Network/privateEndpoints/{0}', parameters('name'))]", + "name": "[coalesce(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'name'), guid(resourceId('Microsoft.Network/privateEndpoints', parameters('name')), coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].principalId, coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].roleDefinitionId))]", + "properties": { + "roleDefinitionId": "[coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].roleDefinitionId]", + "principalId": "[coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].principalId]", + "description": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'description')]", + "principalType": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'principalType')]", + "condition": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'condition')]", + "conditionVersion": "[if(not(empty(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'condition'))), coalesce(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'conditionVersion'), '2.0'), null())]", + "delegatedManagedIdentityResourceId": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'delegatedManagedIdentityResourceId')]" + }, + "dependsOn": [ + "privateEndpoint" + ] + }, + "privateEndpoint_privateDnsZoneGroup": { + "condition": "[not(empty(parameters('privateDnsZoneGroup')))]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "[format('{0}-PrivateEndpoint-PrivateDnsZoneGroup', uniqueString(deployment().name))]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "name": { + "value": "[tryGet(parameters('privateDnsZoneGroup'), 'name')]" + }, + "privateEndpointName": { + "value": "[parameters('name')]" + }, + "privateDnsZoneConfigs": { + "value": "[parameters('privateDnsZoneGroup').privateDnsZoneGroupConfigs]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "languageVersion": "2.0", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.38.5.1644", + "templateHash": "24141742673128945" + }, + "name": "Private Endpoint Private DNS Zone Groups", + "description": "This module deploys a Private Endpoint Private DNS Zone Group." + }, + "definitions": { + "privateDnsZoneGroupConfigType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The name of the private DNS zone group config." + } + }, + "privateDnsZoneResourceId": { + "type": "string", + "metadata": { + "description": "Required. The resource id of the private DNS zone." + } + } + }, + "metadata": { + "__bicep_export!": true, + "description": "The type of a private DNS zone group configuration." + } + } + }, + "parameters": { + "privateEndpointName": { + "type": "string", + "metadata": { + "description": "Conditional. The name of the parent private endpoint. Required if the template is used in a standalone deployment." + } + }, + "privateDnsZoneConfigs": { + "type": "array", + "items": { + "$ref": "#/definitions/privateDnsZoneGroupConfigType" + }, + "minLength": 1, + "maxLength": 5, + "metadata": { + "description": "Required. Array of private DNS zone configurations of the private DNS zone group. A DNS zone group can support up to 5 DNS zones." + } + }, + "name": { + "type": "string", + "defaultValue": "default", + "metadata": { + "description": "Optional. The name of the private DNS zone group." + } + } + }, + "resources": { + "privateEndpoint": { + "existing": true, + "type": "Microsoft.Network/privateEndpoints", + "apiVersion": "2024-10-01", + "name": "[parameters('privateEndpointName')]" + }, + "privateDnsZoneGroup": { + "type": "Microsoft.Network/privateEndpoints/privateDnsZoneGroups", + "apiVersion": "2024-10-01", + "name": "[format('{0}/{1}', parameters('privateEndpointName'), parameters('name'))]", + "properties": { + "copy": [ + { + "name": "privateDnsZoneConfigs", + "count": "[length(parameters('privateDnsZoneConfigs'))]", + "input": { + "name": "[coalesce(tryGet(parameters('privateDnsZoneConfigs')[copyIndex('privateDnsZoneConfigs')], 'name'), last(split(parameters('privateDnsZoneConfigs')[copyIndex('privateDnsZoneConfigs')].privateDnsZoneResourceId, '/')))]", + "properties": { + "privateDnsZoneId": "[parameters('privateDnsZoneConfigs')[copyIndex('privateDnsZoneConfigs')].privateDnsZoneResourceId]" + } + } + } + ] + } + } + }, + "outputs": { + "name": { + "type": "string", + "metadata": { + "description": "The name of the private endpoint DNS zone group." + }, + "value": "[parameters('name')]" + }, + "resourceId": { + "type": "string", + "metadata": { + "description": "The resource ID of the private endpoint DNS zone group." + }, + "value": "[resourceId('Microsoft.Network/privateEndpoints/privateDnsZoneGroups', parameters('privateEndpointName'), parameters('name'))]" + }, + "resourceGroupName": { + "type": "string", + "metadata": { + "description": "The resource group the private endpoint DNS zone group was deployed into." + }, + "value": "[resourceGroup().name]" + } + } + } + }, + "dependsOn": [ + "privateEndpoint" + ] + } + }, "outputs": { - "telemetry": { - "type": "String", - "value": "For more information, see https://aka.ms/avm/TelemetryInfo" + "resourceGroupName": { + "type": "string", + "metadata": { + "description": "The resource group the private endpoint was deployed into." + }, + "value": "[resourceGroup().name]" + }, + "resourceId": { + "type": "string", + "metadata": { + "description": "The resource ID of the private endpoint." + }, + "value": "[resourceId('Microsoft.Network/privateEndpoints', parameters('name'))]" + }, + "name": { + "type": "string", + "metadata": { + "description": "The name of the private endpoint." + }, + "value": "[parameters('name')]" + }, + "location": { + "type": "string", + "metadata": { + "description": "The location the resource was deployed into." + }, + "value": "[reference('privateEndpoint', '2024-10-01', 'full').location]" + }, + "customDnsConfigs": { + "type": "array", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.Network/privateEndpoints@2024-01-01#properties/properties/properties/customDnsConfigs", + "output": true + }, + "description": "The custom DNS configurations of the private endpoint." + }, + "value": "[reference('privateEndpoint').customDnsConfigs]" + }, + "networkInterfaceResourceIds": { + "type": "array", + "items": { + "type": "string" + }, + "metadata": { + "description": "The resource IDs of the network interfaces associated with the private endpoint." + }, + "value": "[map(reference('privateEndpoint').networkInterfaces, lambda('nic', lambdaVariables('nic').id))]" + }, + "groupId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "The group Id for the private endpoint Group." + }, + "value": "[coalesce(tryGet(tryGet(tryGet(tryGet(reference('privateEndpoint'), 'manualPrivateLinkServiceConnections'), 0, 'properties'), 'groupIds'), 0), tryGet(tryGet(tryGet(tryGet(reference('privateEndpoint'), 'privateLinkServiceConnections'), 0, 'properties'), 'groupIds'), 0))]" } } } - } - }, - "appServicePlan": { - "type": "Microsoft.Web/serverfarms", - "apiVersion": "2024-11-01", - "name": "[parameters('name')]", - "kind": "[parameters('kind')]", - "location": "[parameters('location')]", - "tags": "[parameters('tags')]", - "sku": "[if(equals(parameters('skuName'), 'FC1'), createObject('name', parameters('skuName'), 'tier', 'FlexConsumption'), createObject('name', parameters('skuName'), 'capacity', parameters('skuCapacity')))]", - "properties": { - "workerTierName": "[parameters('workerTierName')]", - "hostingEnvironmentProfile": "[if(not(empty(parameters('appServiceEnvironmentResourceId'))), createObject('id', parameters('appServiceEnvironmentResourceId')), null())]", - "perSiteScaling": "[parameters('perSiteScaling')]", - "maximumElasticWorkerCount": "[parameters('maximumElasticWorkerCount')]", - "elasticScaleEnabled": "[parameters('elasticScaleEnabled')]", - "reserved": "[parameters('reserved')]", - "targetWorkerCount": "[parameters('targetWorkerCount')]", - "targetWorkerSizeId": "[parameters('targetWorkerSize')]", - "zoneRedundant": "[parameters('zoneRedundant')]" - } - }, - "appServicePlan_diagnosticSettings": { - "copy": { - "name": "appServicePlan_diagnosticSettings", - "count": "[length(coalesce(parameters('diagnosticSettings'), createArray()))]" - }, - "type": "Microsoft.Insights/diagnosticSettings", - "apiVersion": "2021-05-01-preview", - "scope": "[format('Microsoft.Web/serverfarms/{0}', parameters('name'))]", - "name": "[coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'name'), format('{0}-diagnosticSettings', parameters('name')))]", - "properties": { - "copy": [ - { - "name": "metrics", - "count": "[length(coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'metricCategories'), createArray(createObject('category', 'AllMetrics'))))]", - "input": { - "category": "[coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'metricCategories'), createArray(createObject('category', 'AllMetrics')))[copyIndex('metrics')].category]", - "enabled": "[coalesce(tryGet(coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'metricCategories'), createArray(createObject('category', 'AllMetrics')))[copyIndex('metrics')], 'enabled'), true())]", - "timeGrain": null - } - } - ], - "storageAccountId": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'storageAccountResourceId')]", - "workspaceId": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'workspaceResourceId')]", - "eventHubAuthorizationRuleId": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'eventHubAuthorizationRuleResourceId')]", - "eventHubName": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'eventHubName')]", - "marketplacePartnerId": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'marketplacePartnerResourceId')]", - "logAnalyticsDestinationType": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'logAnalyticsDestinationType')]" - }, - "dependsOn": [ - "appServicePlan" - ] - }, - "appServicePlan_lock": { - "condition": "[and(not(empty(coalesce(parameters('lock'), createObject()))), not(equals(tryGet(parameters('lock'), 'kind'), 'None')))]", - "type": "Microsoft.Authorization/locks", - "apiVersion": "2020-05-01", - "scope": "[format('Microsoft.Web/serverfarms/{0}', parameters('name'))]", - "name": "[coalesce(tryGet(parameters('lock'), 'name'), format('lock-{0}', parameters('name')))]", - "properties": { - "level": "[coalesce(tryGet(parameters('lock'), 'kind'), '')]", - "notes": "[coalesce(tryGet(parameters('lock'), 'notes'), if(equals(tryGet(parameters('lock'), 'kind'), 'CanNotDelete'), 'Cannot delete resource or child resources.', 'Cannot delete or modify the resource or child resources.'))]" - }, - "dependsOn": [ - "appServicePlan" - ] - }, - "appServicePlan_roleAssignments": { - "copy": { - "name": "appServicePlan_roleAssignments", - "count": "[length(coalesce(variables('formattedRoleAssignments'), createArray()))]" - }, - "type": "Microsoft.Authorization/roleAssignments", - "apiVersion": "2022-04-01", - "scope": "[format('Microsoft.Web/serverfarms/{0}', parameters('name'))]", - "name": "[coalesce(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'name'), guid(resourceId('Microsoft.Web/serverfarms', parameters('name')), coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].principalId, coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].roleDefinitionId))]", - "properties": { - "roleDefinitionId": "[coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].roleDefinitionId]", - "principalId": "[coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].principalId]", - "description": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'description')]", - "principalType": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'principalType')]", - "condition": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'condition')]", - "conditionVersion": "[if(not(empty(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'condition'))), coalesce(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'conditionVersion'), '2.0'), null())]", - "delegatedManagedIdentityResourceId": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'delegatedManagedIdentityResourceId')]" }, "dependsOn": [ - "appServicePlan" + "storageAccount" ] - } - }, - "outputs": { - "resourceGroupName": { - "type": "string", - "metadata": { - "description": "The resource group the app service plan was deployed into." - }, - "value": "[resourceGroup().name]" - }, - "name": { - "type": "string", - "metadata": { - "description": "The name of the app service plan." - }, - "value": "[parameters('name')]" - }, - "resourceId": { - "type": "string", - "metadata": { - "description": "The resource ID of the app service plan." - }, - "value": "[resourceId('Microsoft.Web/serverfarms', parameters('name'))]" }, - "location": { - "type": "string", - "metadata": { - "description": "The location the resource was deployed into." - }, - "value": "[reference('appServicePlan', '2024-11-01', 'full').location]" - } - } - } - }, - "dependsOn": [ - "logAnalyticsWorkspace" - ] - }, - "webSite": { - "type": "Microsoft.Resources/deployments", - "apiVersion": "2025-04-01", - "name": "[take(format('module.web-sites.{0}', variables('webSiteResourceName')), 64)]", - "properties": { - "expressionEvaluationOptions": { - "scope": "inner" - }, - "mode": "Incremental", - "parameters": { - "name": { - "value": "[variables('webSiteResourceName')]" - }, - "tags": { - "value": "[parameters('tags')]" - }, - "location": { - "value": "[variables('solutionLocation')]" - }, - "kind": { - "value": "app,linux,container" - }, - "serverFarmResourceId": { - "value": "[reference('webServerFarm').outputs.resourceId.value]" - }, - "managedIdentities": { - "value": { - "userAssignedResourceIds": [ - "[reference('userAssignedIdentity').outputs.resourceId.value]" - ] - } - }, - "siteConfig": { - "value": { - "linuxFxVersion": "[format('DOCKER|{0}.azurecr.io/content-gen-app:{1}', variables('acrResourceName'), parameters('imageTag'))]", - "minTlsVersion": "1.2", - "alwaysOn": true, - "ftpsState": "FtpsOnly" - } - }, - "virtualNetworkSubnetId": "[if(parameters('enablePrivateNetworking'), createObject('value', reference('virtualNetwork').outputs.webSubnetResourceId.value), createObject('value', null()))]", - "configs": { - "value": "[concat(createArray(createObject('name', 'appsettings', 'properties', createObject('DOCKER_REGISTRY_SERVER_URL', format('https://{0}.azurecr.io', variables('acrResourceName')), 'BACKEND_URL', variables('aciBackendUrl'), 'AZURE_CLIENT_ID', reference('userAssignedIdentity').outputs.clientId.value), 'applicationInsightResourceId', if(parameters('enableMonitoring'), reference('applicationInsights').outputs.resourceId.value, null()))), if(parameters('enableMonitoring'), createArray(createObject('name', 'logs', 'properties', createObject())), createArray()))]" - }, - "enableMonitoring": { - "value": "[parameters('enableMonitoring')]" - }, - "diagnosticSettings": "[if(parameters('enableMonitoring'), createObject('value', createArray(createObject('workspaceResourceId', if(variables('useExistingLogAnalytics'), parameters('existingLogAnalyticsWorkspaceId'), if(parameters('enableMonitoring'), reference('logAnalyticsWorkspace').outputs.resourceId.value, ''))))), createObject('value', null()))]", - "vnetRouteAllEnabled": { - "value": "[parameters('enablePrivateNetworking')]" - }, - "vnetImagePullEnabled": { - "value": "[parameters('enablePrivateNetworking')]" - }, - "publicNetworkAccess": { - "value": "Enabled" - } - }, - "template": { - "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", - "languageVersion": "2.0", - "contentVersion": "1.0.0.0", - "metadata": { - "_generator": { - "name": "bicep", - "version": "0.39.26.7824", - "templateHash": "11911473200605315360" - } - }, - "definitions": { - "appSettingsConfigType": { - "type": "object", + "storageAccount_managementPolicies": { + "condition": "[not(empty(coalesce(variables('formattedManagementPolicies'), createArray())))]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "[format('{0}-Storage-ManagementPolicies', uniqueString(deployment().name, parameters('location')))]", "properties": { - "name": { - "type": "string", - "allowedValues": [ - "appsettings", - "logs" - ], - "metadata": { - "description": "Required. The type of config." - } - }, - "storageAccountUseIdentityAuthentication": { - "type": "bool", - "nullable": true, - "metadata": { - "description": "Optional. If the provided storage account requires Identity based authentication." - } - }, - "storageAccountResourceId": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. Required if app of kind functionapp. Resource ID of the storage account to manage triggers and logging function executions." - } + "expressionEvaluationOptions": { + "scope": "inner" }, - "applicationInsightResourceId": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. Resource ID of the application insight to leverage for this resource." + "mode": "Incremental", + "parameters": { + "storageAccountName": { + "value": "[parameters('name')]" + }, + "rules": { + "value": "[variables('formattedManagementPolicies')]" } }, - "retainCurrentAppSettings": { - "type": "bool", - "nullable": true, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", "metadata": { - "description": "Optional. The retain the current app settings. Defaults to true." - } - }, - "properties": { - "type": "object", - "properties": {}, - "additionalProperties": { - "type": "string", - "metadata": { - "description": "Required. An app settings key-value pair." + "_generator": { + "name": "bicep", + "version": "0.39.26.7824", + "templateHash": "4538661605890101674" + }, + "name": "Storage Account Management Policies", + "description": "This module deploys a Storage Account Management Policy." + }, + "parameters": { + "storageAccountName": { + "type": "string", + "maxLength": 24, + "metadata": { + "description": "Conditional. The name of the parent Storage Account. Required if the template is used in a standalone deployment." + } + }, + "rules": { + "type": "array", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.Storage/storageAccounts/managementPolicies@2024-01-01#properties/properties/properties/policy/properties/rules" + }, + "description": "Required. The Storage Account ManagementPolicies Rules." + } } }, - "nullable": true, - "metadata": { - "description": "Optional. The app settings key-value pairs." + "resources": [ + { + "type": "Microsoft.Storage/storageAccounts/managementPolicies", + "apiVersion": "2024-01-01", + "name": "[format('{0}/{1}', parameters('storageAccountName'), 'default')]", + "properties": { + "policy": { + "rules": "[parameters('rules')]" + } + } + } + ], + "outputs": { + "resourceId": { + "type": "string", + "metadata": { + "description": "The resource ID of the deployed management policy." + }, + "value": "default" + }, + "name": { + "type": "string", + "metadata": { + "description": "The name of the deployed management policy." + }, + "value": "default" + }, + "resourceGroupName": { + "type": "string", + "metadata": { + "description": "The resource group of the deployed management policy." + }, + "value": "[resourceGroup().name]" + } } } }, - "metadata": { - "__bicep_export!": true, - "description": "The type of an app settings configuration." - } + "dependsOn": [ + "storageAccount", + "storageAccount_blobServices" + ] }, - "_1.lockType": { - "type": "object", + "storageAccount_localUsers": { + "copy": { + "name": "storageAccount_localUsers", + "count": "[length(coalesce(parameters('localUsers'), createArray()))]" + }, + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "[format('{0}-Storage-LocalUsers-{1}', uniqueString(deployment().name, parameters('location')), copyIndex())]", "properties": { - "name": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. Specify the name of lock." + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "storageAccountName": { + "value": "[parameters('name')]" + }, + "name": { + "value": "[coalesce(parameters('localUsers'), createArray())[copyIndex()].name]" + }, + "hasSshKey": { + "value": "[coalesce(parameters('localUsers'), createArray())[copyIndex()].hasSshKey]" + }, + "hasSshPassword": { + "value": "[coalesce(parameters('localUsers'), createArray())[copyIndex()].hasSshPassword]" + }, + "permissionScopes": { + "value": "[coalesce(parameters('localUsers'), createArray())[copyIndex()].permissionScopes]" + }, + "hasSharedKey": { + "value": "[tryGet(coalesce(parameters('localUsers'), createArray())[copyIndex()], 'hasSharedKey')]" + }, + "homeDirectory": { + "value": "[tryGet(coalesce(parameters('localUsers'), createArray())[copyIndex()], 'homeDirectory')]" + }, + "sshAuthorizedKeys": { + "value": "[tryGet(coalesce(parameters('localUsers'), createArray())[copyIndex()], 'sshAuthorizedKeys')]" } }, - "kind": { - "type": "string", - "allowedValues": [ - "CanNotDelete", - "None", - "ReadOnly" - ], - "nullable": true, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "languageVersion": "2.0", + "contentVersion": "1.0.0.0", "metadata": { - "description": "Optional. Specify the type of lock." + "_generator": { + "name": "bicep", + "version": "0.39.26.7824", + "templateHash": "17421429164012186211" + }, + "name": "Storage Account Local Users", + "description": "This module deploys a Storage Account Local User, which is used for SFTP authentication." + }, + "definitions": { + "sshAuthorizedKeyType": { + "type": "object", + "properties": { + "description": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Description used to store the function/usage of the key." + } + }, + "key": { + "type": "securestring", + "metadata": { + "description": "Required. SSH public key base64 encoded. The format should be: '{keyType} {keyData}', e.g. ssh-rsa AAAABBBB." + } + } + }, + "metadata": { + "__bicep_export!": true + } + }, + "permissionScopeType": { + "type": "object", + "properties": { + "permissions": { + "type": "string", + "metadata": { + "description": "Required. The permissions for the local user. Possible values include: Read (r), Write (w), Delete (d), List (l), and Create (c)." + } + }, + "resourceName": { + "type": "string", + "metadata": { + "description": "Required. The name of resource, normally the container name or the file share name, used by the local user." + } + }, + "service": { + "type": "string", + "metadata": { + "description": "Required. The service used by the local user, e.g. blob, file." + } + } + }, + "metadata": { + "__bicep_export!": true + } + } + }, + "parameters": { + "storageAccountName": { + "type": "string", + "maxLength": 24, + "metadata": { + "description": "Conditional. The name of the parent Storage Account. Required if the template is used in a standalone deployment." + } + }, + "name": { + "type": "string", + "metadata": { + "description": "Required. The name of the local user used for SFTP Authentication." + } + }, + "hasSharedKey": { + "type": "bool", + "defaultValue": false, + "metadata": { + "description": "Optional. Indicates whether shared key exists. Set it to false to remove existing shared key." + } + }, + "hasSshKey": { + "type": "bool", + "metadata": { + "description": "Required. Indicates whether SSH key exists. Set it to false to remove existing SSH key." + } + }, + "hasSshPassword": { + "type": "bool", + "metadata": { + "description": "Required. Indicates whether SSH password exists. Set it to false to remove existing SSH password." + } + }, + "homeDirectory": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "Optional. The local user home directory." + } + }, + "permissionScopes": { + "type": "array", + "items": { + "$ref": "#/definitions/permissionScopeType" + }, + "metadata": { + "description": "Required. The permission scopes of the local user." + } + }, + "sshAuthorizedKeys": { + "type": "array", + "items": { + "$ref": "#/definitions/sshAuthorizedKeyType" + }, + "nullable": true, + "metadata": { + "description": "Optional. The local user SSH authorized keys for SFTP." + } + } + }, + "resources": { + "storageAccount": { + "existing": true, + "type": "Microsoft.Storage/storageAccounts", + "apiVersion": "2024-01-01", + "name": "[parameters('storageAccountName')]" + }, + "localUsers": { + "type": "Microsoft.Storage/storageAccounts/localUsers", + "apiVersion": "2024-01-01", + "name": "[format('{0}/{1}', parameters('storageAccountName'), parameters('name'))]", + "properties": { + "hasSharedKey": "[parameters('hasSharedKey')]", + "hasSshKey": "[parameters('hasSshKey')]", + "hasSshPassword": "[parameters('hasSshPassword')]", + "homeDirectory": "[parameters('homeDirectory')]", + "permissionScopes": "[parameters('permissionScopes')]", + "sshAuthorizedKeys": "[parameters('sshAuthorizedKeys')]" + } + } + }, + "outputs": { + "name": { + "type": "string", + "metadata": { + "description": "The name of the deployed local user." + }, + "value": "[parameters('name')]" + }, + "resourceGroupName": { + "type": "string", + "metadata": { + "description": "The resource group of the deployed local user." + }, + "value": "[resourceGroup().name]" + }, + "resourceId": { + "type": "string", + "metadata": { + "description": "The resource ID of the deployed local user." + }, + "value": "[resourceId('Microsoft.Storage/storageAccounts/localUsers', parameters('storageAccountName'), parameters('name'))]" + } } } }, - "metadata": { - "description": "An AVM-aligned type for a lock.", - "__bicep_imported_from!": { - "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.5.1" - } - } + "dependsOn": [ + "storageAccount" + ] }, - "_1.privateEndpointCustomDnsConfigType": { - "type": "object", + "storageAccount_blobServices": { + "condition": "[not(empty(parameters('blobServices')))]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "[format('{0}-Storage-BlobServices', uniqueString(deployment().name, parameters('location')))]", "properties": { - "fqdn": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. FQDN that resolves to private endpoint IP address." - } + "expressionEvaluationOptions": { + "scope": "inner" }, - "ipAddresses": { - "type": "array", - "items": { - "type": "string" + "mode": "Incremental", + "parameters": { + "storageAccountName": { + "value": "[parameters('name')]" }, - "metadata": { - "description": "Required. A list of private IP addresses of the private endpoint." - } - } - }, - "metadata": { - "__bicep_imported_from!": { - "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.5.1" - } - } - }, - "_1.privateEndpointIpConfigurationType": { - "type": "object", - "properties": { - "name": { - "type": "string", - "metadata": { - "description": "Required. The name of the resource that is unique within a resource group." + "containers": { + "value": "[tryGet(parameters('blobServices'), 'containers')]" + }, + "automaticSnapshotPolicyEnabled": { + "value": "[tryGet(parameters('blobServices'), 'automaticSnapshotPolicyEnabled')]" + }, + "changeFeedEnabled": { + "value": "[tryGet(parameters('blobServices'), 'changeFeedEnabled')]" + }, + "changeFeedRetentionInDays": { + "value": "[tryGet(parameters('blobServices'), 'changeFeedRetentionInDays')]" + }, + "containerDeleteRetentionPolicyEnabled": { + "value": "[tryGet(parameters('blobServices'), 'containerDeleteRetentionPolicyEnabled')]" + }, + "containerDeleteRetentionPolicyDays": { + "value": "[tryGet(parameters('blobServices'), 'containerDeleteRetentionPolicyDays')]" + }, + "containerDeleteRetentionPolicyAllowPermanentDelete": { + "value": "[tryGet(parameters('blobServices'), 'containerDeleteRetentionPolicyAllowPermanentDelete')]" + }, + "corsRules": { + "value": "[tryGet(parameters('blobServices'), 'corsRules')]" + }, + "defaultServiceVersion": { + "value": "[tryGet(parameters('blobServices'), 'defaultServiceVersion')]" + }, + "deleteRetentionPolicyAllowPermanentDelete": { + "value": "[tryGet(parameters('blobServices'), 'deleteRetentionPolicyAllowPermanentDelete')]" + }, + "deleteRetentionPolicyEnabled": { + "value": "[tryGet(parameters('blobServices'), 'deleteRetentionPolicyEnabled')]" + }, + "deleteRetentionPolicyDays": { + "value": "[tryGet(parameters('blobServices'), 'deleteRetentionPolicyDays')]" + }, + "isVersioningEnabled": { + "value": "[tryGet(parameters('blobServices'), 'isVersioningEnabled')]" + }, + "lastAccessTimeTrackingPolicyEnabled": { + "value": "[tryGet(parameters('blobServices'), 'lastAccessTimeTrackingPolicyEnabled')]" + }, + "restorePolicyEnabled": { + "value": "[tryGet(parameters('blobServices'), 'restorePolicyEnabled')]" + }, + "restorePolicyDays": { + "value": "[tryGet(parameters('blobServices'), 'restorePolicyDays')]" + }, + "diagnosticSettings": { + "value": "[tryGet(parameters('blobServices'), 'diagnosticSettings')]" } }, - "properties": { - "type": "object", - "properties": { - "groupId": { - "type": "string", + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "languageVersion": "2.0", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.39.26.7824", + "templateHash": "4804748808287128942" + }, + "name": "Storage Account blob Services", + "description": "This module deploys a Storage Account Blob Service." + }, + "definitions": { + "corsRuleType": { + "type": "object", + "properties": { + "allowedHeaders": { + "type": "array", + "items": { + "type": "string" + }, + "metadata": { + "description": "Required. A list of headers allowed to be part of the cross-origin request." + } + }, + "allowedMethods": { + "type": "array", + "allowedValues": [ + "CONNECT", + "DELETE", + "GET", + "HEAD", + "MERGE", + "OPTIONS", + "PATCH", + "POST", + "PUT", + "TRACE" + ], + "metadata": { + "description": "Required. A list of HTTP methods that are allowed to be executed by the origin." + } + }, + "allowedOrigins": { + "type": "array", + "items": { + "type": "string" + }, + "metadata": { + "description": "Required. A list of origin domains that will be allowed via CORS, or \"*\" to allow all domains." + } + }, + "exposedHeaders": { + "type": "array", + "items": { + "type": "string" + }, + "metadata": { + "description": "Required. A list of response headers to expose to CORS clients." + } + }, + "maxAgeInSeconds": { + "type": "int", + "metadata": { + "description": "Required. The number of seconds that the client/browser should cache a preflight response." + } + } + }, + "metadata": { + "__bicep_export!": true, + "description": "The type for a cors rule." + } + }, + "containerType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "metadata": { + "description": "Required. The name of the Storage Container to deploy." + } + }, + "defaultEncryptionScope": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Default the container to use specified encryption scope for all writes." + } + }, + "denyEncryptionScopeOverride": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. Block override of encryption scope from the container default." + } + }, + "enableNfsV3AllSquash": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. Enable NFSv3 all squash on blob container." + } + }, + "enableNfsV3RootSquash": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. Enable NFSv3 root squash on blob container." + } + }, + "immutableStorageWithVersioningEnabled": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. This is an immutable property, when set to true it enables object level immutability at the container level. The property is immutable and can only be set to true at the container creation time. Existing containers must undergo a migration process." + } + }, + "immutabilityPolicy": { + "$ref": "#/definitions/immutabilityPolicyType", + "nullable": true, + "metadata": { + "description": "Optional. Configure immutability policy." + } + }, + "metadata": { + "type": "object", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.Storage/storageAccounts/blobServices/containers@2024-01-01#properties/properties/properties/metadata" + }, + "description": "Optional. A name-value pair to associate with the container as metadata." + }, + "nullable": true + }, + "publicAccess": { + "type": "string", + "allowedValues": [ + "Blob", + "Container", + "None" + ], + "nullable": true, + "metadata": { + "description": "Optional. Specifies whether data in the container may be accessed publicly and the level of access." + } + }, + "roleAssignments": { + "type": "array", + "items": { + "$ref": "#/definitions/roleAssignmentType" + }, + "nullable": true, + "metadata": { + "description": "Optional. Array of role assignments to create." + } + } + }, + "metadata": { + "__bicep_export!": true, + "description": "The type of a storage container." + } + }, + "diagnosticSettingFullType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The name of the diagnostic setting." + } + }, + "logCategoriesAndGroups": { + "type": "array", + "items": { + "type": "object", + "properties": { + "category": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Name of a Diagnostic Log category for a resource type this setting is applied to. Set the specific logs to collect here." + } + }, + "categoryGroup": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Name of a Diagnostic Log category group for a resource type this setting is applied to. Set to `allLogs` to collect all logs." + } + }, + "enabled": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. Enable or disable the category explicitly. Default is `true`." + } + } + } + }, + "nullable": true, + "metadata": { + "description": "Optional. The name of logs that will be streamed. \"allLogs\" includes all possible logs for the resource. Set to `[]` to disable log collection." + } + }, + "metricCategories": { + "type": "array", + "items": { + "type": "object", + "properties": { + "category": { + "type": "string", + "metadata": { + "description": "Required. Name of a Diagnostic Metric category for a resource type this setting is applied to. Set to `AllMetrics` to collect all metrics." + } + }, + "enabled": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. Enable or disable the category explicitly. Default is `true`." + } + } + } + }, + "nullable": true, + "metadata": { + "description": "Optional. The name of metrics that will be streamed. \"allMetrics\" includes all possible metrics for the resource. Set to `[]` to disable metric collection." + } + }, + "logAnalyticsDestinationType": { + "type": "string", + "allowedValues": [ + "AzureDiagnostics", + "Dedicated" + ], + "nullable": true, + "metadata": { + "description": "Optional. A string indicating whether the export to Log Analytics should use the default destination type, i.e. AzureDiagnostics, or use a destination type." + } + }, + "workspaceResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Resource ID of the diagnostic log analytics workspace. For security reasons, it is recommended to set diagnostic settings to send data to either storage account, log analytics workspace or event hub." + } + }, + "storageAccountResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Resource ID of the diagnostic storage account. For security reasons, it is recommended to set diagnostic settings to send data to either storage account, log analytics workspace or event hub." + } + }, + "eventHubAuthorizationRuleResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Resource ID of the diagnostic event hub authorization rule for the Event Hubs namespace in which the event hub should be created or streamed to." + } + }, + "eventHubName": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Name of the diagnostic event hub within the namespace to which logs are streamed. Without this, an event hub is created for each log category. For security reasons, it is recommended to set diagnostic settings to send data to either storage account, log analytics workspace or event hub." + } + }, + "marketplacePartnerResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The full ARM resource ID of the Marketplace resource to which you would like to send Diagnostic Logs." + } + } + }, "metadata": { - "description": "Required. The ID of a group obtained from the remote resource that this private endpoint should connect to." + "description": "An AVM-aligned type for a diagnostic setting. To be used if both logs & metrics are supported by the resource provider.", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.1" + } } }, - "memberName": { - "type": "string", + "immutabilityPolicyType": { + "type": "object", + "properties": { + "immutabilityPeriodSinceCreationInDays": { + "type": "int", + "nullable": true, + "metadata": { + "description": "Optional. The immutability period for the blobs in the container since the policy creation, in days." + } + }, + "allowProtectedAppendWrites": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. This property can only be changed for unlocked time-based retention policies. When enabled, new blocks can be written to an append blob while maintaining immutability protection and compliance. Only new blocks can be added and any existing blocks cannot be modified or deleted. This property cannot be changed with ExtendImmutabilityPolicy API." + } + }, + "allowProtectedAppendWritesAll": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. This property can only be changed for unlocked time-based retention policies. When enabled, new blocks can be written to both \"Append and Block Blobs\" while maintaining immutability protection and compliance. Only new blocks can be added and any existing blocks cannot be modified or deleted. This property cannot be changed with ExtendImmutabilityPolicy API. The \"allowProtectedAppendWrites\" and \"allowProtectedAppendWritesAll\" properties are mutually exclusive." + } + } + }, "metadata": { - "description": "Required. The member name of a group obtained from the remote resource that this private endpoint should connect to." + "description": "The type for an immutability policy.", + "__bicep_imported_from!": { + "sourceTemplate": "container/main.bicep" + } } }, - "privateIPAddress": { - "type": "string", - "metadata": { - "description": "Required. A private IP address obtained from the private endpoint's subnet." - } - } - }, - "metadata": { - "description": "Required. Properties of private endpoint IP configurations." - } - } - }, - "metadata": { - "__bicep_imported_from!": { - "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.5.1" - } - } - }, - "_1.privateEndpointPrivateDnsZoneGroupType": { - "type": "object", - "properties": { - "name": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The name of the Private DNS Zone Group." - } - }, - "privateDnsZoneGroupConfigs": { - "type": "array", - "items": { - "type": "object", - "properties": { - "name": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The name of the private DNS Zone Group config." + "roleAssignmentType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The name (as GUID) of the role assignment. If not provided, a GUID will be generated." + } + }, + "roleDefinitionIdOrName": { + "type": "string", + "metadata": { + "description": "Required. The role to assign. You can provide either the display name of the role definition, the role definition GUID, or its fully qualified ID in the following format: '/providers/Microsoft.Authorization/roleDefinitions/c2f4ef07-c644-48eb-af81-4b1b4947fb11'." + } + }, + "principalId": { + "type": "string", + "metadata": { + "description": "Required. The principal ID of the principal (user/group/identity) to assign the role to." + } + }, + "principalType": { + "type": "string", + "allowedValues": [ + "Device", + "ForeignGroup", + "Group", + "ServicePrincipal", + "User" + ], + "nullable": true, + "metadata": { + "description": "Optional. The principal type of the assigned principal ID." + } + }, + "description": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The description of the role assignment." + } + }, + "condition": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The conditions on the role assignment. This limits the resources it can be assigned to. e.g.: @Resource[Microsoft.Storage/storageAccounts/blobServices/containers:ContainerName] StringEqualsIgnoreCase \"foo_storage_container\"." + } + }, + "conditionVersion": { + "type": "string", + "allowedValues": [ + "2.0" + ], + "nullable": true, + "metadata": { + "description": "Optional. Version of the condition." + } + }, + "delegatedManagedIdentityResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The Resource Id of the delegated managed identity resource." + } } }, - "privateDnsZoneResourceId": { - "type": "string", - "metadata": { - "description": "Required. The resource id of the private DNS zone." + "metadata": { + "description": "An AVM-aligned type for a role assignment.", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.1" } } } }, - "metadata": { - "description": "Required. The private DNS Zone Groups to associate the Private Endpoint. A DNS Zone Group can support up to 5 DNS zones." - } - } - }, - "metadata": { - "__bicep_imported_from!": { - "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.5.1" - } - } - }, - "_1.roleAssignmentType": { - "type": "object", - "properties": { - "name": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The name (as GUID) of the role assignment. If not provided, a GUID will be generated." - } - }, - "roleDefinitionIdOrName": { - "type": "string", - "metadata": { - "description": "Required. The role to assign. You can provide either the display name of the role definition, the role definition GUID, or its fully qualified ID in the following format: '/providers/Microsoft.Authorization/roleDefinitions/c2f4ef07-c644-48eb-af81-4b1b4947fb11'." - } - }, - "principalId": { - "type": "string", - "metadata": { - "description": "Required. The principal ID of the principal (user/group/identity) to assign the role to." - } - }, - "principalType": { - "type": "string", - "allowedValues": [ - "Device", - "ForeignGroup", - "Group", - "ServicePrincipal", - "User" - ], - "nullable": true, - "metadata": { - "description": "Optional. The principal type of the assigned principal ID." - } - }, - "description": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The description of the role assignment." - } - }, - "condition": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The conditions on the role assignment. This limits the resources it can be assigned to. e.g.: @Resource[Microsoft.Storage/storageAccounts/blobServices/containers:ContainerName] StringEqualsIgnoreCase \"foo_storage_container\"." - } - }, - "conditionVersion": { - "type": "string", - "allowedValues": [ - "2.0" - ], - "nullable": true, - "metadata": { - "description": "Optional. Version of the condition." - } - }, - "delegatedManagedIdentityResourceId": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The Resource Id of the delegated managed identity resource." - } - } - }, - "metadata": { - "description": "An AVM-aligned type for a role assignment.", - "__bicep_imported_from!": { - "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.5.1" - } - } - }, - "diagnosticSettingFullType": { - "type": "object", - "properties": { - "name": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The name of the diagnostic setting." - } - }, - "logCategoriesAndGroups": { - "type": "array", - "items": { - "type": "object", - "properties": { - "category": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. Name of a Diagnostic Log category for a resource type this setting is applied to. Set the specific logs to collect here." - } - }, - "categoryGroup": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. Name of a Diagnostic Log category group for a resource type this setting is applied to. Set to `allLogs` to collect all logs." - } + "parameters": { + "storageAccountName": { + "type": "string", + "maxLength": 24, + "metadata": { + "description": "Conditional. The name of the parent Storage Account. Required if the template is used in a standalone deployment." + } + }, + "automaticSnapshotPolicyEnabled": { + "type": "bool", + "defaultValue": false, + "metadata": { + "description": "Optional. Automatic Snapshot is enabled if set to true." + } + }, + "changeFeedEnabled": { + "type": "bool", + "defaultValue": false, + "metadata": { + "description": "Optional. The blob service properties for change feed events. Indicates whether change feed event logging is enabled for the Blob service." + } + }, + "changeFeedRetentionInDays": { + "type": "int", + "nullable": true, + "minValue": 1, + "maxValue": 146000, + "metadata": { + "description": "Optional. Indicates whether change feed event logging is enabled for the Blob service. Indicates the duration of changeFeed retention in days. If left blank, it indicates an infinite retention of the change feed." + } + }, + "containerDeleteRetentionPolicyEnabled": { + "type": "bool", + "defaultValue": true, + "metadata": { + "description": "Optional. The blob service properties for container soft delete. Indicates whether DeleteRetentionPolicy is enabled." + } + }, + "containerDeleteRetentionPolicyDays": { + "type": "int", + "nullable": true, + "minValue": 1, + "maxValue": 365, + "metadata": { + "description": "Optional. Indicates the number of days that the deleted item should be retained." + } + }, + "containerDeleteRetentionPolicyAllowPermanentDelete": { + "type": "bool", + "defaultValue": false, + "metadata": { + "description": "Optional. This property when set to true allows deletion of the soft deleted blob versions and snapshots. This property cannot be used with blob restore policy. This property only applies to blob service and does not apply to containers or file share." + } + }, + "corsRules": { + "type": "array", + "items": { + "$ref": "#/definitions/corsRuleType" }, - "enabled": { - "type": "bool", - "nullable": true, - "metadata": { - "description": "Optional. Enable or disable the category explicitly. Default is `true`." - } + "nullable": true, + "metadata": { + "description": "Optional. The List of CORS rules. You can include up to five CorsRule elements in the request." } - } - }, - "nullable": true, - "metadata": { - "description": "Optional. The name of logs that will be streamed. \"allLogs\" includes all possible logs for the resource. Set to `[]` to disable log collection." - } - }, - "metricCategories": { - "type": "array", - "items": { - "type": "object", - "properties": { - "category": { - "type": "string", - "metadata": { - "description": "Required. Name of a Diagnostic Metric category for a resource type this setting is applied to. Set to `AllMetrics` to collect all metrics." - } + }, + "defaultServiceVersion": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Indicates the default version to use for requests to the Blob service if an incoming request's version is not specified. Possible values include version 2008-10-27 and all more recent versions." + } + }, + "deleteRetentionPolicyEnabled": { + "type": "bool", + "defaultValue": true, + "metadata": { + "description": "Optional. The blob service properties for blob soft delete." + } + }, + "deleteRetentionPolicyDays": { + "type": "int", + "defaultValue": 7, + "minValue": 1, + "maxValue": 365, + "metadata": { + "description": "Optional. Indicates the number of days that the deleted blob should be retained." + } + }, + "deleteRetentionPolicyAllowPermanentDelete": { + "type": "bool", + "defaultValue": false, + "metadata": { + "description": "Optional. This property when set to true allows deletion of the soft deleted blob versions and snapshots. This property cannot be used with blob restore policy. This property only applies to blob service and does not apply to containers or file share." + } + }, + "isVersioningEnabled": { + "type": "bool", + "defaultValue": false, + "metadata": { + "description": "Optional. Use versioning to automatically maintain previous versions of your blobs. Cannot be enabled for ADLS Gen2 storage accounts." + } + }, + "lastAccessTimeTrackingPolicyEnabled": { + "type": "bool", + "defaultValue": false, + "metadata": { + "description": "Optional. The blob service property to configure last access time based tracking policy. When set to true last access time based tracking is enabled." + } + }, + "restorePolicyEnabled": { + "type": "bool", + "defaultValue": false, + "metadata": { + "description": "Optional. The blob service properties for blob restore policy. If point-in-time restore is enabled, then versioning, change feed, and blob soft delete must also be enabled." + } + }, + "restorePolicyDays": { + "type": "int", + "defaultValue": 7, + "minValue": 1, + "metadata": { + "description": "Optional. How long this blob can be restored. It should be less than DeleteRetentionPolicy days." + } + }, + "containers": { + "type": "array", + "items": { + "$ref": "#/definitions/containerType" + }, + "nullable": true, + "metadata": { + "description": "Optional. Blob containers to create." + } + }, + "diagnosticSettings": { + "type": "array", + "items": { + "$ref": "#/definitions/diagnosticSettingFullType" }, - "enabled": { - "type": "bool", - "nullable": true, - "metadata": { - "description": "Optional. Enable or disable the category explicitly. Default is `true`." - } + "nullable": true, + "metadata": { + "description": "Optional. The diagnostic settings of the service." } } }, - "nullable": true, - "metadata": { - "description": "Optional. The name of metrics that will be streamed. \"allMetrics\" includes all possible metrics for the resource. Set to `[]` to disable metric collection." - } - }, - "logAnalyticsDestinationType": { - "type": "string", - "allowedValues": [ - "AzureDiagnostics", - "Dedicated" - ], - "nullable": true, - "metadata": { - "description": "Optional. A string indicating whether the export to Log Analytics should use the default destination type, i.e. AzureDiagnostics, or use a destination type." - } - }, - "workspaceResourceId": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. Resource ID of the diagnostic log analytics workspace. For security reasons, it is recommended to set diagnostic settings to send data to either storage account, log analytics workspace or event hub." - } - }, - "storageAccountResourceId": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. Resource ID of the diagnostic storage account. For security reasons, it is recommended to set diagnostic settings to send data to either storage account, log analytics workspace or event hub." - } - }, - "eventHubAuthorizationRuleResourceId": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. Resource ID of the diagnostic event hub authorization rule for the Event Hubs namespace in which the event hub should be created or streamed to." - } - }, - "eventHubName": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. Name of the diagnostic event hub within the namespace to which logs are streamed. Without this, an event hub is created for each log category. For security reasons, it is recommended to set diagnostic settings to send data to either storage account, log analytics workspace or event hub." - } - }, - "marketplacePartnerResourceId": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The full ARM resource ID of the Marketplace resource to which you would like to send Diagnostic Logs." - } - } - }, - "metadata": { - "description": "An AVM-aligned type for a diagnostic setting. To be used if both logs & metrics are supported by the resource provider.", - "__bicep_imported_from!": { - "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.5.1" - } - } - }, - "managedIdentityAllType": { - "type": "object", - "properties": { - "systemAssigned": { - "type": "bool", - "nullable": true, - "metadata": { - "description": "Optional. Enables system assigned managed identity on the resource." - } - }, - "userAssignedResourceIds": { - "type": "array", - "items": { - "type": "string" - }, - "nullable": true, - "metadata": { - "description": "Optional. The resource ID(s) to assign to the resource. Required if a user assigned identity is used for encryption." - } - } - }, - "metadata": { - "description": "An AVM-aligned type for a managed identity configuration. To be used if both a system-assigned & user-assigned identities are supported by the resource provider.", - "__bicep_imported_from!": { - "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.5.1" - } - } - }, - "privateEndpointSingleServiceType": { - "type": "object", - "properties": { - "name": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The name of the Private Endpoint." - } - }, - "location": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The location to deploy the Private Endpoint to." - } - }, - "privateLinkServiceConnectionName": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The name of the private link connection to create." - } - }, - "service": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The subresource to deploy the Private Endpoint for. For example \"vault\" for a Key Vault Private Endpoint." - } - }, - "subnetResourceId": { - "type": "string", - "metadata": { - "description": "Required. Resource ID of the subnet where the endpoint needs to be created." - } - }, - "resourceGroupResourceId": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The resource ID of the Resource Group the Private Endpoint will be created in. If not specified, the Resource Group of the provided Virtual Network Subnet is used." - } - }, - "privateDnsZoneGroup": { - "$ref": "#/definitions/_1.privateEndpointPrivateDnsZoneGroupType", - "nullable": true, - "metadata": { - "description": "Optional. The private DNS Zone Group to configure for the Private Endpoint." - } - }, - "isManualConnection": { - "type": "bool", - "nullable": true, - "metadata": { - "description": "Optional. If Manual Private Link Connection is required." - } - }, - "manualConnectionRequestMessage": { - "type": "string", - "nullable": true, - "maxLength": 140, - "metadata": { - "description": "Optional. A message passed to the owner of the remote resource with the manual connection request." - } - }, - "customDnsConfigs": { - "type": "array", - "items": { - "$ref": "#/definitions/_1.privateEndpointCustomDnsConfigType" - }, - "nullable": true, - "metadata": { - "description": "Optional. Custom DNS configurations." - } - }, - "ipConfigurations": { - "type": "array", - "items": { - "$ref": "#/definitions/_1.privateEndpointIpConfigurationType" - }, - "nullable": true, - "metadata": { - "description": "Optional. A list of IP configurations of the Private Endpoint. This will be used to map to the first-party Service endpoints." - } - }, - "applicationSecurityGroupResourceIds": { - "type": "array", - "items": { - "type": "string" - }, - "nullable": true, - "metadata": { - "description": "Optional. Application security groups in which the Private Endpoint IP configuration is included." - } - }, - "customNetworkInterfaceName": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The custom name of the network interface attached to the Private Endpoint." - } - }, - "lock": { - "$ref": "#/definitions/_1.lockType", - "nullable": true, - "metadata": { - "description": "Optional. Specify the type of lock." - } - }, - "roleAssignments": { - "type": "array", - "items": { - "$ref": "#/definitions/_1.roleAssignmentType" + "variables": { + "enableReferencedModulesTelemetry": false, + "name": "default" }, - "nullable": true, - "metadata": { - "description": "Optional. Array of role assignments to create." - } - }, - "tags": { - "type": "object", - "nullable": true, - "metadata": { - "description": "Optional. Tags to be applied on all resources/Resource Groups in this deployment." - } - }, - "enableTelemetry": { - "type": "bool", - "nullable": true, - "metadata": { - "description": "Optional. Enable/Disable usage telemetry for module." - } - } - }, - "metadata": { - "description": "An AVM-aligned type for a private endpoint. To be used if the private endpoint's default service / groupId can be assumed (i.e., for services that only have one Private Endpoint type like 'vault' for key vault).", - "__bicep_imported_from!": { - "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.5.1" - } - } - } - }, - "parameters": { - "name": { - "type": "string", - "metadata": { - "description": "Required. Name of the site." - } - }, - "location": { - "type": "string", - "defaultValue": "[resourceGroup().location]", - "metadata": { - "description": "Optional. Location for all Resources." - } - }, - "kind": { - "type": "string", - "allowedValues": [ - "functionapp", - "functionapp,linux", - "functionapp,workflowapp", - "functionapp,workflowapp,linux", - "functionapp,linux,container", - "functionapp,linux,container,azurecontainerapps", - "app,linux", - "app", - "linux,api", - "api", - "app,linux,container", - "app,container,windows" - ], - "metadata": { - "description": "Required. Type of site to deploy." - } - }, - "serverFarmResourceId": { - "type": "string", - "metadata": { - "description": "Required. The resource ID of the app service plan to use for the site." - } - }, - "managedEnvironmentId": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. Azure Resource Manager ID of the customers selected Managed Environment on which to host this app." - } - }, - "httpsOnly": { - "type": "bool", - "defaultValue": true, - "metadata": { - "description": "Optional. Configures a site to accept only HTTPS requests." - } - }, - "clientAffinityEnabled": { - "type": "bool", - "defaultValue": true, - "metadata": { - "description": "Optional. If client affinity is enabled." - } - }, - "appServiceEnvironmentResourceId": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The resource ID of the app service environment to use for this resource." - } - }, - "managedIdentities": { - "$ref": "#/definitions/managedIdentityAllType", - "nullable": true, - "metadata": { - "description": "Optional. The managed identity definition for this resource." - } - }, - "keyVaultAccessIdentityResourceId": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The resource ID of the assigned identity to be used to access a key vault with." - } - }, - "storageAccountRequired": { - "type": "bool", - "defaultValue": false, - "metadata": { - "description": "Optional. Checks if Customer provided storage account is required." - } - }, - "enableMonitoring": { - "type": "bool", - "defaultValue": false, - "metadata": { - "description": "Optional. Enable monitoring and logging configuration." - } - }, - "virtualNetworkSubnetId": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. Azure Resource Manager ID of the Virtual network and subnet to be joined by Regional VNET Integration." - } - }, - "vnetContentShareEnabled": { - "type": "bool", - "defaultValue": false, - "metadata": { - "description": "Optional. To enable accessing content over virtual network." - } - }, - "vnetImagePullEnabled": { - "type": "bool", - "defaultValue": false, - "metadata": { - "description": "Optional. To enable pulling image over Virtual Network." - } - }, - "vnetRouteAllEnabled": { - "type": "bool", - "defaultValue": false, - "metadata": { - "description": "Optional. Virtual Network Route All enabled." - } - }, - "scmSiteAlsoStopped": { - "type": "bool", - "defaultValue": false, - "metadata": { - "description": "Optional. Stop SCM (KUDU) site when the app is stopped." - } - }, - "siteConfig": { - "type": "object", - "metadata": { - "__bicep_resource_derived_type!": { - "source": "Microsoft.Web/sites@2024-04-01#properties/properties/properties/siteConfig" - }, - "description": "Optional. The site config object." - }, - "defaultValue": { - "alwaysOn": true, - "minTlsVersion": "1.2", - "ftpsState": "FtpsOnly" - } - }, - "configs": { - "type": "array", - "items": { - "$ref": "#/definitions/appSettingsConfigType" - }, - "nullable": true, - "metadata": { - "description": "Optional. The web site config." - } - }, - "functionAppConfig": { - "type": "object", - "metadata": { - "__bicep_resource_derived_type!": { - "source": "Microsoft.Web/sites@2024-04-01#properties/properties/properties/functionAppConfig" - }, - "description": "Optional. The Function App configuration object." - }, - "nullable": true - }, - "privateEndpoints": { - "type": "array", - "items": { - "$ref": "#/definitions/privateEndpointSingleServiceType" - }, - "nullable": true, - "metadata": { - "description": "Optional. Configuration details for private endpoints." - } - }, - "tags": { - "type": "object", - "nullable": true, - "metadata": { - "description": "Optional. Tags of the resource." - } - }, - "diagnosticSettings": { - "type": "array", - "items": { - "$ref": "#/definitions/diagnosticSettingFullType" - }, - "nullable": true, - "metadata": { - "description": "Optional. The diagnostic settings of the service." - } - }, - "clientCertEnabled": { - "type": "bool", - "defaultValue": false, - "metadata": { - "description": "Optional. To enable client certificate authentication (TLS mutual authentication)." - } - }, - "clientCertExclusionPaths": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. Client certificate authentication comma-separated exclusion paths." - } - }, - "clientCertMode": { - "type": "string", - "defaultValue": "Optional", - "allowedValues": [ - "Optional", - "OptionalInteractiveUser", - "Required" - ], - "metadata": { - "description": "Optional. Client certificate mode." - } - }, - "cloningInfo": { - "type": "object", - "metadata": { - "__bicep_resource_derived_type!": { - "source": "Microsoft.Web/sites@2024-04-01#properties/properties/properties/cloningInfo" - }, - "description": "Optional. If specified during app creation, the app is cloned from a source app." + "resources": { + "storageAccount": { + "existing": true, + "type": "Microsoft.Storage/storageAccounts", + "apiVersion": "2025-01-01", + "name": "[parameters('storageAccountName')]" + }, + "blobServices": { + "type": "Microsoft.Storage/storageAccounts/blobServices", + "apiVersion": "2025-01-01", + "name": "[format('{0}/{1}', parameters('storageAccountName'), variables('name'))]", + "properties": { + "automaticSnapshotPolicyEnabled": "[parameters('automaticSnapshotPolicyEnabled')]", + "changeFeed": "[if(parameters('changeFeedEnabled'), createObject('enabled', true(), 'retentionInDays', parameters('changeFeedRetentionInDays')), null())]", + "containerDeleteRetentionPolicy": { + "enabled": "[parameters('containerDeleteRetentionPolicyEnabled')]", + "days": "[parameters('containerDeleteRetentionPolicyDays')]", + "allowPermanentDelete": "[if(equals(parameters('containerDeleteRetentionPolicyEnabled'), true()), parameters('containerDeleteRetentionPolicyAllowPermanentDelete'), null())]" + }, + "cors": "[if(not(equals(parameters('corsRules'), null())), createObject('corsRules', parameters('corsRules')), null())]", + "defaultServiceVersion": "[parameters('defaultServiceVersion')]", + "deleteRetentionPolicy": { + "enabled": "[parameters('deleteRetentionPolicyEnabled')]", + "days": "[parameters('deleteRetentionPolicyDays')]", + "allowPermanentDelete": "[if(and(parameters('deleteRetentionPolicyEnabled'), parameters('deleteRetentionPolicyAllowPermanentDelete')), true(), null())]" + }, + "isVersioningEnabled": "[parameters('isVersioningEnabled')]", + "lastAccessTimeTrackingPolicy": "[if(and(not(equals(reference('storageAccount', '2025-01-01', 'full').kind, 'Storage')), empty(tryGet(reference('storageAccount', '2025-01-01', 'full'), 'extendedLocation'))), createObject('enable', parameters('lastAccessTimeTrackingPolicyEnabled'), 'name', if(equals(parameters('lastAccessTimeTrackingPolicyEnabled'), true()), 'AccessTimeTracking', null()), 'trackingGranularityInDays', if(equals(parameters('lastAccessTimeTrackingPolicyEnabled'), true()), 1, null())), null())]", + "restorePolicy": "[if(parameters('restorePolicyEnabled'), createObject('enabled', true(), 'days', parameters('restorePolicyDays')), null())]" + }, + "dependsOn": [ + "storageAccount" + ] + }, + "blobServices_diagnosticSettings": { + "copy": { + "name": "blobServices_diagnosticSettings", + "count": "[length(coalesce(parameters('diagnosticSettings'), createArray()))]" + }, + "type": "Microsoft.Insights/diagnosticSettings", + "apiVersion": "2021-05-01-preview", + "scope": "[format('Microsoft.Storage/storageAccounts/{0}/blobServices/{1}', parameters('storageAccountName'), variables('name'))]", + "name": "[coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'name'), format('{0}-diagnosticSettings', variables('name')))]", + "properties": { + "copy": [ + { + "name": "metrics", + "count": "[length(coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'metricCategories'), createArray(createObject('category', 'AllMetrics'))))]", + "input": { + "category": "[coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'metricCategories'), createArray(createObject('category', 'AllMetrics')))[copyIndex('metrics')].category]", + "enabled": "[coalesce(tryGet(coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'metricCategories'), createArray(createObject('category', 'AllMetrics')))[copyIndex('metrics')], 'enabled'), true())]", + "timeGrain": null + } + }, + { + "name": "logs", + "count": "[length(coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'logCategoriesAndGroups'), createArray(createObject('categoryGroup', 'allLogs'))))]", + "input": { + "categoryGroup": "[tryGet(coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'logCategoriesAndGroups'), createArray(createObject('categoryGroup', 'allLogs')))[copyIndex('logs')], 'categoryGroup')]", + "category": "[tryGet(coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'logCategoriesAndGroups'), createArray(createObject('categoryGroup', 'allLogs')))[copyIndex('logs')], 'category')]", + "enabled": "[coalesce(tryGet(coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'logCategoriesAndGroups'), createArray(createObject('categoryGroup', 'allLogs')))[copyIndex('logs')], 'enabled'), true())]" + } + } + ], + "storageAccountId": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'storageAccountResourceId')]", + "workspaceId": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'workspaceResourceId')]", + "eventHubAuthorizationRuleId": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'eventHubAuthorizationRuleResourceId')]", + "eventHubName": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'eventHubName')]", + "marketplacePartnerId": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'marketplacePartnerResourceId')]", + "logAnalyticsDestinationType": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'logAnalyticsDestinationType')]" + }, + "dependsOn": [ + "blobServices" + ] + }, + "blobServices_container": { + "copy": { + "name": "blobServices_container", + "count": "[length(coalesce(parameters('containers'), createArray()))]" + }, + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "[format('{0}-Container-{1}', deployment().name, copyIndex())]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "storageAccountName": { + "value": "[parameters('storageAccountName')]" + }, + "blobServiceName": { + "value": "[variables('name')]" + }, + "name": { + "value": "[coalesce(parameters('containers'), createArray())[copyIndex()].name]" + }, + "defaultEncryptionScope": { + "value": "[tryGet(coalesce(parameters('containers'), createArray())[copyIndex()], 'defaultEncryptionScope')]" + }, + "denyEncryptionScopeOverride": { + "value": "[tryGet(coalesce(parameters('containers'), createArray())[copyIndex()], 'denyEncryptionScopeOverride')]" + }, + "enableNfsV3AllSquash": { + "value": "[tryGet(coalesce(parameters('containers'), createArray())[copyIndex()], 'enableNfsV3AllSquash')]" + }, + "enableNfsV3RootSquash": { + "value": "[tryGet(coalesce(parameters('containers'), createArray())[copyIndex()], 'enableNfsV3RootSquash')]" + }, + "immutableStorageWithVersioningEnabled": { + "value": "[tryGet(coalesce(parameters('containers'), createArray())[copyIndex()], 'immutableStorageWithVersioningEnabled')]" + }, + "metadata": { + "value": "[tryGet(coalesce(parameters('containers'), createArray())[copyIndex()], 'metadata')]" + }, + "publicAccess": { + "value": "[tryGet(coalesce(parameters('containers'), createArray())[copyIndex()], 'publicAccess')]" + }, + "roleAssignments": { + "value": "[tryGet(coalesce(parameters('containers'), createArray())[copyIndex()], 'roleAssignments')]" + }, + "immutabilityPolicy": { + "value": "[tryGet(coalesce(parameters('containers'), createArray())[copyIndex()], 'immutabilityPolicy')]" + }, + "enableTelemetry": { + "value": "[variables('enableReferencedModulesTelemetry')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "languageVersion": "2.0", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.39.26.7824", + "templateHash": "4737601268768949442" + }, + "name": "Storage Account Blob Containers", + "description": "This module deploys a Storage Account Blob Container." + }, + "definitions": { + "immutabilityPolicyType": { + "type": "object", + "properties": { + "immutabilityPeriodSinceCreationInDays": { + "type": "int", + "nullable": true, + "metadata": { + "description": "Optional. The immutability period for the blobs in the container since the policy creation, in days." + } + }, + "allowProtectedAppendWrites": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. This property can only be changed for unlocked time-based retention policies. When enabled, new blocks can be written to an append blob while maintaining immutability protection and compliance. Only new blocks can be added and any existing blocks cannot be modified or deleted. This property cannot be changed with ExtendImmutabilityPolicy API." + } + }, + "allowProtectedAppendWritesAll": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. This property can only be changed for unlocked time-based retention policies. When enabled, new blocks can be written to both \"Append and Block Blobs\" while maintaining immutability protection and compliance. Only new blocks can be added and any existing blocks cannot be modified or deleted. This property cannot be changed with ExtendImmutabilityPolicy API. The \"allowProtectedAppendWrites\" and \"allowProtectedAppendWritesAll\" properties are mutually exclusive." + } + } + }, + "metadata": { + "__bicep_export!": true, + "description": "The type for an immutability policy." + } + }, + "roleAssignmentType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The name (as GUID) of the role assignment. If not provided, a GUID will be generated." + } + }, + "roleDefinitionIdOrName": { + "type": "string", + "metadata": { + "description": "Required. The role to assign. You can provide either the display name of the role definition, the role definition GUID, or its fully qualified ID in the following format: '/providers/Microsoft.Authorization/roleDefinitions/c2f4ef07-c644-48eb-af81-4b1b4947fb11'." + } + }, + "principalId": { + "type": "string", + "metadata": { + "description": "Required. The principal ID of the principal (user/group/identity) to assign the role to." + } + }, + "principalType": { + "type": "string", + "allowedValues": [ + "Device", + "ForeignGroup", + "Group", + "ServicePrincipal", + "User" + ], + "nullable": true, + "metadata": { + "description": "Optional. The principal type of the assigned principal ID." + } + }, + "description": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The description of the role assignment." + } + }, + "condition": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The conditions on the role assignment. This limits the resources it can be assigned to. e.g.: @Resource[Microsoft.Storage/storageAccounts/blobServices/containers:ContainerName] StringEqualsIgnoreCase \"foo_storage_container\"." + } + }, + "conditionVersion": { + "type": "string", + "allowedValues": [ + "2.0" + ], + "nullable": true, + "metadata": { + "description": "Optional. Version of the condition." + } + }, + "delegatedManagedIdentityResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The Resource Id of the delegated managed identity resource." + } + } + }, + "metadata": { + "description": "An AVM-aligned type for a role assignment.", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.1" + } + } + } + }, + "parameters": { + "storageAccountName": { + "type": "string", + "maxLength": 24, + "metadata": { + "description": "Conditional. The name of the parent Storage Account. Required if the template is used in a standalone deployment." + } + }, + "blobServiceName": { + "type": "string", + "defaultValue": "default", + "metadata": { + "description": "Optional. The name of the parent Blob Service. Required if the template is used in a standalone deployment." + } + }, + "name": { + "type": "string", + "metadata": { + "description": "Required. The name of the Storage Container to deploy." + } + }, + "defaultEncryptionScope": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Default the container to use specified encryption scope for all writes." + } + }, + "denyEncryptionScopeOverride": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. Block override of encryption scope from the container default." + } + }, + "enableNfsV3AllSquash": { + "type": "bool", + "defaultValue": false, + "metadata": { + "description": "Optional. Enable NFSv3 all squash on blob container." + } + }, + "enableNfsV3RootSquash": { + "type": "bool", + "defaultValue": false, + "metadata": { + "description": "Optional. Enable NFSv3 root squash on blob container." + } + }, + "immutableStorageWithVersioningEnabled": { + "type": "bool", + "defaultValue": false, + "metadata": { + "description": "Optional. This is an immutable property, when set to true it enables object level immutability at the container level. The property is immutable and can only be set to true at the container creation time. Existing containers must undergo a migration process." + } + }, + "immutabilityPolicy": { + "$ref": "#/definitions/immutabilityPolicyType", + "nullable": true, + "metadata": { + "description": "Optional. Configure immutability policy." + } + }, + "metadata": { + "type": "object", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.Storage/storageAccounts/blobServices/containers@2024-01-01#properties/properties/properties/metadata" + }, + "description": "Optional. A name-value pair to associate with the container as metadata." + }, + "defaultValue": {} + }, + "publicAccess": { + "type": "string", + "defaultValue": "None", + "allowedValues": [ + "Container", + "Blob", + "None" + ], + "metadata": { + "description": "Optional. Specifies whether data in the container may be accessed publicly and the level of access." + } + }, + "enableTelemetry": { + "type": "bool", + "defaultValue": true, + "metadata": { + "description": "Optional. Enable/Disable usage telemetry for module." + } + }, + "roleAssignments": { + "type": "array", + "items": { + "$ref": "#/definitions/roleAssignmentType" + }, + "nullable": true, + "metadata": { + "description": "Optional. Array of role assignments to create." + } + } + }, + "variables": { + "copy": [ + { + "name": "formattedRoleAssignments", + "count": "[length(coalesce(parameters('roleAssignments'), createArray()))]", + "input": "[union(coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')], createObject('roleDefinitionId', coalesce(tryGet(variables('builtInRoleNames'), coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName), if(contains(coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName, '/providers/Microsoft.Authorization/roleDefinitions/'), coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName, subscriptionResourceId('Microsoft.Authorization/roleDefinitions', coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName)))))]" + } + ], + "builtInRoleNames": { + "Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'b24988ac-6180-42a0-ab88-20f7382dd24c')]", + "Owner": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '8e3af657-a8ff-443c-a75c-2fe8c4bcb635')]", + "Reader": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'acdd72a7-3385-48ef-bd42-f606fba81ae7')]", + "Reader and Data Access": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'c12c1c16-33a1-487b-954d-41c89c60f349')]", + "Role Based Access Control Administrator": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'f58310d9-a9f6-439a-9e8d-f62e7b41a168')]", + "Storage Account Backup Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'e5e2a7ff-d759-4cd2-bb51-3152d37e2eb1')]", + "Storage Account Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '17d1049b-9a84-46fb-8f53-869881c3d3ab')]", + "Storage Account Key Operator Service Role": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '81a9662b-bebf-436f-a333-f67b29880f12')]", + "Storage Blob Data Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'ba92f5b4-2d11-453d-a403-e96b0029c9fe')]", + "Storage Blob Data Owner": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'b7e6dc6d-f1e8-4753-8033-0f276bb0955b')]", + "Storage Blob Data Reader": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '2a2b9908-6ea1-4ae2-8e65-a410df84e7d1')]", + "Storage Blob Delegator": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'db58b8e5-c6ad-4a2a-8342-4190687cbf4a')]", + "User Access Administrator": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '18d7d88d-d35e-4fb5-a5c3-7773c20a72d9')]" + } + }, + "resources": { + "storageAccount::blobServices": { + "existing": true, + "type": "Microsoft.Storage/storageAccounts/blobServices", + "apiVersion": "2025-01-01", + "name": "[format('{0}/{1}', parameters('storageAccountName'), parameters('blobServiceName'))]" + }, + "avmTelemetry": { + "condition": "[parameters('enableTelemetry')]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2024-03-01", + "name": "[format('46d3xbcp.res.storage-blobcontainer.{0}.{1}', replace('-..--..-', '.', '-'), substring(uniqueString(deployment().name), 0, 4))]", + "properties": { + "mode": "Incremental", + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "resources": [], + "outputs": { + "telemetry": { + "type": "String", + "value": "For more information, see https://aka.ms/avm/TelemetryInfo" + } + } + } + } + }, + "storageAccount": { + "existing": true, + "type": "Microsoft.Storage/storageAccounts", + "apiVersion": "2025-01-01", + "name": "[parameters('storageAccountName')]" + }, + "container": { + "type": "Microsoft.Storage/storageAccounts/blobServices/containers", + "apiVersion": "2025-01-01", + "name": "[format('{0}/{1}/{2}', parameters('storageAccountName'), parameters('blobServiceName'), parameters('name'))]", + "properties": { + "defaultEncryptionScope": "[parameters('defaultEncryptionScope')]", + "denyEncryptionScopeOverride": "[parameters('denyEncryptionScopeOverride')]", + "enableNfsV3AllSquash": "[if(equals(parameters('enableNfsV3AllSquash'), true()), parameters('enableNfsV3AllSquash'), null())]", + "enableNfsV3RootSquash": "[if(equals(parameters('enableNfsV3RootSquash'), true()), parameters('enableNfsV3RootSquash'), null())]", + "immutableStorageWithVersioning": "[if(parameters('immutableStorageWithVersioningEnabled'), createObject('enabled', parameters('immutableStorageWithVersioningEnabled')), null())]", + "metadata": "[parameters('metadata')]", + "publicAccess": "[parameters('publicAccess')]" + } + }, + "container_roleAssignments": { + "copy": { + "name": "container_roleAssignments", + "count": "[length(coalesce(variables('formattedRoleAssignments'), createArray()))]" + }, + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2022-04-01", + "scope": "[format('Microsoft.Storage/storageAccounts/{0}/blobServices/{1}/containers/{2}', parameters('storageAccountName'), parameters('blobServiceName'), parameters('name'))]", + "name": "[coalesce(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'name'), guid(resourceId('Microsoft.Storage/storageAccounts/blobServices/containers', parameters('storageAccountName'), parameters('blobServiceName'), parameters('name')), coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].principalId, coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].roleDefinitionId))]", + "properties": { + "roleDefinitionId": "[coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].roleDefinitionId]", + "principalId": "[coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].principalId]", + "description": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'description')]", + "principalType": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'principalType')]", + "condition": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'condition')]", + "conditionVersion": "[if(not(empty(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'condition'))), coalesce(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'conditionVersion'), '2.0'), null())]", + "delegatedManagedIdentityResourceId": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'delegatedManagedIdentityResourceId')]" + }, + "dependsOn": [ + "container" + ] + }, + "container_immutabilityPolicy": { + "condition": "[not(empty(coalesce(parameters('immutabilityPolicy'), createObject())))]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "[take(format('{0}-ImmutPol', deployment().name), 64)]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "storageAccountName": { + "value": "[parameters('storageAccountName')]" + }, + "containerName": { + "value": "[parameters('name')]" + }, + "immutabilityPeriodSinceCreationInDays": { + "value": "[tryGet(parameters('immutabilityPolicy'), 'immutabilityPeriodSinceCreationInDays')]" + }, + "allowProtectedAppendWrites": { + "value": "[tryGet(parameters('immutabilityPolicy'), 'allowProtectedAppendWrites')]" + }, + "allowProtectedAppendWritesAll": { + "value": "[tryGet(parameters('immutabilityPolicy'), 'allowProtectedAppendWritesAll')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.39.26.7824", + "templateHash": "9416359146780945405" + }, + "name": "Storage Account Blob Container Immutability Policies", + "description": "This module deploys a Storage Account Blob Container Immutability Policy." + }, + "parameters": { + "storageAccountName": { + "type": "string", + "maxLength": 24, + "metadata": { + "description": "Conditional. The name of the parent Storage Account. Required if the template is used in a standalone deployment." + } + }, + "containerName": { + "type": "string", + "metadata": { + "description": "Conditional. The name of the parent container to apply the policy to. Required if the template is used in a standalone deployment." + } + }, + "immutabilityPeriodSinceCreationInDays": { + "type": "int", + "defaultValue": 365, + "metadata": { + "description": "Optional. The immutability period for the blobs in the container since the policy creation, in days." + } + }, + "allowProtectedAppendWrites": { + "type": "bool", + "defaultValue": true, + "metadata": { + "description": "Optional. This property can only be changed for unlocked time-based retention policies. When enabled, new blocks can be written to an append blob while maintaining immutability protection and compliance. Only new blocks can be added and any existing blocks cannot be modified or deleted. This property cannot be changed with ExtendImmutabilityPolicy API. The \"allowProtectedAppendWrites\" and \"allowProtectedAppendWritesAll\" properties are mutually exclusive." + } + }, + "allowProtectedAppendWritesAll": { + "type": "bool", + "defaultValue": true, + "metadata": { + "description": "Optional. This property can only be changed for unlocked time-based retention policies. When enabled, new blocks can be written to both \"Append and Block Blobs\" while maintaining immutability protection and compliance. Only new blocks can be added and any existing blocks cannot be modified or deleted. This property cannot be changed with ExtendImmutabilityPolicy API. The \"allowProtectedAppendWrites\" and \"allowProtectedAppendWritesAll\" properties are mutually exclusive." + } + } + }, + "variables": { + "name": "default" + }, + "resources": [ + { + "type": "Microsoft.Storage/storageAccounts/blobServices/containers/immutabilityPolicies", + "apiVersion": "2025-01-01", + "name": "[format('{0}/{1}/{2}/{3}', parameters('storageAccountName'), 'default', parameters('containerName'), variables('name'))]", + "properties": { + "immutabilityPeriodSinceCreationInDays": "[parameters('immutabilityPeriodSinceCreationInDays')]", + "allowProtectedAppendWrites": "[parameters('allowProtectedAppendWrites')]", + "allowProtectedAppendWritesAll": "[parameters('allowProtectedAppendWritesAll')]" + } + } + ], + "outputs": { + "name": { + "type": "string", + "metadata": { + "description": "The name of the deployed immutability policy." + }, + "value": "[variables('name')]" + }, + "resourceId": { + "type": "string", + "metadata": { + "description": "The resource ID of the deployed immutability policy." + }, + "value": "[resourceId('Microsoft.Storage/storageAccounts/blobServices/containers/immutabilityPolicies', parameters('storageAccountName'), 'default', parameters('containerName'), variables('name'))]" + }, + "resourceGroupName": { + "type": "string", + "metadata": { + "description": "The resource group of the deployed immutability policy." + }, + "value": "[resourceGroup().name]" + } + } + } + }, + "dependsOn": [ + "container" + ] + } + }, + "outputs": { + "name": { + "type": "string", + "metadata": { + "description": "The name of the deployed container." + }, + "value": "[parameters('name')]" + }, + "resourceId": { + "type": "string", + "metadata": { + "description": "The resource ID of the deployed container." + }, + "value": "[resourceId('Microsoft.Storage/storageAccounts/blobServices/containers', parameters('storageAccountName'), parameters('blobServiceName'), parameters('name'))]" + }, + "resourceGroupName": { + "type": "string", + "metadata": { + "description": "The resource group of the deployed container." + }, + "value": "[resourceGroup().name]" + } + } + } + }, + "dependsOn": [ + "blobServices" + ] + } + }, + "outputs": { + "name": { + "type": "string", + "metadata": { + "description": "The name of the deployed blob service." + }, + "value": "[variables('name')]" + }, + "resourceId": { + "type": "string", + "metadata": { + "description": "The resource ID of the deployed blob service." + }, + "value": "[resourceId('Microsoft.Storage/storageAccounts/blobServices', parameters('storageAccountName'), variables('name'))]" + }, + "resourceGroupName": { + "type": "string", + "metadata": { + "description": "The name of the deployed blob service." + }, + "value": "[resourceGroup().name]" + } + } + } }, - "nullable": true - }, - "containerSize": { - "type": "int", - "nullable": true, - "metadata": { - "description": "Optional. Size of the function container." - } - }, - "dailyMemoryTimeQuota": { - "type": "int", - "nullable": true, - "metadata": { - "description": "Optional. Maximum allowed daily memory-time quota (applicable on dynamic apps only)." - } - }, - "enabled": { - "type": "bool", - "defaultValue": true, - "metadata": { - "description": "Optional. Setting this value to false disables the app (takes the app offline)." - } + "dependsOn": [ + "storageAccount" + ] }, - "hostNameSslStates": { - "type": "array", - "metadata": { - "__bicep_resource_derived_type!": { - "source": "Microsoft.Web/sites@2024-04-01#properties/properties/properties/hostNameSslStates" + "storageAccount_fileServices": { + "condition": "[not(empty(parameters('fileServices')))]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "[format('{0}-Storage-FileServices', uniqueString(deployment().name, parameters('location')))]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" }, - "description": "Optional. Hostname SSL states are used to manage the SSL bindings for app's hostnames." - }, - "nullable": true - }, - "hyperV": { - "type": "bool", - "defaultValue": false, - "metadata": { - "description": "Optional. Hyper-V sandbox." - } - }, - "redundancyMode": { - "type": "string", - "defaultValue": "None", - "allowedValues": [ - "ActiveActive", - "Failover", - "GeoRedundant", - "Manual", - "None" - ], - "metadata": { - "description": "Optional. Site redundancy mode." - } - }, - "publicNetworkAccess": { - "type": "string", - "nullable": true, - "allowedValues": [ - "Enabled", - "Disabled" - ], - "metadata": { - "description": "Optional. Whether or not public network access is allowed for this resource." - } - }, - "e2eEncryptionEnabled": { - "type": "bool", - "nullable": true, - "metadata": { - "description": "Optional. End to End Encryption Setting." - } - }, - "dnsConfiguration": { - "type": "object", - "metadata": { - "__bicep_resource_derived_type!": { - "source": "Microsoft.Web/sites@2024-04-01#properties/properties/properties/dnsConfiguration" + "mode": "Incremental", + "parameters": { + "storageAccountName": { + "value": "[parameters('name')]" + }, + "diagnosticSettings": { + "value": "[tryGet(parameters('fileServices'), 'diagnosticSettings')]" + }, + "protocolSettings": { + "value": "[tryGet(parameters('fileServices'), 'protocolSettings')]" + }, + "shareDeleteRetentionPolicy": { + "value": "[tryGet(parameters('fileServices'), 'shareDeleteRetentionPolicy')]" + }, + "shares": { + "value": "[tryGet(parameters('fileServices'), 'shares')]" + }, + "corsRules": { + "value": "[tryGet(parameters('fileServices'), 'corsRules')]" + } }, - "description": "Optional. Property to configure various DNS related settings for a site." - }, - "nullable": true - }, - "autoGeneratedDomainNameLabelScope": { - "type": "string", - "nullable": true, - "allowedValues": [ - "NoReuse", - "ResourceGroupReuse", - "SubscriptionReuse", - "TenantReuse" - ], - "metadata": { - "description": "Optional. Specifies the scope of uniqueness for the default hostname during resource creation." - } - } - }, - "variables": { - "formattedUserAssignedIdentities": "[reduce(map(coalesce(tryGet(parameters('managedIdentities'), 'userAssignedResourceIds'), createArray()), lambda('id', createObject(format('{0}', lambdaVariables('id')), createObject()))), createObject(), lambda('cur', 'next', union(lambdaVariables('cur'), lambdaVariables('next'))))]", - "identity": "[if(not(empty(parameters('managedIdentities'))), createObject('type', if(coalesce(tryGet(parameters('managedIdentities'), 'systemAssigned'), false()), if(not(empty(coalesce(tryGet(parameters('managedIdentities'), 'userAssignedResourceIds'), createObject()))), 'SystemAssigned, UserAssigned', 'SystemAssigned'), if(not(empty(coalesce(tryGet(parameters('managedIdentities'), 'userAssignedResourceIds'), createObject()))), 'UserAssigned', 'None')), 'userAssignedIdentities', if(not(empty(variables('formattedUserAssignedIdentities'))), variables('formattedUserAssignedIdentities'), null())), null())]" - }, - "resources": { - "app": { - "type": "Microsoft.Web/sites", - "apiVersion": "2024-04-01", - "name": "[parameters('name')]", - "location": "[parameters('location')]", - "kind": "[parameters('kind')]", - "tags": "[parameters('tags')]", - "identity": "[variables('identity')]", - "properties": { - "managedEnvironmentId": "[if(not(empty(parameters('managedEnvironmentId'))), parameters('managedEnvironmentId'), null())]", - "serverFarmId": "[parameters('serverFarmResourceId')]", - "clientAffinityEnabled": "[parameters('clientAffinityEnabled')]", - "httpsOnly": "[parameters('httpsOnly')]", - "hostingEnvironmentProfile": "[if(not(empty(parameters('appServiceEnvironmentResourceId'))), createObject('id', parameters('appServiceEnvironmentResourceId')), null())]", - "storageAccountRequired": "[parameters('storageAccountRequired')]", - "keyVaultReferenceIdentity": "[parameters('keyVaultAccessIdentityResourceId')]", - "virtualNetworkSubnetId": "[parameters('virtualNetworkSubnetId')]", - "siteConfig": "[parameters('siteConfig')]", - "functionAppConfig": "[parameters('functionAppConfig')]", - "clientCertEnabled": "[parameters('clientCertEnabled')]", - "clientCertExclusionPaths": "[parameters('clientCertExclusionPaths')]", - "clientCertMode": "[parameters('clientCertMode')]", - "cloningInfo": "[parameters('cloningInfo')]", - "containerSize": "[parameters('containerSize')]", - "dailyMemoryTimeQuota": "[parameters('dailyMemoryTimeQuota')]", - "enabled": "[parameters('enabled')]", - "hostNameSslStates": "[parameters('hostNameSslStates')]", - "hyperV": "[parameters('hyperV')]", - "redundancyMode": "[parameters('redundancyMode')]", - "publicNetworkAccess": "[if(not(empty(parameters('publicNetworkAccess'))), parameters('publicNetworkAccess'), if(not(empty(parameters('privateEndpoints'))), 'Disabled', 'Enabled'))]", - "vnetContentShareEnabled": "[parameters('vnetContentShareEnabled')]", - "vnetImagePullEnabled": "[parameters('vnetImagePullEnabled')]", - "vnetRouteAllEnabled": "[parameters('vnetRouteAllEnabled')]", - "scmSiteAlsoStopped": "[parameters('scmSiteAlsoStopped')]", - "endToEndEncryptionEnabled": "[parameters('e2eEncryptionEnabled')]", - "dnsConfiguration": "[parameters('dnsConfiguration')]", - "autoGeneratedDomainNameLabelScope": "[parameters('autoGeneratedDomainNameLabelScope')]" - } - }, - "app_diagnosticSettings": { - "copy": { - "name": "app_diagnosticSettings", - "count": "[length(coalesce(parameters('diagnosticSettings'), createArray()))]" - }, - "type": "Microsoft.Insights/diagnosticSettings", - "apiVersion": "2021-05-01-preview", - "scope": "[format('Microsoft.Web/sites/{0}', parameters('name'))]", - "name": "[coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'name'), format('{0}-diagnosticSettings', parameters('name')))]", - "properties": { - "copy": [ - { - "name": "metrics", - "count": "[length(coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'metricCategories'), createArray(createObject('category', 'AllMetrics'))))]", - "input": { - "category": "[coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'metricCategories'), createArray(createObject('category', 'AllMetrics')))[copyIndex('metrics')].category]", - "enabled": "[coalesce(tryGet(coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'metricCategories'), createArray(createObject('category', 'AllMetrics')))[copyIndex('metrics')], 'enabled'), true())]", - "timeGrain": null + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "languageVersion": "2.0", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.39.26.7824", + "templateHash": "8847095544204825048" + }, + "name": "Storage Account File Share Services", + "description": "This module deploys a Storage Account File Share Service." + }, + "definitions": { + "corsRuleType": { + "type": "object", + "properties": { + "allowedHeaders": { + "type": "array", + "items": { + "type": "string" + }, + "metadata": { + "description": "Required. A list of headers allowed to be part of the cross-origin request." + } + }, + "allowedMethods": { + "type": "array", + "allowedValues": [ + "CONNECT", + "DELETE", + "GET", + "HEAD", + "MERGE", + "OPTIONS", + "PATCH", + "POST", + "PUT", + "TRACE" + ], + "metadata": { + "description": "Required. A list of HTTP methods that are allowed to be executed by the origin." + } + }, + "allowedOrigins": { + "type": "array", + "items": { + "type": "string" + }, + "metadata": { + "description": "Required. A list of origin domains that will be allowed via CORS, or \"*\" to allow all domains." + } + }, + "exposedHeaders": { + "type": "array", + "items": { + "type": "string" + }, + "metadata": { + "description": "Required. A list of response headers to expose to CORS clients." + } + }, + "maxAgeInSeconds": { + "type": "int", + "metadata": { + "description": "Required. The number of seconds that the client/browser should cache a preflight response." + } + } + }, + "metadata": { + "__bicep_export!": true, + "description": "The type for a cors rule." + } + }, + "fileShareType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "metadata": { + "description": "Required. The name of the file share." + } + }, + "accessTier": { + "type": "string", + "allowedValues": [ + "Cool", + "Hot", + "Premium", + "TransactionOptimized" + ], + "nullable": true, + "metadata": { + "description": "Optional. Access tier for specific share. Required if the Storage Account kind is set to FileStorage (should be set to \"Premium\"). GpV2 account can choose between TransactionOptimized (default), Hot, and Cool." + } + }, + "enabledProtocols": { + "type": "string", + "allowedValues": [ + "NFS", + "SMB" + ], + "nullable": true, + "metadata": { + "description": "Optional. The authentication protocol that is used for the file share. Can only be specified when creating a share." + } + }, + "rootSquash": { + "type": "string", + "allowedValues": [ + "AllSquash", + "NoRootSquash", + "RootSquash" + ], + "nullable": true, + "metadata": { + "description": "Optional. Permissions for NFS file shares are enforced by the client OS rather than the Azure Files service. Toggling the root squash behavior reduces the rights of the root user for NFS shares." + } + }, + "shareQuota": { + "type": "int", + "nullable": true, + "metadata": { + "description": "Optional. The maximum size of the share, in gigabytes. Must be greater than 0, and less than or equal to 5120 (5TB). For Large File Shares, the maximum size is 102400 (100TB)." + } + }, + "roleAssignments": { + "type": "array", + "items": { + "$ref": "#/definitions/roleAssignmentType" + }, + "nullable": true, + "metadata": { + "description": "Optional. Array of role assignments to create." + } + } + }, + "metadata": { + "__bicep_export!": true, + "description": "The type for a file share." + } + }, + "diagnosticSettingFullType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The name of the diagnostic setting." + } + }, + "logCategoriesAndGroups": { + "type": "array", + "items": { + "type": "object", + "properties": { + "category": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Name of a Diagnostic Log category for a resource type this setting is applied to. Set the specific logs to collect here." + } + }, + "categoryGroup": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Name of a Diagnostic Log category group for a resource type this setting is applied to. Set to `allLogs` to collect all logs." + } + }, + "enabled": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. Enable or disable the category explicitly. Default is `true`." + } + } + } + }, + "nullable": true, + "metadata": { + "description": "Optional. The name of logs that will be streamed. \"allLogs\" includes all possible logs for the resource. Set to `[]` to disable log collection." + } + }, + "metricCategories": { + "type": "array", + "items": { + "type": "object", + "properties": { + "category": { + "type": "string", + "metadata": { + "description": "Required. Name of a Diagnostic Metric category for a resource type this setting is applied to. Set to `AllMetrics` to collect all metrics." + } + }, + "enabled": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. Enable or disable the category explicitly. Default is `true`." + } + } + } + }, + "nullable": true, + "metadata": { + "description": "Optional. The name of metrics that will be streamed. \"allMetrics\" includes all possible metrics for the resource. Set to `[]` to disable metric collection." + } + }, + "logAnalyticsDestinationType": { + "type": "string", + "allowedValues": [ + "AzureDiagnostics", + "Dedicated" + ], + "nullable": true, + "metadata": { + "description": "Optional. A string indicating whether the export to Log Analytics should use the default destination type, i.e. AzureDiagnostics, or use a destination type." + } + }, + "workspaceResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Resource ID of the diagnostic log analytics workspace. For security reasons, it is recommended to set diagnostic settings to send data to either storage account, log analytics workspace or event hub." + } + }, + "storageAccountResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Resource ID of the diagnostic storage account. For security reasons, it is recommended to set diagnostic settings to send data to either storage account, log analytics workspace or event hub." + } + }, + "eventHubAuthorizationRuleResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Resource ID of the diagnostic event hub authorization rule for the Event Hubs namespace in which the event hub should be created or streamed to." + } + }, + "eventHubName": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Name of the diagnostic event hub within the namespace to which logs are streamed. Without this, an event hub is created for each log category. For security reasons, it is recommended to set diagnostic settings to send data to either storage account, log analytics workspace or event hub." + } + }, + "marketplacePartnerResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The full ARM resource ID of the Marketplace resource to which you would like to send Diagnostic Logs." + } + } + }, + "metadata": { + "description": "An AVM-aligned type for a diagnostic setting. To be used if both logs & metrics are supported by the resource provider.", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.0" + } + } + }, + "roleAssignmentType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The name (as GUID) of the role assignment. If not provided, a GUID will be generated." + } + }, + "roleDefinitionIdOrName": { + "type": "string", + "metadata": { + "description": "Required. The role to assign. You can provide either the display name of the role definition, the role definition GUID, or its fully qualified ID in the following format: '/providers/Microsoft.Authorization/roleDefinitions/c2f4ef07-c644-48eb-af81-4b1b4947fb11'." + } + }, + "principalId": { + "type": "string", + "metadata": { + "description": "Required. The principal ID of the principal (user/group/identity) to assign the role to." + } + }, + "principalType": { + "type": "string", + "allowedValues": [ + "Device", + "ForeignGroup", + "Group", + "ServicePrincipal", + "User" + ], + "nullable": true, + "metadata": { + "description": "Optional. The principal type of the assigned principal ID." + } + }, + "description": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The description of the role assignment." + } + }, + "condition": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The conditions on the role assignment. This limits the resources it can be assigned to. e.g.: @Resource[Microsoft.Storage/storageAccounts/blobServices/containers:ContainerName] StringEqualsIgnoreCase \"foo_storage_container\"." + } + }, + "conditionVersion": { + "type": "string", + "allowedValues": [ + "2.0" + ], + "nullable": true, + "metadata": { + "description": "Optional. Version of the condition." + } + }, + "delegatedManagedIdentityResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The Resource Id of the delegated managed identity resource." + } + } + }, + "metadata": { + "description": "An AVM-aligned type for a role assignment.", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.1" + } + } + } + }, + "parameters": { + "storageAccountName": { + "type": "string", + "maxLength": 24, + "metadata": { + "description": "Conditional. The name of the parent Storage Account. Required if the template is used in a standalone deployment." + } + }, + "name": { + "type": "string", + "defaultValue": "default", + "metadata": { + "description": "Optional. The name of the file service." + } + }, + "protocolSettings": { + "type": "object", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.Storage/storageAccounts/fileServices@2024-01-01#properties/properties/properties/protocolSettings" + }, + "description": "Optional. Protocol settings for file service." + }, + "defaultValue": {} + }, + "shareDeleteRetentionPolicy": { + "type": "object", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.Storage/storageAccounts/fileServices@2024-01-01#properties/properties/properties/shareDeleteRetentionPolicy" + }, + "description": "Optional. The service properties for soft delete." + }, + "defaultValue": { + "enabled": true, + "days": 7 + } + }, + "corsRules": { + "type": "array", + "items": { + "$ref": "#/definitions/corsRuleType" + }, + "nullable": true, + "metadata": { + "description": "Optional. The List of CORS rules. You can include up to five CorsRule elements in the request." + } + }, + "diagnosticSettings": { + "type": "array", + "items": { + "$ref": "#/definitions/diagnosticSettingFullType" + }, + "nullable": true, + "metadata": { + "description": "Optional. The diagnostic settings of the service." + } + }, + "shares": { + "type": "array", + "items": { + "$ref": "#/definitions/fileShareType" + }, + "nullable": true, + "metadata": { + "description": "Optional. File shares to create." + } + } + }, + "variables": { + "enableReferencedModulesTelemetry": false + }, + "resources": { + "storageAccount": { + "existing": true, + "type": "Microsoft.Storage/storageAccounts", + "apiVersion": "2024-01-01", + "name": "[parameters('storageAccountName')]" + }, + "fileServices": { + "type": "Microsoft.Storage/storageAccounts/fileServices", + "apiVersion": "2024-01-01", + "name": "[format('{0}/{1}', parameters('storageAccountName'), parameters('name'))]", + "properties": { + "cors": "[if(not(equals(parameters('corsRules'), null())), createObject('corsRules', parameters('corsRules')), null())]", + "protocolSettings": "[parameters('protocolSettings')]", + "shareDeleteRetentionPolicy": "[parameters('shareDeleteRetentionPolicy')]" + } + }, + "fileServices_diagnosticSettings": { + "copy": { + "name": "fileServices_diagnosticSettings", + "count": "[length(coalesce(parameters('diagnosticSettings'), createArray()))]" + }, + "type": "Microsoft.Insights/diagnosticSettings", + "apiVersion": "2021-05-01-preview", + "scope": "[format('Microsoft.Storage/storageAccounts/{0}/fileServices/{1}', parameters('storageAccountName'), parameters('name'))]", + "name": "[coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'name'), format('{0}-diagnosticSettings', parameters('name')))]", + "properties": { + "copy": [ + { + "name": "metrics", + "count": "[length(coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'metricCategories'), createArray(createObject('category', 'AllMetrics'))))]", + "input": { + "category": "[coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'metricCategories'), createArray(createObject('category', 'AllMetrics')))[copyIndex('metrics')].category]", + "enabled": "[coalesce(tryGet(coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'metricCategories'), createArray(createObject('category', 'AllMetrics')))[copyIndex('metrics')], 'enabled'), true())]", + "timeGrain": null + } + }, + { + "name": "logs", + "count": "[length(coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'logCategoriesAndGroups'), createArray(createObject('categoryGroup', 'allLogs'))))]", + "input": { + "categoryGroup": "[tryGet(coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'logCategoriesAndGroups'), createArray(createObject('categoryGroup', 'allLogs')))[copyIndex('logs')], 'categoryGroup')]", + "category": "[tryGet(coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'logCategoriesAndGroups'), createArray(createObject('categoryGroup', 'allLogs')))[copyIndex('logs')], 'category')]", + "enabled": "[coalesce(tryGet(coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'logCategoriesAndGroups'), createArray(createObject('categoryGroup', 'allLogs')))[copyIndex('logs')], 'enabled'), true())]" + } + } + ], + "storageAccountId": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'storageAccountResourceId')]", + "workspaceId": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'workspaceResourceId')]", + "eventHubAuthorizationRuleId": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'eventHubAuthorizationRuleResourceId')]", + "eventHubName": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'eventHubName')]", + "marketplacePartnerId": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'marketplacePartnerResourceId')]", + "logAnalyticsDestinationType": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'logAnalyticsDestinationType')]" + }, + "dependsOn": [ + "fileServices" + ] + }, + "fileServices_shares": { + "copy": { + "name": "fileServices_shares", + "count": "[length(coalesce(parameters('shares'), createArray()))]" + }, + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "[format('{0}-FileShare-{1}', deployment().name, copyIndex())]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "storageAccountName": { + "value": "[parameters('storageAccountName')]" + }, + "fileServicesName": { + "value": "[parameters('name')]" + }, + "name": { + "value": "[coalesce(parameters('shares'), createArray())[copyIndex()].name]" + }, + "accessTier": { + "value": "[coalesce(tryGet(coalesce(parameters('shares'), createArray())[copyIndex()], 'accessTier'), if(equals(reference('storageAccount', '2024-01-01', 'full').kind, 'FileStorage'), 'Premium', 'TransactionOptimized'))]" + }, + "enabledProtocols": { + "value": "[tryGet(coalesce(parameters('shares'), createArray())[copyIndex()], 'enabledProtocols')]" + }, + "rootSquash": { + "value": "[tryGet(coalesce(parameters('shares'), createArray())[copyIndex()], 'rootSquash')]" + }, + "shareQuota": { + "value": "[tryGet(coalesce(parameters('shares'), createArray())[copyIndex()], 'shareQuota')]" + }, + "roleAssignments": { + "value": "[tryGet(coalesce(parameters('shares'), createArray())[copyIndex()], 'roleAssignments')]" + }, + "enableTelemetry": { + "value": "[variables('enableReferencedModulesTelemetry')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "languageVersion": "2.0", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.39.26.7824", + "templateHash": "1953115828549574279" + }, + "name": "Storage Account File Shares", + "description": "This module deploys a Storage Account File Share." + }, + "definitions": { + "roleAssignmentType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The name (as GUID) of the role assignment. If not provided, a GUID will be generated." + } + }, + "roleDefinitionIdOrName": { + "type": "string", + "metadata": { + "description": "Required. The role to assign. You can provide either the display name of the role definition, the role definition GUID, or its fully qualified ID in the following format: '/providers/Microsoft.Authorization/roleDefinitions/c2f4ef07-c644-48eb-af81-4b1b4947fb11'." + } + }, + "principalId": { + "type": "string", + "metadata": { + "description": "Required. The principal ID of the principal (user/group/identity) to assign the role to." + } + }, + "principalType": { + "type": "string", + "allowedValues": [ + "Device", + "ForeignGroup", + "Group", + "ServicePrincipal", + "User" + ], + "nullable": true, + "metadata": { + "description": "Optional. The principal type of the assigned principal ID." + } + }, + "description": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The description of the role assignment." + } + }, + "condition": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The conditions on the role assignment. This limits the resources it can be assigned to. e.g.: @Resource[Microsoft.Storage/storageAccounts/blobServices/containers:ContainerName] StringEqualsIgnoreCase \"foo_storage_container\"." + } + }, + "conditionVersion": { + "type": "string", + "allowedValues": [ + "2.0" + ], + "nullable": true, + "metadata": { + "description": "Optional. Version of the condition." + } + }, + "delegatedManagedIdentityResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The Resource Id of the delegated managed identity resource." + } + } + }, + "metadata": { + "description": "An AVM-aligned type for a role assignment.", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.1" + } + } + } + }, + "parameters": { + "storageAccountName": { + "type": "string", + "maxLength": 24, + "metadata": { + "description": "Conditional. The name of the parent Storage Account. Required if the template is used in a standalone deployment." + } + }, + "fileServicesName": { + "type": "string", + "defaultValue": "default", + "metadata": { + "description": "Conditional. The name of the parent file service. Required if the template is used in a standalone deployment." + } + }, + "name": { + "type": "string", + "metadata": { + "description": "Required. The name of the file share to create." + } + }, + "accessTier": { + "type": "string", + "defaultValue": "TransactionOptimized", + "allowedValues": [ + "Premium", + "Hot", + "Cool", + "TransactionOptimized" + ], + "metadata": { + "description": "Conditional. Access tier for specific share. Required if the Storage Account kind is set to FileStorage (should be set to \"Premium\"). GpV2 account can choose between TransactionOptimized (default), Hot, and Cool." + } + }, + "shareQuota": { + "type": "int", + "defaultValue": 5120, + "metadata": { + "description": "Optional. The maximum size of the share, in gigabytes. Must be greater than 0, and less than or equal to 5120 (5TB). For Large File Shares, the maximum size is 102400 (100TB)." + } + }, + "enabledProtocols": { + "type": "string", + "defaultValue": "SMB", + "allowedValues": [ + "NFS", + "SMB" + ], + "metadata": { + "description": "Optional. The authentication protocol that is used for the file share. Can only be specified when creating a share." + } + }, + "rootSquash": { + "type": "string", + "defaultValue": "NoRootSquash", + "allowedValues": [ + "AllSquash", + "NoRootSquash", + "RootSquash" + ], + "metadata": { + "description": "Optional. Permissions for NFS file shares are enforced by the client OS rather than the Azure Files service. Toggling the root squash behavior reduces the rights of the root user for NFS shares." + } + }, + "enableTelemetry": { + "type": "bool", + "defaultValue": true, + "metadata": { + "description": "Optional. Enable/Disable usage telemetry for module." + } + }, + "roleAssignments": { + "type": "array", + "items": { + "$ref": "#/definitions/roleAssignmentType" + }, + "nullable": true, + "metadata": { + "description": "Optional. Array of role assignments to create." + } + } + }, + "variables": { + "copy": [ + { + "name": "formattedRoleAssignments", + "count": "[length(coalesce(parameters('roleAssignments'), createArray()))]", + "input": "[union(coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')], createObject('roleDefinitionId', coalesce(tryGet(variables('builtInRoleNames'), coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName), if(contains(coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName, '/providers/Microsoft.Authorization/roleDefinitions/'), coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName, subscriptionResourceId('Microsoft.Authorization/roleDefinitions', coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName)))))]" + } + ], + "builtInRoleNames": { + "Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'b24988ac-6180-42a0-ab88-20f7382dd24c')]", + "Owner": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '8e3af657-a8ff-443c-a75c-2fe8c4bcb635')]", + "Reader": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'acdd72a7-3385-48ef-bd42-f606fba81ae7')]", + "Reader and Data Access": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'c12c1c16-33a1-487b-954d-41c89c60f349')]", + "Role Based Access Control Administrator": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'f58310d9-a9f6-439a-9e8d-f62e7b41a168')]", + "Storage Account Backup Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'e5e2a7ff-d759-4cd2-bb51-3152d37e2eb1')]", + "Storage Account Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '17d1049b-9a84-46fb-8f53-869881c3d3ab')]", + "Storage Account Key Operator Service Role": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '81a9662b-bebf-436f-a333-f67b29880f12')]", + "Storage File Data SMB Share Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '0c867c2a-1d8c-454a-a3db-ab2ea1bdc8bb')]", + "Storage File Data SMB Share Elevated Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'a7264617-510b-434b-a828-9731dc254ea7')]", + "Storage File Data SMB Share Reader": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'aba4ae5f-2193-4029-9191-0cb91df5e314')]", + "User Access Administrator": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '18d7d88d-d35e-4fb5-a5c3-7773c20a72d9')]" + } + }, + "resources": { + "storageAccount::fileService": { + "existing": true, + "type": "Microsoft.Storage/storageAccounts/fileServices", + "apiVersion": "2024-01-01", + "name": "[format('{0}/{1}', parameters('storageAccountName'), parameters('fileServicesName'))]" + }, + "avmTelemetry": { + "condition": "[parameters('enableTelemetry')]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2024-03-01", + "name": "[format('46d3xbcp.res.storage-fileshare.{0}.{1}', replace('-..--..-', '.', '-'), substring(uniqueString(deployment().name), 0, 4))]", + "properties": { + "mode": "Incremental", + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "resources": [], + "outputs": { + "telemetry": { + "type": "String", + "value": "For more information, see https://aka.ms/avm/TelemetryInfo" + } + } + } + } + }, + "storageAccount": { + "existing": true, + "type": "Microsoft.Storage/storageAccounts", + "apiVersion": "2024-01-01", + "name": "[parameters('storageAccountName')]" + }, + "fileShare": { + "type": "Microsoft.Storage/storageAccounts/fileServices/shares", + "apiVersion": "2024-01-01", + "name": "[format('{0}/{1}/{2}', parameters('storageAccountName'), parameters('fileServicesName'), parameters('name'))]", + "properties": { + "accessTier": "[parameters('accessTier')]", + "shareQuota": "[parameters('shareQuota')]", + "rootSquash": "[if(equals(parameters('enabledProtocols'), 'NFS'), parameters('rootSquash'), null())]", + "enabledProtocols": "[parameters('enabledProtocols')]" + } + }, + "fileShare_roleAssignments": { + "copy": { + "name": "fileShare_roleAssignments", + "count": "[length(coalesce(variables('formattedRoleAssignments'), createArray()))]" + }, + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "[format('{0}-Share-Rbac-{1}', uniqueString(deployment().name), copyIndex())]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "scope": { + "value": "[replace(resourceId('Microsoft.Storage/storageAccounts/fileServices/shares', parameters('storageAccountName'), parameters('fileServicesName'), parameters('name')), '/shares/', '/fileshares/')]" + }, + "name": { + "value": "[coalesce(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'name'), guid(resourceId('Microsoft.Storage/storageAccounts/fileServices/shares', parameters('storageAccountName'), parameters('fileServicesName'), parameters('name')), coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].principalId, coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].roleDefinitionId))]" + }, + "roleDefinitionId": { + "value": "[coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].roleDefinitionId]" + }, + "principalId": { + "value": "[coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].principalId]" + }, + "principalType": { + "value": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'principalType')]" + }, + "condition": { + "value": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'condition')]" + }, + "conditionVersion": "[if(not(empty(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'condition'))), createObject('value', coalesce(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'conditionVersion'), '2.0')), createObject('value', null()))]", + "delegatedManagedIdentityResourceId": { + "value": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'delegatedManagedIdentityResourceId')]" + }, + "description": { + "value": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'description')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "scope": { + "type": "string", + "metadata": { + "description": "Required. The scope to deploy the role assignment to." + } + }, + "name": { + "type": "string", + "metadata": { + "description": "Required. The name of the role assignment." + } + }, + "roleDefinitionId": { + "type": "string", + "metadata": { + "description": "Required. The role definition Id to assign." + } + }, + "principalId": { + "type": "string", + "metadata": { + "description": "Required. The principal ID of the principal (user/group/identity) to assign the role to." + } + }, + "principalType": { + "type": "string", + "allowedValues": [ + "Device", + "ForeignGroup", + "Group", + "ServicePrincipal", + "User", + "" + ], + "defaultValue": "", + "metadata": { + "description": "Optional. The principal type of the assigned principal ID." + } + }, + "description": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "Optional. The description of the role assignment." + } + }, + "condition": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "Optional. The conditions on the role assignment. This limits the resources it can be assigned to. e.g.: @Resource[Microsoft.Storage/storageAccounts/blobServices/containers:ContainerName] StringEqualsIgnoreCase \"foo_storage_container\"" + } + }, + "conditionVersion": { + "type": "string", + "allowedValues": [ + "2.0" + ], + "defaultValue": "2.0", + "metadata": { + "description": "Optional. Version of the condition." + } + }, + "delegatedManagedIdentityResourceId": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "Optional. The Resource Id of the delegated managed identity resource." + } + } + }, + "resources": [ + { + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2022-04-01", + "scope": "[parameters('scope')]", + "name": "[parameters('name')]", + "properties": { + "roleDefinitionId": "[parameters('roleDefinitionId')]", + "principalId": "[parameters('principalId')]", + "description": "[parameters('description')]", + "principalType": "[if(not(empty(parameters('principalType'))), parameters('principalType'), null())]", + "condition": "[if(not(empty(parameters('condition'))), parameters('condition'), null())]", + "conditionVersion": "[if(and(not(empty(parameters('conditionVersion'))), not(empty(parameters('condition')))), parameters('conditionVersion'), null())]", + "delegatedManagedIdentityResourceId": "[if(not(empty(parameters('delegatedManagedIdentityResourceId'))), parameters('delegatedManagedIdentityResourceId'), null())]" + } + } + ] + } + }, + "dependsOn": [ + "fileShare" + ] + } + }, + "outputs": { + "name": { + "type": "string", + "metadata": { + "description": "The name of the deployed file share." + }, + "value": "[parameters('name')]" + }, + "resourceId": { + "type": "string", + "metadata": { + "description": "The resource ID of the deployed file share." + }, + "value": "[resourceId('Microsoft.Storage/storageAccounts/fileServices/shares', parameters('storageAccountName'), parameters('fileServicesName'), parameters('name'))]" + }, + "resourceGroupName": { + "type": "string", + "metadata": { + "description": "The resource group of the deployed file share." + }, + "value": "[resourceGroup().name]" + } + } + } + }, + "dependsOn": [ + "fileServices", + "storageAccount" + ] } }, - { - "name": "logs", - "count": "[length(coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'logCategoriesAndGroups'), createArray(createObject('categoryGroup', 'allLogs'))))]", - "input": { - "categoryGroup": "[tryGet(coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'logCategoriesAndGroups'), createArray(createObject('categoryGroup', 'allLogs')))[copyIndex('logs')], 'categoryGroup')]", - "category": "[tryGet(coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'logCategoriesAndGroups'), createArray(createObject('categoryGroup', 'allLogs')))[copyIndex('logs')], 'category')]", - "enabled": "[coalesce(tryGet(coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'logCategoriesAndGroups'), createArray(createObject('categoryGroup', 'allLogs')))[copyIndex('logs')], 'enabled'), true())]" + "outputs": { + "name": { + "type": "string", + "metadata": { + "description": "The name of the deployed file share service." + }, + "value": "[parameters('name')]" + }, + "resourceId": { + "type": "string", + "metadata": { + "description": "The resource ID of the deployed file share service." + }, + "value": "[resourceId('Microsoft.Storage/storageAccounts/fileServices', parameters('storageAccountName'), parameters('name'))]" + }, + "resourceGroupName": { + "type": "string", + "metadata": { + "description": "The resource group of the deployed file share service." + }, + "value": "[resourceGroup().name]" } } - ], - "storageAccountId": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'storageAccountResourceId')]", - "workspaceId": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'workspaceResourceId')]", - "eventHubAuthorizationRuleId": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'eventHubAuthorizationRuleResourceId')]", - "eventHubName": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'eventHubName')]", - "marketplacePartnerId": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'marketplacePartnerResourceId')]", - "logAnalyticsDestinationType": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'logAnalyticsDestinationType')]" + } }, "dependsOn": [ - "app" + "storageAccount" ] }, - "app_config": { - "copy": { - "name": "app_config", - "count": "[length(coalesce(parameters('configs'), createArray()))]" - }, + "storageAccount_queueServices": { + "condition": "[not(empty(parameters('queueServices')))]", "type": "Microsoft.Resources/deployments", "apiVersion": "2025-04-01", - "name": "[format('{0}-Site-Config-{1}', uniqueString(deployment().name, parameters('location')), copyIndex())]", + "name": "[format('{0}-Storage-QueueServices', uniqueString(deployment().name, parameters('location')))]", "properties": { "expressionEvaluationOptions": { "scope": "inner" }, "mode": "Incremental", "parameters": { - "appName": { + "storageAccountName": { "value": "[parameters('name')]" }, - "name": { - "value": "[coalesce(parameters('configs'), createArray())[copyIndex()].name]" - }, - "applicationInsightResourceId": { - "value": "[tryGet(coalesce(parameters('configs'), createArray())[copyIndex()], 'applicationInsightResourceId')]" - }, - "storageAccountResourceId": { - "value": "[tryGet(coalesce(parameters('configs'), createArray())[copyIndex()], 'storageAccountResourceId')]" - }, - "storageAccountUseIdentityAuthentication": { - "value": "[tryGet(coalesce(parameters('configs'), createArray())[copyIndex()], 'storageAccountUseIdentityAuthentication')]" + "diagnosticSettings": { + "value": "[tryGet(parameters('queueServices'), 'diagnosticSettings')]" }, - "properties": { - "value": "[tryGet(coalesce(parameters('configs'), createArray())[copyIndex()], 'properties')]" + "queues": { + "value": "[tryGet(parameters('queueServices'), 'queues')]" }, - "currentAppSettings": "[if(coalesce(tryGet(coalesce(parameters('configs'), createArray())[copyIndex()], 'retainCurrentAppSettings'), and(true(), equals(coalesce(parameters('configs'), createArray())[copyIndex()].name, 'appsettings'))), createObject('value', list(format('{0}/config/appsettings', resourceId('Microsoft.Web/sites', parameters('name'))), '2023-12-01').properties), createObject('value', createObject()))]", - "enableMonitoring": { - "value": "[parameters('enableMonitoring')]" + "corsRules": { + "value": "[tryGet(parameters('queueServices'), 'corsRules')]" } }, "template": { @@ -32276,145 +33315,669 @@ "_generator": { "name": "bicep", "version": "0.39.26.7824", - "templateHash": "13592577410661714505" + "templateHash": "762865197442503763" }, - "name": "Site App Settings", - "description": "This module deploys a Site App Setting." + "name": "Storage Account Queue Services", + "description": "This module deploys a Storage Account Queue Service." }, - "parameters": { - "appName": { - "type": "string", + "definitions": { + "corsRuleType": { + "type": "object", + "properties": { + "allowedHeaders": { + "type": "array", + "items": { + "type": "string" + }, + "metadata": { + "description": "Required. A list of headers allowed to be part of the cross-origin request." + } + }, + "allowedMethods": { + "type": "array", + "allowedValues": [ + "CONNECT", + "DELETE", + "GET", + "HEAD", + "MERGE", + "OPTIONS", + "PATCH", + "POST", + "PUT", + "TRACE" + ], + "metadata": { + "description": "Required. A list of HTTP methods that are allowed to be executed by the origin." + } + }, + "allowedOrigins": { + "type": "array", + "items": { + "type": "string" + }, + "metadata": { + "description": "Required. A list of origin domains that will be allowed via CORS, or \"*\" to allow all domains." + } + }, + "exposedHeaders": { + "type": "array", + "items": { + "type": "string" + }, + "metadata": { + "description": "Required. A list of response headers to expose to CORS clients." + } + }, + "maxAgeInSeconds": { + "type": "int", + "metadata": { + "description": "Required. The number of seconds that the client/browser should cache a preflight response." + } + } + }, "metadata": { - "description": "Conditional. The name of the parent site resource. Required if the template is used in a standalone deployment." + "__bicep_export!": true, + "description": "The type for a cors rule." } }, - "name": { - "type": "string", - "allowedValues": [ - "appsettings", - "authsettings", - "authsettingsV2", - "azurestorageaccounts", - "backup", - "connectionstrings", - "logs", - "metadata", - "pushsettings", - "slotConfigNames", - "web" - ], + "queueType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "metadata": { + "description": "Required. The name of the queue." + } + }, + "metadata": { + "type": "object", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.Storage/storageAccounts/queueServices/queues@2024-01-01#properties/properties/properties/metadata" + }, + "description": "Optional. Metadata to set on the queue." + }, + "nullable": true + }, + "roleAssignments": { + "type": "array", + "items": { + "$ref": "#/definitions/roleAssignmentType" + }, + "nullable": true, + "metadata": { + "description": "Optional. Array of role assignments to create." + } + } + }, "metadata": { - "description": "Required. The name of the config." + "__bicep_export!": true, + "description": "The type for a queue." } }, - "properties": { + "diagnosticSettingFullType": { "type": "object", - "defaultValue": {}, + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The name of the diagnostic setting." + } + }, + "logCategoriesAndGroups": { + "type": "array", + "items": { + "type": "object", + "properties": { + "category": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Name of a Diagnostic Log category for a resource type this setting is applied to. Set the specific logs to collect here." + } + }, + "categoryGroup": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Name of a Diagnostic Log category group for a resource type this setting is applied to. Set to `allLogs` to collect all logs." + } + }, + "enabled": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. Enable or disable the category explicitly. Default is `true`." + } + } + } + }, + "nullable": true, + "metadata": { + "description": "Optional. The name of logs that will be streamed. \"allLogs\" includes all possible logs for the resource. Set to `[]` to disable log collection." + } + }, + "metricCategories": { + "type": "array", + "items": { + "type": "object", + "properties": { + "category": { + "type": "string", + "metadata": { + "description": "Required. Name of a Diagnostic Metric category for a resource type this setting is applied to. Set to `AllMetrics` to collect all metrics." + } + }, + "enabled": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. Enable or disable the category explicitly. Default is `true`." + } + } + } + }, + "nullable": true, + "metadata": { + "description": "Optional. The name of metrics that will be streamed. \"allMetrics\" includes all possible metrics for the resource. Set to `[]` to disable metric collection." + } + }, + "logAnalyticsDestinationType": { + "type": "string", + "allowedValues": [ + "AzureDiagnostics", + "Dedicated" + ], + "nullable": true, + "metadata": { + "description": "Optional. A string indicating whether the export to Log Analytics should use the default destination type, i.e. AzureDiagnostics, or use a destination type." + } + }, + "workspaceResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Resource ID of the diagnostic log analytics workspace. For security reasons, it is recommended to set diagnostic settings to send data to either storage account, log analytics workspace or event hub." + } + }, + "storageAccountResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Resource ID of the diagnostic storage account. For security reasons, it is recommended to set diagnostic settings to send data to either storage account, log analytics workspace or event hub." + } + }, + "eventHubAuthorizationRuleResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Resource ID of the diagnostic event hub authorization rule for the Event Hubs namespace in which the event hub should be created or streamed to." + } + }, + "eventHubName": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Name of the diagnostic event hub within the namespace to which logs are streamed. Without this, an event hub is created for each log category. For security reasons, it is recommended to set diagnostic settings to send data to either storage account, log analytics workspace or event hub." + } + }, + "marketplacePartnerResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The full ARM resource ID of the Marketplace resource to which you would like to send Diagnostic Logs." + } + } + }, "metadata": { - "description": "Optional. The properties of the config." + "description": "An AVM-aligned type for a diagnostic setting. To be used if both logs & metrics are supported by the resource provider.", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.1" + } } }, - "storageAccountUseIdentityAuthentication": { - "type": "bool", - "defaultValue": false, + "roleAssignmentType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The name (as GUID) of the role assignment. If not provided, a GUID will be generated." + } + }, + "roleDefinitionIdOrName": { + "type": "string", + "metadata": { + "description": "Required. The role to assign. You can provide either the display name of the role definition, the role definition GUID, or its fully qualified ID in the following format: '/providers/Microsoft.Authorization/roleDefinitions/c2f4ef07-c644-48eb-af81-4b1b4947fb11'." + } + }, + "principalId": { + "type": "string", + "metadata": { + "description": "Required. The principal ID of the principal (user/group/identity) to assign the role to." + } + }, + "principalType": { + "type": "string", + "allowedValues": [ + "Device", + "ForeignGroup", + "Group", + "ServicePrincipal", + "User" + ], + "nullable": true, + "metadata": { + "description": "Optional. The principal type of the assigned principal ID." + } + }, + "description": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The description of the role assignment." + } + }, + "condition": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The conditions on the role assignment. This limits the resources it can be assigned to. e.g.: @Resource[Microsoft.Storage/storageAccounts/blobServices/containers:ContainerName] StringEqualsIgnoreCase \"foo_storage_container\"." + } + }, + "conditionVersion": { + "type": "string", + "allowedValues": [ + "2.0" + ], + "nullable": true, + "metadata": { + "description": "Optional. Version of the condition." + } + }, + "delegatedManagedIdentityResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The Resource Id of the delegated managed identity resource." + } + } + }, "metadata": { - "description": "Optional. If the provided storage account requires Identity based authentication." + "description": "An AVM-aligned type for a role assignment.", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.1" + } } - }, - "storageAccountResourceId": { + } + }, + "parameters": { + "storageAccountName": { "type": "string", - "nullable": true, + "maxLength": 24, "metadata": { - "description": "Optional. Required if app of kind functionapp. Resource ID of the storage account." + "description": "Conditional. The name of the parent Storage Account. Required if the template is used in a standalone deployment." } }, - "applicationInsightResourceId": { - "type": "string", - "nullable": true, + "queues": { + "type": "array", + "items": { + "$ref": "#/definitions/queueType" + }, + "defaultValue": [], "metadata": { - "description": "Optional. Resource ID of the application insight to leverage for this resource." + "description": "Optional. Queues to create." } }, - "currentAppSettings": { - "type": "object", - "properties": {}, - "additionalProperties": { - "type": "string", - "metadata": { - "description": "Required. The key-values pairs of the current app settings." - } + "corsRules": { + "type": "array", + "items": { + "$ref": "#/definitions/corsRuleType" }, - "defaultValue": {}, + "nullable": true, "metadata": { - "description": "Optional. The current app settings." + "description": "Optional. The List of CORS rules. You can include up to five CorsRule elements in the request." } }, - "enableMonitoring": { - "type": "bool", - "defaultValue": false, + "diagnosticSettings": { + "type": "array", + "items": { + "$ref": "#/definitions/diagnosticSettingFullType" + }, + "nullable": true, "metadata": { - "description": "Optional. Enable monitoring and logging configuration." + "description": "Optional. The diagnostic settings of the service." } } }, "variables": { - "loggingProperties": "[if(and(parameters('enableMonitoring'), equals(parameters('name'), 'logs')), createObject('applicationLogs', createObject('fileSystem', createObject('level', 'Verbose')), 'httpLogs', createObject('fileSystem', createObject('enabled', true(), 'retentionInDays', 3, 'retentionInMb', 100)), 'detailedErrorMessages', createObject('enabled', true()), 'failedRequestsTracing', createObject('enabled', true())), createObject())]" + "name": "default" }, "resources": { - "applicationInsights": { - "condition": "[not(empty(parameters('applicationInsightResourceId')))]", - "existing": true, - "type": "Microsoft.Insights/components", - "apiVersion": "2020-02-02", - "subscriptionId": "[split(parameters('applicationInsightResourceId'), '/')[2]]", - "resourceGroup": "[split(parameters('applicationInsightResourceId'), '/')[4]]", - "name": "[last(split(parameters('applicationInsightResourceId'), '/'))]" - }, "storageAccount": { - "condition": "[not(empty(parameters('storageAccountResourceId')))]", "existing": true, "type": "Microsoft.Storage/storageAccounts", "apiVersion": "2024-01-01", - "subscriptionId": "[split(parameters('storageAccountResourceId'), '/')[2]]", - "resourceGroup": "[split(parameters('storageAccountResourceId'), '/')[4]]", - "name": "[last(split(parameters('storageAccountResourceId'), '/'))]" + "name": "[parameters('storageAccountName')]" }, - "app": { - "existing": true, - "type": "Microsoft.Web/sites", - "apiVersion": "2023-12-01", - "name": "[parameters('appName')]" + "queueServices": { + "type": "Microsoft.Storage/storageAccounts/queueServices", + "apiVersion": "2024-01-01", + "name": "[format('{0}/{1}', parameters('storageAccountName'), variables('name'))]", + "properties": { + "cors": "[if(not(equals(parameters('corsRules'), null())), createObject('corsRules', parameters('corsRules')), null())]" + } }, - "config": { - "type": "Microsoft.Web/sites/config", - "apiVersion": "2024-04-01", - "name": "[format('{0}/{1}', parameters('appName'), parameters('name'))]", - "properties": "[union(parameters('properties'), parameters('currentAppSettings'), if(and(not(empty(parameters('storageAccountResourceId'))), not(parameters('storageAccountUseIdentityAuthentication'))), createObject('AzureWebJobsStorage', format('DefaultEndpointsProtocol=https;AccountName={0};AccountKey={1};EndpointSuffix={2}', last(split(parameters('storageAccountResourceId'), '/')), listKeys('storageAccount', '2024-01-01').keys[0].value, environment().suffixes.storage)), if(and(not(empty(parameters('storageAccountResourceId'))), parameters('storageAccountUseIdentityAuthentication')), createObject('AzureWebJobsStorage__accountName', last(split(parameters('storageAccountResourceId'), '/')), 'AzureWebJobsStorage__blobServiceUri', reference('storageAccount').primaryEndpoints.blob, 'AzureWebJobsStorage__queueServiceUri', reference('storageAccount').primaryEndpoints.queue, 'AzureWebJobsStorage__tableServiceUri', reference('storageAccount').primaryEndpoints.table), createObject())), if(not(empty(parameters('applicationInsightResourceId'))), createObject('APPLICATIONINSIGHTS_CONNECTION_STRING', reference('applicationInsights').ConnectionString), createObject()), variables('loggingProperties'))]", + "queueServices_diagnosticSettings": { + "copy": { + "name": "queueServices_diagnosticSettings", + "count": "[length(coalesce(parameters('diagnosticSettings'), createArray()))]" + }, + "type": "Microsoft.Insights/diagnosticSettings", + "apiVersion": "2021-05-01-preview", + "scope": "[format('Microsoft.Storage/storageAccounts/{0}/queueServices/{1}', parameters('storageAccountName'), variables('name'))]", + "name": "[coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'name'), format('{0}-diagnosticSettings', variables('name')))]", + "properties": { + "copy": [ + { + "name": "metrics", + "count": "[length(coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'metricCategories'), createArray(createObject('category', 'AllMetrics'))))]", + "input": { + "category": "[coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'metricCategories'), createArray(createObject('category', 'AllMetrics')))[copyIndex('metrics')].category]", + "enabled": "[coalesce(tryGet(coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'metricCategories'), createArray(createObject('category', 'AllMetrics')))[copyIndex('metrics')], 'enabled'), true())]", + "timeGrain": null + } + }, + { + "name": "logs", + "count": "[length(coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'logCategoriesAndGroups'), createArray(createObject('categoryGroup', 'allLogs'))))]", + "input": { + "categoryGroup": "[tryGet(coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'logCategoriesAndGroups'), createArray(createObject('categoryGroup', 'allLogs')))[copyIndex('logs')], 'categoryGroup')]", + "category": "[tryGet(coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'logCategoriesAndGroups'), createArray(createObject('categoryGroup', 'allLogs')))[copyIndex('logs')], 'category')]", + "enabled": "[coalesce(tryGet(coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'logCategoriesAndGroups'), createArray(createObject('categoryGroup', 'allLogs')))[copyIndex('logs')], 'enabled'), true())]" + } + } + ], + "storageAccountId": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'storageAccountResourceId')]", + "workspaceId": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'workspaceResourceId')]", + "eventHubAuthorizationRuleId": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'eventHubAuthorizationRuleResourceId')]", + "eventHubName": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'eventHubName')]", + "marketplacePartnerId": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'marketplacePartnerResourceId')]", + "logAnalyticsDestinationType": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'logAnalyticsDestinationType')]" + }, "dependsOn": [ - "applicationInsights", - "storageAccount" + "queueServices" ] + }, + "queueServices_queues": { + "copy": { + "name": "queueServices_queues", + "count": "[length(coalesce(parameters('queues'), createArray()))]" + }, + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "[format('{0}-Queue-{1}', deployment().name, copyIndex())]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "storageAccountName": { + "value": "[parameters('storageAccountName')]" + }, + "name": { + "value": "[coalesce(parameters('queues'), createArray())[copyIndex()].name]" + }, + "metadata": { + "value": "[tryGet(coalesce(parameters('queues'), createArray())[copyIndex()], 'metadata')]" + }, + "roleAssignments": { + "value": "[tryGet(coalesce(parameters('queues'), createArray())[copyIndex()], 'roleAssignments')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "languageVersion": "2.0", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.39.26.7824", + "templateHash": "2653192815476217627" + }, + "name": "Storage Account Queues", + "description": "This module deploys a Storage Account Queue." + }, + "definitions": { + "roleAssignmentType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The name (as GUID) of the role assignment. If not provided, a GUID will be generated." + } + }, + "roleDefinitionIdOrName": { + "type": "string", + "metadata": { + "description": "Required. The role to assign. You can provide either the display name of the role definition, the role definition GUID, or its fully qualified ID in the following format: '/providers/Microsoft.Authorization/roleDefinitions/c2f4ef07-c644-48eb-af81-4b1b4947fb11'." + } + }, + "principalId": { + "type": "string", + "metadata": { + "description": "Required. The principal ID of the principal (user/group/identity) to assign the role to." + } + }, + "principalType": { + "type": "string", + "allowedValues": [ + "Device", + "ForeignGroup", + "Group", + "ServicePrincipal", + "User" + ], + "nullable": true, + "metadata": { + "description": "Optional. The principal type of the assigned principal ID." + } + }, + "description": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The description of the role assignment." + } + }, + "condition": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The conditions on the role assignment. This limits the resources it can be assigned to. e.g.: @Resource[Microsoft.Storage/storageAccounts/blobServices/containers:ContainerName] StringEqualsIgnoreCase \"foo_storage_container\"." + } + }, + "conditionVersion": { + "type": "string", + "allowedValues": [ + "2.0" + ], + "nullable": true, + "metadata": { + "description": "Optional. Version of the condition." + } + }, + "delegatedManagedIdentityResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The Resource Id of the delegated managed identity resource." + } + } + }, + "metadata": { + "description": "An AVM-aligned type for a role assignment.", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.1" + } + } + } + }, + "parameters": { + "storageAccountName": { + "type": "string", + "maxLength": 24, + "metadata": { + "description": "Conditional. The name of the parent Storage Account. Required if the template is used in a standalone deployment." + } + }, + "name": { + "type": "string", + "metadata": { + "description": "Required. The name of the storage queue to deploy." + } + }, + "metadata": { + "type": "object", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.Storage/storageAccounts/queueServices/queues@2024-01-01#properties/properties/properties/metadata" + }, + "description": "Optional. A name-value pair that represents queue metadata." + }, + "defaultValue": {} + }, + "roleAssignments": { + "type": "array", + "items": { + "$ref": "#/definitions/roleAssignmentType" + }, + "nullable": true, + "metadata": { + "description": "Optional. Array of role assignments to create." + } + } + }, + "variables": { + "copy": [ + { + "name": "formattedRoleAssignments", + "count": "[length(coalesce(parameters('roleAssignments'), createArray()))]", + "input": "[union(coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')], createObject('roleDefinitionId', coalesce(tryGet(variables('builtInRoleNames'), coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName), if(contains(coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName, '/providers/Microsoft.Authorization/roleDefinitions/'), coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName, subscriptionResourceId('Microsoft.Authorization/roleDefinitions', coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName)))))]" + } + ], + "builtInRoleNames": { + "Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'b24988ac-6180-42a0-ab88-20f7382dd24c')]", + "Owner": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '8e3af657-a8ff-443c-a75c-2fe8c4bcb635')]", + "Reader": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'acdd72a7-3385-48ef-bd42-f606fba81ae7')]", + "Reader and Data Access": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'c12c1c16-33a1-487b-954d-41c89c60f349')]", + "Role Based Access Control Administrator": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'f58310d9-a9f6-439a-9e8d-f62e7b41a168')]", + "Storage Account Backup Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'e5e2a7ff-d759-4cd2-bb51-3152d37e2eb1')]", + "Storage Account Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '17d1049b-9a84-46fb-8f53-869881c3d3ab')]", + "Storage Account Key Operator Service Role": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '81a9662b-bebf-436f-a333-f67b29880f12')]", + "Storage Queue Data Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '974c5e8b-45b9-4653-ba55-5f855dd0fb88')]", + "Storage Queue Data Message Processor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '8a0f0c08-91a1-4084-bc3d-661d67233fed')]", + "Storage Queue Data Message Sender": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'c6a89b2d-59bc-44d0-9896-0f6e12d7b80a')]", + "Storage Queue Data Reader": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '19e7f393-937e-4f77-808e-94535e297925')]", + "User Access Administrator": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '18d7d88d-d35e-4fb5-a5c3-7773c20a72d9')]" + } + }, + "resources": { + "storageAccount::queueServices": { + "existing": true, + "type": "Microsoft.Storage/storageAccounts/queueServices", + "apiVersion": "2024-01-01", + "name": "[format('{0}/{1}', parameters('storageAccountName'), 'default')]" + }, + "storageAccount": { + "existing": true, + "type": "Microsoft.Storage/storageAccounts", + "apiVersion": "2024-01-01", + "name": "[parameters('storageAccountName')]" + }, + "queue": { + "type": "Microsoft.Storage/storageAccounts/queueServices/queues", + "apiVersion": "2024-01-01", + "name": "[format('{0}/{1}/{2}', parameters('storageAccountName'), 'default', parameters('name'))]", + "properties": { + "metadata": "[parameters('metadata')]" + } + }, + "queue_roleAssignments": { + "copy": { + "name": "queue_roleAssignments", + "count": "[length(coalesce(variables('formattedRoleAssignments'), createArray()))]" + }, + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2022-04-01", + "scope": "[format('Microsoft.Storage/storageAccounts/{0}/queueServices/{1}/queues/{2}', parameters('storageAccountName'), 'default', parameters('name'))]", + "name": "[coalesce(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'name'), guid(resourceId('Microsoft.Storage/storageAccounts/queueServices/queues', parameters('storageAccountName'), 'default', parameters('name')), coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].principalId, coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].roleDefinitionId))]", + "properties": { + "roleDefinitionId": "[coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].roleDefinitionId]", + "principalId": "[coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].principalId]", + "description": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'description')]", + "principalType": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'principalType')]", + "condition": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'condition')]", + "conditionVersion": "[if(not(empty(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'condition'))), coalesce(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'conditionVersion'), '2.0'), null())]", + "delegatedManagedIdentityResourceId": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'delegatedManagedIdentityResourceId')]" + }, + "dependsOn": [ + "queue" + ] + } + }, + "outputs": { + "name": { + "type": "string", + "metadata": { + "description": "The name of the deployed queue." + }, + "value": "[parameters('name')]" + }, + "resourceId": { + "type": "string", + "metadata": { + "description": "The resource ID of the deployed queue." + }, + "value": "[resourceId('Microsoft.Storage/storageAccounts/queueServices/queues', parameters('storageAccountName'), 'default', parameters('name'))]" + }, + "resourceGroupName": { + "type": "string", + "metadata": { + "description": "The resource group of the deployed queue." + }, + "value": "[resourceGroup().name]" + } + } + } + } } }, "outputs": { "name": { "type": "string", "metadata": { - "description": "The name of the site config." + "description": "The name of the deployed queue service." }, - "value": "[parameters('name')]" + "value": "[variables('name')]" }, "resourceId": { "type": "string", "metadata": { - "description": "The resource ID of the site config." + "description": "The resource ID of the deployed queue service." }, - "value": "[resourceId('Microsoft.Web/sites/config', parameters('appName'), parameters('name'))]" + "value": "[resourceId('Microsoft.Storage/storageAccounts/queueServices', parameters('storageAccountName'), variables('name'))]" }, "resourceGroupName": { "type": "string", "metadata": { - "description": "The resource group the site config was deployed into." + "description": "The resource group of the deployed queue service." }, "value": "[resourceGroup().name]" } @@ -32422,62 +33985,31 @@ } }, "dependsOn": [ - "app" + "storageAccount" ] }, - "app_privateEndpoints": { - "copy": { - "name": "app_privateEndpoints", - "count": "[length(coalesce(parameters('privateEndpoints'), createArray()))]" - }, + "storageAccount_tableServices": { + "condition": "[not(empty(parameters('tableServices')))]", "type": "Microsoft.Resources/deployments", "apiVersion": "2025-04-01", - "name": "[format('{0}-app-PrivateEndpoint-{1}', uniqueString(deployment().name, parameters('location')), copyIndex())]", - "subscriptionId": "[split(coalesce(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'resourceGroupResourceId'), resourceGroup().id), '/')[2]]", - "resourceGroup": "[split(coalesce(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'resourceGroupResourceId'), resourceGroup().id), '/')[4]]", + "name": "[format('{0}-Storage-TableServices', uniqueString(deployment().name, parameters('location')))]", "properties": { "expressionEvaluationOptions": { "scope": "inner" }, "mode": "Incremental", "parameters": { - "name": { - "value": "[coalesce(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'name'), format('pep-{0}-{1}-{2}', last(split(resourceId('Microsoft.Web/sites', parameters('name')), '/')), coalesce(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'service'), 'sites'), copyIndex()))]" - }, - "privateLinkServiceConnections": "[if(not(equals(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'isManualConnection'), true())), createObject('value', createArray(createObject('name', coalesce(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'privateLinkServiceConnectionName'), format('{0}-{1}-{2}', last(split(resourceId('Microsoft.Web/sites', parameters('name')), '/')), coalesce(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'service'), 'sites'), copyIndex())), 'properties', createObject('privateLinkServiceId', resourceId('Microsoft.Web/sites', parameters('name')), 'groupIds', createArray(coalesce(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'service'), 'sites')))))), createObject('value', null()))]", - "manualPrivateLinkServiceConnections": "[if(equals(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'isManualConnection'), true()), createObject('value', createArray(createObject('name', coalesce(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'privateLinkServiceConnectionName'), format('{0}-{1}-{2}', last(split(resourceId('Microsoft.Web/sites', parameters('name')), '/')), coalesce(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'service'), 'sites'), copyIndex())), 'properties', createObject('privateLinkServiceId', resourceId('Microsoft.Web/sites', parameters('name')), 'groupIds', createArray(coalesce(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'service'), 'sites')), 'requestMessage', coalesce(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'manualConnectionRequestMessage'), 'Manual approval required.'))))), createObject('value', null()))]", - "subnetResourceId": { - "value": "[coalesce(parameters('privateEndpoints'), createArray())[copyIndex()].subnetResourceId]" - }, - "enableTelemetry": { - "value": false - }, - "location": { - "value": "[coalesce(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'location'), reference(split(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()].subnetResourceId, '/subnets/')[0], '2020-06-01', 'Full').location)]" - }, - "lock": { - "value": "[coalesce(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'lock'), null())]" - }, - "privateDnsZoneGroup": { - "value": "[tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'privateDnsZoneGroup')]" - }, - "roleAssignments": { - "value": "[tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'roleAssignments')]" - }, - "tags": { - "value": "[coalesce(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'tags'), parameters('tags'))]" - }, - "customDnsConfigs": { - "value": "[tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'customDnsConfigs')]" + "storageAccountName": { + "value": "[parameters('name')]" }, - "ipConfigurations": { - "value": "[tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'ipConfigurations')]" + "diagnosticSettings": { + "value": "[tryGet(parameters('tableServices'), 'diagnosticSettings')]" }, - "applicationSecurityGroupResourceIds": { - "value": "[tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'applicationSecurityGroupResourceIds')]" + "tables": { + "value": "[tryGet(parameters('tableServices'), 'tables')]" }, - "customNetworkInterfaceName": { - "value": "[tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'customNetworkInterfaceName')]" + "corsRules": { + "value": "[tryGet(parameters('tableServices'), 'corsRules')]" } }, "template": { @@ -32487,195 +34019,217 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.34.44.8038", - "templateHash": "12389807800450456797" + "version": "0.39.26.7824", + "templateHash": "17140438874562378925" }, - "name": "Private Endpoints", - "description": "This module deploys a Private Endpoint." + "name": "Storage Account Table Services", + "description": "This module deploys a Storage Account Table Service." }, "definitions": { - "privateDnsZoneGroupType": { + "corsRuleType": { "type": "object", "properties": { - "name": { - "type": "string", - "nullable": true, + "allowedHeaders": { + "type": "array", + "items": { + "type": "string" + }, "metadata": { - "description": "Optional. The name of the Private DNS Zone Group." + "description": "Required. A list of headers allowed to be part of the cross-origin request." } }, - "privateDnsZoneGroupConfigs": { + "allowedMethods": { + "type": "array", + "allowedValues": [ + "CONNECT", + "DELETE", + "GET", + "HEAD", + "MERGE", + "OPTIONS", + "PATCH", + "POST", + "PUT", + "TRACE" + ], + "metadata": { + "description": "Required. A list of HTTP methods that are allowed to be executed by the origin." + } + }, + "allowedOrigins": { "type": "array", "items": { - "$ref": "#/definitions/privateDnsZoneGroupConfigType" + "type": "string" }, "metadata": { - "description": "Required. The private DNS zone groups to associate the private endpoint. A DNS zone group can support up to 5 DNS zones." + "description": "Required. A list of origin domains that will be allowed via CORS, or \"*\" to allow all domains." + } + }, + "exposedHeaders": { + "type": "array", + "items": { + "type": "string" + }, + "metadata": { + "description": "Required. A list of response headers to expose to CORS clients." + } + }, + "maxAgeInSeconds": { + "type": "int", + "metadata": { + "description": "Required. The number of seconds that the client/browser should cache a preflight response." } } }, "metadata": { - "__bicep_export!": true + "__bicep_export!": true, + "description": "The type for a cors rule." } }, - "ipConfigurationType": { + "tableType": { "type": "object", "properties": { "name": { "type": "string", "metadata": { - "description": "Required. The name of the resource that is unique within a resource group." + "description": "Required. The name of the table." } }, - "properties": { - "type": "object", - "properties": { - "groupId": { - "type": "string", - "metadata": { - "description": "Required. The ID of a group obtained from the remote resource that this private endpoint should connect to. If used with private link service connection, this property must be defined as empty string." - } - }, - "memberName": { - "type": "string", - "metadata": { - "description": "Required. The member name of a group obtained from the remote resource that this private endpoint should connect to. If used with private link service connection, this property must be defined as empty string." - } - }, - "privateIPAddress": { - "type": "string", - "metadata": { - "description": "Required. A private IP address obtained from the private endpoint's subnet." - } - } + "roleAssignments": { + "type": "array", + "items": { + "$ref": "#/definitions/roleAssignmentType" }, + "nullable": true, "metadata": { - "description": "Required. Properties of private endpoint IP configurations." + "description": "Optional. Array of role assignments to create." } } }, "metadata": { - "__bicep_export!": true + "__bicep_export!": true, + "description": "The type for a table." } }, - "privateLinkServiceConnectionType": { + "diagnosticSettingFullType": { "type": "object", "properties": { "name": { "type": "string", + "nullable": true, "metadata": { - "description": "Required. The name of the private link service connection." + "description": "Optional. The name of the diagnostic setting." } }, - "properties": { - "type": "object", - "properties": { - "groupIds": { - "type": "array", - "items": { - "type": "string" + "logCategoriesAndGroups": { + "type": "array", + "items": { + "type": "object", + "properties": { + "category": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Name of a Diagnostic Log category for a resource type this setting is applied to. Set the specific logs to collect here." + } }, - "metadata": { - "description": "Required. The ID of a group obtained from the remote resource that this private endpoint should connect to. If used with private link service connection, this property must be defined as empty string array `[]`." - } - }, - "privateLinkServiceId": { - "type": "string", - "metadata": { - "description": "Required. The resource id of private link service." + "categoryGroup": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Name of a Diagnostic Log category group for a resource type this setting is applied to. Set to `allLogs` to collect all logs." + } + }, + "enabled": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. Enable or disable the category explicitly. Default is `true`." + } } - }, - "requestMessage": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. A message passed to the owner of the remote resource with this connection request. Restricted to 140 chars." + } + }, + "nullable": true, + "metadata": { + "description": "Optional. The name of logs that will be streamed. \"allLogs\" includes all possible logs for the resource. Set to `[]` to disable log collection." + } + }, + "metricCategories": { + "type": "array", + "items": { + "type": "object", + "properties": { + "category": { + "type": "string", + "metadata": { + "description": "Required. Name of a Diagnostic Metric category for a resource type this setting is applied to. Set to `AllMetrics` to collect all metrics." + } + }, + "enabled": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. Enable or disable the category explicitly. Default is `true`." + } } } }, + "nullable": true, "metadata": { - "description": "Required. Properties of private link service connection." + "description": "Optional. The name of metrics that will be streamed. \"allMetrics\" includes all possible metrics for the resource. Set to `[]` to disable metric collection." } - } - }, - "metadata": { - "__bicep_export!": true - } - }, - "customDnsConfigType": { - "type": "object", - "properties": { - "fqdn": { + }, + "logAnalyticsDestinationType": { "type": "string", + "allowedValues": [ + "AzureDiagnostics", + "Dedicated" + ], "nullable": true, "metadata": { - "description": "Optional. FQDN that resolves to private endpoint IP address." + "description": "Optional. A string indicating whether the export to Log Analytics should use the default destination type, i.e. AzureDiagnostics, or use a destination type." } }, - "ipAddresses": { - "type": "array", - "items": { - "type": "string" - }, + "workspaceResourceId": { + "type": "string", + "nullable": true, "metadata": { - "description": "Required. A list of private IP addresses of the private endpoint." + "description": "Optional. Resource ID of the diagnostic log analytics workspace. For security reasons, it is recommended to set diagnostic settings to send data to either storage account, log analytics workspace or event hub." } - } - }, - "metadata": { - "__bicep_export!": true - } - }, - "lockType": { - "type": "object", - "properties": { - "name": { + }, + "storageAccountResourceId": { "type": "string", "nullable": true, "metadata": { - "description": "Optional. Specify the name of lock." + "description": "Optional. Resource ID of the diagnostic storage account. For security reasons, it is recommended to set diagnostic settings to send data to either storage account, log analytics workspace or event hub." } }, - "kind": { + "eventHubAuthorizationRuleResourceId": { "type": "string", - "allowedValues": [ - "CanNotDelete", - "None", - "ReadOnly" - ], "nullable": true, "metadata": { - "description": "Optional. Specify the type of lock." + "description": "Optional. Resource ID of the diagnostic event hub authorization rule for the Event Hubs namespace in which the event hub should be created or streamed to." } - } - }, - "metadata": { - "description": "An AVM-aligned type for a lock.", - "__bicep_imported_from!": { - "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.5.1" - } - } - }, - "privateDnsZoneGroupConfigType": { - "type": "object", - "properties": { - "name": { + }, + "eventHubName": { "type": "string", "nullable": true, "metadata": { - "description": "Optional. The name of the private DNS zone group config." + "description": "Optional. Name of the diagnostic event hub within the namespace to which logs are streamed. Without this, an event hub is created for each log category. For security reasons, it is recommended to set diagnostic settings to send data to either storage account, log analytics workspace or event hub." } }, - "privateDnsZoneResourceId": { + "marketplacePartnerResourceId": { "type": "string", + "nullable": true, "metadata": { - "description": "Required. The resource id of the private DNS zone." + "description": "Optional. The full ARM resource ID of the Marketplace resource to which you would like to send Diagnostic Logs." } } }, "metadata": { + "description": "An AVM-aligned type for a diagnostic setting. To be used if both logs & metrics are supported by the resource provider.", "__bicep_imported_from!": { - "sourceTemplate": "private-dns-zone-group/main.bicep" + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.1" } } }, @@ -32750,236 +34304,117 @@ "metadata": { "description": "An AVM-aligned type for a role assignment.", "__bicep_imported_from!": { - "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.5.1" + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.1" } } } }, "parameters": { - "name": { - "type": "string", - "metadata": { - "description": "Required. Name of the private endpoint resource to create." - } - }, - "subnetResourceId": { - "type": "string", - "metadata": { - "description": "Required. Resource ID of the subnet where the endpoint needs to be created." - } - }, - "applicationSecurityGroupResourceIds": { - "type": "array", - "items": { - "type": "string" - }, - "nullable": true, - "metadata": { - "description": "Optional. Application security groups in which the private endpoint IP configuration is included." - } - }, - "customNetworkInterfaceName": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The custom name of the network interface attached to the private endpoint." - } - }, - "ipConfigurations": { - "type": "array", - "items": { - "$ref": "#/definitions/ipConfigurationType" - }, - "nullable": true, - "metadata": { - "description": "Optional. A list of IP configurations of the private endpoint. This will be used to map to the First Party Service endpoints." - } - }, - "privateDnsZoneGroup": { - "$ref": "#/definitions/privateDnsZoneGroupType", - "nullable": true, - "metadata": { - "description": "Optional. The private DNS zone group to configure for the private endpoint." - } - }, - "location": { + "storageAccountName": { "type": "string", - "defaultValue": "[resourceGroup().location]", - "metadata": { - "description": "Optional. Location for all Resources." - } - }, - "lock": { - "$ref": "#/definitions/lockType", - "nullable": true, - "metadata": { - "description": "Optional. The lock settings of the service." - } - }, - "roleAssignments": { - "type": "array", - "items": { - "$ref": "#/definitions/roleAssignmentType" - }, - "nullable": true, - "metadata": { - "description": "Optional. Array of role assignments to create." - } - }, - "tags": { - "type": "object", - "nullable": true, + "maxLength": 24, "metadata": { - "description": "Optional. Tags to be applied on all resources/resource groups in this deployment." + "description": "Conditional. The name of the parent Storage Account. Required if the template is used in a standalone deployment." } }, - "customDnsConfigs": { + "tables": { "type": "array", "items": { - "$ref": "#/definitions/customDnsConfigType" + "$ref": "#/definitions/tableType" }, "nullable": true, "metadata": { - "description": "Optional. Custom DNS configurations." + "description": "Optional. Tables to create." } }, - "manualPrivateLinkServiceConnections": { + "corsRules": { "type": "array", "items": { - "$ref": "#/definitions/privateLinkServiceConnectionType" + "$ref": "#/definitions/corsRuleType" }, "nullable": true, "metadata": { - "description": "Conditional. A grouping of information about the connection to the remote resource. Used when the network admin does not have access to approve connections to the remote resource. Required if `privateLinkServiceConnections` is empty." + "description": "Optional. The List of CORS rules. You can include up to five CorsRule elements in the request." } }, - "privateLinkServiceConnections": { + "diagnosticSettings": { "type": "array", "items": { - "$ref": "#/definitions/privateLinkServiceConnectionType" + "$ref": "#/definitions/diagnosticSettingFullType" }, "nullable": true, "metadata": { - "description": "Conditional. A grouping of information about the connection to the remote resource. Required if `manualPrivateLinkServiceConnections` is empty." - } - }, - "enableTelemetry": { - "type": "bool", - "defaultValue": true, - "metadata": { - "description": "Optional. Enable/Disable usage telemetry for module." + "description": "Optional. The diagnostic settings of the service." } } }, "variables": { - "copy": [ - { - "name": "formattedRoleAssignments", - "count": "[length(coalesce(parameters('roleAssignments'), createArray()))]", - "input": "[union(coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')], createObject('roleDefinitionId', coalesce(tryGet(variables('builtInRoleNames'), coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName), if(contains(coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName, '/providers/Microsoft.Authorization/roleDefinitions/'), coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName, subscriptionResourceId('Microsoft.Authorization/roleDefinitions', coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName)))))]" - } - ], - "builtInRoleNames": { - "Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'b24988ac-6180-42a0-ab88-20f7382dd24c')]", - "DNS Resolver Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '0f2ebee7-ffd4-4fc0-b3b7-664099fdad5d')]", - "DNS Zone Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'befefa01-2a29-4197-83a8-272ff33ce314')]", - "Domain Services Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'eeaeda52-9324-47f6-8069-5d5bade478b2')]", - "Domain Services Reader": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '361898ef-9ed1-48c2-849c-a832951106bb')]", - "Network Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '4d97b98b-1d4f-4787-a291-c67834d212e7')]", - "Owner": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '8e3af657-a8ff-443c-a75c-2fe8c4bcb635')]", - "Private DNS Zone Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'b12aa53e-6015-4669-85d0-8515ebb3ae7f')]", - "Reader": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'acdd72a7-3385-48ef-bd42-f606fba81ae7')]", - "Role Based Access Control Administrator": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'f58310d9-a9f6-439a-9e8d-f62e7b41a168')]" - } + "name": "default" }, "resources": { - "avmTelemetry": { - "condition": "[parameters('enableTelemetry')]", - "type": "Microsoft.Resources/deployments", - "apiVersion": "2024-03-01", - "name": "[format('46d3xbcp.res.network-privateendpoint.{0}.{1}', replace('0.11.0', '.', '-'), substring(uniqueString(deployment().name, parameters('location')), 0, 4))]", + "storageAccount": { + "existing": true, + "type": "Microsoft.Storage/storageAccounts", + "apiVersion": "2024-01-01", + "name": "[parameters('storageAccountName')]" + }, + "tableServices": { + "type": "Microsoft.Storage/storageAccounts/tableServices", + "apiVersion": "2024-01-01", + "name": "[format('{0}/{1}', parameters('storageAccountName'), variables('name'))]", "properties": { - "mode": "Incremental", - "template": { - "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", - "contentVersion": "1.0.0.0", - "resources": [], - "outputs": { - "telemetry": { - "type": "String", - "value": "For more information, see https://aka.ms/avm/TelemetryInfo" - } - } - } + "cors": "[if(not(equals(parameters('corsRules'), null())), createObject('corsRules', parameters('corsRules')), null())]" } }, - "privateEndpoint": { - "type": "Microsoft.Network/privateEndpoints", - "apiVersion": "2024-05-01", - "name": "[parameters('name')]", - "location": "[parameters('location')]", - "tags": "[parameters('tags')]", + "tableServices_diagnosticSettings": { + "copy": { + "name": "tableServices_diagnosticSettings", + "count": "[length(coalesce(parameters('diagnosticSettings'), createArray()))]" + }, + "type": "Microsoft.Insights/diagnosticSettings", + "apiVersion": "2021-05-01-preview", + "scope": "[format('Microsoft.Storage/storageAccounts/{0}/tableServices/{1}', parameters('storageAccountName'), variables('name'))]", + "name": "[coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'name'), format('{0}-diagnosticSettings', variables('name')))]", "properties": { "copy": [ { - "name": "applicationSecurityGroups", - "count": "[length(coalesce(parameters('applicationSecurityGroupResourceIds'), createArray()))]", + "name": "metrics", + "count": "[length(coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'metricCategories'), createArray(createObject('category', 'AllMetrics'))))]", "input": { - "id": "[coalesce(parameters('applicationSecurityGroupResourceIds'), createArray())[copyIndex('applicationSecurityGroups')]]" + "category": "[coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'metricCategories'), createArray(createObject('category', 'AllMetrics')))[copyIndex('metrics')].category]", + "enabled": "[coalesce(tryGet(coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'metricCategories'), createArray(createObject('category', 'AllMetrics')))[copyIndex('metrics')], 'enabled'), true())]", + "timeGrain": null + } + }, + { + "name": "logs", + "count": "[length(coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'logCategoriesAndGroups'), createArray(createObject('categoryGroup', 'allLogs'))))]", + "input": { + "categoryGroup": "[tryGet(coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'logCategoriesAndGroups'), createArray(createObject('categoryGroup', 'allLogs')))[copyIndex('logs')], 'categoryGroup')]", + "category": "[tryGet(coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'logCategoriesAndGroups'), createArray(createObject('categoryGroup', 'allLogs')))[copyIndex('logs')], 'category')]", + "enabled": "[coalesce(tryGet(coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'logCategoriesAndGroups'), createArray(createObject('categoryGroup', 'allLogs')))[copyIndex('logs')], 'enabled'), true())]" } } ], - "customDnsConfigs": "[coalesce(parameters('customDnsConfigs'), createArray())]", - "customNetworkInterfaceName": "[coalesce(parameters('customNetworkInterfaceName'), '')]", - "ipConfigurations": "[coalesce(parameters('ipConfigurations'), createArray())]", - "manualPrivateLinkServiceConnections": "[coalesce(parameters('manualPrivateLinkServiceConnections'), createArray())]", - "privateLinkServiceConnections": "[coalesce(parameters('privateLinkServiceConnections'), createArray())]", - "subnet": { - "id": "[parameters('subnetResourceId')]" - } - } - }, - "privateEndpoint_lock": { - "condition": "[and(not(empty(coalesce(parameters('lock'), createObject()))), not(equals(tryGet(parameters('lock'), 'kind'), 'None')))]", - "type": "Microsoft.Authorization/locks", - "apiVersion": "2020-05-01", - "scope": "[format('Microsoft.Network/privateEndpoints/{0}', parameters('name'))]", - "name": "[coalesce(tryGet(parameters('lock'), 'name'), format('lock-{0}', parameters('name')))]", - "properties": { - "level": "[coalesce(tryGet(parameters('lock'), 'kind'), '')]", - "notes": "[if(equals(tryGet(parameters('lock'), 'kind'), 'CanNotDelete'), 'Cannot delete resource or child resources.', 'Cannot delete or modify the resource or child resources.')]" + "storageAccountId": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'storageAccountResourceId')]", + "workspaceId": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'workspaceResourceId')]", + "eventHubAuthorizationRuleId": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'eventHubAuthorizationRuleResourceId')]", + "eventHubName": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'eventHubName')]", + "marketplacePartnerId": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'marketplacePartnerResourceId')]", + "logAnalyticsDestinationType": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'logAnalyticsDestinationType')]" }, "dependsOn": [ - "privateEndpoint" + "tableServices" ] }, - "privateEndpoint_roleAssignments": { + "tableServices_tables": { "copy": { - "name": "privateEndpoint_roleAssignments", - "count": "[length(coalesce(variables('formattedRoleAssignments'), createArray()))]" - }, - "type": "Microsoft.Authorization/roleAssignments", - "apiVersion": "2022-04-01", - "scope": "[format('Microsoft.Network/privateEndpoints/{0}', parameters('name'))]", - "name": "[coalesce(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'name'), guid(resourceId('Microsoft.Network/privateEndpoints', parameters('name')), coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].principalId, coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].roleDefinitionId))]", - "properties": { - "roleDefinitionId": "[coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].roleDefinitionId]", - "principalId": "[coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].principalId]", - "description": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'description')]", - "principalType": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'principalType')]", - "condition": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'condition')]", - "conditionVersion": "[if(not(empty(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'condition'))), coalesce(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'conditionVersion'), '2.0'), null())]", - "delegatedManagedIdentityResourceId": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'delegatedManagedIdentityResourceId')]" + "name": "tableServices_tables", + "count": "[length(coalesce(parameters('tables'), createArray()))]" }, - "dependsOn": [ - "privateEndpoint" - ] - }, - "privateEndpoint_privateDnsZoneGroup": { - "condition": "[not(empty(parameters('privateDnsZoneGroup')))]", "type": "Microsoft.Resources/deployments", - "apiVersion": "2022-09-01", - "name": "[format('{0}-PrivateEndpoint-PrivateDnsZoneGroup', uniqueString(deployment().name))]", + "apiVersion": "2025-04-01", + "name": "[format('{0}-Table-{1}', deployment().name, copyIndex())]", "properties": { "expressionEvaluationOptions": { "scope": "inner" @@ -32987,13 +34422,13 @@ "mode": "Incremental", "parameters": { "name": { - "value": "[tryGet(parameters('privateDnsZoneGroup'), 'name')]" + "value": "[coalesce(parameters('tables'), createArray())[copyIndex()].name]" }, - "privateEndpointName": { - "value": "[parameters('name')]" + "storageAccountName": { + "value": "[parameters('storageAccountName')]" }, - "privateDnsZoneConfigs": { - "value": "[parameters('privateDnsZoneGroup').privateDnsZoneGroupConfigs]" + "roleAssignments": { + "value": "[tryGet(coalesce(parameters('tables'), createArray())[copyIndex()], 'roleAssignments')]" } }, "template": { @@ -33003,2089 +34438,2282 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.34.44.8038", - "templateHash": "13997305779829540948" + "version": "0.39.26.7824", + "templateHash": "11466809443516053137" }, - "name": "Private Endpoint Private DNS Zone Groups", - "description": "This module deploys a Private Endpoint Private DNS Zone Group." + "name": "Storage Account Table", + "description": "This module deploys a Storage Account Table." }, "definitions": { - "privateDnsZoneGroupConfigType": { + "roleAssignmentType": { "type": "object", "properties": { "name": { "type": "string", "nullable": true, "metadata": { - "description": "Optional. The name of the private DNS zone group config." + "description": "Optional. The name (as GUID) of the role assignment. If not provided, a GUID will be generated." } }, - "privateDnsZoneResourceId": { + "roleDefinitionIdOrName": { "type": "string", "metadata": { - "description": "Required. The resource id of the private DNS zone." + "description": "Required. The role to assign. You can provide either the display name of the role definition, the role definition GUID, or its fully qualified ID in the following format: '/providers/Microsoft.Authorization/roleDefinitions/c2f4ef07-c644-48eb-af81-4b1b4947fb11'." + } + }, + "principalId": { + "type": "string", + "metadata": { + "description": "Required. The principal ID of the principal (user/group/identity) to assign the role to." + } + }, + "principalType": { + "type": "string", + "allowedValues": [ + "Device", + "ForeignGroup", + "Group", + "ServicePrincipal", + "User" + ], + "nullable": true, + "metadata": { + "description": "Optional. The principal type of the assigned principal ID." + } + }, + "description": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The description of the role assignment." + } + }, + "condition": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The conditions on the role assignment. This limits the resources it can be assigned to. e.g.: @Resource[Microsoft.Storage/storageAccounts/blobServices/containers:ContainerName] StringEqualsIgnoreCase \"foo_storage_container\"." + } + }, + "conditionVersion": { + "type": "string", + "allowedValues": [ + "2.0" + ], + "nullable": true, + "metadata": { + "description": "Optional. Version of the condition." + } + }, + "delegatedManagedIdentityResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The Resource Id of the delegated managed identity resource." } } - }, - "metadata": { - "__bicep_export!": true + }, + "metadata": { + "description": "An AVM-aligned type for a role assignment.", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.1" + } } } }, "parameters": { - "privateEndpointName": { + "storageAccountName": { "type": "string", + "maxLength": 24, "metadata": { - "description": "Conditional. The name of the parent private endpoint. Required if the template is used in a standalone deployment." + "description": "Conditional. The name of the parent Storage Account. Required if the template is used in a standalone deployment." } }, - "privateDnsZoneConfigs": { + "roleAssignments": { "type": "array", "items": { - "$ref": "#/definitions/privateDnsZoneGroupConfigType" + "$ref": "#/definitions/roleAssignmentType" }, - "minLength": 1, - "maxLength": 5, + "nullable": true, "metadata": { - "description": "Required. Array of private DNS zone configurations of the private DNS zone group. A DNS zone group can support up to 5 DNS zones." + "description": "Optional. Array of role assignments to create." } }, "name": { "type": "string", - "defaultValue": "default", "metadata": { - "description": "Optional. The name of the private DNS zone group." + "description": "Required. Name of the table." } } }, "variables": { "copy": [ { - "name": "privateDnsZoneConfigsVar", - "count": "[length(parameters('privateDnsZoneConfigs'))]", - "input": { - "name": "[coalesce(tryGet(parameters('privateDnsZoneConfigs')[copyIndex('privateDnsZoneConfigsVar')], 'name'), last(split(parameters('privateDnsZoneConfigs')[copyIndex('privateDnsZoneConfigsVar')].privateDnsZoneResourceId, '/')))]", - "properties": { - "privateDnsZoneId": "[parameters('privateDnsZoneConfigs')[copyIndex('privateDnsZoneConfigsVar')].privateDnsZoneResourceId]" - } - } + "name": "formattedRoleAssignments", + "count": "[length(coalesce(parameters('roleAssignments'), createArray()))]", + "input": "[union(coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')], createObject('roleDefinitionId', coalesce(tryGet(variables('builtInRoleNames'), coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName), if(contains(coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName, '/providers/Microsoft.Authorization/roleDefinitions/'), coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName, subscriptionResourceId('Microsoft.Authorization/roleDefinitions', coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName)))))]" } - ] + ], + "builtInRoleNames": { + "Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'b24988ac-6180-42a0-ab88-20f7382dd24c')]", + "Owner": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '8e3af657-a8ff-443c-a75c-2fe8c4bcb635')]", + "Reader": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'acdd72a7-3385-48ef-bd42-f606fba81ae7')]", + "Reader and Data Access": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'c12c1c16-33a1-487b-954d-41c89c60f349')]", + "Role Based Access Control Administrator": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'f58310d9-a9f6-439a-9e8d-f62e7b41a168')]", + "Storage Account Backup Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'e5e2a7ff-d759-4cd2-bb51-3152d37e2eb1')]", + "Storage Account Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '17d1049b-9a84-46fb-8f53-869881c3d3ab')]", + "Storage Account Key Operator Service Role": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '81a9662b-bebf-436f-a333-f67b29880f12')]", + "Storage Table Data Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '0a9a7e1f-b9d0-4cc4-a60d-0319b160aaa3')]", + "Storage Table Data Reader": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '76199698-9eea-4c19-bc75-cec21354c6b6')]", + "User Access Administrator": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '18d7d88d-d35e-4fb5-a5c3-7773c20a72d9')]" + } }, "resources": { - "privateEndpoint": { + "storageAccount::tableServices": { "existing": true, - "type": "Microsoft.Network/privateEndpoints", - "apiVersion": "2024-05-01", - "name": "[parameters('privateEndpointName')]" + "type": "Microsoft.Storage/storageAccounts/tableServices", + "apiVersion": "2024-01-01", + "name": "[format('{0}/{1}', parameters('storageAccountName'), 'default')]" }, - "privateDnsZoneGroup": { - "type": "Microsoft.Network/privateEndpoints/privateDnsZoneGroups", - "apiVersion": "2024-05-01", - "name": "[format('{0}/{1}', parameters('privateEndpointName'), parameters('name'))]", + "storageAccount": { + "existing": true, + "type": "Microsoft.Storage/storageAccounts", + "apiVersion": "2024-01-01", + "name": "[parameters('storageAccountName')]" + }, + "table": { + "type": "Microsoft.Storage/storageAccounts/tableServices/tables", + "apiVersion": "2024-01-01", + "name": "[format('{0}/{1}/{2}', parameters('storageAccountName'), 'default', parameters('name'))]" + }, + "table_roleAssignments": { + "copy": { + "name": "table_roleAssignments", + "count": "[length(coalesce(variables('formattedRoleAssignments'), createArray()))]" + }, + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2022-04-01", + "scope": "[format('Microsoft.Storage/storageAccounts/{0}/tableServices/{1}/tables/{2}', parameters('storageAccountName'), 'default', parameters('name'))]", + "name": "[coalesce(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'name'), guid(resourceId('Microsoft.Storage/storageAccounts/tableServices/tables', parameters('storageAccountName'), 'default', parameters('name')), coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].principalId, coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].roleDefinitionId))]", "properties": { - "privateDnsZoneConfigs": "[variables('privateDnsZoneConfigsVar')]" - } + "roleDefinitionId": "[coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].roleDefinitionId]", + "principalId": "[coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].principalId]", + "description": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'description')]", + "principalType": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'principalType')]", + "condition": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'condition')]", + "conditionVersion": "[if(not(empty(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'condition'))), coalesce(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'conditionVersion'), '2.0'), null())]", + "delegatedManagedIdentityResourceId": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'delegatedManagedIdentityResourceId')]" + }, + "dependsOn": [ + "table" + ] } }, "outputs": { "name": { "type": "string", "metadata": { - "description": "The name of the private endpoint DNS zone group." + "description": "The name of the deployed table." }, "value": "[parameters('name')]" }, "resourceId": { "type": "string", "metadata": { - "description": "The resource ID of the private endpoint DNS zone group." + "description": "The resource ID of the deployed table." }, - "value": "[resourceId('Microsoft.Network/privateEndpoints/privateDnsZoneGroups', parameters('privateEndpointName'), parameters('name'))]" + "value": "[resourceId('Microsoft.Storage/storageAccounts/tableServices/tables', parameters('storageAccountName'), 'default', parameters('name'))]" }, "resourceGroupName": { "type": "string", "metadata": { - "description": "The resource group the private endpoint DNS zone group was deployed into." + "description": "The resource group of the deployed table." }, "value": "[resourceGroup().name]" } } } - }, - "dependsOn": [ - "privateEndpoint" - ] + } } }, "outputs": { - "resourceGroupName": { - "type": "string", - "metadata": { - "description": "The resource group the private endpoint was deployed into." - }, - "value": "[resourceGroup().name]" - }, - "resourceId": { - "type": "string", - "metadata": { - "description": "The resource ID of the private endpoint." - }, - "value": "[resourceId('Microsoft.Network/privateEndpoints', parameters('name'))]" - }, "name": { "type": "string", "metadata": { - "description": "The name of the private endpoint." - }, - "value": "[parameters('name')]" - }, - "location": { - "type": "string", - "metadata": { - "description": "The location the resource was deployed into." - }, - "value": "[reference('privateEndpoint', '2024-05-01', 'full').location]" - }, - "customDnsConfigs": { - "type": "array", - "items": { - "$ref": "#/definitions/customDnsConfigType" - }, - "metadata": { - "description": "The custom DNS configurations of the private endpoint." - }, - "value": "[reference('privateEndpoint').customDnsConfigs]" - }, - "networkInterfaceResourceIds": { - "type": "array", - "items": { - "type": "string" - }, - "metadata": { - "description": "The resource IDs of the network interfaces associated with the private endpoint." + "description": "The name of the deployed table service." }, - "value": "[map(reference('privateEndpoint').networkInterfaces, lambda('nic', lambdaVariables('nic').id))]" + "value": "[variables('name')]" }, - "groupId": { + "resourceId": { "type": "string", - "nullable": true, "metadata": { - "description": "The group Id for the private endpoint Group." - }, - "value": "[coalesce(tryGet(tryGet(tryGet(tryGet(reference('privateEndpoint'), 'manualPrivateLinkServiceConnections'), 0, 'properties'), 'groupIds'), 0), tryGet(tryGet(tryGet(tryGet(reference('privateEndpoint'), 'privateLinkServiceConnections'), 0, 'properties'), 'groupIds'), 0))]" - } - } - } - }, - "dependsOn": [ - "app" - ] - } - }, - "outputs": { - "name": { - "type": "string", - "metadata": { - "description": "The name of the site." - }, - "value": "[parameters('name')]" - }, - "resourceId": { - "type": "string", - "metadata": { - "description": "The resource ID of the site." - }, - "value": "[resourceId('Microsoft.Web/sites', parameters('name'))]" - }, - "resourceGroupName": { - "type": "string", - "metadata": { - "description": "The resource group the site was deployed into." - }, - "value": "[resourceGroup().name]" - }, - "systemAssignedMIPrincipalId": { - "type": "string", - "nullable": true, - "metadata": { - "description": "The principal ID of the system assigned identity." - }, - "value": "[tryGet(tryGet(reference('app', '2024-04-01', 'full'), 'identity'), 'principalId')]" - }, - "location": { - "type": "string", - "metadata": { - "description": "The location the resource was deployed into." - }, - "value": "[reference('app', '2024-04-01', 'full').location]" - }, - "defaultHostname": { - "type": "string", - "metadata": { - "description": "Default hostname of the app." - }, - "value": "[format('https://{0}.azurewebsites.net', parameters('name'))]" - }, - "customDomainVerificationId": { - "type": "string", - "metadata": { - "description": "Unique identifier that verifies the custom domains assigned to the app." - }, - "value": "[reference('app').customDomainVerificationId]" - }, - "outboundIpAddresses": { - "type": "string", - "metadata": { - "description": "The outbound IP addresses of the app." - }, - "value": "[reference('app').outboundIpAddresses]" - } - } - } - }, - "dependsOn": [ - "applicationInsights", - "logAnalyticsWorkspace", - "userAssignedIdentity", - "virtualNetwork", - "webServerFarm" - ] - }, - "containerInstance": { - "type": "Microsoft.Resources/deployments", - "apiVersion": "2025-04-01", - "name": "[take(format('module.container-instance.{0}', variables('containerInstanceName')), 64)]", - "properties": { - "expressionEvaluationOptions": { - "scope": "inner" - }, - "mode": "Incremental", - "parameters": { - "name": { - "value": "[variables('containerInstanceName')]" - }, - "location": { - "value": "[variables('solutionLocation')]" - }, - "tags": { - "value": "[parameters('tags')]" - }, - "containerImage": { - "value": "[format('{0}.azurecr.io/content-gen-api:{1}', variables('acrResourceName'), parameters('imageTag'))]" - }, - "cpu": { - "value": 2 - }, - "memoryInGB": { - "value": 4 - }, - "port": { - "value": 8000 - }, - "subnetResourceId": "[if(parameters('enablePrivateNetworking'), createObject('value', reference('virtualNetwork').outputs.aciSubnetResourceId.value), createObject('value', ''))]", - "registryServer": { - "value": "[format('{0}.azurecr.io', variables('acrResourceName'))]" - }, - "userAssignedIdentityResourceId": { - "value": "[reference('userAssignedIdentity').outputs.resourceId.value]" - }, - "enableTelemetry": { - "value": "[parameters('enableTelemetry')]" - }, - "environmentVariables": { - "value": [ - { - "name": "AZURE_OPENAI_ENDPOINT", - "value": "[format('https://{0}.openai.azure.com/', variables('aiFoundryAiServicesResourceName'))]" - }, - { - "name": "AZURE_OPENAI_GPT_MODEL", - "value": "[parameters('gptModelName')]" - }, - { - "name": "AZURE_OPENAI_IMAGE_MODEL", - "value": "[variables('imageModelConfig')[parameters('imageModelChoice')].name]" - }, - { - "name": "AZURE_OPENAI_GPT_IMAGE_ENDPOINT", - "value": "[if(not(equals(parameters('imageModelChoice'), 'none')), format('https://{0}.openai.azure.com/', variables('aiFoundryAiServicesResourceName')), '')]" - }, - { - "name": "AZURE_OPENAI_API_VERSION", - "value": "[parameters('azureOpenaiAPIVersion')]" - }, - { - "name": "AZURE_COSMOS_ENDPOINT", - "value": "[format('https://cosmos-{0}.documents.azure.com:443/', variables('solutionSuffix'))]" - }, - { - "name": "AZURE_COSMOS_DATABASE_NAME", - "value": "[variables('cosmosDBDatabaseName')]" - }, - { - "name": "AZURE_COSMOS_PRODUCTS_CONTAINER", - "value": "[variables('cosmosDBProductsContainer')]" - }, - { - "name": "AZURE_COSMOS_CONVERSATIONS_CONTAINER", - "value": "[variables('cosmosDBConversationsContainer')]" - }, - { - "name": "AZURE_BLOB_ACCOUNT_NAME", - "value": "[variables('storageAccountName')]" - }, - { - "name": "AZURE_BLOB_PRODUCT_IMAGES_CONTAINER", - "value": "[variables('productImagesContainer')]" - }, - { - "name": "AZURE_BLOB_GENERATED_IMAGES_CONTAINER", - "value": "[variables('generatedImagesContainer')]" - }, - { - "name": "AZURE_AI_SEARCH_ENDPOINT", - "value": "[format('https://{0}.search.windows.net', variables('aiSearchName'))]" - }, - { - "name": "AZURE_AI_SEARCH_PRODUCTS_INDEX", - "value": "[variables('azureSearchIndex')]" - }, - { - "name": "AZURE_AI_SEARCH_IMAGE_INDEX", - "value": "product-images" - }, - { - "name": "AZURE_CLIENT_ID", - "value": "[reference('userAssignedIdentity').outputs.clientId.value]" - }, - { - "name": "PORT", - "value": "8000" - }, - { - "name": "WORKERS", - "value": "4" - }, - { - "name": "RUNNING_IN_PRODUCTION", - "value": "true" - }, - { - "name": "USE_FOUNDRY", - "value": "[if(parameters('useFoundryMode'), 'true', 'false')]" - }, - { - "name": "AZURE_AI_PROJECT_ENDPOINT", - "value": "[if(variables('useExistingAiFoundryAiProject'), format('https://{0}.services.ai.azure.com/api/projects/{1}', variables('aiFoundryAiServicesResourceName'), variables('aiFoundryAiProjectResourceName')), reference('aiFoundryAiServicesProject').outputs.apiEndpoint.value)]" - }, - { - "name": "AZURE_AI_MODEL_DEPLOYMENT_NAME", - "value": "[parameters('gptModelName')]" - }, - { - "name": "AZURE_AI_IMAGE_MODEL_DEPLOYMENT", - "value": "[variables('imageModelConfig')[parameters('imageModelChoice')].name]" - } - ] - } - }, - "template": { - "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", - "contentVersion": "1.0.0.0", - "metadata": { - "_generator": { - "name": "bicep", - "version": "0.39.26.7824", - "templateHash": "11247487291315089538" - } - }, - "parameters": { - "name": { - "type": "string", - "metadata": { - "description": "Required. Name of the container group." - } - }, - "location": { - "type": "string", - "metadata": { - "description": "Required. Location for the container instance." - } - }, - "tags": { - "type": "object", - "defaultValue": {}, - "metadata": { - "description": "Optional. Tags for all resources." - } - }, - "containerImage": { - "type": "string", - "metadata": { - "description": "Required. Container image to deploy." - } - }, - "cpu": { - "type": "int", - "defaultValue": 2, - "metadata": { - "description": "Optional. CPU cores for the container." - } - }, - "memoryInGB": { - "type": "int", - "defaultValue": 4, - "metadata": { - "description": "Optional. Memory in GB for the container." - } - }, - "port": { - "type": "int", - "defaultValue": 8000, - "metadata": { - "description": "Optional. Port to expose." - } - }, - "subnetResourceId": { - "type": "string", - "defaultValue": "", - "metadata": { - "description": "Optional. Subnet resource ID for VNet integration. If empty, public IP will be used." - } - }, - "environmentVariables": { - "type": "array", - "metadata": { - "description": "Required. Environment variables for the container." - } - }, - "enableTelemetry": { - "type": "bool", - "defaultValue": true, - "metadata": { - "description": "Optional. Enable telemetry." - } - }, - "registryServer": { - "type": "string", - "metadata": { - "description": "Required. Container registry server." - } - }, - "userAssignedIdentityResourceId": { - "type": "string", - "defaultValue": "", - "metadata": { - "description": "Optional. User-assigned managed identity resource ID for ACR pull." - } - } - }, - "variables": { - "isPrivateNetworking": "[not(empty(parameters('subnetResourceId')))]" - }, - "resources": [ - { - "condition": "[parameters('enableTelemetry')]", - "type": "Microsoft.Resources/deployments", - "apiVersion": "2024-03-01", - "name": "[format('46d3xbcp.res.containerinstance.{0}.{1}', replace('-..--..-', '.', '-'), substring(uniqueString(deployment().name, parameters('location')), 0, 4))]", - "properties": { - "mode": "Incremental", - "template": { - "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", - "contentVersion": "1.0.0.0", - "resources": [] - } - } - }, - { - "type": "Microsoft.ContainerInstance/containerGroups", - "apiVersion": "2023-05-01", - "name": "[parameters('name')]", - "location": "[parameters('location')]", - "tags": "[parameters('tags')]", - "identity": { - "type": "UserAssigned", - "userAssignedIdentities": { - "[format('{0}', parameters('userAssignedIdentityResourceId'))]": {} - } - }, - "properties": { - "containers": [ - { - "name": "[parameters('name')]", - "properties": { - "image": "[parameters('containerImage')]", - "resources": { - "requests": { - "cpu": "[parameters('cpu')]", - "memoryInGB": "[parameters('memoryInGB')]" - } + "description": "The resource ID of the deployed table service." }, - "ports": [ - { - "port": "[parameters('port')]", - "protocol": "TCP" - } - ], - "environmentVariables": "[parameters('environmentVariables')]" + "value": "[resourceId('Microsoft.Storage/storageAccounts/tableServices', parameters('storageAccountName'), variables('name'))]" + }, + "resourceGroupName": { + "type": "string", + "metadata": { + "description": "The resource group of the deployed table service." + }, + "value": "[resourceGroup().name]" } } - ], - "osType": "Linux", - "restartPolicy": "Always", - "subnetIds": "[if(variables('isPrivateNetworking'), createArray(createObject('id', parameters('subnetResourceId'))), null())]", - "ipAddress": { - "type": "[if(variables('isPrivateNetworking'), 'Private', 'Public')]", - "ports": [ - { - "port": "[parameters('port')]", - "protocol": "TCP" - } - ], - "dnsNameLabel": "[if(variables('isPrivateNetworking'), null(), parameters('name'))]" } - } - } - ], - "outputs": { - "name": { - "type": "string", - "metadata": { - "description": "The name of the container group." - }, - "value": "[parameters('name')]" - }, - "resourceId": { - "type": "string", - "metadata": { - "description": "The resource ID of the container group." - }, - "value": "[resourceId('Microsoft.ContainerInstance/containerGroups', parameters('name'))]" - }, - "ipAddress": { - "type": "string", - "metadata": { - "description": "The IP address of the container (private or public depending on mode)." - }, - "value": "[reference(resourceId('Microsoft.ContainerInstance/containerGroups', parameters('name')), '2023-05-01').ipAddress.ip]" - }, - "fqdn": { - "type": "string", - "metadata": { - "description": "The FQDN of the container (only available for public mode)." }, - "value": "[if(variables('isPrivateNetworking'), '', reference(resourceId('Microsoft.ContainerInstance/containerGroups', parameters('name')), '2023-05-01').ipAddress.fqdn)]" - } - } - } - }, - "dependsOn": [ - "aiFoundryAiServicesProject", - "userAssignedIdentity", - "virtualNetwork" - ] - }, - "jumpboxVM": { - "condition": "[and(parameters('enablePrivateNetworking'), parameters('deployBastionAndJumpbox'))]", - "type": "Microsoft.Resources/deployments", - "apiVersion": "2025-04-01", - "name": "[take(format('avm.res.compute.virtual-machine.{0}', variables('jumpboxVmName')), 64)]", - "properties": { - "expressionEvaluationOptions": { - "scope": "inner" - }, - "mode": "Incremental", - "parameters": { - "name": { - "value": "[take(variables('jumpboxVmName'), 15)]" - }, - "enableTelemetry": { - "value": "[parameters('enableTelemetry')]" - }, - "computerName": { - "value": "[take(variables('jumpboxVmName'), 15)]" - }, - "osType": { - "value": "Windows" - }, - "vmSize": "[if(empty(parameters('vmSize')), createObject('value', 'Standard_D2s_v5'), createObject('value', parameters('vmSize')))]", - "adminUsername": "[if(empty(parameters('vmAdminUsername')), createObject('value', 'JumpboxAdminUser'), createObject('value', parameters('vmAdminUsername')))]", - "adminPassword": "[if(empty(parameters('vmAdminPassword')), createObject('value', 'JumpboxAdminP@ssw0rd1234!'), createObject('value', parameters('vmAdminPassword')))]", - "managedIdentities": { - "value": { - "userAssignedResourceIds": [ - "[reference('userAssignedIdentity').outputs.resourceId.value]" + "dependsOn": [ + "storageAccount" ] - } - }, - "availabilityZone": { - "value": 1 - }, - "imageReference": { - "value": { - "publisher": "MicrosoftWindowsDesktop", - "offer": "windows-11", - "sku": "win11-24h2-pro", - "version": "latest" - } - }, - "nicConfigurations": { - "value": [ - { - "name": "[format('nic-{0}', variables('jumpboxVmName'))]", - "enableAcceleratedNetworking": true, - "ipConfigurations": [ - { - "name": "ipconfig01", - "subnetResourceId": "[reference('virtualNetwork').outputs.jumpboxSubnetResourceId.value]" - } - ] - } - ] - }, - "osDisk": { - "value": { - "caching": "ReadWrite", - "diskSizeGB": 128, - "managedDisk": { - "storageAccountType": "Premium_LRS" - } - } - }, - "location": { - "value": "[variables('solutionLocation')]" - }, - "tags": { - "value": "[parameters('tags')]" - } - }, - "template": { - "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", - "languageVersion": "2.0", - "contentVersion": "1.0.0.0", - "metadata": { - "_generator": { - "name": "bicep", - "version": "0.37.4.10188", - "templateHash": "10754907249846822047" }, - "name": "Virtual Machines", - "description": "This module deploys a Virtual Machine with one or multiple NICs and optionally one or multiple public IPs." - }, - "definitions": { - "osDiskType": { - "type": "object", + "secretsExport": { + "condition": "[not(equals(parameters('secretsExportConfiguration'), null()))]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "[format('{0}-secrets-kv', uniqueString(deployment().name, parameters('location')))]", + "subscriptionId": "[split(tryGet(parameters('secretsExportConfiguration'), 'keyVaultResourceId'), '/')[2]]", + "resourceGroup": "[split(tryGet(parameters('secretsExportConfiguration'), 'keyVaultResourceId'), '/')[4]]", "properties": { - "name": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The disk name." - } - }, - "diskSizeGB": { - "type": "int", - "nullable": true, - "metadata": { - "description": "Optional. Specifies the size of an empty data disk in gigabytes." - } - }, - "createOption": { - "type": "string", - "allowedValues": [ - "Attach", - "Empty", - "FromImage" - ], - "nullable": true, - "metadata": { - "description": "Optional. Specifies how the virtual machine should be created." - } + "expressionEvaluationOptions": { + "scope": "inner" }, - "deleteOption": { - "type": "string", - "allowedValues": [ - "Delete", - "Detach" - ], - "nullable": true, - "metadata": { - "description": "Optional. Specifies whether data disk should be deleted or detached upon VM deletion." + "mode": "Incremental", + "parameters": { + "keyVaultName": { + "value": "[last(split(tryGet(parameters('secretsExportConfiguration'), 'keyVaultResourceId'), '/'))]" + }, + "secretsToSet": { + "value": "[union(createArray(), if(contains(parameters('secretsExportConfiguration'), 'accessKey1Name'), createArray(createObject('name', tryGet(parameters('secretsExportConfiguration'), 'accessKey1Name'), 'value', listKeys('storageAccount', '2025-01-01').keys[0].value)), createArray()), if(contains(parameters('secretsExportConfiguration'), 'connectionString1Name'), createArray(createObject('name', tryGet(parameters('secretsExportConfiguration'), 'connectionString1Name'), 'value', format('DefaultEndpointsProtocol=https;AccountName={0};AccountKey={1};EndpointSuffix={2}', parameters('name'), listKeys('storageAccount', '2025-01-01').keys[0].value, environment().suffixes.storage))), createArray()), if(contains(parameters('secretsExportConfiguration'), 'accessKey2Name'), createArray(createObject('name', tryGet(parameters('secretsExportConfiguration'), 'accessKey2Name'), 'value', listKeys('storageAccount', '2025-01-01').keys[1].value)), createArray()), if(contains(parameters('secretsExportConfiguration'), 'connectionString2Name'), createArray(createObject('name', tryGet(parameters('secretsExportConfiguration'), 'connectionString2Name'), 'value', format('DefaultEndpointsProtocol=https;AccountName={0};AccountKey={1};EndpointSuffix={2}', parameters('name'), listKeys('storageAccount', '2025-01-01').keys[1].value, environment().suffixes.storage))), createArray()))]" } }, - "caching": { - "type": "string", - "allowedValues": [ - "None", - "ReadOnly", - "ReadWrite" - ], - "nullable": true, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "languageVersion": "2.0", + "contentVersion": "1.0.0.0", "metadata": { - "description": "Optional. Specifies the caching requirements." - } - }, - "diffDiskSettings": { - "type": "object", - "properties": { - "placement": { - "type": "string", - "allowedValues": [ - "CacheDisk", - "NvmeDisk", - "ResourceDisk" - ], + "_generator": { + "name": "bicep", + "version": "0.39.26.7824", + "templateHash": "13614544361780789643" + } + }, + "definitions": { + "secretSetOutputType": { + "type": "object", + "properties": { + "secretResourceId": { + "type": "string", + "metadata": { + "description": "The resourceId of the exported secret." + } + }, + "secretUri": { + "type": "string", + "metadata": { + "description": "The secret URI of the exported secret." + } + }, + "secretUriWithVersion": { + "type": "string", + "metadata": { + "description": "The secret URI with version of the exported secret." + } + } + }, "metadata": { - "description": "Required. Specifies the ephemeral disk placement for the operating system disk." + "description": "An AVM-aligned type for the output of the secret set via the secrets export feature.", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.0" + } + } + }, + "secretToSetType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "metadata": { + "description": "Required. The name of the secret to set." + } + }, + "value": { + "type": "securestring", + "metadata": { + "description": "Required. The value of the secret to set." + } + } + }, + "metadata": { + "description": "An AVM-aligned type for the secret to set via the secrets export feature.", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.0" + } } } }, - "nullable": true, - "metadata": { - "description": "Optional. Specifies the ephemeral Disk Settings for the operating system disk." - } - }, - "managedDisk": { - "type": "object", - "properties": { - "storageAccountType": { + "parameters": { + "keyVaultName": { "type": "string", - "allowedValues": [ - "PremiumV2_LRS", - "Premium_LRS", - "Premium_ZRS", - "StandardSSD_LRS", - "StandardSSD_ZRS", - "Standard_LRS", - "UltraSSD_LRS" - ], - "nullable": true, "metadata": { - "description": "Optional. Specifies the storage account type for the managed disk." + "description": "Required. The name of the Key Vault to set the ecrets in." } }, - "diskEncryptionSetResourceId": { - "type": "string", - "nullable": true, + "secretsToSet": { + "type": "array", + "items": { + "$ref": "#/definitions/secretToSetType" + }, "metadata": { - "description": "Optional. Specifies the customer managed disk encryption set resource id for the managed disk." + "description": "Required. The secrets to set in the Key Vault." } } }, - "metadata": { - "description": "Required. The managed disk parameters." + "resources": { + "keyVault": { + "existing": true, + "type": "Microsoft.KeyVault/vaults", + "apiVersion": "2024-11-01", + "name": "[parameters('keyVaultName')]" + }, + "secrets": { + "copy": { + "name": "secrets", + "count": "[length(parameters('secretsToSet'))]" + }, + "type": "Microsoft.KeyVault/vaults/secrets", + "apiVersion": "2024-11-01", + "name": "[format('{0}/{1}', parameters('keyVaultName'), parameters('secretsToSet')[copyIndex()].name)]", + "properties": { + "value": "[parameters('secretsToSet')[copyIndex()].value]" + } + } + }, + "outputs": { + "secretsSet": { + "type": "array", + "items": { + "$ref": "#/definitions/secretSetOutputType" + }, + "metadata": { + "description": "The references to the secrets exported to the provided Key Vault." + }, + "copy": { + "count": "[length(range(0, length(coalesce(parameters('secretsToSet'), createArray()))))]", + "input": { + "secretResourceId": "[resourceId('Microsoft.KeyVault/vaults/secrets', parameters('keyVaultName'), parameters('secretsToSet')[range(0, length(coalesce(parameters('secretsToSet'), createArray())))[copyIndex()]].name)]", + "secretUri": "[reference(format('secrets[{0}]', range(0, length(coalesce(parameters('secretsToSet'), createArray())))[copyIndex()])).secretUri]", + "secretUriWithVersion": "[reference(format('secrets[{0}]', range(0, length(coalesce(parameters('secretsToSet'), createArray())))[copyIndex()])).secretUriWithVersion]" + } + } + } } } }, - "metadata": { - "__bicep_export!": true, - "description": "The type describing an OS disk." - } + "dependsOn": [ + "storageAccount" + ] }, - "dataDiskType": { - "type": "object", + "storageAccount_objectReplicationPolicies": { + "copy": { + "name": "storageAccount_objectReplicationPolicies", + "count": "[length(coalesce(parameters('objectReplicationPolicies'), createArray()))]" + }, + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "[format('{0}-Storage-ObjRepPolicy-{1}', uniqueString(deployment().name, parameters('location')), copyIndex())]", "properties": { - "name": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The disk name. When attaching a pre-existing disk, this name is ignored and the name of the existing disk is used." - } - }, - "lun": { - "type": "int", - "nullable": true, - "metadata": { - "description": "Optional. Specifies the logical unit number of the data disk." - } - }, - "diskSizeGB": { - "type": "int", - "nullable": true, - "metadata": { - "description": "Optional. Specifies the size of an empty data disk in gigabytes. This property is ignored when attaching a pre-existing disk." - } - }, - "createOption": { - "type": "string", - "allowedValues": [ - "Attach", - "Empty", - "FromImage" - ], - "nullable": true, - "metadata": { - "description": "Optional. Specifies how the virtual machine should be created. This property is automatically set to 'Attach' when attaching a pre-existing disk." - } - }, - "deleteOption": { - "type": "string", - "allowedValues": [ - "Delete", - "Detach" - ], - "nullable": true, - "metadata": { - "description": "Optional. Specifies whether data disk should be deleted or detached upon VM deletion. This property is automatically set to 'Detach' when attaching a pre-existing disk." - } - }, - "caching": { - "type": "string", - "allowedValues": [ - "None", - "ReadOnly", - "ReadWrite" - ], - "nullable": true, - "metadata": { - "description": "Optional. Specifies the caching requirements. This property is automatically set to 'None' when attaching a pre-existing disk." - } + "expressionEvaluationOptions": { + "scope": "inner" }, - "diskIOPSReadWrite": { - "type": "int", - "nullable": true, - "metadata": { - "description": "Optional. The number of IOPS allowed for this disk; only settable for UltraSSD disks. One operation can transfer between 4k and 256k bytes. Ignored when attaching a pre-existing disk." + "mode": "Incremental", + "parameters": { + "storageAccountName": { + "value": "[parameters('name')]" + }, + "destinationAccountResourceId": { + "value": "[coalesce(parameters('objectReplicationPolicies'), createArray())[copyIndex()].destinationStorageAccountResourceId]" + }, + "enableMetrics": { + "value": "[coalesce(tryGet(coalesce(parameters('objectReplicationPolicies'), createArray())[copyIndex()], 'enableMetrics'), false())]" + }, + "rules": { + "value": "[tryGet(coalesce(parameters('objectReplicationPolicies'), createArray())[copyIndex()], 'rules')]" } }, - "diskMBpsReadWrite": { - "type": "int", - "nullable": true, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "languageVersion": "2.0", + "contentVersion": "1.0.0.0", "metadata": { - "description": "Optional. The bandwidth allowed for this disk; only settable for UltraSSD disks. MBps means millions of bytes per second - MB here uses the ISO notation, of powers of 10. Ignored when attaching a pre-existing disk." - } - }, - "managedDisk": { - "type": "object", - "properties": { - "storageAccountType": { + "_generator": { + "name": "bicep", + "version": "0.39.26.7824", + "templateHash": "3528396711847214833" + }, + "name": "Storage Account Object Replication Policy", + "description": "This module deploys a Storage Account Object Replication Policy for both the source account and destination account." + }, + "definitions": { + "objectReplicationPolicyRuleType": { + "type": "object", + "properties": { + "ruleId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The ID of the rule. Auto-generated on destination account. Required for source account." + } + }, + "containerName": { + "type": "string", + "metadata": { + "description": "Required. The name of the source container." + } + }, + "destinationContainerName": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The name of the destination container. If not provided, the same name as the source container will be used." + } + }, + "filters": { + "type": "object", + "properties": { + "prefixMatch": { + "type": "array", + "items": { + "type": "string" + }, + "nullable": true, + "metadata": { + "description": "Optional. The prefix to match for the replication policy rule." + } + }, + "minCreationTime": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The minimum creation time to match for the replication policy rule." + } + } + }, + "nullable": true, + "metadata": { + "description": "Optional. The filters for the object replication policy rule." + } + } + }, + "metadata": { + "description": "The type of an object replication policy rule.", + "__bicep_imported_from!": { + "sourceTemplate": "policy/main.bicep" + } + } + } + }, + "parameters": { + "name": { "type": "string", - "allowedValues": [ - "PremiumV2_LRS", - "Premium_LRS", - "Premium_ZRS", - "StandardSSD_LRS", - "StandardSSD_ZRS", - "Standard_LRS", - "UltraSSD_LRS" - ], "nullable": true, "metadata": { - "description": "Optional. Specifies the storage account type for the managed disk. Ignored when attaching a pre-existing disk." + "description": "Optional. Name of the policy." } }, - "diskEncryptionSetResourceId": { + "storageAccountName": { "type": "string", - "nullable": true, + "maxLength": 24, "metadata": { - "description": "Optional. Specifies the customer managed disk encryption set resource id for the managed disk." + "description": "Required. The name of the parent Storage Account." } }, - "id": { + "destinationAccountResourceId": { "type": "string", + "metadata": { + "description": "Required. Resource ID of the destination storage account for replication." + } + }, + "enableMetrics": { + "type": "bool", "nullable": true, "metadata": { - "description": "Optional. Specifies the resource id of a pre-existing managed disk. If the disk should be created, this property should be empty." + "description": "Optional. Whether metrics are enabled for the object replication policy." + } + }, + "rules": { + "type": "array", + "items": { + "$ref": "#/definitions/objectReplicationPolicyRuleType" + }, + "metadata": { + "description": "Required. Rules for the object replication policy." } } }, - "metadata": { - "description": "Required. The managed disk parameters." - } - }, - "tags": { - "type": "object", - "nullable": true, - "metadata": { - "description": "Optional. The tags of the public IP address. Valid only when creating a new managed disk." + "variables": { + "destAccountResourceIdParts": "[split(parameters('destinationAccountResourceId'), '/')]", + "destAccountName": "[if(not(empty(variables('destAccountResourceIdParts'))), last(variables('destAccountResourceIdParts')), parameters('destinationAccountResourceId'))]", + "destAccountSubscription": "[if(greater(length(variables('destAccountResourceIdParts')), 2), variables('destAccountResourceIdParts')[2], subscription().subscriptionId)]", + "destAccountResourceGroupName": "[if(greater(length(variables('destAccountResourceIdParts')), 4), variables('destAccountResourceIdParts')[4], resourceGroup().name)]" + }, + "resources": { + "storageAccount": { + "existing": true, + "type": "Microsoft.Storage/storageAccounts", + "apiVersion": "2025-01-01", + "name": "[parameters('storageAccountName')]" + }, + "destinationPolicy": { + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "[take(format('{0}-ObjRep-Policy-dest-{1}', deployment().name, variables('destAccountName')), 64)]", + "subscriptionId": "[variables('destAccountSubscription')]", + "resourceGroup": "[variables('destAccountResourceGroupName')]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "name": { + "value": "[coalesce(parameters('name'), 'default')]" + }, + "storageAccountName": { + "value": "[variables('destAccountName')]" + }, + "sourceStorageAccountResourceId": { + "value": "[resourceId('Microsoft.Storage/storageAccounts', parameters('storageAccountName'))]" + }, + "destinationAccountResourceId": { + "value": "[parameters('destinationAccountResourceId')]" + }, + "enableMetrics": { + "value": "[parameters('enableMetrics')]" + }, + "rules": { + "value": "[parameters('rules')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "languageVersion": "2.0", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.39.26.7824", + "templateHash": "4325417308313318683" + }, + "name": "Storage Account Object Replication Policy", + "description": "This module deploys a Storage Account Object Replication Policy for a provided storage account." + }, + "definitions": { + "objectReplicationPolicyRuleType": { + "type": "object", + "properties": { + "ruleId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The ID of the rule. Auto-generated on destination account. Required for source account." + } + }, + "containerName": { + "type": "string", + "metadata": { + "description": "Required. The name of the source container." + } + }, + "destinationContainerName": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The name of the destination container. If not provided, the same name as the source container will be used." + } + }, + "filters": { + "type": "object", + "properties": { + "prefixMatch": { + "type": "array", + "items": { + "type": "string" + }, + "nullable": true, + "metadata": { + "description": "Optional. The prefix to match for the replication policy rule." + } + }, + "minCreationTime": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The minimum creation time to match for the replication policy rule." + } + } + }, + "nullable": true, + "metadata": { + "description": "Optional. The filters for the object replication policy rule." + } + } + }, + "metadata": { + "__bicep_export!": true, + "description": "The type of an object replication policy rule." + } + } + }, + "parameters": { + "name": { + "type": "string", + "metadata": { + "description": "Required. Name of the policy." + } + }, + "storageAccountName": { + "type": "string", + "maxLength": 24, + "metadata": { + "description": "Required. The name of the Storage Account on which to create the policy." + } + }, + "sourceStorageAccountResourceId": { + "type": "string", + "metadata": { + "description": "Required. Resource ID of the source storage account for replication." + } + }, + "destinationAccountResourceId": { + "type": "string", + "metadata": { + "description": "Required. Resource ID of the destination storage account for replication." + } + }, + "enableMetrics": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. Whether metrics are enabled for the object replication policy." + } + }, + "rules": { + "type": "array", + "items": { + "$ref": "#/definitions/objectReplicationPolicyRuleType" + }, + "metadata": { + "description": "Required. Rules for the object replication policy." + } + } + }, + "resources": { + "storageAccount": { + "existing": true, + "type": "Microsoft.Storage/storageAccounts", + "apiVersion": "2025-01-01", + "name": "[parameters('storageAccountName')]" + }, + "objectReplicationPolicy": { + "type": "Microsoft.Storage/storageAccounts/objectReplicationPolicies", + "apiVersion": "2025-01-01", + "name": "[format('{0}/{1}', parameters('storageAccountName'), parameters('name'))]", + "properties": { + "copy": [ + { + "name": "rules", + "count": "[length(parameters('rules'))]", + "input": { + "ruleId": "[tryGet(parameters('rules')[copyIndex('rules')], 'ruleId')]", + "sourceContainer": "[parameters('rules')[copyIndex('rules')].containerName]", + "destinationContainer": "[coalesce(tryGet(parameters('rules')[copyIndex('rules')], 'destinationContainerName'), parameters('rules')[copyIndex('rules')].containerName)]", + "filters": "[if(not(equals(tryGet(parameters('rules')[copyIndex('rules')], 'filters'), null())), createObject('prefixMatch', tryGet(tryGet(parameters('rules')[copyIndex('rules')], 'filters'), 'prefixMatch'), 'minCreationTime', tryGet(tryGet(parameters('rules')[copyIndex('rules')], 'filters'), 'minCreationTime')), null())]" + } + } + ], + "destinationAccount": "[parameters('destinationAccountResourceId')]", + "metrics": { + "enabled": "[coalesce(parameters('enableMetrics'), false())]" + }, + "sourceAccount": "[parameters('sourceStorageAccountResourceId')]" + } + } + }, + "outputs": { + "resourceGroupName": { + "type": "string", + "metadata": { + "description": "Resource group name of the provisioned resources." + }, + "value": "[resourceGroup().name]" + }, + "objectReplicationPolicyId": { + "type": "string", + "metadata": { + "description": "Resource ID of the created Object Replication Policy." + }, + "value": "[resourceId('Microsoft.Storage/storageAccounts/objectReplicationPolicies', parameters('storageAccountName'), parameters('name'))]" + }, + "policyId": { + "type": "string", + "metadata": { + "description": "Policy ID of the created Object Replication Policy." + }, + "value": "[reference('objectReplicationPolicy').policyId]" + }, + "rules": { + "type": "array", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.Storage/storageAccounts/objectReplicationPolicies@2025-01-01#properties/properties/properties/rules", + "output": true + }, + "description": "Rules created Object Replication Policy." + }, + "value": "[reference('objectReplicationPolicy').rules]" + } + } + } + } + }, + "sourcePolicy": { + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "[take(format('{0}-ObjRep-Policy-source-{1}', deployment().name, parameters('storageAccountName')), 64)]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "name": { + "value": "[reference('destinationPolicy').outputs.policyId.value]" + }, + "storageAccountName": { + "value": "[parameters('storageAccountName')]" + }, + "sourceStorageAccountResourceId": { + "value": "[resourceId('Microsoft.Storage/storageAccounts', parameters('storageAccountName'))]" + }, + "destinationAccountResourceId": { + "value": "[parameters('destinationAccountResourceId')]" + }, + "enableMetrics": { + "value": "[parameters('enableMetrics')]" + }, + "rules": { + "copy": [ + { + "name": "value", + "count": "[length(parameters('rules'))]", + "input": "[union(parameters('rules')[copyIndex('value')], createObject('ruleId', reference('destinationPolicy').outputs.rules.value[copyIndex('value')].ruleId))]" + } + ] + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "languageVersion": "2.0", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.39.26.7824", + "templateHash": "4325417308313318683" + }, + "name": "Storage Account Object Replication Policy", + "description": "This module deploys a Storage Account Object Replication Policy for a provided storage account." + }, + "definitions": { + "objectReplicationPolicyRuleType": { + "type": "object", + "properties": { + "ruleId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The ID of the rule. Auto-generated on destination account. Required for source account." + } + }, + "containerName": { + "type": "string", + "metadata": { + "description": "Required. The name of the source container." + } + }, + "destinationContainerName": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The name of the destination container. If not provided, the same name as the source container will be used." + } + }, + "filters": { + "type": "object", + "properties": { + "prefixMatch": { + "type": "array", + "items": { + "type": "string" + }, + "nullable": true, + "metadata": { + "description": "Optional. The prefix to match for the replication policy rule." + } + }, + "minCreationTime": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The minimum creation time to match for the replication policy rule." + } + } + }, + "nullable": true, + "metadata": { + "description": "Optional. The filters for the object replication policy rule." + } + } + }, + "metadata": { + "__bicep_export!": true, + "description": "The type of an object replication policy rule." + } + } + }, + "parameters": { + "name": { + "type": "string", + "metadata": { + "description": "Required. Name of the policy." + } + }, + "storageAccountName": { + "type": "string", + "maxLength": 24, + "metadata": { + "description": "Required. The name of the Storage Account on which to create the policy." + } + }, + "sourceStorageAccountResourceId": { + "type": "string", + "metadata": { + "description": "Required. Resource ID of the source storage account for replication." + } + }, + "destinationAccountResourceId": { + "type": "string", + "metadata": { + "description": "Required. Resource ID of the destination storage account for replication." + } + }, + "enableMetrics": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. Whether metrics are enabled for the object replication policy." + } + }, + "rules": { + "type": "array", + "items": { + "$ref": "#/definitions/objectReplicationPolicyRuleType" + }, + "metadata": { + "description": "Required. Rules for the object replication policy." + } + } + }, + "resources": { + "storageAccount": { + "existing": true, + "type": "Microsoft.Storage/storageAccounts", + "apiVersion": "2025-01-01", + "name": "[parameters('storageAccountName')]" + }, + "objectReplicationPolicy": { + "type": "Microsoft.Storage/storageAccounts/objectReplicationPolicies", + "apiVersion": "2025-01-01", + "name": "[format('{0}/{1}', parameters('storageAccountName'), parameters('name'))]", + "properties": { + "copy": [ + { + "name": "rules", + "count": "[length(parameters('rules'))]", + "input": { + "ruleId": "[tryGet(parameters('rules')[copyIndex('rules')], 'ruleId')]", + "sourceContainer": "[parameters('rules')[copyIndex('rules')].containerName]", + "destinationContainer": "[coalesce(tryGet(parameters('rules')[copyIndex('rules')], 'destinationContainerName'), parameters('rules')[copyIndex('rules')].containerName)]", + "filters": "[if(not(equals(tryGet(parameters('rules')[copyIndex('rules')], 'filters'), null())), createObject('prefixMatch', tryGet(tryGet(parameters('rules')[copyIndex('rules')], 'filters'), 'prefixMatch'), 'minCreationTime', tryGet(tryGet(parameters('rules')[copyIndex('rules')], 'filters'), 'minCreationTime')), null())]" + } + } + ], + "destinationAccount": "[parameters('destinationAccountResourceId')]", + "metrics": { + "enabled": "[coalesce(parameters('enableMetrics'), false())]" + }, + "sourceAccount": "[parameters('sourceStorageAccountResourceId')]" + } + } + }, + "outputs": { + "resourceGroupName": { + "type": "string", + "metadata": { + "description": "Resource group name of the provisioned resources." + }, + "value": "[resourceGroup().name]" + }, + "objectReplicationPolicyId": { + "type": "string", + "metadata": { + "description": "Resource ID of the created Object Replication Policy." + }, + "value": "[resourceId('Microsoft.Storage/storageAccounts/objectReplicationPolicies', parameters('storageAccountName'), parameters('name'))]" + }, + "policyId": { + "type": "string", + "metadata": { + "description": "Policy ID of the created Object Replication Policy." + }, + "value": "[reference('objectReplicationPolicy').policyId]" + }, + "rules": { + "type": "array", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.Storage/storageAccounts/objectReplicationPolicies@2025-01-01#properties/properties/properties/rules", + "output": true + }, + "description": "Rules created Object Replication Policy." + }, + "value": "[reference('objectReplicationPolicy').rules]" + } + } + } + }, + "dependsOn": [ + "destinationPolicy" + ] + } + }, + "outputs": { + "resourceGroupName": { + "type": "string", + "metadata": { + "description": "Resource group name of the provisioned resources." + }, + "value": "[resourceGroup().name]" + }, + "objectReplicationPolicyId": { + "type": "string", + "metadata": { + "description": "Resource ID of the created Object Replication Policy in the source account." + }, + "value": "[reference('sourcePolicy').outputs.objectReplicationPolicyId.value]" + }, + "policyId": { + "type": "string", + "metadata": { + "description": "Policy ID of the created Object Replication Policy in the source account." + }, + "value": "[reference('sourcePolicy').outputs.policyId.value]" + } } } }, + "dependsOn": [ + "storageAccount", + "storageAccount_blobServices" + ] + } + }, + "outputs": { + "resourceId": { + "type": "string", "metadata": { - "__bicep_export!": true, - "description": "The type describing a data disk." - } + "description": "The resource ID of the deployed storage account." + }, + "value": "[resourceId('Microsoft.Storage/storageAccounts', parameters('name'))]" }, - "publicKeyType": { + "name": { + "type": "string", + "metadata": { + "description": "The name of the deployed storage account." + }, + "value": "[parameters('name')]" + }, + "resourceGroupName": { + "type": "string", + "metadata": { + "description": "The resource group of the deployed storage account." + }, + "value": "[resourceGroup().name]" + }, + "primaryBlobEndpoint": { + "type": "string", + "metadata": { + "description": "The primary blob endpoint reference if blob services are deployed." + }, + "value": "[if(and(not(empty(parameters('blobServices'))), contains(parameters('blobServices'), 'containers')), reference(format('Microsoft.Storage/storageAccounts/{0}', parameters('name')), '2019-04-01').primaryEndpoints.blob, '')]" + }, + "systemAssignedMIPrincipalId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "The principal ID of the system assigned identity." + }, + "value": "[tryGet(tryGet(reference('storageAccount', '2025-01-01', 'full'), 'identity'), 'principalId')]" + }, + "location": { + "type": "string", + "metadata": { + "description": "The location the resource was deployed into." + }, + "value": "[reference('storageAccount', '2025-01-01', 'full').location]" + }, + "serviceEndpoints": { "type": "object", - "properties": { - "keyData": { - "type": "string", - "metadata": { - "description": "Required. Specifies the SSH public key data used to authenticate through ssh." - } - }, - "path": { - "type": "string", - "metadata": { - "description": "Required. Specifies the full path on the created VM where ssh public key is stored. If the file already exists, the specified key is appended to the file." - } + "metadata": { + "description": "All service endpoints of the deployed storage account, Note Standard_LRS and Standard_ZRS accounts only have a blob service endpoint." + }, + "value": "[reference('storageAccount').primaryEndpoints]" + }, + "privateEndpoints": { + "type": "array", + "items": { + "$ref": "#/definitions/privateEndpointOutputType" + }, + "metadata": { + "description": "The private endpoints of the Storage Account." + }, + "copy": { + "count": "[length(coalesce(parameters('privateEndpoints'), createArray()))]", + "input": { + "name": "[reference(format('storageAccount_privateEndpoints[{0}]', copyIndex())).outputs.name.value]", + "resourceId": "[reference(format('storageAccount_privateEndpoints[{0}]', copyIndex())).outputs.resourceId.value]", + "groupId": "[tryGet(tryGet(reference(format('storageAccount_privateEndpoints[{0}]', copyIndex())).outputs, 'groupId'), 'value')]", + "customDnsConfigs": "[reference(format('storageAccount_privateEndpoints[{0}]', copyIndex())).outputs.customDnsConfigs.value]", + "networkInterfaceResourceIds": "[reference(format('storageAccount_privateEndpoints[{0}]', copyIndex())).outputs.networkInterfaceResourceIds.value]" } } }, - "nicConfigurationType": { - "type": "object", - "properties": { - "name": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The name of the NIC configuration." - } - }, - "nicSuffix": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The suffix to append to the NIC name." - } - }, - "enableIPForwarding": { - "type": "bool", - "nullable": true, - "metadata": { - "description": "Optional. Indicates whether IP forwarding is enabled on this network interface." - } - }, - "enableAcceleratedNetworking": { - "type": "bool", - "nullable": true, - "metadata": { - "description": "Optional. If the network interface is accelerated networking enabled." - } - }, - "deleteOption": { - "type": "string", - "allowedValues": [ - "Delete", - "Detach" - ], - "nullable": true, - "metadata": { - "description": "Optional. Specify what happens to the network interface when the VM is deleted." - } - }, - "dnsServers": { - "type": "array", - "items": { - "type": "string" - }, - "nullable": true, - "metadata": { - "description": "Optional. List of DNS servers IP addresses. Use 'AzureProvidedDNS' to switch to azure provided DNS resolution. 'AzureProvidedDNS' value cannot be combined with other IPs, it must be the only value in dnsServers collection." - } - }, - "networkSecurityGroupResourceId": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The network security group (NSG) to attach to the network interface." - } - }, - "ipConfigurations": { - "type": "array", - "items": { - "$ref": "#/definitions/ipConfigurationType" - }, - "metadata": { - "description": "Required. The IP configurations of the network interface." - } - }, - "lock": { - "$ref": "#/definitions/lockType", - "nullable": true, - "metadata": { - "description": "Optional. The lock settings of the service." - } - }, - "tags": { - "type": "object", - "nullable": true, - "metadata": { - "description": "Optional. The tags of the public IP address." - } - }, - "enableTelemetry": { - "type": "bool", - "nullable": true, - "metadata": { - "description": "Optional. Enable/Disable usage telemetry for the module." - } - }, - "diagnosticSettings": { - "type": "array", - "items": { - "$ref": "#/definitions/diagnosticSettingFullType" - }, - "nullable": true, - "metadata": { - "description": "Optional. The diagnostic settings of the IP configuration." - } - }, - "roleAssignments": { - "type": "array", - "items": { - "$ref": "#/definitions/roleAssignmentType" - }, - "nullable": true, - "metadata": { - "description": "Optional. Array of role assignments to create." - } - } + "exportedSecrets": { + "$ref": "#/definitions/secretsOutputType", + "metadata": { + "description": "A hashtable of references to the secrets exported to the provided Key Vault. The key of each reference is each secret's name." }, + "value": "[if(not(equals(parameters('secretsExportConfiguration'), null())), toObject(reference('secretsExport').outputs.secretsSet.value, lambda('secret', last(split(lambdaVariables('secret').secretResourceId, '/'))), lambda('secret', lambdaVariables('secret'))), createObject())]" + }, + "primaryAccessKey": { + "type": "securestring", "metadata": { - "__bicep_export!": true, - "description": "The type for the NIC configuration." - } + "description": "The primary access key of the storage account." + }, + "value": "[listKeys('storageAccount', '2025-01-01').keys[0].value]" }, - "imageReferenceType": { - "type": "object", - "properties": { - "communityGalleryImageId": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. Specified the community gallery image unique id for vm deployment. This can be fetched from community gallery image GET call." - } - }, - "id": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The resource Id of the image reference." - } - }, - "offer": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. Specifies the offer of the platform image or marketplace image used to create the virtual machine." - } - }, - "publisher": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The image publisher." - } - }, - "sku": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The SKU of the image." - } - }, - "version": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. Specifies the version of the platform image or marketplace image used to create the virtual machine. The allowed formats are Major.Minor.Build or 'latest'. Even if you use 'latest', the VM image will not automatically update after deploy time even if a new version becomes available." - } - }, - "sharedGalleryImageId": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. Specified the shared gallery image unique id for vm deployment. This can be fetched from shared gallery image GET call." - } - } + "secondaryAccessKey": { + "type": "securestring", + "metadata": { + "description": "The secondary access key of the storage account." }, + "value": "[listKeys('storageAccount', '2025-01-01').keys[1].value]" + }, + "primaryConnectionString": { + "type": "securestring", "metadata": { - "__bicep_export!": true, - "description": "The type describing the image reference." - } + "description": "The primary connection string of the storage account." + }, + "value": "[format('DefaultEndpointsProtocol=https;AccountName={0};AccountKey={1};EndpointSuffix={2}', parameters('name'), listKeys('storageAccount', '2025-01-01').keys[0].value, environment().suffixes.storage)]" }, - "planType": { - "type": "object", - "properties": { - "name": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The name of the plan." - } - }, - "product": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. Specifies the product of the image from the marketplace." - } - }, - "publisher": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The publisher ID." - } - }, - "promotionCode": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The promotion code." + "secondaryConnectionString": { + "type": "securestring", + "metadata": { + "description": "The secondary connection string of the storage account." + }, + "value": "[format('DefaultEndpointsProtocol=https;AccountName={0};AccountKey={1};EndpointSuffix={2}', parameters('name'), listKeys('storageAccount', '2025-01-01').keys[1].value, environment().suffixes.storage)]" + } + } + } + }, + "dependsOn": [ + "[format('avmPrivateDnsZones[{0}]', variables('dnsZoneIndex').storageBlob)]", + "logAnalyticsWorkspace", + "userAssignedIdentity", + "virtualNetwork" + ] + }, + "cosmosDB": { + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "[take(format('avm.res.document-db.database-account.{0}', variables('cosmosDBResourceName')), 64)]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "name": { + "value": "[format('cosmos-{0}', variables('solutionSuffix'))]" + }, + "location": { + "value": "[parameters('secondaryLocation')]" + }, + "tags": { + "value": "[parameters('tags')]" + }, + "enableTelemetry": { + "value": "[parameters('enableTelemetry')]" + }, + "sqlDatabases": { + "value": [ + { + "name": "[variables('cosmosDBDatabaseName')]", + "containers": [ + { + "name": "[variables('cosmosDBConversationsContainer')]", + "paths": [ + "/userId" + ] + }, + { + "name": "[variables('cosmosDBProductsContainer')]", + "paths": [ + "/category" + ] } - } + ] + } + ] + }, + "sqlRoleDefinitions": { + "value": [ + { + "roleName": "contentgen-data-contributor", + "dataActions": [ + "Microsoft.DocumentDB/databaseAccounts/readMetadata", + "Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers/*", + "Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers/items/*" + ] + } + ] + }, + "sqlRoleAssignments": { + "value": [ + { + "principalId": "[reference('userAssignedIdentity').outputs.principalId.value]", + "roleDefinitionId": "00000000-0000-0000-0000-000000000002" }, - "metadata": { - "__bicep_export!": true, - "description": "Specifies information about the marketplace image used to create the virtual machine." + { + "principalId": "[deployer().objectId]", + "roleDefinitionId": "00000000-0000-0000-0000-000000000002" } + ] + }, + "diagnosticSettings": "[if(parameters('enableMonitoring'), createObject('value', createArray(createObject('workspaceResourceId', if(variables('useExistingLogAnalytics'), parameters('existingLogAnalyticsWorkspaceId'), if(parameters('enableMonitoring'), reference('logAnalyticsWorkspace').outputs.resourceId.value, ''))))), createObject('value', null()))]", + "networkRestrictions": { + "value": { + "networkAclBypass": "AzureServices", + "publicNetworkAccess": "[if(parameters('enablePrivateNetworking'), 'Disabled', 'Enabled')]" + } + }, + "zoneRedundant": { + "value": "[parameters('enableRedundancy')]" + }, + "capabilitiesToAdd": "[if(parameters('enableRedundancy'), createObject('value', null()), createObject('value', createArray('EnableServerless')))]", + "enableAutomaticFailover": { + "value": "[parameters('enableRedundancy')]" + }, + "failoverLocations": "[if(parameters('enableRedundancy'), createObject('value', createArray(createObject('failoverPriority', 0, 'isZoneRedundant', true(), 'locationName', parameters('secondaryLocation')), createObject('failoverPriority', 1, 'isZoneRedundant', true(), 'locationName', variables('cosmosDbHaLocation')))), createObject('value', createArray(createObject('locationName', parameters('secondaryLocation'), 'failoverPriority', 0, 'isZoneRedundant', false()))))]", + "privateEndpoints": "[if(parameters('enablePrivateNetworking'), createObject('value', createArray(createObject('service', 'Sql', 'subnetResourceId', reference('virtualNetwork').outputs.pepsSubnetResourceId.value, 'privateDnsZoneGroup', createObject('privateDnsZoneGroupConfigs', createArray(createObject('privateDnsZoneResourceId', reference(format('avmPrivateDnsZones[{0}]', variables('dnsZoneIndex').cosmosDB)).outputs.resourceId.value)))))), createObject('value', null()))]" + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "languageVersion": "2.0", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.38.33.27573", + "templateHash": "11889744396543212232" }, - "autoShutDownConfigType": { + "name": "Azure Cosmos DB account", + "description": "This module deploys an Azure Cosmos DB account. The API used for the account is determined by the child resources that are deployed." + }, + "definitions": { + "privateEndpointOutputType": { "type": "object", "properties": { - "status": { + "name": { "type": "string", - "allowedValues": [ - "Disabled", - "Enabled" - ], - "nullable": true, "metadata": { - "description": "Optional. The status of the auto shutdown configuration." + "description": "The name of the private endpoint." } }, - "timeZone": { + "resourceId": { "type": "string", - "nullable": true, "metadata": { - "description": "Optional. The time zone ID (e.g. China Standard Time, Greenland Standard Time, Pacific Standard time, etc.)." + "description": "The resource ID of the private endpoint." } }, - "dailyRecurrenceTime": { + "groupId": { "type": "string", "nullable": true, "metadata": { - "description": "Optional. The time of day the schedule will occur." - } - }, - "notificationSettings": { - "type": "object", - "properties": { - "status": { - "type": "string", - "allowedValues": [ - "Disabled", - "Enabled" - ], - "nullable": true, - "metadata": { - "description": "Optional. The status of the notification settings." - } - }, - "emailRecipient": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The email address to send notifications to (can be a list of semi-colon separated email addresses)." - } - }, - "notificationLocale": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The locale to use when sending a notification (fallback for unsupported languages is EN)." - } - }, - "webhookUrl": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The webhook URL to which the notification will be sent." - } - }, - "timeInMinutes": { - "type": "int", - "nullable": true, - "metadata": { - "description": "Optional. The time in minutes before shutdown to send notifications." - } - } - }, - "nullable": true, - "metadata": { - "description": "Optional. The resource ID of the schedule." - } - } - }, - "metadata": { - "__bicep_export!": true, - "description": "The type describing the configuration profile." - } - }, - "vaultSecretGroupType": { - "type": "object", - "properties": { - "sourceVault": { - "$ref": "#/definitions/subResourceType", - "nullable": true, - "metadata": { - "description": "Optional. The relative URL of the Key Vault containing all of the certificates in VaultCertificates." + "description": "The group ID for the private endpoint group." } }, - "vaultCertificates": { + "customDnsConfigs": { "type": "array", "items": { "type": "object", "properties": { - "certificateStore": { + "fqdn": { "type": "string", "nullable": true, "metadata": { - "description": "Optional. For Windows VMs, specifies the certificate store on the Virtual Machine to which the certificate should be added. The specified certificate store is implicitly in the LocalMachine account. For Linux VMs, the certificate file is placed under the /var/lib/waagent directory, with the file name .crt for the X509 certificate file and .prv for private key. Both of these files are .pem formatted." + "description": "fully-qualified domain name (FQDN) that resolves to private endpoint IP address." } }, - "certificateUrl": { - "type": "string", - "nullable": true, + "ipAddresses": { + "type": "array", + "items": { + "type": "string" + }, "metadata": { - "description": "Optional. This is the URL of a certificate that has been uploaded to Key Vault as a secret." + "description": "A list of private IP addresses for the private endpoint." } } } }, - "nullable": true, "metadata": { - "description": "Optional. The list of key vault references in SourceVault which contain certificates." + "description": "The custom DNS configurations of the private endpoint." + } + }, + "networkInterfaceResourceIds": { + "type": "array", + "items": { + "type": "string" + }, + "metadata": { + "description": "The IDs of the network interfaces associated with the private endpoint." } } }, "metadata": { "__bicep_export!": true, - "description": "The type describing the set of certificates that should be installed onto the virtual machine." + "description": "The type for the private endpoint output." } }, - "vmGalleryApplicationType": { + "failoverLocationType": { "type": "object", "properties": { - "packageReferenceId": { - "type": "string", + "failoverPriority": { + "type": "int", "metadata": { - "description": "Required. Specifies the GalleryApplicationVersion resource id on the form of /subscriptions/{SubscriptionId}/resourceGroups/{ResourceGroupName}/providers/Microsoft.Compute/galleries/{galleryName}/applications/{application}/versions/{version}." + "description": "Required. The failover priority of the region. A failover priority of 0 indicates a write region. The maximum value for a failover priority = (total number of regions - 1). Failover priority values must be unique for each of the regions in which the database account exists." } }, - "configurationReference": { - "type": "string", + "isZoneRedundant": { + "type": "bool", "nullable": true, "metadata": { - "description": "Optional. Specifies the uri to an azure blob that will replace the default configuration for the package if provided." + "description": "Optional. Flag to indicate whether or not this region is an AvailabilityZone region. Defaults to true." } }, - "enableAutomaticUpgrade": { - "type": "bool", + "locationName": { + "type": "string", + "metadata": { + "description": "Required. The name of the region." + } + } + }, + "metadata": { + "__bicep_export!": true, + "description": "The type for the failover location." + } + }, + "sqlRoleAssignmentType": { + "type": "object", + "properties": { + "name": { + "type": "string", "nullable": true, "metadata": { - "description": "Optional. If set to true, when a new Gallery Application version is available in PIR/SIG, it will be automatically updated for the VM/VMSS." + "description": "Optional. The unique name of the role assignment." } }, - "order": { - "type": "int", - "nullable": true, + "roleDefinitionId": { + "type": "string", "metadata": { - "description": "Optional. Specifies the order in which the packages have to be installed." + "description": "Required. The unique identifier of the Azure Cosmos DB for NoSQL native role-based access control definition." } }, - "tags": { + "principalId": { "type": "string", - "nullable": true, "metadata": { - "description": "Optional. Specifies a passthrough value for more generic context." + "description": "Required. The unique identifier for the associated Microsoft Entra ID principal to which access is being granted through this role-based access control assignment. The tenant ID for the principal is inferred using the tenant associated with the subscription." } }, - "treatFailureAsDeploymentFailure": { - "type": "bool", + "scope": { + "type": "string", "nullable": true, "metadata": { - "description": "Optional. If true, any failure for any operation in the VmApplication will fail the deployment." + "description": "Optional. The data plane resource id for which access is being granted through this Role Assignment. Defaults to the root of the database account, but can also be scoped to e.g., the container and database level." } } }, "metadata": { "__bicep_export!": true, - "description": "The type describing the gallery application that should be made available to the VM/VMSS." + "description": "The type for an Azure Cosmos DB for NoSQL native role-based access control assignment." } }, - "additionalUnattendContentType": { + "sqlRoleDefinitionType": { "type": "object", "properties": { - "settingName": { + "name": { "type": "string", - "allowedValues": [ - "AutoLogon", - "FirstLogonCommands" - ], "nullable": true, "metadata": { - "description": "Optional. Specifies the name of the setting to which the content applies." + "description": "Optional. The unique identifier of the role-based access control definition." } }, - "content": { + "roleName": { "type": "string", + "metadata": { + "description": "Required. A user-friendly name for the role-based access control definition. This must be unique within the database account." + } + }, + "dataActions": { + "type": "array", + "items": { + "type": "string" + }, + "minLength": 1, + "metadata": { + "description": "Required. An array of data actions that are allowed." + } + }, + "assignableScopes": { + "type": "array", + "items": { + "type": "string" + }, "nullable": true, "metadata": { - "description": "Optional. Specifies the XML formatted content that is added to the unattend.xml file for the specified path and component. The XML must be less than 4KB and must include the root element for the setting or feature that is being inserted." + "description": "Optional. A set of fully-qualified scopes at or below which role-based access control assignments may be created using this definition. This setting allows application of this definition on the entire account or any underlying resource. This setting must have at least one element. Scopes higher than the account level are not enforceable as assignable scopes. Resources referenced in assignable scopes do not need to exist at creation. Defaults to the current account scope." + } + }, + "assignments": { + "type": "array", + "items": { + "$ref": "#/definitions/nestedSqlRoleAssignmentType" + }, + "nullable": true, + "metadata": { + "description": "Optional. An array of role-based access control assignments to be created for the definition." } } }, "metadata": { "__bicep_export!": true, - "description": "The type describing additional base-64 encoded XML formatted information that can be included in the Unattend.xml file, which is used by Windows Setup." + "description": "The type for an Azure Cosmos DB for NoSQL or Table native role-based access control definition." } }, - "winRMListenerType": { + "networkRestrictionType": { "type": "object", "properties": { - "certificateUrl": { + "ipRules": { + "type": "array", + "items": { + "type": "string" + }, + "nullable": true, + "metadata": { + "description": "Optional. A single IPv4 address or a single IPv4 address range in Classless Inter-Domain Routing (CIDR) format. Provided IPs must be well-formatted and cannot be contained in one of the following ranges: `10.0.0.0/8`, `100.64.0.0/10`, `172.16.0.0/12`, `192.168.0.0/16`, since these are not enforceable by the IP address filter. Example of valid inputs: `23.40.210.245` or `23.40.210.0/8`." + } + }, + "networkAclBypass": { "type": "string", + "allowedValues": [ + "AzureServices", + "None" + ], "nullable": true, "metadata": { - "description": "Optional. The URL of a certificate that has been uploaded to Key Vault as a secret." + "description": "Optional. Specifies the network ACL bypass for Azure services. Default to \"None\"." } }, - "protocol": { + "publicNetworkAccess": { "type": "string", "allowedValues": [ - "Http", - "Https" + "Disabled", + "Enabled" ], "nullable": true, "metadata": { - "description": "Optional. Specifies the protocol of WinRM listener." + "description": "Optional. Whether requests from the public network are allowed. Default to \"Disabled\"." + } + }, + "virtualNetworkRules": { + "type": "array", + "items": { + "type": "object", + "properties": { + "subnetResourceId": { + "type": "string", + "metadata": { + "description": "Required. Resource ID of a subnet." + } + } + } + }, + "nullable": true, + "metadata": { + "description": "Optional. List of virtual network access control list (ACL) rules configured for the account." + } + }, + "networkAclBypassResourceIds": { + "type": "array", + "items": { + "type": "string" + }, + "nullable": true, + "metadata": { + "description": "Optional. An array that contains the Resource Ids for Network Acl Bypass for the Cosmos DB account." } } }, "metadata": { "__bicep_export!": true, - "description": "The type describing a Windows Remote Management listener." + "description": "The type for the network restriction." } }, - "nicConfigurationOutputType": { + "gremlinDatabaseType": { "type": "object", "properties": { "name": { "type": "string", "metadata": { - "description": "Required. The name of the NIC configuration." + "description": "Required. Name of the Gremlin database." } }, - "ipConfigurations": { + "tags": { + "type": "object", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.DocumentDB/databaseAccounts/gremlinDatabases@2024-11-15#properties/tags" + }, + "description": "Optional. Tags of the Gremlin database resource." + }, + "nullable": true + }, + "graphs": { "type": "array", "items": { - "$ref": "#/definitions/networkInterfaceIPConfigurationOutputType" + "$ref": "#/definitions/graphType" }, + "nullable": true, "metadata": { - "description": "Required. List of IP configurations of the NIC configuration." + "description": "Optional. Array of graphs to deploy in the Gremlin database." + } + }, + "maxThroughput": { + "type": "int", + "nullable": true, + "metadata": { + "description": "Optional. Represents maximum throughput, the resource can scale up to. Cannot be set together with `throughput`. If `throughput` is set to something else than -1, this autoscale setting is ignored. Setting throughput at the database level is only recommended for development/test or when workload across all graphs in the shared throughput database is uniform. For best performance for large production workloads, it is recommended to set dedicated throughput (autoscale or manual) at the graph level and not at the database level." + } + }, + "throughput": { + "type": "int", + "nullable": true, + "metadata": { + "description": "Optional. Request Units per second (for example 10000). Cannot be set together with `maxThroughput`. Setting throughput at the database level is only recommended for development/test or when workload across all graphs in the shared throughput database is uniform. For best performance for large production workloads, it is recommended to set dedicated throughput (autoscale or manual) at the graph level and not at the database level." } } }, "metadata": { "__bicep_export!": true, - "description": "The type describing the network interface configuration output." + "description": "The type for a gremlin databae." } }, - "extensionCustomScriptConfigType": { + "mongoDbType": { "type": "object", "properties": { "name": { "type": "string", - "nullable": true, "metadata": { - "description": "Optional. The name of the virtual machine extension. Defaults to `CustomScriptExtension`." + "description": "Required. Name of the mongodb database." } }, - "typeHandlerVersion": { - "type": "string", + "throughput": { + "type": "int", "nullable": true, "metadata": { - "description": "Optional. Specifies the version of the script handler. Defaults to `1.10` for Windows and `2.1` for Linux." + "description": "Optional. Request Units per second. Setting throughput at the database level is only recommended for development/test or when workload across all collections in the shared throughput database is uniform. For best performance for large production workloads, it is recommended to set dedicated throughput (autoscale or manual) at the collection level and not at the database level." } }, - "autoUpgradeMinorVersion": { - "type": "bool", + "collections": { + "type": "array", + "items": { + "$ref": "#/definitions/collectionType" + }, "nullable": true, "metadata": { - "description": "Optional. Indicates whether the extension should use a newer minor version if one is available at deployment time. Once deployed, however, the extension will not upgrade minor versions unless redeployed, even with this property set to true. Defaults to `true`." + "description": "Optional. Collections in the mongodb database." } }, - "forceUpdateTag": { - "type": "string", - "nullable": true, + "autoscaleSettings": { + "type": "object", "metadata": { - "description": "Optional. How the extension handler should be forced to update even if the extension configuration has not changed." - } + "__bicep_resource_derived_type!": { + "source": "Microsoft.DocumentDB/databaseAccounts/mongodbDatabases@2025-04-15#properties/properties/properties/options/properties/autoscaleSettings" + }, + "description": "Optional. Specifies the Autoscale settings. Note: Either throughput or autoscaleSettings is required, but not both." + }, + "nullable": true }, - "settings": { + "tags": { "type": "object", - "properties": { - "commandToExecute": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Conditional. The entry point script to run. If the command contains any credentials, use the same property of the `protectedSettings` instead. Required if `protectedSettings.commandToExecute` is not provided." - } + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.DocumentDB/databaseAccounts/mongodbDatabases@2025-04-15#properties/tags" }, - "fileUris": { - "type": "array", - "items": { - "type": "string" - }, - "nullable": true, - "metadata": { - "description": "Optional. URLs for files to be downloaded. If URLs are sensitive, for example, if they contain keys, this field should be specified in `protectedSettings`." - } - } + "description": "Optional. Tags of the resource." }, - "nullable": true, + "nullable": true + } + }, + "metadata": { + "__bicep_export!": true, + "description": "The type for a mongo databae." + } + }, + "sqlDatabaseType": { + "type": "object", + "properties": { + "name": { + "type": "string", "metadata": { - "description": "Optional. The configuration of the custom script extension. Note: You can provide any property either in the `settings` or `protectedSettings` but not both. If your property contains secrets, use `protectedSettings`." + "description": "Required. Name of the SQL database ." } }, - "protectedSettings": { - "type": "secureObject", - "properties": { - "commandToExecute": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Conditional. The entry point script to run. Use this property if your command contains secrets such as passwords or if your file URIs are sensitive. Required if `settings.commandToExecute` is not provided." - } - }, - "storageAccountName": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The name of storage account. If you specify storage credentials, all fileUris values must be URLs for Azure blobs.." - } - }, - "storageAccountKey": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The access key of the storage account." - } - }, - "managedIdentityResourceId": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The managed identity for downloading files. Must not be used in conjunction with the `storageAccountName` or `storageAccountKey` property. If you want to use the VM's system assigned identity, set the `value` to an empty string." - } - }, - "fileUris": { - "type": "array", - "items": { - "type": "string" - }, - "nullable": true, - "metadata": { - "description": "Optional. URLs for files to be downloaded." - } - } + "containers": { + "type": "array", + "items": { + "$ref": "#/definitions/containerType" }, "nullable": true, "metadata": { - "description": "Optional. The configuration of the custom script extension. Note: You can provide any property either in the `settings` or `protectedSettings` but not both. If your property contains secrets, use `protectedSettings`." + "description": "Optional. Array of containers to deploy in the SQL database." } }, - "supressFailures": { - "type": "bool", + "throughput": { + "type": "int", "nullable": true, "metadata": { - "description": "Optional. Indicates whether failures stemming from the extension will be suppressed (Operational failures such as not connecting to the VM will not be suppressed regardless of this value). Defaults to `false`." + "description": "Optional. Request units per second. Will be ignored if autoscaleSettingsMaxThroughput is used. Setting throughput at the database level is only recommended for development/test or when workload across all containers in the shared throughput database is uniform. For best performance for large production workloads, it is recommended to set dedicated throughput (autoscale or manual) at the container level and not at the database level." } }, - "enableAutomaticUpgrade": { - "type": "bool", + "autoscaleSettingsMaxThroughput": { + "type": "int", "nullable": true, "metadata": { - "description": "Optional. Indicates whether the extension should be automatically upgraded by the platform if there is a newer version of the extension available. Defaults to `false`." + "description": "Optional. Specifies the Autoscale settings and represents maximum throughput, the resource can scale up to. The autoscale throughput should have valid throughput values between 1000 and 1000000 inclusive in increments of 1000. If value is set to null, then autoscale will be disabled. Setting throughput at the database level is only recommended for development/test or when workload across all containers in the shared throughput database is uniform. For best performance for large production workloads, it is recommended to set dedicated throughput (autoscale or manual) at the container level and not at the database level." } }, "tags": { "type": "object", "metadata": { "__bicep_resource_derived_type!": { - "source": "Microsoft.Compute/virtualMachines/extensions@2024-11-01#properties/tags" + "source": "Microsoft.DocumentDB/databaseAccounts/sqlDatabases@2025-04-15#properties/tags" }, - "description": "Optional. Tags of the resource." + "description": "Optional. Tags of the SQL database resource." }, "nullable": true + } + }, + "metadata": { + "__bicep_export!": true, + "description": "The type for a sql database." + } + }, + "tableType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "metadata": { + "description": "Required. Name of the table." + } }, - "protectedSettingsFromKeyVault": { + "tags": { "type": "object", "metadata": { "__bicep_resource_derived_type!": { - "source": "Microsoft.Compute/virtualMachines/extensions@2024-11-01#properties/properties/properties/protectedSettingsFromKeyVault" + "source": "Microsoft.DocumentDB/databaseAccounts/tables@2025-04-15#properties/tags" }, - "description": "Optional. The extensions protected settings that are passed by reference, and consumed from key vault." + "description": "Optional. Tags for the table." }, "nullable": true }, - "provisionAfterExtensions": { - "type": "array", + "maxThroughput": { + "type": "int", + "nullable": true, "metadata": { - "__bicep_resource_derived_type!": { - "source": "Microsoft.Compute/virtualMachines/extensions@2024-11-01#properties/properties/properties/provisionAfterExtensions" - }, - "description": "Optional. Collection of extension names after which this extension needs to be provisioned." - }, - "nullable": true + "description": "Optional. Represents maximum throughput, the resource can scale up to. Cannot be set together with `throughput`. If `throughput` is set to something else than -1, this autoscale setting is ignored." + } + }, + "throughput": { + "type": "int", + "nullable": true, + "metadata": { + "description": "Optional. Request Units per second (for example 10000). Cannot be set together with `maxThroughput`." + } } }, "metadata": { "__bicep_export!": true, - "description": "The type of a 'CustomScriptExtension' extension." + "description": "The type for a table." } }, - "_1.applicationGatewayBackendAddressPoolsType": { + "cassandraStandaloneRoleAssignmentType": { "type": "object", "properties": { - "id": { + "name": { "type": "string", "nullable": true, "metadata": { - "description": "Optional. Resource ID of the backend address pool." + "description": "Optional. The unique name of the role assignment." + } + }, + "roleDefinitionId": { + "type": "string", + "metadata": { + "description": "Required. The unique identifier of the Azure Cosmos DB for Apache Cassandra native role-based access control definition." + } + }, + "principalId": { + "type": "string", + "metadata": { + "description": "Required. The unique identifier for the associated Microsoft Entra ID principal to which access is being granted through this role-based access control assignment. The tenant ID for the principal is inferred using the tenant associated with the subscription." } }, + "scope": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The data plane resource path for which access is being granted through this role-based access control assignment. Defaults to the current account." + } + } + }, + "metadata": { + "__bicep_export!": true, + "description": "The type for an Azure Cosmos DB for Apache Cassandra native role-based access control assignment." + } + }, + "cassandraRoleDefinitionType": { + "type": "object", + "properties": { "name": { "type": "string", "nullable": true, "metadata": { - "description": "Optional. Name of the backend address pool that is unique within an Application Gateway." + "description": "Optional. The unique identifier of the role-based access control definition." } }, - "properties": { - "type": "object", - "properties": { - "backendAddresses": { - "type": "array", - "items": { - "type": "object", - "properties": { - "ipAddress": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. IP address of the backend address." - } - }, - "fqdn": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. FQDN of the backend address." - } - } - } - }, - "nullable": true, - "metadata": { - "description": "Optional. Backend addresses." - } - } + "roleName": { + "type": "string", + "metadata": { + "description": "Required. A user-friendly name for the role-based access control definition. Must be unique for the database account." + } + }, + "dataActions": { + "type": "array", + "items": { + "type": "string" }, "nullable": true, "metadata": { - "description": "Optional. Properties of the application gateway backend address pool." + "description": "Optional. An array of data actions that are allowed. Note: Valid data action strings are currently undocumented (API version 2025-05-01-preview). Expected to follow format similar to SQL RBAC once documented by Microsoft." + } + }, + "notDataActions": { + "type": "array", + "items": { + "type": "string" + }, + "nullable": true, + "metadata": { + "description": "Optional. An array of data actions that are denied. Note: Unlike SQL RBAC, Cassandra supports deny rules for granular access control. Valid data action strings are currently undocumented (API version 2025-05-01-preview)." + } + }, + "assignableScopes": { + "type": "array", + "items": { + "type": "string" + }, + "nullable": true, + "metadata": { + "description": "Optional. A set of fully qualified Scopes at or below which Role Assignments may be created using this Role Definition." + } + }, + "assignments": { + "type": "array", + "items": { + "$ref": "#/definitions/cassandraRoleAssignmentType" + }, + "nullable": true, + "metadata": { + "description": "Optional. An array of role-based access control assignments to be created for the definition." } } }, "metadata": { - "description": "The type for the application gateway backend address pool.", - "__bicep_imported_from!": { - "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/res/network/network-interface:0.5.1" - } + "__bicep_export!": true, + "description": "The type for an Azure Cosmos DB for Apache Cassandra native role-based access control definition." } }, - "_1.applicationSecurityGroupType": { + "cassandraKeyspaceType": { "type": "object", "properties": { - "id": { + "name": { "type": "string", + "metadata": { + "description": "Required. Name of the Cassandra keyspace." + } + }, + "tables": { + "type": "array", + "items": { + "$ref": "#/definitions/cassandraTableType" + }, "nullable": true, "metadata": { - "description": "Optional. Resource ID of the application security group." + "description": "Optional. Array of Cassandra tables to deploy in the keyspace." } }, - "location": { - "type": "string", + "views": { + "type": "array", + "items": { + "$ref": "#/definitions/cassandraViewType" + }, "nullable": true, "metadata": { - "description": "Optional. Location of the application security group." + "description": "Optional. Array of Cassandra views (materialized views) to deploy in the keyspace." } }, - "properties": { - "type": "object", + "autoscaleSettingsMaxThroughput": { + "type": "int", "nullable": true, "metadata": { - "description": "Optional. Properties of the application security group." + "description": "Optional. Represents maximum throughput, the resource can scale up to. Cannot be set together with `throughput`. If `throughput` is set to something else than -1, this autoscale setting is ignored. Setting throughput at the keyspace level is only recommended for development/test or when workload across all tables in the shared throughput keyspace is uniform. For best performance for large production workloads, it is recommended to set dedicated throughput (autoscale or manual) at the table level and not at the keyspace level." + } + }, + "throughput": { + "type": "int", + "nullable": true, + "metadata": { + "description": "Optional. Request Units per second (for example 10000). Cannot be set together with `autoscaleSettingsMaxThroughput`. Setting throughput at the keyspace level is only recommended for development/test or when workload across all tables in the shared throughput keyspace is uniform. For best performance for large production workloads, it is recommended to set dedicated throughput (autoscale or manual) at the table level and not at the keyspace level." } }, "tags": { "type": "object", - "nullable": true, "metadata": { - "description": "Optional. Tags of the application security group." + "__bicep_resource_derived_type!": { + "source": "Microsoft.DocumentDB/databaseAccounts/cassandraKeyspaces@2024-11-15#properties/tags" + }, + "description": "Optional. Tags of the Cassandra keyspace resource." + }, + "nullable": true + } + }, + "metadata": { + "__bicep_export!": true, + "description": "The type for an Azure Cosmos DB Cassandra keyspace." + } + }, + "defaultIdentityType": { + "type": "object", + "discriminator": { + "propertyName": "name", + "mapping": { + "FirstPartyIdentity": { + "$ref": "#/definitions/defaultIdentityFirstPartyType" + }, + "SystemAssignedIdentity": { + "$ref": "#/definitions/defaultIdentitySystemAssignedType" + }, + "UserAssignedIdentity": { + "$ref": "#/definitions/defaultIdentityUserAssignedType" } } }, "metadata": { - "description": "The type for the application security group.", - "__bicep_imported_from!": { - "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/res/network/network-interface:0.5.1" + "__bicep_export!": true, + "description": "The type for the default identity." + } + }, + "defaultIdentityFirstPartyType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "allowedValues": [ + "FirstPartyIdentity" + ], + "metadata": { + "description": "Required. The type of default identity to use." + } } } }, - "_1.backendAddressPoolType": { + "defaultIdentitySystemAssignedType": { "type": "object", "properties": { - "id": { + "name": { "type": "string", - "nullable": true, + "allowedValues": [ + "SystemAssignedIdentity" + ], "metadata": { - "description": "Optional. The resource ID of the backend address pool." + "description": "Required. The type of default identity to use." } - }, + } + } + }, + "defaultIdentityUserAssignedType": { + "type": "object", + "properties": { "name": { "type": "string", - "nullable": true, + "allowedValues": [ + "UserAssignedIdentity" + ], "metadata": { - "description": "Optional. The name of the backend address pool." + "description": "Required. The type of default identity to use." } }, - "properties": { - "type": "object", - "nullable": true, + "resourceId": { + "type": "string", "metadata": { - "description": "Optional. The properties of the backend address pool." + "description": "Required. The resource ID of the user assigned identity to use as the default identity." } } - }, - "metadata": { - "description": "The type for a backend address pool.", - "__bicep_imported_from!": { - "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/res/network/network-interface:0.5.1" - } } }, - "_1.inboundNatRuleType": { + "_1.privateEndpointCustomDnsConfigType": { "type": "object", "properties": { - "id": { + "fqdn": { "type": "string", "nullable": true, "metadata": { - "description": "Optional. Resource ID of the inbound NAT rule." + "description": "Optional. FQDN that resolves to private endpoint IP address." } }, + "ipAddresses": { + "type": "array", + "items": { + "type": "string" + }, + "metadata": { + "description": "Required. A list of private IP addresses of the private endpoint." + } + } + }, + "metadata": { + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.1" + } + } + }, + "_1.privateEndpointIpConfigurationType": { + "type": "object", + "properties": { "name": { "type": "string", - "nullable": true, "metadata": { - "description": "Optional. Name of the resource that is unique within the set of inbound NAT rules used by the load balancer. This name can be used to access the resource." + "description": "Required. The name of the resource that is unique within a resource group." } }, "properties": { "type": "object", "properties": { - "backendAddressPool": { - "$ref": "#/definitions/subResourceType", - "nullable": true, - "metadata": { - "description": "Optional. A reference to backendAddressPool resource." - } - }, - "backendPort": { - "type": "int", - "nullable": true, - "metadata": { - "description": "Optional. The port used for the internal endpoint. Acceptable values range from 1 to 65535." - } - }, - "enableFloatingIP": { - "type": "bool", - "nullable": true, - "metadata": { - "description": "Optional. Configures a virtual machine's endpoint for the floating IP capability required to configure a SQL AlwaysOn Availability Group. This setting is required when using the SQL AlwaysOn Availability Groups in SQL server. This setting can't be changed after you create the endpoint." - } - }, - "enableTcpReset": { - "type": "bool", - "nullable": true, - "metadata": { - "description": "Optional. Receive bidirectional TCP Reset on TCP flow idle timeout or unexpected connection termination. This element is only used when the protocol is set to TCP." - } - }, - "frontendIPConfiguration": { - "$ref": "#/definitions/subResourceType", - "nullable": true, - "metadata": { - "description": "Optional. A reference to frontend IP addresses." - } - }, - "frontendPort": { - "type": "int", - "nullable": true, - "metadata": { - "description": "Optional. The port for the external endpoint. Port numbers for each rule must be unique within the Load Balancer. Acceptable values range from 1 to 65534." - } - }, - "frontendPortRangeStart": { - "type": "int", - "nullable": true, + "groupId": { + "type": "string", "metadata": { - "description": "Optional. The port range start for the external endpoint. This property is used together with BackendAddressPool and FrontendPortRangeEnd. Individual inbound NAT rule port mappings will be created for each backend address from BackendAddressPool. Acceptable values range from 1 to 65534." + "description": "Required. The ID of a group obtained from the remote resource that this private endpoint should connect to." } }, - "frontendPortRangeEnd": { - "type": "int", - "nullable": true, + "memberName": { + "type": "string", "metadata": { - "description": "Optional. The port range end for the external endpoint. This property is used together with BackendAddressPool and FrontendPortRangeStart. Individual inbound NAT rule port mappings will be created for each backend address from BackendAddressPool. Acceptable values range from 1 to 65534." + "description": "Required. The member name of a group obtained from the remote resource that this private endpoint should connect to." } }, - "protocol": { + "privateIPAddress": { "type": "string", - "allowedValues": [ - "All", - "Tcp", - "Udp" - ], - "nullable": true, "metadata": { - "description": "Optional. The reference to the transport protocol used by the load balancing rule." + "description": "Required. A private IP address obtained from the private endpoint's subnet." } } }, - "nullable": true, "metadata": { - "description": "Optional. Properties of the inbound NAT rule." + "description": "Required. Properties of private endpoint IP configurations." } } }, "metadata": { - "description": "The type for the inbound NAT rule.", "__bicep_imported_from!": { - "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/res/network/network-interface:0.5.1" + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.1" } } }, - "_1.virtualNetworkTapType": { + "_1.privateEndpointPrivateDnsZoneGroupType": { "type": "object", "properties": { - "id": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. Resource ID of the virtual network tap." - } - }, - "location": { + "name": { "type": "string", "nullable": true, "metadata": { - "description": "Optional. Location of the virtual network tap." - } - }, - "properties": { - "type": "object", - "nullable": true, - "metadata": { - "description": "Optional. Properties of the virtual network tap." + "description": "Optional. The name of the Private DNS Zone Group." } }, - "tags": { - "type": "object", - "nullable": true, + "privateDnsZoneGroupConfigs": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The name of the private DNS Zone Group config." + } + }, + "privateDnsZoneResourceId": { + "type": "string", + "metadata": { + "description": "Required. The resource id of the private DNS zone." + } + } + } + }, "metadata": { - "description": "Optional. Tags of the virtual network tap." + "description": "Required. The private DNS Zone Groups to associate the Private Endpoint. A DNS Zone Group can support up to 5 DNS zones." } } }, "metadata": { - "description": "The type for the virtual network tap.", "__bicep_imported_from!": { - "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/res/network/network-interface:0.5.1" + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.1" } } }, - "_2.ddosSettingsType": { + "cassandraRoleAssignmentType": { "type": "object", "properties": { - "ddosProtectionPlan": { - "type": "object", - "properties": { - "id": { - "type": "string", - "metadata": { - "description": "Required. The resource ID of the DDOS protection plan associated with the public IP address." - } - } - }, + "name": { + "type": "string", "nullable": true, "metadata": { - "description": "Optional. The DDoS protection plan associated with the public IP address." + "description": "Optional. The unique identifier of the role assignment." } }, - "protectionMode": { + "principalId": { "type": "string", - "allowedValues": [ - "Enabled" - ], "metadata": { - "description": "Required. The DDoS protection policy customizations." + "description": "Required. The unique identifier for the associated AAD principal." + } + }, + "scope": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The data plane resource path for which access is being granted. Defaults to the current account." } } }, "metadata": { "__bicep_imported_from!": { - "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/res/network/public-ip-address:0.8.0" + "sourceTemplate": "cassandra-role-definition/main.bicep" } } }, - "_2.dnsSettingsType": { + "cassandraTableType": { "type": "object", "properties": { - "domainNameLabel": { + "name": { "type": "string", "metadata": { - "description": "Required. The domain name label. The concatenation of the domain name label and the regionalized DNS zone make up the fully qualified domain name associated with the public IP address. If a domain name label is specified, an A DNS record is created for the public IP in the Microsoft Azure DNS system." + "description": "Required. Name of the table." } }, - "domainNameLabelScope": { - "type": "string", - "allowedValues": [ - "NoReuse", - "ResourceGroupReuse", - "SubscriptionReuse", - "TenantReuse" - ], - "nullable": true, + "schema": { + "type": "object", "metadata": { - "description": "Optional. The domain name label scope. If a domain name label and a domain name label scope are specified, an A DNS record is created for the public IP in the Microsoft Azure DNS system with a hashed value includes in FQDN." + "__bicep_resource_derived_type!": { + "source": "Microsoft.DocumentDB/databaseAccounts/cassandraKeyspaces/tables@2024-11-15#properties/properties/properties/resource/properties/schema" + }, + "description": "Required. Schema definition for the table." } }, - "fqdn": { - "type": "string", + "tags": { + "type": "object", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.DocumentDB/databaseAccounts/cassandraKeyspaces/tables@2024-11-15#properties/tags" + }, + "description": "Optional. Tags for the table." + }, + "nullable": true + }, + "defaultTtl": { + "type": "int", "nullable": true, "metadata": { - "description": "Optional. The Fully Qualified Domain Name of the A DNS record associated with the public IP. This is the concatenation of the domainNameLabel and the regionalized DNS zone." + "description": "Optional. Default TTL (Time To Live) in seconds for data in the table." } }, - "reverseFqdn": { - "type": "string", + "analyticalStorageTtl": { + "type": "int", "nullable": true, "metadata": { - "description": "Optional. The reverse FQDN. A user-visible, fully qualified domain name that resolves to this public IP address. If the reverseFqdn is specified, then a PTR DNS record is created pointing from the IP address in the in-addr.arpa domain to the reverse FQDN." + "description": "Optional. Analytical TTL for the table." } - } - }, - "metadata": { - "__bicep_imported_from!": { - "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/res/network/public-ip-address:0.8.0" - } - } - }, - "_2.ipTagType": { - "type": "object", - "properties": { - "ipTagType": { - "type": "string", + }, + "throughput": { + "type": "int", + "nullable": true, "metadata": { - "description": "Required. The IP tag type." + "description": "Optional. Request units per second. Cannot be used with autoscaleSettingsMaxThroughput." } }, - "tag": { - "type": "string", + "autoscaleSettingsMaxThroughput": { + "type": "int", + "nullable": true, "metadata": { - "description": "Required. The IP tag." + "description": "Optional. Maximum autoscale throughput for the table. Cannot be used with throughput." } } }, "metadata": { + "description": "The type of a Cassandra table.", "__bicep_imported_from!": { - "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/res/network/public-ip-address:0.8.0" + "sourceTemplate": "cassandra-keyspace/main.bicep", + "originalIdentifier": "tableType" } } }, - "_3.publicIPConfigurationType": { + "cassandraViewType": { "type": "object", "properties": { "name": { "type": "string", - "nullable": true, "metadata": { - "description": "Optional. The name of the Public IP Address." + "description": "Required. Name of the view." } }, - "publicIPAddressResourceId": { + "viewDefinition": { "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The resource ID of the public IP address." - } - }, - "diagnosticSettings": { - "type": "array", - "items": { - "$ref": "#/definitions/diagnosticSettingFullType" - }, - "nullable": true, "metadata": { - "description": "Optional. Diagnostic settings for the public IP address." + "description": "Required. View definition (CQL statement)." } }, - "location": { - "type": "string", - "nullable": true, + "tags": { + "type": "object", "metadata": { - "description": "Optional. The idle timeout in minutes." - } + "__bicep_resource_derived_type!": { + "source": "Microsoft.DocumentDB/databaseAccounts/cassandraKeyspaces/views@2025-05-01-preview#properties/tags" + }, + "description": "Optional. Tags for the view." + }, + "nullable": true }, - "lock": { - "$ref": "#/definitions/lockType", + "throughput": { + "type": "int", "nullable": true, "metadata": { - "description": "Optional. The lock settings of the public IP address." + "description": "Optional. Request units per second. Cannot be used with autoscaleSettingsMaxThroughput." } }, - "idleTimeoutInMinutes": { + "autoscaleSettingsMaxThroughput": { "type": "int", "nullable": true, "metadata": { - "description": "Optional. The idle timeout of the public IP address." + "description": "Optional. Maximum autoscale throughput for the view. Cannot be used with throughput." } - }, - "ddosSettings": { - "$ref": "#/definitions/_2.ddosSettingsType", - "nullable": true, + } + }, + "metadata": { + "description": "The type of a Cassandra view (materialized view).", + "__bicep_imported_from!": { + "sourceTemplate": "cassandra-keyspace/main.bicep", + "originalIdentifier": "viewType" + } + } + }, + "collectionType": { + "type": "object", + "properties": { + "name": { + "type": "string", "metadata": { - "description": "Optional. The DDoS protection plan configuration associated with the public IP address." + "description": "Required. Name of the collection." } }, - "dnsSettings": { - "$ref": "#/definitions/_2.dnsSettingsType", + "throughput": { + "type": "int", "nullable": true, "metadata": { - "description": "Optional. The DNS settings of the public IP address." + "description": "Optional. Request Units per second. For best performance for large production workloads, it is recommended to set dedicated throughput (autoscale or manual) at the collection level and not at the database level." } }, - "publicIPAddressVersion": { - "type": "string", - "allowedValues": [ - "IPv4", - "IPv6" - ], - "nullable": true, + "indexes": { + "type": "array", "metadata": { - "description": "Optional. The public IP address version." + "__bicep_resource_derived_type!": { + "source": "Microsoft.DocumentDB/databaseAccounts/mongodbDatabases/collections@2025-04-15#properties/properties/properties/resource/properties/indexes" + }, + "description": "Required. Indexes for the collection." } }, - "publicIPAllocationMethod": { - "type": "string", - "allowedValues": [ - "Dynamic", - "Static" - ], - "nullable": true, + "shardKey": { + "type": "object", "metadata": { - "description": "Optional. The public IP address allocation method." + "__bicep_resource_derived_type!": { + "source": "Microsoft.DocumentDB/databaseAccounts/mongodbDatabases/collections@2025-04-15#properties/properties/properties/resource/properties/shardKey" + }, + "description": "Required. ShardKey for the collection." } - }, - "publicIpPrefixResourceId": { + } + }, + "metadata": { + "description": "The type of a collection.", + "__bicep_imported_from!": { + "sourceTemplate": "mongodb-database/main.bicep" + } + } + }, + "containerType": { + "type": "object", + "properties": { + "name": { "type": "string", - "nullable": true, "metadata": { - "description": "Optional. Resource ID of the Public IP Prefix object. This is only needed if you want your Public IPs created in a PIP Prefix." + "description": "Required. Name of the container." } }, - "publicIpNameSuffix": { - "type": "string", + "analyticalStorageTtl": { + "type": "int", "nullable": true, "metadata": { - "description": "Optional. The name suffix of the public IP address resource." + "description": "Optional. Default to 0. Indicates how long data should be retained in the analytical store, for a container. Analytical store is enabled when ATTL is set with a value other than 0. If the value is set to -1, the analytical store retains all historical data, irrespective of the retention of the data in the transactional store." } }, - "roleAssignments": { - "type": "array", - "items": { - "$ref": "#/definitions/roleAssignmentType" + "conflictResolutionPolicy": { + "type": "object", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers@2025-04-15#properties/properties/properties/resource/properties/conflictResolutionPolicy" + }, + "description": "Optional. The conflict resolution policy for the container. Conflicts and conflict resolution policies are applicable if the Azure Cosmos DB account is configured with multiple write regions." }, + "nullable": true + }, + "defaultTtl": { + "type": "int", "nullable": true, + "minValue": -1, + "maxValue": 2147483647, "metadata": { - "description": "Optional. Array of role assignments to create." + "description": "Optional. Default to -1. Default time to live (in seconds). With Time to Live or TTL, Azure Cosmos DB provides the ability to delete items automatically from a container after a certain time period. If the value is set to \"-1\", it is equal to infinity, and items don't expire by default." } }, - "skuName": { - "type": "string", - "allowedValues": [ - "Basic", - "Standard" - ], + "throughput": { + "type": "int", "nullable": true, "metadata": { - "description": "Optional. The SKU name of the public IP address." + "description": "Optional. Default to 400. Request Units per second. Will be ignored if autoscaleSettingsMaxThroughput is used. For best performance for large production workloads, it is recommended to set dedicated throughput (autoscale or manual) at the container level and not at the database level." } }, - "skuTier": { - "type": "string", - "allowedValues": [ - "Global", - "Regional" - ], + "autoscaleSettingsMaxThroughput": { + "type": "int", "nullable": true, + "maxValue": 1000000, "metadata": { - "description": "Optional. The SKU tier of the public IP address." + "description": "Optional. Specifies the Autoscale settings and represents maximum throughput, the resource can scale up to. The autoscale throughput should have valid throughput values between 1000 and 1000000 inclusive in increments of 1000. If value is set to null, then autoscale will be disabled. For best performance for large production workloads, it is recommended to set dedicated throughput (autoscale or manual) at the container level and not at the database level." } }, "tags": { "type": "object", "metadata": { "__bicep_resource_derived_type!": { - "source": "Microsoft.Network/publicIPAddresses@2024-07-01#properties/tags" + "source": "Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers@2025-04-15#properties/tags" }, - "description": "Optional. The tags of the public IP address." + "description": "Optional. Tags of the SQL Database resource." }, "nullable": true }, - "availabilityZones": { + "paths": { "type": "array", - "allowedValues": [ - 1, - 2, - 3 - ], - "nullable": true, + "items": { + "type": "string" + }, + "minLength": 1, + "maxLength": 3, "metadata": { - "description": "Optional. The zones of the public IP address." + "description": "Required. List of paths using which data within the container can be partitioned. For kind=MultiHash it can be up to 3. For anything else it needs to be exactly 1." } }, - "ipTags": { + "indexingPolicy": { + "type": "object", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers@2025-04-15#properties/properties/properties/resource/properties/indexingPolicy" + }, + "description": "Optional. Indexing policy of the container." + }, + "nullable": true + }, + "uniqueKeyPolicyKeys": { "type": "array", - "items": { - "$ref": "#/definitions/_2.ipTagType" + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers@2025-04-15#properties/properties/properties/resource/properties/uniqueKeyPolicy/properties/uniqueKeys" + }, + "description": "Optional. The unique key policy configuration containing a list of unique keys that enforces uniqueness constraint on documents in the collection in the Azure Cosmos DB service." }, + "nullable": true + }, + "kind": { + "type": "string", + "allowedValues": [ + "Hash", + "MultiHash" + ], "nullable": true, "metadata": { - "description": "Optional. The list of tags associated with the public IP address." + "description": "Optional. Default to Hash. Indicates the kind of algorithm used for partitioning." } }, - "enableTelemetry": { - "type": "bool", + "version": { + "type": "int", + "allowedValues": [ + 1, + 2 + ], "nullable": true, "metadata": { - "description": "Optional. Enable/Disable usage telemetry for the module." + "description": "Optional. Default to 1 for Hash and 2 for MultiHash - 1 is not allowed for MultiHash. Version of the partition key definition." } } }, "metadata": { - "description": "The type for the public IP address configuration.", + "description": "The type of a container.", "__bicep_imported_from!": { - "sourceTemplate": "modules/nic-configuration.bicep" + "sourceTemplate": "sql-database/main.bicep" } } }, @@ -35207,151 +36835,54 @@ "metadata": { "description": "An AVM-aligned type for a diagnostic setting. To be used if both logs & metrics are supported by the resource provider.", "__bicep_imported_from!": { - "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.5.1" + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.1" } } }, - "ipConfigurationType": { + "graphType": { "type": "object", "properties": { "name": { "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The name of the IP configuration." - } - }, - "privateIPAllocationMethod": { - "type": "string", - "allowedValues": [ - "Dynamic", - "Static" - ], - "nullable": true, - "metadata": { - "description": "Optional. The private IP address allocation method." - } - }, - "privateIPAddress": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The private IP address." - } - }, - "subnetResourceId": { - "type": "string", - "metadata": { - "description": "Required. The resource ID of the subnet." - } - }, - "loadBalancerBackendAddressPools": { - "type": "array", - "items": { - "$ref": "#/definitions/_1.backendAddressPoolType" - }, - "nullable": true, - "metadata": { - "description": "Optional. The load balancer backend address pools." - } - }, - "applicationSecurityGroups": { - "type": "array", - "items": { - "$ref": "#/definitions/_1.applicationSecurityGroupType" - }, - "nullable": true, - "metadata": { - "description": "Optional. The application security groups." - } - }, - "applicationGatewayBackendAddressPools": { - "type": "array", - "items": { - "$ref": "#/definitions/_1.applicationGatewayBackendAddressPoolsType" - }, - "nullable": true, "metadata": { - "description": "Optional. The application gateway backend address pools." + "description": "Required. Name of the graph." } }, - "gatewayLoadBalancer": { - "$ref": "#/definitions/subResourceType", - "nullable": true, + "tags": { + "type": "object", "metadata": { - "description": "Optional. The gateway load balancer settings." - } - }, - "loadBalancerInboundNatRules": { - "type": "array", - "items": { - "$ref": "#/definitions/_1.inboundNatRuleType" + "__bicep_resource_derived_type!": { + "source": "Microsoft.DocumentDB/databaseAccounts/gremlinDatabases/graphs@2025-04-15#properties/tags" + }, + "description": "Optional. Tags of the Gremlin graph resource." }, - "nullable": true, - "metadata": { - "description": "Optional. The load balancer inbound NAT rules." - } + "nullable": true }, - "privateIPAddressVersion": { - "type": "string", - "allowedValues": [ - "IPv4", - "IPv6" - ], - "nullable": true, + "indexingPolicy": { + "type": "object", "metadata": { - "description": "Optional. The private IP address version." - } - }, - "virtualNetworkTaps": { - "type": "array", - "items": { - "$ref": "#/definitions/_1.virtualNetworkTapType" + "__bicep_resource_derived_type!": { + "source": "Microsoft.DocumentDB/databaseAccounts/gremlinDatabases/graphs@2025-04-15#properties/properties/properties/resource/properties/indexingPolicy" + }, + "description": "Optional. Indexing policy of the graph." }, - "nullable": true, - "metadata": { - "description": "Optional. The virtual network taps." - } - }, - "pipConfiguration": { - "$ref": "#/definitions/_3.publicIPConfigurationType", - "nullable": true, - "metadata": { - "description": "Optional. The public IP address configuration." - } + "nullable": true }, - "diagnosticSettings": { + "partitionKeyPaths": { "type": "array", - "items": { - "$ref": "#/definitions/diagnosticSettingFullType" - }, - "nullable": true, - "metadata": { - "description": "Optional. The diagnostic settings of the IP configuration." - } - }, - "tags": { - "type": "object", "metadata": { "__bicep_resource_derived_type!": { - "source": "Microsoft.Network/networkInterfaces@2024-07-01#properties/tags" + "source": "Microsoft.DocumentDB/databaseAccounts/gremlinDatabases/graphs@2025-04-15#properties/properties/properties/resource/properties/partitionKey/properties/paths" }, - "description": "Optional. The tags of the public IP address." + "description": "Optional. List of paths using which data within the container can be partitioned." }, "nullable": true - }, - "enableTelemetry": { - "type": "bool", - "nullable": true, - "metadata": { - "description": "Optional. Enable/Disable usage telemetry for the module." - } } }, "metadata": { - "description": "The type for the IP configuration.", + "description": "The type of a graph.", "__bicep_imported_from!": { - "sourceTemplate": "modules/nic-configuration.bicep" + "sourceTemplate": "gremlin-database/main.bicep" } } }, @@ -35388,7 +36919,7 @@ "metadata": { "description": "An AVM-aligned type for a lock.", "__bicep_imported_from!": { - "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.0" + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.1" } } }, @@ -35416,578 +36947,472 @@ "metadata": { "description": "An AVM-aligned type for a managed identity configuration. To be used if both a system-assigned & user-assigned identities are supported by the resource provider.", "__bicep_imported_from!": { - "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.5.1" + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.1" } } }, - "networkInterfaceIPConfigurationOutputType": { + "nestedSqlRoleAssignmentType": { "type": "object", "properties": { "name": { "type": "string", + "nullable": true, "metadata": { - "description": "The name of the IP configuration." + "description": "Optional. Name unique identifier of the SQL Role Assignment." } }, - "privateIP": { + "principalId": { "type": "string", - "nullable": true, "metadata": { - "description": "The private IP address." + "description": "Required. The unique identifier for the associated AAD principal in the AAD graph to which access is being granted through this Role Assignment. Tenant ID for the principal is inferred using the tenant associated with the subscription." } }, - "publicIP": { + "scope": { "type": "string", "nullable": true, "metadata": { - "description": "The public IP address." + "description": "Optional. The data plane resource id for which access is being granted through this Role Assignment. Defaults to the root of the database account, but can also be scoped to e.g., the container and database level." } } }, "metadata": { + "description": "The type for the SQL Role Assignments.", "__bicep_imported_from!": { - "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/res/network/network-interface:0.5.1" + "sourceTemplate": "sql-role-definition/main.bicep", + "originalIdentifier": "sqlRoleAssignmentType" } } }, - "roleAssignmentType": { + "privateEndpointMultiServiceType": { "type": "object", "properties": { "name": { "type": "string", "nullable": true, "metadata": { - "description": "Optional. The name (as GUID) of the role assignment. If not provided, a GUID will be generated." + "description": "Optional. The name of the private endpoint." } }, - "roleDefinitionIdOrName": { + "location": { "type": "string", + "nullable": true, "metadata": { - "description": "Required. The role to assign. You can provide either the display name of the role definition, the role definition GUID, or its fully qualified ID in the following format: '/providers/Microsoft.Authorization/roleDefinitions/c2f4ef07-c644-48eb-af81-4b1b4947fb11'." + "description": "Optional. The location to deploy the private endpoint to." } }, - "principalId": { + "privateLinkServiceConnectionName": { "type": "string", + "nullable": true, "metadata": { - "description": "Required. The principal ID of the principal (user/group/identity) to assign the role to." + "description": "Optional. The name of the private link connection to create." } }, - "principalType": { + "service": { "type": "string", - "allowedValues": [ - "Device", - "ForeignGroup", - "Group", - "ServicePrincipal", - "User" - ], - "nullable": true, "metadata": { - "description": "Optional. The principal type of the assigned principal ID." + "description": "Required. The subresource to deploy the private endpoint for. For example \"blob\", \"table\", \"queue\" or \"file\" for a Storage Account's Private Endpoints." } }, - "description": { + "subnetResourceId": { "type": "string", - "nullable": true, "metadata": { - "description": "Optional. The description of the role assignment." + "description": "Required. Resource ID of the subnet where the endpoint needs to be created." } }, - "condition": { + "resourceGroupResourceId": { "type": "string", "nullable": true, "metadata": { - "description": "Optional. The conditions on the role assignment. This limits the resources it can be assigned to. e.g.: @Resource[Microsoft.Storage/storageAccounts/blobServices/containers:ContainerName] StringEqualsIgnoreCase \"foo_storage_container\"." + "description": "Optional. The resource ID of the Resource Group the Private Endpoint will be created in. If not specified, the Resource Group of the provided Virtual Network Subnet is used." } }, - "conditionVersion": { - "type": "string", - "allowedValues": [ - "2.0" - ], + "privateDnsZoneGroup": { + "$ref": "#/definitions/_1.privateEndpointPrivateDnsZoneGroupType", "nullable": true, "metadata": { - "description": "Optional. Version of the condition." + "description": "Optional. The private DNS zone group to configure for the private endpoint." } }, - "delegatedManagedIdentityResourceId": { + "isManualConnection": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. If Manual Private Link Connection is required." + } + }, + "manualConnectionRequestMessage": { "type": "string", "nullable": true, + "maxLength": 140, "metadata": { - "description": "Optional. The Resource Id of the delegated managed identity resource." + "description": "Optional. A message passed to the owner of the remote resource with the manual connection request." } - } - }, - "metadata": { - "description": "An AVM-aligned type for a role assignment.", - "__bicep_imported_from!": { - "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.5.1" - } - } - }, - "subResourceType": { - "type": "object", - "properties": { - "id": { + }, + "customDnsConfigs": { + "type": "array", + "items": { + "$ref": "#/definitions/_1.privateEndpointCustomDnsConfigType" + }, + "nullable": true, + "metadata": { + "description": "Optional. Custom DNS configurations." + } + }, + "ipConfigurations": { + "type": "array", + "items": { + "$ref": "#/definitions/_1.privateEndpointIpConfigurationType" + }, + "nullable": true, + "metadata": { + "description": "Optional. A list of IP configurations of the private endpoint. This will be used to map to the First Party Service endpoints." + } + }, + "applicationSecurityGroupResourceIds": { + "type": "array", + "items": { + "type": "string" + }, + "nullable": true, + "metadata": { + "description": "Optional. Application security groups in which the private endpoint IP configuration is included." + } + }, + "customNetworkInterfaceName": { "type": "string", "nullable": true, "metadata": { - "description": "Optional. Resource ID of the sub resource." + "description": "Optional. The custom name of the network interface attached to the private endpoint." } - } - }, - "metadata": { - "description": "The type for the sub resource.", - "__bicep_imported_from!": { - "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/res/network/network-interface:0.5.1" - } - } - } - }, - "parameters": { - "name": { - "type": "string", - "metadata": { - "description": "Required. The name of the virtual machine to be created. You should use a unique prefix to reduce name collisions in Active Directory." - } - }, - "computerName": { - "type": "string", - "defaultValue": "[parameters('name')]", - "metadata": { - "description": "Optional. Can be used if the computer name needs to be different from the Azure VM resource name. If not used, the resource name will be used as computer name." - } - }, - "vmSize": { - "type": "string", - "metadata": { - "description": "Required. Specifies the size for the VMs." - } - }, - "encryptionAtHost": { - "type": "bool", - "defaultValue": false, - "metadata": { - "description": "Optional. This property can be used by user in the request to enable or disable the Host Encryption for the virtual machine. This will enable the encryption for all the disks including Resource/Temp disk at host itself. For security reasons, it is recommended to set encryptionAtHost to True. Restrictions: Cannot be enabled if Azure Disk Encryption (guest-VM encryption using bitlocker/DM-Crypt) is enabled on your VMs." - } - }, - "securityType": { - "type": "string", - "defaultValue": "", - "allowedValues": [ - "", - "ConfidentialVM", - "TrustedLaunch" - ], - "metadata": { - "description": "Optional. Specifies the SecurityType of the virtual machine. It has to be set to any specified value to enable UefiSettings. The default behavior is: UefiSettings will not be enabled unless this property is set." - } - }, - "secureBootEnabled": { - "type": "bool", - "defaultValue": false, - "metadata": { - "description": "Optional. Specifies whether secure boot should be enabled on the virtual machine. This parameter is part of the UefiSettings. SecurityType should be set to TrustedLaunch to enable UefiSettings." - } - }, - "vTpmEnabled": { - "type": "bool", - "defaultValue": false, - "metadata": { - "description": "Optional. Specifies whether vTPM should be enabled on the virtual machine. This parameter is part of the UefiSettings. SecurityType should be set to TrustedLaunch to enable UefiSettings." - } - }, - "imageReference": { - "$ref": "#/definitions/imageReferenceType", - "metadata": { - "description": "Required. OS image reference. In case of marketplace images, it's the combination of the publisher, offer, sku, version attributes. In case of custom images it's the resource ID of the custom image." - } - }, - "plan": { - "$ref": "#/definitions/planType", - "nullable": true, - "metadata": { - "description": "Optional. Specifies information about the marketplace image used to create the virtual machine. This element is only used for marketplace images. Before you can use a marketplace image from an API, you must enable the image for programmatic use." - } - }, - "osDisk": { - "$ref": "#/definitions/osDiskType", - "metadata": { - "description": "Required. Specifies the OS disk. For security reasons, it is recommended to specify DiskEncryptionSet into the osDisk object. Restrictions: DiskEncryptionSet cannot be enabled if Azure Disk Encryption (guest-VM encryption using bitlocker/DM-Crypt) is enabled on your VMs." - } - }, - "dataDisks": { - "type": "array", - "items": { - "$ref": "#/definitions/dataDiskType" - }, - "nullable": true, - "metadata": { - "description": "Optional. Specifies the data disks. For security reasons, it is recommended to specify DiskEncryptionSet into the dataDisk object. Restrictions: DiskEncryptionSet cannot be enabled if Azure Disk Encryption (guest-VM encryption using bitlocker/DM-Crypt) is enabled on your VMs." - } - }, - "ultraSSDEnabled": { - "type": "bool", - "defaultValue": false, - "metadata": { - "description": "Optional. The flag that enables or disables a capability to have one or more managed data disks with UltraSSD_LRS storage account type on the VM or VMSS. Managed disks with storage account type UltraSSD_LRS can be added to a virtual machine or virtual machine scale set only if this property is enabled." - } - }, - "hibernationEnabled": { - "type": "bool", - "defaultValue": false, - "metadata": { - "description": "Optional. The flag that enables or disables hibernation capability on the VM." - } - }, - "adminUsername": { - "type": "securestring", - "metadata": { - "description": "Required. Administrator username." - } - }, - "adminPassword": { - "type": "securestring", - "defaultValue": "", - "metadata": { - "description": "Optional. When specifying a Windows Virtual Machine, this value should be passed." - } - }, - "userData": { - "type": "string", - "defaultValue": "", - "metadata": { - "description": "Optional. UserData for the VM, which must be base-64 encoded. Customer should not pass any secrets in here." - } - }, - "customData": { - "type": "string", - "defaultValue": "", - "metadata": { - "description": "Optional. Custom data associated to the VM, this value will be automatically converted into base64 to account for the expected VM format." - } - }, - "certificatesToBeInstalled": { - "type": "array", - "items": { - "$ref": "#/definitions/vaultSecretGroupType" + }, + "lock": { + "$ref": "#/definitions/lockType", + "nullable": true, + "metadata": { + "description": "Optional. Specify the type of lock." + } + }, + "roleAssignments": { + "type": "array", + "items": { + "$ref": "#/definitions/roleAssignmentType" + }, + "nullable": true, + "metadata": { + "description": "Optional. Array of role assignments to create." + } + }, + "tags": { + "type": "object", + "nullable": true, + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.Network/privateEndpoints@2024-07-01#properties/tags" + }, + "description": "Optional. Tags to be applied on all resources/resource groups in this deployment." + } + }, + "enableTelemetry": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. Enable/Disable usage telemetry for module." + } + } }, - "nullable": true, - "metadata": { - "description": "Optional. Specifies set of certificates that should be installed onto the virtual machine." - } - }, - "priority": { - "type": "string", - "nullable": true, - "allowedValues": [ - "Regular", - "Low", - "Spot" - ], "metadata": { - "description": "Optional. Specifies the priority for the virtual machine." + "description": "An AVM-aligned type for a private endpoint. To be used if the private endpoint's default service / groupId can NOT be assumed (i.e., for services that have more than one subresource, like Storage Account with Blob (blob, table, queue, file, ...).", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.1" + } } }, - "evictionPolicy": { - "type": "string", - "defaultValue": "Deallocate", - "allowedValues": [ - "Deallocate", - "Delete" - ], + "roleAssignmentType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The name (as GUID) of the role assignment. If not provided, a GUID will be generated." + } + }, + "roleDefinitionIdOrName": { + "type": "string", + "metadata": { + "description": "Required. The role to assign. You can provide either the display name of the role definition, the role definition GUID, or its fully qualified ID in the following format: '/providers/Microsoft.Authorization/roleDefinitions/c2f4ef07-c644-48eb-af81-4b1b4947fb11'." + } + }, + "principalId": { + "type": "string", + "metadata": { + "description": "Required. The principal ID of the principal (user/group/identity) to assign the role to." + } + }, + "principalType": { + "type": "string", + "allowedValues": [ + "Device", + "ForeignGroup", + "Group", + "ServicePrincipal", + "User" + ], + "nullable": true, + "metadata": { + "description": "Optional. The principal type of the assigned principal ID." + } + }, + "description": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The description of the role assignment." + } + }, + "condition": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The conditions on the role assignment. This limits the resources it can be assigned to. e.g.: @Resource[Microsoft.Storage/storageAccounts/blobServices/containers:ContainerName] StringEqualsIgnoreCase \"foo_storage_container\"." + } + }, + "conditionVersion": { + "type": "string", + "allowedValues": [ + "2.0" + ], + "nullable": true, + "metadata": { + "description": "Optional. Version of the condition." + } + }, + "delegatedManagedIdentityResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The Resource Id of the delegated managed identity resource." + } + } + }, "metadata": { - "description": "Optional. Specifies the eviction policy for the low priority virtual machine." + "description": "An AVM-aligned type for a role assignment.", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.1" + } } - }, - "maxPriceForLowPriorityVm": { + } + }, + "parameters": { + "name": { "type": "string", - "defaultValue": "", "metadata": { - "description": "Optional. Specifies the maximum price you are willing to pay for a low priority VM/VMSS. This price is in US Dollars." + "description": "Required. The name of the account." } }, - "dedicatedHostResourceId": { + "location": { "type": "string", - "defaultValue": "", + "defaultValue": "[resourceGroup().location]", "metadata": { - "description": "Optional. Specifies resource ID about the dedicated host that the virtual machine resides in." + "description": "Optional. Defaults to the current resource group scope location. Location for all resources." } }, - "licenseType": { - "type": "string", - "nullable": true, - "allowedValues": [ - "RHEL_BYOS", - "SLES_BYOS", - "Windows_Client", - "Windows_Server" - ], + "tags": { + "type": "object", "metadata": { - "description": "Optional. Specifies that the image or disk that is being used was licensed on-premises." - } - }, - "publicKeys": { - "type": "array", - "items": { - "$ref": "#/definitions/publicKeyType" + "__bicep_resource_derived_type!": { + "source": "Microsoft.DocumentDB/databaseAccounts@2024-11-15#properties/tags" + }, + "description": "Optional. Tags for the resource." }, - "defaultValue": [], - "metadata": { - "description": "Optional. The list of SSH public keys used to authenticate with linux based VMs." - } + "nullable": true }, "managedIdentities": { "$ref": "#/definitions/managedIdentityAllType", "nullable": true, "metadata": { - "description": "Optional. The managed identity definition for this resource. The system-assigned managed identity will automatically be enabled if extensionAadJoinConfig.enabled = \"True\"." - } - }, - "bootDiagnostics": { - "type": "bool", - "defaultValue": false, - "metadata": { - "description": "Optional. Whether boot diagnostics should be enabled on the Virtual Machine. Boot diagnostics will be enabled with a managed storage account if no bootDiagnosticsStorageAccountName value is provided. If bootDiagnostics and bootDiagnosticsStorageAccountName values are not provided, boot diagnostics will be disabled." - } - }, - "bootDiagnosticStorageAccountName": { - "type": "string", - "defaultValue": "", - "metadata": { - "description": "Optional. Custom storage account used to store boot diagnostic information. Boot diagnostics will be enabled with a custom storage account if a value is provided." - } - }, - "bootDiagnosticStorageAccountUri": { - "type": "string", - "defaultValue": "[format('.blob.{0}/', environment().suffixes.storage)]", - "metadata": { - "description": "Optional. Storage account boot diagnostic base URI." - } - }, - "proximityPlacementGroupResourceId": { - "type": "string", - "defaultValue": "", - "metadata": { - "description": "Optional. Resource ID of a proximity placement group." - } - }, - "virtualMachineScaleSetResourceId": { - "type": "string", - "defaultValue": "", - "metadata": { - "description": "Optional. Resource ID of a virtual machine scale set, where the VM should be added." + "description": "Optional. The managed identity definition for this resource." } }, - "availabilitySetResourceId": { + "databaseAccountOfferType": { "type": "string", - "defaultValue": "", - "metadata": { - "description": "Optional. Resource ID of an availability set. Cannot be used in combination with availability zone nor scale set." - } - }, - "galleryApplications": { - "type": "array", - "items": { - "$ref": "#/definitions/vmGalleryApplicationType" - }, - "nullable": true, - "metadata": { - "description": "Optional. Specifies the gallery applications that should be made available to the VM/VMSS." - } - }, - "availabilityZone": { - "type": "int", + "defaultValue": "Standard", "allowedValues": [ - -1, - 1, - 2, - 3 + "Standard" ], "metadata": { - "description": "Required. If set to 1, 2 or 3, the availability zone is hardcoded to that value. If set to -1, no zone is defined. Note that the availability zone numbers here are the logical availability zone in your Azure subscription. Different subscriptions might have a different mapping of the physical zone and logical zone. To understand more, please refer to [Physical and logical availability zones](https://learn.microsoft.com/en-us/azure/reliability/availability-zones-overview?tabs=azure-cli#physical-and-logical-availability-zones)." + "description": "Optional. The offer type for the account. Defaults to \"Standard\"." } }, - "nicConfigurations": { + "failoverLocations": { "type": "array", "items": { - "$ref": "#/definitions/nicConfigurationType" + "$ref": "#/definitions/failoverLocationType" }, + "nullable": true, "metadata": { - "description": "Required. Configures NICs and PIPs." - } - }, - "backupVaultName": { - "type": "string", - "defaultValue": "", - "metadata": { - "description": "Optional. Recovery service vault name to add VMs to backup." + "description": "Optional. The set of locations enabled for the account. Defaults to the location where the account is deployed." } }, - "backupVaultResourceGroup": { - "type": "string", - "defaultValue": "[resourceGroup().name]", + "zoneRedundant": { + "type": "bool", + "defaultValue": true, "metadata": { - "description": "Optional. Resource group of the backup recovery service vault. If not provided the current resource group name is considered by default." + "description": "Optional. Indicates whether the single-region account is zone redundant. Defaults to true. This property is ignored for multi-region accounts." } }, - "backupPolicyName": { + "defaultConsistencyLevel": { "type": "string", - "defaultValue": "DefaultPolicy", + "defaultValue": "Session", + "allowedValues": [ + "Eventual", + "ConsistentPrefix", + "Session", + "BoundedStaleness", + "Strong" + ], "metadata": { - "description": "Optional. Backup policy the VMs should be using for backup. If not provided, it will use the DefaultPolicy from the backup recovery service vault." + "description": "Optional. The default consistency level of the account. Defaults to \"Session\"." } }, - "autoShutdownConfig": { - "$ref": "#/definitions/autoShutDownConfigType", - "defaultValue": {}, + "disableLocalAuthentication": { + "type": "bool", + "defaultValue": true, "metadata": { - "description": "Optional. The configuration for auto-shutdown." + "description": "Optional. Opt-out of local authentication and ensure that only Microsoft Entra can be used exclusively for authentication. Defaults to true." } }, - "maintenanceConfigurationResourceId": { - "type": "string", - "defaultValue": "", + "enableAnalyticalStorage": { + "type": "bool", + "defaultValue": false, "metadata": { - "description": "Optional. The resource Id of a maintenance configuration for this VM." + "description": "Optional. Flag to indicate whether to enable storage analytics. Defaults to false." } }, - "allowExtensionOperations": { + "enableAutomaticFailover": { "type": "bool", "defaultValue": true, "metadata": { - "description": "Optional. Specifies whether extension operations should be allowed on the virtual machine. This may only be set to False when no extensions are present on the virtual machine." - } - }, - "extensionDomainJoinPassword": { - "type": "securestring", - "defaultValue": "", - "metadata": { - "description": "Optional. Required if name is specified. Password of the user specified in user parameter." + "description": "Optional. Enable automatic failover for regions. Defaults to true." } }, - "extensionDomainJoinConfig": { - "type": "secureObject", - "defaultValue": {}, + "enableFreeTier": { + "type": "bool", + "defaultValue": false, "metadata": { - "description": "Optional. The configuration for the [Domain Join] extension. Must at least contain the [\"enabled\": true] property to be executed." + "description": "Optional. Flag to indicate whether \"Free Tier\" is enabled. Defaults to false." } }, - "extensionAadJoinConfig": { - "type": "object", - "defaultValue": { - "enabled": false - }, + "enableMultipleWriteLocations": { + "type": "bool", + "defaultValue": false, "metadata": { - "description": "Optional. The configuration for the [AAD Join] extension. Must at least contain the [\"enabled\": true] property to be executed. To enroll in Intune, add the setting mdmId: \"0000000a-0000-0000-c000-000000000000\"." + "description": "Optional. Enables the account to write in multiple locations. Periodic backup must be used if enabled. Defaults to false." } }, - "extensionAntiMalwareConfig": { - "type": "object", - "defaultValue": "[if(equals(parameters('osType'), 'Windows'), createObject('enabled', true()), createObject('enabled', false()))]", + "disableKeyBasedMetadataWriteAccess": { + "type": "bool", + "defaultValue": true, "metadata": { - "description": "Optional. The configuration for the [Anti Malware] extension. Must at least contain the [\"enabled\": true] property to be executed." + "description": "Optional. Disable write operations on metadata resources (databases, containers, throughput) via account keys. Defaults to true." } }, - "extensionMonitoringAgentConfig": { - "type": "object", - "defaultValue": { - "enabled": false, - "dataCollectionRuleAssociations": [] - }, + "maxStalenessPrefix": { + "type": "int", + "defaultValue": 100000, + "minValue": 1, + "maxValue": 2147483647, "metadata": { - "description": "Optional. The configuration for the [Monitoring Agent] extension. Must at least contain the [\"enabled\": true] property to be executed." + "description": "Optional. The maximum stale requests. Required for \"BoundedStaleness\" consistency level. Valid ranges, Single Region: 10 to 1000000. Multi Region: 100000 to 1000000. Defaults to 100000." } }, - "extensionDependencyAgentConfig": { - "type": "object", - "defaultValue": { - "enabled": false - }, + "maxIntervalInSeconds": { + "type": "int", + "defaultValue": 300, + "minValue": 5, + "maxValue": 86400, "metadata": { - "description": "Optional. The configuration for the [Dependency Agent] extension. Must at least contain the [\"enabled\": true] property to be executed." + "description": "Optional. The maximum lag time in minutes. Required for \"BoundedStaleness\" consistency level. Valid ranges, Single Region: 5 to 84600. Multi Region: 300 to 86400. Defaults to 300." } }, - "extensionNetworkWatcherAgentConfig": { - "type": "object", - "defaultValue": { - "enabled": false - }, + "serverVersion": { + "type": "string", + "defaultValue": "4.2", + "allowedValues": [ + "3.2", + "3.6", + "4.0", + "4.2", + "5.0", + "6.0", + "7.0" + ], "metadata": { - "description": "Optional. The configuration for the [Network Watcher Agent] extension. Must at least contain the [\"enabled\": true] property to be executed." + "description": "Optional. Specifies the MongoDB server version to use if using Azure Cosmos DB for MongoDB RU. Defaults to \"4.2\"." } }, - "extensionAzureDiskEncryptionConfig": { - "type": "object", - "defaultValue": { - "enabled": false + "sqlDatabases": { + "type": "array", + "items": { + "$ref": "#/definitions/sqlDatabaseType" }, + "nullable": true, "metadata": { - "description": "Optional. The configuration for the [Azure Disk Encryption] extension. Must at least contain the [\"enabled\": true] property to be executed. Restrictions: Cannot be enabled on disks that have encryption at host enabled. Managed disks encrypted using Azure Disk Encryption cannot be encrypted using customer-managed keys." + "description": "Optional. Configuration for databases when using Azure Cosmos DB for NoSQL." } }, - "extensionDSCConfig": { - "type": "object", - "defaultValue": { - "enabled": false + "mongodbDatabases": { + "type": "array", + "items": { + "$ref": "#/definitions/mongoDbType" }, - "metadata": { - "description": "Optional. The configuration for the [Desired State Configuration] extension. Must at least contain the [\"enabled\": true] property to be executed." - } - }, - "extensionCustomScriptConfig": { - "$ref": "#/definitions/extensionCustomScriptConfigType", "nullable": true, "metadata": { - "description": "Optional. The configuration for the [Custom Script] extension." + "description": "Optional. Configuration for databases when using Azure Cosmos DB for MongoDB RU." } }, - "extensionNvidiaGpuDriverWindows": { - "type": "object", - "defaultValue": { - "enabled": false + "gremlinDatabases": { + "type": "array", + "items": { + "$ref": "#/definitions/gremlinDatabaseType" }, + "nullable": true, "metadata": { - "description": "Optional. The configuration for the [Nvidia Gpu Driver Windows] extension. Must at least contain the [\"enabled\": true] property to be executed." + "description": "Optional. Configuration for databases when using Azure Cosmos DB for Apache Gremlin." } }, - "extensionHostPoolRegistration": { - "type": "secureObject", - "defaultValue": { - "enabled": false + "tables": { + "type": "array", + "items": { + "$ref": "#/definitions/tableType" }, + "nullable": true, "metadata": { - "description": "Optional. The configuration for the [Host Pool Registration] extension. Must at least contain the [\"enabled\": true] property to be executed. Needs a managed identity." + "description": "Optional. Configuration for databases when using Azure Cosmos DB for Table." } }, - "extensionGuestConfigurationExtension": { - "type": "object", - "defaultValue": { - "enabled": false + "cassandraKeyspaces": { + "type": "array", + "items": { + "$ref": "#/definitions/cassandraKeyspaceType" }, + "nullable": true, "metadata": { - "description": "Optional. The configuration for the [Guest Configuration] extension. Must at least contain the [\"enabled\": true] property to be executed. Needs a managed identity." - } - }, - "guestConfiguration": { - "type": "object", - "defaultValue": {}, - "metadata": { - "description": "Optional. The guest configuration for the virtual machine. Needs the Guest Configuration extension to be enabled." + "description": "Optional. Configuration for keyspaces when using Azure Cosmos DB for Apache Cassandra." } }, - "extensionGuestConfigurationExtensionProtectedSettings": { - "type": "secureObject", - "defaultValue": {}, + "enableTelemetry": { + "type": "bool", + "defaultValue": true, "metadata": { - "description": "Optional. An object that contains the extension specific protected settings." + "description": "Optional. Enable/Disable usage telemetry for module." } }, - "location": { - "type": "string", - "defaultValue": "[resourceGroup().location]", + "totalThroughputLimit": { + "type": "int", + "defaultValue": -1, "metadata": { - "description": "Optional. Location for all resources." + "description": "Optional. The total throughput limit imposed on this account in request units per second (RU/s). Default to unlimited throughput." } }, "lock": { @@ -36004,246 +37429,252 @@ }, "nullable": true, "metadata": { - "description": "Optional. Array of role assignments to create." + "description": "Optional. An array of control plane Azure role-based access control assignments." } }, - "tags": { - "type": "object", - "metadata": { - "__bicep_resource_derived_type!": { - "source": "Microsoft.Compute/virtualMachines@2024-11-01#properties/tags" - }, - "description": "Optional. Tags of the resource." + "sqlRoleDefinitions": { + "type": "array", + "items": { + "$ref": "#/definitions/sqlRoleDefinitionType" }, - "nullable": true - }, - "enableTelemetry": { - "type": "bool", - "defaultValue": true, + "nullable": true, "metadata": { - "description": "Optional. Enable/Disable usage telemetry for module." + "description": "Optional. Configurations for Azure Cosmos DB for NoSQL native role-based access control definitions. Allows the creations of custom role definitions." } }, - "osType": { - "type": "string", - "allowedValues": [ - "Windows", - "Linux" - ], + "sqlRoleAssignments": { + "type": "array", + "items": { + "$ref": "#/definitions/sqlRoleAssignmentType" + }, + "nullable": true, "metadata": { - "description": "Required. The chosen OS type." + "description": "Optional. Configurations for Azure Cosmos DB for NoSQL native role-based access control assignments." } }, - "disablePasswordAuthentication": { - "type": "bool", - "defaultValue": false, + "cassandraRoleDefinitions": { + "type": "array", + "items": { + "$ref": "#/definitions/cassandraRoleDefinitionType" + }, + "nullable": true, "metadata": { - "description": "Optional. Specifies whether password authentication should be disabled." + "description": "Optional. Configurations for Azure Cosmos DB for Apache Cassandra native role-based access control definitions. Allows the creations of custom role definitions." } }, - "provisionVMAgent": { - "type": "bool", - "defaultValue": true, + "cassandraRoleAssignments": { + "type": "array", + "items": { + "$ref": "#/definitions/cassandraStandaloneRoleAssignmentType" + }, + "nullable": true, "metadata": { - "description": "Optional. Indicates whether virtual machine agent should be provisioned on the virtual machine. When this property is not specified in the request body, default behavior is to set it to true. This will ensure that VM Agent is installed on the VM so that extensions can be added to the VM later." + "description": "Optional. Azure Cosmos DB for Apache Cassandra native data plane role-based access control assignments. Each assignment references a role definition unique identifier and a principal identifier." } }, - "enableAutomaticUpdates": { - "type": "bool", - "defaultValue": true, + "diagnosticSettings": { + "type": "array", + "items": { + "$ref": "#/definitions/diagnosticSettingFullType" + }, + "nullable": true, "metadata": { - "description": "Optional. Indicates whether Automatic Updates is enabled for the Windows virtual machine. Default value is true. When patchMode is set to Manual, this parameter must be set to false. For virtual machine scale sets, this property can be updated and updates will take effect on OS reprovisioning." + "description": "Optional. The diagnostic settings for the service." } }, - "patchMode": { - "type": "string", - "defaultValue": "", + "capabilitiesToAdd": { + "type": "array", + "items": { + "type": "string" + }, + "nullable": true, "allowedValues": [ - "AutomaticByPlatform", - "AutomaticByOS", - "Manual", - "ImageDefault", - "" + "EnableCassandra", + "EnableTable", + "EnableGremlin", + "EnableMongo", + "DisableRateLimitingResponses", + "EnableServerless", + "EnableNoSQLVectorSearch", + "EnableNoSQLFullTextSearch", + "EnableMaterializedViews", + "DeleteAllItemsByPartitionKey" ], "metadata": { - "description": "Optional. VM guest patching orchestration mode. 'AutomaticByOS' & 'Manual' are for Windows only, 'ImageDefault' for Linux only. Refer to 'https://learn.microsoft.com/en-us/azure/virtual-machines/automatic-vm-guest-patching'." - } - }, - "bypassPlatformSafetyChecksOnUserSchedule": { - "type": "bool", - "defaultValue": true, - "metadata": { - "description": "Optional. Enables customer to schedule patching without accidental upgrades." + "description": "Optional. A list of Azure Cosmos DB specific capabilities for the account." } }, - "rebootSetting": { + "backupPolicyType": { "type": "string", - "defaultValue": "IfRequired", + "defaultValue": "Continuous", "allowedValues": [ - "Always", - "IfRequired", - "Never", - "Unknown" + "Periodic", + "Continuous" ], "metadata": { - "description": "Optional. Specifies the reboot setting for all AutomaticByPlatform patch installation operations." + "description": "Optional. Configures the backup mode. Periodic backup must be used if multiple write locations are used. Defaults to \"Continuous\"." } }, - "patchAssessmentMode": { + "backupPolicyContinuousTier": { "type": "string", - "defaultValue": "ImageDefault", + "defaultValue": "Continuous30Days", "allowedValues": [ - "AutomaticByPlatform", - "ImageDefault" + "Continuous30Days", + "Continuous7Days" ], "metadata": { - "description": "Optional. VM guest patching assessment mode. Set it to 'AutomaticByPlatform' to enable automatically check for updates every 24 hours." + "description": "Optional. Configuration values to specify the retention period for continuous mode backup. Default to \"Continuous30Days\"." } }, - "enableHotpatching": { - "type": "bool", - "defaultValue": false, + "backupIntervalInMinutes": { + "type": "int", + "defaultValue": 240, + "minValue": 60, + "maxValue": 1440, "metadata": { - "description": "Optional. Enables customers to patch their Azure VMs without requiring a reboot. For enableHotpatching, the 'provisionVMAgent' must be set to true and 'patchMode' must be set to 'AutomaticByPlatform'." + "description": "Optional. An integer representing the interval in minutes between two backups. This setting only applies to the periodic backup type. Defaults to 240." } }, - "timeZone": { + "backupRetentionIntervalInHours": { + "type": "int", + "defaultValue": 8, + "minValue": 2, + "maxValue": 720, + "metadata": { + "description": "Optional. An integer representing the time (in hours) that each backup is retained. This setting only applies to the periodic backup type. Defaults to 8." + } + }, + "backupStorageRedundancy": { "type": "string", - "defaultValue": "", + "defaultValue": "Local", + "allowedValues": [ + "Geo", + "Local", + "Zone" + ], "metadata": { - "description": "Optional. Specifies the time zone of the virtual machine. e.g. 'Pacific Standard Time'. Possible values can be `TimeZoneInfo.id` value from time zones returned by `TimeZoneInfo.GetSystemTimeZones`." + "description": "Optional. Setting that indicates the type of backup residency. This setting only applies to the periodic backup type. Defaults to \"Local\"." } }, - "additionalUnattendContent": { + "privateEndpoints": { "type": "array", "items": { - "$ref": "#/definitions/additionalUnattendContentType" + "$ref": "#/definitions/privateEndpointMultiServiceType" }, "nullable": true, "metadata": { - "description": "Optional. Specifies additional XML formatted information that can be included in the Unattend.xml file, which is used by Windows Setup. Contents are defined by setting name, component name, and the pass in which the content is applied." + "description": "Optional. Configuration details for private endpoints. For security reasons, it is advised to use private endpoints whenever possible." } }, - "winRMListeners": { - "type": "array", - "items": { - "$ref": "#/definitions/winRMListenerType" + "networkRestrictions": { + "$ref": "#/definitions/networkRestrictionType", + "defaultValue": { + "ipRules": [], + "virtualNetworkRules": [], + "publicNetworkAccess": "Disabled" }, - "nullable": true, "metadata": { - "description": "Optional. Specifies the Windows Remote Management listeners. This enables remote Windows PowerShell." + "description": "Optional. The network configuration of this module. Defaults to `{ ipRules: [], virtualNetworkRules: [], publicNetworkAccess: 'Disabled' }`." } }, - "configurationProfile": { + "minimumTlsVersion": { "type": "string", - "defaultValue": "", + "defaultValue": "Tls12", + "allowedValues": [ + "Tls12" + ], "metadata": { - "description": "Optional. The configuration profile of automanage. Either '/providers/Microsoft.Automanage/bestPractices/AzureBestPracticesProduction', 'providers/Microsoft.Automanage/bestPractices/AzureBestPracticesDevTest' or the resource Id of custom profile." + "description": "Optional. Setting that indicates the minimum allowed TLS version. Azure Cosmos DB for MongoDB RU and Apache Cassandra only work with TLS 1.2 or later. Defaults to \"Tls12\" (TLS 1.2)." } }, - "capacityReservationGroupResourceId": { - "type": "string", - "defaultValue": "", + "enableBurstCapacity": { + "type": "bool", + "defaultValue": true, "metadata": { - "description": "Optional. Capacity reservation group resource id that should be used for allocating the virtual machine vm instances provided enough capacity has been reserved." + "description": "Optional. Flag to indicate enabling/disabling of Burst Capacity feature on the account. Cannot be enabled for serverless accounts." } }, - "networkAccessPolicy": { - "type": "string", - "defaultValue": "DenyAll", - "allowedValues": [ - "AllowAll", - "AllowPrivate", - "DenyAll" - ], + "enableCassandraConnector": { + "type": "bool", + "defaultValue": false, "metadata": { - "description": "Optional. Policy for accessing the disk via network." + "description": "Optional. Enables the cassandra connector on the Cosmos DB C* account." } }, - "publicNetworkAccess": { - "type": "string", - "defaultValue": "Disabled", - "allowedValues": [ - "Disabled", - "Enabled" - ], + "enablePartitionMerge": { + "type": "bool", + "defaultValue": false, "metadata": { - "description": "Optional. Policy for controlling export on the disk." + "description": "Optional. Flag to enable/disable the 'Partition Merge' feature on the account." + } + }, + "enablePerRegionPerPartitionAutoscale": { + "type": "bool", + "defaultValue": false, + "metadata": { + "description": "Optional. Flag to enable/disable the 'PerRegionPerPartitionAutoscale' feature on the account." + } + }, + "analyticalStorageConfiguration": { + "type": "object", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.DocumentDB/databaseAccounts@2025-04-15#properties/properties/properties/analyticalStorageConfiguration" + }, + "description": "Optional. Analytical storage specific properties." + }, + "nullable": true + }, + "cors": { + "type": "array", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.DocumentDB/databaseAccounts@2025-04-15#properties/properties/properties/cors" + }, + "description": "Optional. The CORS policy for the Cosmos DB database account." + }, + "nullable": true + }, + "defaultIdentity": { + "$ref": "#/definitions/defaultIdentityType", + "defaultValue": { + "name": "FirstPartyIdentity" + }, + "metadata": { + "description": "Optional. The default identity for accessing key vault used in features like customer managed keys. Use `FirstPartyIdentity` to use the tenant-level CosmosDB enterprise application. The default identity needs to be explicitly set by the users." } } }, "variables": { "copy": [ - { - "name": "publicKeysFormatted", - "count": "[length(parameters('publicKeys'))]", - "input": { - "path": "[parameters('publicKeys')[copyIndex('publicKeysFormatted')].path]", - "keyData": "[parameters('publicKeys')[copyIndex('publicKeysFormatted')].keyData]" - } - }, - { - "name": "additionalUnattendContentFormatted", - "count": "[length(coalesce(parameters('additionalUnattendContent'), createArray()))]", - "input": { - "settingName": "[coalesce(parameters('additionalUnattendContent'), createArray())[copyIndex('additionalUnattendContentFormatted')].settingName]", - "content": "[coalesce(parameters('additionalUnattendContent'), createArray())[copyIndex('additionalUnattendContentFormatted')].content]", - "componentName": "Microsoft-Windows-Shell-Setup", - "passName": "OobeSystem" - } - }, { "name": "formattedRoleAssignments", "count": "[length(coalesce(parameters('roleAssignments'), createArray()))]", - "input": "[union(coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')], createObject('roleDefinitionId', coalesce(tryGet(variables('builtInRoleNames'), coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName), if(contains(coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName, '/providers/Microsoft.Authorization/roleDefinitions/'), coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName, subscriptionResourceId('Microsoft.Authorization/roleDefinitions', coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName)))))]" + "input": "[union(coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')], createObject('roleDefinitionId', coalesce(tryGet(variables('builtInControlPlaneRoleNames'), coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName), if(contains(coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName, '/providers/Microsoft.Authorization/roleDefinitions/'), coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName, subscriptionResourceId('Microsoft.Authorization/roleDefinitions', coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName)))))]" } ], "enableReferencedModulesTelemetry": false, - "linuxConfiguration": { - "disablePasswordAuthentication": "[parameters('disablePasswordAuthentication')]", - "ssh": { - "publicKeys": "[variables('publicKeysFormatted')]" - }, - "provisionVMAgent": "[parameters('provisionVMAgent')]", - "patchSettings": "[if(and(parameters('provisionVMAgent'), or(equals(toLower(parameters('patchMode')), toLower('AutomaticByPlatform')), equals(toLower(parameters('patchMode')), toLower('ImageDefault')))), createObject('patchMode', parameters('patchMode'), 'assessmentMode', parameters('patchAssessmentMode'), 'automaticByPlatformSettings', if(equals(toLower(parameters('patchMode')), toLower('AutomaticByPlatform')), createObject('bypassPlatformSafetyChecksOnUserSchedule', parameters('bypassPlatformSafetyChecksOnUserSchedule'), 'rebootSetting', parameters('rebootSetting')), null())), null())]" - }, - "windowsConfiguration": { - "provisionVMAgent": "[parameters('provisionVMAgent')]", - "enableAutomaticUpdates": "[parameters('enableAutomaticUpdates')]", - "patchSettings": "[if(and(parameters('provisionVMAgent'), or(or(equals(toLower(parameters('patchMode')), toLower('AutomaticByPlatform')), equals(toLower(parameters('patchMode')), toLower('AutomaticByOS'))), equals(toLower(parameters('patchMode')), toLower('Manual')))), createObject('patchMode', parameters('patchMode'), 'assessmentMode', parameters('patchAssessmentMode'), 'enableHotpatching', if(equals(toLower(parameters('patchMode')), toLower('AutomaticByPlatform')), parameters('enableHotpatching'), false()), 'automaticByPlatformSettings', if(equals(toLower(parameters('patchMode')), toLower('AutomaticByPlatform')), createObject('bypassPlatformSafetyChecksOnUserSchedule', parameters('bypassPlatformSafetyChecksOnUserSchedule'), 'rebootSetting', parameters('rebootSetting')), null())), null())]", - "timeZone": "[if(empty(parameters('timeZone')), null(), parameters('timeZone'))]", - "additionalUnattendContent": "[if(empty(parameters('additionalUnattendContent')), null(), variables('additionalUnattendContentFormatted'))]", - "winRM": "[if(not(empty(parameters('winRMListeners'))), createObject('listeners', parameters('winRMListeners')), null())]" - }, "formattedUserAssignedIdentities": "[reduce(map(coalesce(tryGet(parameters('managedIdentities'), 'userAssignedResourceIds'), createArray()), lambda('id', createObject(format('{0}', lambdaVariables('id')), createObject()))), createObject(), lambda('cur', 'next', union(lambdaVariables('cur'), lambdaVariables('next'))))]", - "identity": "[if(not(empty(parameters('managedIdentities'))), createObject('type', if(if(parameters('extensionAadJoinConfig').enabled, true(), coalesce(tryGet(parameters('managedIdentities'), 'systemAssigned'), false())), if(not(empty(coalesce(tryGet(parameters('managedIdentities'), 'userAssignedResourceIds'), createObject()))), 'SystemAssigned, UserAssigned', 'SystemAssigned'), if(not(empty(coalesce(tryGet(parameters('managedIdentities'), 'userAssignedResourceIds'), createObject()))), 'UserAssigned', null())), 'userAssignedIdentities', if(not(empty(variables('formattedUserAssignedIdentities'))), variables('formattedUserAssignedIdentities'), null())), null())]", - "builtInRoleNames": { + "identity": "[if(not(empty(parameters('managedIdentities'))), createObject('type', if(coalesce(tryGet(parameters('managedIdentities'), 'systemAssigned'), false()), if(not(empty(coalesce(tryGet(parameters('managedIdentities'), 'userAssignedResourceIds'), createObject()))), 'SystemAssigned,UserAssigned', 'SystemAssigned'), if(not(empty(coalesce(tryGet(parameters('managedIdentities'), 'userAssignedResourceIds'), createObject()))), 'UserAssigned', null())), 'userAssignedIdentities', if(not(empty(variables('formattedUserAssignedIdentities'))), variables('formattedUserAssignedIdentities'), null())), null())]", + "builtInControlPlaneRoleNames": { "Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'b24988ac-6180-42a0-ab88-20f7382dd24c')]", - "Data Operator for Managed Disks": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '959f8984-c045-4866-89c7-12bf9737be2e')]", - "Desktop Virtualization Power On Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '489581de-a3bd-480d-9518-53dea7416b33')]", - "Desktop Virtualization Power On Off Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '40c5ff49-9181-41f8-ae61-143b0e78555e')]", - "Desktop Virtualization Virtual Machine Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'a959dbd1-f747-45e3-8ba6-dd80f235f97c')]", - "DevTest Labs User": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '76283e04-6283-4c54-8f91-bcf1374a3c64')]", - "Disk Backup Reader": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '3e5e47e6-65f7-47ef-90b5-e5dd4d455f24')]", - "Disk Pool Operator": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '60fc6e62-5479-42d4-8bf4-67625fcc2840')]", - "Disk Restore Operator": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'b50d9833-a0cb-478e-945f-707fcc997c13')]", - "Disk Snapshot Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '7efff54f-a5b4-42b5-a1c5-5411624893ce')]", + "Cosmos DB Account Reader Role": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'fbdf93bf-df7d-467e-a4d2-9458aa1360c8')]", + "Cosmos DB Operator": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '230815da-be43-4aae-9cb4-875f7bd000aa')]", + "CosmosBackupOperator": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'db7b14f2-5adf-42da-9f96-f2ee17bab5cb')]", + "CosmosRestoreOperator": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '5432c526-bc82-444a-b7ba-57c5b0b5b34f')]", + "DocumentDB Account Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '5bd9cd88-fe45-4216-938b-f97437e15450')]", "Owner": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '8e3af657-a8ff-443c-a75c-2fe8c4bcb635')]", "Reader": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'acdd72a7-3385-48ef-bd42-f606fba81ae7')]", "Role Based Access Control Administrator": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'f58310d9-a9f6-439a-9e8d-f62e7b41a168')]", - "User Access Administrator": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '18d7d88d-d35e-4fb5-a5c3-7773c20a72d9')]", - "Virtual Machine Administrator Login": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '1c0163c0-47e6-4577-8991-ea5c82e286e4')]", - "Virtual Machine Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '9980e02c-c2be-4d73-94e8-173b1dc7cf3c')]", - "Virtual Machine User Login": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'fb879df8-f326-4884-b1cf-06f3ad86be52')]", - "VM Scanner Operator": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'd24ecba3-c1f4-40fa-a7bb-4588a071e8fd')]" + "User Access Administrator": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '18d7d88d-d35e-4fb5-a5c3-7773c20a72d9')]" } }, "resources": { "avmTelemetry": { "condition": "[parameters('enableTelemetry')]", "type": "Microsoft.Resources/deployments", - "apiVersion": "2024-03-01", - "name": "[format('46d3xbcp.res.compute-virtualmachine.{0}.{1}', replace('0.20.0', '.', '-'), substring(uniqueString(deployment().name, parameters('location')), 0, 4))]", + "apiVersion": "2024-07-01", + "name": "[format('46d3xbcp.res.documentdb-databaseaccount.{0}.{1}', replace('0.18.0', '.', '-'), substring(uniqueString(deployment().name, parameters('location')), 0, 4))]", "properties": { "mode": "Incremental", "template": { @@ -36259,245 +37690,80 @@ } } }, - "managedDataDisks": { - "copy": { - "name": "managedDataDisks", - "count": "[length(coalesce(parameters('dataDisks'), createArray()))]" - }, - "condition": "[empty(tryGet(coalesce(parameters('dataDisks'), createArray())[copyIndex()].managedDisk, 'id'))]", - "type": "Microsoft.Compute/disks", - "apiVersion": "2024-03-02", - "name": "[coalesce(tryGet(coalesce(parameters('dataDisks'), createArray())[copyIndex()], 'name'), format('{0}-disk-data-{1}', parameters('name'), padLeft(add(copyIndex(), 1), 2, '0')))]", - "location": "[parameters('location')]", - "sku": { - "name": "[tryGet(coalesce(parameters('dataDisks'), createArray())[copyIndex()].managedDisk, 'storageAccountType')]" - }, - "properties": { - "diskSizeGB": "[coalesce(parameters('dataDisks'), createArray())[copyIndex()].diskSizeGB]", - "creationData": { - "createOption": "[coalesce(tryGet(coalesce(parameters('dataDisks'), createArray())[copyIndex()], 'createoption'), 'Empty')]" - }, - "diskIOPSReadWrite": "[tryGet(coalesce(parameters('dataDisks'), createArray())[copyIndex()], 'diskIOPSReadWrite')]", - "diskMBpsReadWrite": "[tryGet(coalesce(parameters('dataDisks'), createArray())[copyIndex()], 'diskMBpsReadWrite')]", - "publicNetworkAccess": "[parameters('publicNetworkAccess')]", - "networkAccessPolicy": "[parameters('networkAccessPolicy')]" - }, - "zones": "[if(and(not(equals(parameters('availabilityZone'), -1)), not(contains(tryGet(coalesce(parameters('dataDisks'), createArray())[copyIndex()].managedDisk, 'storageAccountType'), 'ZRS'))), array(string(parameters('availabilityZone'))), null())]", - "tags": "[coalesce(tryGet(coalesce(parameters('dataDisks'), createArray())[copyIndex()], 'tags'), parameters('tags'))]" - }, - "vm": { - "type": "Microsoft.Compute/virtualMachines", - "apiVersion": "2024-07-01", + "databaseAccount": { + "type": "Microsoft.DocumentDB/databaseAccounts", + "apiVersion": "2025-04-15", "name": "[parameters('name')]", "location": "[parameters('location')]", - "identity": "[variables('identity')]", "tags": "[parameters('tags')]", - "zones": "[if(not(equals(parameters('availabilityZone'), -1)), array(string(parameters('availabilityZone'))), null())]", - "plan": "[parameters('plan')]", - "properties": { - "hardwareProfile": { - "vmSize": "[parameters('vmSize')]" - }, - "securityProfile": "[shallowMerge(createArray(if(parameters('encryptionAtHost'), createObject('encryptionAtHost', parameters('encryptionAtHost')), createObject()), createObject('securityType', parameters('securityType'), 'uefiSettings', if(equals(parameters('securityType'), 'TrustedLaunch'), createObject('secureBootEnabled', parameters('secureBootEnabled'), 'vTpmEnabled', parameters('vTpmEnabled')), null()))))]", - "storageProfile": { - "copy": [ - { - "name": "dataDisks", - "count": "[length(coalesce(parameters('dataDisks'), createArray()))]", - "input": { - "lun": "[coalesce(tryGet(coalesce(parameters('dataDisks'), createArray())[copyIndex('dataDisks')], 'lun'), copyIndex('dataDisks'))]", - "name": "[if(not(empty(tryGet(coalesce(parameters('dataDisks'), createArray())[copyIndex('dataDisks')].managedDisk, 'id'))), last(split(coalesce(coalesce(parameters('dataDisks'), createArray())[copyIndex('dataDisks')].managedDisk.id, ''), '/')), coalesce(tryGet(coalesce(parameters('dataDisks'), createArray())[copyIndex('dataDisks')], 'name'), format('{0}-disk-data-{1}', parameters('name'), padLeft(add(copyIndex('dataDisks'), 1), 2, '0'))))]", - "createOption": "[if(or(not(equals(if(empty(tryGet(coalesce(parameters('dataDisks'), createArray())[copyIndex('dataDisks')].managedDisk, 'id')), resourceId('Microsoft.Compute/disks', coalesce(tryGet(coalesce(parameters('dataDisks'), createArray())[copyIndex('dataDisks')], 'name'), format('{0}-disk-data-{1}', parameters('name'), padLeft(add(copyIndex('dataDisks'), 1), 2, '0')))), null()), null())), not(empty(tryGet(coalesce(parameters('dataDisks'), createArray())[copyIndex('dataDisks')].managedDisk, 'id')))), 'Attach', coalesce(tryGet(coalesce(parameters('dataDisks'), createArray())[copyIndex('dataDisks')], 'createoption'), 'Empty'))]", - "deleteOption": "[if(not(empty(tryGet(coalesce(parameters('dataDisks'), createArray())[copyIndex('dataDisks')].managedDisk, 'id'))), 'Detach', coalesce(tryGet(coalesce(parameters('dataDisks'), createArray())[copyIndex('dataDisks')], 'deleteOption'), 'Delete'))]", - "caching": "[if(not(empty(tryGet(coalesce(parameters('dataDisks'), createArray())[copyIndex('dataDisks')].managedDisk, 'id'))), 'None', coalesce(tryGet(coalesce(parameters('dataDisks'), createArray())[copyIndex('dataDisks')], 'caching'), 'ReadOnly'))]", - "managedDisk": { - "id": "[coalesce(tryGet(coalesce(parameters('dataDisks'), createArray())[copyIndex('dataDisks')].managedDisk, 'id'), if(empty(tryGet(coalesce(parameters('dataDisks'), createArray())[copyIndex('dataDisks')].managedDisk, 'id')), resourceId('Microsoft.Compute/disks', coalesce(tryGet(coalesce(parameters('dataDisks'), createArray())[copyIndex('dataDisks')], 'name'), format('{0}-disk-data-{1}', parameters('name'), padLeft(add(copyIndex('dataDisks'), 1), 2, '0')))), null()))]", - "diskEncryptionSet": "[if(contains(coalesce(parameters('dataDisks'), createArray())[copyIndex('dataDisks')].managedDisk, 'diskEncryptionSet'), createObject('id', coalesce(parameters('dataDisks'), createArray())[copyIndex('dataDisks')].managedDisk.diskEncryptionSet.id), null())]" - } - } - } - ], - "imageReference": "[parameters('imageReference')]", - "osDisk": { - "name": "[coalesce(tryGet(parameters('osDisk'), 'name'), format('{0}-disk-os-01', parameters('name')))]", - "createOption": "[coalesce(tryGet(parameters('osDisk'), 'createOption'), 'FromImage')]", - "deleteOption": "[coalesce(tryGet(parameters('osDisk'), 'deleteOption'), 'Delete')]", - "diffDiskSettings": "[if(empty(coalesce(tryGet(parameters('osDisk'), 'diffDiskSettings'), createObject())), null(), createObject('option', 'Local', 'placement', parameters('osDisk').diffDiskSettings.placement))]", - "diskSizeGB": "[tryGet(parameters('osDisk'), 'diskSizeGB')]", - "caching": "[coalesce(tryGet(parameters('osDisk'), 'caching'), 'ReadOnly')]", - "managedDisk": { - "storageAccountType": "[tryGet(parameters('osDisk').managedDisk, 'storageAccountType')]", - "diskEncryptionSet": { - "id": "[tryGet(parameters('osDisk').managedDisk, 'diskEncryptionSetResourceId')]" - } - } - } - }, - "additionalCapabilities": { - "ultraSSDEnabled": "[parameters('ultraSSDEnabled')]", - "hibernationEnabled": "[parameters('hibernationEnabled')]" - }, - "osProfile": { - "computerName": "[parameters('computerName')]", - "adminUsername": "[parameters('adminUsername')]", - "adminPassword": "[parameters('adminPassword')]", - "customData": "[if(not(empty(parameters('customData'))), base64(parameters('customData')), null())]", - "windowsConfiguration": "[if(equals(parameters('osType'), 'Windows'), variables('windowsConfiguration'), null())]", - "linuxConfiguration": "[if(equals(parameters('osType'), 'Linux'), variables('linuxConfiguration'), null())]", - "secrets": "[parameters('certificatesToBeInstalled')]", - "allowExtensionOperations": "[parameters('allowExtensionOperations')]" - }, - "networkProfile": { - "copy": [ - { - "name": "networkInterfaces", - "count": "[length(parameters('nicConfigurations'))]", - "input": { - "properties": { - "deleteOption": "[coalesce(tryGet(parameters('nicConfigurations')[copyIndex('networkInterfaces')], 'deleteOption'), 'Delete')]", - "primary": "[if(equals(copyIndex('networkInterfaces'), 0), true(), false())]" - }, - "id": "[resourceId('Microsoft.Network/networkInterfaces', coalesce(tryGet(parameters('nicConfigurations')[copyIndex('networkInterfaces')], 'name'), format('{0}{1}', parameters('name'), tryGet(parameters('nicConfigurations')[copyIndex('networkInterfaces')], 'nicSuffix'))))]" - } - } - ] - }, - "capacityReservation": "[if(not(empty(parameters('capacityReservationGroupResourceId'))), createObject('capacityReservationGroup', createObject('id', parameters('capacityReservationGroupResourceId'))), null())]", - "diagnosticsProfile": { - "bootDiagnostics": { - "enabled": "[if(not(empty(parameters('bootDiagnosticStorageAccountName'))), true(), parameters('bootDiagnostics'))]", - "storageUri": "[if(not(empty(parameters('bootDiagnosticStorageAccountName'))), format('https://{0}{1}', parameters('bootDiagnosticStorageAccountName'), parameters('bootDiagnosticStorageAccountUri')), null())]" - } - }, - "applicationProfile": "[if(not(empty(parameters('galleryApplications'))), createObject('galleryApplications', parameters('galleryApplications')), null())]", - "availabilitySet": "[if(not(empty(parameters('availabilitySetResourceId'))), createObject('id', parameters('availabilitySetResourceId')), null())]", - "proximityPlacementGroup": "[if(not(empty(parameters('proximityPlacementGroupResourceId'))), createObject('id', parameters('proximityPlacementGroupResourceId')), null())]", - "virtualMachineScaleSet": "[if(not(empty(parameters('virtualMachineScaleSetResourceId'))), createObject('id', parameters('virtualMachineScaleSetResourceId')), null())]", - "priority": "[parameters('priority')]", - "evictionPolicy": "[if(and(not(empty(parameters('priority'))), not(equals(parameters('priority'), 'Regular'))), parameters('evictionPolicy'), null())]", - "billingProfile": "[if(and(not(empty(parameters('priority'))), not(empty(parameters('maxPriceForLowPriorityVm')))), createObject('maxPrice', json(parameters('maxPriceForLowPriorityVm'))), null())]", - "host": "[if(not(empty(parameters('dedicatedHostResourceId'))), createObject('id', parameters('dedicatedHostResourceId')), null())]", - "licenseType": "[parameters('licenseType')]", - "userData": "[if(not(empty(parameters('userData'))), base64(parameters('userData')), null())]" - }, - "dependsOn": [ - "managedDataDisks", - "vm_nic" - ] - }, - "vm_configurationAssignment": { - "condition": "[not(empty(parameters('maintenanceConfigurationResourceId')))]", - "type": "Microsoft.Maintenance/configurationAssignments", - "apiVersion": "2023-04-01", - "scope": "[format('Microsoft.Compute/virtualMachines/{0}', parameters('name'))]", - "name": "[format('{0}assignment', parameters('name'))]", - "location": "[parameters('location')]", - "properties": { - "maintenanceConfigurationId": "[parameters('maintenanceConfigurationResourceId')]", - "resourceId": "[resourceId('Microsoft.Compute/virtualMachines', parameters('name'))]" - }, - "dependsOn": [ - "vm" - ] - }, - "vm_configurationProfileAssignment": { - "condition": "[not(empty(parameters('configurationProfile')))]", - "type": "Microsoft.Automanage/configurationProfileAssignments", - "apiVersion": "2022-05-04", - "scope": "[format('Microsoft.Compute/virtualMachines/{0}', parameters('name'))]", - "name": "default", - "properties": { - "configurationProfile": "[parameters('configurationProfile')]" - }, - "dependsOn": [ - "vm" - ] + "identity": "[variables('identity')]", + "kind": "[if(not(empty(parameters('mongodbDatabases'))), 'MongoDB', 'GlobalDocumentDB')]", + "properties": "[shallowMerge(createArray(createObject('enableBurstCapacity', if(not(contains(coalesce(parameters('capabilitiesToAdd'), createArray()), 'EnableServerless')), parameters('enableBurstCapacity'), false()), 'analyticalStorageConfiguration', parameters('analyticalStorageConfiguration'), 'defaultIdentity', if(and(not(empty(parameters('defaultIdentity'))), not(equals(tryGet(parameters('defaultIdentity'), 'name'), 'UserAssignedIdentity'))), parameters('defaultIdentity').name, format('UserAssignedIdentity={0}', tryGet(parameters('defaultIdentity'), 'resourceId'))), 'enablePartitionMerge', parameters('enablePartitionMerge'), 'enablePerRegionPerPartitionAutoscale', parameters('enablePerRegionPerPartitionAutoscale'), 'databaseAccountOfferType', parameters('databaseAccountOfferType'), 'backupPolicy', shallowMerge(createArray(createObject('type', parameters('backupPolicyType')), if(equals(parameters('backupPolicyType'), 'Continuous'), createObject('continuousModeProperties', createObject('tier', parameters('backupPolicyContinuousTier'))), createObject()), if(equals(parameters('backupPolicyType'), 'Periodic'), createObject('periodicModeProperties', createObject('backupIntervalInMinutes', parameters('backupIntervalInMinutes'), 'backupRetentionIntervalInHours', parameters('backupRetentionIntervalInHours'), 'backupStorageRedundancy', parameters('backupStorageRedundancy'))), createObject()))), 'capabilities', map(coalesce(parameters('capabilitiesToAdd'), createArray()), lambda('capability', createObject('name', lambdaVariables('capability'))))), if(not(empty(parameters('cors'))), createObject('cors', parameters('cors')), createObject()), if(contains(coalesce(parameters('capabilitiesToAdd'), createArray()), 'EnableCassandra'), createObject('connectorOffer', if(parameters('enableCassandraConnector'), 'Small', null()), 'enableCassandraConnector', parameters('enableCassandraConnector')), createObject()), createObject('minimalTlsVersion', parameters('minimumTlsVersion'), 'capacity', createObject('totalThroughputLimit', parameters('totalThroughputLimit')), 'publicNetworkAccess', coalesce(tryGet(parameters('networkRestrictions'), 'publicNetworkAccess'), 'Disabled')), if(or(or(or(or(not(empty(parameters('sqlDatabases'))), not(empty(parameters('mongodbDatabases')))), not(empty(parameters('gremlinDatabases')))), not(empty(parameters('tables')))), not(empty(parameters('cassandraKeyspaces')))), createObject('consistencyPolicy', shallowMerge(createArray(createObject('defaultConsistencyLevel', parameters('defaultConsistencyLevel')), if(equals(parameters('defaultConsistencyLevel'), 'BoundedStaleness'), createObject('maxStalenessPrefix', parameters('maxStalenessPrefix'), 'maxIntervalInSeconds', parameters('maxIntervalInSeconds')), createObject()))), 'enableMultipleWriteLocations', parameters('enableMultipleWriteLocations'), 'locations', if(not(empty(parameters('failoverLocations'))), map(parameters('failoverLocations'), lambda('failoverLocation', createObject('failoverPriority', lambdaVariables('failoverLocation').failoverPriority, 'locationName', lambdaVariables('failoverLocation').locationName, 'isZoneRedundant', coalesce(tryGet(lambdaVariables('failoverLocation'), 'isZoneRedundant'), true())))), createArray(createObject('failoverPriority', 0, 'locationName', parameters('location'), 'isZoneRedundant', parameters('zoneRedundant')))), 'ipRules', map(coalesce(tryGet(parameters('networkRestrictions'), 'ipRules'), createArray()), lambda('ipRule', createObject('ipAddressOrRange', lambdaVariables('ipRule')))), 'virtualNetworkRules', map(coalesce(tryGet(parameters('networkRestrictions'), 'virtualNetworkRules'), createArray()), lambda('rule', createObject('id', lambdaVariables('rule').subnetResourceId, 'ignoreMissingVNetServiceEndpoint', false()))), 'networkAclBypass', coalesce(tryGet(parameters('networkRestrictions'), 'networkAclBypass'), 'None'), 'networkAclBypassResourceIds', tryGet(parameters('networkRestrictions'), 'networkAclBypassResourceIds'), 'isVirtualNetworkFilterEnabled', or(not(empty(tryGet(parameters('networkRestrictions'), 'ipRules'))), not(empty(tryGet(parameters('networkRestrictions'), 'virtualNetworkRules')))), 'enableFreeTier', parameters('enableFreeTier'), 'enableAutomaticFailover', parameters('enableAutomaticFailover'), 'enableAnalyticalStorage', parameters('enableAnalyticalStorage')), createObject()), if(or(or(not(empty(parameters('mongodbDatabases'))), not(empty(parameters('gremlinDatabases')))), not(empty(parameters('cassandraKeyspaces')))), createObject('disableLocalAuth', false(), 'disableKeyBasedMetadataWriteAccess', false()), createObject('disableLocalAuth', parameters('disableLocalAuthentication'), 'disableKeyBasedMetadataWriteAccess', parameters('disableKeyBasedMetadataWriteAccess'))), if(not(empty(parameters('mongodbDatabases'))), createObject('apiProperties', createObject('serverVersion', parameters('serverVersion'))), createObject())))]" }, - "vm_autoShutdownConfiguration": { - "condition": "[not(empty(parameters('autoShutdownConfig')))]", - "type": "Microsoft.DevTestLab/schedules", - "apiVersion": "2018-09-15", - "name": "[format('shutdown-computevm-{0}', parameters('name'))]", - "location": "[parameters('location')]", + "databaseAccount_lock": { + "condition": "[and(not(empty(coalesce(parameters('lock'), createObject()))), not(equals(tryGet(parameters('lock'), 'kind'), 'None')))]", + "type": "Microsoft.Authorization/locks", + "apiVersion": "2020-05-01", + "scope": "[format('Microsoft.DocumentDB/databaseAccounts/{0}', parameters('name'))]", + "name": "[coalesce(tryGet(parameters('lock'), 'name'), format('lock-{0}', parameters('name')))]", "properties": { - "status": "[coalesce(tryGet(parameters('autoShutdownConfig'), 'status'), 'Disabled')]", - "targetResourceId": "[resourceId('Microsoft.Compute/virtualMachines', parameters('name'))]", - "taskType": "ComputeVmShutdownTask", - "dailyRecurrence": { - "time": "[coalesce(tryGet(parameters('autoShutdownConfig'), 'dailyRecurrenceTime'), '19:00')]" - }, - "timeZoneId": "[coalesce(tryGet(parameters('autoShutdownConfig'), 'timeZone'), 'UTC')]", - "notificationSettings": "[if(contains(parameters('autoShutdownConfig'), 'notificationSettings'), createObject('status', coalesce(tryGet(parameters('autoShutdownConfig'), 'status'), 'Disabled'), 'emailRecipient', coalesce(tryGet(tryGet(parameters('autoShutdownConfig'), 'notificationSettings'), 'emailRecipient'), ''), 'notificationLocale', coalesce(tryGet(tryGet(parameters('autoShutdownConfig'), 'notificationSettings'), 'notificationLocale'), 'en'), 'webhookUrl', coalesce(tryGet(tryGet(parameters('autoShutdownConfig'), 'notificationSettings'), 'webhookUrl'), ''), 'timeInMinutes', coalesce(tryGet(tryGet(parameters('autoShutdownConfig'), 'notificationSettings'), 'timeInMinutes'), 30)), null())]" + "level": "[coalesce(tryGet(parameters('lock'), 'kind'), '')]", + "notes": "[coalesce(tryGet(parameters('lock'), 'notes'), if(equals(tryGet(parameters('lock'), 'kind'), 'CanNotDelete'), 'Cannot delete resource or child resources.', 'Cannot delete or modify the resource or child resources.'))]" }, "dependsOn": [ - "vm" + "databaseAccount" ] }, - "vm_dataCollectionRuleAssociations": { + "databaseAccount_diagnosticSettings": { "copy": { - "name": "vm_dataCollectionRuleAssociations", - "count": "[length(parameters('extensionMonitoringAgentConfig').dataCollectionRuleAssociations)]" - }, - "condition": "[parameters('extensionMonitoringAgentConfig').enabled]", - "type": "Microsoft.Insights/dataCollectionRuleAssociations", - "apiVersion": "2023-03-11", - "scope": "[format('Microsoft.Compute/virtualMachines/{0}', parameters('name'))]", - "name": "[parameters('extensionMonitoringAgentConfig').dataCollectionRuleAssociations[copyIndex()].name]", - "properties": { - "dataCollectionRuleId": "[parameters('extensionMonitoringAgentConfig').dataCollectionRuleAssociations[copyIndex()].dataCollectionRuleResourceId]" - }, - "dependsOn": [ - "vm", - "vm_azureMonitorAgentExtension" - ] - }, - "cseIdentity": { - "condition": "[not(empty(tryGet(tryGet(parameters('extensionCustomScriptConfig'), 'protectedSettings'), 'managedIdentityResourceId')))]", - "existing": true, - "type": "Microsoft.ManagedIdentity/userAssignedIdentities", - "apiVersion": "2024-11-30", - "subscriptionId": "[split(parameters('extensionCustomScriptConfig').protectedSettings.managedIdentityResourceId, '/')[2]]", - "resourceGroup": "[split(parameters('extensionCustomScriptConfig').protectedSettings.managedIdentityResourceId, '/')[4]]", - "name": "[last(split(parameters('extensionCustomScriptConfig').protectedSettings.managedIdentityResourceId, '/'))]" - }, - "AzureWindowsBaseline": { - "condition": "[not(empty(parameters('guestConfiguration')))]", - "type": "Microsoft.GuestConfiguration/guestConfigurationAssignments", - "apiVersion": "2024-04-05", - "scope": "[format('Microsoft.Compute/virtualMachines/{0}', parameters('name'))]", - "name": "[coalesce(tryGet(parameters('guestConfiguration'), 'name'), 'AzureWindowsBaseline')]", - "location": "[parameters('location')]", - "properties": { - "guestConfiguration": "[parameters('guestConfiguration')]" + "name": "databaseAccount_diagnosticSettings", + "count": "[length(coalesce(parameters('diagnosticSettings'), createArray()))]" }, - "dependsOn": [ - "vm", - "vm_azureGuestConfigurationExtension" - ] - }, - "vm_lock": { - "condition": "[and(not(empty(coalesce(parameters('lock'), createObject()))), not(equals(tryGet(parameters('lock'), 'kind'), 'None')))]", - "type": "Microsoft.Authorization/locks", - "apiVersion": "2020-05-01", - "scope": "[format('Microsoft.Compute/virtualMachines/{0}', parameters('name'))]", - "name": "[coalesce(tryGet(parameters('lock'), 'name'), format('lock-{0}', parameters('name')))]", + "type": "Microsoft.Insights/diagnosticSettings", + "apiVersion": "2021-05-01-preview", + "scope": "[format('Microsoft.DocumentDB/databaseAccounts/{0}', parameters('name'))]", + "name": "[coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'name'), format('{0}-diagnosticSettings', parameters('name')))]", "properties": { - "level": "[coalesce(tryGet(parameters('lock'), 'kind'), '')]", - "notes": "[coalesce(tryGet(parameters('lock'), 'notes'), if(equals(tryGet(parameters('lock'), 'kind'), 'CanNotDelete'), 'Cannot delete resource or child resources.', 'Cannot delete or modify the resource or child resources.'))]" + "copy": [ + { + "name": "metrics", + "count": "[length(coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'metricCategories'), createArray(createObject('category', 'AllMetrics'))))]", + "input": { + "category": "[coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'metricCategories'), createArray(createObject('category', 'AllMetrics')))[copyIndex('metrics')].category]", + "enabled": "[coalesce(tryGet(coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'metricCategories'), createArray(createObject('category', 'AllMetrics')))[copyIndex('metrics')], 'enabled'), true())]", + "timeGrain": null + } + }, + { + "name": "logs", + "count": "[length(coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'logCategoriesAndGroups'), createArray(createObject('categoryGroup', 'allLogs'))))]", + "input": { + "categoryGroup": "[tryGet(coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'logCategoriesAndGroups'), createArray(createObject('categoryGroup', 'allLogs')))[copyIndex('logs')], 'categoryGroup')]", + "category": "[tryGet(coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'logCategoriesAndGroups'), createArray(createObject('categoryGroup', 'allLogs')))[copyIndex('logs')], 'category')]", + "enabled": "[coalesce(tryGet(coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'logCategoriesAndGroups'), createArray(createObject('categoryGroup', 'allLogs')))[copyIndex('logs')], 'enabled'), true())]" + } + } + ], + "storageAccountId": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'storageAccountResourceId')]", + "workspaceId": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'workspaceResourceId')]", + "eventHubAuthorizationRuleId": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'eventHubAuthorizationRuleResourceId')]", + "eventHubName": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'eventHubName')]", + "marketplacePartnerId": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'marketplacePartnerResourceId')]", + "logAnalyticsDestinationType": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'logAnalyticsDestinationType')]" }, "dependsOn": [ - "vm" + "databaseAccount" ] }, - "vm_roleAssignments": { + "databaseAccount_roleAssignments": { "copy": { - "name": "vm_roleAssignments", + "name": "databaseAccount_roleAssignments", "count": "[length(coalesce(variables('formattedRoleAssignments'), createArray()))]" }, "type": "Microsoft.Authorization/roleAssignments", "apiVersion": "2022-04-01", - "scope": "[format('Microsoft.Compute/virtualMachines/{0}', parameters('name'))]", - "name": "[coalesce(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'name'), guid(resourceId('Microsoft.Compute/virtualMachines', parameters('name')), coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].principalId, coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].roleDefinitionId))]", + "scope": "[format('Microsoft.DocumentDB/databaseAccounts/{0}', parameters('name'))]", + "name": "[coalesce(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'name'), guid(resourceId('Microsoft.DocumentDB/databaseAccounts', parameters('name')), coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].principalId, coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].roleDefinitionId))]", "properties": { "roleDefinitionId": "[coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].roleDefinitionId]", "principalId": "[coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].principalId]", @@ -36508,59 +37774,37 @@ "delegatedManagedIdentityResourceId": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'delegatedManagedIdentityResourceId')]" }, "dependsOn": [ - "vm" + "databaseAccount" ] }, - "vm_nic": { + "databaseAccount_sqlDatabases": { "copy": { - "name": "vm_nic", - "count": "[length(parameters('nicConfigurations'))]" + "name": "databaseAccount_sqlDatabases", + "count": "[length(coalesce(parameters('sqlDatabases'), createArray()))]" }, "type": "Microsoft.Resources/deployments", - "apiVersion": "2022-09-01", - "name": "[format('{0}-VM-Nic-{1}', uniqueString(deployment().name, parameters('location')), copyIndex())]", + "apiVersion": "2025-04-01", + "name": "[format('{0}-sqldb-{1}', uniqueString(deployment().name, parameters('location')), coalesce(parameters('sqlDatabases'), createArray())[copyIndex()].name)]", "properties": { - "expressionEvaluationOptions": { - "scope": "inner" - }, - "mode": "Incremental", - "parameters": { - "networkInterfaceName": { - "value": "[coalesce(tryGet(parameters('nicConfigurations')[copyIndex()], 'name'), format('{0}{1}', parameters('name'), tryGet(parameters('nicConfigurations')[copyIndex()], 'nicSuffix')))]" - }, - "virtualMachineName": { - "value": "[parameters('name')]" - }, - "location": { - "value": "[parameters('location')]" - }, - "enableIPForwarding": { - "value": "[coalesce(tryGet(parameters('nicConfigurations')[copyIndex()], 'enableIPForwarding'), false())]" - }, - "enableAcceleratedNetworking": { - "value": "[coalesce(tryGet(parameters('nicConfigurations')[copyIndex()], 'enableAcceleratedNetworking'), true())]" - }, - "dnsServers": "[if(contains(parameters('nicConfigurations')[copyIndex()], 'dnsServers'), if(not(empty(tryGet(parameters('nicConfigurations')[copyIndex()], 'dnsServers'))), createObject('value', tryGet(parameters('nicConfigurations')[copyIndex()], 'dnsServers')), createObject('value', createArray())), createObject('value', createArray()))]", - "networkSecurityGroupResourceId": { - "value": "[coalesce(tryGet(parameters('nicConfigurations')[copyIndex()], 'networkSecurityGroupResourceId'), '')]" - }, - "ipConfigurations": { - "value": "[parameters('nicConfigurations')[copyIndex()].ipConfigurations]" - }, - "lock": { - "value": "[coalesce(tryGet(parameters('nicConfigurations')[copyIndex()], 'lock'), parameters('lock'))]" + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "name": { + "value": "[coalesce(parameters('sqlDatabases'), createArray())[copyIndex()].name]" }, - "tags": { - "value": "[coalesce(tryGet(parameters('nicConfigurations')[copyIndex()], 'tags'), parameters('tags'))]" + "containers": { + "value": "[tryGet(coalesce(parameters('sqlDatabases'), createArray())[copyIndex()], 'containers')]" }, - "diagnosticSettings": { - "value": "[tryGet(parameters('nicConfigurations')[copyIndex()], 'diagnosticSettings')]" + "throughput": { + "value": "[tryGet(coalesce(parameters('sqlDatabases'), createArray())[copyIndex()], 'throughput')]" }, - "roleAssignments": { - "value": "[tryGet(parameters('nicConfigurations')[copyIndex()], 'roleAssignments')]" + "databaseAccountName": { + "value": "[parameters('name')]" }, - "enableTelemetry": { - "value": "[variables('enableReferencedModulesTelemetry')]" + "autoscaleSettingsMaxThroughput": { + "value": "[tryGet(coalesce(parameters('sqlDatabases'), createArray())[copyIndex()], 'autoscaleSettingsMaxThroughput')]" } }, "template": { @@ -36570,2808 +37814,2830 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.37.4.10188", - "templateHash": "774019590280042559" - } + "version": "0.38.33.27573", + "templateHash": "1549250134356326406" + }, + "name": "DocumentDB Database Account SQL Databases", + "description": "This module deploys a SQL Database in a CosmosDB Account." }, "definitions": { - "publicIPConfigurationType": { + "containerType": { "type": "object", "properties": { "name": { "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The name of the Public IP Address." - } - }, - "publicIPAddressResourceId": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The resource ID of the public IP address." - } - }, - "diagnosticSettings": { - "type": "array", - "items": { - "$ref": "#/definitions/diagnosticSettingFullType" - }, - "nullable": true, "metadata": { - "description": "Optional. Diagnostic settings for the public IP address." + "description": "Required. Name of the container." } }, - "location": { - "type": "string", + "analyticalStorageTtl": { + "type": "int", "nullable": true, "metadata": { - "description": "Optional. The idle timeout in minutes." + "description": "Optional. Default to 0. Indicates how long data should be retained in the analytical store, for a container. Analytical store is enabled when ATTL is set with a value other than 0. If the value is set to -1, the analytical store retains all historical data, irrespective of the retention of the data in the transactional store." } }, - "lock": { - "$ref": "#/definitions/lockType", - "nullable": true, + "conflictResolutionPolicy": { + "type": "object", "metadata": { - "description": "Optional. The lock settings of the public IP address." - } + "__bicep_resource_derived_type!": { + "source": "Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers@2025-04-15#properties/properties/properties/resource/properties/conflictResolutionPolicy" + }, + "description": "Optional. The conflict resolution policy for the container. Conflicts and conflict resolution policies are applicable if the Azure Cosmos DB account is configured with multiple write regions." + }, + "nullable": true }, - "idleTimeoutInMinutes": { + "defaultTtl": { "type": "int", "nullable": true, + "minValue": -1, + "maxValue": 2147483647, "metadata": { - "description": "Optional. The idle timeout of the public IP address." - } - }, - "ddosSettings": { - "$ref": "#/definitions/ddosSettingsType", - "nullable": true, - "metadata": { - "description": "Optional. The DDoS protection plan configuration associated with the public IP address." + "description": "Optional. Default to -1. Default time to live (in seconds). With Time to Live or TTL, Azure Cosmos DB provides the ability to delete items automatically from a container after a certain time period. If the value is set to \"-1\", it is equal to infinity, and items don't expire by default." } }, - "dnsSettings": { - "$ref": "#/definitions/dnsSettingsType", + "throughput": { + "type": "int", "nullable": true, "metadata": { - "description": "Optional. The DNS settings of the public IP address." + "description": "Optional. Default to 400. Request Units per second. Will be ignored if autoscaleSettingsMaxThroughput is used. For best performance for large production workloads, it is recommended to set dedicated throughput (autoscale or manual) at the container level and not at the database level." } }, - "publicIPAddressVersion": { - "type": "string", - "allowedValues": [ - "IPv4", - "IPv6" - ], + "autoscaleSettingsMaxThroughput": { + "type": "int", "nullable": true, + "maxValue": 1000000, "metadata": { - "description": "Optional. The public IP address version." + "description": "Optional. Specifies the Autoscale settings and represents maximum throughput, the resource can scale up to. The autoscale throughput should have valid throughput values between 1000 and 1000000 inclusive in increments of 1000. If value is set to null, then autoscale will be disabled. For best performance for large production workloads, it is recommended to set dedicated throughput (autoscale or manual) at the container level and not at the database level." } }, - "publicIPAllocationMethod": { - "type": "string", - "allowedValues": [ - "Dynamic", - "Static" - ], - "nullable": true, + "tags": { + "type": "object", "metadata": { - "description": "Optional. The public IP address allocation method." - } + "__bicep_resource_derived_type!": { + "source": "Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers@2025-04-15#properties/tags" + }, + "description": "Optional. Tags of the SQL Database resource." + }, + "nullable": true }, - "publicIpPrefixResourceId": { - "type": "string", - "nullable": true, + "paths": { + "type": "array", + "items": { + "type": "string" + }, + "minLength": 1, + "maxLength": 3, "metadata": { - "description": "Optional. Resource ID of the Public IP Prefix object. This is only needed if you want your Public IPs created in a PIP Prefix." + "description": "Required. List of paths using which data within the container can be partitioned. For kind=MultiHash it can be up to 3. For anything else it needs to be exactly 1." } }, - "publicIpNameSuffix": { - "type": "string", - "nullable": true, + "indexingPolicy": { + "type": "object", "metadata": { - "description": "Optional. The name suffix of the public IP address resource." - } + "__bicep_resource_derived_type!": { + "source": "Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers@2025-04-15#properties/properties/properties/resource/properties/indexingPolicy" + }, + "description": "Optional. Indexing policy of the container." + }, + "nullable": true }, - "roleAssignments": { + "uniqueKeyPolicyKeys": { "type": "array", - "items": { - "$ref": "#/definitions/roleAssignmentType" - }, - "nullable": true, "metadata": { - "description": "Optional. Array of role assignments to create." - } + "__bicep_resource_derived_type!": { + "source": "Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers@2025-04-15#properties/properties/properties/resource/properties/uniqueKeyPolicy/properties/uniqueKeys" + }, + "description": "Optional. The unique key policy configuration containing a list of unique keys that enforces uniqueness constraint on documents in the collection in the Azure Cosmos DB service." + }, + "nullable": true }, - "skuName": { + "kind": { "type": "string", "allowedValues": [ - "Basic", - "Standard" + "Hash", + "MultiHash" ], "nullable": true, "metadata": { - "description": "Optional. The SKU name of the public IP address." + "description": "Optional. Default to Hash. Indicates the kind of algorithm used for partitioning." } }, - "skuTier": { - "type": "string", + "version": { + "type": "int", "allowedValues": [ - "Global", - "Regional" + 1, + 2 ], "nullable": true, "metadata": { - "description": "Optional. The SKU tier of the public IP address." + "description": "Optional. Default to 1 for Hash and 2 for MultiHash - 1 is not allowed for MultiHash. Version of the partition key definition." } + } + }, + "metadata": { + "__bicep_export!": true, + "description": "The type of a container." + } + } + }, + "parameters": { + "databaseAccountName": { + "type": "string", + "metadata": { + "description": "Conditional. The name of the parent Database Account. Required if the template is used in a standalone deployment." + } + }, + "name": { + "type": "string", + "metadata": { + "description": "Required. Name of the SQL database ." + } + }, + "containers": { + "type": "array", + "items": { + "$ref": "#/definitions/containerType" + }, + "nullable": true, + "metadata": { + "description": "Optional. Array of containers to deploy in the SQL database." + } + }, + "throughput": { + "type": "int", + "nullable": true, + "metadata": { + "description": "Optional. Request units per second. Will be ignored if autoscaleSettingsMaxThroughput is used. Setting throughput at the database level is only recommended for development/test or when workload across all containers in the shared throughput database is uniform. For best performance for large production workloads, it is recommended to set dedicated throughput (autoscale or manual) at the container level and not at the database level." + } + }, + "autoscaleSettingsMaxThroughput": { + "type": "int", + "nullable": true, + "metadata": { + "description": "Optional. Specifies the Autoscale settings and represents maximum throughput, the resource can scale up to. The autoscale throughput should have valid throughput values between 1000 and 1000000 inclusive in increments of 1000. If value is set to null, then autoscale will be disabled. Setting throughput at the database level is only recommended for development/test or when workload across all containers in the shared throughput database is uniform. For best performance for large production workloads, it is recommended to set dedicated throughput (autoscale or manual) at the container level and not at the database level." + } + }, + "tags": { + "type": "object", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.DocumentDB/databaseAccounts/sqlDatabases@2025-04-15#properties/tags" }, - "tags": { - "type": "object", - "metadata": { - "__bicep_resource_derived_type!": { - "source": "Microsoft.Network/publicIPAddresses@2024-07-01#properties/tags" - }, - "description": "Optional. The tags of the public IP address." - }, - "nullable": true + "description": "Optional. Tags of the SQL database resource." + }, + "nullable": true + } + }, + "resources": { + "databaseAccount": { + "existing": true, + "type": "Microsoft.DocumentDB/databaseAccounts", + "apiVersion": "2025-04-15", + "name": "[parameters('databaseAccountName')]" + }, + "sqlDatabase": { + "type": "Microsoft.DocumentDB/databaseAccounts/sqlDatabases", + "apiVersion": "2025-04-15", + "name": "[format('{0}/{1}', parameters('databaseAccountName'), parameters('name'))]", + "tags": "[parameters('tags')]", + "properties": { + "resource": { + "id": "[parameters('name')]" }, - "availabilityZones": { - "type": "array", - "allowedValues": [ - 1, - 2, - 3 - ], - "nullable": true, - "metadata": { - "description": "Optional. The zones of the public IP address." - } + "options": "[if(contains(reference('databaseAccount').capabilities, createObject('name', 'EnableServerless')), null(), createObject('throughput', if(equals(parameters('autoscaleSettingsMaxThroughput'), null()), parameters('throughput'), null()), 'autoscaleSettings', if(not(equals(parameters('autoscaleSettingsMaxThroughput'), null())), createObject('maxThroughput', parameters('autoscaleSettingsMaxThroughput')), null())))]" + }, + "dependsOn": [ + "databaseAccount" + ] + }, + "container": { + "copy": { + "name": "container", + "count": "[length(coalesce(parameters('containers'), createArray()))]" + }, + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "[format('{0}-sqldb-{1}', uniqueString(deployment().name, parameters('name')), coalesce(parameters('containers'), createArray())[copyIndex()].name)]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" }, - "ipTags": { - "type": "array", - "items": { - "$ref": "#/definitions/ipTagType" + "mode": "Incremental", + "parameters": { + "databaseAccountName": { + "value": "[parameters('databaseAccountName')]" }, - "nullable": true, - "metadata": { - "description": "Optional. The list of tags associated with the public IP address." + "sqlDatabaseName": { + "value": "[parameters('name')]" + }, + "name": { + "value": "[coalesce(parameters('containers'), createArray())[copyIndex()].name]" + }, + "analyticalStorageTtl": { + "value": "[tryGet(coalesce(parameters('containers'), createArray())[copyIndex()], 'analyticalStorageTtl')]" + }, + "autoscaleSettingsMaxThroughput": { + "value": "[tryGet(coalesce(parameters('containers'), createArray())[copyIndex()], 'autoscaleSettingsMaxThroughput')]" + }, + "conflictResolutionPolicy": { + "value": "[tryGet(coalesce(parameters('containers'), createArray())[copyIndex()], 'conflictResolutionPolicy')]" + }, + "defaultTtl": { + "value": "[tryGet(coalesce(parameters('containers'), createArray())[copyIndex()], 'defaultTtl')]" + }, + "indexingPolicy": { + "value": "[tryGet(coalesce(parameters('containers'), createArray())[copyIndex()], 'indexingPolicy')]" + }, + "kind": { + "value": "[tryGet(coalesce(parameters('containers'), createArray())[copyIndex()], 'kind')]" + }, + "version": { + "value": "[tryGet(coalesce(parameters('containers'), createArray())[copyIndex()], 'version')]" + }, + "paths": { + "value": "[tryGet(coalesce(parameters('containers'), createArray())[copyIndex()], 'paths')]" + }, + "throughput": "[if(and(or(not(equals(parameters('throughput'), null())), not(equals(parameters('autoscaleSettingsMaxThroughput'), null()))), equals(tryGet(coalesce(parameters('containers'), createArray())[copyIndex()], 'throughput'), null())), createObject('value', -1), createObject('value', tryGet(coalesce(parameters('containers'), createArray())[copyIndex()], 'throughput')))]", + "uniqueKeyPolicyKeys": { + "value": "[tryGet(coalesce(parameters('containers'), createArray())[copyIndex()], 'uniqueKeyPolicyKeys')]" } }, - "enableTelemetry": { - "type": "bool", - "nullable": true, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "languageVersion": "2.0", + "contentVersion": "1.0.0.0", "metadata": { - "description": "Optional. Enable/Disable usage telemetry for the module." + "_generator": { + "name": "bicep", + "version": "0.38.33.27573", + "templateHash": "1005439058963058082" + }, + "name": "DocumentDB Database Account SQL Database Containers", + "description": "This module deploys a SQL Database Container in a CosmosDB Account." + }, + "parameters": { + "databaseAccountName": { + "type": "string", + "metadata": { + "description": "Conditional. The name of the parent Database Account. Required if the template is used in a standalone deployment." + } + }, + "sqlDatabaseName": { + "type": "string", + "metadata": { + "description": "Conditional. The name of the parent SQL Database. Required if the template is used in a standalone deployment." + } + }, + "name": { + "type": "string", + "metadata": { + "description": "Required. Name of the container." + } + }, + "analyticalStorageTtl": { + "type": "int", + "defaultValue": 0, + "metadata": { + "description": "Optional. Default to 0. Indicates how long data should be retained in the analytical store, for a container. Analytical store is enabled when ATTL is set with a value other than 0. If the value is set to -1, the analytical store retains all historical data, irrespective of the retention of the data in the transactional store." + } + }, + "conflictResolutionPolicy": { + "type": "object", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers@2025-04-15#properties/properties/properties/resource/properties/conflictResolutionPolicy" + }, + "description": "Optional. The conflict resolution policy for the container. Conflicts and conflict resolution policies are applicable if the Azure Cosmos DB account is configured with multiple write regions." + }, + "nullable": true + }, + "defaultTtl": { + "type": "int", + "nullable": true, + "minValue": -1, + "maxValue": 2147483647, + "metadata": { + "description": "Optional. Default to -1. Default time to live (in seconds). With Time to Live or TTL, Azure Cosmos DB provides the ability to delete items automatically from a container after a certain time period. If the value is set to \"-1\", it is equal to infinity, and items don't expire by default." + } + }, + "throughput": { + "type": "int", + "defaultValue": 400, + "metadata": { + "description": "Optional. Default to 400. Request Units per second. Will be ignored if autoscaleSettingsMaxThroughput is used. For best performance for large production workloads, it is recommended to set dedicated throughput (autoscale or manual) at the container level and not at the database level." + } + }, + "autoscaleSettingsMaxThroughput": { + "type": "int", + "nullable": true, + "maxValue": 1000000, + "metadata": { + "description": "Optional. Specifies the Autoscale settings and represents maximum throughput, the resource can scale up to. The autoscale throughput should have valid throughput values between 1000 and 1000000 inclusive in increments of 1000. If value is set to null, then autoscale will be disabled. For best performance for large production workloads, it is recommended to set dedicated throughput (autoscale or manual) at the container level and not at the database level." + } + }, + "tags": { + "type": "object", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers@2025-04-15#properties/tags" + }, + "description": "Optional. Tags of the SQL Database resource." + }, + "nullable": true + }, + "paths": { + "type": "array", + "items": { + "type": "string" + }, + "minLength": 1, + "maxLength": 3, + "metadata": { + "description": "Required. List of paths using which data within the container can be partitioned. For kind=MultiHash it can be up to 3. For anything else it needs to be exactly 1." + } + }, + "indexingPolicy": { + "type": "object", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers@2025-04-15#properties/properties/properties/resource/properties/indexingPolicy" + }, + "description": "Optional. Indexing policy of the container." + }, + "nullable": true + }, + "uniqueKeyPolicyKeys": { + "type": "array", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers@2025-04-15#properties/properties/properties/resource/properties/uniqueKeyPolicy/properties/uniqueKeys" + }, + "description": "Optional. The unique key policy configuration containing a list of unique keys that enforces uniqueness constraint on documents in the collection in the Azure Cosmos DB service." + }, + "nullable": true + }, + "kind": { + "type": "string", + "defaultValue": "Hash", + "allowedValues": [ + "Hash", + "MultiHash" + ], + "metadata": { + "description": "Optional. Default to Hash. Indicates the kind of algorithm used for partitioning." + } + }, + "version": { + "type": "int", + "defaultValue": 1, + "allowedValues": [ + 1, + 2 + ], + "metadata": { + "description": "Optional. Default to 1 for Hash and 2 for MultiHash - 1 is not allowed for MultiHash. Version of the partition key definition." + } + } + }, + "variables": { + "copy": [ + { + "name": "partitionKeyPaths", + "count": "[length(parameters('paths'))]", + "input": "[if(startsWith(parameters('paths')[copyIndex('partitionKeyPaths')], '/'), parameters('paths')[copyIndex('partitionKeyPaths')], format('/{0}', parameters('paths')[copyIndex('partitionKeyPaths')]))]" + } + ] + }, + "resources": { + "databaseAccount::sqlDatabase": { + "existing": true, + "type": "Microsoft.DocumentDB/databaseAccounts/sqlDatabases", + "apiVersion": "2025-04-15", + "name": "[format('{0}/{1}', parameters('databaseAccountName'), parameters('sqlDatabaseName'))]" + }, + "databaseAccount": { + "existing": true, + "type": "Microsoft.DocumentDB/databaseAccounts", + "apiVersion": "2025-04-15", + "name": "[parameters('databaseAccountName')]" + }, + "container": { + "type": "Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers", + "apiVersion": "2025-04-15", + "name": "[format('{0}/{1}/{2}', parameters('databaseAccountName'), parameters('sqlDatabaseName'), parameters('name'))]", + "tags": "[parameters('tags')]", + "properties": { + "resource": "[shallowMerge(createArray(createObject('conflictResolutionPolicy', parameters('conflictResolutionPolicy'), 'id', parameters('name'), 'indexingPolicy', parameters('indexingPolicy'), 'partitionKey', createObject('paths', variables('partitionKeyPaths'), 'kind', parameters('kind'), 'version', if(equals(parameters('kind'), 'MultiHash'), 2, parameters('version'))), 'uniqueKeyPolicy', if(not(empty(parameters('uniqueKeyPolicyKeys'))), createObject('uniqueKeys', parameters('uniqueKeyPolicyKeys')), null())), if(not(equals(parameters('analyticalStorageTtl'), 0)), createObject('analyticalStorageTtl', parameters('analyticalStorageTtl')), createObject()), if(not(equals(parameters('defaultTtl'), null())), createObject('defaultTtl', parameters('defaultTtl')), createObject())))]", + "options": "[if(contains(reference('databaseAccount').capabilities, createObject('name', 'EnableServerless')), null(), createObject('throughput', if(and(equals(parameters('autoscaleSettingsMaxThroughput'), null()), not(equals(parameters('throughput'), -1))), parameters('throughput'), null()), 'autoscaleSettings', if(not(equals(parameters('autoscaleSettingsMaxThroughput'), null())), createObject('maxThroughput', parameters('autoscaleSettingsMaxThroughput')), null())))]" + }, + "dependsOn": [ + "databaseAccount" + ] + } + }, + "outputs": { + "name": { + "type": "string", + "metadata": { + "description": "The name of the container." + }, + "value": "[parameters('name')]" + }, + "resourceId": { + "type": "string", + "metadata": { + "description": "The resource ID of the container." + }, + "value": "[resourceId('Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers', parameters('databaseAccountName'), parameters('sqlDatabaseName'), parameters('name'))]" + }, + "resourceGroupName": { + "type": "string", + "metadata": { + "description": "The name of the resource group the container was created in." + }, + "value": "[resourceGroup().name]" + } } } }, + "dependsOn": [ + "sqlDatabase" + ] + } + }, + "outputs": { + "name": { + "type": "string", "metadata": { - "__bicep_export!": true, - "description": "The type for the public IP address configuration." - } + "description": "The name of the SQL database." + }, + "value": "[parameters('name')]" }, - "ipConfigurationType": { + "resourceId": { + "type": "string", + "metadata": { + "description": "The resource ID of the SQL database." + }, + "value": "[resourceId('Microsoft.DocumentDB/databaseAccounts/sqlDatabases', parameters('databaseAccountName'), parameters('name'))]" + }, + "resourceGroupName": { + "type": "string", + "metadata": { + "description": "The name of the resource group the SQL database was created in." + }, + "value": "[resourceGroup().name]" + } + } + } + }, + "dependsOn": [ + "databaseAccount" + ] + }, + "databaseAccount_sqlRoleDefinitions": { + "copy": { + "name": "databaseAccount_sqlRoleDefinitions", + "count": "[length(coalesce(parameters('sqlRoleDefinitions'), createArray()))]" + }, + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "[format('{0}-sqlrd-{1}', uniqueString(deployment().name, parameters('location')), copyIndex())]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "databaseAccountName": { + "value": "[parameters('name')]" + }, + "name": { + "value": "[tryGet(coalesce(parameters('sqlRoleDefinitions'), createArray())[copyIndex()], 'name')]" + }, + "dataActions": { + "value": "[coalesce(parameters('sqlRoleDefinitions'), createArray())[copyIndex()].dataActions]" + }, + "roleName": { + "value": "[coalesce(parameters('sqlRoleDefinitions'), createArray())[copyIndex()].roleName]" + }, + "assignableScopes": { + "value": "[tryGet(coalesce(parameters('sqlRoleDefinitions'), createArray())[copyIndex()], 'assignableScopes')]" + }, + "sqlRoleAssignments": { + "value": "[tryGet(coalesce(parameters('sqlRoleDefinitions'), createArray())[copyIndex()], 'assignments')]" + }, + "enableTelemetry": { + "value": "[variables('enableReferencedModulesTelemetry')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "languageVersion": "2.0", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.38.33.27573", + "templateHash": "8600771348637416058" + }, + "name": "DocumentDB Database Account SQL Role Definitions.", + "description": "This module deploys a SQL Role Definision in a CosmosDB Account." + }, + "definitions": { + "sqlRoleAssignmentType": { "type": "object", "properties": { "name": { "type": "string", "nullable": true, "metadata": { - "description": "Optional. The name of the IP configuration." + "description": "Optional. Name unique identifier of the SQL Role Assignment." } }, - "privateIPAllocationMethod": { + "principalId": { "type": "string", - "allowedValues": [ - "Dynamic", - "Static" - ], - "nullable": true, "metadata": { - "description": "Optional. The private IP address allocation method." + "description": "Required. The unique identifier for the associated AAD principal in the AAD graph to which access is being granted through this Role Assignment. Tenant ID for the principal is inferred using the tenant associated with the subscription." } }, - "privateIPAddress": { + "scope": { "type": "string", "nullable": true, "metadata": { - "description": "Optional. The private IP address." + "description": "Optional. The data plane resource id for which access is being granted through this Role Assignment. Defaults to the root of the database account, but can also be scoped to e.g., the container and database level." } - }, - "subnetResourceId": { - "type": "string", - "metadata": { - "description": "Required. The resource ID of the subnet." + } + }, + "metadata": { + "__bicep_export!": true, + "description": "The type for the SQL Role Assignments." + } + } + }, + "parameters": { + "databaseAccountName": { + "type": "string", + "metadata": { + "description": "Conditional. The name of the parent Database Account. Required if the template is used in a standalone deployment." + } + }, + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The unique identifier of the Role Definition." + } + }, + "roleName": { + "type": "string", + "metadata": { + "description": "Required. A user-friendly name for the Role Definition. Must be unique for the database account." + } + }, + "dataActions": { + "type": "array", + "items": { + "type": "string" + }, + "minLength": 1, + "metadata": { + "description": "Required. An array of data actions that are allowed." + } + }, + "assignableScopes": { + "type": "array", + "items": { + "type": "string" + }, + "nullable": true, + "metadata": { + "description": "Optional. A set of fully qualified Scopes at or below which Role Assignments may be created using this Role Definition. This will allow application of this Role Definition on the entire database account or any underlying Database / Collection. Must have at least one element. Scopes higher than Database account are not enforceable as assignable Scopes. Note that resources referenced in assignable Scopes need not exist. Defaults to the current account." + } + }, + "sqlRoleAssignments": { + "type": "array", + "items": { + "$ref": "#/definitions/sqlRoleAssignmentType" + }, + "nullable": true, + "metadata": { + "description": "Optional. An array of SQL Role Assignments to be created for the SQL Role Definition." + } + }, + "enableTelemetry": { + "type": "bool", + "defaultValue": true, + "metadata": { + "description": "Optional. Enable/Disable usage telemetry for module." + } + } + }, + "variables": { + "enableReferencedModulesTelemetry": false + }, + "resources": { + "avmTelemetry": { + "condition": "[parameters('enableTelemetry')]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2024-03-01", + "name": "[format('46d3xbcp.res.doctdb-dbacct-sqlroledefinition.{0}.{1}', replace('-..--..-', '.', '-'), substring(uniqueString(deployment().name), 0, 4))]", + "properties": { + "mode": "Incremental", + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "resources": [], + "outputs": { + "telemetry": { + "type": "String", + "value": "For more information, see https://aka.ms/avm/TelemetryInfo" + } } - }, - "loadBalancerBackendAddressPools": { - "type": "array", - "items": { - "$ref": "#/definitions/backendAddressPoolType" - }, - "nullable": true, - "metadata": { - "description": "Optional. The load balancer backend address pools." + } + } + }, + "databaseAccount": { + "existing": true, + "type": "Microsoft.DocumentDB/databaseAccounts", + "apiVersion": "2024-11-15", + "name": "[parameters('databaseAccountName')]" + }, + "sqlRoleDefinition": { + "type": "Microsoft.DocumentDB/databaseAccounts/sqlRoleDefinitions", + "apiVersion": "2024-11-15", + "name": "[format('{0}/{1}', parameters('databaseAccountName'), coalesce(parameters('name'), guid(resourceId('Microsoft.DocumentDB/databaseAccounts', parameters('databaseAccountName')), parameters('databaseAccountName'), parameters('roleName'))))]", + "properties": { + "assignableScopes": "[coalesce(parameters('assignableScopes'), createArray(resourceId('Microsoft.DocumentDB/databaseAccounts', parameters('databaseAccountName'))))]", + "permissions": [ + { + "dataActions": "[parameters('dataActions')]" } + ], + "roleName": "[parameters('roleName')]", + "type": "CustomRole" + } + }, + "databaseAccount_sqlRoleAssignments": { + "copy": { + "name": "databaseAccount_sqlRoleAssignments", + "count": "[length(coalesce(parameters('sqlRoleAssignments'), createArray()))]" + }, + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "[format('{0}-sqlra-{1}', uniqueString(deployment().name), copyIndex())]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" }, - "applicationSecurityGroups": { - "type": "array", - "items": { - "$ref": "#/definitions/applicationSecurityGroupType" + "mode": "Incremental", + "parameters": { + "databaseAccountName": { + "value": "[parameters('databaseAccountName')]" }, - "nullable": true, - "metadata": { - "description": "Optional. The application security groups." - } - }, - "applicationGatewayBackendAddressPools": { - "type": "array", - "items": { - "$ref": "#/definitions/applicationGatewayBackendAddressPoolsType" + "roleDefinitionIdOrName": { + "value": "[resourceId('Microsoft.DocumentDB/databaseAccounts/sqlRoleDefinitions', parameters('databaseAccountName'), coalesce(parameters('name'), guid(resourceId('Microsoft.DocumentDB/databaseAccounts', parameters('databaseAccountName')), parameters('databaseAccountName'), parameters('roleName'))))]" }, - "nullable": true, - "metadata": { - "description": "Optional. The application gateway backend address pools." - } - }, - "gatewayLoadBalancer": { - "$ref": "#/definitions/subResourceType", - "nullable": true, - "metadata": { - "description": "Optional. The gateway load balancer settings." - } - }, - "loadBalancerInboundNatRules": { - "type": "array", - "items": { - "$ref": "#/definitions/inboundNatRuleType" + "principalId": { + "value": "[coalesce(parameters('sqlRoleAssignments'), createArray())[copyIndex()].principalId]" }, - "nullable": true, - "metadata": { - "description": "Optional. The load balancer inbound NAT rules." - } - }, - "privateIPAddressVersion": { - "type": "string", - "allowedValues": [ - "IPv4", - "IPv6" - ], - "nullable": true, - "metadata": { - "description": "Optional. The private IP address version." - } - }, - "virtualNetworkTaps": { - "type": "array", - "items": { - "$ref": "#/definitions/virtualNetworkTapType" + "name": { + "value": "[tryGet(coalesce(parameters('sqlRoleAssignments'), createArray())[copyIndex()], 'name')]" }, - "nullable": true, - "metadata": { - "description": "Optional. The virtual network taps." + "enableTelemetry": { + "value": "[variables('enableReferencedModulesTelemetry')]" } }, - "pipConfiguration": { - "$ref": "#/definitions/publicIPConfigurationType", - "nullable": true, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "languageVersion": "2.0", + "contentVersion": "1.0.0.0", "metadata": { - "description": "Optional. The public IP address configuration." - } - }, - "diagnosticSettings": { - "type": "array", - "items": { - "$ref": "#/definitions/diagnosticSettingFullType" + "_generator": { + "name": "bicep", + "version": "0.38.33.27573", + "templateHash": "17007224102611744259" + }, + "name": "DocumentDB Database Account SQL Role Assignments.", + "description": "This module deploys a SQL Role Assignment in a CosmosDB Account." }, - "nullable": true, - "metadata": { - "description": "Optional. The diagnostic settings of the IP configuration." - } - }, - "tags": { - "type": "object", - "metadata": { - "__bicep_resource_derived_type!": { - "source": "Microsoft.Network/networkInterfaces@2024-07-01#properties/tags" + "parameters": { + "databaseAccountName": { + "type": "string", + "metadata": { + "description": "Conditional. The name of the parent Database Account. Required if the template is used in a standalone deployment." + } }, - "description": "Optional. The tags of the public IP address." + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Name unique identifier of the SQL Role Assignment." + } + }, + "principalId": { + "type": "string", + "metadata": { + "description": "Required. The unique identifier for the associated AAD principal in the AAD graph to which access is being granted through this Role Assignment. Tenant ID for the principal is inferred using the tenant associated with the subscription." + } + }, + "roleDefinitionIdOrName": { + "type": "string", + "metadata": { + "description": "Required. The unique identifier of the associated SQL Role Definition." + } + }, + "enableTelemetry": { + "type": "bool", + "defaultValue": true, + "metadata": { + "description": "Optional. Enable/Disable usage telemetry for module." + } + }, + "scope": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The data plane resource id for which access is being granted through this Role Assignment. Defaults to the root of the database account, but can also be scoped to e.g., the container and database level." + } + } }, - "nullable": true - }, - "enableTelemetry": { - "type": "bool", - "nullable": true, - "metadata": { - "description": "Optional. Enable/Disable usage telemetry for the module." + "variables": { + "builtInDataPlaneRoleNames": { + "Cosmos DB Built-in Data Reader": "[format('{0}/sqlRoleDefinitions/00000000-0000-0000-0000-000000000001', resourceId('Microsoft.DocumentDB/databaseAccounts', parameters('databaseAccountName')))]", + "Cosmos DB Built-in Data Contributor": "[format('{0}/sqlRoleDefinitions/00000000-0000-0000-0000-000000000002', resourceId('Microsoft.DocumentDB/databaseAccounts', parameters('databaseAccountName')))]" + }, + "formattedRoleDefinition": "[coalesce(tryGet(variables('builtInDataPlaneRoleNames'), parameters('roleDefinitionIdOrName')), if(contains(parameters('roleDefinitionIdOrName'), '/sqlRoleDefinitions/'), parameters('roleDefinitionIdOrName'), format('{0}/sqlRoleDefinitions/{1}', resourceId('Microsoft.DocumentDB/databaseAccounts', parameters('databaseAccountName')), parameters('roleDefinitionIdOrName'))))]", + "formattedScope": "[replace(replace(coalesce(parameters('scope'), resourceId('Microsoft.DocumentDB/databaseAccounts', parameters('databaseAccountName'))), '/sqlDatabases/', '/dbs/'), '/containers/', '/colls/')]" + }, + "resources": { + "avmTelemetry": { + "condition": "[parameters('enableTelemetry')]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2024-03-01", + "name": "[format('46d3xbcp.res.doctdb-dbacct-sqlroleassignment.{0}.{1}', replace('-..--..-', '.', '-'), substring(uniqueString(deployment().name), 0, 4))]", + "properties": { + "mode": "Incremental", + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "resources": [], + "outputs": { + "telemetry": { + "type": "String", + "value": "For more information, see https://aka.ms/avm/TelemetryInfo" + } + } + } + } + }, + "databaseAccount": { + "existing": true, + "type": "Microsoft.DocumentDB/databaseAccounts", + "apiVersion": "2024-11-15", + "name": "[parameters('databaseAccountName')]" + }, + "sqlRoleAssignment": { + "type": "Microsoft.DocumentDB/databaseAccounts/sqlRoleAssignments", + "apiVersion": "2024-11-15", + "name": "[format('{0}/{1}', parameters('databaseAccountName'), coalesce(parameters('name'), guid(variables('formattedRoleDefinition'), parameters('principalId'), variables('formattedScope'))))]", + "properties": { + "principalId": "[parameters('principalId')]", + "roleDefinitionId": "[variables('formattedRoleDefinition')]", + "scope": "[variables('formattedScope')]" + } + } + }, + "outputs": { + "name": { + "type": "string", + "metadata": { + "description": "The name of the SQL Role Assignment." + }, + "value": "[coalesce(parameters('name'), guid(variables('formattedRoleDefinition'), parameters('principalId'), variables('formattedScope')))]" + }, + "resourceId": { + "type": "string", + "metadata": { + "description": "The resource ID of the SQL Role Assignment." + }, + "value": "[resourceId('Microsoft.DocumentDB/databaseAccounts/sqlRoleAssignments', parameters('databaseAccountName'), coalesce(parameters('name'), guid(variables('formattedRoleDefinition'), parameters('principalId'), variables('formattedScope'))))]" + }, + "resourceGroupName": { + "type": "string", + "metadata": { + "description": "The name of the resource group the SQL Role Definition was created in." + }, + "value": "[resourceGroup().name]" + } } } }, + "dependsOn": [ + "sqlRoleDefinition" + ] + } + }, + "outputs": { + "name": { + "type": "string", + "metadata": { + "description": "The name of the SQL Role Definition." + }, + "value": "[coalesce(parameters('name'), guid(resourceId('Microsoft.DocumentDB/databaseAccounts', parameters('databaseAccountName')), parameters('databaseAccountName'), parameters('roleName')))]" + }, + "resourceId": { + "type": "string", + "metadata": { + "description": "The resource ID of the SQL Role Definition." + }, + "value": "[resourceId('Microsoft.DocumentDB/databaseAccounts/sqlRoleDefinitions', parameters('databaseAccountName'), coalesce(parameters('name'), guid(resourceId('Microsoft.DocumentDB/databaseAccounts', parameters('databaseAccountName')), parameters('databaseAccountName'), parameters('roleName'))))]" + }, + "resourceGroupName": { + "type": "string", + "metadata": { + "description": "The name of the resource group the SQL Role Definition was created in." + }, + "value": "[resourceGroup().name]" + }, + "roleName": { + "type": "string", + "metadata": { + "description": "The role name of the SQL Role Definition." + }, + "value": "[reference('sqlRoleDefinition').roleName]" + } + } + } + }, + "dependsOn": [ + "databaseAccount" + ] + }, + "databaseAccount_sqlRoleAssignments": { + "copy": { + "name": "databaseAccount_sqlRoleAssignments", + "count": "[length(coalesce(parameters('sqlRoleAssignments'), createArray()))]" + }, + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "[format('{0}-sqlra-{1}', uniqueString(deployment().name), copyIndex())]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "databaseAccountName": { + "value": "[parameters('name')]" + }, + "roleDefinitionIdOrName": { + "value": "[coalesce(parameters('sqlRoleAssignments'), createArray())[copyIndex()].roleDefinitionId]" + }, + "principalId": { + "value": "[coalesce(parameters('sqlRoleAssignments'), createArray())[copyIndex()].principalId]" + }, + "name": { + "value": "[tryGet(coalesce(parameters('sqlRoleAssignments'), createArray())[copyIndex()], 'name')]" + }, + "scope": { + "value": "[tryGet(coalesce(parameters('sqlRoleAssignments'), createArray())[copyIndex()], 'scope')]" + }, + "enableTelemetry": { + "value": "[variables('enableReferencedModulesTelemetry')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "languageVersion": "2.0", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.38.33.27573", + "templateHash": "17007224102611744259" + }, + "name": "DocumentDB Database Account SQL Role Assignments.", + "description": "This module deploys a SQL Role Assignment in a CosmosDB Account." + }, + "parameters": { + "databaseAccountName": { + "type": "string", "metadata": { - "__bicep_export!": true, - "description": "The type for the IP configuration." + "description": "Conditional. The name of the parent Database Account. Required if the template is used in a standalone deployment." } }, - "applicationGatewayBackendAddressPoolsType": { - "type": "object", - "properties": { - "id": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. Resource ID of the backend address pool." - } - }, - "name": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. Name of the backend address pool that is unique within an Application Gateway." - } - }, - "properties": { - "type": "object", - "properties": { - "backendAddresses": { - "type": "array", - "items": { - "type": "object", - "properties": { - "ipAddress": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. IP address of the backend address." - } - }, - "fqdn": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. FQDN of the backend address." - } - } - } - }, - "nullable": true, - "metadata": { - "description": "Optional. Backend addresses." - } - } - }, - "nullable": true, - "metadata": { - "description": "Optional. Properties of the application gateway backend address pool." - } - } - }, + "name": { + "type": "string", + "nullable": true, "metadata": { - "description": "The type for the application gateway backend address pool.", - "__bicep_imported_from!": { - "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/res/network/network-interface:0.5.1" - } + "description": "Optional. Name unique identifier of the SQL Role Assignment." } }, - "applicationSecurityGroupType": { - "type": "object", - "properties": { - "id": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. Resource ID of the application security group." - } - }, - "location": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. Location of the application security group." - } - }, - "properties": { - "type": "object", - "nullable": true, - "metadata": { - "description": "Optional. Properties of the application security group." - } - }, - "tags": { - "type": "object", - "nullable": true, - "metadata": { - "description": "Optional. Tags of the application security group." - } - } - }, + "principalId": { + "type": "string", "metadata": { - "description": "The type for the application security group.", - "__bicep_imported_from!": { - "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/res/network/network-interface:0.5.1" - } + "description": "Required. The unique identifier for the associated AAD principal in the AAD graph to which access is being granted through this Role Assignment. Tenant ID for the principal is inferred using the tenant associated with the subscription." } }, - "backendAddressPoolType": { - "type": "object", - "properties": { - "id": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The resource ID of the backend address pool." - } - }, - "name": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The name of the backend address pool." - } - }, - "properties": { - "type": "object", - "nullable": true, - "metadata": { - "description": "Optional. The properties of the backend address pool." - } - } - }, + "roleDefinitionIdOrName": { + "type": "string", "metadata": { - "description": "The type for a backend address pool.", - "__bicep_imported_from!": { - "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/res/network/network-interface:0.5.1" - } + "description": "Required. The unique identifier of the associated SQL Role Definition." } }, - "ddosSettingsType": { - "type": "object", + "enableTelemetry": { + "type": "bool", + "defaultValue": true, + "metadata": { + "description": "Optional. Enable/Disable usage telemetry for module." + } + }, + "scope": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The data plane resource id for which access is being granted through this Role Assignment. Defaults to the root of the database account, but can also be scoped to e.g., the container and database level." + } + } + }, + "variables": { + "builtInDataPlaneRoleNames": { + "Cosmos DB Built-in Data Reader": "[format('{0}/sqlRoleDefinitions/00000000-0000-0000-0000-000000000001', resourceId('Microsoft.DocumentDB/databaseAccounts', parameters('databaseAccountName')))]", + "Cosmos DB Built-in Data Contributor": "[format('{0}/sqlRoleDefinitions/00000000-0000-0000-0000-000000000002', resourceId('Microsoft.DocumentDB/databaseAccounts', parameters('databaseAccountName')))]" + }, + "formattedRoleDefinition": "[coalesce(tryGet(variables('builtInDataPlaneRoleNames'), parameters('roleDefinitionIdOrName')), if(contains(parameters('roleDefinitionIdOrName'), '/sqlRoleDefinitions/'), parameters('roleDefinitionIdOrName'), format('{0}/sqlRoleDefinitions/{1}', resourceId('Microsoft.DocumentDB/databaseAccounts', parameters('databaseAccountName')), parameters('roleDefinitionIdOrName'))))]", + "formattedScope": "[replace(replace(coalesce(parameters('scope'), resourceId('Microsoft.DocumentDB/databaseAccounts', parameters('databaseAccountName'))), '/sqlDatabases/', '/dbs/'), '/containers/', '/colls/')]" + }, + "resources": { + "avmTelemetry": { + "condition": "[parameters('enableTelemetry')]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2024-03-01", + "name": "[format('46d3xbcp.res.doctdb-dbacct-sqlroleassignment.{0}.{1}', replace('-..--..-', '.', '-'), substring(uniqueString(deployment().name), 0, 4))]", "properties": { - "ddosProtectionPlan": { - "type": "object", - "properties": { - "id": { - "type": "string", - "metadata": { - "description": "Required. The resource ID of the DDOS protection plan associated with the public IP address." - } + "mode": "Incremental", + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "resources": [], + "outputs": { + "telemetry": { + "type": "String", + "value": "For more information, see https://aka.ms/avm/TelemetryInfo" } - }, - "nullable": true, - "metadata": { - "description": "Optional. The DDoS protection plan associated with the public IP address." - } - }, - "protectionMode": { - "type": "string", - "allowedValues": [ - "Enabled" - ], - "metadata": { - "description": "Required. The DDoS protection policy customizations." } } + } + }, + "databaseAccount": { + "existing": true, + "type": "Microsoft.DocumentDB/databaseAccounts", + "apiVersion": "2024-11-15", + "name": "[parameters('databaseAccountName')]" + }, + "sqlRoleAssignment": { + "type": "Microsoft.DocumentDB/databaseAccounts/sqlRoleAssignments", + "apiVersion": "2024-11-15", + "name": "[format('{0}/{1}', parameters('databaseAccountName'), coalesce(parameters('name'), guid(variables('formattedRoleDefinition'), parameters('principalId'), variables('formattedScope'))))]", + "properties": { + "principalId": "[parameters('principalId')]", + "roleDefinitionId": "[variables('formattedRoleDefinition')]", + "scope": "[variables('formattedScope')]" + } + } + }, + "outputs": { + "name": { + "type": "string", + "metadata": { + "description": "The name of the SQL Role Assignment." }, + "value": "[coalesce(parameters('name'), guid(variables('formattedRoleDefinition'), parameters('principalId'), variables('formattedScope')))]" + }, + "resourceId": { + "type": "string", "metadata": { - "__bicep_imported_from!": { - "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/res/network/public-ip-address:0.8.0" - } - } + "description": "The resource ID of the SQL Role Assignment." + }, + "value": "[resourceId('Microsoft.DocumentDB/databaseAccounts/sqlRoleAssignments', parameters('databaseAccountName'), coalesce(parameters('name'), guid(variables('formattedRoleDefinition'), parameters('principalId'), variables('formattedScope'))))]" }, - "diagnosticSettingFullType": { + "resourceGroupName": { + "type": "string", + "metadata": { + "description": "The name of the resource group the SQL Role Definition was created in." + }, + "value": "[resourceGroup().name]" + } + } + } + }, + "dependsOn": [ + "databaseAccount", + "databaseAccount_sqlDatabases", + "databaseAccount_sqlRoleDefinitions" + ] + }, + "databaseAccount_cassandraRoleDefinitions": { + "copy": { + "name": "databaseAccount_cassandraRoleDefinitions", + "count": "[length(coalesce(parameters('cassandraRoleDefinitions'), createArray()))]" + }, + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "[format('{0}-cassandra-rd-{1}', uniqueString(deployment().name, parameters('location')), copyIndex())]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "databaseAccountName": { + "value": "[parameters('name')]" + }, + "name": { + "value": "[tryGet(coalesce(parameters('cassandraRoleDefinitions'), createArray())[copyIndex()], 'name')]" + }, + "roleName": { + "value": "[coalesce(parameters('cassandraRoleDefinitions'), createArray())[copyIndex()].roleName]" + }, + "dataActions": { + "value": "[tryGet(coalesce(parameters('cassandraRoleDefinitions'), createArray())[copyIndex()], 'dataActions')]" + }, + "notDataActions": { + "value": "[tryGet(coalesce(parameters('cassandraRoleDefinitions'), createArray())[copyIndex()], 'notDataActions')]" + }, + "assignableScopes": { + "value": "[tryGet(coalesce(parameters('cassandraRoleDefinitions'), createArray())[copyIndex()], 'assignableScopes')]" + }, + "cassandraRoleAssignments": { + "value": "[tryGet(coalesce(parameters('cassandraRoleDefinitions'), createArray())[copyIndex()], 'assignments')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "languageVersion": "2.0", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.38.33.27573", + "templateHash": "17859939500809924517" + }, + "name": "DocumentDB Database Account Cassandra Role Definitions.", + "description": "This module deploys a Cassandra Role Definition in a CosmosDB Account." + }, + "definitions": { + "cassandraRoleAssignmentType": { "type": "object", "properties": { "name": { "type": "string", "nullable": true, "metadata": { - "description": "Optional. The name of the diagnostic setting." - } - }, - "logCategoriesAndGroups": { - "type": "array", - "items": { - "type": "object", - "properties": { - "category": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. Name of a Diagnostic Log category for a resource type this setting is applied to. Set the specific logs to collect here." - } - }, - "categoryGroup": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. Name of a Diagnostic Log category group for a resource type this setting is applied to. Set to `allLogs` to collect all logs." - } - }, - "enabled": { - "type": "bool", - "nullable": true, - "metadata": { - "description": "Optional. Enable or disable the category explicitly. Default is `true`." - } - } - } - }, - "nullable": true, - "metadata": { - "description": "Optional. The name of logs that will be streamed. \"allLogs\" includes all possible logs for the resource. Set to `[]` to disable log collection." - } - }, - "metricCategories": { - "type": "array", - "items": { - "type": "object", - "properties": { - "category": { - "type": "string", - "metadata": { - "description": "Required. Name of a Diagnostic Metric category for a resource type this setting is applied to. Set to `AllMetrics` to collect all metrics." - } - }, - "enabled": { - "type": "bool", - "nullable": true, - "metadata": { - "description": "Optional. Enable or disable the category explicitly. Default is `true`." - } - } - } - }, - "nullable": true, - "metadata": { - "description": "Optional. The name of metrics that will be streamed. \"allMetrics\" includes all possible metrics for the resource. Set to `[]` to disable metric collection." - } - }, - "logAnalyticsDestinationType": { - "type": "string", - "allowedValues": [ - "AzureDiagnostics", - "Dedicated" - ], - "nullable": true, - "metadata": { - "description": "Optional. A string indicating whether the export to Log Analytics should use the default destination type, i.e. AzureDiagnostics, or use a destination type." - } - }, - "workspaceResourceId": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. Resource ID of the diagnostic log analytics workspace. For security reasons, it is recommended to set diagnostic settings to send data to either storage account, log analytics workspace or event hub." - } - }, - "storageAccountResourceId": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. Resource ID of the diagnostic storage account. For security reasons, it is recommended to set diagnostic settings to send data to either storage account, log analytics workspace or event hub." - } - }, - "eventHubAuthorizationRuleResourceId": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. Resource ID of the diagnostic event hub authorization rule for the Event Hubs namespace in which the event hub should be created or streamed to." + "description": "Optional. The unique identifier of the role assignment." } }, - "eventHubName": { + "principalId": { "type": "string", - "nullable": true, "metadata": { - "description": "Optional. Name of the diagnostic event hub within the namespace to which logs are streamed. Without this, an event hub is created for each log category. For security reasons, it is recommended to set diagnostic settings to send data to either storage account, log analytics workspace or event hub." + "description": "Required. The unique identifier for the associated AAD principal." } }, - "marketplacePartnerResourceId": { + "scope": { "type": "string", "nullable": true, "metadata": { - "description": "Optional. The full ARM resource ID of the Marketplace resource to which you would like to send Diagnostic Logs." + "description": "Optional. The data plane resource path for which access is being granted. Defaults to the current account." } } }, "metadata": { - "description": "An AVM-aligned type for a diagnostic setting. To be used if both logs & metrics are supported by the resource provider.", - "__bicep_imported_from!": { - "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.5.1" - } + "__bicep_export!": true + } + } + }, + "parameters": { + "databaseAccountName": { + "type": "string", + "metadata": { + "description": "Conditional. The name of the parent Database Account. Required if the template is used in a standalone deployment." } }, - "dnsSettingsType": { - "type": "object", - "properties": { - "domainNameLabel": { - "type": "string", - "metadata": { - "description": "Required. The domain name label. The concatenation of the domain name label and the regionalized DNS zone make up the fully qualified domain name associated with the public IP address. If a domain name label is specified, an A DNS record is created for the public IP in the Microsoft Azure DNS system." - } - }, - "domainNameLabelScope": { - "type": "string", - "allowedValues": [ - "NoReuse", - "ResourceGroupReuse", - "SubscriptionReuse", - "TenantReuse" - ], - "nullable": true, - "metadata": { - "description": "Optional. The domain name label scope. If a domain name label and a domain name label scope are specified, an A DNS record is created for the public IP in the Microsoft Azure DNS system with a hashed value includes in FQDN." - } - }, - "fqdn": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The Fully Qualified Domain Name of the A DNS record associated with the public IP. This is the concatenation of the domainNameLabel and the regionalized DNS zone." - } - }, - "reverseFqdn": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The reverse FQDN. A user-visible, fully qualified domain name that resolves to this public IP address. If the reverseFqdn is specified, then a PTR DNS record is created pointing from the IP address in the in-addr.arpa domain to the reverse FQDN." - } - } + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The unique identifier of the Role Definition." + } + }, + "roleName": { + "type": "string", + "metadata": { + "description": "Required. A user-friendly name for the Role Definition. Must be unique for the database account." + } + }, + "dataActions": { + "type": "array", + "items": { + "type": "string" + }, + "defaultValue": [], + "metadata": { + "description": "Optional. An array of data actions that are allowed. Note: Valid data action strings for Cassandra API are currently undocumented (as of API version 2025-05-01-preview). Please refer to official Azure documentation once available." + } + }, + "notDataActions": { + "type": "array", + "items": { + "type": "string" }, + "defaultValue": [], "metadata": { - "__bicep_imported_from!": { - "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/res/network/public-ip-address:0.8.0" - } + "description": "Optional. An array of data actions that are denied. Note: Unlike SQL RBAC, Cassandra RBAC supports deny rules (notDataActions) for granular access control. Valid data action strings are currently undocumented (as of API version 2025-05-01-preview)." } }, - "inboundNatRuleType": { - "type": "object", + "assignableScopes": { + "type": "array", + "items": { + "type": "string" + }, + "nullable": true, + "metadata": { + "description": "Optional. A set of fully qualified Scopes at or below which Role Assignments may be created using this Role Definition. This will allow application of this Role Definition on the entire database account or any underlying Database / Keyspace. Must have at least one element. Scopes higher than Database account are not enforceable as assignable Scopes. Note that resources referenced in assignable Scopes need not exist. Defaults to the current account." + } + }, + "cassandraRoleAssignments": { + "type": "array", + "items": { + "$ref": "#/definitions/cassandraRoleAssignmentType" + }, + "nullable": true, + "metadata": { + "description": "Optional. An array of Cassandra Role Assignments to be created for the Cassandra Role Definition." + } + } + }, + "resources": { + "databaseAccount": { + "existing": true, + "type": "Microsoft.DocumentDB/databaseAccounts", + "apiVersion": "2024-11-15", + "name": "[parameters('databaseAccountName')]" + }, + "cassandraRoleDefinition": { + "type": "Microsoft.DocumentDB/databaseAccounts/cassandraRoleDefinitions", + "apiVersion": "2025-05-01-preview", + "name": "[format('{0}/{1}', parameters('databaseAccountName'), coalesce(parameters('name'), guid(resourceId('Microsoft.DocumentDB/databaseAccounts', parameters('databaseAccountName')), parameters('databaseAccountName'), parameters('roleName'))))]", "properties": { - "id": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. Resource ID of the inbound NAT rule." + "assignableScopes": "[coalesce(parameters('assignableScopes'), createArray(resourceId('Microsoft.DocumentDB/databaseAccounts', parameters('databaseAccountName'))))]", + "permissions": [ + { + "dataActions": "[parameters('dataActions')]", + "notDataActions": "[parameters('notDataActions')]" } + ], + "roleName": "[parameters('roleName')]", + "type": "CustomRole" + } + }, + "databaseAccount_cassandraRoleAssignments": { + "copy": { + "name": "databaseAccount_cassandraRoleAssignments", + "count": "[length(coalesce(parameters('cassandraRoleAssignments'), createArray()))]" + }, + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "[format('{0}-cassandra-ra-{1}', uniqueString(deployment().name), copyIndex())]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" }, - "name": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. Name of the resource that is unique within the set of inbound NAT rules used by the load balancer. This name can be used to access the resource." + "mode": "Incremental", + "parameters": { + "databaseAccountName": { + "value": "[parameters('databaseAccountName')]" + }, + "roleDefinitionId": { + "value": "[resourceId('Microsoft.DocumentDB/databaseAccounts/cassandraRoleDefinitions', parameters('databaseAccountName'), coalesce(parameters('name'), guid(resourceId('Microsoft.DocumentDB/databaseAccounts', parameters('databaseAccountName')), parameters('databaseAccountName'), parameters('roleName'))))]" + }, + "principalId": { + "value": "[coalesce(parameters('cassandraRoleAssignments'), createArray())[copyIndex()].principalId]" + }, + "name": { + "value": "[tryGet(coalesce(parameters('cassandraRoleAssignments'), createArray())[copyIndex()], 'name')]" + }, + "scope": { + "value": "[tryGet(coalesce(parameters('cassandraRoleAssignments'), createArray())[copyIndex()], 'scope')]" } }, - "properties": { - "type": "object", - "properties": { - "backendAddressPool": { - "$ref": "#/definitions/subResourceType", - "nullable": true, - "metadata": { - "description": "Optional. A reference to backendAddressPool resource." - } + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "languageVersion": "2.0", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.38.33.27573", + "templateHash": "552115240340341941" }, - "backendPort": { - "type": "int", - "nullable": true, + "name": "DocumentDB Database Account Cassandra Role Assignments.", + "description": "This module deploys a Cassandra Role Assignment in a CosmosDB Account." + }, + "parameters": { + "databaseAccountName": { + "type": "string", "metadata": { - "description": "Optional. The port used for the internal endpoint. Acceptable values range from 1 to 65535." + "description": "Conditional. The name of the parent Database Account. Required if the template is used in a standalone deployment." } }, - "enableFloatingIP": { - "type": "bool", + "name": { + "type": "string", "nullable": true, "metadata": { - "description": "Optional. Configures a virtual machine's endpoint for the floating IP capability required to configure a SQL AlwaysOn Availability Group. This setting is required when using the SQL AlwaysOn Availability Groups in SQL server. This setting can't be changed after you create the endpoint." + "description": "Optional. Name unique identifier of the Cassandra Role Assignment." } }, - "enableTcpReset": { - "type": "bool", - "nullable": true, + "principalId": { + "type": "string", "metadata": { - "description": "Optional. Receive bidirectional TCP Reset on TCP flow idle timeout or unexpected connection termination. This element is only used when the protocol is set to TCP." + "description": "Required. The unique identifier for the associated AAD principal in the AAD graph to which access is being granted through this Role Assignment. Tenant ID for the principal is inferred using the tenant associated with the subscription." } }, - "frontendIPConfiguration": { - "$ref": "#/definitions/subResourceType", - "nullable": true, + "roleDefinitionId": { + "type": "string", "metadata": { - "description": "Optional. A reference to frontend IP addresses." + "description": "Required. The unique identifier of the associated Cassandra Role Definition." } }, - "frontendPort": { - "type": "int", + "scope": { + "type": "string", "nullable": true, "metadata": { - "description": "Optional. The port for the external endpoint. Port numbers for each rule must be unique within the Load Balancer. Acceptable values range from 1 to 65534." + "description": "Optional. The data plane resource path for which access is being granted through this Cassandra Role Assignment. Defaults to the current account." } + } + }, + "resources": { + "databaseAccount": { + "existing": true, + "type": "Microsoft.DocumentDB/databaseAccounts", + "apiVersion": "2024-11-15", + "name": "[parameters('databaseAccountName')]" }, - "frontendPortRangeStart": { - "type": "int", - "nullable": true, - "metadata": { - "description": "Optional. The port range start for the external endpoint. This property is used together with BackendAddressPool and FrontendPortRangeEnd. Individual inbound NAT rule port mappings will be created for each backend address from BackendAddressPool. Acceptable values range from 1 to 65534." + "cassandraRoleAssignment": { + "type": "Microsoft.DocumentDB/databaseAccounts/cassandraRoleAssignments", + "apiVersion": "2025-05-01-preview", + "name": "[format('{0}/{1}', parameters('databaseAccountName'), coalesce(parameters('name'), guid(parameters('roleDefinitionId'), parameters('principalId'), resourceId('Microsoft.DocumentDB/databaseAccounts', parameters('databaseAccountName')))))]", + "properties": { + "principalId": "[parameters('principalId')]", + "roleDefinitionId": "[parameters('roleDefinitionId')]", + "scope": "[coalesce(parameters('scope'), resourceId('Microsoft.DocumentDB/databaseAccounts', parameters('databaseAccountName')))]" } + } + }, + "outputs": { + "name": { + "type": "string", + "metadata": { + "description": "The name of the Cassandra Role Assignment." + }, + "value": "[coalesce(parameters('name'), guid(parameters('roleDefinitionId'), parameters('principalId'), resourceId('Microsoft.DocumentDB/databaseAccounts', parameters('databaseAccountName'))))]" }, - "frontendPortRangeEnd": { - "type": "int", - "nullable": true, + "resourceId": { + "type": "string", "metadata": { - "description": "Optional. The port range end for the external endpoint. This property is used together with BackendAddressPool and FrontendPortRangeStart. Individual inbound NAT rule port mappings will be created for each backend address from BackendAddressPool. Acceptable values range from 1 to 65534." - } + "description": "The resource ID of the Cassandra Role Assignment." + }, + "value": "[resourceId('Microsoft.DocumentDB/databaseAccounts/cassandraRoleAssignments', parameters('databaseAccountName'), coalesce(parameters('name'), guid(parameters('roleDefinitionId'), parameters('principalId'), resourceId('Microsoft.DocumentDB/databaseAccounts', parameters('databaseAccountName')))))]" }, - "protocol": { + "resourceGroupName": { "type": "string", - "allowedValues": [ - "All", - "Tcp", - "Udp" - ], - "nullable": true, "metadata": { - "description": "Optional. The reference to the transport protocol used by the load balancing rule." - } + "description": "The name of the resource group the Cassandra Role Assignment was created in." + }, + "value": "[resourceGroup().name]" } - }, - "nullable": true, - "metadata": { - "description": "Optional. Properties of the inbound NAT rule." } } }, + "dependsOn": [ + "cassandraRoleDefinition" + ] + } + }, + "outputs": { + "name": { + "type": "string", "metadata": { - "description": "The type for the inbound NAT rule.", - "__bicep_imported_from!": { - "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/res/network/network-interface:0.5.1" - } - } + "description": "The name of the cassandra role definition." + }, + "value": "[coalesce(parameters('name'), guid(resourceId('Microsoft.DocumentDB/databaseAccounts', parameters('databaseAccountName')), parameters('databaseAccountName'), parameters('roleName')))]" }, - "ipTagType": { - "type": "object", - "properties": { - "ipTagType": { - "type": "string", - "metadata": { - "description": "Required. The IP tag type." - } - }, - "tag": { - "type": "string", - "metadata": { - "description": "Required. The IP tag." - } - } + "resourceId": { + "type": "string", + "metadata": { + "description": "The resource ID of the cassandra role definition." }, + "value": "[resourceId('Microsoft.DocumentDB/databaseAccounts/cassandraRoleDefinitions', parameters('databaseAccountName'), coalesce(parameters('name'), guid(resourceId('Microsoft.DocumentDB/databaseAccounts', parameters('databaseAccountName')), parameters('databaseAccountName'), parameters('roleName'))))]" + }, + "resourceGroupName": { + "type": "string", "metadata": { - "__bicep_imported_from!": { - "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/res/network/public-ip-address:0.8.0" - } + "description": "The name of the resource group the cassandra role definition was created in." + }, + "value": "[resourceGroup().name]" + } + } + } + }, + "dependsOn": [ + "databaseAccount" + ] + }, + "databaseAccount_cassandraRoleAssignments": { + "copy": { + "name": "databaseAccount_cassandraRoleAssignments", + "count": "[length(coalesce(parameters('cassandraRoleAssignments'), createArray()))]" + }, + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "[format('{0}-cassandra-ra-{1}', uniqueString(deployment().name), copyIndex())]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "databaseAccountName": { + "value": "[parameters('name')]" + }, + "roleDefinitionId": { + "value": "[coalesce(parameters('cassandraRoleAssignments'), createArray())[copyIndex()].roleDefinitionId]" + }, + "principalId": { + "value": "[coalesce(parameters('cassandraRoleAssignments'), createArray())[copyIndex()].principalId]" + }, + "name": { + "value": "[tryGet(coalesce(parameters('cassandraRoleAssignments'), createArray())[copyIndex()], 'name')]" + }, + "scope": { + "value": "[tryGet(coalesce(parameters('cassandraRoleAssignments'), createArray())[copyIndex()], 'scope')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "languageVersion": "2.0", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.38.33.27573", + "templateHash": "552115240340341941" + }, + "name": "DocumentDB Database Account Cassandra Role Assignments.", + "description": "This module deploys a Cassandra Role Assignment in a CosmosDB Account." + }, + "parameters": { + "databaseAccountName": { + "type": "string", + "metadata": { + "description": "Conditional. The name of the parent Database Account. Required if the template is used in a standalone deployment." } }, - "lockType": { - "type": "object", - "properties": { - "name": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. Specify the name of lock." - } - }, - "kind": { - "type": "string", - "allowedValues": [ - "CanNotDelete", - "None", - "ReadOnly" - ], - "nullable": true, - "metadata": { - "description": "Optional. Specify the type of lock." - } - }, - "notes": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. Specify the notes of the lock." - } - } - }, + "name": { + "type": "string", + "nullable": true, "metadata": { - "description": "An AVM-aligned type for a lock.", - "__bicep_imported_from!": { - "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.0" - } + "description": "Optional. Name unique identifier of the Cassandra Role Assignment." } }, - "networkInterfaceIPConfigurationOutputType": { - "type": "object", - "properties": { - "name": { - "type": "string", - "metadata": { - "description": "The name of the IP configuration." - } - }, - "privateIP": { - "type": "string", - "nullable": true, - "metadata": { - "description": "The private IP address." - } - }, - "publicIP": { - "type": "string", - "nullable": true, - "metadata": { - "description": "The public IP address." - } - } - }, + "principalId": { + "type": "string", "metadata": { - "__bicep_imported_from!": { - "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/res/network/network-interface:0.5.1" - } + "description": "Required. The unique identifier for the associated AAD principal in the AAD graph to which access is being granted through this Role Assignment. Tenant ID for the principal is inferred using the tenant associated with the subscription." } }, - "roleAssignmentType": { - "type": "object", - "properties": { - "name": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The name (as GUID) of the role assignment. If not provided, a GUID will be generated." - } - }, - "roleDefinitionIdOrName": { - "type": "string", - "metadata": { - "description": "Required. The role to assign. You can provide either the display name of the role definition, the role definition GUID, or its fully qualified ID in the following format: '/providers/Microsoft.Authorization/roleDefinitions/c2f4ef07-c644-48eb-af81-4b1b4947fb11'." - } - }, - "principalId": { - "type": "string", - "metadata": { - "description": "Required. The principal ID of the principal (user/group/identity) to assign the role to." - } - }, - "principalType": { - "type": "string", - "allowedValues": [ - "Device", - "ForeignGroup", - "Group", - "ServicePrincipal", - "User" - ], - "nullable": true, - "metadata": { - "description": "Optional. The principal type of the assigned principal ID." - } - }, - "description": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The description of the role assignment." - } - }, - "condition": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The conditions on the role assignment. This limits the resources it can be assigned to. e.g.: @Resource[Microsoft.Storage/storageAccounts/blobServices/containers:ContainerName] StringEqualsIgnoreCase \"foo_storage_container\"." - } - }, - "conditionVersion": { - "type": "string", - "allowedValues": [ - "2.0" - ], - "nullable": true, - "metadata": { - "description": "Optional. Version of the condition." - } - }, - "delegatedManagedIdentityResourceId": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The Resource Id of the delegated managed identity resource." - } - } - }, + "roleDefinitionId": { + "type": "string", "metadata": { - "description": "An AVM-aligned type for a role assignment.", - "__bicep_imported_from!": { - "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.5.1" - } + "description": "Required. The unique identifier of the associated Cassandra Role Definition." } }, - "subResourceType": { - "type": "object", + "scope": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The data plane resource path for which access is being granted through this Cassandra Role Assignment. Defaults to the current account." + } + } + }, + "resources": { + "databaseAccount": { + "existing": true, + "type": "Microsoft.DocumentDB/databaseAccounts", + "apiVersion": "2024-11-15", + "name": "[parameters('databaseAccountName')]" + }, + "cassandraRoleAssignment": { + "type": "Microsoft.DocumentDB/databaseAccounts/cassandraRoleAssignments", + "apiVersion": "2025-05-01-preview", + "name": "[format('{0}/{1}', parameters('databaseAccountName'), coalesce(parameters('name'), guid(parameters('roleDefinitionId'), parameters('principalId'), resourceId('Microsoft.DocumentDB/databaseAccounts', parameters('databaseAccountName')))))]", "properties": { - "id": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. Resource ID of the sub resource." - } - } + "principalId": "[parameters('principalId')]", + "roleDefinitionId": "[parameters('roleDefinitionId')]", + "scope": "[coalesce(parameters('scope'), resourceId('Microsoft.DocumentDB/databaseAccounts', parameters('databaseAccountName')))]" + } + } + }, + "outputs": { + "name": { + "type": "string", + "metadata": { + "description": "The name of the Cassandra Role Assignment." }, + "value": "[coalesce(parameters('name'), guid(parameters('roleDefinitionId'), parameters('principalId'), resourceId('Microsoft.DocumentDB/databaseAccounts', parameters('databaseAccountName'))))]" + }, + "resourceId": { + "type": "string", "metadata": { - "description": "The type for the sub resource.", - "__bicep_imported_from!": { - "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/res/network/network-interface:0.5.1" - } - } + "description": "The resource ID of the Cassandra Role Assignment." + }, + "value": "[resourceId('Microsoft.DocumentDB/databaseAccounts/cassandraRoleAssignments', parameters('databaseAccountName'), coalesce(parameters('name'), guid(parameters('roleDefinitionId'), parameters('principalId'), resourceId('Microsoft.DocumentDB/databaseAccounts', parameters('databaseAccountName')))))]" + }, + "resourceGroupName": { + "type": "string", + "metadata": { + "description": "The name of the resource group the Cassandra Role Assignment was created in." + }, + "value": "[resourceGroup().name]" + } + } + } + }, + "dependsOn": [ + "databaseAccount", + "databaseAccount_cassandraKeyspaces", + "databaseAccount_cassandraRoleDefinitions" + ] + }, + "databaseAccount_mongodbDatabases": { + "copy": { + "name": "databaseAccount_mongodbDatabases", + "count": "[length(coalesce(parameters('mongodbDatabases'), createArray()))]" + }, + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "[format('{0}-mongodb-{1}', uniqueString(deployment().name, parameters('location')), coalesce(parameters('mongodbDatabases'), createArray())[copyIndex()].name)]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "databaseAccountName": { + "value": "[parameters('name')]" + }, + "name": { + "value": "[coalesce(parameters('mongodbDatabases'), createArray())[copyIndex()].name]" + }, + "tags": { + "value": "[coalesce(tryGet(coalesce(parameters('mongodbDatabases'), createArray())[copyIndex()], 'tags'), parameters('tags'))]" + }, + "collections": { + "value": "[tryGet(coalesce(parameters('mongodbDatabases'), createArray())[copyIndex()], 'collections')]" + }, + "throughput": { + "value": "[tryGet(coalesce(parameters('mongodbDatabases'), createArray())[copyIndex()], 'throughput')]" + }, + "autoscaleSettings": { + "value": "[tryGet(coalesce(parameters('mongodbDatabases'), createArray())[copyIndex()], 'autoscaleSettings')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "languageVersion": "2.0", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.38.33.27573", + "templateHash": "7289795303297936310" }, - "virtualNetworkTapType": { + "name": "DocumentDB Database Account MongoDB Databases", + "description": "This module deploys a MongoDB Database within a CosmosDB Account." + }, + "definitions": { + "collectionType": { "type": "object", "properties": { - "id": { + "name": { "type": "string", - "nullable": true, "metadata": { - "description": "Optional. Resource ID of the virtual network tap." + "description": "Required. Name of the collection." } }, - "location": { - "type": "string", + "throughput": { + "type": "int", "nullable": true, "metadata": { - "description": "Optional. Location of the virtual network tap." + "description": "Optional. Request Units per second. For best performance for large production workloads, it is recommended to set dedicated throughput (autoscale or manual) at the collection level and not at the database level." } }, - "properties": { - "type": "object", - "nullable": true, + "indexes": { + "type": "array", "metadata": { - "description": "Optional. Properties of the virtual network tap." + "__bicep_resource_derived_type!": { + "source": "Microsoft.DocumentDB/databaseAccounts/mongodbDatabases/collections@2025-04-15#properties/properties/properties/resource/properties/indexes" + }, + "description": "Required. Indexes for the collection." } }, - "tags": { + "shardKey": { "type": "object", - "nullable": true, "metadata": { - "description": "Optional. Tags of the virtual network tap." + "__bicep_resource_derived_type!": { + "source": "Microsoft.DocumentDB/databaseAccounts/mongodbDatabases/collections@2025-04-15#properties/properties/properties/resource/properties/shardKey" + }, + "description": "Required. ShardKey for the collection." } } }, "metadata": { - "description": "The type for the virtual network tap.", - "__bicep_imported_from!": { - "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/res/network/network-interface:0.5.1" - } + "__bicep_export!": true, + "description": "The type of a collection." } } }, "parameters": { - "networkInterfaceName": { - "type": "string" - }, - "virtualMachineName": { - "type": "string" - }, - "ipConfigurations": { - "type": "array", - "items": { - "$ref": "#/definitions/ipConfigurationType" - } - }, - "location": { + "databaseAccountName": { "type": "string", "metadata": { - "description": "Optional. Location for all resources." - } - }, - "tags": { - "type": "object", - "nullable": true, - "metadata": { - "description": "Optional. Tags of the resource." - } - }, - "enableIPForwarding": { - "type": "bool", - "defaultValue": false - }, - "enableAcceleratedNetworking": { - "type": "bool", - "defaultValue": false - }, - "dnsServers": { - "type": "array", - "items": { - "type": "string" - }, - "defaultValue": [] - }, - "enableTelemetry": { - "type": "bool", - "metadata": { - "description": "Required. Enable telemetry via a Globally Unique Identifier (GUID)." + "description": "Conditional. The name of the parent Cosmos DB database account. Required if the template is used in a standalone deployment." } }, - "networkSecurityGroupResourceId": { + "name": { "type": "string", - "defaultValue": "", "metadata": { - "description": "Optional. The network security group (NSG) to attach to the network interface." + "description": "Required. Name of the mongodb database." } }, - "lock": { - "$ref": "#/definitions/lockType", - "nullable": true, + "throughput": { + "type": "int", + "defaultValue": 400, "metadata": { - "description": "Optional. The lock settings of the service." + "description": "Optional. Request Units per second. Setting throughput at the database level is only recommended for development/test or when workload across all collections in the shared throughput database is uniform. For best performance for large production workloads, it is recommended to set dedicated throughput (autoscale or manual) at the collection level and not at the database level." } }, - "diagnosticSettings": { + "collections": { "type": "array", "items": { - "$ref": "#/definitions/diagnosticSettingFullType" + "$ref": "#/definitions/collectionType" }, "nullable": true, "metadata": { - "description": "Optional. The diagnostic settings of the service." + "description": "Optional. Collections in the mongodb database." } }, - "roleAssignments": { - "type": "array", - "items": { - "$ref": "#/definitions/roleAssignmentType" + "tags": { + "type": "object", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.DocumentDB/databaseAccounts/mongodbDatabases@2025-04-15#properties/tags" + }, + "description": "Optional. Tags of the resource." }, - "nullable": true, + "nullable": true + }, + "autoscaleSettings": { + "type": "object", "metadata": { - "description": "Optional. Array of role assignments to create." - } + "__bicep_resource_derived_type!": { + "source": "Microsoft.DocumentDB/databaseAccounts/mongodbDatabases@2025-04-15#properties/properties/properties/options/properties/autoscaleSettings" + }, + "description": "Optional. Specifies the Autoscale settings. Note: Either throughput or autoscaleSettings is required, but not both." + }, + "nullable": true } }, "resources": { - "networkInterface_publicIPAddresses": { + "databaseAccount": { + "existing": true, + "type": "Microsoft.DocumentDB/databaseAccounts", + "apiVersion": "2025-04-15", + "name": "[parameters('databaseAccountName')]" + }, + "mongodbDatabase": { + "type": "Microsoft.DocumentDB/databaseAccounts/mongodbDatabases", + "apiVersion": "2025-04-15", + "name": "[format('{0}/{1}', parameters('databaseAccountName'), parameters('name'))]", + "tags": "[parameters('tags')]", + "properties": { + "resource": { + "id": "[parameters('name')]" + }, + "options": "[if(contains(reference('databaseAccount').capabilities, createObject('name', 'EnableServerless')), null(), createObject('throughput', parameters('throughput'), 'autoscaleSettings', parameters('autoscaleSettings')))]" + }, + "dependsOn": [ + "databaseAccount" + ] + }, + "mongodbDatabase_collections": { "copy": { - "name": "networkInterface_publicIPAddresses", - "count": "[length(parameters('ipConfigurations'))]" + "name": "mongodbDatabase_collections", + "count": "[length(coalesce(parameters('collections'), createArray()))]" }, - "condition": "[and(not(empty(tryGet(parameters('ipConfigurations')[copyIndex()], 'pipConfiguration'))), empty(tryGet(tryGet(parameters('ipConfigurations')[copyIndex()], 'pipConfiguration'), 'publicIPAddressResourceId')))]", "type": "Microsoft.Resources/deployments", - "apiVersion": "2022-09-01", - "name": "[format('{0}-publicIP-{1}', deployment().name, copyIndex())]", - "properties": { - "expressionEvaluationOptions": { - "scope": "inner" - }, - "mode": "Incremental", - "parameters": { - "name": { - "value": "[coalesce(tryGet(tryGet(parameters('ipConfigurations')[copyIndex()], 'pipConfiguration'), 'name'), format('{0}{1}', parameters('virtualMachineName'), tryGet(tryGet(parameters('ipConfigurations')[copyIndex()], 'pipConfiguration'), 'publicIpNameSuffix')))]" - }, - "diagnosticSettings": { - "value": "[coalesce(tryGet(tryGet(parameters('ipConfigurations')[copyIndex()], 'pipConfiguration'), 'diagnosticSettings'), tryGet(parameters('ipConfigurations')[copyIndex()], 'diagnosticSettings'))]" - }, - "location": { - "value": "[parameters('location')]" - }, - "lock": { - "value": "[parameters('lock')]" - }, - "idleTimeoutInMinutes": { - "value": "[tryGet(tryGet(parameters('ipConfigurations')[copyIndex()], 'pipConfiguration'), 'idleTimeoutInMinutes')]" - }, - "ddosSettings": { - "value": "[tryGet(tryGet(parameters('ipConfigurations')[copyIndex()], 'pipConfiguration'), 'ddosSettings')]" - }, - "dnsSettings": { - "value": "[tryGet(tryGet(parameters('ipConfigurations')[copyIndex()], 'pipConfiguration'), 'dnsSettings')]" - }, - "publicIPAddressVersion": { - "value": "[tryGet(tryGet(parameters('ipConfigurations')[copyIndex()], 'pipConfiguration'), 'publicIPAddressVersion')]" - }, - "publicIPAllocationMethod": { - "value": "[tryGet(tryGet(parameters('ipConfigurations')[copyIndex()], 'pipConfiguration'), 'publicIPAllocationMethod')]" - }, - "publicIpPrefixResourceId": { - "value": "[tryGet(tryGet(parameters('ipConfigurations')[copyIndex()], 'pipConfiguration'), 'publicIpPrefixResourceId')]" - }, - "roleAssignments": { - "value": "[tryGet(tryGet(parameters('ipConfigurations')[copyIndex()], 'pipConfiguration'), 'roleAssignments')]" - }, - "skuName": { - "value": "[tryGet(tryGet(parameters('ipConfigurations')[copyIndex()], 'pipConfiguration'), 'skuName')]" - }, - "skuTier": { - "value": "[tryGet(tryGet(parameters('ipConfigurations')[copyIndex()], 'pipConfiguration'), 'skuTier')]" - }, - "tags": { - "value": "[coalesce(tryGet(parameters('ipConfigurations')[copyIndex()], 'tags'), parameters('tags'))]" - }, - "availabilityZones": { - "value": "[tryGet(tryGet(parameters('ipConfigurations')[copyIndex()], 'pipConfiguration'), 'availabilityZones')]" - }, - "enableTelemetry": { - "value": "[coalesce(coalesce(tryGet(tryGet(parameters('ipConfigurations')[copyIndex()], 'pipConfiguration'), 'enableTelemetry'), tryGet(parameters('ipConfigurations')[copyIndex()], 'enableTelemetry')), parameters('enableTelemetry'))]" - }, - "ipTags": { - "value": "[tryGet(tryGet(parameters('ipConfigurations')[copyIndex()], 'pipConfiguration'), 'ipTags')]" - } - }, - "template": { - "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", - "languageVersion": "2.0", - "contentVersion": "1.0.0.0", - "metadata": { - "_generator": { - "name": "bicep", - "version": "0.36.177.2456", - "templateHash": "14921988046704902194" - }, - "name": "Public IP Addresses", - "description": "This module deploys a Public IP Address." - }, - "definitions": { - "dnsSettingsType": { - "type": "object", - "properties": { - "domainNameLabel": { - "type": "string", - "metadata": { - "description": "Required. The domain name label. The concatenation of the domain name label and the regionalized DNS zone make up the fully qualified domain name associated with the public IP address. If a domain name label is specified, an A DNS record is created for the public IP in the Microsoft Azure DNS system." - } - }, - "domainNameLabelScope": { - "type": "string", - "allowedValues": [ - "NoReuse", - "ResourceGroupReuse", - "SubscriptionReuse", - "TenantReuse" - ], - "nullable": true, - "metadata": { - "description": "Optional. The domain name label scope. If a domain name label and a domain name label scope are specified, an A DNS record is created for the public IP in the Microsoft Azure DNS system with a hashed value includes in FQDN." - } - }, - "fqdn": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The Fully Qualified Domain Name of the A DNS record associated with the public IP. This is the concatenation of the domainNameLabel and the regionalized DNS zone." - } - }, - "reverseFqdn": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The reverse FQDN. A user-visible, fully qualified domain name that resolves to this public IP address. If the reverseFqdn is specified, then a PTR DNS record is created pointing from the IP address in the in-addr.arpa domain to the reverse FQDN." - } - } - }, - "metadata": { - "__bicep_export!": true - } - }, - "ddosSettingsType": { - "type": "object", - "properties": { - "ddosProtectionPlan": { - "type": "object", - "properties": { - "id": { - "type": "string", - "metadata": { - "description": "Required. The resource ID of the DDOS protection plan associated with the public IP address." - } - } - }, - "nullable": true, - "metadata": { - "description": "Optional. The DDoS protection plan associated with the public IP address." - } - }, - "protectionMode": { - "type": "string", - "allowedValues": [ - "Enabled" - ], - "metadata": { - "description": "Required. The DDoS protection policy customizations." - } - } - }, - "metadata": { - "__bicep_export!": true - } - }, - "ipTagType": { - "type": "object", - "properties": { - "ipTagType": { - "type": "string", - "metadata": { - "description": "Required. The IP tag type." - } - }, - "tag": { - "type": "string", - "metadata": { - "description": "Required. The IP tag." - } - } - }, - "metadata": { - "__bicep_export!": true - } - }, - "diagnosticSettingFullType": { - "type": "object", - "properties": { - "name": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The name of the diagnostic setting." - } - }, - "logCategoriesAndGroups": { - "type": "array", - "items": { - "type": "object", - "properties": { - "category": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. Name of a Diagnostic Log category for a resource type this setting is applied to. Set the specific logs to collect here." - } - }, - "categoryGroup": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. Name of a Diagnostic Log category group for a resource type this setting is applied to. Set to `allLogs` to collect all logs." - } - }, - "enabled": { - "type": "bool", - "nullable": true, - "metadata": { - "description": "Optional. Enable or disable the category explicitly. Default is `true`." - } - } - } - }, - "nullable": true, - "metadata": { - "description": "Optional. The name of logs that will be streamed. \"allLogs\" includes all possible logs for the resource. Set to `[]` to disable log collection." - } - }, - "metricCategories": { - "type": "array", - "items": { - "type": "object", - "properties": { - "category": { - "type": "string", - "metadata": { - "description": "Required. Name of a Diagnostic Metric category for a resource type this setting is applied to. Set to `AllMetrics` to collect all metrics." - } - }, - "enabled": { - "type": "bool", - "nullable": true, - "metadata": { - "description": "Optional. Enable or disable the category explicitly. Default is `true`." - } - } - } - }, - "nullable": true, - "metadata": { - "description": "Optional. The name of metrics that will be streamed. \"allMetrics\" includes all possible metrics for the resource. Set to `[]` to disable metric collection." - } - }, - "logAnalyticsDestinationType": { - "type": "string", - "allowedValues": [ - "AzureDiagnostics", - "Dedicated" - ], - "nullable": true, - "metadata": { - "description": "Optional. A string indicating whether the export to Log Analytics should use the default destination type, i.e. AzureDiagnostics, or use a destination type." - } - }, - "workspaceResourceId": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. Resource ID of the diagnostic log analytics workspace. For security reasons, it is recommended to set diagnostic settings to send data to either storage account, log analytics workspace or event hub." - } - }, - "storageAccountResourceId": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. Resource ID of the diagnostic storage account. For security reasons, it is recommended to set diagnostic settings to send data to either storage account, log analytics workspace or event hub." - } - }, - "eventHubAuthorizationRuleResourceId": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. Resource ID of the diagnostic event hub authorization rule for the Event Hubs namespace in which the event hub should be created or streamed to." - } - }, - "eventHubName": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. Name of the diagnostic event hub within the namespace to which logs are streamed. Without this, an event hub is created for each log category. For security reasons, it is recommended to set diagnostic settings to send data to either storage account, log analytics workspace or event hub." - } - }, - "marketplacePartnerResourceId": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The full ARM resource ID of the Marketplace resource to which you would like to send Diagnostic Logs." - } - } - }, - "metadata": { - "description": "An AVM-aligned type for a diagnostic setting. To be used if both logs & metrics are supported by the resource provider.", - "__bicep_imported_from!": { - "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.2.1" - } - } - }, - "lockType": { - "type": "object", - "properties": { - "name": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. Specify the name of lock." - } - }, - "kind": { - "type": "string", - "allowedValues": [ - "CanNotDelete", - "None", - "ReadOnly" - ], - "nullable": true, - "metadata": { - "description": "Optional. Specify the type of lock." - } - } - }, - "metadata": { - "description": "An AVM-aligned type for a lock.", - "__bicep_imported_from!": { - "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.2.1" - } - } + "apiVersion": "2025-04-01", + "name": "[format('{0}-collection-{1}', uniqueString(deployment().name, parameters('name')), coalesce(parameters('collections'), createArray())[copyIndex()].name)]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "databaseAccountName": { + "value": "[parameters('databaseAccountName')]" + }, + "mongodbDatabaseName": { + "value": "[parameters('name')]" + }, + "name": { + "value": "[coalesce(parameters('collections'), createArray())[copyIndex()].name]" + }, + "indexes": { + "value": "[coalesce(parameters('collections'), createArray())[copyIndex()].indexes]" + }, + "shardKey": { + "value": "[coalesce(parameters('collections'), createArray())[copyIndex()].shardKey]" + }, + "throughput": { + "value": "[tryGet(coalesce(parameters('collections'), createArray())[copyIndex()], 'throughput')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.38.33.27573", + "templateHash": "4317369978166598876" }, - "roleAssignmentType": { - "type": "object", - "properties": { - "name": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The name (as GUID) of the role assignment. If not provided, a GUID will be generated." - } - }, - "roleDefinitionIdOrName": { - "type": "string", - "metadata": { - "description": "Required. The role to assign. You can provide either the display name of the role definition, the role definition GUID, or its fully qualified ID in the following format: '/providers/Microsoft.Authorization/roleDefinitions/c2f4ef07-c644-48eb-af81-4b1b4947fb11'." - } - }, - "principalId": { - "type": "string", - "metadata": { - "description": "Required. The principal ID of the principal (user/group/identity) to assign the role to." - } - }, - "principalType": { - "type": "string", - "allowedValues": [ - "Device", - "ForeignGroup", - "Group", - "ServicePrincipal", - "User" - ], - "nullable": true, - "metadata": { - "description": "Optional. The principal type of the assigned principal ID." - } - }, - "description": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The description of the role assignment." - } - }, - "condition": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The conditions on the role assignment. This limits the resources it can be assigned to. e.g.: @Resource[Microsoft.Storage/storageAccounts/blobServices/containers:ContainerName] StringEqualsIgnoreCase \"foo_storage_container\"." - } - }, - "conditionVersion": { - "type": "string", - "allowedValues": [ - "2.0" - ], - "nullable": true, - "metadata": { - "description": "Optional. Version of the condition." - } - }, - "delegatedManagedIdentityResourceId": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The Resource Id of the delegated managed identity resource." - } - } - }, - "metadata": { - "description": "An AVM-aligned type for a role assignment.", - "__bicep_imported_from!": { - "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.2.1" - } - } - } + "name": "DocumentDB Database Account MongoDB Database Collections", + "description": "This module deploys a MongoDB Database Collection." }, "parameters": { - "name": { + "databaseAccountName": { "type": "string", "metadata": { - "description": "Required. The name of the Public IP Address." + "description": "Conditional. The name of the parent Cosmos DB database account. Required if the template is used in a standalone deployment." } }, - "publicIpPrefixResourceId": { + "mongodbDatabaseName": { "type": "string", - "nullable": true, "metadata": { - "description": "Optional. Resource ID of the Public IP Prefix object. This is only needed if you want your Public IPs created in a PIP Prefix." + "description": "Conditional. The name of the parent mongodb database. Required if the template is used in a standalone deployment." } }, - "publicIPAllocationMethod": { + "name": { "type": "string", - "defaultValue": "Static", - "allowedValues": [ - "Dynamic", - "Static" - ], "metadata": { - "description": "Optional. The public IP address allocation method." + "description": "Required. Name of the collection." } }, - "availabilityZones": { - "type": "array", - "items": { - "type": "int" - }, - "defaultValue": [ - 1, - 2, - 3 - ], - "allowedValues": [ - 1, - 2, - 3 - ], + "throughput": { + "type": "int", + "defaultValue": 400, "metadata": { - "description": "Optional. A list of availability zones denoting the IP allocated for the resource needs to come from." + "description": "Optional. Request Units per second. For best performance for large production workloads, it is recommended to set dedicated throughput (autoscale or manual) at the collection level and not at the database level." } }, - "publicIPAddressVersion": { - "type": "string", - "defaultValue": "IPv4", - "allowedValues": [ - "IPv4", - "IPv6" - ], + "indexes": { + "type": "array", "metadata": { - "description": "Optional. IP address version." + "__bicep_resource_derived_type!": { + "source": "Microsoft.DocumentDB/databaseAccounts/mongodbDatabases/collections@2025-04-15#properties/properties/properties/resource/properties/indexes" + }, + "description": "Required. Indexes for the collection." } }, - "dnsSettings": { - "$ref": "#/definitions/dnsSettingsType", - "nullable": true, + "shardKey": { + "type": "object", "metadata": { - "description": "Optional. The DNS settings of the public IP address." + "__bicep_resource_derived_type!": { + "source": "Microsoft.DocumentDB/databaseAccounts/mongodbDatabases/collections@2025-04-15#properties/properties/properties/resource/properties/shardKey" + }, + "description": "Required. ShardKey for the collection." } - }, - "ipTags": { - "type": "array", - "items": { - "$ref": "#/definitions/ipTagType" - }, - "nullable": true, - "metadata": { - "description": "Optional. The list of tags associated with the public IP address." + } + }, + "resources": [ + { + "type": "Microsoft.DocumentDB/databaseAccounts/mongodbDatabases/collections", + "apiVersion": "2025-04-15", + "name": "[format('{0}/{1}/{2}', parameters('databaseAccountName'), parameters('mongodbDatabaseName'), parameters('name'))]", + "properties": { + "options": "[if(contains(reference(resourceId('Microsoft.DocumentDB/databaseAccounts', parameters('databaseAccountName')), '2025-04-15').capabilities, createObject('name', 'EnableServerless')), null(), createObject('throughput', parameters('throughput')))]", + "resource": { + "id": "[parameters('name')]", + "indexes": "[parameters('indexes')]", + "shardKey": "[parameters('shardKey')]" + } } - }, - "lock": { - "$ref": "#/definitions/lockType", - "nullable": true, + } + ], + "outputs": { + "name": { + "type": "string", "metadata": { - "description": "Optional. The lock settings of the service." - } + "description": "The name of the mongodb database collection." + }, + "value": "[parameters('name')]" }, - "skuName": { + "resourceId": { "type": "string", - "defaultValue": "Standard", - "allowedValues": [ - "Basic", - "Standard" - ], "metadata": { - "description": "Optional. Name of a public IP address SKU." - } + "description": "The resource ID of the mongodb database collection." + }, + "value": "[resourceId('Microsoft.DocumentDB/databaseAccounts/mongodbDatabases/collections', parameters('databaseAccountName'), parameters('mongodbDatabaseName'), parameters('name'))]" }, - "skuTier": { + "resourceGroupName": { "type": "string", - "defaultValue": "Regional", - "allowedValues": [ - "Global", - "Regional" - ], "metadata": { - "description": "Optional. Tier of a public IP address SKU." - } + "description": "The name of the resource group the mongodb database collection was created in." + }, + "value": "[resourceGroup().name]" + } + } + } + }, + "dependsOn": [ + "mongodbDatabase" + ] + } + }, + "outputs": { + "name": { + "type": "string", + "metadata": { + "description": "The name of the mongodb database." + }, + "value": "[parameters('name')]" + }, + "resourceId": { + "type": "string", + "metadata": { + "description": "The resource ID of the mongodb database." + }, + "value": "[resourceId('Microsoft.DocumentDB/databaseAccounts/mongodbDatabases', parameters('databaseAccountName'), parameters('name'))]" + }, + "resourceGroupName": { + "type": "string", + "metadata": { + "description": "The name of the resource group the mongodb database was created in." + }, + "value": "[resourceGroup().name]" + } + } + } + }, + "dependsOn": [ + "databaseAccount" + ] + }, + "databaseAccount_gremlinDatabases": { + "copy": { + "name": "databaseAccount_gremlinDatabases", + "count": "[length(coalesce(parameters('gremlinDatabases'), createArray()))]" + }, + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "[format('{0}-gremlin-{1}', uniqueString(deployment().name, parameters('location')), coalesce(parameters('gremlinDatabases'), createArray())[copyIndex()].name)]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "databaseAccountName": { + "value": "[parameters('name')]" + }, + "name": { + "value": "[coalesce(parameters('gremlinDatabases'), createArray())[copyIndex()].name]" + }, + "tags": { + "value": "[coalesce(tryGet(coalesce(parameters('gremlinDatabases'), createArray())[copyIndex()], 'tags'), parameters('tags'))]" + }, + "graphs": { + "value": "[tryGet(coalesce(parameters('gremlinDatabases'), createArray())[copyIndex()], 'graphs')]" + }, + "maxThroughput": { + "value": "[tryGet(coalesce(parameters('gremlinDatabases'), createArray())[copyIndex()], 'maxThroughput')]" + }, + "throughput": { + "value": "[tryGet(coalesce(parameters('gremlinDatabases'), createArray())[copyIndex()], 'throughput')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "languageVersion": "2.0", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.38.33.27573", + "templateHash": "14708982296215631776" + }, + "name": "DocumentDB Database Account Gremlin Databases", + "description": "This module deploys a Gremlin Database within a CosmosDB Account." + }, + "definitions": { + "graphType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "metadata": { + "description": "Required. Name of the graph." + } + }, + "tags": { + "type": "object", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.DocumentDB/databaseAccounts/gremlinDatabases/graphs@2025-04-15#properties/tags" }, - "ddosSettings": { - "$ref": "#/definitions/ddosSettingsType", - "nullable": true, - "metadata": { - "description": "Optional. The DDoS protection plan configuration associated with the public IP address." - } + "description": "Optional. Tags of the Gremlin graph resource." + }, + "nullable": true + }, + "indexingPolicy": { + "type": "object", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.DocumentDB/databaseAccounts/gremlinDatabases/graphs@2025-04-15#properties/properties/properties/resource/properties/indexingPolicy" }, - "location": { + "description": "Optional. Indexing policy of the graph." + }, + "nullable": true + }, + "partitionKeyPaths": { + "type": "array", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.DocumentDB/databaseAccounts/gremlinDatabases/graphs@2025-04-15#properties/properties/properties/resource/properties/partitionKey/properties/paths" + }, + "description": "Optional. List of paths using which data within the container can be partitioned." + }, + "nullable": true + } + }, + "metadata": { + "__bicep_export!": true, + "description": "The type of a graph." + } + } + }, + "parameters": { + "name": { + "type": "string", + "metadata": { + "description": "Required. Name of the Gremlin database." + } + }, + "tags": { + "type": "object", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.DocumentDB/databaseAccounts/gremlinDatabases@2024-11-15#properties/tags" + }, + "description": "Optional. Tags of the Gremlin database resource." + }, + "nullable": true + }, + "databaseAccountName": { + "type": "string", + "metadata": { + "description": "Conditional. The name of the parent Gremlin database. Required if the template is used in a standalone deployment." + } + }, + "graphs": { + "type": "array", + "items": { + "$ref": "#/definitions/graphType" + }, + "nullable": true, + "metadata": { + "description": "Optional. Array of graphs to deploy in the Gremlin database." + } + }, + "maxThroughput": { + "type": "int", + "defaultValue": 4000, + "metadata": { + "description": "Optional. Represents maximum throughput, the resource can scale up to. Cannot be set together with `throughput`. If `throughput` is set to something else than -1, this autoscale setting is ignored. Setting throughput at the database level is only recommended for development/test or when workload across all graphs in the shared throughput database is uniform. For best performance for large production workloads, it is recommended to set dedicated throughput (autoscale or manual) at the graph level and not at the database level." + } + }, + "throughput": { + "type": "int", + "nullable": true, + "metadata": { + "description": "Optional. Request Units per second (for example 10000). Cannot be set together with `maxThroughput`. Setting throughput at the database level is only recommended for development/test or when workload across all graphs in the shared throughput database is uniform. For best performance for large production workloads, it is recommended to set dedicated throughput (autoscale or manual) at the graph level and not at the database level." + } + } + }, + "resources": { + "databaseAccount": { + "existing": true, + "type": "Microsoft.DocumentDB/databaseAccounts", + "apiVersion": "2025-04-15", + "name": "[parameters('databaseAccountName')]" + }, + "gremlinDatabase": { + "type": "Microsoft.DocumentDB/databaseAccounts/gremlinDatabases", + "apiVersion": "2025-04-15", + "name": "[format('{0}/{1}', parameters('databaseAccountName'), parameters('name'))]", + "tags": "[parameters('tags')]", + "properties": { + "options": "[if(contains(reference('databaseAccount').capabilities, createObject('name', 'EnableServerless')), createObject(), createObject('autoscaleSettings', if(equals(parameters('throughput'), null()), createObject('maxThroughput', parameters('maxThroughput')), null()), 'throughput', parameters('throughput')))]", + "resource": { + "id": "[parameters('name')]" + } + }, + "dependsOn": [ + "databaseAccount" + ] + }, + "gremlinDatabase_gremlinGraphs": { + "copy": { + "name": "gremlinDatabase_gremlinGraphs", + "count": "[length(coalesce(parameters('graphs'), createArray()))]" + }, + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "[format('{0}-gremlindb-{1}', uniqueString(deployment().name, parameters('name')), coalesce(parameters('graphs'), createArray())[copyIndex()].name)]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "name": { + "value": "[coalesce(parameters('graphs'), createArray())[copyIndex()].name]" + }, + "gremlinDatabaseName": { + "value": "[parameters('name')]" + }, + "databaseAccountName": { + "value": "[parameters('databaseAccountName')]" + }, + "indexingPolicy": { + "value": "[tryGet(coalesce(parameters('graphs'), createArray())[copyIndex()], 'indexingPolicy')]" + }, + "partitionKeyPaths": { + "value": "[tryGet(coalesce(parameters('graphs'), createArray())[copyIndex()], 'partitionKeyPaths')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "languageVersion": "2.0", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.38.33.27573", + "templateHash": "15097132107382000570" + }, + "name": "DocumentDB Database Accounts Gremlin Databases Graphs", + "description": "This module deploys a DocumentDB Database Accounts Gremlin Database Graph." + }, + "parameters": { + "name": { "type": "string", - "defaultValue": "[resourceGroup().location]", "metadata": { - "description": "Optional. Location for all resources." + "description": "Required. Name of the graph." } }, - "roleAssignments": { - "type": "array", - "items": { - "$ref": "#/definitions/roleAssignmentType" - }, - "nullable": true, + "tags": { + "type": "object", "metadata": { - "description": "Optional. Array of role assignments to create." - } + "__bicep_resource_derived_type!": { + "source": "Microsoft.DocumentDB/databaseAccounts/gremlinDatabases/graphs@2025-04-15#properties/tags" + }, + "description": "Optional. Tags of the Gremlin graph resource." + }, + "nullable": true }, - "enableTelemetry": { - "type": "bool", - "defaultValue": true, + "databaseAccountName": { + "type": "string", "metadata": { - "description": "Optional. Enable/Disable usage telemetry for module." + "description": "Conditional. The name of the parent Database Account. Required if the template is used in a standalone deployment." } }, - "idleTimeoutInMinutes": { - "type": "int", - "defaultValue": 4, + "gremlinDatabaseName": { + "type": "string", "metadata": { - "description": "Optional. The idle timeout of the public IP address." + "description": "Conditional. The name of the parent Gremlin Database. Required if the template is used in a standalone deployment." } }, - "tags": { + "indexingPolicy": { "type": "object", - "nullable": true, "metadata": { - "description": "Optional. Tags of the resource." - } + "__bicep_resource_derived_type!": { + "source": "Microsoft.DocumentDB/databaseAccounts/gremlinDatabases/graphs@2025-04-15#properties/properties/properties/resource/properties/indexingPolicy" + }, + "description": "Optional. Indexing policy of the graph." + }, + "nullable": true }, - "diagnosticSettings": { + "partitionKeyPaths": { "type": "array", - "items": { - "$ref": "#/definitions/diagnosticSettingFullType" - }, - "nullable": true, "metadata": { - "description": "Optional. The diagnostic settings of the service." - } - } - }, - "variables": { - "copy": [ - { - "name": "formattedRoleAssignments", - "count": "[length(coalesce(parameters('roleAssignments'), createArray()))]", - "input": "[union(coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')], createObject('roleDefinitionId', coalesce(tryGet(variables('builtInRoleNames'), coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName), if(contains(coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName, '/providers/Microsoft.Authorization/roleDefinitions/'), coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName, subscriptionResourceId('Microsoft.Authorization/roleDefinitions', coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName)))))]" - } - ], - "builtInRoleNames": { - "Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'b24988ac-6180-42a0-ab88-20f7382dd24c')]", - "DNS Resolver Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '0f2ebee7-ffd4-4fc0-b3b7-664099fdad5d')]", - "DNS Zone Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'befefa01-2a29-4197-83a8-272ff33ce314')]", - "Domain Services Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'eeaeda52-9324-47f6-8069-5d5bade478b2')]", - "Domain Services Reader": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '361898ef-9ed1-48c2-849c-a832951106bb')]", - "Network Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '4d97b98b-1d4f-4787-a291-c67834d212e7')]", - "Owner": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '8e3af657-a8ff-443c-a75c-2fe8c4bcb635')]", - "Private DNS Zone Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'b12aa53e-6015-4669-85d0-8515ebb3ae7f')]", - "Reader": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'acdd72a7-3385-48ef-bd42-f606fba81ae7')]", - "Role Based Access Control Administrator": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'f58310d9-a9f6-439a-9e8d-f62e7b41a168')]" + "__bicep_resource_derived_type!": { + "source": "Microsoft.DocumentDB/databaseAccounts/gremlinDatabases/graphs@2025-04-15#properties/properties/properties/resource/properties/partitionKey/properties/paths" + }, + "description": "Optional. List of paths using which data within the container can be partitioned." + }, + "nullable": true } }, "resources": { - "avmTelemetry": { - "condition": "[parameters('enableTelemetry')]", - "type": "Microsoft.Resources/deployments", - "apiVersion": "2024-03-01", - "name": "[format('46d3xbcp.res.network-publicipaddress.{0}.{1}', replace('0.9.0', '.', '-'), substring(uniqueString(deployment().name, parameters('location')), 0, 4))]", - "properties": { - "mode": "Incremental", - "template": { - "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", - "contentVersion": "1.0.0.0", - "resources": [], - "outputs": { - "telemetry": { - "type": "String", - "value": "For more information, see https://aka.ms/avm/TelemetryInfo" - } - } - } - } - }, - "publicIpAddress": { - "type": "Microsoft.Network/publicIPAddresses", - "apiVersion": "2024-05-01", - "name": "[parameters('name')]", - "location": "[parameters('location')]", - "tags": "[parameters('tags')]", - "sku": { - "name": "[parameters('skuName')]", - "tier": "[parameters('skuTier')]" - }, - "zones": "[map(parameters('availabilityZones'), lambda('zone', string(lambdaVariables('zone'))))]", - "properties": { - "ddosSettings": "[parameters('ddosSettings')]", - "dnsSettings": "[parameters('dnsSettings')]", - "publicIPAddressVersion": "[parameters('publicIPAddressVersion')]", - "publicIPAllocationMethod": "[parameters('publicIPAllocationMethod')]", - "publicIPPrefix": "[if(not(empty(parameters('publicIpPrefixResourceId'))), createObject('id', parameters('publicIpPrefixResourceId')), null())]", - "idleTimeoutInMinutes": "[parameters('idleTimeoutInMinutes')]", - "ipTags": "[parameters('ipTags')]" - } - }, - "publicIpAddress_lock": { - "condition": "[and(not(empty(coalesce(parameters('lock'), createObject()))), not(equals(tryGet(parameters('lock'), 'kind'), 'None')))]", - "type": "Microsoft.Authorization/locks", - "apiVersion": "2020-05-01", - "scope": "[format('Microsoft.Network/publicIPAddresses/{0}', parameters('name'))]", - "name": "[coalesce(tryGet(parameters('lock'), 'name'), format('lock-{0}', parameters('name')))]", - "properties": { - "level": "[coalesce(tryGet(parameters('lock'), 'kind'), '')]", - "notes": "[if(equals(tryGet(parameters('lock'), 'kind'), 'CanNotDelete'), 'Cannot delete resource or child resources.', 'Cannot delete or modify the resource or child resources.')]" - }, - "dependsOn": [ - "publicIpAddress" - ] + "databaseAccount::gremlinDatabase": { + "existing": true, + "type": "Microsoft.DocumentDB/databaseAccounts/gremlinDatabases", + "apiVersion": "2025-04-15", + "name": "[format('{0}/{1}', parameters('databaseAccountName'), parameters('gremlinDatabaseName'))]" }, - "publicIpAddress_roleAssignments": { - "copy": { - "name": "publicIpAddress_roleAssignments", - "count": "[length(coalesce(variables('formattedRoleAssignments'), createArray()))]" - }, - "type": "Microsoft.Authorization/roleAssignments", - "apiVersion": "2022-04-01", - "scope": "[format('Microsoft.Network/publicIPAddresses/{0}', parameters('name'))]", - "name": "[coalesce(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'name'), guid(resourceId('Microsoft.Network/publicIPAddresses', parameters('name')), coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].principalId, coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].roleDefinitionId))]", - "properties": { - "roleDefinitionId": "[coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].roleDefinitionId]", - "principalId": "[coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].principalId]", - "description": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'description')]", - "principalType": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'principalType')]", - "condition": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'condition')]", - "conditionVersion": "[if(not(empty(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'condition'))), coalesce(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'conditionVersion'), '2.0'), null())]", - "delegatedManagedIdentityResourceId": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'delegatedManagedIdentityResourceId')]" - }, - "dependsOn": [ - "publicIpAddress" - ] + "databaseAccount": { + "existing": true, + "type": "Microsoft.DocumentDB/databaseAccounts", + "apiVersion": "2025-04-15", + "name": "[parameters('databaseAccountName')]" }, - "publicIpAddress_diagnosticSettings": { - "copy": { - "name": "publicIpAddress_diagnosticSettings", - "count": "[length(coalesce(parameters('diagnosticSettings'), createArray()))]" - }, - "type": "Microsoft.Insights/diagnosticSettings", - "apiVersion": "2021-05-01-preview", - "scope": "[format('Microsoft.Network/publicIPAddresses/{0}', parameters('name'))]", - "name": "[coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'name'), format('{0}-diagnosticSettings', parameters('name')))]", + "gremlinGraph": { + "type": "Microsoft.DocumentDB/databaseAccounts/gremlinDatabases/graphs", + "apiVersion": "2025-04-15", + "name": "[format('{0}/{1}/{2}', parameters('databaseAccountName'), parameters('gremlinDatabaseName'), parameters('name'))]", + "tags": "[parameters('tags')]", "properties": { - "copy": [ - { - "name": "metrics", - "count": "[length(coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'metricCategories'), createArray(createObject('category', 'AllMetrics'))))]", - "input": { - "category": "[coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'metricCategories'), createArray(createObject('category', 'AllMetrics')))[copyIndex('metrics')].category]", - "enabled": "[coalesce(tryGet(coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'metricCategories'), createArray(createObject('category', 'AllMetrics')))[copyIndex('metrics')], 'enabled'), true())]", - "timeGrain": null - } - }, - { - "name": "logs", - "count": "[length(coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'logCategoriesAndGroups'), createArray(createObject('categoryGroup', 'allLogs'))))]", - "input": { - "categoryGroup": "[tryGet(coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'logCategoriesAndGroups'), createArray(createObject('categoryGroup', 'allLogs')))[copyIndex('logs')], 'categoryGroup')]", - "category": "[tryGet(coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'logCategoriesAndGroups'), createArray(createObject('categoryGroup', 'allLogs')))[copyIndex('logs')], 'category')]", - "enabled": "[coalesce(tryGet(coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'logCategoriesAndGroups'), createArray(createObject('categoryGroup', 'allLogs')))[copyIndex('logs')], 'enabled'), true())]" - } + "resource": { + "id": "[parameters('name')]", + "indexingPolicy": "[parameters('indexingPolicy')]", + "partitionKey": { + "paths": "[parameters('partitionKeyPaths')]" } - ], - "storageAccountId": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'storageAccountResourceId')]", - "workspaceId": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'workspaceResourceId')]", - "eventHubAuthorizationRuleId": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'eventHubAuthorizationRuleResourceId')]", - "eventHubName": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'eventHubName')]", - "marketplacePartnerId": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'marketplacePartnerResourceId')]", - "logAnalyticsDestinationType": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'logAnalyticsDestinationType')]" - }, - "dependsOn": [ - "publicIpAddress" - ] + } + } } }, "outputs": { - "resourceGroupName": { - "type": "string", - "metadata": { - "description": "The resource group the public IP address was deployed into." - }, - "value": "[resourceGroup().name]" - }, "name": { "type": "string", "metadata": { - "description": "The name of the public IP address." + "description": "The name of the graph." }, "value": "[parameters('name')]" }, "resourceId": { "type": "string", "metadata": { - "description": "The resource ID of the public IP address." - }, - "value": "[resourceId('Microsoft.Network/publicIPAddresses', parameters('name'))]" - }, - "ipAddress": { - "type": "string", - "metadata": { - "description": "The public IP address of the public IP address resource." + "description": "The resource ID of the graph." }, - "value": "[coalesce(tryGet(reference('publicIpAddress'), 'ipAddress'), '')]" + "value": "[resourceId('Microsoft.DocumentDB/databaseAccounts/gremlinDatabases/graphs', parameters('databaseAccountName'), parameters('gremlinDatabaseName'), parameters('name'))]" }, - "location": { + "resourceGroupName": { "type": "string", "metadata": { - "description": "The location the resource was deployed into." + "description": "The name of the resource group the graph was created in." }, - "value": "[reference('publicIpAddress', '2024-05-01', 'full').location]" + "value": "[resourceGroup().name]" } } } + }, + "dependsOn": [ + "gremlinDatabase" + ] + } + }, + "outputs": { + "name": { + "type": "string", + "metadata": { + "description": "The name of the Gremlin database." + }, + "value": "[parameters('name')]" + }, + "resourceId": { + "type": "string", + "metadata": { + "description": "The resource ID of the Gremlin database." + }, + "value": "[resourceId('Microsoft.DocumentDB/databaseAccounts/gremlinDatabases', parameters('databaseAccountName'), parameters('name'))]" + }, + "resourceGroupName": { + "type": "string", + "metadata": { + "description": "The name of the resource group the Gremlin database was created in." + }, + "value": "[resourceGroup().name]" + } + } + } + }, + "dependsOn": [ + "databaseAccount" + ] + }, + "databaseAccount_tables": { + "copy": { + "name": "databaseAccount_tables", + "count": "[length(coalesce(parameters('tables'), createArray()))]" + }, + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "[format('{0}-table-{1}', uniqueString(deployment().name, parameters('location')), coalesce(parameters('tables'), createArray())[copyIndex()].name)]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "databaseAccountName": { + "value": "[parameters('name')]" + }, + "name": { + "value": "[coalesce(parameters('tables'), createArray())[copyIndex()].name]" + }, + "tags": { + "value": "[coalesce(tryGet(coalesce(parameters('tables'), createArray())[copyIndex()], 'tags'), parameters('tags'))]" + }, + "maxThroughput": { + "value": "[tryGet(coalesce(parameters('tables'), createArray())[copyIndex()], 'maxThroughput')]" + }, + "throughput": { + "value": "[tryGet(coalesce(parameters('tables'), createArray())[copyIndex()], 'throughput')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "languageVersion": "2.0", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.38.33.27573", + "templateHash": "11768488776074268398" + }, + "name": "Azure Cosmos DB account tables", + "description": "This module deploys a table within an Azure Cosmos DB Account." + }, + "parameters": { + "name": { + "type": "string", + "metadata": { + "description": "Required. Name of the table." } }, - "networkInterface": { - "type": "Microsoft.Resources/deployments", - "apiVersion": "2022-09-01", - "name": "[format('{0}-NetworkInterface', deployment().name)]", + "tags": { + "type": "object", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.DocumentDB/databaseAccounts/tables@2025-04-15#properties/tags" + }, + "description": "Optional. Tags for the table." + }, + "nullable": true + }, + "databaseAccountName": { + "type": "string", + "metadata": { + "description": "Conditional. The name of the parent Azure Cosmos DB account. Required if the template is used in a standalone deployment." + } + }, + "maxThroughput": { + "type": "int", + "defaultValue": 4000, + "metadata": { + "description": "Optional. Represents maximum throughput, the resource can scale up to. Cannot be set together with `throughput`. If `throughput` is set to something else than -1, this autoscale setting is ignored." + } + }, + "throughput": { + "type": "int", + "nullable": true, + "metadata": { + "description": "Optional. Request Units per second (for example 10000). Cannot be set together with `maxThroughput`." + } + } + }, + "resources": { + "databaseAccount": { + "existing": true, + "type": "Microsoft.DocumentDB/databaseAccounts", + "apiVersion": "2025-04-15", + "name": "[parameters('databaseAccountName')]" + }, + "table": { + "type": "Microsoft.DocumentDB/databaseAccounts/tables", + "apiVersion": "2025-04-15", + "name": "[format('{0}/{1}', parameters('databaseAccountName'), parameters('name'))]", + "tags": "[parameters('tags')]", "properties": { - "expressionEvaluationOptions": { - "scope": "inner" + "options": "[if(contains(reference('databaseAccount').capabilities, createObject('name', 'EnableServerless')), createObject(), createObject('autoscaleSettings', if(equals(parameters('throughput'), null()), createObject('maxThroughput', parameters('maxThroughput')), null()), 'throughput', parameters('throughput')))]", + "resource": { + "id": "[parameters('name')]" + } + }, + "dependsOn": [ + "databaseAccount" + ] + } + }, + "outputs": { + "name": { + "type": "string", + "metadata": { + "description": "The name of the table." + }, + "value": "[parameters('name')]" + }, + "resourceId": { + "type": "string", + "metadata": { + "description": "The resource ID of the table." + }, + "value": "[resourceId('Microsoft.DocumentDB/databaseAccounts/tables', parameters('databaseAccountName'), parameters('name'))]" + }, + "resourceGroupName": { + "type": "string", + "metadata": { + "description": "The name of the resource group the table was created in." + }, + "value": "[resourceGroup().name]" + } + } + } + }, + "dependsOn": [ + "databaseAccount" + ] + }, + "databaseAccount_cassandraKeyspaces": { + "copy": { + "name": "databaseAccount_cassandraKeyspaces", + "count": "[length(coalesce(parameters('cassandraKeyspaces'), createArray()))]" + }, + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "[format('{0}-cassandradb-{1}', uniqueString(deployment().name, parameters('location')), coalesce(parameters('cassandraKeyspaces'), createArray())[copyIndex()].name)]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "databaseAccountName": { + "value": "[parameters('name')]" + }, + "name": { + "value": "[coalesce(parameters('cassandraKeyspaces'), createArray())[copyIndex()].name]" + }, + "tags": { + "value": "[coalesce(tryGet(coalesce(parameters('cassandraKeyspaces'), createArray())[copyIndex()], 'tags'), parameters('tags'))]" + }, + "tables": { + "value": "[tryGet(coalesce(parameters('cassandraKeyspaces'), createArray())[copyIndex()], 'tables')]" + }, + "views": { + "value": "[tryGet(coalesce(parameters('cassandraKeyspaces'), createArray())[copyIndex()], 'views')]" + }, + "autoscaleSettingsMaxThroughput": { + "value": "[tryGet(coalesce(parameters('cassandraKeyspaces'), createArray())[copyIndex()], 'autoscaleSettingsMaxThroughput')]" + }, + "throughput": { + "value": "[tryGet(coalesce(parameters('cassandraKeyspaces'), createArray())[copyIndex()], 'throughput')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "languageVersion": "2.0", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.38.33.27573", + "templateHash": "63327155428300562" + }, + "name": "DocumentDB Database Account Cassandra Keyspaces", + "description": "This module deploys a Cassandra Keyspace within a CosmosDB Account." + }, + "definitions": { + "tableType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "metadata": { + "description": "Required. Name of the table." + } }, - "mode": "Incremental", - "parameters": { - "name": { - "value": "[parameters('networkInterfaceName')]" - }, - "ipConfigurations": { - "copy": [ - { - "name": "value", - "count": "[length(parameters('ipConfigurations'))]", - "input": "[createObject('name', tryGet(parameters('ipConfigurations')[copyIndex('value')], 'name'), 'privateIPAllocationMethod', tryGet(parameters('ipConfigurations')[copyIndex('value')], 'privateIPAllocationMethod'), 'privateIPAddress', tryGet(parameters('ipConfigurations')[copyIndex('value')], 'privateIPAddress'), 'publicIPAddressResourceId', if(not(empty(tryGet(parameters('ipConfigurations')[copyIndex('value')], 'pipConfiguration'))), if(not(contains(coalesce(tryGet(parameters('ipConfigurations')[copyIndex('value')], 'pipConfiguration'), createObject()), 'publicIPAddressResourceId')), resourceId('Microsoft.Network/publicIPAddresses', coalesce(tryGet(tryGet(parameters('ipConfigurations')[copyIndex('value')], 'pipConfiguration'), 'name'), format('{0}{1}', parameters('virtualMachineName'), tryGet(tryGet(parameters('ipConfigurations')[copyIndex('value')], 'pipConfiguration'), 'publicIpNameSuffix')))), tryGet(parameters('ipConfigurations')[copyIndex('value')], 'pipConfiguration', 'publicIPAddressResourceId')), null()), 'subnetResourceId', parameters('ipConfigurations')[copyIndex('value')].subnetResourceId, 'loadBalancerBackendAddressPools', tryGet(parameters('ipConfigurations')[copyIndex('value')], 'loadBalancerBackendAddressPools'), 'applicationSecurityGroups', tryGet(parameters('ipConfigurations')[copyIndex('value')], 'applicationSecurityGroups'), 'applicationGatewayBackendAddressPools', tryGet(parameters('ipConfigurations')[copyIndex('value')], 'applicationGatewayBackendAddressPools'), 'gatewayLoadBalancer', tryGet(parameters('ipConfigurations')[copyIndex('value')], 'gatewayLoadBalancer'), 'loadBalancerInboundNatRules', tryGet(parameters('ipConfigurations')[copyIndex('value')], 'loadBalancerInboundNatRules'), 'privateIPAddressVersion', tryGet(parameters('ipConfigurations')[copyIndex('value')], 'privateIPAddressVersion'), 'virtualNetworkTaps', tryGet(parameters('ipConfigurations')[copyIndex('value')], 'virtualNetworkTaps'))]" - } - ] - }, - "location": { - "value": "[parameters('location')]" - }, - "tags": { - "value": "[parameters('tags')]" - }, - "diagnosticSettings": { - "value": "[parameters('diagnosticSettings')]" - }, - "dnsServers": { - "value": "[parameters('dnsServers')]" - }, - "enableAcceleratedNetworking": { - "value": "[parameters('enableAcceleratedNetworking')]" - }, - "enableTelemetry": { - "value": "[parameters('enableTelemetry')]" - }, - "enableIPForwarding": { - "value": "[parameters('enableIPForwarding')]" - }, - "lock": { - "value": "[parameters('lock')]" + "schema": { + "type": "object", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.DocumentDB/databaseAccounts/cassandraKeyspaces/tables@2024-11-15#properties/properties/properties/resource/properties/schema" + }, + "description": "Required. Schema definition for the table." + } + }, + "tags": { + "type": "object", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.DocumentDB/databaseAccounts/cassandraKeyspaces/tables@2024-11-15#properties/tags" + }, + "description": "Optional. Tags for the table." }, - "networkSecurityGroupResourceId": "[if(not(empty(parameters('networkSecurityGroupResourceId'))), createObject('value', parameters('networkSecurityGroupResourceId')), createObject('value', ''))]", - "roleAssignments": { - "value": "[parameters('roleAssignments')]" + "nullable": true + }, + "defaultTtl": { + "type": "int", + "nullable": true, + "metadata": { + "description": "Optional. Default TTL (Time To Live) in seconds for data in the table." + } + }, + "analyticalStorageTtl": { + "type": "int", + "nullable": true, + "metadata": { + "description": "Optional. Analytical TTL for the table." + } + }, + "throughput": { + "type": "int", + "nullable": true, + "metadata": { + "description": "Optional. Request units per second. Cannot be used with autoscaleSettingsMaxThroughput." } }, - "template": { - "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", - "languageVersion": "2.0", - "contentVersion": "1.0.0.0", + "autoscaleSettingsMaxThroughput": { + "type": "int", + "nullable": true, "metadata": { - "_generator": { - "name": "bicep", - "version": "0.34.44.8038", - "templateHash": "10218370167882238860" + "description": "Optional. Maximum autoscale throughput for the table. Cannot be used with throughput." + } + } + }, + "metadata": { + "__bicep_export!": true, + "description": "The type of a Cassandra table." + } + }, + "viewType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "metadata": { + "description": "Required. Name of the view." + } + }, + "viewDefinition": { + "type": "string", + "metadata": { + "description": "Required. View definition (CQL statement)." + } + }, + "tags": { + "type": "object", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.DocumentDB/databaseAccounts/cassandraKeyspaces/views@2025-05-01-preview#properties/tags" }, - "name": "Network Interface", - "description": "This module deploys a Network Interface." + "description": "Optional. Tags for the view." }, - "definitions": { - "networkInterfaceIPConfigurationType": { - "type": "object", - "properties": { - "name": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The name of the IP configuration." - } - }, - "privateIPAllocationMethod": { - "type": "string", - "allowedValues": [ - "Dynamic", - "Static" - ], - "nullable": true, - "metadata": { - "description": "Optional. The private IP address allocation method." - } - }, - "privateIPAddress": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The private IP address." - } - }, - "publicIPAddressResourceId": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The resource ID of the public IP address." - } - }, - "subnetResourceId": { - "type": "string", - "metadata": { - "description": "Required. The resource ID of the subnet." - } - }, - "loadBalancerBackendAddressPools": { - "type": "array", - "items": { - "$ref": "#/definitions/backendAddressPoolType" - }, - "nullable": true, - "metadata": { - "description": "Optional. Array of load balancer backend address pools." - } - }, - "loadBalancerInboundNatRules": { - "type": "array", - "items": { - "$ref": "#/definitions/inboundNatRuleType" - }, - "nullable": true, - "metadata": { - "description": "Optional. A list of references of LoadBalancerInboundNatRules." - } - }, - "applicationSecurityGroups": { - "type": "array", - "items": { - "$ref": "#/definitions/applicationSecurityGroupType" - }, - "nullable": true, - "metadata": { - "description": "Optional. Application security groups in which the IP configuration is included." - } - }, - "applicationGatewayBackendAddressPools": { - "type": "array", - "items": { - "$ref": "#/definitions/applicationGatewayBackendAddressPoolsType" - }, - "nullable": true, - "metadata": { - "description": "Optional. The reference to Application Gateway Backend Address Pools." - } - }, - "gatewayLoadBalancer": { - "$ref": "#/definitions/subResourceType", - "nullable": true, - "metadata": { - "description": "Optional. The reference to gateway load balancer frontend IP." - } - }, - "privateIPAddressVersion": { - "type": "string", - "allowedValues": [ - "IPv4", - "IPv6" - ], - "nullable": true, - "metadata": { - "description": "Optional. Whether the specific IP configuration is IPv4 or IPv6." - } - }, - "virtualNetworkTaps": { - "type": "array", - "items": { - "$ref": "#/definitions/virtualNetworkTapType" - }, - "nullable": true, - "metadata": { - "description": "Optional. The reference to Virtual Network Taps." - } - } - }, - "metadata": { - "__bicep_export!": true, - "description": "The resource ID of the deployed resource." - } - }, - "backendAddressPoolType": { - "type": "object", - "properties": { - "id": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The resource ID of the backend address pool." - } - }, - "name": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The name of the backend address pool." - } - }, - "properties": { - "type": "object", - "nullable": true, - "metadata": { - "description": "Optional. The properties of the backend address pool." - } - } - }, - "metadata": { - "__bicep_export!": true, - "description": "The type for a backend address pool." - } - }, - "applicationSecurityGroupType": { - "type": "object", - "properties": { - "id": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. Resource ID of the application security group." - } - }, - "location": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. Location of the application security group." - } - }, - "properties": { - "type": "object", - "nullable": true, - "metadata": { - "description": "Optional. Properties of the application security group." - } - }, - "tags": { - "type": "object", - "nullable": true, - "metadata": { - "description": "Optional. Tags of the application security group." - } - } - }, - "metadata": { - "__bicep_export!": true, - "description": "The type for the application security group." - } - }, - "applicationGatewayBackendAddressPoolsType": { - "type": "object", - "properties": { - "id": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. Resource ID of the backend address pool." - } - }, - "name": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. Name of the backend address pool that is unique within an Application Gateway." - } - }, - "properties": { - "type": "object", - "properties": { - "backendAddresses": { - "type": "array", - "items": { - "type": "object", - "properties": { - "ipAddress": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. IP address of the backend address." - } - }, - "fqdn": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. FQDN of the backend address." - } - } - } - }, - "nullable": true, - "metadata": { - "description": "Optional. Backend addresses." - } - } - }, - "nullable": true, - "metadata": { - "description": "Optional. Properties of the application gateway backend address pool." - } - } - }, - "metadata": { - "__bicep_export!": true, - "description": "The type for the application gateway backend address pool." - } - }, - "subResourceType": { - "type": "object", - "properties": { - "id": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. Resource ID of the sub resource." - } - } - }, - "metadata": { - "__bicep_export!": true, - "description": "The type for the sub resource." - } - }, - "inboundNatRuleType": { - "type": "object", - "properties": { - "id": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. Resource ID of the inbound NAT rule." - } - }, - "name": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. Name of the resource that is unique within the set of inbound NAT rules used by the load balancer. This name can be used to access the resource." - } - }, - "properties": { - "type": "object", - "properties": { - "backendAddressPool": { - "$ref": "#/definitions/subResourceType", - "nullable": true, - "metadata": { - "description": "Optional. A reference to backendAddressPool resource." - } - }, - "backendPort": { - "type": "int", - "nullable": true, - "metadata": { - "description": "Optional. The port used for the internal endpoint. Acceptable values range from 1 to 65535." - } - }, - "enableFloatingIP": { - "type": "bool", - "nullable": true, - "metadata": { - "description": "Optional. Configures a virtual machine's endpoint for the floating IP capability required to configure a SQL AlwaysOn Availability Group. This setting is required when using the SQL AlwaysOn Availability Groups in SQL server. This setting can't be changed after you create the endpoint." - } - }, - "enableTcpReset": { - "type": "bool", - "nullable": true, - "metadata": { - "description": "Optional. Receive bidirectional TCP Reset on TCP flow idle timeout or unexpected connection termination. This element is only used when the protocol is set to TCP." - } - }, - "frontendIPConfiguration": { - "$ref": "#/definitions/subResourceType", - "nullable": true, - "metadata": { - "description": "Optional. A reference to frontend IP addresses." - } - }, - "frontendPort": { - "type": "int", - "nullable": true, - "metadata": { - "description": "Optional. The port for the external endpoint. Port numbers for each rule must be unique within the Load Balancer. Acceptable values range from 1 to 65534." - } - }, - "frontendPortRangeStart": { - "type": "int", - "nullable": true, - "metadata": { - "description": "Optional. The port range start for the external endpoint. This property is used together with BackendAddressPool and FrontendPortRangeEnd. Individual inbound NAT rule port mappings will be created for each backend address from BackendAddressPool. Acceptable values range from 1 to 65534." - } - }, - "frontendPortRangeEnd": { - "type": "int", - "nullable": true, - "metadata": { - "description": "Optional. The port range end for the external endpoint. This property is used together with BackendAddressPool and FrontendPortRangeStart. Individual inbound NAT rule port mappings will be created for each backend address from BackendAddressPool. Acceptable values range from 1 to 65534." - } - }, - "protocol": { - "type": "string", - "allowedValues": [ - "All", - "Tcp", - "Udp" - ], - "nullable": true, - "metadata": { - "description": "Optional. The reference to the transport protocol used by the load balancing rule." - } - } - }, - "nullable": true, - "metadata": { - "description": "Optional. Properties of the inbound NAT rule." - } - } - }, - "metadata": { - "__bicep_export!": true, - "description": "The type for the inbound NAT rule." - } + "nullable": true + }, + "throughput": { + "type": "int", + "nullable": true, + "metadata": { + "description": "Optional. Request units per second. Cannot be used with autoscaleSettingsMaxThroughput." + } + }, + "autoscaleSettingsMaxThroughput": { + "type": "int", + "nullable": true, + "metadata": { + "description": "Optional. Maximum autoscale throughput for the view. Cannot be used with throughput." + } + } + }, + "metadata": { + "__bicep_export!": true, + "description": "The type of a Cassandra view (materialized view)." + } + } + }, + "parameters": { + "name": { + "type": "string", + "metadata": { + "description": "Required. Name of the Cassandra keyspace." + } + }, + "tags": { + "type": "object", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.DocumentDB/databaseAccounts/cassandraKeyspaces@2024-11-15#properties/tags" + }, + "description": "Optional. Tags of the Cassandra keyspace resource." + }, + "nullable": true + }, + "databaseAccountName": { + "type": "string", + "metadata": { + "description": "Conditional. The name of the parent Cosmos DB account. Required if the template is used in a standalone deployment." + } + }, + "tables": { + "type": "array", + "items": { + "$ref": "#/definitions/tableType" + }, + "defaultValue": [], + "metadata": { + "description": "Optional. Array of Cassandra tables to deploy in the keyspace." + } + }, + "views": { + "type": "array", + "items": { + "$ref": "#/definitions/viewType" + }, + "defaultValue": [], + "metadata": { + "description": "Optional. Array of Cassandra views (materialized views) to deploy in the keyspace." + } + }, + "autoscaleSettingsMaxThroughput": { + "type": "int", + "defaultValue": 4000, + "metadata": { + "description": "Optional. Maximum autoscale throughput for the keyspace. If not set, autoscale will be disabled. Setting throughput at the keyspace level is only recommended for development/test or when workload across all tables in the shared throughput keyspace is uniform. For best performance for large production workloads, it is recommended to set dedicated throughput (autoscale or manual) at the table level." + } + }, + "throughput": { + "type": "int", + "nullable": true, + "metadata": { + "description": "Optional. Request units per second. Cannot be used with autoscaleSettingsMaxThroughput. Setting throughput at the keyspace level is only recommended for development/test or when workload across all tables in the shared throughput keyspace is uniform. For best performance for large production workloads, it is recommended to set dedicated throughput (autoscale or manual) at the table level." + } + } + }, + "resources": { + "databaseAccount": { + "existing": true, + "type": "Microsoft.DocumentDB/databaseAccounts", + "apiVersion": "2024-11-15", + "name": "[parameters('databaseAccountName')]" + }, + "cassandraKeyspace": { + "type": "Microsoft.DocumentDB/databaseAccounts/cassandraKeyspaces", + "apiVersion": "2024-11-15", + "name": "[format('{0}/{1}', parameters('databaseAccountName'), parameters('name'))]", + "tags": "[parameters('tags')]", + "properties": { + "options": "[if(contains(reference('databaseAccount').capabilities, createObject('name', 'EnableServerless')), createObject(), createObject('autoscaleSettings', if(equals(parameters('throughput'), null()), createObject('maxThroughput', parameters('autoscaleSettingsMaxThroughput')), null()), 'throughput', parameters('throughput')))]", + "resource": { + "id": "[parameters('name')]" + } + }, + "dependsOn": [ + "databaseAccount" + ] + }, + "cassandraKeyspace_tables": { + "copy": { + "name": "cassandraKeyspace_tables", + "count": "[length(parameters('tables'))]" + }, + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "[format('{0}-cassandradb-{1}', uniqueString(deployment().name, parameters('name')), parameters('tables')[copyIndex()].name)]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "name": { + "value": "[parameters('tables')[copyIndex()].name]" + }, + "cassandraKeyspaceName": { + "value": "[parameters('name')]" + }, + "databaseAccountName": { + "value": "[parameters('databaseAccountName')]" + }, + "schema": { + "value": "[parameters('tables')[copyIndex()].schema]" + }, + "analyticalStorageTtl": { + "value": "[tryGet(parameters('tables')[copyIndex()], 'analyticalStorageTtl')]" + }, + "throughput": { + "value": "[tryGet(parameters('tables')[copyIndex()], 'throughput')]" + }, + "autoscaleSettingsMaxThroughput": { + "value": "[tryGet(parameters('tables')[copyIndex()], 'autoscaleSettingsMaxThroughput')]" + }, + "defaultTtl": { + "value": "[tryGet(parameters('tables')[copyIndex()], 'defaultTtl')]" + }, + "tags": { + "value": "[coalesce(tryGet(parameters('tables')[copyIndex()], 'tags'), parameters('tags'))]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "languageVersion": "2.0", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.38.33.27573", + "templateHash": "785607874724829202" }, - "virtualNetworkTapType": { - "type": "object", - "properties": { - "id": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. Resource ID of the virtual network tap." - } - }, - "location": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. Location of the virtual network tap." - } - }, - "properties": { - "type": "object", - "nullable": true, - "metadata": { - "description": "Optional. Properties of the virtual network tap." - } - }, - "tags": { - "type": "object", - "nullable": true, - "metadata": { - "description": "Optional. Tags of the virtual network tap." - } - } - }, + "name": "DocumentDB Database Account Cassandra Keyspaces Tables", + "description": "This module deploys a Cassandra Table within a Cassandra Keyspace in a CosmosDB Account." + }, + "parameters": { + "name": { + "type": "string", "metadata": { - "__bicep_export!": true, - "description": "The type for the virtual network tap." + "description": "Required. Name of the Cassandra table." } }, - "networkInterfaceIPConfigurationOutputType": { + "tags": { "type": "object", - "properties": { - "name": { - "type": "string", - "metadata": { - "description": "The name of the IP configuration." - } - }, - "privateIP": { - "type": "string", - "nullable": true, - "metadata": { - "description": "The private IP address." - } - }, - "publicIP": { - "type": "string", - "nullable": true, - "metadata": { - "description": "The public IP address." - } - } - }, "metadata": { - "__bicep_export!": true, - "description": "The type for the network interface IP configuration output." - } - }, - "diagnosticSettingFullType": { - "type": "object", - "properties": { - "name": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The name of the diagnostic setting." - } - }, - "logCategoriesAndGroups": { - "type": "array", - "items": { - "type": "object", - "properties": { - "category": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. Name of a Diagnostic Log category for a resource type this setting is applied to. Set the specific logs to collect here." - } - }, - "categoryGroup": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. Name of a Diagnostic Log category group for a resource type this setting is applied to. Set to `allLogs` to collect all logs." - } - }, - "enabled": { - "type": "bool", - "nullable": true, - "metadata": { - "description": "Optional. Enable or disable the category explicitly. Default is `true`." - } - } - } - }, - "nullable": true, - "metadata": { - "description": "Optional. The name of logs that will be streamed. \"allLogs\" includes all possible logs for the resource. Set to `[]` to disable log collection." - } - }, - "metricCategories": { - "type": "array", - "items": { - "type": "object", - "properties": { - "category": { - "type": "string", - "metadata": { - "description": "Required. Name of a Diagnostic Metric category for a resource type this setting is applied to. Set to `AllMetrics` to collect all metrics." - } - }, - "enabled": { - "type": "bool", - "nullable": true, - "metadata": { - "description": "Optional. Enable or disable the category explicitly. Default is `true`." - } - } - } - }, - "nullable": true, - "metadata": { - "description": "Optional. The name of metrics that will be streamed. \"allMetrics\" includes all possible metrics for the resource. Set to `[]` to disable metric collection." - } - }, - "logAnalyticsDestinationType": { - "type": "string", - "allowedValues": [ - "AzureDiagnostics", - "Dedicated" - ], - "nullable": true, - "metadata": { - "description": "Optional. A string indicating whether the export to Log Analytics should use the default destination type, i.e. AzureDiagnostics, or use a destination type." - } - }, - "workspaceResourceId": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. Resource ID of the diagnostic log analytics workspace. For security reasons, it is recommended to set diagnostic settings to send data to either storage account, log analytics workspace or event hub." - } - }, - "storageAccountResourceId": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. Resource ID of the diagnostic storage account. For security reasons, it is recommended to set diagnostic settings to send data to either storage account, log analytics workspace or event hub." - } - }, - "eventHubAuthorizationRuleResourceId": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. Resource ID of the diagnostic event hub authorization rule for the Event Hubs namespace in which the event hub should be created or streamed to." - } - }, - "eventHubName": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. Name of the diagnostic event hub within the namespace to which logs are streamed. Without this, an event hub is created for each log category. For security reasons, it is recommended to set diagnostic settings to send data to either storage account, log analytics workspace or event hub." - } + "__bicep_resource_derived_type!": { + "source": "Microsoft.DocumentDB/databaseAccounts/cassandraKeyspaces/tables@2024-11-15#properties/tags" }, - "marketplacePartnerResourceId": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The full ARM resource ID of the Marketplace resource to which you would like to send Diagnostic Logs." - } - } + "description": "Optional. Tags of the Cassandra table resource." }, + "nullable": true + }, + "databaseAccountName": { + "type": "string", "metadata": { - "description": "An AVM-aligned type for a diagnostic setting. To be used if both logs & metrics are supported by the resource provider.", - "__bicep_imported_from!": { - "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.5.1" - } + "description": "Conditional. The name of the parent Database Account. Required if the template is used in a standalone deployment." } }, - "lockType": { - "type": "object", - "properties": { - "name": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. Specify the name of lock." - } - }, - "kind": { - "type": "string", - "allowedValues": [ - "CanNotDelete", - "None", - "ReadOnly" - ], - "nullable": true, - "metadata": { - "description": "Optional. Specify the type of lock." - } - } - }, + "cassandraKeyspaceName": { + "type": "string", "metadata": { - "description": "An AVM-aligned type for a lock.", - "__bicep_imported_from!": { - "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.5.1" - } + "description": "Conditional. The name of the parent Cassandra Keyspace. Required if the template is used in a standalone deployment." } }, - "roleAssignmentType": { + "schema": { "type": "object", - "properties": { - "name": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The name (as GUID) of the role assignment. If not provided, a GUID will be generated." - } - }, - "roleDefinitionIdOrName": { - "type": "string", - "metadata": { - "description": "Required. The role to assign. You can provide either the display name of the role definition, the role definition GUID, or its fully qualified ID in the following format: '/providers/Microsoft.Authorization/roleDefinitions/c2f4ef07-c644-48eb-af81-4b1b4947fb11'." - } - }, - "principalId": { - "type": "string", - "metadata": { - "description": "Required. The principal ID of the principal (user/group/identity) to assign the role to." - } - }, - "principalType": { - "type": "string", - "allowedValues": [ - "Device", - "ForeignGroup", - "Group", - "ServicePrincipal", - "User" - ], - "nullable": true, - "metadata": { - "description": "Optional. The principal type of the assigned principal ID." - } - }, - "description": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The description of the role assignment." - } - }, - "condition": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The conditions on the role assignment. This limits the resources it can be assigned to. e.g.: @Resource[Microsoft.Storage/storageAccounts/blobServices/containers:ContainerName] StringEqualsIgnoreCase \"foo_storage_container\"." - } - }, - "conditionVersion": { - "type": "string", - "allowedValues": [ - "2.0" - ], - "nullable": true, - "metadata": { - "description": "Optional. Version of the condition." - } - }, - "delegatedManagedIdentityResourceId": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. The Resource Id of the delegated managed identity resource." - } - } - }, - "metadata": { - "description": "An AVM-aligned type for a role assignment.", - "__bicep_imported_from!": { - "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.5.1" - } - } - } - }, - "parameters": { - "name": { - "type": "string", "metadata": { - "description": "Required. The name of the network interface." + "__bicep_resource_derived_type!": { + "source": "Microsoft.DocumentDB/databaseAccounts/cassandraKeyspaces/tables@2024-11-15#properties/properties/properties/resource/properties/schema" + }, + "description": "Required. Schema definition for the Cassandra table." } }, - "location": { - "type": "string", - "defaultValue": "[resourceGroup().location]", + "analyticalStorageTtl": { + "type": "int", + "defaultValue": 0, "metadata": { - "description": "Optional. Location for all resources." + "description": "Optional. Analytical TTL for the table. Default to 0 (disabled). Analytical store is enabled when set to a value other than 0. If set to -1, analytical store retains all historical data." } }, - "tags": { - "type": "object", + "throughput": { + "type": "int", "nullable": true, "metadata": { - "description": "Optional. Resource tags." + "description": "Optional. Request units per second. Cannot be used with autoscaleSettingsMaxThroughput. If not specified, the table will inherit throughput from the keyspace." } }, - "enableTelemetry": { - "type": "bool", - "defaultValue": true, + "autoscaleSettingsMaxThroughput": { + "type": "int", + "nullable": true, "metadata": { - "description": "Optional. Enable/Disable usage telemetry for module." + "description": "Optional. Maximum autoscale throughput for the table. Cannot be used with throughput. If not specified, the table will inherit throughput from the keyspace." } }, - "enableIPForwarding": { - "type": "bool", - "defaultValue": false, + "defaultTtl": { + "type": "int", + "defaultValue": 0, "metadata": { - "description": "Optional. Indicates whether IP forwarding is enabled on this network interface." + "description": "Optional. Default time to live in seconds. Default to 0 (disabled). If set to -1, items do not expire." } + } + }, + "resources": { + "databaseAccount::cassandraKeyspace": { + "existing": true, + "type": "Microsoft.DocumentDB/databaseAccounts/cassandraKeyspaces", + "apiVersion": "2024-11-15", + "name": "[format('{0}/{1}', parameters('databaseAccountName'), parameters('cassandraKeyspaceName'))]" }, - "enableAcceleratedNetworking": { - "type": "bool", - "defaultValue": false, - "metadata": { - "description": "Optional. If the network interface is accelerated networking enabled." - } + "databaseAccount": { + "existing": true, + "type": "Microsoft.DocumentDB/databaseAccounts", + "apiVersion": "2024-11-15", + "name": "[parameters('databaseAccountName')]" }, - "dnsServers": { - "type": "array", - "items": { - "type": "string" + "cassandraTable": { + "type": "Microsoft.DocumentDB/databaseAccounts/cassandraKeyspaces/tables", + "apiVersion": "2024-11-15", + "name": "[format('{0}/{1}/{2}', parameters('databaseAccountName'), parameters('cassandraKeyspaceName'), parameters('name'))]", + "tags": "[parameters('tags')]", + "properties": { + "resource": { + "id": "[parameters('name')]", + "schema": "[parameters('schema')]", + "defaultTtl": "[parameters('defaultTtl')]", + "analyticalStorageTtl": "[parameters('analyticalStorageTtl')]" + }, + "options": "[if(contains(reference('databaseAccount').capabilities, createObject('name', 'EnableServerless')), createObject(), createObject('autoscaleSettings', if(and(equals(parameters('throughput'), null()), not(equals(parameters('autoscaleSettingsMaxThroughput'), null()))), createObject('maxThroughput', parameters('autoscaleSettingsMaxThroughput')), null()), 'throughput', parameters('throughput')))]" }, - "defaultValue": [], + "dependsOn": [ + "databaseAccount" + ] + } + }, + "outputs": { + "name": { + "type": "string", "metadata": { - "description": "Optional. List of DNS servers IP addresses. Use 'AzureProvidedDNS' to switch to azure provided DNS resolution. 'AzureProvidedDNS' value cannot be combined with other IPs, it must be the only value in dnsServers collection." - } + "description": "The name of the Cassandra table." + }, + "value": "[parameters('name')]" }, - "networkSecurityGroupResourceId": { + "resourceId": { "type": "string", - "defaultValue": "", "metadata": { - "description": "Optional. The network security group (NSG) to attach to the network interface." - } + "description": "The resource ID of the Cassandra table." + }, + "value": "[resourceId('Microsoft.DocumentDB/databaseAccounts/cassandraKeyspaces/tables', parameters('databaseAccountName'), parameters('cassandraKeyspaceName'), parameters('name'))]" }, - "auxiliaryMode": { + "resourceGroupName": { + "type": "string", + "metadata": { + "description": "The name of the resource group the Cassandra table was created in." + }, + "value": "[resourceGroup().name]" + } + } + } + }, + "dependsOn": [ + "cassandraKeyspace" + ] + }, + "cassandraKeyspace_views": { + "copy": { + "name": "cassandraKeyspace_views", + "count": "[length(parameters('views'))]" + }, + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "[format('{0}-cassandraview-{1}', uniqueString(deployment().name, parameters('name')), parameters('views')[copyIndex()].name)]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "name": { + "value": "[parameters('views')[copyIndex()].name]" + }, + "cassandraKeyspaceName": { + "value": "[parameters('name')]" + }, + "databaseAccountName": { + "value": "[parameters('databaseAccountName')]" + }, + "viewDefinition": { + "value": "[parameters('views')[copyIndex()].viewDefinition]" + }, + "throughput": { + "value": "[tryGet(parameters('views')[copyIndex()], 'throughput')]" + }, + "autoscaleSettingsMaxThroughput": { + "value": "[tryGet(parameters('views')[copyIndex()], 'autoscaleSettingsMaxThroughput')]" + }, + "tags": { + "value": "[coalesce(tryGet(parameters('views')[copyIndex()], 'tags'), parameters('tags'))]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "languageVersion": "2.0", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.38.33.27573", + "templateHash": "14021794949328228224" + }, + "name": "DocumentDB Database Account Cassandra Keyspaces Views", + "description": "This module deploys a Cassandra View (Materialized View) within a Cassandra Keyspace in a CosmosDB Account." + }, + "parameters": { + "name": { "type": "string", - "defaultValue": "None", - "allowedValues": [ - "Floating", - "MaxConnections", - "None" - ], "metadata": { - "description": "Optional. Auxiliary mode of Network Interface resource. Not all regions are enabled for Auxiliary Mode Nic." + "description": "Required. Name of the Cassandra view." } }, - "auxiliarySku": { + "tags": { + "type": "object", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.DocumentDB/databaseAccounts/cassandraKeyspaces/views@2025-05-01-preview#properties/tags" + }, + "description": "Optional. Tags of the Cassandra view resource." + }, + "nullable": true + }, + "databaseAccountName": { "type": "string", - "defaultValue": "None", - "allowedValues": [ - "A1", - "A2", - "A4", - "A8", - "None" - ], "metadata": { - "description": "Optional. Auxiliary sku of Network Interface resource. Not all regions are enabled for Auxiliary Mode Nic." + "description": "Conditional. The name of the parent Database Account. Required if the template is used in a standalone deployment." } }, - "disableTcpStateTracking": { - "type": "bool", - "defaultValue": false, + "cassandraKeyspaceName": { + "type": "string", "metadata": { - "description": "Optional. Indicates whether to disable tcp state tracking. Subscription must be registered for the Microsoft.Network/AllowDisableTcpStateTracking feature before this property can be set to true." + "description": "Conditional. The name of the parent Cassandra Keyspace. Required if the template is used in a standalone deployment." } }, - "ipConfigurations": { - "type": "array", - "items": { - "$ref": "#/definitions/networkInterfaceIPConfigurationType" - }, + "viewDefinition": { + "type": "string", "metadata": { - "description": "Required. A list of IPConfigurations of the network interface." + "description": "Required. View definition of the Cassandra view. This is the CQL statement that defines the materialized view." } }, - "lock": { - "$ref": "#/definitions/lockType", + "throughput": { + "type": "int", "nullable": true, "metadata": { - "description": "Optional. The lock settings of the service." + "description": "Optional. Request units per second. Cannot be used with autoscaleSettingsMaxThroughput." } }, - "roleAssignments": { - "type": "array", - "items": { - "$ref": "#/definitions/roleAssignmentType" - }, + "autoscaleSettingsMaxThroughput": { + "type": "int", "nullable": true, "metadata": { - "description": "Optional. Array of role assignments to create." + "description": "Optional. Maximum autoscale throughput for the view. Cannot be used with throughput." } }, - "diagnosticSettings": { - "type": "array", - "items": { - "$ref": "#/definitions/diagnosticSettingFullType" - }, - "nullable": true, + "location": { + "type": "string", + "defaultValue": "[resourceGroup().location]", "metadata": { - "description": "Optional. The diagnostic settings of the service." - } - } - }, - "variables": { - "copy": [ - { - "name": "formattedRoleAssignments", - "count": "[length(coalesce(parameters('roleAssignments'), createArray()))]", - "input": "[union(coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')], createObject('roleDefinitionId', coalesce(tryGet(variables('builtInRoleNames'), coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName), if(contains(coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName, '/providers/Microsoft.Authorization/roleDefinitions/'), coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName, subscriptionResourceId('Microsoft.Authorization/roleDefinitions', coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName)))))]" + "description": "Optional. Location for all resources." } - ], - "builtInRoleNames": { - "Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'b24988ac-6180-42a0-ab88-20f7382dd24c')]", - "DNS Resolver Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '0f2ebee7-ffd4-4fc0-b3b7-664099fdad5d')]", - "DNS Zone Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'befefa01-2a29-4197-83a8-272ff33ce314')]", - "Network Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '4d97b98b-1d4f-4787-a291-c67834d212e7')]", - "Owner": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '8e3af657-a8ff-443c-a75c-2fe8c4bcb635')]", - "Private DNS Zone Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'b12aa53e-6015-4669-85d0-8515ebb3ae7f')]", - "Reader": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'acdd72a7-3385-48ef-bd42-f606fba81ae7')]", - "Role Based Access Control Administrator": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'f58310d9-a9f6-439a-9e8d-f62e7b41a168')]" } }, "resources": { - "publicIp": { - "copy": { - "name": "publicIp", - "count": "[length(parameters('ipConfigurations'))]" - }, - "condition": "[and(contains(parameters('ipConfigurations')[copyIndex()], 'publicIPAddressResourceId'), not(equals(tryGet(parameters('ipConfigurations')[copyIndex()], 'publicIPAddressResourceId'), null())))]", + "databaseAccount::cassandraKeyspace": { "existing": true, - "type": "Microsoft.Network/publicIPAddresses", - "apiVersion": "2024-05-01", - "resourceGroup": "[split(coalesce(tryGet(parameters('ipConfigurations')[copyIndex()], 'publicIPAddressResourceId'), ''), '/')[4]]", - "name": "[last(split(coalesce(tryGet(parameters('ipConfigurations')[copyIndex()], 'publicIPAddressResourceId'), ''), '/'))]" + "type": "Microsoft.DocumentDB/databaseAccounts/cassandraKeyspaces", + "apiVersion": "2025-05-01-preview", + "name": "[format('{0}/{1}', parameters('databaseAccountName'), parameters('cassandraKeyspaceName'))]" }, - "avmTelemetry": { - "condition": "[parameters('enableTelemetry')]", - "type": "Microsoft.Resources/deployments", - "apiVersion": "2024-03-01", - "name": "[format('46d3xbcp.res.network-networkinterface.{0}.{1}', replace('0.5.2', '.', '-'), substring(uniqueString(deployment().name, parameters('location')), 0, 4))]", - "properties": { - "mode": "Incremental", - "template": { - "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", - "contentVersion": "1.0.0.0", - "resources": [], - "outputs": { - "telemetry": { - "type": "String", - "value": "For more information, see https://aka.ms/avm/TelemetryInfo" - } - } - } - } + "databaseAccount": { + "existing": true, + "type": "Microsoft.DocumentDB/databaseAccounts", + "apiVersion": "2025-05-01-preview", + "name": "[parameters('databaseAccountName')]" }, - "networkInterface": { - "type": "Microsoft.Network/networkInterfaces", - "apiVersion": "2024-05-01", - "name": "[parameters('name')]", - "location": "[parameters('location')]", + "cassandraView": { + "type": "Microsoft.DocumentDB/databaseAccounts/cassandraKeyspaces/views", + "apiVersion": "2025-05-01-preview", + "name": "[format('{0}/{1}/{2}', parameters('databaseAccountName'), parameters('cassandraKeyspaceName'), parameters('name'))]", "tags": "[parameters('tags')]", + "location": "[parameters('location')]", "properties": { - "copy": [ - { - "name": "ipConfigurations", - "count": "[length(parameters('ipConfigurations'))]", - "input": { - "name": "[coalesce(tryGet(parameters('ipConfigurations')[copyIndex('ipConfigurations')], 'name'), format('ipconfig{0}', padLeft(add(copyIndex('ipConfigurations'), 1), 2, '0')))]", - "properties": { - "primary": "[if(equals(copyIndex('ipConfigurations'), 0), true(), false())]", - "privateIPAllocationMethod": "[tryGet(parameters('ipConfigurations')[copyIndex('ipConfigurations')], 'privateIPAllocationMethod')]", - "privateIPAddress": "[tryGet(parameters('ipConfigurations')[copyIndex('ipConfigurations')], 'privateIPAddress')]", - "publicIPAddress": "[if(contains(parameters('ipConfigurations')[copyIndex('ipConfigurations')], 'publicIPAddressResourceId'), if(not(equals(tryGet(parameters('ipConfigurations')[copyIndex('ipConfigurations')], 'publicIPAddressResourceId'), null())), createObject('id', tryGet(parameters('ipConfigurations')[copyIndex('ipConfigurations')], 'publicIPAddressResourceId')), null()), null())]", - "subnet": { - "id": "[parameters('ipConfigurations')[copyIndex('ipConfigurations')].subnetResourceId]" - }, - "loadBalancerBackendAddressPools": "[tryGet(parameters('ipConfigurations')[copyIndex('ipConfigurations')], 'loadBalancerBackendAddressPools')]", - "applicationSecurityGroups": "[tryGet(parameters('ipConfigurations')[copyIndex('ipConfigurations')], 'applicationSecurityGroups')]", - "applicationGatewayBackendAddressPools": "[tryGet(parameters('ipConfigurations')[copyIndex('ipConfigurations')], 'applicationGatewayBackendAddressPools')]", - "gatewayLoadBalancer": "[tryGet(parameters('ipConfigurations')[copyIndex('ipConfigurations')], 'gatewayLoadBalancer')]", - "loadBalancerInboundNatRules": "[tryGet(parameters('ipConfigurations')[copyIndex('ipConfigurations')], 'loadBalancerInboundNatRules')]", - "privateIPAddressVersion": "[tryGet(parameters('ipConfigurations')[copyIndex('ipConfigurations')], 'privateIPAddressVersion')]", - "virtualNetworkTaps": "[tryGet(parameters('ipConfigurations')[copyIndex('ipConfigurations')], 'virtualNetworkTaps')]" - } - } - } - ], - "auxiliaryMode": "[parameters('auxiliaryMode')]", - "auxiliarySku": "[parameters('auxiliarySku')]", - "disableTcpStateTracking": "[parameters('disableTcpStateTracking')]", - "dnsSettings": "[if(not(empty(parameters('dnsServers'))), createObject('dnsServers', parameters('dnsServers')), null())]", - "enableAcceleratedNetworking": "[parameters('enableAcceleratedNetworking')]", - "enableIPForwarding": "[parameters('enableIPForwarding')]", - "networkSecurityGroup": "[if(not(empty(parameters('networkSecurityGroupResourceId'))), createObject('id', parameters('networkSecurityGroupResourceId')), null())]" - } - }, - "networkInterface_lock": { - "condition": "[and(not(empty(coalesce(parameters('lock'), createObject()))), not(equals(tryGet(parameters('lock'), 'kind'), 'None')))]", - "type": "Microsoft.Authorization/locks", - "apiVersion": "2020-05-01", - "scope": "[format('Microsoft.Network/networkInterfaces/{0}', parameters('name'))]", - "name": "[coalesce(tryGet(parameters('lock'), 'name'), format('lock-{0}', parameters('name')))]", - "properties": { - "level": "[coalesce(tryGet(parameters('lock'), 'kind'), '')]", - "notes": "[if(equals(tryGet(parameters('lock'), 'kind'), 'CanNotDelete'), 'Cannot delete resource or child resources.', 'Cannot delete or modify the resource or child resources.')]" - }, - "dependsOn": [ - "networkInterface" - ] - }, - "networkInterface_diagnosticSettings": { - "copy": { - "name": "networkInterface_diagnosticSettings", - "count": "[length(coalesce(parameters('diagnosticSettings'), createArray()))]" - }, - "type": "Microsoft.Insights/diagnosticSettings", - "apiVersion": "2021-05-01-preview", - "scope": "[format('Microsoft.Network/networkInterfaces/{0}', parameters('name'))]", - "name": "[coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'name'), format('{0}-diagnosticSettings', parameters('name')))]", - "properties": { - "copy": [ - { - "name": "metrics", - "count": "[length(coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'metricCategories'), createArray(createObject('category', 'AllMetrics'))))]", - "input": { - "category": "[coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'metricCategories'), createArray(createObject('category', 'AllMetrics')))[copyIndex('metrics')].category]", - "enabled": "[coalesce(tryGet(coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'metricCategories'), createArray(createObject('category', 'AllMetrics')))[copyIndex('metrics')], 'enabled'), true())]", - "timeGrain": null - } - } - ], - "storageAccountId": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'storageAccountResourceId')]", - "workspaceId": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'workspaceResourceId')]", - "eventHubAuthorizationRuleId": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'eventHubAuthorizationRuleResourceId')]", - "eventHubName": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'eventHubName')]", - "marketplacePartnerId": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'marketplacePartnerResourceId')]", - "logAnalyticsDestinationType": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'logAnalyticsDestinationType')]" - }, - "dependsOn": [ - "networkInterface" - ] - }, - "networkInterface_roleAssignments": { - "copy": { - "name": "networkInterface_roleAssignments", - "count": "[length(coalesce(variables('formattedRoleAssignments'), createArray()))]" - }, - "type": "Microsoft.Authorization/roleAssignments", - "apiVersion": "2022-04-01", - "scope": "[format('Microsoft.Network/networkInterfaces/{0}', parameters('name'))]", - "name": "[coalesce(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'name'), guid(resourceId('Microsoft.Network/networkInterfaces', parameters('name')), coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].principalId, coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].roleDefinitionId))]", - "properties": { - "roleDefinitionId": "[coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].roleDefinitionId]", - "principalId": "[coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].principalId]", - "description": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'description')]", - "principalType": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'principalType')]", - "condition": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'condition')]", - "conditionVersion": "[if(not(empty(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'condition'))), coalesce(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'conditionVersion'), '2.0'), null())]", - "delegatedManagedIdentityResourceId": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'delegatedManagedIdentityResourceId')]" + "resource": { + "id": "[parameters('name')]", + "viewDefinition": "[parameters('viewDefinition')]" + }, + "options": "[if(contains(reference('databaseAccount').capabilities, createObject('name', 'EnableServerless')), createObject(), createObject('autoscaleSettings', if(and(equals(parameters('throughput'), null()), not(equals(parameters('autoscaleSettingsMaxThroughput'), null()))), createObject('maxThroughput', parameters('autoscaleSettingsMaxThroughput')), null()), 'throughput', parameters('throughput')))]" }, "dependsOn": [ - "networkInterface" + "databaseAccount" ] } }, @@ -39379,2211 +40645,2505 @@ "name": { "type": "string", "metadata": { - "description": "The name of the deployed resource." + "description": "The name of the Cassandra view." }, "value": "[parameters('name')]" }, "resourceId": { "type": "string", "metadata": { - "description": "The resource ID of the deployed resource." + "description": "The resource ID of the Cassandra view." }, - "value": "[resourceId('Microsoft.Network/networkInterfaces', parameters('name'))]" + "value": "[resourceId('Microsoft.DocumentDB/databaseAccounts/cassandraKeyspaces/views', parameters('databaseAccountName'), parameters('cassandraKeyspaceName'), parameters('name'))]" }, "resourceGroupName": { "type": "string", "metadata": { - "description": "The resource group of the deployed resource." + "description": "The name of the resource group the Cassandra view was created in." }, "value": "[resourceGroup().name]" - }, - "location": { - "type": "string", - "metadata": { - "description": "The location the resource was deployed into." - }, - "value": "[reference('networkInterface', '2024-05-01', 'full').location]" - }, - "ipConfigurations": { - "type": "array", - "items": { - "$ref": "#/definitions/networkInterfaceIPConfigurationOutputType" - }, - "metadata": { - "description": "The list of IP configurations of the network interface." - }, - "copy": { - "count": "[length(parameters('ipConfigurations'))]", - "input": { - "name": "[reference('networkInterface').ipConfigurations[copyIndex()].name]", - "privateIP": "[coalesce(tryGet(reference('networkInterface').ipConfigurations[copyIndex()].properties, 'privateIPAddress'), '')]", - "publicIP": "[if(and(contains(parameters('ipConfigurations')[copyIndex()], 'publicIPAddressResourceId'), not(equals(tryGet(parameters('ipConfigurations')[copyIndex()], 'publicIPAddressResourceId'), null()))), coalesce(reference(format('publicIp[{0}]', copyIndex())).ipAddress, ''), '')]" - } - } } } } }, - "dependsOn": [ - "networkInterface_publicIPAddresses" - ] - } - }, - "outputs": { - "name": { - "type": "string", - "metadata": { - "description": "The name of the network interface." - }, - "value": "[reference('networkInterface').outputs.name.value]" - }, - "ipConfigurations": { - "type": "array", - "items": { - "$ref": "#/definitions/networkInterfaceIPConfigurationOutputType" - }, + "dependsOn": [ + "cassandraKeyspace" + ] + } + }, + "outputs": { + "name": { + "type": "string", + "metadata": { + "description": "The name of the Cassandra keyspace." + }, + "value": "[parameters('name')]" + }, + "resourceId": { + "type": "string", + "metadata": { + "description": "The resource ID of the Cassandra keyspace." + }, + "value": "[resourceId('Microsoft.DocumentDB/databaseAccounts/cassandraKeyspaces', parameters('databaseAccountName'), parameters('name'))]" + }, + "resourceGroupName": { + "type": "string", + "metadata": { + "description": "The name of the resource group the Cassandra keyspace was created in." + }, + "value": "[resourceGroup().name]" + } + } + } + }, + "dependsOn": [ + "databaseAccount" + ] + }, + "databaseAccount_privateEndpoints": { + "copy": { + "name": "databaseAccount_privateEndpoints", + "count": "[length(coalesce(parameters('privateEndpoints'), createArray()))]" + }, + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "[format('{0}-dbAccount-PrivateEndpoint-{1}', uniqueString(deployment().name, parameters('location')), copyIndex())]", + "subscriptionId": "[split(coalesce(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'resourceGroupResourceId'), resourceGroup().id), '/')[2]]", + "resourceGroup": "[split(coalesce(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'resourceGroupResourceId'), resourceGroup().id), '/')[4]]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "name": { + "value": "[coalesce(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'name'), format('pep-{0}-{1}-{2}', last(split(resourceId('Microsoft.DocumentDB/databaseAccounts', parameters('name')), '/')), coalesce(parameters('privateEndpoints'), createArray())[copyIndex()].service, copyIndex()))]" + }, + "privateLinkServiceConnections": "[if(not(equals(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'isManualConnection'), true())), createObject('value', createArray(createObject('name', coalesce(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'privateLinkServiceConnectionName'), format('{0}-{1}-{2}', last(split(resourceId('Microsoft.DocumentDB/databaseAccounts', parameters('name')), '/')), coalesce(parameters('privateEndpoints'), createArray())[copyIndex()].service, copyIndex())), 'properties', createObject('privateLinkServiceId', resourceId('Microsoft.DocumentDB/databaseAccounts', parameters('name')), 'groupIds', createArray(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()].service))))), createObject('value', null()))]", + "manualPrivateLinkServiceConnections": "[if(equals(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'isManualConnection'), true()), createObject('value', createArray(createObject('name', coalesce(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'privateLinkServiceConnectionName'), format('{0}-{1}-{2}', last(split(resourceId('Microsoft.DocumentDB/databaseAccounts', parameters('name')), '/')), coalesce(parameters('privateEndpoints'), createArray())[copyIndex()].service, copyIndex())), 'properties', createObject('privateLinkServiceId', resourceId('Microsoft.DocumentDB/databaseAccounts', parameters('name')), 'groupIds', createArray(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()].service), 'requestMessage', coalesce(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'manualConnectionRequestMessage'), 'Manual approval required.'))))), createObject('value', null()))]", + "subnetResourceId": { + "value": "[coalesce(parameters('privateEndpoints'), createArray())[copyIndex()].subnetResourceId]" + }, + "enableTelemetry": { + "value": "[variables('enableReferencedModulesTelemetry')]" + }, + "location": { + "value": "[coalesce(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'location'), reference(split(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()].subnetResourceId, '/subnets/')[0], '2020-06-01', 'Full').location)]" + }, + "lock": { + "value": "[coalesce(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'lock'), parameters('lock'))]" + }, + "privateDnsZoneGroup": { + "value": "[tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'privateDnsZoneGroup')]" + }, + "roleAssignments": { + "value": "[tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'roleAssignments')]" + }, + "tags": { + "value": "[coalesce(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'tags'), parameters('tags'))]" + }, + "customDnsConfigs": { + "value": "[tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'customDnsConfigs')]" + }, + "ipConfigurations": { + "value": "[tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'ipConfigurations')]" + }, + "applicationSecurityGroupResourceIds": { + "value": "[tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'applicationSecurityGroupResourceIds')]" + }, + "customNetworkInterfaceName": { + "value": "[tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'customNetworkInterfaceName')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "languageVersion": "2.0", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.38.5.1644", + "templateHash": "16604612898799598358" + }, + "name": "Private Endpoints", + "description": "This module deploys a Private Endpoint." + }, + "definitions": { + "privateDnsZoneGroupType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The name of the Private DNS Zone Group." + } + }, + "privateDnsZoneGroupConfigs": { + "type": "array", + "items": { + "$ref": "#/definitions/privateDnsZoneGroupConfigType" + }, + "metadata": { + "description": "Required. The private DNS zone groups to associate the private endpoint. A DNS zone group can support up to 5 DNS zones." + } + } + }, + "metadata": { + "__bicep_export!": true, + "description": "The type of a private dns zone group." + } + }, + "lockType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Specify the name of lock." + } + }, + "kind": { + "type": "string", + "allowedValues": [ + "CanNotDelete", + "None", + "ReadOnly" + ], + "nullable": true, + "metadata": { + "description": "Optional. Specify the type of lock." + } + }, + "notes": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Specify the notes of the lock." + } + } + }, + "metadata": { + "description": "An AVM-aligned type for a lock.", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.1" + } + } + }, + "privateDnsZoneGroupConfigType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The name of the private DNS zone group config." + } + }, + "privateDnsZoneResourceId": { + "type": "string", + "metadata": { + "description": "Required. The resource id of the private DNS zone." + } + } + }, + "metadata": { + "description": "The type of a private DNS zone group configuration.", + "__bicep_imported_from!": { + "sourceTemplate": "private-dns-zone-group/main.bicep" + } + } + }, + "roleAssignmentType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The name (as GUID) of the role assignment. If not provided, a GUID will be generated." + } + }, + "roleDefinitionIdOrName": { + "type": "string", + "metadata": { + "description": "Required. The role to assign. You can provide either the display name of the role definition, the role definition GUID, or its fully qualified ID in the following format: '/providers/Microsoft.Authorization/roleDefinitions/c2f4ef07-c644-48eb-af81-4b1b4947fb11'." + } + }, + "principalId": { + "type": "string", + "metadata": { + "description": "Required. The principal ID of the principal (user/group/identity) to assign the role to." + } + }, + "principalType": { + "type": "string", + "allowedValues": [ + "Device", + "ForeignGroup", + "Group", + "ServicePrincipal", + "User" + ], + "nullable": true, + "metadata": { + "description": "Optional. The principal type of the assigned principal ID." + } + }, + "description": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The description of the role assignment." + } + }, + "condition": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The conditions on the role assignment. This limits the resources it can be assigned to. e.g.: @Resource[Microsoft.Storage/storageAccounts/blobServices/containers:ContainerName] StringEqualsIgnoreCase \"foo_storage_container\"." + } + }, + "conditionVersion": { + "type": "string", + "allowedValues": [ + "2.0" + ], + "nullable": true, + "metadata": { + "description": "Optional. Version of the condition." + } + }, + "delegatedManagedIdentityResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The Resource Id of the delegated managed identity resource." + } + } + }, "metadata": { - "description": "The list of IP configurations of the network interface." - }, - "value": "[reference('networkInterface').outputs.ipConfigurations.value]" - } - } - } - } - }, - "vm_domainJoinExtension": { - "condition": "[and(contains(parameters('extensionDomainJoinConfig'), 'enabled'), parameters('extensionDomainJoinConfig').enabled)]", - "type": "Microsoft.Resources/deployments", - "apiVersion": "2022-09-01", - "name": "[format('{0}-VM-DomainJoin', uniqueString(deployment().name, parameters('location')))]", - "properties": { - "expressionEvaluationOptions": { - "scope": "inner" - }, - "mode": "Incremental", - "parameters": { - "virtualMachineName": { - "value": "[parameters('name')]" - }, - "name": { - "value": "[coalesce(tryGet(parameters('extensionDomainJoinConfig'), 'name'), 'DomainJoin')]" - }, - "location": { - "value": "[parameters('location')]" - }, - "publisher": { - "value": "Microsoft.Compute" - }, - "type": { - "value": "JsonADDomainExtension" - }, - "typeHandlerVersion": { - "value": "[coalesce(tryGet(parameters('extensionDomainJoinConfig'), 'typeHandlerVersion'), '1.3')]" - }, - "autoUpgradeMinorVersion": { - "value": "[coalesce(tryGet(parameters('extensionDomainJoinConfig'), 'autoUpgradeMinorVersion'), true())]" - }, - "enableAutomaticUpgrade": { - "value": "[coalesce(tryGet(parameters('extensionDomainJoinConfig'), 'enableAutomaticUpgrade'), false())]" - }, - "settings": { - "value": "[parameters('extensionDomainJoinConfig').settings]" - }, - "supressFailures": { - "value": "[coalesce(tryGet(parameters('extensionDomainJoinConfig'), 'supressFailures'), false())]" - }, - "tags": { - "value": "[coalesce(tryGet(parameters('extensionDomainJoinConfig'), 'tags'), parameters('tags'))]" - }, - "protectedSettings": { - "value": { - "Password": "[parameters('extensionDomainJoinPassword')]" + "description": "An AVM-aligned type for a role assignment.", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.1" + } + } } - } - }, - "template": { - "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", - "languageVersion": "2.0", - "contentVersion": "1.0.0.0", - "metadata": { - "_generator": { - "name": "bicep", - "version": "0.37.4.10188", - "templateHash": "13125609748815648088" - }, - "name": "Virtual Machine Extensions", - "description": "This module deploys a Virtual Machine Extension." }, "parameters": { - "virtualMachineName": { - "type": "string", - "metadata": { - "description": "Conditional. The name of the parent virtual machine that extension is provisioned for. Required if the template is used in a standalone deployment." - } - }, "name": { "type": "string", "metadata": { - "description": "Required. The name of the virtual machine extension." + "description": "Required. Name of the private endpoint resource to create." } }, - "location": { + "subnetResourceId": { "type": "string", - "defaultValue": "[resourceGroup().location]", "metadata": { - "description": "Optional. The location the extension is deployed to." + "description": "Required. Resource ID of the subnet where the endpoint needs to be created." } }, - "publisher": { - "type": "string", + "applicationSecurityGroupResourceIds": { + "type": "array", + "items": { + "type": "string" + }, + "nullable": true, "metadata": { - "description": "Required. The name of the extension handler publisher." + "description": "Optional. Application security groups in which the private endpoint IP configuration is included." } }, - "type": { + "customNetworkInterfaceName": { "type": "string", + "nullable": true, "metadata": { - "description": "Required. Specifies the type of the extension; an example is \"CustomScriptExtension\"." + "description": "Optional. The custom name of the network interface attached to the private endpoint." } }, - "typeHandlerVersion": { - "type": "string", + "ipConfigurations": { + "type": "array", "metadata": { - "description": "Required. Specifies the version of the script handler." - } + "__bicep_resource_derived_type!": { + "source": "Microsoft.Network/privateEndpoints@2024-01-01#properties/properties/properties/ipConfigurations" + }, + "description": "Optional. A list of IP configurations of the private endpoint. This will be used to map to the First Party Service endpoints." + }, + "nullable": true }, - "autoUpgradeMinorVersion": { - "type": "bool", + "privateDnsZoneGroup": { + "$ref": "#/definitions/privateDnsZoneGroupType", + "nullable": true, "metadata": { - "description": "Required. Indicates whether the extension should use a newer minor version if one is available at deployment time. Once deployed, however, the extension will not upgrade minor versions unless redeployed, even with this property set to true." + "description": "Optional. The private DNS zone group to configure for the private endpoint." } }, - "forceUpdateTag": { + "location": { "type": "string", - "nullable": true, + "defaultValue": "[resourceGroup().location]", "metadata": { - "description": "Optional. How the extension handler should be forced to update even if the extension configuration has not changed." + "description": "Optional. Location for all Resources." } }, - "settings": { - "type": "object", + "lock": { + "$ref": "#/definitions/lockType", "nullable": true, "metadata": { - "description": "Optional. Any object that contains the extension specific settings." + "description": "Optional. The lock settings of the service." } }, - "protectedSettings": { - "type": "secureObject", + "roleAssignments": { + "type": "array", + "items": { + "$ref": "#/definitions/roleAssignmentType" + }, "nullable": true, "metadata": { - "description": "Optional. Any object that contains the extension specific protected settings." - } - }, - "supressFailures": { - "type": "bool", - "defaultValue": false, - "metadata": { - "description": "Optional. Indicates whether failures stemming from the extension will be suppressed (Operational failures such as not connecting to the VM will not be suppressed regardless of this value). The default is false." - } - }, - "enableAutomaticUpgrade": { - "type": "bool", - "metadata": { - "description": "Required. Indicates whether the extension should be automatically upgraded by the platform if there is a newer version of the extension available." + "description": "Optional. Array of role assignments to create." } }, "tags": { "type": "object", "metadata": { "__bicep_resource_derived_type!": { - "source": "Microsoft.Compute/virtualMachines/extensions@2024-11-01#properties/tags" + "source": "Microsoft.Network/privateEndpoints@2024-01-01#properties/tags" }, - "description": "Optional. Tags of the resource." + "description": "Optional. Tags to be applied on all resources/resource groups in this deployment." }, "nullable": true }, - "protectedSettingsFromKeyVault": { - "type": "object", + "customDnsConfigs": { + "type": "array", "metadata": { "__bicep_resource_derived_type!": { - "source": "Microsoft.Compute/virtualMachines/extensions@2024-11-01#properties/properties/properties/protectedSettingsFromKeyVault" + "source": "Microsoft.Network/privateEndpoints@2024-01-01#properties/properties/properties/customDnsConfigs" }, - "description": "Optional. The extensions protected settings that are passed by reference, and consumed from key vault." + "description": "Optional. Custom DNS configurations." }, "nullable": true }, - "provisionAfterExtensions": { + "manualPrivateLinkServiceConnections": { "type": "array", "metadata": { "__bicep_resource_derived_type!": { - "source": "Microsoft.Compute/virtualMachines/extensions@2024-11-01#properties/properties/properties/provisionAfterExtensions" + "source": "Microsoft.Network/privateEndpoints@2024-01-01#properties/properties/properties/manualPrivateLinkServiceConnections" }, - "description": "Optional. Collection of extension names after which this extension needs to be provisioned." + "description": "Conditional. A grouping of information about the connection to the remote resource. Used when the network admin does not have access to approve connections to the remote resource. Required if `privateLinkServiceConnections` is empty." + }, + "nullable": true + }, + "privateLinkServiceConnections": { + "type": "array", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.Network/privateEndpoints@2024-01-01#properties/properties/properties/privateLinkServiceConnections" + }, + "description": "Conditional. A grouping of information about the connection to the remote resource. Required if `manualPrivateLinkServiceConnections` is empty." }, "nullable": true + }, + "enableTelemetry": { + "type": "bool", + "defaultValue": true, + "metadata": { + "description": "Optional. Enable/Disable usage telemetry for module." + } + } + }, + "variables": { + "copy": [ + { + "name": "formattedRoleAssignments", + "count": "[length(coalesce(parameters('roleAssignments'), createArray()))]", + "input": "[union(coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')], createObject('roleDefinitionId', coalesce(tryGet(variables('builtInRoleNames'), coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName), if(contains(coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName, '/providers/Microsoft.Authorization/roleDefinitions/'), coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName, subscriptionResourceId('Microsoft.Authorization/roleDefinitions', coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName)))))]" + } + ], + "builtInRoleNames": { + "Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'b24988ac-6180-42a0-ab88-20f7382dd24c')]", + "DNS Resolver Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '0f2ebee7-ffd4-4fc0-b3b7-664099fdad5d')]", + "DNS Zone Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'befefa01-2a29-4197-83a8-272ff33ce314')]", + "Domain Services Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'eeaeda52-9324-47f6-8069-5d5bade478b2')]", + "Domain Services Reader": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '361898ef-9ed1-48c2-849c-a832951106bb')]", + "Network Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '4d97b98b-1d4f-4787-a291-c67834d212e7')]", + "Owner": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '8e3af657-a8ff-443c-a75c-2fe8c4bcb635')]", + "Private DNS Zone Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'b12aa53e-6015-4669-85d0-8515ebb3ae7f')]", + "Reader": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'acdd72a7-3385-48ef-bd42-f606fba81ae7')]", + "Role Based Access Control Administrator": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'f58310d9-a9f6-439a-9e8d-f62e7b41a168')]" } }, "resources": { - "virtualMachine": { - "existing": true, - "type": "Microsoft.Compute/virtualMachines", - "apiVersion": "2024-11-01", - "name": "[parameters('virtualMachineName')]" + "avmTelemetry": { + "condition": "[parameters('enableTelemetry')]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "[format('46d3xbcp.res.network-privateendpoint.{0}.{1}', replace('0.11.1', '.', '-'), substring(uniqueString(deployment().name, parameters('location')), 0, 4))]", + "properties": { + "mode": "Incremental", + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "resources": [], + "outputs": { + "telemetry": { + "type": "String", + "value": "For more information, see https://aka.ms/avm/TelemetryInfo" + } + } + } + } }, - "extension": { - "type": "Microsoft.Compute/virtualMachines/extensions", - "apiVersion": "2024-11-01", - "name": "[format('{0}/{1}', parameters('virtualMachineName'), parameters('name'))]", + "privateEndpoint": { + "type": "Microsoft.Network/privateEndpoints", + "apiVersion": "2024-10-01", + "name": "[parameters('name')]", "location": "[parameters('location')]", "tags": "[parameters('tags')]", "properties": { - "publisher": "[parameters('publisher')]", - "type": "[parameters('type')]", - "typeHandlerVersion": "[parameters('typeHandlerVersion')]", - "autoUpgradeMinorVersion": "[parameters('autoUpgradeMinorVersion')]", - "enableAutomaticUpgrade": "[parameters('enableAutomaticUpgrade')]", - "forceUpdateTag": "[parameters('forceUpdateTag')]", - "settings": "[parameters('settings')]", - "protectedSettings": "[parameters('protectedSettings')]", - "suppressFailures": "[parameters('supressFailures')]", - "protectedSettingsFromKeyVault": "[parameters('protectedSettingsFromKeyVault')]", - "provisionAfterExtensions": "[parameters('provisionAfterExtensions')]" + "copy": [ + { + "name": "applicationSecurityGroups", + "count": "[length(coalesce(parameters('applicationSecurityGroupResourceIds'), createArray()))]", + "input": { + "id": "[coalesce(parameters('applicationSecurityGroupResourceIds'), createArray())[copyIndex('applicationSecurityGroups')]]" + } + } + ], + "customDnsConfigs": "[coalesce(parameters('customDnsConfigs'), createArray())]", + "customNetworkInterfaceName": "[coalesce(parameters('customNetworkInterfaceName'), '')]", + "ipConfigurations": "[coalesce(parameters('ipConfigurations'), createArray())]", + "manualPrivateLinkServiceConnections": "[coalesce(parameters('manualPrivateLinkServiceConnections'), createArray())]", + "privateLinkServiceConnections": "[coalesce(parameters('privateLinkServiceConnections'), createArray())]", + "subnet": { + "id": "[parameters('subnetResourceId')]" + } } - } - }, - "outputs": { - "name": { - "type": "string", - "metadata": { - "description": "The name of the extension." - }, - "value": "[parameters('name')]" }, - "resourceId": { - "type": "string", - "metadata": { - "description": "The resource ID of the extension." + "privateEndpoint_lock": { + "condition": "[and(not(empty(coalesce(parameters('lock'), createObject()))), not(equals(tryGet(parameters('lock'), 'kind'), 'None')))]", + "type": "Microsoft.Authorization/locks", + "apiVersion": "2020-05-01", + "scope": "[format('Microsoft.Network/privateEndpoints/{0}', parameters('name'))]", + "name": "[coalesce(tryGet(parameters('lock'), 'name'), format('lock-{0}', parameters('name')))]", + "properties": { + "level": "[coalesce(tryGet(parameters('lock'), 'kind'), '')]", + "notes": "[coalesce(tryGet(parameters('lock'), 'notes'), if(equals(tryGet(parameters('lock'), 'kind'), 'CanNotDelete'), 'Cannot delete resource or child resources.', 'Cannot delete or modify the resource or child resources.'))]" }, - "value": "[resourceId('Microsoft.Compute/virtualMachines/extensions', parameters('virtualMachineName'), parameters('name'))]" + "dependsOn": [ + "privateEndpoint" + ] }, - "resourceGroupName": { - "type": "string", - "metadata": { - "description": "The name of the Resource Group the extension was created in." + "privateEndpoint_roleAssignments": { + "copy": { + "name": "privateEndpoint_roleAssignments", + "count": "[length(coalesce(variables('formattedRoleAssignments'), createArray()))]" }, - "value": "[resourceGroup().name]" - }, - "location": { - "type": "string", - "metadata": { - "description": "The location the resource was deployed into." + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2022-04-01", + "scope": "[format('Microsoft.Network/privateEndpoints/{0}', parameters('name'))]", + "name": "[coalesce(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'name'), guid(resourceId('Microsoft.Network/privateEndpoints', parameters('name')), coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].principalId, coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].roleDefinitionId))]", + "properties": { + "roleDefinitionId": "[coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].roleDefinitionId]", + "principalId": "[coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].principalId]", + "description": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'description')]", + "principalType": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'principalType')]", + "condition": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'condition')]", + "conditionVersion": "[if(not(empty(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'condition'))), coalesce(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'conditionVersion'), '2.0'), null())]", + "delegatedManagedIdentityResourceId": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'delegatedManagedIdentityResourceId')]" }, - "value": "[reference('extension', '2024-11-01', 'full').location]" - } - } - } - }, - "dependsOn": [ - "vm" - ] - }, - "vm_aadJoinExtension": { - "condition": "[parameters('extensionAadJoinConfig').enabled]", - "type": "Microsoft.Resources/deployments", - "apiVersion": "2022-09-01", - "name": "[format('{0}-VM-AADLogin', uniqueString(deployment().name, parameters('location')))]", - "properties": { - "expressionEvaluationOptions": { - "scope": "inner" - }, - "mode": "Incremental", - "parameters": { - "virtualMachineName": { - "value": "[parameters('name')]" - }, - "name": { - "value": "[coalesce(tryGet(parameters('extensionAadJoinConfig'), 'name'), 'AADLogin')]" - }, - "location": { - "value": "[parameters('location')]" - }, - "publisher": { - "value": "Microsoft.Azure.ActiveDirectory" - }, - "type": "[if(equals(parameters('osType'), 'Windows'), createObject('value', 'AADLoginForWindows'), createObject('value', 'AADSSHLoginforLinux'))]", - "typeHandlerVersion": { - "value": "[coalesce(tryGet(parameters('extensionAadJoinConfig'), 'typeHandlerVersion'), if(equals(parameters('osType'), 'Windows'), '2.0', '1.0'))]" - }, - "autoUpgradeMinorVersion": { - "value": "[coalesce(tryGet(parameters('extensionAadJoinConfig'), 'autoUpgradeMinorVersion'), true())]" - }, - "enableAutomaticUpgrade": { - "value": "[coalesce(tryGet(parameters('extensionAadJoinConfig'), 'enableAutomaticUpgrade'), false())]" - }, - "settings": { - "value": "[coalesce(tryGet(parameters('extensionAadJoinConfig'), 'settings'), createObject())]" - }, - "supressFailures": { - "value": "[coalesce(tryGet(parameters('extensionAadJoinConfig'), 'supressFailures'), false())]" - }, - "tags": { - "value": "[coalesce(tryGet(parameters('extensionAadJoinConfig'), 'tags'), parameters('tags'))]" - } - }, - "template": { - "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", - "languageVersion": "2.0", - "contentVersion": "1.0.0.0", - "metadata": { - "_generator": { - "name": "bicep", - "version": "0.37.4.10188", - "templateHash": "13125609748815648088" + "dependsOn": [ + "privateEndpoint" + ] }, - "name": "Virtual Machine Extensions", - "description": "This module deploys a Virtual Machine Extension." + "privateEndpoint_privateDnsZoneGroup": { + "condition": "[not(empty(parameters('privateDnsZoneGroup')))]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "[format('{0}-PrivateEndpoint-PrivateDnsZoneGroup', uniqueString(deployment().name))]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "name": { + "value": "[tryGet(parameters('privateDnsZoneGroup'), 'name')]" + }, + "privateEndpointName": { + "value": "[parameters('name')]" + }, + "privateDnsZoneConfigs": { + "value": "[parameters('privateDnsZoneGroup').privateDnsZoneGroupConfigs]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "languageVersion": "2.0", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.38.5.1644", + "templateHash": "24141742673128945" + }, + "name": "Private Endpoint Private DNS Zone Groups", + "description": "This module deploys a Private Endpoint Private DNS Zone Group." + }, + "definitions": { + "privateDnsZoneGroupConfigType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The name of the private DNS zone group config." + } + }, + "privateDnsZoneResourceId": { + "type": "string", + "metadata": { + "description": "Required. The resource id of the private DNS zone." + } + } + }, + "metadata": { + "__bicep_export!": true, + "description": "The type of a private DNS zone group configuration." + } + } + }, + "parameters": { + "privateEndpointName": { + "type": "string", + "metadata": { + "description": "Conditional. The name of the parent private endpoint. Required if the template is used in a standalone deployment." + } + }, + "privateDnsZoneConfigs": { + "type": "array", + "items": { + "$ref": "#/definitions/privateDnsZoneGroupConfigType" + }, + "minLength": 1, + "maxLength": 5, + "metadata": { + "description": "Required. Array of private DNS zone configurations of the private DNS zone group. A DNS zone group can support up to 5 DNS zones." + } + }, + "name": { + "type": "string", + "defaultValue": "default", + "metadata": { + "description": "Optional. The name of the private DNS zone group." + } + } + }, + "resources": { + "privateEndpoint": { + "existing": true, + "type": "Microsoft.Network/privateEndpoints", + "apiVersion": "2024-10-01", + "name": "[parameters('privateEndpointName')]" + }, + "privateDnsZoneGroup": { + "type": "Microsoft.Network/privateEndpoints/privateDnsZoneGroups", + "apiVersion": "2024-10-01", + "name": "[format('{0}/{1}', parameters('privateEndpointName'), parameters('name'))]", + "properties": { + "copy": [ + { + "name": "privateDnsZoneConfigs", + "count": "[length(parameters('privateDnsZoneConfigs'))]", + "input": { + "name": "[coalesce(tryGet(parameters('privateDnsZoneConfigs')[copyIndex('privateDnsZoneConfigs')], 'name'), last(split(parameters('privateDnsZoneConfigs')[copyIndex('privateDnsZoneConfigs')].privateDnsZoneResourceId, '/')))]", + "properties": { + "privateDnsZoneId": "[parameters('privateDnsZoneConfigs')[copyIndex('privateDnsZoneConfigs')].privateDnsZoneResourceId]" + } + } + } + ] + } + } + }, + "outputs": { + "name": { + "type": "string", + "metadata": { + "description": "The name of the private endpoint DNS zone group." + }, + "value": "[parameters('name')]" + }, + "resourceId": { + "type": "string", + "metadata": { + "description": "The resource ID of the private endpoint DNS zone group." + }, + "value": "[resourceId('Microsoft.Network/privateEndpoints/privateDnsZoneGroups', parameters('privateEndpointName'), parameters('name'))]" + }, + "resourceGroupName": { + "type": "string", + "metadata": { + "description": "The resource group the private endpoint DNS zone group was deployed into." + }, + "value": "[resourceGroup().name]" + } + } + } + }, + "dependsOn": [ + "privateEndpoint" + ] + } }, - "parameters": { - "virtualMachineName": { - "type": "string", - "metadata": { - "description": "Conditional. The name of the parent virtual machine that extension is provisioned for. Required if the template is used in a standalone deployment." - } - }, - "name": { - "type": "string", - "metadata": { - "description": "Required. The name of the virtual machine extension." - } - }, - "location": { - "type": "string", - "defaultValue": "[resourceGroup().location]", - "metadata": { - "description": "Optional. The location the extension is deployed to." - } - }, - "publisher": { - "type": "string", - "metadata": { - "description": "Required. The name of the extension handler publisher." - } - }, - "type": { + "outputs": { + "resourceGroupName": { "type": "string", "metadata": { - "description": "Required. Specifies the type of the extension; an example is \"CustomScriptExtension\"." - } + "description": "The resource group the private endpoint was deployed into." + }, + "value": "[resourceGroup().name]" }, - "typeHandlerVersion": { + "resourceId": { "type": "string", "metadata": { - "description": "Required. Specifies the version of the script handler." - } - }, - "autoUpgradeMinorVersion": { - "type": "bool", - "metadata": { - "description": "Required. Indicates whether the extension should use a newer minor version if one is available at deployment time. Once deployed, however, the extension will not upgrade minor versions unless redeployed, even with this property set to true." - } + "description": "The resource ID of the private endpoint." + }, + "value": "[resourceId('Microsoft.Network/privateEndpoints', parameters('name'))]" }, - "forceUpdateTag": { + "name": { "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. How the extension handler should be forced to update even if the extension configuration has not changed." - } - }, - "settings": { - "type": "object", - "nullable": true, - "metadata": { - "description": "Optional. Any object that contains the extension specific settings." - } - }, - "protectedSettings": { - "type": "secureObject", - "nullable": true, - "metadata": { - "description": "Optional. Any object that contains the extension specific protected settings." - } - }, - "supressFailures": { - "type": "bool", - "defaultValue": false, - "metadata": { - "description": "Optional. Indicates whether failures stemming from the extension will be suppressed (Operational failures such as not connecting to the VM will not be suppressed regardless of this value). The default is false." - } - }, - "enableAutomaticUpgrade": { - "type": "bool", - "metadata": { - "description": "Required. Indicates whether the extension should be automatically upgraded by the platform if there is a newer version of the extension available." - } - }, - "tags": { - "type": "object", "metadata": { - "__bicep_resource_derived_type!": { - "source": "Microsoft.Compute/virtualMachines/extensions@2024-11-01#properties/tags" - }, - "description": "Optional. Tags of the resource." + "description": "The name of the private endpoint." }, - "nullable": true + "value": "[parameters('name')]" }, - "protectedSettingsFromKeyVault": { - "type": "object", + "location": { + "type": "string", "metadata": { - "__bicep_resource_derived_type!": { - "source": "Microsoft.Compute/virtualMachines/extensions@2024-11-01#properties/properties/properties/protectedSettingsFromKeyVault" - }, - "description": "Optional. The extensions protected settings that are passed by reference, and consumed from key vault." + "description": "The location the resource was deployed into." }, - "nullable": true + "value": "[reference('privateEndpoint', '2024-10-01', 'full').location]" }, - "provisionAfterExtensions": { + "customDnsConfigs": { "type": "array", "metadata": { "__bicep_resource_derived_type!": { - "source": "Microsoft.Compute/virtualMachines/extensions@2024-11-01#properties/properties/properties/provisionAfterExtensions" + "source": "Microsoft.Network/privateEndpoints@2024-01-01#properties/properties/properties/customDnsConfigs", + "output": true }, - "description": "Optional. Collection of extension names after which this extension needs to be provisioned." + "description": "The custom DNS configurations of the private endpoint." }, - "nullable": true - } - }, - "resources": { - "virtualMachine": { - "existing": true, - "type": "Microsoft.Compute/virtualMachines", - "apiVersion": "2024-11-01", - "name": "[parameters('virtualMachineName')]" + "value": "[reference('privateEndpoint').customDnsConfigs]" }, - "extension": { - "type": "Microsoft.Compute/virtualMachines/extensions", - "apiVersion": "2024-11-01", - "name": "[format('{0}/{1}', parameters('virtualMachineName'), parameters('name'))]", - "location": "[parameters('location')]", - "tags": "[parameters('tags')]", - "properties": { - "publisher": "[parameters('publisher')]", - "type": "[parameters('type')]", - "typeHandlerVersion": "[parameters('typeHandlerVersion')]", - "autoUpgradeMinorVersion": "[parameters('autoUpgradeMinorVersion')]", - "enableAutomaticUpgrade": "[parameters('enableAutomaticUpgrade')]", - "forceUpdateTag": "[parameters('forceUpdateTag')]", - "settings": "[parameters('settings')]", - "protectedSettings": "[parameters('protectedSettings')]", - "suppressFailures": "[parameters('supressFailures')]", - "protectedSettingsFromKeyVault": "[parameters('protectedSettingsFromKeyVault')]", - "provisionAfterExtensions": "[parameters('provisionAfterExtensions')]" - } - } - }, - "outputs": { - "name": { - "type": "string", - "metadata": { - "description": "The name of the extension." + "networkInterfaceResourceIds": { + "type": "array", + "items": { + "type": "string" }, - "value": "[parameters('name')]" - }, - "resourceId": { - "type": "string", "metadata": { - "description": "The resource ID of the extension." + "description": "The resource IDs of the network interfaces associated with the private endpoint." }, - "value": "[resourceId('Microsoft.Compute/virtualMachines/extensions', parameters('virtualMachineName'), parameters('name'))]" + "value": "[map(reference('privateEndpoint').networkInterfaces, lambda('nic', lambdaVariables('nic').id))]" }, - "resourceGroupName": { + "groupId": { "type": "string", + "nullable": true, "metadata": { - "description": "The name of the Resource Group the extension was created in." + "description": "The group Id for the private endpoint Group." }, - "value": "[resourceGroup().name]" - }, - "location": { - "type": "string", - "metadata": { - "description": "The location the resource was deployed into." + "value": "[coalesce(tryGet(tryGet(tryGet(tryGet(reference('privateEndpoint'), 'manualPrivateLinkServiceConnections'), 0, 'properties'), 'groupIds'), 0), tryGet(tryGet(tryGet(tryGet(reference('privateEndpoint'), 'privateLinkServiceConnections'), 0, 'properties'), 'groupIds'), 0))]" + } + } + } + }, + "dependsOn": [ + "databaseAccount" + ] + } + }, + "outputs": { + "name": { + "type": "string", + "metadata": { + "description": "The name of the database account." + }, + "value": "[parameters('name')]" + }, + "resourceId": { + "type": "string", + "metadata": { + "description": "The resource ID of the database account." + }, + "value": "[resourceId('Microsoft.DocumentDB/databaseAccounts', parameters('name'))]" + }, + "resourceGroupName": { + "type": "string", + "metadata": { + "description": "The name of the resource group the database account was created in." + }, + "value": "[resourceGroup().name]" + }, + "systemAssignedMIPrincipalId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "The principal ID of the system assigned identity." + }, + "value": "[tryGet(tryGet(reference('databaseAccount', '2025-04-15', 'full'), 'identity'), 'principalId')]" + }, + "location": { + "type": "string", + "metadata": { + "description": "The location the resource was deployed into." + }, + "value": "[reference('databaseAccount', '2025-04-15', 'full').location]" + }, + "endpoint": { + "type": "string", + "metadata": { + "description": "The endpoint of the database account." + }, + "value": "[reference('databaseAccount').documentEndpoint]" + }, + "privateEndpoints": { + "type": "array", + "items": { + "$ref": "#/definitions/privateEndpointOutputType" + }, + "metadata": { + "description": "The private endpoints of the database account." + }, + "copy": { + "count": "[length(coalesce(parameters('privateEndpoints'), createArray()))]", + "input": { + "name": "[reference(format('databaseAccount_privateEndpoints[{0}]', copyIndex())).outputs.name.value]", + "resourceId": "[reference(format('databaseAccount_privateEndpoints[{0}]', copyIndex())).outputs.resourceId.value]", + "groupId": "[tryGet(tryGet(reference(format('databaseAccount_privateEndpoints[{0}]', copyIndex())).outputs, 'groupId'), 'value')]", + "customDnsConfigs": "[reference(format('databaseAccount_privateEndpoints[{0}]', copyIndex())).outputs.customDnsConfigs.value]", + "networkInterfaceResourceIds": "[reference(format('databaseAccount_privateEndpoints[{0}]', copyIndex())).outputs.networkInterfaceResourceIds.value]" + } + } + }, + "primaryReadWriteKey": { + "type": "securestring", + "metadata": { + "description": "The primary read-write key." + }, + "value": "[listKeys('databaseAccount', '2025-04-15').primaryMasterKey]" + }, + "primaryReadOnlyKey": { + "type": "securestring", + "metadata": { + "description": "The primary read-only key." + }, + "value": "[listKeys('databaseAccount', '2025-04-15').primaryReadonlyMasterKey]" + }, + "primaryReadWriteConnectionString": { + "type": "securestring", + "metadata": { + "description": "The primary read-write connection string." + }, + "value": "[listConnectionStrings('databaseAccount', '2025-04-15').connectionStrings[0].connectionString]" + }, + "primaryReadOnlyConnectionString": { + "type": "securestring", + "metadata": { + "description": "The primary read-only connection string." + }, + "value": "[listConnectionStrings('databaseAccount', '2025-04-15').connectionStrings[2].connectionString]" + }, + "secondaryReadWriteKey": { + "type": "securestring", + "metadata": { + "description": "The secondary read-write key." + }, + "value": "[listKeys('databaseAccount', '2025-04-15').secondaryMasterKey]" + }, + "secondaryReadOnlyKey": { + "type": "securestring", + "metadata": { + "description": "The secondary read-only key." + }, + "value": "[listKeys('databaseAccount', '2025-04-15').secondaryReadonlyMasterKey]" + }, + "secondaryReadWriteConnectionString": { + "type": "securestring", + "metadata": { + "description": "The secondary read-write connection string." + }, + "value": "[listConnectionStrings('databaseAccount', '2025-04-15').connectionStrings[1].connectionString]" + }, + "secondaryReadOnlyConnectionString": { + "type": "securestring", + "metadata": { + "description": "The secondary read-only connection string." + }, + "value": "[listConnectionStrings('databaseAccount', '2025-04-15').connectionStrings[3].connectionString]" + } + } + } + }, + "dependsOn": [ + "[format('avmPrivateDnsZones[{0}]', variables('dnsZoneIndex').cosmosDB)]", + "logAnalyticsWorkspace", + "userAssignedIdentity", + "virtualNetwork" + ] + }, + "webServerFarm": { + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "[take(format('avm.res.web.serverfarm.{0}', variables('webServerFarmResourceName')), 64)]", + "resourceGroup": "[resourceGroup().name]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "name": { + "value": "[variables('webServerFarmResourceName')]" + }, + "tags": { + "value": "[parameters('tags')]" + }, + "enableTelemetry": { + "value": "[parameters('enableTelemetry')]" + }, + "location": { + "value": "[variables('solutionLocation')]" + }, + "reserved": { + "value": true + }, + "kind": { + "value": "linux" + }, + "diagnosticSettings": "[if(parameters('enableMonitoring'), createObject('value', createArray(createObject('workspaceResourceId', if(variables('useExistingLogAnalytics'), parameters('existingLogAnalyticsWorkspaceId'), if(parameters('enableMonitoring'), reference('logAnalyticsWorkspace').outputs.resourceId.value, ''))))), createObject('value', null()))]", + "skuName": "[if(or(parameters('enableScalability'), parameters('enableRedundancy')), createObject('value', 'P1v3'), createObject('value', 'B1'))]", + "skuCapacity": { + "value": 1 + }, + "zoneRedundant": "[if(parameters('enableRedundancy'), createObject('value', true()), createObject('value', false()))]" + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "languageVersion": "2.0", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.36.177.2456", + "templateHash": "16945786131371363466" + }, + "name": "App Service Plan", + "description": "This module deploys an App Service Plan." + }, + "definitions": { + "diagnosticSettingMetricsOnlyType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The name of diagnostic setting." + } + }, + "metricCategories": { + "type": "array", + "items": { + "type": "object", + "properties": { + "category": { + "type": "string", + "metadata": { + "description": "Required. Name of a Diagnostic Metric category for a resource type this setting is applied to. Set to `AllMetrics` to collect all metrics." + } }, - "value": "[reference('extension', '2024-11-01', 'full').location]" + "enabled": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. Enable or disable the category explicitly. Default is `true`." + } + } } + }, + "nullable": true, + "metadata": { + "description": "Optional. The name of metrics that will be streamed. \"allMetrics\" includes all possible metrics for the resource. Set to `[]` to disable metric collection." + } + }, + "logAnalyticsDestinationType": { + "type": "string", + "allowedValues": [ + "AzureDiagnostics", + "Dedicated" + ], + "nullable": true, + "metadata": { + "description": "Optional. A string indicating whether the export to Log Analytics should use the default destination type, i.e. AzureDiagnostics, or use a destination type." + } + }, + "workspaceResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Resource ID of the diagnostic log analytics workspace. For security reasons, it is recommended to set diagnostic settings to send data to either storage account, log analytics workspace or event hub." + } + }, + "storageAccountResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Resource ID of the diagnostic storage account. For security reasons, it is recommended to set diagnostic settings to send data to either storage account, log analytics workspace or event hub." + } + }, + "eventHubAuthorizationRuleResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Resource ID of the diagnostic event hub authorization rule for the Event Hubs namespace in which the event hub should be created or streamed to." + } + }, + "eventHubName": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Name of the diagnostic event hub within the namespace to which logs are streamed. Without this, an event hub is created for each log category. For security reasons, it is recommended to set diagnostic settings to send data to either storage account, log analytics workspace or event hub." + } + }, + "marketplacePartnerResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The full ARM resource ID of the Marketplace resource to which you would like to send Diagnostic Logs." + } + } + }, + "metadata": { + "description": "An AVM-aligned type for a diagnostic setting. To be used if only metrics are supported by the resource provider.", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.0" + } + } + }, + "lockType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Specify the name of lock." + } + }, + "kind": { + "type": "string", + "allowedValues": [ + "CanNotDelete", + "None", + "ReadOnly" + ], + "nullable": true, + "metadata": { + "description": "Optional. Specify the type of lock." + } + }, + "notes": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Specify the notes of the lock." } } }, - "dependsOn": [ - "vm", - "vm_domainJoinExtension" - ] + "metadata": { + "description": "An AVM-aligned type for a lock.", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.0" + } + } }, - "vm_microsoftAntiMalwareExtension": { - "condition": "[parameters('extensionAntiMalwareConfig').enabled]", - "type": "Microsoft.Resources/deployments", - "apiVersion": "2022-09-01", - "name": "[format('{0}-VM-MicrosoftAntiMalware', uniqueString(deployment().name, parameters('location')))]", + "roleAssignmentType": { + "type": "object", "properties": { - "expressionEvaluationOptions": { - "scope": "inner" + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The name (as GUID) of the role assignment. If not provided, a GUID will be generated." + } }, - "mode": "Incremental", - "parameters": { - "virtualMachineName": { - "value": "[parameters('name')]" - }, - "name": { - "value": "[coalesce(tryGet(parameters('extensionAntiMalwareConfig'), 'name'), 'MicrosoftAntiMalware')]" - }, - "location": { - "value": "[parameters('location')]" - }, - "publisher": { - "value": "Microsoft.Azure.Security" - }, - "type": { - "value": "IaaSAntimalware" - }, - "typeHandlerVersion": { - "value": "[coalesce(tryGet(parameters('extensionAntiMalwareConfig'), 'typeHandlerVersion'), '1.3')]" - }, - "autoUpgradeMinorVersion": { - "value": "[coalesce(tryGet(parameters('extensionAntiMalwareConfig'), 'autoUpgradeMinorVersion'), true())]" - }, - "enableAutomaticUpgrade": { - "value": "[coalesce(tryGet(parameters('extensionAntiMalwareConfig'), 'enableAutomaticUpgrade'), false())]" - }, - "settings": { - "value": "[coalesce(tryGet(parameters('extensionAntiMalwareConfig'), 'settings'), createObject('AntimalwareEnabled', 'true', 'Exclusions', createObject(), 'RealtimeProtectionEnabled', 'true', 'ScheduledScanSettings', createObject('day', '7', 'isEnabled', 'true', 'scanType', 'Quick', 'time', '120')))]" - }, - "supressFailures": { - "value": "[coalesce(tryGet(parameters('extensionAntiMalwareConfig'), 'supressFailures'), false())]" - }, - "tags": { - "value": "[coalesce(tryGet(parameters('extensionAntiMalwareConfig'), 'tags'), parameters('tags'))]" + "roleDefinitionIdOrName": { + "type": "string", + "metadata": { + "description": "Required. The role to assign. You can provide either the display name of the role definition, the role definition GUID, or its fully qualified ID in the following format: '/providers/Microsoft.Authorization/roleDefinitions/c2f4ef07-c644-48eb-af81-4b1b4947fb11'." } }, - "template": { - "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", - "languageVersion": "2.0", - "contentVersion": "1.0.0.0", + "principalId": { + "type": "string", "metadata": { - "_generator": { - "name": "bicep", - "version": "0.37.4.10188", - "templateHash": "13125609748815648088" - }, - "name": "Virtual Machine Extensions", - "description": "This module deploys a Virtual Machine Extension." - }, - "parameters": { - "virtualMachineName": { - "type": "string", - "metadata": { - "description": "Conditional. The name of the parent virtual machine that extension is provisioned for. Required if the template is used in a standalone deployment." - } - }, - "name": { - "type": "string", - "metadata": { - "description": "Required. The name of the virtual machine extension." - } - }, - "location": { - "type": "string", - "defaultValue": "[resourceGroup().location]", - "metadata": { - "description": "Optional. The location the extension is deployed to." - } - }, - "publisher": { - "type": "string", - "metadata": { - "description": "Required. The name of the extension handler publisher." - } - }, - "type": { - "type": "string", - "metadata": { - "description": "Required. Specifies the type of the extension; an example is \"CustomScriptExtension\"." - } - }, - "typeHandlerVersion": { - "type": "string", - "metadata": { - "description": "Required. Specifies the version of the script handler." - } - }, - "autoUpgradeMinorVersion": { - "type": "bool", - "metadata": { - "description": "Required. Indicates whether the extension should use a newer minor version if one is available at deployment time. Once deployed, however, the extension will not upgrade minor versions unless redeployed, even with this property set to true." - } - }, - "forceUpdateTag": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. How the extension handler should be forced to update even if the extension configuration has not changed." - } - }, - "settings": { - "type": "object", - "nullable": true, - "metadata": { - "description": "Optional. Any object that contains the extension specific settings." - } - }, - "protectedSettings": { - "type": "secureObject", - "nullable": true, - "metadata": { - "description": "Optional. Any object that contains the extension specific protected settings." - } - }, - "supressFailures": { - "type": "bool", - "defaultValue": false, - "metadata": { - "description": "Optional. Indicates whether failures stemming from the extension will be suppressed (Operational failures such as not connecting to the VM will not be suppressed regardless of this value). The default is false." - } - }, - "enableAutomaticUpgrade": { - "type": "bool", - "metadata": { - "description": "Required. Indicates whether the extension should be automatically upgraded by the platform if there is a newer version of the extension available." - } - }, - "tags": { - "type": "object", - "metadata": { - "__bicep_resource_derived_type!": { - "source": "Microsoft.Compute/virtualMachines/extensions@2024-11-01#properties/tags" - }, - "description": "Optional. Tags of the resource." - }, - "nullable": true - }, - "protectedSettingsFromKeyVault": { - "type": "object", - "metadata": { - "__bicep_resource_derived_type!": { - "source": "Microsoft.Compute/virtualMachines/extensions@2024-11-01#properties/properties/properties/protectedSettingsFromKeyVault" - }, - "description": "Optional. The extensions protected settings that are passed by reference, and consumed from key vault." - }, - "nullable": true - }, - "provisionAfterExtensions": { - "type": "array", - "metadata": { - "__bicep_resource_derived_type!": { - "source": "Microsoft.Compute/virtualMachines/extensions@2024-11-01#properties/properties/properties/provisionAfterExtensions" - }, - "description": "Optional. Collection of extension names after which this extension needs to be provisioned." - }, - "nullable": true - } - }, - "resources": { - "virtualMachine": { - "existing": true, - "type": "Microsoft.Compute/virtualMachines", - "apiVersion": "2024-11-01", - "name": "[parameters('virtualMachineName')]" - }, - "extension": { - "type": "Microsoft.Compute/virtualMachines/extensions", - "apiVersion": "2024-11-01", - "name": "[format('{0}/{1}', parameters('virtualMachineName'), parameters('name'))]", - "location": "[parameters('location')]", - "tags": "[parameters('tags')]", - "properties": { - "publisher": "[parameters('publisher')]", - "type": "[parameters('type')]", - "typeHandlerVersion": "[parameters('typeHandlerVersion')]", - "autoUpgradeMinorVersion": "[parameters('autoUpgradeMinorVersion')]", - "enableAutomaticUpgrade": "[parameters('enableAutomaticUpgrade')]", - "forceUpdateTag": "[parameters('forceUpdateTag')]", - "settings": "[parameters('settings')]", - "protectedSettings": "[parameters('protectedSettings')]", - "suppressFailures": "[parameters('supressFailures')]", - "protectedSettingsFromKeyVault": "[parameters('protectedSettingsFromKeyVault')]", - "provisionAfterExtensions": "[parameters('provisionAfterExtensions')]" - } - } - }, - "outputs": { - "name": { - "type": "string", - "metadata": { - "description": "The name of the extension." - }, - "value": "[parameters('name')]" - }, - "resourceId": { - "type": "string", - "metadata": { - "description": "The resource ID of the extension." - }, - "value": "[resourceId('Microsoft.Compute/virtualMachines/extensions', parameters('virtualMachineName'), parameters('name'))]" - }, - "resourceGroupName": { - "type": "string", - "metadata": { - "description": "The name of the Resource Group the extension was created in." - }, - "value": "[resourceGroup().name]" - }, - "location": { - "type": "string", - "metadata": { - "description": "The location the resource was deployed into." - }, - "value": "[reference('extension', '2024-11-01', 'full').location]" - } + "description": "Required. The principal ID of the principal (user/group/identity) to assign the role to." + } + }, + "principalType": { + "type": "string", + "allowedValues": [ + "Device", + "ForeignGroup", + "Group", + "ServicePrincipal", + "User" + ], + "nullable": true, + "metadata": { + "description": "Optional. The principal type of the assigned principal ID." + } + }, + "description": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The description of the role assignment." + } + }, + "condition": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The conditions on the role assignment. This limits the resources it can be assigned to. e.g.: @Resource[Microsoft.Storage/storageAccounts/blobServices/containers:ContainerName] StringEqualsIgnoreCase \"foo_storage_container\"." + } + }, + "conditionVersion": { + "type": "string", + "allowedValues": [ + "2.0" + ], + "nullable": true, + "metadata": { + "description": "Optional. Version of the condition." + } + }, + "delegatedManagedIdentityResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The Resource Id of the delegated managed identity resource." } } }, - "dependsOn": [ - "vm", - "vm_aadJoinExtension" - ] + "metadata": { + "description": "An AVM-aligned type for a role assignment.", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.0" + } + } + } + }, + "parameters": { + "name": { + "type": "string", + "minLength": 1, + "maxLength": 60, + "metadata": { + "description": "Required. Name of the app service plan." + } }, - "vm_azureMonitorAgentExtension": { - "condition": "[parameters('extensionMonitoringAgentConfig').enabled]", - "type": "Microsoft.Resources/deployments", - "apiVersion": "2022-09-01", - "name": "[format('{0}-VM-AzureMonitorAgent', uniqueString(deployment().name, parameters('location')))]", - "properties": { - "expressionEvaluationOptions": { - "scope": "inner" - }, - "mode": "Incremental", - "parameters": { - "virtualMachineName": { - "value": "[parameters('name')]" - }, - "name": { - "value": "[coalesce(tryGet(parameters('extensionMonitoringAgentConfig'), 'name'), 'AzureMonitorAgent')]" - }, - "location": { - "value": "[parameters('location')]" - }, - "publisher": { - "value": "Microsoft.Azure.Monitor" - }, - "type": "[if(equals(parameters('osType'), 'Windows'), createObject('value', 'AzureMonitorWindowsAgent'), createObject('value', 'AzureMonitorLinuxAgent'))]", - "typeHandlerVersion": { - "value": "[coalesce(tryGet(parameters('extensionMonitoringAgentConfig'), 'typeHandlerVersion'), if(equals(parameters('osType'), 'Windows'), '1.22', '1.29'))]" - }, - "autoUpgradeMinorVersion": { - "value": "[coalesce(tryGet(parameters('extensionMonitoringAgentConfig'), 'autoUpgradeMinorVersion'), true())]" - }, - "enableAutomaticUpgrade": { - "value": "[coalesce(tryGet(parameters('extensionMonitoringAgentConfig'), 'enableAutomaticUpgrade'), false())]" - }, - "supressFailures": { - "value": "[coalesce(tryGet(parameters('extensionMonitoringAgentConfig'), 'supressFailures'), false())]" - }, - "tags": { - "value": "[coalesce(tryGet(parameters('extensionMonitoringAgentConfig'), 'tags'), parameters('tags'))]" - } + "skuName": { + "type": "string", + "defaultValue": "P1v3", + "metadata": { + "example": " 'F1'\n 'B1'\n 'P1v3'\n 'I1v2'\n 'FC1'\n ", + "description": "Optional. The name of the SKU will Determine the tier, size, family of the App Service Plan. This defaults to P1v3 to leverage availability zones." + } + }, + "skuCapacity": { + "type": "int", + "defaultValue": 3, + "metadata": { + "description": "Optional. Number of workers associated with the App Service Plan. This defaults to 3, to leverage availability zones." + } + }, + "location": { + "type": "string", + "defaultValue": "[resourceGroup().location]", + "metadata": { + "description": "Optional. Location for all resources." + } + }, + "kind": { + "type": "string", + "defaultValue": "app", + "allowedValues": [ + "app", + "elastic", + "functionapp", + "windows", + "linux" + ], + "metadata": { + "description": "Optional. Kind of server OS." + } + }, + "reserved": { + "type": "bool", + "defaultValue": "[equals(parameters('kind'), 'linux')]", + "metadata": { + "description": "Conditional. Defaults to false when creating Windows/app App Service Plan. Required if creating a Linux App Service Plan and must be set to true." + } + }, + "appServiceEnvironmentResourceId": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "Optional. The Resource ID of the App Service Environment to use for the App Service Plan." + } + }, + "workerTierName": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "Optional. Target worker tier assigned to the App Service plan." + } + }, + "perSiteScaling": { + "type": "bool", + "defaultValue": false, + "metadata": { + "description": "Optional. If true, apps assigned to this App Service plan can be scaled independently. If false, apps assigned to this App Service plan will scale to all instances of the plan." + } + }, + "elasticScaleEnabled": { + "type": "bool", + "defaultValue": "[greater(parameters('maximumElasticWorkerCount'), 1)]", + "metadata": { + "description": "Optional. Enable/Disable ElasticScaleEnabled App Service Plan." + } + }, + "maximumElasticWorkerCount": { + "type": "int", + "defaultValue": 1, + "metadata": { + "description": "Optional. Maximum number of total workers allowed for this ElasticScaleEnabled App Service Plan." + } + }, + "targetWorkerCount": { + "type": "int", + "defaultValue": 0, + "metadata": { + "description": "Optional. Scaling worker count." + } + }, + "targetWorkerSize": { + "type": "int", + "defaultValue": 0, + "allowedValues": [ + 0, + 1, + 2 + ], + "metadata": { + "description": "Optional. The instance size of the hosting plan (small, medium, or large)." + } + }, + "zoneRedundant": { + "type": "bool", + "defaultValue": "[if(or(startsWith(parameters('skuName'), 'P'), startsWith(parameters('skuName'), 'EP')), true(), false())]", + "metadata": { + "description": "Optional. Zone Redundant server farms can only be used on Premium or ElasticPremium SKU tiers within ZRS Supported regions (https://learn.microsoft.com/en-us/azure/storage/common/redundancy-regions-zrs)." + } + }, + "lock": { + "$ref": "#/definitions/lockType", + "nullable": true, + "metadata": { + "description": "Optional. The lock settings of the service." + } + }, + "roleAssignments": { + "type": "array", + "items": { + "$ref": "#/definitions/roleAssignmentType" + }, + "nullable": true, + "metadata": { + "description": "Optional. Array of role assignments to create." + } + }, + "tags": { + "type": "object", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.Web/serverfarms@2024-11-01#properties/tags" }, + "description": "Optional. Tags of the resource." + }, + "nullable": true + }, + "enableTelemetry": { + "type": "bool", + "defaultValue": true, + "metadata": { + "description": "Optional. Enable/Disable usage telemetry for module." + } + }, + "diagnosticSettings": { + "type": "array", + "items": { + "$ref": "#/definitions/diagnosticSettingMetricsOnlyType" + }, + "nullable": true, + "metadata": { + "description": "Optional. The diagnostic settings of the service." + } + } + }, + "variables": { + "copy": [ + { + "name": "formattedRoleAssignments", + "count": "[length(coalesce(parameters('roleAssignments'), createArray()))]", + "input": "[union(coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')], createObject('roleDefinitionId', coalesce(tryGet(variables('builtInRoleNames'), coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName), if(contains(coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName, '/providers/Microsoft.Authorization/roleDefinitions/'), coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName, subscriptionResourceId('Microsoft.Authorization/roleDefinitions', coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName)))))]" + } + ], + "builtInRoleNames": { + "Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'b24988ac-6180-42a0-ab88-20f7382dd24c')]", + "Owner": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '8e3af657-a8ff-443c-a75c-2fe8c4bcb635')]", + "Reader": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'acdd72a7-3385-48ef-bd42-f606fba81ae7')]", + "Role Based Access Control Administrator": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'f58310d9-a9f6-439a-9e8d-f62e7b41a168')]", + "User Access Administrator": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '18d7d88d-d35e-4fb5-a5c3-7773c20a72d9')]", + "Web Plan Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '2cc479cb-7b4d-49a8-b449-8c00fd0f0a4b')]", + "Website Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'de139f84-1756-47ae-9be6-808fbbe84772')]" + } + }, + "resources": { + "avmTelemetry": { + "condition": "[parameters('enableTelemetry')]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2024-03-01", + "name": "[format('46d3xbcp.res.web-serverfarm.{0}.{1}', replace('0.5.0', '.', '-'), substring(uniqueString(deployment().name, parameters('location')), 0, 4))]", + "properties": { + "mode": "Incremental", "template": { "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", - "languageVersion": "2.0", "contentVersion": "1.0.0.0", - "metadata": { - "_generator": { - "name": "bicep", - "version": "0.37.4.10188", - "templateHash": "13125609748815648088" - }, - "name": "Virtual Machine Extensions", - "description": "This module deploys a Virtual Machine Extension." - }, - "parameters": { - "virtualMachineName": { - "type": "string", - "metadata": { - "description": "Conditional. The name of the parent virtual machine that extension is provisioned for. Required if the template is used in a standalone deployment." - } - }, - "name": { - "type": "string", - "metadata": { - "description": "Required. The name of the virtual machine extension." - } - }, - "location": { - "type": "string", - "defaultValue": "[resourceGroup().location]", - "metadata": { - "description": "Optional. The location the extension is deployed to." - } - }, - "publisher": { - "type": "string", - "metadata": { - "description": "Required. The name of the extension handler publisher." - } - }, - "type": { - "type": "string", - "metadata": { - "description": "Required. Specifies the type of the extension; an example is \"CustomScriptExtension\"." - } - }, - "typeHandlerVersion": { - "type": "string", - "metadata": { - "description": "Required. Specifies the version of the script handler." - } - }, - "autoUpgradeMinorVersion": { - "type": "bool", - "metadata": { - "description": "Required. Indicates whether the extension should use a newer minor version if one is available at deployment time. Once deployed, however, the extension will not upgrade minor versions unless redeployed, even with this property set to true." - } - }, - "forceUpdateTag": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. How the extension handler should be forced to update even if the extension configuration has not changed." - } - }, - "settings": { - "type": "object", - "nullable": true, - "metadata": { - "description": "Optional. Any object that contains the extension specific settings." - } - }, - "protectedSettings": { - "type": "secureObject", - "nullable": true, - "metadata": { - "description": "Optional. Any object that contains the extension specific protected settings." - } - }, - "supressFailures": { - "type": "bool", - "defaultValue": false, - "metadata": { - "description": "Optional. Indicates whether failures stemming from the extension will be suppressed (Operational failures such as not connecting to the VM will not be suppressed regardless of this value). The default is false." - } - }, - "enableAutomaticUpgrade": { - "type": "bool", - "metadata": { - "description": "Required. Indicates whether the extension should be automatically upgraded by the platform if there is a newer version of the extension available." - } - }, - "tags": { - "type": "object", - "metadata": { - "__bicep_resource_derived_type!": { - "source": "Microsoft.Compute/virtualMachines/extensions@2024-11-01#properties/tags" - }, - "description": "Optional. Tags of the resource." - }, - "nullable": true - }, - "protectedSettingsFromKeyVault": { - "type": "object", - "metadata": { - "__bicep_resource_derived_type!": { - "source": "Microsoft.Compute/virtualMachines/extensions@2024-11-01#properties/properties/properties/protectedSettingsFromKeyVault" - }, - "description": "Optional. The extensions protected settings that are passed by reference, and consumed from key vault." - }, - "nullable": true - }, - "provisionAfterExtensions": { - "type": "array", - "metadata": { - "__bicep_resource_derived_type!": { - "source": "Microsoft.Compute/virtualMachines/extensions@2024-11-01#properties/properties/properties/provisionAfterExtensions" - }, - "description": "Optional. Collection of extension names after which this extension needs to be provisioned." - }, - "nullable": true - } - }, - "resources": { - "virtualMachine": { - "existing": true, - "type": "Microsoft.Compute/virtualMachines", - "apiVersion": "2024-11-01", - "name": "[parameters('virtualMachineName')]" - }, - "extension": { - "type": "Microsoft.Compute/virtualMachines/extensions", - "apiVersion": "2024-11-01", - "name": "[format('{0}/{1}', parameters('virtualMachineName'), parameters('name'))]", - "location": "[parameters('location')]", - "tags": "[parameters('tags')]", - "properties": { - "publisher": "[parameters('publisher')]", - "type": "[parameters('type')]", - "typeHandlerVersion": "[parameters('typeHandlerVersion')]", - "autoUpgradeMinorVersion": "[parameters('autoUpgradeMinorVersion')]", - "enableAutomaticUpgrade": "[parameters('enableAutomaticUpgrade')]", - "forceUpdateTag": "[parameters('forceUpdateTag')]", - "settings": "[parameters('settings')]", - "protectedSettings": "[parameters('protectedSettings')]", - "suppressFailures": "[parameters('supressFailures')]", - "protectedSettingsFromKeyVault": "[parameters('protectedSettingsFromKeyVault')]", - "provisionAfterExtensions": "[parameters('provisionAfterExtensions')]" - } - } - }, + "resources": [], "outputs": { - "name": { - "type": "string", - "metadata": { - "description": "The name of the extension." - }, - "value": "[parameters('name')]" - }, - "resourceId": { - "type": "string", - "metadata": { - "description": "The resource ID of the extension." - }, - "value": "[resourceId('Microsoft.Compute/virtualMachines/extensions', parameters('virtualMachineName'), parameters('name'))]" - }, - "resourceGroupName": { - "type": "string", - "metadata": { - "description": "The name of the Resource Group the extension was created in." - }, - "value": "[resourceGroup().name]" - }, - "location": { - "type": "string", - "metadata": { - "description": "The location the resource was deployed into." - }, - "value": "[reference('extension', '2024-11-01', 'full').location]" + "telemetry": { + "type": "String", + "value": "For more information, see https://aka.ms/avm/TelemetryInfo" } } } + } + }, + "appServicePlan": { + "type": "Microsoft.Web/serverfarms", + "apiVersion": "2024-11-01", + "name": "[parameters('name')]", + "kind": "[parameters('kind')]", + "location": "[parameters('location')]", + "tags": "[parameters('tags')]", + "sku": "[if(equals(parameters('skuName'), 'FC1'), createObject('name', parameters('skuName'), 'tier', 'FlexConsumption'), createObject('name', parameters('skuName'), 'capacity', parameters('skuCapacity')))]", + "properties": { + "workerTierName": "[parameters('workerTierName')]", + "hostingEnvironmentProfile": "[if(not(empty(parameters('appServiceEnvironmentResourceId'))), createObject('id', parameters('appServiceEnvironmentResourceId')), null())]", + "perSiteScaling": "[parameters('perSiteScaling')]", + "maximumElasticWorkerCount": "[parameters('maximumElasticWorkerCount')]", + "elasticScaleEnabled": "[parameters('elasticScaleEnabled')]", + "reserved": "[parameters('reserved')]", + "targetWorkerCount": "[parameters('targetWorkerCount')]", + "targetWorkerSizeId": "[parameters('targetWorkerSize')]", + "zoneRedundant": "[parameters('zoneRedundant')]" + } + }, + "appServicePlan_diagnosticSettings": { + "copy": { + "name": "appServicePlan_diagnosticSettings", + "count": "[length(coalesce(parameters('diagnosticSettings'), createArray()))]" + }, + "type": "Microsoft.Insights/diagnosticSettings", + "apiVersion": "2021-05-01-preview", + "scope": "[format('Microsoft.Web/serverfarms/{0}', parameters('name'))]", + "name": "[coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'name'), format('{0}-diagnosticSettings', parameters('name')))]", + "properties": { + "copy": [ + { + "name": "metrics", + "count": "[length(coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'metricCategories'), createArray(createObject('category', 'AllMetrics'))))]", + "input": { + "category": "[coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'metricCategories'), createArray(createObject('category', 'AllMetrics')))[copyIndex('metrics')].category]", + "enabled": "[coalesce(tryGet(coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'metricCategories'), createArray(createObject('category', 'AllMetrics')))[copyIndex('metrics')], 'enabled'), true())]", + "timeGrain": null + } + } + ], + "storageAccountId": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'storageAccountResourceId')]", + "workspaceId": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'workspaceResourceId')]", + "eventHubAuthorizationRuleId": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'eventHubAuthorizationRuleResourceId')]", + "eventHubName": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'eventHubName')]", + "marketplacePartnerId": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'marketplacePartnerResourceId')]", + "logAnalyticsDestinationType": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'logAnalyticsDestinationType')]" }, "dependsOn": [ - "vm", - "vm_microsoftAntiMalwareExtension" + "appServicePlan" ] }, - "vm_dependencyAgentExtension": { - "condition": "[parameters('extensionDependencyAgentConfig').enabled]", - "type": "Microsoft.Resources/deployments", - "apiVersion": "2022-09-01", - "name": "[format('{0}-VM-DependencyAgent', uniqueString(deployment().name, parameters('location')))]", + "appServicePlan_lock": { + "condition": "[and(not(empty(coalesce(parameters('lock'), createObject()))), not(equals(tryGet(parameters('lock'), 'kind'), 'None')))]", + "type": "Microsoft.Authorization/locks", + "apiVersion": "2020-05-01", + "scope": "[format('Microsoft.Web/serverfarms/{0}', parameters('name'))]", + "name": "[coalesce(tryGet(parameters('lock'), 'name'), format('lock-{0}', parameters('name')))]", "properties": { - "expressionEvaluationOptions": { - "scope": "inner" - }, - "mode": "Incremental", - "parameters": { - "virtualMachineName": { - "value": "[parameters('name')]" - }, - "name": { - "value": "[coalesce(tryGet(parameters('extensionDependencyAgentConfig'), 'name'), 'DependencyAgent')]" - }, - "location": { - "value": "[parameters('location')]" - }, - "publisher": { - "value": "Microsoft.Azure.Monitoring.DependencyAgent" - }, - "type": "[if(equals(parameters('osType'), 'Windows'), createObject('value', 'DependencyAgentWindows'), createObject('value', 'DependencyAgentLinux'))]", - "typeHandlerVersion": { - "value": "[coalesce(tryGet(parameters('extensionDependencyAgentConfig'), 'typeHandlerVersion'), '9.10')]" - }, - "autoUpgradeMinorVersion": { - "value": "[coalesce(tryGet(parameters('extensionDependencyAgentConfig'), 'autoUpgradeMinorVersion'), true())]" - }, - "enableAutomaticUpgrade": { - "value": "[coalesce(tryGet(parameters('extensionDependencyAgentConfig'), 'enableAutomaticUpgrade'), true())]" - }, - "settings": { - "value": { - "enableAMA": "[coalesce(tryGet(parameters('extensionDependencyAgentConfig'), 'enableAMA'), true())]" - } - }, - "supressFailures": { - "value": "[coalesce(tryGet(parameters('extensionDependencyAgentConfig'), 'supressFailures'), false())]" - }, - "tags": { - "value": "[coalesce(tryGet(parameters('extensionDependencyAgentConfig'), 'tags'), parameters('tags'))]" - } - }, - "template": { - "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", - "languageVersion": "2.0", - "contentVersion": "1.0.0.0", - "metadata": { - "_generator": { - "name": "bicep", - "version": "0.37.4.10188", - "templateHash": "13125609748815648088" - }, - "name": "Virtual Machine Extensions", - "description": "This module deploys a Virtual Machine Extension." - }, - "parameters": { - "virtualMachineName": { - "type": "string", - "metadata": { - "description": "Conditional. The name of the parent virtual machine that extension is provisioned for. Required if the template is used in a standalone deployment." - } - }, - "name": { - "type": "string", - "metadata": { - "description": "Required. The name of the virtual machine extension." - } - }, - "location": { - "type": "string", - "defaultValue": "[resourceGroup().location]", - "metadata": { - "description": "Optional. The location the extension is deployed to." - } - }, - "publisher": { - "type": "string", - "metadata": { - "description": "Required. The name of the extension handler publisher." - } - }, - "type": { - "type": "string", - "metadata": { - "description": "Required. Specifies the type of the extension; an example is \"CustomScriptExtension\"." - } - }, - "typeHandlerVersion": { - "type": "string", - "metadata": { - "description": "Required. Specifies the version of the script handler." - } - }, - "autoUpgradeMinorVersion": { - "type": "bool", - "metadata": { - "description": "Required. Indicates whether the extension should use a newer minor version if one is available at deployment time. Once deployed, however, the extension will not upgrade minor versions unless redeployed, even with this property set to true." - } - }, - "forceUpdateTag": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. How the extension handler should be forced to update even if the extension configuration has not changed." - } - }, - "settings": { - "type": "object", - "nullable": true, - "metadata": { - "description": "Optional. Any object that contains the extension specific settings." - } - }, - "protectedSettings": { - "type": "secureObject", - "nullable": true, - "metadata": { - "description": "Optional. Any object that contains the extension specific protected settings." - } - }, - "supressFailures": { - "type": "bool", - "defaultValue": false, - "metadata": { - "description": "Optional. Indicates whether failures stemming from the extension will be suppressed (Operational failures such as not connecting to the VM will not be suppressed regardless of this value). The default is false." - } - }, - "enableAutomaticUpgrade": { - "type": "bool", - "metadata": { - "description": "Required. Indicates whether the extension should be automatically upgraded by the platform if there is a newer version of the extension available." - } - }, - "tags": { - "type": "object", - "metadata": { - "__bicep_resource_derived_type!": { - "source": "Microsoft.Compute/virtualMachines/extensions@2024-11-01#properties/tags" - }, - "description": "Optional. Tags of the resource." - }, - "nullable": true - }, - "protectedSettingsFromKeyVault": { - "type": "object", - "metadata": { - "__bicep_resource_derived_type!": { - "source": "Microsoft.Compute/virtualMachines/extensions@2024-11-01#properties/properties/properties/protectedSettingsFromKeyVault" - }, - "description": "Optional. The extensions protected settings that are passed by reference, and consumed from key vault." - }, - "nullable": true - }, - "provisionAfterExtensions": { - "type": "array", - "metadata": { - "__bicep_resource_derived_type!": { - "source": "Microsoft.Compute/virtualMachines/extensions@2024-11-01#properties/properties/properties/provisionAfterExtensions" - }, - "description": "Optional. Collection of extension names after which this extension needs to be provisioned." - }, - "nullable": true - } - }, - "resources": { - "virtualMachine": { - "existing": true, - "type": "Microsoft.Compute/virtualMachines", - "apiVersion": "2024-11-01", - "name": "[parameters('virtualMachineName')]" - }, - "extension": { - "type": "Microsoft.Compute/virtualMachines/extensions", - "apiVersion": "2024-11-01", - "name": "[format('{0}/{1}', parameters('virtualMachineName'), parameters('name'))]", - "location": "[parameters('location')]", - "tags": "[parameters('tags')]", - "properties": { - "publisher": "[parameters('publisher')]", - "type": "[parameters('type')]", - "typeHandlerVersion": "[parameters('typeHandlerVersion')]", - "autoUpgradeMinorVersion": "[parameters('autoUpgradeMinorVersion')]", - "enableAutomaticUpgrade": "[parameters('enableAutomaticUpgrade')]", - "forceUpdateTag": "[parameters('forceUpdateTag')]", - "settings": "[parameters('settings')]", - "protectedSettings": "[parameters('protectedSettings')]", - "suppressFailures": "[parameters('supressFailures')]", - "protectedSettingsFromKeyVault": "[parameters('protectedSettingsFromKeyVault')]", - "provisionAfterExtensions": "[parameters('provisionAfterExtensions')]" - } + "level": "[coalesce(tryGet(parameters('lock'), 'kind'), '')]", + "notes": "[coalesce(tryGet(parameters('lock'), 'notes'), if(equals(tryGet(parameters('lock'), 'kind'), 'CanNotDelete'), 'Cannot delete resource or child resources.', 'Cannot delete or modify the resource or child resources.'))]" + }, + "dependsOn": [ + "appServicePlan" + ] + }, + "appServicePlan_roleAssignments": { + "copy": { + "name": "appServicePlan_roleAssignments", + "count": "[length(coalesce(variables('formattedRoleAssignments'), createArray()))]" + }, + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2022-04-01", + "scope": "[format('Microsoft.Web/serverfarms/{0}', parameters('name'))]", + "name": "[coalesce(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'name'), guid(resourceId('Microsoft.Web/serverfarms', parameters('name')), coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].principalId, coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].roleDefinitionId))]", + "properties": { + "roleDefinitionId": "[coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].roleDefinitionId]", + "principalId": "[coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].principalId]", + "description": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'description')]", + "principalType": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'principalType')]", + "condition": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'condition')]", + "conditionVersion": "[if(not(empty(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'condition'))), coalesce(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'conditionVersion'), '2.0'), null())]", + "delegatedManagedIdentityResourceId": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'delegatedManagedIdentityResourceId')]" + }, + "dependsOn": [ + "appServicePlan" + ] + } + }, + "outputs": { + "resourceGroupName": { + "type": "string", + "metadata": { + "description": "The resource group the app service plan was deployed into." + }, + "value": "[resourceGroup().name]" + }, + "name": { + "type": "string", + "metadata": { + "description": "The name of the app service plan." + }, + "value": "[parameters('name')]" + }, + "resourceId": { + "type": "string", + "metadata": { + "description": "The resource ID of the app service plan." + }, + "value": "[resourceId('Microsoft.Web/serverfarms', parameters('name'))]" + }, + "location": { + "type": "string", + "metadata": { + "description": "The location the resource was deployed into." + }, + "value": "[reference('appServicePlan', '2024-11-01', 'full').location]" + } + } + } + }, + "dependsOn": [ + "logAnalyticsWorkspace" + ] + }, + "webSite": { + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "[take(format('module.web-sites.{0}', variables('webSiteResourceName')), 64)]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "name": { + "value": "[variables('webSiteResourceName')]" + }, + "tags": { + "value": "[parameters('tags')]" + }, + "location": { + "value": "[variables('solutionLocation')]" + }, + "kind": { + "value": "app,linux,container" + }, + "serverFarmResourceId": { + "value": "[reference('webServerFarm').outputs.resourceId.value]" + }, + "managedIdentities": { + "value": { + "userAssignedResourceIds": [ + "[reference('userAssignedIdentity').outputs.resourceId.value]" + ] + } + }, + "siteConfig": { + "value": { + "linuxFxVersion": "[format('DOCKER|{0}.azurecr.io/content-gen-app:{1}', variables('acrResourceName'), parameters('imageTag'))]", + "minTlsVersion": "1.2", + "alwaysOn": true, + "ftpsState": "FtpsOnly" + } + }, + "virtualNetworkSubnetId": "[if(parameters('enablePrivateNetworking'), createObject('value', reference('virtualNetwork').outputs.webSubnetResourceId.value), createObject('value', null()))]", + "configs": { + "value": "[concat(createArray(createObject('name', 'appsettings', 'properties', createObject('DOCKER_REGISTRY_SERVER_URL', format('https://{0}.azurecr.io', variables('acrResourceName')), 'BACKEND_URL', variables('aciBackendUrl'), 'AZURE_CLIENT_ID', reference('userAssignedIdentity').outputs.clientId.value), 'applicationInsightResourceId', if(parameters('enableMonitoring'), reference('applicationInsights').outputs.resourceId.value, null()))), if(parameters('enableMonitoring'), createArray(createObject('name', 'logs', 'properties', createObject())), createArray()))]" + }, + "enableMonitoring": { + "value": "[parameters('enableMonitoring')]" + }, + "diagnosticSettings": "[if(parameters('enableMonitoring'), createObject('value', createArray(createObject('workspaceResourceId', if(variables('useExistingLogAnalytics'), parameters('existingLogAnalyticsWorkspaceId'), if(parameters('enableMonitoring'), reference('logAnalyticsWorkspace').outputs.resourceId.value, ''))))), createObject('value', null()))]", + "vnetRouteAllEnabled": { + "value": "[parameters('enablePrivateNetworking')]" + }, + "vnetImagePullEnabled": { + "value": "[parameters('enablePrivateNetworking')]" + }, + "publicNetworkAccess": { + "value": "Enabled" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "languageVersion": "2.0", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.39.26.7824", + "templateHash": "11911473200605315360" + } + }, + "definitions": { + "appSettingsConfigType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "allowedValues": [ + "appsettings", + "logs" + ], + "metadata": { + "description": "Required. The type of config." + } + }, + "storageAccountUseIdentityAuthentication": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. If the provided storage account requires Identity based authentication." + } + }, + "storageAccountResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Required if app of kind functionapp. Resource ID of the storage account to manage triggers and logging function executions." + } + }, + "applicationInsightResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Resource ID of the application insight to leverage for this resource." + } + }, + "retainCurrentAppSettings": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. The retain the current app settings. Defaults to true." + } + }, + "properties": { + "type": "object", + "properties": {}, + "additionalProperties": { + "type": "string", + "metadata": { + "description": "Required. An app settings key-value pair." } }, - "outputs": { - "name": { - "type": "string", - "metadata": { - "description": "The name of the extension." - }, - "value": "[parameters('name')]" - }, - "resourceId": { - "type": "string", - "metadata": { - "description": "The resource ID of the extension." - }, - "value": "[resourceId('Microsoft.Compute/virtualMachines/extensions', parameters('virtualMachineName'), parameters('name'))]" - }, - "resourceGroupName": { - "type": "string", - "metadata": { - "description": "The name of the Resource Group the extension was created in." - }, - "value": "[resourceGroup().name]" - }, - "location": { - "type": "string", - "metadata": { - "description": "The location the resource was deployed into." - }, - "value": "[reference('extension', '2024-11-01', 'full').location]" - } + "nullable": true, + "metadata": { + "description": "Optional. The app settings key-value pairs." } } }, - "dependsOn": [ - "vm", - "vm_azureMonitorAgentExtension" - ] + "metadata": { + "__bicep_export!": true, + "description": "The type of an app settings configuration." + } }, - "vm_networkWatcherAgentExtension": { - "condition": "[parameters('extensionNetworkWatcherAgentConfig').enabled]", - "type": "Microsoft.Resources/deployments", - "apiVersion": "2022-09-01", - "name": "[format('{0}-VM-NetworkWatcherAgent', uniqueString(deployment().name, parameters('location')))]", + "_1.lockType": { + "type": "object", "properties": { - "expressionEvaluationOptions": { - "scope": "inner" - }, - "mode": "Incremental", - "parameters": { - "virtualMachineName": { - "value": "[parameters('name')]" - }, - "name": { - "value": "[coalesce(tryGet(parameters('extensionNetworkWatcherAgentConfig'), 'name'), 'NetworkWatcherAgent')]" - }, - "location": { - "value": "[parameters('location')]" - }, - "publisher": { - "value": "Microsoft.Azure.NetworkWatcher" - }, - "type": "[if(equals(parameters('osType'), 'Windows'), createObject('value', 'NetworkWatcherAgentWindows'), createObject('value', 'NetworkWatcherAgentLinux'))]", - "typeHandlerVersion": { - "value": "[coalesce(tryGet(parameters('extensionNetworkWatcherAgentConfig'), 'typeHandlerVersion'), '1.4')]" - }, - "autoUpgradeMinorVersion": { - "value": "[coalesce(tryGet(parameters('extensionNetworkWatcherAgentConfig'), 'autoUpgradeMinorVersion'), true())]" - }, - "enableAutomaticUpgrade": { - "value": "[coalesce(tryGet(parameters('extensionNetworkWatcherAgentConfig'), 'enableAutomaticUpgrade'), false())]" - }, - "supressFailures": { - "value": "[coalesce(tryGet(parameters('extensionNetworkWatcherAgentConfig'), 'supressFailures'), false())]" - }, - "tags": { - "value": "[coalesce(tryGet(parameters('extensionNetworkWatcherAgentConfig'), 'tags'), parameters('tags'))]" + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Specify the name of lock." } }, - "template": { - "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", - "languageVersion": "2.0", - "contentVersion": "1.0.0.0", + "kind": { + "type": "string", + "allowedValues": [ + "CanNotDelete", + "None", + "ReadOnly" + ], + "nullable": true, "metadata": { - "_generator": { - "name": "bicep", - "version": "0.37.4.10188", - "templateHash": "13125609748815648088" - }, - "name": "Virtual Machine Extensions", - "description": "This module deploys a Virtual Machine Extension." - }, - "parameters": { - "virtualMachineName": { - "type": "string", - "metadata": { - "description": "Conditional. The name of the parent virtual machine that extension is provisioned for. Required if the template is used in a standalone deployment." - } - }, - "name": { - "type": "string", - "metadata": { - "description": "Required. The name of the virtual machine extension." - } - }, - "location": { - "type": "string", - "defaultValue": "[resourceGroup().location]", - "metadata": { - "description": "Optional. The location the extension is deployed to." - } - }, - "publisher": { - "type": "string", - "metadata": { - "description": "Required. The name of the extension handler publisher." - } - }, - "type": { - "type": "string", - "metadata": { - "description": "Required. Specifies the type of the extension; an example is \"CustomScriptExtension\"." - } - }, - "typeHandlerVersion": { - "type": "string", - "metadata": { - "description": "Required. Specifies the version of the script handler." - } - }, - "autoUpgradeMinorVersion": { - "type": "bool", - "metadata": { - "description": "Required. Indicates whether the extension should use a newer minor version if one is available at deployment time. Once deployed, however, the extension will not upgrade minor versions unless redeployed, even with this property set to true." - } - }, - "forceUpdateTag": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. How the extension handler should be forced to update even if the extension configuration has not changed." - } - }, - "settings": { - "type": "object", - "nullable": true, - "metadata": { - "description": "Optional. Any object that contains the extension specific settings." - } - }, - "protectedSettings": { - "type": "secureObject", - "nullable": true, - "metadata": { - "description": "Optional. Any object that contains the extension specific protected settings." - } - }, - "supressFailures": { - "type": "bool", - "defaultValue": false, - "metadata": { - "description": "Optional. Indicates whether failures stemming from the extension will be suppressed (Operational failures such as not connecting to the VM will not be suppressed regardless of this value). The default is false." - } - }, - "enableAutomaticUpgrade": { - "type": "bool", - "metadata": { - "description": "Required. Indicates whether the extension should be automatically upgraded by the platform if there is a newer version of the extension available." - } - }, - "tags": { - "type": "object", - "metadata": { - "__bicep_resource_derived_type!": { - "source": "Microsoft.Compute/virtualMachines/extensions@2024-11-01#properties/tags" - }, - "description": "Optional. Tags of the resource." - }, - "nullable": true - }, - "protectedSettingsFromKeyVault": { - "type": "object", - "metadata": { - "__bicep_resource_derived_type!": { - "source": "Microsoft.Compute/virtualMachines/extensions@2024-11-01#properties/properties/properties/protectedSettingsFromKeyVault" - }, - "description": "Optional. The extensions protected settings that are passed by reference, and consumed from key vault." - }, - "nullable": true - }, - "provisionAfterExtensions": { - "type": "array", - "metadata": { - "__bicep_resource_derived_type!": { - "source": "Microsoft.Compute/virtualMachines/extensions@2024-11-01#properties/properties/properties/provisionAfterExtensions" - }, - "description": "Optional. Collection of extension names after which this extension needs to be provisioned." - }, - "nullable": true - } - }, - "resources": { - "virtualMachine": { - "existing": true, - "type": "Microsoft.Compute/virtualMachines", - "apiVersion": "2024-11-01", - "name": "[parameters('virtualMachineName')]" - }, - "extension": { - "type": "Microsoft.Compute/virtualMachines/extensions", - "apiVersion": "2024-11-01", - "name": "[format('{0}/{1}', parameters('virtualMachineName'), parameters('name'))]", - "location": "[parameters('location')]", - "tags": "[parameters('tags')]", - "properties": { - "publisher": "[parameters('publisher')]", - "type": "[parameters('type')]", - "typeHandlerVersion": "[parameters('typeHandlerVersion')]", - "autoUpgradeMinorVersion": "[parameters('autoUpgradeMinorVersion')]", - "enableAutomaticUpgrade": "[parameters('enableAutomaticUpgrade')]", - "forceUpdateTag": "[parameters('forceUpdateTag')]", - "settings": "[parameters('settings')]", - "protectedSettings": "[parameters('protectedSettings')]", - "suppressFailures": "[parameters('supressFailures')]", - "protectedSettingsFromKeyVault": "[parameters('protectedSettingsFromKeyVault')]", - "provisionAfterExtensions": "[parameters('provisionAfterExtensions')]" - } - } - }, - "outputs": { - "name": { - "type": "string", - "metadata": { - "description": "The name of the extension." - }, - "value": "[parameters('name')]" - }, - "resourceId": { - "type": "string", - "metadata": { - "description": "The resource ID of the extension." - }, - "value": "[resourceId('Microsoft.Compute/virtualMachines/extensions', parameters('virtualMachineName'), parameters('name'))]" - }, - "resourceGroupName": { - "type": "string", - "metadata": { - "description": "The name of the Resource Group the extension was created in." - }, - "value": "[resourceGroup().name]" - }, - "location": { - "type": "string", - "metadata": { - "description": "The location the resource was deployed into." - }, - "value": "[reference('extension', '2024-11-01', 'full').location]" - } + "description": "Optional. Specify the type of lock." } } }, - "dependsOn": [ - "vm", - "vm_dependencyAgentExtension" - ] + "metadata": { + "description": "An AVM-aligned type for a lock.", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.5.1" + } + } }, - "vm_desiredStateConfigurationExtension": { - "condition": "[parameters('extensionDSCConfig').enabled]", - "type": "Microsoft.Resources/deployments", - "apiVersion": "2022-09-01", - "name": "[format('{0}-VM-DesiredStateConfiguration', uniqueString(deployment().name, parameters('location')))]", + "_1.privateEndpointCustomDnsConfigType": { + "type": "object", "properties": { - "expressionEvaluationOptions": { - "scope": "inner" + "fqdn": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. FQDN that resolves to private endpoint IP address." + } }, - "mode": "Incremental", - "parameters": { - "virtualMachineName": { - "value": "[parameters('name')]" - }, - "name": { - "value": "[coalesce(tryGet(parameters('extensionDSCConfig'), 'name'), 'DesiredStateConfiguration')]" - }, - "location": { - "value": "[parameters('location')]" - }, - "publisher": { - "value": "Microsoft.Powershell" - }, - "type": { - "value": "DSC" - }, - "typeHandlerVersion": { - "value": "[coalesce(tryGet(parameters('extensionDSCConfig'), 'typeHandlerVersion'), '2.77')]" - }, - "autoUpgradeMinorVersion": { - "value": "[coalesce(tryGet(parameters('extensionDSCConfig'), 'autoUpgradeMinorVersion'), true())]" - }, - "enableAutomaticUpgrade": { - "value": "[coalesce(tryGet(parameters('extensionDSCConfig'), 'enableAutomaticUpgrade'), false())]" - }, - "settings": { - "value": "[coalesce(tryGet(parameters('extensionDSCConfig'), 'settings'), createObject())]" - }, - "supressFailures": { - "value": "[coalesce(tryGet(parameters('extensionDSCConfig'), 'supressFailures'), false())]" - }, - "tags": { - "value": "[coalesce(tryGet(parameters('extensionDSCConfig'), 'tags'), parameters('tags'))]" + "ipAddresses": { + "type": "array", + "items": { + "type": "string" }, - "protectedSettings": { - "value": "[coalesce(tryGet(parameters('extensionDSCConfig'), 'protectedSettings'), createObject())]" + "metadata": { + "description": "Required. A list of private IP addresses of the private endpoint." } - }, - "template": { - "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", - "languageVersion": "2.0", - "contentVersion": "1.0.0.0", + } + }, + "metadata": { + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.5.1" + } + } + }, + "_1.privateEndpointIpConfigurationType": { + "type": "object", + "properties": { + "name": { + "type": "string", "metadata": { - "_generator": { - "name": "bicep", - "version": "0.37.4.10188", - "templateHash": "13125609748815648088" - }, - "name": "Virtual Machine Extensions", - "description": "This module deploys a Virtual Machine Extension." - }, - "parameters": { - "virtualMachineName": { - "type": "string", - "metadata": { - "description": "Conditional. The name of the parent virtual machine that extension is provisioned for. Required if the template is used in a standalone deployment." - } - }, - "name": { - "type": "string", - "metadata": { - "description": "Required. The name of the virtual machine extension." - } - }, - "location": { - "type": "string", - "defaultValue": "[resourceGroup().location]", - "metadata": { - "description": "Optional. The location the extension is deployed to." - } - }, - "publisher": { - "type": "string", - "metadata": { - "description": "Required. The name of the extension handler publisher." - } - }, - "type": { - "type": "string", - "metadata": { - "description": "Required. Specifies the type of the extension; an example is \"CustomScriptExtension\"." - } - }, - "typeHandlerVersion": { + "description": "Required. The name of the resource that is unique within a resource group." + } + }, + "properties": { + "type": "object", + "properties": { + "groupId": { "type": "string", "metadata": { - "description": "Required. Specifies the version of the script handler." - } - }, - "autoUpgradeMinorVersion": { - "type": "bool", - "metadata": { - "description": "Required. Indicates whether the extension should use a newer minor version if one is available at deployment time. Once deployed, however, the extension will not upgrade minor versions unless redeployed, even with this property set to true." + "description": "Required. The ID of a group obtained from the remote resource that this private endpoint should connect to." } }, - "forceUpdateTag": { + "memberName": { "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. How the extension handler should be forced to update even if the extension configuration has not changed." - } - }, - "settings": { - "type": "object", - "nullable": true, - "metadata": { - "description": "Optional. Any object that contains the extension specific settings." - } - }, - "protectedSettings": { - "type": "secureObject", - "nullable": true, - "metadata": { - "description": "Optional. Any object that contains the extension specific protected settings." - } - }, - "supressFailures": { - "type": "bool", - "defaultValue": false, "metadata": { - "description": "Optional. Indicates whether failures stemming from the extension will be suppressed (Operational failures such as not connecting to the VM will not be suppressed regardless of this value). The default is false." + "description": "Required. The member name of a group obtained from the remote resource that this private endpoint should connect to." } }, - "enableAutomaticUpgrade": { - "type": "bool", + "privateIPAddress": { + "type": "string", "metadata": { - "description": "Required. Indicates whether the extension should be automatically upgraded by the platform if there is a newer version of the extension available." + "description": "Required. A private IP address obtained from the private endpoint's subnet." } - }, - "tags": { - "type": "object", - "metadata": { - "__bicep_resource_derived_type!": { - "source": "Microsoft.Compute/virtualMachines/extensions@2024-11-01#properties/tags" - }, - "description": "Optional. Tags of the resource." - }, - "nullable": true - }, - "protectedSettingsFromKeyVault": { - "type": "object", - "metadata": { - "__bicep_resource_derived_type!": { - "source": "Microsoft.Compute/virtualMachines/extensions@2024-11-01#properties/properties/properties/protectedSettingsFromKeyVault" - }, - "description": "Optional. The extensions protected settings that are passed by reference, and consumed from key vault." - }, - "nullable": true - }, - "provisionAfterExtensions": { - "type": "array", - "metadata": { - "__bicep_resource_derived_type!": { - "source": "Microsoft.Compute/virtualMachines/extensions@2024-11-01#properties/properties/properties/provisionAfterExtensions" - }, - "description": "Optional. Collection of extension names after which this extension needs to be provisioned." - }, - "nullable": true } }, - "resources": { - "virtualMachine": { - "existing": true, - "type": "Microsoft.Compute/virtualMachines", - "apiVersion": "2024-11-01", - "name": "[parameters('virtualMachineName')]" - }, - "extension": { - "type": "Microsoft.Compute/virtualMachines/extensions", - "apiVersion": "2024-11-01", - "name": "[format('{0}/{1}', parameters('virtualMachineName'), parameters('name'))]", - "location": "[parameters('location')]", - "tags": "[parameters('tags')]", - "properties": { - "publisher": "[parameters('publisher')]", - "type": "[parameters('type')]", - "typeHandlerVersion": "[parameters('typeHandlerVersion')]", - "autoUpgradeMinorVersion": "[parameters('autoUpgradeMinorVersion')]", - "enableAutomaticUpgrade": "[parameters('enableAutomaticUpgrade')]", - "forceUpdateTag": "[parameters('forceUpdateTag')]", - "settings": "[parameters('settings')]", - "protectedSettings": "[parameters('protectedSettings')]", - "suppressFailures": "[parameters('supressFailures')]", - "protectedSettingsFromKeyVault": "[parameters('protectedSettingsFromKeyVault')]", - "provisionAfterExtensions": "[parameters('provisionAfterExtensions')]" + "metadata": { + "description": "Required. Properties of private endpoint IP configurations." + } + } + }, + "metadata": { + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.5.1" + } + } + }, + "_1.privateEndpointPrivateDnsZoneGroupType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The name of the Private DNS Zone Group." + } + }, + "privateDnsZoneGroupConfigs": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The name of the private DNS Zone Group config." + } + }, + "privateDnsZoneResourceId": { + "type": "string", + "metadata": { + "description": "Required. The resource id of the private DNS zone." + } } } }, - "outputs": { - "name": { - "type": "string", - "metadata": { - "description": "The name of the extension." - }, - "value": "[parameters('name')]" - }, - "resourceId": { - "type": "string", - "metadata": { - "description": "The resource ID of the extension." + "metadata": { + "description": "Required. The private DNS Zone Groups to associate the Private Endpoint. A DNS Zone Group can support up to 5 DNS zones." + } + } + }, + "metadata": { + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.5.1" + } + } + }, + "_1.roleAssignmentType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The name (as GUID) of the role assignment. If not provided, a GUID will be generated." + } + }, + "roleDefinitionIdOrName": { + "type": "string", + "metadata": { + "description": "Required. The role to assign. You can provide either the display name of the role definition, the role definition GUID, or its fully qualified ID in the following format: '/providers/Microsoft.Authorization/roleDefinitions/c2f4ef07-c644-48eb-af81-4b1b4947fb11'." + } + }, + "principalId": { + "type": "string", + "metadata": { + "description": "Required. The principal ID of the principal (user/group/identity) to assign the role to." + } + }, + "principalType": { + "type": "string", + "allowedValues": [ + "Device", + "ForeignGroup", + "Group", + "ServicePrincipal", + "User" + ], + "nullable": true, + "metadata": { + "description": "Optional. The principal type of the assigned principal ID." + } + }, + "description": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The description of the role assignment." + } + }, + "condition": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The conditions on the role assignment. This limits the resources it can be assigned to. e.g.: @Resource[Microsoft.Storage/storageAccounts/blobServices/containers:ContainerName] StringEqualsIgnoreCase \"foo_storage_container\"." + } + }, + "conditionVersion": { + "type": "string", + "allowedValues": [ + "2.0" + ], + "nullable": true, + "metadata": { + "description": "Optional. Version of the condition." + } + }, + "delegatedManagedIdentityResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The Resource Id of the delegated managed identity resource." + } + } + }, + "metadata": { + "description": "An AVM-aligned type for a role assignment.", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.5.1" + } + } + }, + "diagnosticSettingFullType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The name of the diagnostic setting." + } + }, + "logCategoriesAndGroups": { + "type": "array", + "items": { + "type": "object", + "properties": { + "category": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Name of a Diagnostic Log category for a resource type this setting is applied to. Set the specific logs to collect here." + } }, - "value": "[resourceId('Microsoft.Compute/virtualMachines/extensions', parameters('virtualMachineName'), parameters('name'))]" - }, - "resourceGroupName": { - "type": "string", - "metadata": { - "description": "The name of the Resource Group the extension was created in." + "categoryGroup": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Name of a Diagnostic Log category group for a resource type this setting is applied to. Set to `allLogs` to collect all logs." + } }, - "value": "[resourceGroup().name]" - }, - "location": { - "type": "string", - "metadata": { - "description": "The location the resource was deployed into." + "enabled": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. Enable or disable the category explicitly. Default is `true`." + } + } + } + }, + "nullable": true, + "metadata": { + "description": "Optional. The name of logs that will be streamed. \"allLogs\" includes all possible logs for the resource. Set to `[]` to disable log collection." + } + }, + "metricCategories": { + "type": "array", + "items": { + "type": "object", + "properties": { + "category": { + "type": "string", + "metadata": { + "description": "Required. Name of a Diagnostic Metric category for a resource type this setting is applied to. Set to `AllMetrics` to collect all metrics." + } }, - "value": "[reference('extension', '2024-11-01', 'full').location]" + "enabled": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. Enable or disable the category explicitly. Default is `true`." + } + } } + }, + "nullable": true, + "metadata": { + "description": "Optional. The name of metrics that will be streamed. \"allMetrics\" includes all possible metrics for the resource. Set to `[]` to disable metric collection." + } + }, + "logAnalyticsDestinationType": { + "type": "string", + "allowedValues": [ + "AzureDiagnostics", + "Dedicated" + ], + "nullable": true, + "metadata": { + "description": "Optional. A string indicating whether the export to Log Analytics should use the default destination type, i.e. AzureDiagnostics, or use a destination type." + } + }, + "workspaceResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Resource ID of the diagnostic log analytics workspace. For security reasons, it is recommended to set diagnostic settings to send data to either storage account, log analytics workspace or event hub." + } + }, + "storageAccountResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Resource ID of the diagnostic storage account. For security reasons, it is recommended to set diagnostic settings to send data to either storage account, log analytics workspace or event hub." + } + }, + "eventHubAuthorizationRuleResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Resource ID of the diagnostic event hub authorization rule for the Event Hubs namespace in which the event hub should be created or streamed to." + } + }, + "eventHubName": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Name of the diagnostic event hub within the namespace to which logs are streamed. Without this, an event hub is created for each log category. For security reasons, it is recommended to set diagnostic settings to send data to either storage account, log analytics workspace or event hub." + } + }, + "marketplacePartnerResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The full ARM resource ID of the Marketplace resource to which you would like to send Diagnostic Logs." } } }, - "dependsOn": [ - "vm", - "vm_networkWatcherAgentExtension" - ] + "metadata": { + "description": "An AVM-aligned type for a diagnostic setting. To be used if both logs & metrics are supported by the resource provider.", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.5.1" + } + } }, - "vm_customScriptExtension": { - "condition": "[not(empty(parameters('extensionCustomScriptConfig')))]", - "type": "Microsoft.Resources/deployments", - "apiVersion": "2022-09-01", - "name": "[format('{0}-VM-CustomScriptExtension', uniqueString(deployment().name, parameters('location')))]", + "managedIdentityAllType": { + "type": "object", "properties": { - "expressionEvaluationOptions": { - "scope": "inner" + "systemAssigned": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. Enables system assigned managed identity on the resource." + } }, - "mode": "Incremental", - "parameters": { - "virtualMachineName": { - "value": "[parameters('name')]" - }, - "name": { - "value": "[coalesce(tryGet(parameters('extensionCustomScriptConfig'), 'name'), 'CustomScriptExtension')]" - }, - "location": { - "value": "[parameters('location')]" - }, - "publisher": "[if(equals(parameters('osType'), 'Windows'), createObject('value', 'Microsoft.Compute'), createObject('value', 'Microsoft.Azure.Extensions'))]", - "type": "[if(equals(parameters('osType'), 'Windows'), createObject('value', 'CustomScriptExtension'), createObject('value', 'CustomScript'))]", - "typeHandlerVersion": { - "value": "[coalesce(tryGet(parameters('extensionCustomScriptConfig'), 'typeHandlerVersion'), if(equals(parameters('osType'), 'Windows'), '1.10', '2.1'))]" - }, - "autoUpgradeMinorVersion": { - "value": "[coalesce(tryGet(parameters('extensionCustomScriptConfig'), 'autoUpgradeMinorVersion'), true())]" - }, - "enableAutomaticUpgrade": { - "value": "[coalesce(tryGet(parameters('extensionCustomScriptConfig'), 'enableAutomaticUpgrade'), false())]" - }, - "forceUpdateTag": { - "value": "[tryGet(parameters('extensionCustomScriptConfig'), 'forceUpdateTag')]" - }, - "provisionAfterExtensions": { - "value": "[tryGet(parameters('extensionCustomScriptConfig'), 'provisionAfterExtensions')]" - }, - "supressFailures": { - "value": "[coalesce(tryGet(parameters('extensionCustomScriptConfig'), 'supressFailures'), false())]" + "userAssignedResourceIds": { + "type": "array", + "items": { + "type": "string" }, - "tags": { - "value": "[coalesce(tryGet(parameters('extensionCustomScriptConfig'), 'tags'), parameters('tags'))]" + "nullable": true, + "metadata": { + "description": "Optional. The resource ID(s) to assign to the resource. Required if a user assigned identity is used for encryption." + } + } + }, + "metadata": { + "description": "An AVM-aligned type for a managed identity configuration. To be used if both a system-assigned & user-assigned identities are supported by the resource provider.", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.5.1" + } + } + }, + "privateEndpointSingleServiceType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The name of the Private Endpoint." + } + }, + "location": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The location to deploy the Private Endpoint to." + } + }, + "privateLinkServiceConnectionName": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The name of the private link connection to create." + } + }, + "service": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The subresource to deploy the Private Endpoint for. For example \"vault\" for a Key Vault Private Endpoint." + } + }, + "subnetResourceId": { + "type": "string", + "metadata": { + "description": "Required. Resource ID of the subnet where the endpoint needs to be created." + } + }, + "resourceGroupResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The resource ID of the Resource Group the Private Endpoint will be created in. If not specified, the Resource Group of the provided Virtual Network Subnet is used." + } + }, + "privateDnsZoneGroup": { + "$ref": "#/definitions/_1.privateEndpointPrivateDnsZoneGroupType", + "nullable": true, + "metadata": { + "description": "Optional. The private DNS Zone Group to configure for the Private Endpoint." + } + }, + "isManualConnection": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. If Manual Private Link Connection is required." + } + }, + "manualConnectionRequestMessage": { + "type": "string", + "nullable": true, + "maxLength": 140, + "metadata": { + "description": "Optional. A message passed to the owner of the remote resource with the manual connection request." + } + }, + "customDnsConfigs": { + "type": "array", + "items": { + "$ref": "#/definitions/_1.privateEndpointCustomDnsConfigType" }, - "protectedSettingsFromKeyVault": { - "value": "[tryGet(parameters('extensionCustomScriptConfig'), 'protectedSettingsFromKeyVault')]" + "nullable": true, + "metadata": { + "description": "Optional. Custom DNS configurations." + } + }, + "ipConfigurations": { + "type": "array", + "items": { + "$ref": "#/definitions/_1.privateEndpointIpConfigurationType" }, - "settings": { - "value": "[shallowMerge(createArray(if(not(empty(tryGet(tryGet(parameters('extensionCustomScriptConfig'), 'settings'), 'commandToExecute'))), createObject('commandToExecute', tryGet(tryGet(parameters('extensionCustomScriptConfig'), 'settings'), 'commandToExecute')), createObject()), if(not(empty(tryGet(tryGet(parameters('extensionCustomScriptConfig'), 'settings'), 'fileUris'))), createObject('fileUris', tryGet(parameters('extensionCustomScriptConfig'), 'settings', 'fileUris')), createObject())))]" + "nullable": true, + "metadata": { + "description": "Optional. A list of IP configurations of the Private Endpoint. This will be used to map to the first-party Service endpoints." + } + }, + "applicationSecurityGroupResourceIds": { + "type": "array", + "items": { + "type": "string" }, - "protectedSettings": { - "value": "[shallowMerge(createArray(if(not(empty(tryGet(tryGet(parameters('extensionCustomScriptConfig'), 'protectedSettings'), 'commandToExecute'))), createObject('commandToExecute', tryGet(parameters('extensionCustomScriptConfig').protectedSettings, 'commandToExecute')), createObject()), if(not(empty(tryGet(tryGet(parameters('extensionCustomScriptConfig'), 'protectedSettings'), 'storageAccountName'))), createObject('storageAccountName', parameters('extensionCustomScriptConfig').protectedSettings.storageAccountName), createObject()), if(not(empty(tryGet(tryGet(parameters('extensionCustomScriptConfig'), 'protectedSettings'), 'storageAccountKey'))), createObject('storageAccountKey', parameters('extensionCustomScriptConfig').protectedSettings.storageAccountKey), createObject()), if(not(empty(tryGet(tryGet(parameters('extensionCustomScriptConfig'), 'protectedSettings'), 'fileUris'))), createObject('fileUris', parameters('extensionCustomScriptConfig').protectedSettings.fileUris), createObject()), if(not(equals(tryGet(tryGet(parameters('extensionCustomScriptConfig'), 'protectedSettings'), 'managedIdentityResourceId'), null())), createObject('managedIdentity', if(not(empty(tryGet(parameters('extensionCustomScriptConfig').protectedSettings, 'managedIdentityResourceId'))), createObject('clientId', reference('cseIdentity').clientId), createObject())), createObject())))]" + "nullable": true, + "metadata": { + "description": "Optional. Application security groups in which the Private Endpoint IP configuration is included." } }, - "template": { - "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", - "languageVersion": "2.0", - "contentVersion": "1.0.0.0", + "customNetworkInterfaceName": { + "type": "string", + "nullable": true, "metadata": { - "_generator": { - "name": "bicep", - "version": "0.37.4.10188", - "templateHash": "13125609748815648088" - }, - "name": "Virtual Machine Extensions", - "description": "This module deploys a Virtual Machine Extension." - }, - "parameters": { - "virtualMachineName": { - "type": "string", - "metadata": { - "description": "Conditional. The name of the parent virtual machine that extension is provisioned for. Required if the template is used in a standalone deployment." - } - }, - "name": { - "type": "string", - "metadata": { - "description": "Required. The name of the virtual machine extension." - } - }, - "location": { - "type": "string", - "defaultValue": "[resourceGroup().location]", - "metadata": { - "description": "Optional. The location the extension is deployed to." - } - }, - "publisher": { - "type": "string", - "metadata": { - "description": "Required. The name of the extension handler publisher." - } - }, - "type": { - "type": "string", - "metadata": { - "description": "Required. Specifies the type of the extension; an example is \"CustomScriptExtension\"." - } - }, - "typeHandlerVersion": { - "type": "string", - "metadata": { - "description": "Required. Specifies the version of the script handler." - } - }, - "autoUpgradeMinorVersion": { - "type": "bool", - "metadata": { - "description": "Required. Indicates whether the extension should use a newer minor version if one is available at deployment time. Once deployed, however, the extension will not upgrade minor versions unless redeployed, even with this property set to true." - } - }, - "forceUpdateTag": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. How the extension handler should be forced to update even if the extension configuration has not changed." - } - }, - "settings": { - "type": "object", - "nullable": true, - "metadata": { - "description": "Optional. Any object that contains the extension specific settings." - } - }, - "protectedSettings": { - "type": "secureObject", - "nullable": true, - "metadata": { - "description": "Optional. Any object that contains the extension specific protected settings." - } - }, - "supressFailures": { - "type": "bool", - "defaultValue": false, - "metadata": { - "description": "Optional. Indicates whether failures stemming from the extension will be suppressed (Operational failures such as not connecting to the VM will not be suppressed regardless of this value). The default is false." - } - }, - "enableAutomaticUpgrade": { - "type": "bool", - "metadata": { - "description": "Required. Indicates whether the extension should be automatically upgraded by the platform if there is a newer version of the extension available." - } - }, - "tags": { - "type": "object", - "metadata": { - "__bicep_resource_derived_type!": { - "source": "Microsoft.Compute/virtualMachines/extensions@2024-11-01#properties/tags" - }, - "description": "Optional. Tags of the resource." - }, - "nullable": true - }, - "protectedSettingsFromKeyVault": { - "type": "object", - "metadata": { - "__bicep_resource_derived_type!": { - "source": "Microsoft.Compute/virtualMachines/extensions@2024-11-01#properties/properties/properties/protectedSettingsFromKeyVault" - }, - "description": "Optional. The extensions protected settings that are passed by reference, and consumed from key vault." - }, - "nullable": true - }, - "provisionAfterExtensions": { - "type": "array", - "metadata": { - "__bicep_resource_derived_type!": { - "source": "Microsoft.Compute/virtualMachines/extensions@2024-11-01#properties/properties/properties/provisionAfterExtensions" - }, - "description": "Optional. Collection of extension names after which this extension needs to be provisioned." - }, - "nullable": true - } - }, - "resources": { - "virtualMachine": { - "existing": true, - "type": "Microsoft.Compute/virtualMachines", - "apiVersion": "2024-11-01", - "name": "[parameters('virtualMachineName')]" - }, - "extension": { - "type": "Microsoft.Compute/virtualMachines/extensions", - "apiVersion": "2024-11-01", - "name": "[format('{0}/{1}', parameters('virtualMachineName'), parameters('name'))]", - "location": "[parameters('location')]", - "tags": "[parameters('tags')]", - "properties": { - "publisher": "[parameters('publisher')]", - "type": "[parameters('type')]", - "typeHandlerVersion": "[parameters('typeHandlerVersion')]", - "autoUpgradeMinorVersion": "[parameters('autoUpgradeMinorVersion')]", - "enableAutomaticUpgrade": "[parameters('enableAutomaticUpgrade')]", - "forceUpdateTag": "[parameters('forceUpdateTag')]", - "settings": "[parameters('settings')]", - "protectedSettings": "[parameters('protectedSettings')]", - "suppressFailures": "[parameters('supressFailures')]", - "protectedSettingsFromKeyVault": "[parameters('protectedSettingsFromKeyVault')]", - "provisionAfterExtensions": "[parameters('provisionAfterExtensions')]" - } - } + "description": "Optional. The custom name of the network interface attached to the Private Endpoint." + } + }, + "lock": { + "$ref": "#/definitions/_1.lockType", + "nullable": true, + "metadata": { + "description": "Optional. Specify the type of lock." + } + }, + "roleAssignments": { + "type": "array", + "items": { + "$ref": "#/definitions/_1.roleAssignmentType" }, - "outputs": { - "name": { - "type": "string", - "metadata": { - "description": "The name of the extension." - }, - "value": "[parameters('name')]" - }, - "resourceId": { - "type": "string", - "metadata": { - "description": "The resource ID of the extension." - }, - "value": "[resourceId('Microsoft.Compute/virtualMachines/extensions', parameters('virtualMachineName'), parameters('name'))]" - }, - "resourceGroupName": { - "type": "string", - "metadata": { - "description": "The name of the Resource Group the extension was created in." - }, - "value": "[resourceGroup().name]" - }, - "location": { - "type": "string", - "metadata": { - "description": "The location the resource was deployed into." - }, - "value": "[reference('extension', '2024-11-01', 'full').location]" - } + "nullable": true, + "metadata": { + "description": "Optional. Array of role assignments to create." + } + }, + "tags": { + "type": "object", + "nullable": true, + "metadata": { + "description": "Optional. Tags to be applied on all resources/Resource Groups in this deployment." + } + }, + "enableTelemetry": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. Enable/Disable usage telemetry for module." } } }, - "dependsOn": [ - "cseIdentity", - "vm", - "vm_desiredStateConfigurationExtension" - ] + "metadata": { + "description": "An AVM-aligned type for a private endpoint. To be used if the private endpoint's default service / groupId can be assumed (i.e., for services that only have one Private Endpoint type like 'vault' for key vault).", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.5.1" + } + } + } + }, + "parameters": { + "name": { + "type": "string", + "metadata": { + "description": "Required. Name of the site." + } + }, + "location": { + "type": "string", + "defaultValue": "[resourceGroup().location]", + "metadata": { + "description": "Optional. Location for all Resources." + } + }, + "kind": { + "type": "string", + "allowedValues": [ + "functionapp", + "functionapp,linux", + "functionapp,workflowapp", + "functionapp,workflowapp,linux", + "functionapp,linux,container", + "functionapp,linux,container,azurecontainerapps", + "app,linux", + "app", + "linux,api", + "api", + "app,linux,container", + "app,container,windows" + ], + "metadata": { + "description": "Required. Type of site to deploy." + } + }, + "serverFarmResourceId": { + "type": "string", + "metadata": { + "description": "Required. The resource ID of the app service plan to use for the site." + } + }, + "managedEnvironmentId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Azure Resource Manager ID of the customers selected Managed Environment on which to host this app." + } + }, + "httpsOnly": { + "type": "bool", + "defaultValue": true, + "metadata": { + "description": "Optional. Configures a site to accept only HTTPS requests." + } + }, + "clientAffinityEnabled": { + "type": "bool", + "defaultValue": true, + "metadata": { + "description": "Optional. If client affinity is enabled." + } + }, + "appServiceEnvironmentResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The resource ID of the app service environment to use for this resource." + } + }, + "managedIdentities": { + "$ref": "#/definitions/managedIdentityAllType", + "nullable": true, + "metadata": { + "description": "Optional. The managed identity definition for this resource." + } + }, + "keyVaultAccessIdentityResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The resource ID of the assigned identity to be used to access a key vault with." + } + }, + "storageAccountRequired": { + "type": "bool", + "defaultValue": false, + "metadata": { + "description": "Optional. Checks if Customer provided storage account is required." + } + }, + "enableMonitoring": { + "type": "bool", + "defaultValue": false, + "metadata": { + "description": "Optional. Enable monitoring and logging configuration." + } + }, + "virtualNetworkSubnetId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Azure Resource Manager ID of the Virtual network and subnet to be joined by Regional VNET Integration." + } + }, + "vnetContentShareEnabled": { + "type": "bool", + "defaultValue": false, + "metadata": { + "description": "Optional. To enable accessing content over virtual network." + } + }, + "vnetImagePullEnabled": { + "type": "bool", + "defaultValue": false, + "metadata": { + "description": "Optional. To enable pulling image over Virtual Network." + } + }, + "vnetRouteAllEnabled": { + "type": "bool", + "defaultValue": false, + "metadata": { + "description": "Optional. Virtual Network Route All enabled." + } + }, + "scmSiteAlsoStopped": { + "type": "bool", + "defaultValue": false, + "metadata": { + "description": "Optional. Stop SCM (KUDU) site when the app is stopped." + } + }, + "siteConfig": { + "type": "object", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.Web/sites@2024-04-01#properties/properties/properties/siteConfig" + }, + "description": "Optional. The site config object." + }, + "defaultValue": { + "alwaysOn": true, + "minTlsVersion": "1.2", + "ftpsState": "FtpsOnly" + } + }, + "configs": { + "type": "array", + "items": { + "$ref": "#/definitions/appSettingsConfigType" + }, + "nullable": true, + "metadata": { + "description": "Optional. The web site config." + } + }, + "functionAppConfig": { + "type": "object", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.Web/sites@2024-04-01#properties/properties/properties/functionAppConfig" + }, + "description": "Optional. The Function App configuration object." + }, + "nullable": true + }, + "privateEndpoints": { + "type": "array", + "items": { + "$ref": "#/definitions/privateEndpointSingleServiceType" + }, + "nullable": true, + "metadata": { + "description": "Optional. Configuration details for private endpoints." + } + }, + "tags": { + "type": "object", + "nullable": true, + "metadata": { + "description": "Optional. Tags of the resource." + } + }, + "diagnosticSettings": { + "type": "array", + "items": { + "$ref": "#/definitions/diagnosticSettingFullType" + }, + "nullable": true, + "metadata": { + "description": "Optional. The diagnostic settings of the service." + } + }, + "clientCertEnabled": { + "type": "bool", + "defaultValue": false, + "metadata": { + "description": "Optional. To enable client certificate authentication (TLS mutual authentication)." + } + }, + "clientCertExclusionPaths": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Client certificate authentication comma-separated exclusion paths." + } + }, + "clientCertMode": { + "type": "string", + "defaultValue": "Optional", + "allowedValues": [ + "Optional", + "OptionalInteractiveUser", + "Required" + ], + "metadata": { + "description": "Optional. Client certificate mode." + } + }, + "cloningInfo": { + "type": "object", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.Web/sites@2024-04-01#properties/properties/properties/cloningInfo" + }, + "description": "Optional. If specified during app creation, the app is cloned from a source app." + }, + "nullable": true + }, + "containerSize": { + "type": "int", + "nullable": true, + "metadata": { + "description": "Optional. Size of the function container." + } + }, + "dailyMemoryTimeQuota": { + "type": "int", + "nullable": true, + "metadata": { + "description": "Optional. Maximum allowed daily memory-time quota (applicable on dynamic apps only)." + } + }, + "enabled": { + "type": "bool", + "defaultValue": true, + "metadata": { + "description": "Optional. Setting this value to false disables the app (takes the app offline)." + } + }, + "hostNameSslStates": { + "type": "array", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.Web/sites@2024-04-01#properties/properties/properties/hostNameSslStates" + }, + "description": "Optional. Hostname SSL states are used to manage the SSL bindings for app's hostnames." + }, + "nullable": true + }, + "hyperV": { + "type": "bool", + "defaultValue": false, + "metadata": { + "description": "Optional. Hyper-V sandbox." + } + }, + "redundancyMode": { + "type": "string", + "defaultValue": "None", + "allowedValues": [ + "ActiveActive", + "Failover", + "GeoRedundant", + "Manual", + "None" + ], + "metadata": { + "description": "Optional. Site redundancy mode." + } + }, + "publicNetworkAccess": { + "type": "string", + "nullable": true, + "allowedValues": [ + "Enabled", + "Disabled" + ], + "metadata": { + "description": "Optional. Whether or not public network access is allowed for this resource." + } + }, + "e2eEncryptionEnabled": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. End to End Encryption Setting." + } + }, + "dnsConfiguration": { + "type": "object", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.Web/sites@2024-04-01#properties/properties/properties/dnsConfiguration" + }, + "description": "Optional. Property to configure various DNS related settings for a site." + }, + "nullable": true }, - "vm_azureDiskEncryptionExtension": { - "condition": "[parameters('extensionAzureDiskEncryptionConfig').enabled]", - "type": "Microsoft.Resources/deployments", - "apiVersion": "2022-09-01", - "name": "[format('{0}-VM-AzureDiskEncryption', uniqueString(deployment().name, parameters('location')))]", + "autoGeneratedDomainNameLabelScope": { + "type": "string", + "nullable": true, + "allowedValues": [ + "NoReuse", + "ResourceGroupReuse", + "SubscriptionReuse", + "TenantReuse" + ], + "metadata": { + "description": "Optional. Specifies the scope of uniqueness for the default hostname during resource creation." + } + } + }, + "variables": { + "formattedUserAssignedIdentities": "[reduce(map(coalesce(tryGet(parameters('managedIdentities'), 'userAssignedResourceIds'), createArray()), lambda('id', createObject(format('{0}', lambdaVariables('id')), createObject()))), createObject(), lambda('cur', 'next', union(lambdaVariables('cur'), lambdaVariables('next'))))]", + "identity": "[if(not(empty(parameters('managedIdentities'))), createObject('type', if(coalesce(tryGet(parameters('managedIdentities'), 'systemAssigned'), false()), if(not(empty(coalesce(tryGet(parameters('managedIdentities'), 'userAssignedResourceIds'), createObject()))), 'SystemAssigned, UserAssigned', 'SystemAssigned'), if(not(empty(coalesce(tryGet(parameters('managedIdentities'), 'userAssignedResourceIds'), createObject()))), 'UserAssigned', 'None')), 'userAssignedIdentities', if(not(empty(variables('formattedUserAssignedIdentities'))), variables('formattedUserAssignedIdentities'), null())), null())]" + }, + "resources": { + "app": { + "type": "Microsoft.Web/sites", + "apiVersion": "2024-04-01", + "name": "[parameters('name')]", + "location": "[parameters('location')]", + "kind": "[parameters('kind')]", + "tags": "[parameters('tags')]", + "identity": "[variables('identity')]", "properties": { - "expressionEvaluationOptions": { - "scope": "inner" - }, - "mode": "Incremental", - "parameters": { - "virtualMachineName": { - "value": "[parameters('name')]" - }, - "name": { - "value": "[coalesce(tryGet(parameters('extensionAzureDiskEncryptionConfig'), 'name'), 'AzureDiskEncryption')]" - }, - "location": { - "value": "[parameters('location')]" - }, - "publisher": { - "value": "Microsoft.Azure.Security" - }, - "type": "[if(equals(parameters('osType'), 'Windows'), createObject('value', 'AzureDiskEncryption'), createObject('value', 'AzureDiskEncryptionForLinux'))]", - "typeHandlerVersion": { - "value": "[coalesce(tryGet(parameters('extensionAzureDiskEncryptionConfig'), 'typeHandlerVersion'), if(equals(parameters('osType'), 'Windows'), '2.2', '1.1'))]" - }, - "autoUpgradeMinorVersion": { - "value": "[coalesce(tryGet(parameters('extensionAzureDiskEncryptionConfig'), 'autoUpgradeMinorVersion'), true())]" - }, - "enableAutomaticUpgrade": { - "value": "[coalesce(tryGet(parameters('extensionAzureDiskEncryptionConfig'), 'enableAutomaticUpgrade'), false())]" - }, - "forceUpdateTag": { - "value": "[coalesce(tryGet(parameters('extensionAzureDiskEncryptionConfig'), 'forceUpdateTag'), '1.0')]" - }, - "settings": { - "value": "[coalesce(tryGet(parameters('extensionAzureDiskEncryptionConfig'), 'settings'), createObject())]" - }, - "supressFailures": { - "value": "[coalesce(tryGet(parameters('extensionAzureDiskEncryptionConfig'), 'supressFailures'), false())]" - }, - "tags": { - "value": "[coalesce(tryGet(parameters('extensionAzureDiskEncryptionConfig'), 'tags'), parameters('tags'))]" - } - }, - "template": { - "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", - "languageVersion": "2.0", - "contentVersion": "1.0.0.0", - "metadata": { - "_generator": { - "name": "bicep", - "version": "0.37.4.10188", - "templateHash": "13125609748815648088" - }, - "name": "Virtual Machine Extensions", - "description": "This module deploys a Virtual Machine Extension." - }, - "parameters": { - "virtualMachineName": { - "type": "string", - "metadata": { - "description": "Conditional. The name of the parent virtual machine that extension is provisioned for. Required if the template is used in a standalone deployment." - } - }, - "name": { - "type": "string", - "metadata": { - "description": "Required. The name of the virtual machine extension." - } - }, - "location": { - "type": "string", - "defaultValue": "[resourceGroup().location]", - "metadata": { - "description": "Optional. The location the extension is deployed to." - } - }, - "publisher": { - "type": "string", - "metadata": { - "description": "Required. The name of the extension handler publisher." - } - }, - "type": { - "type": "string", - "metadata": { - "description": "Required. Specifies the type of the extension; an example is \"CustomScriptExtension\"." - } - }, - "typeHandlerVersion": { - "type": "string", - "metadata": { - "description": "Required. Specifies the version of the script handler." - } - }, - "autoUpgradeMinorVersion": { - "type": "bool", - "metadata": { - "description": "Required. Indicates whether the extension should use a newer minor version if one is available at deployment time. Once deployed, however, the extension will not upgrade minor versions unless redeployed, even with this property set to true." - } - }, - "forceUpdateTag": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. How the extension handler should be forced to update even if the extension configuration has not changed." - } - }, - "settings": { - "type": "object", - "nullable": true, - "metadata": { - "description": "Optional. Any object that contains the extension specific settings." - } - }, - "protectedSettings": { - "type": "secureObject", - "nullable": true, - "metadata": { - "description": "Optional. Any object that contains the extension specific protected settings." - } - }, - "supressFailures": { - "type": "bool", - "defaultValue": false, - "metadata": { - "description": "Optional. Indicates whether failures stemming from the extension will be suppressed (Operational failures such as not connecting to the VM will not be suppressed regardless of this value). The default is false." - } - }, - "enableAutomaticUpgrade": { - "type": "bool", - "metadata": { - "description": "Required. Indicates whether the extension should be automatically upgraded by the platform if there is a newer version of the extension available." - } - }, - "tags": { - "type": "object", - "metadata": { - "__bicep_resource_derived_type!": { - "source": "Microsoft.Compute/virtualMachines/extensions@2024-11-01#properties/tags" - }, - "description": "Optional. Tags of the resource." - }, - "nullable": true - }, - "protectedSettingsFromKeyVault": { - "type": "object", - "metadata": { - "__bicep_resource_derived_type!": { - "source": "Microsoft.Compute/virtualMachines/extensions@2024-11-01#properties/properties/properties/protectedSettingsFromKeyVault" - }, - "description": "Optional. The extensions protected settings that are passed by reference, and consumed from key vault." - }, - "nullable": true - }, - "provisionAfterExtensions": { - "type": "array", - "metadata": { - "__bicep_resource_derived_type!": { - "source": "Microsoft.Compute/virtualMachines/extensions@2024-11-01#properties/properties/properties/provisionAfterExtensions" - }, - "description": "Optional. Collection of extension names after which this extension needs to be provisioned." - }, - "nullable": true - } - }, - "resources": { - "virtualMachine": { - "existing": true, - "type": "Microsoft.Compute/virtualMachines", - "apiVersion": "2024-11-01", - "name": "[parameters('virtualMachineName')]" - }, - "extension": { - "type": "Microsoft.Compute/virtualMachines/extensions", - "apiVersion": "2024-11-01", - "name": "[format('{0}/{1}', parameters('virtualMachineName'), parameters('name'))]", - "location": "[parameters('location')]", - "tags": "[parameters('tags')]", - "properties": { - "publisher": "[parameters('publisher')]", - "type": "[parameters('type')]", - "typeHandlerVersion": "[parameters('typeHandlerVersion')]", - "autoUpgradeMinorVersion": "[parameters('autoUpgradeMinorVersion')]", - "enableAutomaticUpgrade": "[parameters('enableAutomaticUpgrade')]", - "forceUpdateTag": "[parameters('forceUpdateTag')]", - "settings": "[parameters('settings')]", - "protectedSettings": "[parameters('protectedSettings')]", - "suppressFailures": "[parameters('supressFailures')]", - "protectedSettingsFromKeyVault": "[parameters('protectedSettingsFromKeyVault')]", - "provisionAfterExtensions": "[parameters('provisionAfterExtensions')]" - } + "managedEnvironmentId": "[if(not(empty(parameters('managedEnvironmentId'))), parameters('managedEnvironmentId'), null())]", + "serverFarmId": "[parameters('serverFarmResourceId')]", + "clientAffinityEnabled": "[parameters('clientAffinityEnabled')]", + "httpsOnly": "[parameters('httpsOnly')]", + "hostingEnvironmentProfile": "[if(not(empty(parameters('appServiceEnvironmentResourceId'))), createObject('id', parameters('appServiceEnvironmentResourceId')), null())]", + "storageAccountRequired": "[parameters('storageAccountRequired')]", + "keyVaultReferenceIdentity": "[parameters('keyVaultAccessIdentityResourceId')]", + "virtualNetworkSubnetId": "[parameters('virtualNetworkSubnetId')]", + "siteConfig": "[parameters('siteConfig')]", + "functionAppConfig": "[parameters('functionAppConfig')]", + "clientCertEnabled": "[parameters('clientCertEnabled')]", + "clientCertExclusionPaths": "[parameters('clientCertExclusionPaths')]", + "clientCertMode": "[parameters('clientCertMode')]", + "cloningInfo": "[parameters('cloningInfo')]", + "containerSize": "[parameters('containerSize')]", + "dailyMemoryTimeQuota": "[parameters('dailyMemoryTimeQuota')]", + "enabled": "[parameters('enabled')]", + "hostNameSslStates": "[parameters('hostNameSslStates')]", + "hyperV": "[parameters('hyperV')]", + "redundancyMode": "[parameters('redundancyMode')]", + "publicNetworkAccess": "[if(not(empty(parameters('publicNetworkAccess'))), parameters('publicNetworkAccess'), if(not(empty(parameters('privateEndpoints'))), 'Disabled', 'Enabled'))]", + "vnetContentShareEnabled": "[parameters('vnetContentShareEnabled')]", + "vnetImagePullEnabled": "[parameters('vnetImagePullEnabled')]", + "vnetRouteAllEnabled": "[parameters('vnetRouteAllEnabled')]", + "scmSiteAlsoStopped": "[parameters('scmSiteAlsoStopped')]", + "endToEndEncryptionEnabled": "[parameters('e2eEncryptionEnabled')]", + "dnsConfiguration": "[parameters('dnsConfiguration')]", + "autoGeneratedDomainNameLabelScope": "[parameters('autoGeneratedDomainNameLabelScope')]" + } + }, + "app_diagnosticSettings": { + "copy": { + "name": "app_diagnosticSettings", + "count": "[length(coalesce(parameters('diagnosticSettings'), createArray()))]" + }, + "type": "Microsoft.Insights/diagnosticSettings", + "apiVersion": "2021-05-01-preview", + "scope": "[format('Microsoft.Web/sites/{0}', parameters('name'))]", + "name": "[coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'name'), format('{0}-diagnosticSettings', parameters('name')))]", + "properties": { + "copy": [ + { + "name": "metrics", + "count": "[length(coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'metricCategories'), createArray(createObject('category', 'AllMetrics'))))]", + "input": { + "category": "[coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'metricCategories'), createArray(createObject('category', 'AllMetrics')))[copyIndex('metrics')].category]", + "enabled": "[coalesce(tryGet(coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'metricCategories'), createArray(createObject('category', 'AllMetrics')))[copyIndex('metrics')], 'enabled'), true())]", + "timeGrain": null } }, - "outputs": { - "name": { - "type": "string", - "metadata": { - "description": "The name of the extension." - }, - "value": "[parameters('name')]" - }, - "resourceId": { - "type": "string", - "metadata": { - "description": "The resource ID of the extension." - }, - "value": "[resourceId('Microsoft.Compute/virtualMachines/extensions', parameters('virtualMachineName'), parameters('name'))]" - }, - "resourceGroupName": { - "type": "string", - "metadata": { - "description": "The name of the Resource Group the extension was created in." - }, - "value": "[resourceGroup().name]" - }, - "location": { - "type": "string", - "metadata": { - "description": "The location the resource was deployed into." - }, - "value": "[reference('extension', '2024-11-01', 'full').location]" + { + "name": "logs", + "count": "[length(coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'logCategoriesAndGroups'), createArray(createObject('categoryGroup', 'allLogs'))))]", + "input": { + "categoryGroup": "[tryGet(coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'logCategoriesAndGroups'), createArray(createObject('categoryGroup', 'allLogs')))[copyIndex('logs')], 'categoryGroup')]", + "category": "[tryGet(coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'logCategoriesAndGroups'), createArray(createObject('categoryGroup', 'allLogs')))[copyIndex('logs')], 'category')]", + "enabled": "[coalesce(tryGet(coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'logCategoriesAndGroups'), createArray(createObject('categoryGroup', 'allLogs')))[copyIndex('logs')], 'enabled'), true())]" } } - } + ], + "storageAccountId": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'storageAccountResourceId')]", + "workspaceId": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'workspaceResourceId')]", + "eventHubAuthorizationRuleId": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'eventHubAuthorizationRuleResourceId')]", + "eventHubName": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'eventHubName')]", + "marketplacePartnerId": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'marketplacePartnerResourceId')]", + "logAnalyticsDestinationType": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'logAnalyticsDestinationType')]" }, "dependsOn": [ - "vm", - "vm_customScriptExtension" + "app" ] }, - "vm_nvidiaGpuDriverWindowsExtension": { - "condition": "[parameters('extensionNvidiaGpuDriverWindows').enabled]", + "app_config": { + "copy": { + "name": "app_config", + "count": "[length(coalesce(parameters('configs'), createArray()))]" + }, "type": "Microsoft.Resources/deployments", - "apiVersion": "2022-09-01", - "name": "[format('{0}-VM-NvidiaGpuDriverWindows', uniqueString(deployment().name, parameters('location')))]", + "apiVersion": "2025-04-01", + "name": "[format('{0}-Site-Config-{1}', uniqueString(deployment().name, parameters('location')), copyIndex())]", "properties": { "expressionEvaluationOptions": { "scope": "inner" }, "mode": "Incremental", "parameters": { - "virtualMachineName": { + "appName": { "value": "[parameters('name')]" }, "name": { - "value": "[coalesce(tryGet(parameters('extensionNvidiaGpuDriverWindows'), 'name'), 'NvidiaGpuDriverWindows')]" - }, - "location": { - "value": "[parameters('location')]" - }, - "publisher": { - "value": "Microsoft.HpcCompute" - }, - "type": { - "value": "NvidiaGpuDriverWindows" + "value": "[coalesce(parameters('configs'), createArray())[copyIndex()].name]" }, - "typeHandlerVersion": { - "value": "[coalesce(tryGet(parameters('extensionNvidiaGpuDriverWindows'), 'typeHandlerVersion'), '1.4')]" + "applicationInsightResourceId": { + "value": "[tryGet(coalesce(parameters('configs'), createArray())[copyIndex()], 'applicationInsightResourceId')]" }, - "autoUpgradeMinorVersion": { - "value": "[coalesce(tryGet(parameters('extensionNvidiaGpuDriverWindows'), 'autoUpgradeMinorVersion'), true())]" + "storageAccountResourceId": { + "value": "[tryGet(coalesce(parameters('configs'), createArray())[copyIndex()], 'storageAccountResourceId')]" }, - "enableAutomaticUpgrade": { - "value": "[coalesce(tryGet(parameters('extensionNvidiaGpuDriverWindows'), 'enableAutomaticUpgrade'), false())]" + "storageAccountUseIdentityAuthentication": { + "value": "[tryGet(coalesce(parameters('configs'), createArray())[copyIndex()], 'storageAccountUseIdentityAuthentication')]" }, - "supressFailures": { - "value": "[coalesce(tryGet(parameters('extensionNvidiaGpuDriverWindows'), 'supressFailures'), false())]" + "properties": { + "value": "[tryGet(coalesce(parameters('configs'), createArray())[copyIndex()], 'properties')]" }, - "tags": { - "value": "[coalesce(tryGet(parameters('extensionNvidiaGpuDriverWindows'), 'tags'), parameters('tags'))]" + "currentAppSettings": "[if(coalesce(tryGet(coalesce(parameters('configs'), createArray())[copyIndex()], 'retainCurrentAppSettings'), and(true(), equals(coalesce(parameters('configs'), createArray())[copyIndex()].name, 'appsettings'))), createObject('value', list(format('{0}/config/appsettings', resourceId('Microsoft.Web/sites', parameters('name'))), '2023-12-01').properties), createObject('value', createObject()))]", + "enableMonitoring": { + "value": "[parameters('enableMonitoring')]" } }, "template": { @@ -41593,235 +43153,209 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.37.4.10188", - "templateHash": "13125609748815648088" + "version": "0.39.26.7824", + "templateHash": "13592577410661714505" }, - "name": "Virtual Machine Extensions", - "description": "This module deploys a Virtual Machine Extension." + "name": "Site App Settings", + "description": "This module deploys a Site App Setting." }, "parameters": { - "virtualMachineName": { + "appName": { "type": "string", "metadata": { - "description": "Conditional. The name of the parent virtual machine that extension is provisioned for. Required if the template is used in a standalone deployment." + "description": "Conditional. The name of the parent site resource. Required if the template is used in a standalone deployment." } }, "name": { "type": "string", + "allowedValues": [ + "appsettings", + "authsettings", + "authsettingsV2", + "azurestorageaccounts", + "backup", + "connectionstrings", + "logs", + "metadata", + "pushsettings", + "slotConfigNames", + "web" + ], "metadata": { - "description": "Required. The name of the virtual machine extension." - } - }, - "location": { - "type": "string", - "defaultValue": "[resourceGroup().location]", - "metadata": { - "description": "Optional. The location the extension is deployed to." - } - }, - "publisher": { - "type": "string", - "metadata": { - "description": "Required. The name of the extension handler publisher." - } - }, - "type": { - "type": "string", - "metadata": { - "description": "Required. Specifies the type of the extension; an example is \"CustomScriptExtension\"." + "description": "Required. The name of the config." } }, - "typeHandlerVersion": { - "type": "string", + "properties": { + "type": "object", + "defaultValue": {}, "metadata": { - "description": "Required. Specifies the version of the script handler." + "description": "Optional. The properties of the config." } }, - "autoUpgradeMinorVersion": { + "storageAccountUseIdentityAuthentication": { "type": "bool", + "defaultValue": false, "metadata": { - "description": "Required. Indicates whether the extension should use a newer minor version if one is available at deployment time. Once deployed, however, the extension will not upgrade minor versions unless redeployed, even with this property set to true." + "description": "Optional. If the provided storage account requires Identity based authentication." } }, - "forceUpdateTag": { + "storageAccountResourceId": { "type": "string", "nullable": true, "metadata": { - "description": "Optional. How the extension handler should be forced to update even if the extension configuration has not changed." - } - }, - "settings": { - "type": "object", - "nullable": true, - "metadata": { - "description": "Optional. Any object that contains the extension specific settings." + "description": "Optional. Required if app of kind functionapp. Resource ID of the storage account." } }, - "protectedSettings": { - "type": "secureObject", + "applicationInsightResourceId": { + "type": "string", "nullable": true, "metadata": { - "description": "Optional. Any object that contains the extension specific protected settings." - } - }, - "supressFailures": { - "type": "bool", - "defaultValue": false, - "metadata": { - "description": "Optional. Indicates whether failures stemming from the extension will be suppressed (Operational failures such as not connecting to the VM will not be suppressed regardless of this value). The default is false." - } - }, - "enableAutomaticUpgrade": { - "type": "bool", - "metadata": { - "description": "Required. Indicates whether the extension should be automatically upgraded by the platform if there is a newer version of the extension available." + "description": "Optional. Resource ID of the application insight to leverage for this resource." } }, - "tags": { + "currentAppSettings": { "type": "object", - "metadata": { - "__bicep_resource_derived_type!": { - "source": "Microsoft.Compute/virtualMachines/extensions@2024-11-01#properties/tags" - }, - "description": "Optional. Tags of the resource." + "properties": {}, + "additionalProperties": { + "type": "string", + "metadata": { + "description": "Required. The key-values pairs of the current app settings." + } }, - "nullable": true - }, - "protectedSettingsFromKeyVault": { - "type": "object", + "defaultValue": {}, "metadata": { - "__bicep_resource_derived_type!": { - "source": "Microsoft.Compute/virtualMachines/extensions@2024-11-01#properties/properties/properties/protectedSettingsFromKeyVault" - }, - "description": "Optional. The extensions protected settings that are passed by reference, and consumed from key vault." - }, - "nullable": true + "description": "Optional. The current app settings." + } }, - "provisionAfterExtensions": { - "type": "array", - "metadata": { - "__bicep_resource_derived_type!": { - "source": "Microsoft.Compute/virtualMachines/extensions@2024-11-01#properties/properties/properties/provisionAfterExtensions" - }, - "description": "Optional. Collection of extension names after which this extension needs to be provisioned." - }, - "nullable": true + "enableMonitoring": { + "type": "bool", + "defaultValue": false, + "metadata": { + "description": "Optional. Enable monitoring and logging configuration." + } } }, + "variables": { + "loggingProperties": "[if(and(parameters('enableMonitoring'), equals(parameters('name'), 'logs')), createObject('applicationLogs', createObject('fileSystem', createObject('level', 'Verbose')), 'httpLogs', createObject('fileSystem', createObject('enabled', true(), 'retentionInDays', 3, 'retentionInMb', 100)), 'detailedErrorMessages', createObject('enabled', true()), 'failedRequestsTracing', createObject('enabled', true())), createObject())]" + }, "resources": { - "virtualMachine": { + "applicationInsights": { + "condition": "[not(empty(parameters('applicationInsightResourceId')))]", "existing": true, - "type": "Microsoft.Compute/virtualMachines", - "apiVersion": "2024-11-01", - "name": "[parameters('virtualMachineName')]" + "type": "Microsoft.Insights/components", + "apiVersion": "2020-02-02", + "subscriptionId": "[split(parameters('applicationInsightResourceId'), '/')[2]]", + "resourceGroup": "[split(parameters('applicationInsightResourceId'), '/')[4]]", + "name": "[last(split(parameters('applicationInsightResourceId'), '/'))]" }, - "extension": { - "type": "Microsoft.Compute/virtualMachines/extensions", - "apiVersion": "2024-11-01", - "name": "[format('{0}/{1}', parameters('virtualMachineName'), parameters('name'))]", - "location": "[parameters('location')]", - "tags": "[parameters('tags')]", - "properties": { - "publisher": "[parameters('publisher')]", - "type": "[parameters('type')]", - "typeHandlerVersion": "[parameters('typeHandlerVersion')]", - "autoUpgradeMinorVersion": "[parameters('autoUpgradeMinorVersion')]", - "enableAutomaticUpgrade": "[parameters('enableAutomaticUpgrade')]", - "forceUpdateTag": "[parameters('forceUpdateTag')]", - "settings": "[parameters('settings')]", - "protectedSettings": "[parameters('protectedSettings')]", - "suppressFailures": "[parameters('supressFailures')]", - "protectedSettingsFromKeyVault": "[parameters('protectedSettingsFromKeyVault')]", - "provisionAfterExtensions": "[parameters('provisionAfterExtensions')]" - } + "storageAccount": { + "condition": "[not(empty(parameters('storageAccountResourceId')))]", + "existing": true, + "type": "Microsoft.Storage/storageAccounts", + "apiVersion": "2024-01-01", + "subscriptionId": "[split(parameters('storageAccountResourceId'), '/')[2]]", + "resourceGroup": "[split(parameters('storageAccountResourceId'), '/')[4]]", + "name": "[last(split(parameters('storageAccountResourceId'), '/'))]" + }, + "app": { + "existing": true, + "type": "Microsoft.Web/sites", + "apiVersion": "2023-12-01", + "name": "[parameters('appName')]" + }, + "config": { + "type": "Microsoft.Web/sites/config", + "apiVersion": "2024-04-01", + "name": "[format('{0}/{1}', parameters('appName'), parameters('name'))]", + "properties": "[union(parameters('properties'), parameters('currentAppSettings'), if(and(not(empty(parameters('storageAccountResourceId'))), not(parameters('storageAccountUseIdentityAuthentication'))), createObject('AzureWebJobsStorage', format('DefaultEndpointsProtocol=https;AccountName={0};AccountKey={1};EndpointSuffix={2}', last(split(parameters('storageAccountResourceId'), '/')), listKeys('storageAccount', '2024-01-01').keys[0].value, environment().suffixes.storage)), if(and(not(empty(parameters('storageAccountResourceId'))), parameters('storageAccountUseIdentityAuthentication')), createObject('AzureWebJobsStorage__accountName', last(split(parameters('storageAccountResourceId'), '/')), 'AzureWebJobsStorage__blobServiceUri', reference('storageAccount').primaryEndpoints.blob, 'AzureWebJobsStorage__queueServiceUri', reference('storageAccount').primaryEndpoints.queue, 'AzureWebJobsStorage__tableServiceUri', reference('storageAccount').primaryEndpoints.table), createObject())), if(not(empty(parameters('applicationInsightResourceId'))), createObject('APPLICATIONINSIGHTS_CONNECTION_STRING', reference('applicationInsights').ConnectionString), createObject()), variables('loggingProperties'))]", + "dependsOn": [ + "applicationInsights", + "storageAccount" + ] } }, "outputs": { "name": { "type": "string", "metadata": { - "description": "The name of the extension." + "description": "The name of the site config." }, "value": "[parameters('name')]" }, "resourceId": { "type": "string", "metadata": { - "description": "The resource ID of the extension." + "description": "The resource ID of the site config." }, - "value": "[resourceId('Microsoft.Compute/virtualMachines/extensions', parameters('virtualMachineName'), parameters('name'))]" + "value": "[resourceId('Microsoft.Web/sites/config', parameters('appName'), parameters('name'))]" }, "resourceGroupName": { "type": "string", "metadata": { - "description": "The name of the Resource Group the extension was created in." + "description": "The resource group the site config was deployed into." }, "value": "[resourceGroup().name]" - }, - "location": { - "type": "string", - "metadata": { - "description": "The location the resource was deployed into." - }, - "value": "[reference('extension', '2024-11-01', 'full').location]" } } } }, "dependsOn": [ - "vm", - "vm_azureDiskEncryptionExtension" + "app" ] }, - "vm_hostPoolRegistrationExtension": { - "condition": "[parameters('extensionHostPoolRegistration').enabled]", + "app_privateEndpoints": { + "copy": { + "name": "app_privateEndpoints", + "count": "[length(coalesce(parameters('privateEndpoints'), createArray()))]" + }, "type": "Microsoft.Resources/deployments", - "apiVersion": "2022-09-01", - "name": "[format('{0}-VM-HostPoolRegistration', uniqueString(deployment().name, parameters('location')))]", + "apiVersion": "2025-04-01", + "name": "[format('{0}-app-PrivateEndpoint-{1}', uniqueString(deployment().name, parameters('location')), copyIndex())]", + "subscriptionId": "[split(coalesce(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'resourceGroupResourceId'), resourceGroup().id), '/')[2]]", + "resourceGroup": "[split(coalesce(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'resourceGroupResourceId'), resourceGroup().id), '/')[4]]", "properties": { "expressionEvaluationOptions": { "scope": "inner" }, "mode": "Incremental", "parameters": { - "virtualMachineName": { - "value": "[parameters('name')]" - }, "name": { - "value": "[coalesce(tryGet(parameters('extensionHostPoolRegistration'), 'name'), 'HostPoolRegistration')]" + "value": "[coalesce(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'name'), format('pep-{0}-{1}-{2}', last(split(resourceId('Microsoft.Web/sites', parameters('name')), '/')), coalesce(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'service'), 'sites'), copyIndex()))]" + }, + "privateLinkServiceConnections": "[if(not(equals(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'isManualConnection'), true())), createObject('value', createArray(createObject('name', coalesce(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'privateLinkServiceConnectionName'), format('{0}-{1}-{2}', last(split(resourceId('Microsoft.Web/sites', parameters('name')), '/')), coalesce(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'service'), 'sites'), copyIndex())), 'properties', createObject('privateLinkServiceId', resourceId('Microsoft.Web/sites', parameters('name')), 'groupIds', createArray(coalesce(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'service'), 'sites')))))), createObject('value', null()))]", + "manualPrivateLinkServiceConnections": "[if(equals(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'isManualConnection'), true()), createObject('value', createArray(createObject('name', coalesce(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'privateLinkServiceConnectionName'), format('{0}-{1}-{2}', last(split(resourceId('Microsoft.Web/sites', parameters('name')), '/')), coalesce(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'service'), 'sites'), copyIndex())), 'properties', createObject('privateLinkServiceId', resourceId('Microsoft.Web/sites', parameters('name')), 'groupIds', createArray(coalesce(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'service'), 'sites')), 'requestMessage', coalesce(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'manualConnectionRequestMessage'), 'Manual approval required.'))))), createObject('value', null()))]", + "subnetResourceId": { + "value": "[coalesce(parameters('privateEndpoints'), createArray())[copyIndex()].subnetResourceId]" + }, + "enableTelemetry": { + "value": false }, "location": { - "value": "[parameters('location')]" + "value": "[coalesce(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'location'), reference(split(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()].subnetResourceId, '/subnets/')[0], '2020-06-01', 'Full').location)]" }, - "publisher": { - "value": "Microsoft.PowerShell" + "lock": { + "value": "[coalesce(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'lock'), null())]" }, - "type": { - "value": "DSC" + "privateDnsZoneGroup": { + "value": "[tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'privateDnsZoneGroup')]" }, - "typeHandlerVersion": { - "value": "[coalesce(tryGet(parameters('extensionHostPoolRegistration'), 'typeHandlerVersion'), '2.77')]" + "roleAssignments": { + "value": "[tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'roleAssignments')]" }, - "autoUpgradeMinorVersion": { - "value": "[coalesce(tryGet(parameters('extensionHostPoolRegistration'), 'autoUpgradeMinorVersion'), true())]" + "tags": { + "value": "[coalesce(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'tags'), parameters('tags'))]" }, - "enableAutomaticUpgrade": { - "value": "[coalesce(tryGet(parameters('extensionHostPoolRegistration'), 'enableAutomaticUpgrade'), false())]" + "customDnsConfigs": { + "value": "[tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'customDnsConfigs')]" }, - "settings": { - "value": { - "modulesUrl": "[parameters('extensionHostPoolRegistration').modulesUrl]", - "configurationFunction": "[parameters('extensionHostPoolRegistration').configurationFunction]", - "properties": { - "hostPoolName": "[parameters('extensionHostPoolRegistration').hostPoolName]", - "registrationInfoToken": "[parameters('extensionHostPoolRegistration').registrationInfoToken]", - "aadJoin": true - }, - "supressFailures": "[coalesce(tryGet(parameters('extensionHostPoolRegistration'), 'supressFailures'), false())]" - } + "ipConfigurations": { + "value": "[tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'ipConfigurations')]" }, - "tags": { - "value": "[coalesce(tryGet(parameters('extensionHostPoolRegistration'), 'tags'), parameters('tags'))]" + "applicationSecurityGroupResourceIds": { + "value": "[tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'applicationSecurityGroupResourceIds')]" + }, + "customNetworkInterfaceName": { + "value": "[tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'customNetworkInterfaceName')]" } }, "template": { @@ -41831,564 +43365,699 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.37.4.10188", - "templateHash": "13125609748815648088" + "version": "0.34.44.8038", + "templateHash": "12389807800450456797" }, - "name": "Virtual Machine Extensions", - "description": "This module deploys a Virtual Machine Extension." + "name": "Private Endpoints", + "description": "This module deploys a Private Endpoint." }, - "parameters": { - "virtualMachineName": { - "type": "string", - "metadata": { - "description": "Conditional. The name of the parent virtual machine that extension is provisioned for. Required if the template is used in a standalone deployment." - } - }, - "name": { - "type": "string", - "metadata": { - "description": "Required. The name of the virtual machine extension." - } - }, - "location": { - "type": "string", - "defaultValue": "[resourceGroup().location]", - "metadata": { - "description": "Optional. The location the extension is deployed to." - } - }, - "publisher": { - "type": "string", - "metadata": { - "description": "Required. The name of the extension handler publisher." - } - }, - "type": { - "type": "string", - "metadata": { - "description": "Required. Specifies the type of the extension; an example is \"CustomScriptExtension\"." - } - }, - "typeHandlerVersion": { - "type": "string", - "metadata": { - "description": "Required. Specifies the version of the script handler." - } - }, - "autoUpgradeMinorVersion": { - "type": "bool", - "metadata": { - "description": "Required. Indicates whether the extension should use a newer minor version if one is available at deployment time. Once deployed, however, the extension will not upgrade minor versions unless redeployed, even with this property set to true." - } - }, - "forceUpdateTag": { - "type": "string", - "nullable": true, - "metadata": { - "description": "Optional. How the extension handler should be forced to update even if the extension configuration has not changed." - } - }, - "settings": { + "definitions": { + "privateDnsZoneGroupType": { "type": "object", - "nullable": true, - "metadata": { - "description": "Optional. Any object that contains the extension specific settings." - } - }, - "protectedSettings": { - "type": "secureObject", - "nullable": true, + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The name of the Private DNS Zone Group." + } + }, + "privateDnsZoneGroupConfigs": { + "type": "array", + "items": { + "$ref": "#/definitions/privateDnsZoneGroupConfigType" + }, + "metadata": { + "description": "Required. The private DNS zone groups to associate the private endpoint. A DNS zone group can support up to 5 DNS zones." + } + } + }, "metadata": { - "description": "Optional. Any object that contains the extension specific protected settings." + "__bicep_export!": true } }, - "supressFailures": { - "type": "bool", - "defaultValue": false, + "ipConfigurationType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "metadata": { + "description": "Required. The name of the resource that is unique within a resource group." + } + }, + "properties": { + "type": "object", + "properties": { + "groupId": { + "type": "string", + "metadata": { + "description": "Required. The ID of a group obtained from the remote resource that this private endpoint should connect to. If used with private link service connection, this property must be defined as empty string." + } + }, + "memberName": { + "type": "string", + "metadata": { + "description": "Required. The member name of a group obtained from the remote resource that this private endpoint should connect to. If used with private link service connection, this property must be defined as empty string." + } + }, + "privateIPAddress": { + "type": "string", + "metadata": { + "description": "Required. A private IP address obtained from the private endpoint's subnet." + } + } + }, + "metadata": { + "description": "Required. Properties of private endpoint IP configurations." + } + } + }, "metadata": { - "description": "Optional. Indicates whether failures stemming from the extension will be suppressed (Operational failures such as not connecting to the VM will not be suppressed regardless of this value). The default is false." + "__bicep_export!": true } }, - "enableAutomaticUpgrade": { - "type": "bool", + "privateLinkServiceConnectionType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "metadata": { + "description": "Required. The name of the private link service connection." + } + }, + "properties": { + "type": "object", + "properties": { + "groupIds": { + "type": "array", + "items": { + "type": "string" + }, + "metadata": { + "description": "Required. The ID of a group obtained from the remote resource that this private endpoint should connect to. If used with private link service connection, this property must be defined as empty string array `[]`." + } + }, + "privateLinkServiceId": { + "type": "string", + "metadata": { + "description": "Required. The resource id of private link service." + } + }, + "requestMessage": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. A message passed to the owner of the remote resource with this connection request. Restricted to 140 chars." + } + } + }, + "metadata": { + "description": "Required. Properties of private link service connection." + } + } + }, "metadata": { - "description": "Required. Indicates whether the extension should be automatically upgraded by the platform if there is a newer version of the extension available." + "__bicep_export!": true } }, - "tags": { + "customDnsConfigType": { "type": "object", - "metadata": { - "__bicep_resource_derived_type!": { - "source": "Microsoft.Compute/virtualMachines/extensions@2024-11-01#properties/tags" + "properties": { + "fqdn": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. FQDN that resolves to private endpoint IP address." + } }, - "description": "Optional. Tags of the resource." + "ipAddresses": { + "type": "array", + "items": { + "type": "string" + }, + "metadata": { + "description": "Required. A list of private IP addresses of the private endpoint." + } + } }, - "nullable": true + "metadata": { + "__bicep_export!": true + } }, - "protectedSettingsFromKeyVault": { + "lockType": { "type": "object", - "metadata": { - "__bicep_resource_derived_type!": { - "source": "Microsoft.Compute/virtualMachines/extensions@2024-11-01#properties/properties/properties/protectedSettingsFromKeyVault" + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Specify the name of lock." + } }, - "description": "Optional. The extensions protected settings that are passed by reference, and consumed from key vault." + "kind": { + "type": "string", + "allowedValues": [ + "CanNotDelete", + "None", + "ReadOnly" + ], + "nullable": true, + "metadata": { + "description": "Optional. Specify the type of lock." + } + } }, - "nullable": true - }, - "provisionAfterExtensions": { - "type": "array", "metadata": { - "__bicep_resource_derived_type!": { - "source": "Microsoft.Compute/virtualMachines/extensions@2024-11-01#properties/properties/properties/provisionAfterExtensions" - }, - "description": "Optional. Collection of extension names after which this extension needs to be provisioned." - }, - "nullable": true - } - }, - "resources": { - "virtualMachine": { - "existing": true, - "type": "Microsoft.Compute/virtualMachines", - "apiVersion": "2024-11-01", - "name": "[parameters('virtualMachineName')]" + "description": "An AVM-aligned type for a lock.", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.5.1" + } + } }, - "extension": { - "type": "Microsoft.Compute/virtualMachines/extensions", - "apiVersion": "2024-11-01", - "name": "[format('{0}/{1}', parameters('virtualMachineName'), parameters('name'))]", - "location": "[parameters('location')]", - "tags": "[parameters('tags')]", + "privateDnsZoneGroupConfigType": { + "type": "object", "properties": { - "publisher": "[parameters('publisher')]", - "type": "[parameters('type')]", - "typeHandlerVersion": "[parameters('typeHandlerVersion')]", - "autoUpgradeMinorVersion": "[parameters('autoUpgradeMinorVersion')]", - "enableAutomaticUpgrade": "[parameters('enableAutomaticUpgrade')]", - "forceUpdateTag": "[parameters('forceUpdateTag')]", - "settings": "[parameters('settings')]", - "protectedSettings": "[parameters('protectedSettings')]", - "suppressFailures": "[parameters('supressFailures')]", - "protectedSettingsFromKeyVault": "[parameters('protectedSettingsFromKeyVault')]", - "provisionAfterExtensions": "[parameters('provisionAfterExtensions')]" - } - } - }, - "outputs": { - "name": { - "type": "string", - "metadata": { - "description": "The name of the extension." + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The name of the private DNS zone group config." + } + }, + "privateDnsZoneResourceId": { + "type": "string", + "metadata": { + "description": "Required. The resource id of the private DNS zone." + } + } }, - "value": "[parameters('name')]" - }, - "resourceId": { - "type": "string", "metadata": { - "description": "The resource ID of the extension." - }, - "value": "[resourceId('Microsoft.Compute/virtualMachines/extensions', parameters('virtualMachineName'), parameters('name'))]" + "__bicep_imported_from!": { + "sourceTemplate": "private-dns-zone-group/main.bicep" + } + } }, - "resourceGroupName": { - "type": "string", - "metadata": { - "description": "The name of the Resource Group the extension was created in." + "roleAssignmentType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The name (as GUID) of the role assignment. If not provided, a GUID will be generated." + } + }, + "roleDefinitionIdOrName": { + "type": "string", + "metadata": { + "description": "Required. The role to assign. You can provide either the display name of the role definition, the role definition GUID, or its fully qualified ID in the following format: '/providers/Microsoft.Authorization/roleDefinitions/c2f4ef07-c644-48eb-af81-4b1b4947fb11'." + } + }, + "principalId": { + "type": "string", + "metadata": { + "description": "Required. The principal ID of the principal (user/group/identity) to assign the role to." + } + }, + "principalType": { + "type": "string", + "allowedValues": [ + "Device", + "ForeignGroup", + "Group", + "ServicePrincipal", + "User" + ], + "nullable": true, + "metadata": { + "description": "Optional. The principal type of the assigned principal ID." + } + }, + "description": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The description of the role assignment." + } + }, + "condition": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The conditions on the role assignment. This limits the resources it can be assigned to. e.g.: @Resource[Microsoft.Storage/storageAccounts/blobServices/containers:ContainerName] StringEqualsIgnoreCase \"foo_storage_container\"." + } + }, + "conditionVersion": { + "type": "string", + "allowedValues": [ + "2.0" + ], + "nullable": true, + "metadata": { + "description": "Optional. Version of the condition." + } + }, + "delegatedManagedIdentityResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The Resource Id of the delegated managed identity resource." + } + } }, - "value": "[resourceGroup().name]" - }, - "location": { - "type": "string", "metadata": { - "description": "The location the resource was deployed into." - }, - "value": "[reference('extension', '2024-11-01', 'full').location]" + "description": "An AVM-aligned type for a role assignment.", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.5.1" + } + } } - } - } - }, - "dependsOn": [ - "vm", - "vm_nvidiaGpuDriverWindowsExtension" - ] - }, - "vm_azureGuestConfigurationExtension": { - "condition": "[parameters('extensionGuestConfigurationExtension').enabled]", - "type": "Microsoft.Resources/deployments", - "apiVersion": "2022-09-01", - "name": "[format('{0}-VM-GuestConfiguration', uniqueString(deployment().name, parameters('location')))]", - "properties": { - "expressionEvaluationOptions": { - "scope": "inner" - }, - "mode": "Incremental", - "parameters": { - "virtualMachineName": { - "value": "[parameters('name')]" - }, - "name": "[if(coalesce(tryGet(parameters('extensionGuestConfigurationExtension'), 'name'), equals(parameters('osType'), 'Windows')), createObject('value', 'AzurePolicyforWindows'), createObject('value', 'AzurePolicyforLinux'))]", - "location": { - "value": "[parameters('location')]" - }, - "publisher": { - "value": "Microsoft.GuestConfiguration" - }, - "type": "[if(equals(parameters('osType'), 'Windows'), createObject('value', 'ConfigurationforWindows'), createObject('value', 'ConfigurationForLinux'))]", - "typeHandlerVersion": { - "value": "[coalesce(tryGet(parameters('extensionGuestConfigurationExtension'), 'typeHandlerVersion'), if(equals(parameters('osType'), 'Windows'), '1.0', '1.0'))]" - }, - "autoUpgradeMinorVersion": { - "value": "[coalesce(tryGet(parameters('extensionGuestConfigurationExtension'), 'autoUpgradeMinorVersion'), true())]" - }, - "enableAutomaticUpgrade": { - "value": "[coalesce(tryGet(parameters('extensionGuestConfigurationExtension'), 'enableAutomaticUpgrade'), true())]" - }, - "forceUpdateTag": { - "value": "[coalesce(tryGet(parameters('extensionGuestConfigurationExtension'), 'forceUpdateTag'), '1.0')]" - }, - "settings": { - "value": "[coalesce(tryGet(parameters('extensionGuestConfigurationExtension'), 'settings'), createObject())]" - }, - "supressFailures": { - "value": "[coalesce(tryGet(parameters('extensionGuestConfigurationExtension'), 'supressFailures'), false())]" - }, - "protectedSettings": { - "value": "[parameters('extensionGuestConfigurationExtensionProtectedSettings')]" - }, - "tags": { - "value": "[coalesce(tryGet(parameters('extensionGuestConfigurationExtension'), 'tags'), parameters('tags'))]" - } - }, - "template": { - "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", - "languageVersion": "2.0", - "contentVersion": "1.0.0.0", - "metadata": { - "_generator": { - "name": "bicep", - "version": "0.37.4.10188", - "templateHash": "13125609748815648088" - }, - "name": "Virtual Machine Extensions", - "description": "This module deploys a Virtual Machine Extension." }, "parameters": { - "virtualMachineName": { + "name": { "type": "string", "metadata": { - "description": "Conditional. The name of the parent virtual machine that extension is provisioned for. Required if the template is used in a standalone deployment." + "description": "Required. Name of the private endpoint resource to create." } }, - "name": { + "subnetResourceId": { "type": "string", "metadata": { - "description": "Required. The name of the virtual machine extension." + "description": "Required. Resource ID of the subnet where the endpoint needs to be created." } }, - "location": { - "type": "string", - "defaultValue": "[resourceGroup().location]", + "applicationSecurityGroupResourceIds": { + "type": "array", + "items": { + "type": "string" + }, + "nullable": true, "metadata": { - "description": "Optional. The location the extension is deployed to." + "description": "Optional. Application security groups in which the private endpoint IP configuration is included." } }, - "publisher": { + "customNetworkInterfaceName": { "type": "string", + "nullable": true, "metadata": { - "description": "Required. The name of the extension handler publisher." + "description": "Optional. The custom name of the network interface attached to the private endpoint." } }, - "type": { - "type": "string", + "ipConfigurations": { + "type": "array", + "items": { + "$ref": "#/definitions/ipConfigurationType" + }, + "nullable": true, "metadata": { - "description": "Required. Specifies the type of the extension; an example is \"CustomScriptExtension\"." + "description": "Optional. A list of IP configurations of the private endpoint. This will be used to map to the First Party Service endpoints." } }, - "typeHandlerVersion": { - "type": "string", + "privateDnsZoneGroup": { + "$ref": "#/definitions/privateDnsZoneGroupType", + "nullable": true, "metadata": { - "description": "Required. Specifies the version of the script handler." + "description": "Optional. The private DNS zone group to configure for the private endpoint." } }, - "autoUpgradeMinorVersion": { - "type": "bool", + "location": { + "type": "string", + "defaultValue": "[resourceGroup().location]", "metadata": { - "description": "Required. Indicates whether the extension should use a newer minor version if one is available at deployment time. Once deployed, however, the extension will not upgrade minor versions unless redeployed, even with this property set to true." + "description": "Optional. Location for all Resources." } }, - "forceUpdateTag": { - "type": "string", + "lock": { + "$ref": "#/definitions/lockType", "nullable": true, "metadata": { - "description": "Optional. How the extension handler should be forced to update even if the extension configuration has not changed." + "description": "Optional. The lock settings of the service." } }, - "settings": { - "type": "object", + "roleAssignments": { + "type": "array", + "items": { + "$ref": "#/definitions/roleAssignmentType" + }, "nullable": true, "metadata": { - "description": "Optional. Any object that contains the extension specific settings." + "description": "Optional. Array of role assignments to create." } }, - "protectedSettings": { - "type": "secureObject", + "tags": { + "type": "object", "nullable": true, "metadata": { - "description": "Optional. Any object that contains the extension specific protected settings." + "description": "Optional. Tags to be applied on all resources/resource groups in this deployment." } }, - "supressFailures": { - "type": "bool", - "defaultValue": false, + "customDnsConfigs": { + "type": "array", + "items": { + "$ref": "#/definitions/customDnsConfigType" + }, + "nullable": true, "metadata": { - "description": "Optional. Indicates whether failures stemming from the extension will be suppressed (Operational failures such as not connecting to the VM will not be suppressed regardless of this value). The default is false." + "description": "Optional. Custom DNS configurations." } }, - "enableAutomaticUpgrade": { - "type": "bool", + "manualPrivateLinkServiceConnections": { + "type": "array", + "items": { + "$ref": "#/definitions/privateLinkServiceConnectionType" + }, + "nullable": true, "metadata": { - "description": "Required. Indicates whether the extension should be automatically upgraded by the platform if there is a newer version of the extension available." + "description": "Conditional. A grouping of information about the connection to the remote resource. Used when the network admin does not have access to approve connections to the remote resource. Required if `privateLinkServiceConnections` is empty." } }, - "tags": { - "type": "object", - "metadata": { - "__bicep_resource_derived_type!": { - "source": "Microsoft.Compute/virtualMachines/extensions@2024-11-01#properties/tags" - }, - "description": "Optional. Tags of the resource." + "privateLinkServiceConnections": { + "type": "array", + "items": { + "$ref": "#/definitions/privateLinkServiceConnectionType" }, - "nullable": true - }, - "protectedSettingsFromKeyVault": { - "type": "object", + "nullable": true, "metadata": { - "__bicep_resource_derived_type!": { - "source": "Microsoft.Compute/virtualMachines/extensions@2024-11-01#properties/properties/properties/protectedSettingsFromKeyVault" - }, - "description": "Optional. The extensions protected settings that are passed by reference, and consumed from key vault." - }, - "nullable": true + "description": "Conditional. A grouping of information about the connection to the remote resource. Required if `manualPrivateLinkServiceConnections` is empty." + } }, - "provisionAfterExtensions": { - "type": "array", + "enableTelemetry": { + "type": "bool", + "defaultValue": true, "metadata": { - "__bicep_resource_derived_type!": { - "source": "Microsoft.Compute/virtualMachines/extensions@2024-11-01#properties/properties/properties/provisionAfterExtensions" - }, - "description": "Optional. Collection of extension names after which this extension needs to be provisioned." - }, - "nullable": true + "description": "Optional. Enable/Disable usage telemetry for module." + } + } + }, + "variables": { + "copy": [ + { + "name": "formattedRoleAssignments", + "count": "[length(coalesce(parameters('roleAssignments'), createArray()))]", + "input": "[union(coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')], createObject('roleDefinitionId', coalesce(tryGet(variables('builtInRoleNames'), coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName), if(contains(coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName, '/providers/Microsoft.Authorization/roleDefinitions/'), coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName, subscriptionResourceId('Microsoft.Authorization/roleDefinitions', coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName)))))]" + } + ], + "builtInRoleNames": { + "Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'b24988ac-6180-42a0-ab88-20f7382dd24c')]", + "DNS Resolver Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '0f2ebee7-ffd4-4fc0-b3b7-664099fdad5d')]", + "DNS Zone Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'befefa01-2a29-4197-83a8-272ff33ce314')]", + "Domain Services Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'eeaeda52-9324-47f6-8069-5d5bade478b2')]", + "Domain Services Reader": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '361898ef-9ed1-48c2-849c-a832951106bb')]", + "Network Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '4d97b98b-1d4f-4787-a291-c67834d212e7')]", + "Owner": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '8e3af657-a8ff-443c-a75c-2fe8c4bcb635')]", + "Private DNS Zone Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'b12aa53e-6015-4669-85d0-8515ebb3ae7f')]", + "Reader": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'acdd72a7-3385-48ef-bd42-f606fba81ae7')]", + "Role Based Access Control Administrator": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'f58310d9-a9f6-439a-9e8d-f62e7b41a168')]" } }, "resources": { - "virtualMachine": { - "existing": true, - "type": "Microsoft.Compute/virtualMachines", - "apiVersion": "2024-11-01", - "name": "[parameters('virtualMachineName')]" + "avmTelemetry": { + "condition": "[parameters('enableTelemetry')]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2024-03-01", + "name": "[format('46d3xbcp.res.network-privateendpoint.{0}.{1}', replace('0.11.0', '.', '-'), substring(uniqueString(deployment().name, parameters('location')), 0, 4))]", + "properties": { + "mode": "Incremental", + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "resources": [], + "outputs": { + "telemetry": { + "type": "String", + "value": "For more information, see https://aka.ms/avm/TelemetryInfo" + } + } + } + } }, - "extension": { - "type": "Microsoft.Compute/virtualMachines/extensions", - "apiVersion": "2024-11-01", - "name": "[format('{0}/{1}', parameters('virtualMachineName'), parameters('name'))]", + "privateEndpoint": { + "type": "Microsoft.Network/privateEndpoints", + "apiVersion": "2024-05-01", + "name": "[parameters('name')]", "location": "[parameters('location')]", "tags": "[parameters('tags')]", "properties": { - "publisher": "[parameters('publisher')]", - "type": "[parameters('type')]", - "typeHandlerVersion": "[parameters('typeHandlerVersion')]", - "autoUpgradeMinorVersion": "[parameters('autoUpgradeMinorVersion')]", - "enableAutomaticUpgrade": "[parameters('enableAutomaticUpgrade')]", - "forceUpdateTag": "[parameters('forceUpdateTag')]", - "settings": "[parameters('settings')]", - "protectedSettings": "[parameters('protectedSettings')]", - "suppressFailures": "[parameters('supressFailures')]", - "protectedSettingsFromKeyVault": "[parameters('protectedSettingsFromKeyVault')]", - "provisionAfterExtensions": "[parameters('provisionAfterExtensions')]" + "copy": [ + { + "name": "applicationSecurityGroups", + "count": "[length(coalesce(parameters('applicationSecurityGroupResourceIds'), createArray()))]", + "input": { + "id": "[coalesce(parameters('applicationSecurityGroupResourceIds'), createArray())[copyIndex('applicationSecurityGroups')]]" + } + } + ], + "customDnsConfigs": "[coalesce(parameters('customDnsConfigs'), createArray())]", + "customNetworkInterfaceName": "[coalesce(parameters('customNetworkInterfaceName'), '')]", + "ipConfigurations": "[coalesce(parameters('ipConfigurations'), createArray())]", + "manualPrivateLinkServiceConnections": "[coalesce(parameters('manualPrivateLinkServiceConnections'), createArray())]", + "privateLinkServiceConnections": "[coalesce(parameters('privateLinkServiceConnections'), createArray())]", + "subnet": { + "id": "[parameters('subnetResourceId')]" + } } - } - }, - "outputs": { - "name": { - "type": "string", - "metadata": { - "description": "The name of the extension." - }, - "value": "[parameters('name')]" }, - "resourceId": { - "type": "string", - "metadata": { - "description": "The resource ID of the extension." + "privateEndpoint_lock": { + "condition": "[and(not(empty(coalesce(parameters('lock'), createObject()))), not(equals(tryGet(parameters('lock'), 'kind'), 'None')))]", + "type": "Microsoft.Authorization/locks", + "apiVersion": "2020-05-01", + "scope": "[format('Microsoft.Network/privateEndpoints/{0}', parameters('name'))]", + "name": "[coalesce(tryGet(parameters('lock'), 'name'), format('lock-{0}', parameters('name')))]", + "properties": { + "level": "[coalesce(tryGet(parameters('lock'), 'kind'), '')]", + "notes": "[if(equals(tryGet(parameters('lock'), 'kind'), 'CanNotDelete'), 'Cannot delete resource or child resources.', 'Cannot delete or modify the resource or child resources.')]" }, - "value": "[resourceId('Microsoft.Compute/virtualMachines/extensions', parameters('virtualMachineName'), parameters('name'))]" + "dependsOn": [ + "privateEndpoint" + ] }, - "resourceGroupName": { - "type": "string", - "metadata": { - "description": "The name of the Resource Group the extension was created in." + "privateEndpoint_roleAssignments": { + "copy": { + "name": "privateEndpoint_roleAssignments", + "count": "[length(coalesce(variables('formattedRoleAssignments'), createArray()))]" }, - "value": "[resourceGroup().name]" - }, - "location": { - "type": "string", - "metadata": { - "description": "The location the resource was deployed into." + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2022-04-01", + "scope": "[format('Microsoft.Network/privateEndpoints/{0}', parameters('name'))]", + "name": "[coalesce(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'name'), guid(resourceId('Microsoft.Network/privateEndpoints', parameters('name')), coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].principalId, coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].roleDefinitionId))]", + "properties": { + "roleDefinitionId": "[coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].roleDefinitionId]", + "principalId": "[coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].principalId]", + "description": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'description')]", + "principalType": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'principalType')]", + "condition": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'condition')]", + "conditionVersion": "[if(not(empty(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'condition'))), coalesce(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'conditionVersion'), '2.0'), null())]", + "delegatedManagedIdentityResourceId": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'delegatedManagedIdentityResourceId')]" }, - "value": "[reference('extension', '2024-11-01', 'full').location]" - } - } - } - }, - "dependsOn": [ - "vm", - "vm_hostPoolRegistrationExtension" - ] - }, - "vm_backup": { - "condition": "[not(empty(parameters('backupVaultName')))]", - "type": "Microsoft.Resources/deployments", - "apiVersion": "2022-09-01", - "name": "[format('{0}-VM-Backup', uniqueString(deployment().name, parameters('location')))]", - "resourceGroup": "[parameters('backupVaultResourceGroup')]", - "properties": { - "expressionEvaluationOptions": { - "scope": "inner" - }, - "mode": "Incremental", - "parameters": { - "name": { - "value": "[format('vm;iaasvmcontainerv2;{0};{1}', resourceGroup().name, parameters('name'))]" - }, - "location": { - "value": "[parameters('location')]" - }, - "policyId": { - "value": "[resourceId(parameters('backupVaultResourceGroup'), 'Microsoft.RecoveryServices/vaults/backupPolicies', parameters('backupVaultName'), parameters('backupPolicyName'))]" - }, - "protectedItemType": { - "value": "Microsoft.Compute/virtualMachines" - }, - "protectionContainerName": { - "value": "[format('iaasvmcontainer;iaasvmcontainerv2;{0};{1}', resourceGroup().name, parameters('name'))]" - }, - "recoveryVaultName": { - "value": "[parameters('backupVaultName')]" - }, - "sourceResourceId": { - "value": "[resourceId('Microsoft.Compute/virtualMachines', parameters('name'))]" - } - }, - "template": { - "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", - "contentVersion": "1.0.0.0", - "metadata": { - "_generator": { - "name": "bicep", - "version": "0.37.4.10188", - "templateHash": "13700395772485726477" + "dependsOn": [ + "privateEndpoint" + ] }, - "name": "Recovery Service Vaults Protection Container Protected Item", - "description": "This module deploys a Recovery Services Vault Protection Container Protected Item." + "privateEndpoint_privateDnsZoneGroup": { + "condition": "[not(empty(parameters('privateDnsZoneGroup')))]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2022-09-01", + "name": "[format('{0}-PrivateEndpoint-PrivateDnsZoneGroup', uniqueString(deployment().name))]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "name": { + "value": "[tryGet(parameters('privateDnsZoneGroup'), 'name')]" + }, + "privateEndpointName": { + "value": "[parameters('name')]" + }, + "privateDnsZoneConfigs": { + "value": "[parameters('privateDnsZoneGroup').privateDnsZoneGroupConfigs]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "languageVersion": "2.0", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.34.44.8038", + "templateHash": "13997305779829540948" + }, + "name": "Private Endpoint Private DNS Zone Groups", + "description": "This module deploys a Private Endpoint Private DNS Zone Group." + }, + "definitions": { + "privateDnsZoneGroupConfigType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The name of the private DNS zone group config." + } + }, + "privateDnsZoneResourceId": { + "type": "string", + "metadata": { + "description": "Required. The resource id of the private DNS zone." + } + } + }, + "metadata": { + "__bicep_export!": true + } + } + }, + "parameters": { + "privateEndpointName": { + "type": "string", + "metadata": { + "description": "Conditional. The name of the parent private endpoint. Required if the template is used in a standalone deployment." + } + }, + "privateDnsZoneConfigs": { + "type": "array", + "items": { + "$ref": "#/definitions/privateDnsZoneGroupConfigType" + }, + "minLength": 1, + "maxLength": 5, + "metadata": { + "description": "Required. Array of private DNS zone configurations of the private DNS zone group. A DNS zone group can support up to 5 DNS zones." + } + }, + "name": { + "type": "string", + "defaultValue": "default", + "metadata": { + "description": "Optional. The name of the private DNS zone group." + } + } + }, + "variables": { + "copy": [ + { + "name": "privateDnsZoneConfigsVar", + "count": "[length(parameters('privateDnsZoneConfigs'))]", + "input": { + "name": "[coalesce(tryGet(parameters('privateDnsZoneConfigs')[copyIndex('privateDnsZoneConfigsVar')], 'name'), last(split(parameters('privateDnsZoneConfigs')[copyIndex('privateDnsZoneConfigsVar')].privateDnsZoneResourceId, '/')))]", + "properties": { + "privateDnsZoneId": "[parameters('privateDnsZoneConfigs')[copyIndex('privateDnsZoneConfigsVar')].privateDnsZoneResourceId]" + } + } + } + ] + }, + "resources": { + "privateEndpoint": { + "existing": true, + "type": "Microsoft.Network/privateEndpoints", + "apiVersion": "2024-05-01", + "name": "[parameters('privateEndpointName')]" + }, + "privateDnsZoneGroup": { + "type": "Microsoft.Network/privateEndpoints/privateDnsZoneGroups", + "apiVersion": "2024-05-01", + "name": "[format('{0}/{1}', parameters('privateEndpointName'), parameters('name'))]", + "properties": { + "privateDnsZoneConfigs": "[variables('privateDnsZoneConfigsVar')]" + } + } + }, + "outputs": { + "name": { + "type": "string", + "metadata": { + "description": "The name of the private endpoint DNS zone group." + }, + "value": "[parameters('name')]" + }, + "resourceId": { + "type": "string", + "metadata": { + "description": "The resource ID of the private endpoint DNS zone group." + }, + "value": "[resourceId('Microsoft.Network/privateEndpoints/privateDnsZoneGroups', parameters('privateEndpointName'), parameters('name'))]" + }, + "resourceGroupName": { + "type": "string", + "metadata": { + "description": "The resource group the private endpoint DNS zone group was deployed into." + }, + "value": "[resourceGroup().name]" + } + } + } + }, + "dependsOn": [ + "privateEndpoint" + ] + } }, - "parameters": { - "name": { + "outputs": { + "resourceGroupName": { "type": "string", "metadata": { - "description": "Required. Name of the resource." - } + "description": "The resource group the private endpoint was deployed into." + }, + "value": "[resourceGroup().name]" }, - "protectionContainerName": { + "resourceId": { "type": "string", "metadata": { - "description": "Conditional. Name of the Azure Recovery Service Vault Protection Container. Required if the template is used in a standalone deployment." - } + "description": "The resource ID of the private endpoint." + }, + "value": "[resourceId('Microsoft.Network/privateEndpoints', parameters('name'))]" }, - "recoveryVaultName": { + "name": { "type": "string", "metadata": { - "description": "Conditional. The name of the parent Azure Recovery Service Vault. Required if the template is used in a standalone deployment." - } + "description": "The name of the private endpoint." + }, + "value": "[parameters('name')]" }, "location": { - "type": "string", - "defaultValue": "[resourceGroup().location]", - "metadata": { - "description": "Optional. Location for all resources." - } - }, - "protectedItemType": { - "type": "string", - "allowedValues": [ - "AzureFileShareProtectedItem", - "AzureVmWorkloadSAPAseDatabase", - "AzureVmWorkloadSAPHanaDatabase", - "AzureVmWorkloadSQLDatabase", - "DPMProtectedItem", - "GenericProtectedItem", - "MabFileFolderProtectedItem", - "Microsoft.ClassicCompute/virtualMachines", - "Microsoft.Compute/virtualMachines", - "Microsoft.Sql/servers/databases" - ], - "metadata": { - "description": "Required. The backup item type." - } - }, - "policyId": { "type": "string", "metadata": { - "description": "Required. ID of the backup policy with which this item is backed up." - } + "description": "The location the resource was deployed into." + }, + "value": "[reference('privateEndpoint', '2024-05-01', 'full').location]" }, - "sourceResourceId": { - "type": "string", - "metadata": { - "description": "Required. Resource ID of the resource to back up." - } - } - }, - "resources": [ - { - "type": "Microsoft.RecoveryServices/vaults/backupFabrics/protectionContainers/protectedItems", - "apiVersion": "2025-02-01", - "name": "[format('{0}/Azure/{1}/{2}', parameters('recoveryVaultName'), parameters('protectionContainerName'), parameters('name'))]", - "location": "[parameters('location')]", - "properties": { - "protectedItemType": "[parameters('protectedItemType')]", - "policyId": "[parameters('policyId')]", - "sourceResourceId": "[parameters('sourceResourceId')]" - } - } - ], - "outputs": { - "resourceGroupName": { - "type": "string", + "customDnsConfigs": { + "type": "array", + "items": { + "$ref": "#/definitions/customDnsConfigType" + }, "metadata": { - "description": "The name of the Resource Group the protected item was created in." + "description": "The custom DNS configurations of the private endpoint." }, - "value": "[resourceGroup().name]" + "value": "[reference('privateEndpoint').customDnsConfigs]" }, - "resourceId": { - "type": "string", + "networkInterfaceResourceIds": { + "type": "array", + "items": { + "type": "string" + }, "metadata": { - "description": "The resource ID of the protected item." + "description": "The resource IDs of the network interfaces associated with the private endpoint." }, - "value": "[resourceId('Microsoft.RecoveryServices/vaults/backupFabrics/protectionContainers/protectedItems', split(format('{0}/Azure/{1}/{2}', parameters('recoveryVaultName'), parameters('protectionContainerName'), parameters('name')), '/')[0], split(format('{0}/Azure/{1}/{2}', parameters('recoveryVaultName'), parameters('protectionContainerName'), parameters('name')), '/')[1], split(format('{0}/Azure/{1}/{2}', parameters('recoveryVaultName'), parameters('protectionContainerName'), parameters('name')), '/')[2], split(format('{0}/Azure/{1}/{2}', parameters('recoveryVaultName'), parameters('protectionContainerName'), parameters('name')), '/')[3])]" + "value": "[map(reference('privateEndpoint').networkInterfaces, lambda('nic', lambdaVariables('nic').id))]" }, - "name": { + "groupId": { "type": "string", + "nullable": true, "metadata": { - "description": "The Name of the protected item." + "description": "The group Id for the private endpoint Group." }, - "value": "[format('{0}/Azure/{1}/{2}', parameters('recoveryVaultName'), parameters('protectionContainerName'), parameters('name'))]" + "value": "[coalesce(tryGet(tryGet(tryGet(tryGet(reference('privateEndpoint'), 'manualPrivateLinkServiceConnections'), 0, 'properties'), 'groupIds'), 0), tryGet(tryGet(tryGet(tryGet(reference('privateEndpoint'), 'privateLinkServiceConnections'), 0, 'properties'), 'groupIds'), 0))]" } } } }, "dependsOn": [ - "vm", - "vm_azureGuestConfigurationExtension" + "app" ] } }, @@ -42396,21 +44065,21 @@ "name": { "type": "string", "metadata": { - "description": "The name of the VM." + "description": "The name of the site." }, "value": "[parameters('name')]" }, "resourceId": { "type": "string", "metadata": { - "description": "The resource ID of the VM." + "description": "The resource ID of the site." }, - "value": "[resourceId('Microsoft.Compute/virtualMachines', parameters('name'))]" + "value": "[resourceId('Microsoft.Web/sites', parameters('name'))]" }, "resourceGroupName": { "type": "string", "metadata": { - "description": "The name of the resource group the VM was created in." + "description": "The resource group the site was deployed into." }, "value": "[resourceGroup().name]" }, @@ -42420,35 +44089,378 @@ "metadata": { "description": "The principal ID of the system assigned identity." }, - "value": "[tryGet(tryGet(reference('vm', '2024-07-01', 'full'), 'identity'), 'principalId')]" + "value": "[tryGet(tryGet(reference('app', '2024-04-01', 'full'), 'identity'), 'principalId')]" }, "location": { "type": "string", "metadata": { "description": "The location the resource was deployed into." }, - "value": "[reference('vm', '2024-07-01', 'full').location]" + "value": "[reference('app', '2024-04-01', 'full').location]" }, - "nicConfigurations": { - "type": "array", - "items": { - "$ref": "#/definitions/nicConfigurationOutputType" + "defaultHostname": { + "type": "string", + "metadata": { + "description": "Default hostname of the app." + }, + "value": "[format('https://{0}.azurewebsites.net', parameters('name'))]" + }, + "customDomainVerificationId": { + "type": "string", + "metadata": { + "description": "Unique identifier that verifies the custom domains assigned to the app." }, + "value": "[reference('app').customDomainVerificationId]" + }, + "outboundIpAddresses": { + "type": "string", "metadata": { - "description": "The list of NIC configurations of the virtual machine." + "description": "The outbound IP addresses of the app." }, - "copy": { - "count": "[length(parameters('nicConfigurations'))]", - "input": { - "name": "[reference(format('vm_nic[{0}]', copyIndex())).outputs.name.value]", - "ipConfigurations": "[reference(format('vm_nic[{0}]', copyIndex())).outputs.ipConfigurations.value]" + "value": "[reference('app').outboundIpAddresses]" + } + } + } + }, + "dependsOn": [ + "applicationInsights", + "logAnalyticsWorkspace", + "userAssignedIdentity", + "virtualNetwork", + "webServerFarm" + ] + }, + "containerInstance": { + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "[take(format('module.container-instance.{0}', variables('containerInstanceName')), 64)]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "name": { + "value": "[variables('containerInstanceName')]" + }, + "location": { + "value": "[variables('solutionLocation')]" + }, + "tags": { + "value": "[parameters('tags')]" + }, + "containerImage": { + "value": "[format('{0}.azurecr.io/content-gen-api:{1}', variables('acrResourceName'), parameters('imageTag'))]" + }, + "cpu": { + "value": 2 + }, + "memoryInGB": { + "value": 4 + }, + "port": { + "value": 8000 + }, + "subnetResourceId": "[if(parameters('enablePrivateNetworking'), createObject('value', reference('virtualNetwork').outputs.aciSubnetResourceId.value), createObject('value', ''))]", + "registryServer": { + "value": "[format('{0}.azurecr.io', variables('acrResourceName'))]" + }, + "userAssignedIdentityResourceId": { + "value": "[reference('userAssignedIdentity').outputs.resourceId.value]" + }, + "enableTelemetry": { + "value": "[parameters('enableTelemetry')]" + }, + "environmentVariables": { + "value": [ + { + "name": "AZURE_OPENAI_ENDPOINT", + "value": "[format('https://{0}.openai.azure.com/', variables('aiFoundryAiServicesResourceName'))]" + }, + { + "name": "AZURE_OPENAI_GPT_MODEL", + "value": "[parameters('gptModelName')]" + }, + { + "name": "AZURE_OPENAI_IMAGE_MODEL", + "value": "[variables('imageModelConfig')[parameters('imageModelChoice')].name]" + }, + { + "name": "AZURE_OPENAI_GPT_IMAGE_ENDPOINT", + "value": "[if(not(equals(parameters('imageModelChoice'), 'none')), format('https://{0}.openai.azure.com/', variables('aiFoundryAiServicesResourceName')), '')]" + }, + { + "name": "AZURE_OPENAI_API_VERSION", + "value": "[parameters('azureOpenaiAPIVersion')]" + }, + { + "name": "AZURE_COSMOS_ENDPOINT", + "value": "[format('https://cosmos-{0}.documents.azure.com:443/', variables('solutionSuffix'))]" + }, + { + "name": "AZURE_COSMOS_DATABASE_NAME", + "value": "[variables('cosmosDBDatabaseName')]" + }, + { + "name": "AZURE_COSMOS_PRODUCTS_CONTAINER", + "value": "[variables('cosmosDBProductsContainer')]" + }, + { + "name": "AZURE_COSMOS_CONVERSATIONS_CONTAINER", + "value": "[variables('cosmosDBConversationsContainer')]" + }, + { + "name": "AZURE_BLOB_ACCOUNT_NAME", + "value": "[variables('storageAccountName')]" + }, + { + "name": "AZURE_BLOB_PRODUCT_IMAGES_CONTAINER", + "value": "[variables('productImagesContainer')]" + }, + { + "name": "AZURE_BLOB_GENERATED_IMAGES_CONTAINER", + "value": "[variables('generatedImagesContainer')]" + }, + { + "name": "AZURE_AI_SEARCH_ENDPOINT", + "value": "[format('https://{0}.search.windows.net', variables('aiSearchName'))]" + }, + { + "name": "AZURE_AI_SEARCH_PRODUCTS_INDEX", + "value": "[variables('azureSearchIndex')]" + }, + { + "name": "AZURE_AI_SEARCH_IMAGE_INDEX", + "value": "product-images" + }, + { + "name": "AZURE_CLIENT_ID", + "value": "[reference('userAssignedIdentity').outputs.clientId.value]" + }, + { + "name": "PORT", + "value": "8000" + }, + { + "name": "WORKERS", + "value": "4" + }, + { + "name": "RUNNING_IN_PRODUCTION", + "value": "true" + }, + { + "name": "USE_FOUNDRY", + "value": "[if(parameters('useFoundryMode'), 'true', 'false')]" + }, + { + "name": "AZURE_AI_PROJECT_ENDPOINT", + "value": "[if(variables('useExistingAiFoundryAiProject'), format('https://{0}.services.ai.azure.com/api/projects/{1}', variables('aiFoundryAiServicesResourceName'), variables('aiFoundryAiProjectResourceName')), reference('aiFoundryAiServicesProject').outputs.apiEndpoint.value)]" + }, + { + "name": "AZURE_AI_MODEL_DEPLOYMENT_NAME", + "value": "[parameters('gptModelName')]" + }, + { + "name": "AZURE_AI_IMAGE_MODEL_DEPLOYMENT", + "value": "[variables('imageModelConfig')[parameters('imageModelChoice')].name]" + } + ] + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.39.26.7824", + "templateHash": "11247487291315089538" + } + }, + "parameters": { + "name": { + "type": "string", + "metadata": { + "description": "Required. Name of the container group." + } + }, + "location": { + "type": "string", + "metadata": { + "description": "Required. Location for the container instance." + } + }, + "tags": { + "type": "object", + "defaultValue": {}, + "metadata": { + "description": "Optional. Tags for all resources." + } + }, + "containerImage": { + "type": "string", + "metadata": { + "description": "Required. Container image to deploy." + } + }, + "cpu": { + "type": "int", + "defaultValue": 2, + "metadata": { + "description": "Optional. CPU cores for the container." + } + }, + "memoryInGB": { + "type": "int", + "defaultValue": 4, + "metadata": { + "description": "Optional. Memory in GB for the container." + } + }, + "port": { + "type": "int", + "defaultValue": 8000, + "metadata": { + "description": "Optional. Port to expose." + } + }, + "subnetResourceId": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "Optional. Subnet resource ID for VNet integration. If empty, public IP will be used." + } + }, + "environmentVariables": { + "type": "array", + "metadata": { + "description": "Required. Environment variables for the container." + } + }, + "enableTelemetry": { + "type": "bool", + "defaultValue": true, + "metadata": { + "description": "Optional. Enable telemetry." + } + }, + "registryServer": { + "type": "string", + "metadata": { + "description": "Required. Container registry server." + } + }, + "userAssignedIdentityResourceId": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "Optional. User-assigned managed identity resource ID for ACR pull." + } + } + }, + "variables": { + "isPrivateNetworking": "[not(empty(parameters('subnetResourceId')))]" + }, + "resources": [ + { + "condition": "[parameters('enableTelemetry')]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2024-03-01", + "name": "[format('46d3xbcp.res.containerinstance.{0}.{1}', replace('-..--..-', '.', '-'), substring(uniqueString(deployment().name, parameters('location')), 0, 4))]", + "properties": { + "mode": "Incremental", + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "resources": [] + } + } + }, + { + "type": "Microsoft.ContainerInstance/containerGroups", + "apiVersion": "2023-05-01", + "name": "[parameters('name')]", + "location": "[parameters('location')]", + "tags": "[parameters('tags')]", + "identity": { + "type": "UserAssigned", + "userAssignedIdentities": { + "[format('{0}', parameters('userAssignedIdentityResourceId'))]": {} + } + }, + "properties": { + "containers": [ + { + "name": "[parameters('name')]", + "properties": { + "image": "[parameters('containerImage')]", + "resources": { + "requests": { + "cpu": "[parameters('cpu')]", + "memoryInGB": "[parameters('memoryInGB')]" + } + }, + "ports": [ + { + "port": "[parameters('port')]", + "protocol": "TCP" + } + ], + "environmentVariables": "[parameters('environmentVariables')]" + } + } + ], + "osType": "Linux", + "restartPolicy": "Always", + "subnetIds": "[if(variables('isPrivateNetworking'), createArray(createObject('id', parameters('subnetResourceId'))), null())]", + "ipAddress": { + "type": "[if(variables('isPrivateNetworking'), 'Private', 'Public')]", + "ports": [ + { + "port": "[parameters('port')]", + "protocol": "TCP" + } + ], + "dnsNameLabel": "[if(variables('isPrivateNetworking'), null(), parameters('name'))]" } } } + ], + "outputs": { + "name": { + "type": "string", + "metadata": { + "description": "The name of the container group." + }, + "value": "[parameters('name')]" + }, + "resourceId": { + "type": "string", + "metadata": { + "description": "The resource ID of the container group." + }, + "value": "[resourceId('Microsoft.ContainerInstance/containerGroups', parameters('name'))]" + }, + "ipAddress": { + "type": "string", + "metadata": { + "description": "The IP address of the container (private or public depending on mode)." + }, + "value": "[reference(resourceId('Microsoft.ContainerInstance/containerGroups', parameters('name')), '2023-05-01').ipAddress.ip]" + }, + "fqdn": { + "type": "string", + "metadata": { + "description": "The FQDN of the container (only available for public mode)." + }, + "value": "[if(variables('isPrivateNetworking'), '', reference(resourceId('Microsoft.ContainerInstance/containerGroups', parameters('name')), '2023-05-01').ipAddress.fqdn)]" + } } } }, "dependsOn": [ + "aiFoundryAiServicesProject", "userAssignedIdentity", "virtualNetwork" ] diff --git a/infra/main.parameters.json b/infra/main.parameters.json index 075a266ee..0bed7465d 100644 --- a/infra/main.parameters.json +++ b/infra/main.parameters.json @@ -29,12 +29,6 @@ "imageModelCapacity": { "value": "${imageModelCapacity}" }, - "embeddingModel": { - "value": "${embeddingModel}" - }, - "embeddingDeploymentCapacity": { - "value": "${embeddingDeploymentCapacity}" - }, "azureOpenaiAPIVersion": { "value": "${azureOpenaiAPIVersion}" }, diff --git a/infra/main.waf.parameters.json b/infra/main.waf.parameters.json index e34bc97cc..84bf143c6 100644 --- a/infra/main.waf.parameters.json +++ b/infra/main.waf.parameters.json @@ -29,12 +29,6 @@ "imageModelCapacity": { "value": "${imageModelCapacity}" }, - "embeddingModel": { - "value": "${embeddingModel}" - }, - "embeddingDeploymentCapacity": { - "value": "${embeddingDeploymentCapacity}" - }, "azureOpenaiAPIVersion": { "value": "${azureOpenaiAPIVersion}" }, @@ -61,6 +55,15 @@ }, "enableScalability": { "value": true + }, + "vmAdminUsername": { + "value": "${AZURE_ENV_VM_ADMIN_USERNAME}" + }, + "vmAdminPassword": { + "value": "${AZURE_ENV_VM_ADMIN_PASSWORD}" + }, + "vmSize": { + "value": "${AZURE_ENV_VM_SIZE}" } } } diff --git a/infra/modules/virtualNetwork.bicep b/infra/modules/virtualNetwork.bicep index b11a6fb61..5ec807734 100644 --- a/infra/modules/virtualNetwork.bicep +++ b/infra/modules/virtualNetwork.bicep @@ -2,16 +2,13 @@ // Networking - NSGs, VNET and Subnets for Content Generation Solution /****************************************************************************************************************************/ @description('Name of the virtual network.') -param vnetName string +param name string @description('Azure region to deploy resources.') -param vnetLocation string = resourceGroup().location +param location string = resourceGroup().location @description('Required. An Array of 1 or more IP Address Prefixes for the Virtual Network.') -param vnetAddressPrefixes array = ['10.0.0.0/20'] - -@description('Optional. Deploy Azure Bastion and Jumpbox subnets for VM-based administration.') -param deployBastionAndJumpbox bool = false +param addressPrefixes array = ['10.0.0.0/20'] @description('An array of subnets to be created within the virtual network.') // Core subnets: web (App Service), peps (Private Endpoints), aci (Container Instance) @@ -101,7 +98,7 @@ var coreSubnets = [ } ] -// Optional Bastion and Jumpbox subnets (only deployed when needed for VM administration) +// Bastion and Jumpbox subnets (always deployed with private networking) // VM Size Notes: // 1 B-series VMs (like Standard_B2ms) do not support accelerated networking. // 2 Pick a VM size that supports accelerated networking + Premium SSD (the usual jump-box candidates): @@ -110,7 +107,7 @@ var coreSubnets = [ // Standard_D2s_v4 (2 vCPU, 8 GiB RAM, Premium SSD) // Previous gen, also broadly available. // Standard_DS2_v2 (2 vCPU, 7 GiB RAM, Premium SSD) // Legacy SKU, being retired from some regions - avoid for new deployments. // 3 A-series (Av2) is NOT suitable: no Premium SSD support, no accelerated networking. -var bastionSubnets = deployBastionAndJumpbox ? [ +var bastionSubnets = [ { name: 'AzureBastionSubnet' addressPrefixes: ['10.0.10.0/26'] @@ -194,7 +191,7 @@ var bastionSubnets = deployBastionAndJumpbox ? [ ] } } -] : [] +] var vnetSubnets = concat(coreSubnets, bastionSubnets) @@ -217,7 +214,7 @@ module nsgs 'br/public:avm/res/network/network-security-group:0.5.2' = [ name: take('avm.res.network.network-security-group.${subnet.?networkSecurityGroup.name}.${resourceSuffix}', 64) params: { name: '${subnet.?networkSecurityGroup.name}-${resourceSuffix}' - location: vnetLocation + location: location securityRules: subnet.?networkSecurityGroup.securityRules tags: tags enableTelemetry: enableTelemetry @@ -227,11 +224,11 @@ module nsgs 'br/public:avm/res/network/network-security-group:0.5.2' = [ // Create VNet and subnets using AVM Virtual Network module module virtualNetwork 'br/public:avm/res/network/virtual-network:0.7.2' = { - name: take('avm.res.network.virtual-network.${vnetName}', 64) + name: take('avm.res.network.virtual-network.${name}', 64) params: { - name: vnetName - location: vnetLocation - addressPrefixes: vnetAddressPrefixes + name: name + location: location + addressPrefixes: addressPrefixes subnets: [ for (subnet, i) in vnetSubnets: { name: subnet.name @@ -273,6 +270,6 @@ output webSubnetResourceId string = contains(map(vnetSubnets, subnet => subnet.n output pepsSubnetResourceId string = contains(map(vnetSubnets, subnet => subnet.name), 'peps') ? virtualNetwork.outputs.subnetResourceIds[indexOf(map(vnetSubnets, subnet => subnet.name), 'peps')] : '' output aciSubnetResourceId string = contains(map(vnetSubnets, subnet => subnet.name), 'aci') ? virtualNetwork.outputs.subnetResourceIds[indexOf(map(vnetSubnets, subnet => subnet.name), 'aci')] : '' -// Optional bastion/jumpbox subnet outputs (only present when deployBastionAndJumpbox is true) -output bastionSubnetResourceId string = deployBastionAndJumpbox && contains(map(vnetSubnets, subnet => subnet.name), 'AzureBastionSubnet') ? virtualNetwork.outputs.subnetResourceIds[indexOf(map(vnetSubnets, subnet => subnet.name), 'AzureBastionSubnet')] : '' -output jumpboxSubnetResourceId string = deployBastionAndJumpbox && contains(map(vnetSubnets, subnet => subnet.name), 'jumpbox') ? virtualNetwork.outputs.subnetResourceIds[indexOf(map(vnetSubnets, subnet => subnet.name), 'jumpbox')] : '' +// Bastion/jumpbox subnet outputs (always present with private networking) +output bastionSubnetResourceId string = contains(map(vnetSubnets, subnet => subnet.name), 'AzureBastionSubnet') ? virtualNetwork.outputs.subnetResourceIds[indexOf(map(vnetSubnets, subnet => subnet.name), 'AzureBastionSubnet')] : '' +output jumpboxSubnetResourceId string = contains(map(vnetSubnets, subnet => subnet.name), 'jumpbox') ? virtualNetwork.outputs.subnetResourceIds[indexOf(map(vnetSubnets, subnet => subnet.name), 'jumpbox')] : '' From 816f8c992e6af7d7cb6ec81bc469fe227896ae07 Mon Sep 17 00:00:00 2001 From: v-maddukuriy Date: Mon, 23 Mar 2026 12:23:02 +0530 Subject: [PATCH 15/72] updated main.json --- infra/main.json | 266 ++++++++++++++++++++++++++++++------------------ 1 file changed, 166 insertions(+), 100 deletions(-) diff --git a/infra/main.json b/infra/main.json index 8f82bb792..6e8421fba 100644 --- a/infra/main.json +++ b/infra/main.json @@ -6,7 +6,7 @@ "_generator": { "name": "bicep", "version": "0.41.2.15936", - "templateHash": "15058520269193157842" + "templateHash": "18140035795753819765" }, "name": "Intelligent Content Generation Accelerator", "description": "Solution Accelerator for multimodal marketing content generation using Microsoft Agent Framework.\n" @@ -169,13 +169,6 @@ "description": "Optional. Resource ID of an existing Foundry project." } }, - "deployBastionAndJumpbox": { - "type": "bool", - "defaultValue": false, - "metadata": { - "description": "Optional. Deploy Azure Bastion and Jumpbox VM for private network administration." - } - }, "vmSize": { "type": "string", "defaultValue": "", @@ -4841,29 +4834,26 @@ }, "mode": "Incremental", "parameters": { - "vnetName": { + "name": { "value": "[format('vnet-{0}', variables('solutionSuffix'))]" }, - "vnetLocation": { - "value": "[variables('solutionLocation')]" - }, - "vnetAddressPrefixes": { + "addressPrefixes": { "value": [ "10.0.0.0/20" ] }, + "location": { + "value": "[parameters('location')]" + }, "tags": { "value": "[parameters('tags')]" }, "logAnalyticsWorkspaceId": "[if(variables('useExistingLogAnalytics'), createObject('value', parameters('existingLogAnalyticsWorkspaceId')), if(parameters('enableMonitoring'), createObject('value', reference('logAnalyticsWorkspace').outputs.resourceId.value), createObject('value', '')))]", - "enableTelemetry": { - "value": "[parameters('enableTelemetry')]" - }, "resourceSuffix": { "value": "[variables('solutionSuffix')]" }, - "deployBastionAndJumpbox": { - "value": "[parameters('deployBastionAndJumpbox')]" + "enableTelemetry": { + "value": "[parameters('enableTelemetry')]" } }, "template": { @@ -4872,25 +4862,25 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.39.26.7824", - "templateHash": "16294706585455769047" + "version": "0.41.2.15936", + "templateHash": "1152564857842534701" } }, "parameters": { - "vnetName": { + "name": { "type": "string", "metadata": { "description": "Name of the virtual network." } }, - "vnetLocation": { + "location": { "type": "string", "defaultValue": "[resourceGroup().location]", "metadata": { "description": "Azure region to deploy resources." } }, - "vnetAddressPrefixes": { + "addressPrefixes": { "type": "array", "defaultValue": [ "10.0.0.0/20" @@ -4899,13 +4889,6 @@ "description": "Required. An Array of 1 or more IP Address Prefixes for the Virtual Network." } }, - "deployBastionAndJumpbox": { - "type": "bool", - "defaultValue": false, - "metadata": { - "description": "Optional. Deploy Azure Bastion and Jumpbox subnets for VM-based administration." - } - }, "tags": { "type": "object", "defaultValue": {}, @@ -5037,7 +5020,102 @@ } } ], - "bastionSubnets": "[if(parameters('deployBastionAndJumpbox'), createArray(createObject('name', 'AzureBastionSubnet', 'addressPrefixes', createArray('10.0.10.0/26'), 'networkSecurityGroup', createObject('name', 'nsg-bastion', 'securityRules', createArray(createObject('name', 'AllowGatewayManager', 'properties', createObject('access', 'Allow', 'direction', 'Inbound', 'priority', 2702, 'protocol', '*', 'sourcePortRange', '*', 'destinationPortRange', '443', 'sourceAddressPrefix', 'GatewayManager', 'destinationAddressPrefix', '*')), createObject('name', 'AllowHttpsInBound', 'properties', createObject('access', 'Allow', 'direction', 'Inbound', 'priority', 2703, 'protocol', '*', 'sourcePortRange', '*', 'destinationPortRange', '443', 'sourceAddressPrefix', 'Internet', 'destinationAddressPrefix', '*')), createObject('name', 'AllowSshRdpOutbound', 'properties', createObject('access', 'Allow', 'direction', 'Outbound', 'priority', 100, 'protocol', '*', 'sourcePortRange', '*', 'destinationPortRanges', createArray('22', '3389'), 'sourceAddressPrefix', '*', 'destinationAddressPrefix', 'VirtualNetwork')), createObject('name', 'AllowAzureCloudOutbound', 'properties', createObject('access', 'Allow', 'direction', 'Outbound', 'priority', 110, 'protocol', 'Tcp', 'sourcePortRange', '*', 'destinationPortRange', '443', 'sourceAddressPrefix', '*', 'destinationAddressPrefix', 'AzureCloud'))))), createObject('name', 'jumpbox', 'addressPrefixes', createArray('10.0.12.0/23'), 'networkSecurityGroup', createObject('name', 'nsg-jumpbox', 'securityRules', createArray(createObject('name', 'AllowRdpFromBastion', 'properties', createObject('access', 'Allow', 'direction', 'Inbound', 'priority', 100, 'protocol', 'Tcp', 'sourcePortRange', '*', 'destinationPortRange', '3389', 'sourceAddressPrefixes', createArray('10.0.10.0/26'), 'destinationAddressPrefixes', createArray('10.0.12.0/23'))))))), createArray())]", + "bastionSubnets": [ + { + "name": "AzureBastionSubnet", + "addressPrefixes": [ + "10.0.10.0/26" + ], + "networkSecurityGroup": { + "name": "nsg-bastion", + "securityRules": [ + { + "name": "AllowGatewayManager", + "properties": { + "access": "Allow", + "direction": "Inbound", + "priority": 2702, + "protocol": "*", + "sourcePortRange": "*", + "destinationPortRange": "443", + "sourceAddressPrefix": "GatewayManager", + "destinationAddressPrefix": "*" + } + }, + { + "name": "AllowHttpsInBound", + "properties": { + "access": "Allow", + "direction": "Inbound", + "priority": 2703, + "protocol": "*", + "sourcePortRange": "*", + "destinationPortRange": "443", + "sourceAddressPrefix": "Internet", + "destinationAddressPrefix": "*" + } + }, + { + "name": "AllowSshRdpOutbound", + "properties": { + "access": "Allow", + "direction": "Outbound", + "priority": 100, + "protocol": "*", + "sourcePortRange": "*", + "destinationPortRanges": [ + "22", + "3389" + ], + "sourceAddressPrefix": "*", + "destinationAddressPrefix": "VirtualNetwork" + } + }, + { + "name": "AllowAzureCloudOutbound", + "properties": { + "access": "Allow", + "direction": "Outbound", + "priority": 110, + "protocol": "Tcp", + "sourcePortRange": "*", + "destinationPortRange": "443", + "sourceAddressPrefix": "*", + "destinationAddressPrefix": "AzureCloud" + } + } + ] + } + }, + { + "name": "jumpbox", + "addressPrefixes": [ + "10.0.12.0/23" + ], + "networkSecurityGroup": { + "name": "nsg-jumpbox", + "securityRules": [ + { + "name": "AllowRdpFromBastion", + "properties": { + "access": "Allow", + "direction": "Inbound", + "priority": 100, + "protocol": "Tcp", + "sourcePortRange": "*", + "destinationPortRange": "3389", + "sourceAddressPrefixes": [ + "10.0.10.0/26" + ], + "destinationAddressPrefixes": [ + "10.0.12.0/23" + ] + } + } + ] + } + } + ], "vnetSubnets": "[concat(variables('coreSubnets'), variables('bastionSubnets'))]" }, "resources": [ @@ -5062,7 +5140,7 @@ "value": "[format('{0}-{1}', tryGet(variables('vnetSubnets')[copyIndex()], 'networkSecurityGroup', 'name'), parameters('resourceSuffix'))]" }, "location": { - "value": "[parameters('vnetLocation')]" + "value": "[parameters('location')]" }, "securityRules": { "value": "[tryGet(variables('vnetSubnets')[copyIndex()], 'networkSecurityGroup', 'securityRules')]" @@ -5713,7 +5791,7 @@ { "type": "Microsoft.Resources/deployments", "apiVersion": "2025-04-01", - "name": "[take(format('avm.res.network.virtual-network.{0}', parameters('vnetName')), 64)]", + "name": "[take(format('avm.res.network.virtual-network.{0}', parameters('name')), 64)]", "properties": { "expressionEvaluationOptions": { "scope": "inner" @@ -5721,13 +5799,13 @@ "mode": "Incremental", "parameters": { "name": { - "value": "[parameters('vnetName')]" + "value": "[parameters('name')]" }, "location": { - "value": "[parameters('vnetLocation')]" + "value": "[parameters('location')]" }, "addressPrefixes": { - "value": "[parameters('vnetAddressPrefixes')]" + "value": "[parameters('addressPrefixes')]" }, "subnets": { "copy": [ @@ -7386,31 +7464,31 @@ "outputs": { "name": { "type": "string", - "value": "[reference(resourceId('Microsoft.Resources/deployments', take(format('avm.res.network.virtual-network.{0}', parameters('vnetName')), 64)), '2025-04-01').outputs.name.value]" + "value": "[reference(resourceId('Microsoft.Resources/deployments', take(format('avm.res.network.virtual-network.{0}', parameters('name')), 64)), '2025-04-01').outputs.name.value]" }, "resourceId": { "type": "string", - "value": "[reference(resourceId('Microsoft.Resources/deployments', take(format('avm.res.network.virtual-network.{0}', parameters('vnetName')), 64)), '2025-04-01').outputs.resourceId.value]" + "value": "[reference(resourceId('Microsoft.Resources/deployments', take(format('avm.res.network.virtual-network.{0}', parameters('name')), 64)), '2025-04-01').outputs.resourceId.value]" }, "webSubnetResourceId": { "type": "string", - "value": "[if(contains(map(variables('vnetSubnets'), lambda('subnet', lambdaVariables('subnet').name)), 'web'), reference(resourceId('Microsoft.Resources/deployments', take(format('avm.res.network.virtual-network.{0}', parameters('vnetName')), 64)), '2025-04-01').outputs.subnetResourceIds.value[indexOf(map(variables('vnetSubnets'), lambda('subnet', lambdaVariables('subnet').name)), 'web')], '')]" + "value": "[if(contains(map(variables('vnetSubnets'), lambda('subnet', lambdaVariables('subnet').name)), 'web'), reference(resourceId('Microsoft.Resources/deployments', take(format('avm.res.network.virtual-network.{0}', parameters('name')), 64)), '2025-04-01').outputs.subnetResourceIds.value[indexOf(map(variables('vnetSubnets'), lambda('subnet', lambdaVariables('subnet').name)), 'web')], '')]" }, "pepsSubnetResourceId": { "type": "string", - "value": "[if(contains(map(variables('vnetSubnets'), lambda('subnet', lambdaVariables('subnet').name)), 'peps'), reference(resourceId('Microsoft.Resources/deployments', take(format('avm.res.network.virtual-network.{0}', parameters('vnetName')), 64)), '2025-04-01').outputs.subnetResourceIds.value[indexOf(map(variables('vnetSubnets'), lambda('subnet', lambdaVariables('subnet').name)), 'peps')], '')]" + "value": "[if(contains(map(variables('vnetSubnets'), lambda('subnet', lambdaVariables('subnet').name)), 'peps'), reference(resourceId('Microsoft.Resources/deployments', take(format('avm.res.network.virtual-network.{0}', parameters('name')), 64)), '2025-04-01').outputs.subnetResourceIds.value[indexOf(map(variables('vnetSubnets'), lambda('subnet', lambdaVariables('subnet').name)), 'peps')], '')]" }, "aciSubnetResourceId": { "type": "string", - "value": "[if(contains(map(variables('vnetSubnets'), lambda('subnet', lambdaVariables('subnet').name)), 'aci'), reference(resourceId('Microsoft.Resources/deployments', take(format('avm.res.network.virtual-network.{0}', parameters('vnetName')), 64)), '2025-04-01').outputs.subnetResourceIds.value[indexOf(map(variables('vnetSubnets'), lambda('subnet', lambdaVariables('subnet').name)), 'aci')], '')]" + "value": "[if(contains(map(variables('vnetSubnets'), lambda('subnet', lambdaVariables('subnet').name)), 'aci'), reference(resourceId('Microsoft.Resources/deployments', take(format('avm.res.network.virtual-network.{0}', parameters('name')), 64)), '2025-04-01').outputs.subnetResourceIds.value[indexOf(map(variables('vnetSubnets'), lambda('subnet', lambdaVariables('subnet').name)), 'aci')], '')]" }, "bastionSubnetResourceId": { "type": "string", - "value": "[if(and(parameters('deployBastionAndJumpbox'), contains(map(variables('vnetSubnets'), lambda('subnet', lambdaVariables('subnet').name)), 'AzureBastionSubnet')), reference(resourceId('Microsoft.Resources/deployments', take(format('avm.res.network.virtual-network.{0}', parameters('vnetName')), 64)), '2025-04-01').outputs.subnetResourceIds.value[indexOf(map(variables('vnetSubnets'), lambda('subnet', lambdaVariables('subnet').name)), 'AzureBastionSubnet')], '')]" + "value": "[if(contains(map(variables('vnetSubnets'), lambda('subnet', lambdaVariables('subnet').name)), 'AzureBastionSubnet'), reference(resourceId('Microsoft.Resources/deployments', take(format('avm.res.network.virtual-network.{0}', parameters('name')), 64)), '2025-04-01').outputs.subnetResourceIds.value[indexOf(map(variables('vnetSubnets'), lambda('subnet', lambdaVariables('subnet').name)), 'AzureBastionSubnet')], '')]" }, "jumpboxSubnetResourceId": { "type": "string", - "value": "[if(and(parameters('deployBastionAndJumpbox'), contains(map(variables('vnetSubnets'), lambda('subnet', lambdaVariables('subnet').name)), 'jumpbox')), reference(resourceId('Microsoft.Resources/deployments', take(format('avm.res.network.virtual-network.{0}', parameters('vnetName')), 64)), '2025-04-01').outputs.subnetResourceIds.value[indexOf(map(variables('vnetSubnets'), lambda('subnet', lambdaVariables('subnet').name)), 'jumpbox')], '')]" + "value": "[if(contains(map(variables('vnetSubnets'), lambda('subnet', lambdaVariables('subnet').name)), 'jumpbox'), reference(resourceId('Microsoft.Resources/deployments', take(format('avm.res.network.virtual-network.{0}', parameters('name')), 64)), '2025-04-01').outputs.subnetResourceIds.value[indexOf(map(variables('vnetSubnets'), lambda('subnet', lambdaVariables('subnet').name)), 'jumpbox')], '')]" } } } @@ -9174,23 +9252,27 @@ "name": { "value": "[take(variables('jumpboxVmName'), 15)]" }, - "vmSize": { - "value": "[coalesce(parameters('vmSize'), 'Standard_D2s_v5')]" - }, - "location": { - "value": "[parameters('location')]" + "enableTelemetry": { + "value": "[parameters('enableTelemetry')]" }, - "adminUsername": { - "value": "[coalesce(parameters('vmAdminUsername'), 'JumpboxAdminUser')]" + "computerName": { + "value": "[take(variables('jumpboxVmName'), 15)]" }, - "adminPassword": { - "value": "[coalesce(parameters('vmAdminPassword'), 'JumpboxAdminP@ssw0rd1234!')]" + "osType": { + "value": "Windows" }, - "tags": { - "value": "[parameters('tags')]" + "vmSize": "[if(empty(parameters('vmSize')), createObject('value', 'Standard_D2s_v5'), createObject('value', parameters('vmSize')))]", + "adminUsername": "[if(empty(parameters('vmAdminUsername')), createObject('value', 'JumpboxAdminUser'), createObject('value', parameters('vmAdminUsername')))]", + "adminPassword": "[if(empty(parameters('vmAdminPassword')), createObject('value', 'JumpboxAdminP@ssw0rd1234!'), createObject('value', parameters('vmAdminPassword')))]", + "managedIdentities": { + "value": { + "userAssignedResourceIds": [ + "[reference('userAssignedIdentity').outputs.resourceId.value]" + ] + } }, "availabilityZone": { - "value": -1 + "value": 1 }, "imageReference": { "value": { @@ -9200,53 +9282,37 @@ "version": "latest" } }, - "osType": { - "value": "Windows" - }, - "osDisk": { - "value": { - "name": "[format('osdisk-{0}', variables('jumpboxVmName'))]", - "managedDisk": { - "storageAccountType": "Standard_LRS" - } - } - }, - "encryptionAtHost": { - "value": false - }, "nicConfigurations": { "value": [ { "name": "[format('nic-{0}', variables('jumpboxVmName'))]", + "enableAcceleratedNetworking": true, "ipConfigurations": [ { - "name": "ipconfig1", + "name": "ipconfig01", "subnetResourceId": "[reference('virtualNetwork').outputs.jumpboxSubnetResourceId.value]" } - ], - "diagnosticSettings": [ - { - "name": "jumpboxDiagnostics", - "workspaceResourceId": "[if(variables('useExistingLogAnalytics'), parameters('existingLogAnalyticsWorkspaceId'), if(parameters('enableMonitoring'), reference('logAnalyticsWorkspace').outputs.resourceId.value, ''))]", - "logCategoriesAndGroups": [ - { - "categoryGroup": "allLogs", - "enabled": true - } - ], - "metricCategories": [ - { - "category": "AllMetrics", - "enabled": true - } - ] - } ] } ] }, - "enableTelemetry": { - "value": "[parameters('enableTelemetry')]" + "osDisk": { + "value": { + "caching": "ReadWrite", + "diskSizeGB": 128, + "managedDisk": { + "storageAccountType": "Premium_LRS" + } + } + }, + "encryptionAtHost": { + "value": false + }, + "location": { + "value": "[variables('solutionLocation')]" + }, + "tags": { + "value": "[parameters('tags')]" } }, "template": { @@ -18293,6 +18359,7 @@ }, "dependsOn": [ "logAnalyticsWorkspace", + "userAssignedIdentity", "virtualNetwork" ] }, @@ -24920,7 +24987,7 @@ "_generator": { "name": "bicep", "version": "0.41.2.15936", - "templateHash": "14729448306021818204" + "templateHash": "5579055444657114163" } }, "parameters": { @@ -25058,7 +25125,7 @@ "_generator": { "name": "bicep", "version": "0.41.2.15936", - "templateHash": "9237732984597233866" + "templateHash": "4896504561894393634" } }, "parameters": { @@ -25100,7 +25167,7 @@ { "type": "Microsoft.Authorization/roleAssignments", "apiVersion": "2022-04-01", - "scope": "[format('Microsoft.CognitiveServices/accounts/{0}', parameters('aiServicesName'))]", + "scope": "[resourceId('Microsoft.CognitiveServices/accounts', parameters('aiServicesName'))]", "name": "[guid(resourceId('Microsoft.CognitiveServices/accounts', parameters('aiServicesName')), parameters('principalId'), resourceId('Microsoft.Authorization/roleDefinitions', '53ca6127-db72-4b80-b1b0-d745d6d5456d'))]", "properties": { "roleDefinitionId": "[resourceId('Microsoft.Authorization/roleDefinitions', '53ca6127-db72-4b80-b1b0-d745d6d5456d')]", @@ -25111,7 +25178,7 @@ { "type": "Microsoft.Authorization/roleAssignments", "apiVersion": "2022-04-01", - "scope": "[format('Microsoft.CognitiveServices/accounts/{0}', parameters('aiServicesName'))]", + "scope": "[resourceId('Microsoft.CognitiveServices/accounts', parameters('aiServicesName'))]", "name": "[guid(resourceId('Microsoft.CognitiveServices/accounts', parameters('aiServicesName')), parameters('principalId'), resourceId('Microsoft.Authorization/roleDefinitions', '5e0bd9bd-7b93-4f28-af87-19fc36ad61bd'))]", "properties": { "roleDefinitionId": "[resourceId('Microsoft.Authorization/roleDefinitions', '5e0bd9bd-7b93-4f28-af87-19fc36ad61bd')]", @@ -42026,7 +42093,7 @@ "_generator": { "name": "bicep", "version": "0.41.2.15936", - "templateHash": "17215033518306564965" + "templateHash": "8373975196748337393" } }, "definitions": { @@ -42976,7 +43043,7 @@ }, "type": "Microsoft.Insights/diagnosticSettings", "apiVersion": "2021-05-01-preview", - "scope": "[format('Microsoft.Web/sites/{0}', parameters('name'))]", + "scope": "[resourceId('Microsoft.Web/sites', parameters('name'))]", "name": "[coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'name'), format('{0}-diagnosticSettings', parameters('name')))]", "properties": { "copy": [ @@ -43055,7 +43122,7 @@ "_generator": { "name": "bicep", "version": "0.41.2.15936", - "templateHash": "26032048033705967" + "templateHash": "5518029515589119726" }, "name": "Site App Settings", "description": "This module deploys a Site App Setting." @@ -44076,7 +44143,7 @@ "_generator": { "name": "bicep", "version": "0.41.2.15936", - "templateHash": "5828006773867864982" + "templateHash": "11066122245438821615" } }, "parameters": { @@ -44148,9 +44215,8 @@ }, "userAssignedIdentityResourceId": { "type": "string", - "defaultValue": "", "metadata": { - "description": "Optional. User-assigned managed identity resource ID for ACR pull." + "description": "Required. User-assigned managed identity resource ID for ACR pull." } } }, From 461350482dddfb54e2eeba375262aa7493e27934 Mon Sep 17 00:00:00 2001 From: Pavan-Microsoft Date: Mon, 23 Mar 2026 21:14:19 +0530 Subject: [PATCH 16/72] Refactor environment variable names for Azure OpenAI settings - Updated environment variable names to follow a consistent naming convention, replacing `AZURE_OPENAI_*` with `AZURE_ENV_*` for clarity and consistency across the codebase. - Modified GitHub Actions workflow to reflect new environment variable names for Docker image tagging. - Adjusted deployment scripts and local development scripts to utilize the new environment variable names. - Updated documentation to reflect changes in environment variable names for Azure OpenAI configuration. - Ensured backward compatibility by adding aliases for the old variable names where necessary. --- .env.sample | 8 +- .github/workflows/azure-dev.yml | 2 +- .github/workflows/deploy-orchestrator.yml | 14 +-- .github/workflows/deploy-v2.yml | 48 ++++---- .github/workflows/job-cleanup-deployment.yml | 8 +- .github/workflows/job-deploy-linux.yml | 112 +++++++++---------- .github/workflows/job-deploy-windows.yml | 110 +++++++++--------- .github/workflows/job-deploy.yml | 86 +++++++------- .github/workflows/job-docker-build.yml | 28 ++--- azure.yaml | 6 +- docs/AZD_DEPLOYMENT.md | 6 +- docs/CustomizingAzdParameters.md | 34 +++--- docs/DEPLOYMENT.md | 2 +- docs/IMAGE_GENERATION.md | 12 +- docs/LOCAL_DEPLOYMENT.md | 12 +- docs/TECHNICAL_GUIDE.md | 4 +- infra/main.bicep | 18 +-- infra/main.json | 25 ++--- infra/main.parameters.json | 26 ++--- infra/main.waf.parameters.json | 26 ++--- scripts/deploy.ps1 | 4 +- scripts/deploy.sh | 20 ++-- scripts/local_dev.ps1 | 4 +- scripts/local_dev.sh | 4 +- scripts/sample_content_generation.py | 4 +- scripts/sample_image_generation.py | 2 +- src/backend/agents/image_content_agent.py | 2 +- src/backend/settings.py | 17 ++- src/tests/conftest.py | 2 +- src/tests/test_settings.py | 8 +- 30 files changed, 331 insertions(+), 323 deletions(-) diff --git a/.env.sample b/.env.sample index 5244d3c41..dad3be411 100644 --- a/.env.sample +++ b/.env.sample @@ -23,16 +23,16 @@ AZURE_AI_IMAGE_MODEL_DEPLOYMENT=gpt-image-1-mini # Azure OpenAI Configuration # ============================================================================= AI_FOUNDRY_RESOURCE_ID=/subscriptions/your-subscription-id/resourceGroups/your-resource-group/providers/Microsoft.CognitiveServices/accounts/your-aif-account -AZURE_EXISTING_AI_PROJECT_RESOURCE_ID=/subscriptions/your-subscription-id/resourceGroups/your-resource-group/providers/Microsoft.CognitiveServices/accounts/your-aif-account/projects/your-project-name +AZURE_ENV_FOUNDRY_PROJECT_RID=/subscriptions/your-subscription-id/resourceGroups/your-resource-group/providers/Microsoft.CognitiveServices/accounts/your-aif-account/projects/your-project-name # Your Azure OpenAI endpoint (e.g., https://your-resource.openai.azure.com/) AZURE_OPENAI_ENDPOINT=https://your-openai.openai.azure.com/ # Model deployments -AZURE_OPENAI_GPT_MODEL=gpt-5.1 +AZURE_ENV_GPT_MODEL_NAME=gpt-5.1 # Image Generation Model Configuration # Supported models: gpt-image-1-mini or gpt-image-1.5 -AZURE_OPENAI_IMAGE_MODEL=gpt-image-1-mini +AZURE_ENV_IMAGE_MODEL_NAME=gpt-image-1-mini # For gpt-image-1-mini or gpt-image-1.5, the endpoint is the same as the main OpenAI endpoint, but you can specify a different one if needed AZURE_OPENAI_GPT_IMAGE_ENDPOINT=https://your-openai.openai.azure.com @@ -43,7 +43,7 @@ AZURE_OPENAI_IMAGE_SIZE=1024x1024 AZURE_OPENAI_IMAGE_QUALITY=medium # API versions -AZURE_OPENAI_API_VERSION=2024-06-01 +AZURE_ENV_OPENAI_API_VERSION=2024-06-01 AZURE_OPENAI_PREVIEW_API_VERSION=2024-02-01 # Generation parameters diff --git a/.github/workflows/azure-dev.yml b/.github/workflows/azure-dev.yml index d2a5231f4..776dc799c 100644 --- a/.github/workflows/azure-dev.yml +++ b/.github/workflows/azure-dev.yml @@ -44,7 +44,7 @@ jobs: AZURE_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }} AZURE_ENV_NAME: ${{ secrets.AZURE_ENV_NAME }} AZURE_LOCATION: ${{ secrets.AZURE_LOCATION }} - AZURE_ENV_OPENAI_LOCATION: ${{ secrets.AZURE_ENV_OPENAI_LOCATION }} + AZURE_ENV_AI_SERVICE_LOCATION: ${{ secrets.AZURE_ENV_AI_SERVICE_LOCATION }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Step 4: Print the result of the validation diff --git a/.github/workflows/deploy-orchestrator.yml b/.github/workflows/deploy-orchestrator.yml index 069e2bc91..1fda89a54 100644 --- a/.github/workflows/deploy-orchestrator.yml +++ b/.github/workflows/deploy-orchestrator.yml @@ -42,12 +42,12 @@ on: required: false default: 'GoldenPath-Testing' type: string - AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID: + AZURE_ENV_LOG_ANALYTICS_WORKSPACE_RID: description: 'Log Analytics Workspace ID (Optional)' required: false default: '' type: string - AZURE_EXISTING_AI_PROJECT_RESOURCE_ID: + AZURE_ENV_FOUNDRY_PROJECT_RID: description: 'AI Project Resource ID (Optional)' required: false default: '' @@ -91,9 +91,9 @@ jobs: EXP: ${{ inputs.EXP }} build_docker_image: ${{ inputs.build_docker_image }} existing_webapp_url: ${{ inputs.existing_webapp_url }} - AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID: ${{ inputs.AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID }} - AZURE_EXISTING_AI_PROJECT_RESOURCE_ID: ${{ inputs.AZURE_EXISTING_AI_PROJECT_RESOURCE_ID }} - docker_image_tag: ${{ needs.docker-build.outputs.IMAGE_TAG }} + AZURE_ENV_LOG_ANALYTICS_WORKSPACE_RID: ${{ inputs.AZURE_ENV_LOG_ANALYTICS_WORKSPACE_RID }} + AZURE_ENV_FOUNDRY_PROJECT_RID: ${{ inputs.AZURE_ENV_FOUNDRY_PROJECT_RID }} + docker_image_tag: ${{ needs.docker-build.outputs.AZURE_ENV_IMAGE_TAG }} run_e2e_tests: ${{ inputs.run_e2e_tests }} cleanup_resources: ${{ inputs.cleanup_resources }} image_model_choice: ${{ inputs.image_model_choice }} @@ -138,7 +138,7 @@ jobs: existing_webapp_url: ${{ inputs.existing_webapp_url }} RESOURCE_GROUP_NAME: ${{ needs.deploy.outputs.RESOURCE_GROUP_NAME }} AZURE_LOCATION: ${{ needs.deploy.outputs.AZURE_LOCATION }} - AZURE_ENV_OPENAI_LOCATION: ${{ needs.deploy.outputs.AZURE_ENV_OPENAI_LOCATION }} + AZURE_ENV_AI_SERVICE_LOCATION: ${{ needs.deploy.outputs.AZURE_ENV_AI_SERVICE_LOCATION }} ENV_NAME: ${{ needs.deploy.outputs.ENV_NAME }} - IMAGE_TAG: ${{ needs.deploy.outputs.IMAGE_TAG }} + AZURE_ENV_IMAGE_TAG: ${{ needs.deploy.outputs.AZURE_ENV_IMAGE_TAG }} secrets: inherit diff --git a/.github/workflows/deploy-v2.yml b/.github/workflows/deploy-v2.yml index c6933c4c1..fe8572460 100644 --- a/.github/workflows/deploy-v2.yml +++ b/.github/workflows/deploy-v2.yml @@ -85,12 +85,12 @@ on: - 'Smoke-Testing' - 'None' - AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID: + AZURE_ENV_LOG_ANALYTICS_WORKSPACE_RID: description: 'Log Analytics Workspace ID (Optional)' required: false default: '' type: string - AZURE_EXISTING_AI_PROJECT_RESOURCE_ID: + AZURE_ENV_FOUNDRY_PROJECT_RID: description: 'AI Project Resource ID (Optional)' required: false default: '' @@ -130,8 +130,8 @@ jobs: build_docker_image: ${{ steps.validate.outputs.build_docker_image }} cleanup_resources: ${{ steps.validate.outputs.cleanup_resources }} run_e2e_tests: ${{ steps.validate.outputs.run_e2e_tests }} - azure_env_log_analytics_workspace_id: ${{ steps.validate.outputs.azure_env_log_analytics_workspace_id }} - azure_existing_ai_project_resource_id: ${{ steps.validate.outputs.azure_existing_ai_project_resource_id }} + AZURE_ENV_LOG_ANALYTICS_WORKSPACE_RID: ${{ steps.validate.outputs.AZURE_ENV_LOG_ANALYTICS_WORKSPACE_RID }} + AZURE_ENV_FOUNDRY_PROJECT_RID: ${{ steps.validate.outputs.AZURE_ENV_FOUNDRY_PROJECT_RID }} existing_webapp_url: ${{ steps.validate.outputs.existing_webapp_url }} image_model_choice: ${{ steps.validate.outputs.image_model_choice }} steps: @@ -147,8 +147,8 @@ jobs: INPUT_BUILD_DOCKER_IMAGE: ${{ github.event.inputs.build_docker_image }} INPUT_CLEANUP_RESOURCES: ${{ github.event.inputs.cleanup_resources }} INPUT_RUN_E2E_TESTS: ${{ github.event.inputs.run_e2e_tests }} - INPUT_AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID: ${{ github.event.inputs.AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID }} - INPUT_AZURE_EXISTING_AI_PROJECT_RESOURCE_ID: ${{ github.event.inputs.AZURE_EXISTING_AI_PROJECT_RESOURCE_ID }} + INPUT_AZURE_ENV_LOG_ANALYTICS_WORKSPACE_RID: ${{ github.event.inputs.AZURE_ENV_LOG_ANALYTICS_WORKSPACE_RID }} + INPUT_AZURE_ENV_FOUNDRY_PROJECT_RID: ${{ github.event.inputs.AZURE_ENV_FOUNDRY_PROJECT_RID }} INPUT_EXISTING_WEBAPP_URL: ${{ github.event.inputs.existing_webapp_url }} INPUT_IMAGE_MODEL_CHOICE: ${{ github.event.inputs.image_model_choice }} run: | @@ -242,32 +242,32 @@ jobs: echo "✅ run_e2e_tests: '$TEST_OPTION' is valid" fi - # Validate AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID (optional, Azure Resource ID format) - if [[ -n "$INPUT_AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID" ]]; then - if [[ ! "$INPUT_AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID" =~ ^/subscriptions/[a-fA-F0-9-]+/[Rr]esource[Gg]roups/[^/]+/providers/[Mm]icrosoft\.[Oo]perational[Ii]nsights/[Ww]orkspaces/[^/]+$ ]]; then - echo "❌ ERROR: AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID is invalid. Must be a valid Azure Resource ID format:" + # Validate AZURE_ENV_LOG_ANALYTICS_WORKSPACE_RID (optional, Azure Resource ID format) + if [[ -n "$INPUT_AZURE_ENV_LOG_ANALYTICS_WORKSPACE_RID" ]]; then + if [[ ! "$INPUT_AZURE_ENV_LOG_ANALYTICS_WORKSPACE_RID" =~ ^/subscriptions/[a-fA-F0-9-]+/[Rr]esource[Gg]roups/[^/]+/providers/[Mm]icrosoft\.[Oo]perational[Ii]nsights/[Ww]orkspaces/[^/]+$ ]]; then + echo "❌ ERROR: AZURE_ENV_LOG_ANALYTICS_WORKSPACE_RID is invalid. Must be a valid Azure Resource ID format:" echo " /subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.OperationalInsights/workspaces/{workspaceName}" - echo " Got: '$INPUT_AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID'" + echo " Got: '$INPUT_AZURE_ENV_LOG_ANALYTICS_WORKSPACE_RID'" VALIDATION_FAILED=true else - echo "✅ AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID: Valid Resource ID format" + echo "✅ AZURE_ENV_LOG_ANALYTICS_WORKSPACE_RID: Valid Resource ID format" fi else - echo "✅ AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID: Not provided (optional)" + echo "✅ AZURE_ENV_LOG_ANALYTICS_WORKSPACE_RID: Not provided (optional)" fi - # Validate AZURE_EXISTING_AI_PROJECT_RESOURCE_ID (optional, Azure Resource ID format) - if [[ -n "$INPUT_AZURE_EXISTING_AI_PROJECT_RESOURCE_ID" ]]; then - if [[ ! "$INPUT_AZURE_EXISTING_AI_PROJECT_RESOURCE_ID" =~ ^/subscriptions/[a-fA-F0-9-]+/[Rr]esource[Gg]roups/[^/]+/providers/([Mm]icrosoft\.[Mm]achine[Ll]earning[Ss]ervices/([Ww]orkspaces|[Pp]rojects)/[^/]+|[Mm]icrosoft\.[Cc]ognitive[Ss]ervices/[Aa]ccounts/[^/]+/[Pp]rojects/[^/]+)$ ]]; then - echo "❌ ERROR: AZURE_EXISTING_AI_PROJECT_RESOURCE_ID is invalid. Must be a valid Azure Resource ID format:" + # Validate AZURE_ENV_FOUNDRY_PROJECT_RID (optional, Azure Resource ID format) + if [[ -n "$INPUT_AZURE_ENV_FOUNDRY_PROJECT_RID" ]]; then + if [[ ! "$INPUT_AZURE_ENV_FOUNDRY_PROJECT_RID" =~ ^/subscriptions/[a-fA-F0-9-]+/[Rr]esource[Gg]roups/[^/]+/providers/([Mm]icrosoft\.[Mm]achine[Ll]earning[Ss]ervices/([Ww]orkspaces|[Pp]rojects)/[^/]+|[Mm]icrosoft\.[Cc]ognitive[Ss]ervices/[Aa]ccounts/[^/]+/[Pp]rojects/[^/]+)$ ]]; then + echo "❌ ERROR: AZURE_ENV_FOUNDRY_PROJECT_RID is invalid. Must be a valid Azure Resource ID format:" echo " /subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.CognitiveServices/accounts/{accountName}/projects/{projectName}" - echo " Got: '$INPUT_AZURE_EXISTING_AI_PROJECT_RESOURCE_ID'" + echo " Got: '$INPUT_AZURE_ENV_FOUNDRY_PROJECT_RID'" VALIDATION_FAILED=true else - echo "✅ AZURE_EXISTING_AI_PROJECT_RESOURCE_ID: Valid Resource ID format" + echo "✅ AZURE_ENV_FOUNDRY_PROJECT_RID: Valid Resource ID format" fi else - echo "✅ AZURE_EXISTING_AI_PROJECT_RESOURCE_ID: Not provided (optional)" + echo "✅ AZURE_ENV_FOUNDRY_PROJECT_RID: Not provided (optional)" fi # Validate existing_webapp_url (optional, must start with https) @@ -302,8 +302,8 @@ jobs: echo "build_docker_image=$BUILD_DOCKER" >> $GITHUB_OUTPUT echo "cleanup_resources=$CLEANUP_RESOURCES" >> $GITHUB_OUTPUT echo "run_e2e_tests=$TEST_OPTION" >> $GITHUB_OUTPUT - echo "azure_env_log_analytics_workspace_id=$INPUT_AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID" >> $GITHUB_OUTPUT - echo "azure_existing_ai_project_resource_id=$INPUT_AZURE_EXISTING_AI_PROJECT_RESOURCE_ID" >> $GITHUB_OUTPUT + echo "AZURE_ENV_LOG_ANALYTICS_WORKSPACE_RID=$INPUT_AZURE_ENV_LOG_ANALYTICS_WORKSPACE_RID" >> $GITHUB_OUTPUT + echo "AZURE_ENV_FOUNDRY_PROJECT_RID=$INPUT_AZURE_ENV_FOUNDRY_PROJECT_RID" >> $GITHUB_OUTPUT echo "existing_webapp_url=$INPUT_EXISTING_WEBAPP_URL" >> $GITHUB_OUTPUT # Validate and output image_model_choice @@ -329,8 +329,8 @@ jobs: build_docker_image: ${{ needs.validate-inputs.outputs.build_docker_image == 'true' }} cleanup_resources: ${{ needs.validate-inputs.outputs.cleanup_resources == 'true' }} run_e2e_tests: ${{ needs.validate-inputs.outputs.run_e2e_tests || 'GoldenPath-Testing' }} - AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID: ${{ needs.validate-inputs.outputs.azure_env_log_analytics_workspace_id || '' }} - AZURE_EXISTING_AI_PROJECT_RESOURCE_ID: ${{ needs.validate-inputs.outputs.azure_existing_ai_project_resource_id || '' }} + AZURE_ENV_LOG_ANALYTICS_WORKSPACE_RID: ${{ needs.validate-inputs.outputs.AZURE_ENV_LOG_ANALYTICS_WORKSPACE_RID || '' }} + AZURE_ENV_FOUNDRY_PROJECT_RID: ${{ needs.validate-inputs.outputs.AZURE_ENV_FOUNDRY_PROJECT_RID || '' }} existing_webapp_url: ${{ needs.validate-inputs.outputs.existing_webapp_url || '' }} trigger_type: ${{ github.event_name }} image_model_choice: ${{ needs.validate-inputs.outputs.image_model_choice || 'gpt-image-1-mini' }} diff --git a/.github/workflows/job-cleanup-deployment.yml b/.github/workflows/job-cleanup-deployment.yml index c06039378..de81dae14 100644 --- a/.github/workflows/job-cleanup-deployment.yml +++ b/.github/workflows/job-cleanup-deployment.yml @@ -28,7 +28,7 @@ on: description: 'Azure Location' required: true type: string - AZURE_ENV_OPENAI_LOCATION: + AZURE_ENV_AI_SERVICE_LOCATION: description: 'Azure OpenAI Location' required: true type: string @@ -36,7 +36,7 @@ on: description: 'Environment Name' required: true type: string - IMAGE_TAG: + AZURE_ENV_IMAGE_TAG: description: 'Docker Image Tag' required: true type: string @@ -49,9 +49,9 @@ jobs: env: RESOURCE_GROUP_NAME: ${{ inputs.RESOURCE_GROUP_NAME }} AZURE_LOCATION: ${{ inputs.AZURE_LOCATION }} - AZURE_ENV_OPENAI_LOCATION: ${{ inputs.AZURE_ENV_OPENAI_LOCATION }} + AZURE_ENV_AI_SERVICE_LOCATION: ${{ inputs.AZURE_ENV_AI_SERVICE_LOCATION }} ENV_NAME: ${{ inputs.ENV_NAME }} - IMAGE_TAG: ${{ inputs.IMAGE_TAG }} + AZURE_ENV_IMAGE_TAG: ${{ inputs.AZURE_ENV_IMAGE_TAG }} steps: - name: Login to Azure (OIDC) uses: azure/login@v2 diff --git a/.github/workflows/job-deploy-linux.yml b/.github/workflows/job-deploy-linux.yml index 6cfa7f910..a0bb3258a 100644 --- a/.github/workflows/job-deploy-linux.yml +++ b/.github/workflows/job-deploy-linux.yml @@ -6,7 +6,7 @@ on: ENV_NAME: required: true type: string - AZURE_ENV_OPENAI_LOCATION: + AZURE_ENV_AI_SERVICE_LOCATION: required: true type: string AZURE_LOCATION: @@ -15,7 +15,7 @@ on: RESOURCE_GROUP_NAME: required: true type: string - IMAGE_TAG: + AZURE_ENV_IMAGE_TAG: required: true type: string BUILD_DOCKER_IMAGE: @@ -28,10 +28,10 @@ on: required: false type: string default: 'false' - AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID: + AZURE_ENV_LOG_ANALYTICS_WORKSPACE_RID: required: false type: string - AZURE_EXISTING_AI_PROJECT_RESOURCE_ID: + AZURE_ENV_FOUNDRY_PROJECT_RID: required: false type: string outputs: @@ -52,15 +52,15 @@ jobs: shell: bash env: INPUT_ENV_NAME: ${{ inputs.ENV_NAME }} - INPUT_AZURE_ENV_OPENAI_LOCATION: ${{ inputs.AZURE_ENV_OPENAI_LOCATION }} + INPUT_AZURE_ENV_AI_SERVICE_LOCATION: ${{ inputs.AZURE_ENV_AI_SERVICE_LOCATION }} INPUT_AZURE_LOCATION: ${{ inputs.AZURE_LOCATION }} INPUT_RESOURCE_GROUP_NAME: ${{ inputs.RESOURCE_GROUP_NAME }} - INPUT_IMAGE_TAG: ${{ inputs.IMAGE_TAG }} + INPUT_AZURE_ENV_IMAGE_TAG: ${{ inputs.AZURE_ENV_IMAGE_TAG }} INPUT_BUILD_DOCKER_IMAGE: ${{ inputs.BUILD_DOCKER_IMAGE }} INPUT_EXP: ${{ inputs.EXP }} INPUT_WAF_ENABLED: ${{ inputs.WAF_ENABLED }} - INPUT_AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID: ${{ inputs.AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID }} - INPUT_AZURE_EXISTING_AI_PROJECT_RESOURCE_ID: ${{ inputs.AZURE_EXISTING_AI_PROJECT_RESOURCE_ID }} + INPUT_AZURE_ENV_LOG_ANALYTICS_WORKSPACE_RID: ${{ inputs.AZURE_ENV_LOG_ANALYTICS_WORKSPACE_RID }} + INPUT_AZURE_ENV_FOUNDRY_PROJECT_RID: ${{ inputs.AZURE_ENV_FOUNDRY_PROJECT_RID }} run: | echo "🔍 Validating workflow input parameters..." VALIDATION_FAILED=false @@ -76,15 +76,15 @@ jobs: echo "✅ ENV_NAME: '$INPUT_ENV_NAME' is valid" fi - # Validate AZURE_ENV_OPENAI_LOCATION (required, Azure region format) - if [[ -z "$INPUT_AZURE_ENV_OPENAI_LOCATION" ]]; then - echo "❌ ERROR: AZURE_ENV_OPENAI_LOCATION is required but not provided" + # Validate AZURE_ENV_AI_SERVICE_LOCATION (required, Azure region format) + if [[ -z "$INPUT_AZURE_ENV_AI_SERVICE_LOCATION" ]]; then + echo "❌ ERROR: AZURE_ENV_AI_SERVICE_LOCATION is required but not provided" VALIDATION_FAILED=true - elif [[ ! "$INPUT_AZURE_ENV_OPENAI_LOCATION" =~ ^[a-z0-9]+$ ]]; then - echo "❌ ERROR: AZURE_ENV_OPENAI_LOCATION '$INPUT_AZURE_ENV_OPENAI_LOCATION' is invalid. Must contain only lowercase letters and numbers" + elif [[ ! "$INPUT_AZURE_ENV_AI_SERVICE_LOCATION" =~ ^[a-z0-9]+$ ]]; then + echo "❌ ERROR: AZURE_ENV_AI_SERVICE_LOCATION '$INPUT_AZURE_ENV_AI_SERVICE_LOCATION' is invalid. Must contain only lowercase letters and numbers" VALIDATION_FAILED=true else - echo "✅ AZURE_ENV_OPENAI_LOCATION: '$INPUT_AZURE_ENV_OPENAI_LOCATION' is valid" + echo "✅ AZURE_ENV_AI_SERVICE_LOCATION: '$INPUT_AZURE_ENV_AI_SERVICE_LOCATION' is valid" fi # Validate AZURE_LOCATION (required, Azure region format) @@ -112,15 +112,15 @@ jobs: echo "✅ RESOURCE_GROUP_NAME: '$INPUT_RESOURCE_GROUP_NAME' is valid" fi - # Validate IMAGE_TAG (required, Docker tag pattern) - if [[ -z "$INPUT_IMAGE_TAG" ]]; then - echo "❌ ERROR: IMAGE_TAG is required but not provided" + # Validate AZURE_ENV_IMAGE_TAG (required, Docker tag pattern) + if [[ -z "$INPUT_AZURE_ENV_IMAGE_TAG" ]]; then + echo "❌ ERROR: AZURE_ENV_IMAGE_TAG is required but not provided" VALIDATION_FAILED=true - elif [[ ! "$INPUT_IMAGE_TAG" =~ ^[a-zA-Z0-9_][a-zA-Z0-9._-]{0,127}$ ]]; then - echo "❌ ERROR: IMAGE_TAG '$INPUT_IMAGE_TAG' is invalid. Must start with alphanumeric or underscore, max 128 characters" + elif [[ ! "$INPUT_AZURE_ENV_IMAGE_TAG" =~ ^[a-zA-Z0-9_][a-zA-Z0-9._-]{0,127}$ ]]; then + echo "❌ ERROR: AZURE_ENV_IMAGE_TAG '$INPUT_AZURE_ENV_IMAGE_TAG' is invalid. Must start with alphanumeric or underscore, max 128 characters" VALIDATION_FAILED=true else - echo "✅ IMAGE_TAG: '$INPUT_IMAGE_TAG' is valid" + echo "✅ AZURE_ENV_IMAGE_TAG: '$INPUT_AZURE_ENV_IMAGE_TAG' is valid" fi # Validate BUILD_DOCKER_IMAGE (required, must be 'true' or 'false') @@ -147,27 +147,27 @@ jobs: echo "✅ WAF_ENABLED: '$INPUT_WAF_ENABLED' is valid" fi - # Validate AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID (optional, if provided must be valid Resource ID) - if [[ -n "$INPUT_AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID" ]]; then - if [[ ! "$INPUT_AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID" =~ ^/subscriptions/[a-fA-F0-9-]+/[Rr]esource[Gg]roups/[^/]+/providers/[Mm]icrosoft\.[Oo]perational[Ii]nsights/[Ww]orkspaces/[^/]+$ ]]; then - echo "❌ ERROR: AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID is invalid. Must be a valid Azure Resource ID format:" + # Validate AZURE_ENV_LOG_ANALYTICS_WORKSPACE_RID (optional, if provided must be valid Resource ID) + if [[ -n "$INPUT_AZURE_ENV_LOG_ANALYTICS_WORKSPACE_RID" ]]; then + if [[ ! "$INPUT_AZURE_ENV_LOG_ANALYTICS_WORKSPACE_RID" =~ ^/subscriptions/[a-fA-F0-9-]+/[Rr]esource[Gg]roups/[^/]+/providers/[Mm]icrosoft\.[Oo]perational[Ii]nsights/[Ww]orkspaces/[^/]+$ ]]; then + echo "❌ ERROR: AZURE_ENV_LOG_ANALYTICS_WORKSPACE_RID is invalid. Must be a valid Azure Resource ID format:" echo " /subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.OperationalInsights/workspaces/{workspaceName}" - echo " Got: '$INPUT_AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID'" + echo " Got: '$INPUT_AZURE_ENV_LOG_ANALYTICS_WORKSPACE_RID'" VALIDATION_FAILED=true else - echo "✅ AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID: Valid Resource ID format" + echo "✅ AZURE_ENV_LOG_ANALYTICS_WORKSPACE_RID: Valid Resource ID format" fi fi - # Validate AZURE_EXISTING_AI_PROJECT_RESOURCE_ID (optional, if provided must be valid Resource ID) - if [[ -n "$INPUT_AZURE_EXISTING_AI_PROJECT_RESOURCE_ID" ]]; then - if [[ ! "$INPUT_AZURE_EXISTING_AI_PROJECT_RESOURCE_ID" =~ ^/subscriptions/[a-fA-F0-9-]+/[Rr]esource[Gg]roups/[^/]+/providers/([Mm]icrosoft\.[Mm]achine[Ll]earning[Ss]ervices/([Ww]orkspaces|[Pp]rojects)/[^/]+|[Mm]icrosoft\.[Cc]ognitive[Ss]ervices/[Aa]ccounts/[^/]+/[Pp]rojects/[^/]+)$ ]]; then - echo "❌ ERROR: AZURE_EXISTING_AI_PROJECT_RESOURCE_ID is invalid. Must be a valid Azure Resource ID format:" + # Validate AZURE_ENV_FOUNDRY_PROJECT_RID (optional, if provided must be valid Resource ID) + if [[ -n "$INPUT_AZURE_ENV_FOUNDRY_PROJECT_RID" ]]; then + if [[ ! "$INPUT_AZURE_ENV_FOUNDRY_PROJECT_RID" =~ ^/subscriptions/[a-fA-F0-9-]+/[Rr]esource[Gg]roups/[^/]+/providers/([Mm]icrosoft\.[Mm]achine[Ll]earning[Ss]ervices/([Ww]orkspaces|[Pp]rojects)/[^/]+|[Mm]icrosoft\.[Cc]ognitive[Ss]ervices/[Aa]ccounts/[^/]+/[Pp]rojects/[^/]+)$ ]]; then + echo "❌ ERROR: AZURE_ENV_FOUNDRY_PROJECT_RID is invalid. Must be a valid Azure Resource ID format:" echo " /subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.CognitiveServices/accounts/{accountName}/projects/{projectName}" - echo " Got: '$INPUT_AZURE_EXISTING_AI_PROJECT_RESOURCE_ID'" + echo " Got: '$INPUT_AZURE_ENV_FOUNDRY_PROJECT_RID'" VALIDATION_FAILED=true else - echo "✅ AZURE_EXISTING_AI_PROJECT_RESOURCE_ID: Valid Resource ID format" + echo "✅ AZURE_ENV_FOUNDRY_PROJECT_RID: Valid Resource ID format" fi fi @@ -215,14 +215,14 @@ jobs: shell: bash env: ENV_NAME: ${{ inputs.ENV_NAME }} - AZURE_ENV_OPENAI_LOCATION: ${{ inputs.AZURE_ENV_OPENAI_LOCATION }} + AZURE_ENV_AI_SERVICE_LOCATION: ${{ inputs.AZURE_ENV_AI_SERVICE_LOCATION }} AZURE_LOCATION: ${{ inputs.AZURE_LOCATION }} RESOURCE_GROUP_NAME: ${{ inputs.RESOURCE_GROUP_NAME }} - IMAGE_TAG: ${{ inputs.IMAGE_TAG }} + AZURE_ENV_IMAGE_TAG: ${{ inputs.AZURE_ENV_IMAGE_TAG }} BUILD_DOCKER_IMAGE: ${{ inputs.BUILD_DOCKER_IMAGE }} EXP: ${{ inputs.EXP }} - INPUT_AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID: ${{ inputs.AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID }} - INPUT_AZURE_EXISTING_AI_PROJECT_RESOURCE_ID: ${{ inputs.AZURE_EXISTING_AI_PROJECT_RESOURCE_ID }} + INPUT_AZURE_ENV_LOG_ANALYTICS_WORKSPACE_RID: ${{ inputs.AZURE_ENV_LOG_ANALYTICS_WORKSPACE_RID }} + INPUT_AZURE_ENV_FOUNDRY_PROJECT_RID: ${{ inputs.AZURE_ENV_FOUNDRY_PROJECT_RID }} run: | set -e @@ -235,17 +235,17 @@ jobs: # Set additional parameters azd env set AZURE_SUBSCRIPTION_ID="${{ secrets.AZURE_SUBSCRIPTION_ID }}" - azd env set AZURE_ENV_OPENAI_LOCATION="$AZURE_ENV_OPENAI_LOCATION" + azd env set AZURE_ENV_AI_SERVICE_LOCATION="$AZURE_ENV_AI_SERVICE_LOCATION" azd env set AZURE_LOCATION="$AZURE_LOCATION" azd env set AZURE_RESOURCE_GROUP="$RESOURCE_GROUP_NAME" - azd env set IMAGE_TAG="$IMAGE_TAG" + azd env set AZURE_ENV_IMAGE_TAG="$AZURE_ENV_IMAGE_TAG" # Set ACR name only when building Docker image if [[ "$BUILD_DOCKER_IMAGE" == "true" ]]; then # Extract ACR name from login server and set as environment variable - ACR_NAME=$(echo "${{ secrets.ACR_TEST_LOGIN_SERVER }}" | cut -d'.' -f1) - azd env set ACR_NAME="$ACR_NAME" - echo "Set ACR name to: $ACR_NAME" + AZURE_ENV_CONTAINER_REGISTRY_NAME=$(echo "${{ secrets.ACR_TEST_LOGIN_SERVER }}" | cut -d'.' -f1) + azd env set AZURE_ENV_CONTAINER_REGISTRY_NAME="$AZURE_ENV_CONTAINER_REGISTRY_NAME" + echo "Set ACR name to: $AZURE_ENV_CONTAINER_REGISTRY_NAME" else echo "Skipping ACR name configuration (using existing image)" fi @@ -254,25 +254,25 @@ jobs: echo "✅ EXP ENABLED - Setting EXP parameters..." # Set EXP variables dynamically - if [[ -n "$INPUT_AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID" ]]; then - EXP_LOG_ANALYTICS_ID="$INPUT_AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID" + if [[ -n "$INPUT_AZURE_ENV_LOG_ANALYTICS_WORKSPACE_RID" ]]; then + EXP_LOG_ANALYTICS_ID="$INPUT_AZURE_ENV_LOG_ANALYTICS_WORKSPACE_RID" else - EXP_LOG_ANALYTICS_ID="${{ secrets.AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID }}" + EXP_LOG_ANALYTICS_ID="${{ secrets.AZURE_ENV_LOG_ANALYTICS_WORKSPACE_RID }}" fi - if [[ -n "$INPUT_AZURE_EXISTING_AI_PROJECT_RESOURCE_ID" ]]; then - EXP_AI_PROJECT_ID="$INPUT_AZURE_EXISTING_AI_PROJECT_RESOURCE_ID" + if [[ -n "$INPUT_AZURE_ENV_FOUNDRY_PROJECT_RID" ]]; then + EXP_AI_PROJECT_ID="$INPUT_AZURE_ENV_FOUNDRY_PROJECT_RID" else - EXP_AI_PROJECT_ID="${{ secrets.AZURE_EXISTING_AI_PROJECT_RESOURCE_ID }}" + EXP_AI_PROJECT_ID="${{ secrets.AZURE_ENV_FOUNDRY_PROJECT_RID }}" fi - echo "AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID: $EXP_LOG_ANALYTICS_ID" - echo "AZURE_EXISTING_AI_PROJECT_RESOURCE_ID: $EXP_AI_PROJECT_ID" - azd env set AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID="$EXP_LOG_ANALYTICS_ID" - azd env set AZURE_EXISTING_AI_PROJECT_RESOURCE_ID="$EXP_AI_PROJECT_ID" + echo "AZURE_ENV_LOG_ANALYTICS_WORKSPACE_RID: $EXP_LOG_ANALYTICS_ID" + echo "AZURE_ENV_FOUNDRY_PROJECT_RID: $EXP_AI_PROJECT_ID" + azd env set AZURE_ENV_LOG_ANALYTICS_WORKSPACE_RID="$EXP_LOG_ANALYTICS_ID" + azd env set AZURE_ENV_FOUNDRY_PROJECT_RID="$EXP_AI_PROJECT_ID" else echo "❌ EXP DISABLED - Skipping EXP parameters" - if [[ -n "$INPUT_AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID" ]] || [[ -n "$INPUT_AZURE_EXISTING_AI_PROJECT_RESOURCE_ID" ]]; then + if [[ -n "$INPUT_AZURE_ENV_LOG_ANALYTICS_WORKSPACE_RID" ]] || [[ -n "$INPUT_AZURE_ENV_FOUNDRY_PROJECT_RID" ]]; then echo "⚠️ Warning: EXP parameter values provided but EXP is disabled. These values will be ignored." fi fi @@ -292,8 +292,8 @@ jobs: WAF_ENABLED: ${{ inputs.WAF_ENABLED }} EXP: ${{ inputs.EXP }} AZURE_LOCATION: ${{ inputs.AZURE_LOCATION }} - AZURE_ENV_OPENAI_LOCATION: ${{ inputs.AZURE_ENV_OPENAI_LOCATION }} - IMAGE_TAG: ${{ inputs.IMAGE_TAG }} + AZURE_ENV_AI_SERVICE_LOCATION: ${{ inputs.AZURE_ENV_AI_SERVICE_LOCATION }} + AZURE_ENV_IMAGE_TAG: ${{ inputs.AZURE_ENV_IMAGE_TAG }} JOB_STATUS: ${{ job.status }} WEB_APP_URL: ${{ steps.get_output_linux.outputs.WEB_APP_URL }} run: | @@ -323,8 +323,8 @@ jobs: echo "| **Configuration Type** | \`$CONFIG_TYPE\` |" >> $GITHUB_STEP_SUMMARY echo "| **Azure Region (Infrastructure)** | \`$AZURE_LOCATION\` |" >> $GITHUB_STEP_SUMMARY - echo "| **Azure OpenAI Region** | \`$AZURE_ENV_OPENAI_LOCATION\` |" >> $GITHUB_STEP_SUMMARY - echo "| **Docker Image Tag** | \`$IMAGE_TAG\` |" >> $GITHUB_STEP_SUMMARY + echo "| **Azure OpenAI Region** | \`$AZURE_ENV_AI_SERVICE_LOCATION\` |" >> $GITHUB_STEP_SUMMARY + echo "| **Docker Image Tag** | \`$AZURE_ENV_IMAGE_TAG\` |" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY if [[ "$JOB_STATUS" == "success" ]]; then diff --git a/.github/workflows/job-deploy-windows.yml b/.github/workflows/job-deploy-windows.yml index ed8cf5a8a..e42320bb7 100644 --- a/.github/workflows/job-deploy-windows.yml +++ b/.github/workflows/job-deploy-windows.yml @@ -6,7 +6,7 @@ on: ENV_NAME: required: true type: string - AZURE_ENV_OPENAI_LOCATION: + AZURE_ENV_AI_SERVICE_LOCATION: required: true type: string AZURE_LOCATION: @@ -15,7 +15,7 @@ on: RESOURCE_GROUP_NAME: required: true type: string - IMAGE_TAG: + AZURE_ENV_IMAGE_TAG: required: true type: string BUILD_DOCKER_IMAGE: @@ -28,10 +28,10 @@ on: required: false type: string default: 'false' - AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID: + AZURE_ENV_LOG_ANALYTICS_WORKSPACE_RID: required: false type: string - AZURE_EXISTING_AI_PROJECT_RESOURCE_ID: + AZURE_ENV_FOUNDRY_PROJECT_RID: required: false type: string outputs: @@ -53,15 +53,15 @@ jobs: shell: bash env: INPUT_ENV_NAME: ${{ inputs.ENV_NAME }} - INPUT_AZURE_ENV_OPENAI_LOCATION: ${{ inputs.AZURE_ENV_OPENAI_LOCATION }} + INPUT_AZURE_ENV_AI_SERVICE_LOCATION: ${{ inputs.AZURE_ENV_AI_SERVICE_LOCATION }} INPUT_AZURE_LOCATION: ${{ inputs.AZURE_LOCATION }} INPUT_RESOURCE_GROUP_NAME: ${{ inputs.RESOURCE_GROUP_NAME }} - INPUT_IMAGE_TAG: ${{ inputs.IMAGE_TAG }} + INPUT_AZURE_ENV_IMAGE_TAG: ${{ inputs.AZURE_ENV_IMAGE_TAG }} INPUT_BUILD_DOCKER_IMAGE: ${{ inputs.BUILD_DOCKER_IMAGE }} INPUT_EXP: ${{ inputs.EXP }} INPUT_WAF_ENABLED: ${{ inputs.WAF_ENABLED }} - INPUT_AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID: ${{ inputs.AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID }} - INPUT_AZURE_EXISTING_AI_PROJECT_RESOURCE_ID: ${{ inputs.AZURE_EXISTING_AI_PROJECT_RESOURCE_ID }} + INPUT_AZURE_ENV_LOG_ANALYTICS_WORKSPACE_RID: ${{ inputs.AZURE_ENV_LOG_ANALYTICS_WORKSPACE_RID }} + INPUT_AZURE_ENV_FOUNDRY_PROJECT_RID: ${{ inputs.AZURE_ENV_FOUNDRY_PROJECT_RID }} run: | echo "🔍 Validating workflow input parameters..." VALIDATION_FAILED=false @@ -77,15 +77,15 @@ jobs: echo "✅ ENV_NAME: '$INPUT_ENV_NAME' is valid" fi - # Validate AZURE_ENV_OPENAI_LOCATION (required, Azure region format) - if [[ -z "$INPUT_AZURE_ENV_OPENAI_LOCATION" ]]; then - echo "❌ ERROR: AZURE_ENV_OPENAI_LOCATION is required but not provided" + # Validate AZURE_ENV_AI_SERVICE_LOCATION (required, Azure region format) + if [[ -z "$INPUT_AZURE_ENV_AI_SERVICE_LOCATION" ]]; then + echo "❌ ERROR: AZURE_ENV_AI_SERVICE_LOCATION is required but not provided" VALIDATION_FAILED=true - elif [[ ! "$INPUT_AZURE_ENV_OPENAI_LOCATION" =~ ^[a-z0-9]+$ ]]; then - echo "❌ ERROR: AZURE_ENV_OPENAI_LOCATION '$INPUT_AZURE_ENV_OPENAI_LOCATION' is invalid. Must contain only lowercase letters and numbers" + elif [[ ! "$INPUT_AZURE_ENV_AI_SERVICE_LOCATION" =~ ^[a-z0-9]+$ ]]; then + echo "❌ ERROR: AZURE_ENV_AI_SERVICE_LOCATION '$INPUT_AZURE_ENV_AI_SERVICE_LOCATION' is invalid. Must contain only lowercase letters and numbers" VALIDATION_FAILED=true else - echo "✅ AZURE_ENV_OPENAI_LOCATION: '$INPUT_AZURE_ENV_OPENAI_LOCATION' is valid" + echo "✅ AZURE_ENV_AI_SERVICE_LOCATION: '$INPUT_AZURE_ENV_AI_SERVICE_LOCATION' is valid" fi # Validate AZURE_LOCATION (required, Azure region format) @@ -113,15 +113,15 @@ jobs: echo "✅ RESOURCE_GROUP_NAME: '$INPUT_RESOURCE_GROUP_NAME' is valid" fi - # Validate IMAGE_TAG (required, Docker tag pattern) - if [[ -z "$INPUT_IMAGE_TAG" ]]; then - echo "❌ ERROR: IMAGE_TAG is required but not provided" + # Validate AZURE_ENV_IMAGE_TAG (required, Docker tag pattern) + if [[ -z "$INPUT_AZURE_ENV_IMAGE_TAG" ]]; then + echo "❌ ERROR: AZURE_ENV_IMAGE_TAG is required but not provided" VALIDATION_FAILED=true - elif [[ ! "$INPUT_IMAGE_TAG" =~ ^[a-zA-Z0-9_][a-zA-Z0-9._-]{0,127}$ ]]; then - echo "❌ ERROR: IMAGE_TAG '$INPUT_IMAGE_TAG' is invalid. Must start with alphanumeric or underscore, max 128 characters" + elif [[ ! "$INPUT_AZURE_ENV_IMAGE_TAG" =~ ^[a-zA-Z0-9_][a-zA-Z0-9._-]{0,127}$ ]]; then + echo "❌ ERROR: AZURE_ENV_IMAGE_TAG '$INPUT_AZURE_ENV_IMAGE_TAG' is invalid. Must start with alphanumeric or underscore, max 128 characters" VALIDATION_FAILED=true else - echo "✅ IMAGE_TAG: '$INPUT_IMAGE_TAG' is valid" + echo "✅ AZURE_ENV_IMAGE_TAG: '$INPUT_AZURE_ENV_IMAGE_TAG' is valid" fi # Validate BUILD_DOCKER_IMAGE (required, must be 'true' or 'false') @@ -148,27 +148,27 @@ jobs: echo "✅ WAF_ENABLED: '$INPUT_WAF_ENABLED' is valid" fi - # Validate AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID (optional, if provided must be valid Resource ID) - if [[ -n "$INPUT_AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID" ]]; then - if [[ ! "$INPUT_AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID" =~ ^/subscriptions/[a-fA-F0-9-]+/[Rr]esource[Gg]roups/[^/]+/providers/[Mm]icrosoft\.[Oo]perational[Ii]nsights/[Ww]orkspaces/[^/]+$ ]]; then - echo "❌ ERROR: AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID is invalid. Must be a valid Azure Resource ID format:" + # Validate AZURE_ENV_LOG_ANALYTICS_WORKSPACE_RID (optional, if provided must be valid Resource ID) + if [[ -n "$INPUT_AZURE_ENV_LOG_ANALYTICS_WORKSPACE_RID" ]]; then + if [[ ! "$INPUT_AZURE_ENV_LOG_ANALYTICS_WORKSPACE_RID" =~ ^/subscriptions/[a-fA-F0-9-]+/[Rr]esource[Gg]roups/[^/]+/providers/[Mm]icrosoft\.[Oo]perational[Ii]nsights/[Ww]orkspaces/[^/]+$ ]]; then + echo "❌ ERROR: AZURE_ENV_LOG_ANALYTICS_WORKSPACE_RID is invalid. Must be a valid Azure Resource ID format:" echo " /subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.OperationalInsights/workspaces/{workspaceName}" - echo " Got: '$INPUT_AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID'" + echo " Got: '$INPUT_AZURE_ENV_LOG_ANALYTICS_WORKSPACE_RID'" VALIDATION_FAILED=true else - echo "✅ AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID: Valid Resource ID format" + echo "✅ AZURE_ENV_LOG_ANALYTICS_WORKSPACE_RID: Valid Resource ID format" fi fi - # Validate AZURE_EXISTING_AI_PROJECT_RESOURCE_ID (optional, if provided must be valid Resource ID) - if [[ -n "$INPUT_AZURE_EXISTING_AI_PROJECT_RESOURCE_ID" ]]; then - if [[ ! "$INPUT_AZURE_EXISTING_AI_PROJECT_RESOURCE_ID" =~ ^/subscriptions/[a-fA-F0-9-]+/[Rr]esource[Gg]roups/[^/]+/providers/([Mm]icrosoft\.[Mm]achine[Ll]earning[Ss]ervices/([Ww]orkspaces|[Pp]rojects)/[^/]+|[Mm]icrosoft\.[Cc]ognitive[Ss]ervices/[Aa]ccounts/[^/]+/[Pp]rojects/[^/]+)$ ]]; then - echo "❌ ERROR: AZURE_EXISTING_AI_PROJECT_RESOURCE_ID is invalid. Must be a valid Azure Resource ID format:" + # Validate AZURE_ENV_FOUNDRY_PROJECT_RID (optional, if provided must be valid Resource ID) + if [[ -n "$INPUT_AZURE_ENV_FOUNDRY_PROJECT_RID" ]]; then + if [[ ! "$INPUT_AZURE_ENV_FOUNDRY_PROJECT_RID" =~ ^/subscriptions/[a-fA-F0-9-]+/[Rr]esource[Gg]roups/[^/]+/providers/([Mm]icrosoft\.[Mm]achine[Ll]earning[Ss]ervices/([Ww]orkspaces|[Pp]rojects)/[^/]+|[Mm]icrosoft\.[Cc]ognitive[Ss]ervices/[Aa]ccounts/[^/]+/[Pp]rojects/[^/]+)$ ]]; then + echo "❌ ERROR: AZURE_ENV_FOUNDRY_PROJECT_RID is invalid. Must be a valid Azure Resource ID format:" echo " /subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.CognitiveServices/accounts/{accountName}/projects/{projectName}" - echo " Got: '$INPUT_AZURE_EXISTING_AI_PROJECT_RESOURCE_ID'" + echo " Got: '$INPUT_AZURE_ENV_FOUNDRY_PROJECT_RID'" VALIDATION_FAILED=true else - echo "✅ AZURE_EXISTING_AI_PROJECT_RESOURCE_ID: Valid Resource ID format" + echo "✅ AZURE_ENV_FOUNDRY_PROJECT_RID: Valid Resource ID format" fi fi @@ -218,14 +218,14 @@ jobs: shell: pwsh env: ENV_NAME: ${{ inputs.ENV_NAME }} - AZURE_ENV_OPENAI_LOCATION: ${{ inputs.AZURE_ENV_OPENAI_LOCATION }} + AZURE_ENV_AI_SERVICE_LOCATION: ${{ inputs.AZURE_ENV_AI_SERVICE_LOCATION }} AZURE_LOCATION: ${{ inputs.AZURE_LOCATION }} RESOURCE_GROUP_NAME: ${{ inputs.RESOURCE_GROUP_NAME }} - IMAGE_TAG: ${{ inputs.IMAGE_TAG }} + AZURE_ENV_IMAGE_TAG: ${{ inputs.AZURE_ENV_IMAGE_TAG }} BUILD_DOCKER_IMAGE: ${{ inputs.BUILD_DOCKER_IMAGE }} EXP: ${{ inputs.EXP }} - INPUT_AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID: ${{ inputs.AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID }} - INPUT_AZURE_EXISTING_AI_PROJECT_RESOURCE_ID: ${{ inputs.AZURE_EXISTING_AI_PROJECT_RESOURCE_ID }} + INPUT_AZURE_ENV_LOG_ANALYTICS_WORKSPACE_RID: ${{ inputs.AZURE_ENV_LOG_ANALYTICS_WORKSPACE_RID }} + INPUT_AZURE_ENV_FOUNDRY_PROJECT_RID: ${{ inputs.AZURE_ENV_FOUNDRY_PROJECT_RID }} run: | $ErrorActionPreference = "Stop" @@ -238,17 +238,17 @@ jobs: # Set additional parameters azd env set AZURE_SUBSCRIPTION_ID="${{ secrets.AZURE_SUBSCRIPTION_ID }}" - azd env set AZURE_ENV_OPENAI_LOCATION="$env:AZURE_ENV_OPENAI_LOCATION" + azd env set AZURE_ENV_AI_SERVICE_LOCATION="$env:AZURE_ENV_AI_SERVICE_LOCATION" azd env set AZURE_LOCATION="$env:AZURE_LOCATION" azd env set AZURE_RESOURCE_GROUP="$env:RESOURCE_GROUP_NAME" - azd env set IMAGE_TAG="$env:IMAGE_TAG" + azd env set AZURE_ENV_IMAGE_TAG="$env:AZURE_ENV_IMAGE_TAG" # Set ACR name only when building Docker image if ($env:BUILD_DOCKER_IMAGE -eq "true") { # Extract ACR name from login server (e.g., myacr.azurecr.io -> myacr) - $ACR_NAME = ("${{ secrets.ACR_TEST_LOGIN_SERVER }}").Split('.')[0] - azd env set ACR_NAME="$ACR_NAME" - Write-Host "Set ACR name to: $ACR_NAME" + $AZURE_ENV_CONTAINER_REGISTRY_NAME = ("${{ secrets.ACR_TEST_LOGIN_SERVER }}").Split('.')[0] + azd env set AZURE_ENV_CONTAINER_REGISTRY_NAME="$AZURE_ENV_CONTAINER_REGISTRY_NAME" + Write-Host "Set ACR name to: $AZURE_ENV_CONTAINER_REGISTRY_NAME" } else { Write-Host "Skipping ACR name configuration (using existing image)" } @@ -257,22 +257,22 @@ jobs: Write-Host "✅ EXP ENABLED - Setting EXP parameters..." # Set EXP variables dynamically - if ($env:INPUT_AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID -ne "") { - $EXP_LOG_ANALYTICS_ID = $env:INPUT_AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID + if ($env:INPUT_AZURE_ENV_LOG_ANALYTICS_WORKSPACE_RID -ne "") { + $EXP_LOG_ANALYTICS_ID = $env:INPUT_AZURE_ENV_LOG_ANALYTICS_WORKSPACE_RID } else { - $EXP_LOG_ANALYTICS_ID = "${{ secrets.AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID }}" + $EXP_LOG_ANALYTICS_ID = "${{ secrets.AZURE_ENV_LOG_ANALYTICS_WORKSPACE_RID }}" } - if ($env:INPUT_AZURE_EXISTING_AI_PROJECT_RESOURCE_ID -ne "") { - $EXP_AI_PROJECT_ID = $env:INPUT_AZURE_EXISTING_AI_PROJECT_RESOURCE_ID + if ($env:INPUT_AZURE_ENV_FOUNDRY_PROJECT_RID -ne "") { + $EXP_AI_PROJECT_ID = $env:INPUT_AZURE_ENV_FOUNDRY_PROJECT_RID } else { - $EXP_AI_PROJECT_ID = "${{ secrets.AZURE_EXISTING_AI_PROJECT_RESOURCE_ID }}" + $EXP_AI_PROJECT_ID = "${{ secrets.AZURE_ENV_FOUNDRY_PROJECT_RID }}" } - Write-Host "AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID: $EXP_LOG_ANALYTICS_ID" - Write-Host "AZURE_EXISTING_AI_PROJECT_RESOURCE_ID: $EXP_AI_PROJECT_ID" - azd env set AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID="$EXP_LOG_ANALYTICS_ID" - azd env set AZURE_EXISTING_AI_PROJECT_RESOURCE_ID="$EXP_AI_PROJECT_ID" + Write-Host "AZURE_ENV_LOG_ANALYTICS_WORKSPACE_RID: $EXP_LOG_ANALYTICS_ID" + Write-Host "AZURE_ENV_FOUNDRY_PROJECT_RID: $EXP_AI_PROJECT_ID" + azd env set AZURE_ENV_LOG_ANALYTICS_WORKSPACE_RID="$EXP_LOG_ANALYTICS_ID" + azd env set AZURE_ENV_FOUNDRY_PROJECT_RID="$EXP_AI_PROJECT_ID" } else { Write-Host "❌ EXP DISABLED - Skipping EXP parameters" } @@ -296,8 +296,8 @@ jobs: WAF_ENABLED: ${{ inputs.WAF_ENABLED }} EXP: ${{ inputs.EXP }} AZURE_LOCATION: ${{ inputs.AZURE_LOCATION }} - AZURE_ENV_OPENAI_LOCATION: ${{ inputs.AZURE_ENV_OPENAI_LOCATION }} - IMAGE_TAG: ${{ inputs.IMAGE_TAG }} + AZURE_ENV_AI_SERVICE_LOCATION: ${{ inputs.AZURE_ENV_AI_SERVICE_LOCATION }} + AZURE_ENV_IMAGE_TAG: ${{ inputs.AZURE_ENV_IMAGE_TAG }} JOB_STATUS: ${{ job.status }} WEB_APPURL: ${{ steps.get_output_windows.outputs.WEB_APPURL }} run: | @@ -325,8 +325,8 @@ jobs: echo "| **Configuration Type** | \`$CONFIG_TYPE\` |" >> $GITHUB_STEP_SUMMARY echo "| **Azure Region (Infrastructure)** | \`$AZURE_LOCATION\` |" >> $GITHUB_STEP_SUMMARY - echo "| **Azure OpenAI Region** | \`$AZURE_ENV_OPENAI_LOCATION\` |" >> $GITHUB_STEP_SUMMARY - echo "| **Docker Image Tag** | \`$IMAGE_TAG\` |" >> $GITHUB_STEP_SUMMARY + echo "| **Azure OpenAI Region** | \`$AZURE_ENV_AI_SERVICE_LOCATION\` |" >> $GITHUB_STEP_SUMMARY + echo "| **Docker Image Tag** | \`$AZURE_ENV_IMAGE_TAG\` |" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY if [[ "$JOB_STATUS" == "success" ]]; then echo "### ✅ Deployment Details" >> $GITHUB_STEP_SUMMARY diff --git a/.github/workflows/job-deploy.yml b/.github/workflows/job-deploy.yml index 429aca483..f135c9b89 100644 --- a/.github/workflows/job-deploy.yml +++ b/.github/workflows/job-deploy.yml @@ -51,12 +51,12 @@ on: required: false default: '' type: string - AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID: + AZURE_ENV_LOG_ANALYTICS_WORKSPACE_RID: description: 'Log Analytics Workspace ID (Optional)' required: false default: '' type: string - AZURE_EXISTING_AI_PROJECT_RESOURCE_ID: + AZURE_ENV_FOUNDRY_PROJECT_RID: description: 'AI Project Resource ID (Optional)' required: false default: '' @@ -84,12 +84,12 @@ on: AZURE_LOCATION: description: "Azure Location" value: ${{ jobs.azure-setup.outputs.AZURE_LOCATION }} - AZURE_ENV_OPENAI_LOCATION: + AZURE_ENV_AI_SERVICE_LOCATION: description: "Azure OpenAI Location" - value: ${{ jobs.azure-setup.outputs.AZURE_ENV_OPENAI_LOCATION }} - IMAGE_TAG: + value: ${{ jobs.azure-setup.outputs.AZURE_ENV_AI_SERVICE_LOCATION }} + AZURE_ENV_IMAGE_TAG: description: "Docker Image Tag Used" - value: ${{ jobs.azure-setup.outputs.IMAGE_TAG }} + value: ${{ jobs.azure-setup.outputs.AZURE_ENV_IMAGE_TAG }} QUOTA_FAILED: description: "Quota Check Failed Flag" value: ${{ jobs.azure-setup.outputs.QUOTA_FAILED || 'false' }} @@ -114,8 +114,8 @@ jobs: RESOURCE_GROUP_NAME: ${{ steps.check_create_rg.outputs.RESOURCE_GROUP_NAME }} ENV_NAME: ${{ steps.generate_env_name.outputs.ENV_NAME }} AZURE_LOCATION: ${{ steps.set_region.outputs.AZURE_LOCATION }} - AZURE_ENV_OPENAI_LOCATION: ${{ steps.set_region.outputs.AZURE_ENV_OPENAI_LOCATION }} - IMAGE_TAG: ${{ steps.determine_image_tag.outputs.IMAGE_TAG }} + AZURE_ENV_AI_SERVICE_LOCATION: ${{ steps.set_region.outputs.AZURE_ENV_AI_SERVICE_LOCATION }} + AZURE_ENV_IMAGE_TAG: ${{ steps.determine_image_tag.outputs.AZURE_ENV_IMAGE_TAG }} QUOTA_FAILED: ${{ steps.quota_failure_output.outputs.QUOTA_FAILED }} EXP_ENABLED: ${{ steps.configure_exp.outputs.EXP_ENABLED }} @@ -132,8 +132,8 @@ jobs: INPUT_EXP: ${{ inputs.EXP }} INPUT_CLEANUP_RESOURCES: ${{ inputs.cleanup_resources }} INPUT_RUN_E2E_TESTS: ${{ inputs.run_e2e_tests }} - INPUT_AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID: ${{ inputs.AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID }} - INPUT_AZURE_EXISTING_AI_PROJECT_RESOURCE_ID: ${{ inputs.AZURE_EXISTING_AI_PROJECT_RESOURCE_ID }} + INPUT_AZURE_ENV_LOG_ANALYTICS_WORKSPACE_RID: ${{ inputs.AZURE_ENV_LOG_ANALYTICS_WORKSPACE_RID }} + INPUT_AZURE_ENV_FOUNDRY_PROJECT_RID: ${{ inputs.AZURE_ENV_FOUNDRY_PROJECT_RID }} INPUT_EXISTING_WEBAPP_URL: ${{ inputs.existing_webapp_url }} INPUT_DOCKER_IMAGE_TAG: ${{ inputs.docker_image_tag }} run: | @@ -229,27 +229,27 @@ jobs: fi fi - # Validate AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID (Azure Resource ID format) - if [[ -n "$INPUT_AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID" ]]; then - if [[ ! "$INPUT_AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID" =~ ^/subscriptions/[a-fA-F0-9-]+/[Rr]esource[Gg]roups/[^/]+/providers/[Mm]icrosoft\.[Oo]perational[Ii]nsights/[Ww]orkspaces/[^/]+$ ]]; then - echo "❌ ERROR: AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID is invalid. Must be a valid Azure Resource ID format:" + # Validate AZURE_ENV_LOG_ANALYTICS_WORKSPACE_RID (Azure Resource ID format) + if [[ -n "$INPUT_AZURE_ENV_LOG_ANALYTICS_WORKSPACE_RID" ]]; then + if [[ ! "$INPUT_AZURE_ENV_LOG_ANALYTICS_WORKSPACE_RID" =~ ^/subscriptions/[a-fA-F0-9-]+/[Rr]esource[Gg]roups/[^/]+/providers/[Mm]icrosoft\.[Oo]perational[Ii]nsights/[Ww]orkspaces/[^/]+$ ]]; then + echo "❌ ERROR: AZURE_ENV_LOG_ANALYTICS_WORKSPACE_RID is invalid. Must be a valid Azure Resource ID format:" echo " /subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.OperationalInsights/workspaces/{workspaceName}" - echo " Got: '$INPUT_AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID'" + echo " Got: '$INPUT_AZURE_ENV_LOG_ANALYTICS_WORKSPACE_RID'" VALIDATION_FAILED=true else - echo "✅ AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID: Valid Resource ID format" + echo "✅ AZURE_ENV_LOG_ANALYTICS_WORKSPACE_RID: Valid Resource ID format" fi fi - # Validate AZURE_EXISTING_AI_PROJECT_RESOURCE_ID (Azure Resource ID format) - if [[ -n "$INPUT_AZURE_EXISTING_AI_PROJECT_RESOURCE_ID" ]]; then - if [[ ! "$INPUT_AZURE_EXISTING_AI_PROJECT_RESOURCE_ID" =~ ^/subscriptions/[a-fA-F0-9-]+/[Rr]esource[Gg]roups/[^/]+/providers/([Mm]icrosoft\.[Mm]achine[Ll]earning[Ss]ervices/([Ww]orkspaces|[Pp]rojects)/[^/]+|[Mm]icrosoft\.[Cc]ognitive[Ss]ervices/[Aa]ccounts/[^/]+/[Pp]rojects/[^/]+)$ ]]; then - echo "❌ ERROR: AZURE_EXISTING_AI_PROJECT_RESOURCE_ID is invalid. Must be a valid Azure Resource ID format:" + # Validate AZURE_ENV_FOUNDRY_PROJECT_RID (Azure Resource ID format) + if [[ -n "$INPUT_AZURE_ENV_FOUNDRY_PROJECT_RID" ]]; then + if [[ ! "$INPUT_AZURE_ENV_FOUNDRY_PROJECT_RID" =~ ^/subscriptions/[a-fA-F0-9-]+/[Rr]esource[Gg]roups/[^/]+/providers/([Mm]icrosoft\.[Mm]achine[Ll]earning[Ss]ervices/([Ww]orkspaces|[Pp]rojects)/[^/]+|[Mm]icrosoft\.[Cc]ognitive[Ss]ervices/[Aa]ccounts/[^/]+/[Pp]rojects/[^/]+)$ ]]; then + echo "❌ ERROR: AZURE_ENV_FOUNDRY_PROJECT_RID is invalid. Must be a valid Azure Resource ID format:" echo " /subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.CognitiveServices/accounts/{accountName}/projects/{projectName}" - echo " Got: '$INPUT_AZURE_EXISTING_AI_PROJECT_RESOURCE_ID'" + echo " Got: '$INPUT_AZURE_ENV_FOUNDRY_PROJECT_RID'" VALIDATION_FAILED=true else - echo "✅ AZURE_EXISTING_AI_PROJECT_RESOURCE_ID: Valid Resource ID format" + echo "✅ AZURE_ENV_FOUNDRY_PROJECT_RID: Valid Resource ID format" fi fi @@ -293,8 +293,8 @@ jobs: shell: bash env: INPUT_EXP: ${{ inputs.EXP }} - INPUT_LOG_ANALYTICS_WORKSPACE_ID: ${{ inputs.AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID }} - INPUT_AI_PROJECT_RESOURCE_ID: ${{ inputs.AZURE_EXISTING_AI_PROJECT_RESOURCE_ID }} + INPUT_LOG_ANALYTICS_WORKSPACE_ID: ${{ inputs.AZURE_ENV_LOG_ANALYTICS_WORKSPACE_RID }} + INPUT_AI_PROJECT_RESOURCE_ID: ${{ inputs.AZURE_ENV_FOUNDRY_PROJECT_RID }} run: | echo "🔍 Validating EXP configuration..." @@ -478,8 +478,8 @@ jobs: run: | echo "Selected Region from OpenAI Quota Check: $VALID_REGION" echo "Selected Region from Search Quota Check: $SEARCH_VALID_REGION" - echo "AZURE_ENV_OPENAI_LOCATION=$VALID_REGION" >> $GITHUB_ENV - echo "AZURE_ENV_OPENAI_LOCATION=$VALID_REGION" >> $GITHUB_OUTPUT + echo "AZURE_ENV_AI_SERVICE_LOCATION=$VALID_REGION" >> $GITHUB_ENV + echo "AZURE_ENV_AI_SERVICE_LOCATION=$VALID_REGION" >> $GITHUB_OUTPUT if [[ "${{ inputs.trigger_type }}" == "workflow_dispatch" && -n "$INPUT_AZURE_LOCATION" ]]; then # Manual trigger: use user's region if it passed search quota, otherwise use search-validated region @@ -559,8 +559,8 @@ jobs: run: | if [[ "${{ env.BUILD_DOCKER_IMAGE }}" == "true" ]]; then if [[ -n "$INPUT_DOCKER_IMAGE_TAG" ]]; then - IMAGE_TAG="$INPUT_DOCKER_IMAGE_TAG" - echo "🔗 Using Docker image tag from build job: $IMAGE_TAG" + AZURE_ENV_IMAGE_TAG="$INPUT_DOCKER_IMAGE_TAG" + echo "🔗 Using Docker image tag from build job: $AZURE_ENV_IMAGE_TAG" else echo "❌ Docker build job failed or was skipped, but BUILD_DOCKER_IMAGE is true" exit 1 @@ -572,24 +572,24 @@ jobs: # Determine image tag based on branch if [[ "$BRANCH_NAME" == "main" ]]; then - IMAGE_TAG="latest" + AZURE_ENV_IMAGE_TAG="latest" echo "Using main branch - image tag: latest" elif [[ "$BRANCH_NAME" == "dev" ]]; then - IMAGE_TAG="dev" + AZURE_ENV_IMAGE_TAG="dev" echo "Using dev branch - image tag: dev" elif [[ "$BRANCH_NAME" == "demo" ]]; then - IMAGE_TAG="demo" + AZURE_ENV_IMAGE_TAG="demo" echo "Using demo branch - image tag: demo" else - IMAGE_TAG="latest" + AZURE_ENV_IMAGE_TAG="latest" echo "Using default for branch '$BRANCH_NAME' - image tag: latest" fi - echo "Using existing Docker image tag: $IMAGE_TAG" + echo "Using existing Docker image tag: $AZURE_ENV_IMAGE_TAG" fi - echo "IMAGE_TAG=$IMAGE_TAG" >> $GITHUB_ENV - echo "IMAGE_TAG=$IMAGE_TAG" >> $GITHUB_OUTPUT + echo "AZURE_ENV_IMAGE_TAG=$AZURE_ENV_IMAGE_TAG" >> $GITHUB_ENV + echo "AZURE_ENV_IMAGE_TAG=$AZURE_ENV_IMAGE_TAG" >> $GITHUB_OUTPUT - name: Generate Unique Environment Name id: generate_env_name @@ -645,15 +645,15 @@ jobs: uses: ./.github/workflows/job-deploy-linux.yml with: ENV_NAME: ${{ needs.azure-setup.outputs.ENV_NAME }} - AZURE_ENV_OPENAI_LOCATION: ${{ needs.azure-setup.outputs.AZURE_ENV_OPENAI_LOCATION }} + AZURE_ENV_AI_SERVICE_LOCATION: ${{ needs.azure-setup.outputs.AZURE_ENV_AI_SERVICE_LOCATION }} AZURE_LOCATION: ${{ needs.azure-setup.outputs.AZURE_LOCATION }} RESOURCE_GROUP_NAME: ${{ needs.azure-setup.outputs.RESOURCE_GROUP_NAME }} - IMAGE_TAG: ${{ needs.azure-setup.outputs.IMAGE_TAG }} + AZURE_ENV_IMAGE_TAG: ${{ needs.azure-setup.outputs.AZURE_ENV_IMAGE_TAG }} BUILD_DOCKER_IMAGE: ${{ inputs.build_docker_image || 'false' }} EXP: ${{ needs.azure-setup.outputs.EXP_ENABLED }} WAF_ENABLED: ${{ inputs.waf_enabled == true && 'true' || 'false' }} - AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID: ${{ inputs.AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID }} - AZURE_EXISTING_AI_PROJECT_RESOURCE_ID: ${{ inputs.AZURE_EXISTING_AI_PROJECT_RESOURCE_ID }} + AZURE_ENV_LOG_ANALYTICS_WORKSPACE_RID: ${{ inputs.AZURE_ENV_LOG_ANALYTICS_WORKSPACE_RID }} + AZURE_ENV_FOUNDRY_PROJECT_RID: ${{ inputs.AZURE_ENV_FOUNDRY_PROJECT_RID }} secrets: inherit deploy-windows: @@ -663,13 +663,13 @@ jobs: uses: ./.github/workflows/job-deploy-windows.yml with: ENV_NAME: ${{ needs.azure-setup.outputs.ENV_NAME }} - AZURE_ENV_OPENAI_LOCATION: ${{ needs.azure-setup.outputs.AZURE_ENV_OPENAI_LOCATION }} + AZURE_ENV_AI_SERVICE_LOCATION: ${{ needs.azure-setup.outputs.AZURE_ENV_AI_SERVICE_LOCATION }} AZURE_LOCATION: ${{ needs.azure-setup.outputs.AZURE_LOCATION }} RESOURCE_GROUP_NAME: ${{ needs.azure-setup.outputs.RESOURCE_GROUP_NAME }} - IMAGE_TAG: ${{ needs.azure-setup.outputs.IMAGE_TAG }} +AZURE_ENV_IMAGE_TAG: ${{ needs.azure-setup.outputs.AZURE_ENV_IMAGE_TAG }} BUILD_DOCKER_IMAGE: ${{ inputs.build_docker_image || 'false' }} EXP: ${{ needs.azure-setup.outputs.EXP_ENABLED }} WAF_ENABLED: ${{ inputs.waf_enabled == true && 'true' || 'false' }} - AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID: ${{ inputs.AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID }} - AZURE_EXISTING_AI_PROJECT_RESOURCE_ID: ${{ inputs.AZURE_EXISTING_AI_PROJECT_RESOURCE_ID }} + AZURE_ENV_LOG_ANALYTICS_WORKSPACE_RID: ${{ inputs.AZURE_ENV_LOG_ANALYTICS_WORKSPACE_RID }} + AZURE_ENV_FOUNDRY_PROJECT_RID: ${{ inputs.AZURE_ENV_FOUNDRY_PROJECT_RID }} secrets: inherit diff --git a/.github/workflows/job-docker-build.yml b/.github/workflows/job-docker-build.yml index 3773ea285..9b927307f 100644 --- a/.github/workflows/job-docker-build.yml +++ b/.github/workflows/job-docker-build.yml @@ -13,9 +13,9 @@ on: default: false type: boolean outputs: - IMAGE_TAG: + AZURE_ENV_IMAGE_TAG: description: "Generated Docker Image Tag" - value: ${{ jobs.docker-build.outputs.IMAGE_TAG }} + value: ${{ jobs.docker-build.outputs.AZURE_ENV_IMAGE_TAG }} env: BRANCH_NAME: ${{ github.event.workflow_run.head_branch || github.head_ref || github.ref_name }} @@ -26,7 +26,7 @@ jobs: runs-on: ubuntu-latest environment: production outputs: - IMAGE_TAG: ${{ steps.generate_docker_tag.outputs.IMAGE_TAG }} + AZURE_ENV_IMAGE_TAG: ${{ steps.generate_docker_tag.outputs.AZURE_ENV_IMAGE_TAG }} steps: - name: Checkout Code uses: actions/checkout@v6 @@ -41,8 +41,8 @@ jobs: BRANCH_NAME="${{ github.head_ref || github.ref_name }}" CLEAN_BRANCH_NAME=$(echo "$BRANCH_NAME" | sed 's/[^a-zA-Z0-9._-]/-/g' | sed 's/--*/-/g' | sed 's/^-\|-$//g') UNIQUE_TAG="${CLEAN_BRANCH_NAME}-${TIMESTAMP}-${RUN_ID}" - echo "IMAGE_TAG=$UNIQUE_TAG" >> $GITHUB_ENV - echo "IMAGE_TAG=$UNIQUE_TAG" >> $GITHUB_OUTPUT + echo "AZURE_ENV_IMAGE_TAG=$UNIQUE_TAG" >> $GITHUB_ENV + echo "AZURE_ENV_IMAGE_TAG=$UNIQUE_TAG" >> $GITHUB_OUTPUT echo "Generated unique Docker tag: $UNIQUE_TAG" - name: Set up Docker Buildx @@ -69,8 +69,8 @@ jobs: file: ./src/app/WebApp.Dockerfile push: true tags: | - ${{ secrets.ACR_TEST_LOGIN_SERVER }}/content-gen-app:${{ steps.generate_docker_tag.outputs.IMAGE_TAG }} - ${{ secrets.ACR_TEST_LOGIN_SERVER }}/content-gen-app:${{ steps.generate_docker_tag.outputs.IMAGE_TAG }}_${{ github.run_number }} + ${{ secrets.ACR_TEST_LOGIN_SERVER }}/content-gen-app:${{ steps.generate_docker_tag.outputs.AZURE_ENV_IMAGE_TAG }} + ${{ secrets.ACR_TEST_LOGIN_SERVER }}/content-gen-app:${{ steps.generate_docker_tag.outputs.AZURE_ENV_IMAGE_TAG }}_${{ github.run_number }} - name: Build and Push Docker Image for Backend Server id: build_push_backend @@ -82,26 +82,26 @@ jobs: file: ./src/backend/ApiApp.Dockerfile push: true tags: | - ${{ secrets.ACR_TEST_LOGIN_SERVER }}/content-gen-api:${{ steps.generate_docker_tag.outputs.IMAGE_TAG }} - ${{ secrets.ACR_TEST_LOGIN_SERVER }}/content-gen-api:${{ steps.generate_docker_tag.outputs.IMAGE_TAG }}_${{ github.run_number }} + ${{ secrets.ACR_TEST_LOGIN_SERVER }}/content-gen-api:${{ steps.generate_docker_tag.outputs.AZURE_ENV_IMAGE_TAG }} + ${{ secrets.ACR_TEST_LOGIN_SERVER }}/content-gen-api:${{ steps.generate_docker_tag.outputs.AZURE_ENV_IMAGE_TAG }}_${{ github.run_number }} - name: Verify Docker Image Build shell: bash run: | echo "✅ Docker images successfully built and pushed" - echo "Image tag: ${{ steps.generate_docker_tag.outputs.IMAGE_TAG }}" + echo "Image tag: ${{ steps.generate_docker_tag.outputs.AZURE_ENV_IMAGE_TAG }}" - name: Generate Docker Build Summary if: always() shell: bash run: | - ACR_NAME=$(echo "${{ secrets.ACR_TEST_LOGIN_SERVER }}") + AZURE_ENV_CONTAINER_REGISTRY_NAME=$(echo "${{ secrets.ACR_TEST_LOGIN_SERVER }}") echo "## 🐳 Docker Build Job Summary" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY echo "| Field | Value |" >> $GITHUB_STEP_SUMMARY echo "|-------|--------|" >> $GITHUB_STEP_SUMMARY echo "| **Job Status** | ${{ job.status == 'success' && '✅ Success' || '❌ Failed' }} |" >> $GITHUB_STEP_SUMMARY - echo "| **Image Tag** | \`${{ steps.generate_docker_tag.outputs.IMAGE_TAG }}\` |" >> $GITHUB_STEP_SUMMARY + echo "| **Image Tag** | \`${{ steps.generate_docker_tag.outputs.AZURE_ENV_IMAGE_TAG }}\` |" >> $GITHUB_STEP_SUMMARY echo "| **Branch** | ${{ env.BRANCH_NAME }} |" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY if [[ "${{ job.status }}" == "success" ]]; then @@ -109,8 +109,8 @@ jobs: echo "Successfully built and pushed Docker images to ACR:" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY echo "**Built Images:**" >> $GITHUB_STEP_SUMMARY - echo "- \`${ACR_NAME}/content-gen-app:${{ steps.generate_docker_tag.outputs.IMAGE_TAG }}\`" >> $GITHUB_STEP_SUMMARY - echo "- \`${ACR_NAME}/content-gen-api:${{ steps.generate_docker_tag.outputs.IMAGE_TAG }}\`" >> $GITHUB_STEP_SUMMARY + echo "- \`${AZURE_ENV_CONTAINER_REGISTRY_NAME}/content-gen-app:${{ steps.generate_docker_tag.outputs.AZURE_ENV_IMAGE_TAG }}\`" >> $GITHUB_STEP_SUMMARY + echo "- \`${AZURE_ENV_CONTAINER_REGISTRY_NAME}/content-gen-api:${{ steps.generate_docker_tag.outputs.AZURE_ENV_IMAGE_TAG }}\`" >> $GITHUB_STEP_SUMMARY else echo "### ❌ Build Failed" >> $GITHUB_STEP_SUMMARY echo "- Docker build process encountered an error" >> $GITHUB_STEP_SUMMARY diff --git a/azure.yaml b/azure.yaml index 1898b4e37..252e186b8 100644 --- a/azure.yaml +++ b/azure.yaml @@ -66,7 +66,7 @@ hooks: Write-Host "AI Search Index: " -NoNewline Write-Host "$env:AZURE_AI_SEARCH_PRODUCTS_INDEX" -ForegroundColor Cyan Write-Host "AI Service Location: " -NoNewline - Write-Host "$env:AZURE_ENV_OPENAI_LOCATION" -ForegroundColor Cyan + Write-Host "$env:AZURE_ENV_AI_SERVICE_LOCATION" -ForegroundColor Cyan Write-Host "Container Instance: " -NoNewline Write-Host "$env:CONTAINER_INSTANCE_NAME" -ForegroundColor Cyan @@ -112,11 +112,11 @@ hooks: echo "Storage Account: $AZURE_BLOB_ACCOUNT_NAME" echo "AI Search Service: $AI_SEARCH_SERVICE_NAME" echo "AI Search Index: $AZURE_AI_SEARCH_PRODUCTS_INDEX" - echo "AI Service Location: $AZURE_ENV_OPENAI_LOCATION" + echo "AI Service Location: $AZURE_ENV_AI_SERVICE_LOCATION" echo "Container Instance: $CONTAINER_INSTANCE_NAME" echo "" - echo "Container Registry: $ACR_NAME" + echo "Container Registry: $AZURE_ENV_CONTAINER_REGISTRY_NAME" # Run post-deploy script to upload sample data and create search index echo "" diff --git a/docs/AZD_DEPLOYMENT.md b/docs/AZD_DEPLOYMENT.md index 942fba86f..42f6a984d 100644 --- a/docs/AZD_DEPLOYMENT.md +++ b/docs/AZD_DEPLOYMENT.md @@ -124,14 +124,14 @@ This single command will: ```bash # Set the resource ID of your existing AI Project -azd env set AZURE_EXISTING_AI_PROJECT_RESOURCE_ID "/subscriptions//resourceGroups//providers/Microsoft.MachineLearningServices/workspaces/" +azd env set AZURE_ENV_FOUNDRY_PROJECT_RID "/subscriptions//resourceGroups//providers/Microsoft.MachineLearningServices/workspaces/" ``` ### Reuse Existing Log Analytics Workspace ```bash # Set the resource ID of your existing Log Analytics workspace -azd env set AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID "/subscriptions//resourceGroups//providers/Microsoft.OperationalInsights/workspaces/" +azd env set AZURE_ENV_LOG_ANALYTICS_WORKSPACE_RID "/subscriptions//resourceGroups//providers/Microsoft.OperationalInsights/workspaces/" ``` ## Post-Deployment @@ -227,7 +227,7 @@ Error: The model 'gpt-4o' is not available in region 'westeurope' **Solution**: Set a different region for AI Services: ```bash -azd env set AZURE_ENV_OPENAI_LOCATION eastus +azd env set AZURE_ENV_AI_SERVICE_LOCATION eastus ``` #### 3. Container Build Fails diff --git a/docs/CustomizingAzdParameters.md b/docs/CustomizingAzdParameters.md index e3c6bb117..0162395b6 100644 --- a/docs/CustomizingAzdParameters.md +++ b/docs/CustomizingAzdParameters.md @@ -10,19 +10,19 @@ By default this template will use the environment name as the prefix to prevent | -------------------------------------- | ------- | ---------------------------- | ----------------------------------------------------------------------------- | | `AZURE_LOCATION` | string | `` | Sets the Azure region for resource deployment. Allowed: `australiaeast`, `centralus`, `eastasia`, `eastus`, `eastus2`, `japaneast`, `northeurope`, `southeastasia`, `swedencentral`, `uksouth`, `westus`, `westus3`. | | `AZURE_ENV_NAME` | string | `contentgen` | Sets the environment name prefix for all Azure resources (3-15 characters). | -| `SECONDARY_LOCATION` | string | `uksouth` | Specifies a secondary Azure region for database creation. | -| `AZURE_OPENAI_GPT_MODEL` | string | `gpt-5.1` | Specifies the GPT model name to deploy. | -| `GPT_MODEL_VERSION` | string | `2025-11-13` | Sets the GPT model version. | -| `GPT_MODEL_DEPLOYMENT_TYPE` | string | `GlobalStandard` | Defines the model deployment type (allowed: `Standard`, `GlobalStandard`). | -| `GPT_MODEL_CAPACITY` | integer | `150` | Sets the GPT model token capacity (minimum: `10`). | -| `AZURE_OPENAI_IMAGE_MODEL` | string | `gpt-image-1-mini` | Image model to deploy (allowed: `gpt-image-1-mini`, `gpt-image-1.5`, `none`). | -| `IMAGE_MODEL_CAPACITY` | integer | `1` | Sets the image model deployment capacity in RPM (minimum: `1`). | -| `AZURE_OPENAI_API_VERSION` | string | `2025-01-01-preview` | Specifies the API version for Azure OpenAI service. | -| `AZURE_ENV_OPENAI_LOCATION` | string | `` | Sets the Azure region for OpenAI resource deployment. | -| `AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID` | string | `""` | Reuses an existing Log Analytics Workspace instead of creating a new one. | -| `AZURE_EXISTING_AI_PROJECT_RESOURCE_ID`| string | `""` | Reuses an existing AI Foundry Project instead of creating a new one. | -| `ACR_NAME` | string | `contentgencontainerreg` | Sets the existing Azure Container Registry name (without `.azurecr.io`). | -| `IMAGE_TAG` | string | `latest` | Sets the container image tag (e.g., `latest`, `dev`, `hotfix`). | +| `AZURE_ENV_SECONDARY_LOCATION` | string | `uksouth` | Specifies a secondary Azure region for database creation. | +| `AZURE_ENV_GPT_MODEL_NAME` | string | `gpt-5.1` | Specifies the GPT model name to deploy. | +| `AZURE_ENV_GPT_MODEL_VERSION` | string | `2025-11-13` | Sets the GPT model version. | +| `AZURE_ENV_MODEL_DEPLOYMENT_TYPE` | string | `GlobalStandard` | Defines the model deployment type (allowed: `Standard`, `GlobalStandard`). | +| `AZURE_ENV_GPT_MODEL_CAPACITY` | integer | `150` | Sets the GPT model token capacity (minimum: `10`). | +| `AZURE_ENV_IMAGE_MODEL_NAME` | string | `gpt-image-1-mini` | Image model to deploy (allowed: `gpt-image-1-mini`, `gpt-image-1.5`, `none`). | +| `AZURE_ENV_IMAGE_MODEL_CAPACITY` | integer | `1` | Sets the image model deployment capacity in RPM (minimum: `1`). | +| `AZURE_ENV_OPENAI_API_VERSION` | string | `2025-01-01-preview` | Specifies the API version for Azure OpenAI service. | +| `AZURE_ENV_AI_SERVICE_LOCATION` | string | `` | Sets the Azure region for OpenAI resource deployment. | +| `AZURE_ENV_LOG_ANALYTICS_WORKSPACE_RID` | string | `""` | Reuses an existing Log Analytics Workspace instead of creating a new one. | +| `AZURE_ENV_FOUNDRY_PROJECT_RID`| string | `""` | Reuses an existing AI Foundry Project instead of creating a new one. | +| `AZURE_ENV_CONTAINER_REGISTRY_NAME` | string | `contentgencontainerreg` | Sets the existing Azure Container Registry name (without `.azurecr.io`). | +| `AZURE_ENV_IMAGE_TAG` | string | `latest` | Sets the container image tag (e.g., `latest`, `dev`, `hotfix`). | ## How to Set a Parameter @@ -36,8 +36,8 @@ azd env set ```bash azd env set AZURE_LOCATION westus2 -azd env set AZURE_OPENAI_GPT_MODEL gpt-5.1 -azd env set GPT_MODEL_DEPLOYMENT_TYPE Standard -azd env set AZURE_OPENAI_IMAGE_MODEL gpt-image-1-mini -azd env set ACR_NAME contentgencontainerreg +azd env set AZURE_ENV_GPT_MODEL_NAME gpt-5.1 +azd env set AZURE_ENV_MODEL_DEPLOYMENT_TYPE Standard +azd env set AZURE_ENV_IMAGE_MODEL_NAME gpt-image-1-mini +azd env set AZURE_ENV_CONTAINER_REGISTRY_NAME contentgencontainerreg ``` diff --git a/docs/DEPLOYMENT.md b/docs/DEPLOYMENT.md index 187640f92..96323c5d0 100644 --- a/docs/DEPLOYMENT.md +++ b/docs/DEPLOYMENT.md @@ -218,7 +218,7 @@ az webapp config set -g $RESOURCE_GROUP -n --http20-enabled false **Solution**: 1. Verify GPT-Image-1-mini or GPT-Image-1.5 deployment exists in Azure OpenAI resource -2. Check `AZURE_OPENAI_IMAGE_MODEL` and `AZURE_OPENAI_GPT_IMAGE_ENDPOINT` environment variables +2. Check `AZURE_ENV_IMAGE_MODEL_NAME` and `AZURE_OPENAI_GPT_IMAGE_ENDPOINT` environment variables diff --git a/docs/IMAGE_GENERATION.md b/docs/IMAGE_GENERATION.md index ed3483741..1d0f54af1 100644 --- a/docs/IMAGE_GENERATION.md +++ b/docs/IMAGE_GENERATION.md @@ -7,7 +7,7 @@ The accelerator supports image generation through Azure OpenAI image models: - `gpt-image-1-mini` - `gpt-image-1.5` -Both models are used through `images.generate()` in the backend image agent. The selected model is controlled by `AZURE_OPENAI_IMAGE_MODEL`. +Both models are used through `images.generate()` in the backend image agent. The selected model is controlled by `AZURE_ENV_IMAGE_MODEL_NAME`. ## Current Model Behavior @@ -139,10 +139,10 @@ async def generate_marketing_image( ### Required Environment Variables - `AZURE_OPENAI_ENDPOINT` -- `AZURE_OPENAI_GPT_MODEL` -- `AZURE_OPENAI_IMAGE_MODEL` (`gpt-image-1-mini`, `gpt-image-1.5`, or `none`) +- `AZURE_ENV_GPT_MODEL_NAME` +- `AZURE_ENV_IMAGE_MODEL_NAME` (`gpt-image-1-mini`, `gpt-image-1.5`, or `none`) - `AZURE_OPENAI_GPT_IMAGE_ENDPOINT` (optional if same as main endpoint) -- `AZURE_OPENAI_API_VERSION` +- `AZURE_ENV_OPENAI_API_VERSION` - `AZURE_OPENAI_IMAGE_API_VERSION` ### Optional Image Controls @@ -155,7 +155,7 @@ async def generate_marketing_image( The backend image generator calls Azure OpenAI with: - `images.generate()` -- `model` set from `AZURE_OPENAI_IMAGE_MODEL` +- `model` set from `AZURE_ENV_IMAGE_MODEL_NAME` - prompt text assembled from brief + product + brand constraints - `size` and `quality` from app settings (or request overrides) @@ -184,7 +184,7 @@ The backend image generator calls Azure OpenAI with: ### Model Availability Notes 1. Deploy either `gpt-image-1-mini` or `gpt-image-1.5` based on quota and regional availability. -2. Set `AZURE_OPENAI_IMAGE_MODEL` to the deployed model name. +2. Set `AZURE_ENV_IMAGE_MODEL_NAME` to the deployed model name. 3. If using a separate image endpoint, set `AZURE_OPENAI_GPT_IMAGE_ENDPOINT`. 4. Keep `AZURE_OPENAI_IMAGE_API_VERSION` aligned with the image model API version required by your deployment. diff --git a/docs/LOCAL_DEPLOYMENT.md b/docs/LOCAL_DEPLOYMENT.md index c3abd177e..addd22410 100644 --- a/docs/LOCAL_DEPLOYMENT.md +++ b/docs/LOCAL_DEPLOYMENT.md @@ -147,10 +147,10 @@ Changes to source files will automatically trigger a reload. | Variable | Required | Description | |----------|----------|-------------| | `AZURE_OPENAI_ENDPOINT` | Yes | Azure OpenAI endpoint URL (e.g., `https://your-resource.openai.azure.com/`) | -| `AZURE_OPENAI_GPT_MODEL` | Yes | GPT model deployment name (e.g., `gpt-4o`, `gpt-5.1`) | -| `AZURE_OPENAI_IMAGE_MODEL` | Yes | Image generation model (`gpt-image-1-mini` or `gpt-image-1.5`) | +| `AZURE_ENV_GPT_MODEL_NAME` | Yes | GPT model deployment name (e.g., `gpt-4o`, `gpt-5.1`) | +| `AZURE_ENV_IMAGE_MODEL_NAME` | Yes | Image generation model (`gpt-image-1-mini` or `gpt-image-1.5`) | | `AZURE_OPENAI_GPT_IMAGE_ENDPOINT` | No | Separate endpoint for gpt-image-1-mini (if different from main endpoint) | -| `AZURE_OPENAI_API_VERSION` | Yes | API version (e.g., `2024-06-01`) | +| `AZURE_ENV_OPENAI_API_VERSION` | Yes | API version (e.g., `2024-06-01`) | | `AZURE_OPENAI_TEMPERATURE` | No | Generation temperature (default: `0.7`) | | `AZURE_OPENAI_MAX_TOKENS` | No | Max tokens for generation (default: `2000`) | @@ -228,12 +228,12 @@ Changes to source files will automatically trigger a reload. ```dotenv # Azure OpenAI AZURE_OPENAI_ENDPOINT=https://my-openai.openai.azure.com/ -AZURE_OPENAI_GPT_MODEL=gpt-4o -AZURE_OPENAI_IMAGE_MODEL=gpt-image-1-mini +AZURE_ENV_GPT_MODEL_NAME=gpt-4o +AZURE_ENV_IMAGE_MODEL_NAME=gpt-image-1-mini AZURE_OPENAI_GPT_IMAGE_ENDPOINT=https://my-openai.openai.azure.com AZURE_OPENAI_IMAGE_SIZE=1024x1024 AZURE_OPENAI_IMAGE_QUALITY=medium -AZURE_OPENAI_API_VERSION=2024-06-01 +AZURE_ENV_OPENAI_API_VERSION=2024-06-01 # Cosmos DB AZURE_COSMOS_ENDPOINT=https://my-cosmos.documents.azure.com:443/ diff --git a/docs/TECHNICAL_GUIDE.md b/docs/TECHNICAL_GUIDE.md index ce41f325e..76c04566c 100644 --- a/docs/TECHNICAL_GUIDE.md +++ b/docs/TECHNICAL_GUIDE.md @@ -156,9 +156,9 @@ See `src/backend/settings.py` for all configuration options. Key settings: | Variable | Description | |----------|-------------| | `AZURE_OPENAI_ENDPOINT` | Azure OpenAI endpoint for GPT model | -| `AZURE_OPENAI_GPT_MODEL` | GPT model deployment name | +| `AZURE_ENV_GPT_MODEL_NAME` | GPT model deployment name | | `AZURE_OPENAI_GPT_IMAGE_ENDPOINT` | Azure OpenAI endpoint for GPT image model (if separate) | -| `AZURE_OPENAI_IMAGE_MODEL` | GPT image model deployment name (gpt-image-1-mini) | +| `AZURE_ENV_IMAGE_MODEL_NAME` | GPT image model deployment name (gpt-image-1-mini) | | `AZURE_COSMOS_ENDPOINT` | Azure Cosmos DB endpoint | | `AZURE_COSMOS_DATABASE_NAME` | Cosmos DB database name | | `AZURE_BLOB_ACCOUNT_NAME` | Storage account name | diff --git a/infra/main.bicep b/infra/main.bicep index 5e1776719..9a7914447 100644 --- a/infra/main.bicep +++ b/infra/main.bicep @@ -860,10 +860,10 @@ module containerInstance 'modules/container-instance.bicep' = { environmentVariables: [ // Azure OpenAI Settings { name: 'AZURE_OPENAI_ENDPOINT', value: 'https://${aiFoundryAiServicesResourceName}.openai.azure.com/' } - { name: 'AZURE_OPENAI_GPT_MODEL', value: gptModelName } - { name: 'AZURE_OPENAI_IMAGE_MODEL', value: imageModelConfig[imageModelChoice].name } + { name: 'AZURE_ENV_GPT_MODEL_NAME', value: gptModelName } + { name: 'AZURE_ENV_IMAGE_MODEL_NAME', value: imageModelConfig[imageModelChoice].name } { name: 'AZURE_OPENAI_GPT_IMAGE_ENDPOINT', value: imageModelChoice != 'none' ? 'https://${aiFoundryAiServicesResourceName}.openai.azure.com/' : '' } - { name: 'AZURE_OPENAI_API_VERSION', value: azureOpenaiAPIVersion } + { name: 'AZURE_ENV_OPENAI_API_VERSION', value: azureOpenaiAPIVersion } // Azure Cosmos DB Settings { name: 'AZURE_COSMOS_ENDPOINT', value: 'https://cosmos-${solutionSuffix}.documents.azure.com:443/' } { name: 'AZURE_COSMOS_DATABASE_NAME', value: cosmosDBDatabaseName } @@ -935,7 +935,7 @@ output AI_FOUNDRY_RG_NAME string = aiFoundryAiServicesResourceGroupName output AI_FOUNDRY_RESOURCE_ID string = useExistingAiFoundryAiProject ? '' : aiFoundryAiServices!.outputs.resourceId @description('Contains existing AI project resource ID.') -output AZURE_EXISTING_AI_PROJECT_RESOURCE_ID string = azureExistingAIProjectResourceId +output AZURE_ENV_FOUNDRY_PROJECT_RID string = azureExistingAIProjectResourceId @description('Contains AI Search Service Endpoint URL') output AZURE_AI_SEARCH_ENDPOINT string = 'https://${aiSearch.outputs.name}.search.windows.net/' @@ -953,16 +953,16 @@ output AZURE_AI_SEARCH_IMAGE_INDEX string = 'product-images' output AZURE_OPENAI_ENDPOINT string = 'https://${aiFoundryAiServicesResourceName}.openai.azure.com/' @description('Contains GPT Model') -output AZURE_OPENAI_GPT_MODEL string = gptModelName +output AZURE_ENV_GPT_MODEL_NAME string = gptModelName @description('Contains Image Model (empty if none selected)') -output AZURE_OPENAI_IMAGE_MODEL string = imageModelConfig[imageModelChoice].name +output AZURE_ENV_IMAGE_MODEL_NAME string = imageModelConfig[imageModelChoice].name @description('Contains Azure OpenAI GPT/Image endpoint URL (empty if no image model selected)') output AZURE_OPENAI_GPT_IMAGE_ENDPOINT string = imageModelChoice != 'none' ? 'https://${aiFoundryAiServicesResourceName}.openai.azure.com/' : '' @description('Contains Azure OpenAI API Version') -output AZURE_OPENAI_API_VERSION string = azureOpenaiAPIVersion +output AZURE_ENV_OPENAI_API_VERSION string = azureOpenaiAPIVersion @description('Contains OpenAI Resource') output AZURE_OPENAI_RESOURCE string = aiFoundryAiServicesResourceName @@ -977,7 +977,7 @@ output AZURE_AI_AGENT_API_VERSION string = azureAiAgentApiVersion output AZURE_APPLICATION_INSIGHTS_CONNECTION_STRING string = (enableMonitoring && !useExistingLogAnalytics) ? applicationInsights!.outputs.connectionString : '' @description('Contains the location used for AI Services deployment') -output AZURE_ENV_OPENAI_LOCATION string = azureAiServiceLocation +output AZURE_ENV_AI_SERVICE_LOCATION string = azureAiServiceLocation @description('Contains Container Instance Name') output CONTAINER_INSTANCE_NAME string = containerInstance.outputs.name @@ -989,7 +989,7 @@ output CONTAINER_INSTANCE_IP string = containerInstance.outputs.ipAddress output CONTAINER_INSTANCE_FQDN string = enablePrivateNetworking ? '' : containerInstance.outputs.fqdn @description('Contains ACR Name') -output ACR_NAME string = acrResourceName +output AZURE_ENV_CONTAINER_REGISTRY_NAME string = acrResourceName @description('Contains flag for Azure AI Foundry usage') output USE_FOUNDRY bool = useFoundryMode ? true : false diff --git a/infra/main.json b/infra/main.json index 4e25c3002..c6d1c2043 100644 --- a/infra/main.json +++ b/infra/main.json @@ -6,7 +6,7 @@ "_generator": { "name": "bicep", "version": "0.41.2.15936", - "templateHash": "7762801937040004596" + "templateHash": "14575344972848807462" }, "name": "Intelligent Content Generation Accelerator", "description": "Solution Accelerator for multimodal marketing content generation using Microsoft Agent Framework.\n" @@ -33079,11 +33079,11 @@ "value": "[format('https://{0}.openai.azure.com/', variables('aiFoundryAiServicesResourceName'))]" }, { - "name": "AZURE_OPENAI_GPT_MODEL", + "name": "AZURE_ENV_GPT_MODEL_NAME", "value": "[parameters('gptModelName')]" }, { - "name": "AZURE_OPENAI_IMAGE_MODEL", + "name": "AZURE_ENV_IMAGE_MODEL_NAME", "value": "[variables('imageModelConfig')[parameters('imageModelChoice')].name]" }, { @@ -33091,7 +33091,7 @@ "value": "[if(not(equals(parameters('imageModelChoice'), 'none')), format('https://{0}.openai.azure.com/', variables('aiFoundryAiServicesResourceName')), '')]" }, { - "name": "AZURE_OPENAI_API_VERSION", + "name": "AZURE_ENV_OPENAI_API_VERSION", "value": "[parameters('azureOpenaiAPIVersion')]" }, { @@ -33176,7 +33176,7 @@ "_generator": { "name": "bicep", "version": "0.41.2.15936", - "templateHash": "4952237836308528733" + "templateHash": "11066122245438821615" } }, "parameters": { @@ -33248,9 +33248,8 @@ }, "userAssignedIdentityResourceId": { "type": "string", - "defaultValue": "", "metadata": { - "description": "Optional. User-assigned managed identity resource ID for ACR pull." + "description": "Required. User-assigned managed identity resource ID for ACR pull." } } }, @@ -33460,7 +33459,7 @@ }, "value": "[if(variables('useExistingAiFoundryAiProject'), '', reference('aiFoundryAiServices').outputs.resourceId.value)]" }, - "AZURE_EXISTING_AI_PROJECT_RESOURCE_ID": { + "AZURE_ENV_FOUNDRY_PROJECT_RID": { "type": "string", "metadata": { "description": "Contains existing AI project resource ID." @@ -33502,14 +33501,14 @@ }, "value": "[format('https://{0}.openai.azure.com/', variables('aiFoundryAiServicesResourceName'))]" }, - "AZURE_OPENAI_GPT_MODEL": { + "AZURE_ENV_GPT_MODEL_NAME": { "type": "string", "metadata": { "description": "Contains GPT Model" }, "value": "[parameters('gptModelName')]" }, - "AZURE_OPENAI_IMAGE_MODEL": { + "AZURE_ENV_IMAGE_MODEL_NAME": { "type": "string", "metadata": { "description": "Contains Image Model (empty if none selected)" @@ -33523,7 +33522,7 @@ }, "value": "[if(not(equals(parameters('imageModelChoice'), 'none')), format('https://{0}.openai.azure.com/', variables('aiFoundryAiServicesResourceName')), '')]" }, - "AZURE_OPENAI_API_VERSION": { + "AZURE_ENV_OPENAI_API_VERSION": { "type": "string", "metadata": { "description": "Contains Azure OpenAI API Version" @@ -33558,7 +33557,7 @@ }, "value": "[if(and(parameters('enableMonitoring'), not(variables('useExistingLogAnalytics'))), reference('applicationInsights').outputs.connectionString.value, '')]" }, - "AZURE_ENV_OPENAI_LOCATION": { + "AZURE_ENV_AI_SERVICE_LOCATION": { "type": "string", "metadata": { "description": "Contains the location used for AI Services deployment" @@ -33586,7 +33585,7 @@ }, "value": "[if(parameters('enablePrivateNetworking'), '', reference('containerInstance').outputs.fqdn.value)]" }, - "ACR_NAME": { + "AZURE_ENV_CONTAINER_REGISTRY_NAME": { "type": "string", "metadata": { "description": "Contains ACR Name" diff --git a/infra/main.parameters.json b/infra/main.parameters.json index 00c481f2e..e2a85f1c5 100644 --- a/infra/main.parameters.json +++ b/infra/main.parameters.json @@ -9,43 +9,43 @@ "value": "${AZURE_LOCATION}" }, "secondaryLocation": { - "value": "${SECONDARY_LOCATION}" + "value": "${AZURE_ENV_SECONDARY_LOCATION}" }, "gptModelName": { - "value": "${AZURE_OPENAI_GPT_MODEL}" + "value": "${AZURE_ENV_GPT_MODEL_NAME}" }, "gptModelVersion": { - "value": "${GPT_MODEL_VERSION}" + "value": "${AZURE_ENV_GPT_MODEL_VERSION}" }, "gptModelDeploymentType": { - "value": "${GPT_MODEL_DEPLOYMENT_TYPE}" + "value": "${AZURE_ENV_MODEL_DEPLOYMENT_TYPE}" }, "gptModelCapacity": { - "value": "${GPT_MODEL_CAPACITY}" + "value": "${AZURE_ENV_GPT_MODEL_CAPACITY}" }, "imageModelChoice": { - "value": "${AZURE_OPENAI_IMAGE_MODEL}" + "value": "${AZURE_ENV_IMAGE_MODEL_NAME}" }, "imageModelCapacity": { - "value": "${IMAGE_MODEL_CAPACITY}" + "value": "${AZURE_ENV_IMAGE_MODEL_CAPACITY}" }, "azureOpenaiAPIVersion": { - "value": "${AZURE_OPENAI_API_VERSION}" + "value": "${AZURE_ENV_OPENAI_API_VERSION}" }, "azureAiServiceLocation": { - "value": "${AZURE_ENV_OPENAI_LOCATION}" + "value": "${AZURE_ENV_AI_SERVICE_LOCATION}" }, "existingLogAnalyticsWorkspaceId": { - "value": "${AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID}" + "value": "${AZURE_ENV_LOG_ANALYTICS_WORKSPACE_RID}" }, "azureExistingAIProjectResourceId": { - "value": "${AZURE_EXISTING_AI_PROJECT_RESOURCE_ID}" + "value": "${AZURE_ENV_FOUNDRY_PROJECT_RID}" }, "acrName": { - "value": "${ACR_NAME}" + "value": "${AZURE_ENV_CONTAINER_REGISTRY_NAME}" }, "imageTag": { - "value": "${IMAGE_TAG=latest}" + "value": "${AZURE_ENV_IMAGE_TAG=latest}" } } } diff --git a/infra/main.waf.parameters.json b/infra/main.waf.parameters.json index 0cc363c9d..bce19cf2b 100644 --- a/infra/main.waf.parameters.json +++ b/infra/main.waf.parameters.json @@ -9,43 +9,43 @@ "value": "${AZURE_LOCATION}" }, "secondaryLocation": { - "value": "${SECONDARY_LOCATION}" + "value": "${AZURE_ENV_SECONDARY_LOCATION}" }, "gptModelName": { - "value": "${AZURE_OPENAI_GPT_MODEL}" + "value": "${AZURE_ENV_GPT_MODEL_NAME}" }, "gptModelVersion": { - "value": "${GPT_MODEL_VERSION}" + "value": "${AZURE_ENV_GPT_MODEL_VERSION}" }, "gptModelDeploymentType": { - "value": "${GPT_MODEL_DEPLOYMENT_TYPE}" + "value": "${AZURE_ENV_MODEL_DEPLOYMENT_TYPE}" }, "gptModelCapacity": { - "value": "${GPT_MODEL_CAPACITY}" + "value": "${AZURE_ENV_GPT_MODEL_CAPACITY}" }, "imageModelChoice": { - "value": "${AZURE_OPENAI_IMAGE_MODEL}" + "value": "${AZURE_ENV_IMAGE_MODEL_NAME}" }, "imageModelCapacity": { - "value": "${IMAGE_MODEL_CAPACITY}" + "value": "${AZURE_ENV_IMAGE_MODEL_CAPACITY}" }, "azureOpenaiAPIVersion": { - "value": "${AZURE_OPENAI_API_VERSION}" + "value": "${AZURE_ENV_OPENAI_API_VERSION}" }, "azureAiServiceLocation": { - "value": "${AZURE_ENV_OPENAI_LOCATION}" + "value": "${AZURE_ENV_AI_SERVICE_LOCATION}" }, "existingLogAnalyticsWorkspaceId": { - "value": "${AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID}" + "value": "${AZURE_ENV_LOG_ANALYTICS_WORKSPACE_RID}" }, "azureExistingAIProjectResourceId": { - "value": "${AZURE_EXISTING_AI_PROJECT_RESOURCE_ID}" + "value": "${AZURE_ENV_FOUNDRY_PROJECT_RID}" }, "acrName": { - "value": "${ACR_NAME}" + "value": "${AZURE_ENV_CONTAINER_REGISTRY_NAME}" }, "imageTag": { - "value": "${IMAGE_TAG=latest}" + "value": "${AZURE_ENV_IMAGE_TAG=latest}" }, "enablePrivateNetworking": { "value": true diff --git a/scripts/deploy.ps1 b/scripts/deploy.ps1 index bed9b90a9..1da9746f8 100644 --- a/scripts/deploy.ps1 +++ b/scripts/deploy.ps1 @@ -42,10 +42,10 @@ Set-Location $ProjectDir # Configuration from environment or prompt $ResourceGroup = if ($env:RESOURCE_GROUP) { $env:RESOURCE_GROUP } else { $null } $Location = if ($env:LOCATION) { $env:LOCATION } else { "eastus" } -$AcrName = if ($env:ACR_NAME) { $env:ACR_NAME } else { $null } +$AcrName = if ($env:AZURE_ENV_CONTAINER_REGISTRY_NAME) { $env:AZURE_ENV_CONTAINER_REGISTRY_NAME } else { $null } $ContainerName = if ($env:CONTAINER_NAME) { $env:CONTAINER_NAME } else { "aci-contentgen-backend" } $AppServiceName = if ($env:APP_SERVICE_NAME) { $env:APP_SERVICE_NAME } else { $null } -$ImageTag = if ($env:IMAGE_TAG) { $env:IMAGE_TAG } else { "latest" } +$ImageTag = if ($env:AZURE_ENV_IMAGE_TAG) { $env:AZURE_ENV_IMAGE_TAG } else { "latest" } Write-Host "" Write-Host "Current configuration:" diff --git a/scripts/deploy.sh b/scripts/deploy.sh index 10bbbbfa4..efc48ccc2 100644 --- a/scripts/deploy.sh +++ b/scripts/deploy.sh @@ -39,19 +39,19 @@ cd "$PROJECT_DIR" # Default configuration RESOURCE_GROUP="${RESOURCE_GROUP:-}" LOCATION="${LOCATION:-eastus}" -ACR_NAME="${ACR_NAME:-}" +AZURE_ENV_CONTAINER_REGISTRY_NAME="${AZURE_ENV_CONTAINER_REGISTRY_NAME:-}" CONTAINER_NAME="${CONTAINER_NAME:-aci-contentgen-backend}" APP_SERVICE_NAME="${APP_SERVICE_NAME:-}" -IMAGE_TAG="${IMAGE_TAG:-latest}" +AZURE_ENV_IMAGE_TAG="${AZURE_ENV_IMAGE_TAG:-latest}" echo "" echo "Current configuration:" echo " Resource Group: ${RESOURCE_GROUP:-}" echo " Location: $LOCATION" -echo " ACR Name: ${ACR_NAME:-}" +echo " ACR Name: ${AZURE_ENV_CONTAINER_REGISTRY_NAME:-}" echo " Container Name: $CONTAINER_NAME" echo " App Service: ${APP_SERVICE_NAME:-}" -echo " Image Tag: $IMAGE_TAG" +echo " Image Tag: $AZURE_ENV_IMAGE_TAG" echo "" # Prompt for missing values @@ -59,8 +59,8 @@ if [ -z "$RESOURCE_GROUP" ]; then read -p "Enter Resource Group name: " RESOURCE_GROUP fi -if [ -z "$ACR_NAME" ]; then - read -p "Enter Azure Container Registry name: " ACR_NAME +if [ -z "$AZURE_ENV_CONTAINER_REGISTRY_NAME" ]; then + read -p "Enter Azure Container Registry name: " AZURE_ENV_CONTAINER_REGISTRY_NAME fi if [ -z "$APP_SERVICE_NAME" ]; then @@ -97,16 +97,16 @@ if [[ $REPLY =~ ^[Yy]$ ]]; then cd "$PROJECT_DIR/src" # Login to ACR - az acr login --name "$ACR_NAME" + az acr login --name "$AZURE_ENV_CONTAINER_REGISTRY_NAME" # Build and push using ACR tasks az acr build \ - --registry "$ACR_NAME" \ - --image "contentgen-backend:$IMAGE_TAG" \ + --registry "$AZURE_ENV_CONTAINER_REGISTRY_NAME" \ + --image "contentgen-backend:$AZURE_ENV_IMAGE_TAG" \ --file WebApp.Dockerfile \ . - echo "✓ Container built and pushed to $ACR_NAME.azurecr.io/contentgen-backend:$IMAGE_TAG" + echo "✓ Container built and pushed to $AZURE_ENV_CONTAINER_REGISTRY_NAME.azurecr.io/contentgen-backend:$AZURE_ENV_IMAGE_TAG" # Step 2: Get the current container's managed identity echo "" diff --git a/scripts/local_dev.ps1 b/scripts/local_dev.ps1 index 6978846ba..854c51079 100644 --- a/scripts/local_dev.ps1 +++ b/scripts/local_dev.ps1 @@ -109,7 +109,7 @@ function Ensure-AzureAIUserRole { $foundryResourceId = $null if (Test-Path ".env") { Get-Content ".env" | ForEach-Object { - if ($_ -match "^AZURE_EXISTING_AI_PROJECT_RESOURCE_ID=(.*)$") { $existingProjectId = $matches[1].Trim('"').Trim("'") } + if ($_ -match "^AZURE_ENV_FOUNDRY_PROJECT_RID=(.*)$") { $existingProjectId = $matches[1].Trim('"').Trim("'") } if ($_ -match "^AI_FOUNDRY_RESOURCE_ID=(.*)$") { $foundryResourceId = $matches[1].Trim('"').Trim("'") } } } @@ -121,7 +121,7 @@ function Ensure-AzureAIUserRole { } elseif ($foundryResourceId) { $scope = $foundryResourceId } else { - Write-Error "Neither AZURE_EXISTING_AI_PROJECT_RESOURCE_ID nor AI_FOUNDRY_RESOURCE_ID found in .env" + Write-Error "Neither AZURE_ENV_FOUNDRY_PROJECT_RID nor AI_FOUNDRY_RESOURCE_ID found in .env" exit 1 } diff --git a/scripts/local_dev.sh b/scripts/local_dev.sh index 8bf811d92..67a55b8a1 100644 --- a/scripts/local_dev.sh +++ b/scripts/local_dev.sh @@ -105,7 +105,7 @@ ensure_azure_ai_user_role() { local existing_project_id="" local foundry_resource_id="" if [ -f ".env" ]; then - existing_project_id=$(grep "^AZURE_EXISTING_AI_PROJECT_RESOURCE_ID=" .env | cut -d'=' -f2- | tr -d '"' | tr -d "'" || echo "") + existing_project_id=$(grep "^AZURE_ENV_FOUNDRY_PROJECT_RID=" .env | cut -d'=' -f2- | tr -d '"' | tr -d "'" || echo "") foundry_resource_id=$(grep "^AI_FOUNDRY_RESOURCE_ID=" .env | cut -d'=' -f2- | tr -d '"' | tr -d "'" || echo "") fi @@ -115,7 +115,7 @@ ensure_azure_ai_user_role() { elif [ -n "$foundry_resource_id" ]; then scope="$foundry_resource_id" else - print_error "Neither AZURE_EXISTING_AI_PROJECT_RESOURCE_ID nor AI_FOUNDRY_RESOURCE_ID found in .env" + print_error "Neither AZURE_ENV_FOUNDRY_PROJECT_RID nor AI_FOUNDRY_RESOURCE_ID found in .env" exit 1 fi diff --git a/scripts/sample_content_generation.py b/scripts/sample_content_generation.py index 1a75d5def..cf9b55c99 100644 --- a/scripts/sample_content_generation.py +++ b/scripts/sample_content_generation.py @@ -8,9 +8,9 @@ Prerequisites: 1. Set up environment variables (or use a .env file): - AZURE_OPENAI_ENDPOINT: Your Azure OpenAI endpoint - - AZURE_OPENAI_GPT_MODEL: GPT model deployment name + - AZURE_ENV_GPT_MODEL_NAME: GPT model deployment name - AZURE_OPENAI_GPT_IMAGE_ENDPOINT: (Optional) Endpoint for images - - AZURE_OPENAI_IMAGE_MODEL: Image model name (e.g., gpt-image-1-mini) + - AZURE_ENV_IMAGE_MODEL_NAME: Image model name (e.g., gpt-image-1-mini) - AZURE_COSMOS_ENDPOINT: Your CosmosDB endpoint - AZURE_COSMOS_DATABASE_NAME: content-generation - AZURE_COSMOS_CONVERSATIONS_CONTAINER: conversations diff --git a/scripts/sample_image_generation.py b/scripts/sample_image_generation.py index 19c858041..d6710d185 100644 --- a/scripts/sample_image_generation.py +++ b/scripts/sample_image_generation.py @@ -9,7 +9,7 @@ 1. Set up environment variables (or use a .env file): - AZURE_OPENAI_ENDPOINT: Your Azure OpenAI endpoint - AZURE_OPENAI_GPT_IMAGE_ENDPOINT: (Optional) Dedicated GPT image endpoint - - AZURE_OPENAI_IMAGE_MODEL: Use "gpt-image-1-mini" or "gpt-image-1.5" + - AZURE_ENV_IMAGE_MODEL_NAME: Use "gpt-image-1-mini" or "gpt-image-1.5" 2. Ensure you have RBAC access: - "Cognitive Services OpenAI User" role on the Azure OpenAI resource diff --git a/src/backend/agents/image_content_agent.py b/src/backend/agents/image_content_agent.py index 0d2d75c02..c5cce59d5 100644 --- a/src/backend/agents/image_content_agent.py +++ b/src/backend/agents/image_content_agent.py @@ -74,7 +74,7 @@ async def generate_image( """ Generate a marketing image using DALL-E 3, gpt-image-1-mini, or gpt-image-1.5. - The model used is determined by AZURE_OPENAI_IMAGE_MODEL setting. + The model used is determined by AZURE_ENV_IMAGE_MODEL_NAME setting. Args: prompt: The main image generation prompt diff --git a/src/backend/settings.py b/src/backend/settings.py index c28a72e70..fd178b029 100644 --- a/src/backend/settings.py +++ b/src/backend/settings.py @@ -9,7 +9,7 @@ import os from typing import List, Optional -from pydantic import BaseModel, Field, model_validator +from pydantic import AliasChoices, BaseModel, Field, model_validator from pydantic_settings import BaseSettings, SettingsConfigDict from typing_extensions import Self @@ -60,12 +60,18 @@ class _AzureOpenAISettings(BaseSettings): env_ignore_empty=True, ) - gpt_model: str = Field(default="gpt-5", alias="AZURE_OPENAI_GPT_MODEL") + gpt_model: str = Field( + default="gpt-5", + validation_alias=AliasChoices("AZURE_ENV_GPT_MODEL_NAME", "AZURE_OPENAI_GPT_MODEL") + ) model: str = "gpt-5" # Image generation model settings # Supported models: "gpt-image-1-mini" or "gpt-image-1.5" - image_model: str = Field(default="gpt-image-1-mini", alias="AZURE_OPENAI_IMAGE_MODEL") + image_model: str = Field( + default="gpt-image-1-mini", + validation_alias=AliasChoices("AZURE_ENV_IMAGE_MODEL_NAME", "AZURE_OPENAI_IMAGE_MODEL") + ) # gpt-image-1-mini or gpt-image-1.5 specific endpoint gpt_image_endpoint: Optional[str] = Field(default=None, alias="AZURE_OPENAI_GPT_IMAGE_ENDPOINT") @@ -76,7 +82,10 @@ class _AzureOpenAISettings(BaseSettings): top_p: float = 0.95 max_tokens: int = 2000 stream: bool = True - api_version: str = "2024-06-01" + api_version: str = Field( + default="2024-06-01", + validation_alias=AliasChoices("AZURE_ENV_OPENAI_API_VERSION", "AZURE_OPENAI_API_VERSION") + ) preview_api_version: str = "2024-02-01" image_api_version: str = Field(default="2025-04-01-preview", alias="AZURE_OPENAI_IMAGE_API_VERSION") diff --git a/src/tests/conftest.py b/src/tests/conftest.py index bd8f06f34..844a2fea9 100644 --- a/src/tests/conftest.py +++ b/src/tests/conftest.py @@ -74,7 +74,7 @@ def mock_environment(monkeypatch): env_vars = { # Azure OpenAI (required - _AzureOpenAISettings) "AZURE_OPENAI_ENDPOINT": "https://test-openai.openai.azure.com/", - "AZURE_OPENAI_API_VERSION": "2024-08-01-preview", + "AZURE_ENV_OPENAI_API_VERSION": "2024-08-01-preview", # Azure Cosmos DB (_CosmosSettings uses AZURE_COSMOS_ prefix) "AZURE_COSMOS_ENDPOINT": "https://test-cosmos.documents.azure.com:443/", diff --git a/src/tests/test_settings.py b/src/tests/test_settings.py index 13968cfde..33f3825f6 100644 --- a/src/tests/test_settings.py +++ b/src/tests/test_settings.py @@ -78,7 +78,7 @@ def test_effective_image_model_returns_image_model(self): with patch.dict(os.environ, { "AZURE_OPENAI_ENDPOINT": "https://test.openai.azure.com", - "AZURE_OPENAI_IMAGE_MODEL": "gpt-image-1.5" + "AZURE_ENV_IMAGE_MODEL_NAME": "gpt-image-1.5" }, clear=False): settings = _AzureOpenAISettings() assert settings.effective_image_model == "gpt-image-1.5" @@ -93,7 +93,7 @@ def test_disabled_with_none_model(self): with patch.dict(os.environ, { "AZURE_OPENAI_ENDPOINT": "https://test.openai.azure.com", - "AZURE_OPENAI_IMAGE_MODEL": "none" + "AZURE_ENV_IMAGE_MODEL_NAME": "none" }, clear=False): settings = _AzureOpenAISettings() assert settings.image_generation_enabled is False @@ -104,7 +104,7 @@ def test_disabled_with_disabled_model(self): with patch.dict(os.environ, { "AZURE_OPENAI_ENDPOINT": "https://test.openai.azure.com", - "AZURE_OPENAI_IMAGE_MODEL": "disabled" + "AZURE_ENV_IMAGE_MODEL_NAME": "disabled" }, clear=False): settings = _AzureOpenAISettings() assert settings.image_generation_enabled is False @@ -115,7 +115,7 @@ def test_enabled_with_valid_model_and_endpoint(self): with patch.dict(os.environ, { "AZURE_OPENAI_ENDPOINT": "https://test.openai.azure.com", - "AZURE_OPENAI_IMAGE_MODEL": "gpt-image-1-mini" + "AZURE_ENV_IMAGE_MODEL_NAME": "gpt-image-1-mini" }, clear=False): settings = _AzureOpenAISettings() assert settings.image_generation_enabled is True From 4211d9dc484d5c3c1d529b25e25eb589b71f173f Mon Sep 17 00:00:00 2001 From: Pavan-Microsoft Date: Tue, 24 Mar 2026 10:36:37 +0530 Subject: [PATCH 17/72] fix: rename environment variable for container registry in Docker build job --- .github/workflows/job-deploy.yml | 2 +- .github/workflows/job-docker-build.yml | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/job-deploy.yml b/.github/workflows/job-deploy.yml index f135c9b89..8d176582e 100644 --- a/.github/workflows/job-deploy.yml +++ b/.github/workflows/job-deploy.yml @@ -666,7 +666,7 @@ jobs: AZURE_ENV_AI_SERVICE_LOCATION: ${{ needs.azure-setup.outputs.AZURE_ENV_AI_SERVICE_LOCATION }} AZURE_LOCATION: ${{ needs.azure-setup.outputs.AZURE_LOCATION }} RESOURCE_GROUP_NAME: ${{ needs.azure-setup.outputs.RESOURCE_GROUP_NAME }} -AZURE_ENV_IMAGE_TAG: ${{ needs.azure-setup.outputs.AZURE_ENV_IMAGE_TAG }} + AZURE_ENV_IMAGE_TAG: ${{ needs.azure-setup.outputs.AZURE_ENV_IMAGE_TAG }} BUILD_DOCKER_IMAGE: ${{ inputs.build_docker_image || 'false' }} EXP: ${{ needs.azure-setup.outputs.EXP_ENABLED }} WAF_ENABLED: ${{ inputs.waf_enabled == true && 'true' || 'false' }} diff --git a/.github/workflows/job-docker-build.yml b/.github/workflows/job-docker-build.yml index 9b927307f..48266fab7 100644 --- a/.github/workflows/job-docker-build.yml +++ b/.github/workflows/job-docker-build.yml @@ -95,7 +95,7 @@ jobs: if: always() shell: bash run: | - AZURE_ENV_CONTAINER_REGISTRY_NAME=$(echo "${{ secrets.ACR_TEST_LOGIN_SERVER }}") + AZURE_ENV_CONTAINER_REGISTRY_SERVER=$(echo "${{ secrets.ACR_TEST_LOGIN_SERVER }}") echo "## 🐳 Docker Build Job Summary" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY echo "| Field | Value |" >> $GITHUB_STEP_SUMMARY @@ -109,8 +109,8 @@ jobs: echo "Successfully built and pushed Docker images to ACR:" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY echo "**Built Images:**" >> $GITHUB_STEP_SUMMARY - echo "- \`${AZURE_ENV_CONTAINER_REGISTRY_NAME}/content-gen-app:${{ steps.generate_docker_tag.outputs.AZURE_ENV_IMAGE_TAG }}\`" >> $GITHUB_STEP_SUMMARY - echo "- \`${AZURE_ENV_CONTAINER_REGISTRY_NAME}/content-gen-api:${{ steps.generate_docker_tag.outputs.AZURE_ENV_IMAGE_TAG }}\`" >> $GITHUB_STEP_SUMMARY + echo "- \`${AZURE_ENV_CONTAINER_REGISTRY_SERVER}/content-gen-app:${{ steps.generate_docker_tag.outputs.AZURE_ENV_IMAGE_TAG }}\`" >> $GITHUB_STEP_SUMMARY + echo "- \`${AZURE_ENV_CONTAINER_REGISTRY_SERVER}/content-gen-api:${{ steps.generate_docker_tag.outputs.AZURE_ENV_IMAGE_TAG }}\`" >> $GITHUB_STEP_SUMMARY else echo "### ❌ Build Failed" >> $GITHUB_STEP_SUMMARY echo "- Docker build process encountered an error" >> $GITHUB_STEP_SUMMARY From 947ae2d1cda242db15406596828fe82c05d3dad5 Mon Sep 17 00:00:00 2001 From: Pavan-Microsoft Date: Tue, 24 Mar 2026 13:52:20 +0530 Subject: [PATCH 18/72] refactor: rename image_model_choice to AZURE_ENV_IMAGE_MODEL_NAME across workflows and scripts --- .github/workflows/deploy-orchestrator.yml | 4 ++-- .github/workflows/deploy-v2.yml | 16 ++++++++-------- .github/workflows/job-deploy.yml | 4 ++-- scripts/checkquota.sh | 2 +- 4 files changed, 13 insertions(+), 13 deletions(-) diff --git a/.github/workflows/deploy-orchestrator.yml b/.github/workflows/deploy-orchestrator.yml index 1fda89a54..82de9002f 100644 --- a/.github/workflows/deploy-orchestrator.yml +++ b/.github/workflows/deploy-orchestrator.yml @@ -61,7 +61,7 @@ on: description: 'Trigger type (workflow_dispatch, pull_request, schedule)' required: true type: string - image_model_choice: + AZURE_ENV_IMAGE_MODEL_NAME: description: 'Image model to deploy (gpt-image-1-mini, gpt-image-1.5, none)' required: false default: 'gpt-image-1-mini' @@ -96,7 +96,7 @@ jobs: docker_image_tag: ${{ needs.docker-build.outputs.AZURE_ENV_IMAGE_TAG }} run_e2e_tests: ${{ inputs.run_e2e_tests }} cleanup_resources: ${{ inputs.cleanup_resources }} - image_model_choice: ${{ inputs.image_model_choice }} + AZURE_ENV_IMAGE_MODEL_NAME: ${{ inputs.AZURE_ENV_IMAGE_MODEL_NAME }} secrets: inherit e2e-test: diff --git a/.github/workflows/deploy-v2.yml b/.github/workflows/deploy-v2.yml index fe8572460..a7ede3ccd 100644 --- a/.github/workflows/deploy-v2.yml +++ b/.github/workflows/deploy-v2.yml @@ -100,7 +100,7 @@ on: required: false default: '' type: string - image_model_choice: + AZURE_ENV_IMAGE_MODEL_NAME: description: 'Image Model to Deploy' required: false default: 'gpt-image-1-mini' @@ -133,7 +133,7 @@ jobs: AZURE_ENV_LOG_ANALYTICS_WORKSPACE_RID: ${{ steps.validate.outputs.AZURE_ENV_LOG_ANALYTICS_WORKSPACE_RID }} AZURE_ENV_FOUNDRY_PROJECT_RID: ${{ steps.validate.outputs.AZURE_ENV_FOUNDRY_PROJECT_RID }} existing_webapp_url: ${{ steps.validate.outputs.existing_webapp_url }} - image_model_choice: ${{ steps.validate.outputs.image_model_choice }} + AZURE_ENV_IMAGE_MODEL_NAME: ${{ steps.validate.outputs.AZURE_ENV_IMAGE_MODEL_NAME }} steps: - name: Validate Workflow Input Parameters id: validate @@ -150,7 +150,7 @@ jobs: INPUT_AZURE_ENV_LOG_ANALYTICS_WORKSPACE_RID: ${{ github.event.inputs.AZURE_ENV_LOG_ANALYTICS_WORKSPACE_RID }} INPUT_AZURE_ENV_FOUNDRY_PROJECT_RID: ${{ github.event.inputs.AZURE_ENV_FOUNDRY_PROJECT_RID }} INPUT_EXISTING_WEBAPP_URL: ${{ github.event.inputs.existing_webapp_url }} - INPUT_IMAGE_MODEL_CHOICE: ${{ github.event.inputs.image_model_choice }} + INPUT_IMAGE_MODEL_CHOICE: ${{ github.event.inputs.AZURE_ENV_IMAGE_MODEL_NAME }} run: | echo "🔍 Validating workflow input parameters..." VALIDATION_FAILED=false @@ -306,15 +306,15 @@ jobs: echo "AZURE_ENV_FOUNDRY_PROJECT_RID=$INPUT_AZURE_ENV_FOUNDRY_PROJECT_RID" >> $GITHUB_OUTPUT echo "existing_webapp_url=$INPUT_EXISTING_WEBAPP_URL" >> $GITHUB_OUTPUT - # Validate and output image_model_choice + # Validate and output AZURE_ENV_IMAGE_MODEL_NAME IMAGE_MODEL="${INPUT_IMAGE_MODEL_CHOICE:-gpt-image-1-mini}" ALLOWED_MODELS=("gpt-image-1-mini" "gpt-image-1.5" "none") if [[ ! " ${ALLOWED_MODELS[@]} " =~ " ${IMAGE_MODEL} " ]]; then - echo "❌ ERROR: image_model_choice '$IMAGE_MODEL' is invalid. Allowed: ${ALLOWED_MODELS[*]}" + echo "❌ ERROR: AZURE_ENV_IMAGE_MODEL_NAME '$IMAGE_MODEL' is invalid. Allowed: ${ALLOWED_MODELS[*]}" exit 1 fi - echo "✅ image_model_choice: '$IMAGE_MODEL' is valid" - echo "image_model_choice=$IMAGE_MODEL" >> $GITHUB_OUTPUT + echo "✅ AZURE_ENV_IMAGE_MODEL_NAME: '$IMAGE_MODEL' is valid" + echo "AZURE_ENV_IMAGE_MODEL_NAME=$IMAGE_MODEL" >> $GITHUB_OUTPUT Run: needs: validate-inputs @@ -333,5 +333,5 @@ jobs: AZURE_ENV_FOUNDRY_PROJECT_RID: ${{ needs.validate-inputs.outputs.AZURE_ENV_FOUNDRY_PROJECT_RID || '' }} existing_webapp_url: ${{ needs.validate-inputs.outputs.existing_webapp_url || '' }} trigger_type: ${{ github.event_name }} - image_model_choice: ${{ needs.validate-inputs.outputs.image_model_choice || 'gpt-image-1-mini' }} + AZURE_ENV_IMAGE_MODEL_NAME: ${{ needs.validate-inputs.outputs.AZURE_ENV_IMAGE_MODEL_NAME || 'gpt-image-1-mini' }} secrets: inherit diff --git a/.github/workflows/job-deploy.yml b/.github/workflows/job-deploy.yml index 8d176582e..813c044d0 100644 --- a/.github/workflows/job-deploy.yml +++ b/.github/workflows/job-deploy.yml @@ -66,7 +66,7 @@ on: required: false default: '' type: string - image_model_choice: + AZURE_ENV_IMAGE_MODEL_NAME: description: 'Image model to deploy (gpt-image-1-mini, gpt-image-1.5, none)' required: false default: 'gpt-image-1-mini' @@ -334,7 +334,7 @@ jobs: env: AZURE_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }} GPT_MIN_CAPACITY: ${{ env.GPT_MIN_CAPACITY }} - IMAGE_MODEL_CHOICE: ${{ inputs.image_model_choice || 'gpt-image-1-mini' }} + AZURE_ENV_IMAGE_MODEL_NAME: ${{ inputs.AZURE_ENV_IMAGE_MODEL_NAME || 'gpt-image-1-mini' }} IMAGE_MODEL_MIN_CAPACITY: ${{ env.IMAGE_MODEL_MIN_CAPACITY }} AZURE_REGIONS: ${{ vars.AZURE_REGIONS }} run: | diff --git a/scripts/checkquota.sh b/scripts/checkquota.sh index 7b87424bd..2b5530ae3 100644 --- a/scripts/checkquota.sh +++ b/scripts/checkquota.sh @@ -9,7 +9,7 @@ # Auto-detects mode based on environment variables. # # Usage (local): -# bash checkquota.sh [image_model_choice] +# bash checkquota.sh [AZURE_ENV_IMAGE_MODEL_NAME] # bash checkquota.sh gpt-image-1-mini # bash checkquota.sh none # From bd0aff56f6a2cfff6f86b0740908b914f1c9b0e5 Mon Sep 17 00:00:00 2001 From: Pavan-Microsoft Date: Tue, 24 Mar 2026 14:51:00 +0530 Subject: [PATCH 19/72] fix: update environment variable references for image model in documentation and scripts --- docs/CustomizingAzdParameters.md | 10 +++++----- scripts/checkquota.sh | 18 +++++++++--------- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/docs/CustomizingAzdParameters.md b/docs/CustomizingAzdParameters.md index 0162395b6..5c07e063b 100644 --- a/docs/CustomizingAzdParameters.md +++ b/docs/CustomizingAzdParameters.md @@ -17,12 +17,12 @@ By default this template will use the environment name as the prefix to prevent | `AZURE_ENV_GPT_MODEL_CAPACITY` | integer | `150` | Sets the GPT model token capacity (minimum: `10`). | | `AZURE_ENV_IMAGE_MODEL_NAME` | string | `gpt-image-1-mini` | Image model to deploy (allowed: `gpt-image-1-mini`, `gpt-image-1.5`, `none`). | | `AZURE_ENV_IMAGE_MODEL_CAPACITY` | integer | `1` | Sets the image model deployment capacity in RPM (minimum: `1`). | -| `AZURE_ENV_OPENAI_API_VERSION` | string | `2025-01-01-preview` | Specifies the API version for Azure OpenAI service. | +| `AZURE_ENV_OPENAI_API_VERSION` | string | `2025-01-01-preview` | Specifies the API version for Azure OpenAI service. | | `AZURE_ENV_AI_SERVICE_LOCATION` | string | `` | Sets the Azure region for OpenAI resource deployment. | -| `AZURE_ENV_LOG_ANALYTICS_WORKSPACE_RID` | string | `""` | Reuses an existing Log Analytics Workspace instead of creating a new one. | -| `AZURE_ENV_FOUNDRY_PROJECT_RID`| string | `""` | Reuses an existing AI Foundry Project instead of creating a new one. | -| `AZURE_ENV_CONTAINER_REGISTRY_NAME` | string | `contentgencontainerreg` | Sets the existing Azure Container Registry name (without `.azurecr.io`). | -| `AZURE_ENV_IMAGE_TAG` | string | `latest` | Sets the container image tag (e.g., `latest`, `dev`, `hotfix`). | +| `AZURE_ENV_LOG_ANALYTICS_WORKSPACE_RID` | string | `""` | Reuses an existing Log Analytics Workspace instead of creating a new one. | +| `AZURE_ENV_FOUNDRY_PROJECT_RID` | string | `""` | Reuses an existing AI Foundry Project instead of creating a new one. | +| `AZURE_ENV_CONTAINER_REGISTRY_NAME` | string | `contentgencontainerreg` | Sets the existing Azure Container Registry name (without `.azurecr.io`). | +| `AZURE_ENV_IMAGE_TAG` | string | `latest` | Sets the container image tag (e.g., `latest`, `dev`, `hotfix`). | ## How to Set a Parameter diff --git a/scripts/checkquota.sh b/scripts/checkquota.sh index 2b5530ae3..af7f37a76 100644 --- a/scripts/checkquota.sh +++ b/scripts/checkquota.sh @@ -14,7 +14,7 @@ # bash checkquota.sh none # # Usage (CI - via env vars): -# Set AZURE_SUBSCRIPTION_ID, GPT_MIN_CAPACITY, AZURE_REGIONS, IMAGE_MODEL_CHOICE +# Set AZURE_SUBSCRIPTION_ID, GPT_MIN_CAPACITY, AZURE_REGIONS, AZURE_ENV_IMAGE_MODEL_NAME # Authentication is handled externally via OIDC (az login already done before this script runs) # ============================================================================= @@ -28,9 +28,9 @@ fi # ---- Configuration ---- # In local mode, image model can be passed as first argument if [[ "$RUN_MODE" == "local" ]]; then - IMAGE_MODEL_CHOICE="${1:-${IMAGE_MODEL_CHOICE:-gpt-image-1-mini}}" + AZURE_ENV_IMAGE_MODEL_NAME="${1:-${AZURE_ENV_IMAGE_MODEL_NAME:-gpt-image-1-mini}}" else - IMAGE_MODEL_CHOICE="${IMAGE_MODEL_CHOICE:-gpt-image-1-mini}" + AZURE_ENV_IMAGE_MODEL_NAME="${AZURE_ENV_IMAGE_MODEL_NAME:-gpt-image-1-mini}" fi GPT_MIN_CAPACITY="${GPT_MIN_CAPACITY:-150}" @@ -53,8 +53,8 @@ IMAGE_MODEL_QUOTA_NAME=( # ---- Validate image model choice ---- ALLOWED_MODELS=("gpt-image-1-mini" "gpt-image-1.5" "none") -if [[ ! " ${ALLOWED_MODELS[@]} " =~ " ${IMAGE_MODEL_CHOICE} " ]]; then - echo "❌ ERROR: Invalid image model choice: '$IMAGE_MODEL_CHOICE'" +if [[ ! " ${ALLOWED_MODELS[@]} " =~ " ${AZURE_ENV_IMAGE_MODEL_NAME} " ]]; then + echo "❌ ERROR: Invalid image model choice: '$AZURE_ENV_IMAGE_MODEL_NAME'" echo " Allowed values: ${ALLOWED_MODELS[*]}" exit 1 fi @@ -91,7 +91,7 @@ fi echo "" echo "📋 Configuration:" echo " Mode: $RUN_MODE" -echo " Image Model Choice: $IMAGE_MODEL_CHOICE" +echo " Image Model Choice: $AZURE_ENV_IMAGE_MODEL_NAME" echo " GPT Min Capacity: $GPT_MIN_CAPACITY" echo " Image Model Min Capacity: $IMAGE_MODEL_MIN_CAPACITY" echo " Regions to check: ${REGIONS[*]}" @@ -104,10 +104,10 @@ MIN_CAPACITY=( ) # Add image model to quota check if not 'none' -IMAGE_QUOTA_NAME="${IMAGE_MODEL_QUOTA_NAME[$IMAGE_MODEL_CHOICE]}" +IMAGE_QUOTA_NAME="${IMAGE_MODEL_QUOTA_NAME[$AZURE_ENV_IMAGE_MODEL_NAME]}" if [[ -n "$IMAGE_QUOTA_NAME" ]]; then MIN_CAPACITY["$IMAGE_QUOTA_NAME"]=$IMAGE_MODEL_MIN_CAPACITY - echo "🖼️ Image model '$IMAGE_MODEL_CHOICE' added to quota check (key: $IMAGE_QUOTA_NAME, min capacity: $IMAGE_MODEL_MIN_CAPACITY)" + echo "🖼️ Image model '$AZURE_ENV_IMAGE_MODEL_NAME' added to quota check (key: $IMAGE_QUOTA_NAME, min capacity: $IMAGE_MODEL_MIN_CAPACITY)" else echo "ℹ️ Image model set to 'none' — skipping image model quota check." fi @@ -170,7 +170,7 @@ echo "" echo "========================================" if [ -z "$VALID_REGION" ]; then echo "❌ No region with sufficient quota found!" - echo " Image Model: $IMAGE_MODEL_CHOICE" + echo " Image Model: $AZURE_ENV_IMAGE_MODEL_NAME" echo " Checked regions: ${REGIONS[*]}" # In CI mode, set GITHUB_ENV variable instead of exiting with error From 14b354792ef22ecd7bd13c5487b145296411b3c9 Mon Sep 17 00:00:00 2001 From: Pavan-Microsoft Date: Tue, 24 Mar 2026 16:49:23 +0530 Subject: [PATCH 20/72] fix: update usage instructions and add legacy environment variable support in tests --- scripts/checkquota.sh | 2 +- src/tests/conftest.py | 1 + src/tests/test_settings.py | 35 +++++++++++++++++++++++++++++++++++ 3 files changed, 37 insertions(+), 1 deletion(-) diff --git a/scripts/checkquota.sh b/scripts/checkquota.sh index af7f37a76..f7a21131c 100644 --- a/scripts/checkquota.sh +++ b/scripts/checkquota.sh @@ -9,7 +9,7 @@ # Auto-detects mode based on environment variables. # # Usage (local): -# bash checkquota.sh [AZURE_ENV_IMAGE_MODEL_NAME] +# bash checkquota.sh [model] # e.g. gpt-image-1-mini | gpt-image-1.5 | none # bash checkquota.sh gpt-image-1-mini # bash checkquota.sh none # diff --git a/src/tests/conftest.py b/src/tests/conftest.py index 844a2fea9..8cff0bac9 100644 --- a/src/tests/conftest.py +++ b/src/tests/conftest.py @@ -75,6 +75,7 @@ def mock_environment(monkeypatch): # Azure OpenAI (required - _AzureOpenAISettings) "AZURE_OPENAI_ENDPOINT": "https://test-openai.openai.azure.com/", "AZURE_ENV_OPENAI_API_VERSION": "2024-08-01-preview", + "AZURE_OPENAI_API_VERSION": "2024-08-01-preview", # Legacy for backward compatibility test # Azure Cosmos DB (_CosmosSettings uses AZURE_COSMOS_ prefix) "AZURE_COSMOS_ENDPOINT": "https://test-cosmos.documents.azure.com:443/", diff --git a/src/tests/test_settings.py b/src/tests/test_settings.py index 33f3825f6..6b5dd2a9e 100644 --- a/src/tests/test_settings.py +++ b/src/tests/test_settings.py @@ -83,6 +83,41 @@ def test_effective_image_model_returns_image_model(self): settings = _AzureOpenAISettings() assert settings.effective_image_model == "gpt-image-1.5" + def test_image_model_with_legacy_env_var(self): + """Test image_model loads from legacy AZURE_OPENAI_IMAGE_MODEL variable.""" + from settings import _AzureOpenAISettings + + with patch.dict(os.environ, { + "AZURE_OPENAI_ENDPOINT": "https://test.openai.azure.com", + "AZURE_OPENAI_IMAGE_MODEL": "gpt-image-1.5" + }, clear=False): + settings = _AzureOpenAISettings() + assert settings.image_model == "gpt-image-1.5" + assert settings.effective_image_model == "gpt-image-1.5" + + def test_gpt_model_with_legacy_env_var(self): + """Test gpt_model loads from legacy AZURE_OPENAI_GPT_MODEL variable.""" + from settings import _AzureOpenAISettings + + with patch.dict(os.environ, { + "AZURE_OPENAI_ENDPOINT": "https://test.openai.azure.com", + "AZURE_OPENAI_GPT_MODEL": "gpt-4o" + }, clear=False): + settings = _AzureOpenAISettings() + assert settings.gpt_model == "gpt-4o" + + def test_api_version_with_legacy_env_var(self): + """Test api_version loads from legacy AZURE_OPENAI_API_VERSION variable.""" + from settings import _AzureOpenAISettings + + with patch.dict(os.environ, { + "AZURE_OPENAI_ENDPOINT": "https://test.openai.azure.com", + "AZURE_OPENAI_API_VERSION": "2023-12-01-preview", + "AZURE_ENV_OPENAI_API_VERSION": "" # Clear new env var to test legacy + }, clear=False): + settings = _AzureOpenAISettings() + assert settings.api_version == "2023-12-01-preview" + class TestImageGenerationEnabled: """Tests for image_generation_enabled property logic.""" From 8cf7f41bf64d7da780f6448a455ccd22a856f0e3 Mon Sep 17 00:00:00 2001 From: v-maddukuriy Date: Thu, 26 Mar 2026 18:19:29 +0530 Subject: [PATCH 21/72] feat: add support for deploying Azure Bastion and Jumpbox resources in private networking --- docs/CustomizingAzdParameters.md | 3 +- infra/main.bicep | 10 ++- infra/main.json | 126 ++++++----------------------- infra/main.waf.parameters.json | 3 + infra/modules/virtualNetwork.bicep | 13 +-- 5 files changed, 44 insertions(+), 111 deletions(-) diff --git a/docs/CustomizingAzdParameters.md b/docs/CustomizingAzdParameters.md index c2bffc67d..f326d65cc 100644 --- a/docs/CustomizingAzdParameters.md +++ b/docs/CustomizingAzdParameters.md @@ -25,9 +25,10 @@ By default this template will use the environment name as the prefix to prevent | `enableScalability` | boolean | `false` | Enable auto-scaling and higher SKUs (WAF-aligned). | | `enableRedundancy` | boolean | `false` | Enable zone redundancy and geo-replication (WAF-aligned). | | `enablePrivateNetworking` | boolean | `false` | Enable VNet integration and private endpoints (WAF-aligned). | +| `deployBastionAndJumpbox` | boolean | `false` | Deploy Azure Bastion and jumpbox admin-path resources when private networking is enabled. | | `AZURE_ENV_VM_SIZE` | string | `""` | Overrides the jumpbox VM size (private networking only). Must support accelerated networking and Premium SSD. | | `AZURE_ENV_VM_ADMIN_USERNAME` | string | `""` | Sets the jumpbox VM admin username (private networking only). | -| `AZURE_ENV_VM_ADMIN_PASSWORD` | string | `""` | Sets the jumpbox VM admin password. Required to deploy the jumpbox when private networking is enabled. | +| `AZURE_ENV_VM_ADMIN_PASSWORD` | string | `""` | Sets the jumpbox VM admin password. Required when `deployBastionAndJumpbox=true`. | | `ACR_NAME` | string | `contentgencontainerreg` | Sets the existing Azure Container Registry name (without `.azurecr.io`). | | `IMAGE_TAG` | string | `latest` | Sets the container image tag (e.g., `latest`, `dev`, `hotfix`). | diff --git a/infra/main.bicep b/infra/main.bicep index 651de80c7..19a7b2ed3 100644 --- a/infra/main.bicep +++ b/infra/main.bicep @@ -107,6 +107,9 @@ param existingLogAnalyticsWorkspaceId string = '' @description('Optional. Resource ID of an existing Foundry project.') param azureExistingAIProjectResourceId string = '' +@description('Optional. Deploy Azure Bastion and Jumpbox resources for private network administration.') +param deployBastionAndJumpbox bool = false + @description('Optional. Jumpbox VM size. Must support accelerated networking and Premium SSD.') param vmSize string = '' @@ -380,6 +383,7 @@ module virtualNetwork 'modules/virtualNetwork.bicep' = if (enablePrivateNetworki name: 'vnet-${solutionSuffix}' addressPrefixes: ['10.0.0.0/20'] // 4096 addresses (enough for 8 /23 subnets or 16 /24) location: location + deployBastionAndJumpbox: enablePrivateNetworking && deployBastionAndJumpbox && !empty(vmAdminPassword) tags: tags logAnalyticsWorkspaceId: logAnalyticsWorkspaceResourceId resourceSuffix: solutionSuffix @@ -401,8 +405,8 @@ var zoneSupportedJumpboxLocations = [ 'uksouth' 'westus3' ] -var deployJumpbox = enablePrivateNetworking && !empty(vmAdminPassword) -module bastionHost 'br/public:avm/res/network/bastion-host:0.8.2' = if (enablePrivateNetworking) { +var deployAdminAccessResources = enablePrivateNetworking && deployBastionAndJumpbox && !empty(vmAdminPassword) +module bastionHost 'br/public:avm/res/network/bastion-host:0.8.2' = if (deployAdminAccessResources) { name: take('avm.res.network.bastion-host.${bastionHostName}', 64) params: { name: bastionHostName @@ -431,7 +435,7 @@ module bastionHost 'br/public:avm/res/network/bastion-host:0.8.2' = if (enablePr // Jumpbox Virtual Machine var jumpboxVmName = take('vm-jumpbox-${solutionSuffix}', 15) -module jumpboxVM 'br/public:avm/res/compute/virtual-machine:0.21.0' = if (deployJumpbox) { +module jumpboxVM 'br/public:avm/res/compute/virtual-machine:0.21.0' = if (deployAdminAccessResources) { name: take('avm.res.compute.virtual-machine.${jumpboxVmName}', 64) params: { name: take(jumpboxVmName, 15) diff --git a/infra/main.json b/infra/main.json index c67b44025..fad7839c9 100644 --- a/infra/main.json +++ b/infra/main.json @@ -6,7 +6,7 @@ "_generator": { "name": "bicep", "version": "0.41.2.15936", - "templateHash": "17721141165286158425" + "templateHash": "11738775177613917473" }, "name": "Intelligent Content Generation Accelerator", "description": "Solution Accelerator for multimodal marketing content generation using Microsoft Agent Framework.\n" @@ -169,6 +169,13 @@ "description": "Optional. Resource ID of an existing Foundry project." } }, + "deployBastionAndJumpbox": { + "type": "bool", + "defaultValue": false, + "metadata": { + "description": "Optional. Deploy Azure Bastion and Jumpbox resources for private network administration." + } + }, "vmSize": { "type": "string", "defaultValue": "", @@ -351,7 +358,7 @@ "uksouth", "westus3" ], - "deployJumpbox": "[and(parameters('enablePrivateNetworking'), not(empty(parameters('vmAdminPassword'))))]", + "deployAdminAccessResources": "[and(and(parameters('enablePrivateNetworking'), parameters('deployBastionAndJumpbox')), not(empty(parameters('vmAdminPassword'))))]", "jumpboxVmName": "[take(format('vm-jumpbox-{0}', variables('solutionSuffix')), 15)]", "privateDnsZones": [ "privatelink.cognitiveservices.azure.com", @@ -4851,6 +4858,9 @@ "location": { "value": "[parameters('location')]" }, + "deployBastionAndJumpbox": { + "value": "[and(and(parameters('enablePrivateNetworking'), parameters('deployBastionAndJumpbox')), not(empty(parameters('vmAdminPassword'))))]" + }, "tags": { "value": "[parameters('tags')]" }, @@ -4869,7 +4879,7 @@ "_generator": { "name": "bicep", "version": "0.41.2.15936", - "templateHash": "1152564857842534701" + "templateHash": "11359193981707837191" } }, "parameters": { @@ -4895,6 +4905,13 @@ "description": "Required. An Array of 1 or more IP Address Prefixes for the Virtual Network." } }, + "deployBastionAndJumpbox": { + "type": "bool", + "defaultValue": false, + "metadata": { + "description": "Optional. Deploy Azure Bastion and Jumpbox subnets for VM-based administration." + } + }, "tags": { "type": "object", "defaultValue": {}, @@ -5026,102 +5043,7 @@ } } ], - "bastionSubnets": [ - { - "name": "AzureBastionSubnet", - "addressPrefixes": [ - "10.0.10.0/26" - ], - "networkSecurityGroup": { - "name": "nsg-bastion", - "securityRules": [ - { - "name": "AllowGatewayManager", - "properties": { - "access": "Allow", - "direction": "Inbound", - "priority": 2702, - "protocol": "*", - "sourcePortRange": "*", - "destinationPortRange": "443", - "sourceAddressPrefix": "GatewayManager", - "destinationAddressPrefix": "*" - } - }, - { - "name": "AllowHttpsInBound", - "properties": { - "access": "Allow", - "direction": "Inbound", - "priority": 2703, - "protocol": "*", - "sourcePortRange": "*", - "destinationPortRange": "443", - "sourceAddressPrefix": "Internet", - "destinationAddressPrefix": "*" - } - }, - { - "name": "AllowSshRdpOutbound", - "properties": { - "access": "Allow", - "direction": "Outbound", - "priority": 100, - "protocol": "*", - "sourcePortRange": "*", - "destinationPortRanges": [ - "22", - "3389" - ], - "sourceAddressPrefix": "*", - "destinationAddressPrefix": "VirtualNetwork" - } - }, - { - "name": "AllowAzureCloudOutbound", - "properties": { - "access": "Allow", - "direction": "Outbound", - "priority": 110, - "protocol": "Tcp", - "sourcePortRange": "*", - "destinationPortRange": "443", - "sourceAddressPrefix": "*", - "destinationAddressPrefix": "AzureCloud" - } - } - ] - } - }, - { - "name": "jumpbox", - "addressPrefixes": [ - "10.0.12.0/23" - ], - "networkSecurityGroup": { - "name": "nsg-jumpbox", - "securityRules": [ - { - "name": "AllowRdpFromBastion", - "properties": { - "access": "Allow", - "direction": "Inbound", - "priority": 100, - "protocol": "Tcp", - "sourcePortRange": "*", - "destinationPortRange": "3389", - "sourceAddressPrefixes": [ - "10.0.10.0/26" - ], - "destinationAddressPrefixes": [ - "10.0.12.0/23" - ] - } - } - ] - } - } - ], + "bastionSubnets": "[if(parameters('deployBastionAndJumpbox'), createArray(createObject('name', 'AzureBastionSubnet', 'addressPrefixes', createArray('10.0.10.0/26'), 'networkSecurityGroup', createObject('name', 'nsg-bastion', 'securityRules', createArray(createObject('name', 'AllowGatewayManager', 'properties', createObject('access', 'Allow', 'direction', 'Inbound', 'priority', 2702, 'protocol', '*', 'sourcePortRange', '*', 'destinationPortRange', '443', 'sourceAddressPrefix', 'GatewayManager', 'destinationAddressPrefix', '*')), createObject('name', 'AllowHttpsInBound', 'properties', createObject('access', 'Allow', 'direction', 'Inbound', 'priority', 2703, 'protocol', '*', 'sourcePortRange', '*', 'destinationPortRange', '443', 'sourceAddressPrefix', 'Internet', 'destinationAddressPrefix', '*')), createObject('name', 'AllowSshRdpOutbound', 'properties', createObject('access', 'Allow', 'direction', 'Outbound', 'priority', 100, 'protocol', '*', 'sourcePortRange', '*', 'destinationPortRanges', createArray('22', '3389'), 'sourceAddressPrefix', '*', 'destinationAddressPrefix', 'VirtualNetwork')), createObject('name', 'AllowAzureCloudOutbound', 'properties', createObject('access', 'Allow', 'direction', 'Outbound', 'priority', 110, 'protocol', 'Tcp', 'sourcePortRange', '*', 'destinationPortRange', '443', 'sourceAddressPrefix', '*', 'destinationAddressPrefix', 'AzureCloud'))))), createObject('name', 'jumpbox', 'addressPrefixes', createArray('10.0.12.0/23'), 'networkSecurityGroup', createObject('name', 'nsg-jumpbox', 'securityRules', createArray(createObject('name', 'AllowRdpFromBastion', 'properties', createObject('access', 'Allow', 'direction', 'Inbound', 'priority', 100, 'protocol', 'Tcp', 'sourcePortRange', '*', 'destinationPortRange', '3389', 'sourceAddressPrefixes', createArray('10.0.10.0/26'), 'destinationAddressPrefixes', createArray('10.0.12.0/23'))))))), createArray())]", "vnetSubnets": "[concat(variables('coreSubnets'), variables('bastionSubnets'))]" }, "resources": [ @@ -7504,7 +7426,7 @@ ] }, "bastionHost": { - "condition": "[parameters('enablePrivateNetworking')]", + "condition": "[variables('deployAdminAccessResources')]", "type": "Microsoft.Resources/deployments", "apiVersion": "2025-04-01", "name": "[take(format('avm.res.network.bastion-host.{0}', variables('bastionHostName')), 64)]", @@ -9245,7 +9167,7 @@ ] }, "jumpboxVM": { - "condition": "[variables('deployJumpbox')]", + "condition": "[variables('deployAdminAccessResources')]", "type": "Microsoft.Resources/deployments", "apiVersion": "2025-04-01", "name": "[take(format('avm.res.compute.virtual-machine.{0}', variables('jumpboxVmName')), 64)]", @@ -24951,8 +24873,8 @@ }, "dependsOn": [ "aiFoundryAiServices", - "[format('avmPrivateDnsZones[{0}]', variables('dnsZoneIndex').openAI)]", "[format('avmPrivateDnsZones[{0}]', variables('dnsZoneIndex').cognitiveServices)]", + "[format('avmPrivateDnsZones[{0}]', variables('dnsZoneIndex').openAI)]", "virtualNetwork" ] }, diff --git a/infra/main.waf.parameters.json b/infra/main.waf.parameters.json index 307bc2230..587d28cdf 100644 --- a/infra/main.waf.parameters.json +++ b/infra/main.waf.parameters.json @@ -56,6 +56,9 @@ "enableScalability": { "value": true }, + "deployBastionAndJumpbox": { + "value": true + }, "vmAdminUsername": { "value": "${AZURE_ENV_VM_ADMIN_USERNAME}" }, diff --git a/infra/modules/virtualNetwork.bicep b/infra/modules/virtualNetwork.bicep index 5ec807734..77ab6895d 100644 --- a/infra/modules/virtualNetwork.bicep +++ b/infra/modules/virtualNetwork.bicep @@ -2,7 +2,7 @@ // Networking - NSGs, VNET and Subnets for Content Generation Solution /****************************************************************************************************************************/ @description('Name of the virtual network.') -param name string +param name string @description('Azure region to deploy resources.') param location string = resourceGroup().location @@ -10,6 +10,9 @@ param location string = resourceGroup().location @description('Required. An Array of 1 or more IP Address Prefixes for the Virtual Network.') param addressPrefixes array = ['10.0.0.0/20'] +@description('Optional. Deploy Azure Bastion and Jumpbox subnets for VM-based administration.') +param deployBastionAndJumpbox bool = false + @description('An array of subnets to be created within the virtual network.') // Core subnets: web (App Service), peps (Private Endpoints), aci (Container Instance) // Optional: AzureBastionSubnet and jumpbox (only when deployBastionAndJumpbox is true) @@ -98,7 +101,7 @@ var coreSubnets = [ } ] -// Bastion and Jumpbox subnets (always deployed with private networking) +// Bastion and Jumpbox subnets (only deployed when deployBastionAndJumpbox is true) // VM Size Notes: // 1 B-series VMs (like Standard_B2ms) do not support accelerated networking. // 2 Pick a VM size that supports accelerated networking + Premium SSD (the usual jump-box candidates): @@ -107,7 +110,7 @@ var coreSubnets = [ // Standard_D2s_v4 (2 vCPU, 8 GiB RAM, Premium SSD) // Previous gen, also broadly available. // Standard_DS2_v2 (2 vCPU, 7 GiB RAM, Premium SSD) // Legacy SKU, being retired from some regions - avoid for new deployments. // 3 A-series (Av2) is NOT suitable: no Premium SSD support, no accelerated networking. -var bastionSubnets = [ +var bastionSubnets = deployBastionAndJumpbox ? [ { name: 'AzureBastionSubnet' addressPrefixes: ['10.0.10.0/26'] @@ -191,7 +194,7 @@ var bastionSubnets = [ ] } } -] +] : [] var vnetSubnets = concat(coreSubnets, bastionSubnets) @@ -270,6 +273,6 @@ output webSubnetResourceId string = contains(map(vnetSubnets, subnet => subnet.n output pepsSubnetResourceId string = contains(map(vnetSubnets, subnet => subnet.name), 'peps') ? virtualNetwork.outputs.subnetResourceIds[indexOf(map(vnetSubnets, subnet => subnet.name), 'peps')] : '' output aciSubnetResourceId string = contains(map(vnetSubnets, subnet => subnet.name), 'aci') ? virtualNetwork.outputs.subnetResourceIds[indexOf(map(vnetSubnets, subnet => subnet.name), 'aci')] : '' -// Bastion/jumpbox subnet outputs (always present with private networking) +// Bastion/jumpbox subnet outputs (present only when deployBastionAndJumpbox is true) output bastionSubnetResourceId string = contains(map(vnetSubnets, subnet => subnet.name), 'AzureBastionSubnet') ? virtualNetwork.outputs.subnetResourceIds[indexOf(map(vnetSubnets, subnet => subnet.name), 'AzureBastionSubnet')] : '' output jumpboxSubnetResourceId string = contains(map(vnetSubnets, subnet => subnet.name), 'jumpbox') ? virtualNetwork.outputs.subnetResourceIds[indexOf(map(vnetSubnets, subnet => subnet.name), 'jumpbox')] : '' From 887db52726f777deaf72e9b021f41ff50c05be19 Mon Sep 17 00:00:00 2001 From: v-maddukuriy Date: Thu, 26 Mar 2026 19:20:58 +0530 Subject: [PATCH 22/72] fix: resolve Copilot review comments - deployment condition, output comment, and docs clarity --- docs/CustomizingAzdParameters.md | 2 +- infra/main.bicep | 2 +- infra/main.json | 6 +++--- infra/modules/virtualNetwork.bicep | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/CustomizingAzdParameters.md b/docs/CustomizingAzdParameters.md index f326d65cc..dd6cbcb89 100644 --- a/docs/CustomizingAzdParameters.md +++ b/docs/CustomizingAzdParameters.md @@ -28,7 +28,7 @@ By default this template will use the environment name as the prefix to prevent | `deployBastionAndJumpbox` | boolean | `false` | Deploy Azure Bastion and jumpbox admin-path resources when private networking is enabled. | | `AZURE_ENV_VM_SIZE` | string | `""` | Overrides the jumpbox VM size (private networking only). Must support accelerated networking and Premium SSD. | | `AZURE_ENV_VM_ADMIN_USERNAME` | string | `""` | Sets the jumpbox VM admin username (private networking only). | -| `AZURE_ENV_VM_ADMIN_PASSWORD` | string | `""` | Sets the jumpbox VM admin password. Required when `deployBastionAndJumpbox=true`. | +| `AZURE_ENV_VM_ADMIN_PASSWORD` | string | `""` | Sets the jumpbox VM admin password. Bastion and jumpbox resources are deployed only when this is set and `deployBastionAndJumpbox=true`. | | `ACR_NAME` | string | `contentgencontainerreg` | Sets the existing Azure Container Registry name (without `.azurecr.io`). | | `IMAGE_TAG` | string | `latest` | Sets the container image tag (e.g., `latest`, `dev`, `hotfix`). | diff --git a/infra/main.bicep b/infra/main.bicep index 19a7b2ed3..5e48eda7b 100644 --- a/infra/main.bicep +++ b/infra/main.bicep @@ -405,7 +405,7 @@ var zoneSupportedJumpboxLocations = [ 'uksouth' 'westus3' ] -var deployAdminAccessResources = enablePrivateNetworking && deployBastionAndJumpbox && !empty(vmAdminPassword) +var deployAdminAccessResources = enablePrivateNetworking && deployBastionAndJumpbox module bastionHost 'br/public:avm/res/network/bastion-host:0.8.2' = if (deployAdminAccessResources) { name: take('avm.res.network.bastion-host.${bastionHostName}', 64) params: { diff --git a/infra/main.json b/infra/main.json index fad7839c9..48b55001c 100644 --- a/infra/main.json +++ b/infra/main.json @@ -6,7 +6,7 @@ "_generator": { "name": "bicep", "version": "0.41.2.15936", - "templateHash": "11738775177613917473" + "templateHash": "14899763561101785269" }, "name": "Intelligent Content Generation Accelerator", "description": "Solution Accelerator for multimodal marketing content generation using Microsoft Agent Framework.\n" @@ -358,7 +358,7 @@ "uksouth", "westus3" ], - "deployAdminAccessResources": "[and(and(parameters('enablePrivateNetworking'), parameters('deployBastionAndJumpbox')), not(empty(parameters('vmAdminPassword'))))]", + "deployAdminAccessResources": "[and(parameters('enablePrivateNetworking'), parameters('deployBastionAndJumpbox'))]", "jumpboxVmName": "[take(format('vm-jumpbox-{0}', variables('solutionSuffix')), 15)]", "privateDnsZones": [ "privatelink.cognitiveservices.azure.com", @@ -24873,8 +24873,8 @@ }, "dependsOn": [ "aiFoundryAiServices", - "[format('avmPrivateDnsZones[{0}]', variables('dnsZoneIndex').cognitiveServices)]", "[format('avmPrivateDnsZones[{0}]', variables('dnsZoneIndex').openAI)]", + "[format('avmPrivateDnsZones[{0}]', variables('dnsZoneIndex').cognitiveServices)]", "virtualNetwork" ] }, diff --git a/infra/modules/virtualNetwork.bicep b/infra/modules/virtualNetwork.bicep index 77ab6895d..dcf49d45c 100644 --- a/infra/modules/virtualNetwork.bicep +++ b/infra/modules/virtualNetwork.bicep @@ -273,6 +273,6 @@ output webSubnetResourceId string = contains(map(vnetSubnets, subnet => subnet.n output pepsSubnetResourceId string = contains(map(vnetSubnets, subnet => subnet.name), 'peps') ? virtualNetwork.outputs.subnetResourceIds[indexOf(map(vnetSubnets, subnet => subnet.name), 'peps')] : '' output aciSubnetResourceId string = contains(map(vnetSubnets, subnet => subnet.name), 'aci') ? virtualNetwork.outputs.subnetResourceIds[indexOf(map(vnetSubnets, subnet => subnet.name), 'aci')] : '' -// Bastion/jumpbox subnet outputs (present only when deployBastionAndJumpbox is true) +// Bastion/jumpbox subnet outputs (always declared; will be empty when those subnets are not deployed) output bastionSubnetResourceId string = contains(map(vnetSubnets, subnet => subnet.name), 'AzureBastionSubnet') ? virtualNetwork.outputs.subnetResourceIds[indexOf(map(vnetSubnets, subnet => subnet.name), 'AzureBastionSubnet')] : '' output jumpboxSubnetResourceId string = contains(map(vnetSubnets, subnet => subnet.name), 'jumpbox') ? virtualNetwork.outputs.subnetResourceIds[indexOf(map(vnetSubnets, subnet => subnet.name), 'jumpbox')] : '' From 36d38156b0e31d14ed9f7dc7127328e84ceca08e Mon Sep 17 00:00:00 2001 From: v-maddukuriy Date: Thu, 26 Mar 2026 19:46:53 +0530 Subject: [PATCH 23/72] fix: honor deployBastionAndJumpbox independently from VM password --- infra/main.bicep | 2 +- infra/main.json | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/infra/main.bicep b/infra/main.bicep index 5e48eda7b..ea28b8e88 100644 --- a/infra/main.bicep +++ b/infra/main.bicep @@ -383,7 +383,7 @@ module virtualNetwork 'modules/virtualNetwork.bicep' = if (enablePrivateNetworki name: 'vnet-${solutionSuffix}' addressPrefixes: ['10.0.0.0/20'] // 4096 addresses (enough for 8 /23 subnets or 16 /24) location: location - deployBastionAndJumpbox: enablePrivateNetworking && deployBastionAndJumpbox && !empty(vmAdminPassword) + deployBastionAndJumpbox: enablePrivateNetworking && deployBastionAndJumpbox tags: tags logAnalyticsWorkspaceId: logAnalyticsWorkspaceResourceId resourceSuffix: solutionSuffix diff --git a/infra/main.json b/infra/main.json index 48b55001c..e522b6d40 100644 --- a/infra/main.json +++ b/infra/main.json @@ -6,7 +6,7 @@ "_generator": { "name": "bicep", "version": "0.41.2.15936", - "templateHash": "14899763561101785269" + "templateHash": "876801466335750099" }, "name": "Intelligent Content Generation Accelerator", "description": "Solution Accelerator for multimodal marketing content generation using Microsoft Agent Framework.\n" @@ -4859,7 +4859,7 @@ "value": "[parameters('location')]" }, "deployBastionAndJumpbox": { - "value": "[and(and(parameters('enablePrivateNetworking'), parameters('deployBastionAndJumpbox')), not(empty(parameters('vmAdminPassword'))))]" + "value": "[and(parameters('enablePrivateNetworking'), parameters('deployBastionAndJumpbox'))]" }, "tags": { "value": "[parameters('tags')]" From b518ac8c4950102fdf748d899448dfdde035937f Mon Sep 17 00:00:00 2001 From: v-maddukuriy Date: Thu, 26 Mar 2026 20:24:50 +0530 Subject: [PATCH 24/72] fix: address remaining PR 777 copilot comments --- infra/main.bicep | 29 ++++++++++++++++------------- infra/main.json | 28 ++++++++-------------------- src/backend/event_utils.py | 14 ++++++++++++-- 3 files changed, 36 insertions(+), 35 deletions(-) diff --git a/infra/main.bicep b/infra/main.bicep index ea28b8e88..7c77fe4be 100644 --- a/infra/main.bicep +++ b/infra/main.bicep @@ -382,7 +382,7 @@ module virtualNetwork 'modules/virtualNetwork.bicep' = if (enablePrivateNetworki params: { name: 'vnet-${solutionSuffix}' addressPrefixes: ['10.0.0.0/20'] // 4096 addresses (enough for 8 /23 subnets or 16 /24) - location: location + location: solutionLocation deployBastionAndJumpbox: enablePrivateNetworking && deployBastionAndJumpbox tags: tags logAnalyticsWorkspaceId: logAnalyticsWorkspaceResourceId @@ -405,26 +405,28 @@ var zoneSupportedJumpboxLocations = [ 'uksouth' 'westus3' ] -var deployAdminAccessResources = enablePrivateNetworking && deployBastionAndJumpbox +var deployAdminAccessResources = enablePrivateNetworking && deployBastionAndJumpbox && !empty(vmAdminPassword) module bastionHost 'br/public:avm/res/network/bastion-host:0.8.2' = if (deployAdminAccessResources) { name: take('avm.res.network.bastion-host.${bastionHostName}', 64) params: { name: bastionHostName skuName: 'Standard' - location: location + location: solutionLocation virtualNetworkResourceId: virtualNetwork!.outputs.resourceId - diagnosticSettings: [ - { - name: 'bastionDiagnostics' - workspaceResourceId: logAnalyticsWorkspaceResourceId - logCategoriesAndGroups: [ + diagnosticSettings: !empty(logAnalyticsWorkspaceResourceId) + ? [ { - categoryGroup: 'allLogs' - enabled: true + name: 'bastionDiagnostics' + workspaceResourceId: logAnalyticsWorkspaceResourceId + logCategoriesAndGroups: [ + { + categoryGroup: 'allLogs' + enabled: true + } + ] } ] - } - ] + : [] tags: tags enableTelemetry: enableTelemetry publicIPAddressObject: { @@ -434,7 +436,8 @@ module bastionHost 'br/public:avm/res/network/bastion-host:0.8.2' = if (deployAd } // Jumpbox Virtual Machine -var jumpboxVmName = take('vm-jumpbox-${solutionSuffix}', 15) +var jumpboxUniqueToken = take(uniqueString(resourceGroup().id, solutionSuffix), 10) +var jumpboxVmName = take('vm-${jumpboxUniqueToken}', 15) module jumpboxVM 'br/public:avm/res/compute/virtual-machine:0.21.0' = if (deployAdminAccessResources) { name: take('avm.res.compute.virtual-machine.${jumpboxVmName}', 64) params: { diff --git a/infra/main.json b/infra/main.json index e522b6d40..67ad82dea 100644 --- a/infra/main.json +++ b/infra/main.json @@ -6,7 +6,7 @@ "_generator": { "name": "bicep", "version": "0.41.2.15936", - "templateHash": "876801466335750099" + "templateHash": "690774053853233065" }, "name": "Intelligent Content Generation Accelerator", "description": "Solution Accelerator for multimodal marketing content generation using Microsoft Agent Framework.\n" @@ -358,8 +358,9 @@ "uksouth", "westus3" ], - "deployAdminAccessResources": "[and(parameters('enablePrivateNetworking'), parameters('deployBastionAndJumpbox'))]", - "jumpboxVmName": "[take(format('vm-jumpbox-{0}', variables('solutionSuffix')), 15)]", + "deployAdminAccessResources": "[and(and(parameters('enablePrivateNetworking'), parameters('deployBastionAndJumpbox')), not(empty(parameters('vmAdminPassword'))))]", + "jumpboxUniqueToken": "[take(uniqueString(resourceGroup().id, variables('solutionSuffix')), 10)]", + "jumpboxVmName": "[take(format('vm-{0}', variables('jumpboxUniqueToken')), 15)]", "privateDnsZones": [ "privatelink.cognitiveservices.azure.com", "privatelink.openai.azure.com", @@ -4856,7 +4857,7 @@ ] }, "location": { - "value": "[parameters('location')]" + "value": "[variables('solutionLocation')]" }, "deployBastionAndJumpbox": { "value": "[and(parameters('enablePrivateNetworking'), parameters('deployBastionAndJumpbox'))]" @@ -7443,25 +7444,12 @@ "value": "Standard" }, "location": { - "value": "[parameters('location')]" + "value": "[variables('solutionLocation')]" }, "virtualNetworkResourceId": { "value": "[reference('virtualNetwork').outputs.resourceId.value]" }, - "diagnosticSettings": { - "value": [ - { - "name": "bastionDiagnostics", - "workspaceResourceId": "[if(variables('useExistingLogAnalytics'), parameters('existingLogAnalyticsWorkspaceId'), if(parameters('enableMonitoring'), reference('logAnalyticsWorkspace').outputs.resourceId.value, ''))]", - "logCategoriesAndGroups": [ - { - "categoryGroup": "allLogs", - "enabled": true - } - ] - } - ] - }, + "diagnosticSettings": "[if(not(empty(if(variables('useExistingLogAnalytics'), parameters('existingLogAnalyticsWorkspaceId'), if(parameters('enableMonitoring'), reference('logAnalyticsWorkspace').outputs.resourceId.value, '')))), createObject('value', createArray(createObject('name', 'bastionDiagnostics', 'workspaceResourceId', if(variables('useExistingLogAnalytics'), parameters('existingLogAnalyticsWorkspaceId'), if(parameters('enableMonitoring'), reference('logAnalyticsWorkspace').outputs.resourceId.value, '')), 'logCategoriesAndGroups', createArray(createObject('categoryGroup', 'allLogs', 'enabled', true()))))), createObject('value', createArray()))]", "tags": { "value": "[parameters('tags')]" }, @@ -24873,8 +24861,8 @@ }, "dependsOn": [ "aiFoundryAiServices", - "[format('avmPrivateDnsZones[{0}]', variables('dnsZoneIndex').openAI)]", "[format('avmPrivateDnsZones[{0}]', variables('dnsZoneIndex').cognitiveServices)]", + "[format('avmPrivateDnsZones[{0}]', variables('dnsZoneIndex').openAI)]", "virtualNetwork" ] }, diff --git a/src/backend/event_utils.py b/src/backend/event_utils.py index dd03f20b7..6fe97345b 100644 --- a/src/backend/event_utils.py +++ b/src/backend/event_utils.py @@ -1,8 +1,18 @@ import os +import logging from azure.monitor.events.extension import track_event +logger = logging.getLogger(__name__) +_telemetry_disabled_logged = False + def track_event_if_configured(event_name: str, event_data: dict): + global _telemetry_disabled_logged connection_string = os.getenv("APPLICATIONINSIGHTS_CONNECTION_STRING") - if connection_string: - track_event(event_name, event_data) + if not connection_string: + if not _telemetry_disabled_logged: + logger.warning("Application Insights connection string is not set; telemetry events are disabled.") + _telemetry_disabled_logged = True + return + + track_event(event_name, event_data) From ef25e8aecfbbe4c5abdcc878742def9be487f83a Mon Sep 17 00:00:00 2001 From: Vamshi-Microsoft Date: Fri, 27 Mar 2026 08:58:59 +0530 Subject: [PATCH 25/72] fix: add RG_TAGS environment variable for resource group creation --- .github/workflows/job-deploy.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/job-deploy.yml b/.github/workflows/job-deploy.yml index 813c044d0..37f771d19 100644 --- a/.github/workflows/job-deploy.yml +++ b/.github/workflows/job-deploy.yml @@ -103,6 +103,7 @@ env: CLEANUP_RESOURCES: ${{ inputs.trigger_type != 'workflow_dispatch' || inputs.cleanup_resources }} RUN_E2E_TESTS: ${{ inputs.trigger_type == 'workflow_dispatch' && (inputs.run_e2e_tests || 'GoldenPath-Testing') || 'GoldenPath-Testing' }} BUILD_DOCKER_IMAGE: ${{ inputs.trigger_type == 'workflow_dispatch' && (inputs.build_docker_image || false) || false }} + RG_TAGS: ${{ vars.RG_TAGS }} jobs: azure-setup: @@ -532,7 +533,7 @@ jobs: rg_exists=$(az group exists --name $RESOURCE_GROUP_NAME) if [ "$rg_exists" = "false" ]; then echo "📦 Resource group does not exist. Creating new resource group '$RESOURCE_GROUP_NAME' in location '$AZURE_LOCATION'..." - az group create --name $RESOURCE_GROUP_NAME --location $AZURE_LOCATION || { echo "❌ Error creating resource group"; exit 1; } + az group create --name $RESOURCE_GROUP_NAME --location $AZURE_LOCATION --tags ${{ env.RG_TAGS }} || { echo "❌ Error creating resource group"; exit 1; } echo "✅ Resource group '$RESOURCE_GROUP_NAME' created successfully." else echo "✅ Resource group '$RESOURCE_GROUP_NAME' already exists. Deploying to existing resource group." From 2d6e64a8c3a1ef3a522246d7dc1e66745d50227e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 27 Mar 2026 07:37:59 +0000 Subject: [PATCH 26/72] fix: align VNet subnet deployment with deployAdminAccessResources condition Agent-Logs-Url: https://github.com/microsoft/content-generation-solution-accelerator/sessions/2a9b47df-250c-4016-85ff-32e6f142f3c9 Co-authored-by: Rafi-Microsoft <207166450+Rafi-Microsoft@users.noreply.github.com> --- infra/main.bicep | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/infra/main.bicep b/infra/main.bicep index 7c77fe4be..7ed41b266 100644 --- a/infra/main.bicep +++ b/infra/main.bicep @@ -377,13 +377,14 @@ module userAssignedIdentity 'br/public:avm/res/managed-identity/user-assigned-id } // ========== Virtual Network and Networking Components ========== // +var deployAdminAccessResources = enablePrivateNetworking && deployBastionAndJumpbox && !empty(vmAdminPassword) module virtualNetwork 'modules/virtualNetwork.bicep' = if (enablePrivateNetworking) { name: take('module.virtualNetwork.${solutionSuffix}', 64) params: { name: 'vnet-${solutionSuffix}' addressPrefixes: ['10.0.0.0/20'] // 4096 addresses (enough for 8 /23 subnets or 16 /24) location: solutionLocation - deployBastionAndJumpbox: enablePrivateNetworking && deployBastionAndJumpbox + deployBastionAndJumpbox: deployAdminAccessResources tags: tags logAnalyticsWorkspaceId: logAnalyticsWorkspaceResourceId resourceSuffix: solutionSuffix @@ -405,7 +406,6 @@ var zoneSupportedJumpboxLocations = [ 'uksouth' 'westus3' ] -var deployAdminAccessResources = enablePrivateNetworking && deployBastionAndJumpbox && !empty(vmAdminPassword) module bastionHost 'br/public:avm/res/network/bastion-host:0.8.2' = if (deployAdminAccessResources) { name: take('avm.res.network.bastion-host.${bastionHostName}', 64) params: { From aaf49e71fd38f00291a0e448f09aceaf0c25c36f Mon Sep 17 00:00:00 2001 From: "Teja Sri Munnangi (Persistent Systems Inc)" Date: Mon, 30 Mar 2026 15:30:32 +0530 Subject: [PATCH 27/72] commit --- .github/workflows/azd-template-validation.yml | 39 ++++++++++ .github/workflows/azure-dev.yml | 75 ++++++++++--------- 2 files changed, 78 insertions(+), 36 deletions(-) create mode 100644 .github/workflows/azd-template-validation.yml diff --git a/.github/workflows/azd-template-validation.yml b/.github/workflows/azd-template-validation.yml new file mode 100644 index 000000000..44a719e8d --- /dev/null +++ b/.github/workflows/azd-template-validation.yml @@ -0,0 +1,39 @@ +name: AZD Template Validation +on: + schedule: + - cron: '30 1 * * 4' # Every Thursday at 7:00 AM IST (1:30 AM UTC) + workflow_dispatch: + +permissions: + contents: read + id-token: write + pull-requests: write + +jobs: + template_validation: + runs-on: ubuntu-latest + environment: production + name: azd template validation + + steps: + - uses: actions/checkout@v4 + + - name: Validate Azure Template + uses: microsoft/template-validation-action@v0.4.3 + with: + validateAzd: ${{ vars.TEMPLATE_VALIDATE_AZD }} + validateTests: ${{ vars.TEMPLATE_VALIDATE_TESTS }} + useDevContainer: ${{ vars.TEMPLATE_USE_DEV_CONTAINER }} + id: validation + env: + AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }} + AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }} + AZURE_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }} + AZURE_ENV_NAME: ${{ secrets.AZURE_ENV_NAME }} + AZURE_LOCATION: ${{ secrets.AZURE_LOCATION }} + AZURE_ENV_OPENAI_LOCATION: ${{ secrets.AZURE_ENV_OPENAI_LOCATION }} + AZURE_AI_MODEL_CAPACITY: 1 + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: print result + run: cat ${{ steps.validation.outputs.resultFile }} \ No newline at end of file diff --git a/.github/workflows/azure-dev.yml b/.github/workflows/azure-dev.yml index d2a5231f4..5ff4acbb7 100644 --- a/.github/workflows/azure-dev.yml +++ b/.github/workflows/azure-dev.yml @@ -1,52 +1,55 @@ -name: Azure Template Validation +name: Azure Dev Deploy on: workflow_dispatch: - push: - branches: - - main permissions: contents: read id-token: write - pull-requests: write jobs: - template_validation_job: + deploy: runs-on: ubuntu-latest + name: azd deploy environment: production - name: Template validation - + env: + AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }} + AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }} + AZURE_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }} + AZURE_ENV_NAME: ${{ secrets.AZURE_ENV_NAME }} + AZURE_LOCATION: ${{ secrets.AZURE_LOCATION }} + AZURE_ENV_OPENAI_LOCATION: ${{ secrets.AZURE_ENV_OPENAI_LOCATION }} + AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID: ${{ secrets.AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID }} + AZURE_EXISTING_AI_PROJECT_RESOURCE_ID: ${{ secrets.AZURE_EXISTING_AI_PROJECT_RESOURCE_ID }} + AZURE_DEV_COLLECT_TELEMETRY: ${{ vars.AZURE_DEV_COLLECT_TELEMETRY }} + steps: - # Step 1: Checkout the code from your repository - - name: Checkout code + - name: Checkout Code uses: actions/checkout@v4 - # Step 2: Pre-authenticate Azure for azd validation + - name: Install azd + uses: Azure/setup-azd@v2 + - name: Login to Azure + uses: azure/login@v2 + with: + client-id: ${{ secrets.AZURE_CLIENT_ID }} + tenant-id: ${{ secrets.AZURE_TENANT_ID }} + subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} + + - name: Login to AZD shell: bash run: | - az login --service-principal -u "${{ secrets.AZURE_CLIENT_ID }}" -p "${{ secrets.AZURE_CLIENT_SECRET }}" --tenant "${{ secrets.AZURE_TENANT_ID }}" - az account set --subscription "${{ secrets.AZURE_SUBSCRIPTION_ID }}" - - # Step 3: Validate the Azure template using microsoft/template-validation-action - - name: Validate Azure Template - uses: microsoft/template-validation-action@v0.4.3 - with: - workingDirectory: . - validateAzd: ${{ vars.TEMPLATE_VALIDATE_AZD }} - useDevContainer: ${{ vars.TEMPLATE_USE_DEV_CONTAINER }} - validateTests: ${{ vars.TEMPLATE_VALIDATE_TESTS }} - id: validation - env: - AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }} - AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }} - AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }} - AZURE_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }} - AZURE_ENV_NAME: ${{ secrets.AZURE_ENV_NAME }} - AZURE_LOCATION: ${{ secrets.AZURE_LOCATION }} - AZURE_ENV_OPENAI_LOCATION: ${{ secrets.AZURE_ENV_OPENAI_LOCATION }} - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - # Step 4: Print the result of the validation - - name: Print result - run: cat ${{ steps.validation.outputs.resultFile }} + azd auth login \ + --client-id "$AZURE_CLIENT_ID" \ + --federated-credential-provider "github" \ + --tenant-id "$AZURE_TENANT_ID" + + - name: Provision and Deploy + shell: bash + run: | + set -euo pipefail + + if ! azd env select "$AZURE_ENV_NAME"; then + azd env new "$AZURE_ENV_NAME" --subscription "$AZURE_SUBSCRIPTION_ID" --location "$AZURE_LOCATION" --no-prompt + fi + azd up --no-prompt From 7abd216075cd3686e690f3677d4b635741bdd433 Mon Sep 17 00:00:00 2001 From: "Teja Sri Munnangi (Persistent Systems Inc)" Date: Mon, 30 Mar 2026 16:20:25 +0530 Subject: [PATCH 28/72] commit --- .github/workflows/azd-template-validation.yml | 1 - .github/workflows/azure-dev.yml | 32 ++++++++++++------- 2 files changed, 21 insertions(+), 12 deletions(-) diff --git a/.github/workflows/azd-template-validation.yml b/.github/workflows/azd-template-validation.yml index 44a719e8d..889ab0cf7 100644 --- a/.github/workflows/azd-template-validation.yml +++ b/.github/workflows/azd-template-validation.yml @@ -8,7 +8,6 @@ permissions: contents: read id-token: write pull-requests: write - jobs: template_validation: runs-on: ubuntu-latest diff --git a/.github/workflows/azure-dev.yml b/.github/workflows/azure-dev.yml index 5ff4acbb7..886407ef5 100644 --- a/.github/workflows/azure-dev.yml +++ b/.github/workflows/azure-dev.yml @@ -11,16 +11,6 @@ jobs: runs-on: ubuntu-latest name: azd deploy environment: production - env: - AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }} - AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }} - AZURE_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }} - AZURE_ENV_NAME: ${{ secrets.AZURE_ENV_NAME }} - AZURE_LOCATION: ${{ secrets.AZURE_LOCATION }} - AZURE_ENV_OPENAI_LOCATION: ${{ secrets.AZURE_ENV_OPENAI_LOCATION }} - AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID: ${{ secrets.AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID }} - AZURE_EXISTING_AI_PROJECT_RESOURCE_ID: ${{ secrets.AZURE_EXISTING_AI_PROJECT_RESOURCE_ID }} - AZURE_DEV_COLLECT_TELEMETRY: ${{ vars.AZURE_DEV_COLLECT_TELEMETRY }} steps: - name: Checkout Code @@ -38,6 +28,9 @@ jobs: - name: Login to AZD shell: bash + env: + AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }} + AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }} run: | azd auth login \ --client-id "$AZURE_CLIENT_ID" \ @@ -46,10 +39,27 @@ jobs: - name: Provision and Deploy shell: bash + env: + AZURE_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }} + AZURE_ENV_NAME: ${{ secrets.AZURE_ENV_NAME }} + AZURE_LOCATION: ${{ secrets.AZURE_LOCATION }} + AZURE_ENV_OPENAI_LOCATION: ${{ secrets.AZURE_ENV_OPENAI_LOCATION }} + AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID: ${{ secrets.AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID }} + AZURE_EXISTING_AI_PROJECT_RESOURCE_ID: ${{ secrets.AZURE_EXISTING_AI_PROJECT_RESOURCE_ID }} + AZURE_DEV_COLLECT_TELEMETRY: ${{ vars.AZURE_DEV_COLLECT_TELEMETRY }} run: | set -euo pipefail - if ! azd env select "$AZURE_ENV_NAME"; then + # Determine whether the AZD environment already exists. + # Fail fast if we cannot list environments. + if ! env_list=$(azd env list); then + echo "Error: Failed to list azd environments. Not creating a new environment." >&2 + exit 1 + fi + + if printf '%s\n' "$env_list" | grep -qx "$AZURE_ENV_NAME"; then + azd env select "$AZURE_ENV_NAME" + else azd env new "$AZURE_ENV_NAME" --subscription "$AZURE_SUBSCRIPTION_ID" --location "$AZURE_LOCATION" --no-prompt fi azd up --no-prompt From d527f131daf45d3e53beb13a410dcb159d776cd1 Mon Sep 17 00:00:00 2001 From: "Teja Sri Munnangi (Persistent Systems Inc)" Date: Mon, 30 Mar 2026 16:43:13 +0530 Subject: [PATCH 29/72] commit --- .github/workflows/azure-dev.yml | 42 ++++++++++++++++----------------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/.github/workflows/azure-dev.yml b/.github/workflows/azure-dev.yml index 886407ef5..32a41e8af 100644 --- a/.github/workflows/azure-dev.yml +++ b/.github/workflows/azure-dev.yml @@ -11,6 +11,16 @@ jobs: runs-on: ubuntu-latest name: azd deploy environment: production + env: + AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }} + AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }} + AZURE_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }} + AZURE_ENV_NAME: ${{ secrets.AZURE_ENV_NAME }} + AZURE_LOCATION: ${{ secrets.AZURE_LOCATION }} + AZURE_ENV_OPENAI_LOCATION: ${{ secrets.AZURE_ENV_OPENAI_LOCATION }} + AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID: ${{ secrets.AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID }} + AZURE_EXISTING_AI_PROJECT_RESOURCE_ID: ${{ secrets.AZURE_EXISTING_AI_PROJECT_RESOURCE_ID }} + AZURE_DEV_COLLECT_TELEMETRY: ${{ vars.AZURE_DEV_COLLECT_TELEMETRY }} steps: - name: Checkout Code @@ -28,9 +38,6 @@ jobs: - name: Login to AZD shell: bash - env: - AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }} - AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }} run: | azd auth login \ --client-id "$AZURE_CLIENT_ID" \ @@ -39,27 +46,20 @@ jobs: - name: Provision and Deploy shell: bash - env: - AZURE_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }} - AZURE_ENV_NAME: ${{ secrets.AZURE_ENV_NAME }} - AZURE_LOCATION: ${{ secrets.AZURE_LOCATION }} - AZURE_ENV_OPENAI_LOCATION: ${{ secrets.AZURE_ENV_OPENAI_LOCATION }} - AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID: ${{ secrets.AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID }} - AZURE_EXISTING_AI_PROJECT_RESOURCE_ID: ${{ secrets.AZURE_EXISTING_AI_PROJECT_RESOURCE_ID }} - AZURE_DEV_COLLECT_TELEMETRY: ${{ vars.AZURE_DEV_COLLECT_TELEMETRY }} run: | - set -euo pipefail + if ! azd env select "$AZURE_ENV_NAME"; then + azd env new "$AZURE_ENV_NAME" --subscription "$AZURE_SUBSCRIPTION_ID" --location "$AZURE_LOCATION" --no-prompt + fi + + azd config set defaults.subscription "$AZURE_SUBSCRIPTION_ID" + - # Determine whether the AZD environment already exists. - # Fail fast if we cannot list environments. - if ! env_list=$(azd env list); then - echo "Error: Failed to list azd environments. Not creating a new environment." >&2 - exit 1 + if [[ -n "${AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID:-}" ]]; then + azd env set AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID "$AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID" fi - if printf '%s\n' "$env_list" | grep -qx "$AZURE_ENV_NAME"; then - azd env select "$AZURE_ENV_NAME" - else - azd env new "$AZURE_ENV_NAME" --subscription "$AZURE_SUBSCRIPTION_ID" --location "$AZURE_LOCATION" --no-prompt + if [[ -n "${AZURE_EXISTING_AI_PROJECT_RESOURCE_ID:-}" ]]; then + azd env set AZURE_EXISTING_AI_PROJECT_RESOURCE_ID "$AZURE_EXISTING_AI_PROJECT_RESOURCE_ID" fi + azd up --no-prompt From 4e5e9449b60ffdc448591783a63dad2e9abfd5b9 Mon Sep 17 00:00:00 2001 From: "Teja Sri Munnangi (Persistent Systems Inc)" Date: Tue, 31 Mar 2026 14:00:14 +0530 Subject: [PATCH 30/72] commit --- .github/workflows/azure-dev.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/workflows/azure-dev.yml b/.github/workflows/azure-dev.yml index 32a41e8af..4786a1c02 100644 --- a/.github/workflows/azure-dev.yml +++ b/.github/workflows/azure-dev.yml @@ -63,3 +63,9 @@ jobs: fi azd up --no-prompt + + - name: Cleanup Deployment + if: always() + shell: bash + run: | + azd down --environment "$AZURE_ENV_NAME" --force --purge --no-prompt || true From fab923f729409aac34edddfb2d6d8b6e599ab7b9 Mon Sep 17 00:00:00 2001 From: "Teja Sri Munnangi (Persistent Systems Inc)" Date: Tue, 31 Mar 2026 14:50:19 +0530 Subject: [PATCH 31/72] commit --- .github/workflows/azd-template-validation.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/azd-template-validation.yml b/.github/workflows/azd-template-validation.yml index 889ab0cf7..a4772ee6d 100644 --- a/.github/workflows/azd-template-validation.yml +++ b/.github/workflows/azd-template-validation.yml @@ -1,7 +1,7 @@ name: AZD Template Validation on: schedule: - - cron: '30 1 * * 4' # Every Thursday at 7:00 AM IST (1:30 AM UTC) + - cron: '30 1 * * 4' # Every Thursday at 7:00 AM IST (1:30 AM UTC) workflow_dispatch: permissions: From b193a5c248eb5a68293c5cc8982e84510bf18ba5 Mon Sep 17 00:00:00 2001 From: "Teja Sri Munnangi (Persistent Systems Inc)" Date: Wed, 1 Apr 2026 10:17:08 +0530 Subject: [PATCH 32/72] commit --- .github/workflows/azure-dev.yml | 5 ----- 1 file changed, 5 deletions(-) diff --git a/.github/workflows/azure-dev.yml b/.github/workflows/azure-dev.yml index 4786a1c02..a646e3407 100644 --- a/.github/workflows/azure-dev.yml +++ b/.github/workflows/azure-dev.yml @@ -64,8 +64,3 @@ jobs: azd up --no-prompt - - name: Cleanup Deployment - if: always() - shell: bash - run: | - azd down --environment "$AZURE_ENV_NAME" --force --purge --no-prompt || true From 14f61f88c25c857fd3977f878fab548e6c22dc9a Mon Sep 17 00:00:00 2001 From: Shubhangi-Microsoft Date: Wed, 1 Apr 2026 12:58:59 +0530 Subject: [PATCH 33/72] refactor: restructure frontend folder - move contents to app/, rename frontend-server to server --- .github/dependabot.yml | 4 ++-- .github/workflows/docker-build.yml | 8 ++++---- .gitignore | 10 +++++----- docs/AZD_DEPLOYMENT.md | 2 +- docs/TECHNICAL_GUIDE.md | 2 +- scripts/deploy.ps1 | 6 +++--- scripts/deploy.sh | 6 +++--- scripts/local_dev.ps1 | 2 +- scripts/local_dev.sh | 2 +- src/app/.dockerignore | 3 +-- src/app/WebApp.Dockerfile | 16 ++++++++-------- src/app/{frontend => }/index.html | 0 src/app/{frontend => }/microsoft.svg | 0 src/app/{frontend => }/package-lock.json | 0 src/app/{frontend => }/package.json | 0 .../package-lock.json | 0 .../{frontend-server => server}/package.json | 0 src/app/{frontend-server => server}/server.js | 0 src/app/{frontend => }/src/App.tsx | 0 src/app/{frontend => }/src/api/index.ts | 0 .../src/components/BriefReview.tsx | 0 .../src/components/ChatHistory.tsx | 0 .../{frontend => }/src/components/ChatPanel.tsx | 0 .../src/components/ConfirmedBriefView.tsx | 0 .../src/components/InlineContentPreview.tsx | 0 .../src/components/ProductReview.tsx | 0 .../src/components/SelectedProductView.tsx | 0 .../src/components/WelcomeCard.tsx | 0 src/app/{frontend => }/src/main.tsx | 0 src/app/{frontend => }/src/styles/global.css | 0 .../src/styles/images/SamplePrompt.png | Bin .../src/styles/images/contoso.svg | 0 .../src/styles/images/firstprompt.png | Bin .../src/styles/images/secondprompt.png | Bin src/app/{frontend => }/src/types/index.ts | 0 src/app/{frontend => }/src/vite-env.d.ts | 0 src/app/{frontend => }/tsconfig.json | 0 src/app/{frontend => }/tsconfig.node.json | 0 src/app/{frontend => }/vite.config.ts | 2 +- 39 files changed, 31 insertions(+), 32 deletions(-) rename src/app/{frontend => }/index.html (100%) rename src/app/{frontend => }/microsoft.svg (100%) rename src/app/{frontend => }/package-lock.json (100%) rename src/app/{frontend => }/package.json (100%) rename src/app/{frontend-server => server}/package-lock.json (100%) rename src/app/{frontend-server => server}/package.json (100%) rename src/app/{frontend-server => server}/server.js (100%) rename src/app/{frontend => }/src/App.tsx (100%) rename src/app/{frontend => }/src/api/index.ts (100%) rename src/app/{frontend => }/src/components/BriefReview.tsx (100%) rename src/app/{frontend => }/src/components/ChatHistory.tsx (100%) rename src/app/{frontend => }/src/components/ChatPanel.tsx (100%) rename src/app/{frontend => }/src/components/ConfirmedBriefView.tsx (100%) rename src/app/{frontend => }/src/components/InlineContentPreview.tsx (100%) rename src/app/{frontend => }/src/components/ProductReview.tsx (100%) rename src/app/{frontend => }/src/components/SelectedProductView.tsx (100%) rename src/app/{frontend => }/src/components/WelcomeCard.tsx (100%) rename src/app/{frontend => }/src/main.tsx (100%) rename src/app/{frontend => }/src/styles/global.css (100%) rename src/app/{frontend => }/src/styles/images/SamplePrompt.png (100%) rename src/app/{frontend => }/src/styles/images/contoso.svg (100%) rename src/app/{frontend => }/src/styles/images/firstprompt.png (100%) rename src/app/{frontend => }/src/styles/images/secondprompt.png (100%) rename src/app/{frontend => }/src/types/index.ts (100%) rename src/app/{frontend => }/src/vite-env.d.ts (100%) rename src/app/{frontend => }/tsconfig.json (100%) rename src/app/{frontend => }/tsconfig.node.json (100%) rename src/app/{frontend => }/vite.config.ts (95%) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index a42156bfc..de7102992 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -35,8 +35,8 @@ updates: # npm dependencies - grouped - package-ecosystem: "npm" directories: - - "/src/app/frontend" - - "/src/app/frontend-server" + - "/src/app" + - "/src/app/server" schedule: interval: "monthly" target-branch: "dependabotchanges" diff --git a/.github/workflows/docker-build.yml b/.github/workflows/docker-build.yml index ba82c1654..cc1197ad8 100644 --- a/.github/workflows/docker-build.yml +++ b/.github/workflows/docker-build.yml @@ -8,8 +8,8 @@ on: - demo paths: - 'src/backend/**' - - 'src/app/frontend/**' - - 'src/app/frontend-server/**' + - 'src/app/**' + - 'src/app/server/**' - '.github/workflows/docker-build.yml' pull_request: types: @@ -23,8 +23,8 @@ on: - demo paths: - 'src/backend/**' - - 'src/app/frontend/**' - - 'src/app/frontend-server/**' + - 'src/app/**' + - 'src/app/server/**' - '.github/workflows/docker-build.yml' workflow_dispatch: diff --git a/.gitignore b/.gitignore index 3696c9d2f..80d4338d3 100644 --- a/.gitignore +++ b/.gitignore @@ -36,15 +36,15 @@ eggs/ *.swo # Node -/src/app/frontend/node_modules/ -/src/app/frontend-server/node_modules/ -/src/app/frontend-server/static/ -/src/app/frontend-server/*.zip +/src/app/node_modules/ +/src/app/server/node_modules/ +/src/app/server/static/ +/src/app/server/*.zip node_modules/ # Build output /src/app/static/ -/src/app/frontend/dist/ +/src/app/dist/ # Logs *.log diff --git a/docs/AZD_DEPLOYMENT.md b/docs/AZD_DEPLOYMENT.md index 942fba86f..a54fb14ae 100644 --- a/docs/AZD_DEPLOYMENT.md +++ b/docs/AZD_DEPLOYMENT.md @@ -251,7 +251,7 @@ Error: az webapp deploy failed **Solution**: Ensure the frontend builds successfully: ```bash -cd src/app/frontend +cd src/app npm install npm run build ``` diff --git a/docs/TECHNICAL_GUIDE.md b/docs/TECHNICAL_GUIDE.md index ce41f325e..dc6288e63 100644 --- a/docs/TECHNICAL_GUIDE.md +++ b/docs/TECHNICAL_GUIDE.md @@ -142,7 +142,7 @@ pip install -r requirements.txt python app.py # Frontend -cd src/app/frontend +cd src/app npm install npm run dev ``` diff --git a/scripts/deploy.ps1 b/scripts/deploy.ps1 index bed9b90a9..c8258b9df 100644 --- a/scripts/deploy.ps1 +++ b/scripts/deploy.ps1 @@ -131,14 +131,14 @@ if ($continue -eq "y" -or $continue -eq "Y") { Write-Host "Step 3: Building and deploying frontend..." -ForegroundColor Green Write-Host "==========================================" -ForegroundColor Green - Set-Location "$ProjectDir\src\frontend" + Set-Location "$ProjectDir\src\app" npm install npm run build # Copy built files to server directory - Copy-Item -Path "$ProjectDir\src\static\*" -Destination "$ProjectDir\src\frontend-server\static\" -Recurse -Force + Copy-Item -Path "$ProjectDir\src\app\static\*" -Destination "$ProjectDir\src\app\server\static\" -Recurse -Force - Set-Location "$ProjectDir\src\frontend-server" + Set-Location "$ProjectDir\src\app\server" # Create deployment package if (Test-Path "frontend-deploy.zip") { diff --git a/scripts/deploy.sh b/scripts/deploy.sh index 10bbbbfa4..17e07451d 100644 --- a/scripts/deploy.sh +++ b/scripts/deploy.sh @@ -129,14 +129,14 @@ if [[ $REPLY =~ ^[Yy]$ ]]; then echo "Step 3: Building and deploying frontend..." echo "==========================================" - cd "$PROJECT_DIR/src/frontend" + cd "$PROJECT_DIR/src/app" npm install npm run build # Copy built files to server directory - cp -r "$PROJECT_DIR/src/static/"* "$PROJECT_DIR/src/frontend-server/static/" + cp -r "$PROJECT_DIR/src/app/static/"* "$PROJECT_DIR/src/app/server/static/" - cd "$PROJECT_DIR/src/frontend-server" + cd "$PROJECT_DIR/src/app/server" # Create deployment package rm -f frontend-deploy.zip diff --git a/scripts/local_dev.ps1 b/scripts/local_dev.ps1 index 6978846ba..792c0b57c 100644 --- a/scripts/local_dev.ps1 +++ b/scripts/local_dev.ps1 @@ -31,7 +31,7 @@ $ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path $ProjectRoot = Split-Path -Parent $ScriptDir $SrcDir = Join-Path $ProjectRoot "src" $BackendDir = Join-Path $SrcDir "backend" -$FrontendDir = Join-Path $SrcDir "app\frontend" +$FrontendDir = Join-Path $SrcDir "app" # Default ports $BackendPort = if ($env:BACKEND_PORT) { $env:BACKEND_PORT } else { "5000" } diff --git a/scripts/local_dev.sh b/scripts/local_dev.sh index 8bf811d92..4fb56bc85 100644 --- a/scripts/local_dev.sh +++ b/scripts/local_dev.sh @@ -34,7 +34,7 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" SRC_DIR="$PROJECT_ROOT/src" BACKEND_DIR="$SRC_DIR/backend" -FRONTEND_DIR="$SRC_DIR/app/frontend" +FRONTEND_DIR="$SRC_DIR/app" # Default ports BACKEND_PORT=${BACKEND_PORT:-5000} diff --git a/src/app/.dockerignore b/src/app/.dockerignore index 01b21e59f..717b602c2 100644 --- a/src/app/.dockerignore +++ b/src/app/.dockerignore @@ -9,7 +9,7 @@ # Build outputs (will be built in container) static/ **/dist/ -frontend-server/static/ +server/static/ # Development files *.log @@ -36,7 +36,6 @@ Thumbs.db # Deployment artifacts *.zip -frontend-deploy.zip # Backend not needed for frontend build (and vice versa) # Comment out if building full-stack image diff --git a/src/app/WebApp.Dockerfile b/src/app/WebApp.Dockerfile index fca82196e..15e91b831 100644 --- a/src/app/WebApp.Dockerfile +++ b/src/app/WebApp.Dockerfile @@ -1,7 +1,7 @@ # ============================================ # Frontend Dockerfile # Multi-stage build for Content Generation Frontend -# Combines: frontend (React/Vite) + frontend-server (Node.js proxy) +# Combines: React/Vite frontend + server (Node.js proxy) # ============================================ # ============================================ @@ -11,14 +11,14 @@ FROM node:20-alpine AS frontend-build WORKDIR /app -# Copy frontend package files -COPY frontend/package*.json ./ +# Copy package files +COPY package*.json ./ # Install dependencies RUN npm ci -# Copy frontend source code -COPY frontend/ ./ +# Copy source code +COPY . ./ # Build the frontend (outputs to ../static, but we're in /app so it goes to /static) # Override outDir to keep it in the container context @@ -31,14 +31,14 @@ FROM node:20-alpine AS production WORKDIR /app -# Copy frontend-server package files -COPY frontend-server/package*.json ./ +# Copy server package files +COPY server/package*.json ./ # Install only production dependencies RUN npm ci --only=production # Copy the server code -COPY frontend-server/server.js ./ +COPY server/server.js ./ # Copy built frontend assets from stage 1 COPY --from=frontend-build /app/dist ./static diff --git a/src/app/frontend/index.html b/src/app/index.html similarity index 100% rename from src/app/frontend/index.html rename to src/app/index.html diff --git a/src/app/frontend/microsoft.svg b/src/app/microsoft.svg similarity index 100% rename from src/app/frontend/microsoft.svg rename to src/app/microsoft.svg diff --git a/src/app/frontend/package-lock.json b/src/app/package-lock.json similarity index 100% rename from src/app/frontend/package-lock.json rename to src/app/package-lock.json diff --git a/src/app/frontend/package.json b/src/app/package.json similarity index 100% rename from src/app/frontend/package.json rename to src/app/package.json diff --git a/src/app/frontend-server/package-lock.json b/src/app/server/package-lock.json similarity index 100% rename from src/app/frontend-server/package-lock.json rename to src/app/server/package-lock.json diff --git a/src/app/frontend-server/package.json b/src/app/server/package.json similarity index 100% rename from src/app/frontend-server/package.json rename to src/app/server/package.json diff --git a/src/app/frontend-server/server.js b/src/app/server/server.js similarity index 100% rename from src/app/frontend-server/server.js rename to src/app/server/server.js diff --git a/src/app/frontend/src/App.tsx b/src/app/src/App.tsx similarity index 100% rename from src/app/frontend/src/App.tsx rename to src/app/src/App.tsx diff --git a/src/app/frontend/src/api/index.ts b/src/app/src/api/index.ts similarity index 100% rename from src/app/frontend/src/api/index.ts rename to src/app/src/api/index.ts diff --git a/src/app/frontend/src/components/BriefReview.tsx b/src/app/src/components/BriefReview.tsx similarity index 100% rename from src/app/frontend/src/components/BriefReview.tsx rename to src/app/src/components/BriefReview.tsx diff --git a/src/app/frontend/src/components/ChatHistory.tsx b/src/app/src/components/ChatHistory.tsx similarity index 100% rename from src/app/frontend/src/components/ChatHistory.tsx rename to src/app/src/components/ChatHistory.tsx diff --git a/src/app/frontend/src/components/ChatPanel.tsx b/src/app/src/components/ChatPanel.tsx similarity index 100% rename from src/app/frontend/src/components/ChatPanel.tsx rename to src/app/src/components/ChatPanel.tsx diff --git a/src/app/frontend/src/components/ConfirmedBriefView.tsx b/src/app/src/components/ConfirmedBriefView.tsx similarity index 100% rename from src/app/frontend/src/components/ConfirmedBriefView.tsx rename to src/app/src/components/ConfirmedBriefView.tsx diff --git a/src/app/frontend/src/components/InlineContentPreview.tsx b/src/app/src/components/InlineContentPreview.tsx similarity index 100% rename from src/app/frontend/src/components/InlineContentPreview.tsx rename to src/app/src/components/InlineContentPreview.tsx diff --git a/src/app/frontend/src/components/ProductReview.tsx b/src/app/src/components/ProductReview.tsx similarity index 100% rename from src/app/frontend/src/components/ProductReview.tsx rename to src/app/src/components/ProductReview.tsx diff --git a/src/app/frontend/src/components/SelectedProductView.tsx b/src/app/src/components/SelectedProductView.tsx similarity index 100% rename from src/app/frontend/src/components/SelectedProductView.tsx rename to src/app/src/components/SelectedProductView.tsx diff --git a/src/app/frontend/src/components/WelcomeCard.tsx b/src/app/src/components/WelcomeCard.tsx similarity index 100% rename from src/app/frontend/src/components/WelcomeCard.tsx rename to src/app/src/components/WelcomeCard.tsx diff --git a/src/app/frontend/src/main.tsx b/src/app/src/main.tsx similarity index 100% rename from src/app/frontend/src/main.tsx rename to src/app/src/main.tsx diff --git a/src/app/frontend/src/styles/global.css b/src/app/src/styles/global.css similarity index 100% rename from src/app/frontend/src/styles/global.css rename to src/app/src/styles/global.css diff --git a/src/app/frontend/src/styles/images/SamplePrompt.png b/src/app/src/styles/images/SamplePrompt.png similarity index 100% rename from src/app/frontend/src/styles/images/SamplePrompt.png rename to src/app/src/styles/images/SamplePrompt.png diff --git a/src/app/frontend/src/styles/images/contoso.svg b/src/app/src/styles/images/contoso.svg similarity index 100% rename from src/app/frontend/src/styles/images/contoso.svg rename to src/app/src/styles/images/contoso.svg diff --git a/src/app/frontend/src/styles/images/firstprompt.png b/src/app/src/styles/images/firstprompt.png similarity index 100% rename from src/app/frontend/src/styles/images/firstprompt.png rename to src/app/src/styles/images/firstprompt.png diff --git a/src/app/frontend/src/styles/images/secondprompt.png b/src/app/src/styles/images/secondprompt.png similarity index 100% rename from src/app/frontend/src/styles/images/secondprompt.png rename to src/app/src/styles/images/secondprompt.png diff --git a/src/app/frontend/src/types/index.ts b/src/app/src/types/index.ts similarity index 100% rename from src/app/frontend/src/types/index.ts rename to src/app/src/types/index.ts diff --git a/src/app/frontend/src/vite-env.d.ts b/src/app/src/vite-env.d.ts similarity index 100% rename from src/app/frontend/src/vite-env.d.ts rename to src/app/src/vite-env.d.ts diff --git a/src/app/frontend/tsconfig.json b/src/app/tsconfig.json similarity index 100% rename from src/app/frontend/tsconfig.json rename to src/app/tsconfig.json diff --git a/src/app/frontend/tsconfig.node.json b/src/app/tsconfig.node.json similarity index 100% rename from src/app/frontend/tsconfig.node.json rename to src/app/tsconfig.node.json diff --git a/src/app/frontend/vite.config.ts b/src/app/vite.config.ts similarity index 95% rename from src/app/frontend/vite.config.ts rename to src/app/vite.config.ts index 829c02469..6f7ea5373 100644 --- a/src/app/frontend/vite.config.ts +++ b/src/app/vite.config.ts @@ -23,7 +23,7 @@ export default defineConfig({ }, }, build: { - outDir: '../static', + outDir: './static', emptyOutDir: true, }, }); From 3d05925d80d449f5911acdf579244f32f1035046 Mon Sep 17 00:00:00 2001 From: Shubhangi-Microsoft Date: Wed, 1 Apr 2026 17:44:11 +0530 Subject: [PATCH 34/72] fix: address PR review - use npm ci --omit=dev, create server/static dir before copy --- scripts/deploy.ps1 | 1 + scripts/deploy.sh | 1 + 2 files changed, 2 insertions(+) diff --git a/scripts/deploy.ps1 b/scripts/deploy.ps1 index c8258b9df..a9370f8e8 100644 --- a/scripts/deploy.ps1 +++ b/scripts/deploy.ps1 @@ -136,6 +136,7 @@ if ($continue -eq "y" -or $continue -eq "Y") { npm run build # Copy built files to server directory + New-Item -ItemType Directory -Force "$ProjectDir\src\app\server\static" | Out-Null Copy-Item -Path "$ProjectDir\src\app\static\*" -Destination "$ProjectDir\src\app\server\static\" -Recurse -Force Set-Location "$ProjectDir\src\app\server" diff --git a/scripts/deploy.sh b/scripts/deploy.sh index 17e07451d..4650bff51 100644 --- a/scripts/deploy.sh +++ b/scripts/deploy.sh @@ -134,6 +134,7 @@ if [[ $REPLY =~ ^[Yy]$ ]]; then npm run build # Copy built files to server directory + mkdir -p "$PROJECT_DIR/src/app/server/static" cp -r "$PROJECT_DIR/src/app/static/"* "$PROJECT_DIR/src/app/server/static/" cd "$PROJECT_DIR/src/app/server" From 7494e38f91b975db4bda8b0202dad06d3528d71c Mon Sep 17 00:00:00 2001 From: v-maddukuriy Date: Thu, 2 Apr 2026 20:39:18 +0530 Subject: [PATCH 35/72] chore: remove event_utils changes from PR --- src/backend/event_utils.py | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/src/backend/event_utils.py b/src/backend/event_utils.py index 6fe97345b..6bd09fab0 100644 --- a/src/backend/event_utils.py +++ b/src/backend/event_utils.py @@ -1,18 +1,11 @@ -import os import logging +import os from azure.monitor.events.extension import track_event -logger = logging.getLogger(__name__) -_telemetry_disabled_logged = False - def track_event_if_configured(event_name: str, event_data: dict): - global _telemetry_disabled_logged connection_string = os.getenv("APPLICATIONINSIGHTS_CONNECTION_STRING") - if not connection_string: - if not _telemetry_disabled_logged: - logger.warning("Application Insights connection string is not set; telemetry events are disabled.") - _telemetry_disabled_logged = True - return - - track_event(event_name, event_data) + if connection_string: + track_event(event_name, event_data) + else: + logging.warning(f"Skipping track_event for {event_name} as Application Insights is not configured") From dafa23d4f286d6b1e47a545c91c635b04b0901e1 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 2 Apr 2026 16:26:10 +0000 Subject: [PATCH 36/72] fix: rename param name to vnetName to avoid collision with output name in virtualNetwork.bicep Agent-Logs-Url: https://github.com/microsoft/content-generation-solution-accelerator/sessions/738e0d94-7901-44a8-a7a0-da1bf0928444 Co-authored-by: Yatish-Microsoft <234036280+Yatish-Microsoft@users.noreply.github.com> --- infra/main.bicep | 2 +- infra/modules/virtualNetwork.bicep | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/infra/main.bicep b/infra/main.bicep index 7ed41b266..02790497b 100644 --- a/infra/main.bicep +++ b/infra/main.bicep @@ -381,7 +381,7 @@ var deployAdminAccessResources = enablePrivateNetworking && deployBastionAndJump module virtualNetwork 'modules/virtualNetwork.bicep' = if (enablePrivateNetworking) { name: take('module.virtualNetwork.${solutionSuffix}', 64) params: { - name: 'vnet-${solutionSuffix}' + vnetName: 'vnet-${solutionSuffix}' addressPrefixes: ['10.0.0.0/20'] // 4096 addresses (enough for 8 /23 subnets or 16 /24) location: solutionLocation deployBastionAndJumpbox: deployAdminAccessResources diff --git a/infra/modules/virtualNetwork.bicep b/infra/modules/virtualNetwork.bicep index dcf49d45c..2d79b1288 100644 --- a/infra/modules/virtualNetwork.bicep +++ b/infra/modules/virtualNetwork.bicep @@ -2,7 +2,7 @@ // Networking - NSGs, VNET and Subnets for Content Generation Solution /****************************************************************************************************************************/ @description('Name of the virtual network.') -param name string +param vnetName string @description('Azure region to deploy resources.') param location string = resourceGroup().location @@ -227,9 +227,9 @@ module nsgs 'br/public:avm/res/network/network-security-group:0.5.2' = [ // Create VNet and subnets using AVM Virtual Network module module virtualNetwork 'br/public:avm/res/network/virtual-network:0.7.2' = { - name: take('avm.res.network.virtual-network.${name}', 64) + name: take('avm.res.network.virtual-network.${vnetName}', 64) params: { - name: name + name: vnetName location: location addressPrefixes: addressPrefixes subnets: [ From bec9fdb0293dcf31ae21558e44b3c7e328c68cbd Mon Sep 17 00:00:00 2001 From: "Teja Sri Munnangi (Persistent Systems Inc)" Date: Thu, 2 Apr 2026 23:21:35 +0530 Subject: [PATCH 37/72] commit --- .github/workflows/azd-template-validation.yml | 6 +++++- .github/workflows/azure-dev.yml | 6 ++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/.github/workflows/azd-template-validation.yml b/.github/workflows/azd-template-validation.yml index a4772ee6d..21723fdd9 100644 --- a/.github/workflows/azd-template-validation.yml +++ b/.github/workflows/azd-template-validation.yml @@ -17,6 +17,10 @@ jobs: steps: - uses: actions/checkout@v4 + - name: Set timestamp + shell: bash + run: echo "HHMM=$(date -u +'%H%M')" >> "$GITHUB_ENV" + - name: Validate Azure Template uses: microsoft/template-validation-action@v0.4.3 with: @@ -28,7 +32,7 @@ jobs: AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }} AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }} AZURE_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }} - AZURE_ENV_NAME: ${{ secrets.AZURE_ENV_NAME }} + AZURE_ENV_NAME: ${{ secrets.AZURE_ENV_NAME }}-${{ env.HHMM }} AZURE_LOCATION: ${{ secrets.AZURE_LOCATION }} AZURE_ENV_OPENAI_LOCATION: ${{ secrets.AZURE_ENV_OPENAI_LOCATION }} AZURE_AI_MODEL_CAPACITY: 1 diff --git a/.github/workflows/azure-dev.yml b/.github/workflows/azure-dev.yml index a646e3407..cc0b0e8cd 100644 --- a/.github/workflows/azure-dev.yml +++ b/.github/workflows/azure-dev.yml @@ -26,6 +26,12 @@ jobs: - name: Checkout Code uses: actions/checkout@v4 + - name: Set timestamp and env name + shell: bash + run: | + HHMM=$(date -u +'%H%M') + echo "AZURE_ENV_NAME=${AZURE_ENV_NAME}-${HHMM}" >> "$GITHUB_ENV" + - name: Install azd uses: Azure/setup-azd@v2 From 5f6e0ddd7e5134369970ae28b5a7dea157643878 Mon Sep 17 00:00:00 2001 From: "Teja Sri Munnangi (Persistent Systems Inc)" Date: Thu, 2 Apr 2026 23:29:29 +0530 Subject: [PATCH 38/72] commit --- .github/workflows/azd-template-validation.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/azd-template-validation.yml b/.github/workflows/azd-template-validation.yml index 21723fdd9..22fc271f7 100644 --- a/.github/workflows/azd-template-validation.yml +++ b/.github/workflows/azd-template-validation.yml @@ -1,5 +1,8 @@ name: AZD Template Validation on: +push: + branches: + - psl-content-gen-templatev schedule: - cron: '30 1 * * 4' # Every Thursday at 7:00 AM IST (1:30 AM UTC) workflow_dispatch: From 2a10da3158bcb63359c6b45f9042d420dad72f98 Mon Sep 17 00:00:00 2001 From: "Teja Sri Munnangi (Persistent Systems Inc)" Date: Thu, 2 Apr 2026 23:35:35 +0530 Subject: [PATCH 39/72] fix: correct azd-template-validation workflow indentation --- .github/workflows/azd-template-validation.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/azd-template-validation.yml b/.github/workflows/azd-template-validation.yml index 22fc271f7..8e350a6f9 100644 --- a/.github/workflows/azd-template-validation.yml +++ b/.github/workflows/azd-template-validation.yml @@ -1,6 +1,6 @@ name: AZD Template Validation on: -push: + push: branches: - psl-content-gen-templatev schedule: From 919d16d94d5c69d2b644178da1902079a22205e6 Mon Sep 17 00:00:00 2001 From: "Teja Sri Munnangi (Persistent Systems Inc)" Date: Fri, 3 Apr 2026 11:31:13 +0530 Subject: [PATCH 40/72] commit --- .github/workflows/azd-template-validation.yml | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/.github/workflows/azd-template-validation.yml b/.github/workflows/azd-template-validation.yml index 8e350a6f9..6579857ba 100644 --- a/.github/workflows/azd-template-validation.yml +++ b/.github/workflows/azd-template-validation.yml @@ -1,8 +1,5 @@ name: AZD Template Validation on: - push: - branches: - - psl-content-gen-templatev schedule: - cron: '30 1 * * 4' # Every Thursday at 7:00 AM IST (1:30 AM UTC) workflow_dispatch: @@ -16,7 +13,8 @@ jobs: runs-on: ubuntu-latest environment: production name: azd template validation - + env: + GH_TOKEN: ${{ github.token }} steps: - uses: actions/checkout@v4 From dcfd389af797d065012d21ae71205a6738afb53c Mon Sep 17 00:00:00 2001 From: "Prekshith D J (Persistent Systems Inc)" Date: Fri, 3 Apr 2026 11:51:50 +0530 Subject: [PATCH 41/72] Filter the paths for pipeline run --- .github/workflows/azure-dev.yml | 4 ++++ .github/workflows/codeql.yml | 4 ++++ .github/workflows/create-release.yml | 9 +++++++++ .github/workflows/deploy-v2.yml | 2 ++ .github/workflows/docker-build.yml | 9 +++++++++ .github/workflows/pylint.yml | 18 ++++++++++++++++++ .github/workflows/test.yml | 12 ++++++++++-- 7 files changed, 56 insertions(+), 2 deletions(-) diff --git a/.github/workflows/azure-dev.yml b/.github/workflows/azure-dev.yml index d2a5231f4..d1b2e10c3 100644 --- a/.github/workflows/azure-dev.yml +++ b/.github/workflows/azure-dev.yml @@ -4,6 +4,10 @@ on: push: branches: - main + paths: + - 'infra/**' + - 'azure*.yaml' + - '.github/workflows/azure-dev.yml' permissions: contents: read diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index ac9b1b756..06d632978 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -14,6 +14,10 @@ on: schedule: - cron: '17 11 * * 0' +concurrency: + group: codeql-${{ github.ref }} + cancel-in-progress: true + jobs: analyze: name: Analyze diff --git a/.github/workflows/create-release.yml b/.github/workflows/create-release.yml index 1ca61df0a..b04f4d3f1 100644 --- a/.github/workflows/create-release.yml +++ b/.github/workflows/create-release.yml @@ -3,6 +3,15 @@ name: "Create Release" on: push: branches: ["main"] + paths-ignore: + - 'docs/**' + - '**/*.md' + - 'LICENSE' + - '**/*.png' + - '**/*.svg' + - '**/*.jpg' + - '**/*.jpeg' + - '**/*.gif' workflow_dispatch: diff --git a/.github/workflows/deploy-v2.yml b/.github/workflows/deploy-v2.yml index c6933c4c1..84f8a43d5 100644 --- a/.github/workflows/deploy-v2.yml +++ b/.github/workflows/deploy-v2.yml @@ -6,8 +6,10 @@ on: paths: - 'src/**' - '!src/tests/**' + - '!src/pytest.ini' - 'infra/**/*.bicep' - 'infra/**/*.json' + - 'infra/scripts/**' - '*.yaml' - 'scripts/**' - '.github/workflows/deploy-*.yml' diff --git a/.github/workflows/docker-build.yml b/.github/workflows/docker-build.yml index ba82c1654..8f7d13a05 100644 --- a/.github/workflows/docker-build.yml +++ b/.github/workflows/docker-build.yml @@ -10,6 +10,8 @@ on: - 'src/backend/**' - 'src/app/frontend/**' - 'src/app/frontend-server/**' + - 'src/app/WebApp.Dockerfile' + - 'src/app/.dockerignore' - '.github/workflows/docker-build.yml' pull_request: types: @@ -25,6 +27,8 @@ on: - 'src/backend/**' - 'src/app/frontend/**' - 'src/app/frontend-server/**' + - 'src/app/WebApp.Dockerfile' + - 'src/app/.dockerignore' - '.github/workflows/docker-build.yml' workflow_dispatch: @@ -32,6 +36,11 @@ permissions: contents: read actions: read id-token: write # Required for OIDC-based Azure authentication + +concurrency: + group: docker-build-${{ github.ref }} + cancel-in-progress: true + jobs: build-and-push: runs-on: ubuntu-latest diff --git a/.github/workflows/pylint.yml b/.github/workflows/pylint.yml index 8e739ab4a..592930f82 100644 --- a/.github/workflows/pylint.yml +++ b/.github/workflows/pylint.yml @@ -2,6 +2,18 @@ name: PyLint on: push: + branches: + - main + - dev + paths: + - 'src/backend/**/*.py' + - 'src/backend/requirements*.txt' + - '.flake8' + - '.github/workflows/pylint.yml' + pull_request: + branches: + - main + - dev paths: - 'src/backend/**/*.py' - 'src/backend/requirements*.txt' @@ -12,6 +24,10 @@ permissions: contents: read actions: read +concurrency: + group: pylint-${{ github.ref }} + cancel-in-progress: true + jobs: build: runs-on: ubuntu-latest @@ -25,6 +41,8 @@ jobs: uses: actions/setup-python@v6 with: python-version: ${{ matrix.python-version }} + cache: 'pip' + cache-dependency-path: src/backend/requirements*.txt - name: Install dependencies run: | diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 47b1faa1c..82573ca99 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -6,8 +6,9 @@ on: - main - dev paths: - - '**/*.py' + - 'src/**/*.py' - 'src/backend/requirements*.txt' + - 'src/pytest.ini' - '.github/workflows/test.yml' pull_request: types: @@ -19,14 +20,19 @@ on: - main - dev paths: - - '**/*.py' + - 'src/**/*.py' - 'src/backend/requirements*.txt' + - 'src/pytest.ini' - '.github/workflows/test.yml' permissions: contents: read actions: read +concurrency: + group: test-${{ github.ref }} + cancel-in-progress: true + jobs: backend_tests: runs-on: ubuntu-latest @@ -39,6 +45,8 @@ jobs: uses: actions/setup-python@v6 with: python-version: "3.11" + cache: 'pip' + cache-dependency-path: src/backend/requirements*.txt - name: Install Backend Dependencies run: | From 1f3ea2fd961bf733b627848d083312f36c5cf77b Mon Sep 17 00:00:00 2001 From: Harsh-Microsoft Date: Fri, 3 Apr 2026 16:30:26 +0530 Subject: [PATCH 42/72] add Bicep parameter validation workflow and script --- .github/workflows/validate-bicep-params.yml | 110 +++++ infra/scripts/validate_bicep_params.py | 421 ++++++++++++++++++++ 2 files changed, 531 insertions(+) create mode 100644 .github/workflows/validate-bicep-params.yml create mode 100644 infra/scripts/validate_bicep_params.py diff --git a/.github/workflows/validate-bicep-params.yml b/.github/workflows/validate-bicep-params.yml new file mode 100644 index 000000000..79fc0bcbe --- /dev/null +++ b/.github/workflows/validate-bicep-params.yml @@ -0,0 +1,110 @@ +name: Validate Bicep Parameters + +permissions: + contents: read + +on: + schedule: + - cron: '30 6 * * 3' # Wednesday 12:00 PM IST (6:30 AM UTC) + pull_request: + branches: + - main + - dev + paths: + - 'infra/**/*.bicep' + - 'infra/**/*.parameters.json' + workflow_dispatch: + push: + branches: + - hb-psl-38859 + +env: + accelerator_name: "Content Generation" + +jobs: + validate: + runs-on: ubuntu-latest + steps: + - name: Checkout Code + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.11' + + - name: Validate infra/ parameters + id: validate_infra + continue-on-error: true + run: | + set +e + python infra/scripts/validate_bicep_params.py --dir infra --strict --no-color --json-output infra_results.json 2>&1 | tee infra_output.txt + EXIT_CODE=${PIPESTATUS[0]} + set -e + echo "## Infra Param Validation" >> "$GITHUB_STEP_SUMMARY" + echo '```' >> "$GITHUB_STEP_SUMMARY" + cat infra_output.txt >> "$GITHUB_STEP_SUMMARY" + echo '```' >> "$GITHUB_STEP_SUMMARY" + exit $EXIT_CODE + + - name: Set overall result + id: result + run: | + if [[ "${{ steps.validate_infra.outcome }}" == "failure" ]]; then + echo "status=failure" >> "$GITHUB_OUTPUT" + else + echo "status=success" >> "$GITHUB_OUTPUT" + fi + + - name: Upload validation results + if: always() + uses: actions/upload-artifact@v4 + with: + name: bicep-validation-results + path: | + infra_results.json + retention-days: 30 + + - name: Send schedule notification on failure + if: steps.result.outputs.status == 'failure' + env: + LOGICAPP_URL: ${{ secrets.EMAILNOTIFICATION_LOGICAPP_URL_TA }} + GITHUB_REPOSITORY: ${{ github.repository }} + GITHUB_RUN_ID: ${{ github.run_id }} + ACCELERATOR_NAME: ${{ env.accelerator_name }} + run: | + RUN_URL="https://github.com/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}" + INFRA_OUTPUT=$(sed 's/&/\&/g; s//\>/g' infra_output.txt) + + jq -n \ + --arg name "${ACCELERATOR_NAME}" \ + --arg infra "$INFRA_OUTPUT" \ + --arg url "$RUN_URL" \ + '{subject: ("Bicep Parameter Validation Report - " + $name + " - Issues Detected"), body: ("

    Dear Team,

    The scheduled Bicep Parameter Validation for " + $name + " has detected parameter mapping errors.

    infra/ Results:

    " + $infra + "

    Run URL: " + $url + "

    Please fix the parameter mapping issues at your earliest convenience.

    Best regards,
    Your Automation Team

    ")}' \ + | curl -X POST "${LOGICAPP_URL}" \ + -H "Content-Type: application/json" \ + -d @- || echo "Failed to send notification" + + - name: Send schedule notification on success + if: steps.result.outputs.status == 'success' + env: + LOGICAPP_URL: ${{ secrets.EMAILNOTIFICATION_LOGICAPP_URL_TA }} + GITHUB_REPOSITORY: ${{ github.repository }} + GITHUB_RUN_ID: ${{ github.run_id }} + ACCELERATOR_NAME: ${{ env.accelerator_name }} + run: | + RUN_URL="https://github.com/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}" + INFRA_OUTPUT=$(sed 's/&/\&/g; s//\>/g' infra_output.txt) + + jq -n \ + --arg name "${ACCELERATOR_NAME}" \ + --arg infra "$INFRA_OUTPUT" \ + --arg url "$RUN_URL" \ + '{subject: ("Bicep Parameter Validation Report - " + $name + " - Passed"), body: ("

    Dear Team,

    The scheduled Bicep Parameter Validation for " + $name + " has completed successfully. All parameter mappings are valid.

    infra/ Results:

    " + $infra + "

    Run URL: " + $url + "

    Best regards,
    Your Automation Team

    ")}' \ + | curl -X POST "${LOGICAPP_URL}" \ + -H "Content-Type: application/json" \ + -d @- || echo "Failed to send notification" + + - name: Fail if errors found + if: steps.result.outputs.status == 'failure' + run: exit 1 diff --git a/infra/scripts/validate_bicep_params.py b/infra/scripts/validate_bicep_params.py new file mode 100644 index 000000000..78c1a61ef --- /dev/null +++ b/infra/scripts/validate_bicep_params.py @@ -0,0 +1,421 @@ +""" +Bicep Parameter Mapping Validator +================================= +Validates that parameter names in *.parameters.json files exactly match +the param declarations in their corresponding Bicep templates. + +Checks performed: + 1. Whitespace – parameter names must have no leading/trailing spaces. + 2. Existence – every JSON parameter must map to a `param` in the Bicep file. + 3. Casing – names must match exactly (case-sensitive). + 4. Orphaned – required Bicep params (no default) missing from the JSON file. + 5. Env vars – parameter values bound to environment variables must use the + AZURE_ENV_* naming convention, except for explicitly allowed + names (for example, AZURE_LOCATION, AZURE_EXISTING_AIPROJECT_RESOURCE_ID). + +Usage: + # Validate a specific pair + python validate_bicep_params.py --bicep main.bicep --params main.parameters.json + + # Auto-discover all *.parameters.json files under infra/ + python validate_bicep_params.py --dir infra + + # CI mode – exit code 1 on any error + python validate_bicep_params.py --dir infra --strict + +Returns exit-code 0 when no errors are found, 1 when errors are found (in --strict mode). +""" + +from __future__ import annotations + +import argparse +import json +import re +import sys +from dataclasses import dataclass, field +from pathlib import Path + +# Environment variables exempt from the AZURE_ENV_ naming convention. +_ENV_VAR_EXCEPTIONS = {"AZURE_LOCATION", "AZURE_EXISTING_AIPROJECT_RESOURCE_ID"} + +# --------------------------------------------------------------------------- +# Bicep param parser +# --------------------------------------------------------------------------- + +# Matches lines like: param environmentName string +# param tags resourceInput<...> +# param gptDeploymentCapacity int = 150 +# Ignores commented-out lines (// param ...). +# Captures the type token and the rest of the line so we can detect defaults. +_PARAM_RE = re.compile( + r"^(?!//)[ \t]*param\s+(?P[A-Za-z_]\w*)\s+(?P\S+)(?P.*)", + re.MULTILINE, +) + + +@dataclass +class BicepParam: + name: str + has_default: bool + + +def parse_bicep_params(bicep_path: Path) -> list[BicepParam]: + """Extract all `param` declarations from a Bicep file.""" + text = bicep_path.read_text(encoding="utf-8-sig") + params: list[BicepParam] = [] + for match in _PARAM_RE.finditer(text): + name = match.group("name") + param_type = match.group("type") + rest = match.group("rest") + # A param is optional if it has a default value (= ...) or is nullable (type ends with ?) + has_default = "=" in rest or param_type.endswith("?") + params.append(BicepParam(name=name, has_default=has_default)) + return params + + +# --------------------------------------------------------------------------- +# Parameters JSON parser +# --------------------------------------------------------------------------- + + +def parse_parameters_json(json_path: Path) -> list[str]: + """Return the raw parameter key names (preserving whitespace) from a + parameters JSON file.""" + text = json_path.read_text(encoding="utf-8-sig") + # azd parameter files may include ${VAR} or ${VAR=default} placeholders inside + # string values. These are valid JSON strings, but we sanitize them so that + # json.loads remains resilient to azd-specific placeholders and any unusual + # default formats. + sanitized = re.sub(r'"\$\{[^}]+\}"', '"__placeholder__"', text) + try: + data = json.loads(sanitized) + except json.JSONDecodeError: + # Fallback: extract keys with regex for resilience. + return _extract_keys_regex(text) + return list(data.get("parameters", {}).keys()) + + +def parse_parameters_env_vars(json_path: Path) -> dict[str, list[str]]: + """Return a mapping of parameter name → list of azd env var names + referenced in its value (e.g. ``${AZURE_ENV_NAME}``).""" + text = json_path.read_text(encoding="utf-8-sig") + result: dict[str, list[str]] = {} + params = {} + + # Parse the JSON to get the proper parameter structure. + sanitized = re.sub(r'"\$\{([^}]+)\}"', r'"__azd_\1__"', text) + try: + data = json.loads(sanitized) + params = data.get("parameters", {}) + except json.JSONDecodeError: + pass + + # Walk each top-level parameter and scan its entire serialized value + # for ${VAR} references from the original text. + for param_name, param_obj in params.items(): + # Find the raw text block for this parameter in the original file + # by scanning for all ${VAR} patterns in the original value section. + raw_value = json.dumps(param_obj) + # Restore original var references from the sanitized placeholders + for m in re.finditer(r'__azd_([^_].*?)__', raw_value): + var_ref = m.group(1) + # var_ref may contain "=default", extract just the var name + var_name = var_ref.split("=")[0].strip() + if re.match(r'^[A-Za-z_][A-Za-z0-9_]*$', var_name): + result.setdefault(param_name, []).append(var_name) + + return result + + +def _extract_keys_regex(text: str) -> list[str]: + """Fallback key extraction via regex when JSON is non-standard.""" + # Matches the key inside "parameters": { "key": ... } + keys: list[str] = [] + in_params = False + for line in text.splitlines(): + if '"parameters"' in line: + in_params = True + continue + if in_params: + m = re.match(r'\s*"([^"]+)"\s*:', line) + if m: + keys.append(m.group(1)) + return keys + + +# --------------------------------------------------------------------------- +# Validation logic +# --------------------------------------------------------------------------- + +@dataclass +class ValidationIssue: + severity: str # "ERROR" or "WARNING" + param_file: str + bicep_file: str + param_name: str + message: str + + +@dataclass +class ValidationResult: + pair: str + issues: list[ValidationIssue] = field(default_factory=list) + + @property + def has_errors(self) -> bool: + return any(i.severity == "ERROR" for i in self.issues) + + +def validate_pair( + bicep_path: Path, + params_path: Path, +) -> ValidationResult: + """Validate a single (bicep, parameters.json) pair.""" + result = ValidationResult( + pair=f"{params_path.name} -> {bicep_path.name}" + ) + + bicep_params = parse_bicep_params(bicep_path) + bicep_names = {p.name for p in bicep_params} + bicep_names_lower = {p.name.lower(): p.name for p in bicep_params} + required_bicep = {p.name for p in bicep_params if not p.has_default} + + json_keys = parse_parameters_json(params_path) + + seen_json_keys: set[str] = set() + + for raw_key in json_keys: + stripped = raw_key.strip() + + # 1. Whitespace check + if raw_key != stripped: + result.issues.append(ValidationIssue( + severity="ERROR", + param_file=str(params_path), + bicep_file=str(bicep_path), + param_name=repr(raw_key), + message=( + f"Parameter name has leading/trailing whitespace. " + f"Raw key: {repr(raw_key)}, expected: {repr(stripped)}" + ), + )) + + # 2. Exact match check + if stripped not in bicep_names: + # 3. Case-insensitive near-match + suggestion = bicep_names_lower.get(stripped.lower()) + if suggestion: + result.issues.append(ValidationIssue( + severity="ERROR", + param_file=str(params_path), + bicep_file=str(bicep_path), + param_name=stripped, + message=( + f"Case mismatch: JSON has '{stripped}', " + f"Bicep declares '{suggestion}'." + ), + )) + else: + result.issues.append(ValidationIssue( + severity="ERROR", + param_file=str(params_path), + bicep_file=str(bicep_path), + param_name=stripped, + message=( + f"Parameter '{stripped}' exists in JSON but has no " + f"matching param in the Bicep template." + ), + )) + seen_json_keys.add(stripped) + + # 4. Required Bicep params missing from JSON + for req in sorted(required_bicep - seen_json_keys): + result.issues.append(ValidationIssue( + severity="WARNING", + param_file=str(params_path), + bicep_file=str(bicep_path), + param_name=req, + message=( + f"Required Bicep param '{req}' (no default value) is not " + f"supplied in the parameters file." + ), + )) + + # 5. Env var naming convention – all azd vars should start with AZURE_ENV_ + env_vars = parse_parameters_env_vars(params_path) + for param_name, var_names in sorted(env_vars.items()): + for var in var_names: + if not var.startswith("AZURE_ENV_") and var not in _ENV_VAR_EXCEPTIONS: + result.issues.append(ValidationIssue( + severity="WARNING", + param_file=str(params_path), + bicep_file=str(bicep_path), + param_name=param_name, + message=( + f"Env var '${{{var}}}' does not follow the " + f"AZURE_ENV_ naming convention." + ), + )) + + return result + + +# --------------------------------------------------------------------------- +# Discovery – find (bicep, params) pairs automatically +# --------------------------------------------------------------------------- + +def discover_pairs(infra_dir: Path) -> list[tuple[Path, Path]]: + """For each *.parameters.json, find the matching Bicep file. + + Naming convention: a file like ``main.waf.parameters.json`` is a + variant of ``main.parameters.json`` — the user copies its contents + into ``main.parameters.json`` before running ``azd up``. Both + files should therefore be validated against ``main.bicep``. + + Resolution order: + 1. Exact stem match (e.g. ``foo.parameters.json`` → ``foo.bicep``). + 2. Base-stem match (e.g. ``main.waf.parameters.json`` → ``main.bicep``). + """ + pairs: list[tuple[Path, Path]] = [] + for pf in sorted(infra_dir.rglob("*.parameters.json")): + stem = pf.name.replace(".parameters.json", "") + bicep_candidate = pf.parent / f"{stem}.bicep" + if bicep_candidate.exists(): + pairs.append((bicep_candidate, pf)) + else: + # Try the base stem (first segment before the first dot). + base_stem = stem.split(".")[0] + base_candidate = pf.parent / f"{base_stem}.bicep" + if base_candidate.exists(): + pairs.append((base_candidate, pf)) + else: + print(f" [SKIP] No matching Bicep file for {pf.name}") + return pairs + + +# --------------------------------------------------------------------------- +# Reporting +# --------------------------------------------------------------------------- + +_COLORS = { + "ERROR": "\033[91m", # red + "WARNING": "\033[93m", # yellow + "OK": "\033[92m", # green + "RESET": "\033[0m", +} + + +def print_report(results: list[ValidationResult], *, use_color: bool = True) -> None: + c = _COLORS if use_color else {k: "" for k in _COLORS} + total_errors = 0 + total_warnings = 0 + + for r in results: + errors = [i for i in r.issues if i.severity == "ERROR"] + warnings = [i for i in r.issues if i.severity == "WARNING"] + total_errors += len(errors) + total_warnings += len(warnings) + + if not r.issues: + print(f"\n{c['OK']}[PASS]{c['RESET']} {r.pair}") + elif errors: + print(f"\n{c['ERROR']}[FAIL]{c['RESET']} {r.pair}") + else: + print(f"\n{c['WARNING']}[WARN]{c['RESET']} {r.pair}") + + for issue in r.issues: + tag = ( + f"{c['ERROR']}ERROR{c['RESET']}" + if issue.severity == "ERROR" + else f"{c['WARNING']}WARN {c['RESET']}" + ) + print(f" {tag} {issue.param_name}: {issue.message}") + + print(f"\n{'='*60}") + print(f"Total: {total_errors} error(s), {total_warnings} warning(s)") + if total_errors == 0: + print(f"{c['OK']}All parameter mappings are valid.{c['RESET']}") + else: + print(f"{c['ERROR']}Parameter mapping issues detected!{c['RESET']}") + + +# --------------------------------------------------------------------------- +# CLI +# --------------------------------------------------------------------------- + +def main() -> int: + parser = argparse.ArgumentParser( + description="Validate Bicep ↔ parameters.json parameter mappings.", + ) + parser.add_argument( + "--bicep", + type=Path, + help="Path to a specific Bicep template.", + ) + parser.add_argument( + "--params", + type=Path, + help="Path to a specific parameters JSON file.", + ) + parser.add_argument( + "--dir", + type=Path, + help="Directory to scan for *.parameters.json files (auto-discovers pairs).", + ) + parser.add_argument( + "--strict", + action="store_true", + help="Exit with code 1 if any errors are found.", + ) + parser.add_argument( + "--no-color", + action="store_true", + help="Disable colored output (useful for CI logs).", + ) + parser.add_argument( + "--json-output", + type=Path, + help="Write results as JSON to the given file path.", + ) + args = parser.parse_args() + + results: list[ValidationResult] = [] + + if args.bicep and args.params: + results.append(validate_pair(args.bicep, args.params)) + elif args.dir: + pairs = discover_pairs(args.dir) + if not pairs: + print(f"No (bicep, parameters.json) pairs found under {args.dir}") + return 0 + for bicep_path, params_path in pairs: + results.append(validate_pair(bicep_path, params_path)) + else: + parser.error("Provide either --bicep/--params or --dir.") + + print_report(results, use_color=not args.no_color) + + # Optional JSON output for CI artifact consumption + if args.json_output: + json_data = [] + for r in results: + for issue in r.issues: + json_data.append({ + "severity": issue.severity, + "paramFile": issue.param_file, + "bicepFile": issue.bicep_file, + "paramName": issue.param_name, + "message": issue.message, + }) + args.json_output.parent.mkdir(parents=True, exist_ok=True) + args.json_output.write_text( + json.dumps(json_data, indent=2), encoding="utf-8" + ) + print(f"\nJSON report written to {args.json_output}") + + has_errors = any(r.has_errors for r in results) + return 1 if args.strict and has_errors else 0 + + +if __name__ == "__main__": + sys.exit(main()) From af14382214aea08918077795abfa9eb56b0e774b Mon Sep 17 00:00:00 2001 From: Harsh-Microsoft Date: Fri, 3 Apr 2026 16:34:55 +0530 Subject: [PATCH 43/72] update workflow triggers and notification conditions in validate-bicep-params.yml --- .github/workflows/validate-bicep-params.yml | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/.github/workflows/validate-bicep-params.yml b/.github/workflows/validate-bicep-params.yml index 79fc0bcbe..d5c7aed00 100644 --- a/.github/workflows/validate-bicep-params.yml +++ b/.github/workflows/validate-bicep-params.yml @@ -14,9 +14,6 @@ on: - 'infra/**/*.bicep' - 'infra/**/*.parameters.json' workflow_dispatch: - push: - branches: - - hb-psl-38859 env: accelerator_name: "Content Generation" @@ -66,7 +63,7 @@ jobs: retention-days: 30 - name: Send schedule notification on failure - if: steps.result.outputs.status == 'failure' + if: github.event_name == 'schedule' && steps.result.outputs.status == 'failure' env: LOGICAPP_URL: ${{ secrets.EMAILNOTIFICATION_LOGICAPP_URL_TA }} GITHUB_REPOSITORY: ${{ github.repository }} @@ -86,7 +83,7 @@ jobs: -d @- || echo "Failed to send notification" - name: Send schedule notification on success - if: steps.result.outputs.status == 'success' + if: github.event_name == 'schedule' && steps.result.outputs.status == 'success' env: LOGICAPP_URL: ${{ secrets.EMAILNOTIFICATION_LOGICAPP_URL_TA }} GITHUB_REPOSITORY: ${{ github.repository }} From 89b6822e9618d910fe54b6b258f8d1f0626eebd2 Mon Sep 17 00:00:00 2001 From: Harsh-Microsoft Date: Fri, 3 Apr 2026 17:24:42 +0530 Subject: [PATCH 44/72] include validate_bicep_params.py in workflow paths --- .github/workflows/validate-bicep-params.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/validate-bicep-params.yml b/.github/workflows/validate-bicep-params.yml index d5c7aed00..bcb03f097 100644 --- a/.github/workflows/validate-bicep-params.yml +++ b/.github/workflows/validate-bicep-params.yml @@ -13,6 +13,7 @@ on: paths: - 'infra/**/*.bicep' - 'infra/**/*.parameters.json' + - 'infra/scripts/validate_bicep_params.py' workflow_dispatch: env: From 939b9eb7d35dae5ac44764c96b46f86abe105eb0 Mon Sep 17 00:00:00 2001 From: Ragini-Microsoft Date: Fri, 3 Apr 2026 19:33:14 +0530 Subject: [PATCH 45/72] Changes for deployment of custom changes --- azure_custom.yaml | 253 +++++++-- infra/main_custom.bicep | 1079 +++++++++++++++++++++++++++++++++++++++ scripts/post_deploy.py | 38 +- 3 files changed, 1324 insertions(+), 46 deletions(-) create mode 100644 infra/main_custom.bicep diff --git a/azure_custom.yaml b/azure_custom.yaml index 3b7bb0261..bdd176687 100644 --- a/azure_custom.yaml +++ b/azure_custom.yaml @@ -1,48 +1,241 @@ -environment: - name: document-generation - location: eastus - -name: document-generation +name: content-generation metadata: - template: document-generation@1.0 + template: content-generation@1.22 requiredVersions: azd: '>= 1.18.0 != 1.23.9' -parameters: - solutionPrefix: - type: string - default: bs-azdtest - otherLocation: - type: string - default: eastus2 - baseUrl: - type: string - default: 'https://github.com/microsoft/document-generation-solution-accelerator' +infra: + path: ./infra + module: main services: - webapp: - project: ./src - language: py + frontend: + project: ./src/app/frontend-server + language: js host: appservice dist: ./dist + resourceName: ${APP_SERVICE_NAME} hooks: prepackage: windows: shell: pwsh - run: ../infra/scripts/package_webapp.ps1 - interactive: true + run: ../../../infra/scripts/package_frontend.ps1 continueOnError: false posix: shell: sh - run: bash ../infra/scripts/package_webapp.sh - interactive: true + run: chmod +x ../../../infra/scripts/package_frontend.sh && ../../../infra/scripts/package_frontend.sh continueOnError: false -deployment: - mode: Incremental - template: ./infra/main.bicep # Path to the main.bicep file inside the 'deployment' folder - parameters: - solutionPrefix: ${parameters.solutionPrefix} - otherLocation: ${parameters.otherLocation} - baseUrl: ${parameters.baseUrl} +hooks: + preprovision: + windows: + shell: pwsh + run: | + Write-Host "Preparing deployment..." -ForegroundColor Cyan + + # Check if this is first run (ACR doesn't exist yet) + # Set IMAGE_TAG='none' to skip ACI deployment until image is built + $currentTag = azd env get-value IMAGE_TAG 2>$null + $global:LASTEXITCODE = 0 + + if (-not $env:AZURE_CONTAINER_REGISTRY_NAME -and $currentTag -ne 'latest') { + Write-Host "First deployment - ACI will be deployed after image build" -ForegroundColor Yellow + azd env set IMAGE_TAG none + } + continueOnError: false + posix: + shell: sh + run: | + echo "Preparing deployment..." + + # Check if this is first run (ACR doesn't exist yet) + current_tag=$(azd env get-value IMAGE_TAG 2>/dev/null || echo "") + + if [ -z "$AZURE_CONTAINER_REGISTRY_NAME" ] && [ "$current_tag" != "latest" ]; then + echo "First deployment - ACI will be deployed after image build" + azd env set IMAGE_TAG none + fi + continueOnError: false + + postprovision: + windows: + shell: pwsh + run: | + $acrName = $env:AZURE_CONTAINER_REGISTRY_NAME + $resourceGroup = $env:RESOURCE_GROUP_NAME + $backendImage = $env:BACKEND_IMAGE_NAME + $appServiceName = $env:APP_SERVICE_NAME + + if (-not $acrName -or -not $resourceGroup -or -not $appServiceName) { + Write-Host "ERROR: Missing required environment variables" -ForegroundColor Red + exit 1 + } + + # Check if ACI already exists (reads from persisted azd env) + $aciName = azd env get-value CONTAINER_INSTANCE_NAME 2>$null + $global:LASTEXITCODE = 0 + + # ===== Build Backend Image (ACR Build) ===== + Write-Host "" + Write-Host "===== Building Backend Image =====" -ForegroundColor Yellow + Write-Host "Registry: $acrName" -ForegroundColor Cyan + Write-Host "Image: ${backendImage}:latest" -ForegroundColor Cyan + + az acr login --name $acrName 2>$null + az acr build --registry $acrName --image "${backendImage}:latest" --file ./src/backend/ApiApp.Dockerfile ./src/backend --no-logs + if ($LASTEXITCODE -ne 0) { + Write-Host "Failed to build container image" -ForegroundColor Red + exit 1 + } + Write-Host "Container image built and pushed successfully!" -ForegroundColor Green + + # ===== Deploy ACI if not already deployed ===== + if (-not $aciName) { + Write-Host "" + Write-Host "===== Deploying Container Instance =====" -ForegroundColor Yellow + azd env set IMAGE_TAG latest + + # Use az deployment instead of azd provision to avoid hook recursion + # Pass parameters inline (main.parameters.json uses AZD ${VAR} syntax not supported by az CLI) + Write-Host "Deploying ACI via Bicep..." -ForegroundColor Cyan + + az deployment group create ` + --resource-group $resourceGroup ` + --template-file ./infra/main.bicep ` + --parameters solutionName=$env:AZURE_ENV_NAME ` + --parameters location=$env:AZURE_LOCATION ` + --parameters azureAiServiceLocation=$env:AZURE_ENV_OPENAI_LOCATION ` + --parameters imageTag=latest ` + --query "properties.outputs" -o json | Out-Null + + if ($LASTEXITCODE -eq 0) { + # Refresh azd env with new outputs + azd env refresh --no-prompt 2>$null + Write-Host "Container Instance deployed successfully!" -ForegroundColor Green + } else { + Write-Host "Container Instance deployment failed" -ForegroundColor Red + } + } else { + Write-Host "" + Write-Host "Container Instance: $aciName" -ForegroundColor Cyan + } + + Write-Host "" + Write-Host "===== Postprovision Complete - Frontend will deploy next =====" -ForegroundColor Green + + # Ensure postprovision exits successfully so frontend deploys + exit 0 + interactive: true + continueOnError: false + + posix: + shell: sh + run: | + ACR_NAME="$AZURE_CONTAINER_REGISTRY_NAME" + RESOURCE_GROUP="$RESOURCE_GROUP_NAME" + BACKEND_IMAGE="$BACKEND_IMAGE_NAME" + APP_SERVICE="$APP_SERVICE_NAME" + + if [ -z "$ACR_NAME" ] || [ -z "$RESOURCE_GROUP" ] || [ -z "$APP_SERVICE" ]; then + echo "ERROR: Missing required environment variables" + exit 1 + fi + + # Check if ACI already exists (reads from persisted azd env) + ACI_NAME=$(azd env get-value CONTAINER_INSTANCE_NAME 2>/dev/null || echo "") + + # ===== Build Backend Image (ACR Build) ===== + echo "" + echo "===== Building Backend Image =====" + echo "Registry: $ACR_NAME" + echo "Image: $BACKEND_IMAGE:latest" + + if az acr build --registry "$ACR_NAME" --image "$BACKEND_IMAGE:latest" --file ./src/backend/ApiApp.Dockerfile ./src/backend --no-logs; then + echo "Container image built and pushed successfully!" + else + echo "Failed to build container image" + exit 1 + fi + + # ===== Deploy ACI if not already deployed ===== + if [ -z "$ACI_NAME" ]; then + echo "" + echo "===== Deploying Container Instance =====" + azd env set IMAGE_TAG latest + + # Use az deployment instead of azd provision to avoid hook recursion + # Pass parameters inline (main.parameters.json uses AZD ${VAR} syntax not supported by az CLI) + echo "Deploying ACI via Bicep..." + if az deployment group create \ + --resource-group "$RESOURCE_GROUP" \ + --template-file ./infra/main.bicep \ + --parameters solutionName="$AZURE_ENV_NAME" \ + --parameters location="$AZURE_LOCATION" \ + --parameters azureAiServiceLocation="$AZURE_ENV_OPENAI_LOCATION" \ + --parameters imageTag=latest \ + --query "properties.outputs" -o json > /dev/null; then + # Refresh azd env with new outputs + azd env refresh --no-prompt 2>/dev/null + echo "Container Instance deployed successfully!" + else + echo "Container Instance deployment failed" + fi + else + echo "" + echo "Container Instance: $ACI_NAME" + fi + + echo "" + echo "===== Postprovision Complete - Frontend will deploy next =====" + + # Ensure postprovision exits successfully so frontend deploys + exit 0 + interactive: true + continueOnError: false + + postdeploy: + windows: + shell: pwsh + run: | + Write-Host "===== Running Post-Deploy Script =====" -ForegroundColor Yellow + $python = "python" + if (Test-Path "./.venv/Scripts/python.exe") { $python = "./.venv/Scripts/python.exe" } + & $python -m pip install -r ./scripts/requirements-post-deploy.txt --quiet 2>$null + + if (Test-Path "./scripts/post_deploy.py") { + & $python ./scripts/post_deploy.py --skip-tests + if ($LASTEXITCODE -eq 0) { + Write-Host "Post-deploy script completed successfully!" -ForegroundColor Green + } else { + Write-Host "Post-deploy script completed with warnings" -ForegroundColor Yellow + } + } + + Write-Host "" + Write-Host "===== Deployment Complete =====" -ForegroundColor Green + Write-Host "Access the web application:" -ForegroundColor White + Write-Host " $env:WEB_APP_URL" -ForegroundColor Cyan + interactive: true + continueOnError: false + + posix: + shell: sh + run: | + echo "===== Running Post-Deploy Script =====" + PYTHON="python3" + if [ -f "./.venv/bin/python" ]; then PYTHON="./.venv/bin/python"; fi + $PYTHON -m pip install -r ./scripts/requirements-post-deploy.txt --quiet 2>/dev/null + + if [ -f "./scripts/post_deploy.py" ]; then + $PYTHON ./scripts/post_deploy.py --skip-tests \ + && echo "Post-deploy script completed successfully!" \ + || echo "Post-deploy script completed with warnings" + fi + + echo "" + echo "===== Deployment Complete =====" + echo "Access the web application:" + echo " $WEB_APP_URL" + interactive: true + continueOnError: false diff --git a/infra/main_custom.bicep b/infra/main_custom.bicep new file mode 100644 index 000000000..e2cf5cf78 --- /dev/null +++ b/infra/main_custom.bicep @@ -0,0 +1,1079 @@ +// ========== main.bicep ========== // +targetScope = 'resourceGroup' + +metadata name = 'Intelligent Content Generation Accelerator' +metadata description = '''Solution Accelerator for multimodal marketing content generation using Microsoft Agent Framework. +''' + +@minLength(3) +@maxLength(15) +@description('Optional. A unique application/solution name for all resources in this deployment.') +param solutionName string = 'contentgen' + +@minLength(3) +@maxLength(5) +@description('Optional. A unique text value for the solution.') +param solutionUniqueText string = substring(uniqueString(subscription().id, resourceGroup().name, solutionName), 0, 5) + +@allowed([ + 'australiaeast' + 'centralus' + 'eastasia' + 'eastus' + 'eastus2' + 'japaneast' + 'northeurope' + 'southeastasia' + 'swedencentral' + 'uksouth' + 'westus' + 'westus3' +]) +@metadata({ azd: { type: 'location' } }) +@description('Required. Azure region for all services.') +param location string + +@minLength(3) +@description('Optional. Secondary location for databases creation.') +param secondaryLocation string = 'uksouth' + +// NOTE: Metadata must be compile-time constants. Update usageName manually if you change model parameters. +// Format: 'OpenAI..,' +// Allowed regions: Union of GPT-5.1, gpt-image-1-mini, and gpt-image-1.5 GlobalStandard availability +@allowed([ + 'australiaeast' + 'canadaeast' + 'eastus2' + 'japaneast' + 'koreacentral' + 'polandcentral' + 'swedencentral' + 'switzerlandnorth' + 'uaenorth' + 'uksouth' + 'westus3' +]) +@metadata({ + azd: { + type: 'location' + usageName: [ + 'OpenAI.GlobalStandard.gpt-5.1,150' + 'OpenAI.GlobalStandard.gpt-image-1-mini,1' + ] + } +}) +@description('Required. Location for AI deployments.') +param azureAiServiceLocation string + +@minLength(1) +@allowed([ + 'Standard' + 'GlobalStandard' +]) +@description('Optional. GPT model deployment type.') +param gptModelDeploymentType string = 'GlobalStandard' + +@minLength(1) +@description('Optional. Name of the GPT model to deploy.') +param gptModelName string = 'gpt-5.1' + +@description('Optional. Version of the GPT model to deploy.') +param gptModelVersion string = '2025-11-13' + +@description('Optional. Image model to deploy: gpt-image-1-mini, gpt-image-1.5, or none to skip.') +@allowed([ + 'gpt-image-1-mini' + 'gpt-image-1.5' + 'none' +]) +param imageModelChoice string = 'gpt-image-1-mini' + +@description('Optional. API version for Azure OpenAI service.') +param azureOpenaiAPIVersion string = '2025-01-01-preview' + +@description('Optional. API version for Azure AI Agent service.') +param azureAiAgentApiVersion string = '2025-05-01' + +@minValue(10) +@description('Optional. AI model deployment token capacity.') +param gptModelCapacity int = 150 + +@minValue(1) +@description('Optional. Image model deployment capacity (RPM).') +param imageModelCapacity int = 1 + +@description('Optional. Existing Log Analytics Workspace Resource ID.') +param existingLogAnalyticsWorkspaceId string = '' + +@description('Optional. Resource ID of an existing Foundry project.') +param azureExistingAIProjectResourceId string = '' + +@description('Optional. Deploy Azure Bastion and Jumpbox VM for private network administration.') +param deployBastionAndJumpbox bool = false + +@description('Optional. The tags to apply to all deployed Azure resources.') +param tags object = {} + +@description('Optional. Enable monitoring for applicable resources (WAF-aligned).') +param enableMonitoring bool = false + +@description('Optional. Enable Azure AI Foundry mode for multi-agent orchestration.') +param useFoundryMode bool = true + +@description('Optional. Enable scalability for applicable resources (WAF-aligned).') +param enableScalability bool = false + +@description('Optional. Enable redundancy for applicable resources (WAF-aligned).') +param enableRedundancy bool = false + +@description('Optional. Enable private networking for applicable resources (WAF-aligned).') +param enablePrivateNetworking bool = false + +@description('Optional. Enable/Disable usage telemetry for module.') +param enableTelemetry bool = true + +@description('Optional. Frontend image name (without tag).') +param frontendImageName string = 'content-gen-app' + +@description('Optional. Backend image name (without tag).') +param backendImageName string = 'content-gen-api' + +@description('Optional. Image tag for container deployment. Leave empty to skip ACI deployment.') +param imageTag string + +@description('Optional. Created by user name.') +param createdBy string = contains(deployer(), 'userPrincipalName')? split(deployer().userPrincipalName, '@')[0]: deployer().objectId + +// ============== // +// Variables // +// ============== // + +var solutionLocation = empty(location) ? resourceGroup().location : location + +var solutionSuffix = toLower(trim(replace( + replace( + replace(replace(replace(replace('${solutionName}${solutionUniqueText}', '-', ''), '_', ''), '.', ''), '/', ''), + ' ', + '' + ), + '*', + '' +))) + +var acrResourceName = 'cr${solutionSuffix}' + +var cosmosDbZoneRedundantHaRegionPairs = { + australiaeast: 'uksouth' + centralus: 'eastus2' + eastasia: 'southeastasia' + eastus: 'centralus' + eastus2: 'centralus' + japaneast: 'australiaeast' + northeurope: 'westeurope' + southeastasia: 'eastasia' + uksouth: 'westeurope' + westus: 'westus3' + westus3: 'westus' +} +var cosmosDbHaLocation = cosmosDbZoneRedundantHaRegionPairs[?resourceGroup().location] ?? secondaryLocation + +var replicaRegionPairs = { + australiaeast: 'australiasoutheast' + centralus: 'westus' + eastasia: 'japaneast' + eastus: 'centralus' + eastus2: 'centralus' + japaneast: 'eastasia' + northeurope: 'westeurope' + southeastasia: 'eastasia' + uksouth: 'westeurope' + westus: 'westus3' + westus3: 'westus' +} +var replicaLocation = replicaRegionPairs[?resourceGroup().location] ?? secondaryLocation + +var azureSearchIndex = 'products' +var aiSearchName = 'srch-${solutionSuffix}' +var aiSearchConnectionName = 'foundry-search-connection-${solutionSuffix}' + +// Extracts subscription, resource group, and workspace name from the resource ID +var useExistingLogAnalytics = !empty(existingLogAnalyticsWorkspaceId) +var useExistingAiFoundryAiProject = !empty(azureExistingAIProjectResourceId) +var aiFoundryAiServicesResourceGroupName = useExistingAiFoundryAiProject + ? split(azureExistingAIProjectResourceId, '/')[4] + : 'rg-${solutionSuffix}' +var aiFoundryAiServicesSubscriptionId = useExistingAiFoundryAiProject + ? split(azureExistingAIProjectResourceId, '/')[2] + : subscription().subscriptionId +var aiFoundryAiServicesResourceName = useExistingAiFoundryAiProject + ? split(azureExistingAIProjectResourceId, '/')[8] + : 'aif-${solutionSuffix}' +var aiFoundryAiProjectResourceName = useExistingAiFoundryAiProject + ? split(azureExistingAIProjectResourceId, '/')[10] + : 'proj-${solutionSuffix}' + +// Base model deployments (GPT only - no embeddings needed for content generation) +var baseModelDeployments = [ + { + format: 'OpenAI' + name: gptModelName + model: gptModelName + sku: { + name: gptModelDeploymentType + capacity: gptModelCapacity + } + version: gptModelVersion + raiPolicyName: 'Microsoft.Default' + } +] + +// Image model configuration based on choice +var imageModelConfig = { + 'gpt-image-1-mini': { + name: 'gpt-image-1-mini' + version: '2025-10-06' + sku: 'GlobalStandard' + } + 'gpt-image-1.5': { + name: 'gpt-image-1.5' + version: '2025-12-16' + sku: 'GlobalStandard' + } + none: { + name: '' + version: '' + sku: '' + } +} + +// Image model deployment (optional) +var imageModelDeployment = imageModelChoice != 'none' ? [ + { + format: 'OpenAI' + name: imageModelConfig[imageModelChoice].name + model: imageModelConfig[imageModelChoice].name + sku: { + name: imageModelConfig[imageModelChoice].sku + capacity: imageModelCapacity + } + version: imageModelConfig[imageModelChoice].version + raiPolicyName: 'Microsoft.Default' + } +] : [] + +// Combine deployments based on imageModelChoice +var aiFoundryAiServicesModelDeployment = concat(baseModelDeployments, imageModelDeployment) + +var aiFoundryAiProjectDescription = 'Content Generation AI Foundry Project' + +// Reference existing resource group to access current tags +resource existingResourceGroup 'Microsoft.Resources/resourceGroups@2024-03-01' existing = { + scope: subscription() + name: resourceGroup().name +} + +var existingTags = existingResourceGroup.tags ?? {} + +// ============== // +// Resources // +// ============== // + +#disable-next-line no-deployments-resources +resource avmTelemetry 'Microsoft.Resources/deployments@2024-03-01' = if (enableTelemetry) { + name: '46d3xbcp.ptn.sa-contentgeneration.${replace('-..--..-', '.', '-')}.${substring(uniqueString(deployment().name, solutionLocation), 0, 4)}' + properties: { + mode: 'Incremental' + template: { + '$schema': 'https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#' + contentVersion: '1.0.0.0' + resources: [] + outputs: { + telemetry: { + type: 'String' + value: 'For more information, see https://aka.ms/avm/TelemetryInfo' + } + } + } + } +} + +// ========== Resource Group Tag ========== // +resource resourceGroupTags 'Microsoft.Resources/tags@2025-04-01' = { + name: 'default' + properties: { + tags: union( + existingTags, + tags, + { + TemplateName: 'ContentGen' + Type: enablePrivateNetworking ? 'WAF' : 'Non-WAF' + CreatedBy: createdBy + } + ) + } +} + +// ========== Log Analytics Workspace ========== // +var logAnalyticsWorkspaceResourceName = 'log-${solutionSuffix}' +module logAnalyticsWorkspace 'br/public:avm/res/operational-insights/workspace:0.15.0' = if (enableMonitoring && !useExistingLogAnalytics) { + name: take('avm.res.operational-insights.workspace.${logAnalyticsWorkspaceResourceName}', 64) + params: { + name: logAnalyticsWorkspaceResourceName + tags: tags + location: solutionLocation + enableTelemetry: enableTelemetry + skuName: 'PerGB2018' + dataRetention: 365 + features: { enableLogAccessUsingOnlyResourcePermissions: true } + diagnosticSettings: [{ useThisWorkspace: true }] + dailyQuotaGb: enableRedundancy ? '10' : null + replication: enableRedundancy + ? { + enabled: true + location: replicaLocation + } + : null + publicNetworkAccessForIngestion: enablePrivateNetworking ? 'Disabled' : 'Enabled' + publicNetworkAccessForQuery: enablePrivateNetworking ? 'Disabled' : 'Enabled' + } +} +var logAnalyticsWorkspaceResourceId = useExistingLogAnalytics + ? existingLogAnalyticsWorkspaceId + : (enableMonitoring ? logAnalyticsWorkspace!.outputs.resourceId : '') + +// ========== Application Insights ========== // +var applicationInsightsResourceName = 'appi-${solutionSuffix}' +module applicationInsights 'br/public:avm/res/insights/component:0.7.1' = if (enableMonitoring) { + name: take('avm.res.insights.component.${applicationInsightsResourceName}', 64) + params: { + name: applicationInsightsResourceName + tags: tags + location: solutionLocation + enableTelemetry: enableTelemetry + retentionInDays: 365 + kind: 'web' + disableIpMasking: false + flowType: 'Bluefield' + workspaceResourceId: logAnalyticsWorkspaceResourceId + } +} + +// ========== User Assigned Identity ========== // +var userAssignedIdentityResourceName = 'id-${solutionSuffix}' +module userAssignedIdentity 'br/public:avm/res/managed-identity/user-assigned-identity:0.5.0' = { + name: take('avm.res.managed-identity.user-assigned-identity.${userAssignedIdentityResourceName}', 64) + params: { + name: userAssignedIdentityResourceName + location: solutionLocation + tags: tags + enableTelemetry: enableTelemetry + } +} + +// ========== Azure Container Registry ========== // +// CUSTOM DEPLOYMENT: ACR for remote Docker builds (AZD pushes images here) +module containerRegistry 'br/public:avm/res/container-registry/registry:0.9.0' = { + name: take('avm.res.container-registry.registry.${acrResourceName}', 64) + params: { + name: acrResourceName + location: solutionLocation + tags: tags + enableTelemetry: enableTelemetry + acrSku: 'Standard' + acrAdminUserEnabled: false + anonymousPullEnabled: true // Allows ACI to pull images without credentials + publicNetworkAccess: 'Enabled' + networkRuleBypassOptions: 'AzureServices' + } +} + +// ========== Virtual Network and Networking Components ========== // +module virtualNetwork 'modules/virtualNetwork.bicep' = if (enablePrivateNetworking) { + name: take('module.virtualNetwork.${solutionSuffix}', 64) + params: { + vnetName: 'vnet-${solutionSuffix}' + vnetLocation: solutionLocation + vnetAddressPrefixes: ['10.0.0.0/20'] + tags: tags + logAnalyticsWorkspaceId: logAnalyticsWorkspaceResourceId + enableTelemetry: enableTelemetry + resourceSuffix: solutionSuffix + deployBastionAndJumpbox: deployBastionAndJumpbox + } + dependsOn: (enableMonitoring && !useExistingLogAnalytics) ? [logAnalyticsWorkspace] : [] +} + +// ========== Private DNS Zones ========== // +// Only create DNS zones for resources that need private endpoints: +// - Cognitive Services (for AI Services) +// - OpenAI (for Azure OpenAI endpoints) +// - Blob Storage +// - Cosmos DB (Documents) +var privateDnsZones = [ + 'privatelink.cognitiveservices.azure.com' + 'privatelink.openai.azure.com' + 'privatelink.blob.${environment().suffixes.storage}' + 'privatelink.documents.azure.com' +] + +var dnsZoneIndex = { + cognitiveServices: 0 + openAI: 1 + storageBlob: 2 + cosmosDB: 3 +} + +@batchSize(5) +module avmPrivateDnsZones 'br/public:avm/res/network/private-dns-zone:0.8.0' = [ + for (zone, i) in privateDnsZones: if (enablePrivateNetworking) { + name: take('avm.res.network.private-dns-zone.${replace(zone, '.', '-')}', 64) + params: { + name: zone + tags: tags + enableTelemetry: enableTelemetry + virtualNetworkLinks: [ + { + virtualNetworkResourceId: enablePrivateNetworking ? virtualNetwork!.outputs.resourceId : '' + registrationEnabled: false + } + ] + } + } +] + +// ========== AI Foundry: AI Services ========== // +module aiFoundryAiServices 'br/public:avm/res/cognitive-services/account:0.14.1' = if (!useExistingAiFoundryAiProject) { + name: take('avm.res.cognitive-services.account.${aiFoundryAiServicesResourceName}', 64) + params: { + name: aiFoundryAiServicesResourceName + location: azureAiServiceLocation + tags: tags + enableTelemetry: enableTelemetry + sku: 'S0' + kind: 'AIServices' + disableLocalAuth: true + allowProjectManagement: true + customSubDomainName: aiFoundryAiServicesResourceName + restrictOutboundNetworkAccess: false + deployments: [ + for deployment in aiFoundryAiServicesModelDeployment: { + name: deployment.name + model: { + format: deployment.format + name: deployment.name + version: deployment.version + } + raiPolicyName: deployment.raiPolicyName + sku: { + name: deployment.sku.name + capacity: deployment.sku.capacity + } + } + ] + networkAcls: { + defaultAction: 'Allow' + virtualNetworkRules: [] + ipRules: [] + } + managedIdentities: { + userAssignedResourceIds: [userAssignedIdentity!.outputs.resourceId] + } + roleAssignments: [ + { + roleDefinitionIdOrName: '53ca6127-db72-4b80-b1b0-d745d6d5456d' // Azure AI User + principalId: userAssignedIdentity.outputs.principalId + principalType: 'ServicePrincipal' + } + { + roleDefinitionIdOrName: '64702f94-c441-49e6-a78b-ef80e0188fee' // Azure AI Developer + principalId: userAssignedIdentity.outputs.principalId + principalType: 'ServicePrincipal' + } + { + roleDefinitionIdOrName: '5e0bd9bd-7b93-4f28-af87-19fc36ad61bd' // Cognitive Services OpenAI User + principalId: userAssignedIdentity.outputs.principalId + principalType: 'ServicePrincipal' + } + { + roleDefinitionIdOrName: '53ca6127-db72-4b80-b1b0-d745d6d5456d' // Azure AI User for deployer + principalId: deployer().objectId + } + ] + diagnosticSettings: enableMonitoring ? [{ workspaceResourceId: logAnalyticsWorkspaceResourceId }] : null + publicNetworkAccess: enablePrivateNetworking ? 'Disabled' : 'Enabled' + // Note: Private endpoint is created separately to avoid timing issues with model deployments + } +} + +// Create private endpoint for AI Services AFTER the account is fully provisioned +module aiServicesPrivateEndpoint 'br/public:avm/res/network/private-endpoint:0.11.1' = if (!useExistingAiFoundryAiProject && enablePrivateNetworking) { + name: take('pep-ai-services-${aiFoundryAiServicesResourceName}', 64) + params: { + name: 'pep-${aiFoundryAiServicesResourceName}' + location: solutionLocation + tags: tags + enableTelemetry: enableTelemetry + subnetResourceId: virtualNetwork!.outputs.pepsSubnetResourceId + privateLinkServiceConnections: [ + { + name: 'pep-${aiFoundryAiServicesResourceName}' + properties: { + privateLinkServiceId: aiFoundryAiServices!.outputs.resourceId + groupIds: ['account'] + } + } + ] + privateDnsZoneGroup: { + privateDnsZoneGroupConfigs: [ + { + name: 'cognitiveservices' + privateDnsZoneResourceId: avmPrivateDnsZones[dnsZoneIndex.cognitiveServices]!.outputs.resourceId + } + { + name: 'openai' + privateDnsZoneResourceId: avmPrivateDnsZones[dnsZoneIndex.openAI]!.outputs.resourceId + } + ] + } + } +} + +module aiFoundryAiServicesProject 'modules/ai-project.bicep' = if (!useExistingAiFoundryAiProject) { + name: take('module.ai-project.${aiFoundryAiProjectResourceName}', 64) + params: { + name: aiFoundryAiProjectResourceName + location: azureAiServiceLocation + tags: tags + desc: aiFoundryAiProjectDescription + aiServicesName: aiFoundryAiServicesResourceName + azureExistingAIProjectResourceId: azureExistingAIProjectResourceId + } + dependsOn: [ + aiFoundryAiServices + ] +} + +var aiFoundryAiProjectEndpoint = useExistingAiFoundryAiProject + ? 'https://${aiFoundryAiServicesResourceName}.services.ai.azure.com/api/projects/${aiFoundryAiProjectResourceName}' + : aiFoundryAiServicesProject!.outputs.apiEndpoint + +// ========== Role Assignments for Existing AI Services ========== // +module existingAiServicesRoleAssignments 'modules/deploy_foundry_role_assignment.bicep' = if (useExistingAiFoundryAiProject) { + name: take('module.foundry-role-assignment.${aiFoundryAiServicesResourceName}', 64) + scope: resourceGroup(aiFoundryAiServicesSubscriptionId, aiFoundryAiServicesResourceGroupName) + params: { + aiServicesName: aiFoundryAiServicesResourceName + principalId: userAssignedIdentity.outputs.principalId + principalType: 'ServicePrincipal' + } +} + +// ========== Model Deployments for Existing AI Services ========== // +module existingAiServicesModelDeployments 'modules/deploy_ai_model.bicep' = if (useExistingAiFoundryAiProject) { + name: take('module.model-deployments-existing.${aiFoundryAiServicesResourceName}', 64) + scope: resourceGroup(aiFoundryAiServicesSubscriptionId, aiFoundryAiServicesResourceGroupName) + params: { + aiServicesName: aiFoundryAiServicesResourceName + deployments: [ + for deployment in aiFoundryAiServicesModelDeployment: { + name: deployment.name + format: deployment.format + model: deployment.model + sku: { + name: deployment.sku.name + capacity: deployment.sku.capacity + } + version: deployment.version + raiPolicyName: deployment.raiPolicyName + } + ] + } + dependsOn: [ + existingAiServicesRoleAssignments + ] +} + +// ========== AI Search ========== // +module aiSearch 'br/public:avm/res/search/search-service:0.12.0' = { + name: take('avm.res.search.search-service.${aiSearchName}', 64) + params: { + name: aiSearchName + location: solutionLocation + tags: tags + enableTelemetry: enableTelemetry + sku: enableScalability ? 'standard' : 'basic' + replicaCount: enableRedundancy ? 3 : 1 + partitionCount: 1 + hostingMode: 'Default' + semanticSearch: 'free' + authOptions: { + aadOrApiKey: { + aadAuthFailureMode: 'http401WithBearerChallenge' + } + } + disableLocalAuth: false + roleAssignments: [ + { + principalId: userAssignedIdentity.outputs.principalId + roleDefinitionIdOrName: 'Search Index Data Contributor' + principalType: 'ServicePrincipal' + } + { + principalId: userAssignedIdentity.outputs.principalId + roleDefinitionIdOrName: 'Search Service Contributor' + principalType: 'ServicePrincipal' + } + ] + diagnosticSettings: enableMonitoring ? [{ workspaceResourceId: logAnalyticsWorkspaceResourceId }] : null + // AI Search remains publicly accessible - accessed from ACI via managed identity + publicNetworkAccess: 'Enabled' + } +} + +// ========== AI Search Connection to AI Services ========== // +resource aiSearchFoundryConnection 'Microsoft.CognitiveServices/accounts/projects/connections@2025-09-01' = if (!useExistingAiFoundryAiProject) { + name: '${aiFoundryAiServicesResourceName}/${aiFoundryAiProjectResourceName}/${aiSearchConnectionName}' + properties: { + category: 'CognitiveSearch' + target: 'https://${aiSearchName}.search.windows.net' + authType: 'AAD' + isSharedToAll: true + metadata: { + ApiVersion: '2024-05-01-preview' + ResourceId: aiSearch.outputs.resourceId + } + } + dependsOn: [aiFoundryAiServicesProject] +} + +// ========== Storage Account ========== // +var storageAccountName = 'st${solutionSuffix}' +var productImagesContainer = 'product-images' +var generatedImagesContainer = 'generated-images' +var dataContainer = 'data' + +module storageAccount 'br/public:avm/res/storage/storage-account:0.31.1' = { + name: take('avm.res.storage.storage-account.${storageAccountName}', 64) + params: { + name: storageAccountName + location: solutionLocation + skuName: enableRedundancy ? 'Standard_ZRS' : 'Standard_LRS' + managedIdentities: { systemAssigned: true } + minimumTlsVersion: 'TLS1_2' + enableTelemetry: enableTelemetry + tags: tags + accessTier: 'Hot' + supportsHttpsTrafficOnly: true + blobServices: { + containerDeleteRetentionPolicyEnabled: true + containerDeleteRetentionPolicyDays: 7 + deleteRetentionPolicyEnabled: true + deleteRetentionPolicyDays: 7 + containers: [ + { + name: productImagesContainer + publicAccess: 'None' + } + { + name: generatedImagesContainer + publicAccess: 'None' + } + { + name: dataContainer + publicAccess: 'None' + } + ] + } + roleAssignments: [ + { + principalId: userAssignedIdentity.outputs.principalId + roleDefinitionIdOrName: 'Storage Blob Data Contributor' + principalType: 'ServicePrincipal' + } + ] + networkAcls: { + bypass: 'AzureServices' + defaultAction: enablePrivateNetworking ? 'Deny' : 'Allow' + } + allowBlobPublicAccess: false + publicNetworkAccess: enablePrivateNetworking ? 'Disabled' : 'Enabled' + privateEndpoints: enablePrivateNetworking + ? [ + { + service: 'blob' + subnetResourceId: virtualNetwork!.outputs.pepsSubnetResourceId + privateDnsZoneGroup: { + privateDnsZoneGroupConfigs: [ + { privateDnsZoneResourceId: avmPrivateDnsZones[dnsZoneIndex.storageBlob]!.outputs.resourceId } + ] + } + } + ] + : null + diagnosticSettings: enableMonitoring ? [{ workspaceResourceId: logAnalyticsWorkspaceResourceId }] : null + } +} + +// ========== Cosmos DB ========== // +var cosmosDBResourceName = 'cosmos-${solutionSuffix}' +var cosmosDBDatabaseName = 'content_generation_db' +var cosmosDBConversationsContainer = 'conversations' +var cosmosDBProductsContainer = 'products' + +module cosmosDB 'br/public:avm/res/document-db/database-account:0.18.0' = { + name: take('avm.res.document-db.database-account.${cosmosDBResourceName}', 64) + params: { + name: 'cosmos-${solutionSuffix}' + location: secondaryLocation + tags: tags + enableTelemetry: enableTelemetry + sqlDatabases: [ + { + name: cosmosDBDatabaseName + containers: [ + { + name: cosmosDBConversationsContainer + paths: [ + '/userId' + ] + } + { + name: cosmosDBProductsContainer + paths: [ + '/category' + ] + } + ] + } + ] + sqlRoleDefinitions: [ + { + roleName: 'contentgen-data-contributor' + dataActions: [ + 'Microsoft.DocumentDB/databaseAccounts/readMetadata' + 'Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers/*' + 'Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers/items/*' + ] + } + ] + sqlRoleAssignments: [ + { + principalId: userAssignedIdentity.outputs.principalId + roleDefinitionId: '00000000-0000-0000-0000-000000000002' // Built-in Cosmos DB Data Contributor + } + { + principalId: deployer().objectId + roleDefinitionId: '00000000-0000-0000-0000-000000000002' // Built-in Cosmos DB Data Contributor to the deployer + } + ] + diagnosticSettings: enableMonitoring ? [{ workspaceResourceId: logAnalyticsWorkspaceResourceId }] : null + networkRestrictions: { + networkAclBypass: 'AzureServices' + publicNetworkAccess: enablePrivateNetworking ? 'Disabled' : 'Enabled' + } + zoneRedundant: enableRedundancy + capabilitiesToAdd: enableRedundancy ? null : ['EnableServerless'] + enableAutomaticFailover: enableRedundancy + failoverLocations: enableRedundancy + ? [ + { + failoverPriority: 0 + isZoneRedundant: true + locationName: secondaryLocation + } + { + failoverPriority: 1 + isZoneRedundant: true + locationName: cosmosDbHaLocation + } + ] + : [ + { + locationName: secondaryLocation + failoverPriority: 0 + isZoneRedundant: false + } + ] + privateEndpoints: enablePrivateNetworking + ? [ + { + service: 'Sql' + subnetResourceId: virtualNetwork!.outputs.pepsSubnetResourceId + privateDnsZoneGroup: { + privateDnsZoneGroupConfigs: [ + { privateDnsZoneResourceId: avmPrivateDnsZones[dnsZoneIndex.cosmosDB]!.outputs.resourceId } + ] + } + } + ] + : null + } +} + +// ========== App Service Plan ========== // +var webServerFarmResourceName = 'asp-${solutionSuffix}' +module webServerFarm 'br/public:avm/res/web/serverfarm:0.7.0' = { + name: take('avm.res.web.serverfarm.${webServerFarmResourceName}', 64) + params: { + name: webServerFarmResourceName + tags: tags + enableTelemetry: enableTelemetry + location: solutionLocation + reserved: true + kind: 'linux' + diagnosticSettings: enableMonitoring ? [{ workspaceResourceId: logAnalyticsWorkspaceResourceId }] : null + skuName: enableScalability || enableRedundancy ? 'P1v3' : 'B1' + skuCapacity: enableRedundancy ? 2 : 1 + zoneRedundant: enableRedundancy ? true : false + } + scope: resourceGroup(resourceGroup().name) +} + +// ========== Web App ========== // +var webSiteResourceName = 'app-${solutionSuffix}' +// Backend URL: Use ACI IP (private or public) or FQDN depending on networking mode +var aciPrivateIpFallback = '10.0.4.4' +var aciPublicFqdnFallback = '${containerInstanceName}.${solutionLocation}.azurecontainer.io' +// For private networking use IP, for public use FQDN +var aciBackendUrl = enablePrivateNetworking + ? 'http://${aciPrivateIpFallback}:8000' + : 'http://${aciPublicFqdnFallback}:8000' +module webSite 'modules/web-sites.bicep' = { + name: take('module.web-sites.${webSiteResourceName}', 64) + params: { + name: webSiteResourceName + tags: union(tags, { 'azd-service-name': 'frontend' }) + location: solutionLocation + kind: 'app,linux' + serverFarmResourceId: webServerFarm.outputs.resourceId + managedIdentities: { userAssignedResourceIds: [userAssignedIdentity!.outputs.resourceId] } + siteConfig: { + // Node.js runtime for frontend server (code deployment via AZD) + linuxFxVersion: 'NODE|22-lts' + minTlsVersion: '1.2' + alwaysOn: true + ftpsState: 'FtpsOnly' + appCommandLine: 'node server.js' + } + virtualNetworkSubnetId: enablePrivateNetworking ? virtualNetwork!.outputs.webSubnetResourceId : null + configs: concat( + [ + { + // Frontend server proxies to ACI backend + name: 'appsettings' + properties: { + WEBSITES_PORT: '8080' + BACKEND_URL: aciBackendUrl + AZURE_CLIENT_ID: userAssignedIdentity.outputs.clientId + SCM_DO_BUILD_DURING_DEPLOYMENT: 'true' // Run npm install during deployment + } + applicationInsightResourceId: enableMonitoring ? applicationInsights!.outputs.resourceId : null + } + ], + enableMonitoring + ? [ + { + name: 'logs' + properties: {} + } + ] + : [] + ) + enableMonitoring: enableMonitoring + enableTelemetry: enableTelemetry + diagnosticSettings: enableMonitoring ? [{ workspaceResourceId: logAnalyticsWorkspaceResourceId }] : null + vnetRouteAllEnabled: enablePrivateNetworking + vnetImagePullEnabled: enablePrivateNetworking + publicNetworkAccess: 'Enabled' + } +} + +// ========== Container Instance (Backend API) ========== // +// CUSTOM DEPLOYMENT: ACI is skipped when imageTag='none' (first run), deployed after images are built +var containerInstanceName = 'aci-${solutionSuffix}' +var backendImageUrl = '${containerRegistry.outputs.loginServer}/${backendImageName}:${imageTag}' +// Deploy ACI only when imageTag is set to a real tag (not 'none') +var shouldDeployACI = !empty(imageTag) && imageTag != 'none' +module containerInstance 'modules/container-instance.bicep' = if (shouldDeployACI) { + name: take('module.container-instance.${containerInstanceName}', 64) + params: { + name: containerInstanceName + location: solutionLocation + tags: tags + containerImage: backendImageUrl + cpu: 2 + memoryInGB: 4 + port: 8000 + // Only pass subnetResourceId when private networking is enabled + subnetResourceId: enablePrivateNetworking ? virtualNetwork!.outputs.aciSubnetResourceId : '' + userAssignedIdentityResourceId: userAssignedIdentity.outputs.resourceId + enableTelemetry: enableTelemetry + environmentVariables: [ + // Azure OpenAI Settings + { name: 'AZURE_OPENAI_ENDPOINT', value: 'https://${aiFoundryAiServicesResourceName}.openai.azure.com/' } + { name: 'AZURE_OPENAI_GPT_MODEL', value: gptModelName } + { name: 'AZURE_OPENAI_IMAGE_MODEL', value: imageModelConfig[imageModelChoice].name } + { name: 'AZURE_OPENAI_GPT_IMAGE_ENDPOINT', value: imageModelChoice != 'none' ? 'https://${aiFoundryAiServicesResourceName}.openai.azure.com/' : '' } + { name: 'AZURE_OPENAI_API_VERSION', value: azureOpenaiAPIVersion } + // Azure Cosmos DB Settings + { name: 'AZURE_COSMOS_ENDPOINT', value: 'https://cosmos-${solutionSuffix}.documents.azure.com:443/' } + { name: 'AZURE_COSMOS_DATABASE_NAME', value: cosmosDBDatabaseName } + { name: 'AZURE_COSMOS_PRODUCTS_CONTAINER', value: cosmosDBProductsContainer } + { name: 'AZURE_COSMOS_CONVERSATIONS_CONTAINER', value: cosmosDBConversationsContainer } + // Azure Blob Storage Settings + { name: 'AZURE_BLOB_ACCOUNT_NAME', value: storageAccountName } + { name: 'AZURE_BLOB_PRODUCT_IMAGES_CONTAINER', value: productImagesContainer } + { name: 'AZURE_BLOB_GENERATED_IMAGES_CONTAINER', value: generatedImagesContainer } + // Azure AI Search Settings + { name: 'AZURE_AI_SEARCH_ENDPOINT', value: 'https://${aiSearchName}.search.windows.net' } + { name: 'AZURE_AI_SEARCH_PRODUCTS_INDEX', value: azureSearchIndex } + { name: 'AZURE_AI_SEARCH_IMAGE_INDEX', value: 'product-images' } + // App Settings + { name: 'AZURE_CLIENT_ID', value: userAssignedIdentity.outputs.clientId } + { name: 'PORT', value: '8000' } + { name: 'WORKERS', value: '4' } + { name: 'RUNNING_IN_PRODUCTION', value: 'true' } + // Azure AI Foundry Settings + { name: 'USE_FOUNDRY', value: useFoundryMode ? 'true' : 'false' } + { name: 'AZURE_AI_PROJECT_ENDPOINT', value: aiFoundryAiProjectEndpoint } + { name: 'AZURE_AI_MODEL_DEPLOYMENT_NAME', value: gptModelName } + { name: 'AZURE_AI_IMAGE_MODEL_DEPLOYMENT', value: imageModelConfig[imageModelChoice].name } + // Logging Settings + { name: 'AZURE_BASIC_LOGGING_LEVEL', value: 'INFO' } + { name: 'AZURE_PACKAGE_LOGGING_LEVEL', value: 'WARNING' } + { name: 'AZURE_LOGGING_PACKAGES', value: '' } + // Application Insights + { name: 'APPLICATIONINSIGHTS_CONNECTION_STRING', value: enableMonitoring ? applicationInsights!.outputs.connectionString : '' } + ] + } +} + +// ========== Outputs ========== // +@description('Contains App Service Name') +output APP_SERVICE_NAME string = webSite.outputs.name + +@description('Contains WebApp URL') +output WEB_APP_URL string = 'https://${webSite.outputs.name}.azurewebsites.net' + +@description('Contains Storage Account Name') +output AZURE_BLOB_ACCOUNT_NAME string = storageAccount.outputs.name + +@description('Contains Product Images Container') +output AZURE_BLOB_PRODUCT_IMAGES_CONTAINER string = productImagesContainer + +@description('Contains Generated Images Container') +output AZURE_BLOB_GENERATED_IMAGES_CONTAINER string = generatedImagesContainer + +@description('Contains CosmosDB Account Name') +output COSMOSDB_ACCOUNT_NAME string = cosmosDB.outputs.name + +@description('Contains CosmosDB Endpoint URL') +output AZURE_COSMOS_ENDPOINT string = 'https://cosmos-${solutionSuffix}.documents.azure.com:443/' + +@description('Contains CosmosDB Database Name') +output AZURE_COSMOS_DATABASE_NAME string = cosmosDBDatabaseName + +@description('Contains CosmosDB Products Container') +output AZURE_COSMOS_PRODUCTS_CONTAINER string = cosmosDBProductsContainer + +@description('Contains CosmosDB Conversations Container') +output AZURE_COSMOS_CONVERSATIONS_CONTAINER string = cosmosDBConversationsContainer + +@description('Contains Resource Group Name') +output RESOURCE_GROUP_NAME string = resourceGroup().name + +@description('Contains AI Foundry Name') +output AI_FOUNDRY_NAME string = aiFoundryAiProjectResourceName + +@description('Contains AI Foundry RG Name') +output AI_FOUNDRY_RG_NAME string = aiFoundryAiServicesResourceGroupName + +@description('Contains AI Foundry Resource ID') +output AI_FOUNDRY_RESOURCE_ID string = useExistingAiFoundryAiProject ? '' : aiFoundryAiServices!.outputs.resourceId + +@description('Contains existing AI project resource ID.') +output AZURE_EXISTING_AI_PROJECT_RESOURCE_ID string = azureExistingAIProjectResourceId + +@description('Contains AI Search Service Endpoint URL') +output AZURE_AI_SEARCH_ENDPOINT string = 'https://${aiSearch.outputs.name}.search.windows.net/' + +@description('Contains AI Search Service Name') +output AI_SEARCH_SERVICE_NAME string = aiSearch.outputs.name + +@description('Contains AI Search Product Index') +output AZURE_AI_SEARCH_PRODUCTS_INDEX string = azureSearchIndex + +@description('Contains AI Search Image Index') +output AZURE_AI_SEARCH_IMAGE_INDEX string = 'product-images' + +@description('Contains Azure OpenAI endpoint URL') +output AZURE_OPENAI_ENDPOINT string = 'https://${aiFoundryAiServicesResourceName}.openai.azure.com/' + +@description('Contains GPT Model') +output AZURE_OPENAI_GPT_MODEL string = gptModelName + +@description('Contains Image Model (empty if none selected)') +output AZURE_OPENAI_IMAGE_MODEL string = imageModelConfig[imageModelChoice].name + +@description('Contains Azure OpenAI GPT/Image endpoint URL (empty if no image model selected)') +output AZURE_OPENAI_GPT_IMAGE_ENDPOINT string = imageModelChoice != 'none' ? 'https://${aiFoundryAiServicesResourceName}.openai.azure.com/' : '' + +@description('Contains Azure OpenAI API Version') +output AZURE_OPENAI_API_VERSION string = azureOpenaiAPIVersion + +@description('Contains OpenAI Resource') +output AZURE_OPENAI_RESOURCE string = aiFoundryAiServicesResourceName + +@description('Contains AI Agent Endpoint') +output AZURE_AI_AGENT_ENDPOINT string = aiFoundryAiProjectEndpoint + +@description('Contains AI Agent API Version') +output AZURE_AI_AGENT_API_VERSION string = azureAiAgentApiVersion + +@description('Contains Application Insights Connection String') +output AZURE_APPLICATION_INSIGHTS_CONNECTION_STRING string = (enableMonitoring && !useExistingLogAnalytics) ? applicationInsights!.outputs.connectionString : '' + +@description('Contains the location used for AI Services deployment') +output AZURE_ENV_OPENAI_LOCATION string = azureAiServiceLocation + +@description('Contains Container Instance Name') +output CONTAINER_INSTANCE_NAME string = shouldDeployACI ? containerInstance!.outputs.name : '' + +@description('Contains Container Instance IP Address') +output CONTAINER_INSTANCE_IP string = shouldDeployACI ? containerInstance!.outputs.ipAddress : '' + +@description('Contains Container Instance FQDN (only for non-private networking)') +output CONTAINER_INSTANCE_FQDN string = (shouldDeployACI && !enablePrivateNetworking) ? containerInstance!.outputs.fqdn : '' + +@description('Contains ACR Name') +output ACR_NAME string = acrResourceName + +@description('Contains Azure Container Registry Endpoint (used by AZD for remote builds)') +output AZURE_CONTAINER_REGISTRY_ENDPOINT string = containerRegistry.outputs.loginServer + +@description('Contains Azure Container Registry Name') +output AZURE_CONTAINER_REGISTRY_NAME string = containerRegistry.outputs.name + +@description('Contains flag for Azure AI Foundry usage') +output USE_FOUNDRY bool = useFoundryMode ? true : false + +@description('Contains Azure AI Project Endpoint') +output AZURE_AI_PROJECT_ENDPOINT string = aiFoundryAiProjectEndpoint + +@description('Contains Azure AI Model Deployment Name') +output AZURE_AI_MODEL_DEPLOYMENT_NAME string = gptModelName + +@description('Contains Azure AI Image Model Deployment Name (empty if none selected)') +output AZURE_AI_IMAGE_MODEL_DEPLOYMENT string = imageModelConfig[imageModelChoice].name + +@description('Contains Managed Identity Client ID') +output AZURE_CLIENT_ID string = userAssignedIdentity.outputs.clientId + +@description('Frontend image name') +output FRONTEND_IMAGE_NAME string = frontendImageName + +@description('Backend image name') +output BACKEND_IMAGE_NAME string = backendImageName + +@description('Image tag') +output IMAGE_TAG string = imageTag diff --git a/scripts/post_deploy.py b/scripts/post_deploy.py index d1eebb3f3..202d7ebeb 100644 --- a/scripts/post_deploy.py +++ b/scripts/post_deploy.py @@ -244,25 +244,31 @@ def get_api_headers(config: ResourceConfig) -> Dict[str, str]: return headers -async def check_admin_api_health(config: ResourceConfig) -> bool: - """Check if the admin API is available.""" +async def check_admin_api_health(config: ResourceConfig, max_retries: int = 5, retry_delay: int = 10) -> bool: + """Check if the admin API is available with retry logic for cold starts.""" print_step("Checking admin API health...") async with httpx.AsyncClient(timeout=30.0) as client: - try: - response = await client.get( - f"{config.app_url}/api/admin/health", - headers=get_api_headers(config) - ) - if response.status_code == 200: - print_success("Admin API is healthy") - return True - else: - print_error(f"Admin API returned {response.status_code}") - return False - except Exception as e: - print_error(f"Failed to reach admin API: {e}") - return False + for attempt in range(1, max_retries + 1): + try: + response = await client.get( + f"{config.app_url}/api/admin/health", + headers=get_api_headers(config) + ) + if response.status_code == 200: + print_success("Admin API is healthy") + return True + else: + print_warning(f"Attempt {attempt}/{max_retries}: Admin API returned {response.status_code}") + except Exception as e: + print_warning(f"Attempt {attempt}/{max_retries}: Failed to reach admin API: {e}") + + if attempt < max_retries: + print(f" Retrying in {retry_delay} seconds...") + await asyncio.sleep(retry_delay) + + print_error(f"Admin API not available after {max_retries} attempts") + return False async def upload_images(config: ResourceConfig, dry_run: bool = False) -> int: From 93c82af5c1f7567087e9903402f62b157decd372 Mon Sep 17 00:00:00 2001 From: Ragini-Microsoft Date: Mon, 6 Apr 2026 13:25:17 +0530 Subject: [PATCH 46/72] updates for custom deployment --- azure_custom.yaml | 4 ++-- infra/scripts/package_frontend.ps1 | 25 +++++++++++++++++++++++++ infra/scripts/package_frontend.sh | 26 ++++++++++++++++++++++++++ 3 files changed, 53 insertions(+), 2 deletions(-) create mode 100644 infra/scripts/package_frontend.ps1 create mode 100644 infra/scripts/package_frontend.sh diff --git a/azure_custom.yaml b/azure_custom.yaml index bdd176687..a79c694e9 100644 --- a/azure_custom.yaml +++ b/azure_custom.yaml @@ -83,7 +83,7 @@ hooks: Write-Host "Image: ${backendImage}:latest" -ForegroundColor Cyan az acr login --name $acrName 2>$null - az acr build --registry $acrName --image "${backendImage}:latest" --file ./src/backend/ApiApp.Dockerfile ./src/backend --no-logs + az acr build --registry $acrName --image "${backendImage}:latest" --file ./src/backend/ApiApp.Dockerfile ./src/backend if ($LASTEXITCODE -ne 0) { Write-Host "Failed to build container image" -ForegroundColor Red exit 1 @@ -151,7 +151,7 @@ hooks: echo "Registry: $ACR_NAME" echo "Image: $BACKEND_IMAGE:latest" - if az acr build --registry "$ACR_NAME" --image "$BACKEND_IMAGE:latest" --file ./src/backend/ApiApp.Dockerfile ./src/backend --no-logs; then + if az acr build --registry "$ACR_NAME" --image "$BACKEND_IMAGE:latest" --file ./src/backend/ApiApp.Dockerfile ./src/backend; then echo "Container image built and pushed successfully!" else echo "Failed to build container image" diff --git a/infra/scripts/package_frontend.ps1 b/infra/scripts/package_frontend.ps1 new file mode 100644 index 000000000..452197122 --- /dev/null +++ b/infra/scripts/package_frontend.ps1 @@ -0,0 +1,25 @@ +# Package frontend for App Service deployment +# This script is called by AZD during prepackage hook +# Working directory is ./src/app/frontend-server (project directory) + +Write-Host "Building React frontend..." -ForegroundColor Cyan + +# Build React frontend (one level up) +Push-Location ../frontend +npm ci --loglevel=error +npm run build -- --outDir ../frontend-server/static +Pop-Location + +Write-Host "Packaging frontend server..." -ForegroundColor Cyan + +# Create dist folder +mkdir dist -Force | Out-Null +rm dist/* -r -Force -ErrorAction SilentlyContinue + +# Copy required files to dist (node_modules excluded - App Service will npm install) +cp -r static dist -Force +cp server.js dist -Force +cp package.json dist -Force +cp package-lock.json dist -Force + +Write-Host "Frontend packaged successfully!" -ForegroundColor Green diff --git a/infra/scripts/package_frontend.sh b/infra/scripts/package_frontend.sh new file mode 100644 index 000000000..682071363 --- /dev/null +++ b/infra/scripts/package_frontend.sh @@ -0,0 +1,26 @@ +#!/bin/bash +# Package frontend for App Service deployment +# This script is called by AZD during prepackage hook +# Working directory is ./src/app/frontend-server (project directory) + +echo "Building React frontend..." + +# Build React frontend (one level up) +cd ../frontend +npm ci --loglevel=error +npm run build -- --outDir ../frontend-server/static +cd ../frontend-server + +echo "Packaging frontend server..." + +# Create dist folder and copy files +rm -rf ./dist +mkdir -p ./dist + +# Copy required files to dist (node_modules excluded - App Service will npm install) +cp -r static ./dist/ +cp server.js ./dist/ +cp package.json ./dist/ +cp package-lock.json ./dist/ + +echo "Frontend packaged successfully!" From 090821c0742cf0b043bf36f3c7f6d0fb92eef442 Mon Sep 17 00:00:00 2001 From: Ragini-Microsoft Date: Wed, 8 Apr 2026 09:57:47 +0530 Subject: [PATCH 47/72] add deployment instructions for local changes --- docs/AZD_DEPLOYMENT.md | 31 +++++++++++++++++++ ...DEPLOYMENT.md => LocalDevelopmentSetup.md} | 0 2 files changed, 31 insertions(+) rename docs/{LOCAL_DEPLOYMENT.md => LocalDevelopmentSetup.md} (100%) diff --git a/docs/AZD_DEPLOYMENT.md b/docs/AZD_DEPLOYMENT.md index 942fba86f..876f3d091 100644 --- a/docs/AZD_DEPLOYMENT.md +++ b/docs/AZD_DEPLOYMENT.md @@ -309,8 +309,39 @@ When `enablePrivateNetworking` is enabled: └─────────────────────────────────────────────────────────────────┘ ``` +## Deploy Local Changes + +If you've made local modifications to the code and want to deploy them to Azure, follow these steps to swap the configuration files: + +> **Note**: To set up and run the application locally for development, see the [Local Development Guide](LocalDevelopmentSetup.md). + +### Step 1: Rename Azure Configuration Files + +In the root directory: + +1. Rename `azure.yaml` to `azure_custom2.yaml` +2. Rename `azure_custom.yaml` to `azure.yaml` + +### Step 2: Rename Infrastructure Files + +In the `infra` directory: + +1. Rename `main.bicep` to `main_custom2.bicep` +2. Rename `main_custom.bicep` to `main.bicep` + +### Step 3: Deploy Changes + +Run the deployment command: + +```bash +azd up +``` + +> **Note**: These custom files are configured to deploy your local code changes instead of pulling from the GitHub repository. + ## Related Documentation - [Deployment Guide](DEPLOYMENT.md) +- [Local Development Guide](LocalDevelopmentSetup.md) - [Image Generation Configuration](IMAGE_GENERATION.md) - [Azure Developer CLI Documentation](https://learn.microsoft.com/azure/developer/azure-developer-cli/) diff --git a/docs/LOCAL_DEPLOYMENT.md b/docs/LocalDevelopmentSetup.md similarity index 100% rename from docs/LOCAL_DEPLOYMENT.md rename to docs/LocalDevelopmentSetup.md From c07d99c8d025f2bbb760f60ccc0ff78fc5d88f70 Mon Sep 17 00:00:00 2001 From: Ragini-Microsoft Date: Wed, 8 Apr 2026 10:02:21 +0530 Subject: [PATCH 48/72] fix: update link to local development guide in documentation --- docs/TECHNICAL_GUIDE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/TECHNICAL_GUIDE.md b/docs/TECHNICAL_GUIDE.md index ce41f325e..63795c0af 100644 --- a/docs/TECHNICAL_GUIDE.md +++ b/docs/TECHNICAL_GUIDE.md @@ -179,7 +179,7 @@ BRAND_SECONDARY_COLOR=#107C10 ## Documentation -- [Local Development Guide](./LOCAL_DEPLOYMENT.md) - Run locally for development +- [Local Development Guide](./LocalDevelopmentSetup.md) - Run locally for development - [AZD Deployment Guide](./AZD_DEPLOYMENT.md) - Deploy with Azure Developer CLI - [Manual Deployment Guide](./DEPLOYMENT.md) - Step-by-step manual deployment - [Image Generation Configuration](./IMAGE_GENERATION.md) - GPT image model setup From 1409848d82ea6ffb078eb18e55c2e25cc3ae2f79 Mon Sep 17 00:00:00 2001 From: "Prekshith D J (Persistent Systems Inc)" Date: Wed, 8 Apr 2026 11:43:01 +0530 Subject: [PATCH 49/72] Fixed the code quality issue --- src/tests/services/test_orchestrator.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/tests/services/test_orchestrator.py b/src/tests/services/test_orchestrator.py index 1c79b00f2..6063dbd2c 100644 --- a/src/tests/services/test_orchestrator.py +++ b/src/tests/services/test_orchestrator.py @@ -1,10 +1,10 @@ import base64 import json +import sys from unittest.mock import AsyncMock, MagicMock, patch import pytest -import orchestrator from orchestrator import (_HARMFUL_PATTERNS_COMPILED, _SYSTEM_PROMPT_PATTERNS_COMPILED, PLANNING_INSTRUCTIONS, RAI_HARMFUL_CONTENT_RESPONSE, @@ -841,7 +841,7 @@ def test_get_orchestrator_singleton(): mock_builder.return_value = mock_builder_instance # Reset the singleton - orchestrator._orchestrator = None + sys.modules["orchestrator"]._orchestrator = None instance1 = get_orchestrator() instance2 = get_orchestrator() @@ -1518,7 +1518,7 @@ async def test_get_chat_client_foundry_mode(): def test_foundry_not_available(): """Test when Foundry SDK is not available.""" # Check that FOUNDRY_AVAILABLE is defined - assert hasattr(orchestrator, 'FOUNDRY_AVAILABLE') + assert hasattr(sys.modules["orchestrator"], 'FOUNDRY_AVAILABLE') # Tests for workflow event handling (lines 736-799, 841-895) # Note: These are integration-level tests that verify the workflow event From 9c51956234e72d82ef318fb77188f196fd0c21fe Mon Sep 17 00:00:00 2001 From: Ragini-Microsoft Date: Wed, 8 Apr 2026 12:13:11 +0530 Subject: [PATCH 50/72] updates in custom deployment --- infra/main_custom.bicep | 181 ++++++++++++++++++++--------- infra/scripts/package_frontend.ps1 | 4 + infra/scripts/package_frontend.sh | 2 + 3 files changed, 132 insertions(+), 55 deletions(-) diff --git a/infra/main_custom.bicep b/infra/main_custom.bicep index e2cf5cf78..7f78ca58b 100644 --- a/infra/main_custom.bicep +++ b/infra/main_custom.bicep @@ -141,6 +141,10 @@ param backendImageName string = 'content-gen-api' @description('Optional. Image tag for container deployment. Leave empty to skip ACI deployment.') param imageTag string +@description('Optional. Azure Container Registry name (unused - ACR name is auto-generated). Declared for parameter file compatibility.') +#disable-next-line no-unused-params +param acrName string = '' + @description('Optional. Created by user name.') param createdBy string = contains(deployer(), 'userPrincipalName')? split(deployer().userPrincipalName, '@')[0]: deployer().objectId @@ -160,6 +164,7 @@ var solutionSuffix = toLower(trim(replace( '' ))) +// ACR name is always auto-generated in custom deployment var acrResourceName = 'cr${solutionSuffix}' var cosmosDbZoneRedundantHaRegionPairs = { @@ -381,9 +386,16 @@ module containerRegistry 'br/public:avm/res/container-registry/registry:0.9.0' = enableTelemetry: enableTelemetry acrSku: 'Standard' acrAdminUserEnabled: false - anonymousPullEnabled: true // Allows ACI to pull images without credentials + anonymousPullEnabled: false publicNetworkAccess: 'Enabled' networkRuleBypassOptions: 'AzureServices' + roleAssignments: [ + { + principalId: userAssignedIdentity.outputs.principalId + roleDefinitionIdOrName: '7f951dda-4ed3-4680-a7ca-43fe172d538d' // AcrPull + principalType: 'ServicePrincipal' + } + ] } } @@ -889,61 +901,120 @@ module webSite 'modules/web-sites.bicep' = { } // ========== Container Instance (Backend API) ========== // -// CUSTOM DEPLOYMENT: ACI is skipped when imageTag='none' (first run), deployed after images are built +// CUSTOM DEPLOYMENT: Inline ACI definition with managed identity auth for ACR var containerInstanceName = 'aci-${solutionSuffix}' var backendImageUrl = '${containerRegistry.outputs.loginServer}/${backendImageName}:${imageTag}' +var aciPort = 8000 +var isPrivateNetworking = enablePrivateNetworking +// Construct identity resource ID from known values (required for deployment-time calculation) +var userAssignedIdentityResourceIdForACI = '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.ManagedIdentity/userAssignedIdentities/${userAssignedIdentityResourceName}' // Deploy ACI only when imageTag is set to a real tag (not 'none') var shouldDeployACI = !empty(imageTag) && imageTag != 'none' -module containerInstance 'modules/container-instance.bicep' = if (shouldDeployACI) { - name: take('module.container-instance.${containerInstanceName}', 64) - params: { - name: containerInstanceName - location: solutionLocation - tags: tags - containerImage: backendImageUrl - cpu: 2 - memoryInGB: 4 - port: 8000 - // Only pass subnetResourceId when private networking is enabled - subnetResourceId: enablePrivateNetworking ? virtualNetwork!.outputs.aciSubnetResourceId : '' - userAssignedIdentityResourceId: userAssignedIdentity.outputs.resourceId - enableTelemetry: enableTelemetry - environmentVariables: [ - // Azure OpenAI Settings - { name: 'AZURE_OPENAI_ENDPOINT', value: 'https://${aiFoundryAiServicesResourceName}.openai.azure.com/' } - { name: 'AZURE_OPENAI_GPT_MODEL', value: gptModelName } - { name: 'AZURE_OPENAI_IMAGE_MODEL', value: imageModelConfig[imageModelChoice].name } - { name: 'AZURE_OPENAI_GPT_IMAGE_ENDPOINT', value: imageModelChoice != 'none' ? 'https://${aiFoundryAiServicesResourceName}.openai.azure.com/' : '' } - { name: 'AZURE_OPENAI_API_VERSION', value: azureOpenaiAPIVersion } - // Azure Cosmos DB Settings - { name: 'AZURE_COSMOS_ENDPOINT', value: 'https://cosmos-${solutionSuffix}.documents.azure.com:443/' } - { name: 'AZURE_COSMOS_DATABASE_NAME', value: cosmosDBDatabaseName } - { name: 'AZURE_COSMOS_PRODUCTS_CONTAINER', value: cosmosDBProductsContainer } - { name: 'AZURE_COSMOS_CONVERSATIONS_CONTAINER', value: cosmosDBConversationsContainer } - // Azure Blob Storage Settings - { name: 'AZURE_BLOB_ACCOUNT_NAME', value: storageAccountName } - { name: 'AZURE_BLOB_PRODUCT_IMAGES_CONTAINER', value: productImagesContainer } - { name: 'AZURE_BLOB_GENERATED_IMAGES_CONTAINER', value: generatedImagesContainer } - // Azure AI Search Settings - { name: 'AZURE_AI_SEARCH_ENDPOINT', value: 'https://${aiSearchName}.search.windows.net' } - { name: 'AZURE_AI_SEARCH_PRODUCTS_INDEX', value: azureSearchIndex } - { name: 'AZURE_AI_SEARCH_IMAGE_INDEX', value: 'product-images' } - // App Settings - { name: 'AZURE_CLIENT_ID', value: userAssignedIdentity.outputs.clientId } - { name: 'PORT', value: '8000' } - { name: 'WORKERS', value: '4' } - { name: 'RUNNING_IN_PRODUCTION', value: 'true' } - // Azure AI Foundry Settings - { name: 'USE_FOUNDRY', value: useFoundryMode ? 'true' : 'false' } - { name: 'AZURE_AI_PROJECT_ENDPOINT', value: aiFoundryAiProjectEndpoint } - { name: 'AZURE_AI_MODEL_DEPLOYMENT_NAME', value: gptModelName } - { name: 'AZURE_AI_IMAGE_MODEL_DEPLOYMENT', value: imageModelConfig[imageModelChoice].name } - // Logging Settings - { name: 'AZURE_BASIC_LOGGING_LEVEL', value: 'INFO' } - { name: 'AZURE_PACKAGE_LOGGING_LEVEL', value: 'WARNING' } - { name: 'AZURE_LOGGING_PACKAGES', value: '' } - // Application Insights - { name: 'APPLICATIONINSIGHTS_CONNECTION_STRING', value: enableMonitoring ? applicationInsights!.outputs.connectionString : '' } + +#disable-next-line no-deployments-resources +resource aciTelemetry 'Microsoft.Resources/deployments@2024-03-01' = if (enableTelemetry && shouldDeployACI) { + name: '46d3xbcp.res.containerinstance.${replace('-..--..-', '.', '-')}.${substring(uniqueString(deployment().name, solutionLocation), 0, 4)}' + properties: { + mode: 'Incremental' + template: { + '$schema': 'https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#' + contentVersion: '1.0.0.0' + resources: [] + } + } +} + +resource containerInstance 'Microsoft.ContainerInstance/containerGroups@2025-09-01' = if (shouldDeployACI) { + name: containerInstanceName + location: solutionLocation + tags: tags + identity: { + type: 'UserAssigned' + userAssignedIdentities: { + '${userAssignedIdentityResourceIdForACI}': {} + } + } + properties: { + containers: [ + { + name: containerInstanceName + properties: { + image: backendImageUrl + resources: { + requests: { + cpu: 2 + memoryInGB: 4 + } + } + ports: [ + { + port: aciPort + protocol: 'TCP' + } + ] + environmentVariables: [ + // Azure OpenAI Settings + { name: 'AZURE_OPENAI_ENDPOINT', value: 'https://${aiFoundryAiServicesResourceName}.openai.azure.com/' } + { name: 'AZURE_OPENAI_GPT_MODEL', value: gptModelName } + { name: 'AZURE_OPENAI_IMAGE_MODEL', value: imageModelConfig[imageModelChoice].name } + { name: 'AZURE_OPENAI_GPT_IMAGE_ENDPOINT', value: imageModelChoice != 'none' ? 'https://${aiFoundryAiServicesResourceName}.openai.azure.com/' : '' } + { name: 'AZURE_OPENAI_API_VERSION', value: azureOpenaiAPIVersion } + // Azure Cosmos DB Settings + { name: 'AZURE_COSMOS_ENDPOINT', value: 'https://cosmos-${solutionSuffix}.documents.azure.com:443/' } + { name: 'AZURE_COSMOS_DATABASE_NAME', value: cosmosDBDatabaseName } + { name: 'AZURE_COSMOS_PRODUCTS_CONTAINER', value: cosmosDBProductsContainer } + { name: 'AZURE_COSMOS_CONVERSATIONS_CONTAINER', value: cosmosDBConversationsContainer } + // Azure Blob Storage Settings + { name: 'AZURE_BLOB_ACCOUNT_NAME', value: storageAccountName } + { name: 'AZURE_BLOB_PRODUCT_IMAGES_CONTAINER', value: productImagesContainer } + { name: 'AZURE_BLOB_GENERATED_IMAGES_CONTAINER', value: generatedImagesContainer } + // Azure AI Search Settings + { name: 'AZURE_AI_SEARCH_ENDPOINT', value: 'https://${aiSearchName}.search.windows.net' } + { name: 'AZURE_AI_SEARCH_PRODUCTS_INDEX', value: azureSearchIndex } + { name: 'AZURE_AI_SEARCH_IMAGE_INDEX', value: 'product-images' } + // App Settings + { name: 'AZURE_CLIENT_ID', value: userAssignedIdentity.outputs.clientId } + { name: 'PORT', value: '8000' } + { name: 'WORKERS', value: '4' } + { name: 'RUNNING_IN_PRODUCTION', value: 'true' } + // Azure AI Foundry Settings + { name: 'USE_FOUNDRY', value: useFoundryMode ? 'true' : 'false' } + { name: 'AZURE_AI_PROJECT_ENDPOINT', value: aiFoundryAiProjectEndpoint } + { name: 'AZURE_AI_MODEL_DEPLOYMENT_NAME', value: gptModelName } + { name: 'AZURE_AI_IMAGE_MODEL_DEPLOYMENT', value: imageModelConfig[imageModelChoice].name } + // Logging Settings + { name: 'AZURE_BASIC_LOGGING_LEVEL', value: 'INFO' } + { name: 'AZURE_PACKAGE_LOGGING_LEVEL', value: 'WARNING' } + { name: 'AZURE_LOGGING_PACKAGES', value: '' } + // Application Insights + { name: 'APPLICATIONINSIGHTS_CONNECTION_STRING', value: enableMonitoring ? applicationInsights!.outputs.connectionString : '' } + ] + } + } + ] + osType: 'Linux' + restartPolicy: 'Always' + subnetIds: isPrivateNetworking ? [ + { + id: virtualNetwork!.outputs.aciSubnetResourceId + } + ] : null + ipAddress: { + type: isPrivateNetworking ? 'Private' : 'Public' + ports: [ + { + port: aciPort + protocol: 'TCP' + } + ] + dnsNameLabel: isPrivateNetworking ? null : containerInstanceName + } + // Managed identity auth for ACR (instead of anonymous pull) + imageRegistryCredentials: [ + { + server: containerRegistry.outputs.loginServer + identity: userAssignedIdentityResourceIdForACI + } ] } } @@ -1037,13 +1108,13 @@ output AZURE_APPLICATION_INSIGHTS_CONNECTION_STRING string = (enableMonitoring & output AZURE_ENV_OPENAI_LOCATION string = azureAiServiceLocation @description('Contains Container Instance Name') -output CONTAINER_INSTANCE_NAME string = shouldDeployACI ? containerInstance!.outputs.name : '' +output CONTAINER_INSTANCE_NAME string = shouldDeployACI ? containerInstance!.name : '' @description('Contains Container Instance IP Address') -output CONTAINER_INSTANCE_IP string = shouldDeployACI ? containerInstance!.outputs.ipAddress : '' +output CONTAINER_INSTANCE_IP string = shouldDeployACI ? containerInstance!.properties.ipAddress.ip : '' @description('Contains Container Instance FQDN (only for non-private networking)') -output CONTAINER_INSTANCE_FQDN string = (shouldDeployACI && !enablePrivateNetworking) ? containerInstance!.outputs.fqdn : '' +output CONTAINER_INSTANCE_FQDN string = (shouldDeployACI && !isPrivateNetworking) ? containerInstance!.properties.ipAddress.fqdn : '' @description('Contains ACR Name') output ACR_NAME string = acrResourceName diff --git a/infra/scripts/package_frontend.ps1 b/infra/scripts/package_frontend.ps1 index 452197122..f711497f6 100644 --- a/infra/scripts/package_frontend.ps1 +++ b/infra/scripts/package_frontend.ps1 @@ -2,12 +2,16 @@ # This script is called by AZD during prepackage hook # Working directory is ./src/app/frontend-server (project directory) +$ErrorActionPreference = 'Stop' + Write-Host "Building React frontend..." -ForegroundColor Cyan # Build React frontend (one level up) Push-Location ../frontend npm ci --loglevel=error +if ($LASTEXITCODE -ne 0) { throw "npm ci failed with exit code $LASTEXITCODE" } npm run build -- --outDir ../frontend-server/static +if ($LASTEXITCODE -ne 0) { throw "npm run build failed with exit code $LASTEXITCODE" } Pop-Location Write-Host "Packaging frontend server..." -ForegroundColor Cyan diff --git a/infra/scripts/package_frontend.sh b/infra/scripts/package_frontend.sh index 682071363..5906d1495 100644 --- a/infra/scripts/package_frontend.sh +++ b/infra/scripts/package_frontend.sh @@ -1,4 +1,6 @@ #!/bin/bash +set -euo pipefail + # Package frontend for App Service deployment # This script is called by AZD during prepackage hook # Working directory is ./src/app/frontend-server (project directory) From 1cb6af4b1213e9cc615258f4c19651ee7d805e78 Mon Sep 17 00:00:00 2001 From: Ragini-Microsoft Date: Wed, 8 Apr 2026 12:51:58 +0530 Subject: [PATCH 51/72] update custom file as per main.bicep changes --- infra/main_custom.bicep | 114 ++++++++++++++++++++++++++++++++++++++-- 1 file changed, 109 insertions(+), 5 deletions(-) diff --git a/infra/main_custom.bicep b/infra/main_custom.bicep index 7f78ca58b..55f0d9f88 100644 --- a/infra/main_custom.bicep +++ b/infra/main_custom.bicep @@ -108,9 +108,19 @@ param existingLogAnalyticsWorkspaceId string = '' @description('Optional. Resource ID of an existing Foundry project.') param azureExistingAIProjectResourceId string = '' -@description('Optional. Deploy Azure Bastion and Jumpbox VM for private network administration.') +@description('Optional. Deploy Azure Bastion and Jumpbox resources for private network administration.') param deployBastionAndJumpbox bool = false +@description('Optional. Jumpbox VM size. Must support accelerated networking and Premium SSD.') +param vmSize string = '' + +@description('Optional. Jumpbox VM admin username.') +param vmAdminUsername string = '' + +@description('Optional. Jumpbox VM admin password.') +@secure() +param vmAdminPassword string = '' + @description('Optional. The tags to apply to all deployed Azure resources.') param tags object = {} @@ -400,17 +410,111 @@ module containerRegistry 'br/public:avm/res/container-registry/registry:0.9.0' = } // ========== Virtual Network and Networking Components ========== // +var deployAdminAccessResources = enablePrivateNetworking && deployBastionAndJumpbox && !empty(vmAdminPassword) module virtualNetwork 'modules/virtualNetwork.bicep' = if (enablePrivateNetworking) { name: take('module.virtualNetwork.${solutionSuffix}', 64) params: { vnetName: 'vnet-${solutionSuffix}' - vnetLocation: solutionLocation - vnetAddressPrefixes: ['10.0.0.0/20'] + addressPrefixes: ['10.0.0.0/20'] // 4096 addresses (enough for 8 /23 subnets or 16 /24) + location: solutionLocation + deployBastionAndJumpbox: deployAdminAccessResources tags: tags logAnalyticsWorkspaceId: logAnalyticsWorkspaceResourceId - enableTelemetry: enableTelemetry resourceSuffix: solutionSuffix - deployBastionAndJumpbox: deployBastionAndJumpbox + enableTelemetry: enableTelemetry + } +} + +// Azure Bastion Host +var bastionHostName = 'bas-${solutionSuffix}' +var zoneSupportedJumpboxLocations = [ + 'australiaeast' + 'centralus' + 'eastus' + 'eastus2' + 'japaneast' + 'northeurope' + 'southeastasia' + 'swedencentral' + 'uksouth' + 'westus3' +] +module bastionHost 'br/public:avm/res/network/bastion-host:0.8.2' = if (deployAdminAccessResources) { + name: take('avm.res.network.bastion-host.${bastionHostName}', 64) + params: { + name: bastionHostName + skuName: 'Standard' + location: solutionLocation + virtualNetworkResourceId: virtualNetwork!.outputs.resourceId + diagnosticSettings: !empty(logAnalyticsWorkspaceResourceId) + ? [ + { + name: 'bastionDiagnostics' + workspaceResourceId: logAnalyticsWorkspaceResourceId + logCategoriesAndGroups: [ + { + categoryGroup: 'allLogs' + enabled: true + } + ] + } + ] + : [] + tags: tags + enableTelemetry: enableTelemetry + publicIPAddressObject: { + name: 'pip-${bastionHostName}' + } + } +} + +// Jumpbox Virtual Machine +var jumpboxUniqueToken = take(uniqueString(resourceGroup().id, solutionSuffix), 10) +var jumpboxVmName = take('vm-${jumpboxUniqueToken}', 15) +module jumpboxVM 'br/public:avm/res/compute/virtual-machine:0.21.0' = if (deployAdminAccessResources) { + name: take('avm.res.compute.virtual-machine.${jumpboxVmName}', 64) + params: { + name: take(jumpboxVmName, 15) + enableTelemetry: enableTelemetry + computerName: take(jumpboxVmName, 15) + osType: 'Windows' + vmSize: empty(vmSize) ? 'Standard_D2s_v5' : vmSize + adminUsername: empty(vmAdminUsername) ? 'JumpboxAdminUser' : vmAdminUsername + adminPassword: vmAdminPassword + managedIdentities: { + userAssignedResourceIds: [ + userAssignedIdentity.outputs.resourceId + ] + } + availabilityZone: contains(zoneSupportedJumpboxLocations, solutionLocation) ? 1 : -1 + imageReference: { + publisher: 'microsoft-dsvm' + offer: 'dsvm-win-2022' + sku: 'winserver-2022' + version: 'latest' + } + nicConfigurations: [ + { + name: 'nic-${jumpboxVmName}' + enableAcceleratedNetworking: true + ipConfigurations: [ + { + name: 'ipconfig01' + subnetResourceId: virtualNetwork!.outputs.jumpboxSubnetResourceId + } + ] + } + ] + osDisk: { + caching: 'ReadWrite' + diskSizeGB: 128 + managedDisk: { + storageAccountType: 'Premium_LRS' + } + } + encryptionAtHost: false // Some Azure subscriptions do not support encryption at host + location: solutionLocation + tags: tags } dependsOn: (enableMonitoring && !useExistingLogAnalytics) ? [logAnalyticsWorkspace] : [] } From b9b95ef40639088ea70c4c80f24cb0220f26d57b Mon Sep 17 00:00:00 2001 From: Shubhangi-Microsoft Date: Wed, 8 Apr 2026 14:12:04 +0530 Subject: [PATCH 52/72] refactor: rename src/app to src/App and update all path references --- .github/dependabot.yml | 4 ++-- .github/workflows/docker-build.yml | 12 ++++++------ .github/workflows/job-docker-build.yml | 4 ++-- .gitignore | 12 ++++++------ docs/AZD_DEPLOYMENT.md | 4 ++-- docs/TECHNICAL_GUIDE.md | 2 +- scripts/deploy.ps1 | 8 ++++---- scripts/deploy.sh | 8 ++++---- scripts/local_dev.ps1 | 2 +- scripts/local_dev.sh | 2 +- src/{app => App}/.dockerignore | 0 src/{app => App}/WebApp.Dockerfile | 0 src/{app => App}/index.html | 0 src/{app => App}/microsoft.svg | 0 src/{app => App}/package-lock.json | 0 src/{app => App}/package.json | 0 src/{app => App}/server/package-lock.json | 0 src/{app => App}/server/package.json | 0 src/{app => App}/server/server.js | 0 src/{app => App}/src/App.tsx | 0 src/{app => App}/src/api/httpClient.ts | 0 src/{app => App}/src/api/index.ts | 0 src/{app => App}/src/components/AppHeader.tsx | 0 src/{app => App}/src/components/BriefReview.tsx | 0 src/{app => App}/src/components/ChatHistory.tsx | 0 src/{app => App}/src/components/ChatInput.tsx | 0 src/{app => App}/src/components/ChatPanel.tsx | 0 .../src/components/ComplianceSection.tsx | 0 .../src/components/ConfirmedBriefView.tsx | 0 .../src/components/ConversationItem.tsx | 0 .../src/components/ImagePreviewCard.tsx | 0 .../src/components/InlineContentPreview.tsx | 0 src/{app => App}/src/components/MessageBubble.tsx | 0 src/{app => App}/src/components/ProductCard.tsx | 0 src/{app => App}/src/components/ProductReview.tsx | 0 .../src/components/SelectedProductView.tsx | 0 src/{app => App}/src/components/SuggestionCard.tsx | 0 src/{app => App}/src/components/TypingIndicator.tsx | 0 src/{app => App}/src/components/ViolationCard.tsx | 0 src/{app => App}/src/components/WelcomeCard.tsx | 0 src/{app => App}/src/hooks/index.ts | 0 src/{app => App}/src/hooks/useAutoScroll.ts | 0 src/{app => App}/src/hooks/useChatOrchestrator.ts | 0 src/{app => App}/src/hooks/useContentGeneration.ts | 0 .../src/hooks/useConversationActions.ts | 0 src/{app => App}/src/hooks/useCopyToClipboard.ts | 0 src/{app => App}/src/hooks/useWindowSize.ts | 0 src/{app => App}/src/main.tsx | 0 src/{app => App}/src/store/appSlice.ts | 0 src/{app => App}/src/store/chatHistorySlice.ts | 0 src/{app => App}/src/store/chatSlice.ts | 0 src/{app => App}/src/store/contentSlice.ts | 0 src/{app => App}/src/store/hooks.ts | 0 src/{app => App}/src/store/index.ts | 0 src/{app => App}/src/store/selectors.ts | 0 src/{app => App}/src/store/store.ts | 0 src/{app => App}/src/styles/global.css | 0 src/{app => App}/src/styles/images/contoso.svg | 0 src/{app => App}/src/styles/images/firstprompt.png | Bin src/{app => App}/src/styles/images/secondprompt.png | Bin src/{app => App}/src/types/index.ts | 0 src/{app => App}/src/utils/briefFields.ts | 0 src/{app => App}/src/utils/contentErrors.ts | 0 src/{app => App}/src/utils/contentParsing.ts | 0 src/{app => App}/src/utils/downloadImage.ts | 0 src/{app => App}/src/utils/generationStages.ts | 0 src/{app => App}/src/utils/index.ts | 0 src/{app => App}/src/utils/messageUtils.ts | 0 src/{app => App}/src/utils/sseParser.ts | 0 src/{app => App}/src/utils/stringUtils.ts | 0 src/{app => App}/src/vite-env.d.ts | 0 src/{app => App}/tsconfig.json | 0 src/{app => App}/tsconfig.node.json | 0 src/{app => App}/vite.config.ts | 0 74 files changed, 29 insertions(+), 29 deletions(-) rename src/{app => App}/.dockerignore (100%) rename src/{app => App}/WebApp.Dockerfile (100%) rename src/{app => App}/index.html (100%) rename src/{app => App}/microsoft.svg (100%) rename src/{app => App}/package-lock.json (100%) rename src/{app => App}/package.json (100%) rename src/{app => App}/server/package-lock.json (100%) rename src/{app => App}/server/package.json (100%) rename src/{app => App}/server/server.js (100%) rename src/{app => App}/src/App.tsx (100%) rename src/{app => App}/src/api/httpClient.ts (100%) rename src/{app => App}/src/api/index.ts (100%) rename src/{app => App}/src/components/AppHeader.tsx (100%) rename src/{app => App}/src/components/BriefReview.tsx (100%) rename src/{app => App}/src/components/ChatHistory.tsx (100%) rename src/{app => App}/src/components/ChatInput.tsx (100%) rename src/{app => App}/src/components/ChatPanel.tsx (100%) rename src/{app => App}/src/components/ComplianceSection.tsx (100%) rename src/{app => App}/src/components/ConfirmedBriefView.tsx (100%) rename src/{app => App}/src/components/ConversationItem.tsx (100%) rename src/{app => App}/src/components/ImagePreviewCard.tsx (100%) rename src/{app => App}/src/components/InlineContentPreview.tsx (100%) rename src/{app => App}/src/components/MessageBubble.tsx (100%) rename src/{app => App}/src/components/ProductCard.tsx (100%) rename src/{app => App}/src/components/ProductReview.tsx (100%) rename src/{app => App}/src/components/SelectedProductView.tsx (100%) rename src/{app => App}/src/components/SuggestionCard.tsx (100%) rename src/{app => App}/src/components/TypingIndicator.tsx (100%) rename src/{app => App}/src/components/ViolationCard.tsx (100%) rename src/{app => App}/src/components/WelcomeCard.tsx (100%) rename src/{app => App}/src/hooks/index.ts (100%) rename src/{app => App}/src/hooks/useAutoScroll.ts (100%) rename src/{app => App}/src/hooks/useChatOrchestrator.ts (100%) rename src/{app => App}/src/hooks/useContentGeneration.ts (100%) rename src/{app => App}/src/hooks/useConversationActions.ts (100%) rename src/{app => App}/src/hooks/useCopyToClipboard.ts (100%) rename src/{app => App}/src/hooks/useWindowSize.ts (100%) rename src/{app => App}/src/main.tsx (100%) rename src/{app => App}/src/store/appSlice.ts (100%) rename src/{app => App}/src/store/chatHistorySlice.ts (100%) rename src/{app => App}/src/store/chatSlice.ts (100%) rename src/{app => App}/src/store/contentSlice.ts (100%) rename src/{app => App}/src/store/hooks.ts (100%) rename src/{app => App}/src/store/index.ts (100%) rename src/{app => App}/src/store/selectors.ts (100%) rename src/{app => App}/src/store/store.ts (100%) rename src/{app => App}/src/styles/global.css (100%) rename src/{app => App}/src/styles/images/contoso.svg (100%) rename src/{app => App}/src/styles/images/firstprompt.png (100%) rename src/{app => App}/src/styles/images/secondprompt.png (100%) rename src/{app => App}/src/types/index.ts (100%) rename src/{app => App}/src/utils/briefFields.ts (100%) rename src/{app => App}/src/utils/contentErrors.ts (100%) rename src/{app => App}/src/utils/contentParsing.ts (100%) rename src/{app => App}/src/utils/downloadImage.ts (100%) rename src/{app => App}/src/utils/generationStages.ts (100%) rename src/{app => App}/src/utils/index.ts (100%) rename src/{app => App}/src/utils/messageUtils.ts (100%) rename src/{app => App}/src/utils/sseParser.ts (100%) rename src/{app => App}/src/utils/stringUtils.ts (100%) rename src/{app => App}/src/vite-env.d.ts (100%) rename src/{app => App}/tsconfig.json (100%) rename src/{app => App}/tsconfig.node.json (100%) rename src/{app => App}/vite.config.ts (100%) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index de7102992..7a32def93 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -35,8 +35,8 @@ updates: # npm dependencies - grouped - package-ecosystem: "npm" directories: - - "/src/app" - - "/src/app/server" + - "/src/App" + - "/src/App/server" schedule: interval: "monthly" target-branch: "dependabotchanges" diff --git a/.github/workflows/docker-build.yml b/.github/workflows/docker-build.yml index cc1197ad8..b3c4d443e 100644 --- a/.github/workflows/docker-build.yml +++ b/.github/workflows/docker-build.yml @@ -8,8 +8,8 @@ on: - demo paths: - 'src/backend/**' - - 'src/app/**' - - 'src/app/server/**' + - 'src/App/**' + - 'src/App/server/**' - '.github/workflows/docker-build.yml' pull_request: types: @@ -23,8 +23,8 @@ on: - demo paths: - 'src/backend/**' - - 'src/app/**' - - 'src/app/server/**' + - 'src/App/**' + - 'src/App/server/**' - '.github/workflows/docker-build.yml' workflow_dispatch: @@ -83,8 +83,8 @@ jobs: - name: Build and Push Docker Image for Frontend Server uses: docker/build-push-action@v6 with: - context: ./src/app - file: ./src/app/WebApp.Dockerfile + context: ./src/App + file: ./src/App/WebApp.Dockerfile push: ${{ github.ref_name == 'main' || github.ref_name == 'dev' || github.ref_name == 'demo' || github.ref_name == 'dependabotchanges' }} tags: | ${{ secrets.ACR_LOGIN_SERVER || 'acrlogin.azurecr.io' }}/content-gen-app:${{ steps.determine_tag.outputs.tagname }} diff --git a/.github/workflows/job-docker-build.yml b/.github/workflows/job-docker-build.yml index 3773ea285..260cfcc65 100644 --- a/.github/workflows/job-docker-build.yml +++ b/.github/workflows/job-docker-build.yml @@ -65,8 +65,8 @@ jobs: env: DOCKER_BUILD_SUMMARY: false with: - context: ./src/app - file: ./src/app/WebApp.Dockerfile + context: ./src/App + file: ./src/App/WebApp.Dockerfile push: true tags: | ${{ secrets.ACR_TEST_LOGIN_SERVER }}/content-gen-app:${{ steps.generate_docker_tag.outputs.IMAGE_TAG }} diff --git a/.gitignore b/.gitignore index 80d4338d3..310b95883 100644 --- a/.gitignore +++ b/.gitignore @@ -36,15 +36,15 @@ eggs/ *.swo # Node -/src/app/node_modules/ -/src/app/server/node_modules/ -/src/app/server/static/ -/src/app/server/*.zip +/src/App/node_modules/ +/src/App/server/node_modules/ +/src/App/server/static/ +/src/App/server/*.zip node_modules/ # Build output -/src/app/static/ -/src/app/dist/ +/src/App/static/ +/src/App/dist/ # Logs *.log diff --git a/docs/AZD_DEPLOYMENT.md b/docs/AZD_DEPLOYMENT.md index a54fb14ae..edb5a69e6 100644 --- a/docs/AZD_DEPLOYMENT.md +++ b/docs/AZD_DEPLOYMENT.md @@ -239,7 +239,7 @@ Error: az acr build failed **Solution**: Check the Dockerfile and ensure all required files are present: ```bash # Manual build for debugging -cd src/app +cd src/App docker build -f WebApp.Dockerfile -t content-gen-app:test . ``` @@ -251,7 +251,7 @@ Error: az webapp deploy failed **Solution**: Ensure the frontend builds successfully: ```bash -cd src/app +cd src/App npm install npm run build ``` diff --git a/docs/TECHNICAL_GUIDE.md b/docs/TECHNICAL_GUIDE.md index dc6288e63..ad2fc7bd2 100644 --- a/docs/TECHNICAL_GUIDE.md +++ b/docs/TECHNICAL_GUIDE.md @@ -142,7 +142,7 @@ pip install -r requirements.txt python app.py # Frontend -cd src/app +cd src/App npm install npm run dev ``` diff --git a/scripts/deploy.ps1 b/scripts/deploy.ps1 index a9370f8e8..7b0844548 100644 --- a/scripts/deploy.ps1 +++ b/scripts/deploy.ps1 @@ -131,15 +131,15 @@ if ($continue -eq "y" -or $continue -eq "Y") { Write-Host "Step 3: Building and deploying frontend..." -ForegroundColor Green Write-Host "==========================================" -ForegroundColor Green - Set-Location "$ProjectDir\src\app" + Set-Location "$ProjectDir\src\App" npm install npm run build # Copy built files to server directory - New-Item -ItemType Directory -Force "$ProjectDir\src\app\server\static" | Out-Null - Copy-Item -Path "$ProjectDir\src\app\static\*" -Destination "$ProjectDir\src\app\server\static\" -Recurse -Force + New-Item -ItemType Directory -Force "$ProjectDir\src\App\server\static" | Out-Null + Copy-Item -Path "$ProjectDir\src\App\static\*" -Destination "$ProjectDir\src\App\server\static\" -Recurse -Force - Set-Location "$ProjectDir\src\app\server" + Set-Location "$ProjectDir\src\App\server" # Create deployment package if (Test-Path "frontend-deploy.zip") { diff --git a/scripts/deploy.sh b/scripts/deploy.sh index 4650bff51..63c8d238d 100644 --- a/scripts/deploy.sh +++ b/scripts/deploy.sh @@ -129,15 +129,15 @@ if [[ $REPLY =~ ^[Yy]$ ]]; then echo "Step 3: Building and deploying frontend..." echo "==========================================" - cd "$PROJECT_DIR/src/app" + cd "$PROJECT_DIR/src/App" npm install npm run build # Copy built files to server directory - mkdir -p "$PROJECT_DIR/src/app/server/static" - cp -r "$PROJECT_DIR/src/app/static/"* "$PROJECT_DIR/src/app/server/static/" + mkdir -p "$PROJECT_DIR/src/App/server/static" + cp -r "$PROJECT_DIR/src/App/static/"* "$PROJECT_DIR/src/App/server/static/" - cd "$PROJECT_DIR/src/app/server" + cd "$PROJECT_DIR/src/App/server" # Create deployment package rm -f frontend-deploy.zip diff --git a/scripts/local_dev.ps1 b/scripts/local_dev.ps1 index 792c0b57c..fc34ec59a 100644 --- a/scripts/local_dev.ps1 +++ b/scripts/local_dev.ps1 @@ -31,7 +31,7 @@ $ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path $ProjectRoot = Split-Path -Parent $ScriptDir $SrcDir = Join-Path $ProjectRoot "src" $BackendDir = Join-Path $SrcDir "backend" -$FrontendDir = Join-Path $SrcDir "app" +$FrontendDir = Join-Path $SrcDir "App" # Default ports $BackendPort = if ($env:BACKEND_PORT) { $env:BACKEND_PORT } else { "5000" } diff --git a/scripts/local_dev.sh b/scripts/local_dev.sh index 4fb56bc85..5d2b8fce9 100644 --- a/scripts/local_dev.sh +++ b/scripts/local_dev.sh @@ -34,7 +34,7 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" SRC_DIR="$PROJECT_ROOT/src" BACKEND_DIR="$SRC_DIR/backend" -FRONTEND_DIR="$SRC_DIR/app" +FRONTEND_DIR="$SRC_DIR/App" # Default ports BACKEND_PORT=${BACKEND_PORT:-5000} diff --git a/src/app/.dockerignore b/src/App/.dockerignore similarity index 100% rename from src/app/.dockerignore rename to src/App/.dockerignore diff --git a/src/app/WebApp.Dockerfile b/src/App/WebApp.Dockerfile similarity index 100% rename from src/app/WebApp.Dockerfile rename to src/App/WebApp.Dockerfile diff --git a/src/app/index.html b/src/App/index.html similarity index 100% rename from src/app/index.html rename to src/App/index.html diff --git a/src/app/microsoft.svg b/src/App/microsoft.svg similarity index 100% rename from src/app/microsoft.svg rename to src/App/microsoft.svg diff --git a/src/app/package-lock.json b/src/App/package-lock.json similarity index 100% rename from src/app/package-lock.json rename to src/App/package-lock.json diff --git a/src/app/package.json b/src/App/package.json similarity index 100% rename from src/app/package.json rename to src/App/package.json diff --git a/src/app/server/package-lock.json b/src/App/server/package-lock.json similarity index 100% rename from src/app/server/package-lock.json rename to src/App/server/package-lock.json diff --git a/src/app/server/package.json b/src/App/server/package.json similarity index 100% rename from src/app/server/package.json rename to src/App/server/package.json diff --git a/src/app/server/server.js b/src/App/server/server.js similarity index 100% rename from src/app/server/server.js rename to src/App/server/server.js diff --git a/src/app/src/App.tsx b/src/App/src/App.tsx similarity index 100% rename from src/app/src/App.tsx rename to src/App/src/App.tsx diff --git a/src/app/src/api/httpClient.ts b/src/App/src/api/httpClient.ts similarity index 100% rename from src/app/src/api/httpClient.ts rename to src/App/src/api/httpClient.ts diff --git a/src/app/src/api/index.ts b/src/App/src/api/index.ts similarity index 100% rename from src/app/src/api/index.ts rename to src/App/src/api/index.ts diff --git a/src/app/src/components/AppHeader.tsx b/src/App/src/components/AppHeader.tsx similarity index 100% rename from src/app/src/components/AppHeader.tsx rename to src/App/src/components/AppHeader.tsx diff --git a/src/app/src/components/BriefReview.tsx b/src/App/src/components/BriefReview.tsx similarity index 100% rename from src/app/src/components/BriefReview.tsx rename to src/App/src/components/BriefReview.tsx diff --git a/src/app/src/components/ChatHistory.tsx b/src/App/src/components/ChatHistory.tsx similarity index 100% rename from src/app/src/components/ChatHistory.tsx rename to src/App/src/components/ChatHistory.tsx diff --git a/src/app/src/components/ChatInput.tsx b/src/App/src/components/ChatInput.tsx similarity index 100% rename from src/app/src/components/ChatInput.tsx rename to src/App/src/components/ChatInput.tsx diff --git a/src/app/src/components/ChatPanel.tsx b/src/App/src/components/ChatPanel.tsx similarity index 100% rename from src/app/src/components/ChatPanel.tsx rename to src/App/src/components/ChatPanel.tsx diff --git a/src/app/src/components/ComplianceSection.tsx b/src/App/src/components/ComplianceSection.tsx similarity index 100% rename from src/app/src/components/ComplianceSection.tsx rename to src/App/src/components/ComplianceSection.tsx diff --git a/src/app/src/components/ConfirmedBriefView.tsx b/src/App/src/components/ConfirmedBriefView.tsx similarity index 100% rename from src/app/src/components/ConfirmedBriefView.tsx rename to src/App/src/components/ConfirmedBriefView.tsx diff --git a/src/app/src/components/ConversationItem.tsx b/src/App/src/components/ConversationItem.tsx similarity index 100% rename from src/app/src/components/ConversationItem.tsx rename to src/App/src/components/ConversationItem.tsx diff --git a/src/app/src/components/ImagePreviewCard.tsx b/src/App/src/components/ImagePreviewCard.tsx similarity index 100% rename from src/app/src/components/ImagePreviewCard.tsx rename to src/App/src/components/ImagePreviewCard.tsx diff --git a/src/app/src/components/InlineContentPreview.tsx b/src/App/src/components/InlineContentPreview.tsx similarity index 100% rename from src/app/src/components/InlineContentPreview.tsx rename to src/App/src/components/InlineContentPreview.tsx diff --git a/src/app/src/components/MessageBubble.tsx b/src/App/src/components/MessageBubble.tsx similarity index 100% rename from src/app/src/components/MessageBubble.tsx rename to src/App/src/components/MessageBubble.tsx diff --git a/src/app/src/components/ProductCard.tsx b/src/App/src/components/ProductCard.tsx similarity index 100% rename from src/app/src/components/ProductCard.tsx rename to src/App/src/components/ProductCard.tsx diff --git a/src/app/src/components/ProductReview.tsx b/src/App/src/components/ProductReview.tsx similarity index 100% rename from src/app/src/components/ProductReview.tsx rename to src/App/src/components/ProductReview.tsx diff --git a/src/app/src/components/SelectedProductView.tsx b/src/App/src/components/SelectedProductView.tsx similarity index 100% rename from src/app/src/components/SelectedProductView.tsx rename to src/App/src/components/SelectedProductView.tsx diff --git a/src/app/src/components/SuggestionCard.tsx b/src/App/src/components/SuggestionCard.tsx similarity index 100% rename from src/app/src/components/SuggestionCard.tsx rename to src/App/src/components/SuggestionCard.tsx diff --git a/src/app/src/components/TypingIndicator.tsx b/src/App/src/components/TypingIndicator.tsx similarity index 100% rename from src/app/src/components/TypingIndicator.tsx rename to src/App/src/components/TypingIndicator.tsx diff --git a/src/app/src/components/ViolationCard.tsx b/src/App/src/components/ViolationCard.tsx similarity index 100% rename from src/app/src/components/ViolationCard.tsx rename to src/App/src/components/ViolationCard.tsx diff --git a/src/app/src/components/WelcomeCard.tsx b/src/App/src/components/WelcomeCard.tsx similarity index 100% rename from src/app/src/components/WelcomeCard.tsx rename to src/App/src/components/WelcomeCard.tsx diff --git a/src/app/src/hooks/index.ts b/src/App/src/hooks/index.ts similarity index 100% rename from src/app/src/hooks/index.ts rename to src/App/src/hooks/index.ts diff --git a/src/app/src/hooks/useAutoScroll.ts b/src/App/src/hooks/useAutoScroll.ts similarity index 100% rename from src/app/src/hooks/useAutoScroll.ts rename to src/App/src/hooks/useAutoScroll.ts diff --git a/src/app/src/hooks/useChatOrchestrator.ts b/src/App/src/hooks/useChatOrchestrator.ts similarity index 100% rename from src/app/src/hooks/useChatOrchestrator.ts rename to src/App/src/hooks/useChatOrchestrator.ts diff --git a/src/app/src/hooks/useContentGeneration.ts b/src/App/src/hooks/useContentGeneration.ts similarity index 100% rename from src/app/src/hooks/useContentGeneration.ts rename to src/App/src/hooks/useContentGeneration.ts diff --git a/src/app/src/hooks/useConversationActions.ts b/src/App/src/hooks/useConversationActions.ts similarity index 100% rename from src/app/src/hooks/useConversationActions.ts rename to src/App/src/hooks/useConversationActions.ts diff --git a/src/app/src/hooks/useCopyToClipboard.ts b/src/App/src/hooks/useCopyToClipboard.ts similarity index 100% rename from src/app/src/hooks/useCopyToClipboard.ts rename to src/App/src/hooks/useCopyToClipboard.ts diff --git a/src/app/src/hooks/useWindowSize.ts b/src/App/src/hooks/useWindowSize.ts similarity index 100% rename from src/app/src/hooks/useWindowSize.ts rename to src/App/src/hooks/useWindowSize.ts diff --git a/src/app/src/main.tsx b/src/App/src/main.tsx similarity index 100% rename from src/app/src/main.tsx rename to src/App/src/main.tsx diff --git a/src/app/src/store/appSlice.ts b/src/App/src/store/appSlice.ts similarity index 100% rename from src/app/src/store/appSlice.ts rename to src/App/src/store/appSlice.ts diff --git a/src/app/src/store/chatHistorySlice.ts b/src/App/src/store/chatHistorySlice.ts similarity index 100% rename from src/app/src/store/chatHistorySlice.ts rename to src/App/src/store/chatHistorySlice.ts diff --git a/src/app/src/store/chatSlice.ts b/src/App/src/store/chatSlice.ts similarity index 100% rename from src/app/src/store/chatSlice.ts rename to src/App/src/store/chatSlice.ts diff --git a/src/app/src/store/contentSlice.ts b/src/App/src/store/contentSlice.ts similarity index 100% rename from src/app/src/store/contentSlice.ts rename to src/App/src/store/contentSlice.ts diff --git a/src/app/src/store/hooks.ts b/src/App/src/store/hooks.ts similarity index 100% rename from src/app/src/store/hooks.ts rename to src/App/src/store/hooks.ts diff --git a/src/app/src/store/index.ts b/src/App/src/store/index.ts similarity index 100% rename from src/app/src/store/index.ts rename to src/App/src/store/index.ts diff --git a/src/app/src/store/selectors.ts b/src/App/src/store/selectors.ts similarity index 100% rename from src/app/src/store/selectors.ts rename to src/App/src/store/selectors.ts diff --git a/src/app/src/store/store.ts b/src/App/src/store/store.ts similarity index 100% rename from src/app/src/store/store.ts rename to src/App/src/store/store.ts diff --git a/src/app/src/styles/global.css b/src/App/src/styles/global.css similarity index 100% rename from src/app/src/styles/global.css rename to src/App/src/styles/global.css diff --git a/src/app/src/styles/images/contoso.svg b/src/App/src/styles/images/contoso.svg similarity index 100% rename from src/app/src/styles/images/contoso.svg rename to src/App/src/styles/images/contoso.svg diff --git a/src/app/src/styles/images/firstprompt.png b/src/App/src/styles/images/firstprompt.png similarity index 100% rename from src/app/src/styles/images/firstprompt.png rename to src/App/src/styles/images/firstprompt.png diff --git a/src/app/src/styles/images/secondprompt.png b/src/App/src/styles/images/secondprompt.png similarity index 100% rename from src/app/src/styles/images/secondprompt.png rename to src/App/src/styles/images/secondprompt.png diff --git a/src/app/src/types/index.ts b/src/App/src/types/index.ts similarity index 100% rename from src/app/src/types/index.ts rename to src/App/src/types/index.ts diff --git a/src/app/src/utils/briefFields.ts b/src/App/src/utils/briefFields.ts similarity index 100% rename from src/app/src/utils/briefFields.ts rename to src/App/src/utils/briefFields.ts diff --git a/src/app/src/utils/contentErrors.ts b/src/App/src/utils/contentErrors.ts similarity index 100% rename from src/app/src/utils/contentErrors.ts rename to src/App/src/utils/contentErrors.ts diff --git a/src/app/src/utils/contentParsing.ts b/src/App/src/utils/contentParsing.ts similarity index 100% rename from src/app/src/utils/contentParsing.ts rename to src/App/src/utils/contentParsing.ts diff --git a/src/app/src/utils/downloadImage.ts b/src/App/src/utils/downloadImage.ts similarity index 100% rename from src/app/src/utils/downloadImage.ts rename to src/App/src/utils/downloadImage.ts diff --git a/src/app/src/utils/generationStages.ts b/src/App/src/utils/generationStages.ts similarity index 100% rename from src/app/src/utils/generationStages.ts rename to src/App/src/utils/generationStages.ts diff --git a/src/app/src/utils/index.ts b/src/App/src/utils/index.ts similarity index 100% rename from src/app/src/utils/index.ts rename to src/App/src/utils/index.ts diff --git a/src/app/src/utils/messageUtils.ts b/src/App/src/utils/messageUtils.ts similarity index 100% rename from src/app/src/utils/messageUtils.ts rename to src/App/src/utils/messageUtils.ts diff --git a/src/app/src/utils/sseParser.ts b/src/App/src/utils/sseParser.ts similarity index 100% rename from src/app/src/utils/sseParser.ts rename to src/App/src/utils/sseParser.ts diff --git a/src/app/src/utils/stringUtils.ts b/src/App/src/utils/stringUtils.ts similarity index 100% rename from src/app/src/utils/stringUtils.ts rename to src/App/src/utils/stringUtils.ts diff --git a/src/app/src/vite-env.d.ts b/src/App/src/vite-env.d.ts similarity index 100% rename from src/app/src/vite-env.d.ts rename to src/App/src/vite-env.d.ts diff --git a/src/app/tsconfig.json b/src/App/tsconfig.json similarity index 100% rename from src/app/tsconfig.json rename to src/App/tsconfig.json diff --git a/src/app/tsconfig.node.json b/src/App/tsconfig.node.json similarity index 100% rename from src/app/tsconfig.node.json rename to src/App/tsconfig.node.json diff --git a/src/app/vite.config.ts b/src/App/vite.config.ts similarity index 100% rename from src/app/vite.config.ts rename to src/App/vite.config.ts From f6722be12658e0d9282c4810d88bfb9519050301 Mon Sep 17 00:00:00 2001 From: Shubhangi-Microsoft Date: Wed, 8 Apr 2026 17:34:25 +0530 Subject: [PATCH 53/72] fix: address PR review - ChatInput event type, userId fallback, contentParsing docstring --- src/App/src/api/httpClient.ts | 2 +- src/App/src/components/ChatInput.tsx | 5 ++--- src/App/src/utils/contentParsing.ts | 4 ++-- 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/src/App/src/api/httpClient.ts b/src/App/src/api/httpClient.ts index 6aaf83b48..1d51ebc68 100644 --- a/src/App/src/api/httpClient.ts +++ b/src/App/src/api/httpClient.ts @@ -178,7 +178,7 @@ httpClient.onRequest(async (_url, init) => { try { const { store } = await import('../store/store'); const state = store?.getState?.(); - const userId: string = state?.app?.userId ?? 'anonymous'; + const userId: string = state?.app?.userId || 'anonymous'; headers.set('X-Ms-Client-Principal-Id', userId); } catch { headers.set('X-Ms-Client-Principal-Id', 'anonymous'); diff --git a/src/App/src/components/ChatInput.tsx b/src/App/src/components/ChatInput.tsx index 5a3efb41f..39adb87cc 100644 --- a/src/App/src/components/ChatInput.tsx +++ b/src/App/src/components/ChatInput.tsx @@ -43,8 +43,7 @@ export const ChatInput = memo(function ChatInput({ if (controlledValue === undefined) setInternalValue(v); }, [controlledOnChange, controlledValue]); - const handleSubmit = useCallback((e: React.FormEvent) => { - e.preventDefault(); + const handleSubmit = useCallback(() => { if (inputValue.trim() && !disabled) { onSendMessage(inputValue.trim()); setInputValue(''); @@ -54,7 +53,7 @@ export const ChatInput = memo(function ChatInput({ const handleKeyDown = useCallback((e: React.KeyboardEvent) => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); - handleSubmit(e); + handleSubmit(); } }, [handleSubmit]); diff --git a/src/App/src/utils/contentParsing.ts b/src/App/src/utils/contentParsing.ts index e59ac85e3..9ad8ae672 100644 --- a/src/App/src/utils/contentParsing.ts +++ b/src/App/src/utils/contentParsing.ts @@ -61,8 +61,8 @@ function parseTextContent( * Resolve the best available image URL from a raw API response. * * Priority: explicit `image_url` (with blob rewrite) → base64 data URI. - * Pass `rewriteBlobs: true` (default) when restoring from a saved - * conversation; `false` when the response just came from the live API. + * Pass `rewriteBlobs: true` when restoring from a saved + * conversation; `false` (default) when the response just came from the live API. */ function resolveImageUrl( raw: { image_url?: string; image_base64?: string }, From 06c7787fb1ee94f636eac69895ee14d856fefad4 Mon Sep 17 00:00:00 2001 From: Shubhangi-Microsoft Date: Wed, 8 Apr 2026 17:48:15 +0530 Subject: [PATCH 54/72] Revert "fix: address PR review - ChatInput event type, userId fallback, contentParsing docstring" This reverts commit f6722be12658e0d9282c4810d88bfb9519050301. --- src/App/src/api/httpClient.ts | 2 +- src/App/src/components/ChatInput.tsx | 5 +++-- src/App/src/utils/contentParsing.ts | 4 ++-- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/App/src/api/httpClient.ts b/src/App/src/api/httpClient.ts index 1d51ebc68..6aaf83b48 100644 --- a/src/App/src/api/httpClient.ts +++ b/src/App/src/api/httpClient.ts @@ -178,7 +178,7 @@ httpClient.onRequest(async (_url, init) => { try { const { store } = await import('../store/store'); const state = store?.getState?.(); - const userId: string = state?.app?.userId || 'anonymous'; + const userId: string = state?.app?.userId ?? 'anonymous'; headers.set('X-Ms-Client-Principal-Id', userId); } catch { headers.set('X-Ms-Client-Principal-Id', 'anonymous'); diff --git a/src/App/src/components/ChatInput.tsx b/src/App/src/components/ChatInput.tsx index 39adb87cc..5a3efb41f 100644 --- a/src/App/src/components/ChatInput.tsx +++ b/src/App/src/components/ChatInput.tsx @@ -43,7 +43,8 @@ export const ChatInput = memo(function ChatInput({ if (controlledValue === undefined) setInternalValue(v); }, [controlledOnChange, controlledValue]); - const handleSubmit = useCallback(() => { + const handleSubmit = useCallback((e: React.FormEvent) => { + e.preventDefault(); if (inputValue.trim() && !disabled) { onSendMessage(inputValue.trim()); setInputValue(''); @@ -53,7 +54,7 @@ export const ChatInput = memo(function ChatInput({ const handleKeyDown = useCallback((e: React.KeyboardEvent) => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); - handleSubmit(); + handleSubmit(e); } }, [handleSubmit]); diff --git a/src/App/src/utils/contentParsing.ts b/src/App/src/utils/contentParsing.ts index 9ad8ae672..e59ac85e3 100644 --- a/src/App/src/utils/contentParsing.ts +++ b/src/App/src/utils/contentParsing.ts @@ -61,8 +61,8 @@ function parseTextContent( * Resolve the best available image URL from a raw API response. * * Priority: explicit `image_url` (with blob rewrite) → base64 data URI. - * Pass `rewriteBlobs: true` when restoring from a saved - * conversation; `false` (default) when the response just came from the live API. + * Pass `rewriteBlobs: true` (default) when restoring from a saved + * conversation; `false` when the response just came from the live API. */ function resolveImageUrl( raw: { image_url?: string; image_base64?: string }, From a812b18998521fd1a613c9012f1eeed3eb4a9d40 Mon Sep 17 00:00:00 2001 From: Pavan-Microsoft Date: Wed, 8 Apr 2026 18:08:57 +0530 Subject: [PATCH 55/72] Refactor Azure resource identifiers in workflows and scripts - Updated workflow files to replace AZURE_ENV_LOG_ANALYTICS_WORKSPACE_RID with AZURE_ENV_EXISTING_LOG_ANALYTICS_WORKSPACE_RID and AZURE_ENV_FOUNDRY_PROJECT_RID with AZURE_EXISTING_AIPROJECT_RESOURCE_ID. - Modified validation logic for the new identifiers in job-deploy, job-deploy-linux, and job-deploy-windows workflows. - Adjusted output parameters in Bicep templates to reflect the new naming conventions. - Updated documentation to guide users on setting the new environment variables for existing resources. - Refactored local development scripts to accommodate the new environment variable names. --- .env.sample | 2 +- .github/workflows/azd-template-validation.yml | 2 +- .github/workflows/azure-dev.yml | 2 +- .github/workflows/deploy-orchestrator.yml | 8 +-- .github/workflows/deploy-v2.yml | 48 +++++++-------- .github/workflows/job-deploy-linux.yml | 58 +++++++++---------- .github/workflows/job-deploy-windows.yml | 56 +++++++++--------- .github/workflows/job-deploy.yml | 44 +++++++------- azure_custom.yaml | 4 +- docs/AZD_DEPLOYMENT.md | 4 +- docs/CustomizingAzdParameters.md | 4 +- infra/main.bicep | 2 +- infra/main.json | 4 +- infra/main.parameters.json | 7 ++- infra/main.waf.parameters.json | 7 ++- infra/main_custom.bicep | 2 +- scripts/local_dev.ps1 | 4 +- scripts/local_dev.sh | 4 +- 18 files changed, 134 insertions(+), 128 deletions(-) diff --git a/.env.sample b/.env.sample index dd9a9ada7..7a2667a8d 100644 --- a/.env.sample +++ b/.env.sample @@ -23,7 +23,7 @@ AZURE_AI_IMAGE_MODEL_DEPLOYMENT=gpt-image-1-mini # Azure OpenAI Configuration # ============================================================================= AI_FOUNDRY_RESOURCE_ID=/subscriptions/your-subscription-id/resourceGroups/your-resource-group/providers/Microsoft.CognitiveServices/accounts/your-aif-account -AZURE_ENV_FOUNDRY_PROJECT_RID=/subscriptions/your-subscription-id/resourceGroups/your-resource-group/providers/Microsoft.CognitiveServices/accounts/your-aif-account/projects/your-project-name +AZURE_EXISTING_AIPROJECT_RESOURCE_ID=/subscriptions/your-subscription-id/resourceGroups/your-resource-group/providers/Microsoft.CognitiveServices/accounts/your-aif-account/projects/your-project-name # Your Azure OpenAI endpoint (e.g., https://your-resource.openai.azure.com/) AZURE_OPENAI_ENDPOINT=https://your-openai.openai.azure.com/ diff --git a/.github/workflows/azd-template-validation.yml b/.github/workflows/azd-template-validation.yml index 6579857ba..d4e0ec858 100644 --- a/.github/workflows/azd-template-validation.yml +++ b/.github/workflows/azd-template-validation.yml @@ -35,7 +35,7 @@ jobs: AZURE_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }} AZURE_ENV_NAME: ${{ secrets.AZURE_ENV_NAME }}-${{ env.HHMM }} AZURE_LOCATION: ${{ secrets.AZURE_LOCATION }} - AZURE_ENV_OPENAI_LOCATION: ${{ secrets.AZURE_ENV_OPENAI_LOCATION }} + AZURE_ENV_AI_SERVICE_LOCATION: ${{ secrets.AZURE_ENV_AI_SERVICE_LOCATION }} AZURE_AI_MODEL_CAPACITY: 1 GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/azure-dev.yml b/.github/workflows/azure-dev.yml index cc0b0e8cd..7ea921464 100644 --- a/.github/workflows/azure-dev.yml +++ b/.github/workflows/azure-dev.yml @@ -17,7 +17,7 @@ jobs: AZURE_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }} AZURE_ENV_NAME: ${{ secrets.AZURE_ENV_NAME }} AZURE_LOCATION: ${{ secrets.AZURE_LOCATION }} - AZURE_ENV_OPENAI_LOCATION: ${{ secrets.AZURE_ENV_OPENAI_LOCATION }} + AZURE_ENV_AI_SERVICE_LOCATION: ${{ secrets.AZURE_ENV_AI_SERVICE_LOCATION }} AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID: ${{ secrets.AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID }} AZURE_EXISTING_AI_PROJECT_RESOURCE_ID: ${{ secrets.AZURE_EXISTING_AI_PROJECT_RESOURCE_ID }} AZURE_DEV_COLLECT_TELEMETRY: ${{ vars.AZURE_DEV_COLLECT_TELEMETRY }} diff --git a/.github/workflows/deploy-orchestrator.yml b/.github/workflows/deploy-orchestrator.yml index 82de9002f..556ee3a6f 100644 --- a/.github/workflows/deploy-orchestrator.yml +++ b/.github/workflows/deploy-orchestrator.yml @@ -42,12 +42,12 @@ on: required: false default: 'GoldenPath-Testing' type: string - AZURE_ENV_LOG_ANALYTICS_WORKSPACE_RID: + AZURE_ENV_EXISTING_LOG_ANALYTICS_WORKSPACE_RID: description: 'Log Analytics Workspace ID (Optional)' required: false default: '' type: string - AZURE_ENV_FOUNDRY_PROJECT_RID: + AZURE_EXISTING_AIPROJECT_RESOURCE_ID: description: 'AI Project Resource ID (Optional)' required: false default: '' @@ -91,8 +91,8 @@ jobs: EXP: ${{ inputs.EXP }} build_docker_image: ${{ inputs.build_docker_image }} existing_webapp_url: ${{ inputs.existing_webapp_url }} - AZURE_ENV_LOG_ANALYTICS_WORKSPACE_RID: ${{ inputs.AZURE_ENV_LOG_ANALYTICS_WORKSPACE_RID }} - AZURE_ENV_FOUNDRY_PROJECT_RID: ${{ inputs.AZURE_ENV_FOUNDRY_PROJECT_RID }} + AZURE_ENV_EXISTING_LOG_ANALYTICS_WORKSPACE_RID: ${{ inputs.AZURE_ENV_EXISTING_LOG_ANALYTICS_WORKSPACE_RID }} + AZURE_EXISTING_AIPROJECT_RESOURCE_ID: ${{ inputs.AZURE_EXISTING_AIPROJECT_RESOURCE_ID }} docker_image_tag: ${{ needs.docker-build.outputs.AZURE_ENV_IMAGE_TAG }} run_e2e_tests: ${{ inputs.run_e2e_tests }} cleanup_resources: ${{ inputs.cleanup_resources }} diff --git a/.github/workflows/deploy-v2.yml b/.github/workflows/deploy-v2.yml index a7ede3ccd..be2eb98f4 100644 --- a/.github/workflows/deploy-v2.yml +++ b/.github/workflows/deploy-v2.yml @@ -85,12 +85,12 @@ on: - 'Smoke-Testing' - 'None' - AZURE_ENV_LOG_ANALYTICS_WORKSPACE_RID: + AZURE_ENV_EXISTING_LOG_ANALYTICS_WORKSPACE_RID: description: 'Log Analytics Workspace ID (Optional)' required: false default: '' type: string - AZURE_ENV_FOUNDRY_PROJECT_RID: + AZURE_EXISTING_AIPROJECT_RESOURCE_ID: description: 'AI Project Resource ID (Optional)' required: false default: '' @@ -130,8 +130,8 @@ jobs: build_docker_image: ${{ steps.validate.outputs.build_docker_image }} cleanup_resources: ${{ steps.validate.outputs.cleanup_resources }} run_e2e_tests: ${{ steps.validate.outputs.run_e2e_tests }} - AZURE_ENV_LOG_ANALYTICS_WORKSPACE_RID: ${{ steps.validate.outputs.AZURE_ENV_LOG_ANALYTICS_WORKSPACE_RID }} - AZURE_ENV_FOUNDRY_PROJECT_RID: ${{ steps.validate.outputs.AZURE_ENV_FOUNDRY_PROJECT_RID }} + AZURE_ENV_EXISTING_LOG_ANALYTICS_WORKSPACE_RID: ${{ steps.validate.outputs.AZURE_ENV_EXISTING_LOG_ANALYTICS_WORKSPACE_RID }} + AZURE_EXISTING_AIPROJECT_RESOURCE_ID: ${{ steps.validate.outputs.AZURE_EXISTING_AIPROJECT_RESOURCE_ID }} existing_webapp_url: ${{ steps.validate.outputs.existing_webapp_url }} AZURE_ENV_IMAGE_MODEL_NAME: ${{ steps.validate.outputs.AZURE_ENV_IMAGE_MODEL_NAME }} steps: @@ -147,8 +147,8 @@ jobs: INPUT_BUILD_DOCKER_IMAGE: ${{ github.event.inputs.build_docker_image }} INPUT_CLEANUP_RESOURCES: ${{ github.event.inputs.cleanup_resources }} INPUT_RUN_E2E_TESTS: ${{ github.event.inputs.run_e2e_tests }} - INPUT_AZURE_ENV_LOG_ANALYTICS_WORKSPACE_RID: ${{ github.event.inputs.AZURE_ENV_LOG_ANALYTICS_WORKSPACE_RID }} - INPUT_AZURE_ENV_FOUNDRY_PROJECT_RID: ${{ github.event.inputs.AZURE_ENV_FOUNDRY_PROJECT_RID }} + INPUT_AZURE_ENV_EXISTING_LOG_ANALYTICS_WORKSPACE_RID: ${{ github.event.inputs.AZURE_ENV_EXISTING_LOG_ANALYTICS_WORKSPACE_RID }} + INPUT_AZURE_EXISTING_AIPROJECT_RESOURCE_ID: ${{ github.event.inputs.AZURE_EXISTING_AIPROJECT_RESOURCE_ID }} INPUT_EXISTING_WEBAPP_URL: ${{ github.event.inputs.existing_webapp_url }} INPUT_IMAGE_MODEL_CHOICE: ${{ github.event.inputs.AZURE_ENV_IMAGE_MODEL_NAME }} run: | @@ -242,32 +242,32 @@ jobs: echo "✅ run_e2e_tests: '$TEST_OPTION' is valid" fi - # Validate AZURE_ENV_LOG_ANALYTICS_WORKSPACE_RID (optional, Azure Resource ID format) - if [[ -n "$INPUT_AZURE_ENV_LOG_ANALYTICS_WORKSPACE_RID" ]]; then - if [[ ! "$INPUT_AZURE_ENV_LOG_ANALYTICS_WORKSPACE_RID" =~ ^/subscriptions/[a-fA-F0-9-]+/[Rr]esource[Gg]roups/[^/]+/providers/[Mm]icrosoft\.[Oo]perational[Ii]nsights/[Ww]orkspaces/[^/]+$ ]]; then - echo "❌ ERROR: AZURE_ENV_LOG_ANALYTICS_WORKSPACE_RID is invalid. Must be a valid Azure Resource ID format:" + # Validate AZURE_ENV_EXISTING_LOG_ANALYTICS_WORKSPACE_RID (optional, Azure Resource ID format) + if [[ -n "$INPUT_AZURE_ENV_EXISTING_LOG_ANALYTICS_WORKSPACE_RID" ]]; then + if [[ ! "$INPUT_AZURE_ENV_EXISTING_LOG_ANALYTICS_WORKSPACE_RID" =~ ^/subscriptions/[a-fA-F0-9-]+/[Rr]esource[Gg]roups/[^/]+/providers/[Mm]icrosoft\.[Oo]perational[Ii]nsights/[Ww]orkspaces/[^/]+$ ]]; then + echo "❌ ERROR: AZURE_ENV_EXISTING_LOG_ANALYTICS_WORKSPACE_RID is invalid. Must be a valid Azure Resource ID format:" echo " /subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.OperationalInsights/workspaces/{workspaceName}" - echo " Got: '$INPUT_AZURE_ENV_LOG_ANALYTICS_WORKSPACE_RID'" + echo " Got: '$INPUT_AZURE_ENV_EXISTING_LOG_ANALYTICS_WORKSPACE_RID'" VALIDATION_FAILED=true else - echo "✅ AZURE_ENV_LOG_ANALYTICS_WORKSPACE_RID: Valid Resource ID format" + echo "✅ AZURE_ENV_EXISTING_LOG_ANALYTICS_WORKSPACE_RID: Valid Resource ID format" fi else - echo "✅ AZURE_ENV_LOG_ANALYTICS_WORKSPACE_RID: Not provided (optional)" + echo "✅ AZURE_ENV_EXISTING_LOG_ANALYTICS_WORKSPACE_RID: Not provided (optional)" fi - # Validate AZURE_ENV_FOUNDRY_PROJECT_RID (optional, Azure Resource ID format) - if [[ -n "$INPUT_AZURE_ENV_FOUNDRY_PROJECT_RID" ]]; then - if [[ ! "$INPUT_AZURE_ENV_FOUNDRY_PROJECT_RID" =~ ^/subscriptions/[a-fA-F0-9-]+/[Rr]esource[Gg]roups/[^/]+/providers/([Mm]icrosoft\.[Mm]achine[Ll]earning[Ss]ervices/([Ww]orkspaces|[Pp]rojects)/[^/]+|[Mm]icrosoft\.[Cc]ognitive[Ss]ervices/[Aa]ccounts/[^/]+/[Pp]rojects/[^/]+)$ ]]; then - echo "❌ ERROR: AZURE_ENV_FOUNDRY_PROJECT_RID is invalid. Must be a valid Azure Resource ID format:" + # Validate AZURE_EXISTING_AIPROJECT_RESOURCE_ID (optional, Azure Resource ID format) + if [[ -n "$INPUT_AZURE_EXISTING_AIPROJECT_RESOURCE_ID" ]]; then + if [[ ! "$INPUT_AZURE_EXISTING_AIPROJECT_RESOURCE_ID" =~ ^/subscriptions/[a-fA-F0-9-]+/[Rr]esource[Gg]roups/[^/]+/providers/([Mm]icrosoft\.[Mm]achine[Ll]earning[Ss]ervices/([Ww]orkspaces|[Pp]rojects)/[^/]+|[Mm]icrosoft\.[Cc]ognitive[Ss]ervices/[Aa]ccounts/[^/]+/[Pp]rojects/[^/]+)$ ]]; then + echo "❌ ERROR: AZURE_EXISTING_AIPROJECT_RESOURCE_ID is invalid. Must be a valid Azure Resource ID format:" echo " /subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.CognitiveServices/accounts/{accountName}/projects/{projectName}" - echo " Got: '$INPUT_AZURE_ENV_FOUNDRY_PROJECT_RID'" + echo " Got: '$INPUT_AZURE_EXISTING_AIPROJECT_RESOURCE_ID'" VALIDATION_FAILED=true else - echo "✅ AZURE_ENV_FOUNDRY_PROJECT_RID: Valid Resource ID format" + echo "✅ AZURE_EXISTING_AIPROJECT_RESOURCE_ID: Valid Resource ID format" fi else - echo "✅ AZURE_ENV_FOUNDRY_PROJECT_RID: Not provided (optional)" + echo "✅ AZURE_EXISTING_AIPROJECT_RESOURCE_ID: Not provided (optional)" fi # Validate existing_webapp_url (optional, must start with https) @@ -302,8 +302,8 @@ jobs: echo "build_docker_image=$BUILD_DOCKER" >> $GITHUB_OUTPUT echo "cleanup_resources=$CLEANUP_RESOURCES" >> $GITHUB_OUTPUT echo "run_e2e_tests=$TEST_OPTION" >> $GITHUB_OUTPUT - echo "AZURE_ENV_LOG_ANALYTICS_WORKSPACE_RID=$INPUT_AZURE_ENV_LOG_ANALYTICS_WORKSPACE_RID" >> $GITHUB_OUTPUT - echo "AZURE_ENV_FOUNDRY_PROJECT_RID=$INPUT_AZURE_ENV_FOUNDRY_PROJECT_RID" >> $GITHUB_OUTPUT + echo "AZURE_ENV_EXISTING_LOG_ANALYTICS_WORKSPACE_RID=$INPUT_AZURE_ENV_EXISTING_LOG_ANALYTICS_WORKSPACE_RID" >> $GITHUB_OUTPUT + echo "AZURE_EXISTING_AIPROJECT_RESOURCE_ID=$INPUT_AZURE_EXISTING_AIPROJECT_RESOURCE_ID" >> $GITHUB_OUTPUT echo "existing_webapp_url=$INPUT_EXISTING_WEBAPP_URL" >> $GITHUB_OUTPUT # Validate and output AZURE_ENV_IMAGE_MODEL_NAME @@ -329,8 +329,8 @@ jobs: build_docker_image: ${{ needs.validate-inputs.outputs.build_docker_image == 'true' }} cleanup_resources: ${{ needs.validate-inputs.outputs.cleanup_resources == 'true' }} run_e2e_tests: ${{ needs.validate-inputs.outputs.run_e2e_tests || 'GoldenPath-Testing' }} - AZURE_ENV_LOG_ANALYTICS_WORKSPACE_RID: ${{ needs.validate-inputs.outputs.AZURE_ENV_LOG_ANALYTICS_WORKSPACE_RID || '' }} - AZURE_ENV_FOUNDRY_PROJECT_RID: ${{ needs.validate-inputs.outputs.AZURE_ENV_FOUNDRY_PROJECT_RID || '' }} + AZURE_ENV_EXISTING_LOG_ANALYTICS_WORKSPACE_RID: ${{ needs.validate-inputs.outputs.AZURE_ENV_EXISTING_LOG_ANALYTICS_WORKSPACE_RID || '' }} + AZURE_EXISTING_AIPROJECT_RESOURCE_ID: ${{ needs.validate-inputs.outputs.AZURE_EXISTING_AIPROJECT_RESOURCE_ID || '' }} existing_webapp_url: ${{ needs.validate-inputs.outputs.existing_webapp_url || '' }} trigger_type: ${{ github.event_name }} AZURE_ENV_IMAGE_MODEL_NAME: ${{ needs.validate-inputs.outputs.AZURE_ENV_IMAGE_MODEL_NAME || 'gpt-image-1-mini' }} diff --git a/.github/workflows/job-deploy-linux.yml b/.github/workflows/job-deploy-linux.yml index a0bb3258a..fe8db94fe 100644 --- a/.github/workflows/job-deploy-linux.yml +++ b/.github/workflows/job-deploy-linux.yml @@ -28,10 +28,10 @@ on: required: false type: string default: 'false' - AZURE_ENV_LOG_ANALYTICS_WORKSPACE_RID: + AZURE_ENV_EXISTING_LOG_ANALYTICS_WORKSPACE_RID: required: false type: string - AZURE_ENV_FOUNDRY_PROJECT_RID: + AZURE_EXISTING_AIPROJECT_RESOURCE_ID: required: false type: string outputs: @@ -59,8 +59,8 @@ jobs: INPUT_BUILD_DOCKER_IMAGE: ${{ inputs.BUILD_DOCKER_IMAGE }} INPUT_EXP: ${{ inputs.EXP }} INPUT_WAF_ENABLED: ${{ inputs.WAF_ENABLED }} - INPUT_AZURE_ENV_LOG_ANALYTICS_WORKSPACE_RID: ${{ inputs.AZURE_ENV_LOG_ANALYTICS_WORKSPACE_RID }} - INPUT_AZURE_ENV_FOUNDRY_PROJECT_RID: ${{ inputs.AZURE_ENV_FOUNDRY_PROJECT_RID }} + INPUT_AZURE_ENV_EXISTING_LOG_ANALYTICS_WORKSPACE_RID: ${{ inputs.AZURE_ENV_EXISTING_LOG_ANALYTICS_WORKSPACE_RID }} + INPUT_AZURE_EXISTING_AIPROJECT_RESOURCE_ID: ${{ inputs.AZURE_EXISTING_AIPROJECT_RESOURCE_ID }} run: | echo "🔍 Validating workflow input parameters..." VALIDATION_FAILED=false @@ -147,27 +147,27 @@ jobs: echo "✅ WAF_ENABLED: '$INPUT_WAF_ENABLED' is valid" fi - # Validate AZURE_ENV_LOG_ANALYTICS_WORKSPACE_RID (optional, if provided must be valid Resource ID) - if [[ -n "$INPUT_AZURE_ENV_LOG_ANALYTICS_WORKSPACE_RID" ]]; then - if [[ ! "$INPUT_AZURE_ENV_LOG_ANALYTICS_WORKSPACE_RID" =~ ^/subscriptions/[a-fA-F0-9-]+/[Rr]esource[Gg]roups/[^/]+/providers/[Mm]icrosoft\.[Oo]perational[Ii]nsights/[Ww]orkspaces/[^/]+$ ]]; then - echo "❌ ERROR: AZURE_ENV_LOG_ANALYTICS_WORKSPACE_RID is invalid. Must be a valid Azure Resource ID format:" + # Validate AZURE_ENV_EXISTING_LOG_ANALYTICS_WORKSPACE_RID (optional, if provided must be valid Resource ID) + if [[ -n "$INPUT_AZURE_ENV_EXISTING_LOG_ANALYTICS_WORKSPACE_RID" ]]; then + if [[ ! "$INPUT_AZURE_ENV_EXISTING_LOG_ANALYTICS_WORKSPACE_RID" =~ ^/subscriptions/[a-fA-F0-9-]+/[Rr]esource[Gg]roups/[^/]+/providers/[Mm]icrosoft\.[Oo]perational[Ii]nsights/[Ww]orkspaces/[^/]+$ ]]; then + echo "❌ ERROR: AZURE_ENV_EXISTING_LOG_ANALYTICS_WORKSPACE_RID is invalid. Must be a valid Azure Resource ID format:" echo " /subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.OperationalInsights/workspaces/{workspaceName}" - echo " Got: '$INPUT_AZURE_ENV_LOG_ANALYTICS_WORKSPACE_RID'" + echo " Got: '$INPUT_AZURE_ENV_EXISTING_LOG_ANALYTICS_WORKSPACE_RID'" VALIDATION_FAILED=true else - echo "✅ AZURE_ENV_LOG_ANALYTICS_WORKSPACE_RID: Valid Resource ID format" + echo "✅ AZURE_ENV_EXISTING_LOG_ANALYTICS_WORKSPACE_RID: Valid Resource ID format" fi fi - # Validate AZURE_ENV_FOUNDRY_PROJECT_RID (optional, if provided must be valid Resource ID) - if [[ -n "$INPUT_AZURE_ENV_FOUNDRY_PROJECT_RID" ]]; then - if [[ ! "$INPUT_AZURE_ENV_FOUNDRY_PROJECT_RID" =~ ^/subscriptions/[a-fA-F0-9-]+/[Rr]esource[Gg]roups/[^/]+/providers/([Mm]icrosoft\.[Mm]achine[Ll]earning[Ss]ervices/([Ww]orkspaces|[Pp]rojects)/[^/]+|[Mm]icrosoft\.[Cc]ognitive[Ss]ervices/[Aa]ccounts/[^/]+/[Pp]rojects/[^/]+)$ ]]; then - echo "❌ ERROR: AZURE_ENV_FOUNDRY_PROJECT_RID is invalid. Must be a valid Azure Resource ID format:" + # Validate AZURE_EXISTING_AIPROJECT_RESOURCE_ID (optional, if provided must be valid Resource ID) + if [[ -n "$INPUT_AZURE_EXISTING_AIPROJECT_RESOURCE_ID" ]]; then + if [[ ! "$INPUT_AZURE_EXISTING_AIPROJECT_RESOURCE_ID" =~ ^/subscriptions/[a-fA-F0-9-]+/[Rr]esource[Gg]roups/[^/]+/providers/([Mm]icrosoft\.[Mm]achine[Ll]earning[Ss]ervices/([Ww]orkspaces|[Pp]rojects)/[^/]+|[Mm]icrosoft\.[Cc]ognitive[Ss]ervices/[Aa]ccounts/[^/]+/[Pp]rojects/[^/]+)$ ]]; then + echo "❌ ERROR: AZURE_EXISTING_AIPROJECT_RESOURCE_ID is invalid. Must be a valid Azure Resource ID format:" echo " /subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.CognitiveServices/accounts/{accountName}/projects/{projectName}" - echo " Got: '$INPUT_AZURE_ENV_FOUNDRY_PROJECT_RID'" + echo " Got: '$INPUT_AZURE_EXISTING_AIPROJECT_RESOURCE_ID'" VALIDATION_FAILED=true else - echo "✅ AZURE_ENV_FOUNDRY_PROJECT_RID: Valid Resource ID format" + echo "✅ AZURE_EXISTING_AIPROJECT_RESOURCE_ID: Valid Resource ID format" fi fi @@ -221,8 +221,8 @@ jobs: AZURE_ENV_IMAGE_TAG: ${{ inputs.AZURE_ENV_IMAGE_TAG }} BUILD_DOCKER_IMAGE: ${{ inputs.BUILD_DOCKER_IMAGE }} EXP: ${{ inputs.EXP }} - INPUT_AZURE_ENV_LOG_ANALYTICS_WORKSPACE_RID: ${{ inputs.AZURE_ENV_LOG_ANALYTICS_WORKSPACE_RID }} - INPUT_AZURE_ENV_FOUNDRY_PROJECT_RID: ${{ inputs.AZURE_ENV_FOUNDRY_PROJECT_RID }} + INPUT_AZURE_ENV_EXISTING_LOG_ANALYTICS_WORKSPACE_RID: ${{ inputs.AZURE_ENV_EXISTING_LOG_ANALYTICS_WORKSPACE_RID }} + INPUT_AZURE_EXISTING_AIPROJECT_RESOURCE_ID: ${{ inputs.AZURE_EXISTING_AIPROJECT_RESOURCE_ID }} run: | set -e @@ -254,25 +254,25 @@ jobs: echo "✅ EXP ENABLED - Setting EXP parameters..." # Set EXP variables dynamically - if [[ -n "$INPUT_AZURE_ENV_LOG_ANALYTICS_WORKSPACE_RID" ]]; then - EXP_LOG_ANALYTICS_ID="$INPUT_AZURE_ENV_LOG_ANALYTICS_WORKSPACE_RID" + if [[ -n "$INPUT_AZURE_ENV_EXISTING_LOG_ANALYTICS_WORKSPACE_RID" ]]; then + EXP_LOG_ANALYTICS_ID="$INPUT_AZURE_ENV_EXISTING_LOG_ANALYTICS_WORKSPACE_RID" else - EXP_LOG_ANALYTICS_ID="${{ secrets.AZURE_ENV_LOG_ANALYTICS_WORKSPACE_RID }}" + EXP_LOG_ANALYTICS_ID="${{ secrets.AZURE_ENV_EXISTING_LOG_ANALYTICS_WORKSPACE_RID }}" fi - if [[ -n "$INPUT_AZURE_ENV_FOUNDRY_PROJECT_RID" ]]; then - EXP_AI_PROJECT_ID="$INPUT_AZURE_ENV_FOUNDRY_PROJECT_RID" + if [[ -n "$INPUT_AZURE_EXISTING_AIPROJECT_RESOURCE_ID" ]]; then + EXP_AI_PROJECT_ID="$INPUT_AZURE_EXISTING_AIPROJECT_RESOURCE_ID" else - EXP_AI_PROJECT_ID="${{ secrets.AZURE_ENV_FOUNDRY_PROJECT_RID }}" + EXP_AI_PROJECT_ID="${{ secrets.AZURE_EXISTING_AIPROJECT_RESOURCE_ID }}" fi - echo "AZURE_ENV_LOG_ANALYTICS_WORKSPACE_RID: $EXP_LOG_ANALYTICS_ID" - echo "AZURE_ENV_FOUNDRY_PROJECT_RID: $EXP_AI_PROJECT_ID" - azd env set AZURE_ENV_LOG_ANALYTICS_WORKSPACE_RID="$EXP_LOG_ANALYTICS_ID" - azd env set AZURE_ENV_FOUNDRY_PROJECT_RID="$EXP_AI_PROJECT_ID" + echo "AZURE_ENV_EXISTING_LOG_ANALYTICS_WORKSPACE_RID: $EXP_LOG_ANALYTICS_ID" + echo "AZURE_EXISTING_AIPROJECT_RESOURCE_ID: $EXP_AI_PROJECT_ID" + azd env set AZURE_ENV_EXISTING_LOG_ANALYTICS_WORKSPACE_RID="$EXP_LOG_ANALYTICS_ID" + azd env set AZURE_EXISTING_AIPROJECT_RESOURCE_ID="$EXP_AI_PROJECT_ID" else echo "❌ EXP DISABLED - Skipping EXP parameters" - if [[ -n "$INPUT_AZURE_ENV_LOG_ANALYTICS_WORKSPACE_RID" ]] || [[ -n "$INPUT_AZURE_ENV_FOUNDRY_PROJECT_RID" ]]; then + if [[ -n "$INPUT_AZURE_ENV_EXISTING_LOG_ANALYTICS_WORKSPACE_RID" ]] || [[ -n "$INPUT_AZURE_EXISTING_AIPROJECT_RESOURCE_ID" ]]; then echo "⚠️ Warning: EXP parameter values provided but EXP is disabled. These values will be ignored." fi fi diff --git a/.github/workflows/job-deploy-windows.yml b/.github/workflows/job-deploy-windows.yml index e42320bb7..0fcb88467 100644 --- a/.github/workflows/job-deploy-windows.yml +++ b/.github/workflows/job-deploy-windows.yml @@ -28,10 +28,10 @@ on: required: false type: string default: 'false' - AZURE_ENV_LOG_ANALYTICS_WORKSPACE_RID: + AZURE_ENV_EXISTING_LOG_ANALYTICS_WORKSPACE_RID: required: false type: string - AZURE_ENV_FOUNDRY_PROJECT_RID: + AZURE_EXISTING_AIPROJECT_RESOURCE_ID: required: false type: string outputs: @@ -60,8 +60,8 @@ jobs: INPUT_BUILD_DOCKER_IMAGE: ${{ inputs.BUILD_DOCKER_IMAGE }} INPUT_EXP: ${{ inputs.EXP }} INPUT_WAF_ENABLED: ${{ inputs.WAF_ENABLED }} - INPUT_AZURE_ENV_LOG_ANALYTICS_WORKSPACE_RID: ${{ inputs.AZURE_ENV_LOG_ANALYTICS_WORKSPACE_RID }} - INPUT_AZURE_ENV_FOUNDRY_PROJECT_RID: ${{ inputs.AZURE_ENV_FOUNDRY_PROJECT_RID }} + INPUT_AZURE_ENV_EXISTING_LOG_ANALYTICS_WORKSPACE_RID: ${{ inputs.AZURE_ENV_EXISTING_LOG_ANALYTICS_WORKSPACE_RID }} + INPUT_AZURE_EXISTING_AIPROJECT_RESOURCE_ID: ${{ inputs.AZURE_EXISTING_AIPROJECT_RESOURCE_ID }} run: | echo "🔍 Validating workflow input parameters..." VALIDATION_FAILED=false @@ -148,27 +148,27 @@ jobs: echo "✅ WAF_ENABLED: '$INPUT_WAF_ENABLED' is valid" fi - # Validate AZURE_ENV_LOG_ANALYTICS_WORKSPACE_RID (optional, if provided must be valid Resource ID) - if [[ -n "$INPUT_AZURE_ENV_LOG_ANALYTICS_WORKSPACE_RID" ]]; then - if [[ ! "$INPUT_AZURE_ENV_LOG_ANALYTICS_WORKSPACE_RID" =~ ^/subscriptions/[a-fA-F0-9-]+/[Rr]esource[Gg]roups/[^/]+/providers/[Mm]icrosoft\.[Oo]perational[Ii]nsights/[Ww]orkspaces/[^/]+$ ]]; then - echo "❌ ERROR: AZURE_ENV_LOG_ANALYTICS_WORKSPACE_RID is invalid. Must be a valid Azure Resource ID format:" + # Validate AZURE_ENV_EXISTING_LOG_ANALYTICS_WORKSPACE_RID (optional, if provided must be valid Resource ID) + if [[ -n "$INPUT_AZURE_ENV_EXISTING_LOG_ANALYTICS_WORKSPACE_RID" ]]; then + if [[ ! "$INPUT_AZURE_ENV_EXISTING_LOG_ANALYTICS_WORKSPACE_RID" =~ ^/subscriptions/[a-fA-F0-9-]+/[Rr]esource[Gg]roups/[^/]+/providers/[Mm]icrosoft\.[Oo]perational[Ii]nsights/[Ww]orkspaces/[^/]+$ ]]; then + echo "❌ ERROR: AZURE_ENV_EXISTING_LOG_ANALYTICS_WORKSPACE_RID is invalid. Must be a valid Azure Resource ID format:" echo " /subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.OperationalInsights/workspaces/{workspaceName}" - echo " Got: '$INPUT_AZURE_ENV_LOG_ANALYTICS_WORKSPACE_RID'" + echo " Got: '$INPUT_AZURE_ENV_EXISTING_LOG_ANALYTICS_WORKSPACE_RID'" VALIDATION_FAILED=true else - echo "✅ AZURE_ENV_LOG_ANALYTICS_WORKSPACE_RID: Valid Resource ID format" + echo "✅ AZURE_ENV_EXISTING_LOG_ANALYTICS_WORKSPACE_RID: Valid Resource ID format" fi fi - # Validate AZURE_ENV_FOUNDRY_PROJECT_RID (optional, if provided must be valid Resource ID) - if [[ -n "$INPUT_AZURE_ENV_FOUNDRY_PROJECT_RID" ]]; then - if [[ ! "$INPUT_AZURE_ENV_FOUNDRY_PROJECT_RID" =~ ^/subscriptions/[a-fA-F0-9-]+/[Rr]esource[Gg]roups/[^/]+/providers/([Mm]icrosoft\.[Mm]achine[Ll]earning[Ss]ervices/([Ww]orkspaces|[Pp]rojects)/[^/]+|[Mm]icrosoft\.[Cc]ognitive[Ss]ervices/[Aa]ccounts/[^/]+/[Pp]rojects/[^/]+)$ ]]; then - echo "❌ ERROR: AZURE_ENV_FOUNDRY_PROJECT_RID is invalid. Must be a valid Azure Resource ID format:" + # Validate AZURE_EXISTING_AIPROJECT_RESOURCE_ID (optional, if provided must be valid Resource ID) + if [[ -n "$INPUT_AZURE_EXISTING_AIPROJECT_RESOURCE_ID" ]]; then + if [[ ! "$INPUT_AZURE_EXISTING_AIPROJECT_RESOURCE_ID" =~ ^/subscriptions/[a-fA-F0-9-]+/[Rr]esource[Gg]roups/[^/]+/providers/([Mm]icrosoft\.[Mm]achine[Ll]earning[Ss]ervices/([Ww]orkspaces|[Pp]rojects)/[^/]+|[Mm]icrosoft\.[Cc]ognitive[Ss]ervices/[Aa]ccounts/[^/]+/[Pp]rojects/[^/]+)$ ]]; then + echo "❌ ERROR: AZURE_EXISTING_AIPROJECT_RESOURCE_ID is invalid. Must be a valid Azure Resource ID format:" echo " /subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.CognitiveServices/accounts/{accountName}/projects/{projectName}" - echo " Got: '$INPUT_AZURE_ENV_FOUNDRY_PROJECT_RID'" + echo " Got: '$INPUT_AZURE_EXISTING_AIPROJECT_RESOURCE_ID'" VALIDATION_FAILED=true else - echo "✅ AZURE_ENV_FOUNDRY_PROJECT_RID: Valid Resource ID format" + echo "✅ AZURE_EXISTING_AIPROJECT_RESOURCE_ID: Valid Resource ID format" fi fi @@ -224,8 +224,8 @@ jobs: AZURE_ENV_IMAGE_TAG: ${{ inputs.AZURE_ENV_IMAGE_TAG }} BUILD_DOCKER_IMAGE: ${{ inputs.BUILD_DOCKER_IMAGE }} EXP: ${{ inputs.EXP }} - INPUT_AZURE_ENV_LOG_ANALYTICS_WORKSPACE_RID: ${{ inputs.AZURE_ENV_LOG_ANALYTICS_WORKSPACE_RID }} - INPUT_AZURE_ENV_FOUNDRY_PROJECT_RID: ${{ inputs.AZURE_ENV_FOUNDRY_PROJECT_RID }} + INPUT_AZURE_ENV_EXISTING_LOG_ANALYTICS_WORKSPACE_RID: ${{ inputs.AZURE_ENV_EXISTING_LOG_ANALYTICS_WORKSPACE_RID }} + INPUT_AZURE_EXISTING_AIPROJECT_RESOURCE_ID: ${{ inputs.AZURE_EXISTING_AIPROJECT_RESOURCE_ID }} run: | $ErrorActionPreference = "Stop" @@ -257,22 +257,22 @@ jobs: Write-Host "✅ EXP ENABLED - Setting EXP parameters..." # Set EXP variables dynamically - if ($env:INPUT_AZURE_ENV_LOG_ANALYTICS_WORKSPACE_RID -ne "") { - $EXP_LOG_ANALYTICS_ID = $env:INPUT_AZURE_ENV_LOG_ANALYTICS_WORKSPACE_RID + if ($env:INPUT_AZURE_ENV_EXISTING_LOG_ANALYTICS_WORKSPACE_RID -ne "") { + $EXP_LOG_ANALYTICS_ID = $env:INPUT_AZURE_ENV_EXISTING_LOG_ANALYTICS_WORKSPACE_RID } else { - $EXP_LOG_ANALYTICS_ID = "${{ secrets.AZURE_ENV_LOG_ANALYTICS_WORKSPACE_RID }}" + $EXP_LOG_ANALYTICS_ID = "${{ secrets.AZURE_ENV_EXISTING_LOG_ANALYTICS_WORKSPACE_RID }}" } - if ($env:INPUT_AZURE_ENV_FOUNDRY_PROJECT_RID -ne "") { - $EXP_AI_PROJECT_ID = $env:INPUT_AZURE_ENV_FOUNDRY_PROJECT_RID + if ($env:INPUT_AZURE_EXISTING_AIPROJECT_RESOURCE_ID -ne "") { + $EXP_AI_PROJECT_ID = $env:INPUT_AZURE_EXISTING_AIPROJECT_RESOURCE_ID } else { - $EXP_AI_PROJECT_ID = "${{ secrets.AZURE_ENV_FOUNDRY_PROJECT_RID }}" + $EXP_AI_PROJECT_ID = "${{ secrets.AZURE_EXISTING_AIPROJECT_RESOURCE_ID }}" } - Write-Host "AZURE_ENV_LOG_ANALYTICS_WORKSPACE_RID: $EXP_LOG_ANALYTICS_ID" - Write-Host "AZURE_ENV_FOUNDRY_PROJECT_RID: $EXP_AI_PROJECT_ID" - azd env set AZURE_ENV_LOG_ANALYTICS_WORKSPACE_RID="$EXP_LOG_ANALYTICS_ID" - azd env set AZURE_ENV_FOUNDRY_PROJECT_RID="$EXP_AI_PROJECT_ID" + Write-Host "AZURE_ENV_EXISTING_LOG_ANALYTICS_WORKSPACE_RID: $EXP_LOG_ANALYTICS_ID" + Write-Host "AZURE_EXISTING_AIPROJECT_RESOURCE_ID: $EXP_AI_PROJECT_ID" + azd env set AZURE_ENV_EXISTING_LOG_ANALYTICS_WORKSPACE_RID="$EXP_LOG_ANALYTICS_ID" + azd env set AZURE_EXISTING_AIPROJECT_RESOURCE_ID="$EXP_AI_PROJECT_ID" } else { Write-Host "❌ EXP DISABLED - Skipping EXP parameters" } diff --git a/.github/workflows/job-deploy.yml b/.github/workflows/job-deploy.yml index 37f771d19..4adbc36bc 100644 --- a/.github/workflows/job-deploy.yml +++ b/.github/workflows/job-deploy.yml @@ -51,12 +51,12 @@ on: required: false default: '' type: string - AZURE_ENV_LOG_ANALYTICS_WORKSPACE_RID: + AZURE_ENV_EXISTING_LOG_ANALYTICS_WORKSPACE_RID: description: 'Log Analytics Workspace ID (Optional)' required: false default: '' type: string - AZURE_ENV_FOUNDRY_PROJECT_RID: + AZURE_EXISTING_AIPROJECT_RESOURCE_ID: description: 'AI Project Resource ID (Optional)' required: false default: '' @@ -133,8 +133,8 @@ jobs: INPUT_EXP: ${{ inputs.EXP }} INPUT_CLEANUP_RESOURCES: ${{ inputs.cleanup_resources }} INPUT_RUN_E2E_TESTS: ${{ inputs.run_e2e_tests }} - INPUT_AZURE_ENV_LOG_ANALYTICS_WORKSPACE_RID: ${{ inputs.AZURE_ENV_LOG_ANALYTICS_WORKSPACE_RID }} - INPUT_AZURE_ENV_FOUNDRY_PROJECT_RID: ${{ inputs.AZURE_ENV_FOUNDRY_PROJECT_RID }} + INPUT_AZURE_ENV_EXISTING_LOG_ANALYTICS_WORKSPACE_RID: ${{ inputs.AZURE_ENV_EXISTING_LOG_ANALYTICS_WORKSPACE_RID }} + INPUT_AZURE_EXISTING_AIPROJECT_RESOURCE_ID: ${{ inputs.AZURE_EXISTING_AIPROJECT_RESOURCE_ID }} INPUT_EXISTING_WEBAPP_URL: ${{ inputs.existing_webapp_url }} INPUT_DOCKER_IMAGE_TAG: ${{ inputs.docker_image_tag }} run: | @@ -230,27 +230,27 @@ jobs: fi fi - # Validate AZURE_ENV_LOG_ANALYTICS_WORKSPACE_RID (Azure Resource ID format) - if [[ -n "$INPUT_AZURE_ENV_LOG_ANALYTICS_WORKSPACE_RID" ]]; then - if [[ ! "$INPUT_AZURE_ENV_LOG_ANALYTICS_WORKSPACE_RID" =~ ^/subscriptions/[a-fA-F0-9-]+/[Rr]esource[Gg]roups/[^/]+/providers/[Mm]icrosoft\.[Oo]perational[Ii]nsights/[Ww]orkspaces/[^/]+$ ]]; then - echo "❌ ERROR: AZURE_ENV_LOG_ANALYTICS_WORKSPACE_RID is invalid. Must be a valid Azure Resource ID format:" + # Validate AZURE_ENV_EXISTING_LOG_ANALYTICS_WORKSPACE_RID (Azure Resource ID format) + if [[ -n "$INPUT_AZURE_ENV_EXISTING_LOG_ANALYTICS_WORKSPACE_RID" ]]; then + if [[ ! "$INPUT_AZURE_ENV_EXISTING_LOG_ANALYTICS_WORKSPACE_RID" =~ ^/subscriptions/[a-fA-F0-9-]+/[Rr]esource[Gg]roups/[^/]+/providers/[Mm]icrosoft\.[Oo]perational[Ii]nsights/[Ww]orkspaces/[^/]+$ ]]; then + echo "❌ ERROR: AZURE_ENV_EXISTING_LOG_ANALYTICS_WORKSPACE_RID is invalid. Must be a valid Azure Resource ID format:" echo " /subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.OperationalInsights/workspaces/{workspaceName}" - echo " Got: '$INPUT_AZURE_ENV_LOG_ANALYTICS_WORKSPACE_RID'" + echo " Got: '$INPUT_AZURE_ENV_EXISTING_LOG_ANALYTICS_WORKSPACE_RID'" VALIDATION_FAILED=true else - echo "✅ AZURE_ENV_LOG_ANALYTICS_WORKSPACE_RID: Valid Resource ID format" + echo "✅ AZURE_ENV_EXISTING_LOG_ANALYTICS_WORKSPACE_RID: Valid Resource ID format" fi fi - # Validate AZURE_ENV_FOUNDRY_PROJECT_RID (Azure Resource ID format) - if [[ -n "$INPUT_AZURE_ENV_FOUNDRY_PROJECT_RID" ]]; then - if [[ ! "$INPUT_AZURE_ENV_FOUNDRY_PROJECT_RID" =~ ^/subscriptions/[a-fA-F0-9-]+/[Rr]esource[Gg]roups/[^/]+/providers/([Mm]icrosoft\.[Mm]achine[Ll]earning[Ss]ervices/([Ww]orkspaces|[Pp]rojects)/[^/]+|[Mm]icrosoft\.[Cc]ognitive[Ss]ervices/[Aa]ccounts/[^/]+/[Pp]rojects/[^/]+)$ ]]; then - echo "❌ ERROR: AZURE_ENV_FOUNDRY_PROJECT_RID is invalid. Must be a valid Azure Resource ID format:" + # Validate AZURE_EXISTING_AIPROJECT_RESOURCE_ID (Azure Resource ID format) + if [[ -n "$INPUT_AZURE_EXISTING_AIPROJECT_RESOURCE_ID" ]]; then + if [[ ! "$INPUT_AZURE_EXISTING_AIPROJECT_RESOURCE_ID" =~ ^/subscriptions/[a-fA-F0-9-]+/[Rr]esource[Gg]roups/[^/]+/providers/([Mm]icrosoft\.[Mm]achine[Ll]earning[Ss]ervices/([Ww]orkspaces|[Pp]rojects)/[^/]+|[Mm]icrosoft\.[Cc]ognitive[Ss]ervices/[Aa]ccounts/[^/]+/[Pp]rojects/[^/]+)$ ]]; then + echo "❌ ERROR: AZURE_EXISTING_AIPROJECT_RESOURCE_ID is invalid. Must be a valid Azure Resource ID format:" echo " /subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.CognitiveServices/accounts/{accountName}/projects/{projectName}" - echo " Got: '$INPUT_AZURE_ENV_FOUNDRY_PROJECT_RID'" + echo " Got: '$INPUT_AZURE_EXISTING_AIPROJECT_RESOURCE_ID'" VALIDATION_FAILED=true else - echo "✅ AZURE_ENV_FOUNDRY_PROJECT_RID: Valid Resource ID format" + echo "✅ AZURE_EXISTING_AIPROJECT_RESOURCE_ID: Valid Resource ID format" fi fi @@ -294,8 +294,8 @@ jobs: shell: bash env: INPUT_EXP: ${{ inputs.EXP }} - INPUT_LOG_ANALYTICS_WORKSPACE_ID: ${{ inputs.AZURE_ENV_LOG_ANALYTICS_WORKSPACE_RID }} - INPUT_AI_PROJECT_RESOURCE_ID: ${{ inputs.AZURE_ENV_FOUNDRY_PROJECT_RID }} + INPUT_LOG_ANALYTICS_WORKSPACE_ID: ${{ inputs.AZURE_ENV_EXISTING_LOG_ANALYTICS_WORKSPACE_RID }} + INPUT_AI_PROJECT_RESOURCE_ID: ${{ inputs.AZURE_EXISTING_AIPROJECT_RESOURCE_ID }} run: | echo "🔍 Validating EXP configuration..." @@ -653,8 +653,8 @@ jobs: BUILD_DOCKER_IMAGE: ${{ inputs.build_docker_image || 'false' }} EXP: ${{ needs.azure-setup.outputs.EXP_ENABLED }} WAF_ENABLED: ${{ inputs.waf_enabled == true && 'true' || 'false' }} - AZURE_ENV_LOG_ANALYTICS_WORKSPACE_RID: ${{ inputs.AZURE_ENV_LOG_ANALYTICS_WORKSPACE_RID }} - AZURE_ENV_FOUNDRY_PROJECT_RID: ${{ inputs.AZURE_ENV_FOUNDRY_PROJECT_RID }} + AZURE_ENV_EXISTING_LOG_ANALYTICS_WORKSPACE_RID: ${{ inputs.AZURE_ENV_EXISTING_LOG_ANALYTICS_WORKSPACE_RID }} + AZURE_EXISTING_AIPROJECT_RESOURCE_ID: ${{ inputs.AZURE_EXISTING_AIPROJECT_RESOURCE_ID }} secrets: inherit deploy-windows: @@ -671,6 +671,6 @@ jobs: BUILD_DOCKER_IMAGE: ${{ inputs.build_docker_image || 'false' }} EXP: ${{ needs.azure-setup.outputs.EXP_ENABLED }} WAF_ENABLED: ${{ inputs.waf_enabled == true && 'true' || 'false' }} - AZURE_ENV_LOG_ANALYTICS_WORKSPACE_RID: ${{ inputs.AZURE_ENV_LOG_ANALYTICS_WORKSPACE_RID }} - AZURE_ENV_FOUNDRY_PROJECT_RID: ${{ inputs.AZURE_ENV_FOUNDRY_PROJECT_RID }} + AZURE_ENV_EXISTING_LOG_ANALYTICS_WORKSPACE_RID: ${{ inputs.AZURE_ENV_EXISTING_LOG_ANALYTICS_WORKSPACE_RID }} + AZURE_EXISTING_AIPROJECT_RESOURCE_ID: ${{ inputs.AZURE_EXISTING_AIPROJECT_RESOURCE_ID }} secrets: inherit diff --git a/azure_custom.yaml b/azure_custom.yaml index a79c694e9..26bdf8f47 100644 --- a/azure_custom.yaml +++ b/azure_custom.yaml @@ -105,7 +105,7 @@ hooks: --template-file ./infra/main.bicep ` --parameters solutionName=$env:AZURE_ENV_NAME ` --parameters location=$env:AZURE_LOCATION ` - --parameters azureAiServiceLocation=$env:AZURE_ENV_OPENAI_LOCATION ` + --parameters azureAiServiceLocation=$env:AZURE_ENV_AI_SERVICE_LOCATION ` --parameters imageTag=latest ` --query "properties.outputs" -o json | Out-Null @@ -172,7 +172,7 @@ hooks: --template-file ./infra/main.bicep \ --parameters solutionName="$AZURE_ENV_NAME" \ --parameters location="$AZURE_LOCATION" \ - --parameters azureAiServiceLocation="$AZURE_ENV_OPENAI_LOCATION" \ + --parameters azureAiServiceLocation="$AZURE_ENV_AI_SERVICE_LOCATION" \ --parameters imageTag=latest \ --query "properties.outputs" -o json > /dev/null; then # Refresh azd env with new outputs diff --git a/docs/AZD_DEPLOYMENT.md b/docs/AZD_DEPLOYMENT.md index a8538e973..342366a99 100644 --- a/docs/AZD_DEPLOYMENT.md +++ b/docs/AZD_DEPLOYMENT.md @@ -124,14 +124,14 @@ This single command will: ```bash # Set the resource ID of your existing AI Project -azd env set AZURE_ENV_FOUNDRY_PROJECT_RID "/subscriptions//resourceGroups//providers/Microsoft.MachineLearningServices/workspaces/" +azd env set AZURE_EXISTING_AIPROJECT_RESOURCE_ID "/subscriptions//resourceGroups//providers/Microsoft.MachineLearningServices/workspaces/" ``` ### Reuse Existing Log Analytics Workspace ```bash # Set the resource ID of your existing Log Analytics workspace -azd env set AZURE_ENV_LOG_ANALYTICS_WORKSPACE_RID "/subscriptions//resourceGroups//providers/Microsoft.OperationalInsights/workspaces/" +azd env set AZURE_ENV_EXISTING_LOG_ANALYTICS_WORKSPACE_RID "/subscriptions//resourceGroups//providers/Microsoft.OperationalInsights/workspaces/" ``` ## Post-Deployment diff --git a/docs/CustomizingAzdParameters.md b/docs/CustomizingAzdParameters.md index 29dcc6d2b..3542f8c45 100644 --- a/docs/CustomizingAzdParameters.md +++ b/docs/CustomizingAzdParameters.md @@ -19,8 +19,8 @@ By default this template will use the environment name as the prefix to prevent | `AZURE_ENV_IMAGE_MODEL_CAPACITY` | integer | `1` | Sets the image model deployment capacity in RPM (minimum: `1`). | | `AZURE_ENV_OPENAI_API_VERSION` | string | `2025-01-01-preview` | Specifies the API version for Azure OpenAI service. | | `AZURE_ENV_AI_SERVICE_LOCATION` | string | `` | Sets the Azure region for OpenAI resource deployment. Allowed: `australiaeast`, `canadaeast`, `eastus2`, `japaneast`, `koreacentral`, `polandcentral`, `swedencentral`, `switzerlandnorth`, `uaenorth`, `uksouth`, `westus3`. | -| `AZURE_ENV_LOG_ANALYTICS_WORKSPACE_RID` | string | `""` | Reuses an existing Log Analytics Workspace instead of creating a new one. | -| `AZURE_ENV_FOUNDRY_PROJECT_RID` | string | `""` | Reuses an existing AI Foundry Project instead of creating a new one. | +| `AZURE_ENV_EXISTING_LOG_ANALYTICS_WORKSPACE_RID` | string | `""` | Reuses an existing Log Analytics Workspace instead of creating a new one. | +| `AZURE_EXISTING_AIPROJECT_RESOURCE_ID` | string | `""` | Reuses an existing AI Foundry Project instead of creating a new one. | | `enableMonitoring` | boolean | `false` | Enable Log Analytics and Application Insights (WAF-aligned). | | `enableScalability` | boolean | `false` | Enable auto-scaling and higher SKUs (WAF-aligned). | | `enableRedundancy` | boolean | `false` | Enable zone redundancy and geo-replication (WAF-aligned). | diff --git a/infra/main.bicep b/infra/main.bicep index c7dc5a6f0..de5c3be3c 100644 --- a/infra/main.bicep +++ b/infra/main.bicep @@ -1069,7 +1069,7 @@ output AI_FOUNDRY_RG_NAME string = aiFoundryAiServicesResourceGroupName output AI_FOUNDRY_RESOURCE_ID string = useExistingAiFoundryAiProject ? '' : aiFoundryAiServices!.outputs.resourceId @description('Contains existing AI project resource ID.') -output AZURE_ENV_FOUNDRY_PROJECT_RID string = azureExistingAIProjectResourceId +output AZURE_EXISTING_AIPROJECT_RESOURCE_ID string = azureExistingAIProjectResourceId @description('Contains AI Search Service Endpoint URL') output AZURE_AI_SEARCH_ENDPOINT string = 'https://${aiSearch.outputs.name}.search.windows.net/' diff --git a/infra/main.json b/infra/main.json index 4670ffccf..b88c84b66 100644 --- a/infra/main.json +++ b/infra/main.json @@ -6,7 +6,7 @@ "_generator": { "name": "bicep", "version": "0.42.1.51946", - "templateHash": "2787206892264299441" + "templateHash": "15695041727004400845" }, "name": "Intelligent Content Generation Accelerator", "description": "Solution Accelerator for multimodal marketing content generation using Microsoft Agent Framework.\n" @@ -44453,7 +44453,7 @@ }, "value": "[if(variables('useExistingAiFoundryAiProject'), '', reference('aiFoundryAiServices').outputs.resourceId.value)]" }, - "AZURE_ENV_FOUNDRY_PROJECT_RID": { + "AZURE_EXISTING_AIPROJECT_RESOURCE_ID": { "type": "string", "metadata": { "description": "Contains existing AI project resource ID." diff --git a/infra/main.parameters.json b/infra/main.parameters.json index e2a85f1c5..2ad8b6e30 100644 --- a/infra/main.parameters.json +++ b/infra/main.parameters.json @@ -36,16 +36,19 @@ "value": "${AZURE_ENV_AI_SERVICE_LOCATION}" }, "existingLogAnalyticsWorkspaceId": { - "value": "${AZURE_ENV_LOG_ANALYTICS_WORKSPACE_RID}" + "value": "${AZURE_ENV_EXISTING_LOG_ANALYTICS_WORKSPACE_RID}" }, "azureExistingAIProjectResourceId": { - "value": "${AZURE_ENV_FOUNDRY_PROJECT_RID}" + "value": "${AZURE_EXISTING_AIPROJECT_RESOURCE_ID}" }, "acrName": { "value": "${AZURE_ENV_CONTAINER_REGISTRY_NAME}" }, "imageTag": { "value": "${AZURE_ENV_IMAGE_TAG=latest}" + }, + "enableTelemetry": { + "value": "${AZURE_ENV_ENABLE_TELEMETRY}" } } } diff --git a/infra/main.waf.parameters.json b/infra/main.waf.parameters.json index 2b43b5c50..119f62f9d 100644 --- a/infra/main.waf.parameters.json +++ b/infra/main.waf.parameters.json @@ -36,10 +36,10 @@ "value": "${AZURE_ENV_AI_SERVICE_LOCATION}" }, "existingLogAnalyticsWorkspaceId": { - "value": "${AZURE_ENV_LOG_ANALYTICS_WORKSPACE_RID}" + "value": "${AZURE_ENV_EXISTING_LOG_ANALYTICS_WORKSPACE_RID}" }, "azureExistingAIProjectResourceId": { - "value": "${AZURE_ENV_FOUNDRY_PROJECT_RID}" + "value": "${AZURE_EXISTING_AIPROJECT_RESOURCE_ID}" }, "acrName": { "value": "${AZURE_ENV_CONTAINER_REGISTRY_NAME}" @@ -47,6 +47,9 @@ "imageTag": { "value": "${AZURE_ENV_IMAGE_TAG=latest}" }, + "enableTelemetry": { + "value": "${AZURE_ENV_ENABLE_TELEMETRY}" + }, "enablePrivateNetworking": { "value": true }, diff --git a/infra/main_custom.bicep b/infra/main_custom.bicep index 55f0d9f88..da54978f3 100644 --- a/infra/main_custom.bicep +++ b/infra/main_custom.bicep @@ -1209,7 +1209,7 @@ output AZURE_AI_AGENT_API_VERSION string = azureAiAgentApiVersion output AZURE_APPLICATION_INSIGHTS_CONNECTION_STRING string = (enableMonitoring && !useExistingLogAnalytics) ? applicationInsights!.outputs.connectionString : '' @description('Contains the location used for AI Services deployment') -output AZURE_ENV_OPENAI_LOCATION string = azureAiServiceLocation +output AZURE_ENV_AI_SERVICE_LOCATION string = azureAiServiceLocation @description('Contains Container Instance Name') output CONTAINER_INSTANCE_NAME string = shouldDeployACI ? containerInstance!.name : '' diff --git a/scripts/local_dev.ps1 b/scripts/local_dev.ps1 index 854c51079..7319aca56 100644 --- a/scripts/local_dev.ps1 +++ b/scripts/local_dev.ps1 @@ -109,7 +109,7 @@ function Ensure-AzureAIUserRole { $foundryResourceId = $null if (Test-Path ".env") { Get-Content ".env" | ForEach-Object { - if ($_ -match "^AZURE_ENV_FOUNDRY_PROJECT_RID=(.*)$") { $existingProjectId = $matches[1].Trim('"').Trim("'") } + if ($_ -match "^AZURE_EXISTING_AIPROJECT_RESOURCE_ID=(.*)$") { $existingProjectId = $matches[1].Trim('"').Trim("'") } if ($_ -match "^AI_FOUNDRY_RESOURCE_ID=(.*)$") { $foundryResourceId = $matches[1].Trim('"').Trim("'") } } } @@ -121,7 +121,7 @@ function Ensure-AzureAIUserRole { } elseif ($foundryResourceId) { $scope = $foundryResourceId } else { - Write-Error "Neither AZURE_ENV_FOUNDRY_PROJECT_RID nor AI_FOUNDRY_RESOURCE_ID found in .env" + Write-Error "Neither AZURE_EXISTING_AIPROJECT_RESOURCE_ID nor AI_FOUNDRY_RESOURCE_ID found in .env" exit 1 } diff --git a/scripts/local_dev.sh b/scripts/local_dev.sh index 67a55b8a1..4f1c084ac 100644 --- a/scripts/local_dev.sh +++ b/scripts/local_dev.sh @@ -105,7 +105,7 @@ ensure_azure_ai_user_role() { local existing_project_id="" local foundry_resource_id="" if [ -f ".env" ]; then - existing_project_id=$(grep "^AZURE_ENV_FOUNDRY_PROJECT_RID=" .env | cut -d'=' -f2- | tr -d '"' | tr -d "'" || echo "") + existing_project_id=$(grep "^AZURE_EXISTING_AIPROJECT_RESOURCE_ID=" .env | cut -d'=' -f2- | tr -d '"' | tr -d "'" || echo "") foundry_resource_id=$(grep "^AI_FOUNDRY_RESOURCE_ID=" .env | cut -d'=' -f2- | tr -d '"' | tr -d "'" || echo "") fi @@ -115,7 +115,7 @@ ensure_azure_ai_user_role() { elif [ -n "$foundry_resource_id" ]; then scope="$foundry_resource_id" else - print_error "Neither AZURE_ENV_FOUNDRY_PROJECT_RID nor AI_FOUNDRY_RESOURCE_ID found in .env" + print_error "Neither AZURE_EXISTING_AIPROJECT_RESOURCE_ID nor AI_FOUNDRY_RESOURCE_ID found in .env" exit 1 fi From 462a53d496675e921e7b229f61ff3f3c395b3626 Mon Sep 17 00:00:00 2001 From: Ragini-Microsoft Date: Wed, 8 Apr 2026 20:52:22 +0530 Subject: [PATCH 56/72] fix: Simplify first run check for ACI deployment in azure_custom.yaml --- azure_custom.yaml | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/azure_custom.yaml b/azure_custom.yaml index a79c694e9..4500fcfc0 100644 --- a/azure_custom.yaml +++ b/azure_custom.yaml @@ -36,10 +36,7 @@ hooks: # Check if this is first run (ACR doesn't exist yet) # Set IMAGE_TAG='none' to skip ACI deployment until image is built - $currentTag = azd env get-value IMAGE_TAG 2>$null - $global:LASTEXITCODE = 0 - - if (-not $env:AZURE_CONTAINER_REGISTRY_NAME -and $currentTag -ne 'latest') { + if (-not $env:AZURE_CONTAINER_REGISTRY_NAME) { Write-Host "First deployment - ACI will be deployed after image build" -ForegroundColor Yellow azd env set IMAGE_TAG none } @@ -50,9 +47,7 @@ hooks: echo "Preparing deployment..." # Check if this is first run (ACR doesn't exist yet) - current_tag=$(azd env get-value IMAGE_TAG 2>/dev/null || echo "") - - if [ -z "$AZURE_CONTAINER_REGISTRY_NAME" ] && [ "$current_tag" != "latest" ]; then + if [ -z "$AZURE_CONTAINER_REGISTRY_NAME" ]; then echo "First deployment - ACI will be deployed after image build" azd env set IMAGE_TAG none fi From 1e74f693cf619117081cffa5a935a34148baa54f Mon Sep 17 00:00:00 2001 From: Pavan-Microsoft Date: Wed, 8 Apr 2026 21:25:59 +0530 Subject: [PATCH 57/72] refactor: update Azure environment variable names and remove telemetry parameters --- .github/workflows/azure-dev.yml | 12 ++++++------ infra/main.parameters.json | 3 --- infra/main.waf.parameters.json | 3 --- 3 files changed, 6 insertions(+), 12 deletions(-) diff --git a/.github/workflows/azure-dev.yml b/.github/workflows/azure-dev.yml index 7ea921464..81b7dc299 100644 --- a/.github/workflows/azure-dev.yml +++ b/.github/workflows/azure-dev.yml @@ -18,8 +18,8 @@ jobs: AZURE_ENV_NAME: ${{ secrets.AZURE_ENV_NAME }} AZURE_LOCATION: ${{ secrets.AZURE_LOCATION }} AZURE_ENV_AI_SERVICE_LOCATION: ${{ secrets.AZURE_ENV_AI_SERVICE_LOCATION }} - AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID: ${{ secrets.AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID }} - AZURE_EXISTING_AI_PROJECT_RESOURCE_ID: ${{ secrets.AZURE_EXISTING_AI_PROJECT_RESOURCE_ID }} + AZURE_ENV_EXISTING_LOG_ANALYTICS_WORKSPACE_RID: ${{ secrets.AZURE_ENV_EXISTING_LOG_ANALYTICS_WORKSPACE_RID }} + AZURE_EXISTING_AIPROJECT_RESOURCE_ID: ${{ secrets.AZURE_EXISTING_AIPROJECT_RESOURCE_ID }} AZURE_DEV_COLLECT_TELEMETRY: ${{ vars.AZURE_DEV_COLLECT_TELEMETRY }} steps: @@ -60,12 +60,12 @@ jobs: azd config set defaults.subscription "$AZURE_SUBSCRIPTION_ID" - if [[ -n "${AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID:-}" ]]; then - azd env set AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID "$AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID" + if [[ -n "${AZURE_ENV_EXISTING_LOG_ANALYTICS_WORKSPACE_RID:-}" ]]; then + azd env set AZURE_ENV_EXISTING_LOG_ANALYTICS_WORKSPACE_RID "$AZURE_ENV_EXISTING_LOG_ANALYTICS_WORKSPACE_RID" fi - if [[ -n "${AZURE_EXISTING_AI_PROJECT_RESOURCE_ID:-}" ]]; then - azd env set AZURE_EXISTING_AI_PROJECT_RESOURCE_ID "$AZURE_EXISTING_AI_PROJECT_RESOURCE_ID" + if [[ -n "${AZURE_EXISTING_AIPROJECT_RESOURCE_ID:-}" ]]; then + azd env set AZURE_EXISTING_AIPROJECT_RESOURCE_ID "$AZURE_EXISTING_AIPROJECT_RESOURCE_ID" fi azd up --no-prompt diff --git a/infra/main.parameters.json b/infra/main.parameters.json index 2ad8b6e30..b830e8365 100644 --- a/infra/main.parameters.json +++ b/infra/main.parameters.json @@ -46,9 +46,6 @@ }, "imageTag": { "value": "${AZURE_ENV_IMAGE_TAG=latest}" - }, - "enableTelemetry": { - "value": "${AZURE_ENV_ENABLE_TELEMETRY}" } } } diff --git a/infra/main.waf.parameters.json b/infra/main.waf.parameters.json index 119f62f9d..e4ec5e0c5 100644 --- a/infra/main.waf.parameters.json +++ b/infra/main.waf.parameters.json @@ -47,9 +47,6 @@ "imageTag": { "value": "${AZURE_ENV_IMAGE_TAG=latest}" }, - "enableTelemetry": { - "value": "${AZURE_ENV_ENABLE_TELEMETRY}" - }, "enablePrivateNetworking": { "value": true }, From e74493fd3b5031c72ed2cc260acd6472d4fd84fe Mon Sep 17 00:00:00 2001 From: Roopan-Microsoft Date: Thu, 9 Apr 2026 11:34:11 +0530 Subject: [PATCH 58/72] fix: add bicep version requirement (>= 0.33.0) to azure.yaml --- azure.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/azure.yaml b/azure.yaml index 521778a24..a03b6c1a9 100644 --- a/azure.yaml +++ b/azure.yaml @@ -8,6 +8,7 @@ metadata: requiredVersions: azd: '>= 1.18.0 != 1.23.9' + bicep: '>= 0.33.0' parameters: solutionPrefix: From 9bccc877ec05378df0cba3b459265cac566d5e54 Mon Sep 17 00:00:00 2001 From: "Prekshith D J (Persistent Systems Inc)" Date: Thu, 9 Apr 2026 11:41:52 +0530 Subject: [PATCH 59/72] fix: Remove create-release.yml path filter changes Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/workflows/create-release.yml | 9 --------- 1 file changed, 9 deletions(-) diff --git a/.github/workflows/create-release.yml b/.github/workflows/create-release.yml index b04f4d3f1..1ca61df0a 100644 --- a/.github/workflows/create-release.yml +++ b/.github/workflows/create-release.yml @@ -3,15 +3,6 @@ name: "Create Release" on: push: branches: ["main"] - paths-ignore: - - 'docs/**' - - '**/*.md' - - 'LICENSE' - - '**/*.png' - - '**/*.svg' - - '**/*.jpg' - - '**/*.jpeg' - - '**/*.gif' workflow_dispatch: From 9f272f9241e79ae848f14a7cb10f1b6e7e97bc0b Mon Sep 17 00:00:00 2001 From: Pavan-Microsoft Date: Thu, 9 Apr 2026 11:58:12 +0530 Subject: [PATCH 60/72] refactor: update environment variable names to use AZURE_ENV prefix --- azure_custom.yaml | 22 +++++++++++----------- infra/main_custom.bicep | 24 +++++++++--------------- 2 files changed, 20 insertions(+), 26 deletions(-) diff --git a/azure_custom.yaml b/azure_custom.yaml index 26bdf8f47..d11b68636 100644 --- a/azure_custom.yaml +++ b/azure_custom.yaml @@ -35,13 +35,13 @@ hooks: Write-Host "Preparing deployment..." -ForegroundColor Cyan # Check if this is first run (ACR doesn't exist yet) - # Set IMAGE_TAG='none' to skip ACI deployment until image is built - $currentTag = azd env get-value IMAGE_TAG 2>$null + # Set AZURE_ENV_IMAGE_TAG='none' to skip ACI deployment until image is built + $currentTag = azd env get-value AZURE_ENV_IMAGE_TAG 2>$null $global:LASTEXITCODE = 0 - if (-not $env:AZURE_CONTAINER_REGISTRY_NAME -and $currentTag -ne 'latest') { + if (-not $env:AZURE_ENV_CONTAINER_REGISTRY_NAME -and $currentTag -ne 'latest') { Write-Host "First deployment - ACI will be deployed after image build" -ForegroundColor Yellow - azd env set IMAGE_TAG none + azd env set AZURE_ENV_IMAGE_TAG none } continueOnError: false posix: @@ -50,11 +50,11 @@ hooks: echo "Preparing deployment..." # Check if this is first run (ACR doesn't exist yet) - current_tag=$(azd env get-value IMAGE_TAG 2>/dev/null || echo "") + current_tag=$(azd env get-value AZURE_ENV_IMAGE_TAG 2>/dev/null || echo "") - if [ -z "$AZURE_CONTAINER_REGISTRY_NAME" ] && [ "$current_tag" != "latest" ]; then + if [ -z "$AZURE_ENV_CONTAINER_REGISTRY_NAME" ] && [ "$current_tag" != "latest" ]; then echo "First deployment - ACI will be deployed after image build" - azd env set IMAGE_TAG none + azd env set AZURE_ENV_IMAGE_TAG none fi continueOnError: false @@ -62,7 +62,7 @@ hooks: windows: shell: pwsh run: | - $acrName = $env:AZURE_CONTAINER_REGISTRY_NAME + $acrName = $env:AZURE_ENV_CONTAINER_REGISTRY_NAME $resourceGroup = $env:RESOURCE_GROUP_NAME $backendImage = $env:BACKEND_IMAGE_NAME $appServiceName = $env:APP_SERVICE_NAME @@ -94,7 +94,7 @@ hooks: if (-not $aciName) { Write-Host "" Write-Host "===== Deploying Container Instance =====" -ForegroundColor Yellow - azd env set IMAGE_TAG latest + azd env set AZURE_ENV_IMAGE_TAG latest # Use az deployment instead of azd provision to avoid hook recursion # Pass parameters inline (main.parameters.json uses AZD ${VAR} syntax not supported by az CLI) @@ -132,7 +132,7 @@ hooks: posix: shell: sh run: | - ACR_NAME="$AZURE_CONTAINER_REGISTRY_NAME" + ACR_NAME="$AZURE_ENV_CONTAINER_REGISTRY_NAME" RESOURCE_GROUP="$RESOURCE_GROUP_NAME" BACKEND_IMAGE="$BACKEND_IMAGE_NAME" APP_SERVICE="$APP_SERVICE_NAME" @@ -162,7 +162,7 @@ hooks: if [ -z "$ACI_NAME" ]; then echo "" echo "===== Deploying Container Instance =====" - azd env set IMAGE_TAG latest + azd env set AZURE_ENV_IMAGE_TAG latest # Use az deployment instead of azd provision to avoid hook recursion # Pass parameters inline (main.parameters.json uses AZD ${VAR} syntax not supported by az CLI) diff --git a/infra/main_custom.bicep b/infra/main_custom.bicep index da54978f3..411f4b0a3 100644 --- a/infra/main_custom.bicep +++ b/infra/main_custom.bicep @@ -1059,10 +1059,10 @@ resource containerInstance 'Microsoft.ContainerInstance/containerGroups@2025-09- environmentVariables: [ // Azure OpenAI Settings { name: 'AZURE_OPENAI_ENDPOINT', value: 'https://${aiFoundryAiServicesResourceName}.openai.azure.com/' } - { name: 'AZURE_OPENAI_GPT_MODEL', value: gptModelName } - { name: 'AZURE_OPENAI_IMAGE_MODEL', value: imageModelConfig[imageModelChoice].name } + { name: 'AZURE_ENV_GPT_MODEL_NAME', value: gptModelName } + { name: 'AZURE_ENV_IMAGE_MODEL_NAME', value: imageModelConfig[imageModelChoice].name } { name: 'AZURE_OPENAI_GPT_IMAGE_ENDPOINT', value: imageModelChoice != 'none' ? 'https://${aiFoundryAiServicesResourceName}.openai.azure.com/' : '' } - { name: 'AZURE_OPENAI_API_VERSION', value: azureOpenaiAPIVersion } + { name: 'AZURE_ENV_OPENAI_API_VERSION', value: azureOpenaiAPIVersion } // Azure Cosmos DB Settings { name: 'AZURE_COSMOS_ENDPOINT', value: 'https://cosmos-${solutionSuffix}.documents.azure.com:443/' } { name: 'AZURE_COSMOS_DATABASE_NAME', value: cosmosDBDatabaseName } @@ -1167,7 +1167,7 @@ output AI_FOUNDRY_RG_NAME string = aiFoundryAiServicesResourceGroupName output AI_FOUNDRY_RESOURCE_ID string = useExistingAiFoundryAiProject ? '' : aiFoundryAiServices!.outputs.resourceId @description('Contains existing AI project resource ID.') -output AZURE_EXISTING_AI_PROJECT_RESOURCE_ID string = azureExistingAIProjectResourceId +output AZURE_EXISTING_AIPROJECT_RESOURCE_ID string = azureExistingAIProjectResourceId @description('Contains AI Search Service Endpoint URL') output AZURE_AI_SEARCH_ENDPOINT string = 'https://${aiSearch.outputs.name}.search.windows.net/' @@ -1185,16 +1185,16 @@ output AZURE_AI_SEARCH_IMAGE_INDEX string = 'product-images' output AZURE_OPENAI_ENDPOINT string = 'https://${aiFoundryAiServicesResourceName}.openai.azure.com/' @description('Contains GPT Model') -output AZURE_OPENAI_GPT_MODEL string = gptModelName +output AZURE_ENV_GPT_MODEL_NAME string = gptModelName @description('Contains Image Model (empty if none selected)') -output AZURE_OPENAI_IMAGE_MODEL string = imageModelConfig[imageModelChoice].name +output AZURE_ENV_IMAGE_MODEL_NAME string = imageModelConfig[imageModelChoice].name @description('Contains Azure OpenAI GPT/Image endpoint URL (empty if no image model selected)') output AZURE_OPENAI_GPT_IMAGE_ENDPOINT string = imageModelChoice != 'none' ? 'https://${aiFoundryAiServicesResourceName}.openai.azure.com/' : '' @description('Contains Azure OpenAI API Version') -output AZURE_OPENAI_API_VERSION string = azureOpenaiAPIVersion +output AZURE_ENV_OPENAI_API_VERSION string = azureOpenaiAPIVersion @description('Contains OpenAI Resource') output AZURE_OPENAI_RESOURCE string = aiFoundryAiServicesResourceName @@ -1221,13 +1221,7 @@ output CONTAINER_INSTANCE_IP string = shouldDeployACI ? containerInstance!.prope output CONTAINER_INSTANCE_FQDN string = (shouldDeployACI && !isPrivateNetworking) ? containerInstance!.properties.ipAddress.fqdn : '' @description('Contains ACR Name') -output ACR_NAME string = acrResourceName - -@description('Contains Azure Container Registry Endpoint (used by AZD for remote builds)') -output AZURE_CONTAINER_REGISTRY_ENDPOINT string = containerRegistry.outputs.loginServer - -@description('Contains Azure Container Registry Name') -output AZURE_CONTAINER_REGISTRY_NAME string = containerRegistry.outputs.name +output AZURE_ENV_CONTAINER_REGISTRY_NAME string = containerRegistry.outputs.name @description('Contains flag for Azure AI Foundry usage') output USE_FOUNDRY bool = useFoundryMode ? true : false @@ -1251,4 +1245,4 @@ output FRONTEND_IMAGE_NAME string = frontendImageName output BACKEND_IMAGE_NAME string = backendImageName @description('Image tag') -output IMAGE_TAG string = imageTag +output AZURE_ENV_IMAGE_TAG string = imageTag From 3dcd6604081d90129d828e99a309fa589f1f8dbd Mon Sep 17 00:00:00 2001 From: Pavan-Microsoft Date: Thu, 9 Apr 2026 12:21:24 +0530 Subject: [PATCH 61/72] fix: address Copilot review comments - deploy.sh Dockerfile path and docs region example - Fix deploy.sh: use backend/ApiApp.Dockerfile instead of WebApp.Dockerfile - Fix AZD_DEPLOYMENT.md: use eastus2 (allowed region) instead of eastus in example Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- azure.yaml | 4 ++-- docs/AZD_DEPLOYMENT.md | 2 +- scripts/deploy.sh | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/azure.yaml b/azure.yaml index 23f3ced27..c35437fe4 100644 --- a/azure.yaml +++ b/azure.yaml @@ -3,8 +3,8 @@ environment: location: eastus name: content-generation -metadata: - template: content-generation@1.22 +# metadata: +# template: content-generation@1.22 requiredVersions: azd: '>= 1.18.0 != 1.23.9' diff --git a/docs/AZD_DEPLOYMENT.md b/docs/AZD_DEPLOYMENT.md index 342366a99..019483934 100644 --- a/docs/AZD_DEPLOYMENT.md +++ b/docs/AZD_DEPLOYMENT.md @@ -227,7 +227,7 @@ Error: The model 'gpt-4o' is not available in region 'westeurope' **Solution**: Set a different region for AI Services: ```bash -azd env set AZURE_ENV_AI_SERVICE_LOCATION eastus +azd env set AZURE_ENV_AI_SERVICE_LOCATION eastus2 ``` #### 3. Container Build Fails diff --git a/scripts/deploy.sh b/scripts/deploy.sh index efc48ccc2..efd2b51b9 100644 --- a/scripts/deploy.sh +++ b/scripts/deploy.sh @@ -103,8 +103,8 @@ if [[ $REPLY =~ ^[Yy]$ ]]; then az acr build \ --registry "$AZURE_ENV_CONTAINER_REGISTRY_NAME" \ --image "contentgen-backend:$AZURE_ENV_IMAGE_TAG" \ - --file WebApp.Dockerfile \ - . + --file backend/ApiApp.Dockerfile \ + backend echo "✓ Container built and pushed to $AZURE_ENV_CONTAINER_REGISTRY_NAME.azurecr.io/contentgen-backend:$AZURE_ENV_IMAGE_TAG" From eff00eda293323140ff2ef27b952cc41bcfe0d2a Mon Sep 17 00:00:00 2001 From: Pavan-Microsoft Date: Thu, 9 Apr 2026 12:24:45 +0530 Subject: [PATCH 62/72] fix: uncomment metadata section in azure.yaml --- azure.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/azure.yaml b/azure.yaml index c35437fe4..23f3ced27 100644 --- a/azure.yaml +++ b/azure.yaml @@ -3,8 +3,8 @@ environment: location: eastus name: content-generation -# metadata: -# template: content-generation@1.22 +metadata: + template: content-generation@1.22 requiredVersions: azd: '>= 1.18.0 != 1.23.9' From c3cd3ed0fedd9d9cff51f88d4c014eb4aa5f68cb Mon Sep 17 00:00:00 2001 From: Pavan-Microsoft Date: Thu, 9 Apr 2026 12:41:47 +0530 Subject: [PATCH 63/72] fix: deploy.ps1 Dockerfile path and remove eastus from checkquota defaults - Fix deploy.ps1: use src\backend\ApiApp.Dockerfile instead of src\WebApp.Dockerfile - Remove eastus from checkquota.sh default regions (not in allowed azureAiServiceLocation values) - Add missing allowed regions to checkquota defaults Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- scripts/checkquota.sh | 2 +- scripts/deploy.ps1 | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/scripts/checkquota.sh b/scripts/checkquota.sh index f7a21131c..c12df68c3 100644 --- a/scripts/checkquota.sh +++ b/scripts/checkquota.sh @@ -40,7 +40,7 @@ IMAGE_MODEL_MIN_CAPACITY="${IMAGE_MODEL_MIN_CAPACITY:-1}" if [[ -n "$AZURE_REGIONS" ]]; then IFS=', ' read -ra REGIONS <<< "$AZURE_REGIONS" else - REGIONS=("westus3" "eastus2" "uaenorth" "swedencentral" "australiaeast" "eastus" "uksouth" "japaneast") + REGIONS=("westus3" "eastus2" "uaenorth" "swedencentral" "australiaeast" "uksouth" "japaneast" "canadaeast" "koreacentral" "polandcentral" "switzerlandnorth") fi # Map image model choice to Azure quota model name diff --git a/scripts/deploy.ps1 b/scripts/deploy.ps1 index 1da9746f8..e562c9185 100644 --- a/scripts/deploy.ps1 +++ b/scripts/deploy.ps1 @@ -96,7 +96,7 @@ if ($continue -eq "y" -or $continue -eq "Y") { Write-Host "Step 1: Building and pushing backend container..." -ForegroundColor Green Write-Host "==========================================" -ForegroundColor Green - Set-Location "$ProjectDir\src" + Set-Location "$ProjectDir\src\backend" # Login to ACR az acr login --name $AcrName @@ -105,7 +105,7 @@ if ($continue -eq "y" -or $continue -eq "Y") { az acr build ` --registry $AcrName ` --image "contentgen-backend:$ImageTag" ` - --file WebApp.Dockerfile ` + --file ApiApp.Dockerfile ` . Write-Host "✓ Container built and pushed to $AcrName.azurecr.io/contentgen-backend:$ImageTag" -ForegroundColor Green From c72e70e8a3f9419a1403e703e7feec85800fb884 Mon Sep 17 00:00:00 2001 From: Pavan-Microsoft Date: Thu, 9 Apr 2026 12:48:10 +0530 Subject: [PATCH 64/72] fix: update environment variable names to use AZURE_ prefix in deploy.ps1 --- scripts/deploy.ps1 | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/scripts/deploy.ps1 b/scripts/deploy.ps1 index e562c9185..cc411a2f3 100644 --- a/scripts/deploy.ps1 +++ b/scripts/deploy.ps1 @@ -40,10 +40,10 @@ $ProjectDir = Split-Path -Parent $ScriptDir Set-Location $ProjectDir # Configuration from environment or prompt -$ResourceGroup = if ($env:RESOURCE_GROUP) { $env:RESOURCE_GROUP } else { $null } -$Location = if ($env:LOCATION) { $env:LOCATION } else { "eastus" } +$ResourceGroup = if ($env:AZURE_RESOURCE_GROUP) { $env:AZURE_RESOURCE_GROUP } else { $null } +$Location = if ($env:AZURE_LOCATION) { $env:AZURE_LOCATION } else { "eastus" } $AcrName = if ($env:AZURE_ENV_CONTAINER_REGISTRY_NAME) { $env:AZURE_ENV_CONTAINER_REGISTRY_NAME } else { $null } -$ContainerName = if ($env:CONTAINER_NAME) { $env:CONTAINER_NAME } else { "aci-contentgen-backend" } +$ContainerName = if ($env:CONTAINER_INSTANCE_NAME) { $env:CONTAINER_INSTANCE_NAME } else { "aci-contentgen-backend" } $AppServiceName = if ($env:APP_SERVICE_NAME) { $env:APP_SERVICE_NAME } else { $null } $ImageTag = if ($env:AZURE_ENV_IMAGE_TAG) { $env:AZURE_ENV_IMAGE_TAG } else { "latest" } From d4bd501a308e47bef3585a009d7b7ab1a68036a2 Mon Sep 17 00:00:00 2001 From: Ragini-Microsoft Date: Thu, 9 Apr 2026 17:29:39 +0530 Subject: [PATCH 65/72] fix: improve error handling for JSON parsing --- infra/scripts/validate_bicep_params.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/infra/scripts/validate_bicep_params.py b/infra/scripts/validate_bicep_params.py index 78c1a61ef..85df7d149 100644 --- a/infra/scripts/validate_bicep_params.py +++ b/infra/scripts/validate_bicep_params.py @@ -108,7 +108,8 @@ def parse_parameters_env_vars(json_path: Path) -> dict[str, list[str]]: data = json.loads(sanitized) params = data.get("parameters", {}) except json.JSONDecodeError: - pass + # Best-effort behavior: if JSON cannot be parsed, treat as no env-var + return result # Walk each top-level parameter and scan its entire serialized value # for ${VAR} references from the original text. From 7f3ec7ddf2870fb22d94e151a600ad6bcf3a4733 Mon Sep 17 00:00:00 2001 From: Thanusree-Microsoft <168087422+Thanusree-Microsoft@users.noreply.github.com> Date: Thu, 9 Apr 2026 17:51:20 +0530 Subject: [PATCH 66/72] Update README Added notes on security restrictions and Azure OpenAI quota availability. --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 8ac7b64b4..ad39192f5 100644 --- a/README.md +++ b/README.md @@ -82,6 +82,8 @@ Follow the quick deploy steps on the deployment guide to deploy this solution to
    +> **Note**: Some tenants may have additional security restrictions that run periodically and could impact the application (e.g., blocking public network access). If you experience issues or the application stops working, check if these restrictions are the cause. In such cases, consider deploying the WAF-supported version to ensure compliance. To configure, [Click here](./docs/DeploymentGuide.md#31-choose-deployment-type-optional). + > ⚠️ **Important: Check Azure OpenAI Quota Availability**
    To ensure sufficient quota is available in your subscription, please follow [quota check instructions guide](./docs/QuotaCheck.md) before you deploy the solution. From ed4ee466d4c2fac771e80ab7cc967f626e1461c8 Mon Sep 17 00:00:00 2001 From: Thanusree-Microsoft <168087422+Thanusree-Microsoft@users.noreply.github.com> Date: Thu, 9 Apr 2026 17:53:59 +0530 Subject: [PATCH 67/72] Update link in README for deployment guide --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index ad39192f5..b3ec4a0c8 100644 --- a/README.md +++ b/README.md @@ -82,7 +82,7 @@ Follow the quick deploy steps on the deployment guide to deploy this solution to
    -> **Note**: Some tenants may have additional security restrictions that run periodically and could impact the application (e.g., blocking public network access). If you experience issues or the application stops working, check if these restrictions are the cause. In such cases, consider deploying the WAF-supported version to ensure compliance. To configure, [Click here](./docs/DeploymentGuide.md#31-choose-deployment-type-optional). +> **Note**: Some tenants may have additional security restrictions that run periodically and could impact the application (e.g., blocking public network access). If you experience issues or the application stops working, check if these restrictions are the cause. In such cases, consider deploying the WAF-supported version to ensure compliance. To configure, [Click here](./docs/DEPLOYMENT.md#31-choose-deployment-type-optional). > ⚠️ **Important: Check Azure OpenAI Quota Availability**
    To ensure sufficient quota is available in your subscription, please follow [quota check instructions guide](./docs/QuotaCheck.md) before you deploy the solution. From f325d2727416816e431f320f2edd6c44d16f633c Mon Sep 17 00:00:00 2001 From: Thanusree-Microsoft <168087422+Thanusree-Microsoft@users.noreply.github.com> Date: Thu, 9 Apr 2026 17:58:05 +0530 Subject: [PATCH 68/72] Fix link to deployment configuration in README Updated deployment documentation to reference AZD_DEPLOYMENT.md. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index b3ec4a0c8..1b6c55592 100644 --- a/README.md +++ b/README.md @@ -82,7 +82,7 @@ Follow the quick deploy steps on the deployment guide to deploy this solution to
    -> **Note**: Some tenants may have additional security restrictions that run periodically and could impact the application (e.g., blocking public network access). If you experience issues or the application stops working, check if these restrictions are the cause. In such cases, consider deploying the WAF-supported version to ensure compliance. To configure, [Click here](./docs/DEPLOYMENT.md#31-choose-deployment-type-optional). +> **Note**: Some tenants may have additional security restrictions that run periodically and could impact the application (e.g., blocking public network access). If you experience issues or the application stops working, check if these restrictions are the cause. In such cases, consider deploying the WAF-supported version to ensure compliance. To configure, [Click here](./docs/AZD_DEPLOYMENT.md#3-choose-deployment-configuration). > ⚠️ **Important: Check Azure OpenAI Quota Availability**
    To ensure sufficient quota is available in your subscription, please follow [quota check instructions guide](./docs/QuotaCheck.md) before you deploy the solution. From d09906890ddc902daf5748e0dab5d6dc8b96a438 Mon Sep 17 00:00:00 2001 From: Thanusree-Microsoft <168087422+Thanusree-Microsoft@users.noreply.github.com> Date: Thu, 9 Apr 2026 17:59:20 +0530 Subject: [PATCH 69/72] Update deployment documentation with security notes Added note about security restrictions and WAF-supported version deployment. --- docs/DEPLOYMENT.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/DEPLOYMENT.md b/docs/DEPLOYMENT.md index 96323c5d0..53b5b67b0 100644 --- a/docs/DEPLOYMENT.md +++ b/docs/DEPLOYMENT.md @@ -16,6 +16,8 @@ Check the [Azure Products by Region](https://azure.microsoft.com/en-us/explore/g Here are some example regions where the services are available: East US, East US2, Australia East, UK South, France Central. +> **Note**: Some tenants may have additional security restrictions that run periodically and could impact the application (e.g., blocking public network access). If you experience issues or the application stops working, check if these restrictions are the cause. In such cases, consider deploying the WAF-supported version to ensure compliance. To configure, [Click here](./docs/AZD_DEPLOYMENT.md#3-choose-deployment-configuration). + ### **Important Note for PowerShell Users** If you encounter issues running PowerShell scripts due to the policy of not being digitally signed, you can temporarily adjust the `ExecutionPolicy` by running the following command in an elevated PowerShell session: From 8ee3dcad04d515855298a6436814f21d4cfb9b60 Mon Sep 17 00:00:00 2001 From: Thanusree-Microsoft <168087422+Thanusree-Microsoft@users.noreply.github.com> Date: Thu, 9 Apr 2026 18:01:05 +0530 Subject: [PATCH 70/72] Fix link to AZD_DEPLOYMENT.md in deployment notes Updated link in deployment notes for clarity. --- docs/DEPLOYMENT.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/DEPLOYMENT.md b/docs/DEPLOYMENT.md index 53b5b67b0..dd3b3d390 100644 --- a/docs/DEPLOYMENT.md +++ b/docs/DEPLOYMENT.md @@ -16,7 +16,7 @@ Check the [Azure Products by Region](https://azure.microsoft.com/en-us/explore/g Here are some example regions where the services are available: East US, East US2, Australia East, UK South, France Central. -> **Note**: Some tenants may have additional security restrictions that run periodically and could impact the application (e.g., blocking public network access). If you experience issues or the application stops working, check if these restrictions are the cause. In such cases, consider deploying the WAF-supported version to ensure compliance. To configure, [Click here](./docs/AZD_DEPLOYMENT.md#3-choose-deployment-configuration). +> **Note**: Some tenants may have additional security restrictions that run periodically and could impact the application (e.g., blocking public network access). If you experience issues or the application stops working, check if these restrictions are the cause. In such cases, consider deploying the WAF-supported version to ensure compliance. To configure, [Click here](./AZD_DEPLOYMENT.md#3-choose-deployment-configuration). ### **Important Note for PowerShell Users** From 45d9c085b2f6122d4723e4362481e72e917aa47c Mon Sep 17 00:00:00 2001 From: Prekshith Date: Fri, 10 Apr 2026 11:09:03 +0530 Subject: [PATCH 71/72] fix: Restore push trigger with path filters for azure-dev.yml Keep dev's workflow structure while adding back push trigger with path filters for infra/**, azure*.yaml, and workflow file changes. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/workflows/azure-dev.yml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.github/workflows/azure-dev.yml b/.github/workflows/azure-dev.yml index 35aaf3843..a60e8c405 100644 --- a/.github/workflows/azure-dev.yml +++ b/.github/workflows/azure-dev.yml @@ -1,6 +1,13 @@ name: Azure Dev Deploy on: workflow_dispatch: + push: + branches: + - main + paths: + - 'infra/**' + - 'azure*.yaml' + - '.github/workflows/azure-dev.yml' permissions: From 9af260adb8a8ee2ec876e1195f6e8a818939e1af Mon Sep 17 00:00:00 2001 From: Pavan-Microsoft Date: Fri, 10 Apr 2026 14:17:46 +0530 Subject: [PATCH 72/72] Revert PR #768: UI refactoring --- .coverage | Bin 53248 -> 0 bytes .github/dependabot.yml | 4 +- .github/workflows/docker-build.yml | 18 +- .github/workflows/job-docker-build.yml | 4 +- .gitignore | 12 +- .../src/app/frontend/optimization-report.html | 1244 ----------------- .../src/app/frontend/src/api/httpClient.ts | 203 --- content-gen/src/app/frontend/src/api/index.ts | 258 ---- .../src/app/frontend/src/hooks/index.ts | 10 - .../app/frontend/src/hooks/useAutoScroll.ts | 30 - .../frontend/src/hooks/useChatOrchestrator.ts | 449 ------ .../src/hooks/useContentGeneration.ts | 111 -- .../src/hooks/useConversationActions.ts | 217 --- .../frontend/src/hooks/useCopyToClipboard.ts | 32 - .../app/frontend/src/hooks/useWindowSize.ts | 19 - .../src/app/frontend/src/store/appSlice.ts | 151 -- .../frontend/src/store/chatHistorySlice.ts | 127 -- .../src/app/frontend/src/store/chatSlice.ts | 67 - .../app/frontend/src/store/contentSlice.ts | 61 - .../src/app/frontend/src/store/hooks.ts | 9 - .../src/app/frontend/src/store/index.ts | 75 - .../src/app/frontend/src/store/selectors.ts | 36 - .../src/app/frontend/src/store/store.ts | 21 - .../src/app/frontend/src/utils/briefFields.ts | 46 - .../app/frontend/src/utils/contentErrors.ts | 31 - .../app/frontend/src/utils/contentParsing.ts | 108 -- .../app/frontend/src/utils/downloadImage.ts | 94 -- .../frontend/src/utils/generationStages.ts | 33 - .../src/app/frontend/src/utils/index.ts | 32 - .../app/frontend/src/utils/messageUtils.ts | 34 - .../src/app/frontend/src/utils/sseParser.ts | 48 - .../src/app/frontend/src/utils/stringUtils.ts | 45 - docs/AZD_DEPLOYMENT.md | 4 +- docs/TECHNICAL_GUIDE.md | 2 +- scripts/deploy.ps1 | 7 +- scripts/deploy.sh | 7 +- scripts/local_dev.ps1 | 2 +- scripts/local_dev.sh | 2 +- src/App/src/App.tsx | 82 -- src/App/src/api/httpClient.ts | 203 --- src/App/src/api/index.ts | 258 ---- src/App/src/components/AppHeader.tsx | 68 - src/App/src/components/ChatHistory.tsx | 345 ----- src/App/src/components/ChatInput.tsx | 149 -- src/App/src/components/ChatPanel.tsx | 168 --- src/App/src/components/ComplianceSection.tsx | 193 --- src/App/src/components/ConversationItem.tsx | 292 ---- src/App/src/components/ImagePreviewCard.tsx | 99 -- .../src/components/InlineContentPreview.tsx | 210 --- src/App/src/components/MessageBubble.tsx | 120 -- src/App/src/components/ProductCard.tsx | 136 -- src/App/src/components/ProductReview.tsx | 136 -- .../src/components/SelectedProductView.tsx | 63 - src/App/src/components/SuggestionCard.tsx | 83 -- src/App/src/components/TypingIndicator.tsx | 76 - src/App/src/components/ViolationCard.tsx | 64 - src/App/src/components/WelcomeCard.tsx | 108 -- src/App/src/hooks/index.ts | 10 - src/App/src/hooks/useAutoScroll.ts | 30 - src/App/src/hooks/useChatOrchestrator.ts | 449 ------ src/App/src/hooks/useContentGeneration.ts | 111 -- src/App/src/hooks/useConversationActions.ts | 217 --- src/App/src/hooks/useCopyToClipboard.ts | 32 - src/App/src/hooks/useWindowSize.ts | 19 - src/App/src/store/appSlice.ts | 151 -- src/App/src/store/chatHistorySlice.ts | 127 -- src/App/src/store/chatSlice.ts | 67 - src/App/src/store/contentSlice.ts | 61 - src/App/src/store/hooks.ts | 9 - src/App/src/store/index.ts | 75 - src/App/src/store/selectors.ts | 36 - src/App/src/store/store.ts | 21 - src/App/src/utils/briefFields.ts | 46 - src/App/src/utils/contentErrors.ts | 31 - src/App/src/utils/contentParsing.ts | 108 -- src/App/src/utils/downloadImage.ts | 94 -- src/App/src/utils/generationStages.ts | 33 - src/App/src/utils/index.ts | 32 - src/App/src/utils/messageUtils.ts | 34 - src/App/src/utils/sseParser.ts | 48 - src/App/src/utils/stringUtils.ts | 45 - src/{App => app}/.dockerignore | 3 +- src/{App => app}/WebApp.Dockerfile | 16 +- .../frontend-server}/package-lock.json | 0 .../frontend-server}/package.json | 0 .../server => app/frontend-server}/server.js | 0 src/{App => app/frontend}/index.html | 0 src/{App => app/frontend}/microsoft.svg | 0 src/{App => app/frontend}/package-lock.json | 110 +- src/{App => app/frontend}/package.json | 4 +- src/app/frontend/src/App.tsx | 766 ++++++++++ src/app/frontend/src/api/index.ts | 252 ++++ .../frontend}/src/components/BriefReview.tsx | 54 +- .../frontend/src/components/ChatHistory.tsx | 643 +++++++++ src/app/frontend/src/components/ChatPanel.tsx | 437 ++++++ .../src/components/ConfirmedBriefView.tsx | 21 +- .../src/components/InlineContentPreview.tsx | 561 ++++++++ .../frontend/src/components/ProductReview.tsx | 227 +++ .../src/components/SelectedProductView.tsx | 138 ++ .../frontend/src/components/WelcomeCard.tsx | 158 +++ src/{App => app/frontend}/src/main.tsx | 10 +- .../frontend}/src/styles/global.css | 0 .../src/styles/images/SamplePrompt.png | Bin 0 -> 4938 bytes .../frontend}/src/styles/images/contoso.svg | 0 .../src/styles/images/firstprompt.png | Bin .../src/styles/images/secondprompt.png | Bin src/{App => app/frontend}/src/types/index.ts | 20 + src/{App => app/frontend}/src/vite-env.d.ts | 0 src/{App => app/frontend}/tsconfig.json | 0 src/{App => app/frontend}/tsconfig.node.json | 0 src/{App => app/frontend}/vite.config.ts | 2 +- 111 files changed, 3303 insertions(+), 8511 deletions(-) delete mode 100644 .coverage delete mode 100644 content-gen/src/app/frontend/optimization-report.html delete mode 100644 content-gen/src/app/frontend/src/api/httpClient.ts delete mode 100644 content-gen/src/app/frontend/src/api/index.ts delete mode 100644 content-gen/src/app/frontend/src/hooks/index.ts delete mode 100644 content-gen/src/app/frontend/src/hooks/useAutoScroll.ts delete mode 100644 content-gen/src/app/frontend/src/hooks/useChatOrchestrator.ts delete mode 100644 content-gen/src/app/frontend/src/hooks/useContentGeneration.ts delete mode 100644 content-gen/src/app/frontend/src/hooks/useConversationActions.ts delete mode 100644 content-gen/src/app/frontend/src/hooks/useCopyToClipboard.ts delete mode 100644 content-gen/src/app/frontend/src/hooks/useWindowSize.ts delete mode 100644 content-gen/src/app/frontend/src/store/appSlice.ts delete mode 100644 content-gen/src/app/frontend/src/store/chatHistorySlice.ts delete mode 100644 content-gen/src/app/frontend/src/store/chatSlice.ts delete mode 100644 content-gen/src/app/frontend/src/store/contentSlice.ts delete mode 100644 content-gen/src/app/frontend/src/store/hooks.ts delete mode 100644 content-gen/src/app/frontend/src/store/index.ts delete mode 100644 content-gen/src/app/frontend/src/store/selectors.ts delete mode 100644 content-gen/src/app/frontend/src/store/store.ts delete mode 100644 content-gen/src/app/frontend/src/utils/briefFields.ts delete mode 100644 content-gen/src/app/frontend/src/utils/contentErrors.ts delete mode 100644 content-gen/src/app/frontend/src/utils/contentParsing.ts delete mode 100644 content-gen/src/app/frontend/src/utils/downloadImage.ts delete mode 100644 content-gen/src/app/frontend/src/utils/generationStages.ts delete mode 100644 content-gen/src/app/frontend/src/utils/index.ts delete mode 100644 content-gen/src/app/frontend/src/utils/messageUtils.ts delete mode 100644 content-gen/src/app/frontend/src/utils/sseParser.ts delete mode 100644 content-gen/src/app/frontend/src/utils/stringUtils.ts delete mode 100644 src/App/src/App.tsx delete mode 100644 src/App/src/api/httpClient.ts delete mode 100644 src/App/src/api/index.ts delete mode 100644 src/App/src/components/AppHeader.tsx delete mode 100644 src/App/src/components/ChatHistory.tsx delete mode 100644 src/App/src/components/ChatInput.tsx delete mode 100644 src/App/src/components/ChatPanel.tsx delete mode 100644 src/App/src/components/ComplianceSection.tsx delete mode 100644 src/App/src/components/ConversationItem.tsx delete mode 100644 src/App/src/components/ImagePreviewCard.tsx delete mode 100644 src/App/src/components/InlineContentPreview.tsx delete mode 100644 src/App/src/components/MessageBubble.tsx delete mode 100644 src/App/src/components/ProductCard.tsx delete mode 100644 src/App/src/components/ProductReview.tsx delete mode 100644 src/App/src/components/SelectedProductView.tsx delete mode 100644 src/App/src/components/SuggestionCard.tsx delete mode 100644 src/App/src/components/TypingIndicator.tsx delete mode 100644 src/App/src/components/ViolationCard.tsx delete mode 100644 src/App/src/components/WelcomeCard.tsx delete mode 100644 src/App/src/hooks/index.ts delete mode 100644 src/App/src/hooks/useAutoScroll.ts delete mode 100644 src/App/src/hooks/useChatOrchestrator.ts delete mode 100644 src/App/src/hooks/useContentGeneration.ts delete mode 100644 src/App/src/hooks/useConversationActions.ts delete mode 100644 src/App/src/hooks/useCopyToClipboard.ts delete mode 100644 src/App/src/hooks/useWindowSize.ts delete mode 100644 src/App/src/store/appSlice.ts delete mode 100644 src/App/src/store/chatHistorySlice.ts delete mode 100644 src/App/src/store/chatSlice.ts delete mode 100644 src/App/src/store/contentSlice.ts delete mode 100644 src/App/src/store/hooks.ts delete mode 100644 src/App/src/store/index.ts delete mode 100644 src/App/src/store/selectors.ts delete mode 100644 src/App/src/store/store.ts delete mode 100644 src/App/src/utils/briefFields.ts delete mode 100644 src/App/src/utils/contentErrors.ts delete mode 100644 src/App/src/utils/contentParsing.ts delete mode 100644 src/App/src/utils/downloadImage.ts delete mode 100644 src/App/src/utils/generationStages.ts delete mode 100644 src/App/src/utils/index.ts delete mode 100644 src/App/src/utils/messageUtils.ts delete mode 100644 src/App/src/utils/sseParser.ts delete mode 100644 src/App/src/utils/stringUtils.ts rename src/{App => app}/.dockerignore (92%) rename src/{App => app}/WebApp.Dockerfile (82%) rename src/{App/server => app/frontend-server}/package-lock.json (100%) rename src/{App/server => app/frontend-server}/package.json (100%) rename src/{App/server => app/frontend-server}/server.js (100%) rename src/{App => app/frontend}/index.html (100%) rename src/{App => app/frontend}/microsoft.svg (100%) rename src/{App => app/frontend}/package-lock.json (98%) rename src/{App => app/frontend}/package.json (90%) create mode 100644 src/app/frontend/src/App.tsx create mode 100644 src/app/frontend/src/api/index.ts rename src/{App => app/frontend}/src/components/BriefReview.tsx (72%) create mode 100644 src/app/frontend/src/components/ChatHistory.tsx create mode 100644 src/app/frontend/src/components/ChatPanel.tsx rename src/{App => app/frontend}/src/components/ConfirmedBriefView.tsx (74%) create mode 100644 src/app/frontend/src/components/InlineContentPreview.tsx create mode 100644 src/app/frontend/src/components/ProductReview.tsx create mode 100644 src/app/frontend/src/components/SelectedProductView.tsx create mode 100644 src/app/frontend/src/components/WelcomeCard.tsx rename src/{App => app/frontend}/src/main.tsx (60%) rename src/{App => app/frontend}/src/styles/global.css (100%) create mode 100644 src/app/frontend/src/styles/images/SamplePrompt.png rename src/{App => app/frontend}/src/styles/images/contoso.svg (100%) rename src/{App => app/frontend}/src/styles/images/firstprompt.png (100%) rename src/{App => app/frontend}/src/styles/images/secondprompt.png (100%) rename src/{App => app/frontend}/src/types/index.ts (87%) rename src/{App => app/frontend}/src/vite-env.d.ts (100%) rename src/{App => app/frontend}/tsconfig.json (100%) rename src/{App => app/frontend}/tsconfig.node.json (100%) rename src/{App => app/frontend}/vite.config.ts (95%) diff --git a/.coverage b/.coverage deleted file mode 100644 index 51fea3fcb04caf1151acc6e59af73d9b154294f3..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 53248 zcmeI)%WoS+90%~-y7k76Bd3ZYE98*5AjhiX$|@4C0|bafMI|T%#0ht7kL?BTuCtFk z4hXqals|&Mu@VPPT=@NFcD;5ISG`qQzN^^T$IR^fKJ&7>ZNGc^#Eq301)(Qn@yOb? zY}@)&2+OiI=(R`B&|O z%J-YD)2?*r0yYRh00IywjKKV9&1vlI+0TECWq+#TP!3dR-nTyg_T>2VM4TRf_T)sE zbKRMvFndTJW*NdBvM0Ov*;YHTAZ+>^YdQM9a64X zjj4!P=z21|62GXcwoZ`VEmoJYIYEUeckFWtahPUriBOGHsQiJ7Of3%GVXNBu{kN*q z*x9jPDjkyy2lTVK5)GZmLwP)+f(Ai2;}G2muis2YItjaZAQyB!ds4p%6i5DJu^AedoAhYnQzINCQmbusB(UgvE~{1 z)|6S-*-yMak^h`{Gj1HpVQw4;>vFEk8wY8yZ`?Mwv}GReN^@r9+@dB|;=5;wS~PxJ zY!wHNm{oW{X{{qjtA z6z1vm-8fq8R8~68;`ExX!`IjM8y+RAorFixbSi?89x|L;n8u;rygX(eW*9Q$fhK}9 zA=UJ)_BO68)j{;9L4UQc=^hg)8XzOw4{&H5jP}Y*SOrzN)Psu0=vx($&lVp}7 zpCz*~l3t;3j+}Dicz<0uX}VrF-PxEscb&%Wu01y+g6AcE-CGHQ&*Vek+CuQ=)t||D zB5u~5PgBL4_sWfrch^;%tI+H6d-=uo9bM%=OIqKfNuOV(l`8X@yvkpxtD|!{rP*XA z-H?VX&B0~`5S(x*DY5JaNv!)XAL{POYfoNZQ$T*4*;A^PPEN!7n;?o@T2^`4iayHD zisnQ{JPSol(GP;DlD=6RD97W7?&(HC*Wu=>^hsVubj4+i&zrhV+2X>}{3#8(Y<$s< z=ShEo6iphG5e?C6WoCt@S9!+D%J0m`giH10&pR@U zy)M7=w`BhIY|RgBr}5x{Jx|gRY$m5(M4tpanZ=qO-dX8c*DmIr>}#5(=_Ar|=VI_B zLq0Tj;2F_0d*9GysnQQf)fRujx9k5}^n(oo5P$##AOHafKmY;|fB*y_0Dd?6EIgkAr~ zq91G!fB*y_009U<00Izz00bZa0SFXHpjz6mq<;#a|AGH@k%B`k5P$##AOHafKmY;| zfB*y_009VG7vSgr^q>A=g8&2|009U<00Izz00bZa0SG{#zyf&wU*N_?Xb^w^1Rwwb z2tWV=5P$##AOL|>0MGx?6(9fs2tWV=5P$##AOHafKmY>87r^uX; - - - - - Content Generation Frontend UI Refactorization KPI Report (dev vs psl-ui-refractoring) - - - -
    - -
    -

    🚀 Content Generation Frontend UI Refactorization KPI Report

    -

    Complete Technical Analysis & Implementation Details

    -

    Generated: July 2025 | Project: Content Generation Solution Accelerator

    - -
    -
    - 100% - Original UI Files Impacted -
    -
    - 4 - Redux Toolkit Slices Added -
    -
    - -48.29% - Original Components LOC Reduction -
    -
    - +29.87% - Net UI Delta Rate -
    -
    -
    - - -
    -

    📊 Executive Summary

    -

    Through comparison of origin/dev...psl-ui-refractoring in content-gen/src/app/frontend/src, this report captures measured UI refactorization impact with consistent diff-based KPIs.

    -
      -
    • 48 UI files changed (100% of original dev UI files touched)
    • -
    • 3,740 additions / 2,453 deletions (6,193 total churn)
    • -
    • +1,091 net lines in UI scope (+29.87% vs dev baseline)
    • -
    • Original components reduced by 1,130 lines (-48.29%)
    • -
    • 4 Redux slices added with full store modularization
    • -
    • 100% changed files are TypeScript (48/48 .ts/.tsx)
    • -
    - -
    -
    - Comparison Coverage Complete ✅ -
    -
    -
    - - -
    -

    🔄 Architecture: Before vs After

    - -

    Before: Monolithic Component Pattern

    -
    -
    Tightly Coupled UI Logic
    - -
    Large Monolith Components
    - -
    Mixed API + UI + State
    - -
    Harder Maintenance
    -
    - -

    After: Redux Toolkit + Custom Hooks + Modular Components

    -
    -
    Redux Store
    - -
    4 Typed Slices
    - -
    21 Granular Selectors
    - -
    7 Custom Hooks
    - -
    18 Memoized Components
    -
    - -

    Redux Slice Architecture

    - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    SliceFileResponsibilityKey Exports
    appSlicestore/appSlice.tsGlobal app-level UI stateTheme, layout, global flags
    chatSlicestore/chatSlice.tsChat state and async chat operationsMessages, streaming, SSE
    contentSlicestore/contentSlice.tsContent preview and generation statePreviews, generation stages
    chatHistorySlicestore/chatHistorySlice.tsConversation history and session dataHistory CRUD, persistence
    -
    - - -
    -

    📈 UI Refactoring Implementation

    - -
    -
    - - - -
    - -
    -

    Phase 1: Component Decomposition

    -

    Monolithic components (App.tsx at 846 lines, ChatHistory at 616 lines) were decomposed into focused, single-responsibility components. 10 new granular components were extracted.

    -
    -
    // Deleted
    -content-gen/src/app/frontend/src/api/index.ts (replaced by httpClient)
    -
    -// Major reductions
    -App.tsx:                846 → 72  lines (-91.5%)
    -ChatHistory.tsx:        616 → 327 lines (-46.9%)
    -InlineContentPreview:   528 → 196 lines (-62.9%)
    -ChatPanel.tsx:          425 → 159 lines (-62.6%)
    -
    -// New granular components (10 files, 1,220 lines)
    -components/AppHeader.tsx
    -components/ChatInput.tsx
    -components/ComplianceSection.tsx
    -components/ConversationItem.tsx
    -components/ImagePreviewCard.tsx
    -components/MessageBubble.tsx
    -components/ProductCard.tsx
    -components/SuggestionCard.tsx
    -components/TypingIndicator.tsx
    -components/ViolationCard.tsx
    -
    -
    - - - - -
    -
    - - -
    -

    📊 KPI 1: Codebase Overview

    - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    MetricBefore (dev)After (local)Delta
    Total UI source files1347+34 files
    Total source lines3,6524,743+1,091 (+29.87%)
    Files added35Added
    Files modified12Modified
    Files deleted1Legacy API file removed
    Line additions3,740+3,740
    Line deletions2,453−2,453
    -
    - - -
    -

    📊 KPI 2: Component Complexity (Lines of Code)

    - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    ComponentBefore (lines)After (lines)Reduction
    App.tsx84672−774 (−91.49%)
    ChatHistory.tsx616327−289 (−46.92%)
    InlineContentPreview.tsx528196−332 (−62.88%)
    ChatPanel.tsx425159−266 (−62.59%)
    api/index.ts321222−99 (−30.84%)
    ProductReview.tsx217128−89 (−41.01%)
    BriefReview.tsx177157−20 (−11.30%)
    WelcomeCard.tsx154103−51 (−33.12%)
    SelectedProductView.tsx13560−75 (−55.56%)
    ConfirmedBriefView.tsx8880−8 (−9.09%)
    - -

    Top Reduction Progress

    -
    -
    -

    App.tsx Reduction

    -
    -
    −91.49%
    -
    -
    -
    -

    InlineContentPreview.tsx Reduction

    -
    -
    −62.88%
    -
    -
    -
    -
    -
    -

    ChatPanel.tsx Reduction

    -
    -
    −62.59%
    -
    -
    -
    -

    SelectedProductView.tsx Reduction

    -
    -
    −55.56%
    -
    -
    -
    -
    -
    -

    ChatHistory.tsx Reduction

    -
    -
    −46.92%
    -
    -
    -
    -

    ProductReview.tsx Reduction

    -
    -
    −41.01%
    -
    -
    -
    -
    - - -
    -

    📊 KPI 3: Architecture & Modularity

    - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    MetricBeforeAfterDelta
    Custom hooks0 files7 files (797 lines)+7 hook modules
    State management files08 files (501 lines)+8 store modules
    Utility modules09 files (423 lines)+9 under utils/
    New components010 files (1,220 lines)+10 granular components
    Typed selectors021+21 centralized in selectors.ts
    - -

    New Files Created

    -
    -
    -

    State Management (8 files)

    -
      -
    • ✅ store/index.ts
    • -
    • ✅ store/store.ts
    • -
    • ✅ store/hooks.ts
    • -
    • ✅ store/selectors.ts
    • -
    • ✅ store/appSlice.ts
    • -
    • ✅ store/chatSlice.ts
    • -
    • ✅ store/contentSlice.ts
    • -
    • ✅ store/chatHistorySlice.ts
    • -
    -
    -
    -

    Custom Hooks (7 files)

    -
      -
    • ✅ hooks/index.ts
    • -
    • ✅ hooks/useAutoScroll.ts
    • -
    • ✅ hooks/useChatOrchestrator.ts
    • -
    • ✅ hooks/useContentGeneration.ts
    • -
    • ✅ hooks/useConversationActions.ts
    • -
    • ✅ hooks/useCopyToClipboard.ts
    • -
    • ✅ hooks/useWindowSize.ts
    • -
    -
    -
    -
    -
    -

    Utility Modules (9 files)

    -
      -
    • ✅ utils/index.ts
    • -
    • ✅ utils/briefFields.ts
    • -
    • ✅ utils/contentErrors.ts
    • -
    • ✅ utils/contentParsing.ts
    • -
    • ✅ utils/downloadImage.ts
    • -
    • ✅ utils/generationStages.ts
    • -
    • ✅ utils/messageUtils.ts
    • -
    • ✅ utils/sseParser.ts
    • -
    • ✅ utils/stringUtils.ts
    • -
    -
    -
    -

    New Components (10 files)

    -
      -
    • ✅ components/AppHeader.tsx (65 lines)
    • -
    • ✅ components/ChatInput.tsx (139 lines)
    • -
    • ✅ components/ComplianceSection.tsx (187 lines)
    • -
    • ✅ components/ConversationItem.tsx (276 lines)
    • -
    • ✅ components/ImagePreviewCard.tsx (95 lines)
    • -
    • ✅ components/MessageBubble.tsx (113 lines)
    • -
    • ✅ components/ProductCard.tsx (132 lines)
    • -
    • ✅ components/SuggestionCard.tsx (81 lines)
    • -
    • ✅ components/TypingIndicator.tsx (71 lines)
    • -
    • ✅ components/ViolationCard.tsx (61 lines)
    • -
    -
    -
    -
    - - -
    -

    📊 KPI 4: Bundle Size (Production Build)

    - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    ChunkBeforeAfterDelta
    Bundle KPINot measuredNot measuredBuild benchmark required
    Comparison basisgit diff origin/dev...HEAD in content-gen/src/app/frontend/srcDiff-based report
    Measured churn6,193 lines3,740 add / 2,453 del
    Net UI delta+1,091 lines+29.87% vs dev UI baseline
    StatusBundle size not included in this KPI setUse build artifacts for bundle KPI
    - -
    -

    💡 Note: This report is strictly based on branch diff metrics. Bundle-size numbers require production build output from both refs and are intentionally excluded here.

    -
    -
    - - -
    -

    📊 KPI 5: Code Quality Patterns

    - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    PatternBeforeAfterDelta
    memo( component wrappers018+18 memoized component wrappers
    useCallback(1326+13 callback memoization
    useMemo(09+9 derived value memoization
    createSlice (Redux Toolkit)04+4 typed state modules
    createAsyncThunk06+6 standardized async
    useAppDispatch013+13 typed dispatch hooks
    useAppSelector048+48 typed selector hooks
    displayName018+18 DevTools identifiers
    -
    - - -
    -

    🐛 KPI 6: Structural Risk Reductions

    - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    #Refactor OutcomeEvidence from DiffStatus
    1Monolith App.tsx decomposedApp.tsx reduced from 846 to 72 lines (−91.5%); logic extracted to hooks, store, and componentsCompleted
    2State flow centralizedAdded 4 Redux slices, 21 selectors, and typed hooks under store/Completed
    3Reusable utility layer introducedAdded 9 utility modules under utils/ (SSE parsing, content parsing, string utils)Completed
    4Chat rendering decomposedChatHistory (616→327), ChatPanel (425→159), extracted MessageBubble, ConversationItem, ChatInputCompleted
    5Content preview modularizedInlineContentPreview (528→196), extracted ComplianceSection, ViolationCard, ImagePreviewCardCompleted
    6API layer refactoredapi/index.ts reduced (321→222), SSE/HTTP client logic extracted to utils/sseParser.tsCompleted
    7Product flow decomposedProductReview (217→128), extracted ProductCard, SuggestionCard sub-componentsCompleted
    8Custom hooks extracted7 domain hooks created: useChatOrchestrator, useContentGeneration, useConversationActions, etc.Completed
    9Performance patterns applied18 memo() wrappers, 18 displayName identifiers, 26 useCallback, 9 useMemoCompleted
    10Type safety enforced0 TypeScript errors; typed Redux hooks (useAppSelector: 48 usages, useAppDispatch: 13 usages)Completed
    -
    - - -
    -

    ⚡ KPI 7: Refactorization and Quality Outcomes

    - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    OptimizationBeforeAfter
    Changed files in TypeScriptN/A48/48 (.ts/.tsx)
    Memoized components0 memo( patterns18 memo( patterns
    Callback stability13 useCallback(26 useCallback(
    Derived memoization0 useMemo(9 useMemo(
    Typed Redux hooks0 usages61 usages (13 dispatch + 48 selector)
    DevTools traceability0 displayName18 displayName identifiers
    Async state managementManual fetch + state6 createAsyncThunk actions
    - -

    Refactorization Delta Snapshot

    -
    -
    git diff --shortstat origin/dev...HEAD -- content-gen/src/app/frontend/src/
    -48 files changed, 3740 insertions(+), 2453 deletions(-)
    -
    -Change mix:
    -A=35 (72.92%)
    -M=12 (25.00%)
    -D=1  (2.08%)
    -
    -
    - - -
    -

    🎯 Summary Scorecard

    - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    KPIBeforeAfterVerdict
    UI files impacted13 files48 changed100% of original files
    State managementNo Redux slicesRedux Toolkit (4 slices)Centralized
    Typed selectors021New capability
    Render optimization0 memo( patterns18 memo( patternsProduction-grade
    Net UI delta rate+29.87%Growth from modularization
    Original components LOC2,3401,210-48.29%
    TypeScript changed-file footprint48/48100%
    TypeScript build errors00Clean build
    - -
    -
    - 48 - Files Changed -
    -
    - 35/12/1 - Added / Modified / Deleted -
    -
    - +1,091 - Net UI Lines -
    -
    - 100% - Changed Files in TS/TSX -
    -
    -
    - - -
    -

    ✨ Best Practices Implemented

    - -
    -
    -

    State Management

    -
      -
    • Redux Toolkit (official)
    • -
    • 4 typed slices
    • -
    • 6 createAsyncThunk actions
    • -
    • 21 centralized selectors
    • -
    • Type-safe hooks
    • -
    -
    - -
    -

    Performance

    -
      -
    • 18x memo() wrappers
    • -
    • 26x useCallback()
    • -
    • 9x useMemo()
    • -
    • 48x useAppSelector
    • -
    • Original LOC −48.29%
    • -
    -
    - -
    -

    Code Quality

    -
      -
    • Single Responsibility
    • -
    • DRY Principle
    • -
    • 100% TypeScript
    • -
    • 18 displayName identifiers
    • -
    • 0 build errors
    • -
    -
    - -
    -

    Architecture

    -
      -
    • 7 custom hooks
    • -
    • 9 utility modules
    • -
    • 10 new components
    • -
    • Store/hooks/utils layers
    • -
    • Typed selector & thunk usage
    • -
    -
    -
    -
    - - - -
    - - - - diff --git a/content-gen/src/app/frontend/src/api/httpClient.ts b/content-gen/src/app/frontend/src/api/httpClient.ts deleted file mode 100644 index 6aaf83b48..000000000 --- a/content-gen/src/app/frontend/src/api/httpClient.ts +++ /dev/null @@ -1,203 +0,0 @@ -/** - * Centralized HTTP client with interceptors. - * - * - Singleton — use the default `httpClient` export everywhere. - * - Request interceptors automatically attach auth headers - * (X-Ms-Client-Principal-Id) so callers never need to remember. - * - Response interceptors provide uniform error handling. - * - Built-in query-param serialization, configurable timeout, and base URL. - */ - -/* ------------------------------------------------------------------ */ -/* Types */ -/* ------------------------------------------------------------------ */ - -/** Options accepted by every request method. */ -export interface RequestOptions extends Omit { - /** Query parameters – appended to the URL automatically. */ - params?: Record; - /** Per-request timeout in ms (default: client-level `timeout`). */ - timeout?: number; -} - -type RequestInterceptor = (url: string, init: RequestInit) => RequestInit | Promise; -type ResponseInterceptor = (response: Response) => Response | Promise; - -/* ------------------------------------------------------------------ */ -/* HttpClient */ -/* ------------------------------------------------------------------ */ - -export class HttpClient { - private baseUrl: string; - private defaultTimeout: number; - private requestInterceptors: RequestInterceptor[] = []; - private responseInterceptors: ResponseInterceptor[] = []; - - constructor(baseUrl = '', timeout = 60_000) { - this.baseUrl = baseUrl; - this.defaultTimeout = timeout; - } - - /* ---------- interceptor registration ---------- */ - - onRequest(fn: RequestInterceptor): void { - this.requestInterceptors.push(fn); - } - - onResponse(fn: ResponseInterceptor): void { - this.responseInterceptors.push(fn); - } - - /* ---------- public request helpers ---------- */ - - async get(path: string, opts: RequestOptions = {}): Promise { - const res = await this.request(path, { ...opts, method: 'GET' }); - return res.json() as Promise; - } - - async post(path: string, body?: unknown, opts: RequestOptions = {}): Promise { - const res = await this.request(path, { - ...opts, - method: 'POST', - body: body != null ? JSON.stringify(body) : undefined, - headers: { - ...(body != null ? { 'Content-Type': 'application/json' } : {}), - ...opts.headers, - }, - }); - return res.json() as Promise; - } - - async put(path: string, body?: unknown, opts: RequestOptions = {}): Promise { - const res = await this.request(path, { - ...opts, - method: 'PUT', - body: body != null ? JSON.stringify(body) : undefined, - headers: { - ...(body != null ? { 'Content-Type': 'application/json' } : {}), - ...opts.headers, - }, - }); - return res.json() as Promise; - } - - async delete(path: string, opts: RequestOptions = {}): Promise { - const res = await this.request(path, { ...opts, method: 'DELETE' }); - return res.json() as Promise; - } - - /** - * Low-level request that returns the raw `Response`. - * Useful for streaming (SSE) endpoints where the caller needs `response.body`. - */ - async raw(path: string, opts: RequestOptions & { method?: string; body?: BodyInit | null } = {}): Promise { - return this.request(path, opts); - } - - /* ---------- internal plumbing ---------- */ - - private buildUrl(path: string, params?: Record): string { - const url = `${this.baseUrl}${path}`; - if (!params) return url; - - const qs = new URLSearchParams(); - for (const [key, value] of Object.entries(params)) { - if (value !== undefined) { - qs.set(key, String(value)); - } - } - const queryString = qs.toString(); - return queryString ? `${url}?${queryString}` : url; - } - - private async request(path: string, opts: RequestOptions & { method?: string; body?: BodyInit | null } = {}): Promise { - const { params, timeout, ...fetchOpts } = opts; - const url = this.buildUrl(path, params); - const effectiveTimeout = timeout ?? this.defaultTimeout; - - // Build the init object - let init: RequestInit = { ...fetchOpts }; - - // Run request interceptors - for (const interceptor of this.requestInterceptors) { - init = await interceptor(url, init); - } - - // Timeout via AbortController (merged with caller-supplied signal) - const timeoutCtrl = new AbortController(); - const callerSignal = init.signal; - - // If caller already passed a signal, listen for its abort - if (callerSignal) { - if (callerSignal.aborted) { - timeoutCtrl.abort(callerSignal.reason); - } else { - callerSignal.addEventListener('abort', () => timeoutCtrl.abort(callerSignal.reason), { once: true }); - } - } - - const timer = effectiveTimeout > 0 - ? setTimeout(() => timeoutCtrl.abort(new DOMException('Request timed out', 'TimeoutError')), effectiveTimeout) - : undefined; - - init.signal = timeoutCtrl.signal; - - try { - let response = await fetch(url, init); - - // Run response interceptors - for (const interceptor of this.responseInterceptors) { - response = await interceptor(response); - } - - return response; - } finally { - if (timer !== undefined) clearTimeout(timer); - } - } -} - -/* ------------------------------------------------------------------ */ -/* Singleton instance with default interceptors */ -/* ------------------------------------------------------------------ */ - -const httpClient = new HttpClient('/api'); - -/** - * Client for Azure platform endpoints (/.auth/me, etc.) — no base URL prefix. - * Shares the same interceptor pattern but targets the host root. - */ -export const platformClient = new HttpClient('', 10_000); - -// ---- request interceptor: auth headers ---- -httpClient.onRequest(async (_url, init) => { - const headers = new Headers(init.headers); - - // Attach userId from Redux store (lazy import to avoid circular deps). - // Falls back to 'anonymous' if store isn't ready yet. - try { - const { store } = await import('../store/store'); - const state = store?.getState?.(); - const userId: string = state?.app?.userId ?? 'anonymous'; - headers.set('X-Ms-Client-Principal-Id', userId); - } catch { - headers.set('X-Ms-Client-Principal-Id', 'anonymous'); - } - - return { ...init, headers }; -}); - -// ---- response interceptor: uniform error handling ---- -httpClient.onResponse((response) => { - if (!response.ok) { - // Don't throw for streaming endpoints — callers handle those manually. - // Clone so the body remains readable for callers that want custom handling. - const cloned = response.clone(); - console.error( - `[httpClient] ${response.status} ${response.statusText} – ${cloned.url}`, - ); - } - return response; -}); - -export default httpClient; diff --git a/content-gen/src/app/frontend/src/api/index.ts b/content-gen/src/app/frontend/src/api/index.ts deleted file mode 100644 index b0c229ce9..000000000 --- a/content-gen/src/app/frontend/src/api/index.ts +++ /dev/null @@ -1,258 +0,0 @@ -/** - * API service for interacting with the Content Generation backend - */ - -import type { - CreativeBrief, - Product, - AgentResponse, - ParsedBriefResponse, - AppConfig, -} from '../types'; -import httpClient from './httpClient'; -export { default as httpClient } from './httpClient'; -import { getGenerationStage } from '../utils'; - -/** Normalize optional userId to a safe fallback. */ -function normalizeUserId(userId?: string): string { - return userId || 'anonymous'; -} - -/** - * Get application configuration including feature flags - */ -export async function getAppConfig(): Promise { - return httpClient.get('/config'); -} - -/** - * Parse a free-text creative brief into structured format - */ -export async function parseBrief( - briefText: string, - conversationId?: string, - userId?: string, - signal?: AbortSignal -): Promise { - return httpClient.post('/chat', { - message: briefText, - conversation_id: conversationId, - user_id: normalizeUserId(userId), - }, { signal }); -} - -/** - * Confirm a parsed creative brief - */ -export async function confirmBrief( - brief: CreativeBrief, - conversationId?: string, - userId?: string -): Promise<{ status: string; conversation_id: string; brief: CreativeBrief }> { - return httpClient.post('/brief/confirm', { - brief, - conversation_id: conversationId, - user_id: normalizeUserId(userId), - }); -} - -/** - * Select or modify products via natural language - */ -export async function selectProducts( - request: string, - currentProducts: Product[], - conversationId?: string, - userId?: string, - signal?: AbortSignal -): Promise<{ products: Product[]; action: string; message: string; conversation_id: string }> { - return httpClient.post('/chat', { - message: request, - current_products: currentProducts, - conversation_id: conversationId, - user_id: normalizeUserId(userId), - }, { signal }); -} - -/** - * Stream chat messages from the agent orchestration. - * - * Note: The /chat endpoint returns JSON (not SSE), so we perform a standard - * POST request and yield the single AgentResponse result. - */ -export async function* streamChat( - message: string, - conversationId?: string, - userId?: string, - signal?: AbortSignal -): AsyncGenerator { - const result = await httpClient.post( - '/chat', - { - message, - conversation_id: conversationId, - user_id: normalizeUserId(userId), - }, - { signal }, - ); - - // Preserve async-iterator interface by yielding the single JSON response. - yield result; -} - -/** - * Generate content from a confirmed brief - */ -export async function* streamGenerateContent( - brief: CreativeBrief, - products?: Product[], - generateImages: boolean = true, - conversationId?: string, - userId?: string, - signal?: AbortSignal -): AsyncGenerator { - // Use polling-based approach for reliability with long-running tasks - const startData = await httpClient.post<{ task_id: string }>('/generate/start', { - brief, - products: products || [], - generate_images: generateImages, - conversation_id: conversationId, - user_id: normalizeUserId(userId), - }, { signal }); - const taskId = startData.task_id; - - // Yield initial status - yield { - type: 'status', - content: 'Generation started...', - is_final: false, - } as AgentResponse; - - // Poll for completion - let attempts = 0; - const maxAttempts = 600; // 10 minutes max with 1-second polling (image generation can take 3-5 min) - const pollInterval = 1000; // 1 second - - while (attempts < maxAttempts) { - // Check if cancelled before waiting - if (signal?.aborted) { - throw new DOMException('Generation cancelled by user', 'AbortError'); - } - - await new Promise(resolve => setTimeout(resolve, pollInterval)); - attempts++; - - // Check if cancelled after waiting - if (signal?.aborted) { - throw new DOMException('Generation cancelled by user', 'AbortError'); - } - - try { - const statusData = await httpClient.get<{ status: string; result?: unknown; error?: string }>( - `/generate/status/${taskId}`, - { signal }, - ); - - if (statusData.status === 'completed') { - // Yield the final result - yield { - type: 'agent_response', - content: JSON.stringify(statusData.result), - is_final: true, - } as AgentResponse; - return; - } else if (statusData.status === 'failed') { - throw new Error(statusData.error || 'Generation failed'); - } else if (statusData.status === 'running') { - const elapsedSeconds = attempts; - const { stage, message: stageMessage } = getGenerationStage(elapsedSeconds); - - // Send status update every second for smoother progress - yield { - type: 'heartbeat', - content: stageMessage, - count: stage, - elapsed: elapsedSeconds, - is_final: false, - } as AgentResponse; - } - } catch (error) { - // Continue polling on transient errors - if (attempts >= maxAttempts) { - throw error; - } - } - } - - throw new Error('Generation timed out after 10 minutes'); -} -/** - * Regenerate image with a modification request - * Used when user wants to change the generated image after initial content generation - */ -export async function* streamRegenerateImage( - modificationRequest: string, - _brief: CreativeBrief, - products?: Product[], - _previousImagePrompt?: string, - conversationId?: string, - userId?: string, - signal?: AbortSignal -): AsyncGenerator { - // Image regeneration uses the unified /chat endpoint with MODIFY_IMAGE intent, - // which returns a task_id for polling via /generate/status. - const startData = await httpClient.post<{ action_type: string; data: { task_id: string; poll_url: string }; conversation_id: string }>( - '/chat', - { - message: modificationRequest, - conversation_id: conversationId, - user_id: normalizeUserId(userId), - selected_products: products || [], - has_generated_content: true, - }, - { signal }, - ); - - const taskId = startData.data?.task_id; - if (!taskId) { - // If no task_id, the response is the final result itself - yield { type: 'agent_response', content: JSON.stringify(startData), is_final: true } as AgentResponse; - return; - } - - yield { type: 'status', content: 'Regeneration started...', is_final: false } as AgentResponse; - - let attempts = 0; - const maxAttempts = 600; - const pollInterval = 1000; - - while (attempts < maxAttempts) { - if (signal?.aborted) { - throw new DOMException('Regeneration cancelled by user', 'AbortError'); - } - - await new Promise(resolve => setTimeout(resolve, pollInterval)); - attempts++; - - if (signal?.aborted) { - throw new DOMException('Regeneration cancelled by user', 'AbortError'); - } - - const statusData = await httpClient.get<{ status: string; result?: unknown; error?: string }>( - `/generate/status/${taskId}`, - { signal }, - ); - - if (statusData.status === 'completed') { - yield { type: 'agent_response', content: JSON.stringify(statusData.result), is_final: true } as AgentResponse; - return; - } else if (statusData.status === 'failed') { - throw new Error(statusData.error || 'Regeneration failed'); - } else { - const { stage, message: stageMessage } = getGenerationStage(attempts); - yield { type: 'heartbeat', content: stageMessage, count: stage, elapsed: attempts, is_final: false } as AgentResponse; - } - } - - throw new Error('Regeneration timed out after 10 minutes'); -} \ No newline at end of file diff --git a/content-gen/src/app/frontend/src/hooks/index.ts b/content-gen/src/app/frontend/src/hooks/index.ts deleted file mode 100644 index f23f96430..000000000 --- a/content-gen/src/app/frontend/src/hooks/index.ts +++ /dev/null @@ -1,10 +0,0 @@ -/** - * Barrel export for all custom hooks. - * Import hooks from '../hooks' instead of individual files. - */ -export { useAutoScroll } from './useAutoScroll'; -export { useChatOrchestrator } from './useChatOrchestrator'; -export { useContentGeneration } from './useContentGeneration'; -export { useConversationActions } from './useConversationActions'; -export { useCopyToClipboard } from './useCopyToClipboard'; -export { useWindowSize } from './useWindowSize'; diff --git a/content-gen/src/app/frontend/src/hooks/useAutoScroll.ts b/content-gen/src/app/frontend/src/hooks/useAutoScroll.ts deleted file mode 100644 index 8b1a2a9d1..000000000 --- a/content-gen/src/app/frontend/src/hooks/useAutoScroll.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { useEffect, useRef } from 'react'; - -/** - * Scrolls a sentinel element into view whenever any dependency changes. - * - * @param deps - React dependency list that triggers the scroll. - * @returns A ref to attach to a zero-height element at the bottom of the - * scrollable container (the "scroll anchor"). - * - * @example - * ```tsx - * const endRef = useAutoScroll([messages, isLoading]); - * return ( - *
    - * {messages.map(m => )} - *
    - *
    - * ); - * ``` - */ -export function useAutoScroll(deps: React.DependencyList) { - const endRef = useRef(null); - - // eslint-disable-next-line react-hooks/exhaustive-deps - useEffect(() => { - endRef.current?.scrollIntoView({ behavior: 'smooth' }); - }, deps); - - return endRef; -} diff --git a/content-gen/src/app/frontend/src/hooks/useChatOrchestrator.ts b/content-gen/src/app/frontend/src/hooks/useChatOrchestrator.ts deleted file mode 100644 index f4edc088c..000000000 --- a/content-gen/src/app/frontend/src/hooks/useChatOrchestrator.ts +++ /dev/null @@ -1,449 +0,0 @@ -import { useCallback, type MutableRefObject } from 'react'; - -import type { AgentResponse, GeneratedContent } from '../types'; -import { createMessage, createErrorMessage, matchesAnyKeyword, createNameSwapper } from '../utils'; -import { - useAppDispatch, - useAppSelector, - selectConversationId, - selectUserId, - selectPendingBrief, - selectConfirmedBrief, - selectAwaitingClarification, - selectSelectedProducts, - selectAvailableProducts, - selectGeneratedContent, - addMessage, - setIsLoading, - setGenerationStatus, - GenerationStatus, - setPendingBrief, - setConfirmedBrief, - setAwaitingClarification, - setSelectedProducts, - setGeneratedContent, - incrementHistoryRefresh, - selectConversationTitle, - setConversationTitle, -} from '../store'; -import type { AppDispatch } from '../store'; - -/* ------------------------------------------------------------------ */ -/* Shared helper — consumes a streamChat generator and dispatches */ -/* the final assistant message. Used by branches 1-b, 3-b, 4-b. */ -/* ------------------------------------------------------------------ */ - -async function consumeStreamChat( - stream: AsyncGenerator, - dispatch: AppDispatch, -): Promise { - let fullContent = ''; - let currentAgent = ''; - let messageAdded = false; - - for await (const response of stream) { - if (response.type === 'agent_response') { - fullContent = response.content; - currentAgent = response.agent || ''; - if ((response.is_final || response.requires_user_input) && !messageAdded) { - dispatch(addMessage(createMessage('assistant', fullContent, currentAgent))); - messageAdded = true; - } - } else if (response.type === 'error') { - dispatch( - addMessage( - createMessage( - 'assistant', - response.content || 'An error occurred while processing your request.', - ), - ), - ); - messageAdded = true; - } - } -} - -/* ------------------------------------------------------------------ */ -/* Hook */ -/* ------------------------------------------------------------------ */ - -/** - * Orchestrates the entire "send a message" flow. - * - * Depending on the current conversation phase it will: - * - refine a pending brief (PlanningAgent) - * - answer a general question while a brief is pending (streamChat) - * - forward a product-selection request (ProductAgent) - * - regenerate an image (ImageAgent) - * - parse a new creative brief (PlanningAgent) - * - fall through to generic chat (streamChat) - * - * All Redux reads/writes happen inside the hook so the caller is kept - * thin and declarative. - * - * @param abortControllerRef Shared ref that lets the parent (or sibling - * hooks) cancel the in-flight request. - * @returns `{ sendMessage }` — the callback to wire into `ChatPanel`. - */ -export function useChatOrchestrator( - abortControllerRef: MutableRefObject, -) { - const dispatch = useAppDispatch(); - const conversationId = useAppSelector(selectConversationId); - const userId = useAppSelector(selectUserId); - const pendingBrief = useAppSelector(selectPendingBrief); - const confirmedBrief = useAppSelector(selectConfirmedBrief); - const awaitingClarification = useAppSelector(selectAwaitingClarification); - const selectedProducts = useAppSelector(selectSelectedProducts); - const availableProducts = useAppSelector(selectAvailableProducts); - const generatedContent = useAppSelector(selectGeneratedContent); - const conversationTitle = useAppSelector(selectConversationTitle); - - const sendMessage = useCallback( - async (content: string) => { - dispatch(addMessage(createMessage('user', content))); - dispatch(setIsLoading(true)); - - // Create new abort controller for this request - abortControllerRef.current = new AbortController(); - const signal = abortControllerRef.current.signal; - - try { - // Dynamic imports to keep the initial bundle lean - const { streamChat, parseBrief, selectProducts } = await import( - '../api' - ); - - /* ---------------------------------------------------------- */ - /* Branch 1 – pending brief, not yet confirmed */ - /* ---------------------------------------------------------- */ - if (pendingBrief && !confirmedBrief) { - const refinementKeywords = [ - 'change', 'update', 'modify', 'add', 'remove', 'delete', - 'set', 'make', 'should be', - ] as const; - const isRefinement = matchesAnyKeyword(content, refinementKeywords); - - if (isRefinement || awaitingClarification) { - // --- 1-a Refine the brief -------------------------------- - const refinementPrompt = `Current creative brief:\n${JSON.stringify(pendingBrief, null, 2)}\n\nUser requested change: ${content}\n\nPlease update the brief accordingly and return the complete updated brief.`; - - dispatch(setGenerationStatus(GenerationStatus.UPDATING_BRIEF)); - const parsed = await parseBrief( - refinementPrompt, - conversationId, - userId, - signal, - ); - - if (parsed.generated_title && !conversationTitle) { - dispatch(setConversationTitle(parsed.generated_title)); - } - - if (parsed.brief) { - dispatch(setPendingBrief(parsed.brief)); - } - - if (parsed.requires_clarification && parsed.clarifying_questions) { - dispatch(setAwaitingClarification(true)); - dispatch(setGenerationStatus(GenerationStatus.IDLE)); - dispatch( - addMessage( - createMessage('assistant', parsed.clarifying_questions, 'PlanningAgent'), - ), - ); - } else { - dispatch(setAwaitingClarification(false)); - dispatch(setGenerationStatus(GenerationStatus.IDLE)); - dispatch( - addMessage( - createMessage( - 'assistant', - "I've updated the brief based on your feedback. Please review the changes above. Let me know if you'd like any other modifications, or click **Confirm Brief** when you're satisfied.", - 'PlanningAgent', - ), - ), - ); - } - } else { - // --- 1-b General question while brief is pending ----------- - dispatch(setGenerationStatus(GenerationStatus.PROCESSING_QUESTION)); - await consumeStreamChat( - streamChat(content, conversationId, userId, signal), - dispatch, - ); - dispatch(setGenerationStatus(GenerationStatus.IDLE)); - } - - /* ---------------------------------------------------------- */ - /* Branch 2 – brief confirmed, in product selection */ - /* ---------------------------------------------------------- */ - } else if (confirmedBrief && !generatedContent) { - dispatch(setGenerationStatus(GenerationStatus.FINDING_PRODUCTS)); - const result = await selectProducts( - content, - selectedProducts, - conversationId, - userId, - signal, - ); - dispatch(setSelectedProducts(result.products || [])); - dispatch(setGenerationStatus(GenerationStatus.IDLE)); - dispatch( - addMessage( - createMessage( - 'assistant', - result.message || 'Products updated.', - 'ProductAgent', - ), - ), - ); - - /* ---------------------------------------------------------- */ - /* Branch 3 – content generated, post-generation phase */ - /* ---------------------------------------------------------- */ - } else if (generatedContent && confirmedBrief) { - const imageModificationKeywords = [ - 'change', 'modify', 'update', 'replace', 'show', 'display', - 'use', 'instead', 'different', 'another', 'make it', 'make the', - 'kitchen', 'dining', 'living', 'bedroom', 'bathroom', 'outdoor', - 'office', 'room', 'scene', 'setting', 'background', 'style', - 'color', 'lighting', - ] as const; - const isImageModification = matchesAnyKeyword(content, imageModificationKeywords); - - if (isImageModification) { - // --- 3-a Regenerate image -------------------------------- - const { streamRegenerateImage } = await import('../api'); - dispatch( - setGenerationStatus(GenerationStatus.REGENERATING_IMAGE), - ); - - let responseData: GeneratedContent | null = null; - let messageContent = ''; - - // Detect if user mentions a different product - const mentionedProduct = availableProducts.find((p) => - content.toLowerCase().includes(p.product_name.toLowerCase()), - ); - const productsForRequest = mentionedProduct - ? [mentionedProduct] - : selectedProducts; - - const previousPrompt = - generatedContent.image_content?.prompt_used; - - for await (const response of streamRegenerateImage( - content, - confirmedBrief, - productsForRequest, - previousPrompt, - conversationId, - userId, - signal, - )) { - if (response.type === 'heartbeat') { - dispatch( - setGenerationStatus({ - status: GenerationStatus.POLLING, - label: response.message || 'Regenerating image...', - }), - ); - } else if ( - response.type === 'agent_response' && - response.is_final - ) { - try { - const parsedContent = JSON.parse(response.content); - - if ( - parsedContent.image_url || - parsedContent.image_base64 - ) { - // Replace old product name in text_content when switching - const swapName = createNameSwapper( - selectedProducts[0]?.product_name, - mentionedProduct?.product_name, - ); - const tc = generatedContent.text_content; - - responseData = { - ...generatedContent, - text_content: mentionedProduct - ? { - ...tc, - headline: swapName?.(tc?.headline) ?? tc?.headline, - body: swapName?.(tc?.body) ?? tc?.body, - tagline: swapName?.(tc?.tagline) ?? tc?.tagline, - cta_text: swapName?.(tc?.cta_text) ?? tc?.cta_text, - } - : tc, - image_content: { - ...generatedContent.image_content, - image_url: - parsedContent.image_url || - generatedContent.image_content?.image_url, - image_base64: parsedContent.image_base64, - prompt_used: - parsedContent.image_prompt || - generatedContent.image_content?.prompt_used, - }, - }; - dispatch(setGeneratedContent(responseData)); - - if (mentionedProduct) { - dispatch(setSelectedProducts([mentionedProduct])); - } - - // Update confirmed brief to include the modification - const updatedBrief = { - ...confirmedBrief, - visual_guidelines: `${confirmedBrief.visual_guidelines}. User modification: ${content}`, - }; - dispatch(setConfirmedBrief(updatedBrief)); - - messageContent = - parsedContent.message || - 'Image regenerated with your requested changes.'; - } else if (parsedContent.error) { - messageContent = parsedContent.error; - } else { - messageContent = - parsedContent.message || 'I processed your request.'; - } - } catch { - messageContent = - response.content || 'Image regenerated.'; - } - } else if (response.type === 'error') { - messageContent = - response.content || - 'An error occurred while regenerating the image.'; - } - } - - dispatch(setGenerationStatus(GenerationStatus.IDLE)); - dispatch( - addMessage(createMessage('assistant', messageContent, 'ImageAgent')), - ); - } else { - // --- 3-b General question after content generation -------- - dispatch(setGenerationStatus(GenerationStatus.PROCESSING_REQUEST)); - await consumeStreamChat( - streamChat(content, conversationId, userId, signal), - dispatch, - ); - dispatch(setGenerationStatus(GenerationStatus.IDLE)); - } - - /* ---------------------------------------------------------- */ - /* Branch 4 – default: initial flow */ - /* ---------------------------------------------------------- */ - } else { - const briefKeywords = [ - 'campaign', 'marketing', 'target audience', 'objective', - 'deliverable', - ] as const; - const isBriefLike = matchesAnyKeyword(content, briefKeywords); - - if (isBriefLike && !confirmedBrief) { - // --- 4-a Parse as creative brief -------------------------- - dispatch(setGenerationStatus(GenerationStatus.ANALYZING_BRIEF)); - const parsed = await parseBrief( - content, - conversationId, - userId, - signal, - ); - - if (parsed.generated_title && !conversationTitle) { - dispatch(setConversationTitle(parsed.generated_title)); - } - - if (parsed.rai_blocked) { - dispatch(setGenerationStatus(GenerationStatus.IDLE)); - dispatch( - addMessage( - createMessage('assistant', parsed.message, 'ContentSafety'), - ), - ); - } else if ( - parsed.requires_clarification && - parsed.clarifying_questions - ) { - if (parsed.brief) { - dispatch(setPendingBrief(parsed.brief)); - } - dispatch(setAwaitingClarification(true)); - dispatch(setGenerationStatus(GenerationStatus.IDLE)); - dispatch( - addMessage( - createMessage( - 'assistant', - parsed.clarifying_questions, - 'PlanningAgent', - ), - ), - ); - } else { - if (parsed.brief) { - dispatch(setPendingBrief(parsed.brief)); - } - dispatch(setAwaitingClarification(false)); - dispatch(setGenerationStatus(GenerationStatus.IDLE)); - dispatch( - addMessage( - createMessage( - 'assistant', - "I've parsed your creative brief. Please review the details below and let me know if you'd like to make any changes. You can say things like \"change the target audience to...\" or \"add a call to action...\". When everything looks good, click **Confirm Brief** to proceed.", - 'PlanningAgent', - ), - ), - ); - } - } else { - // --- 4-b Generic chat ----------------------------------- - dispatch(setGenerationStatus(GenerationStatus.PROCESSING_REQUEST)); - await consumeStreamChat( - streamChat(content, conversationId, userId, signal), - dispatch, - ); - dispatch(setGenerationStatus(GenerationStatus.IDLE)); - } - } - } catch (error) { - if (error instanceof Error && error.name === 'AbortError') { - dispatch(addMessage(createMessage('assistant', 'Generation stopped.'))); - } else { - dispatch( - addMessage( - createErrorMessage( - 'Sorry, there was an error processing your request. Please try again.', - ), - ), - ); - } - } finally { - dispatch(setIsLoading(false)); - dispatch(setGenerationStatus(GenerationStatus.IDLE)); - abortControllerRef.current = null; - dispatch(incrementHistoryRefresh()); - } - }, - [ - conversationId, - userId, - confirmedBrief, - pendingBrief, - selectedProducts, - generatedContent, - availableProducts, - dispatch, - awaitingClarification, - conversationTitle, - abortControllerRef, - ], - ); - - return { sendMessage }; -} diff --git a/content-gen/src/app/frontend/src/hooks/useContentGeneration.ts b/content-gen/src/app/frontend/src/hooks/useContentGeneration.ts deleted file mode 100644 index 895c20025..000000000 --- a/content-gen/src/app/frontend/src/hooks/useContentGeneration.ts +++ /dev/null @@ -1,111 +0,0 @@ -import { useCallback, type MutableRefObject } from 'react'; - -import { createMessage, createErrorMessage, buildGeneratedContent } from '../utils'; -import { - useAppDispatch, - useAppSelector, - selectConfirmedBrief, - selectSelectedProducts, - selectConversationId, - selectUserId, - addMessage, - setIsLoading, - setGenerationStatus, - GenerationStatus, - setGeneratedContent, -} from '../store'; - -/** - * Handles the full content-generation lifecycle (start → poll → result) - * and exposes a way to abort the in-flight request. - * - * @param abortControllerRef Shared ref so the UI can cancel either - * chat-orchestration **or** content-generation with one button. - */ -export function useContentGeneration( - abortControllerRef: MutableRefObject, -) { - const dispatch = useAppDispatch(); - const confirmedBrief = useAppSelector(selectConfirmedBrief); - const selectedProducts = useAppSelector(selectSelectedProducts); - const conversationId = useAppSelector(selectConversationId); - const userId = useAppSelector(selectUserId); - - /** Kick off polling-based content generation. */ - const generateContent = useCallback(async () => { - if (!confirmedBrief) return; - - dispatch(setIsLoading(true)); - dispatch(setGenerationStatus(GenerationStatus.STARTING_GENERATION)); - - abortControllerRef.current = new AbortController(); - const signal = abortControllerRef.current.signal; - - try { - const { streamGenerateContent } = await import('../api'); - - for await (const response of streamGenerateContent( - confirmedBrief, - selectedProducts, - true, - conversationId, - userId, - signal, - )) { - // Heartbeat → update the status bar - if (response.type === 'heartbeat') { - const statusMessage = response.content || 'Generating content...'; - const elapsed = (response as { elapsed?: number }).elapsed || 0; - dispatch(setGenerationStatus({ - status: GenerationStatus.POLLING, - label: `${statusMessage} (${elapsed}s)`, - })); - continue; - } - - if (response.is_final && response.type !== 'error') { - dispatch(setGenerationStatus(GenerationStatus.PROCESSING_RESULTS)); - try { - const rawContent = JSON.parse(response.content); - const genContent = buildGeneratedContent(rawContent); - dispatch(setGeneratedContent(genContent)); - dispatch(setGenerationStatus(GenerationStatus.IDLE)); - } catch { - // Content parse failure — non-critical, generation result may be malformed - } - } else if (response.type === 'error') { - dispatch(setGenerationStatus(GenerationStatus.IDLE)); - dispatch(addMessage(createErrorMessage( - `Error generating content: ${response.content}`, - ))); - } - } - } catch (error) { - if (error instanceof Error && error.name === 'AbortError') { - dispatch(addMessage(createMessage('assistant', 'Content generation stopped.'))); - } else { - dispatch(addMessage(createErrorMessage( - 'Sorry, there was an error generating content. Please try again.', - ))); - } - } finally { - dispatch(setIsLoading(false)); - dispatch(setGenerationStatus(GenerationStatus.IDLE)); - abortControllerRef.current = null; - } - }, [ - confirmedBrief, - selectedProducts, - conversationId, - dispatch, - userId, - abortControllerRef, - ]); - - /** Abort whichever request is currently in-flight. */ - const stopGeneration = useCallback(() => { - abortControllerRef.current?.abort(); - }, [abortControllerRef]); - - return { generateContent, stopGeneration }; -} diff --git a/content-gen/src/app/frontend/src/hooks/useConversationActions.ts b/content-gen/src/app/frontend/src/hooks/useConversationActions.ts deleted file mode 100644 index 5dd8e87b9..000000000 --- a/content-gen/src/app/frontend/src/hooks/useConversationActions.ts +++ /dev/null @@ -1,217 +0,0 @@ -import { useCallback } from 'react'; - -import type { ChatMessage, Product, CreativeBrief } from '../types'; -import { createMessage, buildGeneratedContent } from '../utils'; -import { httpClient } from '../api'; -import { - useAppDispatch, - useAppSelector, - selectUserId, - selectConversationId, - selectPendingBrief, - selectSelectedProducts, - resetChat, - resetContent, - setConversationId, - setConversationTitle, - setMessages, - addMessage, - setPendingBrief, - setConfirmedBrief, - setAwaitingClarification, - setSelectedProducts, - setAvailableProducts, - setGeneratedContent, - toggleChatHistory, -} from '../store'; - -/* ------------------------------------------------------------------ */ -/* Hook */ -/* ------------------------------------------------------------------ */ - -/** - * Encapsulates every conversation-level user action: - * - * - Loading a saved conversation from history - * - Starting a brand-new conversation - * - Confirming / cancelling a creative brief - * - Starting over with products - * - Toggling a product selection - * - Toggling the chat-history sidebar - * - * All Redux reads/writes are internal so the consumer stays declarative. - */ -export function useConversationActions() { - const dispatch = useAppDispatch(); - const userId = useAppSelector(selectUserId); - const conversationId = useAppSelector(selectConversationId); - const pendingBrief = useAppSelector(selectPendingBrief); - const selectedProducts = useAppSelector(selectSelectedProducts); - - /* ------------------------------------------------------------ */ - /* Select (load) a conversation from history */ - /* ------------------------------------------------------------ */ - const selectConversation = useCallback( - async (selectedConversationId: string) => { - try { - const data = await httpClient.get<{ - messages?: { - role: string; - content: string; - timestamp?: string; - agent?: string; - }[]; - brief?: unknown; - generated_content?: Record; - }>(`/conversations/${selectedConversationId}`, { - params: { user_id: userId }, - }); - - dispatch(setConversationId(selectedConversationId)); - dispatch(setConversationTitle(null)); // Will use title from conversation list - - const loadedMessages: ChatMessage[] = (data.messages || []).map( - (m, index) => ({ - id: `${selectedConversationId}-${index}`, - role: m.role as 'user' | 'assistant', - content: m.content, - timestamp: m.timestamp || new Date().toISOString(), - agent: m.agent, - }), - ); - dispatch(setMessages(loadedMessages)); - dispatch(setPendingBrief(null)); - dispatch(setAwaitingClarification(false)); - dispatch( - setConfirmedBrief( - (data.brief as CreativeBrief) || null, - ), - ); - - // Restore availableProducts so product/color name detection works - // when regenerating images in a restored conversation - if (data.brief) { - try { - const productsData = await httpClient.get<{ - products?: Product[]; - }>('/products'); - dispatch(setAvailableProducts(productsData.products || [])); - } catch { - // Non-critical — product load failure for restored conversation - } - } - - if (data.generated_content) { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const gc = data.generated_content as any; - const restoredContent = buildGeneratedContent(gc, true); - dispatch(setGeneratedContent(restoredContent)); - - if ( - gc.selected_products && - Array.isArray(gc.selected_products) - ) { - dispatch(setSelectedProducts(gc.selected_products)); - } else { - dispatch(setSelectedProducts([])); - } - } else { - dispatch(setGeneratedContent(null)); - dispatch(setSelectedProducts([])); - } - } catch { - // Error loading conversation — swallowed silently - } - }, - [userId, dispatch], - ); - - /* ------------------------------------------------------------ */ - /* Start a new conversation */ - /* ------------------------------------------------------------ */ - const newConversation = useCallback(() => { - dispatch(resetChat(undefined)); - dispatch(resetContent()); - }, [dispatch]); - - /* ------------------------------------------------------------ */ - /* Brief lifecycle */ - /* ------------------------------------------------------------ */ - const confirmBrief = useCallback(async () => { - if (!pendingBrief) return; - - try { - const { confirmBrief: confirmBriefApi } = await import('../api'); - await confirmBriefApi(pendingBrief, conversationId, userId); - dispatch(setConfirmedBrief(pendingBrief)); - dispatch(setPendingBrief(null)); - dispatch(setAwaitingClarification(false)); - - const productsData = await httpClient.get<{ products?: Product[] }>( - '/products', - ); - dispatch(setAvailableProducts(productsData.products || [])); - - dispatch( - addMessage( - createMessage( - 'assistant', - "Great! Your creative brief has been confirmed. Here are the available products for your campaign. Select the ones you'd like to feature, or tell me what you're looking for.", - 'ProductAgent', - ), - ), - ); - } catch { - // Error confirming brief — swallowed silently - } - }, [conversationId, userId, pendingBrief, dispatch]); - - const cancelBrief = useCallback(() => { - dispatch(setPendingBrief(null)); - dispatch(setAwaitingClarification(false)); - dispatch( - addMessage( - createMessage( - 'assistant', - 'No problem. Please provide your creative brief again or ask me any questions.', - ), - ), - ); - }, [dispatch]); - - /* ------------------------------------------------------------ */ - /* Product actions */ - /* ------------------------------------------------------------ */ - const selectProduct = useCallback( - (product: Product) => { - const isSelected = selectedProducts.some( - (p) => - (p.sku || p.product_name) === - (product.sku || product.product_name), - ); - if (isSelected) { - dispatch(setSelectedProducts([])); - } else { - // Single selection mode — replace any existing selection - dispatch(setSelectedProducts([product])); - } - }, - [selectedProducts, dispatch], - ); - - /* ------------------------------------------------------------ */ - /* Sidebar toggle */ - /* ------------------------------------------------------------ */ - const toggleHistory = useCallback(() => { - dispatch(toggleChatHistory()); - }, [dispatch]); - - return { - selectConversation, - newConversation, - confirmBrief, - cancelBrief, - selectProduct, - toggleHistory, - }; -} diff --git a/content-gen/src/app/frontend/src/hooks/useCopyToClipboard.ts b/content-gen/src/app/frontend/src/hooks/useCopyToClipboard.ts deleted file mode 100644 index 5887c9532..000000000 --- a/content-gen/src/app/frontend/src/hooks/useCopyToClipboard.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { useState, useCallback, useRef, useEffect } from 'react'; - -/** - * Copy text to the clipboard and expose a transient `copied` flag. - * - * @param resetTimeout - Milliseconds before `copied` resets to `false` (default 2 000). - * @returns `{ copied, copy }` — `copy(text)` writes to the clipboard and - * flips `copied` to `true` for `resetTimeout` ms. - */ -export function useCopyToClipboard(resetTimeout = 2000) { - const [copied, setCopied] = useState(false); - const timerRef = useRef>(); - - const copy = useCallback( - (text: string) => { - navigator.clipboard.writeText(text).catch(() => { - // Clipboard write failure — non-critical - }); - setCopied(true); - clearTimeout(timerRef.current); - timerRef.current = setTimeout(() => setCopied(false), resetTimeout); - }, - [resetTimeout], - ); - - // Cleanup on unmount - useEffect(() => { - return () => clearTimeout(timerRef.current); - }, []); - - return { copied, copy }; -} diff --git a/content-gen/src/app/frontend/src/hooks/useWindowSize.ts b/content-gen/src/app/frontend/src/hooks/useWindowSize.ts deleted file mode 100644 index da662eb41..000000000 --- a/content-gen/src/app/frontend/src/hooks/useWindowSize.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { useState, useEffect } from 'react'; - -/** - * Returns the current window inner-width, updating on resize. - * Falls back to 1200 during SSR. - */ -export function useWindowSize(): number { - const [windowWidth, setWindowWidth] = useState( - typeof window !== 'undefined' ? window.innerWidth : 1200, - ); - - useEffect(() => { - const handleResize = () => setWindowWidth(window.innerWidth); - window.addEventListener('resize', handleResize); - return () => window.removeEventListener('resize', handleResize); - }, []); - - return windowWidth; -} diff --git a/content-gen/src/app/frontend/src/store/appSlice.ts b/content-gen/src/app/frontend/src/store/appSlice.ts deleted file mode 100644 index 952b6ae1e..000000000 --- a/content-gen/src/app/frontend/src/store/appSlice.ts +++ /dev/null @@ -1,151 +0,0 @@ -/** - * App slice — application-level state (user info, config, feature flags, UI toggles). - * createSlice + createAsyncThunk replaces manual dispatch + string constants. - * Granular selectors — each component subscribes only to the state it needs. - */ -import { createSlice, createAsyncThunk, type PayloadAction } from '@reduxjs/toolkit'; - -/* ------------------------------------------------------------------ */ -/* Generation-status enum */ -/* ------------------------------------------------------------------ */ - -/** - * Finite set of generation-status values. Components that read - * `generationStatus` can compare against these constants instead of - * relying on magic strings. - * - * `IDLE` means "no status to display". Every other member maps to a - * user-facing label via {@link GENERATION_STATUS_LABELS}. - */ -export enum GenerationStatus { - IDLE = '', - UPDATING_BRIEF = 'UPDATING_BRIEF', - PROCESSING_QUESTION = 'PROCESSING_QUESTION', - FINDING_PRODUCTS = 'FINDING_PRODUCTS', - REGENERATING_IMAGE = 'REGENERATING_IMAGE', - PROCESSING_REQUEST = 'PROCESSING_REQUEST', - ANALYZING_BRIEF = 'ANALYZING_BRIEF', - STARTING_GENERATION = 'STARTING_GENERATION', - PROCESSING_RESULTS = 'PROCESSING_RESULTS', - /** Used for heartbeat polling where the label is dynamic. */ - POLLING = 'POLLING', -} - -/** Display strings shown in the UI for each status. */ -const GENERATION_STATUS_LABELS: Record = { - [GenerationStatus.IDLE]: '', - [GenerationStatus.UPDATING_BRIEF]: 'Updating creative brief...', - [GenerationStatus.PROCESSING_QUESTION]: 'Processing your question...', - [GenerationStatus.FINDING_PRODUCTS]: 'Finding products...', - [GenerationStatus.REGENERATING_IMAGE]: 'Regenerating image with your changes...', - [GenerationStatus.PROCESSING_REQUEST]: 'Processing your request...', - [GenerationStatus.ANALYZING_BRIEF]: 'Analyzing creative brief...', - [GenerationStatus.STARTING_GENERATION]: 'Starting content generation...', - [GenerationStatus.PROCESSING_RESULTS]: 'Processing results...', - [GenerationStatus.POLLING]: 'Generating content...', -}; - -/* ------------------------------------------------------------------ */ -/* Async Thunks */ -/* ------------------------------------------------------------------ */ - -export const fetchAppConfig = createAsyncThunk( - 'app/fetchAppConfig', - async () => { - const { getAppConfig } = await import('../api'); - const config = await getAppConfig(); - return config; - }, -); - -export const fetchUserInfo = createAsyncThunk( - 'app/fetchUserInfo', - async () => { - const { platformClient } = await import('../api/httpClient'); - const response = await platformClient.raw('/.auth/me'); - if (!response.ok) return { userId: 'anonymous', userName: '' }; - - const payload = await response.json(); - const claims: { typ: string; val: string }[] = payload[0]?.user_claims || []; - - const objectId = claims.find( - (c) => c.typ === 'http://schemas.microsoft.com/identity/claims/objectidentifier', - )?.val || 'anonymous'; - - const name = claims.find((c) => c.typ === 'name')?.val || ''; - - return { userId: objectId, userName: name }; - }, -); - -/* ------------------------------------------------------------------ */ -/* Slice */ -/* ------------------------------------------------------------------ */ - -interface AppState { - userId: string; - userName: string; - isLoading: boolean; - imageGenerationEnabled: boolean; - showChatHistory: boolean; - /** Current generation status enum value. */ - generationStatus: GenerationStatus; - /** Dynamic label override (used with GenerationStatus.POLLING). */ - generationStatusLabel: string; -} - -const initialState: AppState = { - userId: '', - userName: '', - isLoading: false, - imageGenerationEnabled: true, - showChatHistory: true, - generationStatus: GenerationStatus.IDLE, - generationStatusLabel: '', -}; - -const appSlice = createSlice({ - name: 'app', - initialState, - reducers: { - setIsLoading(state, action: PayloadAction) { - state.isLoading = action.payload; - }, - setGenerationStatus( - state, - action: PayloadAction, - ) { - if (typeof action.payload === 'string') { - state.generationStatus = action.payload; - state.generationStatusLabel = GENERATION_STATUS_LABELS[action.payload]; - } else { - state.generationStatus = action.payload.status; - state.generationStatusLabel = action.payload.label; - } - }, - toggleChatHistory(state) { - state.showChatHistory = !state.showChatHistory; - }, - }, - extraReducers: (builder) => { - builder - .addCase(fetchAppConfig.fulfilled, (state, action) => { - state.imageGenerationEnabled = action.payload.enable_image_generation; - }) - .addCase(fetchAppConfig.rejected, (state) => { - state.imageGenerationEnabled = true; // default when fetch fails - }) - .addCase(fetchUserInfo.fulfilled, (state, action) => { - state.userId = action.payload.userId; - state.userName = action.payload.userName; - }) - .addCase(fetchUserInfo.rejected, (state) => { - state.userId = 'anonymous'; - state.userName = ''; - }); - }, -}); - -export const { setIsLoading, setGenerationStatus, toggleChatHistory } = - appSlice.actions; -export default appSlice.reducer; diff --git a/content-gen/src/app/frontend/src/store/chatHistorySlice.ts b/content-gen/src/app/frontend/src/store/chatHistorySlice.ts deleted file mode 100644 index b97b14e31..000000000 --- a/content-gen/src/app/frontend/src/store/chatHistorySlice.ts +++ /dev/null @@ -1,127 +0,0 @@ -/** - * Chat history slice — conversation list CRUD via async thunks. - * createAsyncThunk replaces inline fetch + manual state updates in ChatHistory.tsx. - * Granular selectors for each piece of history state. - */ -import { createSlice, createAsyncThunk, type PayloadAction } from '@reduxjs/toolkit'; -import { httpClient } from '../api'; - -export interface ConversationSummary { - id: string; - title: string; - lastMessage: string; - timestamp: string; - messageCount: number; -} - -interface ChatHistoryState { - conversations: ConversationSummary[]; - isLoading: boolean; - error: string | null; - showAll: boolean; - isClearAllDialogOpen: boolean; - isClearing: boolean; -} - -const initialState: ChatHistoryState = { - conversations: [], - isLoading: true, - error: null, - showAll: false, - isClearAllDialogOpen: false, - isClearing: false, -}; - -/* ------------------------------------------------------------------ */ -/* Async Thunks */ -/* ------------------------------------------------------------------ */ - -export const fetchConversations = createAsyncThunk( - 'chatHistory/fetchConversations', - async () => { - const data = await httpClient.get<{ conversations?: ConversationSummary[] }>('/conversations'); - return (data.conversations || []) as ConversationSummary[]; - }, -); - -export const deleteConversation = createAsyncThunk( - 'chatHistory/deleteConversation', - async (conversationId: string) => { - await httpClient.delete(`/conversations/${conversationId}`); - return conversationId; - }, -); - -export const renameConversation = createAsyncThunk( - 'chatHistory/renameConversation', - async ({ conversationId, newTitle }: { conversationId: string; newTitle: string }) => { - await httpClient.put(`/conversations/${conversationId}`, { title: newTitle }); - return { conversationId, newTitle }; - }, -); - -export const clearAllConversations = createAsyncThunk( - 'chatHistory/clearAllConversations', - async () => { - await httpClient.delete('/conversations'); - }, -); - -/* ------------------------------------------------------------------ */ -/* Slice */ -/* ------------------------------------------------------------------ */ - -const chatHistorySlice = createSlice({ - name: 'chatHistory', - initialState, - reducers: { - setShowAll(state, action: PayloadAction) { - state.showAll = action.payload; - }, - setIsClearAllDialogOpen(state, action: PayloadAction) { - state.isClearAllDialogOpen = action.payload; - }, - }, - extraReducers: (builder) => { - builder - // Fetch - .addCase(fetchConversations.pending, (state) => { - state.isLoading = true; - state.error = null; - }) - .addCase(fetchConversations.fulfilled, (state, action) => { - state.conversations = action.payload; - state.isLoading = false; - }) - .addCase(fetchConversations.rejected, (state) => { - state.error = 'Unable to load conversation history'; - state.conversations = []; - state.isLoading = false; - }) - // Delete single - .addCase(deleteConversation.fulfilled, (state, action) => { - state.conversations = state.conversations.filter((c) => c.id !== action.payload); - }) - // Rename - .addCase(renameConversation.fulfilled, (state, action) => { - const conv = state.conversations.find((c) => c.id === action.payload.conversationId); - if (conv) conv.title = action.payload.newTitle; - }) - // Clear all - .addCase(clearAllConversations.pending, (state) => { - state.isClearing = true; - }) - .addCase(clearAllConversations.fulfilled, (state) => { - state.conversations = []; - state.isClearing = false; - state.isClearAllDialogOpen = false; - }) - .addCase(clearAllConversations.rejected, (state) => { - state.isClearing = false; - }); - }, -}); - -export const { setShowAll, setIsClearAllDialogOpen } = - chatHistorySlice.actions; -export default chatHistorySlice.reducer; diff --git a/content-gen/src/app/frontend/src/store/chatSlice.ts b/content-gen/src/app/frontend/src/store/chatSlice.ts deleted file mode 100644 index 71b25330e..000000000 --- a/content-gen/src/app/frontend/src/store/chatSlice.ts +++ /dev/null @@ -1,67 +0,0 @@ -/** - * Chat slice — conversation state, messages, clarification flow. - * Typed createSlice replaces scattered useState-based state in App.tsx. - * Granular selectors for each piece of chat state. - */ -import { createSlice, type PayloadAction } from '@reduxjs/toolkit'; -import { v4 as uuidv4 } from 'uuid'; -import type { ChatMessage } from '../types'; - -interface ChatState { - conversationId: string; - conversationTitle: string | null; - messages: ChatMessage[]; - awaitingClarification: boolean; - historyRefreshTrigger: number; -} - -const initialState: ChatState = { - conversationId: uuidv4(), - conversationTitle: null, - messages: [], - awaitingClarification: false, - historyRefreshTrigger: 0, -}; - -const chatSlice = createSlice({ - name: 'chat', - initialState, - reducers: { - setConversationId(state, action: PayloadAction) { - state.conversationId = action.payload; - }, - setConversationTitle(state, action: PayloadAction) { - state.conversationTitle = action.payload; - }, - setMessages(state, action: PayloadAction) { - state.messages = action.payload; - }, - addMessage(state, action: PayloadAction) { - state.messages.push(action.payload); - }, - setAwaitingClarification(state, action: PayloadAction) { - state.awaitingClarification = action.payload; - }, - incrementHistoryRefresh(state) { - state.historyRefreshTrigger += 1; - }, - /** Reset chat to a fresh conversation. Optionally provide a new ID. */ - resetChat(state, action: PayloadAction) { - state.conversationId = action.payload ?? uuidv4(); - state.conversationTitle = null; - state.messages = []; - state.awaitingClarification = false; - }, - }, -}); - -export const { - setConversationId, - setConversationTitle, - setMessages, - addMessage, - setAwaitingClarification, - incrementHistoryRefresh, - resetChat, -} = chatSlice.actions; -export default chatSlice.reducer; diff --git a/content-gen/src/app/frontend/src/store/contentSlice.ts b/content-gen/src/app/frontend/src/store/contentSlice.ts deleted file mode 100644 index 15736efd5..000000000 --- a/content-gen/src/app/frontend/src/store/contentSlice.ts +++ /dev/null @@ -1,61 +0,0 @@ -/** - * Content slice — creative brief, product selection, generated content. - * Typed createSlice with granular selectors. - */ -import { createSlice, type PayloadAction } from '@reduxjs/toolkit'; -import type { CreativeBrief, Product, GeneratedContent } from '../types'; - -interface ContentState { - pendingBrief: CreativeBrief | null; - confirmedBrief: CreativeBrief | null; - selectedProducts: Product[]; - availableProducts: Product[]; - generatedContent: GeneratedContent | null; -} - -const initialState: ContentState = { - pendingBrief: null, - confirmedBrief: null, - selectedProducts: [], - availableProducts: [], - generatedContent: null, -}; - -const contentSlice = createSlice({ - name: 'content', - initialState, - reducers: { - setPendingBrief(state, action: PayloadAction) { - state.pendingBrief = action.payload; - }, - setConfirmedBrief(state, action: PayloadAction) { - state.confirmedBrief = action.payload; - }, - setSelectedProducts(state, action: PayloadAction) { - state.selectedProducts = action.payload; - }, - setAvailableProducts(state, action: PayloadAction) { - state.availableProducts = action.payload; - }, - setGeneratedContent(state, action: PayloadAction) { - state.generatedContent = action.payload; - }, - resetContent(state) { - state.pendingBrief = null; - state.confirmedBrief = null; - state.selectedProducts = []; - state.availableProducts = []; - state.generatedContent = null; - }, - }, -}); - -export const { - setPendingBrief, - setConfirmedBrief, - setSelectedProducts, - setAvailableProducts, - setGeneratedContent, - resetContent, -} = contentSlice.actions; -export default contentSlice.reducer; diff --git a/content-gen/src/app/frontend/src/store/hooks.ts b/content-gen/src/app/frontend/src/store/hooks.ts deleted file mode 100644 index c9c663095..000000000 --- a/content-gen/src/app/frontend/src/store/hooks.ts +++ /dev/null @@ -1,9 +0,0 @@ -/** - * Typed Redux hooks for type-safe store access throughout the app. - * Use useAppDispatch and useAppSelector instead of raw useDispatch/useSelector. - */ -import { useDispatch, useSelector } from 'react-redux'; -import type { RootState, AppDispatch } from './store'; - -export const useAppDispatch = useDispatch.withTypes(); -export const useAppSelector = useSelector.withTypes(); diff --git a/content-gen/src/app/frontend/src/store/index.ts b/content-gen/src/app/frontend/src/store/index.ts deleted file mode 100644 index 1a7a98623..000000000 --- a/content-gen/src/app/frontend/src/store/index.ts +++ /dev/null @@ -1,75 +0,0 @@ -/** - * Barrel export for the Redux store. - * Import everything you need from '../store'. - */ -export { store } from './store'; -export type { RootState, AppDispatch } from './store'; -export { useAppDispatch, useAppSelector } from './hooks'; - -// App slice – actions, thunks & enums -export { - fetchAppConfig, - fetchUserInfo, - setIsLoading, - setGenerationStatus, - toggleChatHistory, - GenerationStatus, -} from './appSlice'; - -// Chat slice – actions -export { - setConversationId, - setConversationTitle, - setMessages, - addMessage, - setAwaitingClarification, - incrementHistoryRefresh, - resetChat, -} from './chatSlice'; - -// Content slice – actions -export { - setPendingBrief, - setConfirmedBrief, - setSelectedProducts, - setAvailableProducts, - setGeneratedContent, - resetContent, -} from './contentSlice'; - -// Chat History slice – actions & thunks -export { - fetchConversations, - deleteConversation, - renameConversation, - clearAllConversations, - setShowAll, - setIsClearAllDialogOpen, -} from './chatHistorySlice'; -export type { ConversationSummary } from './chatHistorySlice'; - -// All selectors (centralized to avoid circular store ↔ slice imports) -export { - selectUserId, - selectUserName, - selectIsLoading, - selectGenerationStatusLabel, - selectImageGenerationEnabled, - selectShowChatHistory, - selectConversationId, - selectConversationTitle, - selectMessages, - selectAwaitingClarification, - selectHistoryRefreshTrigger, - selectPendingBrief, - selectConfirmedBrief, - selectSelectedProducts, - selectAvailableProducts, - selectGeneratedContent, - selectConversations, - selectIsHistoryLoading, - selectHistoryError, - selectShowAll, - selectIsClearAllDialogOpen, - selectIsClearing, -} from './selectors'; diff --git a/content-gen/src/app/frontend/src/store/selectors.ts b/content-gen/src/app/frontend/src/store/selectors.ts deleted file mode 100644 index a837844c5..000000000 --- a/content-gen/src/app/frontend/src/store/selectors.ts +++ /dev/null @@ -1,36 +0,0 @@ -/** - * All Redux selectors in one place. - * Importing RootState here (and ONLY here) avoids the circular dependency - * between store.ts ↔ slice files that confuses VS Code's TypeScript server. - */ -import type { RootState } from './store'; - -/* ---- App selectors ---- */ -export const selectUserId = (state: RootState) => state.app.userId; -export const selectUserName = (state: RootState) => state.app.userName; -export const selectIsLoading = (state: RootState) => state.app.isLoading; -export const selectGenerationStatusLabel = (state: RootState) => state.app.generationStatusLabel; -export const selectImageGenerationEnabled = (state: RootState) => state.app.imageGenerationEnabled; -export const selectShowChatHistory = (state: RootState) => state.app.showChatHistory; - -/* ---- Chat selectors ---- */ -export const selectConversationId = (state: RootState) => state.chat.conversationId; -export const selectConversationTitle = (state: RootState) => state.chat.conversationTitle; -export const selectMessages = (state: RootState) => state.chat.messages; -export const selectAwaitingClarification = (state: RootState) => state.chat.awaitingClarification; -export const selectHistoryRefreshTrigger = (state: RootState) => state.chat.historyRefreshTrigger; - -/* ---- Content selectors ---- */ -export const selectPendingBrief = (state: RootState) => state.content.pendingBrief; -export const selectConfirmedBrief = (state: RootState) => state.content.confirmedBrief; -export const selectSelectedProducts = (state: RootState) => state.content.selectedProducts; -export const selectAvailableProducts = (state: RootState) => state.content.availableProducts; -export const selectGeneratedContent = (state: RootState) => state.content.generatedContent; - -/* ---- Chat History selectors ---- */ -export const selectConversations = (state: RootState) => state.chatHistory.conversations; -export const selectIsHistoryLoading = (state: RootState) => state.chatHistory.isLoading; -export const selectHistoryError = (state: RootState) => state.chatHistory.error; -export const selectShowAll = (state: RootState) => state.chatHistory.showAll; -export const selectIsClearAllDialogOpen = (state: RootState) => state.chatHistory.isClearAllDialogOpen; -export const selectIsClearing = (state: RootState) => state.chatHistory.isClearing; diff --git a/content-gen/src/app/frontend/src/store/store.ts b/content-gen/src/app/frontend/src/store/store.ts deleted file mode 100644 index 81e515742..000000000 --- a/content-gen/src/app/frontend/src/store/store.ts +++ /dev/null @@ -1,21 +0,0 @@ -/** - * Redux store — central state for the application. - * configureStore combines all domain-specific slices. - */ -import { configureStore } from '@reduxjs/toolkit'; -import appReducer from './appSlice'; -import chatReducer from './chatSlice'; -import contentReducer from './contentSlice'; -import chatHistoryReducer from './chatHistorySlice'; - -export const store = configureStore({ - reducer: { - app: appReducer, - chat: chatReducer, - content: contentReducer, - chatHistory: chatHistoryReducer, - }, -}); - -export type RootState = ReturnType; -export type AppDispatch = typeof store.dispatch; diff --git a/content-gen/src/app/frontend/src/utils/briefFields.ts b/content-gen/src/app/frontend/src/utils/briefFields.ts deleted file mode 100644 index beb44a88e..000000000 --- a/content-gen/src/app/frontend/src/utils/briefFields.ts +++ /dev/null @@ -1,46 +0,0 @@ -/** - * Brief-field metadata shared between BriefReview and ConfirmedBriefView. - * - * Eliminates the duplicated field-label arrays. - */ -import type { CreativeBrief } from '../types'; - -/** - * Canonical map from `CreativeBrief` keys to user-friendly labels. - * Used by BriefReview (completeness gauges) and ConfirmedBriefView. - */ -export const BRIEF_FIELD_LABELS: Record = { - overview: 'Overview', - objectives: 'Objectives', - target_audience: 'Target Audience', - key_message: 'Key Message', - tone_and_style: 'Tone & Style', - deliverable: 'Deliverable', - timelines: 'Timelines', - visual_guidelines: 'Visual Guidelines', - cta: 'Call to Action', -}; - -/** - * Display order for brief fields in review UIs. - * - * The first element in each tuple is the `CreativeBrief` key, the second - * is the UI label (which may differ slightly from `BRIEF_FIELD_LABELS` - * for contextual reasons, e.g. "Campaign Objective" vs "Overview"). - */ -export const BRIEF_DISPLAY_ORDER: { key: keyof CreativeBrief; label: string }[] = [ - { key: 'overview', label: 'Campaign Objective' }, - { key: 'objectives', label: 'Objectives' }, - { key: 'target_audience', label: 'Target Audience' }, - { key: 'key_message', label: 'Key Message' }, - { key: 'tone_and_style', label: 'Tone & Style' }, - { key: 'visual_guidelines', label: 'Visual Guidelines' }, - { key: 'deliverable', label: 'Deliverables' }, - { key: 'timelines', label: 'Timelines' }, - { key: 'cta', label: 'Call to Action' }, -]; - -/** - * The canonical list of all nine brief field keys, in display order. - */ -export const BRIEF_FIELD_KEYS: (keyof CreativeBrief)[] = BRIEF_DISPLAY_ORDER.map((f) => f.key); diff --git a/content-gen/src/app/frontend/src/utils/contentErrors.ts b/content-gen/src/app/frontend/src/utils/contentErrors.ts deleted file mode 100644 index dc9ca497a..000000000 --- a/content-gen/src/app/frontend/src/utils/contentErrors.ts +++ /dev/null @@ -1,31 +0,0 @@ -/** - * Detect whether an error message originates from a content-safety filter. - */ -export function isContentFilterError(errorMessage?: string): boolean { - if (!errorMessage) return false; - const filterPatterns = [ - 'content_filter', 'ContentFilter', 'content management policy', - 'ResponsibleAI', 'responsible_ai_policy', 'content filtering', - 'filtered', 'safety system', 'self_harm', 'sexual', 'violence', 'hate', - ]; - return filterPatterns.some((pattern) => - errorMessage.toLowerCase().includes(pattern.toLowerCase()), - ); -} - -/** - * Return a user-friendly title/description for a generation error. - */ -export function getErrorMessage(errorMessage?: string): { title: string; description: string } { - if (isContentFilterError(errorMessage)) { - return { - title: 'Content Filtered', - description: - 'Your request was blocked by content safety filters. Please try modifying your creative brief.', - }; - } - return { - title: 'Generation Failed', - description: errorMessage || 'An error occurred. Please try again.', - }; -} diff --git a/content-gen/src/app/frontend/src/utils/contentParsing.ts b/content-gen/src/app/frontend/src/utils/contentParsing.ts deleted file mode 100644 index e59ac85e3..000000000 --- a/content-gen/src/app/frontend/src/utils/contentParsing.ts +++ /dev/null @@ -1,108 +0,0 @@ -/** - * Content parsing utilities — raw API response → typed domain objects. - * - * Centralizes the duplicated `textContent` string-to-object parsing, - * image URL resolution (blob rewriting, base64 fallback), and the - * `GeneratedContent` assembly that was copy-pasted across - * useContentGeneration, useConversationActions, and useChatOrchestrator. - */ -import type { GeneratedContent } from '../types'; - -/* ------------------------------------------------------------------ */ -/* Internal helpers (not exported — reduces public API surface) */ -/* ------------------------------------------------------------------ */ - -/** - * Rewrite Azure Blob Storage URLs to the application's proxy endpoint - * so the browser can fetch images without CORS issues. - */ -function rewriteBlobUrl(url: string): string { - if (!url.includes('blob.core.windows.net')) return url; - const parts = url.split('/'); - const filename = parts[parts.length - 1]; - const convId = parts[parts.length - 2]; - return `/api/images/${convId}/${filename}`; -} - -/* ------------------------------------------------------------------ */ -/* Parsing helpers (module-internal — not re-exported) */ -/* ------------------------------------------------------------------ */ - -/** - * Parse `text_content` which may arrive as a JSON string or an object. - * Returns an object with known fields, or `undefined` if unusable. - */ -function parseTextContent( - raw: unknown, -): { headline?: string; body?: string; cta_text?: string; tagline?: string } | undefined { - let textContent = raw; - - if (typeof textContent === 'string') { - try { - textContent = JSON.parse(textContent); - } catch { - // Not valid JSON — treat as unusable - return undefined; - } - } - - if (typeof textContent !== 'object' || textContent === null) return undefined; - - const tc = textContent as Record; - return { - headline: tc.headline as string | undefined, - body: tc.body as string | undefined, - cta_text: (tc.cta_text ?? tc.cta) as string | undefined, - tagline: tc.tagline as string | undefined, - }; -} - -/** - * Resolve the best available image URL from a raw API response. - * - * Priority: explicit `image_url` (with blob rewrite) → base64 data URI. - * Pass `rewriteBlobs: true` (default) when restoring from a saved - * conversation; `false` when the response just came from the live API. - */ -function resolveImageUrl( - raw: { image_url?: string; image_base64?: string }, - rewriteBlobs = false, -): string | undefined { - let url = raw.image_url; - if (url && rewriteBlobs) { - url = rewriteBlobUrl(url); - } - if (url) return url; - if (raw.image_base64) return `data:image/png;base64,${raw.image_base64}`; - return undefined; -} - -/** - * Build a fully-typed `GeneratedContent` from an arbitrary raw API payload. - * - * @param raw The parsed JSON object from the backend. - * @param rewriteBlobs Pass `true` when restoring from a saved conversation - * so Azure Blob URLs get proxied. - */ -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export function buildGeneratedContent(raw: any, rewriteBlobs = false): GeneratedContent { - const textContent = parseTextContent(raw.text_content); - const imageUrl = resolveImageUrl(raw, rewriteBlobs); - - return { - text_content: textContent, - image_content: - imageUrl || raw.image_prompt - ? { - image_url: imageUrl, - prompt_used: raw.image_prompt, - alt_text: raw.image_revised_prompt || 'Generated marketing image', - } - : undefined, - violations: raw.violations || [], - requires_modification: raw.requires_modification || false, - error: raw.error, - image_error: raw.image_error, - text_error: raw.text_error, - }; -} diff --git a/content-gen/src/app/frontend/src/utils/downloadImage.ts b/content-gen/src/app/frontend/src/utils/downloadImage.ts deleted file mode 100644 index 08e752c20..000000000 --- a/content-gen/src/app/frontend/src/utils/downloadImage.ts +++ /dev/null @@ -1,94 +0,0 @@ -/** - * Download the generated marketing image with a product-name / tagline - * banner composited at the bottom. - * - * Falls back to a plain download when canvas compositing fails. - */ -export async function downloadImage( - imageUrl: string, - productName?: string, - tagline?: string, -): Promise { - try { - const canvas = document.createElement('canvas'); - const ctx = canvas.getContext('2d'); - if (!ctx) return; - - const img = new Image(); - img.crossOrigin = 'anonymous'; - - img.onload = () => { - const bannerHeight = Math.max(60, img.height * 0.1); - const padding = Math.max(16, img.width * 0.03); - - canvas.width = img.width; - canvas.height = img.height + bannerHeight; - - // Draw the image at the top - ctx.drawImage(img, 0, 0); - - // White banner at the bottom - ctx.fillStyle = '#ffffff'; - ctx.fillRect(0, img.height, img.width, bannerHeight); - - ctx.strokeStyle = '#e5e5e5'; - ctx.lineWidth = 1; - ctx.beginPath(); - ctx.moveTo(0, img.height); - ctx.lineTo(img.width, img.height); - ctx.stroke(); - - // Headline text - const headlineText = productName || 'Your Product'; - const headlineFontSize = Math.max(18, Math.min(36, img.width * 0.04)); - const taglineFontSize = Math.max(12, Math.min(20, img.width * 0.025)); - - ctx.font = `600 ${headlineFontSize}px Georgia, serif`; - ctx.fillStyle = '#1a1a1a'; - ctx.fillText( - headlineText, - padding, - img.height + padding + headlineFontSize * 0.8, - img.width - padding * 2, - ); - - // Tagline - if (tagline) { - ctx.font = `400 italic ${taglineFontSize}px -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif`; - ctx.fillStyle = '#666666'; - ctx.fillText( - tagline, - padding, - img.height + padding + headlineFontSize + taglineFontSize * 0.8 + 4, - img.width - padding * 2, - ); - } - - canvas.toBlob((blob) => { - if (blob) { - const url = URL.createObjectURL(blob); - const link = document.createElement('a'); - link.href = url; - link.download = 'generated-marketing-image.png'; - link.click(); - URL.revokeObjectURL(url); - } - }, 'image/png'); - }; - - img.onerror = () => { - plainDownload(imageUrl); - }; - - img.src = imageUrl; - } catch { - plainDownload(imageUrl); - } -} - -function plainDownload(url: string) { - const link = document.createElement('a'); - link.href = url; - link.download = 'generated-image.png'; - link.click(); -} diff --git a/content-gen/src/app/frontend/src/utils/generationStages.ts b/content-gen/src/app/frontend/src/utils/generationStages.ts deleted file mode 100644 index 03399bc0d..000000000 --- a/content-gen/src/app/frontend/src/utils/generationStages.ts +++ /dev/null @@ -1,33 +0,0 @@ -/** - * Generation progress stage mapping. - * - * Pure function that converts elapsed seconds into a human-readable - * stage label + ordinal — used by the polling loop in `streamGenerateContent`. - */ - -export interface GenerationStage { - /** Ordinal stage index (0–5) for progress indicators. */ - stage: number; - /** Human-readable status message. */ - message: string; -} - -/** - * Map elapsed seconds to the current generation stage. - * - * Typical generation timeline: - * - 0 – 10 s → Briefing analysis - * - 10 – 25 s → Copy generation - * - 25 – 35 s → Image prompt creation - * - 35 – 55 s → Image generation - * - 55 – 70 s → Compliance check - * - 70 s+ → Finalizing - */ -export function getGenerationStage(elapsedSeconds: number): GenerationStage { - if (elapsedSeconds < 10) return { stage: 0, message: 'Analyzing creative brief...' }; - if (elapsedSeconds < 25) return { stage: 1, message: 'Generating marketing copy...' }; - if (elapsedSeconds < 35) return { stage: 2, message: 'Creating image prompt...' }; - if (elapsedSeconds < 55) return { stage: 3, message: 'Generating image with AI...' }; - if (elapsedSeconds < 70) return { stage: 4, message: 'Running compliance check...' }; - return { stage: 5, message: 'Finalizing content...' }; -} diff --git a/content-gen/src/app/frontend/src/utils/index.ts b/content-gen/src/app/frontend/src/utils/index.ts deleted file mode 100644 index 94ba048c3..000000000 --- a/content-gen/src/app/frontend/src/utils/index.ts +++ /dev/null @@ -1,32 +0,0 @@ -/** - * Barrel export for all utility modules. - * - * Import everything you need from '../utils'. - */ - -// Message factories & formatting -export { createMessage, createErrorMessage } from './messageUtils'; - -// Content parsing (raw API → typed domain objects) -export { buildGeneratedContent } from './contentParsing'; - -// SSE stream parser -export { parseSSEStream } from './sseParser'; - -// Generation progress stages -export { getGenerationStage } from './generationStages'; - -// Brief-field metadata -export { BRIEF_FIELD_LABELS, BRIEF_DISPLAY_ORDER, BRIEF_FIELD_KEYS } from './briefFields'; - -// String utilities -export { createNameSwapper, matchesAnyKeyword } from './stringUtils'; - -// Content error detection -export { isContentFilterError, getErrorMessage } from './contentErrors'; - -// Image download -export { downloadImage } from './downloadImage'; - -// Shared UI constants -export const AI_DISCLAIMER = 'AI-generated content may be incorrect'; diff --git a/content-gen/src/app/frontend/src/utils/messageUtils.ts b/content-gen/src/app/frontend/src/utils/messageUtils.ts deleted file mode 100644 index 45a7ea5ac..000000000 --- a/content-gen/src/app/frontend/src/utils/messageUtils.ts +++ /dev/null @@ -1,34 +0,0 @@ -/** - * Message utilities — ChatMessage factory and formatting helpers. - * - * Replaces duplicated `msg()` helpers in useChatOrchestrator and - * useConversationActions with a single, tested source of truth. - */ -import { v4 as uuidv4 } from 'uuid'; -import type { ChatMessage } from '../types'; - -/** - * Create a `ChatMessage` literal with a fresh UUID and ISO timestamp. - */ -export function createMessage( - role: 'user' | 'assistant', - content: string, - agent?: string, -): ChatMessage { - return { - id: uuidv4(), - role, - content, - agent, - timestamp: new Date().toISOString(), - }; -} - -/** - * Shorthand for creating an assistant error message. - * Consolidates the repeated `createMessage('assistant', errorText)` pattern - * used in error catch blocks across multiple hooks. - */ -export function createErrorMessage(content: string): ChatMessage { - return createMessage('assistant', content); -} diff --git a/content-gen/src/app/frontend/src/utils/sseParser.ts b/content-gen/src/app/frontend/src/utils/sseParser.ts deleted file mode 100644 index 7767c0b5e..000000000 --- a/content-gen/src/app/frontend/src/utils/sseParser.ts +++ /dev/null @@ -1,48 +0,0 @@ -/** - * SSE (Server-Sent Events) stream parser. - * - * Eliminates the duplicated TextDecoder + buffer + line-split logic - * that was copy-pasted in `streamChat` and `streamRegenerateImage`. - */ -import type { AgentResponse } from '../types'; - -/** - * Parse an SSE stream from a `ReadableStreamDefaultReader` into an - * `AsyncGenerator` of `AgentResponse` objects. - * - * Protocol assumed: - * - Events delimited by `\n\n` - * - Each event starts with `data: ` - * - `data: [DONE]` terminates the stream - * - * @param reader The reader obtained via `response.body.getReader()` - */ -export async function* parseSSEStream( - reader: ReadableStreamDefaultReader, -): AsyncGenerator { - const decoder = new TextDecoder(); - let buffer = ''; - - while (true) { - const { done, value } = await reader.read(); - if (done) break; - - buffer += decoder.decode(value, { stream: true }); - const lines = buffer.split('\n\n'); - buffer = lines.pop() || ''; - - for (const line of lines) { - if (line.startsWith('data: ')) { - const data = line.slice(6); - if (data === '[DONE]') { - return; - } - try { - yield JSON.parse(data) as AgentResponse; - } catch { - // Malformed SSE frame — skip silently - } - } - } - } -} diff --git a/content-gen/src/app/frontend/src/utils/stringUtils.ts b/content-gen/src/app/frontend/src/utils/stringUtils.ts deleted file mode 100644 index 387e07ff5..000000000 --- a/content-gen/src/app/frontend/src/utils/stringUtils.ts +++ /dev/null @@ -1,45 +0,0 @@ -/** - * String utilities — regex escaping, name swapping, keyword matching. - * - * Extracts the duplicated keyword-matching pattern and the regex-escape + - * swapName closure from useChatOrchestrator into reusable, testable functions. - */ - -/** - * Escape a string so it can be safely embedded in a `RegExp` pattern. - * @internal — only used by `createNameSwapper` within this module. - */ -function escapeRegex(str: string): string { - return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); -} - -/** - * Create a function that replaces all case-insensitive occurrences of - * `oldName` with `newName` in a string. - * - * Returns `undefined` if no swap is possible (names are the same, etc.). - */ -export function createNameSwapper( - oldName: string | undefined, - newName: string | undefined, -): ((text?: string) => string | undefined) | undefined { - if (!oldName || !newName || oldName === newName) return undefined; - - const regex = new RegExp(escapeRegex(oldName), 'gi'); - return (text?: string) => { - if (!text) return text; - return text.replace(regex, () => newName); - }; -} - -/** - * Check whether `text` contains **any** of the given keywords - * (case-insensitive substring match). - * - * Used for intent classification (brief detection, refinement detection, - * image modification detection) repeated 3× in useChatOrchestrator. - */ -export function matchesAnyKeyword(text: string, keywords: readonly string[]): boolean { - const lower = text.toLowerCase(); - return keywords.some((kw) => lower.includes(kw)); -} diff --git a/docs/AZD_DEPLOYMENT.md b/docs/AZD_DEPLOYMENT.md index f8e8c5f3c..019483934 100644 --- a/docs/AZD_DEPLOYMENT.md +++ b/docs/AZD_DEPLOYMENT.md @@ -239,7 +239,7 @@ Error: az acr build failed **Solution**: Check the Dockerfile and ensure all required files are present: ```bash # Manual build for debugging -cd src/App +cd src/app docker build -f WebApp.Dockerfile -t content-gen-app:test . ``` @@ -251,7 +251,7 @@ Error: az webapp deploy failed **Solution**: Ensure the frontend builds successfully: ```bash -cd src/App +cd src/app/frontend npm install npm run build ``` diff --git a/docs/TECHNICAL_GUIDE.md b/docs/TECHNICAL_GUIDE.md index fcedf6f38..69a28172b 100644 --- a/docs/TECHNICAL_GUIDE.md +++ b/docs/TECHNICAL_GUIDE.md @@ -142,7 +142,7 @@ pip install -r requirements.txt python app.py # Frontend -cd src/App +cd src/app/frontend npm install npm run dev ``` diff --git a/scripts/deploy.ps1 b/scripts/deploy.ps1 index 19245cc40..cc411a2f3 100644 --- a/scripts/deploy.ps1 +++ b/scripts/deploy.ps1 @@ -131,15 +131,14 @@ if ($continue -eq "y" -or $continue -eq "Y") { Write-Host "Step 3: Building and deploying frontend..." -ForegroundColor Green Write-Host "==========================================" -ForegroundColor Green - Set-Location "$ProjectDir\src\App" + Set-Location "$ProjectDir\src\frontend" npm install npm run build # Copy built files to server directory - New-Item -ItemType Directory -Force "$ProjectDir\src\App\server\static" | Out-Null - Copy-Item -Path "$ProjectDir\src\App\static\*" -Destination "$ProjectDir\src\App\server\static\" -Recurse -Force + Copy-Item -Path "$ProjectDir\src\static\*" -Destination "$ProjectDir\src\frontend-server\static\" -Recurse -Force - Set-Location "$ProjectDir\src\App\server" + Set-Location "$ProjectDir\src\frontend-server" # Create deployment package if (Test-Path "frontend-deploy.zip") { diff --git a/scripts/deploy.sh b/scripts/deploy.sh index 1883e1d00..efd2b51b9 100644 --- a/scripts/deploy.sh +++ b/scripts/deploy.sh @@ -129,15 +129,14 @@ if [[ $REPLY =~ ^[Yy]$ ]]; then echo "Step 3: Building and deploying frontend..." echo "==========================================" - cd "$PROJECT_DIR/src/App" + cd "$PROJECT_DIR/src/frontend" npm install npm run build # Copy built files to server directory - mkdir -p "$PROJECT_DIR/src/App/server/static" - cp -r "$PROJECT_DIR/src/App/static/"* "$PROJECT_DIR/src/App/server/static/" + cp -r "$PROJECT_DIR/src/static/"* "$PROJECT_DIR/src/frontend-server/static/" - cd "$PROJECT_DIR/src/App/server" + cd "$PROJECT_DIR/src/frontend-server" # Create deployment package rm -f frontend-deploy.zip diff --git a/scripts/local_dev.ps1 b/scripts/local_dev.ps1 index b4d45ba96..7319aca56 100644 --- a/scripts/local_dev.ps1 +++ b/scripts/local_dev.ps1 @@ -31,7 +31,7 @@ $ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path $ProjectRoot = Split-Path -Parent $ScriptDir $SrcDir = Join-Path $ProjectRoot "src" $BackendDir = Join-Path $SrcDir "backend" -$FrontendDir = Join-Path $SrcDir "App" +$FrontendDir = Join-Path $SrcDir "app\frontend" # Default ports $BackendPort = if ($env:BACKEND_PORT) { $env:BACKEND_PORT } else { "5000" } diff --git a/scripts/local_dev.sh b/scripts/local_dev.sh index 31b342fc8..4f1c084ac 100644 --- a/scripts/local_dev.sh +++ b/scripts/local_dev.sh @@ -34,7 +34,7 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" SRC_DIR="$PROJECT_ROOT/src" BACKEND_DIR="$SRC_DIR/backend" -FRONTEND_DIR="$SRC_DIR/App" +FRONTEND_DIR="$SRC_DIR/app/frontend" # Default ports BACKEND_PORT=${BACKEND_PORT:-5000} diff --git a/src/App/src/App.tsx b/src/App/src/App.tsx deleted file mode 100644 index 28d241c28..000000000 --- a/src/App/src/App.tsx +++ /dev/null @@ -1,82 +0,0 @@ -import { useEffect, useRef } from 'react'; - -import { ChatPanel } from './components/ChatPanel'; -import { ChatHistory } from './components/ChatHistory'; -import { AppHeader } from './components/AppHeader'; -import { - useAppDispatch, - useAppSelector, - fetchAppConfig, - fetchUserInfo, - selectUserName, - selectShowChatHistory, -} from './store'; -import { useChatOrchestrator, useContentGeneration, useConversationActions } from './hooks'; - - -function App() { - const dispatch = useAppDispatch(); - const userName = useAppSelector(selectUserName); - const showChatHistory = useAppSelector(selectShowChatHistory); - - // Shared abort controller for chat & content-generation - const abortControllerRef = useRef(null); - - // Business-logic hooks - const { sendMessage } = useChatOrchestrator(abortControllerRef); - const { generateContent, stopGeneration } = useContentGeneration(abortControllerRef); - const { - selectConversation, - newConversation, - confirmBrief, - cancelBrief, - selectProduct, - toggleHistory, - } = useConversationActions(); - - // Fetch app config & current user on mount - useEffect(() => { - dispatch(fetchAppConfig()); - dispatch(fetchUserInfo()); - }, [dispatch]); - - return ( -
    - {/* Header */} - - - {/* Main Content */} -
    - {/* Chat Panel - main area */} -
    - -
    - - {/* Chat History Sidebar - RIGHT side */} - {showChatHistory && ( -
    - -
    - )} -
    -
    - ); -} - -export default App; diff --git a/src/App/src/api/httpClient.ts b/src/App/src/api/httpClient.ts deleted file mode 100644 index 6aaf83b48..000000000 --- a/src/App/src/api/httpClient.ts +++ /dev/null @@ -1,203 +0,0 @@ -/** - * Centralized HTTP client with interceptors. - * - * - Singleton — use the default `httpClient` export everywhere. - * - Request interceptors automatically attach auth headers - * (X-Ms-Client-Principal-Id) so callers never need to remember. - * - Response interceptors provide uniform error handling. - * - Built-in query-param serialization, configurable timeout, and base URL. - */ - -/* ------------------------------------------------------------------ */ -/* Types */ -/* ------------------------------------------------------------------ */ - -/** Options accepted by every request method. */ -export interface RequestOptions extends Omit { - /** Query parameters – appended to the URL automatically. */ - params?: Record; - /** Per-request timeout in ms (default: client-level `timeout`). */ - timeout?: number; -} - -type RequestInterceptor = (url: string, init: RequestInit) => RequestInit | Promise; -type ResponseInterceptor = (response: Response) => Response | Promise; - -/* ------------------------------------------------------------------ */ -/* HttpClient */ -/* ------------------------------------------------------------------ */ - -export class HttpClient { - private baseUrl: string; - private defaultTimeout: number; - private requestInterceptors: RequestInterceptor[] = []; - private responseInterceptors: ResponseInterceptor[] = []; - - constructor(baseUrl = '', timeout = 60_000) { - this.baseUrl = baseUrl; - this.defaultTimeout = timeout; - } - - /* ---------- interceptor registration ---------- */ - - onRequest(fn: RequestInterceptor): void { - this.requestInterceptors.push(fn); - } - - onResponse(fn: ResponseInterceptor): void { - this.responseInterceptors.push(fn); - } - - /* ---------- public request helpers ---------- */ - - async get(path: string, opts: RequestOptions = {}): Promise { - const res = await this.request(path, { ...opts, method: 'GET' }); - return res.json() as Promise; - } - - async post(path: string, body?: unknown, opts: RequestOptions = {}): Promise { - const res = await this.request(path, { - ...opts, - method: 'POST', - body: body != null ? JSON.stringify(body) : undefined, - headers: { - ...(body != null ? { 'Content-Type': 'application/json' } : {}), - ...opts.headers, - }, - }); - return res.json() as Promise; - } - - async put(path: string, body?: unknown, opts: RequestOptions = {}): Promise { - const res = await this.request(path, { - ...opts, - method: 'PUT', - body: body != null ? JSON.stringify(body) : undefined, - headers: { - ...(body != null ? { 'Content-Type': 'application/json' } : {}), - ...opts.headers, - }, - }); - return res.json() as Promise; - } - - async delete(path: string, opts: RequestOptions = {}): Promise { - const res = await this.request(path, { ...opts, method: 'DELETE' }); - return res.json() as Promise; - } - - /** - * Low-level request that returns the raw `Response`. - * Useful for streaming (SSE) endpoints where the caller needs `response.body`. - */ - async raw(path: string, opts: RequestOptions & { method?: string; body?: BodyInit | null } = {}): Promise { - return this.request(path, opts); - } - - /* ---------- internal plumbing ---------- */ - - private buildUrl(path: string, params?: Record): string { - const url = `${this.baseUrl}${path}`; - if (!params) return url; - - const qs = new URLSearchParams(); - for (const [key, value] of Object.entries(params)) { - if (value !== undefined) { - qs.set(key, String(value)); - } - } - const queryString = qs.toString(); - return queryString ? `${url}?${queryString}` : url; - } - - private async request(path: string, opts: RequestOptions & { method?: string; body?: BodyInit | null } = {}): Promise { - const { params, timeout, ...fetchOpts } = opts; - const url = this.buildUrl(path, params); - const effectiveTimeout = timeout ?? this.defaultTimeout; - - // Build the init object - let init: RequestInit = { ...fetchOpts }; - - // Run request interceptors - for (const interceptor of this.requestInterceptors) { - init = await interceptor(url, init); - } - - // Timeout via AbortController (merged with caller-supplied signal) - const timeoutCtrl = new AbortController(); - const callerSignal = init.signal; - - // If caller already passed a signal, listen for its abort - if (callerSignal) { - if (callerSignal.aborted) { - timeoutCtrl.abort(callerSignal.reason); - } else { - callerSignal.addEventListener('abort', () => timeoutCtrl.abort(callerSignal.reason), { once: true }); - } - } - - const timer = effectiveTimeout > 0 - ? setTimeout(() => timeoutCtrl.abort(new DOMException('Request timed out', 'TimeoutError')), effectiveTimeout) - : undefined; - - init.signal = timeoutCtrl.signal; - - try { - let response = await fetch(url, init); - - // Run response interceptors - for (const interceptor of this.responseInterceptors) { - response = await interceptor(response); - } - - return response; - } finally { - if (timer !== undefined) clearTimeout(timer); - } - } -} - -/* ------------------------------------------------------------------ */ -/* Singleton instance with default interceptors */ -/* ------------------------------------------------------------------ */ - -const httpClient = new HttpClient('/api'); - -/** - * Client for Azure platform endpoints (/.auth/me, etc.) — no base URL prefix. - * Shares the same interceptor pattern but targets the host root. - */ -export const platformClient = new HttpClient('', 10_000); - -// ---- request interceptor: auth headers ---- -httpClient.onRequest(async (_url, init) => { - const headers = new Headers(init.headers); - - // Attach userId from Redux store (lazy import to avoid circular deps). - // Falls back to 'anonymous' if store isn't ready yet. - try { - const { store } = await import('../store/store'); - const state = store?.getState?.(); - const userId: string = state?.app?.userId ?? 'anonymous'; - headers.set('X-Ms-Client-Principal-Id', userId); - } catch { - headers.set('X-Ms-Client-Principal-Id', 'anonymous'); - } - - return { ...init, headers }; -}); - -// ---- response interceptor: uniform error handling ---- -httpClient.onResponse((response) => { - if (!response.ok) { - // Don't throw for streaming endpoints — callers handle those manually. - // Clone so the body remains readable for callers that want custom handling. - const cloned = response.clone(); - console.error( - `[httpClient] ${response.status} ${response.statusText} – ${cloned.url}`, - ); - } - return response; -}); - -export default httpClient; diff --git a/src/App/src/api/index.ts b/src/App/src/api/index.ts deleted file mode 100644 index b0c229ce9..000000000 --- a/src/App/src/api/index.ts +++ /dev/null @@ -1,258 +0,0 @@ -/** - * API service for interacting with the Content Generation backend - */ - -import type { - CreativeBrief, - Product, - AgentResponse, - ParsedBriefResponse, - AppConfig, -} from '../types'; -import httpClient from './httpClient'; -export { default as httpClient } from './httpClient'; -import { getGenerationStage } from '../utils'; - -/** Normalize optional userId to a safe fallback. */ -function normalizeUserId(userId?: string): string { - return userId || 'anonymous'; -} - -/** - * Get application configuration including feature flags - */ -export async function getAppConfig(): Promise { - return httpClient.get('/config'); -} - -/** - * Parse a free-text creative brief into structured format - */ -export async function parseBrief( - briefText: string, - conversationId?: string, - userId?: string, - signal?: AbortSignal -): Promise { - return httpClient.post('/chat', { - message: briefText, - conversation_id: conversationId, - user_id: normalizeUserId(userId), - }, { signal }); -} - -/** - * Confirm a parsed creative brief - */ -export async function confirmBrief( - brief: CreativeBrief, - conversationId?: string, - userId?: string -): Promise<{ status: string; conversation_id: string; brief: CreativeBrief }> { - return httpClient.post('/brief/confirm', { - brief, - conversation_id: conversationId, - user_id: normalizeUserId(userId), - }); -} - -/** - * Select or modify products via natural language - */ -export async function selectProducts( - request: string, - currentProducts: Product[], - conversationId?: string, - userId?: string, - signal?: AbortSignal -): Promise<{ products: Product[]; action: string; message: string; conversation_id: string }> { - return httpClient.post('/chat', { - message: request, - current_products: currentProducts, - conversation_id: conversationId, - user_id: normalizeUserId(userId), - }, { signal }); -} - -/** - * Stream chat messages from the agent orchestration. - * - * Note: The /chat endpoint returns JSON (not SSE), so we perform a standard - * POST request and yield the single AgentResponse result. - */ -export async function* streamChat( - message: string, - conversationId?: string, - userId?: string, - signal?: AbortSignal -): AsyncGenerator { - const result = await httpClient.post( - '/chat', - { - message, - conversation_id: conversationId, - user_id: normalizeUserId(userId), - }, - { signal }, - ); - - // Preserve async-iterator interface by yielding the single JSON response. - yield result; -} - -/** - * Generate content from a confirmed brief - */ -export async function* streamGenerateContent( - brief: CreativeBrief, - products?: Product[], - generateImages: boolean = true, - conversationId?: string, - userId?: string, - signal?: AbortSignal -): AsyncGenerator { - // Use polling-based approach for reliability with long-running tasks - const startData = await httpClient.post<{ task_id: string }>('/generate/start', { - brief, - products: products || [], - generate_images: generateImages, - conversation_id: conversationId, - user_id: normalizeUserId(userId), - }, { signal }); - const taskId = startData.task_id; - - // Yield initial status - yield { - type: 'status', - content: 'Generation started...', - is_final: false, - } as AgentResponse; - - // Poll for completion - let attempts = 0; - const maxAttempts = 600; // 10 minutes max with 1-second polling (image generation can take 3-5 min) - const pollInterval = 1000; // 1 second - - while (attempts < maxAttempts) { - // Check if cancelled before waiting - if (signal?.aborted) { - throw new DOMException('Generation cancelled by user', 'AbortError'); - } - - await new Promise(resolve => setTimeout(resolve, pollInterval)); - attempts++; - - // Check if cancelled after waiting - if (signal?.aborted) { - throw new DOMException('Generation cancelled by user', 'AbortError'); - } - - try { - const statusData = await httpClient.get<{ status: string; result?: unknown; error?: string }>( - `/generate/status/${taskId}`, - { signal }, - ); - - if (statusData.status === 'completed') { - // Yield the final result - yield { - type: 'agent_response', - content: JSON.stringify(statusData.result), - is_final: true, - } as AgentResponse; - return; - } else if (statusData.status === 'failed') { - throw new Error(statusData.error || 'Generation failed'); - } else if (statusData.status === 'running') { - const elapsedSeconds = attempts; - const { stage, message: stageMessage } = getGenerationStage(elapsedSeconds); - - // Send status update every second for smoother progress - yield { - type: 'heartbeat', - content: stageMessage, - count: stage, - elapsed: elapsedSeconds, - is_final: false, - } as AgentResponse; - } - } catch (error) { - // Continue polling on transient errors - if (attempts >= maxAttempts) { - throw error; - } - } - } - - throw new Error('Generation timed out after 10 minutes'); -} -/** - * Regenerate image with a modification request - * Used when user wants to change the generated image after initial content generation - */ -export async function* streamRegenerateImage( - modificationRequest: string, - _brief: CreativeBrief, - products?: Product[], - _previousImagePrompt?: string, - conversationId?: string, - userId?: string, - signal?: AbortSignal -): AsyncGenerator { - // Image regeneration uses the unified /chat endpoint with MODIFY_IMAGE intent, - // which returns a task_id for polling via /generate/status. - const startData = await httpClient.post<{ action_type: string; data: { task_id: string; poll_url: string }; conversation_id: string }>( - '/chat', - { - message: modificationRequest, - conversation_id: conversationId, - user_id: normalizeUserId(userId), - selected_products: products || [], - has_generated_content: true, - }, - { signal }, - ); - - const taskId = startData.data?.task_id; - if (!taskId) { - // If no task_id, the response is the final result itself - yield { type: 'agent_response', content: JSON.stringify(startData), is_final: true } as AgentResponse; - return; - } - - yield { type: 'status', content: 'Regeneration started...', is_final: false } as AgentResponse; - - let attempts = 0; - const maxAttempts = 600; - const pollInterval = 1000; - - while (attempts < maxAttempts) { - if (signal?.aborted) { - throw new DOMException('Regeneration cancelled by user', 'AbortError'); - } - - await new Promise(resolve => setTimeout(resolve, pollInterval)); - attempts++; - - if (signal?.aborted) { - throw new DOMException('Regeneration cancelled by user', 'AbortError'); - } - - const statusData = await httpClient.get<{ status: string; result?: unknown; error?: string }>( - `/generate/status/${taskId}`, - { signal }, - ); - - if (statusData.status === 'completed') { - yield { type: 'agent_response', content: JSON.stringify(statusData.result), is_final: true } as AgentResponse; - return; - } else if (statusData.status === 'failed') { - throw new Error(statusData.error || 'Regeneration failed'); - } else { - const { stage, message: stageMessage } = getGenerationStage(attempts); - yield { type: 'heartbeat', content: stageMessage, count: stage, elapsed: attempts, is_final: false } as AgentResponse; - } - } - - throw new Error('Regeneration timed out after 10 minutes'); -} \ No newline at end of file diff --git a/src/App/src/components/AppHeader.tsx b/src/App/src/components/AppHeader.tsx deleted file mode 100644 index 810f0a072..000000000 --- a/src/App/src/components/AppHeader.tsx +++ /dev/null @@ -1,68 +0,0 @@ -import { memo } from 'react'; -import { - Text, - Avatar, - Button, - Tooltip, - tokens, -} from '@fluentui/react-components'; -import { - History24Regular, - History24Filled, -} from '@fluentui/react-icons'; -import ContosoLogo from '../styles/images/contoso.svg'; - -export interface AppHeaderProps { - userName?: string | null; - showChatHistory: boolean; - onToggleChatHistory: () => void; -} - -/** - * Top-level application header with logo, title, history toggle and avatar. - */ -export const AppHeader = memo(function AppHeader({ userName, showChatHistory, onToggleChatHistory }: AppHeaderProps) { - return ( -
    -
    - Contoso - - Contoso - -
    - -
    - -
    -
    - ); -}); -AppHeader.displayName = 'AppHeader'; diff --git a/src/App/src/components/ChatHistory.tsx b/src/App/src/components/ChatHistory.tsx deleted file mode 100644 index c393fdec9..000000000 --- a/src/App/src/components/ChatHistory.tsx +++ /dev/null @@ -1,345 +0,0 @@ -import { useEffect, useCallback, useMemo, memo } from 'react'; -import { - Button, - Text, - Spinner, - tokens, - Link, - Menu, - MenuTrigger, - MenuPopover, - MenuList, - MenuItem, - Dialog, - DialogSurface, - DialogTitle, - DialogBody, - DialogActions, - DialogContent, -} from '@fluentui/react-components'; -import { - Chat24Regular, - MoreHorizontal20Regular, - Compose20Regular, - DismissCircle20Regular, -} from '@fluentui/react-icons'; -import { - useAppDispatch, - useAppSelector, - fetchConversations, - deleteConversation, - renameConversation, - clearAllConversations, - setShowAll as setShowAllAction, - setIsClearAllDialogOpen, - selectConversations, - selectIsHistoryLoading, - selectHistoryError, - selectShowAll, - selectIsClearAllDialogOpen, - selectIsClearing, - selectConversationId, - selectConversationTitle, - selectMessages, - selectIsLoading, - selectHistoryRefreshTrigger, -} from '../store'; -import type { ConversationSummary } from '../store'; -import { ConversationItem } from './ConversationItem'; - -interface ChatHistoryProps { - onSelectConversation: (conversationId: string) => void; - onNewConversation: () => void; -} - -export const ChatHistory = memo(function ChatHistory({ - onSelectConversation, - onNewConversation, -}: ChatHistoryProps) { - const dispatch = useAppDispatch(); - const conversations = useAppSelector(selectConversations); - const isLoading = useAppSelector(selectIsHistoryLoading); - const error = useAppSelector(selectHistoryError); - const showAll = useAppSelector(selectShowAll); - const isClearAllDialogOpen = useAppSelector(selectIsClearAllDialogOpen); - const isClearing = useAppSelector(selectIsClearing); - const currentConversationId = useAppSelector(selectConversationId); - const currentConversationTitle = useAppSelector(selectConversationTitle); - const currentMessages = useAppSelector(selectMessages); - const isGenerating = useAppSelector(selectIsLoading); - const refreshTrigger = useAppSelector(selectHistoryRefreshTrigger); - - const INITIAL_COUNT = 5; - - const handleClearAllConversations = useCallback(async () => { - try { - await dispatch(clearAllConversations()).unwrap(); - onNewConversation(); - } catch { - // Error clearing all conversations - } - }, [dispatch, onNewConversation]); - - const handleDeleteConversation = useCallback(async (conversationId: string) => { - try { - await dispatch(deleteConversation(conversationId)).unwrap(); - if (conversationId === currentConversationId) { - onNewConversation(); - } - } catch { - // Error deleting conversation - } - }, [dispatch, currentConversationId, onNewConversation]); - - const handleRenameConversation = useCallback(async (conversationId: string, newTitle: string) => { - try { - await dispatch(renameConversation({ conversationId, newTitle })).unwrap(); - } catch { - // Error renaming conversation - } - }, [dispatch]); - - useEffect(() => { - dispatch(fetchConversations()); - }, [dispatch, refreshTrigger]); - - // Reset showAll when conversations change significantly - useEffect(() => { - dispatch(setShowAllAction(false)); - }, [dispatch, refreshTrigger]); - - // Build the current session conversation summary if it has messages - const currentSessionConversation = useMemo(() => - currentMessages.length > 0 && currentConversationTitle ? { - id: currentConversationId, - title: currentConversationTitle, - lastMessage: currentMessages[currentMessages.length - 1]?.content?.substring(0, 100) || '', - timestamp: new Date().toISOString(), - messageCount: currentMessages.length, - } : null, - [currentMessages, currentConversationId, currentConversationTitle], - ); - - // Merge current session with saved conversations, updating the current one with live data - const displayConversations = useMemo(() => { - const existingIndex = conversations.findIndex(c => c.id === currentConversationId); - - if (existingIndex >= 0 && currentSessionConversation) { - const updated = [...conversations]; - updated[existingIndex] = { - ...updated[existingIndex], - messageCount: currentMessages.length, - lastMessage: currentMessages[currentMessages.length - 1]?.content?.substring(0, 100) || updated[existingIndex].lastMessage, - }; - return updated; - } else if (currentSessionConversation) { - return [currentSessionConversation, ...conversations]; - } - return conversations; - }, [conversations, currentConversationId, currentSessionConversation, currentMessages]); - - const visibleConversations = useMemo( - () => showAll ? displayConversations : displayConversations.slice(0, INITIAL_COUNT), - [showAll, displayConversations], - ); - const hasMore = displayConversations.length > INITIAL_COUNT; - - const handleRefreshConversations = useCallback(() => { - dispatch(fetchConversations()); - }, [dispatch]); - - return ( -
    -
    - - Chat History - - - - -
    - -
    - -
    - {isLoading ? ( -
    - -
    - ) : error ? ( -
    - {error} - - Retry - -
    - ) : displayConversations.length === 0 ? ( -
    - - No conversations yet -
    - ) : ( - <> - {visibleConversations.map((conversation) => ( - onSelectConversation(conversation.id)} - onDelete={handleDeleteConversation} - onRename={handleRenameConversation} - onRefresh={handleRefreshConversations} - disabled={isGenerating} - /> - ))} - - )} - -
    - {hasMore && ( - dispatch(setShowAllAction(!showAll))} - style={{ - fontSize: '13px', - color: isGenerating ? tokens.colorNeutralForegroundDisabled : tokens.colorBrandForeground1, - cursor: isGenerating ? 'not-allowed' : 'pointer', - pointerEvents: isGenerating ? 'none' : 'auto', - }} - > - {showAll ? 'Show less' : 'See all'} - - )} - - - Start new chat - -
    -
    - - {/* Clear All Confirmation Dialog */} - !isClearing && dispatch(setIsClearAllDialogOpen(data.open))}> - - Clear all chat history - - - - Are you sure you want to delete all chat history? This action cannot be undone and all conversations will be permanently removed. - - - - - - - - - -
    - ); -}); - -ChatHistory.displayName = 'ChatHistory'; diff --git a/src/App/src/components/ChatInput.tsx b/src/App/src/components/ChatInput.tsx deleted file mode 100644 index 5a3efb41f..000000000 --- a/src/App/src/components/ChatInput.tsx +++ /dev/null @@ -1,149 +0,0 @@ -import { memo, useState, useCallback } from 'react'; -import { - Button, - Text, - Tooltip, - tokens, -} from '@fluentui/react-components'; -import { AI_DISCLAIMER } from '../utils'; -import { - Send20Regular, - Add20Regular, -} from '@fluentui/react-icons'; - -export interface ChatInputProps { - /** Called with the trimmed message text when the user submits. */ - onSendMessage: (message: string) => void; - /** Called when the user clicks the "New chat" button. */ - onNewConversation?: () => void; - /** Disables the input and buttons while a request is in flight. */ - disabled?: boolean; - /** Allows the parent to drive the input value (e.g. from WelcomeCard suggestions). */ - value?: string; - /** Notifies the parent when the user types. */ - onChange?: (value: string) => void; -} - -/** - * Chat input bar with send & new-chat buttons, plus an AI disclaimer. - */ -export const ChatInput = memo(function ChatInput({ - onSendMessage, - onNewConversation, - disabled = false, - value: controlledValue, - onChange: controlledOnChange, -}: ChatInputProps) { - const [internalValue, setInternalValue] = useState(''); - - // Support both controlled & uncontrolled modes - const inputValue = controlledValue ?? internalValue; - const setInputValue = useCallback((v: string) => { - controlledOnChange?.(v); - if (controlledValue === undefined) setInternalValue(v); - }, [controlledOnChange, controlledValue]); - - const handleSubmit = useCallback((e: React.FormEvent) => { - e.preventDefault(); - if (inputValue.trim() && !disabled) { - onSendMessage(inputValue.trim()); - setInputValue(''); - } - }, [inputValue, disabled, onSendMessage, setInputValue]); - - const handleKeyDown = useCallback((e: React.KeyboardEvent) => { - if (e.key === 'Enter' && !e.shiftKey) { - e.preventDefault(); - handleSubmit(e); - } - }, [handleSubmit]); - - return ( -
    - {/* Input Box */} -
    - setInputValue(e.target.value)} - onKeyDown={handleKeyDown} - placeholder="Type a message" - disabled={disabled} - style={{ - flex: 1, - border: 'none', - outline: 'none', - backgroundColor: 'transparent', - fontFamily: 'var(--fontFamilyBase)', - fontSize: '14px', - color: tokens.colorNeutralForeground1, - }} - /> - - {/* Icons on the right */} -
    - -
    -
    - - {/* Disclaimer */} - - {AI_DISCLAIMER} - -
    - ); -}); -ChatInput.displayName = 'ChatInput'; diff --git a/src/App/src/components/ChatPanel.tsx b/src/App/src/components/ChatPanel.tsx deleted file mode 100644 index a10709cae..000000000 --- a/src/App/src/components/ChatPanel.tsx +++ /dev/null @@ -1,168 +0,0 @@ -import { useState, useCallback, memo } from 'react'; -import type { Product } from '../types'; -import { BriefReview } from './BriefReview'; -import { ConfirmedBriefView } from './ConfirmedBriefView'; -import { SelectedProductView } from './SelectedProductView'; -import { ProductReview } from './ProductReview'; -import { InlineContentPreview } from './InlineContentPreview'; -import { WelcomeCard } from './WelcomeCard'; -import { MessageBubble } from './MessageBubble'; -import { TypingIndicator } from './TypingIndicator'; -import { ChatInput } from './ChatInput'; -import { useAutoScroll } from '../hooks'; -import { - useAppSelector, - selectMessages, - selectIsLoading, - selectGenerationStatusLabel, - selectPendingBrief, - selectConfirmedBrief, - selectGeneratedContent, - selectSelectedProducts, - selectAvailableProducts, - selectImageGenerationEnabled, -} from '../store'; - -interface ChatPanelProps { - onSendMessage: (message: string) => void; - onStopGeneration?: () => void; - onBriefConfirm?: () => void; - onBriefCancel?: () => void; - onGenerateContent?: () => void; - onRegenerateContent?: () => void; - onProductSelect?: (product: Product) => void; - onNewConversation?: () => void; -} - -export const ChatPanel = memo(function ChatPanel({ - onSendMessage, - onStopGeneration, - onBriefConfirm, - onBriefCancel, - onGenerateContent, - onRegenerateContent, - onProductSelect, - onNewConversation, -}: ChatPanelProps) { - const messages = useAppSelector(selectMessages); - const isLoading = useAppSelector(selectIsLoading); - const generationStatus = useAppSelector(selectGenerationStatusLabel); - const pendingBrief = useAppSelector(selectPendingBrief); - const confirmedBrief = useAppSelector(selectConfirmedBrief); - const generatedContent = useAppSelector(selectGeneratedContent); - const selectedProducts = useAppSelector(selectSelectedProducts); - const availableProducts = useAppSelector(selectAvailableProducts); - const imageGenerationEnabled = useAppSelector(selectImageGenerationEnabled); - - const [inputValue, setInputValue] = useState(''); - - // Auto-scroll to bottom when messages or state changes - const messagesEndRef = useAutoScroll([ - messages, pendingBrief, confirmedBrief, generatedContent, isLoading, generationStatus, - ]); - - // Determine if we should show inline components - const showBriefReview = !!(pendingBrief && onBriefConfirm && onBriefCancel); - const showProductReview = !!(confirmedBrief && !generatedContent && onGenerateContent); - const showContentPreview = !!(generatedContent && onRegenerateContent); - const showWelcome = messages.length === 0 && !showBriefReview && !showProductReview && !showContentPreview; - - // Handle suggestion click from welcome card - const handleSuggestionClick = useCallback((prompt: string) => { - setInputValue(prompt); - }, []); - - return ( -
    - {/* Messages Area */} -
    - {showWelcome ? ( - - ) : ( - <> - {messages.map((message) => ( - - ))} - - {/* Brief Review - Read Only with Conversational Prompts */} - {showBriefReview && ( - - )} - - {/* Confirmed Brief View - Persistent read-only view */} - {confirmedBrief && !pendingBrief && ( - - )} - - {/* Selected Product View - Persistent read-only view after content generation */} - {generatedContent && selectedProducts.length > 0 && ( - - )} - - {/* Product Review - Conversational Product Selection */} - {showProductReview && ( - - )} - - {/* Inline Content Preview */} - {showContentPreview && ( - 0 ? selectedProducts[0] : undefined} - imageGenerationEnabled={imageGenerationEnabled} - /> - )} - - {/* Loading/Typing Indicator */} - {isLoading && ( - - )} - - )} - -
    -
    - - {/* Input Area */} - -
    - ); -}); - -ChatPanel.displayName = 'ChatPanel'; diff --git a/src/App/src/components/ComplianceSection.tsx b/src/App/src/components/ComplianceSection.tsx deleted file mode 100644 index 216a11f60..000000000 --- a/src/App/src/components/ComplianceSection.tsx +++ /dev/null @@ -1,193 +0,0 @@ -import { memo } from 'react'; -import { AI_DISCLAIMER } from '../utils'; -import { - Text, - Badge, - Button, - Tooltip, - Accordion, - AccordionItem, - AccordionHeader, - AccordionPanel, - tokens, -} from '@fluentui/react-components'; -import { - ArrowSync20Regular, - CheckmarkCircle20Regular, - Warning20Regular, - Info20Regular, - ErrorCircle20Regular, - Copy20Regular, -} from '@fluentui/react-icons'; -import type { ComplianceViolation } from '../types'; -import { ViolationCard } from './ViolationCard'; - -export interface ComplianceSectionProps { - violations: ComplianceViolation[]; - requiresModification: boolean; - /** Callback to copy generated text. */ - onCopyText: () => void; - /** Callback to regenerate content. */ - onRegenerate: () => void; - /** Whether regeneration is in progress. */ - isLoading?: boolean; - /** Whether the copy-text button shows "Copied!". */ - copied?: boolean; -} - -/** - * Compliance callout (action-needed / review-recommended), status footer - * with badges and actions, and the collapsible violations accordion. - */ -export const ComplianceSection = memo(function ComplianceSection({ - violations, - requiresModification, - onCopyText, - onRegenerate, - isLoading, - copied = false, -}: ComplianceSectionProps) { - return ( - <> - {/* User guidance callout */} - {requiresModification ? ( -
    - - Action needed: This content has compliance issues that must be - addressed before use. Please review the details in the Compliance Guidelines - section below and regenerate with modifications, or manually edit the content to - resolve the flagged items. - -
    - ) : violations.length > 0 ? ( -
    - - Optional review: This content is approved but has minor - suggestions for improvement. You can use it as-is or review the recommendations - in the Compliance Guidelines section below. - -
    - ) : null} - - {/* Footer with actions */} -
    -
    - {requiresModification ? ( - }> - Requires Modification - - ) : violations.length > 0 ? ( - }> - Review Recommended - - ) : ( - } - > - Approved - - )} -
    - -
    - -
    -
    - - {/* AI disclaimer */} - - {AI_DISCLAIMER} - - - {/* Collapsible Compliance Accordion */} - {violations.length > 0 && ( - - - -
    - {requiresModification ? ( - - ) : violations.some((v) => v.severity === 'error') ? ( - - ) : violations.some((v) => v.severity === 'warning') ? ( - - ) : ( - - )} - - Compliance Guidelines ({violations.length}{' '} - {violations.length === 1 ? 'item' : 'items'}) - -
    -
    - -
    - {violations.map((violation, index) => ( - - ))} -
    -
    -
    -
    - )} - - ); -}); -ComplianceSection.displayName = 'ComplianceSection'; diff --git a/src/App/src/components/ConversationItem.tsx b/src/App/src/components/ConversationItem.tsx deleted file mode 100644 index c40d89d1d..000000000 --- a/src/App/src/components/ConversationItem.tsx +++ /dev/null @@ -1,292 +0,0 @@ -import { memo, useState, useEffect, useRef, useCallback } from 'react'; -import { - Button, - Text, - tokens, - Menu, - MenuTrigger, - MenuPopover, - MenuList, - MenuItem, - Input, - Dialog, - DialogSurface, - DialogTitle, - DialogBody, - DialogActions, - DialogContent, -} from '@fluentui/react-components'; -import { - MoreHorizontal20Regular, - Delete20Regular, - Edit20Regular, -} from '@fluentui/react-icons'; -import type { ConversationSummary } from '../store'; - -/* ------------------------------------------------------------------ */ -/* Validation constants & helper */ -/* ------------------------------------------------------------------ */ - -const NAME_MIN_LENGTH = 5; -const NAME_MAX_LENGTH = 50; - -/** Returns an error message, or `''` when the value is valid. */ -function validateConversationName(value: string): string { - const trimmed = value.trim(); - if (trimmed === '') return 'Conversation name cannot be empty or contain only spaces'; - if (trimmed.length < NAME_MIN_LENGTH) return `Conversation name must be at least ${NAME_MIN_LENGTH} characters`; - if (value.length > NAME_MAX_LENGTH) return `Conversation name cannot exceed ${NAME_MAX_LENGTH} characters`; - if (!/[a-zA-Z0-9]/.test(trimmed)) return 'Conversation name must contain at least one letter or number'; - return ''; -} - -export interface ConversationItemProps { - conversation: ConversationSummary; - isActive: boolean; - onSelect: () => void; - onDelete: (conversationId: string) => void; - onRename: (conversationId: string, newTitle: string) => void; - onRefresh: () => void; - disabled?: boolean; -} - -/** - * A single row in the chat-history sidebar — - * title, context-menu (rename / delete) and confirmation dialogs. - */ -export const ConversationItem = memo(function ConversationItem({ - conversation, - isActive, - onSelect, - onDelete, - onRename, - onRefresh, - disabled = false, -}: ConversationItemProps) { - const [isMenuOpen, setIsMenuOpen] = useState(false); - const [isRenameDialogOpen, setIsRenameDialogOpen] = useState(false); - const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false); - const [renameValue, setRenameValue] = useState(conversation.title || ''); - const [renameError, setRenameError] = useState(''); - const renameInputRef = useRef(null); - - const handleRenameClick = useCallback(() => { - setRenameValue(conversation.title || ''); - setRenameError(''); - setIsRenameDialogOpen(true); - setIsMenuOpen(false); - }, [conversation.title]); - - const handleRenameConfirm = useCallback(async () => { - const error = validateConversationName(renameValue); - if (error) { - setRenameError(error); - return; - } - - const trimmedValue = renameValue.trim(); - if (trimmedValue === conversation.title) { - setIsRenameDialogOpen(false); - setRenameError(''); - return; - } - - await onRename(conversation.id, trimmedValue); - onRefresh(); - setIsRenameDialogOpen(false); - setRenameError(''); - }, [renameValue, conversation.id, conversation.title, onRename, onRefresh]); - - const handleDeleteClick = useCallback(() => { - setIsDeleteDialogOpen(true); - setIsMenuOpen(false); - }, []); - - const handleDeleteConfirm = useCallback(async () => { - await onDelete(conversation.id); - setIsDeleteDialogOpen(false); - }, [conversation.id, onDelete]); - - useEffect(() => { - if (isRenameDialogOpen && renameInputRef.current) { - renameInputRef.current.focus(); - renameInputRef.current.select(); - } - }, [isRenameDialogOpen]); - - return ( - <> -
    - - {conversation.title || 'Untitled'} - - - setIsMenuOpen(data.open)}> - - -
    - - {/* Rename dialog */} - setIsRenameDialogOpen(data.open)}> - - Rename conversation - - - { - const newValue = e.target.value; - setRenameValue(newValue); - setRenameError(validateConversationName(newValue)); - }} - onKeyDown={(e) => { - if (e.key === 'Enter' && renameValue.trim()) { - handleRenameConfirm(); - } else if (e.key === 'Escape') { - setIsRenameDialogOpen(false); - } - }} - placeholder="Enter conversation name" - style={{ width: '100%' }} - /> - - Maximum {NAME_MAX_LENGTH} characters ({renameValue.length}/{NAME_MAX_LENGTH}) - - {renameError && ( - - {renameError} - - )} - - - - - - - - - - {/* Delete dialog */} - setIsDeleteDialogOpen(data.open)}> - - Delete conversation - - - - Are you sure you want to delete "{conversation.title || 'Untitled'}"? This action - cannot be undone. - - - - - - - - - - - ); -}); -ConversationItem.displayName = 'ConversationItem'; diff --git a/src/App/src/components/ImagePreviewCard.tsx b/src/App/src/components/ImagePreviewCard.tsx deleted file mode 100644 index b4e1ee50a..000000000 --- a/src/App/src/components/ImagePreviewCard.tsx +++ /dev/null @@ -1,99 +0,0 @@ -import { memo } from 'react'; -import { - Button, - Text, - Tooltip, - tokens, -} from '@fluentui/react-components'; -import { ArrowDownload20Regular } from '@fluentui/react-icons'; - -export interface ImagePreviewCardProps { - imageUrl: string; - altText?: string; - productName?: string; - tagline?: string; - isSmall?: boolean; - onDownload: () => void; -} - -/** - * Image preview with download button overlay and a product-name / tagline - * text banner below the image. - */ -export const ImagePreviewCard = memo(function ImagePreviewCard({ - imageUrl, - altText = 'Generated marketing image', - productName = 'Your Product', - tagline, - isSmall = false, - onDownload, -}: ImagePreviewCardProps) { - return ( -
    - {/* Image container */} -
    - {altText} - - -
    - - {/* Text banner below image */} -
    - - {productName} - - {tagline && ( - - {tagline} - - )} -
    -
    - ); -}); -ImagePreviewCard.displayName = 'ImagePreviewCard'; diff --git a/src/App/src/components/InlineContentPreview.tsx b/src/App/src/components/InlineContentPreview.tsx deleted file mode 100644 index dc0f27491..000000000 --- a/src/App/src/components/InlineContentPreview.tsx +++ /dev/null @@ -1,210 +0,0 @@ -import { memo, useCallback, useMemo } from 'react'; -import { - Text, - Divider, - tokens, -} from '@fluentui/react-components'; -import { ShieldError20Regular } from '@fluentui/react-icons'; -import type { GeneratedContent, Product } from '../types'; -import { useWindowSize, useCopyToClipboard } from '../hooks'; -import { isContentFilterError, getErrorMessage, downloadImage } from '../utils'; -import { ImagePreviewCard } from './ImagePreviewCard'; -import { ComplianceSection } from './ComplianceSection'; - -interface InlineContentPreviewProps { - content: GeneratedContent; - onRegenerate: () => void; - isLoading?: boolean; - selectedProduct?: Product; - imageGenerationEnabled?: boolean; -} - -export const InlineContentPreview = memo(function InlineContentPreview({ - content, - onRegenerate, - isLoading, - selectedProduct, - imageGenerationEnabled = true, -}: InlineContentPreviewProps) { - const { text_content, image_content, violations, requires_modification, error, image_error, text_error } = content; - const { copied, copy } = useCopyToClipboard(); - const windowWidth = useWindowSize(); - - const isSmall = windowWidth < 768; - - const handleCopyText = useCallback(() => { - const textToCopy = [ - text_content?.headline && `✨ ${text_content.headline} ✨`, - text_content?.body, - text_content?.tagline, - ].filter(Boolean).join('\n\n'); - copy(textToCopy); - }, [text_content, copy]); - - const handleDownloadImage = useCallback(() => { - if (!image_content?.image_url) return; - downloadImage( - image_content.image_url, - selectedProduct?.product_name || text_content?.headline || 'Your Product', - text_content?.tagline, - ); - }, [image_content, selectedProduct, text_content]); - - // Get product display name - const productDisplayName = useMemo(() => { - if (selectedProduct) { - return selectedProduct.product_name; - } - return text_content?.headline || 'Your Content'; - }, [selectedProduct, text_content?.headline]); - - return ( -
    - {/* Selection confirmation */} - {selectedProduct && ( - - You selected "{selectedProduct.product_name}". Here's what I've created – let me know if you need anything changed. - - )} - - {/* Sparkle Headline - Figma style */} - {text_content?.headline && ( - - ✨ Discover the serene elegance of {productDisplayName}. - - )} - - {/* Body Copy */} - {text_content?.body && ( - - {text_content.body} - - )} - - {/* Hashtags */} - {text_content?.tagline && ( - - {text_content.tagline} - - )} - - {/* Error Banner */} - {(error || text_error) && !violations.some(v => v.message.toLowerCase().includes('filter')) && ( -
    - -
    - - {getErrorMessage(error || text_error).title} - - - {getErrorMessage(error || text_error).description} - -
    -
    - )} - - {/* Image Preview */} - {imageGenerationEnabled && image_content?.image_url && ( - - )} - - {/* Image Error State */} - {imageGenerationEnabled && !image_content?.image_url && (image_error || error) && ( -
    - - - {getErrorMessage(image_error || error).title} - - - Click Regenerate to try again - -
    - )} - - - - {/* Compliance + Footer + Accordion */} - -
    - ); -}); -InlineContentPreview.displayName = 'InlineContentPreview'; diff --git a/src/App/src/components/MessageBubble.tsx b/src/App/src/components/MessageBubble.tsx deleted file mode 100644 index d4509a0c5..000000000 --- a/src/App/src/components/MessageBubble.tsx +++ /dev/null @@ -1,120 +0,0 @@ -import { memo, useCallback } from 'react'; -import { - Text, - Badge, - Button, - Tooltip, - tokens, -} from '@fluentui/react-components'; -import { Copy20Regular } from '@fluentui/react-icons'; -import ReactMarkdown from 'react-markdown'; -import type { ChatMessage } from '../types'; -import { useCopyToClipboard } from '../hooks'; -import { AI_DISCLAIMER } from '../utils'; - -export interface MessageBubbleProps { - message: ChatMessage; -} - -/** - * Renders a single chat message — user or assistant. - * - * - User messages: right-aligned, brand-coloured bubble. - * - Assistant messages: left-aligned, full-width, with optional agent badge, - * markdown rendering, copy button and AI disclaimer. - */ -export const MessageBubble = memo(function MessageBubble({ message }: MessageBubbleProps) { - const isUser = message.role === 'user'; - const { copied, copy } = useCopyToClipboard(); - const handleCopy = useCallback(() => copy(message.content), [copy, message.content]); - - return ( -
    - {/* Agent badge for assistant messages */} - {!isUser && message.agent && ( - - {message.agent} - - )} - - {/* Message content with markdown */} -
    - {message.content} - - {/* Footer for assistant messages */} - {!isUser && ( -
    - - {AI_DISCLAIMER} - - -
    - -
    -
    - )} -
    -
    - ); -}); -MessageBubble.displayName = 'MessageBubble'; diff --git a/src/App/src/components/ProductCard.tsx b/src/App/src/components/ProductCard.tsx deleted file mode 100644 index 873ae6620..000000000 --- a/src/App/src/components/ProductCard.tsx +++ /dev/null @@ -1,136 +0,0 @@ -import { memo } from 'react'; -import { - Text, - tokens, -} from '@fluentui/react-components'; -import { Box20Regular } from '@fluentui/react-icons'; -import type { Product } from '../types'; - -export interface ProductCardProps { - product: Product; - /** Visual size variant — "normal" for product review grid, "compact" for selected-product view. */ - size?: 'normal' | 'compact'; - /** Whether the card is currently selected (shows brand border). */ - isSelected?: boolean; - /** Click handler. Omit for read-only cards. */ - onClick?: () => void; - disabled?: boolean; -} - -/** - * Reusable product card with image/placeholder, name, tags and price. - * Used by both ProductReview (selectable) and SelectedProductView (read-only). - */ -export const ProductCard = memo(function ProductCard({ - product, - size = 'normal', - isSelected = false, - onClick, - disabled = false, -}: ProductCardProps) { - const isCompact = size === 'compact'; - const imgSize = isCompact ? 56 : 80; - const isInteractive = !!onClick && !disabled; - - return ( -
    - {/* Image or placeholder */} - {product.image_url ? ( - {product.product_name} - ) : ( -
    - -
    - )} - - {/* Product info */} -
    - - {product.product_name} - - - {product.tags || product.description || 'soft white, airy, minimal, fresh'} - - - ${product.price?.toFixed(2) || '59.95'} USD - -
    -
    - ); -}); -ProductCard.displayName = 'ProductCard'; diff --git a/src/App/src/components/ProductReview.tsx b/src/App/src/components/ProductReview.tsx deleted file mode 100644 index a7fa7e9d3..000000000 --- a/src/App/src/components/ProductReview.tsx +++ /dev/null @@ -1,136 +0,0 @@ -import { memo, useMemo, useCallback } from 'react'; -import { - Button, - Text, - tokens, -} from '@fluentui/react-components'; -import { - Sparkle20Regular, -} from '@fluentui/react-icons'; -import type { Product } from '../types'; -import { AI_DISCLAIMER } from '../utils'; -import { ProductCard } from './ProductCard'; - -interface ProductReviewProps { - products: Product[]; - onConfirm: () => void; - isAwaitingResponse?: boolean; - availableProducts?: Product[]; - onProductSelect?: (product: Product) => void; - disabled?: boolean; -} - -export const ProductReview = memo(function ProductReview({ - products, - onConfirm, - isAwaitingResponse = false, - availableProducts = [], - onProductSelect, - disabled = false, -}: ProductReviewProps) { - const displayProducts = useMemo( - () => availableProducts.length > 0 ? availableProducts : products, - [availableProducts, products], - ); - const selectedProductIds = useMemo( - () => new Set(products.map(p => p.sku || p.product_name)), - [products], - ); - - const isProductSelected = useCallback((product: Product): boolean => { - return selectedProductIds.has(product.sku || product.product_name); - }, [selectedProductIds]); - - const handleProductClick = useCallback((product: Product) => { - if (onProductSelect) { - onProductSelect(product); - } - }, [onProductSelect]); - - return ( -
    -
    - - Here is the list of available paints: - -
    - - {displayProducts.length > 0 ? ( -
    - {displayProducts.map((product, index) => ( - handleProductClick(product)} - disabled={disabled} - /> - ))} -
    - ) : ( -
    - - No products available. - -
    - )} - - {displayProducts.length > 0 && ( -
    - - {products.length === 0 && ( - - Select a product to continue - - )} -
    - )} - -
    - - {AI_DISCLAIMER} - -
    -
    - ); -}); -ProductReview.displayName = 'ProductReview'; diff --git a/src/App/src/components/SelectedProductView.tsx b/src/App/src/components/SelectedProductView.tsx deleted file mode 100644 index 743a5f86f..000000000 --- a/src/App/src/components/SelectedProductView.tsx +++ /dev/null @@ -1,63 +0,0 @@ -import { memo } from 'react'; -import { - Badge, - tokens, -} from '@fluentui/react-components'; -import { - Checkmark20Regular, -} from '@fluentui/react-icons'; -import type { Product } from '../types'; -import { ProductCard } from './ProductCard'; - -interface SelectedProductViewProps { - products: Product[]; -} - -export const SelectedProductView = memo(function SelectedProductView({ products }: SelectedProductViewProps) { - if (products.length === 0) return null; - - return ( -
    -
    - } - > - Products Selected - -
    - -
    - {products.map((product, index) => ( - - ))} -
    -
    - ); -}); -SelectedProductView.displayName = 'SelectedProductView'; diff --git a/src/App/src/components/SuggestionCard.tsx b/src/App/src/components/SuggestionCard.tsx deleted file mode 100644 index 55e8e1570..000000000 --- a/src/App/src/components/SuggestionCard.tsx +++ /dev/null @@ -1,83 +0,0 @@ -import { memo } from 'react'; -import { - Card, - Text, - tokens, -} from '@fluentui/react-components'; - -export interface SuggestionCardProps { - title: string; - icon: string; - isSelected?: boolean; - onClick: () => void; -} - -/** - * A single suggestion prompt card shown on the WelcomeCard screen. - * Handles its own hover / selected styling. - */ -export const SuggestionCard = memo(function SuggestionCard({ - title, - icon, - isSelected = false, - onClick, -}: SuggestionCardProps) { - return ( - { - if (!isSelected) { - e.currentTarget.style.backgroundColor = tokens.colorBrandBackground2; - } - }} - onMouseLeave={(e) => { - if (!isSelected) { - e.currentTarget.style.backgroundColor = tokens.colorNeutralBackground1; - } - }} - > -
    -
    - Prompt icon -
    -
    - - {title} - -
    -
    -
    - ); -}); -SuggestionCard.displayName = 'SuggestionCard'; diff --git a/src/App/src/components/TypingIndicator.tsx b/src/App/src/components/TypingIndicator.tsx deleted file mode 100644 index 36fdbbf89..000000000 --- a/src/App/src/components/TypingIndicator.tsx +++ /dev/null @@ -1,76 +0,0 @@ -import { memo, useMemo } from 'react'; -import { - Button, - Text, - Tooltip, - tokens, -} from '@fluentui/react-components'; -import { Stop24Regular } from '@fluentui/react-icons'; - -export interface TypingIndicatorProps { - /** Status text shown next to the dots (e.g. "Generating image…"). Falls back to "Thinking…". */ - statusText?: string; - /** Callback wired to the Stop button. If omitted the button is hidden. */ - onStop?: () => void; -} - -/** - * Animated "thinking" indicator with optional status text and a Stop button. - */ -export const TypingIndicator = memo(function TypingIndicator({ statusText, onStop }: TypingIndicatorProps) { - const dotStyle = useMemo(() => (delay: string): React.CSSProperties => ({ - width: '8px', - height: '8px', - borderRadius: '50%', - backgroundColor: tokens.colorBrandBackground, - animation: 'pulse 1.4s infinite ease-in-out', - animationDelay: delay, - }), []); - - return ( -
    -
    - - - - - -
    - - - {statusText || 'Thinking...'} - - - {onStop && ( - - - - )} -
    - ); -}); -TypingIndicator.displayName = 'TypingIndicator'; diff --git a/src/App/src/components/ViolationCard.tsx b/src/App/src/components/ViolationCard.tsx deleted file mode 100644 index 479bbbfd6..000000000 --- a/src/App/src/components/ViolationCard.tsx +++ /dev/null @@ -1,64 +0,0 @@ -import { memo, useMemo } from 'react'; -import { - Text, -} from '@fluentui/react-components'; -import { - ErrorCircle20Regular, - Warning20Regular, - Info20Regular, -} from '@fluentui/react-icons'; -import type { ComplianceViolation } from '../types'; - -export interface ViolationCardProps { - violation: ComplianceViolation; -} - -/** - * A single compliance violation row with severity-coloured icon and background. - */ -export const ViolationCard = memo(function ViolationCard({ violation }: ViolationCardProps) { - const { icon, bg } = useMemo(() => { - switch (violation.severity) { - case 'error': - return { - icon: , - bg: '#fde7e9', - }; - case 'warning': - return { - icon: , - bg: '#fff4ce', - }; - case 'info': - return { - icon: , - bg: '#deecf9', - }; - } - }, [violation.severity]); - - return ( -
    - {icon} -
    - - {violation.message} - - - {violation.suggestion} - -
    -
    - ); -}); -ViolationCard.displayName = 'ViolationCard'; diff --git a/src/App/src/components/WelcomeCard.tsx b/src/App/src/components/WelcomeCard.tsx deleted file mode 100644 index b56b781c4..000000000 --- a/src/App/src/components/WelcomeCard.tsx +++ /dev/null @@ -1,108 +0,0 @@ -import { memo, useMemo } from 'react'; -import { - Text, - tokens, -} from '@fluentui/react-components'; -import { SuggestionCard } from './SuggestionCard'; -import FirstPromptIcon from '../styles/images/firstprompt.png'; -import SecondPromptIcon from '../styles/images/secondprompt.png'; - -interface SuggestionData { - title: string; - icon: string; -} - -const suggestions: SuggestionData[] = [ - { - title: "I need to create a social media post about paint products for home remodels. The campaign is titled \"Brighten Your Springtime\" and the audience is new homeowners. I need marketing copy plus an image. The image should be an informal living room with tasteful furnishings.", - icon: FirstPromptIcon, - }, - { - title: "Generate a social media campaign with ad copy and an image. This is for \"Back to School\" and the audience is parents of school age children. Tone is playful and humorous. The image must have minimal kids accessories in a children's bedroom. Show the room in a wide view.", - icon: SecondPromptIcon, - } -]; - -interface WelcomeCardProps { - onSuggestionClick: (prompt: string) => void; - currentInput?: string; -} - -export const WelcomeCard = memo(function WelcomeCard({ onSuggestionClick, currentInput = '' }: WelcomeCardProps) { - const selectedIndex = useMemo( - () => suggestions.findIndex(s => s.title === currentInput), - [currentInput], - ); - - return ( -
    - {/* Welcome card with suggestions inside */} -
    - {/* Header with icon and welcome message */} -
    - - Welcome to your Content Generation Accelerator - - - Here are the options I can assist you with today - -
    - - {/* Suggestion cards - vertical layout */} -
    - {suggestions.map((suggestion, index) => { - const isSelected = index === selectedIndex; - return ( - onSuggestionClick(suggestion.title)} - /> - ); - })} -
    -
    -
    - ); -}); -WelcomeCard.displayName = 'WelcomeCard'; diff --git a/src/App/src/hooks/index.ts b/src/App/src/hooks/index.ts deleted file mode 100644 index f23f96430..000000000 --- a/src/App/src/hooks/index.ts +++ /dev/null @@ -1,10 +0,0 @@ -/** - * Barrel export for all custom hooks. - * Import hooks from '../hooks' instead of individual files. - */ -export { useAutoScroll } from './useAutoScroll'; -export { useChatOrchestrator } from './useChatOrchestrator'; -export { useContentGeneration } from './useContentGeneration'; -export { useConversationActions } from './useConversationActions'; -export { useCopyToClipboard } from './useCopyToClipboard'; -export { useWindowSize } from './useWindowSize'; diff --git a/src/App/src/hooks/useAutoScroll.ts b/src/App/src/hooks/useAutoScroll.ts deleted file mode 100644 index 8b1a2a9d1..000000000 --- a/src/App/src/hooks/useAutoScroll.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { useEffect, useRef } from 'react'; - -/** - * Scrolls a sentinel element into view whenever any dependency changes. - * - * @param deps - React dependency list that triggers the scroll. - * @returns A ref to attach to a zero-height element at the bottom of the - * scrollable container (the "scroll anchor"). - * - * @example - * ```tsx - * const endRef = useAutoScroll([messages, isLoading]); - * return ( - *
    - * {messages.map(m => )} - *
    - *
    - * ); - * ``` - */ -export function useAutoScroll(deps: React.DependencyList) { - const endRef = useRef(null); - - // eslint-disable-next-line react-hooks/exhaustive-deps - useEffect(() => { - endRef.current?.scrollIntoView({ behavior: 'smooth' }); - }, deps); - - return endRef; -} diff --git a/src/App/src/hooks/useChatOrchestrator.ts b/src/App/src/hooks/useChatOrchestrator.ts deleted file mode 100644 index f4edc088c..000000000 --- a/src/App/src/hooks/useChatOrchestrator.ts +++ /dev/null @@ -1,449 +0,0 @@ -import { useCallback, type MutableRefObject } from 'react'; - -import type { AgentResponse, GeneratedContent } from '../types'; -import { createMessage, createErrorMessage, matchesAnyKeyword, createNameSwapper } from '../utils'; -import { - useAppDispatch, - useAppSelector, - selectConversationId, - selectUserId, - selectPendingBrief, - selectConfirmedBrief, - selectAwaitingClarification, - selectSelectedProducts, - selectAvailableProducts, - selectGeneratedContent, - addMessage, - setIsLoading, - setGenerationStatus, - GenerationStatus, - setPendingBrief, - setConfirmedBrief, - setAwaitingClarification, - setSelectedProducts, - setGeneratedContent, - incrementHistoryRefresh, - selectConversationTitle, - setConversationTitle, -} from '../store'; -import type { AppDispatch } from '../store'; - -/* ------------------------------------------------------------------ */ -/* Shared helper — consumes a streamChat generator and dispatches */ -/* the final assistant message. Used by branches 1-b, 3-b, 4-b. */ -/* ------------------------------------------------------------------ */ - -async function consumeStreamChat( - stream: AsyncGenerator, - dispatch: AppDispatch, -): Promise { - let fullContent = ''; - let currentAgent = ''; - let messageAdded = false; - - for await (const response of stream) { - if (response.type === 'agent_response') { - fullContent = response.content; - currentAgent = response.agent || ''; - if ((response.is_final || response.requires_user_input) && !messageAdded) { - dispatch(addMessage(createMessage('assistant', fullContent, currentAgent))); - messageAdded = true; - } - } else if (response.type === 'error') { - dispatch( - addMessage( - createMessage( - 'assistant', - response.content || 'An error occurred while processing your request.', - ), - ), - ); - messageAdded = true; - } - } -} - -/* ------------------------------------------------------------------ */ -/* Hook */ -/* ------------------------------------------------------------------ */ - -/** - * Orchestrates the entire "send a message" flow. - * - * Depending on the current conversation phase it will: - * - refine a pending brief (PlanningAgent) - * - answer a general question while a brief is pending (streamChat) - * - forward a product-selection request (ProductAgent) - * - regenerate an image (ImageAgent) - * - parse a new creative brief (PlanningAgent) - * - fall through to generic chat (streamChat) - * - * All Redux reads/writes happen inside the hook so the caller is kept - * thin and declarative. - * - * @param abortControllerRef Shared ref that lets the parent (or sibling - * hooks) cancel the in-flight request. - * @returns `{ sendMessage }` — the callback to wire into `ChatPanel`. - */ -export function useChatOrchestrator( - abortControllerRef: MutableRefObject, -) { - const dispatch = useAppDispatch(); - const conversationId = useAppSelector(selectConversationId); - const userId = useAppSelector(selectUserId); - const pendingBrief = useAppSelector(selectPendingBrief); - const confirmedBrief = useAppSelector(selectConfirmedBrief); - const awaitingClarification = useAppSelector(selectAwaitingClarification); - const selectedProducts = useAppSelector(selectSelectedProducts); - const availableProducts = useAppSelector(selectAvailableProducts); - const generatedContent = useAppSelector(selectGeneratedContent); - const conversationTitle = useAppSelector(selectConversationTitle); - - const sendMessage = useCallback( - async (content: string) => { - dispatch(addMessage(createMessage('user', content))); - dispatch(setIsLoading(true)); - - // Create new abort controller for this request - abortControllerRef.current = new AbortController(); - const signal = abortControllerRef.current.signal; - - try { - // Dynamic imports to keep the initial bundle lean - const { streamChat, parseBrief, selectProducts } = await import( - '../api' - ); - - /* ---------------------------------------------------------- */ - /* Branch 1 – pending brief, not yet confirmed */ - /* ---------------------------------------------------------- */ - if (pendingBrief && !confirmedBrief) { - const refinementKeywords = [ - 'change', 'update', 'modify', 'add', 'remove', 'delete', - 'set', 'make', 'should be', - ] as const; - const isRefinement = matchesAnyKeyword(content, refinementKeywords); - - if (isRefinement || awaitingClarification) { - // --- 1-a Refine the brief -------------------------------- - const refinementPrompt = `Current creative brief:\n${JSON.stringify(pendingBrief, null, 2)}\n\nUser requested change: ${content}\n\nPlease update the brief accordingly and return the complete updated brief.`; - - dispatch(setGenerationStatus(GenerationStatus.UPDATING_BRIEF)); - const parsed = await parseBrief( - refinementPrompt, - conversationId, - userId, - signal, - ); - - if (parsed.generated_title && !conversationTitle) { - dispatch(setConversationTitle(parsed.generated_title)); - } - - if (parsed.brief) { - dispatch(setPendingBrief(parsed.brief)); - } - - if (parsed.requires_clarification && parsed.clarifying_questions) { - dispatch(setAwaitingClarification(true)); - dispatch(setGenerationStatus(GenerationStatus.IDLE)); - dispatch( - addMessage( - createMessage('assistant', parsed.clarifying_questions, 'PlanningAgent'), - ), - ); - } else { - dispatch(setAwaitingClarification(false)); - dispatch(setGenerationStatus(GenerationStatus.IDLE)); - dispatch( - addMessage( - createMessage( - 'assistant', - "I've updated the brief based on your feedback. Please review the changes above. Let me know if you'd like any other modifications, or click **Confirm Brief** when you're satisfied.", - 'PlanningAgent', - ), - ), - ); - } - } else { - // --- 1-b General question while brief is pending ----------- - dispatch(setGenerationStatus(GenerationStatus.PROCESSING_QUESTION)); - await consumeStreamChat( - streamChat(content, conversationId, userId, signal), - dispatch, - ); - dispatch(setGenerationStatus(GenerationStatus.IDLE)); - } - - /* ---------------------------------------------------------- */ - /* Branch 2 – brief confirmed, in product selection */ - /* ---------------------------------------------------------- */ - } else if (confirmedBrief && !generatedContent) { - dispatch(setGenerationStatus(GenerationStatus.FINDING_PRODUCTS)); - const result = await selectProducts( - content, - selectedProducts, - conversationId, - userId, - signal, - ); - dispatch(setSelectedProducts(result.products || [])); - dispatch(setGenerationStatus(GenerationStatus.IDLE)); - dispatch( - addMessage( - createMessage( - 'assistant', - result.message || 'Products updated.', - 'ProductAgent', - ), - ), - ); - - /* ---------------------------------------------------------- */ - /* Branch 3 – content generated, post-generation phase */ - /* ---------------------------------------------------------- */ - } else if (generatedContent && confirmedBrief) { - const imageModificationKeywords = [ - 'change', 'modify', 'update', 'replace', 'show', 'display', - 'use', 'instead', 'different', 'another', 'make it', 'make the', - 'kitchen', 'dining', 'living', 'bedroom', 'bathroom', 'outdoor', - 'office', 'room', 'scene', 'setting', 'background', 'style', - 'color', 'lighting', - ] as const; - const isImageModification = matchesAnyKeyword(content, imageModificationKeywords); - - if (isImageModification) { - // --- 3-a Regenerate image -------------------------------- - const { streamRegenerateImage } = await import('../api'); - dispatch( - setGenerationStatus(GenerationStatus.REGENERATING_IMAGE), - ); - - let responseData: GeneratedContent | null = null; - let messageContent = ''; - - // Detect if user mentions a different product - const mentionedProduct = availableProducts.find((p) => - content.toLowerCase().includes(p.product_name.toLowerCase()), - ); - const productsForRequest = mentionedProduct - ? [mentionedProduct] - : selectedProducts; - - const previousPrompt = - generatedContent.image_content?.prompt_used; - - for await (const response of streamRegenerateImage( - content, - confirmedBrief, - productsForRequest, - previousPrompt, - conversationId, - userId, - signal, - )) { - if (response.type === 'heartbeat') { - dispatch( - setGenerationStatus({ - status: GenerationStatus.POLLING, - label: response.message || 'Regenerating image...', - }), - ); - } else if ( - response.type === 'agent_response' && - response.is_final - ) { - try { - const parsedContent = JSON.parse(response.content); - - if ( - parsedContent.image_url || - parsedContent.image_base64 - ) { - // Replace old product name in text_content when switching - const swapName = createNameSwapper( - selectedProducts[0]?.product_name, - mentionedProduct?.product_name, - ); - const tc = generatedContent.text_content; - - responseData = { - ...generatedContent, - text_content: mentionedProduct - ? { - ...tc, - headline: swapName?.(tc?.headline) ?? tc?.headline, - body: swapName?.(tc?.body) ?? tc?.body, - tagline: swapName?.(tc?.tagline) ?? tc?.tagline, - cta_text: swapName?.(tc?.cta_text) ?? tc?.cta_text, - } - : tc, - image_content: { - ...generatedContent.image_content, - image_url: - parsedContent.image_url || - generatedContent.image_content?.image_url, - image_base64: parsedContent.image_base64, - prompt_used: - parsedContent.image_prompt || - generatedContent.image_content?.prompt_used, - }, - }; - dispatch(setGeneratedContent(responseData)); - - if (mentionedProduct) { - dispatch(setSelectedProducts([mentionedProduct])); - } - - // Update confirmed brief to include the modification - const updatedBrief = { - ...confirmedBrief, - visual_guidelines: `${confirmedBrief.visual_guidelines}. User modification: ${content}`, - }; - dispatch(setConfirmedBrief(updatedBrief)); - - messageContent = - parsedContent.message || - 'Image regenerated with your requested changes.'; - } else if (parsedContent.error) { - messageContent = parsedContent.error; - } else { - messageContent = - parsedContent.message || 'I processed your request.'; - } - } catch { - messageContent = - response.content || 'Image regenerated.'; - } - } else if (response.type === 'error') { - messageContent = - response.content || - 'An error occurred while regenerating the image.'; - } - } - - dispatch(setGenerationStatus(GenerationStatus.IDLE)); - dispatch( - addMessage(createMessage('assistant', messageContent, 'ImageAgent')), - ); - } else { - // --- 3-b General question after content generation -------- - dispatch(setGenerationStatus(GenerationStatus.PROCESSING_REQUEST)); - await consumeStreamChat( - streamChat(content, conversationId, userId, signal), - dispatch, - ); - dispatch(setGenerationStatus(GenerationStatus.IDLE)); - } - - /* ---------------------------------------------------------- */ - /* Branch 4 – default: initial flow */ - /* ---------------------------------------------------------- */ - } else { - const briefKeywords = [ - 'campaign', 'marketing', 'target audience', 'objective', - 'deliverable', - ] as const; - const isBriefLike = matchesAnyKeyword(content, briefKeywords); - - if (isBriefLike && !confirmedBrief) { - // --- 4-a Parse as creative brief -------------------------- - dispatch(setGenerationStatus(GenerationStatus.ANALYZING_BRIEF)); - const parsed = await parseBrief( - content, - conversationId, - userId, - signal, - ); - - if (parsed.generated_title && !conversationTitle) { - dispatch(setConversationTitle(parsed.generated_title)); - } - - if (parsed.rai_blocked) { - dispatch(setGenerationStatus(GenerationStatus.IDLE)); - dispatch( - addMessage( - createMessage('assistant', parsed.message, 'ContentSafety'), - ), - ); - } else if ( - parsed.requires_clarification && - parsed.clarifying_questions - ) { - if (parsed.brief) { - dispatch(setPendingBrief(parsed.brief)); - } - dispatch(setAwaitingClarification(true)); - dispatch(setGenerationStatus(GenerationStatus.IDLE)); - dispatch( - addMessage( - createMessage( - 'assistant', - parsed.clarifying_questions, - 'PlanningAgent', - ), - ), - ); - } else { - if (parsed.brief) { - dispatch(setPendingBrief(parsed.brief)); - } - dispatch(setAwaitingClarification(false)); - dispatch(setGenerationStatus(GenerationStatus.IDLE)); - dispatch( - addMessage( - createMessage( - 'assistant', - "I've parsed your creative brief. Please review the details below and let me know if you'd like to make any changes. You can say things like \"change the target audience to...\" or \"add a call to action...\". When everything looks good, click **Confirm Brief** to proceed.", - 'PlanningAgent', - ), - ), - ); - } - } else { - // --- 4-b Generic chat ----------------------------------- - dispatch(setGenerationStatus(GenerationStatus.PROCESSING_REQUEST)); - await consumeStreamChat( - streamChat(content, conversationId, userId, signal), - dispatch, - ); - dispatch(setGenerationStatus(GenerationStatus.IDLE)); - } - } - } catch (error) { - if (error instanceof Error && error.name === 'AbortError') { - dispatch(addMessage(createMessage('assistant', 'Generation stopped.'))); - } else { - dispatch( - addMessage( - createErrorMessage( - 'Sorry, there was an error processing your request. Please try again.', - ), - ), - ); - } - } finally { - dispatch(setIsLoading(false)); - dispatch(setGenerationStatus(GenerationStatus.IDLE)); - abortControllerRef.current = null; - dispatch(incrementHistoryRefresh()); - } - }, - [ - conversationId, - userId, - confirmedBrief, - pendingBrief, - selectedProducts, - generatedContent, - availableProducts, - dispatch, - awaitingClarification, - conversationTitle, - abortControllerRef, - ], - ); - - return { sendMessage }; -} diff --git a/src/App/src/hooks/useContentGeneration.ts b/src/App/src/hooks/useContentGeneration.ts deleted file mode 100644 index 895c20025..000000000 --- a/src/App/src/hooks/useContentGeneration.ts +++ /dev/null @@ -1,111 +0,0 @@ -import { useCallback, type MutableRefObject } from 'react'; - -import { createMessage, createErrorMessage, buildGeneratedContent } from '../utils'; -import { - useAppDispatch, - useAppSelector, - selectConfirmedBrief, - selectSelectedProducts, - selectConversationId, - selectUserId, - addMessage, - setIsLoading, - setGenerationStatus, - GenerationStatus, - setGeneratedContent, -} from '../store'; - -/** - * Handles the full content-generation lifecycle (start → poll → result) - * and exposes a way to abort the in-flight request. - * - * @param abortControllerRef Shared ref so the UI can cancel either - * chat-orchestration **or** content-generation with one button. - */ -export function useContentGeneration( - abortControllerRef: MutableRefObject, -) { - const dispatch = useAppDispatch(); - const confirmedBrief = useAppSelector(selectConfirmedBrief); - const selectedProducts = useAppSelector(selectSelectedProducts); - const conversationId = useAppSelector(selectConversationId); - const userId = useAppSelector(selectUserId); - - /** Kick off polling-based content generation. */ - const generateContent = useCallback(async () => { - if (!confirmedBrief) return; - - dispatch(setIsLoading(true)); - dispatch(setGenerationStatus(GenerationStatus.STARTING_GENERATION)); - - abortControllerRef.current = new AbortController(); - const signal = abortControllerRef.current.signal; - - try { - const { streamGenerateContent } = await import('../api'); - - for await (const response of streamGenerateContent( - confirmedBrief, - selectedProducts, - true, - conversationId, - userId, - signal, - )) { - // Heartbeat → update the status bar - if (response.type === 'heartbeat') { - const statusMessage = response.content || 'Generating content...'; - const elapsed = (response as { elapsed?: number }).elapsed || 0; - dispatch(setGenerationStatus({ - status: GenerationStatus.POLLING, - label: `${statusMessage} (${elapsed}s)`, - })); - continue; - } - - if (response.is_final && response.type !== 'error') { - dispatch(setGenerationStatus(GenerationStatus.PROCESSING_RESULTS)); - try { - const rawContent = JSON.parse(response.content); - const genContent = buildGeneratedContent(rawContent); - dispatch(setGeneratedContent(genContent)); - dispatch(setGenerationStatus(GenerationStatus.IDLE)); - } catch { - // Content parse failure — non-critical, generation result may be malformed - } - } else if (response.type === 'error') { - dispatch(setGenerationStatus(GenerationStatus.IDLE)); - dispatch(addMessage(createErrorMessage( - `Error generating content: ${response.content}`, - ))); - } - } - } catch (error) { - if (error instanceof Error && error.name === 'AbortError') { - dispatch(addMessage(createMessage('assistant', 'Content generation stopped.'))); - } else { - dispatch(addMessage(createErrorMessage( - 'Sorry, there was an error generating content. Please try again.', - ))); - } - } finally { - dispatch(setIsLoading(false)); - dispatch(setGenerationStatus(GenerationStatus.IDLE)); - abortControllerRef.current = null; - } - }, [ - confirmedBrief, - selectedProducts, - conversationId, - dispatch, - userId, - abortControllerRef, - ]); - - /** Abort whichever request is currently in-flight. */ - const stopGeneration = useCallback(() => { - abortControllerRef.current?.abort(); - }, [abortControllerRef]); - - return { generateContent, stopGeneration }; -} diff --git a/src/App/src/hooks/useConversationActions.ts b/src/App/src/hooks/useConversationActions.ts deleted file mode 100644 index 5dd8e87b9..000000000 --- a/src/App/src/hooks/useConversationActions.ts +++ /dev/null @@ -1,217 +0,0 @@ -import { useCallback } from 'react'; - -import type { ChatMessage, Product, CreativeBrief } from '../types'; -import { createMessage, buildGeneratedContent } from '../utils'; -import { httpClient } from '../api'; -import { - useAppDispatch, - useAppSelector, - selectUserId, - selectConversationId, - selectPendingBrief, - selectSelectedProducts, - resetChat, - resetContent, - setConversationId, - setConversationTitle, - setMessages, - addMessage, - setPendingBrief, - setConfirmedBrief, - setAwaitingClarification, - setSelectedProducts, - setAvailableProducts, - setGeneratedContent, - toggleChatHistory, -} from '../store'; - -/* ------------------------------------------------------------------ */ -/* Hook */ -/* ------------------------------------------------------------------ */ - -/** - * Encapsulates every conversation-level user action: - * - * - Loading a saved conversation from history - * - Starting a brand-new conversation - * - Confirming / cancelling a creative brief - * - Starting over with products - * - Toggling a product selection - * - Toggling the chat-history sidebar - * - * All Redux reads/writes are internal so the consumer stays declarative. - */ -export function useConversationActions() { - const dispatch = useAppDispatch(); - const userId = useAppSelector(selectUserId); - const conversationId = useAppSelector(selectConversationId); - const pendingBrief = useAppSelector(selectPendingBrief); - const selectedProducts = useAppSelector(selectSelectedProducts); - - /* ------------------------------------------------------------ */ - /* Select (load) a conversation from history */ - /* ------------------------------------------------------------ */ - const selectConversation = useCallback( - async (selectedConversationId: string) => { - try { - const data = await httpClient.get<{ - messages?: { - role: string; - content: string; - timestamp?: string; - agent?: string; - }[]; - brief?: unknown; - generated_content?: Record; - }>(`/conversations/${selectedConversationId}`, { - params: { user_id: userId }, - }); - - dispatch(setConversationId(selectedConversationId)); - dispatch(setConversationTitle(null)); // Will use title from conversation list - - const loadedMessages: ChatMessage[] = (data.messages || []).map( - (m, index) => ({ - id: `${selectedConversationId}-${index}`, - role: m.role as 'user' | 'assistant', - content: m.content, - timestamp: m.timestamp || new Date().toISOString(), - agent: m.agent, - }), - ); - dispatch(setMessages(loadedMessages)); - dispatch(setPendingBrief(null)); - dispatch(setAwaitingClarification(false)); - dispatch( - setConfirmedBrief( - (data.brief as CreativeBrief) || null, - ), - ); - - // Restore availableProducts so product/color name detection works - // when regenerating images in a restored conversation - if (data.brief) { - try { - const productsData = await httpClient.get<{ - products?: Product[]; - }>('/products'); - dispatch(setAvailableProducts(productsData.products || [])); - } catch { - // Non-critical — product load failure for restored conversation - } - } - - if (data.generated_content) { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const gc = data.generated_content as any; - const restoredContent = buildGeneratedContent(gc, true); - dispatch(setGeneratedContent(restoredContent)); - - if ( - gc.selected_products && - Array.isArray(gc.selected_products) - ) { - dispatch(setSelectedProducts(gc.selected_products)); - } else { - dispatch(setSelectedProducts([])); - } - } else { - dispatch(setGeneratedContent(null)); - dispatch(setSelectedProducts([])); - } - } catch { - // Error loading conversation — swallowed silently - } - }, - [userId, dispatch], - ); - - /* ------------------------------------------------------------ */ - /* Start a new conversation */ - /* ------------------------------------------------------------ */ - const newConversation = useCallback(() => { - dispatch(resetChat(undefined)); - dispatch(resetContent()); - }, [dispatch]); - - /* ------------------------------------------------------------ */ - /* Brief lifecycle */ - /* ------------------------------------------------------------ */ - const confirmBrief = useCallback(async () => { - if (!pendingBrief) return; - - try { - const { confirmBrief: confirmBriefApi } = await import('../api'); - await confirmBriefApi(pendingBrief, conversationId, userId); - dispatch(setConfirmedBrief(pendingBrief)); - dispatch(setPendingBrief(null)); - dispatch(setAwaitingClarification(false)); - - const productsData = await httpClient.get<{ products?: Product[] }>( - '/products', - ); - dispatch(setAvailableProducts(productsData.products || [])); - - dispatch( - addMessage( - createMessage( - 'assistant', - "Great! Your creative brief has been confirmed. Here are the available products for your campaign. Select the ones you'd like to feature, or tell me what you're looking for.", - 'ProductAgent', - ), - ), - ); - } catch { - // Error confirming brief — swallowed silently - } - }, [conversationId, userId, pendingBrief, dispatch]); - - const cancelBrief = useCallback(() => { - dispatch(setPendingBrief(null)); - dispatch(setAwaitingClarification(false)); - dispatch( - addMessage( - createMessage( - 'assistant', - 'No problem. Please provide your creative brief again or ask me any questions.', - ), - ), - ); - }, [dispatch]); - - /* ------------------------------------------------------------ */ - /* Product actions */ - /* ------------------------------------------------------------ */ - const selectProduct = useCallback( - (product: Product) => { - const isSelected = selectedProducts.some( - (p) => - (p.sku || p.product_name) === - (product.sku || product.product_name), - ); - if (isSelected) { - dispatch(setSelectedProducts([])); - } else { - // Single selection mode — replace any existing selection - dispatch(setSelectedProducts([product])); - } - }, - [selectedProducts, dispatch], - ); - - /* ------------------------------------------------------------ */ - /* Sidebar toggle */ - /* ------------------------------------------------------------ */ - const toggleHistory = useCallback(() => { - dispatch(toggleChatHistory()); - }, [dispatch]); - - return { - selectConversation, - newConversation, - confirmBrief, - cancelBrief, - selectProduct, - toggleHistory, - }; -} diff --git a/src/App/src/hooks/useCopyToClipboard.ts b/src/App/src/hooks/useCopyToClipboard.ts deleted file mode 100644 index 5887c9532..000000000 --- a/src/App/src/hooks/useCopyToClipboard.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { useState, useCallback, useRef, useEffect } from 'react'; - -/** - * Copy text to the clipboard and expose a transient `copied` flag. - * - * @param resetTimeout - Milliseconds before `copied` resets to `false` (default 2 000). - * @returns `{ copied, copy }` — `copy(text)` writes to the clipboard and - * flips `copied` to `true` for `resetTimeout` ms. - */ -export function useCopyToClipboard(resetTimeout = 2000) { - const [copied, setCopied] = useState(false); - const timerRef = useRef>(); - - const copy = useCallback( - (text: string) => { - navigator.clipboard.writeText(text).catch(() => { - // Clipboard write failure — non-critical - }); - setCopied(true); - clearTimeout(timerRef.current); - timerRef.current = setTimeout(() => setCopied(false), resetTimeout); - }, - [resetTimeout], - ); - - // Cleanup on unmount - useEffect(() => { - return () => clearTimeout(timerRef.current); - }, []); - - return { copied, copy }; -} diff --git a/src/App/src/hooks/useWindowSize.ts b/src/App/src/hooks/useWindowSize.ts deleted file mode 100644 index da662eb41..000000000 --- a/src/App/src/hooks/useWindowSize.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { useState, useEffect } from 'react'; - -/** - * Returns the current window inner-width, updating on resize. - * Falls back to 1200 during SSR. - */ -export function useWindowSize(): number { - const [windowWidth, setWindowWidth] = useState( - typeof window !== 'undefined' ? window.innerWidth : 1200, - ); - - useEffect(() => { - const handleResize = () => setWindowWidth(window.innerWidth); - window.addEventListener('resize', handleResize); - return () => window.removeEventListener('resize', handleResize); - }, []); - - return windowWidth; -} diff --git a/src/App/src/store/appSlice.ts b/src/App/src/store/appSlice.ts deleted file mode 100644 index 952b6ae1e..000000000 --- a/src/App/src/store/appSlice.ts +++ /dev/null @@ -1,151 +0,0 @@ -/** - * App slice — application-level state (user info, config, feature flags, UI toggles). - * createSlice + createAsyncThunk replaces manual dispatch + string constants. - * Granular selectors — each component subscribes only to the state it needs. - */ -import { createSlice, createAsyncThunk, type PayloadAction } from '@reduxjs/toolkit'; - -/* ------------------------------------------------------------------ */ -/* Generation-status enum */ -/* ------------------------------------------------------------------ */ - -/** - * Finite set of generation-status values. Components that read - * `generationStatus` can compare against these constants instead of - * relying on magic strings. - * - * `IDLE` means "no status to display". Every other member maps to a - * user-facing label via {@link GENERATION_STATUS_LABELS}. - */ -export enum GenerationStatus { - IDLE = '', - UPDATING_BRIEF = 'UPDATING_BRIEF', - PROCESSING_QUESTION = 'PROCESSING_QUESTION', - FINDING_PRODUCTS = 'FINDING_PRODUCTS', - REGENERATING_IMAGE = 'REGENERATING_IMAGE', - PROCESSING_REQUEST = 'PROCESSING_REQUEST', - ANALYZING_BRIEF = 'ANALYZING_BRIEF', - STARTING_GENERATION = 'STARTING_GENERATION', - PROCESSING_RESULTS = 'PROCESSING_RESULTS', - /** Used for heartbeat polling where the label is dynamic. */ - POLLING = 'POLLING', -} - -/** Display strings shown in the UI for each status. */ -const GENERATION_STATUS_LABELS: Record = { - [GenerationStatus.IDLE]: '', - [GenerationStatus.UPDATING_BRIEF]: 'Updating creative brief...', - [GenerationStatus.PROCESSING_QUESTION]: 'Processing your question...', - [GenerationStatus.FINDING_PRODUCTS]: 'Finding products...', - [GenerationStatus.REGENERATING_IMAGE]: 'Regenerating image with your changes...', - [GenerationStatus.PROCESSING_REQUEST]: 'Processing your request...', - [GenerationStatus.ANALYZING_BRIEF]: 'Analyzing creative brief...', - [GenerationStatus.STARTING_GENERATION]: 'Starting content generation...', - [GenerationStatus.PROCESSING_RESULTS]: 'Processing results...', - [GenerationStatus.POLLING]: 'Generating content...', -}; - -/* ------------------------------------------------------------------ */ -/* Async Thunks */ -/* ------------------------------------------------------------------ */ - -export const fetchAppConfig = createAsyncThunk( - 'app/fetchAppConfig', - async () => { - const { getAppConfig } = await import('../api'); - const config = await getAppConfig(); - return config; - }, -); - -export const fetchUserInfo = createAsyncThunk( - 'app/fetchUserInfo', - async () => { - const { platformClient } = await import('../api/httpClient'); - const response = await platformClient.raw('/.auth/me'); - if (!response.ok) return { userId: 'anonymous', userName: '' }; - - const payload = await response.json(); - const claims: { typ: string; val: string }[] = payload[0]?.user_claims || []; - - const objectId = claims.find( - (c) => c.typ === 'http://schemas.microsoft.com/identity/claims/objectidentifier', - )?.val || 'anonymous'; - - const name = claims.find((c) => c.typ === 'name')?.val || ''; - - return { userId: objectId, userName: name }; - }, -); - -/* ------------------------------------------------------------------ */ -/* Slice */ -/* ------------------------------------------------------------------ */ - -interface AppState { - userId: string; - userName: string; - isLoading: boolean; - imageGenerationEnabled: boolean; - showChatHistory: boolean; - /** Current generation status enum value. */ - generationStatus: GenerationStatus; - /** Dynamic label override (used with GenerationStatus.POLLING). */ - generationStatusLabel: string; -} - -const initialState: AppState = { - userId: '', - userName: '', - isLoading: false, - imageGenerationEnabled: true, - showChatHistory: true, - generationStatus: GenerationStatus.IDLE, - generationStatusLabel: '', -}; - -const appSlice = createSlice({ - name: 'app', - initialState, - reducers: { - setIsLoading(state, action: PayloadAction) { - state.isLoading = action.payload; - }, - setGenerationStatus( - state, - action: PayloadAction, - ) { - if (typeof action.payload === 'string') { - state.generationStatus = action.payload; - state.generationStatusLabel = GENERATION_STATUS_LABELS[action.payload]; - } else { - state.generationStatus = action.payload.status; - state.generationStatusLabel = action.payload.label; - } - }, - toggleChatHistory(state) { - state.showChatHistory = !state.showChatHistory; - }, - }, - extraReducers: (builder) => { - builder - .addCase(fetchAppConfig.fulfilled, (state, action) => { - state.imageGenerationEnabled = action.payload.enable_image_generation; - }) - .addCase(fetchAppConfig.rejected, (state) => { - state.imageGenerationEnabled = true; // default when fetch fails - }) - .addCase(fetchUserInfo.fulfilled, (state, action) => { - state.userId = action.payload.userId; - state.userName = action.payload.userName; - }) - .addCase(fetchUserInfo.rejected, (state) => { - state.userId = 'anonymous'; - state.userName = ''; - }); - }, -}); - -export const { setIsLoading, setGenerationStatus, toggleChatHistory } = - appSlice.actions; -export default appSlice.reducer; diff --git a/src/App/src/store/chatHistorySlice.ts b/src/App/src/store/chatHistorySlice.ts deleted file mode 100644 index b97b14e31..000000000 --- a/src/App/src/store/chatHistorySlice.ts +++ /dev/null @@ -1,127 +0,0 @@ -/** - * Chat history slice — conversation list CRUD via async thunks. - * createAsyncThunk replaces inline fetch + manual state updates in ChatHistory.tsx. - * Granular selectors for each piece of history state. - */ -import { createSlice, createAsyncThunk, type PayloadAction } from '@reduxjs/toolkit'; -import { httpClient } from '../api'; - -export interface ConversationSummary { - id: string; - title: string; - lastMessage: string; - timestamp: string; - messageCount: number; -} - -interface ChatHistoryState { - conversations: ConversationSummary[]; - isLoading: boolean; - error: string | null; - showAll: boolean; - isClearAllDialogOpen: boolean; - isClearing: boolean; -} - -const initialState: ChatHistoryState = { - conversations: [], - isLoading: true, - error: null, - showAll: false, - isClearAllDialogOpen: false, - isClearing: false, -}; - -/* ------------------------------------------------------------------ */ -/* Async Thunks */ -/* ------------------------------------------------------------------ */ - -export const fetchConversations = createAsyncThunk( - 'chatHistory/fetchConversations', - async () => { - const data = await httpClient.get<{ conversations?: ConversationSummary[] }>('/conversations'); - return (data.conversations || []) as ConversationSummary[]; - }, -); - -export const deleteConversation = createAsyncThunk( - 'chatHistory/deleteConversation', - async (conversationId: string) => { - await httpClient.delete(`/conversations/${conversationId}`); - return conversationId; - }, -); - -export const renameConversation = createAsyncThunk( - 'chatHistory/renameConversation', - async ({ conversationId, newTitle }: { conversationId: string; newTitle: string }) => { - await httpClient.put(`/conversations/${conversationId}`, { title: newTitle }); - return { conversationId, newTitle }; - }, -); - -export const clearAllConversations = createAsyncThunk( - 'chatHistory/clearAllConversations', - async () => { - await httpClient.delete('/conversations'); - }, -); - -/* ------------------------------------------------------------------ */ -/* Slice */ -/* ------------------------------------------------------------------ */ - -const chatHistorySlice = createSlice({ - name: 'chatHistory', - initialState, - reducers: { - setShowAll(state, action: PayloadAction) { - state.showAll = action.payload; - }, - setIsClearAllDialogOpen(state, action: PayloadAction) { - state.isClearAllDialogOpen = action.payload; - }, - }, - extraReducers: (builder) => { - builder - // Fetch - .addCase(fetchConversations.pending, (state) => { - state.isLoading = true; - state.error = null; - }) - .addCase(fetchConversations.fulfilled, (state, action) => { - state.conversations = action.payload; - state.isLoading = false; - }) - .addCase(fetchConversations.rejected, (state) => { - state.error = 'Unable to load conversation history'; - state.conversations = []; - state.isLoading = false; - }) - // Delete single - .addCase(deleteConversation.fulfilled, (state, action) => { - state.conversations = state.conversations.filter((c) => c.id !== action.payload); - }) - // Rename - .addCase(renameConversation.fulfilled, (state, action) => { - const conv = state.conversations.find((c) => c.id === action.payload.conversationId); - if (conv) conv.title = action.payload.newTitle; - }) - // Clear all - .addCase(clearAllConversations.pending, (state) => { - state.isClearing = true; - }) - .addCase(clearAllConversations.fulfilled, (state) => { - state.conversations = []; - state.isClearing = false; - state.isClearAllDialogOpen = false; - }) - .addCase(clearAllConversations.rejected, (state) => { - state.isClearing = false; - }); - }, -}); - -export const { setShowAll, setIsClearAllDialogOpen } = - chatHistorySlice.actions; -export default chatHistorySlice.reducer; diff --git a/src/App/src/store/chatSlice.ts b/src/App/src/store/chatSlice.ts deleted file mode 100644 index 71b25330e..000000000 --- a/src/App/src/store/chatSlice.ts +++ /dev/null @@ -1,67 +0,0 @@ -/** - * Chat slice — conversation state, messages, clarification flow. - * Typed createSlice replaces scattered useState-based state in App.tsx. - * Granular selectors for each piece of chat state. - */ -import { createSlice, type PayloadAction } from '@reduxjs/toolkit'; -import { v4 as uuidv4 } from 'uuid'; -import type { ChatMessage } from '../types'; - -interface ChatState { - conversationId: string; - conversationTitle: string | null; - messages: ChatMessage[]; - awaitingClarification: boolean; - historyRefreshTrigger: number; -} - -const initialState: ChatState = { - conversationId: uuidv4(), - conversationTitle: null, - messages: [], - awaitingClarification: false, - historyRefreshTrigger: 0, -}; - -const chatSlice = createSlice({ - name: 'chat', - initialState, - reducers: { - setConversationId(state, action: PayloadAction) { - state.conversationId = action.payload; - }, - setConversationTitle(state, action: PayloadAction) { - state.conversationTitle = action.payload; - }, - setMessages(state, action: PayloadAction) { - state.messages = action.payload; - }, - addMessage(state, action: PayloadAction) { - state.messages.push(action.payload); - }, - setAwaitingClarification(state, action: PayloadAction) { - state.awaitingClarification = action.payload; - }, - incrementHistoryRefresh(state) { - state.historyRefreshTrigger += 1; - }, - /** Reset chat to a fresh conversation. Optionally provide a new ID. */ - resetChat(state, action: PayloadAction) { - state.conversationId = action.payload ?? uuidv4(); - state.conversationTitle = null; - state.messages = []; - state.awaitingClarification = false; - }, - }, -}); - -export const { - setConversationId, - setConversationTitle, - setMessages, - addMessage, - setAwaitingClarification, - incrementHistoryRefresh, - resetChat, -} = chatSlice.actions; -export default chatSlice.reducer; diff --git a/src/App/src/store/contentSlice.ts b/src/App/src/store/contentSlice.ts deleted file mode 100644 index 15736efd5..000000000 --- a/src/App/src/store/contentSlice.ts +++ /dev/null @@ -1,61 +0,0 @@ -/** - * Content slice — creative brief, product selection, generated content. - * Typed createSlice with granular selectors. - */ -import { createSlice, type PayloadAction } from '@reduxjs/toolkit'; -import type { CreativeBrief, Product, GeneratedContent } from '../types'; - -interface ContentState { - pendingBrief: CreativeBrief | null; - confirmedBrief: CreativeBrief | null; - selectedProducts: Product[]; - availableProducts: Product[]; - generatedContent: GeneratedContent | null; -} - -const initialState: ContentState = { - pendingBrief: null, - confirmedBrief: null, - selectedProducts: [], - availableProducts: [], - generatedContent: null, -}; - -const contentSlice = createSlice({ - name: 'content', - initialState, - reducers: { - setPendingBrief(state, action: PayloadAction) { - state.pendingBrief = action.payload; - }, - setConfirmedBrief(state, action: PayloadAction) { - state.confirmedBrief = action.payload; - }, - setSelectedProducts(state, action: PayloadAction) { - state.selectedProducts = action.payload; - }, - setAvailableProducts(state, action: PayloadAction) { - state.availableProducts = action.payload; - }, - setGeneratedContent(state, action: PayloadAction) { - state.generatedContent = action.payload; - }, - resetContent(state) { - state.pendingBrief = null; - state.confirmedBrief = null; - state.selectedProducts = []; - state.availableProducts = []; - state.generatedContent = null; - }, - }, -}); - -export const { - setPendingBrief, - setConfirmedBrief, - setSelectedProducts, - setAvailableProducts, - setGeneratedContent, - resetContent, -} = contentSlice.actions; -export default contentSlice.reducer; diff --git a/src/App/src/store/hooks.ts b/src/App/src/store/hooks.ts deleted file mode 100644 index c9c663095..000000000 --- a/src/App/src/store/hooks.ts +++ /dev/null @@ -1,9 +0,0 @@ -/** - * Typed Redux hooks for type-safe store access throughout the app. - * Use useAppDispatch and useAppSelector instead of raw useDispatch/useSelector. - */ -import { useDispatch, useSelector } from 'react-redux'; -import type { RootState, AppDispatch } from './store'; - -export const useAppDispatch = useDispatch.withTypes(); -export const useAppSelector = useSelector.withTypes(); diff --git a/src/App/src/store/index.ts b/src/App/src/store/index.ts deleted file mode 100644 index 1a7a98623..000000000 --- a/src/App/src/store/index.ts +++ /dev/null @@ -1,75 +0,0 @@ -/** - * Barrel export for the Redux store. - * Import everything you need from '../store'. - */ -export { store } from './store'; -export type { RootState, AppDispatch } from './store'; -export { useAppDispatch, useAppSelector } from './hooks'; - -// App slice – actions, thunks & enums -export { - fetchAppConfig, - fetchUserInfo, - setIsLoading, - setGenerationStatus, - toggleChatHistory, - GenerationStatus, -} from './appSlice'; - -// Chat slice – actions -export { - setConversationId, - setConversationTitle, - setMessages, - addMessage, - setAwaitingClarification, - incrementHistoryRefresh, - resetChat, -} from './chatSlice'; - -// Content slice – actions -export { - setPendingBrief, - setConfirmedBrief, - setSelectedProducts, - setAvailableProducts, - setGeneratedContent, - resetContent, -} from './contentSlice'; - -// Chat History slice – actions & thunks -export { - fetchConversations, - deleteConversation, - renameConversation, - clearAllConversations, - setShowAll, - setIsClearAllDialogOpen, -} from './chatHistorySlice'; -export type { ConversationSummary } from './chatHistorySlice'; - -// All selectors (centralized to avoid circular store ↔ slice imports) -export { - selectUserId, - selectUserName, - selectIsLoading, - selectGenerationStatusLabel, - selectImageGenerationEnabled, - selectShowChatHistory, - selectConversationId, - selectConversationTitle, - selectMessages, - selectAwaitingClarification, - selectHistoryRefreshTrigger, - selectPendingBrief, - selectConfirmedBrief, - selectSelectedProducts, - selectAvailableProducts, - selectGeneratedContent, - selectConversations, - selectIsHistoryLoading, - selectHistoryError, - selectShowAll, - selectIsClearAllDialogOpen, - selectIsClearing, -} from './selectors'; diff --git a/src/App/src/store/selectors.ts b/src/App/src/store/selectors.ts deleted file mode 100644 index a837844c5..000000000 --- a/src/App/src/store/selectors.ts +++ /dev/null @@ -1,36 +0,0 @@ -/** - * All Redux selectors in one place. - * Importing RootState here (and ONLY here) avoids the circular dependency - * between store.ts ↔ slice files that confuses VS Code's TypeScript server. - */ -import type { RootState } from './store'; - -/* ---- App selectors ---- */ -export const selectUserId = (state: RootState) => state.app.userId; -export const selectUserName = (state: RootState) => state.app.userName; -export const selectIsLoading = (state: RootState) => state.app.isLoading; -export const selectGenerationStatusLabel = (state: RootState) => state.app.generationStatusLabel; -export const selectImageGenerationEnabled = (state: RootState) => state.app.imageGenerationEnabled; -export const selectShowChatHistory = (state: RootState) => state.app.showChatHistory; - -/* ---- Chat selectors ---- */ -export const selectConversationId = (state: RootState) => state.chat.conversationId; -export const selectConversationTitle = (state: RootState) => state.chat.conversationTitle; -export const selectMessages = (state: RootState) => state.chat.messages; -export const selectAwaitingClarification = (state: RootState) => state.chat.awaitingClarification; -export const selectHistoryRefreshTrigger = (state: RootState) => state.chat.historyRefreshTrigger; - -/* ---- Content selectors ---- */ -export const selectPendingBrief = (state: RootState) => state.content.pendingBrief; -export const selectConfirmedBrief = (state: RootState) => state.content.confirmedBrief; -export const selectSelectedProducts = (state: RootState) => state.content.selectedProducts; -export const selectAvailableProducts = (state: RootState) => state.content.availableProducts; -export const selectGeneratedContent = (state: RootState) => state.content.generatedContent; - -/* ---- Chat History selectors ---- */ -export const selectConversations = (state: RootState) => state.chatHistory.conversations; -export const selectIsHistoryLoading = (state: RootState) => state.chatHistory.isLoading; -export const selectHistoryError = (state: RootState) => state.chatHistory.error; -export const selectShowAll = (state: RootState) => state.chatHistory.showAll; -export const selectIsClearAllDialogOpen = (state: RootState) => state.chatHistory.isClearAllDialogOpen; -export const selectIsClearing = (state: RootState) => state.chatHistory.isClearing; diff --git a/src/App/src/store/store.ts b/src/App/src/store/store.ts deleted file mode 100644 index 81e515742..000000000 --- a/src/App/src/store/store.ts +++ /dev/null @@ -1,21 +0,0 @@ -/** - * Redux store — central state for the application. - * configureStore combines all domain-specific slices. - */ -import { configureStore } from '@reduxjs/toolkit'; -import appReducer from './appSlice'; -import chatReducer from './chatSlice'; -import contentReducer from './contentSlice'; -import chatHistoryReducer from './chatHistorySlice'; - -export const store = configureStore({ - reducer: { - app: appReducer, - chat: chatReducer, - content: contentReducer, - chatHistory: chatHistoryReducer, - }, -}); - -export type RootState = ReturnType; -export type AppDispatch = typeof store.dispatch; diff --git a/src/App/src/utils/briefFields.ts b/src/App/src/utils/briefFields.ts deleted file mode 100644 index beb44a88e..000000000 --- a/src/App/src/utils/briefFields.ts +++ /dev/null @@ -1,46 +0,0 @@ -/** - * Brief-field metadata shared between BriefReview and ConfirmedBriefView. - * - * Eliminates the duplicated field-label arrays. - */ -import type { CreativeBrief } from '../types'; - -/** - * Canonical map from `CreativeBrief` keys to user-friendly labels. - * Used by BriefReview (completeness gauges) and ConfirmedBriefView. - */ -export const BRIEF_FIELD_LABELS: Record = { - overview: 'Overview', - objectives: 'Objectives', - target_audience: 'Target Audience', - key_message: 'Key Message', - tone_and_style: 'Tone & Style', - deliverable: 'Deliverable', - timelines: 'Timelines', - visual_guidelines: 'Visual Guidelines', - cta: 'Call to Action', -}; - -/** - * Display order for brief fields in review UIs. - * - * The first element in each tuple is the `CreativeBrief` key, the second - * is the UI label (which may differ slightly from `BRIEF_FIELD_LABELS` - * for contextual reasons, e.g. "Campaign Objective" vs "Overview"). - */ -export const BRIEF_DISPLAY_ORDER: { key: keyof CreativeBrief; label: string }[] = [ - { key: 'overview', label: 'Campaign Objective' }, - { key: 'objectives', label: 'Objectives' }, - { key: 'target_audience', label: 'Target Audience' }, - { key: 'key_message', label: 'Key Message' }, - { key: 'tone_and_style', label: 'Tone & Style' }, - { key: 'visual_guidelines', label: 'Visual Guidelines' }, - { key: 'deliverable', label: 'Deliverables' }, - { key: 'timelines', label: 'Timelines' }, - { key: 'cta', label: 'Call to Action' }, -]; - -/** - * The canonical list of all nine brief field keys, in display order. - */ -export const BRIEF_FIELD_KEYS: (keyof CreativeBrief)[] = BRIEF_DISPLAY_ORDER.map((f) => f.key); diff --git a/src/App/src/utils/contentErrors.ts b/src/App/src/utils/contentErrors.ts deleted file mode 100644 index dc9ca497a..000000000 --- a/src/App/src/utils/contentErrors.ts +++ /dev/null @@ -1,31 +0,0 @@ -/** - * Detect whether an error message originates from a content-safety filter. - */ -export function isContentFilterError(errorMessage?: string): boolean { - if (!errorMessage) return false; - const filterPatterns = [ - 'content_filter', 'ContentFilter', 'content management policy', - 'ResponsibleAI', 'responsible_ai_policy', 'content filtering', - 'filtered', 'safety system', 'self_harm', 'sexual', 'violence', 'hate', - ]; - return filterPatterns.some((pattern) => - errorMessage.toLowerCase().includes(pattern.toLowerCase()), - ); -} - -/** - * Return a user-friendly title/description for a generation error. - */ -export function getErrorMessage(errorMessage?: string): { title: string; description: string } { - if (isContentFilterError(errorMessage)) { - return { - title: 'Content Filtered', - description: - 'Your request was blocked by content safety filters. Please try modifying your creative brief.', - }; - } - return { - title: 'Generation Failed', - description: errorMessage || 'An error occurred. Please try again.', - }; -} diff --git a/src/App/src/utils/contentParsing.ts b/src/App/src/utils/contentParsing.ts deleted file mode 100644 index e59ac85e3..000000000 --- a/src/App/src/utils/contentParsing.ts +++ /dev/null @@ -1,108 +0,0 @@ -/** - * Content parsing utilities — raw API response → typed domain objects. - * - * Centralizes the duplicated `textContent` string-to-object parsing, - * image URL resolution (blob rewriting, base64 fallback), and the - * `GeneratedContent` assembly that was copy-pasted across - * useContentGeneration, useConversationActions, and useChatOrchestrator. - */ -import type { GeneratedContent } from '../types'; - -/* ------------------------------------------------------------------ */ -/* Internal helpers (not exported — reduces public API surface) */ -/* ------------------------------------------------------------------ */ - -/** - * Rewrite Azure Blob Storage URLs to the application's proxy endpoint - * so the browser can fetch images without CORS issues. - */ -function rewriteBlobUrl(url: string): string { - if (!url.includes('blob.core.windows.net')) return url; - const parts = url.split('/'); - const filename = parts[parts.length - 1]; - const convId = parts[parts.length - 2]; - return `/api/images/${convId}/${filename}`; -} - -/* ------------------------------------------------------------------ */ -/* Parsing helpers (module-internal — not re-exported) */ -/* ------------------------------------------------------------------ */ - -/** - * Parse `text_content` which may arrive as a JSON string or an object. - * Returns an object with known fields, or `undefined` if unusable. - */ -function parseTextContent( - raw: unknown, -): { headline?: string; body?: string; cta_text?: string; tagline?: string } | undefined { - let textContent = raw; - - if (typeof textContent === 'string') { - try { - textContent = JSON.parse(textContent); - } catch { - // Not valid JSON — treat as unusable - return undefined; - } - } - - if (typeof textContent !== 'object' || textContent === null) return undefined; - - const tc = textContent as Record; - return { - headline: tc.headline as string | undefined, - body: tc.body as string | undefined, - cta_text: (tc.cta_text ?? tc.cta) as string | undefined, - tagline: tc.tagline as string | undefined, - }; -} - -/** - * Resolve the best available image URL from a raw API response. - * - * Priority: explicit `image_url` (with blob rewrite) → base64 data URI. - * Pass `rewriteBlobs: true` (default) when restoring from a saved - * conversation; `false` when the response just came from the live API. - */ -function resolveImageUrl( - raw: { image_url?: string; image_base64?: string }, - rewriteBlobs = false, -): string | undefined { - let url = raw.image_url; - if (url && rewriteBlobs) { - url = rewriteBlobUrl(url); - } - if (url) return url; - if (raw.image_base64) return `data:image/png;base64,${raw.image_base64}`; - return undefined; -} - -/** - * Build a fully-typed `GeneratedContent` from an arbitrary raw API payload. - * - * @param raw The parsed JSON object from the backend. - * @param rewriteBlobs Pass `true` when restoring from a saved conversation - * so Azure Blob URLs get proxied. - */ -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export function buildGeneratedContent(raw: any, rewriteBlobs = false): GeneratedContent { - const textContent = parseTextContent(raw.text_content); - const imageUrl = resolveImageUrl(raw, rewriteBlobs); - - return { - text_content: textContent, - image_content: - imageUrl || raw.image_prompt - ? { - image_url: imageUrl, - prompt_used: raw.image_prompt, - alt_text: raw.image_revised_prompt || 'Generated marketing image', - } - : undefined, - violations: raw.violations || [], - requires_modification: raw.requires_modification || false, - error: raw.error, - image_error: raw.image_error, - text_error: raw.text_error, - }; -} diff --git a/src/App/src/utils/downloadImage.ts b/src/App/src/utils/downloadImage.ts deleted file mode 100644 index 08e752c20..000000000 --- a/src/App/src/utils/downloadImage.ts +++ /dev/null @@ -1,94 +0,0 @@ -/** - * Download the generated marketing image with a product-name / tagline - * banner composited at the bottom. - * - * Falls back to a plain download when canvas compositing fails. - */ -export async function downloadImage( - imageUrl: string, - productName?: string, - tagline?: string, -): Promise { - try { - const canvas = document.createElement('canvas'); - const ctx = canvas.getContext('2d'); - if (!ctx) return; - - const img = new Image(); - img.crossOrigin = 'anonymous'; - - img.onload = () => { - const bannerHeight = Math.max(60, img.height * 0.1); - const padding = Math.max(16, img.width * 0.03); - - canvas.width = img.width; - canvas.height = img.height + bannerHeight; - - // Draw the image at the top - ctx.drawImage(img, 0, 0); - - // White banner at the bottom - ctx.fillStyle = '#ffffff'; - ctx.fillRect(0, img.height, img.width, bannerHeight); - - ctx.strokeStyle = '#e5e5e5'; - ctx.lineWidth = 1; - ctx.beginPath(); - ctx.moveTo(0, img.height); - ctx.lineTo(img.width, img.height); - ctx.stroke(); - - // Headline text - const headlineText = productName || 'Your Product'; - const headlineFontSize = Math.max(18, Math.min(36, img.width * 0.04)); - const taglineFontSize = Math.max(12, Math.min(20, img.width * 0.025)); - - ctx.font = `600 ${headlineFontSize}px Georgia, serif`; - ctx.fillStyle = '#1a1a1a'; - ctx.fillText( - headlineText, - padding, - img.height + padding + headlineFontSize * 0.8, - img.width - padding * 2, - ); - - // Tagline - if (tagline) { - ctx.font = `400 italic ${taglineFontSize}px -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif`; - ctx.fillStyle = '#666666'; - ctx.fillText( - tagline, - padding, - img.height + padding + headlineFontSize + taglineFontSize * 0.8 + 4, - img.width - padding * 2, - ); - } - - canvas.toBlob((blob) => { - if (blob) { - const url = URL.createObjectURL(blob); - const link = document.createElement('a'); - link.href = url; - link.download = 'generated-marketing-image.png'; - link.click(); - URL.revokeObjectURL(url); - } - }, 'image/png'); - }; - - img.onerror = () => { - plainDownload(imageUrl); - }; - - img.src = imageUrl; - } catch { - plainDownload(imageUrl); - } -} - -function plainDownload(url: string) { - const link = document.createElement('a'); - link.href = url; - link.download = 'generated-image.png'; - link.click(); -} diff --git a/src/App/src/utils/generationStages.ts b/src/App/src/utils/generationStages.ts deleted file mode 100644 index 03399bc0d..000000000 --- a/src/App/src/utils/generationStages.ts +++ /dev/null @@ -1,33 +0,0 @@ -/** - * Generation progress stage mapping. - * - * Pure function that converts elapsed seconds into a human-readable - * stage label + ordinal — used by the polling loop in `streamGenerateContent`. - */ - -export interface GenerationStage { - /** Ordinal stage index (0–5) for progress indicators. */ - stage: number; - /** Human-readable status message. */ - message: string; -} - -/** - * Map elapsed seconds to the current generation stage. - * - * Typical generation timeline: - * - 0 – 10 s → Briefing analysis - * - 10 – 25 s → Copy generation - * - 25 – 35 s → Image prompt creation - * - 35 – 55 s → Image generation - * - 55 – 70 s → Compliance check - * - 70 s+ → Finalizing - */ -export function getGenerationStage(elapsedSeconds: number): GenerationStage { - if (elapsedSeconds < 10) return { stage: 0, message: 'Analyzing creative brief...' }; - if (elapsedSeconds < 25) return { stage: 1, message: 'Generating marketing copy...' }; - if (elapsedSeconds < 35) return { stage: 2, message: 'Creating image prompt...' }; - if (elapsedSeconds < 55) return { stage: 3, message: 'Generating image with AI...' }; - if (elapsedSeconds < 70) return { stage: 4, message: 'Running compliance check...' }; - return { stage: 5, message: 'Finalizing content...' }; -} diff --git a/src/App/src/utils/index.ts b/src/App/src/utils/index.ts deleted file mode 100644 index 94ba048c3..000000000 --- a/src/App/src/utils/index.ts +++ /dev/null @@ -1,32 +0,0 @@ -/** - * Barrel export for all utility modules. - * - * Import everything you need from '../utils'. - */ - -// Message factories & formatting -export { createMessage, createErrorMessage } from './messageUtils'; - -// Content parsing (raw API → typed domain objects) -export { buildGeneratedContent } from './contentParsing'; - -// SSE stream parser -export { parseSSEStream } from './sseParser'; - -// Generation progress stages -export { getGenerationStage } from './generationStages'; - -// Brief-field metadata -export { BRIEF_FIELD_LABELS, BRIEF_DISPLAY_ORDER, BRIEF_FIELD_KEYS } from './briefFields'; - -// String utilities -export { createNameSwapper, matchesAnyKeyword } from './stringUtils'; - -// Content error detection -export { isContentFilterError, getErrorMessage } from './contentErrors'; - -// Image download -export { downloadImage } from './downloadImage'; - -// Shared UI constants -export const AI_DISCLAIMER = 'AI-generated content may be incorrect'; diff --git a/src/App/src/utils/messageUtils.ts b/src/App/src/utils/messageUtils.ts deleted file mode 100644 index 45a7ea5ac..000000000 --- a/src/App/src/utils/messageUtils.ts +++ /dev/null @@ -1,34 +0,0 @@ -/** - * Message utilities — ChatMessage factory and formatting helpers. - * - * Replaces duplicated `msg()` helpers in useChatOrchestrator and - * useConversationActions with a single, tested source of truth. - */ -import { v4 as uuidv4 } from 'uuid'; -import type { ChatMessage } from '../types'; - -/** - * Create a `ChatMessage` literal with a fresh UUID and ISO timestamp. - */ -export function createMessage( - role: 'user' | 'assistant', - content: string, - agent?: string, -): ChatMessage { - return { - id: uuidv4(), - role, - content, - agent, - timestamp: new Date().toISOString(), - }; -} - -/** - * Shorthand for creating an assistant error message. - * Consolidates the repeated `createMessage('assistant', errorText)` pattern - * used in error catch blocks across multiple hooks. - */ -export function createErrorMessage(content: string): ChatMessage { - return createMessage('assistant', content); -} diff --git a/src/App/src/utils/sseParser.ts b/src/App/src/utils/sseParser.ts deleted file mode 100644 index 7767c0b5e..000000000 --- a/src/App/src/utils/sseParser.ts +++ /dev/null @@ -1,48 +0,0 @@ -/** - * SSE (Server-Sent Events) stream parser. - * - * Eliminates the duplicated TextDecoder + buffer + line-split logic - * that was copy-pasted in `streamChat` and `streamRegenerateImage`. - */ -import type { AgentResponse } from '../types'; - -/** - * Parse an SSE stream from a `ReadableStreamDefaultReader` into an - * `AsyncGenerator` of `AgentResponse` objects. - * - * Protocol assumed: - * - Events delimited by `\n\n` - * - Each event starts with `data: ` - * - `data: [DONE]` terminates the stream - * - * @param reader The reader obtained via `response.body.getReader()` - */ -export async function* parseSSEStream( - reader: ReadableStreamDefaultReader, -): AsyncGenerator { - const decoder = new TextDecoder(); - let buffer = ''; - - while (true) { - const { done, value } = await reader.read(); - if (done) break; - - buffer += decoder.decode(value, { stream: true }); - const lines = buffer.split('\n\n'); - buffer = lines.pop() || ''; - - for (const line of lines) { - if (line.startsWith('data: ')) { - const data = line.slice(6); - if (data === '[DONE]') { - return; - } - try { - yield JSON.parse(data) as AgentResponse; - } catch { - // Malformed SSE frame — skip silently - } - } - } - } -} diff --git a/src/App/src/utils/stringUtils.ts b/src/App/src/utils/stringUtils.ts deleted file mode 100644 index 387e07ff5..000000000 --- a/src/App/src/utils/stringUtils.ts +++ /dev/null @@ -1,45 +0,0 @@ -/** - * String utilities — regex escaping, name swapping, keyword matching. - * - * Extracts the duplicated keyword-matching pattern and the regex-escape + - * swapName closure from useChatOrchestrator into reusable, testable functions. - */ - -/** - * Escape a string so it can be safely embedded in a `RegExp` pattern. - * @internal — only used by `createNameSwapper` within this module. - */ -function escapeRegex(str: string): string { - return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); -} - -/** - * Create a function that replaces all case-insensitive occurrences of - * `oldName` with `newName` in a string. - * - * Returns `undefined` if no swap is possible (names are the same, etc.). - */ -export function createNameSwapper( - oldName: string | undefined, - newName: string | undefined, -): ((text?: string) => string | undefined) | undefined { - if (!oldName || !newName || oldName === newName) return undefined; - - const regex = new RegExp(escapeRegex(oldName), 'gi'); - return (text?: string) => { - if (!text) return text; - return text.replace(regex, () => newName); - }; -} - -/** - * Check whether `text` contains **any** of the given keywords - * (case-insensitive substring match). - * - * Used for intent classification (brief detection, refinement detection, - * image modification detection) repeated 3× in useChatOrchestrator. - */ -export function matchesAnyKeyword(text: string, keywords: readonly string[]): boolean { - const lower = text.toLowerCase(); - return keywords.some((kw) => lower.includes(kw)); -} diff --git a/src/App/.dockerignore b/src/app/.dockerignore similarity index 92% rename from src/App/.dockerignore rename to src/app/.dockerignore index 717b602c2..01b21e59f 100644 --- a/src/App/.dockerignore +++ b/src/app/.dockerignore @@ -9,7 +9,7 @@ # Build outputs (will be built in container) static/ **/dist/ -server/static/ +frontend-server/static/ # Development files *.log @@ -36,6 +36,7 @@ Thumbs.db # Deployment artifacts *.zip +frontend-deploy.zip # Backend not needed for frontend build (and vice versa) # Comment out if building full-stack image diff --git a/src/App/WebApp.Dockerfile b/src/app/WebApp.Dockerfile similarity index 82% rename from src/App/WebApp.Dockerfile rename to src/app/WebApp.Dockerfile index 15e91b831..fca82196e 100644 --- a/src/App/WebApp.Dockerfile +++ b/src/app/WebApp.Dockerfile @@ -1,7 +1,7 @@ # ============================================ # Frontend Dockerfile # Multi-stage build for Content Generation Frontend -# Combines: React/Vite frontend + server (Node.js proxy) +# Combines: frontend (React/Vite) + frontend-server (Node.js proxy) # ============================================ # ============================================ @@ -11,14 +11,14 @@ FROM node:20-alpine AS frontend-build WORKDIR /app -# Copy package files -COPY package*.json ./ +# Copy frontend package files +COPY frontend/package*.json ./ # Install dependencies RUN npm ci -# Copy source code -COPY . ./ +# Copy frontend source code +COPY frontend/ ./ # Build the frontend (outputs to ../static, but we're in /app so it goes to /static) # Override outDir to keep it in the container context @@ -31,14 +31,14 @@ FROM node:20-alpine AS production WORKDIR /app -# Copy server package files -COPY server/package*.json ./ +# Copy frontend-server package files +COPY frontend-server/package*.json ./ # Install only production dependencies RUN npm ci --only=production # Copy the server code -COPY server/server.js ./ +COPY frontend-server/server.js ./ # Copy built frontend assets from stage 1 COPY --from=frontend-build /app/dist ./static diff --git a/src/App/server/package-lock.json b/src/app/frontend-server/package-lock.json similarity index 100% rename from src/App/server/package-lock.json rename to src/app/frontend-server/package-lock.json diff --git a/src/App/server/package.json b/src/app/frontend-server/package.json similarity index 100% rename from src/App/server/package.json rename to src/app/frontend-server/package.json diff --git a/src/App/server/server.js b/src/app/frontend-server/server.js similarity index 100% rename from src/App/server/server.js rename to src/app/frontend-server/server.js diff --git a/src/App/index.html b/src/app/frontend/index.html similarity index 100% rename from src/App/index.html rename to src/app/frontend/index.html diff --git a/src/App/microsoft.svg b/src/app/frontend/microsoft.svg similarity index 100% rename from src/App/microsoft.svg rename to src/app/frontend/microsoft.svg diff --git a/src/App/package-lock.json b/src/app/frontend/package-lock.json similarity index 98% rename from src/App/package-lock.json rename to src/app/frontend/package-lock.json index 1e9d5efcb..59a539864 100644 --- a/src/App/package-lock.json +++ b/src/app/frontend/package-lock.json @@ -9,12 +9,10 @@ "version": "1.0.0", "dependencies": { "@fluentui/react-components": "^9.54.0", - "@fluentui/react-icons": "^2.0.320", - "@reduxjs/toolkit": "^2.11.2", + "@fluentui/react-icons": "^2.0.245", "react": "^18.3.1", "react-dom": "^18.3.1", "react-markdown": "^9.0.1", - "react-redux": "^9.2.0", "uuid": "^10.0.0" }, "devDependencies": { @@ -1423,9 +1421,9 @@ } }, "node_modules/@fluentui/react-icons": { - "version": "2.0.320", - "resolved": "https://registry.npmjs.org/@fluentui/react-icons/-/react-icons-2.0.320.tgz", - "integrity": "sha512-NU4gErPeaTD/T6Z9g3Uvp898lIFS6fDLr3++vpT8pcI4Ds0fZqQdrwNi3dF0R/SVws8DXQaRYiGlPHxszo4J4g==", + "version": "2.0.315", + "resolved": "https://registry.npmjs.org/@fluentui/react-icons/-/react-icons-2.0.315.tgz", + "integrity": "sha512-IITWAQGgU7I32eHPDHi+TUCUF6malP27wZLUV3bqjGVF/x/lfxvTIx8yqv/cxuwF3+ITGFDpl+278ZYJtOI7ww==", "license": "MIT", "dependencies": { "@griffel/react": "^1.0.0", @@ -2651,32 +2649,6 @@ "node": ">= 8" } }, - "node_modules/@reduxjs/toolkit": { - "version": "2.11.2", - "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.11.2.tgz", - "integrity": "sha512-Kd6kAHTA6/nUpp8mySPqj3en3dm0tdMIgbttnQ1xFMVpufoj+ADi8pXLBsd4xzTRHQa7t/Jv8W5UnCuW4kuWMQ==", - "license": "MIT", - "dependencies": { - "@standard-schema/spec": "^1.0.0", - "@standard-schema/utils": "^0.3.0", - "immer": "^11.0.0", - "redux": "^5.0.1", - "redux-thunk": "^3.1.0", - "reselect": "^5.1.0" - }, - "peerDependencies": { - "react": "^16.9.0 || ^17.0.0 || ^18 || ^19", - "react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0" - }, - "peerDependenciesMeta": { - "react": { - "optional": true - }, - "react-redux": { - "optional": true - } - } - }, "node_modules/@rolldown/pluginutils": { "version": "1.0.0-beta.27", "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", @@ -3033,18 +3005,6 @@ "win32" ] }, - "node_modules/@standard-schema/spec": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", - "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", - "license": "MIT" - }, - "node_modules/@standard-schema/utils": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/@standard-schema/utils/-/utils-0.3.0.tgz", - "integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==", - "license": "MIT" - }, "node_modules/@swc/helpers": { "version": "0.5.17", "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.17.tgz", @@ -3191,12 +3151,6 @@ "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", "license": "MIT" }, - "node_modules/@types/use-sync-external-store": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz", - "integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==", - "license": "MIT" - }, "node_modules/@types/uuid": { "version": "10.0.0", "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-10.0.0.tgz", @@ -4485,16 +4439,6 @@ "node": ">= 4" } }, - "node_modules/immer": { - "version": "11.1.4", - "resolved": "https://registry.npmjs.org/immer/-/immer-11.1.4.tgz", - "integrity": "sha512-XREFCPo6ksxVzP4E0ekD5aMdf8WMwmdNaz6vuvxgI40UaEiu6q3p8X52aU6GdyvLY3XXX/8R7JOTXStz/nBbRw==", - "license": "MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/immer" - } - }, "node_modules/import-fresh": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", @@ -5785,30 +5729,6 @@ "react": ">=18" } }, - "node_modules/react-redux": { - "version": "9.2.0", - "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz", - "integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==", - "license": "MIT", - "peer": true, - "dependencies": { - "@types/use-sync-external-store": "^0.0.6", - "use-sync-external-store": "^1.4.0" - }, - "peerDependencies": { - "@types/react": "^18.2.25 || ^19", - "react": "^18.0 || ^19", - "redux": "^5.0.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "redux": { - "optional": true - } - } - }, "node_modules/react-refresh": { "version": "0.17.0", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", @@ -5819,22 +5739,6 @@ "node": ">=0.10.0" } }, - "node_modules/redux": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", - "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==", - "license": "MIT", - "peer": true - }, - "node_modules/redux-thunk": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-3.1.0.tgz", - "integrity": "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==", - "license": "MIT", - "peerDependencies": { - "redux": "^5.0.0" - } - }, "node_modules/remark-parse": { "version": "11.0.0", "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-11.0.0.tgz", @@ -5868,12 +5772,6 @@ "url": "https://opencollective.com/unified" } }, - "node_modules/reselect": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/reselect/-/reselect-5.1.1.tgz", - "integrity": "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==", - "license": "MIT" - }, "node_modules/resolve-from": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", diff --git a/src/App/package.json b/src/app/frontend/package.json similarity index 90% rename from src/App/package.json rename to src/app/frontend/package.json index d70638e23..2479885d7 100644 --- a/src/App/package.json +++ b/src/app/frontend/package.json @@ -11,12 +11,10 @@ }, "dependencies": { "@fluentui/react-components": "^9.54.0", - "@fluentui/react-icons": "^2.0.320", - "@reduxjs/toolkit": "^2.11.2", + "@fluentui/react-icons": "^2.0.245", "react": "^18.3.1", "react-dom": "^18.3.1", "react-markdown": "^9.0.1", - "react-redux": "^9.2.0", "uuid": "^10.0.0" }, "devDependencies": { diff --git a/src/app/frontend/src/App.tsx b/src/app/frontend/src/App.tsx new file mode 100644 index 000000000..bc6beff8c --- /dev/null +++ b/src/app/frontend/src/App.tsx @@ -0,0 +1,766 @@ +import { useState, useCallback, useEffect, useRef } from 'react'; +import { + Text, + Avatar, + Button, + Tooltip, + tokens, +} from '@fluentui/react-components'; +import { + History24Regular, + History24Filled, +} from '@fluentui/react-icons'; +import { v4 as uuidv4 } from 'uuid'; + +import { ChatPanel } from './components/ChatPanel'; +import { ChatHistory } from './components/ChatHistory'; +import type { ChatMessage, CreativeBrief, Product, GeneratedContent } from './types'; +import ContosoLogo from './styles/images/contoso.svg'; + + +function App() { + const [conversationId, setConversationId] = useState(() => uuidv4()); + const [conversationTitle, setConversationTitle] = useState(null); + const [userId, setUserId] = useState(''); + const [userName, setUserName] = useState(''); + const [messages, setMessages] = useState([]); + const [isLoading, setIsLoading] = useState(false); + const [generationStatus, setGenerationStatus] = useState(''); + + // Feature flags from config + const [imageGenerationEnabled, setImageGenerationEnabled] = useState(true); + + // Brief confirmation flow + const [pendingBrief, setPendingBrief] = useState(null); + const [confirmedBrief, setConfirmedBrief] = useState(null); + + // Product selection + const [selectedProducts, setSelectedProducts] = useState([]); + const [availableProducts, setAvailableProducts] = useState([]); + + // Generated content + const [generatedContent, setGeneratedContent] = useState(null); + + // Trigger for refreshing chat history + const [historyRefreshTrigger, setHistoryRefreshTrigger] = useState(0); + + // Toggle for showing/hiding chat history panel + const [showChatHistory, setShowChatHistory] = useState(true); + + // Abort controller for cancelling ongoing requests + const abortControllerRef = useRef(null); + + // Fetch app config on mount + useEffect(() => { + const fetchConfig = async () => { + try { + const { getAppConfig } = await import('./api'); + const config = await getAppConfig(); + setImageGenerationEnabled(config.enable_image_generation); + } catch (err) { + console.error('Error fetching config:', err); + // Default to enabled if config fetch fails + setImageGenerationEnabled(true); + } + }; + fetchConfig(); + }, []); + + // Fetch current user on mount - using /.auth/me (Azure App Service built-in auth endpoint) + useEffect(() => { + const fetchUser = async () => { + try { + const response = await fetch('/.auth/me'); + if (response.ok) { + const payload = await response.json(); + + // Extract user ID from objectidentifier claim + const userClaims = payload[0]?.user_claims || []; + const objectIdClaim = userClaims.find( + (claim: { typ: string; val: string }) => + claim.typ === 'http://schemas.microsoft.com/identity/claims/objectidentifier' + ); + setUserId(objectIdClaim?.val || 'anonymous'); + + // Extract display name from 'name' claim + const nameClaim = userClaims.find( + (claim: { typ: string; val: string }) => claim.typ === 'name' + ); + setUserName(nameClaim?.val || ''); + } + } catch (err) { + console.error('Error fetching user:', err); + setUserId('anonymous'); + setUserName(''); + } + }; + fetchUser(); + }, []); + + // Handle selecting a conversation from history + const handleSelectConversation = useCallback(async (selectedConversationId: string) => { + try { + const response = await fetch(`/api/conversations/${selectedConversationId}?user_id=${encodeURIComponent(userId)}`); + if (response.ok) { + const data = await response.json(); + setConversationId(selectedConversationId); + setConversationTitle(null); // Will use title from conversation list + const loadedMessages: ChatMessage[] = (data.messages || []).map((msg: { role: string; content: string; timestamp?: string; agent?: string }, index: number) => ({ + id: `${selectedConversationId}-${index}`, + role: msg.role as 'user' | 'assistant', + content: msg.content, + timestamp: msg.timestamp || new Date().toISOString(), + agent: msg.agent, + })); + setMessages(loadedMessages); + + // Only set confirmedBrief if the brief was actually confirmed + // Check metadata.brief_confirmed flag or if content was generated (implying confirmation) + const briefWasConfirmed = data.metadata?.brief_confirmed || data.generated_content; + if (briefWasConfirmed && data.brief) { + setConfirmedBrief(data.brief); + setPendingBrief(null); + } else if (data.brief) { + // Brief exists but wasn't confirmed - show it as pending for confirmation + setPendingBrief(data.brief); + setConfirmedBrief(null); + } else { + setPendingBrief(null); + setConfirmedBrief(null); + } + + // Restore availableProducts so product/color name detection works + // when regenerating images in a restored conversation + if (data.brief && availableProducts.length === 0) { + try { + const productsResponse = await fetch('/api/products'); + if (productsResponse.ok) { + const productsData = await productsResponse.json(); + setAvailableProducts(productsData.products || []); + } + } catch (err) { + console.error('Error loading products for restored conversation:', err); + } + } + + if (data.generated_content) { + const gc = data.generated_content; + let textContent = gc.text_content; + if (typeof textContent === 'string') { + try { + textContent = JSON.parse(textContent); + } catch { + } + } + + let imageUrl: string | undefined = gc.image_url; + if (imageUrl && imageUrl.includes('blob.core.windows.net')) { + const parts = imageUrl.split('/'); + const filename = parts[parts.length - 1]; + const convId = parts[parts.length - 2]; + imageUrl = `/api/images/${convId}/${filename}`; + } + if (!imageUrl && gc.image_base64) { + imageUrl = `data:image/png;base64,${gc.image_base64}`; + } + + const restoredContent: GeneratedContent = { + text_content: typeof textContent === 'object' && textContent ? { + headline: textContent?.headline, + body: textContent?.body, + cta_text: textContent?.cta, + tagline: textContent?.tagline, + } : undefined, + image_content: (imageUrl || gc.image_prompt) ? { + image_url: imageUrl, + prompt_used: gc.image_prompt, + alt_text: gc.image_revised_prompt || 'Generated marketing image', + } : undefined, + violations: gc.violations || [], + requires_modification: gc.requires_modification || false, + error: gc.error, + image_error: gc.image_error, + text_error: gc.text_error, + }; + setGeneratedContent(restoredContent); + + if (gc.selected_products && Array.isArray(gc.selected_products)) { + setSelectedProducts(gc.selected_products); + } else { + setSelectedProducts([]); + } + } else { + setGeneratedContent(null); + setSelectedProducts([]); + } + } + } catch (error) { + console.error('Error loading conversation:', error); + } + }, [userId, availableProducts.length]); + + // Handle starting a new conversation + const handleNewConversation = useCallback(() => { + setConversationId(uuidv4()); + setConversationTitle(null); + setMessages([]); + setPendingBrief(null); + setConfirmedBrief(null); + setGeneratedContent(null); + setSelectedProducts([]); + }, []); + + const handleSendMessage = useCallback(async (content: string) => { + const userMessage: ChatMessage = { + id: uuidv4(), + role: 'user', + content, + timestamp: new Date().toISOString(), + }; + + setMessages(prev => [...prev, userMessage]); + setIsLoading(true); + setGenerationStatus('Processing your request...'); + + // Create new abort controller for this request + abortControllerRef.current = new AbortController(); + const signal = abortControllerRef.current.signal; + + try { + const { sendMessage } = await import('./api'); + + let productsToSend = selectedProducts; + if (generatedContent && confirmedBrief && availableProducts.length > 0) { + const contentLower = content.toLowerCase(); + const mentionedProduct = availableProducts.find(p => + contentLower.includes(p.product_name.toLowerCase()) + ); + if (mentionedProduct && mentionedProduct.product_name !== selectedProducts[0]?.product_name) { + productsToSend = [mentionedProduct]; + } + } + + // Send message - include brief if confirmed, products if we have them + const response = await sendMessage({ + conversation_id: conversationId, + user_id: userId, + message: content, + ...(confirmedBrief && { brief: confirmedBrief }), + ...(productsToSend.length > 0 && { selected_products: productsToSend }), + ...(generatedContent && { has_generated_content: true }), + }, signal); + + // Handle response based on action_type + switch (response.action_type) { + case 'brief_parsed': { + const brief = response.data?.brief as CreativeBrief | undefined; + const title = response.data?.generated_title as string | undefined; + if (brief) { + setPendingBrief(brief); + } + if (title && !conversationTitle) { + setConversationTitle(title); + } + break; + } + + case 'clarification_needed': { + const brief = response.data?.brief as CreativeBrief | undefined; + if (brief) { + setPendingBrief(brief); + } + break; + } + + case 'brief_confirmed': { + const brief = response.data?.brief as CreativeBrief | undefined; + const products = response.data?.products as Product[] | undefined; + if (brief) { + setConfirmedBrief(brief); + setPendingBrief(null); + } + if (products) { + setAvailableProducts(products); + } + break; + } + + case 'products_selected': { + const products = response.data?.products as Product[] | undefined; + if (products) { + setSelectedProducts(products); + } + break; + } + + case 'content_generated': { + const generatedContent = response.data?.generated_content as GeneratedContent | undefined; + if (generatedContent) { + setGeneratedContent(generatedContent); + } + break; + } + + case 'image_regenerated': { + const generatedContent = response.data?.generated_content as GeneratedContent | undefined; + if (generatedContent) { + setGeneratedContent(generatedContent); + } + break; + } + + case 'regeneration_started': { + // Poll for completion using task_id from response + // Backend already started the regeneration task via /api/chat + const { pollTaskStatus } = await import('./api'); + const taskId = response.data?.task_id as string; + + if (!taskId) { + throw new Error('No task_id received for regeneration'); + } + + setGenerationStatus('Regenerating image...'); + + for await (const event of pollTaskStatus(taskId, signal)) { + + if (event.type === 'heartbeat') { + const statusMessage = (event.content as string) || 'Regenerating image...'; + const elapsed = (event as { elapsed?: number }).elapsed || 0; + setGenerationStatus(elapsed > 0 ? `${statusMessage} (${elapsed}s)` : statusMessage); + } else if (event.type === 'agent_response' && event.is_final) { + let result: Record | undefined = event.content as unknown as Record; + if (typeof event.content === 'string') { + try { result = JSON.parse(event.content); } catch { result = {}; } + } + + let imageUrl = result?.image_url as string | undefined; + if (imageUrl && imageUrl.includes('blob.core.windows.net')) { + const parts = imageUrl.split('/'); + const filename = parts[parts.length - 1]; + const convId = parts[parts.length - 2]; + imageUrl = `/api/images/${convId}/${filename}`; + } + + // Parse text_content if it's a JSON string + let textContent = result?.text_content; + if (typeof textContent === 'string') { + try { textContent = JSON.parse(textContent); } catch { /* keep as-is */ } + } + + // Update selected products if backend provided new ones (product change) + const newProducts = result?.selected_products as Product[] | undefined; + if (newProducts && newProducts.length > 0) { + setSelectedProducts(newProducts); + } + + // Update confirmed brief if backend provided an updated one (with accumulated modifications) + const updatedBrief = result?.updated_brief as CreativeBrief | undefined; + if (updatedBrief) { + setConfirmedBrief(updatedBrief); + } + + setGeneratedContent(prev => ({ + ...prev, + // Update text content if provided (with new product name) + text_content: textContent ? { + headline: (textContent as Record).headline as string | undefined, + body: (textContent as Record).body as string | undefined, + cta_text: (textContent as Record).cta as string | undefined, + } : prev?.text_content, + image_content: imageUrl ? { + image_url: imageUrl, + prompt_used: result?.image_prompt as string | undefined, + alt_text: (result?.image_revised_prompt as string) || 'Regenerated marketing image', + } : prev?.image_content, + violations: prev?.violations || [], + requires_modification: prev?.requires_modification || false, + })); + } else if (event.type === 'error') { + throw new Error(event.content || 'Regeneration failed'); + } + } + + setGenerationStatus(''); + break; + } + + case 'start_over': { + setPendingBrief(null); + setConfirmedBrief(null); + setSelectedProducts([]); + setGeneratedContent(null); + break; + } + + case 'rai_blocked': + case 'error': + case 'chat_response': + default: + // Just show the message + break; + } + + // Add assistant message from response + if (response.message) { + const assistantMessage: ChatMessage = { + id: uuidv4(), + role: 'assistant', + content: response.message, + timestamp: new Date().toISOString(), + }; + setMessages(prev => [...prev, assistantMessage]); + } + + } catch (error) { + // Check if this was a user-initiated cancellation + if (error instanceof Error && error.name === 'AbortError') { + console.log('Request cancelled by user'); + const cancelMessage: ChatMessage = { + id: uuidv4(), + role: 'assistant', + content: 'Generation stopped.', + timestamp: new Date().toISOString(), + }; + setMessages(prev => [...prev, cancelMessage]); + } else { + console.error('Error sending message:', error); + const errorMessage: ChatMessage = { + id: uuidv4(), + role: 'assistant', + content: 'Sorry, there was an error processing your request. Please try again.', + timestamp: new Date().toISOString(), + }; + setMessages(prev => [...prev, errorMessage]); + } + } finally { + setIsLoading(false); + setGenerationStatus(''); + abortControllerRef.current = null; + // Trigger refresh of chat history after message is sent + setHistoryRefreshTrigger(prev => prev + 1); + } + }, [conversationId, userId, conversationTitle, confirmedBrief, selectedProducts, generatedContent]); + + const handleBriefConfirm = useCallback(async () => { + if (!pendingBrief) return; + + try { + const { sendMessage } = await import('./api'); + + const response = await sendMessage({ + conversation_id: conversationId, + user_id: userId, + action: 'confirm_brief', + brief: pendingBrief, + }); + + // Update state based on response + if (response.action_type === 'brief_confirmed') { + const brief = response.data?.brief as CreativeBrief | undefined; + if (brief) { + setConfirmedBrief(brief); + } else { + setConfirmedBrief(pendingBrief); + } + setPendingBrief(null); + + // Fetch products separately after confirmation + try { + const productsResponse = await fetch('/api/products'); + if (productsResponse.ok) { + const productsData = await productsResponse.json(); + setAvailableProducts(productsData.products || []); + } + } catch (err) { + console.error('Error loading products after confirmation:', err); + } + } + + // Add assistant message + if (response.message) { + const assistantMessage: ChatMessage = { + id: uuidv4(), + role: 'assistant', + content: response.message, + agent: 'ProductAgent', + timestamp: new Date().toISOString(), + }; + setMessages(prev => [...prev, assistantMessage]); + } + } catch (error) { + console.error('Error confirming brief:', error); + } + }, [conversationId, userId, pendingBrief]); + + const handleBriefCancel = useCallback(async () => { + setPendingBrief(null); + + const assistantMessage: ChatMessage = { + id: uuidv4(), + role: 'assistant', + content: 'No problem. Please provide your creative brief again or ask me any questions.', + timestamp: new Date().toISOString(), + }; + setMessages(prev => [...prev, assistantMessage]); + }, []); + + const handleProductsStartOver = useCallback(async () => { + try { + const { sendMessage } = await import('./api'); + + const response = await sendMessage({ + conversation_id: conversationId, + user_id: userId, + action: 'start_over', + }); + + console.log('Start over response:', response); + + // Reset all local state + setPendingBrief(null); + setConfirmedBrief(null); + setSelectedProducts([]); + setGeneratedContent(null); + + // Add assistant message + if (response.message) { + const assistantMessage: ChatMessage = { + id: uuidv4(), + role: 'assistant', + content: response.message, + timestamp: new Date().toISOString(), + }; + setMessages(prev => [...prev, assistantMessage]); + } + } catch (error) { + console.error('Error starting over:', error); + // Still reset local state even if backend call fails + setPendingBrief(null); + setConfirmedBrief(null); + setSelectedProducts([]); + setGeneratedContent(null); + + const assistantMessage: ChatMessage = { + id: uuidv4(), + role: 'assistant', + content: 'Starting over. Please provide your creative brief to begin a new campaign.', + timestamp: new Date().toISOString(), + }; + setMessages(prev => [...prev, assistantMessage]); + } + }, [conversationId, userId]); + + const handleProductSelect = useCallback((product: Product) => { + const isSelected = selectedProducts.some( + p => (p.sku || p.product_name) === (product.sku || product.product_name) + ); + + if (isSelected) { + // Deselect - but user must have at least one selected to proceed + setSelectedProducts([]); + } else { + // Single selection mode - replace any existing selection + setSelectedProducts([product]); + } + }, [selectedProducts]); + + const handleStopGeneration = useCallback(() => { + if (abortControllerRef.current) { + abortControllerRef.current.abort(); + } + }, []); + + const handleGenerateContent = useCallback(async () => { + if (!confirmedBrief) return; + + setIsLoading(true); + setGenerationStatus('Starting content generation...'); + + // Create new abort controller for this request + abortControllerRef.current = new AbortController(); + const signal = abortControllerRef.current.signal; + + try { + const { streamGenerateContent } = await import('./api'); + + for await (const event of streamGenerateContent({ + conversation_id: conversationId, + user_id: userId, + brief: confirmedBrief as unknown as Record, + products: selectedProducts as unknown as Array>, + generate_images: imageGenerationEnabled, + }, signal)) { + + if (event.type === 'heartbeat') { + const statusMessage = (event.content as string) || 'Generating content...'; + const elapsed = (event as { elapsed?: number }).elapsed || 0; + setGenerationStatus(elapsed > 0 ? `${statusMessage} (${elapsed}s)` : statusMessage); + } else if (event.type === 'agent_response' && event.is_final) { + let result: Record | undefined = event.content as unknown as Record; + if (typeof event.content === 'string') { + try { + result = JSON.parse(event.content); + } catch { + result = {}; + } + } + + let imageUrl = result?.image_url as string | undefined; + + // Convert blob URLs to proxy URLs + if (imageUrl && imageUrl.includes('blob.core.windows.net')) { + const parts = imageUrl.split('/'); + const filename = parts[parts.length - 1]; + const convId = parts[parts.length - 2]; + imageUrl = `/api/images/${convId}/${filename}`; + } + + // Parse text_content - it may be a JSON string from backend + let textContent: Record | undefined; + const rawTextContent = result?.text_content; + if (typeof rawTextContent === 'string') { + try { + textContent = JSON.parse(rawTextContent); + } catch { + console.error('Failed to parse text_content JSON string'); + textContent = undefined; + } + } else { + textContent = rawTextContent as Record | undefined; + } + + const generatedContent: GeneratedContent = { + text_content: textContent ? { + headline: textContent.headline as string | undefined, + body: textContent.body as string | undefined, + cta_text: textContent.cta as string | undefined, + } : { + headline: result?.headline as string | undefined, + body: result?.body as string | undefined, + cta_text: result?.cta as string | undefined, + }, + image_content: imageUrl ? { + image_url: imageUrl, + prompt_used: result?.image_prompt as string | undefined, + alt_text: (result?.image_revised_prompt as string) || 'Generated marketing image', + } : undefined, + violations: (result?.violations as unknown as GeneratedContent['violations']) || [], + requires_modification: (result?.requires_modification as boolean) || false, + }; + + setGeneratedContent(generatedContent); + } else if (event.type === 'error') { + throw new Error(event.content || 'Generation failed'); + } + } + + setGenerationStatus(''); + } catch (error) { + // Check if this was a user-initiated cancellation + if (error instanceof Error && error.name === 'AbortError') { + console.log('Content generation cancelled by user'); + const cancelMessage: ChatMessage = { + id: uuidv4(), + role: 'assistant', + content: 'Content generation stopped.', + timestamp: new Date().toISOString(), + }; + setMessages(prev => [...prev, cancelMessage]); + } else { + console.error('Error generating content:', error); + const errorMessage: ChatMessage = { + id: uuidv4(), + role: 'assistant', + content: 'Sorry, there was an error generating content. Please try again.', + timestamp: new Date().toISOString(), + }; + setMessages(prev => [...prev, errorMessage]); + } + } finally { + setIsLoading(false); + setGenerationStatus(''); + abortControllerRef.current = null; + } + }, [confirmedBrief, selectedProducts, conversationId, userId, imageGenerationEnabled]); + + return ( +
    + {/* Header */} +
    +
    + Contoso + + Contoso + +
    +
    + +
    +
    + + {/* Main Content */} +
    + {/* Chat Panel - main area */} +
    + +
    + + {/* Chat History Sidebar - RIGHT side */} + {showChatHistory && ( +
    + +
    + )} +
    +
    + ); +} + +export default App; diff --git a/src/app/frontend/src/api/index.ts b/src/app/frontend/src/api/index.ts new file mode 100644 index 000000000..36781b77b --- /dev/null +++ b/src/app/frontend/src/api/index.ts @@ -0,0 +1,252 @@ +/** + * API service for interacting with the Content Generation backend + */ + +import type { + AgentResponse, + AppConfig, + MessageRequest, + MessageResponse, +} from '../types'; + +const API_BASE = '/api'; + +/** + * Send a message or action to the /api/chat endpoint + */ +export async function sendMessage( + request: MessageRequest, + signal?: AbortSignal +): Promise { + const response = await fetch(`${API_BASE}/chat`, { + signal, + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(request), + }); + + if (!response.ok) { + throw new Error(`Message request failed: ${response.statusText}`); + } + + return response.json(); +} + +/** + * Get application configuration including feature flags + */ +export async function getAppConfig(): Promise { + const response = await fetch(`${API_BASE}/config`); + + if (!response.ok) { + throw new Error(`Failed to get config: ${response.statusText}`); + } + + return response.json(); +} + +/** + * Request for content generation + */ +export interface GenerateRequest { + conversation_id: string; + user_id: string; + brief: Record; + products: Array>; + generate_images: boolean; +} + +/** + * Generate content from a confirmed brief + */ +export async function* streamGenerateContent( + request: GenerateRequest, + signal?: AbortSignal +): AsyncGenerator { + // Use polling-based approach for reliability with long-running tasks + const startResponse = await fetch(`${API_BASE}/generate/start`, { + signal, + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + brief: request.brief, + products: request.products || [], + generate_images: request.generate_images, + conversation_id: request.conversation_id, + user_id: request.user_id || 'anonymous', + }), + }); + + if (!startResponse.ok) { + throw new Error(`Content generation failed to start: ${startResponse.statusText}`); + } + + const startData = await startResponse.json(); + const taskId = startData.task_id; + + console.log(`Generation started with task ID: ${taskId}`); + + // Yield initial status + yield { + type: 'status', + content: 'Generation started...', + is_final: false, + } as AgentResponse; + + // Poll for completion + let attempts = 0; + const maxAttempts = 600; // 10 minutes max with 1-second polling (image generation can take 3-5 min) + const pollInterval = 1000; // 1 second + + while (attempts < maxAttempts) { + // Check if cancelled before waiting + if (signal?.aborted) { + throw new DOMException('Generation cancelled by user', 'AbortError'); + } + + await new Promise(resolve => setTimeout(resolve, pollInterval)); + attempts++; + + // Check if cancelled after waiting + if (signal?.aborted) { + throw new DOMException('Generation cancelled by user', 'AbortError'); + } + + try { + const statusResponse = await fetch(`${API_BASE}/generate/status/${taskId}`, { signal }); + if (!statusResponse.ok) { + throw new Error(`Failed to get task status: ${statusResponse.statusText}`); + } + + const statusData = await statusResponse.json(); + + if (statusData.status === 'completed') { + // Yield the final result + yield { + type: 'agent_response', + content: JSON.stringify(statusData.result), + is_final: true, + } as AgentResponse; + return; + } else if (statusData.status === 'failed') { + throw new Error(statusData.error || 'Generation failed'); + } else if (statusData.status === 'running') { + // Determine progress stage based on elapsed time + // Typical generation: 0-10s briefing, 10-25s copy, 25-45s image, 45-60s compliance + const elapsedSeconds = attempts; + let stage: number; + let stageMessage: string; + + if (elapsedSeconds < 10) { + stage = 0; + stageMessage = 'Analyzing creative brief...'; + } else if (elapsedSeconds < 25) { + stage = 1; + stageMessage = 'Generating marketing copy...'; + } else if (elapsedSeconds < 35) { + stage = 2; + stageMessage = 'Creating image prompt...'; + } else if (elapsedSeconds < 55) { + stage = 3; + stageMessage = 'Generating image with AI...'; + } else if (elapsedSeconds < 70) { + stage = 4; + stageMessage = 'Running compliance check...'; + } else { + stage = 5; + stageMessage = 'Finalizing content...'; + } + + // Send status update every second for smoother progress + yield { + type: 'heartbeat', + content: stageMessage, + count: stage, + elapsed: elapsedSeconds, + is_final: false, + } as AgentResponse; + } + } catch (error) { + console.error(`Error polling task ${taskId}:`, error); + // Continue polling on transient errors + if (attempts >= maxAttempts) { + throw error; + } + } + } + + throw new Error('Generation timed out after 10 minutes'); +} + +/** + * Poll for task completion using task_id + * Used for both content generation and image regeneration + */ +export async function* pollTaskStatus( + taskId: string, + signal?: AbortSignal +): AsyncGenerator { + let attempts = 0; + const maxAttempts = 600; // 10 minutes max with 1-second polling + const pollInterval = 1000; // 1 second + + while (attempts < maxAttempts) { + if (signal?.aborted) { + throw new DOMException('Operation cancelled by user', 'AbortError'); + } + + await new Promise(resolve => setTimeout(resolve, pollInterval)); + attempts++; + + if (signal?.aborted) { + throw new DOMException('Operation cancelled by user', 'AbortError'); + } + + try { + const statusResponse = await fetch(`${API_BASE}/generate/status/${taskId}`, { signal }); + if (!statusResponse.ok) { + throw new Error(`Failed to get task status: ${statusResponse.statusText}`); + } + + const statusData = await statusResponse.json(); + + if (statusData.status === 'completed') { + yield { + type: 'agent_response', + content: JSON.stringify(statusData.result), + is_final: true, + } as AgentResponse; + return; + } else if (statusData.status === 'failed') { + throw new Error(statusData.error || 'Task failed'); + } else { + // Yield heartbeat with progress + const elapsedSeconds = attempts; + let stageMessage: string; + + if (elapsedSeconds < 10) { + stageMessage = 'Starting regeneration...'; + } else if (elapsedSeconds < 30) { + stageMessage = 'Generating new image...'; + } else if (elapsedSeconds < 50) { + stageMessage = 'Processing image...'; + } else { + stageMessage = 'Finalizing...'; + } + + yield { + type: 'heartbeat', + content: stageMessage, + elapsed: elapsedSeconds, + is_final: false, + } as AgentResponse; + } + } catch (error) { + if (attempts >= maxAttempts) { + throw error; + } + } + } + + throw new Error('Task timed out after 10 minutes'); +} \ No newline at end of file diff --git a/src/App/src/components/BriefReview.tsx b/src/app/frontend/src/components/BriefReview.tsx similarity index 72% rename from src/App/src/components/BriefReview.tsx rename to src/app/frontend/src/components/BriefReview.tsx index b4a1ce1de..6ea755905 100644 --- a/src/App/src/components/BriefReview.tsx +++ b/src/app/frontend/src/components/BriefReview.tsx @@ -1,11 +1,9 @@ -import { memo, useMemo } from 'react'; import { Button, Text, tokens, } from '@fluentui/react-components'; import type { CreativeBrief } from '../types'; -import { BRIEF_FIELD_LABELS, BRIEF_DISPLAY_ORDER, BRIEF_FIELD_KEYS, AI_DISCLAIMER } from '../utils'; interface BriefReviewProps { brief: CreativeBrief; @@ -14,22 +12,47 @@ interface BriefReviewProps { isAwaitingResponse?: boolean; } -export const BriefReview = memo(function BriefReview({ +// Mapping of field keys to user-friendly labels for the 9 key areas +const fieldLabels: Record = { + overview: 'Overview', + objectives: 'Objectives', + target_audience: 'Target Audience', + key_message: 'Key Message', + tone_and_style: 'Tone and Style', + deliverable: 'Deliverable', + timelines: 'Timelines', + visual_guidelines: 'Visual Guidelines', + cta: 'Call to Action', +}; + +export function BriefReview({ brief, onConfirm, onStartOver, isAwaitingResponse = false, }: BriefReviewProps) { - const { populatedFields, missingFields, populatedDisplayFields } = useMemo(() => { - const populated = BRIEF_FIELD_KEYS.filter(key => brief[key]?.trim()).length; - const missing = BRIEF_FIELD_KEYS.filter(key => !brief[key]?.trim()); + const allFields: (keyof CreativeBrief)[] = [ + 'overview', 'objectives', 'target_audience', 'key_message', + 'tone_and_style', 'deliverable', 'timelines', 'visual_guidelines', 'cta' + ]; + const populatedFields = allFields.filter(key => brief[key]?.trim()).length; + const missingFields = allFields.filter(key => !brief[key]?.trim()); + + // Define the order and labels for display in the card + const displayOrder: { key: keyof CreativeBrief; label: string }[] = [ + { key: 'overview', label: 'Campaign Objective' }, + { key: 'objectives', label: 'Objectives' }, + { key: 'target_audience', label: 'Target Audience' }, + { key: 'key_message', label: 'Key Message' }, + { key: 'tone_and_style', label: 'Tone & Style' }, + { key: 'visual_guidelines', label: 'Visual Guidelines' }, + { key: 'deliverable', label: 'Deliverables' }, + { key: 'timelines', label: 'Timelines' }, + { key: 'cta', label: 'Call to Action' }, + ]; - return { - populatedFields: populated, - missingFields: missing, - populatedDisplayFields: BRIEF_DISPLAY_ORDER.filter(({ key }) => brief[key]?.trim()), - }; - }, [brief]); + // Filter to only populated fields + const populatedDisplayFields = displayOrder.filter(({ key }) => brief[key]?.trim()); return (
    I've captured {populatedFields} of 9 key areas. Would you like to add more details? - You are missing: {missingFields.map(f => BRIEF_FIELD_LABELS[f]).join(', ')}. + You are missing: {missingFields.map(f => fieldLabels[f]).join(', ')}.

    You can tell me things like:
      @@ -154,10 +177,9 @@ export const BriefReview = memo(function BriefReview({ paddingTop: '8px', }}> - {AI_DISCLAIMER} + AI-generated content may be incorrect
    ); -}); -BriefReview.displayName = 'BriefReview'; +} diff --git a/src/app/frontend/src/components/ChatHistory.tsx b/src/app/frontend/src/components/ChatHistory.tsx new file mode 100644 index 000000000..f258d2a48 --- /dev/null +++ b/src/app/frontend/src/components/ChatHistory.tsx @@ -0,0 +1,643 @@ +import { useState, useEffect, useCallback, useRef } from 'react'; +import { + Button, + Text, + Spinner, + tokens, + Link, + Menu, + MenuTrigger, + MenuPopover, + MenuList, + MenuItem, + Input, + Dialog, + DialogSurface, + DialogTitle, + DialogBody, + DialogActions, + DialogContent, +} from '@fluentui/react-components'; +import { + Chat24Regular, + MoreHorizontal20Regular, + Compose20Regular, + Delete20Regular, + Edit20Regular, + DismissCircle20Regular, +} from '@fluentui/react-icons'; + +interface ConversationSummary { + id: string; + title: string; + lastMessage: string; + timestamp: string; + messageCount: number; +} + +interface ChatHistoryProps { + currentConversationId: string; + currentConversationTitle?: string | null; + currentMessages?: { role: string; content: string }[]; // Current session messages + onSelectConversation: (conversationId: string) => void; + onNewConversation: () => void; + refreshTrigger?: number; // Increment to trigger refresh + isGenerating?: boolean; // True when content generation is in progress +} + +export function ChatHistory({ + currentConversationId, + currentConversationTitle, + currentMessages = [], + onSelectConversation, + onNewConversation, + refreshTrigger = 0, + isGenerating = false +}: ChatHistoryProps) { + const [conversations, setConversations] = useState([]); + const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(null); + const [showAll, setShowAll] = useState(false); + const [isClearAllDialogOpen, setIsClearAllDialogOpen] = useState(false); + const [isClearing, setIsClearing] = useState(false); + const INITIAL_COUNT = 5; + + const handleClearAllConversations = useCallback(async () => { + setIsClearing(true); + try { + const response = await fetch('/api/conversations', { + method: 'DELETE', + }); + if (response.ok) { + setConversations([]); + onNewConversation(); + setIsClearAllDialogOpen(false); + } else { + console.error('Failed to clear all conversations'); + } + } catch (err) { + console.error('Error clearing all conversations:', err); + } finally { + setIsClearing(false); + } + }, [onNewConversation]); + + const handleDeleteConversation = useCallback(async (conversationId: string) => { + try { + const response = await fetch(`/api/conversations/${conversationId}`, { + method: 'DELETE', + }); + if (response.ok) { + setConversations(prev => prev.filter(c => c.id !== conversationId)); + if (conversationId === currentConversationId) { + onNewConversation(); + } + } else { + console.error('Failed to delete conversation'); + } + } catch (err) { + console.error('Error deleting conversation:', err); + } + }, [currentConversationId, onNewConversation]); + + const handleRenameConversation = useCallback(async (conversationId: string, newTitle: string) => { + try { + const response = await fetch(`/api/conversations/${conversationId}`, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ title: newTitle }), + }); + + if (response.ok) { + setConversations(prev => prev.map(c => + c.id === conversationId ? { ...c, title: newTitle } : c + )); + } else { + console.error('Failed to rename conversation'); + } + } catch (err) { + console.error('Error renaming conversation:', err); + } + }, []); + + const loadConversations = useCallback(async () => { + setIsLoading(true); + setError(null); + try { + // Backend gets user from auth headers, no need to pass user_id + const response = await fetch('/api/conversations'); + if (response.ok) { + const data = await response.json(); + setConversations(data.conversations || []); + } else { + // If no conversations endpoint, use empty list + setConversations([]); + } + } catch (err) { + console.error('Error loading conversations:', err); + setError('Unable to load conversation history'); + setConversations([]); + } finally { + setIsLoading(false); + } + }, []); + + useEffect(() => { + loadConversations(); + }, [loadConversations, refreshTrigger]); + + // Reset showAll when conversations change significantly + useEffect(() => { + setShowAll(false); + }, [refreshTrigger]); + + // Build the current session conversation summary if it has messages + const currentSessionConversation: ConversationSummary | null = + currentMessages.length > 0 && currentConversationTitle ? { + id: currentConversationId, + title: currentConversationTitle, + lastMessage: currentMessages[currentMessages.length - 1]?.content?.substring(0, 100) || '', + timestamp: new Date().toISOString(), + messageCount: currentMessages.length, + } : null; + + // Merge current session with saved conversations, updating the current one with live data + const displayConversations = (() => { + // Find if current conversation exists in saved list + const existingIndex = conversations.findIndex(c => c.id === currentConversationId); + + if (existingIndex >= 0 && currentSessionConversation) { + // Update the saved conversation with current session data (live message count) + const updated = [...conversations]; + updated[existingIndex] = { + ...updated[existingIndex], + messageCount: currentMessages.length, + lastMessage: currentMessages[currentMessages.length - 1]?.content?.substring(0, 100) || updated[existingIndex].lastMessage, + }; + return updated; + } else if (currentSessionConversation) { + // Add current session at the top if it has messages and isn't saved yet + return [currentSessionConversation, ...conversations]; + } + return conversations; + })(); + + const visibleConversations = showAll ? displayConversations : displayConversations.slice(0, INITIAL_COUNT); + const hasMore = displayConversations.length > INITIAL_COUNT; + + return ( +
    +
    + + Chat History + + + + +
    + +
    + +
    + {isLoading ? ( +
    + +
    + ) : error ? ( +
    + {error} + + Retry + +
    + ) : displayConversations.length === 0 ? ( +
    + + No conversations yet +
    + ) : ( + <> + {visibleConversations.map((conversation) => ( + onSelectConversation(conversation.id)} + onDelete={handleDeleteConversation} + onRename={handleRenameConversation} + onRefresh={loadConversations} + disabled={isGenerating} + /> + ))} + + )} + +
    + {hasMore && ( + setShowAll(!showAll)} + style={{ + fontSize: '13px', + color: isGenerating ? tokens.colorNeutralForegroundDisabled : tokens.colorBrandForeground1, + cursor: isGenerating ? 'not-allowed' : 'pointer', + pointerEvents: isGenerating ? 'none' : 'auto', + }} + > + {showAll ? 'Show less' : 'See all'} + + )} + + + Start new chat + +
    +
    + + {/* Clear All Confirmation Dialog */} + !isClearing && setIsClearAllDialogOpen(data.open)}> + + Clear all chat history + + + + Are you sure you want to delete all chat history? This action cannot be undone and all conversations will be permanently removed. + + + + + + + + + +
    + ); +} + +interface ConversationItemProps { + conversation: ConversationSummary; + isActive: boolean; + onSelect: () => void; + onDelete: (conversationId: string) => void; + onRename: (conversationId: string, newTitle: string) => void; + onRefresh: () => void; + disabled?: boolean; +} + +function ConversationItem({ + conversation, + isActive, + onSelect, + onDelete, + onRename, + onRefresh, + disabled = false, +}: ConversationItemProps) { + const [isMenuOpen, setIsMenuOpen] = useState(false); + const [isRenameDialogOpen, setIsRenameDialogOpen] = useState(false); + const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false); + const [renameValue, setRenameValue] = useState(conversation.title || ''); + const [renameError, setRenameError] = useState(''); + const renameInputRef = useRef(null); + + const handleRenameClick = () => { + setRenameValue(conversation.title || ''); + setRenameError(''); + setIsRenameDialogOpen(true); + setIsMenuOpen(false); + }; + + const handleRenameConfirm = async () => { + const trimmedValue = renameValue.trim(); + + // Validate before API call + if (trimmedValue.length < 5) { + setRenameError('Conversation name must be at least 5 characters'); + return; + } + if (trimmedValue.length > 50) { + setRenameError('Conversation name cannot exceed 50 characters'); + return; + } + if (!/[a-zA-Z0-9]/.test(trimmedValue)) { + setRenameError('Conversation name must contain at least one letter or number'); + return; + } + + if (trimmedValue === conversation.title) { + setIsRenameDialogOpen(false); + setRenameError(''); + return; + } + + await onRename(conversation.id, trimmedValue); + onRefresh(); + setIsRenameDialogOpen(false); + setRenameError(''); + }; + + const handleDeleteClick = () => { + setIsDeleteDialogOpen(true); + setIsMenuOpen(false); + }; + + const handleDeleteConfirm = async () => { + await onDelete(conversation.id); + setIsDeleteDialogOpen(false); + }; + + useEffect(() => { + if (isRenameDialogOpen && renameInputRef.current) { + renameInputRef.current.focus(); + renameInputRef.current.select(); + } + }, [isRenameDialogOpen]); + + return ( + <> +
    + + {conversation.title || 'Untitled'} + + + setIsMenuOpen(data.open)}> + + +
    + + setIsRenameDialogOpen(data.open)}> + + Rename conversation + + + { + const newValue = e.target.value; + setRenameValue(newValue); + if (newValue.trim() === '') { + setRenameError('Conversation name cannot be empty or contain only spaces'); + } else if (newValue.trim().length < 5) { + setRenameError('Conversation name must be at least 5 characters'); + } else if (!/[a-zA-Z0-9]/.test(newValue)) { + setRenameError('Conversation name must contain at least one letter or number'); + } else if (newValue.length > 50) { + setRenameError('Conversation name cannot exceed 50 characters'); + } else { + setRenameError(''); + } + }} + onKeyDown={(e) => { + if (e.key === 'Enter' && renameValue.trim()) { + handleRenameConfirm(); + } else if (e.key === 'Escape') { + setIsRenameDialogOpen(false); + } + }} + placeholder="Enter conversation name" + style={{ width: '100%' }} + /> + + Maximum 50 characters ({renameValue.length}/50) + + {renameError && ( + + {renameError} + + )} + + + + + + + + + + setIsDeleteDialogOpen(data.open)}> + + Delete conversation + + + + Are you sure you want to delete "{conversation.title || 'Untitled'}"? This action cannot be undone. + + + + + + + + + + + ); +} diff --git a/src/app/frontend/src/components/ChatPanel.tsx b/src/app/frontend/src/components/ChatPanel.tsx new file mode 100644 index 000000000..bf757acf9 --- /dev/null +++ b/src/app/frontend/src/components/ChatPanel.tsx @@ -0,0 +1,437 @@ +import { useState, useRef, useEffect } from 'react'; +import { + Button, + Text, + Badge, + tokens, + Tooltip, +} from '@fluentui/react-components'; +import { + Send20Regular, + Stop24Regular, + Add20Regular, + Copy20Regular, +} from '@fluentui/react-icons'; +import ReactMarkdown from 'react-markdown'; +import type { ChatMessage, CreativeBrief, Product, GeneratedContent } from '../types'; +import { BriefReview } from './BriefReview'; +import { ConfirmedBriefView } from './ConfirmedBriefView'; +import { SelectedProductView } from './SelectedProductView'; +import { ProductReview } from './ProductReview'; +import { InlineContentPreview } from './InlineContentPreview'; +import { WelcomeCard } from './WelcomeCard'; + +interface ChatPanelProps { + messages: ChatMessage[]; + onSendMessage: (message: string) => void; + isLoading: boolean; + generationStatus?: string; + onStopGeneration?: () => void; + // Inline component props + pendingBrief?: CreativeBrief | null; + confirmedBrief?: CreativeBrief | null; + generatedContent?: GeneratedContent | null; + selectedProducts?: Product[]; + availableProducts?: Product[]; + onBriefConfirm?: () => void; + onBriefCancel?: () => void; + onGenerateContent?: () => void; + onRegenerateContent?: () => void; + onProductsStartOver?: () => void; + onProductSelect?: (product: Product) => void; + // Feature flags + imageGenerationEnabled?: boolean; + // New chat + onNewConversation?: () => void; +} + +export function ChatPanel({ + messages, + onSendMessage, + isLoading, + generationStatus, + onStopGeneration, + pendingBrief, + confirmedBrief, + generatedContent, + selectedProducts = [], + availableProducts = [], + onBriefConfirm, + onBriefCancel, + onGenerateContent, + onRegenerateContent, + onProductsStartOver, + onProductSelect, + imageGenerationEnabled = true, + onNewConversation, +}: ChatPanelProps) { + const [inputValue, setInputValue] = useState(''); + const messagesEndRef = useRef(null); + const messagesContainerRef = useRef(null); + const inputContainerRef = useRef(null); + + // Scroll to bottom when messages change + useEffect(() => { + messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }); + }, [messages, pendingBrief, confirmedBrief, generatedContent, isLoading, generationStatus]); + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + if (inputValue.trim() && !isLoading) { + onSendMessage(inputValue.trim()); + setInputValue(''); + } + }; + + // Determine if we should show inline components + const showBriefReview = pendingBrief && onBriefConfirm && onBriefCancel; + const showProductReview = confirmedBrief && !generatedContent && onGenerateContent; + const showContentPreview = generatedContent && onRegenerateContent; + const showWelcome = messages.length === 0 && !showBriefReview && !showProductReview && !showContentPreview; + + // Handle suggestion click from welcome card + const handleSuggestionClick = (prompt: string) => { + setInputValue(prompt); + }; + + return ( +
    + {/* Messages Area */} +
    + {showWelcome ? ( + + ) : ( + <> + {messages.map((message) => ( + + ))} + + {/* Brief Review - Read Only with Conversational Prompts */} + {showBriefReview && ( + + )} + + {/* Confirmed Brief View - Persistent read-only view */} + {confirmedBrief && !pendingBrief && ( + + )} + + {/* Selected Product View - Persistent read-only view after content generation */} + {generatedContent && selectedProducts.length > 0 && ( + + )} + + {/* Product Review - Conversational Product Selection */} + {showProductReview && ( + {})} + isAwaitingResponse={isLoading} + onProductSelect={onProductSelect} + disabled={isLoading} + /> + )} + + {/* Inline Content Preview */} + {showContentPreview && ( + 0 ? selectedProducts[0] : undefined} + imageGenerationEnabled={imageGenerationEnabled} + /> + )} + + {/* Loading/Typing Indicator - Coral Style */} + {isLoading && ( +
    +
    + + + + + +
    + + {generationStatus || 'Thinking...'} + + {onStopGeneration && ( + + + + )} +
    + )} + + )} + +
    +
    + + {/* Input Area - Simple single-line like Figma */} +
    + {/* Input Box */} +
    + setInputValue(e.target.value)} + onKeyDown={(e) => { + if (e.key === 'Enter' && !e.shiftKey) { + e.preventDefault(); + handleSubmit(e); + } + }} + placeholder="Type a message" + disabled={isLoading} + style={{ + flex: 1, + border: 'none', + outline: 'none', + backgroundColor: 'transparent', + fontFamily: 'var(--fontFamilyBase)', + fontSize: '14px', + color: tokens.colorNeutralForeground1, + }} + /> + + {/* Icons on the right */} +
    + +
    +
    + + {/* Disclaimer - Outside the input box */} + + AI generated content may be incorrect + +
    +
    + ); +} + +// Copy function for messages +const handleCopy = (text: string) => { + navigator.clipboard.writeText(text).catch((err) => { + console.error('Failed to copy text:', err); + }); +}; + +function MessageBubble({ message }: { message: ChatMessage }) { + const isUser = message.role === 'user'; + const [copied, setCopied] = useState(false); + + const onCopy = () => { + handleCopy(message.content); + setCopied(true); + setTimeout(() => setCopied(false), 2000); + }; + + return ( +
    + {/* Agent badge for assistant messages */} + {!isUser && message.agent && ( + + {message.agent} + + )} + + {/* Message content with markdown */} +
    + + {message.content} + + + {/* Footer for assistant messages - Coral style */} + {!isUser && ( +
    + + AI-generated content may be incorrect + + +
    + +
    +
    + )} +
    +
    + ); +} diff --git a/src/App/src/components/ConfirmedBriefView.tsx b/src/app/frontend/src/components/ConfirmedBriefView.tsx similarity index 74% rename from src/App/src/components/ConfirmedBriefView.tsx rename to src/app/frontend/src/components/ConfirmedBriefView.tsx index e6697151a..e7feb9416 100644 --- a/src/App/src/components/ConfirmedBriefView.tsx +++ b/src/app/frontend/src/components/ConfirmedBriefView.tsx @@ -1,4 +1,3 @@ -import { memo } from 'react'; import { Text, Badge, @@ -8,13 +7,24 @@ import { Checkmark20Regular, } from '@fluentui/react-icons'; import type { CreativeBrief } from '../types'; -import { BRIEF_DISPLAY_ORDER } from '../utils'; interface ConfirmedBriefViewProps { brief: CreativeBrief; } -export const ConfirmedBriefView = memo(function ConfirmedBriefView({ brief }: ConfirmedBriefViewProps) { +const briefFields: { key: keyof CreativeBrief; label: string }[] = [ + { key: 'overview', label: 'Overview' }, + { key: 'objectives', label: 'Objectives' }, + { key: 'target_audience', label: 'Target Audience' }, + { key: 'key_message', label: 'Key Message' }, + { key: 'tone_and_style', label: 'Tone & Style' }, + { key: 'deliverable', label: 'Deliverable' }, + { key: 'timelines', label: 'Timelines' }, + { key: 'visual_guidelines', label: 'Visual Guidelines' }, + { key: 'cta', label: 'Call to Action' }, +]; + +export function ConfirmedBriefView({ brief }: ConfirmedBriefViewProps) { return (
    - {BRIEF_DISPLAY_ORDER.map(({ key, label }) => { + {briefFields.map(({ key, label }) => { const value = brief[key]; if (!value?.trim()) return null; @@ -78,5 +88,4 @@ export const ConfirmedBriefView = memo(function ConfirmedBriefView({ brief }: Co
    ); -}); -ConfirmedBriefView.displayName = 'ConfirmedBriefView'; +} diff --git a/src/app/frontend/src/components/InlineContentPreview.tsx b/src/app/frontend/src/components/InlineContentPreview.tsx new file mode 100644 index 000000000..3ee0eead2 --- /dev/null +++ b/src/app/frontend/src/components/InlineContentPreview.tsx @@ -0,0 +1,561 @@ +import { useState, useEffect } from 'react'; +import { + Button, + Text, + Badge, + Divider, + tokens, + Tooltip, + Accordion, + AccordionItem, + AccordionHeader, + AccordionPanel, +} from '@fluentui/react-components'; +import { + ArrowSync20Regular, + CheckmarkCircle20Regular, + Warning20Regular, + Info20Regular, + ErrorCircle20Regular, + Copy20Regular, + ArrowDownload20Regular, + ShieldError20Regular, +} from '@fluentui/react-icons'; +import type { GeneratedContent, ComplianceViolation, Product } from '../types'; + +interface InlineContentPreviewProps { + content: GeneratedContent; + onRegenerate: () => void; + isLoading?: boolean; + selectedProduct?: Product; + imageGenerationEnabled?: boolean; +} + +// Custom hook for responsive breakpoints +function useWindowSize() { + const [windowWidth, setWindowWidth] = useState(typeof window !== 'undefined' ? window.innerWidth : 1200); + + useEffect(() => { + const handleResize = () => setWindowWidth(window.innerWidth); + window.addEventListener('resize', handleResize); + return () => window.removeEventListener('resize', handleResize); + }, []); + + return windowWidth; +} + +export function InlineContentPreview({ + content, + onRegenerate, + isLoading, + selectedProduct, + imageGenerationEnabled = true, +}: InlineContentPreviewProps) { + const { text_content, image_content, violations, requires_modification, error, image_error, text_error } = content; + const [copied, setCopied] = useState(false); + const windowWidth = useWindowSize(); + + const isSmall = windowWidth < 768; + + // Helper to detect content filter errors + const isContentFilterError = (errorMessage?: string): boolean => { + if (!errorMessage) return false; + const filterPatterns = [ + 'content_filter', 'ContentFilter', 'content management policy', + 'ResponsibleAI', 'responsible_ai_policy', 'content filtering', + 'filtered', 'safety system', 'self_harm', 'sexual', 'violence', 'hate', + ]; + return filterPatterns.some(pattern => + errorMessage.toLowerCase().includes(pattern.toLowerCase()) + ); + }; + + const getErrorMessage = (errorMessage?: string): { title: string; description: string } => { + if (isContentFilterError(errorMessage)) { + return { + title: 'Content Filtered', + description: 'Your request was blocked by content safety filters. Please try modifying your creative brief.', + }; + } + return { + title: 'Generation Failed', + description: errorMessage || 'An error occurred. Please try again.', + }; + }; + + const handleCopyText = () => { + const textToCopy = [ + text_content?.headline && `✨ ${text_content.headline} ✨`, + text_content?.body, + text_content?.tagline, + ].filter(Boolean).join('\n\n'); + + navigator.clipboard.writeText(textToCopy); + setCopied(true); + setTimeout(() => setCopied(false), 2000); + }; + + const handleDownloadImage = async () => { + if (!image_content?.image_url) return; + + try { + const canvas = document.createElement('canvas'); + const ctx = canvas.getContext('2d'); + if (!ctx) return; + + const img = new Image(); + img.crossOrigin = 'anonymous'; + + img.onload = () => { + // Calculate banner height + const bannerHeight = Math.max(60, img.height * 0.1); + const padding = Math.max(16, img.width * 0.03); + + // Set canvas size to include bottom banner + canvas.width = img.width; + canvas.height = img.height + bannerHeight; + + // Draw the image at the top + ctx.drawImage(img, 0, 0); + + // Draw white banner at the bottom + ctx.fillStyle = '#ffffff'; + ctx.fillRect(0, img.height, img.width, bannerHeight); + + // Draw banner border line + ctx.strokeStyle = '#e5e5e5'; + ctx.lineWidth = 1; + ctx.beginPath(); + ctx.moveTo(0, img.height); + ctx.lineTo(img.width, img.height); + ctx.stroke(); + + // Draw text in the banner + const headlineText = selectedProduct?.product_name || text_content?.headline || 'Your Product'; + const headlineFontSize = Math.max(18, Math.min(36, img.width * 0.04)); + const taglineText = text_content?.tagline || ''; + const taglineFontSize = Math.max(12, Math.min(20, img.width * 0.025)); + + // Draw headline + ctx.font = `600 ${headlineFontSize}px Georgia, serif`; + ctx.fillStyle = '#1a1a1a'; + ctx.fillText(headlineText, padding, img.height + padding + headlineFontSize * 0.8, img.width - padding * 2); + + // Draw tagline if available + if (taglineText) { + ctx.font = `400 italic ${taglineFontSize}px -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif`; + ctx.fillStyle = '#666666'; + ctx.fillText(taglineText, padding, img.height + padding + headlineFontSize + taglineFontSize * 0.8 + 4, img.width - padding * 2); + } + + canvas.toBlob((blob) => { + if (blob) { + const url = URL.createObjectURL(blob); + const link = document.createElement('a'); + link.href = url; + link.download = 'generated-marketing-image.png'; + link.click(); + URL.revokeObjectURL(url); + } + }, 'image/png'); + }; + + img.onerror = () => { + if (image_content?.image_url) { + const link = document.createElement('a'); + link.href = image_content.image_url; + link.download = 'generated-image.png'; + link.click(); + } + }; + + img.src = image_content.image_url; + } catch { + if (image_content?.image_url) { + const link = document.createElement('a'); + link.href = image_content.image_url; + link.download = 'generated-image.png'; + link.click(); + } + } + }; + + // Get product display name + const getProductDisplayName = () => { + if (selectedProduct) { + return selectedProduct.product_name; + } + return text_content?.headline || 'Your Content'; + }; + + return ( +
    + {/* Selection confirmation */} + {selectedProduct && ( + + You selected "{selectedProduct.product_name}". Here's what I've created – let me know if you need anything changed. + + )} + + {/* Sparkle Headline - Figma style */} + {text_content?.headline && ( + + ✨ Discover the serene elegance of {getProductDisplayName()}. + + )} + + {/* Body Copy */} + {text_content?.body && ( + + {text_content.body} + + )} + + {/* Hashtags */} + {text_content?.tagline && ( + + {text_content.tagline} + + )} + + {/* Error Banner */} + {(error || text_error) && !violations.some(v => v.message.toLowerCase().includes('filter')) && ( +
    + +
    + + {getErrorMessage(error || text_error).title} + + + {getErrorMessage(error || text_error).description} + +
    +
    + )} + + {/* Image Preview - with bottom banner for text */} + {imageGenerationEnabled && image_content?.image_url && ( +
    + {/* Image container */} +
    + {image_content.alt_text + + {/* Download button on image */} + +
    + + {/* Text banner below image */} +
    + + {selectedProduct?.product_name || text_content?.headline || 'Your Product'} + + {text_content?.tagline && ( + + {text_content.tagline} + + )} +
    +
    + )} + + {/* Image Error State */} + {imageGenerationEnabled && !image_content?.image_url && (image_error || error) && ( +
    + + + {getErrorMessage(image_error || error).title} + + + Click Regenerate to try again + +
    + )} + + + + {/* User guidance callout for compliance status */} + {requires_modification ? ( +
    + + Action needed: This content has compliance issues that must be addressed before use. + Please review the details in the Compliance Guidelines section below and regenerate with modifications, + or manually edit the content to resolve the flagged items. + +
    + ) : violations.length > 0 ? ( +
    + + Optional review: This content is approved but has minor suggestions for improvement. + You can use it as-is or review the recommendations in the Compliance Guidelines section below. + +
    + ) : null} + + {/* Footer with actions */} +
    +
    + {/* Approval Status Badge */} + {requires_modification ? ( + }> + Requires Modification + + ) : violations.length > 0 ? ( + }> + Review Recommended + + ) : ( + }> + Approved + + )} +
    + +
    + +
    +
    + + {/* AI disclaimer */} + + AI-generated content may be incorrect + + + {/* Collapsible Compliance Section */} + {violations.length > 0 && ( + + + +
    + {requires_modification ? ( + + ) : violations.some(v => v.severity === 'error') ? ( + + ) : violations.some(v => v.severity === 'warning') ? ( + + ) : ( + + )} + + Compliance Guidelines ({violations.length} {violations.length === 1 ? 'item' : 'items'}) + +
    +
    + +
    + {violations.map((violation, index) => ( + + ))} +
    +
    +
    +
    + )} +
    + ); +} + +function ViolationCard({ violation }: { violation: ComplianceViolation }) { + const getSeverityStyles = () => { + switch (violation.severity) { + case 'error': + return { + icon: , + bg: '#fde7e9', + }; + case 'warning': + return { + icon: , + bg: '#fff4ce', + }; + case 'info': + return { + icon: , + bg: '#deecf9', + }; + } + }; + + const { icon, bg } = getSeverityStyles(); + + return ( +
    + {icon} +
    + + {violation.message} + + + {violation.suggestion} + +
    +
    + ); +} diff --git a/src/app/frontend/src/components/ProductReview.tsx b/src/app/frontend/src/components/ProductReview.tsx new file mode 100644 index 000000000..9c0d9e960 --- /dev/null +++ b/src/app/frontend/src/components/ProductReview.tsx @@ -0,0 +1,227 @@ +import { + Button, + Text, + tokens, +} from '@fluentui/react-components'; +import { + Sparkle20Regular, + Box20Regular, +} from '@fluentui/react-icons'; +import type { Product } from '../types'; + +interface ProductReviewProps { + products: Product[]; + onConfirm: () => void; + onStartOver: () => void; + isAwaitingResponse?: boolean; + availableProducts?: Product[]; + onProductSelect?: (product: Product) => void; + disabled?: boolean; +} + +export function ProductReview({ + products, + onConfirm, + onStartOver: _onStartOver, + isAwaitingResponse = false, + availableProducts = [], + onProductSelect, + disabled = false, +}: ProductReviewProps) { + const displayProducts = availableProducts.length > 0 ? availableProducts : products; + const selectedProductIds = new Set(products.map(p => p.sku || p.product_name)); + + const isProductSelected = (product: Product): boolean => { + return selectedProductIds.has(product.sku || product.product_name); + }; + + const handleProductClick = (product: Product) => { + if (onProductSelect) { + onProductSelect(product); + } + }; + + return ( +
    +
    + + Here is the list of available paints: + +
    + + {displayProducts.length > 0 ? ( +
    + {displayProducts.map((product, index) => ( + handleProductClick(product)} + disabled={disabled} + /> + ))} +
    + ) : ( +
    + + No products available. + +
    + )} + + {displayProducts.length > 0 && ( +
    + + {products.length === 0 && ( + + Select a product to continue + + )} +
    + )} + +
    + + AI-generated content may be incorrect + +
    +
    + ); +} + +interface ProductCardGridProps { + product: Product; + isSelected: boolean; + onClick: () => void; + disabled?: boolean; +} + +function ProductCardGrid({ product, isSelected, onClick, disabled = false }: ProductCardGridProps) { + return ( +
    + {product.image_url ? ( + {product.product_name} + ) : ( +
    + +
    + )} + +
    + + {product.product_name} + + + {product.tags || product.description || 'soft white, airy, minimal, fresh'} + + + ${product.price?.toFixed(2) || '59.95'} USD + +
    +
    + ); +} diff --git a/src/app/frontend/src/components/SelectedProductView.tsx b/src/app/frontend/src/components/SelectedProductView.tsx new file mode 100644 index 000000000..a4c4540f6 --- /dev/null +++ b/src/app/frontend/src/components/SelectedProductView.tsx @@ -0,0 +1,138 @@ +import { + Text, + Badge, + tokens, +} from '@fluentui/react-components'; +import { + Checkmark20Regular, + Box20Regular, +} from '@fluentui/react-icons'; +import type { Product } from '../types'; + +interface SelectedProductViewProps { + products: Product[]; +} + +export function SelectedProductView({ products }: SelectedProductViewProps) { + if (products.length === 0) return null; + + return ( +
    +
    + } + > + Products Selected + +
    + +
    + {products.map((product, index) => ( +
    + {product.image_url ? ( + {product.product_name} + ) : ( +
    + +
    + )} + +
    + + {product.product_name} + + + {product.tags || product.description || 'soft white, airy, minimal, fresh'} + + + ${product.price?.toFixed(2) || '59.95'} USD + +
    +
    + ))} +
    +
    + ); +} diff --git a/src/app/frontend/src/components/WelcomeCard.tsx b/src/app/frontend/src/components/WelcomeCard.tsx new file mode 100644 index 000000000..ab5740708 --- /dev/null +++ b/src/app/frontend/src/components/WelcomeCard.tsx @@ -0,0 +1,158 @@ +import { + Card, + Text, + tokens, +} from '@fluentui/react-components'; +import FirstPromptIcon from '../styles/images/firstprompt.png'; +import SecondPromptIcon from '../styles/images/secondprompt.png'; + +interface SuggestionCard { + title: string; + prompt: string; + icon: string; +} + +const suggestions: SuggestionCard[] = [ + { + title: "I need to create a social media post about paint products for home remodels. The campaign is titled \"Brighten Your Springtime\" and the audience is new homeowners. I need marketing copy plus an image. The image should be an informal living room with tasteful furnishings.", + prompt: "I need to create a social media post about paint products for home remodels. The campaign is titled \"Brighten Your Springtime\" and the audience is new homeowners. I need marketing copy plus an image. The image should be an informal living room with tasteful furnishings.", + icon: FirstPromptIcon, + }, + { + title: "Generate a social media campaign with ad copy and an image. This is for \"Back to School\" and the audience is parents of school age children. Tone is playful and humorous. The image must have minimal kids accessories in a children's bedroom. Show the room in a wide view.", + prompt: "Generate a social media campaign with ad copy and an image. This is for \"Back to School\" and the audience is parents of school age children. Tone is playful and humorous. The image must have minimal kids accessories in a children's bedroom. Show the room in a wide view.", + icon: SecondPromptIcon, + } +]; + +interface WelcomeCardProps { + onSuggestionClick: (prompt: string) => void; + currentInput?: string; +} + +export function WelcomeCard({ onSuggestionClick, currentInput = '' }: WelcomeCardProps) { + const selectedIndex = suggestions.findIndex(s => s.prompt === currentInput); + return ( +
    + {/* Welcome card with suggestions inside */} +
    + {/* Header with icon and welcome message */} +
    + + Welcome to your Content Generation Accelerator + + + Here are the options I can assist you with today + +
    + + {/* Suggestion cards - vertical layout */} +
    + {suggestions.map((suggestion, index) => { + const isSelected = index === selectedIndex; + return ( + onSuggestionClick(suggestion.prompt)} + style={{ + padding: 'clamp(12px, 2vw, 16px)', + cursor: 'pointer', + backgroundColor: isSelected ? tokens.colorBrandBackground2 : tokens.colorNeutralBackground1, + border: 'none', + borderRadius: '16px', + transition: 'all 0.2s ease', + }} + onMouseEnter={(e) => { + if (!isSelected) { + e.currentTarget.style.backgroundColor = tokens.colorBrandBackground2; + } + }} + onMouseLeave={(e) => { + if (!isSelected) { + e.currentTarget.style.backgroundColor = tokens.colorNeutralBackground1; + } + }} + > +
    +
    + Prompt icon +
    +
    + + {suggestion.title} + +
    +
    +
    + ); + })} +
    +
    +
    + ); +} diff --git a/src/App/src/main.tsx b/src/app/frontend/src/main.tsx similarity index 60% rename from src/App/src/main.tsx rename to src/app/frontend/src/main.tsx index db4a8119d..e59c93df9 100644 --- a/src/App/src/main.tsx +++ b/src/app/frontend/src/main.tsx @@ -1,17 +1,13 @@ import React from 'react'; import ReactDOM from 'react-dom/client'; import { FluentProvider, webLightTheme } from '@fluentui/react-components'; -import { Provider } from 'react-redux'; -import { store } from './store'; import App from './App'; import './styles/global.css'; ReactDOM.createRoot(document.getElementById('root')!).render( - - - - - + + + ); diff --git a/src/App/src/styles/global.css b/src/app/frontend/src/styles/global.css similarity index 100% rename from src/App/src/styles/global.css rename to src/app/frontend/src/styles/global.css diff --git a/src/app/frontend/src/styles/images/SamplePrompt.png b/src/app/frontend/src/styles/images/SamplePrompt.png new file mode 100644 index 0000000000000000000000000000000000000000..9a57c67965c7e119704e1fcbcb0728dc016d34c6 GIT binary patch literal 4938 zcmV-Q6SeG#P)uHZn41`pLiGc!F@vyX@oMK_SaIr|La8j@U7T8%)EK;(!Y>+pQ z1r#*d*|N&(Vg#{?*9M2Gj}F(?%12C zrPlxM?(e+M=_{f;?M}PXew>@4F`N0$+lyLNPpc?;)DCBMz7*lUHo=sds4CD)cg)F@ z<+;3Cf41|`rzxQrN2M^`V%SHqI zz?i(R0Nzl(rD)JoChrmr&_M)K_E>P1=p$@<65!1@-+Wjp_2ghMcocoU-|y4b))w&* zV5uDRC}Gx4%qzYQ+rdLcBlg@_+{;{n2qfT{>w`_A{w66tf(SWXd%d2VbL#-&FRL74g!Of1Fj|am+^7++ z+!{A4{s7q*<3~&|0A>kLIGTPJpXF5H`|tkT;}p%_MQNIHH!K0@`ak%K_^5MgW55GcVv>i48gFnrbW~wu_ zu>i%o$OK{0_u}t~00{IAlQjeEF#Ud}346?jfiS1r?KX8f9pixWoW$LUzVh$NkZui5 z3ZBNT{Urb<{WT|MOa$X5s&$b?k{8EBVToY%@Wi>@#$)pLFm%Fg?tIR~;QQYAIng(6 zeq%pX#u0+=JxpV^QC{FaKZzGTuZxZ=vOr!ebB)!bfHCCm4I~p}J7DgfaQ^$SJ8}1% zColSexO-1i6M?K@++PBCZhNDnEDPk|7s#T|9Airj@ilzpdjKxyArbsIj({={6TpuZ zba}={Z|K7Hc(LD3*nKBJsB7eez;$neWx-zT*;VmT^qUJy&;|FV?dB0mH5yk zATVft!G;@DvnlJ2;|2UiC*=+p$!MerXdCY9am zl*XL=rq&qxAjz(jZnuTDM7eM`ju8VWO;HbXu%}6;L>$?8vXR|p499MxY`_kE_br0h zwn(GFP*ovtVUYL!{52|icT<#<@Mo3Wqit@` zeH@JL!Smn99+lJ+}OZ%XGPSZUbiQgBO=3EsLbbRZnGCXRV9O`qbQlnQmkeQ4-s&cfAZjRF~~pP`Qn9h zqcHBM2N<^`=Ly^zg$Rbz;hTeq@_0fDU?WII+wGHp_nD#pCiGx7tdUudEm`pVB#$QP zea@cCA`G~8#2HA3=88m}HQGEwgK{o{IwsK;#To3#WJd8+YcXp_XY)V4_sJ0p9^nRS z$76NP7bUzE-t6_v`leBx&IAg%rE)1V3cTe9Hi9+Tf;=JSL%&#MbAxT5ZeIwKXUMAMT%*aDZKp55xqt>2q_W&TU6%Rhtw)UbeYXLq7@!$F&jrwb9{>3LKx%z)3Q?P@`B4v80Q*yoa<^ZvT5*t7w z4YnYobN$#4C4k`xgyXKHX_^GYWolu4z>BDHI=UknBCD@8_q-VUVn;xf`kI=PI#jOt zGgwy(F9H0!7iF)7HB`orZAGpcfB+kY;`avBGN@2@T2hkJJuqui&%OOfd-23q`^&o% zz|9?2wLBPY#Pk^-Dm$_Cm7}2$ItX&MTgxFUxh=BWe(q^u=L52ZFaM@WZv^1Ak-ZlF zD$0}*0`w6|-9Q0%(5H0LZo2flineYMt@bBj9Qu1m0C^7h(w?P)5+&0fQ5cLgX0s_+ z(x@a#?Vi02*%|k}3ZJQBoff|Ggi3E-Aj458yDJ8dPb@L;CJSRrDb2-Zm;jh2idMCd z65A29)m6)%`qt`dIPpjVC?Tx&%4a-y0c3pkN86*aVLc5v8A?J(92eT|Adm{Gzucm7 zgBJef2})NlLe!O%UzuD&1P((u2CFPFR0|K_kg6@Es!bd*1wSiFCW3ixJKhmCz$soM z2~Ou72w-D>M#QB{tS0}4M&A?4gVTJ<+8hS^FsKvubE7sJ)zXSZ*MbUGu9C#Z# z$T~#|$V!CRthQniF$H$punmDDdoH!CwkDs466{R?k^!M_!g;Uv%~~@gD`)m{LXib`<{+2&+K8`Ib6)y;pTU;qE70RFOwJ#$|G6Im6IH98Mq6H?d$VLU|M zZAi1|CbWzy_WV5N8&3ghFCDYE^xNnEiSFv% zsFlU4rl~oi$$D!B3*cf18NIRRqGxz~X~I+|@BeyriFRo_2%w$KH-2AJPDx45@8Wrs zgCK$+M3OqOF(U3+t3>oxB9BMaI4y9r*HhUhEo6!gWLtC>t`D@=K#X-O9dQ4g|HqN# zZ5xDq{2TuP@T*p*>d@3!j;lrjj7zr#JP1)Sco;l`!Zhw(LX2|q#MgEQ&n+4)0k(=I z0VWG7PmUD~5bAzr(OGf|azE%}oPr3T2U1XovZ0iB&9NEOMz%OJg|KyMl@_LM(t+t4 zv@mfK@kyUL8G@Arsu4qqpgscrTL8bl+9XNNAG3HH=kAQLZqSN+#BmPrJN>sz1ZaT& z>S(dwQvz&6VZ%%7^R)HEI2J`|{@_05Tbur7nRkcngh|YXj(g<{o;QHtXtG5U(YhGD z$=DuaWi7mEW(4?>9(xVoH&=&zgaE1}m~%Ssvr}gWKn)uWx@#CbV{Q*T(Fj-c=KE_B zy1E!o!yVv#F8UBHNdbU;7M2YyV^A zn|&Na8QaBVcsG4uch)(;(TM1(E+le`k8;mwHboU@kaVf4ZXZ0i(Yb(#zIYp(62QEv z6v;^idkWpxP=#)e%#X7_?Dm$}A4Dc-^1(pyJ?#@AU5%VHOo+HYQ2aSX{r`0$-^hjK zsC9GV0=1~3=d_mu7E-VZB3obUb}9lh~ohuI7rjkfyD5x7Kpa&&f}qQDNA zZ5XQxiQeEVX*Cjp3>c-|U&?)J#D#dK|ArUJjX3zs+ptI{oNPo#LKiyW3*8!;6fkEXu~Gi)M_9U z)8tcRY7#?+YU)?(nxfATYtbefqGjFxq&UCgU$zGZjg25Zh{+6KLy*pD`9%$ELBT&I zL@$x&PamU=-+GbE+%X!rjo2Hk*C}y8#1g?QaR!n7E>dooL$>5~reOIUxyd-> z5MlBAe?eZ{<^3`e@TLzh(}#OS{?6{t5&$JO_P+cRqs~#f_4xA?%^ltSCvIaA!0uSs z)l+B?E!fsT(Z#=CrSt#%K5g6@41d=o(d4=6YV5%?qPf|t^hdvc!CKZ5_Z^1~ikTH^ zdZ=MK;I0SYZ~Vqz63rZOewsc60;n*^Z3+~8k4@dBuWhNfzK*aJ8zgEjrNN+}A}_3t z*2ECEX#*oixjJS?J8|qa333SFiN!kSk`4cYPFe6Evi8A`fH%}#c#z&Zb%x@JS-4{3 zc8sz^?XmYAU?RJaGi3*h#h%3r12$K;M;ToMIM0QsZ zyDDw!pirDBMml!)Ti`Yc5g4pNJzoQSxuxY=M88x*0QLg!e)0>n@iU(_*6nh}qdE4k z3B0cakRhEO`{JmQVhd;cimS^2%#{MWq1i%4s9`2rnYGl~fwm=aJD|}`(S-jwe()^` zaSd}9+-GA~^=!y1h7KI(UGeqV2kHBt_(Ll1{%MLUUyqD!9td|3n-4((#AP*`x;05y z4Z2vwPyA9u-~LKbgE4illXh~}l4^Tsix%~8)9Gd5xPTWG00BzlT%Vz}*(F-J|98yh zfhEO8I1=#I6lp|Z8Pf5IcFSa0I<^(qMFN82o?Sn*p;9@*+q(wk7XdPf)|iBZM* z0RTtKFUi)pj%TSm`IBPZW0xuNk8aW=F=-sD4l>S88y%Z3S;{}22A4f1~-Y1MBlP0ms&{Iyh>~ zP-Jln<*!VFbYrF^BdmWaV?75;4AV@&ds7wp=(!1`g%DCd#n_SC3XGh-`}gWPdh-ib zo3b0TN@KPWMld>y$)5DdaN`wx2IR{%nl}pt6mnZ}7^=30uBPTd%U|9E@ik|^#pa@a za@#S=rjR}Xv&C8YrHF18;kN5tTKzJO*>?C5@bm@avRC7i9&!d=cE}>P)fqL<$V#}j zHAJ$HB@Zy3V2Z1s{y$bhjf_ncYypPHQq&Y(mCZ7Z-!$^;FcN>8Ai#cs_L~5|FrA*m zWY1ZE%1-FAz@S*_1R}`zWStEViC2h%ZUJkT{MP@&%!2i1!|w2Sbytz!0`m;<-}CWV z7{GHhX4}<*zS^x$W2Vz$2b@m(gqUiQcx!LSdjQoG5nOTI)CrLL&m@vuMao651hZ#?ljwe00b0jbY2gQj{pDw07*qo IM6N<$f}h89BLDyZ literal 0 HcmV?d00001 diff --git a/src/App/src/styles/images/contoso.svg b/src/app/frontend/src/styles/images/contoso.svg similarity index 100% rename from src/App/src/styles/images/contoso.svg rename to src/app/frontend/src/styles/images/contoso.svg diff --git a/src/App/src/styles/images/firstprompt.png b/src/app/frontend/src/styles/images/firstprompt.png similarity index 100% rename from src/App/src/styles/images/firstprompt.png rename to src/app/frontend/src/styles/images/firstprompt.png diff --git a/src/App/src/styles/images/secondprompt.png b/src/app/frontend/src/styles/images/secondprompt.png similarity index 100% rename from src/App/src/styles/images/secondprompt.png rename to src/app/frontend/src/styles/images/secondprompt.png diff --git a/src/App/src/types/index.ts b/src/app/frontend/src/types/index.ts similarity index 87% rename from src/App/src/types/index.ts rename to src/app/frontend/src/types/index.ts index 16a854bd4..33de59481 100644 --- a/src/App/src/types/index.ts +++ b/src/app/frontend/src/types/index.ts @@ -47,6 +47,14 @@ export interface ChatMessage { violations?: ComplianceViolation[]; } +export interface Conversation { + id: string; + user_id: string; + messages: ChatMessage[]; + brief?: CreativeBrief; + updated_at: string; +} + export interface AgentResponse { type: 'agent_response' | 'error' | 'status' | 'heartbeat'; agent?: string; @@ -64,6 +72,18 @@ export interface AgentResponse { }; } +export interface BrandGuidelines { + tone: string; + voice: string; + primary_color: string; + secondary_color: string; + prohibited_words: string[]; + required_disclosures: string[]; + max_headline_length: number; + max_body_length: number; + require_cta: boolean; +} + export interface ParsedBriefResponse { brief?: CreativeBrief; requires_confirmation: boolean; diff --git a/src/App/src/vite-env.d.ts b/src/app/frontend/src/vite-env.d.ts similarity index 100% rename from src/App/src/vite-env.d.ts rename to src/app/frontend/src/vite-env.d.ts diff --git a/src/App/tsconfig.json b/src/app/frontend/tsconfig.json similarity index 100% rename from src/App/tsconfig.json rename to src/app/frontend/tsconfig.json diff --git a/src/App/tsconfig.node.json b/src/app/frontend/tsconfig.node.json similarity index 100% rename from src/App/tsconfig.node.json rename to src/app/frontend/tsconfig.node.json diff --git a/src/App/vite.config.ts b/src/app/frontend/vite.config.ts similarity index 95% rename from src/App/vite.config.ts rename to src/app/frontend/vite.config.ts index 6f7ea5373..829c02469 100644 --- a/src/App/vite.config.ts +++ b/src/app/frontend/vite.config.ts @@ -23,7 +23,7 @@ export default defineConfig({ }, }, build: { - outDir: './static', + outDir: '../static', emptyOutDir: true, }, });