Add Windows userdata

Signed-off-by: Gabriel Adrian Samfira <gsamfira@cloudbasesolutions.com>
This commit is contained in:
Gabriel Adrian Samfira 2023-03-20 18:54:45 +02:00
parent 47790b565e
commit c6366ab896
No known key found for this signature in database
GPG key ID: 7D073DCC2C074CB5
3 changed files with 263 additions and 19 deletions

View file

@ -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")
}

View file

@ -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: {},

View file

@ -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
}