CloudNaming.psm1

# Copyright (c) TY Consulting.
# Licensed under the MIT License.

function ReadConfigFile {
  [CmdletBinding()]
  [OutputType([Object])]
  Param
  (
    [Parameter(Mandatory = $true)][string]$configFilePath
  )

  #Config File schema validation
  Write-Verbose "JSON schema validation for configuration file '$ConfigFilePath'"
  if (ValidateConfigFileSchema -configFilePath $configFilePath) {
    Write-Verbose "the configuration file '$ConfigFilePath' is valid against the schema."
  } else {
    Throw "The configuration file '$ConfigFilePath' is not valid against the schema."
    Exit -1
  }

  $config = Get-Content $configFilePath | ConvertFrom-Json
  $config
}

function ValidateConfigFileSchema {
  [CmdletBinding()]
  [OutputType([Object])]
  Param
  (
    [Parameter(Mandatory = $true)][string]$configFilePath
  )

  $schemaPath = Join-Path $PSScriptRoot 'CloudNaming.schema.json'
  $schema = get-content -Path $schemaPath -Raw
  $configFileContent = Get-Content -Path $configFilePath -Raw
  Test-Json -Json $configFileContent -Schema $schema
}
function ValidateInput {
  [CmdletBinding()]
  [OutputType([System.Collections.Hashtable])]
  Param
  (
    [Parameter(Mandatory = $true)][object]$config,
    [Parameter(Mandatory = $true)][object]$cloud,
    [Parameter(Mandatory = $false)][String[]]$type,
    [Parameter(Mandatory = $true)][String]$company,
    [Parameter(Mandatory = $false)][string]$environment,
    [Parameter(Mandatory = $false)][string]$location,
    [Parameter(Mandatory = $false)][string]$appIdentifier,
    [Parameter(Mandatory = $false)][string]$workloadType,
    [Parameter(Mandatory = $false)][string]$associatedResourceType,
    [Parameter(Mandatory = $false)][string]$associatedResourceName,
    [Parameter(Mandatory = $true)][int]$startInstanceNumber,
    [Parameter(Mandatory = $true)][int]$InstanceCount
  )

  #validate cloud proivder
  $bValidCloud = $false
  if ($config.allowedValues.cloud | Where-Object { $_.name -ieq $cloud }) {
    $bValidCloud = $true
  }
  #Validate type
  $bValidType = $true
  if ($PSBoundParameters.ContainsKey('type')) {
    foreach ($item in $type) {
      if (!($config.allowedValues.resourceType | Where-Object { $_.value -ieq $item -and $_.cloud -ieq $cloud })) {
        $bValidType = $false
      }
    }
  }

  #Validate company
  $bValidCompany = $false
  if ($config.allowedValues.company | Where-Object { $_.value -ieq $company }) {
    $bValidCompany = $true
  }

  #Validate environment
  $bValidEnvironment = $false
  if ($PSBoundParameters.ContainsKey('environment')) {
    if ($($environment.Length) -ge $config.control.environment.minLength -and $($environment.Length) -le $config.control.environment.maxLength -and $environment -match $config.control.environment.regex) {
      $bValidEnvironment = $true
    }
  }

  #Validate location
  $bValidLocation = $false
  if ($PSBoundParameters.ContainsKey('location')) {
    if ($($location.Length) -ge $config.control.location.minLength -and $($location.Length) -le $config.control.location.maxLength) {
      $bValidLocation = $true
    }
  }

  #Validate appIdentifier
  $bValidAppIdentifier = $false
  if ($PSBoundParameters.ContainsKey('appIdentifier')) {
    if ($($appIdentifier.Length) -ge $config.control.appIdentifier.minLength -and $($appIdentifier.Length) -le $config.control.appIdentifier.maxLength) {
      $bValidAppIdentifier = $true
    }
  }

  #validate workloadType
  $bValidWorkloadType = $true
  if ($PSBoundParameters.ContainsKey('workloadType')) {
    if ($($workloadType.Length) -lt $config.control.workloadType.minLength -or $($workloadType.Length) -gt $config.control.workloadType.maxLength) {
      $bValidWorkloadType = $false
    }
  }

  #validate associatedResourceType
  $bValidateAssociatedResourceType = $true
  if ($PSBoundParameters.ContainsKey('associatedResourceType')) {
    if (!($config.allowedValues.resourceType | Where-Object { $_.value -ieq $associatedResourceType })) {
      $bValidateAssociatedResourceType = $false
    }
  }

  #validate associatedResourceName
  $bValidateAssociatedResourceName = $true
  if ($PSBoundParameters.ContainsKey('associatedResourceName')) {
    if ($($associatedResourceName.Length) -ge $config.control.associatedResourceName.minLength -and $($associatedResourceName.Length) -le $config.control.associatedResourceName.maxLength -and $associatedResourceName -match $config.control.associatedResourceName.regex) {
      $bValidateAssociatedResourceName = $true
    }
  }

  #Validate instance number
  $bValidInstanceNumber = $false
  if ($startInstanceNumber -ge $config.control.instance.minValue -and $startInstanceNumber -le $config.control.instance.maxValue) {
    if (($startInstanceNumber + $InstanceCount - 1) -le $config.control.instance.maxValue) {
      $bValidInstanceNumber = $true
    }
  }
  #Generate result
  $result = @{
    'cloud'          = $bValidCloud
    'type'           = $bValidType
    'company'        = $bValidCompany
    'instanceNumber' = $bValidInstanceNumber
  }

  if ($PSBoundParameters.ContainsKey('appIdentifier')) {
    $result.add('appIdentifier', $bValidAppIdentifier)
  }
  if ($PSBoundParameters.ContainsKey('environment')) {
    $result.add('environment', $bValidEnvironment)
  }
  if ($PSBoundParameters.ContainsKey('location')) {
    $result.add('location', $bValidLocation)
  }
  if ($PSBoundParameters.ContainsKey('workloadType')) {
    $result.add('workloadType', $bValidWorkloadType)
  }
  if ($PSBoundParameters.ContainsKey('associatedResourceType')) {
    $result.add('associatedResourceType', $bValidateAssociatedResourceType)
  }
  if ($PSBoundParameters.ContainsKey('associatedResourceName')) {
    $result.add('associatedResourceName', $bValidateAssociatedResourceName)
  }
  $result
}

