functions/resource.ps1

<#PSScriptInfo
.VERSION 1.3.0
.GUID 7eadb850-7e43-4308-a9fa-0119a0a883a3
.AUTHOR Black Duck
.COPYRIGHT Copyright 2024 Black Duck Software, Inc. All rights reserved.
.DESCRIPTION Includes resource-related helpers
#>


function Get-ResourceDirectoryPath([string] $kind) {
    return "./Resources/$kind"
}

function Set-ResourceDirectory([string] $kind) {

    $directory = Get-ResourceDirectoryPath $kind
    if (Test-Path $directory -PathType Container) {
        New-Object IO.DirectoryInfo($directory)
    } else {
        New-Item $directory -ItemType Directory
    }
}

function New-ResourceFile([string] $kind, [string] $namespace, [string] $name, [string[]] $resourceFile) {

    $directory = Set-ResourceDirectory $kind
    $kind = $kind.ToLower()
    
    $name = $name -replace '[/\\]','-' # replace any path characters
    $filename = $namespace -eq '' ? "$kind-$name.yaml" : "$kind-$namespace-$name.yaml"
    $resourcePath = join-path $directory $filename

    $resourceFile | Out-File $resourcePath -Encoding ascii -Force
    Get-ChildItem $resourcePath
}

function New-SealedSecretFile([io.fileinfo] $secretFileInfo,
    [string] $sealedSecretsNamespace,
    [string] $sealedSecretsControllerName,
    [string] $sealedSecretsPublicKeyPath,
    [switch] $keepSecretFile) {

    $kind = 'SealedSecret'
    $directory = Set-ResourceDirectory $kind
    $kind = $kind.ToLower()
    $filename = "$kind-$namespace-$name.yaml"
    $resourcePath = join-path $directory $filename

    New-SealedSecret $secretFileInfo $sealedSecretsNamespace $sealedSecretsControllerName $sealedSecretsPublicKeyPath $resourcePath -keepSecretFile:$keepSecretFile
}

