...

Text file src/k8s.io/kubernetes/cluster/gce/windows/common.psm1

Documentation: k8s.io/kubernetes/cluster/gce/windows

     1# Copyright 2019 The Kubernetes Authors.
     2#
     3# Licensed under the Apache License, Version 2.0 (the "License");
     4# you may not use this file except in compliance with the License.
     5# You may obtain a copy of the License at
     6#
     7#     http://www.apache.org/licenses/LICENSE-2.0
     8#
     9# Unless required by applicable law or agreed to in writing, software
    10# distributed under the License is distributed on an "AS IS" BASIS,
    11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12# See the License for the specific language governing permissions and
    13# limitations under the License.
    14
    15<#
    16.SYNOPSIS
    17  Library containing common variables and code used by other PowerShell modules
    18  and scripts for configuring Windows nodes.
    19#>
    20
    21# IMPORTANT PLEASE NOTE:
    22# Any time the file structure in the `windows` directory changes,
    23# `windows/BUILD` and `k8s.io/release/lib/releaselib.sh` must be manually
    24# updated with the changes.
    25# We HIGHLY recommend not changing the file structure, because consumers of
    26# Kubernetes releases depend on the release structure remaining stable.
    27
    28# Disable progress bar to increase download speed.
    29$ProgressPreference = 'SilentlyContinue'
    30
    31# REDO_STEPS affects the behavior of a node that is rebooted after initial
    32# bringup. When true, on a reboot the scripts will redo steps that were
    33# determined to have already been completed once (e.g. to overwrite
    34# already-existing config files). When false the scripts will perform the
    35# minimum required steps to re-join this node to the cluster.
    36$REDO_STEPS = $false
    37Export-ModuleMember -Variable REDO_STEPS
    38
    39# Writes $Message to the console. Terminates the script if $Fatal is set.
    40function Log-Output {
    41  param (
    42    [parameter(Mandatory=$true)] [string]$Message,
    43    [switch]$Fatal
    44  )
    45  Write-Host "${Message}"
    46  if (${Fatal}) {
    47    Exit 1
    48  }
    49}
    50
    51# Dumps detailed information about the specified service to the console output.
    52# $Delay can be set to a positive value to introduce some seconds of delay
    53# before querying the service information, which may produce more consistent
    54# results if this function is called immediately after changing a service's
    55# configuration.
    56function Write-VerboseServiceInfoToConsole {
    57  param (
    58    [parameter(Mandatory=$true)] [string]$Service,
    59    [parameter(Mandatory=$false)] [int]$Delay = 0
    60  )
    61  if ($Delay -gt 0) {
    62    Start-Sleep $Delay
    63  }
    64  Get-Service -ErrorAction Continue $Service | Select-Object * | Out-String
    65  & sc.exe queryex $Service
    66  & sc.exe qc $Service
    67  & sc.exe qfailure $Service
    68}
    69
    70# Checks if a file should be written or overwritten by testing if it already
    71# exists and checking the value of the global $REDO_STEPS variable. Emits an
    72# informative message if the file already exists.
    73#
    74# Returns $true if the file does not exist, or if it does but the global
    75# $REDO_STEPS variable is set to $true. Returns $false if the file exists and
    76# the caller should not overwrite it.
    77function ShouldWrite-File {
    78  param (
    79    [parameter(Mandatory=$true)] [string]$Filename
    80  )
    81  if (Test-Path $Filename) {
    82    if ($REDO_STEPS) {
    83      Log-Output "Warning: $Filename already exists, will overwrite it"
    84      return $true
    85    }
    86    Log-Output "Skip: $Filename already exists, not overwriting it"
    87    return $false
    88  }
    89  return $true
    90}
    91
    92# Returns the GCE instance metadata value for $Key. If the key is not present
    93# in the instance metadata returns $Default if set, otherwise returns $null.
    94function Get-InstanceMetadata {
    95  param (
    96    [parameter(Mandatory=$true)] [string]$Key,
    97    [parameter(Mandatory=$false)] [string]$Default
    98  )
    99
   100  $url = "http://metadata.google.internal/computeMetadata/v1/instance/$Key"
   101  try {
   102    $client = New-Object Net.WebClient
   103    $client.Headers.Add('Metadata-Flavor', 'Google')
   104    return ($client.DownloadString($url)).Trim()
   105  }
   106  catch [System.Net.WebException] {
   107    if ($Default) {
   108      return $Default
   109    }
   110    else {
   111      Log-Output "Failed to retrieve value for $Key."
   112      return $null
   113    }
   114  }
   115}
   116
   117# Returns the GCE instance metadata value for $Key where key is an "attribute"
   118# of the instance. If the key is not present in the instance metadata returns
   119# $Default if set, otherwise returns $null.
   120function Get-InstanceMetadataAttribute {
   121  param (
   122    [parameter(Mandatory=$true)] [string]$Key,
   123    [parameter(Mandatory=$false)] [string]$Default
   124  )
   125
   126  return Get-InstanceMetadata "attributes/$Key" $Default
   127}
   128
   129function Validate-SHA {
   130  param(
   131    [parameter(Mandatory=$true)] [string]$Hash,
   132    [parameter(Mandatory=$true)] [string]$Path,
   133    [parameter(Mandatory=$true)] [string]$Algorithm
   134  )
   135  $actual = Get-FileHash -Path $Path -Algorithm $Algorithm
   136  # Note: Powershell string comparisons are case-insensitive by default, and this
   137  # is important here because Linux shell scripts produce lowercase hashes but
   138  # Powershell Get-FileHash produces uppercase hashes. This must be case-insensitive
   139  # to work.
   140  if ($actual.Hash -ne $Hash) {
   141    Log-Output "$Path corrupted, $Algorithm $actual doesn't match expected $Hash"
   142    Throw ("$Path corrupted, $Algorithm $actual doesn't match expected $Hash")
   143  }
   144}
   145
   146# Attempts to download the file from URLs, trying each URL until it succeeds.
   147# It will loop through the URLs list forever until it has a success. If
   148# successful, it will write the file to OutFile. You can optionally provide a
   149# Hash argument with an optional Algorithm, in which case it will attempt to
   150# validate the downloaded file against the hash. SHA512 will be used if
   151# -Algorithm is not provided.
   152# This function is idempotent, if OutFile already exists and has the correct Hash
   153# then the download will be skipped. If the Hash is incorrect, the file will be
   154# overwritten.
   155function MustDownload-File {
   156  param (
   157    [parameter(Mandatory = $false)] [string]$Hash,
   158    [parameter(Mandatory = $false)] [string]$Algorithm = 'SHA512',
   159    [parameter(Mandatory = $true)] [string]$OutFile,
   160    [parameter(Mandatory = $true)] [System.Collections.Generic.List[String]]$URLs,
   161    [parameter(Mandatory = $false)] [System.Collections.IDictionary]$Headers = @{},
   162    [parameter(Mandatory = $false)] [int]$Attempts = 0
   163  )
   164
   165  # If the file is already downloaded and matches the expected hash, skip the download.
   166  if ((Test-Path -Path $OutFile) -And -Not [string]::IsNullOrEmpty($Hash)) {
   167    try {
   168      Validate-SHA -Hash $Hash -Path $OutFile -Algorithm $Algorithm
   169      Log-Output "Skip download of ${OutFile}, it already exists with expected hash."
   170      return
   171    }
   172    catch {
   173      # The hash does not match the file on disk.
   174      # Proceed with the download and overwrite the file.
   175      Log-Output "${OutFile} exists but had wrong hash. Redownloading."
   176    }
   177  }
   178
   179  $currentAttempt = 0
   180  while ($true) {
   181    foreach ($url in $URLs) {
   182      if (($Attempts -ne 0) -And ($currentAttempt -Gt 5)) {
   183        throw "Attempted to download ${url} ${currentAttempt} times. Giving up."
   184      }
   185      $currentAttempt++
   186      try {
   187        Get-RemoteFile -OutFile $OutFile -Url $url -Headers $Headers
   188      }
   189      catch {
   190        $message = $_.Exception.ToString()
   191        Log-Output "Failed to download file from ${Url}. Will retry. Error: ${message}"
   192        continue
   193      }
   194      # Attempt to validate the hash
   195      if (-Not [string]::IsNullOrEmpty($Hash)) {
   196        try {
   197          Validate-SHA -Hash $Hash -Path $OutFile -Algorithm $Algorithm
   198        }
   199        catch {
   200          $message = $_.Exception.ToString()
   201          Log-Output "Hash validation of ${url} failed. Will retry. Error: ${message}"
   202          continue
   203        }
   204        Log-Output "Downloaded ${url} (${Algorithm} = ${Hash})"
   205        return
   206      }
   207      Log-Output "Downloaded ${url}"
   208      return
   209    }
   210  }
   211}
   212
   213# Downloads a file via HTTP/HTTPS.
   214# If the file is stored in GCS and this is running on a GCE node with a service account
   215# with credentials that have the devstore.read_only auth scope the bearer token will be
   216# automatically added to download the file.
   217function Get-RemoteFile {
   218  param (
   219    [parameter(Mandatory = $true)] [string]$OutFile,
   220    [parameter(Mandatory = $true)] [string]$Url,
   221    [parameter(Mandatory = $false)] [System.Collections.IDictionary]$Headers = @{}
   222  )
   223
   224  # Load the System.Net.Http assembly if it's not loaded yet.
   225  if ("System.Net.Http.HttpClient" -as [type]) {} else {
   226    Add-Type -AssemblyName System.Net.Http
   227  }
   228
   229  $timeout = New-TimeSpan -Minutes 5
   230
   231  try {
   232    # Use HttpClient in favor of WebClient.
   233    # https://docs.microsoft.com/en-us/dotnet/api/system.net.webclient?view=net-5.0#remarks
   234    $httpClient = New-Object -TypeName System.Net.Http.HttpClient
   235    $httpClient.Timeout = $timeout
   236    foreach ($key in $Headers.Keys) {
   237      $httpClient.DefaultRequestHeaders.Add($key, $Headers[$key])
   238    }
   239    # If the URL is for GCS and the node has dev storage scope, add the
   240    # service account OAuth2 bearer token to the request headers.
   241    # https://cloud.google.com/compute/docs/access/create-enable-service-accounts-for-instances#applications
   242    if (($Url -match "^https://storage`.googleapis`.com.*") -and $(Check-StorageScope)) {
   243      $httpClient.DefaultRequestHeaders.Add("Authorization", "Bearer $(Get-Credentials)")
   244    }
   245
   246    # Attempt to download the file
   247    $httpResponseMessage = $httpClient.GetAsync([System.Uri]::new($Url))
   248    $httpResponseMessage.Wait()
   249    if (-not $httpResponseMessage.IsCanceled) {
   250      # Check if the request was successful.
   251      #
   252      # DO NOT replace with EnsureSuccessStatusCode(), it prints the
   253      # OAuth2 bearer token.
   254      if (-not $httpResponseMessage.Result.IsSuccessStatusCode) {
   255        $statusCode = $httpResponseMessage.Result.StatusCode
   256        throw "Downloading ${Url} returned status code ${statusCode}, retrying."
   257      }
   258      try {
   259        $outFileStream = [System.IO.FileStream]::new($OutFile, [System.IO.FileMode]::Create, [System.IO.FileAccess]::Write)
   260        $copyResult = $httpResponseMessage.Result.Content.CopyToAsync($outFileStream)
   261        $copyResult.Wait()
   262        $outFileStream.Close()
   263        if ($null -ne $copyResult.Exception) {
   264          throw $copyResult.Exception
   265        }
   266      }
   267      finally {
   268        if ($null -ne $outFileStream) {
   269          $outFileStream.Dispose()
   270        }
   271      }
   272    }
   273  }
   274  finally {
   275    if ($null -ne $httpClient) {
   276      $httpClient.Dispose()
   277    }
   278  }
   279}
   280
   281# Returns the default service account token for the VM, retrieved from
   282# the instance metadata.
   283function Get-Credentials {
   284  While($true) {
   285    $data = Get-InstanceMetadata -Key "service-accounts/default/token"
   286    if ($data) {
   287      return ($data | ConvertFrom-Json).access_token
   288    }
   289    Start-Sleep -Seconds 1
   290  }
   291}
   292
   293# Returns True if the VM has the dev storage scope, False otherwise.
   294function Check-StorageScope {
   295  While($true) {
   296    $data = Get-InstanceMetadata -Key "service-accounts/default/scopes"
   297    if ($data) {
   298      return ($data -match "auth/devstorage") -or ($data -match "auth/cloud-platform")
   299    }
   300    Start-Sleep -Seconds 1
   301  }
   302}
   303
   304# This compiles some C# code that can make syscalls, and pulls the
   305# result into our powershell environment so we can make syscalls from this script.
   306# We make syscalls directly, because whatever the powershell cmdlets do under the hood,
   307# they can't seem to open the log files concurrently with writers.
   308# See https://docs.microsoft.com/en-us/dotnet/framework/interop/marshaling-data-with-platform-invoke
   309# for details on which unmanaged types map to managed types.
   310$SyscallDefinitions = @'
   311[DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)]
   312public static extern IntPtr CreateFileW(
   313  String lpFileName,
   314  UInt32 dwDesiredAccess,
   315  UInt32 dwShareMode,
   316  IntPtr lpSecurityAttributes,
   317  UInt32 dwCreationDisposition,
   318  UInt32 dwFlagsAndAttributes,
   319  IntPtr hTemplateFile
   320);
   321
   322[DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)]
   323public static extern bool SetFilePointer(
   324  IntPtr hFile,
   325  Int32  lDistanceToMove,
   326  IntPtr lpDistanceToMoveHigh,
   327  UInt32 dwMoveMethod
   328);
   329
   330[DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)]
   331public static extern bool SetEndOfFile(
   332  IntPtr hFile
   333);
   334
   335[DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)]
   336public static extern bool CloseHandle(
   337  IntPtr hObject
   338);
   339'@
   340$Kernel32 = Add-Type -MemberDefinition $SyscallDefinitions -Name 'Kernel32' -Namespace 'Win32' -PassThru
   341
   342# Close-Handle closes the specified open file handle.
   343# On failure, throws an exception.
   344function Close-Handle {
   345  param (
   346    [parameter(Mandatory=$true)] [System.IntPtr]$Handle
   347  )
   348  $ret = $Kernel32::CloseHandle($Handle)
   349  $err = [System.Runtime.InteropServices.Marshal]::GetLastWin32Error()
   350  if (-not $ret) {
   351    throw "Failed to close open file handle ${Handle}, system error code: ${err}"
   352  }
   353}
   354
   355# Open-File tries to open the file at the specified path with ReadWrite access mode and ReadWrite file share mode.
   356# On success, returns an open file handle.
   357# On failure, throws an exception.
   358function Open-File {
   359  param (
   360    [parameter(Mandatory=$true)] [string]$Path
   361  )
   362
   363  $lpFileName = $Path
   364  $dwDesiredAccess = [System.IO.FileAccess]::ReadWrite
   365  $dwShareMode = [System.IO.FileShare]::ReadWrite # Fortunately golang also passes these same flags when it creates the log files, so we can open it concurrently.
   366  $lpSecurityAttributes = [System.IntPtr]::Zero
   367  $dwCreationDisposition = [System.IO.FileMode]::Open
   368  $dwFlagsAndAttributes = [System.IO.FileAttributes]::Normal
   369  $hTemplateFile = [System.IntPtr]::Zero
   370
   371  $handle = $Kernel32::CreateFileW($lpFileName, $dwDesiredAccess, $dwShareMode, $lpSecurityAttributes, $dwCreationDisposition, $dwFlagsAndAttributes, $hTemplateFile)
   372  $err = [System.Runtime.InteropServices.Marshal]::GetLastWin32Error()
   373  if ($handle -eq -1) {
   374    throw "Failed to open file ${Path}, system error code: ${err}"
   375  }
   376
   377  return $handle
   378}
   379
   380# Truncate-File truncates the file in-place by opening it, moving the file pointer to the beginning,
   381# and setting the end of file to the file pointer's location.
   382# On failure, throws an exception.
   383# The file must have been originally created with FILE_SHARE_WRITE for this to be possible.
   384# Fortunately Go creates files with FILE_SHARE_READ|FILE_SHARE_WRITE by for all os.Open calls,
   385# so our log writers should be doing the right thing.
   386function Truncate-File {
   387  param (
   388    [parameter(Mandatory=$true)] [string]$Path
   389  )
   390  $INVALID_SET_FILE_POINTER = 0xffffffff
   391  $NO_ERROR = 0
   392  $FILE_BEGIN = 0
   393
   394  $handle = Open-File -Path $Path
   395
   396  # https://docs.microsoft.com/en-us/windows/desktop/api/fileapi/nf-fileapi-setfilepointer
   397  # Docs: Because INVALID_SET_FILE_POINTER is a valid value for the low-order DWORD of the new file pointer,
   398  # you must check both the return value of the function and the error code returned by GetLastError to
   399  # determine whether or not an error has occurred. If an error has occurred, the return value of SetFilePointer
   400  # is INVALID_SET_FILE_POINTER and GetLastError returns a value other than NO_ERROR.
   401  $ret = $Kernel32::SetFilePointer($handle, 0, [System.IntPtr]::Zero, $FILE_BEGIN)
   402  $err = [System.Runtime.InteropServices.Marshal]::GetLastWin32Error()
   403  if ($ret -eq $INVALID_SET_FILE_POINTER -and $err -ne $NO_ERROR) {
   404    Close-Handle -Handle $handle
   405    throw "Failed to set file pointer for handle ${handle}, system error code: ${err}"
   406  }
   407
   408  $ret = $Kernel32::SetEndOfFile($handle)
   409  $err = [System.Runtime.InteropServices.Marshal]::GetLastWin32Error()
   410  if ($ret -eq 0) {
   411    Close-Handle -Handle $handle
   412    throw "Failed to set end of file for handle ${handle}, system error code: ${err}"
   413  }
   414  Close-Handle -Handle $handle
   415}
   416
   417# FileRotationConfig defines the common options for file rotation.
   418class FileRotationConfig {
   419  # Force rotation, ignoring $MaxBackupInterval and $MaxSize criteria.
   420  [bool]$Force
   421  # Maximum time since last backup, after which file will be rotated.
   422  # When no backups exist, Rotate-File acts as if -MaxBackupInterval has not elapsed,
   423  # instead relying on the other criteria.
   424  [TimeSpan]$MaxBackupInterval
   425  # Maximum file size, after which file will be rotated.
   426  [int]$MaxSize
   427  # Maximum number of backup archives to maintain.
   428  [int]$MaxBackups
   429}
   430
   431# New-FileRotationConfig constructs a FileRotationConfig with default options.
   432function New-FileRotationConfig {
   433  param (
   434    # Force rotation, ignoring $MaxBackupInterval and $MaxSize criteria.
   435    [parameter(Mandatory=$false)] [switch]$Force,
   436    # Maximum time since last backup, after which file will be rotated.
   437    # When no backups exist, Rotate-File acts as if -MaxBackupInterval has not elapsed,
   438    # instead relying on the other criteria.
   439    # Defaults to daily rotations.
   440    [parameter(Mandatory=$false)] [TimeSpan]$MaxBackupInterval = $(New-TimeSpan -Day 1),
   441    # Maximum file size, after which file will be rotated.
   442    [parameter(Mandatory=$false)] [int]$MaxSize = 100mb,
   443    # Maximum number of backup archives to maintain.
   444    [parameter(Mandatory=$false)] [int]$MaxBackups = 5
   445  )
   446  $config = [FileRotationConfig]::new()
   447  $config.Force = $Force
   448  $config.MaxBackupInterval = $MaxBackupInterval
   449  $config.MaxSize = $MaxSize
   450  $config.MaxBackups = $MaxBackups
   451  return $config
   452}
   453
   454# Get-Backups returns a list of paths to backup files for the original file path -Path,
   455# assuming that backup files are in the same directory, with a prefix matching
   456# the original file name and a .zip suffix.
   457function Get-Backups {
   458  param (
   459    # Original path of the file for which backups were created (no suffix).
   460    [parameter(Mandatory=$true)] [string]$Path
   461  )
   462  $parent = Split-Path -Parent -Path $Path
   463  $leaf = Split-Path -Leaf -Path $Path
   464  $files = Get-ChildItem -File -Path $parent |
   465           Where-Object Name -like "${leaf}*.zip"
   466  return $files
   467}
   468
   469# Trim-Backups deletes old backups for the log file identified by -Path until only -Count remain.
   470# Deletes backups with the oldest CreationTime first.
   471function Trim-Backups {
   472  param (
   473    [parameter(Mandatory=$true)] [int]$Count,
   474    [parameter(Mandatory=$true)] [string]$Path
   475  )
   476  if ($Count -lt 0) {
   477    $Count = 0
   478  }
   479  # If creating a new backup will exceed $Count, delete the oldest files
   480  # until we have one less than $Count, leaving room for the new one.
   481  # If the pipe results in zero items, $backups is $null, and if it results
   482  # in only one item, PowerShell doesn't wrap in an array, so we check both cases.
   483  # In the latter case, this actually caused it to often trim all backups, because
   484  # .Length is also a property of FileInfo (size of the file)!
   485  $backups = Get-Backups -Path $Path | Sort-Object -Property CreationTime
   486  if ($backups -and $backups.GetType() -eq @().GetType() -and $backups.Length -gt $Count) {
   487    $num = $backups.Length - $Count
   488    $rmFiles = $backups | Select-Object -First $num
   489    ForEach ($file in $rmFiles) {
   490      Remove-Item $file.FullName
   491    }
   492  }
   493}
   494
   495# Backup-File creates a copy of the file at -Path.
   496# The name of the backup is the same as the file,
   497# with the suffix "-%Y%m%d-%s" to identify the time of the backup.
   498# Returns the path to the backup file.
   499function Backup-File {
   500  param (
   501    [parameter(Mandatory=$true)] [string]$Path
   502  )
   503  $date = Get-Date -UFormat "%Y%m%d-%s"
   504  $dest = "${Path}-${date}"
   505  Copy-Item -Path $Path -Destination $dest
   506  return $dest
   507}
   508
   509# Compress-BackupFile creates a compressed archive containing the file
   510# at -Path and subsequently deletes the file at -Path. We split backup
   511# and compression steps to minimize time between backup and truncation,
   512# which helps minimize log loss.
   513function Compress-BackupFile {
   514  param (
   515    [parameter(Mandatory=$true)] [string]$Path
   516  )
   517  Compress-Archive -Path $Path -DestinationPath "${Path}.zip"
   518  Remove-Item -Path $Path
   519}
   520
   521# Rotate-File rotates the log file at -Path by first making a compressed copy of the original
   522# log file with the suffix "-%Y%m%d-%s" to identify the time of the backup, then truncating
   523# the original file in-place. Rotation is performed according to the options in -Config.
   524function Rotate-File {
   525  param (
   526    # Path to the log file to rotate.
   527    [parameter(Mandatory=$true)] [string]$Path,
   528    # Config for file rotation.
   529    [parameter(Mandatory=$true)] [FileRotationConfig]$Config
   530  )
   531  function rotate {
   532    # If creating a new backup will exceed $MaxBackups, delete the oldest files
   533    # until we have one less than $MaxBackups, leaving room for the new one.
   534    Trim-Backups -Count ($Config.MaxBackups - 1) -Path $Path
   535
   536    $backupPath = Backup-File -Path $Path
   537    Truncate-File -Path $Path
   538    Compress-BackupFile -Path $backupPath
   539  }
   540
   541  # Check Force
   542  if ($Config.Force) {
   543    rotate
   544    return
   545  }
   546
   547  # Check MaxSize.
   548  $file = Get-Item $Path
   549  if ($file.Length -gt $Config.MaxSize) {
   550    rotate
   551    return
   552  }
   553
   554  # Check MaxBackupInterval.
   555  $backups = Get-Backups -Path $Path | Sort-Object -Property CreationTime
   556  if ($backups.Length -ge 1) {
   557    $lastBackupTime = $backups[0].CreationTime
   558    $now = Get-Date
   559    if ($now - $lastBackupTime -gt $Config.MaxBackupInterval) {
   560      rotate
   561      return
   562    }
   563  }
   564}
   565
   566# Rotate-Files rotates the log files in directory -Path that match -Pattern.
   567# Rotation is performed by Rotate-File, according to -Config.
   568function Rotate-Files {
   569  param (
   570    # Pattern that file names must match to be rotated. Does not include parent path.
   571    [parameter(Mandatory=$true)] [string]$Pattern,
   572    # Path to the log directory containing files to rotate.
   573    [parameter(Mandatory=$true)] [string]$Path,
   574    # Config for file rotation.
   575    [parameter(Mandatory=$true)] [FileRotationConfig]$Config
   576
   577  )
   578  $files = Get-ChildItem -File -Path $Path | Where-Object Name -match $Pattern
   579  ForEach ($file in $files) {
   580    try {
   581      Rotate-File -Path $file.FullName -Config $Config
   582    } catch {
   583      Log-Output "Caught exception rotating $($file.FullName): $($_.Exception)"
   584    }
   585  }
   586}
   587
   588# Schedule-LogRotation schedules periodic log rotation with the Windows Task Scheduler.
   589# Rotation is performed by Rotate-Files, according to -Pattern and -Config.
   590# The system will check whether log files need to be rotated at -RepetitionInterval.
   591function Schedule-LogRotation {
   592  param (
   593    # Pattern that file names must match to be rotated. Does not include parent path.
   594    [parameter(Mandatory=$true)] [string]$Pattern,
   595    # Path to the log directory containing files to rotate.
   596    [parameter(Mandatory=$true)] [string]$Path,
   597    # Interval at which to check logs against rotation criteria.
   598    # Minimum 1 minute, maximum 31 days (see https://docs.microsoft.com/en-us/windows/desktop/taskschd/taskschedulerschema-interval-repetitiontype-element).
   599    [parameter(Mandatory=$true)] [TimeSpan]$RepetitionInterval,
   600    # Config for file rotation.
   601    [parameter(Mandatory=$true)] [FileRotationConfig]$Config
   602  )
   603  # Write a powershell script to a file that imports this module ($PSCommandPath)
   604  # and calls Rotate-Files with the configured arguments.
   605  $scriptPath = "C:\rotate-kube-logs.ps1"
   606  New-Item -Force -ItemType file -Path $scriptPath | Out-Null
   607  Set-Content -Path $scriptPath @"
   608`$ErrorActionPreference = 'Stop'
   609Import-Module -Force ${PSCommandPath}
   610`$maxBackupInterval = New-Timespan -Days $($Config.MaxBackupInterval.Days) -Hours $($Config.MaxBackupInterval.Hours) -Minutes $($Config.MaxBackupInterval.Minutes) -Seconds $($Config.MaxBackupInterval.Seconds)
   611`$config = New-FileRotationConfig -Force:`$$($Config.Force) -MaxBackupInterval `$maxBackupInterval -MaxSize $($Config.MaxSize) -MaxBackups $($Config.MaxBackups)
   612Rotate-Files -Pattern '${Pattern}' -Path '${Path}' -Config `$config
   613"@
   614  # The task will execute the rotate-kube-logs.ps1 script created above.
   615  # We explicitly set -WorkingDirectory to $Path for safety's sake, otherwise
   616  # it runs in %windir%\system32 by default, which sounds dangerous.
   617  $action = New-ScheduledTaskAction -Execute "powershell" -Argument "-NoLogo -NonInteractive -File ${scriptPath}" -WorkingDirectory $Path
   618  # Start the task immediately, and trigger the task once every $RepetitionInterval.
   619  $trigger = New-ScheduledTaskTrigger -Once -At $(Get-Date) -RepetitionInterval $RepetitionInterval
   620  # Run the task as the same user who is currently running this script.
   621  $principal = New-ScheduledTaskPrincipal $([System.Security.Principal.WindowsIdentity]::GetCurrent().Name)
   622  # Just use the default task settings.
   623  $settings = New-ScheduledTaskSettingsSet
   624  # Create the ScheduledTask object from the above parameters.
   625  $task = New-ScheduledTask -Action $action -Principal $principal -Trigger $trigger -Settings $settings -Description "Rotate Kubernetes logs"
   626  # Register the new ScheduledTask with the Task Scheduler.
   627  # Always try to unregister and re-register, in case it already exists (e.g. across reboots).
   628  $name = "RotateKubeLogs"
   629  try {
   630    Unregister-ScheduledTask -Confirm:$false -TaskName $name
   631  } catch {} finally {
   632    Register-ScheduledTask -TaskName $name -InputObject $task
   633  }
   634}
   635
   636# Returns true if this node is part of a test cluster (see
   637# cluster/gce/config-test.sh). $KubeEnv is a hash table containing the kube-env
   638# metadata keys+values.
   639function Test-IsTestCluster {
   640  param (
   641    [parameter(Mandatory=$true)] [hashtable]$KubeEnv
   642  )
   643
   644  if ($KubeEnv.Contains('TEST_CLUSTER') -and `
   645      ($KubeEnv['TEST_CLUSTER'] -eq 'true')) {
   646    return $true
   647  }
   648  return $false
   649}
   650
   651# Permanently adds a directory to the $env:PATH environment variable.
   652function Add-MachineEnvironmentPath {
   653  param (
   654    [parameter(Mandatory=$true)] [string]$Path
   655  )
   656  # Verify that the $Path is not already in the $env:Path variable.
   657  $pathForCompare = $Path.TrimEnd('\').ToLower()
   658  foreach ($p in $env:Path.Split(";")) {
   659    if ($p.TrimEnd('\').ToLower() -eq $pathForCompare) {
   660        return
   661    }
   662  }
   663
   664  $newMachinePath = $Path + ";" + `
   665    [System.Environment]::GetEnvironmentVariable("Path","Machine")
   666  [Environment]::SetEnvironmentVariable("Path", $newMachinePath, `
   667    [System.EnvironmentVariableTarget]::Machine)
   668  $env:Path = $Path + ";" + $env:Path
   669}
   670
   671# Export all public functions:
   672Export-ModuleMember -Function *-*

View as plain text