function GenerateResourceName {
  [CmdletBinding()]
  [OutputType([Object])]
  Param
  (
    [Parameter(Mandatory = $true)][String]$type,
    [Parameter(Mandatory = $true)][String]$company,
    [Parameter(Mandatory = $false)][string]$environment,
    [Parameter(Mandatory = $false)][string]$location,
    [Parameter(Mandatory = $false)][string]$appIdentifier,
    [Parameter(Mandatory = $false)][string]$workloadType,
    [Parameter(Mandatory = $false)][string]$associatedResourceType,
    [Parameter(Mandatory = $false)][string]$associatedResourceName,
    [Parameter(Mandatory = $true)][int]$instanceNumber,
    [Parameter(Mandatory = $true)][object]$resourcePattern,
    [Parameter(Mandatory = $true)][int]$leadingZeros
  )
  $pattern = $resourcePattern.pattern.tolower()
  if ($resourcePattern.leadingZeros) {
    $strInstanceNumber = "{0:d$leadingZeros}" -f $instanceNumber
  } else {
    $strInstanceNumber = $instanceNumber.tostring()
  }
  write-verbose "name pattern for $type`: $pattern. Instance Number: $strInstanceNumber"

  #insert type into name
  $name = $pattern -replace '{resourcetype}', $type
  #insert company into name
  $name = $name -replace '{company}', $company
  #insert environment into name
  if ($PSBoundParameters.ContainsKey('environment')) {
    $name = $name -replace '{environment}', $environment
  }
  #insert location into name
  if ($PSBoundParameters.ContainsKey('location')) {
    $name = $name -replace '{location}', $location
  }
  #insert appIdentifier into name
  if ($PSBoundParameters.ContainsKey('appIdentifier')) {
    $name = $name -replace '{appIdentifier}', $appIdentifier
  }

  #insert workloadType into name
  if ($PSBoundParameters.ContainsKey('workloadType')) {
    $name = $name -replace '{workloadType}', $workloadType
  }
  #insert associatedResourceType into name
  if ($PSBoundParameters.ContainsKey('associatedResourceType')) {
    $name = $name -replace '{associatedResourceType}', $associatedResourceType
  }
  #insert associatedResourceName into name
  if ($PSBoundParameters.ContainsKey('associatedResourceName')) {
    $name = $name -replace '{associatedResourceName}', $associatedResourceName
  }
  #insert instance number into name
  $name = $name -replace '{instance}', $strInstanceNumber

  #convert case
  if ($resourcePattern.case -ieq 'lower') {
    $name = $name.tolower()
  } elseif ($resourcePattern.case -ieq 'upper') {
    $name = $name.toupper()
  } else {
    throw "invalid case '$($resourcePattern.case)' for $($resourcePattern.description). Valid values are 'lower' and 'upper'"
    exit -1
  }
  Write-verbose "Name generated for $($resourcePattern.description): $($name)"
  #validate the generated name before returning the result
  #make sure all the fields from the pattern are processed
  $bValidName = $true
  if ($name -match '{[^}]*}') {
    Write-Verbose "invalid pattern '$($pattern)' for $($resourcePattern.description). Pattern contains unprocessed fields. The name will be ignored."
    $bValidName = $false
  }
  #length validation
  if ($bValidName) {
    if ($name.length -lt $resourcePattern.minLength -or $name.length -gt $resourcePattern.maxLength) {
      Write-Warning "invalid length for tne name generated for $($resourcePattern.description) - $name. Valid length is between $($resourcePattern.minLength) and $($resourcePattern.maxLength) and the actual length is $($name.length). The name will be ignored."
      $bValidName = $false
    }
  }

  if ($bValidName) {
    $name
  } else {
    $null
  }
}

# .EXTERNALHELP en-US/CloudNaming-Help.xml
function GetCloudResourceName {
  [CmdletBinding()]
  [OutputType([string])]
  Param
  (
    [Parameter(ParameterSetName = 'ByTypeNames', Mandatory = $false, HelpMessage = "OPTIONAL: The custom configuration file to use. If not specified, the default configuration file 'CloudNaming.json' from the module directory will be used.")]
    [Parameter(ParameterSetName = 'AllSupportedTypes', Mandatory = $false, HelpMessage = "OPTIONAL: The custom configuration file to use. If not specified, the default configuration file 'CloudNaming.json' from the module directory will be used.")]
    [ValidateScript({ $(test-Path -Path $_ -PathType Leaf) -and $($(Get-ItemProperty -Path $_).Extension -ieq '.json') })]
    [String]$configFilePath,

    [Parameter(ParameterSetName = 'ByTypeNames', Mandatory = $true, HelpMessage = "Cloud Provider")]
    [Parameter(ParameterSetName = 'AllSupportedTypes', Mandatory = $true, HelpMessage = "Cloud Provider")]
    [String[]]$cloud,

    [Parameter(ParameterSetName = 'ByTypeNames', Mandatory = $true, HelpMessage = "OPTIONAL: Cloud resource type. If not specified, all supported types will be returned.")]
    [String[]]$type,

    [Parameter(ParameterSetName = 'ByTypeNames', Mandatory = $false, HelpMessage = "OPTIONAL: Company or Business Unit name. If not specified, the first value defined in the allowedValue section in the configuration file is the default value.")]
    [Parameter(ParameterSetName = 'AllSupportedTypes', Mandatory = $false, HelpMessage = "OPTIONAL: Company or Business Unit name. If not specified, the first value defined in the allowedValue section in the configuration file is the default value.")]
    [Alias('businessUnit')]
    [ValidateNotNullOrEmpty()][String]$company,

    [Parameter(ParameterSetName = 'ByTypeNames', Mandatory = $false, HelpMessage = "OPTIONAL: Specify the environment")]
    [Parameter(ParameterSetName = 'AllSupportedTypes', Mandatory = $false, HelpMessage = "OPTIONAL: Specify the environment")]
    [string]$environment,

    [Parameter(ParameterSetName = 'ByTypeNames', Mandatory = $false, HelpMessage = "OPTIONAL: Specify the location or region of the cloud provider")]
    [Parameter(ParameterSetName = 'AllSupportedTypes', Mandatory = $false, HelpMessage = "OPTIONAL: Specify the location or region of the cloud provider")]
    [string]$location,

    [Parameter(ParameterSetName = 'ByTypeNames', Mandatory = $false, HelpMessage = "Specify the application identifier")]
    [Parameter(ParameterSetName = 'AllSupportedTypes', Mandatory = $false, HelpMessage = "Specify the application identifier")]
    [string]$appIdentifier,

    [Parameter(ParameterSetName = 'ByTypeNames', Mandatory = $false, HelpMessage = "OPTIONAL: Associated resource type. Used for resource types such as private endpoints and public IPs.")]
    [Parameter(ParameterSetName = 'AllSupportedTypes', Mandatory = $false, HelpMessage = "Associated resource type. Used for resource types such as private endpoints and public IPs.")]
    [string]$associatedResourceType,

    [Parameter(ParameterSetName = 'ByTypeNames', Mandatory = $false, HelpMessage = "OPTIONAL: Associated resource name. Used for resource types such as managed disks and NICs.")]
    [Parameter(ParameterSetName = 'AllSupportedTypes', Mandatory = $false, HelpMessage = "Associated resource name. Used for resource types such as managed disks and NICs.")]
    [string]$associatedResourceName,

    [Parameter(ParameterSetName = 'ByTypeNames', Mandatory = $false, HelpMessage = "OPTIONAL: Workload Type. Used for resource types such as Virtual Machines. i.e. 'db', 'app', 'web', 'etc'.")]
    [Parameter(ParameterSetName = 'AllSupportedTypes', Mandatory = $false, HelpMessage = "Workload Type. Used for resource types such as Virtual Machines. i.e. 'db', 'app', 'web', 'etc'")]
    [string]$workloadType,

    [Parameter(ParameterSetName = 'ByTypeNames', Mandatory = $false, HelpMessage = "OPTIONAL: Specify the starting instance number. Default value is 1")]
    [Parameter(ParameterSetName = 'AllSupportedTypes', Mandatory = $false, HelpMessage = "OPTIONAL: Specify the starting instance number. Default value is 1")]
    [int]$startInstanceNumber = 1,

    [Parameter(ParameterSetName = 'ByTypeNames', Mandatory = $false, HelpMessage = "OPTIONAL: Specify the instance count. Default value is 1")]
    [Parameter(ParameterSetName = 'AllSupportedTypes', Mandatory = $false, HelpMessage = "OPTIONAL: Specify the instance count. Default value is 1")]
    [int]$instanceCount = 1
  )

  #Read configuration file
  if ($PSBoundParameters.ContainsKey('configFilePath')) {
    Write-Verbose "Custom configuration file specified: '$configFilePath'"
  } else {
    $configFilePath = Join-Path $PSScriptRoot 'CloudNaming.json' -Resolve
    Write-Verbose "No custom configuration file specified. Using default configuration file from the CloudNaming module directory: '$configFilePath'"
  }
  Write-verbose "Read Cloud Naming configuration file '$configFilePath'"

  try {
    $config = ReadConfigFile -configFilePath $configFilePath
  } catch {
    #throw "Unable to read configuration file"
    throw $_
    exit -1
  }

  #Get company value
  if (!($PSBoundParameters.ContainsKey('company'))) {
    $company = $config.allowedValues.company[0].value
    if (!$company) {
      Throw "No company value defined in the configuration file. Please define at least one company value in the configuration file."
      Exit -1
    } else {
      Write-verbose "The 'Company' parameter is not specified. Using the first Allowed Value '$company' from the configuration file instead."
    }
  }
  #input validation
  Write-verbose "Input validation"
  $validateParams = @{
    'config'              = $config
    'cloud'               = $cloud
    'company'             = $company
    'startInstanceNumber' = $startInstanceNumber
    'instanceCount'       = $instanceCount
  }

  if ($PSBoundParameters.ContainsKey('appIdentifier')) {
    $validateParams.Add('appIdentifier', $appIdentifier)
  }
  if ($PSBoundParameters.ContainsKey('type')) {
    $validateParams.Add('type', $type)
  }
  if ($PSBoundParameters.ContainsKey('environment')) {
    $validateParams.Add('environment', $environment)
  }
  if ($PSBoundParameters.ContainsKey('location')) {
    $validateParams.Add('location', $location)
  }
  if ($PSBoundParameters.ContainsKey('associatedResourceType')) {
    $validateParams.Add('associatedResourceType', $associatedResourceType)
  }
  if ($PSBoundParameters.ContainsKey('associatedResourceName')) {
    $validateParams.Add('associatedResourceName', $associatedResourceType)
  }
  if ($PSBoundParameters.ContainsKey('workloadType')) {
    $validateParams.Add('workloadType', $workloadType)
  }
  $validationResult = ValidateInput @validateParams

  $bAllValid = $true
  foreach ($property in  $validationResult.getEnumerator()) {
    if ($($property.value) -eq $false) {
      Write-Error "Invalid value specified for $($property.name)"
      $bAllValid = $false
    } else {
      Write-verbose "Valid value specified for $($property.name)."
    }
  }
  If (!$bAllValid) {
    throw "One or more input parameters are invalid."
    exit -1
  }
  #Get all allowed values for the specified cloud provider
  $availableResourcesForCloud = $config.allowedValues.resourceType | Where-Object { $_.cloud -ieq $cloud }

  #determine resource types in scope
  Write-verbose "Determine resource types in scope"
  if (!$PSBoundParameters.ContainsKey('type')) {
    $type = $availableResourcesForCloud.value
  }
  Write-verbose "type: $($type)"
  #determine leading zeros for the instance number
  write-verbose "Determine leading zeros for the instance number"
  $leadingZeros = $config.control.instance.maxValue.tostring().length

  #Generate resource names
  Write-verbose "Generate resource names"
  $arrNames = @()
  foreach ($item in $type) {
    Write-verbose "Generate resource names for type '$item' in cloud $cloud. Instance Count: $instanceCount"
    $resourcePattern = $availableResourcesForCloud | Where-Object { $_.value -ieq $item }
    $objNames = @{
      'type'        = $item
      'description' = $resourcePattern.description
      'names'       = @()
    }
    for ($i = 0; $i -lt $instanceCount; $i++) {
      $instanceNumber = $startInstanceNumber + $i
      Write-verbose "Generate resource names for type '$item' and instance number '$instanceNumber'"
      $generateNameParam = @{
        'type'            = $item
        'company'         = $company
        'instanceNumber'  = $instanceNumber
        'resourcePattern' = $resourcePattern
        'leadingZeros'    = $leadingZeros
      }
      if ($PSBoundParameters.ContainsKey('appIdentifier')) {
        $generateNameParam.Add('appIdentifier', $appIdentifier)
      }
      if ($PSBoundParameters.ContainsKey('environment')) {
        $generateNameParam.Add('environment', $environment)
      }
      if ($PSBoundParameters.ContainsKey('location')) {
        $generateNameParam.Add('location', $location)
      }
      if ($PSBoundParameters.ContainsKey('workloadType')) {
        $generateNameParam.Add('workloadType', $workloadType)
      }
      if ($PSBoundParameters.ContainsKey('associatedResourceType')) {
        $generateNameParam.Add('associatedResourceType', $associatedResourceType)
      }
      if ($PSBoundParameters.ContainsKey('associatedResourceName')) {
        $generateNameParam.Add('associatedResourceName', $associatedResourceName)
      }
      $name = GenerateResourceName @generateNameParam
      if ($name) {
        #deduplicate names
        if (!($objNames.names -contains $name)) {
          $objNames.names += $name
        } else {
          write-verbose "Duplicate name for $type found: $name. Skipping."
        }
        #$objNames.names += $name
      }
    }
    if ($objNames.names.count -gt 0) {
      $arrNames += $objNames
    }
  }

  #deserialize the result
  Write-Verbose "Deserialize the result"
  $output = $arrNames | sort-object -Property 'type' | ConvertTo-Json -Compress -Depth 3
  $output
}

