DevOpsKitDsc.psm1

#
# DevOps Kit for Desired State Configuration
#

# Import helper classes
if (!$PSVersionTable.PSEdition -or $PSVersionTable.PSEdition -eq 'Desktop') {
    Add-Type -Path (Join-Path -Path $PSScriptRoot -ChildPath "/bin/Debug/net451/publish/DevOpsKitDsc.dll") | Out-Null;
}
else {
    Add-Type -Path (Join-Path -Path $PSScriptRoot -ChildPath "/bin/Debug/netstandard1.6/publish/DevOpsKitDsc.dll") | Out-Null;
}

#
# Localization
#

$LocalizedData = data {

}

Import-LocalizedData -BindingVariable LocalizedData -FileName 'DevOpsKitDsc.Resources.psd1' -ErrorAction SilentlyContinue;

#
# Public functions
#

#region Public functions

# Bootstrap a DSC node with an encryption certificate and modules
function Register-DOKDscNode {

    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $False)]
        [String[]]$InstanceName,

        [Parameter(Mandatory = $False)]
        [Alias('Path')]
        [String]$WorkspacePath = $PWD
    )

    begin {
        Write-Verbose -Message "[DOKDsc] BEGIN::";
    }

    process {

        if (!(Test-Path -Path $WorkspacePath)) {
            
            return;
        }

        # Get workspace settings
        $setting = Import-DOKDscWorkspaceSetting -WorkspacePath $WorkspacePath -Verbose:$VerbosePreference;

        $nodePath = GetWorkspacePath -WorkspacePath $WorkspacePath -Path $setting.Options.NodePath -Verbose:$VerbosePreference;

        CreatePath -Path $nodePath;

        # Import node data
        $nodeData = ImportNodeData -WorkspacePath $WorkspacePath -NodePath $nodePath -InstanceName $InstanceName -Verbose:$VerbosePreference;

        if ($Null -eq $nodeData -or $nodeData.Length -eq 0) {
            Write-Error -Message $LocalizedData.ErrorMissingNodeData -Category ObjectNotFound -TargetObject $nodePath -ErrorAction Stop;
        }

        foreach ($node in $nodeData) {
            # Merge certificate information into node data
            MergeNodeCertificate -InputObject $node -Path $nodePath -InstanceName $node.InstanceName -Verbose:$VerbosePreference;

            # Register the node
            RegisterNode -Node $node -OutputPath $nodePath -Verbose:$VerbosePreference;

            # Copy required modules
            # CopyModules -Session $session -Path '';
        }
    }

    end {
        Write-Verbose -Message "[DOKDsc] END::";
    }
}

function Import-DOKDscNodeConfiguration {

    [CmdletBinding()]
    [OutputType([System.Collections.Hashtable])]
    param (
        [Parameter(Mandatory = $False)]
        [String[]]$InstanceName,

        [Parameter(Mandatory = $False)]
        [Alias('Path')]
        [String]$WorkspacePath = $PWD
    )

    begin {
        Write-Verbose -Message "[DOKDsc] BEGIN::";
    }

    process {

        if (!(Test-Path -Path $WorkspacePath)) {
            Write-Error -Message ($LocalizedData.WorkspacePathDoesNotExist) -Category ObjectNotFound -TargetObject $WorkspacePath -ErrorAction Stop;
        }

        # Get workspace settings
        $setting = Import-DOKDscWorkspaceSetting -WorkspacePath $WorkspacePath -Verbose:$VerbosePreference;

        $nodePath = GetWorkspacePath -WorkspacePath $WorkspacePath -Path $setting.Options.NodePath -Verbose:$VerbosePreference;

        # Import node data
        $nodeData = ImportNodeData -WorkspacePath $WorkspacePath -NodePath $nodePath -InstanceName $InstanceName -Verbose:$VerbosePreference;

        if ($Null -eq $nodeData -or $nodeData.Length -eq 0) {
            Write-Error -Message ($LocalizedData.ErrorMissingNodeData -f $WorkspacePath) -Category ObjectNotFound -ErrorAction Stop;
        }

        foreach ($node in $nodeData) {
            # Merge certificate information into node data
            MergeNodeCertificate -InputObject $node -Path $nodePath -InstanceName $node.InstanceName -Verbose:$VerbosePreference;

            $node.ConfigurationData;
        }
    }

    end {
        Write-Verbose -Message "[DOKDsc] END::";
    }
}

function Get-DOKDscCollection {

    [CmdletBinding(DefaultParameterSetName = 'Path')]
    [OutputType([DevOpsKitDsc.Workspace.Collection])]
    param (
        [Parameter(Mandatory = $False, ParameterSetName = 'Path')]
        [String]$WorkspacePath = $PWD,

        [Parameter(Mandatory = $True, ParameterSetName = 'Setting')]
        [DevOpsKitDsc.Workspace.WorkspaceSetting]$Workspace,

        [Parameter(Mandatory = $False)]
        [String]$Name
    )

    process {

        $setting = $Workspace;

        if ($PSCmdlet.ParameterSetName -eq 'Path') {
            
            # Get workspace settings
            $setting = Import-DOKDscWorkspaceSetting -WorkspacePath $WorkspacePath -Verbose:$VerbosePreference;
        }
        
        $collections = $setting.Collections | Where-Object -FilterScript {
            (!$PSBoundParameters.ContainsKey('Name') -or $Name -contains $_.Name)
        };

        return $collections;
    }
}

function New-DOKDscCollection {

    [CmdletBinding(DefaultParameterSetName = 'Path', SupportsShouldProcess = $True)]
    [OutputType([DevOpsKitDsc.Workspace.Collection])]
    param (
        [Parameter(Mandatory = $False, ParameterSetName = 'Path')]
        [String]$WorkspacePath = $PWD,

        [Parameter(Mandatory = $True, ParameterSetName = 'Workspace')]
        [DevOpsKitDsc.Workspace.WorkspaceSetting]$Workspace,

        [Parameter(Position = 0, Mandatory = $True)]
        [String]$Name,

        [Parameter(Position = 1, Mandatory = $False)]
        [String]$Path,

        [Parameter(Mandatory = $False)]
        [DevOpsKitDsc.Workspace.CollectionOption]$Options,

        [Parameter(Mandatory = $False)]
        [String[]]$Nodes
    )

    process {
        # Get workspace settings
        $setting = Import-DOKDscWorkspaceSetting -WorkspacePath $WorkspacePath -Verbose:$VerbosePreference;
        
        $filteredConfigurations = $setting.Collections | Where-Object -FilterScript {
            ($Name -eq $_.Name)
        };

        if ($Null -ne $filteredConfigurations) {
            Write-Error -Message $LocalizedData.ConfigurationAlreadyExists -ErrorAction Stop;
        }

        [String]$configurationPath = GetWorkspacePath -WorkspacePath $WorkspacePath -Path $Path;

        # Use default configuration path if not specified
        if ([String]::IsNullOrEmpty($Path)) {
            $configurationPath = GetDefaultConfigurationPath -WorkspacePath $WorkspacePath -Setting $setting -ConfigurationName $Name -Verbose:$VerbosePreference;
        }

        # Check of the configuration script already exists
        if (!(Test-Path -Path $configurationPath)) {
            
            if ($PSCmdlet.ShouldProcess($LocalizedData.CreatingFromTemplate, $configurationPath)) {

                # Create a configuration from a template
                CopyTemplate -Name 'NewConfiguration.ps1' -Path $configurationPath -Verbose:$VerbosePreference;
            }
        }

        $relativePath = GetWorkspacePath -WorkspacePath $WorkspacePath -Path $configurationPath -Relative;

        $c = New-Object -TypeName DevOpsKitDsc.Workspace.Collection -Property @{
            Name = $Name;
            Path = $relativePath;
        };

        if ($PSBoundParameters.ContainsKey('Options')) {
            $c.Options = $Options;
        }

        if ($PSBoundParameters.ContainsKey('Nodes')) {
            $c.Nodes = New-Object -TypeName 'System.Collections.Generic.List[String]';
            $c.Nodes.AddRange($Nodes);
        }
        
        $setting.Collections.Add($c);

        if ($PSCmdlet.ShouldProcess($LocalizedData.WritingWorkspaceSettings, $WorkspacePath)) {
            WriteWorkspaceSetting -WorkspacePath $WorkspacePath -InputObject $setting -Verbose:$VerbosePreference;
        }

        return $c;
    }
}

function Publish-DOKDscCollection { 

    [CmdletBinding()]
    [OutputType([void])]
    param (
        [Parameter(Mandatory = $False)]
        [String[]]$Name,

        [Parameter(Mandatory = $False)]
        [String]$WorkspacePath = $PWD,

        [Parameter(Mandatory = $False)]
        [DevOpsKitDsc.Workspace.ConfigurationOptionTarget]$Target
    )

    begin {
        $dokOperation = 'Publish';

        Write-Verbose -Message "[DOKDsc][$dokOperation] BEGIN::";
    }

    process {

        # Get workspace settings
        $setting = Import-DOKDscWorkspaceSetting -WorkspacePath $WorkspacePath -Verbose:$VerbosePreference;

        Write-Verbose -Message "[DOKDsc][$dokOperation] -- Using collection: $Name";

        # Filter collections by name as required
        $collections = GetCollection -Setting $setting -Name $Name -Verbose:$VerbosePreference;

        # Process each matching collection
        foreach ($collection in $collections) {

            Write-Verbose -Message "[DOKDsc][$dokOperation] -- Processing collection: $Name";

            $outputPath = GetWorkspacePath -WorkspacePath $WorkspacePath -Path $setting.Options.OutputPath -Verbose:$VerbosePreference;

            # Override the collection target if specified
            if ($PSBoundParameters.ContainsKey('Target')) {
                if ($Null -eq $collection.Options) {
                    $collection.Options = New-Object -TypeName DevOpsKitDsc.Workspace.CollectionOption;
                }

                $collection.Options.Target = $Target;
            }
            
            $publishParams = @{
                OutputPath = $outputPath;
                Path = GetWorkspacePath -WorkspacePath $WorkspacePath -Path $collection.Path -Verbose:$VerbosePreference;
                Name = $collection.Name;
                Collection = $collection;
            };

            PublishConfiguration @publishParams -Verbose:$VerbosePreference;
        }
    }

    end {
        Write-Verbose -Message "[DOKDsc][$dokOperation] END::";
    }
}

# Generate a unique .mof configuration document
function Invoke-DOKDscBuild {

    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $False)]
        [String]$Name,

        [Parameter(Mandatory = $False)]
        [String[]]$InstanceName,

        [Parameter(Mandatory = $False)]
        [Alias('Path')]
        [String]$WorkspacePath = $PWD,

        [Parameter(Mandatory = $False)]
        [Object]$ConfigurationData,

        [Parameter(Mandatory = $False)]
        [System.Collections.IDictionary]$Parameters,

        # Force build to occur even if configuration is not stale
        [Parameter(Mandatory = $False)]
        [Switch]$Force = $False
    )

    begin {
        $dokOperation = 'Deploy';

        Write-Verbose -Message "[DOKDsc][$dokOperation] BEGIN::";
    }

    process {

        # Get workspace settings
        $setting = Import-DOKDscWorkspaceSetting -WorkspacePath $WorkspacePath -Verbose:$VerbosePreference;

        Write-Verbose -Message "[DOKDsc][$dokOperation] -- Using Collection: $Name";
        Write-Verbose -Message "[DOKDsc][$dokOperation] -- Using BuildPath: $outputPath";
        Write-Verbose -Message "[DOKDsc][$dokOperation] -- Using NodePath: $($setting.Options.NodePath)";
        # Write-Verbose -Message "[DOKDsc][$dokOperation] -- Using SourcePath: $($setting.Workspace.SourcePath)";
        Write-Verbose -Message "[DOKDsc][$dokOperation] -- Using ModulePath: $($setting.Options.ModulePath)";

        # # Check if the path exists
        # if (!(Test-Path -Path $nodePath)) {
        # # No node data to process
        # return;
        # }

        $availableModules = $Null;

        # Build module cache
        if ($Null -ne $setting.Modules -and $setting.Modules.Length -gt 0) {
            $availableModules = BuildModuleCache;
        }

        # Get the module path
        $modulePath = GetWorkspacePath -WorkspacePath $WorkspacePath -Path $setting.Options.ModulePath;
        $modulePath = CreatePath -Path $modulePath -PassThru -Verbose:$VerbosePreference;

        $configFilterParams = @{ Workspace = $setting; };

        if ($PSBoundParameters.ContainsKey('Name')) {
            $configFilterParams['Name'] = $Name;
        }

        $collections = Get-DOKDscCollection @configFilterParams -Verbose:$VerbosePreference;
        
        # Process each environment
        foreach ($collection in $collections) {

            Write-Verbose -Message "[DOKDsc][$dokOperation] -- Processing collection: $($collection.Name)";
            
            # Get the output path
            $outputPath = GetWorkspacePath -WorkspacePath $WorkspacePath -Path $setting.Options.OutputPath -ChildPath $collection.Name;

            # Ensure that the output path exists
            $outputPath = CreatePath -Path $outputPath -PassThru -Verbose:$VerbosePreference;

            $sourcePath = GetWorkspacePath -WorkspacePath $WorkspacePath -Path $collection.Path;

            $nodePath = $collection.Nodes;

            if ($Null -ne $nodePath) {

                # Import node data
                $nodeData = ImportNodeData -WorkspacePath $WorkspacePath -NodePath $nodePath -InstanceName $InstanceName -Verbose:$VerbosePreference;
                
                foreach ($node in $nodeData) {

                    Write-Verbose -Message "[DOKDsc][$dokOperation] -- Processing node: $($node.InstanceName)";
                    
                    try {
                        # Merge certificate information into node data
                        MergeNodeCertificate -InputObject $node -Path $node.BaseDirectory -InstanceName $node.InstanceName -Verbose:$VerbosePreference;
    
                        MergeConfiguration -InputObject $node -Collection $collection -Verbose:$VerbosePreference;

                        $signatureParams = @{
                            InstanceName = $node.InstanceName
                            WorkspacePath = $WorkspacePath
                            Collection = $collection
                            Node = $node.ConfigurationData
                        }

                        # Create a build signature
                        $signature = NewBuildSignature @signatureParams -Verbose:$VerbosePreference;
                        
                        # Get the default signature path
                        $signaturePath = GetWorkspacePath -WorkspacePath $WorkspacePath -Path '.dokd-obj';
                        
                        # Use an alternative path for storing signatures
                        if (![String]::IsNullOrEmpty($collection.Options.SignaturePath)) {

                            if ($collection.Options.SignaturePath -match '^http(s){0,1}\:\/\/') {
                                $signaturePath = $collection.Options.SignaturePath;
                            } else {
                                $signaturePath = GetWorkspacePath -WorkspacePath $WorkspacePath -Path $collection.Options.SignaturePath;
                            }
                        }

                        ImportModule -Module $setting.Modules -OutputPath $modulePath -Verbose:$VerbosePreference;

                        if ($Force -or ($Null -ne $collection.Options -and $collection.Options.BuildMode -eq [DevOpsKitDsc.Workspace.CollectionBuildMode]::Full) -or (ShouldBuildConfiguration -Signature $signature -SasToken $collection.Options.SignatureSasToken -Path $signaturePath -InstanceName $node.InstanceName -CollectionName $collection.Name)) {

                            # Create job parameters
                            $jobParams = New-Object -TypeName PSObject -Property @{
                                ConfigurationName = $configuration.Name;
                                ConfigurationData = $node.ConfigurationData;
                                Parameters = $Parameters;
                                Path = $sourcePath;
                                OutputPath = $outputPath;
                                ModulePath = [String[]]$setting.Options.ModulePath;
                                AddModulesToSearchPath = $setting.Options.AddModulesToSearchPath;
                            }
                            
                            # Save the build signature
                            WriteBuildSignature -Path $signaturePath -SasToken $collection.Options.SignatureSasToken -Signature $signature;
                            
                            # Build the configuration
                            BuildConfiguration -InputObject $jobParams -Verbose:$VerbosePreference;

                            # Build documentation
                            BuildDocumentation -WorkspacePath $WorkspacePath -Collection $collection -Path $outputPath -OutputPath $outputPath -Verbose:$VerbosePreference;
                        }
                    } catch {
                        Write-Error -Message "Failed to build configuration for $($node.InstanceName). $($_.Exception.Message)" -Exception $_.Exception;
                    }
                }
            }
        }
    }

    end {
        Write-Verbose -Message "[DOKDsc][$dokOperation] END::";
    }
}

function Publish-DOKDscModule {

    [CmdletBinding()]
    [OutputType([void])]
    param (
        [Parameter(Mandatory = $False)]
        [String]$WorkspacePath = $PWD,

        # The name of the collection
        [Parameter(Mandatory = $False)]
        [String]$Name,

        # The name of the module
        [Parameter(Mandatory = $False)]
        [String]$ModuleName,

        # The version of the module
        [Parameter(Mandatory = $False)]
        [String]$ModuleVersion
    )

    begin {
        $dokOperation = 'Publish';

        Write-Verbose -Message "[DOKDsc][$dokOperation] BEGIN::";
    }

    process {
        
        # Get workspace settings
        $setting = Import-DOKDscWorkspaceSetting -WorkspacePath $WorkspacePath -Verbose:$VerbosePreference;
        
        $modules = GetModule -WorkspacePath $WorkspacePath -Workspace $setting -Verbose:$VerbosePreference;

        if ($PSBoundParameters.ContainsKey('ModuleName') -or $PSBoundParameters.ContainsKey('ModuleVersion')) {
            $modules = $modules | Where-Object -FilterScript {
                ([String]::IsNullOrEmpty($ModuleName) -or $ModuleName -eq $_.ModuleName) -and
                ([String]::IsNullOrEmpty($ModuleVerion) -or $ModuleVersion -eq $_.ModuleVersion)
            }

            if ($Null -eq $modules) {
                Write-Error -Message ($LocalizedData.ModuleDoesNotExist) -Category ObjectNotFound -ErrorAction Stop;
            }
        }

        $collections = GetCollection -Setting $setting -Name $Name -Verbose:$VerbosePreference;

        # Process each environment
        foreach ($module in $modules) {

            $outputPath = GetWorkspacePath -WorkspacePath $WorkspacePath -Path $setting.Options.OutputPath;
            
            $publishParams = @{
                Module = $module;
                OutputPath = $outputPath;
            };

            foreach ($collection in $collections) {

                if ($collection.Options.Target -eq [DevOpsKitDsc.Workspace.ConfigurationOptionTarget]::AzureAutomationService) {
                    $publishParams['Target'] = [DevOpsKitDsc.Workspace.ConfigurationOptionTarget]::AzureAutomationService;
                } else {
                    $publishParams['Target'] = [DevOpsKitDsc.Workspace.ConfigurationOptionTarget]::FileSystem;
                }

                PublishModule @publishParams;
            }
        }
    }

    end {
        Write-Verbose -Message "[DOKDsc][$dokOperation] END::";
    }
}

function Get-DOKDscModule {

    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $False)]
        [String]$WorkspacePath = $PWD,

        [Parameter(Mandatory = $False)]
        [String]$ModuleName,

        [Parameter(Mandatory = $False)]
        [String]$ModuleVersion
    )

    process {

        # Get workspace settings
        $setting = Import-DOKDscWorkspaceSetting -WorkspacePath $WorkspacePath -Verbose:$VerbosePreference;

        # Get matching modules
        GetModule -WorkspacePath $WorkspacePath -Workspace $setting -Verbose:$VerbosePreference | Where-Object -FilterScript {
            ([String]::IsNullOrEmpty($ModuleName) -or $_.ModuleName -eq $ModuleName) -and
            ([String]::IsNullOrEmpty($ModuleVersion) -or $_.ModuleVersion -eq $ModuleVersion)
        }
    }
}

function Start-DOKDscSite {

    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $False)]
        [String]$Path = $PWD
    )

    process {

        if (!(Test-Path -Path $Path -PathType Container)) {
            Write-Error -Message 'The path is not valid';

            return;
        }

        # $setting = ReadSetting -Path $Path -Verbose:$VerbosePreference;
        

        docfx "$Path\.docfx\docfx.json" --serve
    }
}

function Publish-DOKDscSite {

    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $False)]
        [String]$WorkspacePath = $PWD
    )

    process {

        # Get workspace settings
        # $setting = Import-DOKDscWorkspaceSetting -WorkspacePath $WorkspacePath -Verbose:$VerbosePreference;

        # Generate a docfx.json file

        # Call docfx build
        BuildSite -Path "$WorkspacePath\.docfx\docfx.json" -Verbose:$VerbosePreference;
    }
}