function New-NamespaceResource([string] $namespace, [Tuple`2[string,string]] $label,
    [switch] $useGitOps) {

    $ns = New-Namespace $namespace $label -dryRun:$useGitOps
    if (-not $useGitOps) {
        return $ns
    }
    New-ResourceFile 'Namespace' '' $namespace $ns
}

function New-PriorityClassResource([string] $name, [int] $values,
    [switch] $useGitOps) {

    $pc = New-PriorityClass $name $values -dryRun:$useGitOps
    if (-not $useGitOps) {
        return $pc
    }
    New-ResourceFile 'PriorityClass' '' $name $pc
}

function New-CertificateSecretResource([string] $namespace, [string] $name, [string] $certFile, [string] $keyFile,
    [switch] $useGitOps,
    [switch] $useSealedSecrets,    [string] $sealedSecretsNamespace, [string] $sealedSecretsControllerName, [string] $sealedSecretsPublicKeyPath) {

    $cs = New-CertificateSecret $namespace $name $certFile $keyFile -dryRun:$useGitOps
    if (-not $useGitOps) {
        return $cs
    }

    New-SecretResourceFile $namespace $name $cs -useSealedSecrets:$useSealedSecrets $sealedSecretsNamespace $sealedSecretsControllerName $sealedSecretsPublicKeyPath
}

function New-CertificateConfigMapResource([string] $namespace, [string] $name, [string] $certFile, [string] $certFilenameInConfigMap, 
    [switch] $useGitOps) {

    $ccm = New-CertificateConfigMap $namespace $name $certFile $certFilenameInConfigMap -dryRun:$useGitOps
    if (-not $useGitOps) {
        return $ccm
    }
    New-ResourceFile 'ConfigMap' $namespace $name $ccm
}

function New-GenericSecretResource([string] $namespace, [string] $name, [hashtable] $keyValues = @{}, [hashtable] $fileKeyValues = @{},
    [switch] $useGitOps,
    [switch] $useSealedSecrets,    [string] $sealedSecretsNamespace, [string] $sealedSecretsControllerName, [string] $sealedSecretsPublicKeyPath) {

    $s = New-GenericSecret $namespace $name $keyValues $fileKeyValues -dryRun:$useGitOps
    if (-not $useGitOps) {
        return $s
    }

    New-SecretResourceFile $namespace $name $s -useSealedSecrets:$useSealedSecrets $sealedSecretsNamespace $sealedSecretsControllerName $sealedSecretsPublicKeyPath
}

function New-DockerImagePullSecretResource([string] $namespace, [string] $name, [string] $dockerRegistry, [string] $dockerRegistryUser,    [string] $dockerRegistryPwd,
    [switch] $useGitOps,
    [switch] $useSealedSecrets,    [string] $sealedSecretsNamespace, [string] $sealedSecretsControllerName, [string] $sealedSecretsPublicKeyPath) {
    
    $s = New-ImagePullSecret $namespace $name $dockerRegistry $dockerRegistryUser $dockerRegistryPwd -dryRun:$useGitOps
    if (-not $useGitOps) {
        return $s
    }

    New-SecretResourceFile $namespace $name $s -useSealedSecrets:$useSealedSecrets $sealedSecretsNamespace $sealedSecretsControllerName $sealedSecretsPublicKeyPath
}

function New-SecretResourceFile([string] $namespace, [string] $name, [string[]] $resourceFile,
    [switch] $useSealedSecrets,    [string] $sealedSecretsNamespace, [string] $sealedSecretsControllerName, [string] $sealedSecretsPublicKeyPath) {

    $file = New-ResourceFile 'Secret' $namespace $name $resourceFile
    if ($useSealedSecrets) {
        return New-SealedSecretFile $file $sealedSecretsNamespace $sealedSecretsControllerName $sealedSecretsPublicKeyPath
    }
    return $file
}

function New-ConfigMapResource([string] $namespace, [string] $name, [hashtable] $keyValues = @{}, [hashtable] $fileKeyValues = @{},
    [switch] $useGitOps) {

    $cm = New-ConfigMap $namespace $name $keyValues $fileKeyValues -dryRun:$useGitOps
    if (-not $useGitOps) {
        return $cm
    }
    New-ResourceFile 'ConfigMap' $namespace $name $cm
}

function New-NamespacedResourceFromYaml([string] $namespace, [string] $resourceKind, [string] $resourceName, [string] $yamlPath,
    [switch] $useGitOps) {

    if ($useGitOps) {
        $resource = Get-Content $yamlPath
        return New-ResourceFile $resourceKind $namespace $resourceName $resource
    }

    New-NamespacedResource $namespace $resourceKind $resourceName $yamlPath
}

function Set-CustomResourceDefinitionResource([string] $name, [string] $path,
    [switch] $useGitOps) {

    $crd = Set-NonNamespacedResource $path 'crd' -dryRun:$useGitOps
    if (-not $useGitOps) {
        return $crd
    }
    New-ResourceFile 'CustomResourceDefinition' '' $name $crd
}

function New-SealedSecret([io.fileinfo] $secretFileInfo,
    [string] $sealedSecretsNamespace,
    [string] $sealedSecretsControllerName,
    [string] $sealedSecretsPublicKeyPath,
    [string] $sealedSecretPath,
    [switch] $keepSecretFile) {

    Get-Content $secretFileInfo.FullName | kubeseal --controller-namespace=$sealedSecretsNamespace --controller-name=$sealedSecretsControllerName --format yaml --cert $sealedSecretsPublicKeyPath > $sealedSecretPath
    if (0 -ne $LASTEXITCODE) {
        throw "Unable to create sealed secret for specified input ($sealedSecretPath)"
    }

    if (-not $keepSecretFile) {
        $secretFileInfo.Delete()

        $secretFileDirectory = Get-ResourceDirectoryPath 'Secret'
        if ((Get-ChildItem $secretFileDirectory).Count -eq 0) {
            Remove-Item $secretFileDirectory
        }
    }

    # replace null/invalid values incompatible with flux v2
    $content = Get-Content $sealedSecretPath
    $creationTimestampPattern = '(?m)^\s+creationTimestamp:\snull$'
    $dataPattern = '(?m)^\s+data:\snull$'
    $creationTimestampPattern,$dataPattern | ForEach-Object {
        $content = $content -replace $_,'' | Where-Object { '' -ne $_ }
    }
    Set-Content $sealedSecretPath $content

    Get-ChildItem $sealedSecretPath
}

function New-HelmOperatorGitSource(
    [string] $chartGit,
    [string] $chartRef,
    [string] $chartPath) {

    return @"
    git: $chartGit
    ref: $chartRef
    path: $chartPath
"@

}

function New-HelmOperatorChartSource(
    [string] $chartRepository,
    [string] $chartName,
    [string] $chartVersion) {

    return @"
    repository: $chartRepository
    name: $chartName
    version: $chartVersion
"@

}

function New-HelmControllerGitSource(
    [string] $chartGitName,
    [string] $chartRef,
    [string] $chartPath) {

    return @"
    spec:
      chart: $chartPath
      sourceRef:
        kind: GitRepository
        name: $chartGitName-$chartRef
"@

}

function New-HelmControllerChartSource(
    [string] $name,
    [string] $chartName,
    [string] $chartVersion) {

    return @"
    spec:
      chart: $chartName
      sourceRef:
        kind: HelmRepository
        name: $name
      version: '$chartVersion'
"@
    
}

function New-HelmOperatorConfigMapValues(
    [string] $configMapName
) {
    return @"

  - configMapKeyRef:
      name: $_
"@

}

function New-HelmControllerConfigMapValues(
    [string] $configMapName
) {
    return @"

  - kind: ConfigMap
    name: $_
"@

}

function New-GitRepository(
    [string] $name,
    [string] $namespace,
    [string] $gitURL,
    [string] $gitRef
) {

    return @"
apiVersion: source.toolkit.fluxcd.io/v1beta1
kind: GitRepository
metadata:
  name: $name-$gitRef
  namespace: $namespace
spec:
  interval: 1m0s
  ref:
    tag: $gitRef
  url: $gitURL
"@

}

function New-HelmRepository(
    [string] $name,
    [string] $namespace,
    [string] $chartRepository
) {

    return @"
apiVersion: source.toolkit.fluxcd.io/v1beta1
kind: HelmRepository
metadata:
  name: $name
  namespace: $namespace
spec:
  interval: 1m0s
  url: $chartRepository
"@

}

function New-HelmRelease(
    [Parameter(Position=0)] [Parameter(ParameterSetName='GitChart')] [Parameter(ParameterSetName='RepoChart')]
    [string]    $name,
    [Parameter(Position=1)] [Parameter(ParameterSetName='GitChart')] [Parameter(ParameterSetName='RepoChart')]
    [string]    $namespace,
    [Parameter(Position=2)] [Parameter(ParameterSetName='GitChart')] [Parameter(ParameterSetName='RepoChart')]
    [string]    $releaseName,
    [Parameter(Position=3)] [Parameter(ParameterSetName='GitChart')] [Parameter(ParameterSetName='RepoChart')]
    [string]    $timeout = '5m0s',
    [Parameter(ParameterSetName='GitChart')]
    [string]    $chartGitName,
    [Parameter(ParameterSetName='GitChart')]
    [string]    $chartGit,
    [Parameter(ParameterSetName='GitChart')]
    [string]    $chartRef,
    [Parameter(ParameterSetName='GitChart')]
    [string]    $chartPath,
    [Parameter(ParameterSetName='RepoChart')]
    [string]    $chartRepository,
    [Parameter(ParameterSetName='RepoChart')]
    [string]    $chartName,
    [Parameter(ParameterSetName='RepoChart')]
    [string]    $chartVersion,
    [Parameter(ParameterSetName='GitChart')] [Parameter(ParameterSetName='RepoChart')]
    [string[]]  $valuesConfigMapNames,
    [Parameter(ParameterSetName='GitChart')] [Parameter(ParameterSetName='RepoChart')]
    [hashtable] $dockerImageNames,
    [Parameter(ParameterSetName='GitChart')] [Parameter(ParameterSetName='RepoChart')]
    [switch]    $useHelmController) {

    $isGitChart = '' -ne $chartGit
    $chartSource = ''

    if ($useHelmController) {

        if ($isGitChart) {

            $gitRepository = New-GitRepository $chartGitName $namespace $chartGit $chartRef
            New-ResourceFile 'GitRepository' $namespace "$chartGitName-$chartRef" $gitRepository

            $chartSource = New-HelmControllerGitSource $chartGitName $chartRef $chartPath
        } else {

            $chartRepository = New-HelmRepository $name $namespace $chartRepository
            New-ResourceFile 'HelmRepository' $namespace $name $chartRepository

            $chartSource = New-HelmControllerChartSource $name $chartName $chartVersion
        }
        
    } else {

        if ($isGitChart) {
            $chartSource = New-HelmOperatorGitSource $chartGit $chartRef $chartPath
        } else {
            $chartSource = New-HelmOperatorChartSource $chartRepository $chartName $chartVersion
        }
    }
    
    $values = ''
    if ($dockerImageNames.Count -gt 0) {

        $values = @'
  values:
'@

        $dockerImageNames.Keys | Sort-Object | ForEach-Object {

            $values += @"

    $_`: $($dockerImageNames[$_])
"@

        }
    }

    $valuesFrom = ''
    if ($valuesConfigMapNames.Count -gt 0) {

        $valuesFrom = @'
  valuesFrom:
'@

         $valuesConfigMapNames | ForEach-Object {
            $valuesFrom += $useHelmController ? (New-HelmControllerConfigMapValues $_) : (New-HelmOperatorConfigMapValues $_)
        }
    }

    # We want to skip CRD deployment on install (deployment skipped by default on upgrade). Flux v2 deprecates
    # the skipCRDs property, so use CRD policy for v2.
    #
    # Flux v1: https://fluxcd.io/legacy/helm-operator/references/helmrelease-custom-resource/#helm.fluxcd.io/v1.HelmReleaseSpec
    # Flux v2: https://fluxcd.io/docs/components/helm/api/#helm.toolkit.fluxcd.io/v2beta1.HelmReleaseSpec
    #

    $helmRelease = @'
