Created
December 27, 2019 14:53
-
-
Save JeffDarchuk/4aa455e4816210d24d09c69d964c7fb2 to your computer and use it in GitHub Desktop.
Unicorn scanner to alert on potential errors in unicorn configurations
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
param( | |
[string] $path | |
) | |
function Expand-Tokens{ | |
param( | |
[string] $configName, | |
[string] $path | |
) | |
$root = $configName.Split(".") | |
if ($root.Length -ge 1){ | |
$path = $path.replace("`$(module)", $root[$root.Length-1]) | |
} | |
if ($root.Length -ge 2){ | |
$path = $path.replace("`$(layer)", $root[$root.Length-2]) | |
} | |
if ($root.Length -ge 3){ | |
$path = $path.replace("`$(tenant)", $root[$root.Length-3]) | |
} | |
return $path | |
} | |
function Expand-ConfigToDescendants{ | |
param( | |
[hashtable] $trie, | |
[string] $config, | |
[string] $dependencies, | |
[System.Xml.XmlElement] $include, | |
[int] $depth = -1 | |
) | |
$s = new-object system.collections.stack | |
$s.Push($trie) | |
while ($s.Count -gt 0 -and $depth -ne 0){ | |
$depth-- | |
$tmp = $s.pop() | |
$null = $tmp.config[$config] = @{ | |
dependencies = $dependencies | |
include = $include | |
includeChildren = $true | |
} | |
$tmp.parent.config.values | foreach-object { | |
if ($_.includeChildren -and -not $tmp.config.ContainsKey($_.config)){ | |
$tmp[$_.config] = $_ | |
} | |
} | |
$tmp.next.Keys | foreach-object{ | |
$s.Push($tmp.next[$_]) | |
} | |
} | |
return $trie | |
} | |
function Expand-Trie{ | |
param( | |
[hashtable] $trie, | |
[string] $key, | |
[string] $dependencies, | |
[System.Xml.XmlElement] $include, | |
[string] $config = $null | |
) | |
if (-not $trie.next.ContainsKey($key)){ | |
$trie.next[$key] = @{ | |
node = $key | |
config = @{} | |
next = @{} | |
parent = $trie | |
path = "$($trie.path)/$key" | |
database = $trie.database | |
} | |
} | |
if ($null -ne $config -and $config -ne ""){ | |
$null = $trie.next[$key].config[$config] = @{ | |
config = $config | |
dependencies = $dependencies | |
include = $include | |
includeChildren = $true | |
} | |
$trie.parent.config.values | foreach-object { | |
if ($_.includeChildren -and -not $trie.config.ContainsKey($_.config)){ | |
$trie.config[$_.config] = $_ | |
} | |
} | |
$mainPath = Expand-Tokens -path $include.GetAttribute("path") -configName $config | |
$parent = $trie.next[$key] | |
if ($include.exclude.Count -ne 0){ | |
$include.exclude | foreach-object{ | |
if ($_.GetAttribute("children").ToLower() -eq "true"){ | |
$trie.next[$key].config[$config].includeChildren = $false | |
} | |
$path = Expand-Tokens -path $_.GetAttribute("path").Replace($mainPath, "") -configName $config | |
if ($path.Length -eq 0){ | |
$path = Expand-Tokens -path $_.GetAttribute("childrenOfPath").Replace($mainPath, "") -configName $config | |
} | |
if ($path.Length -ne 0){ | |
$tmp = $parent | |
$path.Split("/") | foreach-object{ | |
if (-not $tmp.next.ContainsKey($_)){ | |
$tmp.next[$_] = @{ | |
node = $_ | |
config = @{} | |
next = @{} | |
parent = $tmp | |
path = "$($tmp.path)/$_" | |
database = $tmp.database | |
} | |
} | |
$null = $tmp.next[$_].config[$config] = @{ | |
config = $config | |
dependencies = $dependencies | |
include = $include | |
includeChildren = $false | |
} | |
$tmp.parent.config.values | foreach-object { | |
if ($_.includeChildren -and -not $tmp.config.ContainsKey($_.config)){ | |
$tmp.config[$_.config] = $_ | |
} | |
} | |
$tmp = $tmp.next[$_] | |
} | |
}else{ | |
$children = $_.GetAttribute("children") | |
if ($children.ToLower() -eq "true"){ | |
$_.except | foreach-object{ | |
if ($null -ne $_){ | |
$exceptName = $_.GetAttribute("name") | |
$exceptIncludeChildren = $_.GetAttribute("includeChildren").ToLower() -eq "true" | |
$tmp = $parent | |
if ($exceptName -eq "*"){ | |
Expand-ConfigToDescendants -trie $parent -config $config -depth 1 -dependencies $dependencies -include $include | |
}else{ | |
$exceptName.Split("/") | foreach-object { | |
if (-not $tmp.next.ContainsKey($_)){ | |
$tmp.next[$_] = @{ | |
node = $_ | |
config = @{} | |
next = @{} | |
parent = $tmp | |
path = "$($tmp.path)/$_" | |
database = $tmp.database | |
} | |
} | |
$null = $tmp.next[$_].config[$config] = @{ | |
config = $config | |
dependencies = $dependencies | |
include = $include | |
includeChildren = $exceptIncludeChildren | |
} | |
$tmp.parent.config.values | foreach-object { | |
if ($_.includeChildren -and -not $tmp.config.ContainsKey($_.config)){ | |
$tmp.config[$_.config] = $_ | |
} | |
} | |
$tmp = $tmp.next[$_] | |
} | |
if ($exceptIncludeChildren){ | |
Expand-ConfigToDescendants -trie $parent -config $config -dependencies $dependencies -include $include | |
} | |
} | |
} | |
} | |
} | |
} | |
} | |
} | |
} | |
$trie = $trie.next[$key] | |
return $trie | |
} | |
function Traverse-Path{ | |
param( | |
[hashtable] $Trie, | |
[string] $database, | |
[string] $config, | |
[string] $path, | |
[string] $dependencies, | |
[System.Xml.XmlElement] $include | |
) | |
$isCore = $database -eq "core" | |
if ($isCore){ | |
$Trie = $Trie.core | |
}else{ | |
$Trie = $Trie.master | |
} | |
$paths = $path.Split("/") | |
for ($i = 0; $i -lt $paths.Length-1;$i++){ | |
if ($paths[$i] -ne ""){ | |
$Trie = Expand-Trie -trie $Trie -key $paths[$i] -dependencies $dependencies -include $include | |
} | |
} | |
#write-host "$($trie.path)/$($paths[$paths.Length - 1]) - $config" -ForegroundColor red | |
$Trie = Expand-Trie -trie $Trie -key $paths[$paths.Length - 1] -dependencies $dependencies -include $include -config $config | |
} | |
function Build-Trie{ | |
param ( | |
[hashtable] $Trie, | |
[System.Xml.XmlElement] $xml, | |
[hashtable] $Abstract | |
) | |
$config = $xml.GetAttribute("name") | |
$extends = $xml.GetAttribute("extends") | |
while($extends -ne ""){ | |
if ($null -ne $Abstract[$extends].include){ | |
$Abstract[$extends].include | ForEach-Object{ | |
Traverse-Path -Trie $Trie -database $_.GetAttribute("database") -config $config -path (Expand-Tokens -path $_.GetAttribute("path") -configName $config) -dependencies $_.ParentNode.ParentNode.GetAttribute("dependencies") -include $_ | |
} | |
$extends = $Abstract[$extends].extends | |
}else{ | |
$extends = "" | |
} | |
} | |
$xml.predicate.include | ForEach-Object { | |
if ($null -ne $_){ | |
Traverse-Path -Trie $Trie -database $_.GetAttribute("database") -config $config -path (Expand-Tokens -path $_.GetAttribute("path") -configName $config) -dependencies $_.ParentNode.ParentNode.GetAttribute("dependencies") -include $_ | |
} | |
} | |
} | |
$mainTrie = @{ | |
master = @{ | |
node = "sitecore" | |
config = @{} | |
next = @{} | |
parent = $null | |
path = "" | |
database = "master" | |
} | |
core = @{ | |
node = "sitecore" | |
config = @{} | |
next = @{} | |
parent = $null | |
path = "" | |
database = "core" | |
} | |
dependencies = @{} | |
} | |
$abstract = @{} | |
$xmls = New-Object System.Collections.Generic.List[System.Xml.XmlElement] | |
get-childitem -Path $path -Filter *.config -Recurse -Exclude node_modules | Foreach-Object{ | |
$xml = $null | |
try{ | |
$xml = [xml] (Get-Content $_.FullName) | |
}catch{} | |
if ($null -ne $xml -and $xml.configuration.sitecore.unicorn -ne $null -and -not $_.FullName.Contains("\obj\")){ | |
$xml.configuration.sitecore.unicorn.configurations.configuration | foreach-object{ | |
if ($null -ne $_){ | |
$configuration = $_ | |
$isAbstract = $null -ne $configuration.GetAttribute("abstract") -and $configuration.GetAttribute("abstract").ToLower() -eq "true" | |
if ($isAbstract){ | |
$_.predicate.include | Foreach-Object { | |
if ($_ -ne $null){ | |
if (-not $abstract.ContainsKey($_.ParentNode.ParentNode.GetAttribute("name"))){ | |
$abstract.Add($_.ParentNode.ParentNode.GetAttribute("name"), @{ | |
include = (New-Object System.Collections.Generic.List[[System.Xml.XmlElement]]) | |
extends = $_.ParentNode.ParentNode.GetAttribute("extends") | |
}) | |
} | |
$abstract[$_.ParentNode.ParentNode.GetAttribute("name")].include.Add($_) | |
} | |
} | |
}else{ | |
$xmls.Add($configuration) | |
$mainTrie.dependencies[$configuration.GetAttribute("name")] = @{ | |
text = $configuration.GetAttribute("dependencies") | |
} | |
} | |
} | |
} | |
} | |
} | |
$mainTrie.dependencies.Keys | foreach-object { | |
$dep = $mainTrie.dependencies[$_] | |
$dep.dependson = New-Object System.Collections.Generic.HashSet[string] | |
$s = new-object system.collections.stack | |
$s.Push($dep) | |
while ($s.Count){ | |
$childDep = $s.Pop() | |
$individualDependencies = $childDep.text.Split(",") | |
$individualDependencies | foreach-object { | |
$dependency = $_.trim() | |
if ($dependency.EndsWith("*")){ | |
$wildcard = $dependency.Replace("*", "") | |
$mainTrie.dependencies.Keys | foreach-object{ | |
if ($_.startsWith($wildcard)){ | |
$null = $dep.dependson.Add($_) | |
if ($mainTrie.dependencies.ContainsKey($_)){ | |
$s.Push($mainTrie.dependencies[$_]) | |
} | |
} | |
} | |
}else{ | |
$null = $dep.dependson.Add($dependency) | |
if ($mainTrie.dependencies.ContainsKey($dependency)){ | |
$s.Push($mainTrie.dependencies[$dependency]) | |
} | |
} | |
} | |
} | |
} | |
$xmls | ForEach-Object { | |
Build-Trie -Trie $mainTrie -xml $_ -Abstract $abstract | |
} | |
return $mainTrie | |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
param( | |
[hashtable] $Trie, | |
[switch] $ErrorOnOrphans, | |
[switch] $ErrorOnDependencyMismatch | |
) | |
write-host "`n=======================================================================================" -ForegroundColor Cyan | |
write-host "Beginning scan of unicorn configurations for potential errors" -ForegroundColor Cyan | |
write-host "=======================================================================================`n" -ForegroundColor Cyan | |
$s = new-object system.collections.stack | |
$s.Push($Trie.core) | |
$s.Push($Trie.master) | |
$PathRequirementsMaster = New-Object System.Collections.Generic.HashSet[string] | |
$PathRequirementsCore = New-Object System.Collections.Generic.HashSet[string] | |
$Orphans = New-Object System.Collections.Generic.HashSet[string] | |
while ($s.Count -ne 0){ | |
$t = $s.Pop() | |
if ($t.config.Count -ne 0 -and $t.parent.config.Count -eq 0){ | |
if ($t.database -eq "master"){ | |
$null = $PathRequirementsMaster.Add($t.parent.path) | |
}elseif($t.database -eq "core"){ | |
$null = $PathRequirementsCore.Add($t.parent.path) | |
} | |
} | |
if ($t.config.Count -eq 0 -and $t.parent.config.Count -ne 0){ | |
$null = $Orphans.Add($t.path) | |
} | |
$parent = $t.parent | |
if ($t.config.Count -gt 0){ | |
$t.config.Keys | foreach-object { | |
$config = $t.config[$_].config | |
$path = $t.path | |
while ($null -ne $parent){ | |
$found = $false | |
if ($parent.config.Count -ne 0){ | |
$parent.config.keys | foreach-object{ | |
if (-not $found -and ($parent.config[$_].config -eq $config -or $Trie.dependencies[$config].dependson.Contains($parent.config[$_].config))){ | |
$found = $true | |
} | |
} | |
}else{ | |
$found = $true | |
} | |
if (-not $found){ | |
if ($ErrorOnDependencyMismatch){ | |
write-error "The following is a report on a dependency mismatch." | |
}else{ | |
write-host "`nIssue found with configuration:" -ForegroundColor red | |
write-host "`n`tConfiguration " -NoNewline | |
write-host $config -ForegroundColor Magenta -NoNewline | |
write-host " has a path dependency on one of [" -NoNewline | |
write-host $parent.config.Keys -ForegroundColor Magenta -NoNewline | |
write-host "] that isn't explicitly defined" | |
write-host "`n`t$($t.database) => $($path)" -ForegroundColor Magenta | |
} | |
} | |
$parent = $parent.parent | |
} | |
} | |
} | |
$t.next.keys | Foreach-object { | |
$s.Push($t.next[$_]) | |
} | |
} | |
if ($Orphans.Count -ne 0){ | |
write-host "`n=======================================================================================" -ForegroundColor Cyan | |
write-host "Orphans detected, items which appear to be required that aren't tracked by any configs" -ForegroundColor Cyan | |
write-host "=======================================================================================`n" -ForegroundColor Cyan | |
if ($ErrorOnOrphans){ | |
write-error "The following are orphans detected." | |
} | |
$Orphans | Sort-Object -Property Length | Foreach-Object { | |
write-host "`t$_" -ForegroundColor Magenta | |
} | |
} | |
write-host "`n=======================================================================================" -ForegroundColor Cyan | |
write-host "Configurations require that these paths exist as part of the default state of Sitecore" -ForegroundColor Cyan | |
write-host "=======================================================================================`n" -ForegroundColor Cyan | |
write-host "Core:" -ForegroundColor Cyan | |
$PathRequirementsCore | Sort-Object -Property Length | Foreach-Object { | |
write-host "`t$_" | |
} | |
write-host "Master:" -ForegroundColor Cyan | |
$PathRequirementsMaster | Sort-Object -Property Length | Foreach-Object { | |
write-host "`t$_" | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment