Move code to external package
Signed-off-by: Gabriel Adrian Samfira <gsamfira@cloudbasesolutions.com>
This commit is contained in:
parent
14586f0154
commit
da13cec2de
195 changed files with 56394 additions and 364 deletions
182
vendor/github.com/cloudbase/garm-provider-common/cloudconfig/cloudconfig.go
generated
vendored
Normal file
182
vendor/github.com/cloudbase/garm-provider-common/cloudconfig/cloudconfig.go
generated
vendored
Normal file
|
|
@ -0,0 +1,182 @@
|
|||
// Copyright 2022 Cloudbase Solutions SRL
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
// not use this file except in compliance with the License. You may obtain
|
||||
// a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
// License for the specific language governing permissions and limitations
|
||||
// under the License.
|
||||
|
||||
package cloudconfig
|
||||
|
||||
import (
|
||||
"crypto/x509"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/cloudbase/garm-provider-common/defaults"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
func NewDefaultCloudInitConfig() *CloudInit {
|
||||
return &CloudInit{
|
||||
PackageUpgrade: true,
|
||||
Packages: []string{
|
||||
"curl",
|
||||
"tar",
|
||||
},
|
||||
SystemInfo: &SystemInfo{
|
||||
DefaultUser: DefaultUser{
|
||||
Name: defaults.DefaultUser,
|
||||
Home: fmt.Sprintf("/home/%s", defaults.DefaultUser),
|
||||
Shell: defaults.DefaultUserShell,
|
||||
Groups: defaults.DefaultUserGroups,
|
||||
Sudo: "ALL=(ALL) NOPASSWD:ALL",
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
type DefaultUser struct {
|
||||
Name string `yaml:"name"`
|
||||
Home string `yaml:"home"`
|
||||
Shell string `yaml:"shell"`
|
||||
Groups []string `yaml:"groups,omitempty"`
|
||||
Sudo string `yaml:"sudo"`
|
||||
}
|
||||
|
||||
type SystemInfo struct {
|
||||
DefaultUser DefaultUser `yaml:"default_user"`
|
||||
}
|
||||
|
||||
type File struct {
|
||||
Encoding string `yaml:"encoding"`
|
||||
Content string `yaml:"content"`
|
||||
Owner string `yaml:"owner"`
|
||||
Path string `yaml:"path"`
|
||||
Permissions string `yaml:"permissions"`
|
||||
}
|
||||
|
||||
type CloudInit struct {
|
||||
mux sync.Mutex
|
||||
|
||||
PackageUpgrade bool `yaml:"package_upgrade"`
|
||||
Packages []string `yaml:"packages,omitempty"`
|
||||
SSHAuthorizedKeys []string `yaml:"ssh_authorized_keys,omitempty"`
|
||||
SystemInfo *SystemInfo `yaml:"system_info,omitempty"`
|
||||
RunCmd []string `yaml:"runcmd,omitempty"`
|
||||
WriteFiles []File `yaml:"write_files,omitempty"`
|
||||
CACerts CACerts `yaml:"ca-certs,omitempty"`
|
||||
}
|
||||
|
||||
type CACerts struct {
|
||||
RemoveDefaults bool `yaml:"remove-defaults"`
|
||||
Trusted []string `yaml:"trusted"`
|
||||
}
|
||||
|
||||
func (c *CloudInit) AddCACert(cert []byte) error {
|
||||
c.mux.Lock()
|
||||
defer c.mux.Unlock()
|
||||
|
||||
if cert == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
roots := x509.NewCertPool()
|
||||
if ok := roots.AppendCertsFromPEM(cert); !ok {
|
||||
return fmt.Errorf("failed to parse CA cert bundle")
|
||||
}
|
||||
c.CACerts.Trusted = append(c.CACerts.Trusted, string(cert))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *CloudInit) AddSSHKey(keys ...string) {
|
||||
c.mux.Lock()
|
||||
defer c.mux.Unlock()
|
||||
|
||||
// TODO(gabriel-samfira): Validate the SSH public key.
|
||||
for _, key := range keys {
|
||||
found := false
|
||||
for _, val := range c.SSHAuthorizedKeys {
|
||||
if val == key {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
c.SSHAuthorizedKeys = append(c.SSHAuthorizedKeys, key)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *CloudInit) AddPackage(pkgs ...string) {
|
||||
c.mux.Lock()
|
||||
defer c.mux.Unlock()
|
||||
|
||||
for _, pkg := range pkgs {
|
||||
found := false
|
||||
for _, val := range c.Packages {
|
||||
if val == pkg {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
c.Packages = append(c.Packages, pkg)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *CloudInit) AddRunCmd(cmd string) {
|
||||
c.mux.Lock()
|
||||
defer c.mux.Unlock()
|
||||
|
||||
c.RunCmd = append(c.RunCmd, cmd)
|
||||
}
|
||||
|
||||
func (c *CloudInit) AddFile(contents []byte, path, owner, permissions string) {
|
||||
c.mux.Lock()
|
||||
defer c.mux.Unlock()
|
||||
|
||||
for _, val := range c.WriteFiles {
|
||||
if val.Path == path {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
file := File{
|
||||
Encoding: "b64",
|
||||
Content: base64.StdEncoding.EncodeToString(contents),
|
||||
Owner: owner,
|
||||
Permissions: permissions,
|
||||
Path: path,
|
||||
}
|
||||
c.WriteFiles = append(c.WriteFiles, file)
|
||||
}
|
||||
|
||||
func (c *CloudInit) Serialize() (string, error) {
|
||||
c.mux.Lock()
|
||||
defer c.mux.Unlock()
|
||||
|
||||
ret := []string{
|
||||
"#cloud-config",
|
||||
}
|
||||
|
||||
asYaml, err := yaml.Marshal(c)
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "marshaling to yaml")
|
||||
}
|
||||
|
||||
ret = append(ret, string(asYaml))
|
||||
return strings.Join(ret, "\n"), nil
|
||||
}
|
||||
451
vendor/github.com/cloudbase/garm-provider-common/cloudconfig/templates.go
generated
vendored
Normal file
451
vendor/github.com/cloudbase/garm-provider-common/cloudconfig/templates.go
generated
vendored
Normal file
|
|
@ -0,0 +1,451 @@
|
|||
// Copyright 2022 Cloudbase Solutions SRL
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
// not use this file except in compliance with the License. You may obtain
|
||||
// a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
// License for the specific language governing permissions and limitations
|
||||
// under the License.
|
||||
|
||||
package cloudconfig
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"text/template"
|
||||
|
||||
"github.com/cloudbase/garm-provider-common/params"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
var CloudConfigTemplate = `#!/bin/bash
|
||||
|
||||
set -e
|
||||
set -o pipefail
|
||||
|
||||
CALLBACK_URL="{{ .CallbackURL }}"
|
||||
METADATA_URL="{{ .MetadataURL }}"
|
||||
BEARER_TOKEN="{{ .CallbackToken }}"
|
||||
|
||||
if [ -z "$METADATA_URL" ];then
|
||||
echo "no token is available and METADATA_URL is not set"
|
||||
exit 1
|
||||
fi
|
||||
GITHUB_TOKEN=$(curl --retry 5 --retry-delay 5 --retry-connrefused --fail -s -X GET -H 'Accept: application/json' -H "Authorization: Bearer ${BEARER_TOKEN}" "${METADATA_URL}/runner-registration-token/")
|
||||
|
||||
function call() {
|
||||
PAYLOAD="$1"
|
||||
[[ $CALLBACK_URL =~ ^(.*)/status$ ]]
|
||||
if [ -z "$BASH_REMATCH" ];then
|
||||
CALLBACK_URL="${CALLBACK_URL}/status"
|
||||
fi
|
||||
curl --retry 5 --retry-delay 5 --retry-connrefused --fail -s -X POST -d "${PAYLOAD}" -H 'Accept: application/json' -H "Authorization: Bearer ${BEARER_TOKEN}" "${CALLBACK_URL}" || echo "failed to call home: exit code ($?)"
|
||||
}
|
||||
|
||||
function sendStatus() {
|
||||
MSG="$1"
|
||||
call "{\"status\": \"installing\", \"message\": \"$MSG\"}"
|
||||
}
|
||||
|
||||
function success() {
|
||||
MSG="$1"
|
||||
ID=$2
|
||||
call "{\"status\": \"idle\", \"message\": \"$MSG\", \"agent_id\": $ID}"
|
||||
}
|
||||
|
||||
function fail() {
|
||||
MSG="$1"
|
||||
call "{\"status\": \"failed\", \"message\": \"$MSG\"}"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# This will echo the version number in the filename. Given a file name like: actions-runner-osx-x64-2.299.1.tar.gz
|
||||
# this will output: 2.299.1
|
||||
function getRunnerVersion() {
|
||||
FILENAME="{{ .FileName }}"
|
||||
[[ $FILENAME =~ ([0-9]+\.[0-9]+\.[0-9+]) ]]
|
||||
echo $BASH_REMATCH
|
||||
}
|
||||
|
||||
function getCachedToolsPath() {
|
||||
CACHED_RUNNER="/opt/cache/actions-runner/latest"
|
||||
if [ -d "$CACHED_RUNNER" ];then
|
||||
echo "$CACHED_RUNNER"
|
||||
return 0
|
||||
fi
|
||||
|
||||
VERSION=$(getRunnerVersion)
|
||||
if [ -z "$VERSION" ]; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
CACHED_RUNNER="/opt/cache/actions-runner/$VERSION"
|
||||
if [ -d "$CACHED_RUNNER" ];then
|
||||
echo "$CACHED_RUNNER"
|
||||
return 0
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
|
||||
function downloadAndExtractRunner() {
|
||||
sendStatus "downloading tools from {{ .DownloadURL }}"
|
||||
if [ ! -z "{{ .TempDownloadToken }}" ]; then
|
||||
TEMP_TOKEN="Authorization: Bearer {{ .TempDownloadToken }}"
|
||||
fi
|
||||
curl --retry 5 --retry-delay 5 --retry-connrefused --fail -L -H "${TEMP_TOKEN}" -o "/home/{{ .RunnerUsername }}/{{ .FileName }}" "{{ .DownloadURL }}" || fail "failed to download tools"
|
||||
mkdir -p /home/{{ .RunnerUsername }}/actions-runner || fail "failed to create actions-runner folder"
|
||||
sendStatus "extracting runner"
|
||||
tar xf "/home/{{ .RunnerUsername }}/{{ .FileName }}" -C /home/{{ .RunnerUsername }}/actions-runner/ || fail "failed to extract runner"
|
||||
# chown {{ .RunnerUsername }}:{{ .RunnerGroup }} -R /home/{{ .RunnerUsername }}/actions-runner/ || fail "failed to change owner"
|
||||
}
|
||||
|
||||
TEMP_TOKEN=""
|
||||
GH_RUNNER_GROUP="{{.GitHubRunnerGroup}}"
|
||||
|
||||
# $RUNNER_GROUP_OPT will be added to the config.sh line. If it's empty, nothing happens
|
||||
# if it holds a value, it will be part of the command.
|
||||
RUNNER_GROUP_OPT=""
|
||||
if [ ! -z $GH_RUNNER_GROUP ];then
|
||||
RUNNER_GROUP_OPT="--runnergroup=$GH_RUNNER_GROUP"
|
||||
fi
|
||||
|
||||
CACHED_RUNNER=$(getCachedToolsPath)
|
||||
if [ -z "$CACHED_RUNNER" ];then
|
||||
downloadAndExtractRunner
|
||||
sendStatus "installing dependencies"
|
||||
cd /home/{{ .RunnerUsername }}/actions-runner
|
||||
sudo ./bin/installdependencies.sh || fail "failed to install dependencies"
|
||||
else
|
||||
sendStatus "using cached runner found in $CACHED_RUNNER"
|
||||
sudo cp -a "$CACHED_RUNNER" "/home/{{ .RunnerUsername }}/actions-runner"
|
||||
sudo chown {{ .RunnerUsername }}:{{ .RunnerGroup }} -R "/home/{{ .RunnerUsername }}/actions-runner" || fail "failed to change owner"
|
||||
cd /home/{{ .RunnerUsername }}/actions-runner
|
||||
fi
|
||||
|
||||
|
||||
sendStatus "configuring runner"
|
||||
set +e
|
||||
attempt=1
|
||||
while true; do
|
||||
ERROUT=$(mktemp)
|
||||
./config.sh --unattended --url "{{ .RepoURL }}" --token "$GITHUB_TOKEN" $RUNNER_GROUP_OPT --name "{{ .RunnerName }}" --labels "{{ .RunnerLabels }}" --ephemeral 2>$ERROUT
|
||||
if [ $? -eq 0 ]; then
|
||||
rm $ERROUT || true
|
||||
sendStatus "runner successfully configured after $attempt attempt(s)"
|
||||
break
|
||||
fi
|
||||
LAST_ERR=$(cat $ERROUT)
|
||||
echo "$LAST_ERR"
|
||||
|
||||
# if the runner is already configured, remove it and try again. In the past configuring a runner
|
||||
# managed to register it but timed out later, resulting in an error.
|
||||
./config.sh remove --token "$GITHUB_TOKEN" || true
|
||||
|
||||
if [ $attempt -gt 5 ];then
|
||||
rm $ERROUT || true
|
||||
fail "failed to configure runner: $LAST_ERR"
|
||||
fi
|
||||
|
||||
sendStatus "failed to configure runner (attempt $attempt): $LAST_ERR (retrying in 5 seconds)"
|
||||
attempt=$((attempt+1))
|
||||
rm $ERROUT || true
|
||||
sleep 5
|
||||
done
|
||||
set -e
|
||||
|
||||
sendStatus "installing runner service"
|
||||
sudo ./svc.sh install {{ .RunnerUsername }} || fail "failed to install service"
|
||||
|
||||
if [ -e "/sys/fs/selinux" ];then
|
||||
sudo chcon -h user_u:object_r:bin_t /home/runner/ || fail "failed to change selinux context"
|
||||
sudo chcon -R -h {{ .RunnerUsername }}:object_r:bin_t /home/runner/* || fail "failed to change selinux context"
|
||||
fi
|
||||
|
||||
sendStatus "starting service"
|
||||
sudo ./svc.sh start || fail "failed to start service"
|
||||
|
||||
set +e
|
||||
AGENT_ID=$(grep "agentId" /home/{{ .RunnerUsername }}/actions-runner/.runner | tr -d -c 0-9)
|
||||
if [ $? -ne 0 ];then
|
||||
fail "failed to get agent ID"
|
||||
fi
|
||||
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}}
|
||||
"@
|
||||
$GHRunnerGroup = "{{.GitHubRunnerGroup}}"
|
||||
|
||||
function Install-Runner() {
|
||||
$CallbackURL="{{.CallbackURL}}"
|
||||
if (!$CallbackURL.EndsWith("/status")) {
|
||||
$CallbackURL = "$CallbackURL/status"
|
||||
}
|
||||
|
||||
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")
|
||||
$runnerGroupOpt = ""
|
||||
if ($GHRunnerGroup.Length -gt 0){
|
||||
$runnerGroupOpt = "--runnergroup $GHRunnerGroup"
|
||||
}
|
||||
Update-GarmStatus -CallbackURL $CallbackURL -Message "configuring and starting runner"
|
||||
cd $runnerDir
|
||||
./config.cmd --unattended --url "{{ .RepoURL }}" --token $GithubRegistrationToken $runnerGroupOpt --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
|
||||
RunnerUsername string
|
||||
RunnerGroup string
|
||||
RepoURL string
|
||||
MetadataURL string
|
||||
RunnerName string
|
||||
RunnerLabels string
|
||||
CallbackURL string
|
||||
CallbackToken string
|
||||
TempDownloadToken string
|
||||
CABundle string
|
||||
GitHubRunnerGroup string
|
||||
}
|
||||
|
||||
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(tpl)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "parsing template")
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
if err := t.Execute(&buf, installParams); err != nil {
|
||||
return nil, errors.Wrap(err, "rendering template")
|
||||
}
|
||||
|
||||
return buf.Bytes(), nil
|
||||
}
|
||||
17
vendor/github.com/cloudbase/garm-provider-common/defaults/defaults.go
generated
vendored
Normal file
17
vendor/github.com/cloudbase/garm-provider-common/defaults/defaults.go
generated
vendored
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
package defaults
|
||||
|
||||
const (
|
||||
// DefaultUser is the default username that should exist on the instances.
|
||||
DefaultUser = "runner"
|
||||
// DefaultUserShell is the shell for the default user.
|
||||
DefaultUserShell = "/bin/bash"
|
||||
)
|
||||
|
||||
var (
|
||||
// DefaultUserGroups are the groups the default user will be part of.
|
||||
DefaultUserGroups = []string{
|
||||
"sudo", "adm", "cdrom", "dialout",
|
||||
"dip", "video", "plugdev", "netdev",
|
||||
"docker", "lxd",
|
||||
}
|
||||
)
|
||||
13
vendor/github.com/cloudbase/garm-provider-common/execution/commands.go
generated
vendored
Normal file
13
vendor/github.com/cloudbase/garm-provider-common/execution/commands.go
generated
vendored
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
package execution
|
||||
|
||||
type ExecutionCommand string
|
||||
|
||||
const (
|
||||
CreateInstanceCommand ExecutionCommand = "CreateInstance"
|
||||
DeleteInstanceCommand ExecutionCommand = "DeleteInstance"
|
||||
GetInstanceCommand ExecutionCommand = "GetInstance"
|
||||
ListInstancesCommand ExecutionCommand = "ListInstances"
|
||||
StartInstanceCommand ExecutionCommand = "StartInstance"
|
||||
StopInstanceCommand ExecutionCommand = "StopInstance"
|
||||
RemoveAllInstancesCommand ExecutionCommand = "RemoveAllInstances"
|
||||
)
|
||||
172
vendor/github.com/cloudbase/garm-provider-common/execution/execution.go
generated
vendored
Normal file
172
vendor/github.com/cloudbase/garm-provider-common/execution/execution.go
generated
vendored
Normal file
|
|
@ -0,0 +1,172 @@
|
|||
package execution
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
|
||||
"github.com/cloudbase/garm-provider-common/params"
|
||||
|
||||
"github.com/mattn/go-isatty"
|
||||
)
|
||||
|
||||
const (
|
||||
// ExitCodeNotFound is an exit code that indicates a Not Found error
|
||||
ExitCodeNotFound int = 30
|
||||
// ExitCodeDuplicate is an exit code that indicates a duplicate error
|
||||
ExitCodeDuplicate int = 31
|
||||
)
|
||||
|
||||
func GetEnvironment() (Environment, error) {
|
||||
env := Environment{
|
||||
Command: ExecutionCommand(os.Getenv("GARM_COMMAND")),
|
||||
ControllerID: os.Getenv("GARM_CONTROLLER_ID"),
|
||||
PoolID: os.Getenv("GARM_POOL_ID"),
|
||||
ProviderConfigFile: os.Getenv("GARM_PROVIDER_CONFIG_FILE"),
|
||||
InstanceID: os.Getenv("GARM_INSTANCE_ID"),
|
||||
}
|
||||
|
||||
// If this is a CreateInstance command, we need to get the bootstrap params
|
||||
// from stdin
|
||||
if env.Command == CreateInstanceCommand {
|
||||
if isatty.IsTerminal(os.Stdin.Fd()) || isatty.IsCygwinTerminal(os.Stdin.Fd()) {
|
||||
return Environment{}, fmt.Errorf("%s requires data passed into stdin", CreateInstanceCommand)
|
||||
}
|
||||
|
||||
var data bytes.Buffer
|
||||
if _, err := io.Copy(&data, os.Stdin); err != nil {
|
||||
return Environment{}, fmt.Errorf("failed to copy bootstrap params")
|
||||
}
|
||||
|
||||
if data.Len() == 0 {
|
||||
return Environment{}, fmt.Errorf("%s requires data passed into stdin", CreateInstanceCommand)
|
||||
}
|
||||
|
||||
var bootstrapParams params.BootstrapInstance
|
||||
if err := json.Unmarshal(data.Bytes(), &bootstrapParams); err != nil {
|
||||
return Environment{}, fmt.Errorf("failed to decode instance params: %w", err)
|
||||
}
|
||||
env.BootstrapParams = bootstrapParams
|
||||
}
|
||||
|
||||
if err := env.Validate(); err != nil {
|
||||
return Environment{}, fmt.Errorf("failed to validate execution environment: %w", err)
|
||||
}
|
||||
|
||||
return env, nil
|
||||
}
|
||||
|
||||
type Environment struct {
|
||||
Command ExecutionCommand
|
||||
ControllerID string
|
||||
PoolID string
|
||||
ProviderConfigFile string
|
||||
InstanceID string
|
||||
BootstrapParams params.BootstrapInstance
|
||||
}
|
||||
|
||||
func (e Environment) Validate() error {
|
||||
if e.Command == "" {
|
||||
return fmt.Errorf("missing GARM_COMMAND")
|
||||
}
|
||||
|
||||
if e.ProviderConfigFile == "" {
|
||||
return fmt.Errorf("missing GARM_PROVIDER_CONFIG_FILE")
|
||||
}
|
||||
|
||||
if _, err := os.Lstat(e.ProviderConfigFile); err != nil {
|
||||
return fmt.Errorf("error accessing config file: %w", err)
|
||||
}
|
||||
|
||||
if e.ControllerID == "" {
|
||||
return fmt.Errorf("missing GARM_CONTROLLER_ID")
|
||||
}
|
||||
|
||||
switch e.Command {
|
||||
case CreateInstanceCommand:
|
||||
if e.BootstrapParams.Name == "" {
|
||||
return fmt.Errorf("missing bootstrap params")
|
||||
}
|
||||
if e.ControllerID == "" {
|
||||
return fmt.Errorf("missing controller ID")
|
||||
}
|
||||
if e.PoolID == "" {
|
||||
return fmt.Errorf("missing pool ID")
|
||||
}
|
||||
case DeleteInstanceCommand, GetInstanceCommand,
|
||||
StartInstanceCommand, StopInstanceCommand:
|
||||
if e.InstanceID == "" {
|
||||
return fmt.Errorf("missing instance ID")
|
||||
}
|
||||
case ListInstancesCommand:
|
||||
if e.PoolID == "" {
|
||||
return fmt.Errorf("missing pool ID")
|
||||
}
|
||||
case RemoveAllInstancesCommand:
|
||||
if e.ControllerID == "" {
|
||||
return fmt.Errorf("missing controller ID")
|
||||
}
|
||||
default:
|
||||
return fmt.Errorf("unknown GARM_COMMAND: %s", e.Command)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func Run(ctx context.Context, provider ExternalProvider, env Environment) (string, error) {
|
||||
var ret string
|
||||
switch env.Command {
|
||||
case CreateInstanceCommand:
|
||||
instance, err := provider.CreateInstance(ctx, env.BootstrapParams)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to create instance in provider: %w", err)
|
||||
}
|
||||
|
||||
asJs, err := json.Marshal(instance)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to marshal response: %w", err)
|
||||
}
|
||||
ret = string(asJs)
|
||||
case GetInstanceCommand:
|
||||
instance, err := provider.GetInstance(ctx, env.InstanceID)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to get instance from provider: %w", err)
|
||||
}
|
||||
asJs, err := json.Marshal(instance)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to marshal response: %w", err)
|
||||
}
|
||||
ret = string(asJs)
|
||||
case ListInstancesCommand:
|
||||
instances, err := provider.ListInstances(ctx, env.PoolID)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to list instances from provider: %w", err)
|
||||
}
|
||||
asJs, err := json.Marshal(instances)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to marshal response: %w", err)
|
||||
}
|
||||
ret = string(asJs)
|
||||
case DeleteInstanceCommand:
|
||||
if err := provider.DeleteInstance(ctx, env.InstanceID); err != nil {
|
||||
return "", fmt.Errorf("failed to delete instance from provider: %w", err)
|
||||
}
|
||||
case RemoveAllInstancesCommand:
|
||||
if err := provider.RemoveAllInstances(ctx); err != nil {
|
||||
return "", fmt.Errorf("failed to destroy environment: %w", err)
|
||||
}
|
||||
case StartInstanceCommand:
|
||||
if err := provider.Start(ctx, env.InstanceID); err != nil {
|
||||
return "", fmt.Errorf("failed to start instance: %w", err)
|
||||
}
|
||||
case StopInstanceCommand:
|
||||
if err := provider.Stop(ctx, env.InstanceID, true); err != nil {
|
||||
return "", fmt.Errorf("failed to stop instance: %w", err)
|
||||
}
|
||||
default:
|
||||
return "", fmt.Errorf("invalid command: %s", env.Command)
|
||||
}
|
||||
return ret, nil
|
||||
}
|
||||
27
vendor/github.com/cloudbase/garm-provider-common/execution/interface.go
generated
vendored
Normal file
27
vendor/github.com/cloudbase/garm-provider-common/execution/interface.go
generated
vendored
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
package execution
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/cloudbase/garm-provider-common/params"
|
||||
)
|
||||
|
||||
// ExternalProvider defines an interface that external providers need to implement.
|
||||
// This is very similar to the common.Provider interface, and was redefined here to
|
||||
// decouple it, in case it may diverge from native providers.
|
||||
type ExternalProvider interface {
|
||||
// CreateInstance creates a new compute instance in the provider.
|
||||
CreateInstance(ctx context.Context, bootstrapParams params.BootstrapInstance) (params.ProviderInstance, error)
|
||||
// Delete instance will delete the instance in a provider.
|
||||
DeleteInstance(ctx context.Context, instance string) error
|
||||
// GetInstance will return details about one instance.
|
||||
GetInstance(ctx context.Context, instance string) (params.ProviderInstance, error)
|
||||
// ListInstances will list all instances for a provider.
|
||||
ListInstances(ctx context.Context, poolID string) ([]params.ProviderInstance, error)
|
||||
// RemoveAllInstances will remove all instances created by this provider.
|
||||
RemoveAllInstances(ctx context.Context) error
|
||||
// Stop shuts down the instance.
|
||||
Stop(ctx context.Context, instance string, force bool) error
|
||||
// Start boots up an instance.
|
||||
Start(ctx context.Context, instance string) error
|
||||
}
|
||||
144
vendor/github.com/cloudbase/garm-provider-common/params/params.go
generated
vendored
Normal file
144
vendor/github.com/cloudbase/garm-provider-common/params/params.go
generated
vendored
Normal file
|
|
@ -0,0 +1,144 @@
|
|||
package params
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/google/go-github/v48/github"
|
||||
)
|
||||
|
||||
type (
|
||||
AddressType string
|
||||
InstanceStatus string
|
||||
OSType string
|
||||
OSArch string
|
||||
)
|
||||
|
||||
const (
|
||||
Windows OSType = "windows"
|
||||
Linux OSType = "linux"
|
||||
Unknown OSType = "unknown"
|
||||
)
|
||||
|
||||
const (
|
||||
Amd64 OSArch = "amd64"
|
||||
I386 OSArch = "i386"
|
||||
Arm64 OSArch = "arm64"
|
||||
Arm OSArch = "arm"
|
||||
)
|
||||
|
||||
const (
|
||||
InstanceRunning InstanceStatus = "running"
|
||||
InstanceStopped InstanceStatus = "stopped"
|
||||
InstanceError InstanceStatus = "error"
|
||||
InstancePendingDelete InstanceStatus = "pending_delete"
|
||||
InstanceDeleting InstanceStatus = "deleting"
|
||||
InstancePendingCreate InstanceStatus = "pending_create"
|
||||
InstanceCreating InstanceStatus = "creating"
|
||||
InstanceStatusUnknown InstanceStatus = "unknown"
|
||||
)
|
||||
|
||||
const (
|
||||
PublicAddress AddressType = "public"
|
||||
PrivateAddress AddressType = "private"
|
||||
)
|
||||
|
||||
type UserDataOptions struct {
|
||||
DisableUpdatesOnBoot bool `json:"disable_updates_on_boot"`
|
||||
ExtraPackages []string `json:"extra_packages"`
|
||||
}
|
||||
|
||||
type BootstrapInstance struct {
|
||||
Name string `json:"name"`
|
||||
Tools []*github.RunnerApplicationDownload `json:"tools"`
|
||||
// RepoURL is the URL the github runner agent needs to configure itself.
|
||||
RepoURL string `json:"repo_url"`
|
||||
// CallbackUrl is the URL where the instance can send a post, signaling
|
||||
// progress or status.
|
||||
CallbackURL string `json:"callback-url"`
|
||||
// MetadataURL is the URL where instances can fetch information needed to set themselves up.
|
||||
MetadataURL string `json:"metadata-url"`
|
||||
// InstanceToken is the token that needs to be set by the instance in the headers
|
||||
// in order to send updated back to the garm via CallbackURL.
|
||||
InstanceToken string `json:"instance-token"`
|
||||
// SSHKeys are the ssh public keys we may want to inject inside the runners, if the
|
||||
// provider supports it.
|
||||
SSHKeys []string `json:"ssh-keys"`
|
||||
// ExtraSpecs is an opaque raw json that gets sent to the provider
|
||||
// as part of the bootstrap params for instances. It can contain
|
||||
// any kind of data needed by providers. The contents of this field means
|
||||
// nothing to garm itself. We don't act on the information in this field at
|
||||
// all. We only validate that it's a proper json.
|
||||
ExtraSpecs json.RawMessage `json:"extra_specs,omitempty"`
|
||||
|
||||
// GitHubRunnerGroup is the github runner group in which the newly installed runner
|
||||
// should be added to. The runner group must be created by someone with access to the
|
||||
// enterprise.
|
||||
GitHubRunnerGroup string `json:"github-runner-group"`
|
||||
|
||||
// CACertBundle is a CA certificate bundle which will be sent to instances and which
|
||||
// will tipically be installed as a system wide trusted root CA. by either cloud-init
|
||||
// or whatever mechanism the provider will use to set up the runner.
|
||||
CACertBundle []byte `json:"ca-cert-bundle"`
|
||||
|
||||
// OSArch is the target OS CPU architecture of the runner.
|
||||
OSArch OSArch `json:"arch"`
|
||||
|
||||
// OSType is the target OS platform of the runner (windows, linux).
|
||||
OSType OSType `json:"os_type"`
|
||||
|
||||
// Flavor is the platform specific abstraction that defines what resources will be allocated
|
||||
// to the runner (CPU, RAM, disk space, etc). This field is meaningful to the provider which
|
||||
// handles the actual creation.
|
||||
Flavor string `json:"flavor"`
|
||||
|
||||
// Image is the platform specific identifier of the operating system template that will be used
|
||||
// to spin up a new machine.
|
||||
Image string `json:"image"`
|
||||
|
||||
// Labels are a list of github runner labels that will be added to the runner.
|
||||
Labels []string `json:"labels"`
|
||||
|
||||
// PoolID is the ID of the garm pool to which this runner belongs.
|
||||
PoolID string `json:"pool_id"`
|
||||
|
||||
// UserDataOptions are the options for the user data generation.
|
||||
UserDataOptions UserDataOptions `json:"user_data_options"`
|
||||
}
|
||||
|
||||
type Address struct {
|
||||
Address string `json:"address"`
|
||||
Type AddressType `json:"type"`
|
||||
}
|
||||
|
||||
type ProviderInstance struct {
|
||||
// PeoviderID is the unique ID the provider associated
|
||||
// with the compute instance. We use this to identify the
|
||||
// instance in the provider.
|
||||
ProviderID string `json:"provider_id,omitempty"`
|
||||
|
||||
// Name is the name associated with an instance. Depending on
|
||||
// the provider, this may or may not be useful in the context of
|
||||
// the provider, but we can use it internally to identify the
|
||||
// instance.
|
||||
Name string `json:"name,omitempty"`
|
||||
|
||||
// OSType is the operating system type. For now, only Linux and
|
||||
// Windows are supported.
|
||||
OSType OSType `json:"os_type,omitempty"`
|
||||
|
||||
// OSName is the name of the OS. Eg: ubuntu, centos, etc.
|
||||
OSName string `json:"os_name,omitempty"`
|
||||
|
||||
// OSVersion is the version of the operating system.
|
||||
OSVersion string `json:"os_version,omitempty"`
|
||||
|
||||
// OSArch is the operating system architecture.
|
||||
OSArch OSArch `json:"os_arch,omitempty"`
|
||||
|
||||
// Addresses is a list of IP addresses the provider reports
|
||||
// for this instance.
|
||||
Addresses []Address `json:"addresses,omitempty"`
|
||||
|
||||
// Status is the status of the instance inside the provider (eg: running, stopped, etc)
|
||||
Status InstanceStatus `json:"status,omitempty"`
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue