Using go entrypoint for Windows agents

This commit is contained in:
Alexey Pustovalov 2026-07-15 17:51:41 +09:00
parent 9e7e5e633f
commit 0866f929e2
56 changed files with 1428 additions and 508 deletions

View file

@ -16,7 +16,8 @@ on:
- '!Dockerfiles/*/windows/*'
- '.github/workflows/images_build.yml'
- '.github/actions/*'
- 'templates/entrypoints/**'
- 'templates/entrypoints/*.sh'
- 'templates/entrypoints/lib/**'
schedule:
- cron: '50 02 * * *'
workflow_dispatch:

View file

@ -13,6 +13,10 @@ on:
- '!**/README.md'
- '!**/README.html'
- '.github/workflows/images_build_rhel.yml'
- 'templates/entrypoints/*.sh'
- 'templates/entrypoints/lib/**'
- '!templates/entrypoints/web-apache-*.sh'
- '!templates/entrypoints/lib/apache.sh'
workflow_dispatch:
inputs:
publish_images:

View file

@ -11,6 +11,10 @@ on:
paths:
- 'build.json'
- 'Dockerfiles/*/windows/*'
- 'templates/config/*_windows/**'
- 'templates/entrypoints/**/*.go'
- 'templates/entrypoints/go.mod'
- 'templates/entrypoints/licenses/go-THIRD-PARTY-NOTICES.txt'
- '!**/README.md'
- '.github/workflows/images_build_windows.yml'
- '.github/actions/docker-build-push-windows/*'
@ -40,6 +44,7 @@ env:
MSFT_BASE_BUILD_IMAGE: "mcr.microsoft.com/windows/servercore"
OS_BASE_IMAGE_NAME: "mcr.microsoft.com/windows/nanoserver"
ENTRYPOINT_BUILD_IMAGE_NAME: "golang:1.26.5-windowsservercore"
BASE_IMAGE_NAME: "build-base"
BASE_BUILD_IMAGE_NAME: "build-mysql"
@ -698,7 +703,7 @@ jobs:
id: docker_build
uses: ./.github/actions/docker-build-push-windows
with:
context: ${{ env.DOCKERFILES_DIRECTORY }}\${{ matrix.component }}\windows
context: .
dockerfile: ${{ env.DOCKERFILES_DIRECTORY }}\${{ matrix.component }}\windows\Dockerfile
tags: ${{ steps.meta.outputs.tags }}
label_revision: ${{ fromJSON(steps.meta.outputs.json).labels['org.opencontainers.image.revision'] }}
@ -706,10 +711,13 @@ jobs:
pull_images: |
${{ steps.base_build.outputs.base_build_image }}
${{ env.OS_BASE_IMAGE_NAME }}:${{ steps.base_os_tag.outputs.os_tag }}
${{ env.ENTRYPOINT_BUILD_IMAGE_NAME }}-${{ steps.base_os_tag.outputs.os_tag }}
build_args: |
BUILD_BASE_IMAGE=${{ steps.base_build.outputs.base_build_image }}
OS_BASE_IMAGE=${{ env.OS_BASE_IMAGE_NAME }}:${{ steps.base_os_tag.outputs.os_tag }}
ENTRYPOINT_BUILD_IMAGE=${{ env.ENTRYPOINT_BUILD_IMAGE_NAME }}-${{ steps.base_os_tag.outputs.os_tag }}
push: ${{ env.AUTO_PUSH_IMAGES }}
remove_readme: false
- name: Sign the images with GitHub OIDC Token
if: ${{ env.AUTO_PUSH_IMAGES == 'true' }}

View file

@ -1,12 +1,26 @@
# syntax=docker/dockerfile:1.6
# escape=`
ARG OS_BASE_IMAGE=mcr.microsoft.com/windows/nanoserver:ltsc2022
ARG ENTRYPOINT_BUILD_IMAGE=golang:1.26.5-windowsservercore-ltsc2022
ARG MAJOR_VERSION=7.4
ARG ZBX_VERSION=${MAJOR_VERSION}.12
ARG BUILD_BASE_IMAGE=zabbix-build-agent:ltsc2022-agent-${ZBX_VERSION}
FROM ${ENTRYPOINT_BUILD_IMAGE} AS entrypoint-builder
COPY ["templates/entrypoints", "C:\\entrypoints"]
WORKDIR C:\entrypoints
SHELL ["powershell", "-Command", "$ErrorActionPreference = 'Stop'; $ProgressPreference = 'SilentlyContinue';"]
RUN $env:CGO_ENABLED = '0'; `
go build -buildvcs=false -trimpath -ldflags='-s -w -buildid=' `
-o C:\docker-entrypoint.exe `
./cmd/agent;
FROM ${BUILD_BASE_IMAGE} AS builder
FROM ${OS_BASE_IMAGE}
@ -30,8 +44,10 @@ LABEL org.opencontainers.image.title="Zabbix agent" `
org.opencontainers.image.source="${ZBX_SOURCES}"
COPY --from=builder ["C:\\zabbix-${ZBX_VERSION}-output", "C:\\zabbix"]
COPY ["templates/config/agent_windows", "C:\\zabbix\\conf"]
COPY --from=builder ["C:\\build_deps\\pwsh", "C:\\Program Files\\PowerShell"]
COPY [".\\docker-entrypoint.ps1", "C:\\zabbix"]
COPY --from=entrypoint-builder ["C:\\docker-entrypoint.exe", "C:\\zabbix\\docker-entrypoint.exe"]
COPY ["templates/entrypoints/licenses/go-THIRD-PARTY-NOTICES.txt", "C:\\licenses\\"]
WORKDIR C:\zabbix\
@ -66,6 +82,6 @@ USER zabbix
EXPOSE 10050/tcp
ENTRYPOINT ["pwsh", "-File", "C:\\zabbix\\docker-entrypoint.ps1"]
ENTRYPOINT ["C:\\zabbix\\docker-entrypoint.exe"]
CMD ["C:\\zabbix\\sbin\\zabbix_agentd.exe", "-c", "C:\\zabbix\\conf\\zabbix_agentd.conf", "-f"]

View file