function Initialize-DOKDsc {
    
    [CmdletBinding()]
    [OutputType([void])]
    param (
        [Parameter(Mandatory = $False)]
        [Alias('Path')]
        [String]$WorkspacePath = $PWD,

        # Use to force the creation of the workspace when the path does not exist
        [Parameter(Mandatory = $False)]
        [Switch]$Force = $False
    )

    begin {
        Write-Verbose -Message "[DOKDsc][Init] BEGIN::";
    }

    process {

        # Check if the workspace path exists
        if (!(Test-Path -Path $WorkspacePath)) {

            if ($Force) {

                # Force creation of the workspace path
                New-Item -Path $WorkspacePath -ItemType Directory -Force | Out-Null;
            } else {
                Write-Error -Message ($LocalizedData.WorkspacePathDoesNotExist) -Category ObjectNotFound -TargetObject $WorkspacePath -ErrorAction Stop;
            }
        }

        if (!(HasWorkspaceSetting -WorkspacePath $WorkspacePath -Verbose:$VerbosePreference)) {
            
            # Create settings
            WriteWorkspaceSetting -WorkspacePath $WorkspacePath -Verbose:$VerbosePreference;
        }
    }

    end {
        Write-Verbose -Message "[DOKDsc][Init] END::";
    }
}

# Restore workspace modules
function Restore-DOKDscModule {

    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $False)]
        [String]$WorkspacePath = $PWD,

        # The name of the module to restore
        [Parameter(Mandatory = $False)]
        [String]$ModuleName,

        # The version of the module to restore
        [Parameter(Mandatory = $False)]
        [String]$ModuleVersion
    )

    process {

        $setting = Import-DOKDscWorkspaceSetting -WorkspacePath $WorkspacePath -Verbose:$VerbosePreference;

        # Get a list of a matching modules
        $modules = GetModule -WorkspacePath $WorkspacePath -Workspace $setting -Verbose:$VerbosePreference | Where-Object -FilterScript {
            ([String]::IsNullOrEmpty($ModuleName) -or $_.ModuleName -eq $ModuleName) -and
            ([String]::IsNullOrEmpty($ModuleVersion) -or $_.ModuleVersion -eq $ModuleVersion)
        }

        # Process each matching module
        foreach ($module in $modules) {

            if ($module.Type -eq 'Workspace') {

            } else {
                RestoreModule -Module $module -OutputPath (GetWorkspacePath -WorkspacePath $WorkspacePath -Path $setting.Options.ModulePath);
            }
        }
    }
}

function Import-DOKDscWorkspaceSetting {

    [CmdletBinding()]
    [OutputType([DevOpsKitDsc.Workspace.WorkspaceSetting])]
    param (
        [Parameter(Mandatory = $False)]
        [Alias('Path')]
        [String]$WorkspacePath = $PWD
    )

    process {

        # Check if the workspace path exists
        if (!(Test-Path -Path $WorkspacePath)) {
            Write-Error -Message ($LocalizedData.WorkspacePathDoesNotExist) -Category ObjectNotFound -TargetObject $WorkspacePath -ErrorAction Stop;
        }

        return ReadWorkspaceSetting -WorkspacePath $WorkspacePath -Verbose:$VerbosePreference;
    }
}

function Set-DOKDscWorkspaceOption {

    [CmdletBinding(SupportsShouldProcess = $True)]
    [OutputType([void])]
    param (
        [Parameter(Mandatory = $False)]
        [String]$WorkspacePath = $PWD,

        [Parameter(Mandatory = $False)]
        [String]$OutputPath,

        [Parameter(Mandatory = $False)]
        [String]$NodePath
    )

    process {

        # Load current workspace settings
        $setting = ReadWorkspaceSetting -WorkspacePath $WorkspacePath -Verbose:$VerbosePreference;

        # Track if settings have been changed
        $settingChanged = $False;

        # Check for OutputPath parameter
        if ($PSBoundParameters.ContainsKey('OutputPath')) {

            # Continue if the parameter is different to the current setting
            if ($OutputPath -ne $setting.Options.OutputPath) {

                # Process WhatIf
                if ($PSCmdlet.ShouldProcess('', '')) {

                    # Update the setting
                    $setting.Options.OutputPath = $OutputPath;

                    # Mark setting as changed
                    $settingChanged = $True;
                }
            }
        }

        # Check for NodePath parameter
        if ($PSBoundParameters.ContainsKey('NodePath')) {

            # Continue if the parameter is different to the current setting
            if ($OutputPath -ne $setting.Options.NodePath) {

                # Process WhatIf
                if ($PSCmdlet.ShouldProcess('', '')) {
                    
                    # Update the setting
                    $setting.Options.NodePath = $NodePath;

                    # Mark setting as changed
                    $settingChanged = $True;
                }
            }
        }

        # Save workspace settings if any changes were made
        if ($settingChanged) {

            # Save workspace settings
            WriteWorkspaceSetting -InputObject $setting -WorkspacePath $WorkspacePath -Verbose:$VerbosePreference;
        }
    }
}

function Get-DOKDscWorkspaceOption {

    [CmdletBinding()]
    [OutputType([DevOpsKitDsc.Workspace.WorkspaceOption])]
    param (
        [Parameter(Mandatory = $False)]
        [String]$WorkspacePath = $PWD
    )

    process {
        
        # Load current workspace settings
        $setting = ReadWorkspaceSetting -WorkspacePath $WorkspacePath -Verbose:$VerbosePreference;

        return $setting.Options;
    }
}

function Set-DOKDscCollectionOption {

    [CmdletBinding(SupportsShouldProcess = $True)]
    [OutputType([void])]
    param (
        [Parameter(Mandatory = $False)]
        [String]$WorkspacePath = $PWD,

        [Parameter(Mandatory = $True)]
        [String]$Name,

        [Parameter(Mandatory = $False)]
        [Nullable[DevOpsKitDsc.Workspace.ConfigurationOptionTarget]]$Target,

        [Parameter(Mandatory = $False)]
        [System.Nullable[System.Boolean]]$ReplaceNodeData,

        [Parameter(Mandatory = $False)]
        [System.Nullable[DevOpsKitDsc.Workspace.CollectionBuildMode]]$BuildMode,

        [Parameter(Mandatory = $False)]
        [String]$SignaturePath,

        [Parameter(Mandatory = $False)]
        [String]$SignatureSasToken
    )

    process {

        # Load current workspace settings
        $setting = ReadWorkspaceSetting -WorkspacePath $WorkspacePath -Verbose:$VerbosePreference;
        
        $collection = GetCollection -Setting $setting -Name $Name -Verbose:$VerbosePreference;

        if ($Null -eq $collection) {
            Write-Error -Message "Failed for find collection" -ErrorAction Stop;
        }

        # Track if options have been changed
        $optionsChanged = $False;

        # Check for Target parameter
        if ($PSBoundParameters.ContainsKey('Target')) {

            # Continue if the parameter is different to the current value
            if ($Null -eq $collection.Options -or $Target -ne $collection.Options.Target) {

                # Process WhatIf
                if ($PSCmdlet.ShouldProcess('', '')) {

                    if ($Null -eq $collection.Options) {

                        $collection.Options = New-Object -TypeName DevOpsKitDsc.Workspace.CollectionOption -Property @{
                            Target = $Target;
                        }
                    } else {

                        # Update the setting
                        $collection.Options.Target = $Target;
                    }

                    # Mark options as changed
                    $optionsChanged = $True;
                }
            }
        }

        # Check for ReplaceNodeData parameter
        if ($PSBoundParameters.ContainsKey('ReplaceNodeData')) {
            
            # Continue if the parameter is different to the current value
            if ($Null -eq $collection.Options -or $ReplaceNodeData -ne $collection.Options.ReplaceNodeData) {

                # Process WhatIf
                if ($PSCmdlet.ShouldProcess('', '')) {

                    if ($Null -eq $collection.Options) {

                        $collection.Options = New-Object -TypeName DevOpsKitDsc.Workspace.CollectionOption -Property @{
                            ReplaceNodeData = $ReplaceNodeData;
                        }
                    } else {

                        # Update the setting
                        $collection.Options.ReplaceNodeData = $ReplaceNodeData;
                    }

                    # Mark options as changed
                    $optionsChanged = $True;
                }
            }
        }

        # Check for BuildMode parameter
        if ($PSBoundParameters.ContainsKey('BuildMode')) {
        
            # Continue if the parameter is different to the current value
            if ($Null -eq $collection.Options -or $BuildMode -ne $collection.Options.BuildMode) {

                # Process WhatIf
                if ($PSCmdlet.ShouldProcess('', '')) {

                    if ($Null -eq $collection.Options) {
                        
                        $collection.Options = New-Object -TypeName DevOpsKitDsc.Workspace.CollectionOption -Property @{
                            BuildMode = $BuildMode;
                        }
                    } else {

                        # Update the setting
                        $collection.Options.BuildMode = $BuildMode;
                    }

                    # Mark options as changed
                    $optionsChanged = $True;
                }
            }
        }

        # Check for SignaturePath parameter
        if ($PSBoundParameters.ContainsKey('SignaturePath')) {
        
            # Continue if the parameter is different to the current value
            if ($Null -eq $collection.Options -or $SignaturePath -ne $collection.Options.SignaturePath) {

                # Process WhatIf
                if ($PSCmdlet.ShouldProcess('', '')) {

                    if ($Null -eq $collection.Options) {
                        
                        $collection.Options = New-Object -TypeName DevOpsKitDsc.Workspace.CollectionOption -Property @{
                            SignaturePath = $SignaturePath;
                        }
                    } else {

                        # Update the setting
                        $collection.Options.SignaturePath = $SignaturePath;
                    }

                    # Mark options as changed
                    $optionsChanged = $True;
                }
            }
        }

        # Check for SignatureSasToken parameter
        if ($PSBoundParameters.ContainsKey('SignatureSasToken')) {
            
            # Continue if the parameter is different to the current value
            if ($Null -eq $collection.Options -or $SignatureSasToken -ne $collection.Options.SignatureSasToken) {

                # Process WhatIf
                if ($PSCmdlet.ShouldProcess('', '')) {

                    if ($Null -eq $collection.Options) {
                        
                        $collection.Options = New-Object -TypeName DevOpsKitDsc.Workspace.CollectionOption -Property @{
                            SignatureSasToken = $SignatureSasToken;
                        }
                    } else {

                        # Update the setting
                        $collection.Options.SignatureSasToken = $SignatureSasToken;
                    }

                    # Mark options as changed
                    $optionsChanged = $True;
                }
            }
        }

        # Save workspace settings if any changes were made
        if ($optionsChanged) {

            # Save workspace settings
            WriteWorkspaceSetting -InputObject $setting -WorkspacePath $WorkspacePath -Verbose:$VerbosePreference;
        }
    }
}

