From c6366ab896b360eb7221f4db1ffbb17b952a86a4 Mon Sep 17 00:00:00 2001 From: Gabriel Adrian Samfira Date: Mon, 20 Mar 2023 18:54:45 +0200 Subject: [PATCH] Add Windows userdata Signed-off-by: Gabriel Adrian Samfira --- cloudconfig/templates.go | 241 ++++++++++++++++++++++++++++++++++++++- runner/types.go | 1 - util/util.go | 40 ++++--- 3 files changed, 263 insertions(+), 19 deletions(-) diff --git a/cloudconfig/templates.go b/cloudconfig/templates.go index 64ca2d55..b2815c59 100644 --- a/cloudconfig/templates.go +++ b/cloudconfig/templates.go @@ -16,8 +16,10 @@ package cloudconfig import ( "bytes" + "fmt" "text/template" + "github.com/cloudbase/garm/params" "github.com/pkg/errors" ) @@ -101,6 +103,229 @@ set -e success "runner successfully installed" $AGENT_ID ` +var WindowsSetupScriptTemplate = `#ps1_sysnative +Param( + [Parameter(Mandatory=$false)] + [string]$Token="{{.CallbackToken}}" +) + +$ErrorActionPreference="Stop" + +function Invoke-FastWebRequest { + [CmdletBinding()] + Param( + [Parameter(Mandatory=$True,ValueFromPipeline=$true,Position=0)] + [System.Uri]$Uri, + [Parameter(Position=1)] + [string]$OutFile, + [Hashtable]$Headers=@{}, + [switch]$SkipIntegrityCheck=$false + ) + PROCESS + { + if(!([System.Management.Automation.PSTypeName]'System.Net.Http.HttpClient').Type) + { + $assembly = [System.Reflection.Assembly]::LoadWithPartialName("System.Net.Http") + } + + if(!$OutFile) { + $OutFile = $Uri.PathAndQuery.Substring($Uri.PathAndQuery.LastIndexOf("/") + 1) + if(!$OutFile) { + throw "The ""OutFile"" parameter needs to be specified" + } + } + + $fragment = $Uri.Fragment.Trim('#') + if ($fragment) { + $details = $fragment.Split("=") + $algorithm = $details[0] + $hash = $details[1] + } + + if (!$SkipIntegrityCheck -and $fragment -and (Test-Path $OutFile)) { + try { + return (Test-FileIntegrity -File $OutFile -Algorithm $algorithm -ExpectedHash $hash) + } catch { + Remove-Item $OutFile + } + } + + $client = new-object System.Net.Http.HttpClient + foreach ($k in $Headers.Keys){ + $client.DefaultRequestHeaders.Add($k, $Headers[$k]) + } + $task = $client.GetStreamAsync($Uri) + $response = $task.Result + if($task.IsFaulted) { + $msg = "Request for URL '{0}' is faulted. Task status: {1}." -f @($Uri, $task.Status) + if($task.Exception) { + $msg += "Exception details: {0}" -f @($task.Exception) + } + Throw $msg + } + $outStream = New-Object IO.FileStream $OutFile, Create, Write, None + + try { + $totRead = 0 + $buffer = New-Object Byte[] 1MB + while (($read = $response.Read($buffer, 0, $buffer.Length)) -gt 0) { + $totRead += $read + $outStream.Write($buffer, 0, $read); + } + } + finally { + $outStream.Close() + } + if(!$SkipIntegrityCheck -and $fragment) { + Test-FileIntegrity -File $OutFile -Algorithm $algorithm -ExpectedHash $hash + } + } +} + +function Import-Certificate() { + [CmdletBinding()] + param ( + [parameter(Mandatory=$true)] + [string]$CertificatePath, + [parameter(Mandatory=$true)] + [System.Security.Cryptography.X509Certificates.StoreLocation]$StoreLocation="LocalMachine", + [parameter(Mandatory=$true)] + [System.Security.Cryptography.X509Certificates.StoreName]$StoreName="TrustedPublisher" + ) + PROCESS + { + $store = New-Object System.Security.Cryptography.X509Certificates.X509Store( + $StoreName, $StoreLocation) + $store.Open([System.Security.Cryptography.X509Certificates.OpenFlags]::ReadWrite) + $cert = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2( + $CertificatePath) + $store.Add($cert) + } +} + +function Invoke-APICall() { + [CmdletBinding()] + param ( + [parameter(Mandatory=$true)] + [object]$Payload, + [parameter(Mandatory=$true)] + [string]$CallbackURL + ) + PROCESS{ + Invoke-WebRequest -UseBasicParsing -Method Post -Headers @{"Accept"="application/json"; "Authorization"="Bearer $Token"} -Uri $CallbackURL -Body (ConvertTo-Json $Payload) | Out-Null + } +} + +function Update-GarmStatus() { + [CmdletBinding()] + param ( + [parameter(Mandatory=$true)] + [string]$Message, + [parameter(Mandatory=$true)] + [string]$CallbackURL + ) + PROCESS{ + $body = @{ + "status"="installing" + "message"=$Message + } + Invoke-APICall -Payload $body -CallbackURL $CallbackURL | Out-Null + } +} + +function Invoke-GarmSuccess() { + [CmdletBinding()] + param ( + [parameter(Mandatory=$true)] + [string]$Message, + [parameter(Mandatory=$true)] + [int64]$AgentID, + [parameter(Mandatory=$true)] + [string]$CallbackURL + ) + PROCESS{ + $body = @{ + "status"="idle" + "message"=$Message + "agent_id"=$AgentID + } + Invoke-APICall -Payload $body -CallbackURL $CallbackURL | Out-Null + } +} + +function Invoke-GarmFailure() { + [CmdletBinding()] + param ( + [parameter(Mandatory=$true)] + [string]$Message, + [parameter(Mandatory=$true)] + [string]$CallbackURL + ) + PROCESS{ + $body = @{ + "status"="failed" + "message"=$Message + } + Invoke-APICall -Payload $body -CallbackURL $CallbackURL | Out-Null + Throw $Message + } +} + +$PEMData = @" +{{.CABundle}} +"@ + +function Install-Runner() { + $CallbackURL="{{.CallbackURL}}" + if ($Token.Length -eq 0) { + Throw "missing callback authentication token" + } + try { + $MetadataURL="{{.MetadataURL}}" + $DownloadURL="{{.DownloadURL}}" + if($MetadataURL -eq ""){ + Throw "missing metadata URL" + } + + if($PEMData.Trim().Length -gt 0){ + Set-Content $env:TMP\garm-ca.pem $PEMData + Import-Certificate -CertificatePath $env:TMP\garm-ca.pem + } + + $GithubRegistrationToken = Invoke-WebRequest -UseBasicParsing -Headers @{"Accept"="application/json"; "Authorization"="Bearer $Token"} -Uri $MetadataURL/runner-registration-token/ + Update-GarmStatus -CallbackURL $CallbackURL -Message "downloading tools from $DownloadURL" + + $downloadToken="{{.TempDownloadToken}}" + $DownloadTokenHeaders=@{} + if ($downloadToken.Length -gt 0) { + $DownloadTokenHeaders=@{ + "Authorization"="Bearer $downloadToken" + } + } + $downloadPath = Join-Path $env:TMP {{.FileName}} + Invoke-FastWebRequest -Uri $DownloadURL -OutFile $downloadPath -Headers $DownloadTokenHeaders + + $runnerDir = "C:\runner" + mkdir $runnerDir + + Update-GarmStatus -CallbackURL $CallbackURL -Message "extracting runner" + Add-Type -AssemblyName System.IO.Compression.FileSystem + [System.IO.Compression.ZipFile]::ExtractToDirectory($downloadPath, "$runnerDir") + + Update-GarmStatus -CallbackURL $CallbackURL -Message "configuring and starting runner" + cd $runnerDir + ./config.cmd --unattended --url "{{ .RepoURL }}" --token $GithubRegistrationToken --name "{{ .RunnerName }}" --labels "{{ .RunnerLabels }}" --ephemeral --runasservice + + $agentInfoFile = Join-Path $runnerDir ".runner" + $agentInfo = ConvertFrom-Json (gc -raw $agentInfoFile) + Invoke-GarmSuccess -CallbackURL $CallbackURL -Message "runner successfully installed" -AgentID $agentInfo.agentId + } catch { + Invoke-GarmFailure -CallbackURL $CallbackURL -Message $_ + } +} +Install-Runner +` + type InstallRunnerParams struct { FileName string DownloadURL string @@ -113,17 +338,27 @@ type InstallRunnerParams struct { CallbackURL string CallbackToken string TempDownloadToken string + CABundle string } -func InstallRunnerScript(params InstallRunnerParams) ([]byte, error) { +func InstallRunnerScript(installParams InstallRunnerParams, osType params.OSType) ([]byte, error) { + var tpl string + switch osType { + case params.Linux: + tpl = CloudConfigTemplate + case params.Windows: + tpl = WindowsSetupScriptTemplate + default: + return nil, fmt.Errorf("unsupported os type: %s", osType) + } - t, err := template.New("").Parse(CloudConfigTemplate) + t, err := template.New("").Parse(tpl) if err != nil { return nil, errors.Wrap(err, "parsing template") } var buf bytes.Buffer - if err := t.Execute(&buf, params); err != nil { + if err := t.Execute(&buf, installParams); err != nil { return nil, errors.Wrap(err, "rendering template") } diff --git a/runner/types.go b/runner/types.go index 3f132730..3a081e09 100644 --- a/runner/types.go +++ b/runner/types.go @@ -25,7 +25,6 @@ const ( ) var ( - // Linux only for now. Will add Windows soon. (famous last words?) supportedOSType map[params.OSType]struct{} = map[params.OSType]struct{}{ params.Linux: {}, params.Windows: {}, diff --git a/util/util.go b/util/util.go index 34d907be..76a36e90 100644 --- a/util/util.go +++ b/util/util.go @@ -226,8 +226,6 @@ func GithubClient(ctx context.Context, token string, credsDetails params.GithubC } func GetCloudConfig(bootstrapParams params.BootstrapInstance, tools github.RunnerApplicationDownload, runnerName string) (string, error) { - cloudCfg := cloudconfig.NewDefaultCloudInitConfig() - if tools.Filename == nil { return "", fmt.Errorf("missing tools filename") } @@ -254,27 +252,39 @@ func GetCloudConfig(bootstrapParams params.BootstrapInstance, tools github.Runne CallbackURL: bootstrapParams.CallbackURL, CallbackToken: bootstrapParams.InstanceToken, } + if bootstrapParams.CACertBundle != nil && len(bootstrapParams.CACertBundle) > 0 { + installRunnerParams.CABundle = string(bootstrapParams.CACertBundle) + } - installScript, err := cloudconfig.InstallRunnerScript(installRunnerParams) + installScript, err := cloudconfig.InstallRunnerScript(installRunnerParams, bootstrapParams.OSType) if err != nil { return "", errors.Wrap(err, "generating script") } - cloudCfg.AddSSHKey(bootstrapParams.SSHKeys...) - cloudCfg.AddFile(installScript, "/install_runner.sh", "root:root", "755") - cloudCfg.AddRunCmd("/install_runner.sh") - cloudCfg.AddRunCmd("rm -f /install_runner.sh") - - if bootstrapParams.CACertBundle != nil && len(bootstrapParams.CACertBundle) > 0 { - if err := cloudCfg.AddCACert(bootstrapParams.CACertBundle); err != nil { - return "", errors.Wrap(err, "adding CA cert bundle") + var asStr string + switch bootstrapParams.OSType { + case params.Linux: + cloudCfg := cloudconfig.NewDefaultCloudInitConfig() + cloudCfg.AddSSHKey(bootstrapParams.SSHKeys...) + cloudCfg.AddFile(installScript, "/install_runner.sh", "root:root", "755") + cloudCfg.AddRunCmd("/install_runner.sh") + cloudCfg.AddRunCmd("rm -f /install_runner.sh") + if bootstrapParams.CACertBundle != nil && len(bootstrapParams.CACertBundle) > 0 { + if err := cloudCfg.AddCACert(bootstrapParams.CACertBundle); err != nil { + return "", errors.Wrap(err, "adding CA cert bundle") + } } + var err error + asStr, err = cloudCfg.Serialize() + if err != nil { + return "", errors.Wrap(err, "creating cloud config") + } + case params.Windows: + asStr = string(installScript) + default: + return "", fmt.Errorf("unknown os type: %s", bootstrapParams.OSType) } - asStr, err := cloudCfg.Serialize() - if err != nil { - return "", errors.Wrap(err, "creating cloud config") - } return asStr, nil }