k8s.io/kubernetes@v1.31.0-alpha.0.0.20240520171757-56147500dadc/cluster/gce/windows/common.psm1 (about)

     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
    37  Export-ModuleMember -Variable REDO_STEPS
    38  
    39  # Writes $Message to the console. Terminates the script if $Fatal is set.
    40  function 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.
    56  function 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.
    77  function 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.
    94  function 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.
   120  function 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  
   129  function 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.
   155  function 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.
   217  function 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.
   283  function 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.
   294  function 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)]
   312  public 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)]
   323  public 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)]
   331  public static extern bool SetEndOfFile(
   332    IntPtr hFile
   333  );
   334  
   335  [DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)]
   336  public 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.
   344  function 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.
   358  function 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.
   386  function 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.
   418  class 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.
   432  function 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.
   457  function 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.
   471  function 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.
   499  function 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.
   513  function 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.
   524  function 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.
   568  function 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.
   591  function 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'
   609  Import-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)
   612  Rotate-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.
   639  function 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.
   652  function 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:
   672  Export-ModuleMember -Function *-*