Functions/GenXdev.AI.DeepStack/EnsureDeepStack.ps1

################################################################################
<#
.SYNOPSIS
Ensures DeepStack face recognition service is installed and running.
 
.DESCRIPTION
This function sets up and manages DeepStack face recognition service using
Docker. It ensures that Docker Desktop is installed, pulls the DeepStack Docker
image, and runs the service in a container with persistent storage for
registered faces.
 
DeepStack provides a simple REST API for face detection, registration, and
recognition that is well-documented and actively maintained.
 
.PARAMETER ContainerName
The name for the Docker container. Default: "deepstack_face_recognition"
 
.PARAMETER VolumeName
The name for the Docker volume for persistent storage.
Default: "deepstack_face_data"
 
.PARAMETER ServicePort
The port number for the DeepStack service. Default: 5000
 
.PARAMETER HealthCheckTimeout
Maximum time in seconds to wait for service health check. Default: 60
 
.PARAMETER HealthCheckInterval
Interval in seconds between health check attempts. Default: 3
 
.PARAMETER ImageName
Custom Docker image name to use. If not specified, uses
deepquestai/deepstack:latest or deepquestai/deepstack:gpu based on UseGPU
parameter.
 
.PARAMETER FacesPath
The path inside the container where faces are stored. Default: "/datastore"
 
.PARAMETER Force
If specified, forces rebuilding of Docker container and removes existing data.
This will remove existing containers and volumes, pull latest DeepStack image,
create a fresh container with clean data, and clear all registered faces.
 
.PARAMETER UseGPU
If specified, uses the GPU-accelerated version of DeepStack (requires NVIDIA
GPU).
 
.EXAMPLE
EnsureDeepStack
 
.EXAMPLE
EnsureDeepStack -ContainerName "my_deepstack" -ServicePort 8080
 
.EXAMPLE
EnsureDeepStack -VolumeName "custom_face_data" -HealthCheckTimeout 120
 
.EXAMPLE
EnsureDeepStack -FacesPath "/custom/faces/path"
 
.EXAMPLE
EnsureDeepStack -Force -UseGPU
 
.NOTES
DeepStack Face Recognition API Endpoints:
- POST /v1/vision/face/register : Register known faces
- POST /v1/vision/face/list : List registered faces
- POST /v1/vision/face/recognize : Recognize faces in image
- POST /v1/vision/face/delete : Remove registered face
 
For more information, see: https://docs.deepstack.cc/face-recognition/
#>