function Add-DOKDscModule {

    [CmdletBinding(DefaultParameterSetName = 'Module')]
    param (
        [Parameter(Mandatory = $False)]
        [String]$WorkspacePath = $PWD,

        [Parameter(Mandatory = $True, ParameterSetName = 'Module')]
        [Alias('Name')]
        [String]$ModuleName,
        
        [Parameter(Mandatory = $True, ParameterSetName = 'Module')]
        [Alias('Version')]
        [String]$ModuleVersion,

        [Parameter(Mandatory = $False, ParameterSetName = 'Module')]
        [String]$Repository,

        [Parameter(Mandatory = $True, ParameterSetName = 'Path')]
        [String]$Path,

        [Parameter(Mandatory = $False)]
        [ValidateSet('Workspace', 'Repository')]
        [String]$Type
    )

    begin {
        # Track if settings have been changed
        $settingChanged = $False;

        # Load current workspace settings
        $setting = ReadWorkspaceSetting -WorkspacePath $WorkspacePath -Verbose:$VerbosePreference;
    }

    process {        

        $moduleProps = @{ };

        if ($PSBoundParameters.ContainsKey('ModuleName')) {
            $moduleProps['ModuleName'] = $ModuleName;
        }

        if ($PSBoundParameters.ContainsKey('ModuleVersion')) {
            $moduleProps['ModuleVersion'] = $ModuleVersion;
        }

        if ($PSBoundParameters.ContainsKey('Repository')) {
            $moduleProps['Repository'] = $Repository;
        }

        if ($PSCmdlet.ParameterSetName -eq 'Path') {

            # Read the module manifest
            $manifestPath = GetWorkspacePath -WorkspacePath $WorkspacePath -Path $Path;
            $manifestName = Split-Path -Path $manifestPath -Leaf;
            Import-LocalizedData -BindingVariable moduleData -BaseDirectory $manifestPath -FileName "$manifestName.psd1";

            $moduleProps['ModuleName'] = $manifestName;
            $moduleProps['ModuleVersion'] = $moduleData.ModuleVersion;

            $moduleProps['Path'] = $Path;
        }

        if ($PSBoundParameters.ContainsKey('Type')) {
            $moduleProps['Type'] = $Type;
        }

        if (AddModuleToWorkspace -Setting $setting @moduleProps -Verbose:$VerbosePreference) {
            $settingChanged = $True;
        }
    }

    end {
        if ($settingChanged) {
            WriteWorkspaceSetting -WorkspacePath $WorkspacePath -InputObject $setting -Verbose:$VerbosePreference;
        }
    }
}

function Compress-DOKDscWorkspaceModule {

    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $False)]
        [Alias('Path')]
        [String]$WorkspacePath = $PWD
    )

    process {

        # Check that the workspace path exists
        if (!(Test-Path -Path $WorkspacePath)) {
            Write-Error -Message ($LocalizedData.WorkspacePathDoesNotExist -f $WorkspacePath) -Category ObjectNotFound;
            
            return;
        }

        # Get workspace settings
        $setting = Import-DOKDscWorkspaceSetting -WorkspacePath $WorkspacePath -Verbose:$VerbosePreference;

        # Check modules and restore as required
        foreach ($module in $setting.Modules) {

            #
            PackageModule -Module $module -Format AzureAutomationService -ModulePath $setting.Options.ModulePath -Verbose:$VerbosePreference;
        }
    }
}

#endregion Public functions

#
# Helper functions
#

#region Helper functions

function RegisterNode {

    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $True)]
        [PSObject]$Node,

        [Parameter(Mandatory = $True)]
        [String]$OutputPath
    )

    process {

        # Setup the encryption certificate
        TryDscEncryptionCertificate -InstanceName $Node.InstanceName -Path $OutputPath -Verbose:$VerbosePreference | Out-Null;
    }
}

function GetNodeSessionConfiguration {

    [CmdletBinding()]
    [OutputType([Hashtable])]
    param (
        [Parameter(Mandatory = $True)]
        [String]$InstanceName
    )

    process {

        return @{
            UseSession = $True;
            CreateCertificate = $True;
        };
    }
}

# Create a public/private keypair as required
function TryDscEncryptionCertificate {

    [CmdletBinding()]
    [OutputType([Security.Cryptography.X509Certificates.X509Certificate2])]
    param (
        # A remoting session to connect to
        [Parameter(Mandatory = $True)]
        [String]$InstanceName,

        # The path to save the encryption public key to
        [Parameter(Mandatory = $True)]
        [String]$Path
    )

    process {

        $sessionConfig = GetNodeSessionConfiguration -InstanceName $InstanceName;

        $commandParams = @{ };

        if ($sessionConfig.UseSession) {

            $sessionParams = @{
                ComputerName = $InstanceName
            };
    
            if ($InstanceName -eq 'localhost' -or $InstanceName -eq $Env:COMPUTERNAME) {
                $sessionParams['EnableNetworkAccess'] = $True;
            }

            $commandParams = @{ Session = (New-PSSession @sessionParams); };
        }

        # Try to get the encryption certificate
        $certificate = Invoke-Command @commandParams -ScriptBlock ${function:GetCertificate} -ArgumentList $sessionConfig;

        # Create a new encryption certificate as required
        if ($Null -eq $certificate -and $sessionConfig.CreateCertificate) {
            $certificate = Invoke-Command @commandParams -ScriptBlock ${function:NewCertificate} -ArgumentList $sessionConfig;
        } else {
            Write-Verbose -Message ($LocalizedData.HasEncryptionCertificate -f $InstanceName, $certificate.Thumbprint);
        }

        if ($Null -eq $certificate) {
            return $Null;
        }

        # Strongly type the result
        $result = New-Object -TypeName Security.Cryptography.X509Certificates.X509Certificate2 -ArgumentList @(,$certificate.GetRawCertData());

        $result | Export-Certificate -FilePath "$Path\$InstanceName.cer" -Force;

        # Return the certificate
        return $result;
    }
}

function GetCertificate {

    [CmdletBinding()]
    param (
        [Parameter(Position = 0)]
        [Hashtable]$Options
    )

    process {
        # Get the DSC encryption certificate with the longest lifetime
        Get-ChildItem -Path 'Certificate::LocalMachine\My' | Where-Object -FilterScript {
            $_.FriendlyName -eq 'DSC Credential Encryption'
        } | Sort-Object -Property NotAfter -Descending | Select-Object -First 1;
    }
}

function NewCertificate {

    [CmdletBinding()]
    param (
        [Parameter(Position = 0)]
        [Hashtable]$Options
    )

    process {

        # This function is not cross platform and needs to be updated to work with Core PowerShell.

        # Subject processing

        [String]$Subject = "CN=$Env:COMPUTERNAME";

        # http://msdn.microsoft.com/en-us/library/aa377051(VS.85).aspx
        $SubjectDN = New-Object -ComObject X509Enrollment.CX500DistinguishedName;
        $SubjectDN.Encode($Subject, 0x0);

        # SANs
        New-Variable -Name OtherName -Value 0x1 -Option Constant
        New-Variable -Name RFC822Name -Value 0x2 -Option Constant
        New-Variable -Name DNSName -Value 0x3 -Option Constant
        New-Variable -Name DirectoryName -Value 0x5 -Option Constant
        New-Variable -Name URL -Value 0x7 -Option Constant
        New-Variable -Name IPAddress -Value 0x8 -Option Constant
        New-Variable -Name RegisteredID -Value 0x9 -Option Constant
        New-Variable -Name Guid -Value 0xa -Option Constant
        New-Variable -Name UPN -Value 0xb -Option Constant
        New-Variable -Name AllowUntrustedCertificate -Value 0x2 -Option Constant

        New-Variable -Name Base64 -Value 0x1 -Option Constant

        $certificateExtensions = @();

        # Enhanced key usage

        $EnhancedKeyUsage = [Security.Cryptography.Oid]'Document Encryption';
        
        [Security.Cryptography.X509Certificates.X509KeyUsageFlags]$KeyUsage = 'KeyEncipherment, DataEncipherment';
        [String[]]$SubjectAlternativeName = $Env:COMPUTERNAME;
        $ProviderName = 'Microsoft Enhanced Cryptographic Provider v1.0';
        $AlgorithmName = 'RSA';
        $SignatureAlgorithm = 'SHA256';
        $KeyLength = 2048;
        [String]$FriendlyName = 'DSC Credential Encryption';
        $Description = 'This is an encryption certificate for DSC.';

        [DateTime]$NotBefore = [DateTime]::Now.AddDays(-1);
        [DateTime]$NotAfter = $NotBefore.AddDays(365);
        
        $OIDs = New-Object -ComObject X509Enrollment.CObjectIDs;

        $OID = New-Object -ComObject X509Enrollment.CObjectID;
        $OID.InitializeFromValue($EnhancedKeyUsage.Value);

        # http://msdn.microsoft.com/en-us/library/aa376785(VS.85).aspx
        $OIDs.Add($OID);
        

        # http://msdn.microsoft.com/en-us/library/aa378132(VS.85).aspx
        $EKU = New-Object -ComObject X509Enrollment.CX509ExtensionEnhancedKeyUsage;
        $EKU.InitializeEncode($OIDs);
        $certificateExtensions += $EKU;

        # Build key usage extension
        $keyUsageExtension = New-Object -ComObject X509Enrollment.CX509ExtensionKeyUsage;
        $keyUsageExtension.InitializeEncode([int]$KeyUsage);
        $keyUsageExtension.Critical = $True;
        $certificateExtensions += $keyUsageExtension;

        # SAN

        if ($SubjectAlternativeName) {

            $SAN = New-Object -ComObject X509Enrollment.CX509ExtensionAlternativeNames
            $Names = New-Object -ComObject X509Enrollment.CAlternativeNames

            foreach ($altname in $SubjectAlternativeName) {

                $Name = New-Object -ComObject X509Enrollment.CAlternativeName
                if ($altname.Contains("@")) {
                    $Name.InitializeFromString($RFC822Name,$altname)
                } else {
                    try {
                        $Bytes = [Net.IPAddress]::Parse($altname).GetAddressBytes()
                        $Name.InitializeFromRawData($IPAddress,$Base64,[Convert]::ToBase64String($Bytes))
                    } catch {
                        try {
                            $Bytes = [Guid]::Parse($altname).ToByteArray()
                            $Name.InitializeFromRawData($Guid,$Base64,[Convert]::ToBase64String($Bytes))
                        } catch {
                            try {
                                $Bytes = ([Security.Cryptography.X509Certificates.X500DistinguishedName]$altname).RawData
                                $Name.InitializeFromRawData($DirectoryName,$Base64,[Convert]::ToBase64String($Bytes))
                            } catch {
                                $Name.InitializeFromString($DNSName,$altname)
                            }
                        }
                    }
                }
                
                $Names.Add($Name)
            }

            $SAN.InitializeEncode($Names);

            $certificateExtensions += $SAN;
        }

        # Get private key algorithm OID
        $algorithmOid = New-Object -ComObject X509Enrollment.CObjectId;
        $algorithmOid.InitializeFromValue(([Security.Cryptography.Oid]$AlgorithmName).Value);

        # Get signature algorithm OID
        $signatureOid = New-Object -ComObject X509Enrollment.CObjectId;
        $signatureOid.InitializeFromValue(([Security.Cryptography.Oid]$SignatureAlgorithm).Value);

        # Generate a private key

        # http://msdn.microsoft.com/en-us/library/aa378921(VS.85).aspx
        $privateKey = New-Object -ComObject X509Enrollment.CX509PrivateKey;
        $privateKey.ProviderName = $ProviderName;
        $privateKey.Algorithm = $algorithmOid;

        # http://msdn.microsoft.com/en-us/library/aa379409(VS.85).aspx
        $privateKey.KeySpec = 1; # Exchange
        $privateKey.Length = $KeyLength;
        $privateKey.MachineContext = $True;
        $privateKey.ExportPolicy = 0; # Not exportable
        $privateKey.Create();

        # Build the certificate request

        # http://msdn.microsoft.com/en-us/library/aa377124(VS.85).aspx
        $csr = New-Object -ComObject X509Enrollment.CX509CertificateRequestCertificate;
        $csr.InitializeFromPrivateKey(0x2, $privateKey, '');

        # Set certificate fields
        $csr.Subject = $SubjectDN;
        $csr.Issuer = $csr.Subject;
        $csr.NotBefore = $NotBefore;
        $csr.NotAfter = $NotAfter;

        # Add certificate extensions
        foreach ($item in $certificateExtensions) {
            $csr.X509Extensions.Add($item);
        };

        $csr.SignatureInformation.HashAlgorithm = $signatureOid;

        # Completing certificate request template building
        $csr.Encode();

        # Enroll the certificate from the provided CSR
        return EnrollCertificate -CSR $csr -FriendlyName $FriendlyName -Description $Description;
    }
}

