From 0866f929e2abd51af0a2d9dbab7f8ba1f7eafea9 Mon Sep 17 00:00:00 2001 From: Alexey Pustovalov Date: Wed, 15 Jul 2026 17:51:41 +0900 Subject: [PATCH] Using go entrypoint for Windows agents --- .github/workflows/images_build.yml | 3 +- .github/workflows/images_build_rhel.yml | 4 + .github/workflows/images_build_windows.yml | 10 +- Dockerfiles/agent/windows/Dockerfile | 20 +- .../agent/windows/Dockerfile.dockerignore | 19 ++ .../agent/windows/docker-entrypoint.ps1 | 233 ---------------- Dockerfiles/agent2/windows/Dockerfile | 20 +- .../agent2/windows/Dockerfile.dockerignore | 19 ++ .../agent2/windows/docker-entrypoint.ps1 | 263 ------------------ .../build-mysql/windows/Dockerfile.agent | 3 - .../build-mysql/windows/Dockerfile.agent2 | 3 - .../config/agent2_windows}/zabbix_agent2.conf | 0 .../zabbix_agent2_active_checks.conf | 0 .../zabbix_agent2_aliases.conf | 0 .../zabbix_agent2_item_keys.conf | 0 .../zabbix_agent2_locations.conf | 0 .../zabbix_agent2_logging.conf | 0 .../zabbix_agent2_network.conf | 0 .../zabbix_agent2_passive_checks.conf | 0 .../zabbix_agent2_timeouts.conf | 0 .../agent2_windows}/zabbix_agent2_tls.conf | 0 .../zabbix_agent2_user_parameters.conf | 0 .../config/agent_windows}/zabbix_agentd.conf | 0 .../zabbix_agentd_active_checks.conf | 0 .../agent_windows}/zabbix_agentd_aliases.conf | 0 .../zabbix_agentd_item_keys.conf | 0 .../agent_windows}/zabbix_agentd_logging.conf | 0 .../agent_windows}/zabbix_agentd_network.conf | 0 .../zabbix_agentd_passive_checks.conf | 0 .../zabbix_agentd_perf_counters.conf | 0 .../zabbix_agentd_timeouts.conf | 0 .../agent_windows}/zabbix_agentd_tls.conf | 0 .../zabbix_agentd_user_parameters.conf | 0 templates/entrypoints/cmd/agent/main.go | 40 +++ .../entrypoints/cmd/agent/platform_windows.go | 3 + .../cmd/agent/platform_windows_test.go | 41 +++ templates/entrypoints/cmd/agent2/main.go | 96 +++++++ .../cmd/agent2/platform_windows.go | 15 + .../cmd/agent2/platform_windows_test.go | 76 +++++ templates/entrypoints/go.mod | 3 + .../entrypoints/internal/agent/config.go | 87 ++++++ .../entrypoints/internal/agent/config_test.go | 20 ++ .../entrypoints/internal/bootstrap/command.go | 75 +++++ .../internal/bootstrap/command_test.go | 55 ++++ .../entrypoints/internal/bootstrap/config.go | 124 +++++++++ .../internal/bootstrap/config_test.go | 101 +++++++ .../internal/bootstrap/environment.go | 159 +++++++++++ .../internal/bootstrap/environment_test.go | 101 +++++++ .../internal/bootstrap/exec_windows.go | 18 ++ .../entrypoints/internal/bootstrap/files.go | 44 +++ .../entrypoints/internal/bootstrap/logging.go | 38 +++ .../internal/hooks/command_windows.go | 18 ++ .../internal/hooks/command_windows_test.go | 27 ++ templates/entrypoints/internal/hooks/hooks.go | 64 +++++ .../entrypoints/internal/hooks/hooks_test.go | 76 +++++ .../licenses/go-THIRD-PARTY-NOTICES.txt | 58 ++++ 56 files changed, 1428 insertions(+), 508 deletions(-) create mode 100644 Dockerfiles/agent/windows/Dockerfile.dockerignore delete mode 100644 Dockerfiles/agent/windows/docker-entrypoint.ps1 create mode 100644 Dockerfiles/agent2/windows/Dockerfile.dockerignore delete mode 100644 Dockerfiles/agent2/windows/docker-entrypoint.ps1 rename {Dockerfiles/build-mysql/windows/config_templates/agent2 => templates/config/agent2_windows}/zabbix_agent2.conf (100%) rename {Dockerfiles/build-mysql/windows/config_templates/agent2 => templates/config/agent2_windows}/zabbix_agent2_active_checks.conf (100%) rename {Dockerfiles/build-mysql/windows/config_templates/agent2 => templates/config/agent2_windows}/zabbix_agent2_aliases.conf (100%) rename {Dockerfiles/build-mysql/windows/config_templates/agent2 => templates/config/agent2_windows}/zabbix_agent2_item_keys.conf (100%) rename {Dockerfiles/build-mysql/windows/config_templates/agent2 => templates/config/agent2_windows}/zabbix_agent2_locations.conf (100%) rename {Dockerfiles/build-mysql/windows/config_templates/agent2 => templates/config/agent2_windows}/zabbix_agent2_logging.conf (100%) rename {Dockerfiles/build-mysql/windows/config_templates/agent2 => templates/config/agent2_windows}/zabbix_agent2_network.conf (100%) rename {Dockerfiles/build-mysql/windows/config_templates/agent2 => templates/config/agent2_windows}/zabbix_agent2_passive_checks.conf (100%) rename {Dockerfiles/build-mysql/windows/config_templates/agent2 => templates/config/agent2_windows}/zabbix_agent2_timeouts.conf (100%) rename {Dockerfiles/build-mysql/windows/config_templates/agent2 => templates/config/agent2_windows}/zabbix_agent2_tls.conf (100%) rename {Dockerfiles/build-mysql/windows/config_templates/agent2 => templates/config/agent2_windows}/zabbix_agent2_user_parameters.conf (100%) rename {Dockerfiles/build-mysql/windows/config_templates/agent => templates/config/agent_windows}/zabbix_agentd.conf (100%) rename {Dockerfiles/build-mysql/windows/config_templates/agent => templates/config/agent_windows}/zabbix_agentd_active_checks.conf (100%) rename {Dockerfiles/build-mysql/windows/config_templates/agent => templates/config/agent_windows}/zabbix_agentd_aliases.conf (100%) rename {Dockerfiles/build-mysql/windows/config_templates/agent => templates/config/agent_windows}/zabbix_agentd_item_keys.conf (100%) rename {Dockerfiles/build-mysql/windows/config_templates/agent => templates/config/agent_windows}/zabbix_agentd_logging.conf (100%) rename {Dockerfiles/build-mysql/windows/config_templates/agent => templates/config/agent_windows}/zabbix_agentd_network.conf (100%) rename {Dockerfiles/build-mysql/windows/config_templates/agent => templates/config/agent_windows}/zabbix_agentd_passive_checks.conf (100%) rename {Dockerfiles/build-mysql/windows/config_templates/agent => templates/config/agent_windows}/zabbix_agentd_perf_counters.conf (100%) rename {Dockerfiles/build-mysql/windows/config_templates/agent => templates/config/agent_windows}/zabbix_agentd_timeouts.conf (100%) rename {Dockerfiles/build-mysql/windows/config_templates/agent => templates/config/agent_windows}/zabbix_agentd_tls.conf (100%) rename {Dockerfiles/build-mysql/windows/config_templates/agent => templates/config/agent_windows}/zabbix_agentd_user_parameters.conf (100%) create mode 100644 templates/entrypoints/cmd/agent/main.go create mode 100644 templates/entrypoints/cmd/agent/platform_windows.go create mode 100644 templates/entrypoints/cmd/agent/platform_windows_test.go create mode 100644 templates/entrypoints/cmd/agent2/main.go create mode 100644 templates/entrypoints/cmd/agent2/platform_windows.go create mode 100644 templates/entrypoints/cmd/agent2/platform_windows_test.go create mode 100644 templates/entrypoints/go.mod create mode 100644 templates/entrypoints/internal/agent/config.go create mode 100644 templates/entrypoints/internal/agent/config_test.go create mode 100644 templates/entrypoints/internal/bootstrap/command.go create mode 100644 templates/entrypoints/internal/bootstrap/command_test.go create mode 100644 templates/entrypoints/internal/bootstrap/config.go create mode 100644 templates/entrypoints/internal/bootstrap/config_test.go create mode 100644 templates/entrypoints/internal/bootstrap/environment.go create mode 100644 templates/entrypoints/internal/bootstrap/environment_test.go create mode 100644 templates/entrypoints/internal/bootstrap/exec_windows.go create mode 100644 templates/entrypoints/internal/bootstrap/files.go create mode 100644 templates/entrypoints/internal/bootstrap/logging.go create mode 100644 templates/entrypoints/internal/hooks/command_windows.go create mode 100644 templates/entrypoints/internal/hooks/command_windows_test.go create mode 100644 templates/entrypoints/internal/hooks/hooks.go create mode 100644 templates/entrypoints/internal/hooks/hooks_test.go create mode 100644 templates/entrypoints/licenses/go-THIRD-PARTY-NOTICES.txt diff --git a/.github/workflows/images_build.yml b/.github/workflows/images_build.yml index 882f1e18..66974fdc 100644 --- a/.github/workflows/images_build.yml +++ b/.github/workflows/images_build.yml @@ -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: diff --git a/.github/workflows/images_build_rhel.yml b/.github/workflows/images_build_rhel.yml index 9c3090b8..bf8a31da 100644 --- a/.github/workflows/images_build_rhel.yml +++ b/.github/workflows/images_build_rhel.yml @@ -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: diff --git a/.github/workflows/images_build_windows.yml b/.github/workflows/images_build_windows.yml index a192ff6f..bcf1a667 100644 --- a/.github/workflows/images_build_windows.yml +++ b/.github/workflows/images_build_windows.yml @@ -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' }} diff --git a/Dockerfiles/agent/windows/Dockerfile b/Dockerfiles/agent/windows/Dockerfile index 425b236a..910fa7d1 100644 --- a/Dockerfiles/agent/windows/Dockerfile +++ b/Dockerfiles/agent/windows/Dockerfile @@ -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"] diff --git a/Dockerfiles/agent/windows/Dockerfile.dockerignore b/Dockerfiles/agent/windows/Dockerfile.dockerignore new file mode 100644 index 00000000..039d8973 --- /dev/null +++ b/Dockerfiles/agent/windows/Dockerfile.dockerignore @@ -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/** diff --git a/Dockerfiles/agent/windows/docker-entrypoint.ps1 b/Dockerfiles/agent/windows/docker-entrypoint.ps1 deleted file mode 100644 index c15ebf7d..00000000 --- a/Dockerfiles/agent/windows/docker-entrypoint.ps1 +++ /dev/null @@ -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 -} diff --git a/Dockerfiles/agent2/windows/Dockerfile b/Dockerfiles/agent2/windows/Dockerfile index 02b487da..81307f0b 100644 --- a/Dockerfiles/agent2/windows/Dockerfile +++ b/Dockerfiles/agent2/windows/Dockerfile @@ -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"] diff --git a/Dockerfiles/agent2/windows/Dockerfile.dockerignore b/Dockerfiles/agent2/windows/Dockerfile.dockerignore new file mode 100644 index 00000000..e6676cec --- /dev/null +++ b/Dockerfiles/agent2/windows/Dockerfile.dockerignore @@ -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/** diff --git a/Dockerfiles/agent2/windows/docker-entrypoint.ps1 b/Dockerfiles/agent2/windows/docker-entrypoint.ps1 deleted file mode 100644 index dadb440f..00000000 --- a/Dockerfiles/agent2/windows/docker-entrypoint.ps1 +++ /dev/null @@ -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 -} diff --git a/Dockerfiles/build-mysql/windows/Dockerfile.agent b/Dockerfiles/build-mysql/windows/Dockerfile.agent index 13527e11..1cee9f02 100644 --- a/Dockerfiles/build-mysql/windows/Dockerfile.agent +++ b/Dockerfiles/build-mysql/windows/Dockerfile.agent @@ -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 ` diff --git a/Dockerfiles/build-mysql/windows/Dockerfile.agent2 b/Dockerfiles/build-mysql/windows/Dockerfile.agent2 index 2abed723..26d890d5 100644 --- a/Dockerfiles/build-mysql/windows/Dockerfile.agent2 +++ b/Dockerfiles/build-mysql/windows/Dockerfile.agent2 @@ -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 { ` diff --git a/Dockerfiles/build-mysql/windows/config_templates/agent2/zabbix_agent2.conf b/templates/config/agent2_windows/zabbix_agent2.conf similarity index 100% rename from Dockerfiles/build-mysql/windows/config_templates/agent2/zabbix_agent2.conf rename to templates/config/agent2_windows/zabbix_agent2.conf diff --git a/Dockerfiles/build-mysql/windows/config_templates/agent2/zabbix_agent2_active_checks.conf b/templates/config/agent2_windows/zabbix_agent2_active_checks.conf similarity index 100% rename from Dockerfiles/build-mysql/windows/config_templates/agent2/zabbix_agent2_active_checks.conf rename to templates/config/agent2_windows/zabbix_agent2_active_checks.conf diff --git a/Dockerfiles/build-mysql/windows/config_templates/agent2/zabbix_agent2_aliases.conf b/templates/config/agent2_windows/zabbix_agent2_aliases.conf similarity index 100% rename from Dockerfiles/build-mysql/windows/config_templates/agent2/zabbix_agent2_aliases.conf rename to templates/config/agent2_windows/zabbix_agent2_aliases.conf diff --git a/Dockerfiles/build-mysql/windows/config_templates/agent2/zabbix_agent2_item_keys.conf b/templates/config/agent2_windows/zabbix_agent2_item_keys.conf similarity index 100% rename from Dockerfiles/build-mysql/windows/config_templates/agent2/zabbix_agent2_item_keys.conf rename to templates/config/agent2_windows/zabbix_agent2_item_keys.conf diff --git a/Dockerfiles/build-mysql/windows/config_templates/agent2/zabbix_agent2_locations.conf b/templates/config/agent2_windows/zabbix_agent2_locations.conf similarity index 100% rename from Dockerfiles/build-mysql/windows/config_templates/agent2/zabbix_agent2_locations.conf rename to templates/config/agent2_windows/zabbix_agent2_locations.conf diff --git a/Dockerfiles/build-mysql/windows/config_templates/agent2/zabbix_agent2_logging.conf b/templates/config/agent2_windows/zabbix_agent2_logging.conf similarity index 100% rename from Dockerfiles/build-mysql/windows/config_templates/agent2/zabbix_agent2_logging.conf rename to templates/config/agent2_windows/zabbix_agent2_logging.conf diff --git a/Dockerfiles/build-mysql/windows/config_templates/agent2/zabbix_agent2_network.conf b/templates/config/agent2_windows/zabbix_agent2_network.conf similarity index 100% rename from Dockerfiles/build-mysql/windows/config_templates/agent2/zabbix_agent2_network.conf rename to templates/config/agent2_windows/zabbix_agent2_network.conf diff --git a/Dockerfiles/build-mysql/windows/config_templates/agent2/zabbix_agent2_passive_checks.conf b/templates/config/agent2_windows/zabbix_agent2_passive_checks.conf similarity index 100% rename from Dockerfiles/build-mysql/windows/config_templates/agent2/zabbix_agent2_passive_checks.conf rename to templates/config/agent2_windows/zabbix_agent2_passive_checks.conf diff --git a/Dockerfiles/build-mysql/windows/config_templates/agent2/zabbix_agent2_timeouts.conf b/templates/config/agent2_windows/zabbix_agent2_timeouts.conf similarity index 100% rename from Dockerfiles/build-mysql/windows/config_templates/agent2/zabbix_agent2_timeouts.conf rename to templates/config/agent2_windows/zabbix_agent2_timeouts.conf diff --git a/Dockerfiles/build-mysql/windows/config_templates/agent2/zabbix_agent2_tls.conf b/templates/config/agent2_windows/zabbix_agent2_tls.conf similarity index 100% rename from Dockerfiles/build-mysql/windows/config_templates/agent2/zabbix_agent2_tls.conf rename to templates/config/agent2_windows/zabbix_agent2_tls.conf diff --git a/Dockerfiles/build-mysql/windows/config_templates/agent2/zabbix_agent2_user_parameters.conf b/templates/config/agent2_windows/zabbix_agent2_user_parameters.conf similarity index 100% rename from Dockerfiles/build-mysql/windows/config_templates/agent2/zabbix_agent2_user_parameters.conf rename to templates/config/agent2_windows/zabbix_agent2_user_parameters.conf diff --git a/Dockerfiles/build-mysql/windows/config_templates/agent/zabbix_agentd.conf b/templates/config/agent_windows/zabbix_agentd.conf similarity index 100% rename from Dockerfiles/build-mysql/windows/config_templates/agent/zabbix_agentd.conf rename to templates/config/agent_windows/zabbix_agentd.conf diff --git a/Dockerfiles/build-mysql/windows/config_templates/agent/zabbix_agentd_active_checks.conf b/templates/config/agent_windows/zabbix_agentd_active_checks.conf similarity index 100% rename from Dockerfiles/build-mysql/windows/config_templates/agent/zabbix_agentd_active_checks.conf rename to templates/config/agent_windows/zabbix_agentd_active_checks.conf diff --git a/Dockerfiles/build-mysql/windows/config_templates/agent/zabbix_agentd_aliases.conf b/templates/config/agent_windows/zabbix_agentd_aliases.conf similarity index 100% rename from Dockerfiles/build-mysql/windows/config_templates/agent/zabbix_agentd_aliases.conf rename to templates/config/agent_windows/zabbix_agentd_aliases.conf diff --git a/Dockerfiles/build-mysql/windows/config_templates/agent/zabbix_agentd_item_keys.conf b/templates/config/agent_windows/zabbix_agentd_item_keys.conf similarity index 100% rename from Dockerfiles/build-mysql/windows/config_templates/agent/zabbix_agentd_item_keys.conf rename to templates/config/agent_windows/zabbix_agentd_item_keys.conf diff --git a/Dockerfiles/build-mysql/windows/config_templates/agent/zabbix_agentd_logging.conf b/templates/config/agent_windows/zabbix_agentd_logging.conf similarity index 100% rename from Dockerfiles/build-mysql/windows/config_templates/agent/zabbix_agentd_logging.conf rename to templates/config/agent_windows/zabbix_agentd_logging.conf diff --git a/Dockerfiles/build-mysql/windows/config_templates/agent/zabbix_agentd_network.conf b/templates/config/agent_windows/zabbix_agentd_network.conf similarity index 100% rename from Dockerfiles/build-mysql/windows/config_templates/agent/zabbix_agentd_network.conf rename to templates/config/agent_windows/zabbix_agentd_network.conf diff --git a/Dockerfiles/build-mysql/windows/config_templates/agent/zabbix_agentd_passive_checks.conf b/templates/config/agent_windows/zabbix_agentd_passive_checks.conf similarity index 100% rename from Dockerfiles/build-mysql/windows/config_templates/agent/zabbix_agentd_passive_checks.conf rename to templates/config/agent_windows/zabbix_agentd_passive_checks.conf diff --git a/Dockerfiles/build-mysql/windows/config_templates/agent/zabbix_agentd_perf_counters.conf b/templates/config/agent_windows/zabbix_agentd_perf_counters.conf similarity index 100% rename from Dockerfiles/build-mysql/windows/config_templates/agent/zabbix_agentd_perf_counters.conf rename to templates/config/agent_windows/zabbix_agentd_perf_counters.conf diff --git a/Dockerfiles/build-mysql/windows/config_templates/agent/zabbix_agentd_timeouts.conf b/templates/config/agent_windows/zabbix_agentd_timeouts.conf similarity index 100% rename from Dockerfiles/build-mysql/windows/config_templates/agent/zabbix_agentd_timeouts.conf rename to templates/config/agent_windows/zabbix_agentd_timeouts.conf diff --git a/Dockerfiles/build-mysql/windows/config_templates/agent/zabbix_agentd_tls.conf b/templates/config/agent_windows/zabbix_agentd_tls.conf similarity index 100% rename from Dockerfiles/build-mysql/windows/config_templates/agent/zabbix_agentd_tls.conf rename to templates/config/agent_windows/zabbix_agentd_tls.conf diff --git a/Dockerfiles/build-mysql/windows/config_templates/agent/zabbix_agentd_user_parameters.conf b/templates/config/agent_windows/zabbix_agentd_user_parameters.conf similarity index 100% rename from Dockerfiles/build-mysql/windows/config_templates/agent/zabbix_agentd_user_parameters.conf rename to templates/config/agent_windows/zabbix_agentd_user_parameters.conf diff --git a/templates/entrypoints/cmd/agent/main.go b/templates/entrypoints/cmd/agent/main.go new file mode 100644 index 00000000..dd1cd6fc --- /dev/null +++ b/templates/entrypoints/cmd/agent/main.go @@ -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)) +} diff --git a/templates/entrypoints/cmd/agent/platform_windows.go b/templates/entrypoints/cmd/agent/platform_windows.go new file mode 100644 index 00000000..78298332 --- /dev/null +++ b/templates/entrypoints/cmd/agent/platform_windows.go @@ -0,0 +1,3 @@ +package main + +const agentBinary = `C:\zabbix\sbin\zabbix_agentd.exe` diff --git a/templates/entrypoints/cmd/agent/platform_windows_test.go b/templates/entrypoints/cmd/agent/platform_windows_test.go new file mode 100644 index 00000000..993d118d --- /dev/null +++ b/templates/entrypoints/cmd/agent/platform_windows_test.go @@ -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") + } +} diff --git a/templates/entrypoints/cmd/agent2/main.go b/templates/entrypoints/cmd/agent2/main.go new file mode 100644 index 00000000..1088cf15 --- /dev/null +++ b/templates/entrypoints/cmd/agent2/main.go @@ -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)) +} diff --git a/templates/entrypoints/cmd/agent2/platform_windows.go b/templates/entrypoints/cmd/agent2/platform_windows.go new file mode 100644 index 00000000..5c67379c --- /dev/null +++ b/templates/entrypoints/cmd/agent2/platform_windows.go @@ -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") +} diff --git a/templates/entrypoints/cmd/agent2/platform_windows_test.go b/templates/entrypoints/cmd/agent2/platform_windows_test.go new file mode 100644 index 00000000..b406dc88 --- /dev/null +++ b/templates/entrypoints/cmd/agent2/platform_windows_test.go @@ -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") + } +} diff --git a/templates/entrypoints/go.mod b/templates/entrypoints/go.mod new file mode 100644 index 00000000..ef195e16 --- /dev/null +++ b/templates/entrypoints/go.mod @@ -0,0 +1,3 @@ +module github.com/zabbix/zabbix-docker/templates/entrypoints + +go 1.25.0 diff --git a/templates/entrypoints/internal/agent/config.go b/templates/entrypoints/internal/agent/config.go new file mode 100644 index 00000000..f14b447c --- /dev/null +++ b/templates/entrypoints/internal/agent/config.go @@ -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 +} diff --git a/templates/entrypoints/internal/agent/config_test.go b/templates/entrypoints/internal/agent/config_test.go new file mode 100644 index 00000000..0e210d57 --- /dev/null +++ b/templates/entrypoints/internal/agent/config_test.go @@ -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) + } +} diff --git a/templates/entrypoints/internal/bootstrap/command.go b/templates/entrypoints/internal/bootstrap/command.go new file mode 100644 index 00000000..46a37a28 --- /dev/null +++ b/templates/entrypoints/internal/bootstrap/command.go @@ -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) +} diff --git a/templates/entrypoints/internal/bootstrap/command_test.go b/templates/entrypoints/internal/bootstrap/command_test.go new file mode 100644 index 00000000..d39b4ac8 --- /dev/null +++ b/templates/entrypoints/internal/bootstrap/command_test.go @@ -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) +} diff --git a/templates/entrypoints/internal/bootstrap/config.go b/templates/entrypoints/internal/bootstrap/config.go new file mode 100644 index 00000000..18ab62ce --- /dev/null +++ b/templates/entrypoints/internal/bootstrap/config.go @@ -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 +} diff --git a/templates/entrypoints/internal/bootstrap/config_test.go b/templates/entrypoints/internal/bootstrap/config_test.go new file mode 100644 index 00000000..873d6762 --- /dev/null +++ b/templates/entrypoints/internal/bootstrap/config_test.go @@ -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()) + } +} diff --git a/templates/entrypoints/internal/bootstrap/environment.go b/templates/entrypoints/internal/bootstrap/environment.go new file mode 100644 index 00000000..27b39794 --- /dev/null +++ b/templates/entrypoints/internal/bootstrap/environment.go @@ -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 "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 /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 +} diff --git a/templates/entrypoints/internal/bootstrap/environment_test.go b/templates/entrypoints/internal/bootstrap/environment_test.go new file mode 100644 index 00000000..e30a1e97 --- /dev/null +++ b/templates/entrypoints/internal/bootstrap/environment_test.go @@ -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") + } + }) + } +} diff --git a/templates/entrypoints/internal/bootstrap/exec_windows.go b/templates/entrypoints/internal/bootstrap/exec_windows.go new file mode 100644 index 00000000..8f7eb589 --- /dev/null +++ b/templates/entrypoints/internal/bootstrap/exec_windows.go @@ -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() +} diff --git a/templates/entrypoints/internal/bootstrap/files.go b/templates/entrypoints/internal/bootstrap/files.go new file mode 100644 index 00000000..7044bbe1 --- /dev/null +++ b/templates/entrypoints/internal/bootstrap/files.go @@ -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) +} diff --git a/templates/entrypoints/internal/bootstrap/logging.go b/templates/entrypoints/internal/bootstrap/logging.go new file mode 100644 index 00000000..60117d05 --- /dev/null +++ b/templates/entrypoints/internal/bootstrap/logging.go @@ -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...)) +} diff --git a/templates/entrypoints/internal/hooks/command_windows.go b/templates/entrypoints/internal/hooks/command_windows.go new file mode 100644 index 00000000..e3ade3d4 --- /dev/null +++ b/templates/entrypoints/internal/hooks/command_windows.go @@ -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 + } +} diff --git a/templates/entrypoints/internal/hooks/command_windows_test.go b/templates/entrypoints/internal/hooks/command_windows_test.go new file mode 100644 index 00000000..7bdbb344 --- /dev/null +++ b/templates/entrypoints/internal/hooks/command_windows_test.go @@ -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) + } +} diff --git a/templates/entrypoints/internal/hooks/hooks.go b/templates/entrypoints/internal/hooks/hooks.go new file mode 100644 index 00000000..f1dc0f5d --- /dev/null +++ b/templates/entrypoints/internal/hooks/hooks.go @@ -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 /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 +} diff --git a/templates/entrypoints/internal/hooks/hooks_test.go b/templates/entrypoints/internal/hooks/hooks_test.go new file mode 100644 index 00000000..b963d03b --- /dev/null +++ b/templates/entrypoints/internal/hooks/hooks_test.go @@ -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) + } +} diff --git a/templates/entrypoints/licenses/go-THIRD-PARTY-NOTICES.txt b/templates/entrypoints/licenses/go-THIRD-PARTY-NOTICES.txt new file mode 100644 index 00000000..37cbacdb --- /dev/null +++ b/templates/entrypoints/licenses/go-THIRD-PARTY-NOTICES.txt @@ -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.