Private/Seed.ps1

# Copies a tree of text files into the distro, relative path = destination
# (files\usr\local\bin\x -> /usr/local/bin/x). An existing link is replaced, not followed.
# PowerShell appends a newline to what it pipes: head -c -1 removes it.
function Copy-FileTree($m, [string]$Source) {
    if (-not (Test-Path -Path $Source)) { return }
    foreach ($file in Get-ChildItem -Path $Source -Recurse -File) {
        $dest = '/' + ([IO.Path]::GetRelativePath($Source, $file.FullName) -replace '\\', '/')
        $dir = $dest.Substring(0, $dest.LastIndexOf('/'))
        Get-Content -Path $file.FullName -Raw |
            Invoke-Root $m "mkdir -p '$dir' && rm -f '$dest' && tr -d '\r' | head -c -1 > '$dest'"
    }
}

# NoCloud seed, then a cloud-init rerun:
# 1. files\ and machines\<name>\files\ copied as is, /etc/ws-creator-profile;
# 2. the fragments, then the machine's user-data.yaml (last: it completes them), validated by
# cloud-init schema and assembled by make-mime; meta-data: only the instance-id;
# 3. clean, restart, wait for the result.
# cloud-init adds and never removes: removing a permission or changing isolation means
# purge, then new.
function Invoke-CloudInit($m, [string[]]$Fragments) {
    $PSNativeCommandUseErrorActionPreference = $false   # exit codes checked below
    $seed = '/var/lib/cloud/seed/nocloud'
    $parts = '/var/lib/ws-creator/cloud-config'

    Copy-FileTree $m (Join-Path -Path $script:Root -ChildPath 'files')
    Copy-FileTree $m (Join-Path -Path $m.Dir -ChildPath 'files')
    $m.Name | Invoke-Root $m "tr -d '\r' > /etc/ws-creator-profile"

    Invoke-Root $m "rm -rf $parts && mkdir -p $parts $seed"
    $sources = @($Fragments | ForEach-Object { Join-Path -Path $script:Root -ChildPath "cloud-config\$_.yaml" }) + $m.UserData
    $names = @($Fragments) + 'user-data'
    $mime = @()
    for ($i = 0; $i -lt $sources.Count; $i++) {
        $part = '{0}/{1:d2}-{2}.yaml' -f $parts, $i, $names[$i]
        $mime += "-a ${part}:cloud-config"
        Get-Content -Path $sources[$i] -Raw | Invoke-Root $m "tr -d '\r' > $part"
        Invoke-Root $m "cloud-init schema -c $part >/dev/null" 2>&1 | Where-Object { $_ -notmatch 'datasource not detected' } |
            ForEach-Object { Write-Warning $_ }
        if ($LASTEXITCODE) { throw "cloud-init schema: $($sources[$i]) is not valid cloud-config" }
    }
    "instance-id: $($m.Name)" | Invoke-Root $m "tr -d '\r' > $seed/meta-data"
    Invoke-Root $m "cloud-init devel make-mime $($mime -join ' ') > $seed/user-data"
    if ($LASTEXITCODE) { throw "cloud-init devel make-mime failed in $($m.Name)" }

    # Ubuntu's first-run setup (wsl-setup) leaves this marker on the first interactive launch
    Invoke-Root $m 'rm -f /etc/cloud/cloud-init.disabled && cloud-init clean --logs'
    Stop-Distro $m
    $status = Invoke-Root $m 'cloud-init status --wait --format json' | ConvertFrom-Json
    Invoke-Root $m "grep -E 'WARNING|ERROR' /var/log/cloud-init.log || true" | ForEach-Object { Write-Warning $_ }
    if ($status.status -ne 'done') {
        throw "cloud-init in $($m.Name): $($status.status) ($($status.boot_status_code)), see /var/log/cloud-init.log"
    }
}