# .EXTERNALHELP en-US/CloudNaming-Help.xml
function GetCloudNamingSupportedTypes {
  [CmdletBinding()]
  [OutputType([string])]
  param(
    [Parameter(Mandatory = $false, HelpMessage = "OPTIONAL: The custom configuration file to use. If not specified, the default configuration file 'CloudNaming.json' from the module directory will be used.")]
    [ValidateScript({ $(test-Path -Path $_ -PathType Leaf) -and $($(Get-ItemProperty -Path $_).Extension -ieq '.json') })]
    [String]$configFilePath,

    [Parameter(Mandatory = $false, HelpMessage = "OPTIONAL: Resource type search string. RegEx is supported. i.e. '^virtual machine$'")]
    [String]$searchString,

    [Parameter(Mandatory = $false, HelpMessage = "OPTIONAL: Cloud Provider to search for. i.e. 'Azure'")]
    [String]$cloud
  )
  #Read configuration file
  if ($configFilePath) {
    Write-Verbose "Custom configuration file specified: '$configFilePath'"
    $config = ReadConfigFile -configFilePath $configFilePath
  } else {
    Write-Verbose "No custom configuration file specified. Using default configuration file from the CloudNaming module directory"
    $config = ReadConfigFile -configFilePath $(Join-Path $PSScriptRoot 'CloudNaming.json' -Resolve)
  }

  try {

  } catch {
    #throw "Unable to read configuration file"
    throw $_
    exit -1
  }

  #get supported types
  if ($PSBoundParameters.ContainsKey('searchString')) {
    $SupportedTypes = $config.allowedValues.resourceType | Where-Object { $_.description -imatch $searchString }
  } else {
    $SupportedTypes = $config.allowedValues.resourceType
  }

  if ($PSBoundParameters.ContainsKey('cloud')) {
    $SupportedTypes = $SupportedTypes | where-object { $_.cloud -imatch $cloud }
  }
  $SupportedTypes | ConvertTo-Json -Compress -Depth 3
}