function EnrollCertificate {

    [CmdletBinding()]
    [OutputType([Security.Cryptography.X509Certificates.X509Certificate2])]
    param (
        [Parameter(Mandatory = $True)]
        [Object]$CSR,

        [Parameter(Mandatory = $True)]
        [String]$FriendlyName,

        [Parameter(Mandatory = $True)]
        [String]$Description
    )

    process {

        # Interface: http://msdn.microsoft.com/en-us/library/aa377809(VS.85).aspx
        $enrollment = New-Object -ComObject X509Enrollment.CX509enrollment;
        $enrollment.InitializeFromRequest($CSR);
        $enrollment.CertificateFriendlyName = $FriendlyName;
        $enrollment.CertificateDescription = $Description;

        # Create the request with base64 encoding
        $endCert = $enrollment.CreateRequest(0x1);
            
        # Install the certificate response
        $enrollment.InstallResponse(
            0x2, # Allow untrusted, this self-signed certificate will not chain to a trust root
            $endCert,
            0x1, # Use base64 encoding
            ''
        );

        [Byte[]]$certBytes = [System.Convert]::FromBase64String($endCert);

        return New-Object -TypeName Security.Cryptography.X509Certificates.X509Certificate2 @(,$certBytes);
    }
}

function CopyModules {

    [CmdletBinding()]
    [OutputType([void])]
    param (
        [Parameter(Mandatory = $True)]
        [System.Management.Automation.Runspaces.PSSession]$Session,

        # The source path for the modules
        [Parameter(Mandatory = $True)]
        [String]$Path
    )

    process {
        # Copy the module paths into the session
        Copy-Item -ToSession $Session -Path $Path -Destination 'C:\Program Files\WindowsPowerShell\Modules\' -Recurse -Force;
    }
}

function BuildConfiguration {

    [CmdletBinding()]
    param (
        [Parameter(ValueFromPipeline = $True)]
        [PSObject]$InputObject
    )

    process {

        Write-Verbose -Message "[DOKDsc][$dokOperation] -- Building configuration";
        
        $Path = $InputObject.Path;
        $OutputPath = $InputObject.OutputPath;
        $ModulePath = $InputObject.ModulePath;
        $ConfigurationData = $InputObject.ConfigurationData;
        $Parameters = $InputObject.Parameters;
        $AddModulesToSearchPath = $InputObject.AddModulesToSearchPath;

        $currentPSModulePath = $Env:PSModulePath;

        try {

            if (![String]::IsNullOrEmpty($ModulePath) -and $AddModulesToSearchPath) {
                $Env:PSModulePath = "$Env:SystemRoot\System32\WindowsPowerShell\v1.0\Modules;$([String]::Join(';', $ModulePath))";

                Write-Verbose -Message "[DOKDsc][$dokOperation] -- Using PSModulePath: $($Env:PSModulePath)";
                # [System.Environment]::SetEnvironmentVariable('PSModulePath', "$Env:SystemRoot\System32\WindowsPowerShell\v1.0\Modules;$ModulePath", 'Process')
            }

            $configurationScript = "$Path";

            Write-Verbose -Message "[DOKDsc][$dokOperation] -- Using configuration script: $configurationScript";

            if (!(Test-Path -Path $configurationScript)) {
                Write-Error -Message "Failed to find configuration script: $configurationScript";
            }

            # By default use the base name of the file as the configuration name
            $scriptItem = Get-Item -Path $configurationScript;
            $configurationName = $scriptItem.BaseName;

            if (![String]::IsNullOrEmpty($InputObject.ConfigurationName)) {
                $configurationName = $InputObject.ConfigurationName;
            }

            Write-Verbose -Message "[DOKDsc][$dokOperation] -- Using configuration name: $ConfigurationName";

            Write-Verbose -Message "[DOKDsc][$dokOperation] -- Using OutputPath: $OutputPath";

            $configParams = @{ OutputPath = $OutputPath; };

            # Bind configuration data
            if ($Null -ne $ConfigurationData) {
                $configParams.Add('ConfigurationData', $ConfigurationData);
            }

            # Bind parameters
            if ($Null -ne $Parameters) {
                $configParams += $Parameters;
            }

            # Dot source the configuration script
            . "$configurationScript";

            # Execute the configuration script
            $buildResult = & $ConfigurationName @configParams;

            if ($Null -ne $buildResult -and $buildResult -is [System.IO.FileInfo]) {
                Write-Verbose -Message "[DOKDsc][$dokOperation] -- Generating checksum: $($buildResult.FullName)";

                New-DscChecksum -Path $buildResult.FullName -Force | Out-Null;
            }
        } catch {
            Write-Error -Message "Failed to build configuration for node. $($_.Exception.Message)" -Exception $_.Exception;
        } finally {

            if (![String]::IsNullOrEmpty($ModulePath) -and $AddModulesToSearchPath) {
                $Env:PSModulePath = $currentPSModulePath;

                # [System.Environment]::SetEnvironmentVariable('PSModulePath', "$currentPSModulePath", 'Process')
            }
        }
    }
}

# Checks if build should continue if existing configuration is stale
function ShouldBuildConfiguration {

    [CmdletBinding()]
    [OutputType([System.Boolean])]
    param (
        [Parameter(Mandatory = $True)]
        [DevOpsKitDsc.Build.BuildSignature]$Signature,

        [Parameter(Mandatory = $True)]
        [String]$Path,

        [Parameter(Mandatory = $False)]
        [String]$SasToken,

        [Parameter(Mandatory = $True)]
        [String]$InstanceName,

        [Parameter(Mandatory = $True)]
        [String]$CollectionName
    )

    process {

        $previousSignature = ReadBuildSignature -InstanceName $InstanceName -CollectionName $CollectionName -Path $Path -SasToken $SasToken -Verbose:$VerbosePreference;

        # Check if the build integrity matches the previous build
        if ($Null -ne $previousSignature -and $Signature.buildIntegrity -eq $previousSignature.buildIntegrity) {

            return $False;
        }

        # Build is stale, build should occur
        return $True;
    }
}

function NewBuildSignature {

    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $True)]
        [String]$WorkspacePath,

        [Parameter(Mandatory = $True)]
        [String]$InstanceName,

        [Parameter(Mandatory = $True)]
        [Hashtable]$Node,

        [Parameter(Mandatory = $True)]
        [DevOpsKitDsc.Workspace.Collection]$Collection
    )

    process {
        
        $signature = New-Object -TypeName DevOpsKitDsc.Build.BuildSignature;

        $signature.InstanceName = $InstanceName;
        $signature.CollectionName = $Collection.Name;

        # Add configuration script
        $signature.Path = GetWorkspacePath -WorkspacePath $WorkspacePath -Path $Collection.Path;

        # Add node data
        $signature.Node = $node;

        # Calculate build integrity
        $signature.Update();

        return $signature;
    }
}

function ReadBuildSignature {

    [CmdletBinding()]
    [OutputType([DevOpsKitDsc.Build.BuildSignature])]
    param (
        [Parameter(Mandatory = $True)]
        [String]$Path,

        [Parameter(Mandatory = $False)]
        [String]$SasToken,

        [Parameter(Mandatory = $True)]
        [String]$InstanceName,

        [Parameter(Mandatory = $True)]
        [String]$CollectionName
    )

    process {

        if ($Path -like "https://*") {

            # Handle reading signatures from HTTPS endpoint
            
            # Get the endpoint URI
            $endpointUri = GetSignatureEndpoint -Uri $Path -SasToken $SasToken -CollectionName $signature.CollectionName -InstanceName $signature.InstanceName;

            # Generate the request
            return ReadBuildSignatureWeb -Uri $endpointUri -Verbose:$VerbosePreference;

        } elseif ($Path -like "http://") {

            # Error if a HTTP endpoint is used
            Write-Error -Message $LocalizedData.HttpNotSupported -Category InvalidOperation;
        } else {

            # Get the file path
            $filePath = Join-Path -Path $Path -ChildPath "$CollectionName.$InstanceName.json";
            
            # Check if the file exists
            if (!(Test-Path -Path $filePath)) {
                return $Null;
            }
            
            # Read the file from the specific location
            return [DevOpsKitDsc.Build.SignatureHelper]::LoadFrom($filePath);
        }
    }
}

function ReadBuildSignatureWeb {

    [CmdletBinding()]
    [OutputType([String])]
    param (
        [Parameter(Mandatory = $True)]
        [System.Uri]$Uri
    )

    process {

        $requestParams = @{
            Uri = $Uri
        }

        if ($Uri.Host -like '*.blob.core.windows.net') {
            $requestParams['Headers'] = @{
                'x-ms-version' = '2017-04-17'
                'x-ms-blob-type' = 'BlockBlob'
            }
        }

        try {
            $response = Invoke-RestMethod @requestParams -UseBasicParsing -Method Get;

            return $response;
        }
        catch {

            if ($_.Exception.Response.StatusCode -eq [System.Net.HttpStatusCode]::NotFound) {
                return $Null;
            } else {
                Write-Error -Exception $_.Exception -ErrorAction Stop;
            }
        }
    }
}