function EnsureDeepStack {

    [CmdletBinding()]
    [OutputType([System.Boolean])]

    param(
        ###################################################################
        [Parameter(
            Position = 0,
            Mandatory = $false,
            HelpMessage = "The name for the Docker container"
        )]
        [ValidateNotNullOrEmpty()]
        [string] $ContainerName = "deepstack_face_recognition",
        ###################################################################
        [Parameter(
            Position = 1,
            Mandatory = $false,
            HelpMessage = "The name for the Docker volume for persistent storage"
        )]
        [ValidateNotNullOrEmpty()]
        [string] $VolumeName = "deepstack_face_data",
        ###################################################################
        [Parameter(
            Position = 2,
            Mandatory = $false,
            HelpMessage = "The port number for the DeepStack service"
        )]
        [ValidateRange(1, 65535)]
        [int] $ServicePort = 5000,
        ###################################################################
        [Parameter(
            Position = 3,
            Mandatory = $false,
            HelpMessage = ("Maximum time in seconds to wait for service " +
                          "health check")
        )]
        [ValidateRange(10, 300)]
        [int] $HealthCheckTimeout = 60,
        ###################################################################
        [Parameter(
            Position = 4,
            Mandatory = $false,
            HelpMessage = "Interval in seconds between health check attempts"
        )]
        [ValidateRange(1, 10)]
        [int] $HealthCheckInterval = 3,
        ###################################################################
        [Parameter(
            Position = 5,
            Mandatory = $false,
            HelpMessage = "Custom Docker image name to use"
        )]
        [ValidateNotNullOrEmpty()]
        [string] $ImageName,
        ###################################################################
        [Parameter(
            Position = 6,
            Mandatory = $false,
            HelpMessage = ("The path inside the container where faces are " +
                          "stored")
        )]
        [ValidateNotNullOrEmpty()]
        [string] $FacesPath = "/datastore",
        ###################################################################
        [Parameter(
            Mandatory = $false,
            HelpMessage = ("Force rebuild of Docker container and remove " +
                          "existing data")
        )]
        [Alias("ForceRebuild")]
        [switch] $Force,
        ###################################################################
        [Parameter(
            Mandatory = $false,
            HelpMessage = ("Use GPU-accelerated version (requires NVIDIA " +
                          "GPU)")
        )]
        [switch] $UseGPU
        ###################################################################
    )

    begin {

        # set script-scoped variables from parameters for container management
        $script:containerName = $ContainerName

        # determine appropriate docker image based on gpu usage preference
        $script:imageName = if ($ImageName) {
            $ImageName
        } elseif ($UseGPU) {
            "deepquestai/deepstack:gpu"
        } else {
            "deepquestai/deepstack:latest"
        }

        # set script-scoped variables for docker volume and networking
        $script:volumeName = $VolumeName

        # construct base api url for service health checks
        $script:apiBaseUrl = "http://127.0.0.1:$ServicePort"

        # set script-scoped variables for service configuration
        $script:servicePort = $ServicePort

        # set script-scoped variables for health monitoring
        $script:healthCheckTimeout = $HealthCheckTimeout

        $script:healthCheckInterval = $HealthCheckInterval

        # set script-scoped variable for persistent storage path
        $script:facesPath = $FacesPath
        # store original location for cleanup at the end of the function
        $script:originalLocation = `
            (Microsoft.PowerShell.Management\Get-Location).Path

        ###################################################################
        <#
        .SYNOPSIS
        Tests if Docker is available and responsive.
        #>

        function Test-DockerAvailability {

            try {

                # attempt to get docker version to verify docker is running
                $null = docker version --format "{{.Server.Version}}" 2>$null

                return $LASTEXITCODE -eq 0
            }
            catch {

                return $false
            }
        }
        ###################################################################

        ###################################################################
        <#
        .SYNOPSIS
        Tests if a Docker image exists.
        #>

        function Test-DockerImage {

            param([string]$ImageName)

            try {

                # query docker for existing images matching the specified name
                $images = docker images $ImageName --format "{{.Repository}}" 2>$null

                return -not [string]::IsNullOrWhiteSpace($images)
            }
            catch {

                return $false
            }
        }

        ###################################################################
        <#
        .SYNOPSIS
        Tests if a Docker container exists (running or stopped).
        #>

        function Test-DockerContainer {

            param([string]$ContainerName)

            try {

                # search for containers with exact name match including stopped ones
                $containers = docker ps -a --filter "name=^${ContainerName}$" `
                    --format "{{.ID}}" 2>$null

                return -not [string]::IsNullOrWhiteSpace($containers)
            }
            catch {

                return $false
            }
        }
        ###################################################################
        <#
        .SYNOPSIS
        Tests if a Docker container is running.
        #>

        function Test-DockerContainerRunning {

            param([string]$ContainerName)

            try {

                # search for running containers with exact name match
                $containers = docker ps --filter "name=^${ContainerName}$" `
                    --format "{{.ID}}" 2>$null

                return -not [string]::IsNullOrWhiteSpace($containers)
            }
            catch {

                return $false
            }
        }
        ###################################################################
        <#
        .SYNOPSIS
        Safely removes a Docker container.
        #>

        function Remove-DockerContainer {

            [CmdletBinding(SupportsShouldProcess)]
            param([string]$ContainerName)

            try {

                # check if container exists before attempting removal
                if (Test-DockerContainer $ContainerName) {

                    if ($PSCmdlet.ShouldProcess($ContainerName,
                        "Stop and remove Docker container")) {

                        # output verbose information about container removal
                        Microsoft.PowerShell.Utility\Write-Verbose `
                            "Stopping and removing container: $ContainerName"

                        # stop the container gracefully
                        $null = docker stop $ContainerName 2>$null

                        # remove the container completely
                        $null = docker rm $ContainerName 2>$null
                    }
                }
            }
            catch {

                # warn about container removal failures
                Microsoft.PowerShell.Utility\Write-Warning `
                    "Failed to remove container ${ContainerName}: $_"
            }
        }
        ###################################################################
        <#
        .SYNOPSIS
        Safely removes a Docker volume.
        #>

        function Remove-DockerVolume {

            [CmdletBinding(SupportsShouldProcess)]
            param([string]$VolumeName)

            try {

                if ($PSCmdlet.ShouldProcess($VolumeName, "Remove Docker volume")) {

                    # output verbose information about volume removal
                    Microsoft.PowerShell.Utility\Write-Verbose `
                        "Removing Docker volume: $VolumeName"

                    # remove the docker volume and discard output
                    $null = docker volume rm $VolumeName 2>$null
                }
            }
            catch {

                # warn about volume removal failures
                Microsoft.PowerShell.Utility\Write-Warning `
                    "Failed to remove volume ${VolumeName}: $_"
            }
        }
        ###################################################################
        <#
        .SYNOPSIS
        Tests if the DeepStack service is healthy by checking the API endpoint.
        #>

        function Test-ServiceHealth {

            try {

                # test the health endpoint with timeout to verify service responsiveness
                $null = Microsoft.PowerShell.Utility\Invoke-RestMethod `
                    -Uri "$script:apiBaseUrl/" `
                    -TimeoutSec 10 `
                    -ErrorAction Stop

                # log successful health check for debugging
                Microsoft.PowerShell.Utility\Write-Verbose `
                    "DeepStack service health check passed"

                return $true
            }
            catch {

                # log failed health check with error details for debugging
                Microsoft.PowerShell.Utility\Write-Verbose `
                    "DeepStack service health check failed: $_"

                return $false
            }
        }
        ###################################################################
        <#
        .SYNOPSIS
        Waits for the service to become healthy.
        #>

        function Wait-ServiceReady {

            # output verbose information about waiting for service readiness
            Microsoft.PowerShell.Utility\Write-Verbose `
                "Waiting for DeepStack service to become ready..."

            # initialize retry counter
            $retryCount = 0

            # calculate maximum retry attempts based on timeout and interval
            $maxRetries = [math]::Floor($script:healthCheckTimeout / `
                $script:healthCheckInterval)

            while ($retryCount -lt $maxRetries) {

                # test service health and return success if ready
                if (Test-ServiceHealth) {

                    # log successful service readiness
                    Microsoft.PowerShell.Utility\Write-Verbose `
                        "DeepStack service is ready and responding"

                    return $true
                }

                # increment retry counter for next attempt
                $retryCount++

                # output verbose information about retry attempt
                Microsoft.PowerShell.Utility\Write-Verbose `
                    "Service not ready yet, attempt $retryCount/$maxRetries..."

                # wait between health check attempts
                Microsoft.PowerShell.Utility\Start-Sleep `
                    -Seconds $script:healthCheckInterval
            }

            # warn about service readiness timeout
            Microsoft.PowerShell.Utility\Write-Warning `
                ("DeepStack service did not become ready within " +
                 "$script:healthCheckTimeout seconds")

            return $false
        }
        ###################################################################
        <#
        .SYNOPSIS
        Pulls the latest DeepStack Docker image.
        #>

        function Get-DeepStackImage {

            try {

                # output verbose information about docker image pull operation
                Microsoft.PowerShell.Utility\Write-Verbose `
                    "Pulling DeepStack image: $script:imageName"

                # pull the specified docker image
                $pullResult = docker pull $script:imageName 2>&1

                # check if docker pull command failed
                if ($LASTEXITCODE -ne 0) {

                    throw "Failed to pull DeepStack image: $pullResult"
                }

                # log successful image pull
                Microsoft.PowerShell.Utility\Write-Verbose `
                    "✅ DeepStack image pulled successfully"

                return $true
            }
            catch {

                # log error details for image pull failure
                Microsoft.PowerShell.Utility\Write-Error `
                    "Failed to pull DeepStack image: $_"

                return $false
            }
        }

        ###################################################################
        <#
        .SYNOPSIS
        Creates and starts a new DeepStack container.
        #>

        function New-DeepStackContainer {

            [CmdletBinding(SupportsShouldProcess)]
            param()

            try {

                # output verbose information about container creation
                Microsoft.PowerShell.Utility\Write-Verbose `
                    "Creating DeepStack container..."

                # check if docker volume already exists
                $volumeExists = docker volume ls `
                    --filter "name=^${script:volumeName}$" `
                    --format "{{.Name}}" 2>$null

                # create docker volume if it doesn't exist
                if ([string]::IsNullOrWhiteSpace($volumeExists)) {

                    # Use ShouldProcess to confirm volume creation
                    if ($PSCmdlet.ShouldProcess("$script:volumeName", "Create Docker volume")) {
                        # output verbose information about volume creation
                        Microsoft.PowerShell.Utility\Write-Verbose `
                            "Creating Docker volume: $script:volumeName"

                        # create the docker volume for persistent storage
                        $volumeResult = docker volume create $script:volumeName 2>&1

                        # check if volume creation failed
                        if ($LASTEXITCODE -ne 0) {

                            throw ("Failed to create Docker volume " +
                                  "$script:volumeName`: $volumeResult")
                        }
                    }
                }

                # prepare docker run arguments for container creation
                $dockerArgs = @(
                    "run", "-d"
                    "--name", $script:containerName
                    "-p", "$($script:servicePort):5000"
                    "-v", "$($script:volumeName):/datastore"
                    "-e", "VISION-FACE=True"
                    "--restart", "unless-stopped"
                )

                # add gpu support if requested
                if ($UseGPU) {

                    $dockerArgs += "--gpus", "all"
                }

                # add the docker image name as final argument
                $dockerArgs += $script:imageName                # output verbose information about docker command
                Microsoft.PowerShell.Utility\Write-Verbose `
                    "Docker command: docker $($dockerArgs -join ' ')"

                # Use ShouldProcess to confirm container creation
                if ($PSCmdlet.ShouldProcess("$script:containerName", "Create DeepStack container")) {
                    # execute docker run command to create container
                    $result = & docker @dockerArgs 2>&1

                    # check if container creation failed
                    if ($LASTEXITCODE -ne 0) {

                        throw "Failed to create container: $result"
                    }
                }

                # wait for container to initialize properly
                Microsoft.PowerShell.Utility\Start-Sleep -Seconds 5

                # log successful container creation
                Microsoft.PowerShell.Utility\Write-Verbose `
                    "✅ DeepStack container created successfully"

                return $true
            }
            catch {

                # log error details for container creation failure
                Microsoft.PowerShell.Utility\Write-Error `
                    "Failed to create DeepStack container: $_"

                return $false
            }
        }
        ###################################################################
    }

    process {

        try {

            # ensure docker desktop is available and running
            Microsoft.PowerShell.Utility\Write-Verbose `
                "Ensuring Docker Desktop is available..."

            GenXdev.AI\EnsureDockerDesktop

            # verify docker is responding to commands
            if (-not (Test-DockerAvailability)) {

                throw "Docker is not available or not responding"
            }

            # handle force cleanup if requested by user
            if ($Force) {

                # output verbose information about forced cleanup
                Microsoft.PowerShell.Utility\Write-Verbose `
                    "Force flag specified - cleaning up existing resources..."

                # remove existing container and volume for clean slate
                Remove-DockerContainer $script:containerName

                Remove-DockerVolume $script:volumeName
            }

            # ensure we have the latest deepstack image available
            if (-not (Test-DockerImage $script:imageName) -or $Force) {

                # pull the docker image if not present or force specified
                if (-not (Get-DeepStackImage)) {

                    throw "Failed to obtain DeepStack Docker image"
                }
            }
            else {

                # log that image is already available
                Microsoft.PowerShell.Utility\Write-Verbose `
                    "✅ DeepStack image already available"
            }

            # check current container state
            $containerExists = Test-DockerContainer $script:containerName

            $containerRunning = Test-DockerContainerRunning $script:containerName

            # handle existing container scenarios
            if ($containerExists) {

                # check if container is currently running
                if ($containerRunning) {

                    # verify container health for running container
                    if (Test-ServiceHealth) {

                        # log successful health check
                        Microsoft.PowerShell.Utility\Write-Verbose `
                            "✅ DeepStack container is healthy and responding"
                    }
                    else {

                        # restart unhealthy running container
                        Microsoft.PowerShell.Utility\Write-Verbose `
                            ("Container is running but not responding - " +
                             "restarting...")

                        # restart the container to fix health issues
                        $null = docker restart $script:containerName 2>$null

                        # wait for container to restart properly
                        Microsoft.PowerShell.Utility\Start-Sleep -Seconds 10

                        # wait for service to become ready after restart
                        $serviceReady = Wait-ServiceReady

                        # warn if service is not ready after restart
                        if (-not $serviceReady) {

                            Microsoft.PowerShell.Utility\Write-Warning `
                                ("DeepStack service may not be fully ready " +
                                 "after restart")
                        }
                    }
                }
                else {

                    # start existing stopped container
                    Microsoft.PowerShell.Utility\Write-Verbose `
                        "Starting existing container..."

                    # start the stopped container
                    $null = docker start $script:containerName 2>$null

                    # wait for container to start properly
                    Microsoft.PowerShell.Utility\Start-Sleep -Seconds 10

                    # wait for service to become ready after start
                    $serviceReady = Wait-ServiceReady

                    # log or warn about service readiness after start
                    if ($serviceReady) {

                        Microsoft.PowerShell.Utility\Write-Verbose `
                            "✅ DeepStack service is ready after container start"
                    }
                    else {

                        Microsoft.PowerShell.Utility\Write-Warning `
                            ("DeepStack service may not be fully ready " +
                             "after start")
                    }
                }
            }
            else {

                # create and start new container when none exists
                Microsoft.PowerShell.Utility\Write-Verbose `
                    "Creating and starting DeepStack container..."

                # attempt to create new deepstack container
                if (-not (New-DeepStackContainer)) {

                    throw "Failed to create DeepStack container"
                }

                # wait for service to be ready after creation
                Microsoft.PowerShell.Utility\Write-Verbose `
                    "Waiting for DeepStack service to be ready..."

                $serviceReady = Wait-ServiceReady

                # log or warn about service readiness after creation
                if ($serviceReady) {

                    Microsoft.PowerShell.Utility\Write-Verbose `
                        "✅ DeepStack service is ready at $script:apiBaseUrl"
                }
                else {

                    Microsoft.PowerShell.Utility\Write-Warning `
                        ("DeepStack service may not be fully ready " +
                         "after creation")
                }
            }

            # perform final validation of service state
            Microsoft.PowerShell.Utility\Write-Verbose `
                "Performing final validation..."

            # check if container is running and service is healthy
            if ((Test-DockerContainerRunning $script:containerName) -and `
                (Test-ServiceHealth)) {

                # log successful service operation
                Microsoft.PowerShell.Utility\Write-Verbose `
                    ("✅ DeepStack face recognition service is fully " +
                     "operational at $script:apiBaseUrl")

                return $true
            }
            else {

                # warn about potential service issues
                Microsoft.PowerShell.Utility\Write-Warning `
                    "DeepStack service may not be fully operational"

                return $false
            }
        }
        catch {

            # log error details for any failures
            Microsoft.PowerShell.Utility\Write-Error `
                "Failed to ensure DeepStack service: $_"

            throw
        }
    }

    end {

        # restore original location for cleanup
        Microsoft.PowerShell.Management\Set-Location $script:originalLocation
    }
}
################################################################################