# .EXTERNALHELP en-US/CloudNaming-Help.xml
function NewCloudNamingConfigFile {
  [CmdletBinding()]
  [OutputType([string])]
  param(
    [Parameter(Mandatory = $true, HelpMessage = "The path to the custom configuration file to create.")]
    [ValidateScript({ $( test-path -literalPath $_ -PathType leaf -IsValid) -and $( $_.split('.')[-1] -ieq 'json') })]
    [String]$configFilePath,

    [Parameter(Mandatory = $false, HelpMessage = "Overwrite existing file if exists.")]
    [switch]$force
  )

  $builtInConfigFile = join-path $PSScriptRoot 'CloudNaming.json' -Resolve
  Write-Verbose "Creating new CloudNaming configuration file at '$configFilePath' based on the built-in configuration file '$builtInConfigFile'"
  #check if file exists already
  $bExistingFile = test-path -literalPath $configFilePath -PathType leaf
  if ($bExistingFile) {
    if ($force) {
      Write-Verbose "File '$configFilePath' already exists. Overwriting."
      $result = (copy-item -path $builtInConfigFile -destination $configFilePath -force -PassThru).fullName
    } else {
      Write-Warning "File '$configFilePath' already exists. Use -force to overwrite."
    }
  } else {
    Write-Verbose "File '$configFilePath' does not exist. Creating."
    $result = (copy-item -path $builtInConfigFile -destination $configFilePath -force).fullName
  }
  $result
}