function WriteBuildSignature {

    [CmdletBinding()]
    [OutputType([void])]
    param (
        [Parameter(Mandatory = $True)]
        [String]$Path,

        [Parameter(Mandatory = $False)]
        [String]$SasToken,

        [Parameter(Mandatory = $True)]
        [DevOpsKitDsc.Build.BuildSignature]$Signature
    )

    process {

        if ($Path -like "https://*") {

            # Handle writing signatures to a HTTPS endpoint

            # Get the endpoint URI
            $endpointUri = GetSignatureEndpoint -Uri $Path -SasToken $SasToken -CollectionName $signature.CollectionName -InstanceName $signature.InstanceName;

            # Generate the request
            WriteBuildSignatureWeb -Uri $endpointUri -Value $Signature.BuildIntegrity;

        } elseif ($Path -like "http://") {

            # Error if a HTTP endpoint is used
            Write-Error -Message $LocalizedData.HttpNotSupported -Category InvalidOperation;
        } else {

            if (!(Test-Path -Path $Path)) {
                New-Item -Path $Path -ItemType Directory -Force | Out-Null;
            }
    
            $filePath = Join-Path -Path $Path -ChildPath "$($signature.CollectionName).$($signature.InstanceName).json";
    
            [DevOpsKitDsc.Build.SignatureHelper]::SaveTo($filePath, $Signature);
        }
    }
}

function WriteBuildSignatureWeb {

    [CmdletBinding()]
    [OutputType([void])]
    param (
        [Parameter(Mandatory = $True)]
        [System.Uri]$Uri,

        [Parameter(Mandatory = $True)]
        [String]$Value
    )

    process {

        $requestParams = @{
            Uri = $Uri
        }

        if ($Uri.Host -like '*.blob.core.windows.net') {
            $requestParams['Headers'] = @{
                'x-ms-version' = '2017-04-17'
                'x-ms-blob-type' = 'BlockBlob'
                'x-ms-date' = [DateTime]::UtcNow.ToString('yyyy-MM-dd')
            }
        }

        try {
            $response = Invoke-RestMethod @requestParams -UseBasicParsing -Method Put -Body $Value;
        }
        catch {

            if ($_.Exception.Response.StatusCode -eq [System.Net.HttpStatusCode]::NotFound) {
                return $Null;
            } else {
                Write-Error -Exception $_.Exception -ErrorAction Stop;
            }
        }
    }
}

function GetSignatureEndpoint {

    [CmdletBinding()]
    [OutputType([System.Uri])]
    param (
        [Parameter(Mandatory = $True)]
        [String]$Uri,

        [Parameter(Mandatory = $False)]
        [String]$SasToken,

        [Parameter(Mandatory = $True)]
        [String]$CollectionName,

        [Parameter(Mandatory = $True)]
        [String]$InstanceName
    )

    process {

        # Generate the endpoint uri based on base uri, collection and instance parameters
        $result = New-Object -TypeName System.Uri -ArgumentList ([String]::Concat($Uri, $CollectionName, '/', $InstanceName, '.json', $SasToken));

        return $result;
    }
}

function ImportNodeData {

    [CmdletBinding()]
    [OutputType([PSObject[]])]
    param (
        [Parameter(Mandatory = $True)]
        [String]$WorkspacePath,

        [Parameter(Mandatory = $True)]
        [String[]]$NodePath,

        [Parameter(Mandatory = $False)]
        [AllowEmptyCollection()]
        [AllowNull()]
        [String[]]$InstanceName = $Null
    )

    process {

        Write-Verbose -Message "[DOKDsc][$dokOperation] -- $($LocalizedData.ImportNodeData -f $NodePath)";

        foreach ($path in $NodePath) {

            # Check if the path exists
            if (!(Test-Path -Path $path)) {

                # if ($PSBoundParameters.ContainsKey('InstanceName')) {
                # Write-Error -Message ($LocalizedData.ErrorMissingNodeData -f $NodePath) -Category ObjectNotFound;
                # }

                # No node data to process
                # return $Null;
            }

            $pathFilter = GetWorkspacePath -WorkspacePath $WorkspacePath -Path $path -ChildPath '/';

            if (!(Test-Path -Path $pathFilter))
            {
                Write-Warning -Message "Node path $path does not exist";

                continue;
            }

            $result = Get-ChildItem -Path $pathFilter -File | Where-Object -FilterScript {
                ($Null -eq $InstanceName -or $InstanceName.Count -eq 0) -or $InstanceName -contains $_.BaseName
            } | ForEach-Object -Process {
                $item = $_;

                $dataFilePath = $item.FullName;

                Write-Verbose -Message "[DOKDsc][$dokOperation] -- $($LocalizedData.FoundNodeData -f $dataFilePath)";

                if ($item.Extension -eq '.psd1') {

                    # Process .psd1 file
                    ReadPSNodeData -Path $dataFilePath -Verbose:$VerbosePreference;
                }
                elseif ($item.Extension -eq '.json') {

                    # Process .json file
                    ReadJsonNodeData -Path $dataFilePath -Verbose:$VerbosePreference;
                }
            }

            $result;
        }
    }
}

# Read node data from a .psd1 file.
function ReadPSNodeData {

    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $True)]
        [String]$Path
    )

    process {
        Write-Verbose -Message "[DOKDsc][$dokOperation] -- Reading node data from $Path";
        
        $baseDirectory = Split-Path -Path $Path -Parent;
        $fileName = Split-Path -Path $Path -Leaf;

        $results = @{ };

        # Read the .psd1 file
        Import-LocalizedData -BaseDirectory $baseDirectory -FileName $fileName -BindingVariable psdFile;

        # Detect if compatible format should be used.
        if ($psdFile.ContainsKey('AllNodes')) {

            foreach ($node in $psdFile.AllNodes) {

                # Create result object
                $result = New-Object -TypeName PSObject -Property @{
                    InstanceName = $node.NodeName;
                    BaseDirectory = $baseDirectory;
                    ConfigurationData = @{ AllNodes = @($node); };
                }

                $results.Add($result.InstanceName, $result);
            }
        } else {

            # Create result object
            $result = New-Object -TypeName PSObject -Property @{
                InstanceName = $psdFile.NodeName;
                BaseDirectory = $baseDirectory;
                ConfigurationData = $psdFile;
            }

            $results.Add($result.InstanceName, $result);
        }

        # Emit result objects to pipeline
        $results.Values;
    }
}

# Read node data from a .json file.
function ReadJsonNodeData {

    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $True)]
        [String]$Path
    )

    process {

        Write-Verbose -Message "[DOKDsc][$dokOperation] -- Reading node data from $Path";

        $baseDirectory = Split-Path -Path $Path -Parent;

        $results = @{ };
        
        # Convert object properties to a hashtable
        function ObjectToHashtable {

            param (
                [Parameter(Mandatory = $True, ValueFromPipeline = $True)]
                [PSObject]$InputObject
            )

            process {
                $result = @{ };

                # Process each property
                $InputObject.PSObject.Properties.GetEnumerator() | ForEach-Object -Process {

                    if ($_.Value -is [Object[]] -and $_.Value[0] -is [String]) {
                        $result[$_.Name] = $_.Value;
                    } elseif ($_.Value -is [Object[]]) {
                        $result[$_.Name] = $_.Value | ObjectToHashtable;
                    } elseif ($_.Value -is [PSCustomObject]) {
                        $result[$_.Name] = ObjectToHashtable -InputObject $_.Value;
                    } else {
                        $result[$_.Name] = $_.Value;
                    }
                }

                $result;
            }
        }
        
        # Read the .json file in as a hashtable
        $jsonFile = Get-Content -Path $Path | ConvertFrom-Json | ObjectToHashtable;

        # Detect if compatible format should be used.
        if ($jsonFile.ContainsKey('AllNodes')) {

            foreach ($node in $jsonFile.AllNodes) {

                # Create result object
                $result = New-Object -TypeName PSObject -Property @{
                    InstanceName = $node.NodeName;
                    BaseDirectory = $baseDirectory;
                    ConfigurationData = @{ AllNodes = @($node); };
                }

                $results.Add($result.InstanceName, $result);
            }
        } else {

            # Create result object
            $result = New-Object -TypeName PSObject -Property @{
                InstanceName = $jsonFile.NodeName;
                BaseDirectory = $baseDirectory;
                ConfigurationData = $jsonFile;
            }

            $results.Add($result.InstanceName, $result);
        }

        # Emit result objects to pipeline
        $results.Values;
    }
}

# Merge encryption certificate data
function MergeNodeCertificate {

    [CmdletBinding()]
    [OutputType([PSObject])]
    param (
        [Parameter(Mandatory = $True, ValueFromPipeline = $True)]
        [PSObject]$InputObject,

        [Parameter(Mandatory = $True)]
        [String]$Path,

        [Parameter(Mandatory = $True)]
        [String]$InstanceName,

        [Parameter(Mandatory = $False)]
        [Switch]$PassThru = $False
    )

    process {
        
        Write-Verbose -Message "[DOKDsc][$dokOperation]`t-- Merging node public key: $InstanceName";

        $certificateFile = Join-Path -Path $Path -ChildPath "$InstanceName.cer";

        if (Test-Path -Path $certificateFile) {
            $InputObject.ConfigurationData.AllNodes[0]['CertificateFile'] = $certificateFile;
            $InputObject.ConfigurationData.AllNodes[0]['Thumbprint'] = (ExtractCertificateThumbprint -Path $certificateFile -Verbose:$VerbosePreference);

            Write-Verbose -Message "Using certificate: $($InputObject.ConfigurationData.AllNodes[0]['CertificateFile'])";
            Write-Verbose -Message "Using thumbprint: $($InputObject.ConfigurationData.AllNodes[0]['Thumbprint'])";
        } else {
            Write-Warning -Message ($LocalizedData.NoEncryptionCertificate -f $InstanceName);
        }

        if ($PassThru) {
            return $InputObject;
        }
    }
}

function MergeConfiguration {

    [CmdletBinding()]
    [OutputType([PSObject])]
    param (
        [Parameter(Mandatory = $True, ValueFromPipeline = $True)]
        [PSObject]$InputObject,

        [Parameter(Mandatory = $True)]
        [DevOpsKitDsc.Workspace.Collection]$Collection,

        [Parameter(Mandatory = $False)]
        [Switch]$PassThru = $False
    )

    begin {
        Write-Verbose -Message "[DOKDsc][MergeConfiguration] BEGIN::";
    }

    process {

        # Check if any configuration data was specified
        if ($Null -ne $Collection.Data -and $Collection.Data.Count -gt 0) {

            # Process each key value pair
            foreach ($kv in $Collection.Data.GetEnumerator()) {

                # Only replace the configuration data from the node if ReplaceNodeData = true, or the key didn't exist
                if ($Null -eq $Collection.Options -or $Collection.Options.ReplaceNodeData -or !$InputObject.ConfigurationData.AllNodes[0].ContainsKey($kv.Key)) {

                    Write-Verbose -Message "[DOKDsc][MergeConfiguration] -- Setting $($kv.Key): $($kv.Value)";

                    $InputObject.ConfigurationData.AllNodes[0][$kv.Key] = $kv.Value;
                }
            }
        }

        if ($PassThru) {
            return $InputObject;
        }
    }

    end {
        Write-Verbose -Message "[DOKDsc][MergeConfiguration] END::";
    }
}