@ -0,0 +1,19 @@
**
!templates/
!templates/entrypoints/
!templates/entrypoints/go.mod
!templates/entrypoints/cmd/
!templates/entrypoints/cmd/agent/
!templates/entrypoints/cmd/agent/**
!templates/entrypoints/internal/
!templates/entrypoints/internal/agent/
!templates/entrypoints/internal/agent/**
!templates/entrypoints/internal/bootstrap/
!templates/entrypoints/internal/bootstrap/**
!templates/entrypoints/internal/hooks/
!templates/entrypoints/internal/hooks/**
!templates/entrypoints/licenses/
!templates/entrypoints/licenses/go-THIRD-PARTY-NOTICES.txt
!templates/config/
!templates/config/agent_windows/
!templates/config/agent_windows/**

View file

@ -1,233 +0,0 @@
# Script trace mode
if ($env:DEBUG_MODE -eq "true") {
Set-PSDebug -trace 1
}
# Default Zabbix server host
if ([string]::IsNullOrEmpty($env:ZBX_SERVER_HOST)) {
$env:ZBX_SERVER_HOST="zabbix-server"
}
# Default Zabbix server port number
if ([string]::IsNullOrEmpty($env:ZBX_SERVER_PORT)) {
$env:ZBX_SERVER_PORT="10051"
}
# Default directories
# Internal directory for TLS related files, used when TLS*File specified as plain text values
$ZabbixInternalEncDir="$env:ZABBIX_USER_HOME_DIR/enc_internal"
function Update-Config-Var {
Param (
[Parameter(Mandatory=$true, Position=0)]
[ValidateNotNullOrEmpty()]
[string] $ConfigPath,
[Parameter(Mandatory=$true, Position=1)]
[ValidateNotNullOrEmpty()]
[string]$VarName,
[Parameter(Mandatory=$false, Position=2)]
[string]$VarValue = $null,
[Parameter(Mandatory=$false, Position=3)]
[bool]$IsMultiple
)
$MaskList = "TLSPSKIdentity"
if (-not(Test-Path -Path $ConfigPath -PathType Leaf)) {
throw "**** Configuration file '$ConfigPath' does not exist"
}
if ($MaskList.Contains($VarName) -eq $true -And [string]::IsNullOrWhitespace($VarValue) -ne $true) {
Write-Host -NoNewline "** Updating '$ConfigPath' parameter ""$VarName"": '****'. Enable DEBUG_MODE to view value ..."
}
else {
Write-Host -NoNewline "** Updating '$ConfigPath' parameter ""$VarName"": '$VarValue'..."
}
if ([string]::IsNullOrWhitespace($VarValue)) {
if ((Get-Content $ConfigPath | %{$_ -match "^$VarName="}) -contains $true) {
(Get-Content $ConfigPath) |
Where-Object {$_ -notmatch "^$VarName=" } |
Set-Content $ConfigPath
}
Write-Host "removed"
return
}
if ($VarValue -eq '""') {
(Get-Content $ConfigPath) | Foreach-Object { $_ -Replace "^($VarName=)(.*)", '$1' } | Set-Content $ConfigPath
Write-Host "undefined"
return
}
if ($VarName -match '^TLS.*File$') {
$VarValue="$env:ZABBIX_USER_HOME_DIR\enc\$VarValue"
}
if ((Get-Content $ConfigPath | %{$_ -match "^$VarName="}) -contains $true -And $IsMultiple -ne $true) {
(Get-Content $ConfigPath) | Foreach-Object { $_ -Replace "^$VarName=.+", "$VarName=$VarValue" } | Set-Content $ConfigPath
Write-Host updated
}
elseif ((Get-Content $ConfigPath | select-string -pattern "^[#;] $VarName=").length -gt 1) {
(Get-Content $ConfigPath) |
Foreach-Object {
$_
if ($_ -match "^[#;] $VarName=$") {
"$VarName=$VarValue"
}
} | Set-Content $ConfigPath
Write-Host "added first occurrence"
}
elseif ((Get-Content $ConfigPath | select-string -pattern "^[#;] $VarName=").length -gt 0) {
(Get-Content $ConfigPath) |
Foreach-Object {
$_
if ($_ -match "^[#;] $VarName=") {
"$VarName=$VarValue"
}
} | Set-Content $ConfigPath
Write-Host "added"
}
else {
Add-Content -Path $ConfigPath -Value "$VarName=$VarValue"
Write-Host "added at the end"
}
}
function Update-Config-Multiple-Var {
Param (
[Parameter(Mandatory=$true, Position=0)]
[ValidateNotNullOrEmpty()]
[string] $ConfigPath,
[Parameter(Mandatory=$true, Position=1)]
[ValidateNotNullOrEmpty()]
[string]$VarName,
[Parameter(Mandatory=$false, Position=2)]
[string]$VarValue = $null
)
foreach ($value in $VarValue.split(',')) {
Update-Config-Var $ConfigPath $VarName $value $true
}
}
function File-Process-From-Env {
Param (
[Parameter(Mandatory=$true, Position=0)]
[ValidateNotNullOrEmpty()]
[string] $VarName,
[Parameter(Mandatory=$false, Position=1)]
[string]$FileName = $null,
[Parameter(Mandatory=$false, Position=2)]
[string]$VarValue = $null
)
if (![string]::IsNullOrEmpty($VarValue)) {
$VarValue | Set-Content "$ZabbixInternalEncDir\$VarName"
$FileName="$ZabbixInternalEncDir\$VarName"
}
if (![string]::IsNullOrEmpty($FileName)) {
Set-Item env:$VarName -Value $FileName
}
$VarName=$VarName -replace 'FILE$'
Set-Item env:$VarName -Value $null
}
function Prepare-Zbx-Agent-Config {
if ([string]::IsNullOrEmpty($env:ZBX_PASSIVESERVERS)) {
$env:ZBX_PASSIVESERVERS=""
}
if ([string]::IsNullOrEmpty($env:ZBX_ACTIVESERVERS)) {
$env:ZBX_ACTIVESERVERS=""
}
if (![string]::IsNullOrEmpty($env:ZBX_SERVER_HOST) -And ![string]::IsNullOrEmpty($env:ZBX_PASSIVESERVERS)) {
$env:ZBX_PASSIVESERVERS="$env:ZBX_SERVER_HOST,$env:ZBX_PASSIVESERVERS"
}
elseif (![string]::IsNullOrEmpty($env:ZBX_SERVER_HOST)) {
$env:ZBX_PASSIVESERVERS=$env:ZBX_SERVER_HOST
}
if (![string]::IsNullOrEmpty($env:ZBX_SERVER_HOST)) {
if (![string]::IsNullOrEmpty($env:ZBX_SERVER_PORT) -And $env:ZBX_SERVER_PORT -ne "10051") {
$env:ZBX_SERVER_HOST="$env:ZBX_SERVER_HOST:$env:ZBX_SERVER_PORT"
}
if (![string]::IsNullOrEmpty($env:ZBX_ACTIVESERVERS)) {
$env:ZBX_ACTIVESERVERS="$env:ZBX_SERVER_HOST,$env:ZBX_ACTIVESERVERS"
}
else {
$env:ZBX_ACTIVESERVERS=$env:ZBX_SERVER_HOST
}
}
if ([string]::IsNullOrWhitespace($env:ZBX_PASSIVE_ALLOW)) {
$env:ZBX_PASSIVE_ALLOW="true"
}
if ($env:ZBX_PASSIVE_ALLOW -eq "true") {
Write-Host "** Using '$env:ZBX_PASSIVESERVERS' servers for passive checks"
}
else {
Set-Item env:ZBX_PASSIVESERVERS -Value $null
}
if ([string]::IsNullOrWhitespace($env:ZBX_ACTIVE_ALLOW)) {
$env:ZBX_ACTIVE_ALLOW="true"
}
if ($env:ZBX_ACTIVE_ALLOW -eq "true") {
Write-Host "** Using '$env:ZBX_ACTIVESERVERS' servers for active checks"
}
else {
Set-Item env:ZBX_ACTIVESERVERS -Value $null
}
Update-Config-Multiple-Var "$env:ZABBIX_CONF_DIR\zabbix_agentd_item_keys.conf" "DenyKey" "$env:ZBX_DENYKEY"
Update-Config-Multiple-Var "$env:ZABBIX_CONF_DIR\zabbix_agentd_item_keys.conf" "AllowKey" "$env:ZBX_ALLOWKEY"
File-Process-From-Env "ZBX_TLSCAFILE" "$env:ZBX_TLSCAFILE" "$env:ZBX_TLSCA"
File-Process-From-Env "ZBX_TLSCRLFILE" "$env:ZBX_TLSCRLFILE" "$env:ZBX_TLSCRL"
File-Process-From-Env "ZBX_TLSCERTFILE" "$env:ZBX_TLSCERTFILE" "$env:ZBX_TLSCERT"
File-Process-From-Env "ZBX_TLSKEYFILE" "$env:ZBX_TLSKEYFILE" "$env:ZBX_TLSKEY"
File-Process-From-Env "ZBX_TLSPSKFILE" "$env:ZBX_TLSPSKFILE" "$env:ZBX_TLSPSK"
}
function ClearZbxEnv() {
if ($env:ZBX_CLEAR_ENV -eq "false") {
return
}
$env_vars=Get-ChildItem env:* | Where-Object {$_.Name -match "^ZABBIX_.*" } | foreach { $_.Name }
foreach ($env_var in $env_vars) {
Set-Item env:$env_var -Value $null
}
}
function PrepareAgent {
Write-Host "** Preparing Zabbix agent"
Prepare-Zbx-Agent-Config
ClearZbxEnv
}
$commandArgs = $args
if ($args.length -gt 0 -And $args[0].StartsWith('-')) {
$commandArgs = @("C:\zabbix\sbin\zabbix_agentd.exe") + $args
}
if ($commandArgs.length -gt 0 -And $commandArgs[0] -eq "C:\zabbix\sbin\zabbix_agentd.exe") {
PrepareAgent
}
if ($commandArgs.length -gt 0) {
$exe, $exeArgs = $commandArgs
& $exe @exeArgs
exit $LASTEXITCODE
}

View file

@ -1,12 +1,26 @@
# syntax=docker/dockerfile:1.6
# escape=`
ARG OS_BASE_IMAGE=mcr.microsoft.com/windows/nanoserver:ltsc2022
ARG ENTRYPOINT_BUILD_IMAGE=golang:1.26.5-windowsservercore-ltsc2022
ARG MAJOR_VERSION=7.4
ARG ZBX_VERSION=${MAJOR_VERSION}.12
ARG BUILD_BASE_IMAGE=zabbix-build-agent:ltsc2022-agent2-${ZBX_VERSION}
FROM ${ENTRYPOINT_BUILD_IMAGE} AS entrypoint-builder
COPY ["templates/entrypoints", "C:\\entrypoints"]
WORKDIR C:\entrypoints
SHELL ["powershell", "-Command", "$ErrorActionPreference = 'Stop'; $ProgressPreference = 'SilentlyContinue';"]
RUN $env:CGO_ENABLED = '0'; `
go build -buildvcs=false -trimpath -ldflags='-s -w -buildid=' `
-o C:\docker-entrypoint.exe `
./cmd/agent2;
FROM ${BUILD_BASE_IMAGE} AS builder
FROM ${OS_BASE_IMAGE}
@ -31,8 +45,10 @@ LABEL org.opencontainers.image.title="Zabbix agent 2" `
org.opencontainers.image.source="${ZBX_SOURCES}"
COPY --from=builder ["C:\\zabbix-${ZBX_VERSION}-output", "C:\\zabbix"]
COPY ["templates/config/agent2_windows", "C:\\zabbix\\conf"]
COPY --from=builder ["C:\\build_deps\\pwsh", "C:\\Program Files\\PowerShell"]
COPY [".\\docker-entrypoint.ps1", "C:\\zabbix"]
COPY --from=entrypoint-builder ["C:\\docker-entrypoint.exe", "C:\\zabbix\\docker-entrypoint.exe"]
COPY ["templates/entrypoints/licenses/go-THIRD-PARTY-NOTICES.txt", "C:\\licenses\\"]
WORKDIR C:\zabbix\
@ -69,6 +85,6 @@ USER zabbix
EXPOSE 10050/tcp 31999/tcp
ENTRYPOINT ["pwsh", "-File", "C:\\zabbix\\docker-entrypoint.ps1"]
ENTRYPOINT ["C:\\zabbix\\docker-entrypoint.exe"]
CMD ["C:\\zabbix\\sbin\\zabbix_agent2.exe", "-c", "C:\\zabbix\\conf\\zabbix_agent2.conf", "-f"]

View file

@ -0,0 +1,19 @@
**
!templates/
!templates/entrypoints/
!templates/entrypoints/go.mod
!templates/entrypoints/cmd/
!templates/entrypoints/cmd/agent2/
!templates/entrypoints/cmd/agent2/**
!templates/entrypoints/internal/
!templates/entrypoints/internal/agent/
!templates/entrypoints/internal/agent/**
!templates/entrypoints/internal/bootstrap/
!templates/entrypoints/internal/bootstrap/**
!templates/entrypoints/internal/hooks/
!templates/entrypoints/internal/hooks/**
!templates/entrypoints/licenses/
!templates/entrypoints/licenses/go-THIRD-PARTY-NOTICES.txt
!templates/config/
!templates/config/agent2_windows/
!templates/config/agent2_windows/**

View file

@ -1,263 +0,0 @@
# Script trace mode
if ($env:DEBUG_MODE -eq "true") {
Set-PSDebug -trace 1
}
# Default Zabbix server host
if ([string]::IsNullOrEmpty($env:ZBX_SERVER_HOST)) {
$env:ZBX_SERVER_HOST="zabbix-server"
}
# Default Zabbix server port number
if ([string]::IsNullOrEmpty($env:ZBX_SERVER_PORT)) {
$env:ZBX_SERVER_PORT="10051"
}
# Default directories
# Internal directory for TLS related files, used when TLS*File specified as plain text values
$ZabbixInternalEncDir="$env:ZABBIX_USER_HOME_DIR/enc_internal"
function Update-Config-Var {
Param (
[Parameter(Mandatory=$true, Position=0)]
[ValidateNotNullOrEmpty()]
[string] $ConfigPath,
[Parameter(Mandatory=$true, Position=1)]
[ValidateNotNullOrEmpty()]
[string]$VarName,
[Parameter(Mandatory=$false, Position=2)]
[string]$VarValue = $null,
[Parameter(Mandatory=$false, Position=3)]
[bool]$IsMultiple
)
$MaskList = "TLSPSKIdentity"
if (-not(Test-Path -Path $ConfigPath -PathType Leaf)) {
throw "**** Configuration file '$ConfigPath' does not exist"
}
if ($MaskList.Contains($VarName) -eq $true -And [string]::IsNullOrWhitespace($VarValue) -ne $true) {
Write-Host -NoNewline "** Updating '$ConfigPath' parameter ""$VarName"": '****'. Enable DEBUG_MODE to view value ..."
}
else {
Write-Host -NoNewline "** Updating '$ConfigPath' parameter ""$VarName"": '$VarValue'..."
}
if ([string]::IsNullOrWhitespace($VarValue)) {
if ((Get-Content $ConfigPath | %{$_ -match "^$VarName="}) -contains $true) {
(Get-Content $ConfigPath) |
Where-Object {$_ -notmatch "^$VarName=" } |
Set-Content $ConfigPath
}
Write-Host "removed"
return
}
if ($VarValue -eq '""') {
(Get-Content $ConfigPath) | Foreach-Object { $_ -Replace "^($VarName=)(.*)", '$1' } | Set-Content $ConfigPath
Write-Host "undefined"
return
}
if ($VarName -match '^TLS.*File$') {
$VarValue="$env:ZABBIX_USER_HOME_DIR\enc\$VarValue"
}
if ((Get-Content $ConfigPath | %{$_ -match "^$VarName="}) -contains $true -And $IsMultiple -ne $true) {
(Get-Content $ConfigPath) | Foreach-Object { $_ -Replace "^$VarName=.+", "$VarName=$VarValue" } | Set-Content $ConfigPath
Write-Host updated
}
elseif ((Get-Content $ConfigPath | select-string -pattern "^[#;] $VarName=").length -gt 1) {
(Get-Content $ConfigPath) |
Foreach-Object {
$_
if ($_ -match "^[#;] $VarName=$") {
"$VarName=$VarValue"
}
} | Set-Content $ConfigPath
Write-Host "added first occurrence"
}
elseif ((Get-Content $ConfigPath | select-string -pattern "^[#;] $VarName=").length -gt 0) {
(Get-Content $ConfigPath) |
Foreach-Object {
$_
if ($_ -match "^[#;] $VarName=") {
"$VarName=$VarValue"
}
} | Set-Content $ConfigPath
Write-Host "added"
}
else {
Add-Content -Path $ConfigPath -Value "$VarName=$VarValue"
Write-Host "added at the end"
}
}
function Update-Config-Multiple-Var {
Param (
[Parameter(Mandatory=$true, Position=0)]
[ValidateNotNullOrEmpty()]
[string] $ConfigPath,
[Parameter(Mandatory=$true, Position=1)]
[ValidateNotNullOrEmpty()]
[string]$VarName,
[Parameter(Mandatory=$false, Position=2)]
[string]$VarValue = $null
)
foreach ($value in $VarValue.split(',')) {
Update-Config-Var $ConfigPath $VarName $value $true
}
}
function File-Process-From-Env {
Param (
[Parameter(Mandatory=$true, Position=0)]
[ValidateNotNullOrEmpty()]
[string] $VarName,
[Parameter(Mandatory=$false, Position=1)]
[string]$FileName = $null,
[Parameter(Mandatory=$false, Position=2)]
[string]$VarValue = $null
)
if (![string]::IsNullOrEmpty($VarValue)) {
$VarValue | Set-Content "$ZabbixInternalEncDir\$VarName"
$FileName="$ZabbixInternalEncDir\$VarName"
}
if (![string]::IsNullOrEmpty($FileName)) {
Set-Item env:$VarName -Value $FileName
}
$VarName=$VarName -replace 'FILE$'
Set-Item env:$VarName -Value $null
}
function Prepare-Zbx-Agent-Config {
if ([string]::IsNullOrEmpty($env:ZBX_PASSIVESERVERS)) {
$env:ZBX_PASSIVESERVERS=""
}
if ([string]::IsNullOrEmpty($env:ZBX_ACTIVESERVERS)) {
$env:ZBX_ACTIVESERVERS=""
}
if (![string]::IsNullOrEmpty($env:ZBX_SERVER_HOST) -And ![string]::IsNullOrEmpty($env:ZBX_PASSIVESERVERS)) {
$env:ZBX_PASSIVESERVERS="$env:ZBX_SERVER_HOST,$env:ZBX_PASSIVESERVERS"
}
elseif (![string]::IsNullOrEmpty($env:ZBX_SERVER_HOST)) {
$env:ZBX_PASSIVESERVERS=$env:ZBX_SERVER_HOST
}
if (![string]::IsNullOrEmpty($env:ZBX_SERVER_HOST)) {
if (![string]::IsNullOrEmpty($env:ZBX_SERVER_PORT) -And $env:ZBX_SERVER_PORT -ne "10051") {
$env:ZBX_SERVER_HOST="$env:ZBX_SERVER_HOST:$env:ZBX_SERVER_PORT"
}
if (![string]::IsNullOrEmpty($env:ZBX_ACTIVESERVERS)) {
$env:ZBX_ACTIVESERVERS="$env:ZBX_SERVER_HOST,$env:ZBX_ACTIVESERVERS"
}
else {
$env:ZBX_ACTIVESERVERS=$env:ZBX_SERVER_HOST
}
}
if ([string]::IsNullOrWhitespace($env:ZBX_PASSIVE_ALLOW)) {
$env:ZBX_PASSIVE_ALLOW="true"
}
if ($env:ZBX_PASSIVE_ALLOW -eq "true") {
Write-Host "** Using '$env:ZBX_PASSIVESERVERS' servers for passive checks"
}
else {
Set-Item env:ZBX_PASSIVESERVERS -Value $null
}
if ([string]::IsNullOrWhitespace($env:ZBX_ACTIVE_ALLOW)) {
$env:ZBX_ACTIVE_ALLOW="true"
}
if ($env:ZBX_ACTIVE_ALLOW -eq "true") {
Write-Host "** Using '$env:ZBX_ACTIVESERVERS' servers for active checks"
}
else {
Set-Item env:ZBX_ACTIVESERVERS -Value $null
}
Set-Item env:ZBX_SERVER_HOST -Value $null
Set-Item env:ZBX_SERVER_PORT -Value $null
if ($env:ZBX_ENABLEPERSISTENTBUFFER -eq "true") {
$env:ZBX_ENABLEPERSISTENTBUFFER="1"
}
else {
Set-Item env:ZBX_ENABLEPERSISTENTBUFFER -Value $null
Set-Item env:ZBX_PERSISTENTBUFFERFILE -Value $null
}
if ($env:ZBX_ENABLESTATUSPORT -eq "true") {
$env:ZBX_STATUSPORT="31999"
}
else {
Set-Item env:ZBX_STATUSPORT -Value $null
}
Update-Config-Multiple-Var "$env:ZABBIX_CONF_DIR\zabbix_agent2_item_keys.conf" "DenyKey" "$env:ZBX_DENYKEY"
Update-Config-Multiple-Var "$env:ZABBIX_CONF_DIR\zabbix_agent2_item_keys.conf" "AllowKey" "$env:ZBX_ALLOWKEY"
File-Process-From-Env "ZBX_TLSCAFILE" "$env:ZBX_TLSCAFILE" "$env:ZBX_TLSCA"
File-Process-From-Env "ZBX_TLSCRLFILE" "$env:ZBX_TLSCRLFILE" "$env:ZBX_TLSCRL"
File-Process-From-Env "ZBX_TLSCERTFILE" "$env:ZBX_TLSCERTFILE" "$env:ZBX_TLSCERT"
File-Process-From-Env "ZBX_TLSKEYFILE" "$env:ZBX_TLSKEYFILE" "$env:ZBX_TLSKEY"
File-Process-From-Env "ZBX_TLSPSKFILE" "$env:ZBX_TLSPSKFILE" "$env:ZBX_TLSPSK"
}
function Prepare-Zbx-Agent-Plugins-Config {
Write-Host "** Preparing Zabbix agent 2 (plugins) configuration files"
Update-Config-Var "$env:ZABBIX_CONF_DIR\zabbix_agent2.d\plugins.d\mongodb.conf" "Plugins.MongoDB.System.Path" "$env:ZABBIX_USER_HOME_DIR\zabbix-agent2-plugin\mongodb.exe"
Update-Config-Var "$env:ZABBIX_CONF_DIR\zabbix_agent2.d\plugins.d\postgresql.conf" "Plugins.PostgreSQL.System.Path" "$env:ZABBIX_USER_HOME_DIR\zabbix-agent2-plugin\postgresql.exe"
Update-Config-Var "$env:ZABBIX_CONF_DIR\zabbix_agent2.d\plugins.d\mssql.conf" "Plugins.MSSQL.System.Path" "$env:ZABBIX_USER_HOME_DIR\zabbix-agent2-plugin\mssql.exe"
Update-Config-Var "$env:ZABBIX_CONF_DIR\zabbix_agent2.d\plugins.d\ember.conf" "Plugins.EmberPlus.System.Path" "$env:ZABBIX_USER_HOME_DIR\zabbix-agent2-plugin\ember-plus.exe"
if (Get-Command nvidia-smi.exe -errorAction SilentlyContinue) {
Update-Config-Var "$env:ZABBIX_CONF_DIR\zabbix_agent2.d\plugins.d\nvidia.conf" "Plugins.NVIDIA.System.Path" "$env:ZABBIX_USER_HOME_DIR\zabbix-agent2-plugin\nvidia-gpu.exe"
}
}
function ClearZbxEnv() {
if ($env:ZBX_CLEAR_ENV -eq "false") {
return
}
$env_vars=Get-ChildItem env:* | Where-Object {$_.Name -match "^ZABBIX_.*" } | foreach { $_.Name }
foreach ($env_var in $env_vars) {
Set-Item env:$env_var -Value $null
}
}
function PrepareAgent {
Write-Host "** Preparing Zabbix agent 2"
Prepare-Zbx-Agent-Config
Prepare-Zbx-Agent-Plugins-Config
ClearZbxEnv
}
$commandArgs = $args
if ($args.length -gt 0 -And $args[0].StartsWith('-')) {
$commandArgs = @("C:\zabbix\sbin\zabbix_agent2.exe") + $args
}
if ($commandArgs.length -gt 0 -And $commandArgs[0] -eq "C:\zabbix\sbin\zabbix_agent2.exe") {
PrepareAgent
}
if ($commandArgs.length -gt 0) {
$exe, $exeArgs = $commandArgs
& $exe @exeArgs
exit $LASTEXITCODE
}

View file

@ -27,8 +27,6 @@ LABEL org.opencontainers.image.title="Zabbix agent build (Windows)" `
org.opencontainers.image.version="${ZBX_VERSION}" `
org.opencontainers.image.source="${ZBX_SOURCES}"
ADD config_templates C:\config_templates
SHELL ["powershell", "-Command", "$ErrorActionPreference = 'Stop'; $ProgressPreference = 'SilentlyContinue';"]
RUN Set-Location -Path $env:SystemDrive\.; `
@ -76,7 +74,6 @@ RUN Set-Location -Path $env:SystemDrive\.; `
Copy-Item -Path $env:ZBX_SOURCES_DIR\bin\win64\zabbix_get.exe $env:ZBX_OUTPUT_DIR\bin; `
Copy-Item -Path $env:ZBX_SOURCES_DIR\bin\win64\zabbix_sender.exe $env:ZBX_OUTPUT_DIR\bin; `
Copy-Item -Path $env:ZBX_SOURCES_DIR\conf\zabbix_agentd.win.conf $env:ZBX_OUTPUT_DIR\conf\zabbix_agentd.conf_template; `
Copy-Item -Recurse -Path $env:SystemDrive\config_templates\agent\* $env:ZBX_OUTPUT_DIR\conf\; `
nmake /S -f Makefile `
PCRE2INCDIR=$env:SystemDrive\build_output\pcre2\include `
PCRE2LIBDIR=$env:SystemDrive\build_output\pcre2\lib `

View file

@ -39,8 +39,6 @@ LABEL org.opencontainers.image.title="Zabbix agent 2 build (Windows)" `
org.opencontainers.image.version="${ZBX_VERSION}" `
org.opencontainers.image.source="${ZBX_SOURCES}"
ADD config_templates C:\config_templates
SHELL ["powershell", "-Command", "$ErrorActionPreference = 'Stop'; $ProgressPreference = 'SilentlyContinue';"]
RUN Set-Location -Path $env:SystemDrive\.; `
@ -78,7 +76,6 @@ RUN Set-Location -Path $env:SystemDrive\.; `
Copy-Item -Path $env:ZBX_SOURCES_DIR\bin\win64\zabbix_agent2.exe $env:ZBX_OUTPUT_DIR\sbin; `
Copy-Item -Path $env:ZBX_SOURCES_DIR\src\go\conf\zabbix_agent2.win.conf $env:ZBX_OUTPUT_DIR\conf\zabbix_agent2.conf_template; `
Copy-Item -Recurse -Path $env:ZBX_SOURCES_DIR\src\go\conf\zabbix_agent2.d $env:ZBX_OUTPUT_DIR\conf\zabbix_agent2.d; `
Copy-Item -Recurse -Path $env:SystemDrive\config_templates\agent2\* $env:ZBX_OUTPUT_DIR\conf; `
mingw32-make -s clean;
RUN function Build-Plugin { `

View file

@ -0,0 +1,40 @@
//go:build windows
package main
import (
config "github.com/zabbix/zabbix-docker/templates/entrypoints/internal/agent"
"github.com/zabbix/zabbix-docker/templates/entrypoints/internal/bootstrap"
"github.com/zabbix/zabbix-docker/templates/entrypoints/internal/hooks"
)
func prepareService(env bootstrap.Environment) error {
bootstrap.LogInfo("** Preparing Zabbix agent")
homeDir, configDir, err := bootstrap.RequiredDirectories(env)
if err != nil {
return err
}
config.ConfigureServers(env)
if err := config.ConfigureAllowDenyKeys(env, configDir, "zabbix_agentd_item_keys.conf"); err != nil {
return err
}
if err := config.ProcessTLSFiles(env, homeDir); err != nil {
return err
}
if err := hooks.Run(env); err != nil {
return err
}
config.ClearPrivateEnv(env)
return nil
}
func main() {
bootstrap.ExitOnError(bootstrap.RunService(agentBinary, prepareService))
}

View file

@ -0,0 +1,3 @@
package main
const agentBinary = `C:\zabbix\sbin\zabbix_agentd.exe`

View file

@ -0,0 +1,41 @@
package main
import (
"os"
"path/filepath"
"testing"
"github.com/zabbix/zabbix-docker/templates/entrypoints/internal/bootstrap"
)
func TestPrepareServiceWindows(t *testing.T) {
root := t.TempDir()
configDir := filepath.Join(root, "conf")
homeDir := filepath.Join(root, "home")
if err := os.MkdirAll(filepath.Join(homeDir, "enc_internal"), 0o700); err != nil {
t.Fatal(err)
}
if err := os.MkdirAll(configDir, 0o700); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(configDir, "zabbix_agentd_item_keys.conf"), []byte("# DenyKey=system.run[*]\n"), 0o600); err != nil {
t.Fatal(err)
}
env := bootstrap.Environment{
"ZABBIX_CONF_DIR": configDir, "ZABBIX_USER_HOME_DIR": homeDir,
"UNRELATED_VARIABLE": "value",
}
if err := prepareService(env); err != nil {
t.Fatal(err)
}
if env["ZBX_PASSIVESERVERS"] != "zabbix-server" || env["ZBX_ACTIVESERVERS"] != "zabbix-server" {
t.Fatalf("unexpected server configuration: %#v", env)
}
if env["UNRELATED_VARIABLE"] != "value" {
t.Fatal("Windows entrypoint removed an unrelated variable")
}
if _, found := env["ZABBIX_CONF_DIR"]; found {
t.Fatal("ZABBIX_CONF_DIR was not removed")
}
}

View file

@ -0,0 +1,96 @@
//go:build windows
package main
import (
"os/exec"
"path/filepath"
config "github.com/zabbix/zabbix-docker/templates/entrypoints/internal/agent"
"github.com/zabbix/zabbix-docker/templates/entrypoints/internal/bootstrap"
"github.com/zabbix/zabbix-docker/templates/entrypoints/internal/hooks"
)
func prepareService(env bootstrap.Environment) error {
bootstrap.LogInfo("** Preparing Zabbix agent 2")
homeDir, configDir, err := bootstrap.RequiredDirectories(env)
if err != nil {
return err
}
config.ConfigureServers(env)
configureFeatureSwitches(env)
if err := config.ConfigureAllowDenyKeys(env, configDir, "zabbix_agent2_item_keys.conf"); err != nil {
return err
}
if err := config.ProcessTLSFiles(env, homeDir); err != nil {
return err
}
if err := updatePluginConfig(homeDir, configDir); err != nil {
return err
}
if err := hooks.Run(env); err != nil {
return err
}
config.ClearPrivateEnv(env)
return nil
}
func configureFeatureSwitches(env bootstrap.Environment) {
if env["ZBX_ENABLEPERSISTENTBUFFER"] == "true" {
env["ZBX_ENABLEPERSISTENTBUFFER"] = "1"
} else {
delete(env, "ZBX_ENABLEPERSISTENTBUFFER")
delete(env, "ZBX_PERSISTENTBUFFERFILE")
}
if env["ZBX_ENABLESTATUSPORT"] == "true" {
env["ZBX_STATUSPORT"] = env.ValueOrDefaultNonEmpty("ZBX_STATUSPORT", "31999")
} else {
delete(env, "ZBX_STATUSPORT")
}
}
func updatePluginConfig(homeDir, configDir string) error {
bootstrap.LogInfo("** Preparing Zabbix agent 2 plugin configuration files")
configDir = filepath.Join(configDir, "zabbix_agent2.d", "plugins.d")
binDir := pluginBinDir(homeDir)
plugins := []struct {
file, parameter, binary string
}{
{"mongodb.conf", "Plugins.MongoDB.System.Path", "mongodb"},
{"postgresql.conf", "Plugins.PostgreSQL.System.Path", "postgresql"},
{"mssql.conf", "Plugins.MSSQL.System.Path", "mssql"},
{"ember.conf", "Plugins.EmberPlus.System.Path", "ember-plus"},
}
if _, err := exec.LookPath(nvidiaCommand); err == nil {
plugins = append(plugins, struct {
file, parameter, binary string
}{"nvidia.conf", "Plugins.NVIDIA.System.Path", "nvidia-gpu"})
}
for _, plugin := range plugins {
if err := bootstrap.UpdateConfigValue(
filepath.Join(configDir, plugin.file),
plugin.parameter,
filepath.Join(binDir, plugin.binary+pluginExecSuffix),
); err != nil {
return err
}
}
return nil
}
func main() {
bootstrap.ExitOnError(bootstrap.RunService(agent2Binary, prepareService))
}

View file

@ -0,0 +1,15 @@
package main
import (
"path/filepath"
)
const (
agent2Binary = `C:\zabbix\sbin\zabbix_agent2.exe`
nvidiaCommand = "nvidia-smi.exe"
pluginExecSuffix = ".exe"
)
func pluginBinDir(homeDir string) string {
return filepath.Join(homeDir, "zabbix-agent2-plugin")
}

View file

@ -0,0 +1,76 @@
package main
import (
"os"
"path/filepath"
"strings"
"testing"
"github.com/zabbix/zabbix-docker/templates/entrypoints/internal/bootstrap"
)
func TestPrepareServiceWindows(t *testing.T) {
t.Setenv("PATH", t.TempDir())
root := t.TempDir()
configDir := filepath.Join(root, "conf")
homeDir := filepath.Join(root, "home")
pluginDirectory := filepath.Join(configDir, "zabbix_agent2.d", "plugins.d")
if err := os.MkdirAll(pluginDirectory, 0o700); err != nil {
t.Fatal(err)
}
if err := os.MkdirAll(filepath.Join(homeDir, "enc_internal"), 0o700); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(configDir, "zabbix_agent2_item_keys.conf"), []byte("# DenyKey=system.run[*]\n"), 0o600); err != nil {
t.Fatal(err)
}
for _, name := range []string{"mongodb.conf", "postgresql.conf", "mssql.conf", "ember.conf"} {
if err := os.WriteFile(filepath.Join(pluginDirectory, name), []byte("# plugin config\n"), 0o600); err != nil {
t.Fatal(err)
}
}
env := bootstrap.Environment{
"ZABBIX_CONF_DIR": configDir, "ZABBIX_USER_HOME_DIR": homeDir,
"ZBX_ENABLESTATUSPORT": "true", "ZBX_STATUSPORT": "12345",
"UNRELATED_VARIABLE": "value",
}
if err := prepareService(env); err != nil {
t.Fatal(err)
}
if env["ZBX_STATUSPORT"] != "12345" {
t.Fatalf("unexpected Windows status port: %q", env["ZBX_STATUSPORT"])
}
if env["UNRELATED_VARIABLE"] != "value" {
t.Fatal("Windows entrypoint removed an unrelated variable")
}
data, err := os.ReadFile(filepath.Join(pluginDirectory, "mongodb.conf"))
if err != nil {
t.Fatal(err)
}
want := filepath.Join(homeDir, "zabbix-agent2-plugin", "mongodb.exe")
if !strings.Contains(string(data), "Plugins.MongoDB.System.Path="+want) {
t.Fatalf("MongoDB plugin path is missing from config: %s", data)
}
}
func TestFeatureSwitchesRequireLowercaseTrue(t *testing.T) {
env := bootstrap.Environment{
"ZBX_ENABLEPERSISTENTBUFFER": "TRUE",
"ZBX_PERSISTENTBUFFERFILE": `C:\zabbix\buffer\agent2.db`,
"ZBX_ENABLESTATUSPORT": "false",
"ZBX_STATUSPORT": "31999",
}
configureFeatureSwitches(env)
if _, found := env["ZBX_PERSISTENTBUFFERFILE"]; found {
t.Fatal("persistent buffer file was retained")
}
if _, found := env["ZBX_ENABLEPERSISTENTBUFFER"]; found {
t.Fatal("persistent buffer was enabled by a case-insensitive value")
}
if _, found := env["ZBX_STATUSPORT"]; found {
t.Fatal("status port was retained")
}
}

View file

@ -0,0 +1,3 @@
module github.com/zabbix/zabbix-docker/templates/entrypoints
go 1.25.0

View file

@ -0,0 +1,87 @@
//go:build windows
// Package agent prepares the runtime environment for Zabbix agent and
// agent 2.
package agent
import (
"path/filepath"
"strings"
"github.com/zabbix/zabbix-docker/templates/entrypoints/internal/bootstrap"
)
// ConfigureServers merges ZBX_SERVER_HOST and ZBX_SERVER_PORT into the
// passive and active server lists, honouring the ZBX_PASSIVE_ALLOW and
// ZBX_ACTIVE_ALLOW switches.
func ConfigureServers(env bootstrap.Environment) {
serverHost := env.ValueOrDefault("ZBX_SERVER_HOST", "zabbix-server")
serverPort := env.ValueOrDefault("ZBX_SERVER_PORT", "10051")
passiveServers := env["ZBX_PASSIVESERVERS"]
activeServers := env["ZBX_ACTIVESERVERS"]
activeServer := serverHost
if serverPort != "" && serverPort != "10051" {
activeServer += ":" + serverPort
}
if serverHost != "" {
passiveServers = prependServer(serverHost, passiveServers)
activeServers = prependServer(activeServer, activeServers)
}
if v := env["ZBX_PASSIVE_ALLOW"]; (v == "" || strings.EqualFold(v, "true")) && passiveServers != "" {
bootstrap.LogInfo("** Using '%s' servers for passive checks", passiveServers)
env["ZBX_PASSIVESERVERS"] = passiveServers
} else {
delete(env, "ZBX_PASSIVESERVERS")
}
if v := env["ZBX_ACTIVE_ALLOW"]; (v == "" || strings.EqualFold(v, "true")) && activeServers != "" {
bootstrap.LogInfo("** Using '%s' servers for active checks", activeServers)
env["ZBX_ACTIVESERVERS"] = activeServers
} else {
delete(env, "ZBX_ACTIVESERVERS")
}
delete(env, "ZBX_SERVER_HOST")
delete(env, "ZBX_SERVER_PORT")
}
// ConfigureAllowDenyKeys writes ZBX_DENYKEY and ZBX_ALLOWKEY into the item key
// configuration file.
func ConfigureAllowDenyKeys(env bootstrap.Environment, configDir, fileName string) error {
path := filepath.Join(configDir, fileName)
if err := bootstrap.UpdateConfigMultiple(path, "DenyKey", env["ZBX_DENYKEY"]); err != nil {
return err
}
return bootstrap.UpdateConfigMultiple(path, "AllowKey", env["ZBX_ALLOWKEY"])
}
// ProcessTLSFiles persists the agent TLS material from the
// environment into files.
func ProcessTLSFiles(env bootstrap.Environment, homeDir string) error {
return bootstrap.ProcessTLSFiles(
env,
homeDir,
"ZBX_TLSCA",
"ZBX_TLSCRL",
"ZBX_TLSCERT",
"ZBX_TLSKEY",
"ZBX_TLSPSK",
)
}
// ClearPrivateEnv drops internal ZABBIX_* variables before the
// agent starts.
func ClearPrivateEnv(env bootstrap.Environment) {
bootstrap.ClearPrivateEnv(env, "ZABBIX_")
}
func prependServer(server, servers string) string {
if servers == "" {
return server
}
return server + "," + servers
}

View file

@ -0,0 +1,20 @@
//go:build windows
package agent
import (
"testing"
"github.com/zabbix/zabbix-docker/templates/entrypoints/internal/bootstrap"
)
func TestConfigureServers(t *testing.T) {
env := bootstrap.Environment{
"ZBX_SERVER_HOST": "server", "ZBX_SERVER_PORT": "10061",
"ZBX_PASSIVESERVERS": "passive", "ZBX_ACTIVESERVERS": "active",
}
ConfigureServers(env)
if env["ZBX_PASSIVESERVERS"] != "server,passive" || env["ZBX_ACTIVESERVERS"] != "server:10061,active" {
t.Fatalf("unexpected servers: %#v", env)
}
}

View file

@ -0,0 +1,75 @@
//go:build windows
package bootstrap
import (
"errors"
"fmt"
"os"
"os/exec"
"strings"
)
// ExitCode maps err to a process exit status, preserving the status of a
// finished child process.
func ExitCode(err error) int {
if err == nil {
return 0
}
var exitError *exec.ExitError
if errors.As(err, &exitError) {
return exitError.ExitCode()
}
return 1
}
// ExitOnError logs err and terminates the process with a matching exit
// status. A nil error is ignored.
func ExitOnError(err error) {
if err == nil {
return
}
LogError("**** %v", err)
os.Exit(ExitCode(err))
}
// Command decides what the container should execute: no arguments start the
// service binary, arguments beginning with a dash are treated as its flags,
// anything else is a user-supplied command.
func Command(args []string, binary string) []string {
if len(args) == 0 {
return []string{binary}
}
if strings.HasPrefix(args[0], "-") {
return append([]string{binary}, args...)
}
return args
}
// Execute hands control over to args with the given environment.
func Execute(args []string, env Environment) error {
if err := Exec(args, env); err != nil {
return fmt.Errorf("execute %s: %w", args[0], err)
}
return nil
}
// RunService implements the common entrypoint flow: when the container is
// about to start the service binary (the image default), prepare is called
// first; custom user commands are executed untouched.
func RunService(binary string, prepare func(Environment) error) error {
env := NewEnvironment(os.Environ())
args := Command(os.Args[1:], binary)
if args[0] == binary {
if err := prepare(env); err != nil {
return err
}
}
return Execute(args, env)
}

View file

@ -0,0 +1,55 @@
//go:build windows
package bootstrap
import (
"errors"
"os"
"os/exec"
"reflect"
"testing"
)
func TestCommand(t *testing.T) {
tests := []struct {
name string
args []string
want []string
}{
{name: "default", want: []string{"component"}},
{name: "component options", args: []string{"--version"}, want: []string{"component", "--version"}},
{name: "custom command", args: []string{"shell", "argument"}, want: []string{"shell", "argument"}},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
if got := Command(test.args, "component"); !reflect.DeepEqual(got, test.want) {
t.Fatalf("Command() = %#v, want %#v", got, test.want)
}
})
}
}
func TestExitCode(t *testing.T) {
if code := ExitCode(nil); code != 0 {
t.Fatalf("ExitCode(nil) = %d, want 0", code)
}
if code := ExitCode(errors.New("failure")); code != 1 {
t.Fatalf("ExitCode(regular error) = %d, want 1", code)
}
command := exec.Command(os.Args[0], "-test.run=TestExitCodeHelperProcess")
command.Env = append(os.Environ(), "ENTRYPOINT_EXIT_CODE_HELPER=1")
err := command.Run()
if code := ExitCode(err); code != 23 {
t.Fatalf("ExitCode(child error) = %d, want 23: %v", code, err)
}
}
func TestExitCodeHelperProcess(t *testing.T) {
if os.Getenv("ENTRYPOINT_EXIT_CODE_HELPER") != "1" {
return
}
os.Exit(23)
}

View file

@ -0,0 +1,124 @@
//go:build windows
package bootstrap
import (
"bytes"
"fmt"
"os"
"strings"
)
// UpdateConfigMultiple replaces the name option in the configuration file
// with one line per comma-separated item of rawValue. An empty value removes
// the option.
func UpdateConfigMultiple(configPath, name, rawValue string) error {
value := strings.Trim(strings.TrimSpace(rawValue), `"`)
if value == "" {
return rewriteConfig(configPath, name, nil, false)
}
items := strings.Split(value, ",")
values := make([]string, 0, len(items))
for _, item := range items {
if item != "" {
values = append(values, item)
}
}
return rewriteConfig(configPath, name, values, true)
}
// UpdateConfigValue sets a single-value option in the configuration file.
func UpdateConfigValue(configPath, name, value string) error {
return rewriteConfig(configPath, name, []string{value}, false)
}
func rewriteConfig(configPath, name string, values []string, preserveExisting bool) error {
data, err := os.ReadFile(configPath)
if err != nil {
return fmt.Errorf("missing configuration file %s: %w", configPath, err)
}
lines := strings.Split(strings.TrimSuffix(string(data), "\n"), "\n")
activePrefix := name + "="
commentPrefixes := []string{"# " + activePrefix, "; " + activePrefix}
output := make([]string, 0, len(lines)+len(values)+1)
existing := make(map[string]struct{})
insertAt := -1
for _, line := range lines {
if strings.HasPrefix(line, activePrefix) {
if preserveExisting {
output = append(output, line)
existing[strings.TrimPrefix(line, activePrefix)] = struct{}{}
insertAt = len(output)
}
continue
}
output = append(output, line)
if insertAt == -1 && hasAnyPrefix(line, commentPrefixes) {
insertAt = len(output)
}
}
newLines := make([]string, 0, len(values))
for _, value := range values {
if value == "" {
continue
}
if _, found := existing[value]; !found {
newLines = append(newLines, activePrefix+value)
}
}
if insertAt >= 0 {
updated := make([]string, 0, len(output)+len(newLines))
updated = append(updated, output[:insertAt]...)
updated = append(updated, newLines...)
updated = append(updated, output[insertAt:]...)
output = updated
} else if len(newLines) > 0 {
if len(output) > 0 && output[len(output)-1] != "" {
output = append(output, "")
}
output = append(output, newLines...)
}
requested := false
for _, value := range values {
if value != "" {
requested = true
break
}
}
updatedData := []byte(strings.Join(output, "\n") + "\n")
changed := !bytes.Equal(data, updatedData)
if changed {
if err := WriteFilePreservingMode(configPath, updatedData); err != nil {
return fmt.Errorf("update configuration file %s: %w", configPath, err)
}
}
if !requested {
if changed {
LogInfo("** Removing %s parameter '%s'", configPath, name)
}
return nil
}
loggedValue := strings.Join(values, ",")
if changed {
LogInfo("** Updating %s parameter '%s': '%s'", configPath, name, loggedValue)
} else {
LogInfo("** Updating %s parameter '%s': '%s'... exists", configPath, name, loggedValue)
}
return nil
}

View file

@ -0,0 +1,101 @@
//go:build windows
package bootstrap
import (
"os"
"path/filepath"
"strings"
"testing"
"time"
)
func TestUpdateConfigMultiple(t *testing.T) {
path := filepath.Join(t.TempDir(), "agent.conf")
if err := os.WriteFile(path, []byte("# DenyKey=system.run[*]\nOther=value\n"), 0o600); err != nil {
t.Fatal(err)
}
if err := UpdateConfigMultiple(path, "DenyKey", `"one,two"`); err != nil {
t.Fatal(err)
}
data, err := os.ReadFile(path)
if err != nil {
t.Fatal(err)
}
want := "# DenyKey=system.run[*]\nDenyKey=one\nDenyKey=two\nOther=value\n"
if string(data) != want {
t.Fatalf("config:\n%s\nwant:\n%s", data, want)
}
info, err := os.Stat(path)
if err != nil {
t.Fatal(err)
}
if info.Mode().Perm() != 0o600 {
t.Fatalf("config mode = %o, want 600", info.Mode().Perm())
}
}
func TestUpdateConfigMultiplePreservesActiveValues(t *testing.T) {
path := filepath.Join(t.TempDir(), "agent.conf")
if err := os.WriteFile(path, []byte("DenyKey=existing\n# DenyKey=system.run[*]\n"), 0o600); err != nil {
t.Fatal(err)
}
if err := UpdateConfigMultiple(path, "DenyKey", "existing,new"); err != nil {
t.Fatal(err)
}
data, err := os.ReadFile(path)
if err != nil {
t.Fatal(err)
}
content := string(data)
if strings.Count(content, "DenyKey=existing\n") != 1 {
t.Fatalf("existing value was removed or duplicated:\n%s", data)
}
if !strings.Contains(content, "DenyKey=new\n") {
t.Fatalf("new value was not added:\n%s", data)
}
}
func TestUpdateConfigMultipleRemovesActiveValuesWhenEmpty(t *testing.T) {
path := filepath.Join(t.TempDir(), "agent.conf")
if err := os.WriteFile(path, []byte("DenyKey=system.run[*]\nOther=value\n"), 0o600); err != nil {
t.Fatal(err)
}
if err := UpdateConfigMultiple(path, "DenyKey", ""); err != nil {
t.Fatal(err)
}
data, err := os.ReadFile(path)
if err != nil {
t.Fatal(err)
}
if strings.Contains(string(data), "DenyKey=") {
t.Fatalf("active DenyKey was not removed:\n%s", data)
}
}
func TestUpdateConfigMultipleDoesNotRewriteUnchangedConfig(t *testing.T) {
path := filepath.Join(t.TempDir(), "agent.conf")
data := []byte("# DenyKey=system.run[*]\nOther=value\n")
if err := os.WriteFile(path, data, 0o600); err != nil {
t.Fatal(err)
}
modified := time.Unix(1_700_000_000, 0)
if err := os.Chtimes(path, modified, modified); err != nil {
t.Fatal(err)
}
if err := UpdateConfigMultiple(path, "DenyKey", ""); err != nil {
t.Fatal(err)
}
info, err := os.Stat(path)
if err != nil {
t.Fatal(err)
}
if !info.ModTime().Equal(modified) {
t.Fatalf("unchanged configuration was rewritten: modification time is %s", info.ModTime())
}
}

View file

@ -0,0 +1,159 @@
//go:build windows
// Package bootstrap provides the shared building blocks of the container
// entrypoints: process environment handling, Zabbix configuration file
// updates, logging and the final hand-off to the service binary.
package bootstrap
import (
"fmt"
"os"
"path/filepath"
"sort"
"strings"
)
// Environment is a mutable set of environment variables keyed by name.
// The entrypoint modifies it while preparing a service and passes the
// result to the final process.
type Environment map[string]string
// NewEnvironment parses "NAME=value" pairs as returned by os.Environ.
func NewEnvironment(values []string) Environment {
env := make(Environment, len(values))
for _, item := range values {
name, value, found := strings.Cut(item, "=")
if found {
env[name] = value
}
}
return env
}
// List returns the variables as sorted "NAME=value" pairs suitable for exec.
func (env Environment) List() []string {
names := make([]string, 0, len(env))
for name := range env {
names = append(names, name)
}
sort.Strings(names)
values := make([]string, 0, len(names))
for _, name := range names {
values = append(values, name+"="+env[name])
}
return values
}
// ValueOrDefault returns the value of name, or defaultValue when the
// variable is not present. An empty value counts as present; use
// ValueOrDefaultNonEmpty to treat it as missing.
func (env Environment) ValueOrDefault(name, defaultValue string) string {
value, found := env[name]
if !found {
return defaultValue
}
return value
}
// ValueOrDefaultNonEmpty returns the value of name, or defaultValue when
// the variable is missing or empty.
func (env Environment) ValueOrDefaultNonEmpty(name, defaultValue string) string {
if value := env[name]; value != "" {
return value
}
return defaultValue
}
// RequiredHomeDirectory returns the Zabbix home directory,
// verifying that it exists.
func RequiredHomeDirectory(env Environment) (string, error) {
return requiredDirectory(env, "ZABBIX_USER_HOME_DIR")
}
// RequiredDirectories returns the Zabbix home and config directories
// (ZABBIX_USER_HOME_DIR and ZABBIX_CONF_DIR), verifying that both exist.
func RequiredDirectories(env Environment) (homeDir, configDir string, err error) {
homeDir, err = RequiredHomeDirectory(env)
if err != nil {
return "", "", err
}
configDir, err = requiredDirectory(env, "ZABBIX_CONF_DIR")
if err != nil {
return "", "", err
}
return homeDir, configDir, nil
}
// requiredDirectory returns directory from env variable.
func requiredDirectory(env Environment, name string) (string, error) {
directory := env[name]
if directory == "" {
return "", fmt.Errorf("%s must be set", name)
}
info, err := os.Stat(directory)
if err != nil {
return "", fmt.Errorf("access %s directory %s: %w", name, directory, err)
}
if !info.IsDir() {
return "", fmt.Errorf("%s path %s is not a directory", name, directory)
}
return directory, nil
}
// ProcessFileFromEnvironment persists the value of variable into a file
// under directory and points the corresponding "<variable>FILE" variable at
// it. The plain variable is always removed so that secrets do not stay in
// the service environment.
func ProcessFileFromEnvironment(env Environment, directory, variable string) error {
fileVariable := variable + "FILE"
if value := env[variable]; value != "" {
path := filepath.Join(directory, fileVariable)
if err := os.WriteFile(path, []byte(value), 0o600); err != nil {
return fmt.Errorf("write %s: %w", path, err)
}
env[fileVariable] = path
}
delete(env, variable)
return nil
}
// ProcessTLSFiles moves TLS material from the listed variables into
// files under <home>/enc_internal, so that Zabbix reads certificates and
// keys from disk instead of the environment.
func ProcessTLSFiles(env Environment, homeDir string, variables ...string) error {
directory := filepath.Join(homeDir, "enc_internal")
for _, variable := range variables {
if err := ProcessFileFromEnvironment(env, directory, variable); err != nil {
return err
}
}
return nil
}
// ClearPrivateEnv removes variables with the supplied prefixes that the
// service should not inherit. ZBX_CLEAR_ENV=false disables the cleanup.
func ClearPrivateEnv(env Environment, prefixes ...string) {
if env["ZBX_CLEAR_ENV"] == "false" {
return
}
for name := range env {
if hasAnyPrefix(name, prefixes) {
delete(env, name)
}
}
}
func hasAnyPrefix(value string, prefixes []string) bool {
for _, prefix := range prefixes {
if strings.HasPrefix(value, prefix) {
return true
}
}
return false
}

View file

@ -0,0 +1,101 @@
//go:build windows
package bootstrap
import (
"os"
"path/filepath"
"strings"
"testing"
)
func TestEnvironmentDefaults(t *testing.T) {
env := Environment{"EMPTY": "", "VALUE": "configured"}
if got := env.ValueOrDefault("EMPTY", "default"); got != "" {
t.Fatalf("ValueOrDefault() = %q, want empty value", got)
}
if got := env.ValueOrDefaultNonEmpty("EMPTY", "default"); got != "default" {
t.Fatalf("ValueOrDefaultNonEmpty() = %q, want default", got)
}
if got := env.ValueOrDefaultNonEmpty("VALUE", "default"); got != "configured" {
t.Fatalf("ValueOrDefaultNonEmpty() = %q, want configured", got)
}
}
func TestProcessFileAndClearEnvironment(t *testing.T) {
directory := t.TempDir()
env := Environment{
"ZBX_TLSPSK": "secret", "ZABBIX_CONF_DIR": `C:\zabbix\conf`,
"UNRELATED_VARIABLE": "value", "VALUE": "a=b",
}
if err := ProcessFileFromEnvironment(env, directory, "ZBX_TLSPSK"); err != nil {
t.Fatal(err)
}
data, err := os.ReadFile(filepath.Join(directory, "ZBX_TLSPSKFILE"))
if err != nil || string(data) != "secret" {
t.Fatalf("TLS file: %q, %v", data, err)
}
if env["ZBX_TLSPSKFILE"] != filepath.Join(directory, "ZBX_TLSPSKFILE") {
t.Fatalf("unexpected TLS file path: %q", env["ZBX_TLSPSKFILE"])
}
if _, found := env["ZBX_TLSPSK"]; found {
t.Fatal("ZBX_TLSPSK was not removed")
}
ClearPrivateEnv(env, "ZABBIX_")
if env["UNRELATED_VARIABLE"] != "value" {
t.Fatal("unrelated variable was unexpectedly removed")
}
if !strings.Contains(strings.Join(env.List(), "\n"), "VALUE=a=b") {
t.Fatalf("environment list: %q", env.List())
}
}
func TestClearPrivateEnvWithPrefixes(t *testing.T) {
env := Environment{
"ZABBIX_CONF_DIR": `C:\zabbix\conf`,
"UNRELATED_VARIABLE": "value",
}
ClearPrivateEnv(env, "ZABBIX_")
if _, found := env["ZABBIX_CONF_DIR"]; found {
t.Fatal("ZABBIX_CONF_DIR was not removed")
}
if env["UNRELATED_VARIABLE"] != "value" {
t.Fatal("unrelated variable was unexpectedly removed")
}
}
func TestRequiredHomeDirectory(t *testing.T) {
homeDir := t.TempDir()
got, err := RequiredHomeDirectory(Environment{"ZABBIX_USER_HOME_DIR": homeDir})
if err != nil {
t.Fatal(err)
}
if got != homeDir {
t.Fatalf("RequiredHomeDirectory() = %q, want %q", got, homeDir)
}
}
func TestRequiredHomeDirectoryRejectsInvalidPaths(t *testing.T) {
filePath := filepath.Join(t.TempDir(), "home")
if err := os.WriteFile(filePath, nil, 0o600); err != nil {
t.Fatal(err)
}
tests := []struct {
name string
path string
}{
{name: "missing"},
{name: "not found", path: filepath.Join(t.TempDir(), "missing")},
{name: "regular file", path: filePath},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
if _, err := RequiredHomeDirectory(Environment{"ZABBIX_USER_HOME_DIR": test.path}); err == nil {
t.Fatal("RequiredHomeDirectory() unexpectedly succeeded")
}
})
}
}

View file

@ -0,0 +1,18 @@
package bootstrap
import (
"os"
"os/exec"
)
// Exec runs args as a child process wired to the entrypoint standard
// streams; Windows has no execve equivalent.
func Exec(args []string, env Environment) error {
command := exec.Command(args[0], args[1:]...)
command.Env = env.List()
command.Stdin = os.Stdin
command.Stdout = os.Stdout
command.Stderr = os.Stderr
return command.Run()
}

View file

@ -0,0 +1,44 @@
//go:build windows
package bootstrap
import (
"os"
"path/filepath"
)
// WriteFilePreservingMode atomically replaces a file while preserving its
// permission bits.
func WriteFilePreservingMode(path string, data []byte) error {
resolvedPath, err := filepath.EvalSymlinks(path)
if err != nil {
return err
}
info, err := os.Stat(resolvedPath)
if err != nil {
return err
}
temporary, err := os.CreateTemp(filepath.Dir(resolvedPath), "."+filepath.Base(resolvedPath)+".tmp-*")
if err != nil {
return err
}
defer os.Remove(temporary.Name())
defer temporary.Close()
if err := temporary.Chmod(info.Mode().Perm()); err != nil {
return err
}
if _, err := temporary.Write(data); err != nil {
return err
}
if err := temporary.Sync(); err != nil {
return err
}
if err := temporary.Close(); err != nil {
return err
}
return os.Rename(temporary.Name(), resolvedPath)
}

View file

@ -0,0 +1,38 @@
//go:build windows
package bootstrap
import (
"fmt"
"os"
"time"
)
// LogInfo writes a timestamped informational message to stdout.
func LogInfo(format string, args ...any) {
logMessage(os.Stdout, "info", format, args...)
}
// LogDebug writes a debug message to stdout when DEBUG_MODE=true.
func LogDebug(env Environment, format string, args ...any) {
if env["DEBUG_MODE"] != "true" {
return
}
logMessage(os.Stdout, "debug", format, args...)
}
// LogWarn writes a warning message to stderr.
func LogWarn(format string, args ...any) {
logMessage(os.Stderr, "warning", format, args...)
}
// LogError writes an error message to stderr.
func LogError(format string, args ...any) {
logMessage(os.Stderr, "error", format, args...)
}
func logMessage(file *os.File, level, format string, args ...any) {
timestamp := time.Now().UTC().Format("2006-01-02T15:04:05Z")
fmt.Fprintf(file, "%s [%s]: %s\n", timestamp, level, fmt.Sprintf(format, args...))
}

View file

@ -0,0 +1,18 @@
package hooks
import (
"os"
"path/filepath"
"strings"
)
func command(path string, _ os.FileMode) ([]string, bool) {
switch strings.ToLower(filepath.Ext(path)) {
case ".ps1":
return []string{"pwsh.exe", "-NoLogo", "-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-File", path}, true
case ".cmd", ".bat":
return []string{"cmd.exe", "/D", "/S", "/C", path}, true
default:
return nil, false
}
}

View file

@ -0,0 +1,27 @@
//go:build windows
package hooks
import (
"os"
"reflect"
"testing"
)
func TestPowerShellCommand(t *testing.T) {
path := `C:\zabbix\entrypoint.d\10-custom.ps1`
args, supported := command(path, os.FileMode(0))
want := []string{"pwsh.exe", "-NoLogo", "-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-File", path}
if !supported || !reflect.DeepEqual(args, want) {
t.Fatalf("command() = %#v, %v; want %#v, true", args, supported, want)
}
}
func TestCmdCommand(t *testing.T) {
path := `C:\zabbix\entrypoint.d\20-custom.cmd`
args, supported := command(path, os.FileMode(0))
want := []string{"cmd.exe", "/D", "/S", "/C", path}
if !supported || !reflect.DeepEqual(args, want) {
t.Fatalf("command() = %#v, %v; want %#v, true", args, supported, want)
}
}

View file

@ -0,0 +1,64 @@
//go:build windows
// Package hooks runs user-provided scripts from the entrypoint.d directory
// before the service starts.
package hooks
import (
"fmt"
"os"
"os/exec"
"path/filepath"
"github.com/zabbix/zabbix-docker/templates/entrypoints/internal/bootstrap"
)
const directoryName = "entrypoint.d"
// Run executes supported PowerShell and cmd scripts from <home>/entrypoint.d
// in file name order. Everything else is skipped. The first failing hook
// aborts the entrypoint.
func Run(env bootstrap.Environment) error {
homeDir, err := bootstrap.RequiredHomeDirectory(env)
if err != nil {
return err
}
directory := filepath.Join(homeDir, directoryName)
entries, err := os.ReadDir(directory)
if os.IsNotExist(err) {
return nil
}
if err != nil {
return fmt.Errorf("read entrypoint hooks directory %s: %w", directory, err)
}
for _, entry := range entries {
path := filepath.Join(directory, entry.Name())
info, err := os.Stat(path)
if err != nil {
return fmt.Errorf("inspect entrypoint hook %s: %w", path, err)
}
if !info.Mode().IsRegular() {
continue
}
args, supported := command(path, info.Mode())
if !supported {
continue
}
bootstrap.LogInfo("** Running entrypoint hook: %s", path)
hook := exec.Command(args[0], args[1:]...)
hook.Env = env.List()
hook.Stdin = os.Stdin
hook.Stdout = os.Stdout
hook.Stderr = os.Stderr
if err := hook.Run(); err != nil {
return fmt.Errorf("entrypoint hook %s failed: %w", path, err)
}
}
return nil
}

View file

@ -0,0 +1,76 @@
//go:build windows
package hooks
import (
"os"
"path/filepath"
"strings"
"testing"
"github.com/zabbix/zabbix-docker/templates/entrypoints/internal/bootstrap"
)
func TestRunExecutesHooksInOrder(t *testing.T) {
homeDir := t.TempDir()
directory := filepath.Join(homeDir, directoryName)
if err := os.Mkdir(directory, 0o700); err != nil {
t.Fatal(err)
}
output := filepath.Join(homeDir, "output")
for name, content := range map[string]string{
"20-second.cmd": "@echo second:%ZABBIX_CONF_DIR%>>\"%HOOK_OUTPUT%\"\r\n",
"10-first.cmd": "@echo first:%ZABBIX_CONF_DIR%>>\"%HOOK_OUTPUT%\"\r\n",
"30-ignored.txt": "@exit /b 1\r\n",
} {
if err := os.WriteFile(filepath.Join(directory, name), []byte(content), 0o600); err != nil {
t.Fatal(err)
}
}
env := bootstrap.NewEnvironment(os.Environ())
env["ZABBIX_USER_HOME_DIR"] = homeDir
env["ZABBIX_CONF_DIR"] = `C:\zabbix\conf`
env["HOOK_OUTPUT"] = output
if err := Run(env); err != nil {
t.Fatal(err)
}
data, err := os.ReadFile(output)
if err != nil {
t.Fatal(err)
}
got := strings.ReplaceAll(string(data), "\r\n", "\n")
want := "first:C:\\zabbix\\conf\nsecond:C:\\zabbix\\conf\n"
if got != want {
t.Fatalf("hook output = %q, want %q", got, want)
}
}
func TestRunReturnsHookFailure(t *testing.T) {
homeDir := t.TempDir()
directory := filepath.Join(homeDir, directoryName)
if err := os.Mkdir(directory, 0o700); err != nil {
t.Fatal(err)
}
path := filepath.Join(directory, "10-fail.cmd")
if err := os.WriteFile(path, []byte("@exit /b 7\r\n"), 0o600); err != nil {
t.Fatal(err)
}
env := bootstrap.NewEnvironment(os.Environ())
env["ZABBIX_USER_HOME_DIR"] = homeDir
err := Run(env)
if err == nil || !strings.Contains(err.Error(), "10-fail.cmd") {
t.Fatalf("unexpected error: %v", err)
}
}
func TestRunIgnoresMissingDirectory(t *testing.T) {
err := Run(bootstrap.Environment{"ZABBIX_USER_HOME_DIR": t.TempDir()})
if err != nil {
t.Fatal(err)
}
}

View file

@ -0,0 +1,58 @@
Go entrypoint third-party notices
=================================
Component: Go standard library
License: BSD-3-Clause
Source: https://go.googlesource.com/go
Copyright 2009 The Go Authors.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above
copyright notice, this list of conditions and the following disclaimer
in the documentation and/or other materials provided with the
distribution.
* Neither the name of Google LLC nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
Additional IP Rights Grant (Patents)
------------------------------------
"This implementation" means the copyrightable works distributed by
Google as part of the Go project.
Google hereby grants to You a perpetual, worldwide, non-exclusive,
no-charge, royalty-free, irrevocable (except as stated in this section)
patent license to make, have made, use, offer to sell, sell, import,
transfer and otherwise run, modify and propagate the contents of this
implementation of Go, where such license applies only to those patent
claims, both currently owned or controlled by Google and acquired in the
future, licensable by Google that are necessarily infringed by this
implementation of Go. This grant does not include claims that would be
infringed only as a consequence of further modification of this
implementation. If you or your agent or exclusive licensee institute or
order or agree to the institution of patent litigation against any
entity (including a cross-claim or counterclaim in a lawsuit) alleging
that this implementation of Go or any code incorporated within this
implementation of Go constitutes direct or contributory patent
infringement, or inducement of patent infringement, then any patent
rights granted to you under this License for this implementation of Go
shall terminate as of the date such litigation is filed.