NewRelicPS.Configuration.SyntheticLocationCondition.psm1

<#
.Synopsis
  Idempotently applies New Relic synthetic condition configurations
.Description
  Idempotently applies New Relic synthetic condition configurations
.Example
  Set-SyntheticConditionConfiguration -AdminAPIKey $AdminAPIKey -DefinedPolicies $Config.AlertPolicies
  Uses New Relic APIs to update synthetic alert conditions to match the conditions defined in $Config.AlertPolicies. Any existing conditions that are not defined will be removed.
.Parameter AdminAPIAPIKey
  Must be an admin user's API Key, not an account-level REST API Key. See more here: https://docs.newrelic.com/docs/apis/get-started/intro-apis/types-new-relic-api-keys
.Parameter DefinedPolicies
  An array of policy objects which define the desired configuration state for New Relic alert policies and conditions
#>

Function Set-NRSyntheticLocationConditionConfiguration {
  [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSShouldProcess", '', Justification = "All CMDLets being called have ShouldProcess in place and the preference is passed to them." )]
  [CMDLetBinding(SupportsShouldProcess = $true)]
  Param (
    [Parameter (Mandatory = $true)]
    [string] $AdminAPIKey,
    [array] $DefinedPolicies
  )

  # Iterate over each defined policy
  $existingPolicies = Get-NRAlertPolicy -APIKey $AdminAPIKey
  Foreach ($policy in $DefinedPolicies) {
    $existingPolicy = $existingPolicies | Where-Object {$_.name -eq $policy.name}
    [System.Collections.ArrayList]$existingConditions = @(Get-NRSyntheticLocationCondition -APIKey $AdminAPIKey -PolicyId $existingPolicy.id)
    $definedConditionCount = ($policy.conditions | Where-Object {$_.type -eq 'SyntheticLocation'}).name.count
    Write-Verbose "Policy $($policy.name) has $definedConditionCount defined Synthetic conditions and $($existingConditions.count) existing Synthetic conditions."

    # Iterate over each synthetic location condition described in the policy
    Foreach ($condition in ($policy.conditions | Where-Object {$_.type -eq 'SyntheticLocation'})) {
      [array]$existingConditionConfig = $existingConditions | Where-Object { $_.name -eq $condition.name }

      # Items are declared by name and are later given an ID by New Relic, therefore resources with the same name will cause issues and are unsupported.
      If ($existingConditionConfig.count -gt 1) {
        Write-Error "Multiple conditions found with the name $($existingConditionConfig.name[0])!
        Ids: $($existingConditionConfig.id -join (','))
        This must be resolved manually before continuing."
 -ErrorAction 'Stop'
      }

      # Update existing condition if needed
      If ($existingConditionConfig) {
        $conditionRequiresUpdate = Compare-NRSyntheticLocationConditionState -APIKey $AdminAPIKey -DefinedCondition $condition -ExistingCondition $existingConditionConfig -Verbose:$PSCMDLet.GetVariableValue('VerbosePreference')

        If ($conditionRequiresUpdate) {
            Write-Verbose "Updating condition $($condition.name) on policy $($policy.name)."
            $params = Get-NRSyntheticLocationConditionParamSet -APIKey $AdminAPIKey -Condition $condition
            Update-NRSyntheticLocationCondition @params -Id $existingConditionConfig.id -PolicyId $existingPolicy.id -Whatif:$WhatIfPreference | Out-Null
        }

        # Any extra existing conditions that are not defined will be removed later
        $existingConditions.Remove($existingConditionConfig[0])
      }

      # Otherwise, create the condition if the policy exists
      ElseIf ($existingPolicy) {
        $params = Get-NRSyntheticLocationConditionParamSet -APIKey $AdminAPIKey -Condition $condition
        Write-Verbose "Creating condition $($condition.name)"
        New-NRSyntheticLocationCondition @params -PolicyId $existingPolicy.id -Whatif:$WhatIfPreference | Out-Null
      }
    }

    # Check for existing conditions not in the policy's definition and remove them
    If ($existingConditions) {
      Write-Verbose "Removing condition(s) $($existingConditions.Name -join (','))"
      $existingConditions.Id | Remove-NRSyntheticLocationCondition $AdminAPIKey  -Whatif:$WhatIfPreference | Out-Null
    }
  }
}

##########################
# Internal Functions
##########################

Function Get-NRSyntheticLocationConditionParamSet {
  [CMDLetBinding()]
  param (
    [Parameter (Mandatory = $true)]
    $APIKey,
    [Parameter (Mandatory = $true)]
    $Condition
  )

  # Take the data available and create a hash table of CMDLET parameters
  $params = @{
    APIKey = $APIKEY
  }

  # Build up all optional parameters
  Foreach ($Property in ($Condition.Keys | Where-Object {$_ -ne 'Type'})) {

    # Entities require special handling because the monitor names are provided in the configuration instead of Ids which are generated by New Relic
    If ($Property -eq 'entities') {

      # Grab the IDs of the associated Synthetic monitors and add them to the params object
      Foreach ($EntityName in $Condition['entities']) {
        [array]$monitorIds += (Get-NRSyntheticMonitor -APIKey $APIKEY | Where-Object {$_.name -eq $EntityName}).id
      }
      $Params += @{
        Entities = $monitorIds
      }
    }

    # Terms require special handling since they are nested objects and a warning term is optional
    ElseIf ($Property -eq 'terms') {
      Foreach ($term in $Condition.$Property) {
        If ($term.priority -eq 'Critical') {
          Switch ($term.keys) {
            'operator' {$params += @{CriticalOperator = $term.operator}}
            'threshold' {$params += @{CriticalThreshold = $term.threshold}}
          }
        }
        ElseIf ($term.priority -eq 'Warning') {
          Switch ($term.keys) {
            'operator' {$params += @{WarningOperator = $term.operator}}
            'threshold' {$params += @{WarningThreshold = $term.threshold}}
          }
        }
        Else {
          Write-Error "$($Condition.name) has terms defined but a priority was not found!" -ErrorAction 'Stop'
        }
      }
    }

    # Process the top-level properties
    Else {
      $params +=  @{
        $Property = $Condition.$Property
      }
    }
  }
  Return $params
}

Function Compare-NRSyntheticLocationConditionState {
  [CMDLetBinding()]
  Param (
    $APIKEY,
    $DefinedCondition,
    $ExistingCondition
  )
  $returnValue = $false
  Foreach ($Property in ($DefinedCondition.Keys | Where-Object {$_ -ne 'Type'})) {

    # Terms require special handling since there may be multiple term objects returned in any order
    If ($Property -eq 'terms') {
      Foreach ($term in $DefinedCondition.$Property) {

        # There can only be one Warning term and one Critical term so match the term based on priority
        $existingTerm = $ExistingCondition.$Property | Where-Object {$_.priority -eq $term.priority}

        # Iterate over sub items in terms and compare them - no need to compare priority as it won't change
        Foreach ($subProperty in ($term.keys | Where-Object {$_ -ne 'priority'})) {

          # If a user specifies 0 to remove a warning term and no warning term exists, do not update the condition
          $warningThresholdAlreadyRemoved = $term.$subProperty -eq 0 -and $null -eq $existingTerm.$subProperty
          If ($term.$subProperty -ne $existingTerm.$subProperty -and -NOT $warningThresholdAlreadyRemoved) {
            Write-Verbose "Condition $($DefinedCondition.Name) has subproperty $subProperty defined as $($term.$subProperty) but it is currently set to $($Existingterm.$subProperty)"
            $returnValue = $true
          }
        }
      }
    }

    # Entities also require special handling since the names must be converted to IDs
    ElseIf ($Property -eq 'entities') {
      Foreach ($EntityName in $Condition['entities']) {
         $entityId = (Get-NRSyntheticMonitor -APIKey $APIKEY | Where-Object {$_.name -eq $EntityName}).id

        # Throw an error if the entity does not exist.
        If (-NOT $entityId) {
          Write-Error "You must define a Synthetic monitor with name $EntityName in order to specify it as a condition entity in condition $($Condition.name)." -ErrorAction 'Stop'
        }

        [array]$definedMonitorIds += $entityId
      }

      $monitorDiff = Compare-Object -ReferenceObject $definedMonitorIds -DifferenceObject $ExistingCondition.$Property -PassThru

      If ($monitorDiff) {
        Write-Verbose "Condition $($DefinedCondition.Name) has an entities list of $($definedMonitorIds) but it is currently set to $($ExistingCondition.$Property)"
        $returnValue = $true
      }
    }

    # Compare top-level properties
    ElseIf ($DefinedCondition.$Property -ne $ExistingCondition.$Property) {
      Write-Verbose "Condition $($DefinedCondition.Name) has property $Property defined as $($DefinedCondition.$Property) but it is currently set to $($ExistingCondition.$Property)"
      $returnValue = $true
    }
  }
  Return $returnValue
}