function ExtractCertificateThumbprint {

    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $True)]
        [String]$Path
    )

    process {

        Write-Verbose -Message "Extracting for $Path";


        $certificate = New-Object -TypeName System.Security.Cryptography.X509Certificates.X509Certificate2 -ArgumentList @($Path);

        return $certificate.Thumbprint;
    }
}

function CreatePath {

    [CmdletBinding()]
    [OutputType([System.String])]
    param (
        [Parameter(Mandatory = $True)]
        [String]$Path,

        [Switch]$File = $False,

        [Parameter(Mandatory = $False)]
        [Switch]$PassThru = $False
    )

    process {

        if ($File) {
            $Path = Split-Path -Path $Path -Parent;
        }

        if (!(Test-Path -Path $Path)) {
            New-Item -Path $Path -ItemType Directory -Force | Out-Null;
        }

        if ($PassThru) {
            $result = (Get-Item -Path $Path).FullName;

            return $result;
        }
    }
}

function PublishConfiguration {

    [CmdletBinding()]
    [OutputType([void])]
    param (
        [Parameter(Mandatory = $True)]
        [String]$Name,

        [Parameter(Mandatory = $True)]
        [String]$Path,

        [Parameter(Mandatory = $True)]
        [String]$OutputPath,

        [Parameter(Mandatory = $True)]
        [DevOpsKitDsc.Workspace.Collection]$Collection
    )

    process {


        if (!(Test-Path -Path $Path)) {
            Write-Error -Message "The specified configuration does not exist.";

            return;
        }

        $collectionOutput = Join-Path -Path $OutputPath -ChildPath $Name;

        if (!(Test-Path -Path $collectionOutput)) {
            New-Item -Path $collectionOutput -ItemType Directory -Force | Out-Null;
        }

        Copy-Item -LiteralPath $Path -Destination $collectionOutput -Force;

        if ($Null -ne $Collection.Options -and $Collection.Options.Target -eq [DevOpsKitDsc.Workspace.ConfigurationOptionTarget]::AzureDscExtension) {
            $publishName = $Name;

            # Get the full path to the package file
            $publishFilePath = Join-Path -Path $collectionOutput -ChildPath $publishName;

            Compress-Archive -Path "$collectionOutput\*" -DestinationPath $publishFilePath -Verbose:$VerbosePreference -Force;

            # Clean up other files
            Remove-Item -Path $collectionOutput -Exclude *.zip -Force -Recurse;
        }
    }
}

function PublishModule {

    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $True)]
        [DevOpsKitDsc.Workspace.Module]$Module,

        [Parameter(Mandatory = $True)]
        [String]$OutputPath,

        [Parameter(Mandatory = $False)]
        [DevOpsKitDsc.Workspace.ConfigurationOptionTarget]$Target = [DevOpsKitDsc.Workspace.ConfigurationOptionTarget]::FileSystem
    )

    process {

        $publishName = "$($Module.ModuleName)_$($Module.ModuleVersion).zip";

        if ($Target -eq [DevOpsKitDsc.Workspace.ConfigurationOptionTarget]::AzureAutomationService) {
            $publishName = "$($Module.ModuleName).zip";
        }

        # Get the full path to the package file
        $publishFilePath = Join-Path -Path $OutputPath -ChildPath $publishName;

        # Get the directory that the package file will be created in
        $publishPath = Split-Path -Path $publishFilePath -Parent;

        # Create the publish directory
        if (!(Test-Path -Path $publishPath)) {
            New-Item -Path $publishPath -ItemType Directory -Force | Out-Null;
        }

        # Compress the module
        Compress-Archive -Path "$($Module.Path)\*" -DestinationPath $publishFilePath -Verbose:$VerbosePreference -Force;
    }
}

function GetModule {

    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $True)]
        [String]$WorkspacePath,
        
        [Parameter(Mandatory = $True)]
        [DevOpsKitDsc.Workspace.WorkspaceSetting]$Workspace
    )

    process {

        $modulesPath = GetWorkspacePath -WorkspacePath $WorkspacePath -Path $Workspace.Options.ModulePath;

        foreach ($m in $Workspace.Modules) {

            if ([String]::IsNullOrEmpty($m.Path)) {
                # Calculate and updates the module path
                $m.Path = "$modulesPath\$($m.ModuleName)\$($m.ModuleVersion)";
            } else {
                $m.Path = GetWorkspacePath -WorkspacePath $WorkspacePath -Path $m.Path;
            }

            # Emit the module back to the pipeline
            $m;
        }
    }
}

function BuildDocumentation {

    [CmdletBinding()]
    [OutputType([void])]
    param (
        [Parameter(Mandatory = $True)]
        [String]$WorkspacePath,

        [Parameter(Mandatory = $True)]
        [DevOpsKitDsc.Workspace.Collection]$Collection,

        # The path to the .mof file
        [Parameter(Mandatory = $True)]
        [String]$Path,

        # The output path to store documentaion
        [Parameter(Mandatory = $True)]
        [String]$OutputPath
    )

    begin {
        Write-Verbose -Message "[DOKDsc][$dokOperation][Doc]::BEGIN";
    }

    process {

        # Only generate documentation if a template has been set
        if ($Null -eq $Collection.Docs -or [String]::IsNullOrEmpty($Collection.Docs.Path)) {
            Write-Verbose -Message "[DOKDsc][$dokOperation][Doc] -- Skipping documentation, template not set";

            return;
        }

        $templatePath = GetWorkspacePath -WorkspacePath $WorkspacePath -Path $Collection.Docs.Path;

        Invoke-DscNodeDocument -DocumentName $Collection.Docs.Name -Script $templatePath -Path $Path -OutputPath $OutputPath -Verbose:$VerbosePreference;

        # Write-Verbose -Message "[DOKDsc][$dokOperation] -- Update TOC: $($buildResult.FullName)";

        # Update TOC
        UpdateToc -OutputPath $OutputPath -Verbose:$VerbosePreference;
    }

    end {
        Write-Verbose -Message "[DOKDsc][$dokOperation][Doc]::END";
    }
}

function UpdateToc {

    [CmdletBinding()]
    [OutputType([void])]
    param (
        [Parameter(Mandatory = $True)]
        [String]$OutputPath
    )

    begin {

        Write-Verbose -Message "[DOKDsc][$dokOperation][Toc]::BEGIN";

        # Get markdown content files
        $contentFilePath = @((Get-ChildItem -Path $OutputPath -File | Where-Object -FilterScript {
            $_.FullName -like '*.md' -and $_.Name -ne 'TOC.md'
        }).FullName);

        $toc = @('# Nodes');

        # Read each file and extract metadata
        foreach ($file in $contentFilePath) {
            $contentHeader = ReadYamlHeader -Path $file -Verbose:$VerbosePreference;

            $href = Split-Path -Path $file -Leaf;
            $title = $href -replace '\.md', '';

            if ($Null -ne $contentHeader -and $contentHeader.ContainsKey('title')) {
                $title = $contentHeader.title;
            }

            $toc += "## [$title]($href)";
        }

        $toc | Set-Content -Path "$OutputPath\TOC.md";

        Write-Verbose -Message "[DOKDsc][$dokOperation][Toc]::END";
    }
}

function ReadYamlHeader {

    [CmdletBinding()]
    [OutputType([System.Collections.Hashtable])]
    param (
        [Parameter(Mandatory = $True)]
        [String]$Path
    )

    process {

        # Read the file
        $content = Get-Content -Path $Path -Raw;

        # Detect Yaml header
        if (![String]::IsNullOrEmpty($content) -and $content -match '^(---\r\n(?<yaml>([A-Z0-9]{1,}:[A-Z0-9 ]{1,}(\r\n){0,}){1,})\r\n---\r\n)') {

            Write-Verbose -Message "[DscDocs][Toc]`t-- Reading Yaml header: $Path";

            # Extract yaml header key value pair
            [String[]]$yamlHeader = $Matches.yaml -split "`n";

            $result = @{ };

            # Read key values into hashtable
            foreach ($item in $yamlHeader) {
                $kv = $item.Split(':', 2, [System.StringSplitOptions]::RemoveEmptyEntries);

                Write-Debug -Message "Found yaml keypair from: $item";

                if ($kv.Length -eq 2) {
                    $result[$kv[0].Trim()] = $kv[1].Trim();
                }
            }

            # Emit result to the pipeline
            return $result;
        }
    }
}

function BuildSite {

    [CmdletBinding()]
    [OutputType([void])]
    param (
        [Parameter(Mandatory = $True)]
        [String]$Path
    )

    process {

        docfx.exe build $Path;
    }
}

function HasWorkspaceSetting {

    [CmdletBinding()]
    [OutputType([DevOpsKitDsc.Workspace.WorkspaceSetting])]
    param (
        [Parameter(Mandatory = $True)]
        [String]$WorkspacePath
    )

    process {
        $settingsPath = Join-Path -Path $WorkspacePath -ChildPath '\.dokd\settings.json';
        
        return (Test-Path -Path $settingsPath); 
    }
}

function ReadWorkspaceSetting {

    [CmdletBinding()]
    [OutputType([DevOpsKitDsc.Workspace.WorkspaceSetting])]
    param (
        [Parameter(Mandatory = $True)]
        [String]$WorkspacePath
    )

    process {

        $settingsPath = Join-Path -Path $WorkspacePath -ChildPath '\.dokd\settings.json';

        if (!(Test-Path -Path $settingsPath)) {
            return [DevOpsKitDsc.Workspace.WorkspaceHelper]::LoadDefault();
        }

        $settingsPath = Resolve-Path -Path $settingsPath;

        Write-Verbose -Message "[DOKDsc][$dokOperation] -- Loading workspace from: $WorkspacePath";

        $result = [DevOpsKitDsc.Workspace.WorkspaceHelper]::LoadFrom($settingsPath);

        if ($Null -eq $result) {
            return [DevOpsKitDsc.Workspace.WorkspaceHelper]::LoadDefault();
        }

        return $result;
    }
}

