1
0
mirror of https://github.com/dockur/windows.git synced 2026-07-27 21:42:36 +07:00

feat: Make autologin configurable (#1903)

This commit is contained in:
Kroese
2026-07-20 11:54:41 +02:00
committed by GitHub
parent 12be0a7d77
commit df5b0e639f
4 changed files with 195 additions and 59 deletions
+5 -2
View File
@@ -11,12 +11,15 @@ An empty default means the variable is unset and its value is determined automat
| `VERSION` | `11` | Windows version to install, such as `10` or `11`. |
| `EDITION` | | Windows edition to install, such as `core` for Windows Server Core. |
| `LANGUAGE` | `en-US` | Windows display language, such as `English`, `en-US`, or `en`. |
| `KEY` | | Windows product key used to install and activate Windows. |
| `REGION` | | Windows regional format. Uses `LANGUAGE` when unset. |
| `KEYBOARD` | | Keyboard layout. Uses `LANGUAGE` when unset. |
| `USERNAME` | `Docker` | Name of the Windows user account. |
| `PASSWORD` | `admin` | Password for the Windows account. |
| `AUTOLOGIN` | `Y` | Automatically signs in to Windows after startup. |
| `DOMAIN` | | Active Directory domain to join during installation. |
| `KEY` | | Windows product key used to install and activate Windows. |
| `DOMAIN_OU` | | Distinguished name of the organizational unit. |
| `WORKGROUP` | | Name of the Windows workgroup to join. |
## 🧠 CPU and Memory
@@ -162,7 +165,7 @@ Also see [Dynamic memory allocation](https://github.com/qemus/qemu/blob/master/d
| Variable | Default | Description |
|---|---|---|
| `SHUTDOWN` | `Y` | Enables graceful ACPI shutdown. |
| `TIMEOUT` | `115` | Maximum time, in seconds, to wait before forcing Windows to stop. |
| `TIMEOUT` | `105` | Maximum time, in seconds, to wait before forcing Windows to stop. |
## 🐞 Debugging
+3 -2
View File
@@ -171,7 +171,7 @@ kubectl apply -f https://raw.githubusercontent.com/dockur/windows/refs/heads/mas
- ./example:/shared
```
Replace the example path `./example` with your desired shared folder, which then will become visible as `Shared`.
Replace the example path `./example` with your desired shared folder, which then will become visible as `Shared` on the desktop and as drive `Z:`.
### How do I change the amount of CPU or RAM?
@@ -225,9 +225,10 @@ kubectl apply -f https://raw.githubusercontent.com/dockur/windows/refs/heads/mas
```yaml
environment:
DOMAIN: "example.com"
DOMAIN_OU: "OU=Virtual Machines,OU=Servers,DC=example,DC=com"
```
Use the domain name, such as `example.com`, rather than a URL. The supplied account is added to the local Administrators group and automatically signed in after installation.
Use the domain name, such as `example.com`, rather than a URL. The supplied account is added to the local Administrators group and automatically signed in after installation. `DOMAIN_OU` is optional and specifies where the computer account should be created.
Windows must be able to resolve and reach the domain controller through the domain's DNS server.
+74 -8
View File
@@ -18,6 +18,9 @@ set -Eeuo pipefail
: "${LANGUAGE:=""}"
: "${USERNAME:=""}"
: "${PASSWORD:=""}"
: "${DOMAIN_OU:=""}"
: "${WORKGROUP:=""}"
: "${AUTOLOGIN:=""}"
# Sanitize variables
KEY=$(strip "$KEY")
@@ -31,6 +34,9 @@ EDITION=$(strip "$EDITION")
KEYBOARD=$(strip "$KEYBOARD")
LANGUAGE=$(strip "$LANGUAGE")
USERNAME=$(strip "$USERNAME")
DOMAIN_OU=$(strip "$DOMAIN_OU")
WORKGROUP=$(strip "$WORKGROUP")
AUTOLOGIN=$(strip "$AUTOLOGIN")
MIRRORS=3
@@ -1365,6 +1371,49 @@ validateComputerName() {
return 0
}
validateWorkgroup() {
local value="$1"
local safe=""
[ -z "$value" ] && return 0
if [ "${#value}" -gt 15 ]; then
error "The WORKGROUP variable cannot contain more than 15 characters!"
return 1
fi
safe=$(printf '%s' "$value" | tr -d '"/\\[]:;|=,+*?<>') || return 1
if [[ "$safe" != "$value" ]]; then
error "The WORKGROUP variable contains characters that are not valid in a NetBIOS name!"
return 1
fi
if [[ "$value" =~ ^[.[:space:]]+$ ]]; then
error "The WORKGROUP variable cannot consist only of spaces or periods!"
return 1
fi
return 0
}
validateMembership() {
if [ -n "$DOMAIN" ] && [ -n "$WORKGROUP" ]; then
error "The DOMAIN and WORKGROUP variables cannot be used together!"
return 1
fi
if [ -n "$DOMAIN_OU" ] && [ -z "$DOMAIN" ]; then
error "The DOMAIN_OU variable requires DOMAIN to be specified!"
return 1
fi
validateWorkgroup "$WORKGROUP" || return 1
return 0
}
validateLegacyText() {
local name="$1"
@@ -1426,6 +1475,11 @@ validateLegacyUsername() {
return 1
fi
if [[ "${value^^}" == "GUEST" ]]; then
error "The USERNAME value \"$value\" is reserved for a built-in Windows account$suffix!"
return 1
fi
return 0
}
@@ -1704,6 +1758,7 @@ prepareInstall() {
validateResolution "WIDTH" "$WIDTH" 320 || return 1
validateResolution "HEIGHT" "$HEIGHT" 200 || return 1
validateMembership || return 1
validateComputerName "$HOST" || return 1
validateLegacyText "APP" "$APP" "$desc" || return 1
validateLegacyText "ENGINE" "$ENGINE" "$desc" || return 1
@@ -1713,7 +1768,8 @@ prepareInstall() {
local username="${USERNAME:-Docker}"
local password="${PASSWORD:-admin}"
local sifHost sifUsername sifPassword sifOrganization
local workgroup="${WORKGROUP:-WORKGROUP}"
local sifHost sifUsername sifPassword sifOrganization sifWorkgroup
local regUsername regPassword
validateLegacyUsername "$username" "$desc" || return 1
@@ -1723,6 +1779,7 @@ prepareInstall() {
sifUsername=$(escapeSIFValue "$username") || return 1
sifPassword=$(escapeSIFValue "$password") || return 1
sifOrganization=$(escapeSIFValue "$APP for $ENGINE") || return 1
sifWorkgroup=$(escapeSIFValue "$workgroup") || return 1
regUsername=$(escapeRegistryValue "$username") || return 1
regPassword=$(escapeRegistryValue "$password") || return 1
@@ -1756,8 +1813,12 @@ prepareInstall() {
echo " OemSkipWelcome=1"
echo " AdminPassword=\"$sifPassword\""
echo " TimeZone=0"
echo " AutoLogon=Yes"
echo " AutoLogonCount=65432"
if disabled "$AUTOLOGIN"; then
echo " AutoLogon=No"
else
echo " AutoLogon=Yes"
echo " AutoLogonCount=65432"
fi
echo ""
echo "[UserData]"
echo " FullName=\"$sifUsername\""
@@ -1766,7 +1827,7 @@ prepareInstall() {
echo " $product"
echo ""
echo "[Identification]"
echo " JoinWorkgroup = WORKGROUP"
echo " JoinWorkgroup = \"$sifWorkgroup\""
echo ""
echo "[Display]"
echo " BitsPerPel=32"
@@ -1827,10 +1888,14 @@ prepareInstall() {
echo "\"Desktopchanged\"=\"1\""
echo ""
echo "[HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon]"
echo "\"AutoAdminLogon\"=\"1\""
echo "\"DefaultUserName\"=\"$regUsername\""
echo "\"DefaultPassword\"=\"$regPassword\""
echo "\"DefaultDomainName\"=\"Dockur\""
if disabled "$AUTOLOGIN"; then
echo "\"AutoAdminLogon\"=\"0\""
else
echo "\"AutoAdminLogon\"=\"1\""
echo "\"DefaultUserName\"=\"$regUsername\""
echo "\"DefaultPassword\"=\"$regPassword\""
echo "\"DefaultDomainName\"=\"Dockur\""
fi
echo ""
echo "[HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Video\{23A77BF7-ED96-40EC-AF06-9B1F4867732A}\0000]"
echo "\"DefaultSettings.BitsPerPel\"=dword:00000020"
@@ -1845,6 +1910,7 @@ prepareInstall() {
echo "[HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\RunOnce]"
echo "\"ScreenSaver\"=\"reg add \\\"HKCU\\\\Control Panel\\\\Desktop\\\" /f /v \\\"SCRNSAVE.EXE\\\" /t REG_SZ /d \\\"off\\\"\""
echo "\"ScreenSaverOff\"=\"reg add \\\"HKCU\\\\Control Panel\\\\Desktop\\\" /f /v \\\"ScreenSaveActive\\\" /t REG_SZ /d \\\"0\\\"\""
echo "\"SharedDrive\"=\"cmd /C net use Z: \\\\\\\\host.lan\\\\Data /persistent:yes\""
echo "$oem"
echo ""
} | unix2dos > "$dir/\$OEM\$/install.reg" || return 1
+113 -47
View File
@@ -771,6 +771,8 @@ setXML() {
local file="/custom.xml"
CUSTOM_XML=""
if [ -d "$file" ]; then
error "The bind $file maps to a file that does not exist!" && exit 67
fi
@@ -781,6 +783,10 @@ setXML() {
[ ! -f "$file" ] || [ ! -s "$file" ] && file="/run/assets/$DETECTED.xml"
[ ! -f "$file" ] || [ ! -s "$file" ] && return 1
case "$file" in
"/custom.xml" | "$STORAGE/custom.xml" ) CUSTOM_XML="Y" ;;
esac
XML="$file"
return 0
}
@@ -924,18 +930,19 @@ updateDomain() {
local asset="$1"
local domain account auth pass
local pw cred_domain tmp result
local pw cred_domain ou tmp result
domain=$(escapeXML "$2") || return 1
account=$(escapeXML "$3") || return 1
auth=$(escapeXML "$4") || return 1
pass=$(escapeXML "$5") || return 1
pw="$6"
ou=$(escapeXML "$7") || return 1
cred_domain="$domain"
case "$4" in
*\\* | *@* ) cred_domain="" ;;
*@* ) cred_domain="" ;;
esac
grep -Eq 'Microsoft-Windows-UnattendedJoin|<DomainAccounts([[:space:]/>])' "$asset" && return 1
@@ -943,8 +950,9 @@ updateDomain() {
tmp=$(mktemp -d) || return 1
result="$tmp/answer.xml"
if ! DOMAIN_XML="$domain" ACCOUNT_XML="$account" AUTH_XML="$auth" \
PASS_XML="$pass" CRED_DOMAIN="$cred_domain" PW="$pw" \
if ! DOMAIN_XML="$domain" ACCOUNT_XML="$account" \
AUTH_XML="$auth" PASS_XML="$pass" \
CRED_DOMAIN="$cred_domain" PW="$pw" OU_XML="$ou" \
awk '
/<settings[^>]*pass="specialize"[^>]*>/ { section = "specialize" }
/<settings[^>]*pass="oobeSystem"[^>]*>/ { section = "oobeSystem" }
@@ -996,8 +1004,13 @@ updateDomain() {
print " <Username>" ENVIRON["AUTH_XML"] "</Username>\n" \
" <Password>" ENVIRON["PASS_XML"] "</Password>\n" \
" </Credentials>\n" \
" <JoinDomain>" ENVIRON["DOMAIN_XML"] "</JoinDomain>\n" \
" </Identification>\n" \
" <JoinDomain>" ENVIRON["DOMAIN_XML"] "</JoinDomain>"
if (ENVIRON["OU_XML"] != "") {
print " <MachineObjectOU>" ENVIRON["OU_XML"] "</MachineObjectOU>"
}
print " </Identification>\n" \
" </component>"
join_added = 1
@@ -1057,7 +1070,7 @@ validateLocalUsername() {
"NONE" )
error "The USERNAME value \"NONE\" is reserved by Windows!"
return 1 ;;
"ADMINISTRATOR" | "GUEST" )
"ADMINISTRATOR" | "GUEST" | "DEFAULTACCOUNT" | "WDAGUTILITYACCOUNT" | "WSIACCOUNT" )
error "The USERNAME value \"$value\" is reserved for a built-in Windows account!"
return 1 ;;
esac
@@ -1075,27 +1088,20 @@ validateDomainName() {
return 1
fi
if [ "${#value}" -gt 255 ]; then
error "The $name variable cannot contain more than 255 characters!"
return 1
fi
if [[ "$value" == *"://"* ]]; then
error "The $name variable must contain a domain name, not a URL!"
return 1
fi
if [[ "$value" =~ [[:cntrl:]] ]] || [[ "$value" =~ [[:space:]] ]]; then
error "The $name variable cannot contain whitespace or control characters!"
if [ "${#value}" -gt 255 ] ||
[[ "$value" =~ [[:cntrl:]] ]] ||
[[ "$value" =~ [[:space:]] ]] ||
[[ ! "$value" =~ ^[A-Za-z0-9]([A-Za-z0-9-]{0,61}[A-Za-z0-9])?(\.[A-Za-z0-9]([A-Za-z0-9-]{0,61}[A-Za-z0-9])?)*$ ]]; then
error "The $name variable does not contain a valid domain name!"
return 1
fi
case "$value" in
*'/'* | *\\* | *'@'* | *':'* )
error "The $name variable does not contain a valid domain name!"
return 1 ;;
esac
return 0
}
@@ -1142,19 +1148,62 @@ validateDomainUsername() {
return 0
}
updateWorkgroup() {
local asset="$1"
local workgroup arch tmp result
workgroup=$(escapeXML "$2") || return 1
arch=$(sed -n -E '0,/processorArchitecture="/s/.*processorArchitecture="([^"]+)".*/\1/p' "$asset") || return 1
[ -z "$arch" ] && return 1
grep -q 'Microsoft-Windows-UnattendedJoin' "$asset" && return 1
tmp=$(mktemp -d) || return 1
result="$tmp/answer.xml"
if ! WORKGROUP_XML="$workgroup" ARCH_XML="$arch" awk '
/<settings[^>]*pass="specialize"[^>]*>/ { section = "specialize" }
section == "specialize" && !workgroup_added &&
/^[[:space:]]*<\/settings>[[:space:]]*$/ {
print " <component name=\"Microsoft-Windows-UnattendedJoin\" processorArchitecture=\"" ENVIRON["ARCH_XML"] "\" publicKeyToken=\"31bf3856ad364e35\" language=\"neutral\" versionScope=\"nonSxS\">\n" \
" <Identification>\n" \
" <JoinWorkgroup>" ENVIRON["WORKGROUP_XML"] "</JoinWorkgroup>\n" \
" </Identification>\n" \
" </component>"
workgroup_added = 1
}
{ print }
/^[[:space:]]*<\/settings>[[:space:]]*$/ { section = "" }
END { exit !workgroup_added }
' "$asset" > "$result" ||
! mv -f "$result" "$asset"; then
rm -rf "$tmp" || true
return 1
fi
rm -rf "$tmp" || return 1
return 0
}
updateXML() {
local asset="$1"
local language="$2"
local app value culture region keyboard edition key
local user user_xml auth_user
local admin pass pw domain qualifier host
local user user_xml auth_user admin pass pw
local domain qualifier host workgroup
[ -z "$WIDTH" ] && WIDTH="1280"
[ -z "$HEIGHT" ] && HEIGHT="720"
validateResolution "WIDTH" "$WIDTH" 320 || return 1
validateResolution "HEIGHT" "$HEIGHT" 200 || return 1
validateMembership || return 1
validateComputerName "$HOST" || return 1
validateProductKey "$KEY" || return 1
validatePassword "$PASSWORD" || return 1
@@ -1164,8 +1213,8 @@ updateXML() {
sed -i "s|>Windows for Docker<|>$app<|g" "$asset" || return 1
sed -i -E "s|<ComputerName>[^<]*</ComputerName>|<ComputerName>$host</ComputerName>|g" "$asset" || return 1
sed -i "s|<VerticalResolution>1080</VerticalResolution>|<VerticalResolution>$HEIGHT</VerticalResolution>|g" "$asset" || return 1
sed -i "s|<HorizontalResolution>1920</HorizontalResolution>|<HorizontalResolution>$WIDTH</HorizontalResolution>|g" "$asset" || return 1
sed -i -E "s|<VerticalResolution>[^<]*</VerticalResolution>|<VerticalResolution>$HEIGHT</VerticalResolution>|g" "$asset" || return 1
sed -i -E "s|<HorizontalResolution>[^<]*</HorizontalResolution>|<HorizontalResolution>$WIDTH</HorizontalResolution>|g" "$asset" || return 1
culture=$(getLanguage "$language" "culture") || return 1
@@ -1193,6 +1242,7 @@ updateXML() {
fi
domain="$DOMAIN"
workgroup="$WORKGROUP"
case "${DETECTED,,}" in
"win10x64"* | "win11x64"* ) ;;
* ) domain="" ;;
@@ -1215,21 +1265,12 @@ updateXML() {
auth_user="$USERNAME"
qualifier=""
if [[ "$auth_user" == *\\* && "$auth_user" == *@* ]]; then
error "The USERNAME variable must use only one domain account format!"
if [[ "$auth_user" == *\\* ]]; then
error "The USERNAME variable must use either \"user\" or \"user@domain\" format!"
return 1
fi
case "$auth_user" in
*\\* )
qualifier="${auth_user%%\\*}"
user="${auth_user#*\\}"
if [ -z "$qualifier" ] || [ -z "$user" ] || [[ "$user" == *\\* ]]; then
error "The USERNAME variable does not contain a valid domain account name!"
return 1
fi
;;
*@* )
user="${auth_user%%@*}"
qualifier="${auth_user#*@}"
@@ -1238,6 +1279,13 @@ updateXML() {
error "The USERNAME variable does not contain a valid domain account name!"
return 1
fi
validateDomainName "$qualifier" "USERNAME" || return 1
if [[ "${qualifier,,}" != "${domain,,}" ]]; then
error "The domain in the USERNAME variable must match the DOMAIN variable!"
return 1
fi
;;
* )
user="$auth_user"
@@ -1246,10 +1294,6 @@ updateXML() {
validateDomainUsername "$user" || return 1
if [ -n "$qualifier" ]; then
validateDomainName "$qualifier" "USERNAME" || return 1
fi
if [[ "${user,,}" == "docker" ]]; then
error "The USERNAME variable must be changed from its default value when joining a domain!"
return 1
@@ -1286,20 +1330,31 @@ updateXML() {
pw=$(printf '%s' "${pass}Password" | iconv -f utf-8 -t utf-16le | base64 -w 0) || return 1
admin=$(printf '%s' "${pass}AdministratorPassword" | iconv -f utf-8 -t utf-16le | base64 -w 0) || return 1
sed -i "s|<Value>password</Value>|<Value>$admin</Value>|g" "$asset" || return 1
sed -i "s|<PlainText>true</PlainText>|<PlainText>false</PlainText>|g" "$asset" || return 1
sed -i -z -E "s|<Password>([[:space:]]*)<Value[[:space:]]*/>|<Password>\1<Value>$pw</Value>|g" "$asset" || return 1
sed -i -z -E "s|<AdministratorPassword>([[:space:]]*)<Value[[:space:]]*/>|<AdministratorPassword>\1<Value>$admin</Value>|g" "$asset" || return 1
sed -i -z -E "s#(<Password>[[:space:]]*<Value)([[:space:]]*/>|>[^<]*</Value>)#\1>$pw</Value>#g" "$asset" || return 1
sed -i -z -E "s#(<AdministratorPassword>[[:space:]]*<Value)([[:space:]]*/>|>[^<]*</Value>)#\1>$admin</Value>#g" "$asset" || return 1
sed -i -E "s|<PlainText>[^<]*</PlainText>|<PlainText>false</PlainText>|g" "$asset" || return 1
if [ -n "$domain" ]; then
pw=$(printf '%s' "${PASSWORD}Password" | iconv -f utf-8 -t utf-16le | base64 -w 0) || return 1
if ! updateDomain "$asset" "$domain" "$user" "$auth_user" "$PASSWORD" "$pw"; then
if ! updateDomain "$asset" "$domain" "$user" \
"$auth_user" "$PASSWORD" "$pw" "$DOMAIN_OU"; then
error "Failed to add domain configuration to answer file!"
return 1
fi
elif [ -n "$workgroup" ]; then
if ! updateWorkgroup "$asset" "$workgroup"; then
error "Failed to add workgroup configuration to answer file!"
return 1
fi
fi
if disabled "$AUTOLOGIN"; then
sed -i -E '/^[[:space:]]*<AutoLogon([[:space:]>])/,/^[[:space:]]*<\/AutoLogon>[[:space:]]*$/d' "$asset" || return 1
fi
if [ -n "$EDITION" ]; then
@@ -1536,9 +1591,20 @@ updateImage() {
fi
if ! updateXML "$answer" "$language"; then
error "Failed to update answer file: $answer"
return 1
if [ -n "${CUSTOM_XML:-}" ]; then
if ! xmllint --nonet --noout "$answer"; then
error "The custom answer file is not valid XML!"
return 1
fi
else
if ! updateXML "$answer" "$language"; then
error "Failed to update answer file: $answer"
return 1
fi
fi
if ! wimlib-imagex update "$wim" "$idx" --command "add $answer /$xml" > /dev/null; then