apiVersion: {0}
kind: HelmRelease
metadata:
  name: {1}
  namespace: {2}
spec:
  releaseName: {3}
  timeout: {9}
  chart:
{4}
{5}
{6}
{7}
  install:
    {8}
'@
 -f 
    ($useHelmController ? 'helm.toolkit.fluxcd.io/v2beta1' : 'helm.fluxcd.io/v1'),
    $name,$namespace,$releaseName,$chartSource,$valuesFrom,$values,
    ($useHelmController ? ' interval: 1m0s' : ''),
    ($useHelmController ? "skipCRDs: true" : "crds: 'Skip'"),
    $timeout

    New-ResourceFile 'HelmRelease' $namespace $name $helmRelease
}


function New-HelmCommand(
    [string]    $namespace,
    [string]    $releaseName,
    [string]    $chartRootPath,
    [string[]]  $valuesPaths,
    [hashtable] $dockerImageNames,
    [string]    $timeout = '5m0s') {

    $crdAction = '--skip-crds'
    $valuesParam = '--reset-values' # merge $values with the latest, default chart values

    $values = @()
    $valuesPaths | ForEach-Object {
        $values += $_
    }

    $dockerImageNamesFileContent = ''
    if ($null -ne $dockerImageNames) {
        $dockerImageNames.Keys | ForEach-Object {
            $dockerImageNamesFileContent += "$_`: $($dockerImageNames[$_])`n"
        }
    }

    $helmValuesPath = @()
    if ('' -ne $dockerImageNamesFileContent) {
        $helmValuesPath += New-ResourceFile 'HelmCommand' $namespace "values-docker-$releaseName" $dockerImageNamesFileContent
    }
    $values += $helmValuesPath

    $helmOutput =  "helm dependency update ""$chartRootPath""`n"
    $helmOutput += "helm upgrade --namespace $namespace --install --timeout $timeout $crdAction $valuesParam "
    $values | ForEach-Object {
        $helmOutput += "--values ""$_"" "
    }
    $helmOutput += "$releaseName ""$chartRootPath"""

    New-ResourceFile 'HelmCommand' $namespace "helmcommand-install-$releaseName" $helmOutput
}