This repository has been archived by the owner on Nov 16, 2023. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 6
/
Copy pathTypes.ps1
492 lines (383 loc) · 17 KB
/
Types.ps1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
<##
# Types.ps1
##
# Assumptions:
# - AzureRM context is initialized
# Design Considerations:
# - Resources with globally unique names are given random IDs and should be accessed by resource group, not name
# - Defining a class "Environment" will cause issues with System.Environment
#>
using namespace Microsoft.Azure.Commands.ResourceManager.Cmdlets.SdkModels
using namespace Microsoft.Azure.Commands.Common.Authentication.Abstractions
using namespace System.Collections
# required for importing types
Import-Module "AzureRm", "$PSScriptRoot\AzureBakery"
class ClusterResourceGroup {
# Indentity vector (path through the service tree)
[string[]]$Identity
# where region-agnostic resources are defined
static [string] $DefaultRegion = "West US 2"
# string preceding a random string on underlying resource names
static [string] $DefaultResourcePrefix = "cluster"
# name of the blob storage container containing service artifacts
static [string] $ArtifactContainerName = "artifacts"
# name of the blob storage container containing VM images
static [string] $ImageContainerName = "images"
# underlying property for lazily instantiating Azure Storage Contexts
hidden [IStorageContext] $_StorageContext
# create a ClusterResourceGroup model object from its resource group name
ClusterResourceGroup([string] $resourceGroupName) {
$this.Identity = $resourceGroupName -split "-"
}
# use the values encapsulated in this model object to provision Azure resources
[void] Create() {
if ($this.Exists()) {
throw "Resource Group '$this' already exists"
}
# determine if this resource group has a speified region (for Environments and Clusters)
$region = @{
$True = $this.Identity[2]
$False = [ClusterResourceGroup]::DefaultRegion
}[$this.Identity.Count -ge 3]
# create and initialize the Azure resources
New-AzureRmResourceGroup -Name $this -Location $region
New-AzureRmStorageAccount `
-ResourceGroupName $this `
-Name ([ClusterResourceGroup]::NewResourceName()) `
-Location $region `
-Type "Standard_LRS" `
-EnableEncryptionService "blob" `
-EnableHttpsTrafficOnly $true
New-AzureRmKeyVault `
-VaultName ([ClusterResourceGroup]::NewResourceName()) `
-ResourceGroupName $this `
-Location $region
New-AzureStorageContainer `
-Context $this.GetStorageContext() `
-Name ([ClusterResourceGroup]::ArtifactContainerName)
New-AzureStorageContainer `
-Context $this.GetStorageContext() `
-Name ([ClusterResourceGroup]::ImageContainerName)
# if the resource group has a parent (isn't a 'Service'), propagate assets from parent to this
$parentId = ($this.Identity | Select -SkipLast 1) -join "-"
if ($parentId) {
$parent = [ClusterResourceGroup]::new($parentId)
$parent.PropagateArtifacts()
$parent.PropagateImages()
$parent.PropagateSecrets()
}
}
# returns whether this model's Azure resources have been created
[bool] Exists() {
$resourceGroup = Get-AzureRmResourceGroup `
-ResourceGroupName $this `
-ErrorAction SilentlyContinue
return $resourceGroup -as [bool]
}
# returns a model for each child service tree node that has been provisioned in Azure
[ClusterResourceGroup[]] GetChildren() {
$children = Get-AzureRmResourceGroup `
| % {$_.ResourceGroupName} `
| ? {$_ -match "^$this-[^-]+$"} `
| % {[ClusterResourceGroup]::new($_)}
return @($children)
}
# returns a model for each descendant service tree node (not leaves/Clusters) that has been provisioned in Azure
[ClusterResourceGroup[]] GetDescendantNodes() {
$descendants = Get-AzureRmResourceGroup `
| % {$_.ResourceGroupName} `
| ? {$_ -like "$this*" -and ($_ -split '-').Count -le 3} `
| % {[ClusterResourceGroup]::new($_)}
return @($descendants)
}
# lazily instantiates an Azure Storage Context for use with the Azure.Storage module
[IStorageContext] GetStorageContext() {
if (-not $this._StorageContext) {
$storageAccount = Get-AzureRmStorageAccount -ResourceGroupName $this
$this._StorageContext = $storageAccount.Context
}
return $this._StorageContext
}
# uses the AzureBakery nested module for creating a generalized Windows VHD containing the specified Windows Features
[void] NewImage([string[]] $WindowsFeature) {
New-BakedImage `
-StorageContext $this.GetStorageContext() `
-WindowsFeature $WindowsFeature `
-StorageContainer ([ClusterResourceGroup]::ImageContainerName)
}
# pushes Artifacts from this service tree node to its descendants
[void] PropagateArtifacts() {
$this.PropagateBlobs([ClusterResourceGroup]::ArtifactContainerName)
}
# pushes Images from this service tree node to its descendants
[void] PropagateImages() {
$this.PropagateBlobs([ClusterResourceGroup]::ImageContainerName)
}
# pushes Blobs in the specified container from this service tree node to its descendants
[void] PropagateBlobs([string] $Container) {
$descendants = $this.GetDescendantNodes()
if (-not $descendants) {
return
}
$descendantContexts = $descendants.GetStorageContext()
$artifactNames = Get-AzureStorageBlob `
-Container $Container `
-Context $this.GetStorageContext() `
| % {$_.Name}
# async start copying blobs
$pendingBlobs = [ArrayList]::new()
foreach ($descendantContext in $descendantContexts) {
foreach ($artifactName in $artifactNames) {
$descendantBlob = Get-AzureStorageBlob `
-Context $descendantContext `
-Container $Container `
-Blob $artifactName `
-ErrorAction SilentlyContinue
if (-not $descendantBlob) {
$descendantBlob = Start-AzureStorageBlobCopy `
-Context $this.GetStorageContext() `
-DestContext $descendantContext `
-SrcContainer $Container `
-DestContainer $Container `
-SrcBlob $artifactName `
-DestBlob $artifactName
$pendingBlobs.Add($descendantBlob)
}
}
}
# block until all copies are complete
foreach ($blob in $pendingBlobs) {
Get-AzureStorageBlobCopyState `
-Context $blob.Context `
-Container $Container `
-Blob $blob.Name `
-WaitForComplete
}
}
# pushes Azure Key Vault Secrets from this service tree node to its descendants
[void] PropagateSecrets() {
$descendants = $this.GetDescendantNodes()
if (-not $descendants) {
return
}
$keyVaultName = (Get-AzureRmKeyVault -ResourceGroupName $this).VaultName
$descendantKeyVaultNames = $descendants `
| % {Get-AzureRmKeyVault -ResourceGroupName $_} `
| % {$_.VaultName}
$secretNames = (Get-AzureKeyVaultSecret -VaultName $keyVaultName).Name
foreach ($childKeyVaultName in $descendantKeyVaultNames) {
foreach ($secretName in $secretNames) {
$secret = Get-AzureKeyVaultSecret `
-VaultName $keyVaultName `
-Name $secretName
Set-AzureKeyVaultSecret `
-VaultName $childKeyVaultName `
-Name $secretName `
-SecretValue $secret.SecretValue `
-ContentType $secret.Attributes.ContentType
}
}
}
# returns this model's associated resource group name
[string] ToString() {
return $this.Identity -join "-"
}
# determines the service tree node type (discouraged as it inherently breaks linting)
[Reflection.TypeInfo] InferType() {
switch ($this.Identity.Count) {
1 {return [ClusterService]}
2 {return [ClusterFlightingRing]}
3 {return [ClusterEnvironment]}
4 {return [Cluster]}
}
throw "Cannot infer type of '$this'"
return [void] # return value to not break linting
}
# uploads an Artifact (file required for the VM/Container/etc to initialize) to the service tree node
[void] UploadArtifact([string] $ArtifactPath) {
Set-AzureStorageBlobContent `
-File $ArtifactPath `
-Container ([ClusterResourceGroup]::ArtifactContainerName) `
-Blob (Split-Path -Path $ArtifactPath -Leaf) `
-Context $this.GetStorageContext() `
-Force
}
# creates a base36 GUID with a prefix and valid length for creating globally unique Azure resource names
static [string] NewResourceName() {
$Length = 24
$allowedChars = "abcdefghijklmnopqrstuvwxyz0123456789"
$chars = 1..($Length - [ClusterResourceGroup]::DefaultResourcePrefix.Length) `
| % {Get-Random -Maximum $allowedChars.Length} `
| % {$allowedChars[$_]}
return [ClusterResourceGroup]::DefaultResourcePrefix + ($chars -join '')
}
}
class ClusterService : ClusterResourceGroup {
[ValidatePattern("^[A-Z][A-z0-9]+$")]
[string]$Service
ClusterService([string] $resourceGroupName) : base($resourceGroupName) {
$this.Service = $this.Identity
}
}
class ClusterFlightingRing : ClusterResourceGroup {
[ValidateNotNullOrEmpty()]
[ClusterService]$Service
[ValidatePattern("^[A-Z]{3,6}$")]
[string]$FlightingRing
ClusterFlightingRing([string] $resourceGroupName) : base($resourceGroupName) {
$this.Service = [ClusterService]::new($this.Identity[0])
$this.FlightingRing = $this.Identity | Select -Last 1
}
}
class ClusterEnvironment : ClusterResourceGroup {
[ValidateNotNullOrEmpty()]
[ClusterFlightingRing]$FlightingRing
[ValidatePattern("^[A-z][A-z0-9 ]+$")]
[string]$Region
static [int] $TTL = 5
static [string] $MonitorPath = "/"
ClusterEnvironment([string] $resourceGroupName) : base($resourceGroupName) {
$this.FlightingRing = [ClusterFlightingRing]::new($this.Identity[0..1] -join "-")
$this.Region = $this.Identity | Select -Last 1
}
# creates a Cluster Azure resource group group that is a child of this model and returns the Cluster's model
[Cluster] NewChildCluster() {
$indexes = ($this.GetChildren() | % {[Cluster]::new($_)}).Index # get currently used indexes
for ($index = 0; $index -in $indexes; $index++) {} # determine lowest available index
$cluster = [Cluster]::new("$this-$index") # create a Cluster model with the index
$cluster.Create() # create the Azure resources from the Cluster model
return $cluster # return the Cluster model
}
[void] Create() {
([ClusterResourceGroup]$this).Create()
New-AzureRmTrafficManagerProfile `
-Name "Profile" `
-ResourceGroupName $this `
-TrafficRoutingMethod Weighted `
-RelativeDnsName ([ClusterResourceGroup]::NewResourceName()) `
-Ttl ([ClusterEnvironment]::TTL) `
-MonitorProtocol HTTP `
-MonitorPort 80 `
-MonitorPath ([ClusterEnvironment]::MonitorPath)
}
}
class Cluster : ClusterResourceGroup {
[ValidateNotNullOrEmpty()]
[ClusterEnvironment]$Environment
[ValidateRange(0, 255)]
[int]$Index
Cluster([string] $resourceGroupName) : base($resourceGroupName) {
$this.Environment = [ClusterEnvironment]::new($this.Identity[0..2] -join "-")
$this.Index = $this.Identity | Select -Last 1
}
# create a service tree node resource group and resources, with additional Clsuter-specific resources
[void] Create() {
New-AzureRmResourceGroup -Name $this -Location $this.Environment.Region
New-AzureRmStorageAccount `
-ResourceGroupName $this `
-Name ([ClusterResourceGroup]::NewResourceName()) `
-Location $this.Environment.Region `
-Type "Standard_LRS" `
-EnableEncryptionService "blob" `
-EnableHttpsTrafficOnly $true
New-AzureStorageContainer `
-Context $this.GetStorageContext() `
-Name "configuration"
New-AzureStorageContainer `
-Context $this.GetStorageContext() `
-Name "disks"
}
# uses the Cluster configuration inheritence model (see README) to identify the most specific config with the specified extension
[string] GetConfig([string]$DefinitionsContainer, [string]$FileExtension) {
($service, $flightingRing, $region, $index) = $this.Identity
$config = $service, "Default" `
| % {"$_.$flightingRing.$region", "$_.$flightingRing", $_} `
| % {"$DefinitionsContainer\$_.$FileExtension"} `
| ? {Test-Path $_} `
| Select -First 1
return $config
}
[PSResourceGroupDeployment] PublishConfiguration([string]$DefinitionsContainer, [datetime]$Expiry) {
$context = $this.GetStorageContext()
# build url components
$vhdContainer = "$($context.BlobEndpoint)disks/"
$configurationSasToken = New-AzureStorageContainerSASToken `
-Context $context `
-Container "configuration" `
-Permission "r" `
-ExpiryTime $expiry
# template deployment parameters
$deploymentParams = @{
ResourceGroupName = $this
TemplateFile = $this.GetConfig($DefinitionsContainer, "template.json")
Environment = $this.Environment
VhdContainer = $vhdContainer
SasToken = $configurationSasToken
}
# package and upload DSC
$dscFile = $this.GetConfig($DefinitionsContainer, "dsc.ps1")
if ($dscFile) {
$publishDscParams = @{
ConfigurationPath = $dscFile
OutputArchivePath = "$env:TEMP\dsc.zip"
Force = $true
}
$dscConfigDataFile = $this.GetConfig($DefinitionsContainer, "dsc.psd1")
if ($dscConfigDataFile) {
$publishDscParams["ConfigurationDataPath"] = $dscConfigDataFile
}
Publish-AzureRmVMDscConfiguration @publishDscParams
Set-AzureStorageBlobContent `
-File "$env:TEMP\dsc.zip" `
-Container "configuration" `
-Blob "dsc.zip" `
-Context $context `
-Force
$deploymentParams["DscUrl"] = "$($context.BlobEndpoint)configuration/dsc.zip"
$deploymentParams["DscFileName"] = Split-Path -Path $dscFile -Leaf
$deploymentParams["DscHash"] = (Get-FileHash "$env:TEMP\dsc.zip").Hash.Substring(0, 50)
}
# package and upload CSE
$cseFile = $this.GetConfig($DefinitionsContainer, "cse.ps1")
if ($cseFile) {
Set-AzureStorageBlobContent `
-File $cseFile `
-Container "configuration" `
-Blob "cse.ps1" `
-Context $context `
-Force
$deploymentParams["CseUrl"] = "$($context.BlobEndPoint)configuration/cse.ps1"
}
# template parameters
$templateParameterFile = $this.GetConfig($DefinitionsContainer, "parameters.json")
if ($templateParameterFile) {
$deploymentParams["TemplateParameterFile"] = $templateParameterFile
}
# freeform json passed to the DSC
$configJsonFile = $this.GetConfig($DefinitionsContainer, "config.json")
if ($configJsonFile) {
$deploymentParams["ConfigJson"] = Get-Content $configJsonFile -Raw
}
# baked Windows Image URL (from parent Environment)
$environmentContext = $this.Environment.GetStorageContext()
$images = Get-AzureStorageBlob `
-Context $environmentContext `
-Container ([ClusterResourceGroup]::ImageContainerName)
if ($images) {
$imageName = $images | Sort LastModified -Descending | Select -First 1 | % Name
$deploymentParams["ImageUrl"] = "$($environmentContext.BlobEndPoint)images/$imageName"
}
# deploy template
$deploymentErrors = $null # redundantly define for linting
$deployment = New-AzureRmResourceGroupDeployment `
-Name ((Get-Date -Format "s") -replace "[^\d]") `
@deploymentParams `
-Verbose `
-ErrorVariable deploymentErrors `
-Force
if ($deploymentErrors) {
throw $deploymentErrors
}
return $deployment
}
}