function WriteWorkspaceSetting {

    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $True)]
        [String]$WorkspacePath,

        [Parameter(Mandatory = $False)]
        [DevOpsKitDsc.Workspace.WorkspaceSetting]$InputObject = [DevOpsKitDsc.Workspace.WorkspaceHelper]::LoadDefault()
    )

    process {

        $dokdPath = Join-Path -Path $WorkspacePath -ChildPath '\.dokd';

        $settingsPath = Join-Path -Path $dokdPath -ChildPath '\settings.json';

        if (!(Test-Path -Path $dokdPath)) {
            New-Item -Path $dokdPath -ItemType Directory -Force | Out-Null;
        }

        [DevOpsKitDsc.Workspace.WorkspaceHelper]::SaveTo($settingsPath, $InputObject);
    }
}

function AddModuleToWorkspace {

    [CmdletBinding()]
    [OutputType([System.Boolean])]
    param (
        [Parameter(Mandatory = $True)]
        [DevOpsKitDsc.Workspace.WorkspaceSetting]$Setting,

        [Parameter(Mandatory = $True)]
        [String]$ModuleName,

        [Parameter(Mandatory = $True)]
        [String]$ModuleVersion,

        [Parameter(Mandatory = $False)]
        [String]$Repository,

        [Parameter(Mandatory = $False)]
        [String]$Path,

        [Parameter(Mandatory = $False)]
        [String]$Type
    )

    process {


        $moduleObject = New-Object -TypeName DevOpsKitDsc.Workspace.Module -Property @{
            ModuleName = $ModuleName
            ModuleVersion = $ModuleVersion
        };

        if ($PSBoundParameters.ContainsKey('Repository')) {
            $moduleObject.Repository = $Repository;
        }

        if ($PSBoundParameters.ContainsKey('Path')) {
            $moduleObject.Path = $Path;
        }

        if ($PSBoundParameters.ContainsKey('Type')) {
            $moduleObject.Type = $TYpe;
        }

        $Setting.Modules.Add($moduleObject);

        return $True;
    }
}

function BuildModuleCache {

    [CmdletBinding()]
    param (

    )

    process {
        Write-Verbose -Message "[DOKDsc][$dokOperation] -- Building module cache";

        $result = New-Object -TypeName 'System.Collections.Generic.Dictionary[[string], [PSObject]]' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase);

        # Get modules in memory first
        foreach ($m in (Get-Module)) {

            $moduleId = "$($m.Name).$($m.Version)";

            $result[$moduleId] = New-Object -TypeName PSObject -Property @{
                Name = $m.Name;
                Version = $m.Version;
                Path = $m.Path;
                IsLoaded = $True;
            }
        }

        # Get all available modules
        foreach ($m in (Get-Module -ListAvailable)) {

            $moduleId = "$($m.Name).$($m.Version)";

            # Ignore modules that are already loaded in memory
            if (!$result.ContainsKey($moduleId)) {
                $result[$moduleId] = New-Object -TypeName PSObject -Property @{
                    Name = $m.Name;
                    Version = $m.Version;
                    Path = $m.Path;
                    IsLoaded = $False;
                }
            }
        }

        return $result;
    }
}

function ImportModule {

    [CmdletBinding()]
    [OutputType([void])]
    param (
        [Parameter(Mandatory = $False)]
        [DevOpsKitDsc.Workspace.Module[]]$Module,

        [Parameter(Mandatory = $True)]
        [String]$OutputPath
    )

    process {

        # Skip if there are not any module dependencies added to the workspace
        if ($Null -eq $Module -or $Module.Length -eq 0) {
            return;
        }

        foreach ($m in $Module) {
            
            # Determine if the module is installed on the system

            $moduleId = "$($m.ModuleName).$($m.ModuleVersion)";

            if ($availableModules.ContainsKey($moduleId)) {

                if (!$availableModules[$moduleId].IsLoaded) {
                    Write-Verbose -Message "[DOKDsc][$dokOperation] -- Importing $($m.ModuleName) v$($m.ModuleVersion)";

                    Import-Module -Name $m.ModuleName -RequiredVersion $m.ModuleVersion;# -Force;
                }
                else {
                    Write-Verbose -Message "[DOKDsc][$dokOperation] -- Already loaded. Skipping $($m.ModuleName) v$($m.ModuleVersion)";
                }
            }
            else {
                RestoreModule -Module $m -OutputPath $OutputPath -Verbose:$VerbosePreference;
            }
        }
    }
}

function RestoreModule {

    [CmdletBinding()]
    [OutputType([void])]
    param (
        [Parameter(Mandatory = $True)]
        [DevOpsKitDsc.Workspace.Module]$Module,

        [Parameter(Mandatory = $True)]
        [String]$OutputPath
    )

    process {

        Write-Verbose -Message "[DOKDsc][$dokOperation] -- Restoring module: $Module";

        # Check if the module has already been saved to the modules path
        $moduleLocation = Join-Path -Path $OutputPath -ChildPath "$($Module.ModuleName)\$($Module.ModuleVersion)";

        if (!(Test-Path -Path "$moduleLocation\$($Module.ModuleName).ps*" -Include '*.psm1', '*.psd1')) {
            
            try {
                SaveModule -Module $Module -OutputPath $OutputPath -ErrorAction Stop;
            }
            catch {
                Write-Error -Message ($LocalizedData.ModuleRestoreError -f $Module.ModuleName, $_.Exception.Message) -Exception $_.Exception -Category ObjectNotFound;
            }
            
        } else {
            Write-Verbose -Message "[DOKDsc][$dokOperation] -- Module already exists: $Module";
        }
    }
}

function SaveModule {

    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $True)]
        [DevOpsKitDsc.Workspace.Module]$Module,

        [Parameter(Mandatory = $True)]
        [String]$OutputPath
    )

    process {

        $moduleFilter = @{ Name = $Module.ModuleName; RequiredVersion = $Module.ModuleVersion; Path = $OutputPath; };

        if (![String]::IsNullOrEmpty($Module.Repository)) {
            $moduleFilter.Add('Repository', $Module.Repository);
        }

        Save-Module @moduleFilter -ErrorAction Stop;
    }
}

# Compress a specific module for either a Pull Server or Azure Automation Service
function PackageModule {

    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $True)]
        [DevOpsKitDsc.Workspace.Module]$Module,

        [Parameter(Mandatory = $True)]
        [ValidateSet('AzureAutomationService')]
        [String]$Format,

        [Parameter(Mandatory = $True)]
        [String]$ModulePath
    )

    begin {
        Write-Verbose -Message "[DOKDsc][PackageModule]`tBEGIN::";
    }

    process {

        Write-Verbose -Message "[DOKDsc][PackageModule] -- Packaging module: $Module";

        if ($Format -eq 'AzureAutomationService') {
            
            

        }
    }

    end {
        Write-Verbose -Message "[DOKDsc][PackageModule]`tEND::";
    }
}

function GetWorkspacePath {

    [CmdletBinding()]
    [OutputType([String])]
    param (
        [Parameter(Mandatory = $True)]
        [String]$WorkspacePath,

        [Parameter(Mandatory = $False)]
        [String]$Path,

        [Parameter(Mandatory = $False)]
        [String]$ChildPath,

        [Parameter(Mandatory = $False)]
        [Switch]$Relative = $False
    )

    process {

        $result = $Path;

        $WorkspacePath = Resolve-Path -Path $WorkspacePath;

        if (![System.IO.Path]::IsPathRooted($Path)) {
            $result = Join-Path -Path $WorkspacePath -ChildPath $Path;
        }

        if ($PSBoundParameters.ContainsKey('ChildPath') -and ![String]::IsNullOrEmpty($ChildPath)) {
            $result = Join-Path -Path $result -ChildPath $ChildPath;
        }

        # Make the path relative to workspace path
        if ($Relative) {
            
            if ($result.Contains($WorkspacePath)) {
                
                $result = $result.Replace($WorkspacePath, '.');
            }

        }

        return $result;
    }
}

function CopyTemplate {

    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $True)]
        [String]$Name,

        [Parameter(Mandatory = $True)]
        [String]$Path
    )

    process {

        $templateFilePath = Join-Path -Path $PSScriptRoot -ChildPath "Templates\$Name";

        CreatePath -Path $configurationPath -File;

        Set-Content -Path $configurationPath -Value (Get-Content -Path $templateFilePath);
    }
}

function GetDefaultConfigurationPath {

    [CmdletBinding()]
    [OutputType([String])]
    param (
        [Parameter(Mandatory = $True)]
        [String]$WorkspacePath,

        [Parameter(Mandatory = $True)]
        [DevOpsKitDsc.Workspace.WorkspaceSetting]$Setting,

        [Parameter(Mandatory = $True)]
        [String]$ConfigurationName
    )

    process {

        return GetWorkspacePath -WorkspacePath $WorkspacePath -Path "src\Configuration\$ConfigurationName.ps1";
    }
}

# Get collection based on name filter.
function GetCollection {

    [CmdletBinding()]
    [OutputType([DevOpsKitDsc.Workspace.Collection])]
    param (
        [Parameter(Mandatory = $True)]
        [DevOpsKitDsc.Workspace.WorkspaceSetting]$Setting,

        [Parameter(Mandatory = $False)]
        [String[]]$Name
    )

    process {

        $Setting.Collections | Where-Object -FilterScript {
            (!$PSBoundParameters.ContainsKey('Name') -or $_.Name -in $Name)
        };
    }
}

#endregion Helper functions

#
# Export module
#

New-Alias -Name 'dokd-init' -Value 'Initialize-DOKDsc' -ErrorAction SilentlyContinue;
New-Alias -Name 'dokd-restore' -Value 'Restore-DOKDscModule' -ErrorAction SilentlyContinue;
New-Alias -Name 'dokd-new' -Value 'New-DOKDscCollection' -ErrorAction SilentlyContinue;
New-Alias -Name 'dokd-build' -Value 'Invoke-DOKDscBuild' -ErrorAction SilentlyContinue;

Export-ModuleMember -Alias @(
    'dokd-init'
    'dokd-restore'
    'dokd-new'
    'dokd-build'
);

Export-ModuleMember -Function @(
    'Initialize-DOKDsc'
    'Import-DOKDscNodeConfiguration'
    'Register-DOKDscNode'
    'Invoke-DOKDscBuild'
    'Publish-DOKDscCollection'
    'New-DOKDscCollection'
    'Get-DOKDscCollection'
    'Set-DOKDscCollectionOption'
    'Import-DOKDscWorkspaceSetting'
    'Set-DOKDscWorkspaceOption'
    'Get-DOKDscWorkspaceOption'
    'Add-DOKDscModule'
    'Get-DOKDscModule'
    'Publish-DOKDscModule'
    'Restore-DOKDscModule'
);

# EOM