Merge pull request #82 from gabriel-samfira/add-execution-env

Add aditional external provider enablement
This commit is contained in:
Gabriel 2023-03-28 12:52:37 +03:00 committed by GitHub
commit 569803e5e6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
45 changed files with 1295 additions and 276 deletions

View file

@ -19,7 +19,7 @@ build-static:
@echo Binaries are available in $(PWD)/bin
install:
@$(GO) install ./...
@$(GO) install -tags osusergo,netgo,sqlite_omit_load_extension ./...
@echo Binaries available in ${GOPATH}
test: verify go-test

View file

@ -16,8 +16,10 @@ package cloudconfig
import (
"bytes"
"fmt"
"text/template"
"github.com/cloudbase/garm/params"
"github.com/pkg/errors"
)
@ -61,6 +63,15 @@ function fail() {
sendStatus "downloading tools from {{ .DownloadURL }}"
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 ]
RUNNER_GROUP_OPT="--runnergroup=$GH_RUNNER_GROUP"
fi
if [ ! -z "{{ .TempDownloadToken }}" ]; then
TEMP_TOKEN="Authorization: Bearer {{ .TempDownloadToken }}"
@ -79,11 +90,15 @@ cd /home/{{ .RunnerUsername }}/actions-runner
sudo ./bin/installdependencies.sh || fail "failed to install dependencies"
sendStatus "configuring runner"
sudo -u {{ .RunnerUsername }} -- ./config.sh --unattended --url "{{ .RepoURL }}" --token "$GITHUB_TOKEN" --name "{{ .RunnerName }}" --labels "{{ .RunnerLabels }}" --ephemeral || fail "failed to configure runner"
sudo -u {{ .RunnerUsername }} -- ./config.sh --unattended --url "{{ .RepoURL }}" --token "$GITHUB_TOKEN" $RUNNER_GROUP_OPT --name "{{ .RunnerName }}" --labels "{{ .RunnerLabels }}" --ephemeral || fail "failed to configure runner"
sendStatus "installing runner service"
./svc.sh install {{ .RunnerUsername }} || fail "failed to install service"
if [ -e "/sys/fs/selinux" ];then
sudo chcon -R -t bin_t /home/runner/
fi
sendStatus "starting service"
./svc.sh start || fail "failed to start service"
@ -97,6 +112,233 @@ 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 ($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
@ -109,17 +351,28 @@ type InstallRunnerParams struct {
CallbackURL string
CallbackToken string
TempDownloadToken string
CABundle string
GitHubRunnerGroup 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

@ -45,6 +45,7 @@ var (
poolExtraSpecsFile string
poolExtraSpecs string
poolAll bool
poolGitHubRunnerGroup string
)
// runnerCmd represents the runner command
@ -196,6 +197,7 @@ var poolAddCmd = &cobra.Command{
Tags: tags,
Enabled: poolEnabled,
RunnerBootstrapTimeout: poolRunnerBootstrapTimeout,
GitHubRunnerGroup: poolGitHubRunnerGroup,
}
if cmd.Flags().Changed("extra-specs") {
@ -299,6 +301,10 @@ explicitly remove them using the runner delete command.
}
}
if cmd.Flags().Changed("runner-group") {
poolUpdateParams.GitHubRunnerGroup = &poolGitHubRunnerGroup
}
if cmd.Flags().Changed("enabled") {
poolUpdateParams.Enabled = &poolEnabled
}
@ -348,6 +354,7 @@ func init() {
poolUpdateCmd.Flags().StringVar(&poolRunnerPrefix, "runner-prefix", "", "The name prefix to use for runners in this pool.")
poolUpdateCmd.Flags().UintVar(&poolMaxRunners, "max-runners", 5, "The maximum number of runner this pool will create.")
poolUpdateCmd.Flags().UintVar(&poolMinIdleRunners, "min-idle-runners", 1, "Attempt to maintain a minimum of idle self-hosted runners of this type.")
poolUpdateCmd.Flags().StringVar(&poolGitHubRunnerGroup, "runner-group", "", "The GitHub runner group in which all runners of this pool will be added.")
poolUpdateCmd.Flags().BoolVar(&poolEnabled, "enabled", false, "Enable this pool.")
poolUpdateCmd.Flags().UintVar(&poolRunnerBootstrapTimeout, "runner-bootstrap-timeout", 20, "Duration in minutes after which a runner is considered failed if it does not join Github.")
poolUpdateCmd.Flags().StringVar(&poolExtraSpecsFile, "extra-specs-file", "", "A file containing a valid json which will be passed to the IaaS provider managing the pool.")
@ -363,6 +370,7 @@ func init() {
poolAddCmd.Flags().StringVar(&poolOSArch, "os-arch", "amd64", "Operating system architecture (amd64, arm, etc).")
poolAddCmd.Flags().StringVar(&poolExtraSpecsFile, "extra-specs-file", "", "A file containing a valid json which will be passed to the IaaS provider managing the pool.")
poolAddCmd.Flags().StringVar(&poolExtraSpecs, "extra-specs", "", "A valid json which will be passed to the IaaS provider managing the pool.")
poolAddCmd.Flags().StringVar(&poolGitHubRunnerGroup, "runner-group", "", "The GitHub runner group in which all runners of this pool will be added.")
poolAddCmd.Flags().UintVar(&poolMaxRunners, "max-runners", 5, "The maximum number of runner this pool will create.")
poolAddCmd.Flags().UintVar(&poolRunnerBootstrapTimeout, "runner-bootstrap-timeout", 20, "Duration in minutes after which a runner is considered failed if it does not join Github.")
poolAddCmd.Flags().UintVar(&poolMinIdleRunners, "min-idle-runners", 1, "Attempt to maintain a minimum of idle self-hosted runners of this type.")

View file

@ -33,14 +33,15 @@ func (s *sqlDatabase) CreateInstance(ctx context.Context, poolID string, param p
}
newInstance := Instance{
Pool: pool,
Name: param.Name,
Status: param.Status,
RunnerStatus: param.RunnerStatus,
OSType: param.OSType,
OSArch: param.OSArch,
CallbackURL: param.CallbackURL,
MetadataURL: param.MetadataURL,
Pool: pool,
Name: param.Name,
Status: param.Status,
RunnerStatus: param.RunnerStatus,
OSType: param.OSType,
OSArch: param.OSArch,
CallbackURL: param.CallbackURL,
MetadataURL: param.MetadataURL,
GitHubRunnerGroup: param.GitHubRunnerGroup,
}
q := s.conn.Create(&newInstance)
if q.Error != nil {

View file

@ -70,7 +70,8 @@ type Pool struct {
// ExtraSpecs is an opaque 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.
ExtraSpecs datatypes.JSON
ExtraSpecs datatypes.JSON
GitHubRunnerGroup string
RepoID uuid.UUID `gorm:"index"`
Repository Repository `gorm:"foreignKey:RepoID"`
@ -136,21 +137,22 @@ type InstanceStatusUpdate struct {
type Instance struct {
Base
ProviderID *string `gorm:"uniqueIndex"`
Name string `gorm:"uniqueIndex"`
AgentID int64
OSType params.OSType
OSArch params.OSArch
OSName string
OSVersion string
Addresses []Address `gorm:"foreignKey:InstanceID"`
Status common.InstanceStatus
RunnerStatus common.RunnerStatus
CallbackURL string
MetadataURL string
ProviderFault []byte `gorm:"type:longblob"`
CreateAttempt int
TokenFetched bool
ProviderID *string `gorm:"uniqueIndex"`
Name string `gorm:"uniqueIndex"`
AgentID int64
OSType params.OSType
OSArch params.OSArch
OSName string
OSVersion string
Addresses []Address `gorm:"foreignKey:InstanceID"`
Status common.InstanceStatus
RunnerStatus common.RunnerStatus
CallbackURL string
MetadataURL string
ProviderFault []byte `gorm:"type:longblob"`
CreateAttempt int
TokenFetched bool
GitHubRunnerGroup string
PoolID uuid.UUID
Pool Pool `gorm:"foreignKey:PoolID"`

View file

@ -128,7 +128,7 @@ func (s *PoolsTestSuite) TestListAllPools() {
func (s *PoolsTestSuite) TestListAllPoolsDBFetchErr() {
s.Fixtures.SQLMock.
ExpectQuery(regexp.QuoteMeta("SELECT `pools`.`id`,`pools`.`created_at`,`pools`.`updated_at`,`pools`.`deleted_at`,`pools`.`provider_name`,`pools`.`runner_prefix`,`pools`.`max_runners`,`pools`.`min_idle_runners`,`pools`.`runner_bootstrap_timeout`,`pools`.`image`,`pools`.`flavor`,`pools`.`os_type`,`pools`.`os_arch`,`pools`.`enabled`,`pools`.`repo_id`,`pools`.`org_id`,`pools`.`enterprise_id` FROM `pools` WHERE `pools`.`deleted_at` IS NULL")).
ExpectQuery(regexp.QuoteMeta("SELECT `pools`.`id`,`pools`.`created_at`,`pools`.`updated_at`,`pools`.`deleted_at`,`pools`.`provider_name`,`pools`.`runner_prefix`,`pools`.`max_runners`,`pools`.`min_idle_runners`,`pools`.`runner_bootstrap_timeout`,`pools`.`image`,`pools`.`flavor`,`pools`.`os_type`,`pools`.`os_arch`,`pools`.`enabled`,`pools`.`git_hub_runner_group`,`pools`.`repo_id`,`pools`.`org_id`,`pools`.`enterprise_id` FROM `pools` WHERE `pools`.`deleted_at` IS NULL")).
WillReturnError(fmt.Errorf("mocked fetching all pools error"))
_, err := s.StoreSQLMocked.ListAllPools(context.Background())

View file

@ -33,23 +33,24 @@ func (s *sqlDatabase) sqlToParamsInstance(instance Instance) params.Instance {
id = *instance.ProviderID
}
ret := params.Instance{
ID: instance.ID.String(),
ProviderID: id,
AgentID: instance.AgentID,
Name: instance.Name,
OSType: instance.OSType,
OSName: instance.OSName,
OSVersion: instance.OSVersion,
OSArch: instance.OSArch,
Status: instance.Status,
RunnerStatus: instance.RunnerStatus,
PoolID: instance.PoolID.String(),
CallbackURL: instance.CallbackURL,
MetadataURL: instance.MetadataURL,
StatusMessages: []params.StatusMessage{},
CreateAttempt: instance.CreateAttempt,
UpdatedAt: instance.UpdatedAt,
TokenFetched: instance.TokenFetched,
ID: instance.ID.String(),
ProviderID: id,
AgentID: instance.AgentID,
Name: instance.Name,
OSType: instance.OSType,
OSName: instance.OSName,
OSVersion: instance.OSVersion,
OSArch: instance.OSArch,
Status: instance.Status,
RunnerStatus: instance.RunnerStatus,
PoolID: instance.PoolID.String(),
CallbackURL: instance.CallbackURL,
MetadataURL: instance.MetadataURL,
StatusMessages: []params.StatusMessage{},
CreateAttempt: instance.CreateAttempt,
UpdatedAt: instance.UpdatedAt,
TokenFetched: instance.TokenFetched,
GitHubRunnerGroup: instance.GitHubRunnerGroup,
}
if len(instance.ProviderFault) > 0 {
@ -144,6 +145,7 @@ func (s *sqlDatabase) sqlToCommonPool(pool Pool) params.Pool {
Instances: make([]params.Instance, len(pool.Instances)),
RunnerBootstrapTimeout: pool.RunnerBootstrapTimeout,
ExtraSpecs: json.RawMessage(pool.ExtraSpecs),
GitHubRunnerGroup: pool.GitHubRunnerGroup,
}
if pool.RepoID != uuid.Nil {
@ -281,6 +283,10 @@ func (s *sqlDatabase) updatePool(pool Pool, param params.UpdatePoolParams) (para
pool.RunnerBootstrapTimeout = *param.RunnerBootstrapTimeout
}
if param.GitHubRunnerGroup != nil {
pool.GitHubRunnerGroup = *param.GitHubRunnerGroup
}
if q := s.conn.Save(&pool); q.Error != nil {
return params.Pool{}, errors.Wrap(q.Error, "saving database entry")
}

12
go.mod
View file

@ -1,6 +1,6 @@
module github.com/cloudbase/garm
go 1.18
go 1.20
require (
github.com/BurntSushi/toml v1.2.1
@ -14,8 +14,9 @@ require (
github.com/jedib0t/go-pretty/v6 v6.4.6
github.com/juju/clock v1.0.3
github.com/juju/retry v1.0.0
github.com/lxc/lxd v0.0.0-20230310224854-36b345fbd578
github.com/lxc/lxd v0.0.0-20230325180147-8d608287b0ce
github.com/manifoldco/promptui v0.9.0
github.com/mattn/go-isatty v0.0.18
github.com/nbutton23/zxcvbn-go v0.0.0-20210217022336-fa2cb2858354
github.com/pkg/errors v0.9.1
github.com/prometheus/client_golang v1.14.0
@ -42,6 +43,7 @@ require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/felixge/httpsnoop v1.0.3 // indirect
github.com/flosch/pongo2 v0.0.0-20200913210552-0d938eb266f3 // indirect
github.com/frankban/quicktest v1.14.3 // indirect
github.com/go-macaroon-bakery/macaroon-bakery/v3 v3.0.1 // indirect
github.com/go-macaroon-bakery/macaroonpb v1.0.0 // indirect
github.com/go-sql-driver/mysql v1.7.0 // indirect
@ -51,11 +53,12 @@ require (
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
github.com/juju/errors v1.0.0 // indirect
github.com/juju/testing v1.0.2 // indirect
github.com/juju/webbrowser v1.0.0 // indirect
github.com/julienschmidt/httprouter v1.3.0 // indirect
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
github.com/kr/fs v0.1.0 // indirect
github.com/kr/pretty v0.3.0 // indirect
github.com/kr/pretty v0.3.1 // indirect
github.com/mattn/go-runewidth v0.0.14 // indirect
github.com/mattn/go-sqlite3 v1.14.16 // indirect
github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect
@ -72,11 +75,10 @@ require (
github.com/sirupsen/logrus v1.9.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/stretchr/objx v0.5.0 // indirect
github.com/xdg-go/stringprep v1.0.3 // indirect
golang.org/x/net v0.8.0 // indirect
golang.org/x/term v0.6.0 // indirect
google.golang.org/appengine v1.6.7 // indirect
google.golang.org/protobuf v1.29.0 // indirect
google.golang.org/protobuf v1.30.0 // indirect
gopkg.in/errgo.v1 v1.0.1 // indirect
gopkg.in/httprequest.v1 v1.2.1 // indirect
gopkg.in/macaroon.v2 v2.1.0 // indirect

33
go.sum
View file

@ -33,7 +33,8 @@ github.com/frankban/quicktest v1.0.0/go.mod h1:R98jIehRai+d1/3Hv2//jOVCTJhW1VBav
github.com/frankban/quicktest v1.2.2/go.mod h1:Qh/WofXFeiAFII1aEBu529AtJo6Zg2VHscnEsbBnJ20=
github.com/frankban/quicktest v1.7.2/go.mod h1:jaStnuzAqU1AJdCO0l53JDCJrVDKcS03DbaAcR7Ks/o=
github.com/frankban/quicktest v1.10.0/go.mod h1:ui7WezCLWMWxVWr1GETZY3smRy0G4KWq9vcPtJmFl7Y=
github.com/frankban/quicktest v1.11.3 h1:8sXhOn0uLys67V8EsXLc6eszDs8VXWxL3iRvebPhedY=
github.com/frankban/quicktest v1.14.3 h1:FJKSZTDHjyhriyC81FLQ0LY93eSai0ZyR/ZIkd3ZUKE=
github.com/frankban/quicktest v1.14.3/go.mod h1:mgiwOwqx65TmIk1wJ6Q7wvnVMocbUorkibMOrVTHZps=
github.com/go-macaroon-bakery/macaroon-bakery/v3 v3.0.1 h1:uvQJoKTHrFFu8zxoaopNKedRzwdy3+8H72we4T/5cGs=
github.com/go-macaroon-bakery/macaroon-bakery/v3 v3.0.1/go.mod h1:H59IYeChwvD1po3dhGUPvq5na+4NVD7SJlbhGKvslr0=
github.com/go-macaroon-bakery/macaroonpb v1.0.0 h1:It9exBaRMZ9iix1iJ6gwzfwsDE6ExNuwtAJ9e09v6XE=
@ -70,6 +71,7 @@ github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/go-github/v48 v48.2.0 h1:68puzySE6WqUY9KWmpOsDEQfDZsso98rT6pZcz9HqcE=
github.com/google/go-github/v48 v48.2.0/go.mod h1:dDlehKBDo850ZPvCTK0sEqTCVWcrGl2LcDiajkYi89Y=
@ -104,19 +106,16 @@ github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/juju/clock v1.0.3 h1:yJHIsWXeU8j3QcBdiess09SzfiXRRrsjKPn2whnMeds=
github.com/juju/clock v1.0.3/go.mod h1:HIBvJ8kiV/n7UHwKuCkdYL4l/MDECztHR2sAvWDxxf0=
github.com/juju/collections v1.0.2 h1:y9t99Nq/uUZksJgWehiWxIr2vB1UG3hUT7LBNy1xiH8=
github.com/juju/errors v1.0.0 h1:yiq7kjCLll1BiaRuNY53MGI0+EQ3rF6GB+wvboZDefM=
github.com/juju/errors v1.0.0/go.mod h1:B5x9thDqx0wIMH3+aLIMP9HjItInYWObRovoCFM5Qe8=
github.com/juju/loggo v1.0.0 h1:Y6ZMQOGR9Aj3BGkiWx7HBbIx6zNwNkxhVNOHU2i1bl0=
github.com/juju/mgo/v2 v2.0.2 h1:ufYtW2OFNjniTuxOngecP3Mk5sSclo8Zl1mnmyGWUWA=
github.com/juju/qthttptest v0.1.1/go.mod h1:aTlAv8TYaflIiTDIQYzxnl1QdPjAg8Q8qJMErpKy6A4=
github.com/juju/qthttptest v0.1.3 h1:M0HdpwsK/UTHRGRcIw5zvh5z+QOgdqyK+ecDMN+swwM=
github.com/juju/retry v1.0.0 h1:Tb1hFdDSPGLH/BGdYQOF7utQ9lA0ouVJX2imqgJK6tk=
github.com/juju/retry v1.0.0/go.mod h1:SssN1eYeK3A2qjnFGTiVMbdzGJ2BfluaJblJXvuvgqA=
github.com/juju/testing v0.0.0-20220203020004-a0ff61f03494 h1:XEDzpuZb8Ma7vLja3+5hzUqVTvAqm5Y+ygvnDs5iTMM=
github.com/juju/utils/v3 v3.0.0-20220203023959-c3fbc78a33b0 h1:bn+2Adl1yWqYjm3KSFlFqsvfLg2eq+XNL7GGMYApdVw=
github.com/juju/version v0.0.0-20210303051006-2015802527a8 h1:BTo6HzRR0zPBcXbs1Sy08aQNfvdm3ey8O+mBTiO3g00=
github.com/juju/version/v2 v2.0.0-20211007103408-2e8da085dc23 h1:wtEPbidt1VyHlb8RSztU6ySQj29FLsOQiI9XiJhXDM4=
github.com/juju/testing v1.0.2 h1:OR90RqCd9CJONxXamZAjLknpZdtqDyxqW8IwCbgw3i4=
github.com/juju/testing v1.0.2/go.mod h1:h3Vd2rzB57KrdsBEy6R7bmSKPzP76BnNavt7i8PerwQ=
github.com/juju/utils/v3 v3.0.0 h1:Gg3n63mGPbBuoXCo+EPJuMi44hGZfloI8nlCIebHu2Q=
github.com/juju/webbrowser v1.0.0 h1:JLdmbFtCGY6Qf2jmS6bVaenJFGIFkdF1/BjUm76af78=
github.com/juju/webbrowser v1.0.0/go.mod h1:RwVlbBcF91Q4vS+iwlkJ6bZTE3EwlrjbYlM3WMVD6Bc=
github.com/julienschmidt/httprouter v1.3.0 h1:U0609e9tgbseu3rBINet9P48AI/D3oJs4dN7jwJOQ1U=
@ -127,16 +126,19 @@ github.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8=
github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/lxc/lxd v0.0.0-20230310224854-36b345fbd578 h1:Rqzj0l43LLcTciHKIwKpTesAv9rSeC4LbUc+BsiIY6Q=
github.com/lxc/lxd v0.0.0-20230310224854-36b345fbd578/go.mod h1:6Z1AwZwLm5Y+tzoW5CdKOo51fyDiCKJz3QgAXJ9/IYI=
github.com/lxc/lxd v0.0.0-20230325180147-8d608287b0ce h1:3zb1HRvOAHOMZ8VGTDEBkKpCUVlF28zalZcb7RFjMnE=
github.com/lxc/lxd v0.0.0-20230325180147-8d608287b0ce/go.mod h1:JJ1ShHzaOzMzU0B5TNcdI9+vq8Y45ijVeNYxE1wJ8zM=
github.com/manifoldco/promptui v0.9.0 h1:3V4HzJk1TtXW1MTZMP7mdlwbBpIinw3HztaIlYthEiA=
github.com/manifoldco/promptui v0.9.0/go.mod h1:ka04sppxSGFAtxX0qhlYQjISsg9mR4GWtQEhdbn6Pgg=
github.com/mattn/go-isatty v0.0.18 h1:DOKFKCQ7FNG2L1rbrmstDN4QVRdS89Nkh85u68Uwp98=
github.com/mattn/go-isatty v0.0.18/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU=
github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
@ -151,6 +153,7 @@ github.com/nbutton23/zxcvbn-go v0.0.0-20210217022336-fa2cb2858354/go.mod h1:KSVJ
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/pborman/uuid v1.2.1 h1:+ZZIw58t/ozdjRaXh/3awHfmWRbzYxJoAdNJxe/3pvw=
github.com/pborman/uuid v1.2.1/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/profile v1.6.0/go.mod h1:qBsxPvzyUincmltOk6iyRVxHYg4adc0OFOv72ZdLa18=
@ -177,8 +180,9 @@ github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
github.com/rogpeppe/fastuuid v1.2.0 h1:Ppwyp6VYCF1nvBTXL3trRso7mXMlRrw9ooo375wvi2s=
github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
github.com/rogpeppe/go-internal v1.6.1 h1:/FiVV8dS/e+YqF2JvO3yXRFbBLTIuSDkuC7aBOAvL+k=
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/satori/go.uuid v1.2.1-0.20181028125025-b2ce2384e17b h1:gQZ0qzfKHQIybLANtM3mBXNUtOfsCFXeTsnBqCsx1KM=
github.com/satori/go.uuid v1.2.1-0.20181028125025-b2ce2384e17b/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
@ -201,8 +205,6 @@ github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/teris-io/shortid v0.0.0-20220617161101-71ec9f2aa569 h1:xzABM9let0HLLqFypcxvLmlvEciCHL7+Lv+4vwZqecI=
github.com/teris-io/shortid v0.0.0-20220617161101-71ec9f2aa569/go.mod h1:2Ly+NIftZN4de9zRmENdYbvPQeaVIYKWpLFStLFEBgI=
github.com/xdg-go/stringprep v1.0.3 h1:kdwGpVNwPFtjs98xCGkHjQtGKh86rDcRZN17QEMCOIs=
github.com/xdg-go/stringprep v1.0.3/go.mod h1:W3f5j4i+9rC0kuIEJL0ky1VpHXQU3ocBgklLGvcBnW8=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
golang.org/x/crypto v0.0.0-20180723164146-c126467f60eb/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
@ -257,7 +259,6 @@ golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.8.0 h1:57P1ETyNKtuIjB4SRd15iJxuhj8Gc416Y78H3qgMh68=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
@ -290,8 +291,8 @@ google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpAD
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.29.0 h1:44S3JjaKmLEE4YIkjzexaP+NzZsudE3Zin5Njn/pYX0=
google.golang.org/protobuf v1.29.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng=
google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
gopkg.in/DATA-DOG/go-sqlmock.v1 v1.3.0 h1:FVCohIoYO7IJoDDVpV2pdq7SgrMH6wHnuTyrdrxJNoY=
gopkg.in/DATA-DOG/go-sqlmock.v1 v1.3.0/go.mod h1:OdE7CF6DbADk7lN8LIKRzRJTTZXIjtWgA5THM5lhBAw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=

View file

@ -90,37 +90,61 @@ type StatusMessage struct {
type Instance struct {
// ID is the database ID of this instance.
ID string `json:"id,omitempty"`
// 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"`
// AgentID is the github runner agent ID.
AgentID int64 `json:"agent_id"`
// 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 common.InstanceStatus `json:"status,omitempty"`
RunnerStatus common.RunnerStatus `json:"runner_status,omitempty"`
PoolID string `json:"pool_id,omitempty"`
ProviderFault []byte `json:"provider_fault,omitempty"`
// Status is the status of the instance inside the provider (eg: running, stopped, etc)
Status common.InstanceStatus `json:"status,omitempty"`
// RunnerStatus is the github runner status as it appears on GitHub.
RunnerStatus common.RunnerStatus `json:"runner_status,omitempty"`
// PoolID is the ID of the garm pool to which a runner belongs.
PoolID string `json:"pool_id,omitempty"`
// ProviderFault holds any error messages captured from the IaaS provider that is
// responsible for managing the lifecycle of the runner.
ProviderFault []byte `json:"provider_fault,omitempty"`
// StatusMessages is a list of status messages sent back by the runner as it sets itself
// up.
StatusMessages []StatusMessage `json:"status_messages,omitempty"`
UpdatedAt time.Time `json:"updated_at"`
// UpdatedAt is the timestamp of the last update to this runner.
UpdatedAt time.Time `json:"updated_at"`
// GithubRunnerGroup is the github runner group to which the runner belongs.
// The runner group must be created by someone with access to the enterprise.
GitHubRunnerGroup string `json:"github-runner-group"`
// Do not serialize sensitive info.
CallbackURL string `json:"-"`
@ -160,13 +184,36 @@ type BootstrapInstance struct {
// 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 OSArch `json:"arch"`
Flavor string `json:"flavor"`
Image string `json:"image"`
// 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 string `json:"pool_id"`
// PoolID is the ID of the garm pool to which this runner belongs.
PoolID string `json:"pool_id"`
}
type Tag struct {
@ -201,6 +248,9 @@ type Pool struct {
// 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 runners will be added.
// The runner group must be created by someone with access to the enterprise.
GitHubRunnerGroup string `json:"github-runner-group"`
}
func (p Pool) GetID() string {

View file

@ -118,17 +118,24 @@ type UpdatePoolParams struct {
OSType OSType `json:"os_type"`
OSArch OSArch `json:"os_arch"`
ExtraSpecs json.RawMessage `json:"extra_specs,omitempty"`
// GithubRunnerGroup is the github runner group in which the runners of this
// pool will be added to.
// The runner group must be created by someone with access to the enterprise.
GitHubRunnerGroup *string `json:"github-runner-group,omitempty"`
}
type CreateInstanceParams struct {
Name string
OSType OSType
OSArch OSArch
Status common.InstanceStatus
RunnerStatus common.RunnerStatus
CallbackURL string
MetadataURL string
CreateAttempt int `json:"-"`
Name string
OSType OSType
OSArch OSArch
Status common.InstanceStatus
RunnerStatus common.RunnerStatus
CallbackURL string
MetadataURL string
// GithubRunnerGroup is the github runner group to which the runner belongs.
// The runner group must be created by someone with access to the enterprise.
GitHubRunnerGroup string
CreateAttempt int `json:"-"`
}
type CreatePoolParams struct {
@ -145,6 +152,10 @@ type CreatePoolParams struct {
Enabled bool `json:"enabled"`
RunnerBootstrapTimeout uint `json:"runner_bootstrap_timeout"`
ExtraSpecs json.RawMessage `json:"extra_specs,omitempty"`
// GithubRunnerGroup is the github runner group in which the runners of this
// pool will be added to.
// The runner group must be created by someone with access to the enterprise.
GitHubRunnerGroup string `json:"github-runner-group"`
}
func (p *CreatePoolParams) Validate() error {

View file

@ -179,13 +179,13 @@ func (r *basePoolManager) reapTimedOutRunners(runners []*github.Runner) error {
return nil
}
func instanceInList(instanceName string, instances []params.Instance) bool {
func instanceInList(instanceName string, instances []params.Instance) (params.Instance, bool) {
for _, val := range instances {
if val.Name == instanceName {
return true
return val, true
}
}
return false
return params.Instance{}, false
}
// cleanupOrphanedGithubRunners will forcefully remove any github runners that appear
@ -252,6 +252,7 @@ func (r *basePoolManager) cleanupOrphanedGithubRunners(runners []*github.Runner)
var poolInstances []params.Instance
poolInstances, ok = poolInstanceCache[pool.ID]
if !ok {
log.Printf("updating instances cache for pool %s", pool.ID)
poolInstances, err = provider.ListInstances(r.ctx, pool.ID)
if err != nil {
return errors.Wrapf(err, "fetching instances for pool %s", pool.ID)
@ -259,7 +260,8 @@ func (r *basePoolManager) cleanupOrphanedGithubRunners(runners []*github.Runner)
poolInstanceCache[pool.ID] = poolInstances
}
if !instanceInList(dbInstance.Name, poolInstances) {
providerInstance, ok := instanceInList(dbInstance.Name, poolInstances)
if !ok {
// The runner instance is no longer on the provider, and it appears offline in github.
// It should be safe to force remove it.
log.Printf("Runner instance for %s is no longer on the provider, removing from github", dbInstance.Name)
@ -286,16 +288,18 @@ func (r *basePoolManager) cleanupOrphanedGithubRunners(runners []*github.Runner)
continue
}
if providerCommon.InstanceStatus(dbInstance.Status) == providerCommon.InstanceRunning {
if providerInstance.Status == providerCommon.InstanceRunning {
// instance is running, but github reports runner as offline. Log the event.
// This scenario requires manual intervention.
// Perhaps it just came online and github did not yet change it's status?
log.Printf("instance %s is online but github reports runner as offline", dbInstance.Name)
continue
}
//start the instance
if err := provider.Start(r.ctx, dbInstance.ProviderID); err != nil {
return errors.Wrapf(err, "starting instance %s", dbInstance.ProviderID)
} else {
log.Printf("instance %s was found in stopped state; starting", dbInstance.Name)
//start the instance
if err := provider.Start(r.ctx, dbInstance.ProviderID); err != nil {
return errors.Wrapf(err, "starting instance %s", dbInstance.ProviderID)
}
}
}
return nil
@ -417,14 +421,15 @@ func (r *basePoolManager) AddRunner(ctx context.Context, poolID string) error {
name := fmt.Sprintf("%s-%s", pool.GetRunnerPrefix(), util.NewID())
createParams := params.CreateInstanceParams{
Name: name,
Status: providerCommon.InstancePendingCreate,
RunnerStatus: providerCommon.RunnerPending,
OSArch: pool.OSArch,
OSType: pool.OSType,
CallbackURL: r.helper.GetCallbackURL(),
MetadataURL: r.helper.GetMetadataURL(),
CreateAttempt: 1,
Name: name,
Status: providerCommon.InstancePendingCreate,
RunnerStatus: providerCommon.RunnerPending,
OSArch: pool.OSArch,
OSType: pool.OSType,
CallbackURL: r.helper.GetCallbackURL(),
MetadataURL: r.helper.GetMetadataURL(),
CreateAttempt: 1,
GitHubRunnerGroup: pool.GitHubRunnerGroup,
}
_, err = r.store.CreateInstance(r.ctx, poolID, createParams)
@ -603,19 +608,21 @@ func (r *basePoolManager) addInstanceToProvider(instance params.Instance) error
}
bootstrapArgs := params.BootstrapInstance{
Name: instance.Name,
Tools: r.tools,
RepoURL: r.helper.GithubURL(),
MetadataURL: instance.MetadataURL,
CallbackURL: instance.CallbackURL,
InstanceToken: jwtToken,
OSArch: pool.OSArch,
Flavor: pool.Flavor,
Image: pool.Image,
ExtraSpecs: pool.ExtraSpecs,
Labels: labels,
PoolID: instance.PoolID,
CACertBundle: r.credsDetails.CABundle,
Name: instance.Name,
Tools: r.tools,
RepoURL: r.helper.GithubURL(),
MetadataURL: instance.MetadataURL,
CallbackURL: instance.CallbackURL,
InstanceToken: jwtToken,
OSArch: pool.OSArch,
OSType: pool.OSType,
Flavor: pool.Flavor,
Image: pool.Image,
ExtraSpecs: pool.ExtraSpecs,
Labels: labels,
PoolID: instance.PoolID,
CACertBundle: r.credsDetails.CABundle,
GitHubRunnerGroup: instance.GitHubRunnerGroup,
}
var instanceIDToDelete string
@ -1031,11 +1038,11 @@ func (r *basePoolManager) deletePendingInstances() {
err = r.deleteInstanceFromProvider(instance)
if err != nil {
log.Printf("failed to delete instance from provider: %+v", err)
return errors.Wrap(err, "removing instance from provider")
}
if err := r.store.DeleteInstance(r.ctx, instance.PoolID, instance.Name); err != nil {
return errors.Wrap(err, "deleting instance from database")
if deleteErr := r.store.DeleteInstance(r.ctx, instance.PoolID, instance.Name); deleteErr != nil {
return errors.Wrap(deleteErr, "deleting instance from database")
}
return
}(instance) //nolint

View file

@ -101,7 +101,7 @@ func (r *Runner) UpdatePoolByID(ctx context.Context, poolID string, param params
}
if param.Tags != nil && len(param.Tags) > 0 {
newTags, err := r.processTags(string(pool.OSArch), string(pool.OSType), param.Tags)
newTags, err := r.processTags(string(pool.OSArch), pool.OSType, param.Tags)
if err != nil {
return params.Pool{}, errors.Wrap(err, "processing tags")
}

View 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"
)

View file

@ -0,0 +1,165 @@
package execution
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"os"
"github.com/cloudbase/garm/params"
"github.com/mattn/go-isatty"
)
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
}

View file

@ -0,0 +1,26 @@
package execution
import (
"errors"
gErrors "github.com/cloudbase/garm/errors"
)
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 ResolveErrorToExitCode(err error) int {
if err != nil {
if errors.Is(err, gErrors.ErrNotFound) {
return ExitCodeNotFound
} else if errors.Is(err, gErrors.ErrDuplicateEntity) {
return ExitCodeDuplicate
}
return 1
}
return 0
}

View file

@ -0,0 +1,27 @@
package execution
import (
"context"
"github.com/cloudbase/garm/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.Instance, 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.Instance, error)
// ListInstances will list all instances for a provider.
ListInstances(ctx context.Context, poolID string) ([]params.Instance, 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
}

View file

@ -5,13 +5,15 @@ import (
"encoding/json"
"fmt"
"log"
"os/exec"
"github.com/cloudbase/garm/config"
garmErrors "github.com/cloudbase/garm/errors"
"github.com/cloudbase/garm/params"
"github.com/cloudbase/garm/runner/common"
providerCommon "github.com/cloudbase/garm/runner/providers/common"
"github.com/cloudbase/garm/util/exec"
"github.com/cloudbase/garm/runner/providers/external/execution"
garmExec "github.com/cloudbase/garm/util/exec"
"github.com/pkg/errors"
)
@ -42,11 +44,7 @@ type external struct {
execPath string
}
func (e *external) configEnvVar() string {
return fmt.Sprintf("GARM_PROVIDER_CONFIG_FILE=%s", e.cfg.External.ConfigFile)
}
func (e *external) validateCreateResult(inst params.Instance, bootstrapParams params.BootstrapInstance) error {
func (e *external) validateCreateResult(inst params.Instance) error {
if inst.ProviderID == "" {
return garmErrors.NewProviderError("missing provider ID after create call")
}
@ -68,18 +66,19 @@ func (e *external) validateCreateResult(inst params.Instance, bootstrapParams pa
// CreateInstance creates a new compute instance in the provider.
func (e *external) CreateInstance(ctx context.Context, bootstrapParams params.BootstrapInstance) (params.Instance, error) {
asEnv := bootstrapParamsToEnv(bootstrapParams)
asEnv = append(asEnv, createInstanceCommand)
asEnv = append(asEnv, fmt.Sprintf("GARM_CONTROLLER_ID=%s", e.controllerID))
asEnv = append(asEnv, fmt.Sprintf("GARM_POOL_ID=%s", bootstrapParams.PoolID))
asEnv = append(asEnv, e.configEnvVar())
asEnv := []string{
fmt.Sprintf("GARM_COMMAND=%s", execution.CreateInstanceCommand),
fmt.Sprintf("GARM_CONTROLLER_ID=%s", e.controllerID),
fmt.Sprintf("GARM_POOL_ID=%s", bootstrapParams.PoolID),
fmt.Sprintf("GARM_PROVIDER_CONFIG_FILE=%s", e.cfg.External.ConfigFile),
}
asJs, err := json.Marshal(bootstrapParams)
if err != nil {
return params.Instance{}, errors.Wrap(err, "serializing bootstrap params")
}
out, err := exec.Exec(ctx, e.execPath, asJs, asEnv)
out, err := garmExec.Exec(ctx, e.execPath, asJs, asEnv)
if err != nil {
return params.Instance{}, garmErrors.NewProviderError("provider binary %s returned error: %s", e.execPath, err)
}
@ -89,7 +88,7 @@ func (e *external) CreateInstance(ctx context.Context, bootstrapParams params.Bo
return params.Instance{}, garmErrors.NewProviderError("failed to decode response from binary: %s", err)
}
if err := e.validateCreateResult(param, bootstrapParams); err != nil {
if err := e.validateCreateResult(param); err != nil {
return params.Instance{}, garmErrors.NewProviderError("failed to validate result: %s", err)
}
@ -101,14 +100,19 @@ func (e *external) CreateInstance(ctx context.Context, bootstrapParams params.Bo
// Delete instance will delete the instance in a provider.
func (e *external) DeleteInstance(ctx context.Context, instance string) error {
asEnv := []string{
deleteInstanceCommand,
e.configEnvVar(),
fmt.Sprintf("GARM_COMMAND=%s", execution.DeleteInstanceCommand),
fmt.Sprintf("GARM_CONTROLLER_ID=%s", e.controllerID),
fmt.Sprintf("GARM_INSTANCE_ID=%s", instance),
fmt.Sprintf("GARM_PROVIDER_CONFIG_FILE=%s", e.cfg.External.ConfigFile),
}
_, err := exec.Exec(ctx, e.execPath, nil, asEnv)
_, err := garmExec.Exec(ctx, e.execPath, nil, asEnv)
if err != nil {
return garmErrors.NewProviderError("provider binary %s returned error: %s", e.execPath, err)
var exitErr exec.ExitError
if !errors.As(err, &exitErr) || exitErr.ExitCode() != execution.ExitCodeNotFound {
return garmErrors.NewProviderError("provider binary %s returned error: %s", e.execPath, err)
}
}
return nil
}
@ -116,14 +120,15 @@ func (e *external) DeleteInstance(ctx context.Context, instance string) error {
// GetInstance will return details about one instance.
func (e *external) GetInstance(ctx context.Context, instance string) (params.Instance, error) {
asEnv := []string{
getInstanceCommand,
e.configEnvVar(),
fmt.Sprintf("GARM_COMMAND=%s", execution.GetInstanceCommand),
fmt.Sprintf("GARM_CONTROLLER_ID=%s", e.controllerID),
fmt.Sprintf("GARM_INSTANCE_ID=%s", instance),
fmt.Sprintf("GARM_PROVIDER_CONFIG_FILE=%s", e.cfg.External.ConfigFile),
}
// TODO(gabriel-samfira): handle error types. Of particular insterest is to
// know when the error is ErrNotFound.
out, err := exec.Exec(ctx, e.execPath, nil, asEnv)
out, err := garmExec.Exec(ctx, e.execPath, nil, asEnv)
if err != nil {
return params.Instance{}, garmErrors.NewProviderError("provider binary %s returned error: %s", e.execPath, err)
}
@ -138,12 +143,13 @@ func (e *external) GetInstance(ctx context.Context, instance string) (params.Ins
// ListInstances will list all instances for a provider.
func (e *external) ListInstances(ctx context.Context, poolID string) ([]params.Instance, error) {
asEnv := []string{
listInstancesCommand,
e.configEnvVar(),
fmt.Sprintf("GARM_COMMAND=%s", execution.ListInstancesCommand),
fmt.Sprintf("GARM_CONTROLLER_ID=%s", e.controllerID),
fmt.Sprintf("GARM_POOL_ID=%s", poolID),
fmt.Sprintf("GARM_PROVIDER_CONFIG_FILE=%s", e.cfg.External.ConfigFile),
}
out, err := exec.Exec(ctx, e.execPath, nil, asEnv)
out, err := garmExec.Exec(ctx, e.execPath, nil, asEnv)
if err != nil {
return []params.Instance{}, garmErrors.NewProviderError("provider binary %s returned error: %s", e.execPath, err)
}
@ -158,11 +164,11 @@ func (e *external) ListInstances(ctx context.Context, poolID string) ([]params.I
// RemoveAllInstances will remove all instances created by this provider.
func (e *external) RemoveAllInstances(ctx context.Context) error {
asEnv := []string{
removeAllInstancesCommand,
e.configEnvVar(),
fmt.Sprintf("GARM_COMMAND=%s", execution.RemoveAllInstancesCommand),
fmt.Sprintf("GARM_CONTROLLER_ID=%s", e.controllerID),
fmt.Sprintf("GARM_PROVIDER_CONFIG_FILE=%s", e.cfg.External.ConfigFile),
}
_, err := exec.Exec(ctx, e.execPath, nil, asEnv)
_, err := garmExec.Exec(ctx, e.execPath, nil, asEnv)
if err != nil {
return garmErrors.NewProviderError("provider binary %s returned error: %s", e.execPath, err)
}
@ -172,11 +178,12 @@ func (e *external) RemoveAllInstances(ctx context.Context) error {
// Stop shuts down the instance.
func (e *external) Stop(ctx context.Context, instance string, force bool) error {
asEnv := []string{
stopInstanceCommand,
e.configEnvVar(),
fmt.Sprintf("GARM_COMMAND=%s", execution.StopInstanceCommand),
fmt.Sprintf("GARM_CONTROLLER_ID=%s", e.controllerID),
fmt.Sprintf("GARM_INSTANCE_ID=%s", instance),
fmt.Sprintf("GARM_PROVIDER_CONFIG_FILE=%s", e.cfg.External.ConfigFile),
}
_, err := exec.Exec(ctx, e.execPath, nil, asEnv)
_, err := garmExec.Exec(ctx, e.execPath, nil, asEnv)
if err != nil {
return garmErrors.NewProviderError("provider binary %s returned error: %s", e.execPath, err)
}
@ -186,11 +193,12 @@ func (e *external) Stop(ctx context.Context, instance string, force bool) error
// Start boots up an instance.
func (e *external) Start(ctx context.Context, instance string) error {
asEnv := []string{
startInstanceCommand,
e.configEnvVar(),
fmt.Sprintf("GARM_COMMAND=%s", execution.StartInstanceCommand),
fmt.Sprintf("GARM_CONTROLLER_ID=%s", e.controllerID),
fmt.Sprintf("GARM_INSTANCE_ID=%s", instance),
fmt.Sprintf("GARM_PROVIDER_CONFIG_FILE=%s", e.cfg.External.ConfigFile),
}
_, err := exec.Exec(ctx, e.execPath, nil, asEnv)
_, err := garmExec.Exec(ctx, e.execPath, nil, asEnv)
if err != nil {
return garmErrors.NewProviderError("provider binary %s returned error: %s", e.execPath, err)
}

View file

@ -1,49 +0,0 @@
package external
import (
"fmt"
"strings"
"github.com/cloudbase/garm/params"
)
const (
envPrefix = "GARM"
createInstanceCommand = "GARM_COMMAND=CreateInstance"
deleteInstanceCommand = "GARM_COMMAND=DeleteInstance"
getInstanceCommand = "GARM_COMMAND=GetInstance"
listInstancesCommand = "GARM_COMMAND=ListInstances"
startInstanceCommand = "GARM_COMMAND=StartInstance"
stopInstanceCommand = "GARM_COMMAND=StopInstance"
removeAllInstancesCommand = "GARM_COMMAND=RemoveAllInstances"
)
func bootstrapParamsToEnv(param params.BootstrapInstance) []string {
ret := []string{
fmt.Sprintf("%s_BOOTSTRAP_NAME='%s'", envPrefix, param.Name),
fmt.Sprintf("%s_BOOTSTRAP_OS_ARCH='%s'", envPrefix, param.OSArch),
fmt.Sprintf("%s_BOOTSTRAP_FLAVOR='%s'", envPrefix, param.Flavor),
fmt.Sprintf("%s_BOOTSTRAP_IMAGE='%s'", envPrefix, param.Image),
fmt.Sprintf("%s_BOOTSTRAP_POOL_ID='%s'", envPrefix, param.PoolID),
fmt.Sprintf("%s_BOOTSTRAP_INSTANCE_TOKEN='%s'", envPrefix, param.InstanceToken),
fmt.Sprintf("%s_BOOTSTRAP_CALLBACK_URL='%s'", envPrefix, param.CallbackURL),
fmt.Sprintf("%s_BOOTSTRAP_REPO_URL='%s'", envPrefix, param.RepoURL),
fmt.Sprintf("%s_BOOTSTRAP_LABELS='%s'", envPrefix, strings.Join(param.Labels, ",")),
}
for idx, tool := range param.Tools {
ret = append(ret, fmt.Sprintf("%s_BOOTSTRAP_TOOLS_DOWNLOAD_URL_%d='%s'", envPrefix, idx, *tool.DownloadURL))
ret = append(ret, fmt.Sprintf("%s_BOOTSTRAP_TOOLS_ARCH_%d='%s'", envPrefix, idx, *tool.Architecture))
ret = append(ret, fmt.Sprintf("%s_BOOTSTRAP_TOOLS_OS_%d='%s'", envPrefix, idx, *tool.OS))
ret = append(ret, fmt.Sprintf("%s_BOOTSTRAP_TOOLS_FILENAME_%d='%s'", envPrefix, idx, *tool.Filename))
ret = append(ret, fmt.Sprintf("%s_BOOTSTRAP_TOOLS_SHA256_%d='%s'", envPrefix, idx, *tool.SHA256Checksum))
}
for idx, sshKey := range param.SSHKeys {
ret = append(ret, fmt.Sprintf("%s_BOOTSTRAP_SSH_KEY_%d='%s'", envPrefix, idx, sshKey))
}
return ret
}

View file

@ -17,6 +17,7 @@ package lxd
import (
"context"
"fmt"
"log"
"sync"
"github.com/cloudbase/garm/config"
@ -38,6 +39,16 @@ const (
// created by us or not.
controllerIDKeyName = "user.runner-controller-id"
poolIDKey = "user.runner-pool-id"
// osTypeKeyName is the key we use in the instance config to indicate the OS
// platform a runner is supposed to have. This value is defined in the pool and
// passed into the provider as bootstrap params.
osTypeKeyName = "user.os-type"
// osArchKeyNAme is the key we use in the instance config to indicate the OS
// architecture a runner is supposed to have. This value is defined in the pool and
// passed into the provider as bootstrap params.
osArchKeyNAme = "user.os-arch"
)
var (
@ -159,7 +170,7 @@ func (l *LXD) getProfiles(flavor string) ([]string, error) {
return ret, nil
}
func (l *LXD) getTools(image *api.Image, tools []*github.RunnerApplicationDownload) (github.RunnerApplicationDownload, error) {
func (l *LXD) getTools(image *api.Image, tools []*github.RunnerApplicationDownload, assumedOSType params.OSType) (github.RunnerApplicationDownload, error) {
if image == nil {
return github.RunnerApplicationDownload{}, fmt.Errorf("nil image received")
}
@ -170,7 +181,8 @@ func (l *LXD) getTools(image *api.Image, tools []*github.RunnerApplicationDownlo
osType, err := util.OSToOSType(osName)
if err != nil {
return github.RunnerApplicationDownload{}, errors.Wrap(err, "fetching OS type")
log.Printf("failed to determine OS type from image, assuming %s", assumedOSType)
osType = assumedOSType
}
// Validate image OS. Linux only for now.
@ -232,7 +244,7 @@ func (l *LXD) getCreateInstanceArgs(bootstrapParams params.BootstrapInstance) (a
return api.InstancesPost{}, errors.Wrap(err, "getting image details")
}
tools, err := l.getTools(image, bootstrapParams.Tools)
tools, err := l.getTools(image, bootstrapParams.Tools, bootstrapParams.OSType)
if err != nil {
return api.InstancesPost{}, errors.Wrap(err, "getting tools")
}
@ -244,6 +256,8 @@ func (l *LXD) getCreateInstanceArgs(bootstrapParams params.BootstrapInstance) (a
configMap := map[string]string{
"user.user-data": cloudCfg,
osTypeKeyName: string(bootstrapParams.OSType),
osArchKeyNAme: string(bootstrapParams.OSArch),
controllerIDKeyName: l.controllerID,
poolIDKey: bootstrapParams.PoolID,
}
@ -370,11 +384,17 @@ func (l *LXD) DeleteInstance(ctx context.Context, instance string) error {
op, err := cli.DeleteInstance(instance)
if err != nil {
if isNotFoundError(err) {
return nil
}
return errors.Wrap(err, "removing instance")
}
err = op.Wait()
if err != nil {
if isNotFoundError(err) {
return nil
}
return errors.Wrap(err, "waiting for instance deletion")
}
return nil

View file

@ -38,6 +38,7 @@ import (
)
var (
//lint:ignore ST1005 imported error from lxd
errInstanceIsStopped error = fmt.Errorf("The instance is already stopped")
)
@ -61,15 +62,24 @@ func isNotFoundError(err error) bool {
}
func lxdInstanceToAPIInstance(instance *api.InstanceFull) params.Instance {
os, ok := instance.ExpandedConfig["image.os"]
lxdOS, ok := instance.ExpandedConfig["image.os"]
if !ok {
log.Printf("failed to find OS in instance config")
}
osType, err := util.OSToOSType(os)
osType, err := util.OSToOSType(lxdOS)
if err != nil {
log.Printf("failed to find OS type for OS %s", os)
log.Printf("failed to find OS type for OS %s", lxdOS)
}
if osType == "" {
osTypeFromTag, ok := instance.ExpandedConfig[osTypeKeyName]
if !ok {
log.Printf("failed to find OS type in fallback location")
}
osType = params.OSType(osTypeFromTag)
}
osRelease, ok := instance.ExpandedConfig["image.release"]
if !ok {
log.Printf("failed to find OS release instance config")
@ -101,7 +111,7 @@ func lxdInstanceToAPIInstance(instance *api.InstanceFull) params.Instance {
ProviderID: instance.Name,
Name: instance.Name,
OSType: osType,
OSName: strings.ToLower(os),
OSName: strings.ToLower(lxdOS),
OSVersion: osRelease,
Addresses: addresses,
Status: lxdStatusToProviderStatus(state.Status),

View file

@ -690,7 +690,7 @@ func (r *Runner) appendTagsToCreatePoolParams(param params.CreatePoolParams) (pa
return params.CreatePoolParams{}, runnerErrors.NewBadRequestError("no such provider %s", param.ProviderName)
}
newTags, err := r.processTags(string(param.OSArch), string(param.OSType), param.Tags)
newTags, err := r.processTags(string(param.OSArch), param.OSType, param.Tags)
if err != nil {
return params.CreatePoolParams{}, errors.Wrap(err, "processing tags")
}
@ -700,7 +700,7 @@ func (r *Runner) appendTagsToCreatePoolParams(param params.CreatePoolParams) (pa
return param, nil
}
func (r *Runner) processTags(osArch, osType string, tags []string) ([]string, error) {
func (r *Runner) processTags(osArch string, osType params.OSType, tags []string) ([]string, error) {
// github automatically adds the "self-hosted" tag as well as the OS type (linux, windows, etc)
// and architecture (arm, x64, etc) to all self hosted runners. When a workflow job comes in, we try
// to find a pool based on the labels that are set in the workflow. If we don't explicitly define these
@ -713,7 +713,7 @@ func (r *Runner) processTags(osArch, osType string, tags []string) ([]string, er
return nil, errors.Wrap(err, "invalid arch")
}
ghOSType, err := util.ResolveToGithubOSType(osType)
ghOSType, err := util.ResolveToGithubTag(osType)
if err != nil {
return nil, errors.Wrap(err, "invalid os type")
}

View file

@ -25,9 +25,9 @@ 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.Linux: {},
params.Windows: {},
}
// These are the architectures that Github supports.

View file

@ -15,6 +15,8 @@
package util
import (
"bytes"
"compress/gzip"
"context"
"crypto/aes"
"crypto/cipher"
@ -22,6 +24,7 @@ import (
"crypto/tls"
"crypto/x509"
"encoding/base64"
"encoding/binary"
"fmt"
"io"
"math/big"
@ -31,6 +34,7 @@ import (
"regexp"
"strings"
"unicode"
"unicode/utf16"
"github.com/cloudbase/garm/cloudconfig"
"github.com/cloudbase/garm/config"
@ -89,8 +93,17 @@ var (
"linux": "linux",
"windows": "win",
}
//
githubOSTag = map[params.OSType]string{
params.Linux: "Linux",
params.Windows: "Windows",
}
)
// ResolveToGithubArch returns the cpu architecture as it is defined in the GitHub
// tools download list. We use it to find the proper tools for the OS/Arch combo we're
// deploying.
func ResolveToGithubArch(arch string) (string, error) {
ghArch, ok := githubArchMapping[arch]
if !ok {
@ -100,6 +113,9 @@ func ResolveToGithubArch(arch string) (string, error) {
return ghArch, nil
}
// ResolveToGithubArch returns the OS type as it is defined in the GitHub
// tools download list. We use it to find the proper tools for the OS/Arch combo we're
// deploying.
func ResolveToGithubOSType(osType string) (string, error) {
ghOS, ok := githubOSTypeMap[osType]
if !ok {
@ -109,6 +125,18 @@ func ResolveToGithubOSType(osType string) (string, error) {
return ghOS, nil
}
// ResolveToGithubTag returns the default OS tag that self hosted runners automatically
// (and forcefully) adds to every runner that gets deployed. We need to keep track of those
// tags internally as well.
func ResolveToGithubTag(os params.OSType) (string, error) {
ghOS, ok := githubOSTag[os]
if !ok {
return "", runnerErrors.NewNotFoundError("os %s is unknown", os)
}
return ghOS, nil
}
// IsValidEmail returs a bool indicating if an email is valid
func IsValidEmail(email string) bool {
if len(email) > 254 || !rxEmail.MatchString(email) {
@ -198,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")
}
@ -225,29 +251,84 @@ func GetCloudConfig(bootstrapParams params.BootstrapInstance, tools github.Runne
RunnerLabels: strings.Join(bootstrapParams.Labels, ","),
CallbackURL: bootstrapParams.CallbackURL,
CallbackToken: bootstrapParams.InstanceToken,
GitHubRunnerGroup: bootstrapParams.GitHubRunnerGroup,
}
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")
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)
}
if bootstrapParams.CACertBundle != nil && len(bootstrapParams.CACertBundle) > 0 {
if err := cloudCfg.AddCACert(bootstrapParams.CACertBundle); err != nil {
return "", errors.Wrap(err, "adding CA cert bundle")
return asStr, nil
}
func GetTools(osType params.OSType, osArch params.OSArch, tools []*github.RunnerApplicationDownload) (github.RunnerApplicationDownload, error) {
// Validate image OS. Linux only for now.
switch osType {
case params.Linux:
case params.Windows:
default:
return github.RunnerApplicationDownload{}, fmt.Errorf("unsupported OS type: %s", osType)
}
switch osArch {
case params.Amd64:
case params.Arm:
case params.Arm64:
default:
return github.RunnerApplicationDownload{}, fmt.Errorf("unsupported OS arch: %s", osArch)
}
// Find tools for OS/Arch.
for _, tool := range tools {
if tool == nil {
continue
}
if tool.OS == nil || tool.Architecture == nil {
continue
}
ghArch, err := ResolveToGithubArch(string(osArch))
if err != nil {
continue
}
ghOS, err := ResolveToGithubOSType(string(osType))
if err != nil {
continue
}
if *tool.Architecture == ghArch && *tool.OS == ghOS {
return *tool, nil
}
}
asStr, err := cloudCfg.Serialize()
if err != nil {
return "", errors.Wrap(err, "creating cloud config")
}
return asStr, nil
return github.RunnerApplicationDownload{}, fmt.Errorf("failed to find tools for OS %s and arch %s", osType, osArch)
}
// GetRandomString returns a secure random string
@ -350,3 +431,58 @@ func NewID() string {
newUUID := uuid.New()
return toBase62(newUUID[:])
}
func UTF16FromString(s string) ([]uint16, error) {
buf := make([]uint16, 0, len(s)*2+1)
for _, r := range s {
buf = utf16.AppendRune(buf, r)
}
return utf16.AppendRune(buf, '\x00'), nil
}
func UTF16ToString(s []uint16) string {
for i, v := range s {
if v == 0 {
s = s[0:i]
break
}
}
return string(utf16.Decode(s))
}
func Uint16ToByteArray(u []uint16) []byte {
ret := make([]byte, (len(u)-1)*2)
for i := 0; i < len(u)-1; i++ {
binary.LittleEndian.PutUint16(ret[i*2:], uint16(u[i]))
}
return ret
}
func UTF16EncodedByteArrayFromString(s string) ([]byte, error) {
asUint16, err := UTF16FromString(s)
if err != nil {
return nil, fmt.Errorf("failed to encode to uint16: %w", err)
}
asBytes := Uint16ToByteArray(asUint16)
return asBytes, nil
}
func CompressData(data []byte) ([]byte, error) {
var b bytes.Buffer
gz := gzip.NewWriter(&b)
_, err := gz.Write(data)
if err != nil {
return nil, fmt.Errorf("failed to compress data: %w", err)
}
if err = gz.Flush(); err != nil {
return nil, fmt.Errorf("failed to flush buffer: %w", err)
}
if err = gz.Close(); err != nil {
return nil, fmt.Errorf("failed to close buffer: %w", err)
}
return b.Bytes(), nil
}

View file

@ -84,11 +84,11 @@ func ConnectLXD(url string, args *ConnectionArgs) (InstanceServer, error) {
//
// Unless the remote server is trusted by the system CA, the remote certificate must be provided (TLSServerCert).
func ConnectLXDWithContext(ctx context.Context, url string, args *ConnectionArgs) (InstanceServer, error) {
logger.Debug("Connecting to a remote LXD over HTTPS")
// Cleanup URL
url = strings.TrimSuffix(url, "/")
logger.Debug("Connecting to a remote LXD over HTTPS", logger.Ctx{"url": url})
return httpsLXD(ctx, url, args)
}

View file

@ -440,7 +440,7 @@ func (r *ProtocolLXD) rawWebsocket(url string) (*websocket.Conn, error) {
if remoteTCP != nil {
err = tcp.SetTimeouts(remoteTCP, 0)
if err != nil {
logger.Error("Failed setting TCP timeouts on remote connection", logger.Ctx{"err": err})
logger.Warn("Failed setting TCP timeouts on remote connection", logger.Ctx{"err": err})
}
}

View file

@ -651,8 +651,8 @@ func (r *ProtocolLXD) ExecContainer(containerName string, exec api.ContainerExec
}
// Call the control handler with a connection to the control socket
if args.Control != nil && fds["control"] != "" {
conn, err := r.GetOperationWebsocket(opAPI.ID, fds["control"])
if args.Control != nil && fds[api.SecretNameControl] != "" {
conn, err := r.GetOperationWebsocket(opAPI.ID, fds[api.SecretNameControl])
if err != nil {
return nil, err
}
@ -1546,11 +1546,11 @@ func (r *ProtocolLXD) ConsoleContainer(containerName string, console api.Contain
var controlConn *websocket.Conn
// Call the control handler with a connection to the control socket
if fds["control"] == "" {
if fds[api.SecretNameControl] == "" {
return nil, fmt.Errorf("Did not receive a file descriptor for the control channel")
}
controlConn, err = r.GetOperationWebsocket(opAPI.ID, fds["control"])
controlConn, err = r.GetOperationWebsocket(opAPI.ID, fds[api.SecretNameControl])
if err != nil {
return nil, err
}

View file

@ -1046,8 +1046,8 @@ func (r *ProtocolLXD) ExecInstance(instanceName string, exec api.InstanceExecPos
}
// Call the control handler with a connection to the control socket
if args.Control != nil && fds["control"] != "" {
conn, err := r.GetOperationWebsocket(opAPI.ID, fds["control"])
if args.Control != nil && fds[api.SecretNameControl] != "" {
conn, err := r.GetOperationWebsocket(opAPI.ID, fds[api.SecretNameControl])
if err != nil {
return nil, err
}
@ -2209,11 +2209,11 @@ func (r *ProtocolLXD) ConsoleInstance(instanceName string, console api.InstanceC
var controlConn *websocket.Conn
// Call the control handler with a connection to the control socket
if fds["control"] == "" {
if fds[api.SecretNameControl] == "" {
return nil, fmt.Errorf("Did not receive a file descriptor for the control channel")
}
controlConn, err = r.GetOperationWebsocket(opAPI.ID, fds["control"])
controlConn, err = r.GetOperationWebsocket(opAPI.ID, fds[api.SecretNameControl])
if err != nil {
return nil, err
}
@ -2296,11 +2296,11 @@ func (r *ProtocolLXD) ConsoleInstanceDynamic(instanceName string, console api.In
}
// Call the control handler with a connection to the control socket.
if fds["control"] == "" {
if fds[api.SecretNameControl] == "" {
return nil, nil, fmt.Errorf("Did not receive a file descriptor for the control channel")
}
controlConn, err := r.GetOperationWebsocket(opAPI.ID, fds["control"])
controlConn, err := r.GetOperationWebsocket(opAPI.ID, fds[api.SecretNameControl])
if err != nil {
return nil, nil, err
}
@ -2600,7 +2600,7 @@ func (r *ProtocolLXD) proxyMigration(targetOp *operation, targetSecrets map[stri
}
}
if targetSecrets["control"] == "" {
if targetSecrets[api.SecretNameControl] == "" {
return fmt.Errorf("Migration target didn't setup the required \"control\" socket")
}
@ -2614,17 +2614,17 @@ func (r *ProtocolLXD) proxyMigration(targetOp *operation, targetSecrets map[stri
proxies := map[string]*proxy{}
// Connect the control socket
sourceConn, err := source.GetOperationWebsocket(sourceOp.ID, sourceSecrets["control"])
sourceConn, err := source.GetOperationWebsocket(sourceOp.ID, sourceSecrets[api.SecretNameControl])
if err != nil {
return err
}
targetConn, err := r.GetOperationWebsocket(targetOp.ID, targetSecrets["control"])
targetConn, err := r.GetOperationWebsocket(targetOp.ID, targetSecrets[api.SecretNameControl])
if err != nil {
return err
}
proxies["control"] = &proxy{
proxies[api.SecretNameControl] = &proxy{
done: shared.WebsocketProxy(sourceConn, targetConn),
sourceConn: sourceConn,
targetConn: targetConn,
@ -2632,7 +2632,7 @@ func (r *ProtocolLXD) proxyMigration(targetOp *operation, targetSecrets map[stri
// Connect the data sockets
for name := range sourceSecrets {
if name == "control" {
if name == api.SecretNameControl {
continue
}
@ -2657,13 +2657,13 @@ func (r *ProtocolLXD) proxyMigration(targetOp *operation, targetSecrets map[stri
// Cleanup once everything is done
go func() {
// Wait for control socket
<-proxies["control"].done
_ = proxies["control"].sourceConn.Close()
_ = proxies["control"].targetConn.Close()
<-proxies[api.SecretNameControl].done
_ = proxies[api.SecretNameControl].sourceConn.Close()
_ = proxies[api.SecretNameControl].targetConn.Close()
// Then deal with the others
for name, proxy := range proxies {
if name == "control" {
if name == api.SecretNameControl {
continue
}

10
vendor/github.com/lxc/lxd/shared/api/migration.go generated vendored Normal file
View file

@ -0,0 +1,10 @@
package api
// SecretNameControl is the secret name used for the migration control connection.
const SecretNameControl = "control"
// SecretNameFilesystem is the secret name used for the migration filesystem connection.
const SecretNameFilesystem = "fs"
// SecretNameState is the secret name used for the migration state connection.
const SecretNameState = "criu" // Legacy value used for backward compatibility for clients.

View file

@ -150,6 +150,7 @@ var InstanceConfigKeysAny = map[string]func(value string) error{
"volatile.apply_quota": validate.IsAny,
"volatile.uuid": validate.Optional(validate.IsUUID),
"volatile.vsock_id": validate.Optional(validate.IsInt64),
"volatile.uuid.generation": validate.Optional(validate.IsUUID),
// Caller is responsible for full validation of any raw.* value.
"raw.idmap": validate.IsAny,
@ -256,8 +257,12 @@ var InstanceConfigKeysVM = map[string]func(value string) error{
"raw.qemu": validate.IsAny,
"raw.qemu.conf": validate.IsAny,
"security.agent.metrics": validate.Optional(validate.IsBool),
"security.secureboot": validate.Optional(validate.IsBool),
"security.agent.metrics": validate.Optional(validate.IsBool),
"security.secureboot": validate.Optional(validate.IsBool),
"security.sev": validate.Optional(validate.IsBool),
"security.sev.policy.es": validate.Optional(validate.IsBool),
"security.sev.session.dh": validate.Optional(validate.IsAny),
"security.sev.session.data": validate.Optional(validate.IsAny),
"agent.nic_config": validate.Optional(validate.IsBool),

View file

@ -375,10 +375,14 @@ func DefaultWriter(conn *websocket.Conn, w io.WriteCloser, writeDone chan<- bool
type WebsocketIO struct {
Conn *websocket.Conn
reader io.Reader
mu sync.Mutex
mur sync.Mutex
muw sync.Mutex
}
func (w *WebsocketIO) Read(p []byte) (n int, err error) {
w.mur.Lock()
defer w.mur.Unlock()
// Get new message if no active one.
if w.reader == nil {
var mt int
@ -410,26 +414,22 @@ func (w *WebsocketIO) Read(p []byte) (n int, err error) {
return n, nil
}
func (w *WebsocketIO) Write(p []byte) (n int, err error) {
w.mu.Lock()
defer w.mu.Unlock()
wr, err := w.Conn.NextWriter(websocket.BinaryMessage)
func (w *WebsocketIO) Write(p []byte) (int, error) {
w.muw.Lock()
defer w.muw.Unlock()
err := w.Conn.WriteMessage(websocket.BinaryMessage, p)
if err != nil {
return -1, err
}
n, err = wr.Write(p)
if err != nil {
return -1, err
}
return n, wr.Close()
return len(p), nil
}
// Close sends a control message indicating the stream is finished, but it does not actually close the socket.
func (w *WebsocketIO) Close() error {
w.mu.Lock()
defer w.mu.Unlock()
w.muw.Lock()
defer w.muw.Unlock()
// Target expects to get a control message indicating stream is finished.
return w.Conn.WriteMessage(websocket.TextMessage, []byte{})
}

View file

@ -138,6 +138,10 @@ func (s *SimpleStreams) cachedDownload(path string) ([]byte, error) {
return nil, err
}
if len(body) == 0 {
return nil, fmt.Errorf("No content in download from %q", uri)
}
// Attempt to store in cache
if s.cachePath != "" {
cacheName := filepath.Join(s.cachePath, fileName)
@ -159,11 +163,13 @@ func (s *SimpleStreams) parseStream() (*Stream, error) {
return nil, err
}
pathURL, _ := shared.JoinUrls(s.url, path)
// Parse the idnex
stream := Stream{}
err = json.Unmarshal(body, &stream)
if err != nil {
return nil, fmt.Errorf("Failed decoding stream JSON from %q: %w", path, err)
return nil, fmt.Errorf("Failed decoding stream JSON from %q: %w (%q)", pathURL, err, string(body))
}
s.cachedStream = &stream

9
vendor/github.com/mattn/go-isatty/LICENSE generated vendored Normal file
View file

@ -0,0 +1,9 @@
Copyright (c) Yasuhiro MATSUMOTO <mattn.jp@gmail.com>
MIT License (Expat)
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

50
vendor/github.com/mattn/go-isatty/README.md generated vendored Normal file
View file

@ -0,0 +1,50 @@
# go-isatty
[![Godoc Reference](https://godoc.org/github.com/mattn/go-isatty?status.svg)](http://godoc.org/github.com/mattn/go-isatty)
[![Codecov](https://codecov.io/gh/mattn/go-isatty/branch/master/graph/badge.svg)](https://codecov.io/gh/mattn/go-isatty)
[![Coverage Status](https://coveralls.io/repos/github/mattn/go-isatty/badge.svg?branch=master)](https://coveralls.io/github/mattn/go-isatty?branch=master)
[![Go Report Card](https://goreportcard.com/badge/mattn/go-isatty)](https://goreportcard.com/report/mattn/go-isatty)
isatty for golang
## Usage
```go
package main
import (
"fmt"
"github.com/mattn/go-isatty"
"os"
)
func main() {
if isatty.IsTerminal(os.Stdout.Fd()) {
fmt.Println("Is Terminal")
} else if isatty.IsCygwinTerminal(os.Stdout.Fd()) {
fmt.Println("Is Cygwin/MSYS2 Terminal")
} else {
fmt.Println("Is Not Terminal")
}
}
```
## Installation
```
$ go get github.com/mattn/go-isatty
```
## License
MIT
## Author
Yasuhiro Matsumoto (a.k.a mattn)
## Thanks
* k-takata: base idea for IsCygwinTerminal
https://github.com/k-takata/go-iscygpty

2
vendor/github.com/mattn/go-isatty/doc.go generated vendored Normal file
View file

@ -0,0 +1,2 @@
// Package isatty implements interface to isatty
package isatty

12
vendor/github.com/mattn/go-isatty/go.test.sh generated vendored Normal file
View file

@ -0,0 +1,12 @@
#!/usr/bin/env bash
set -e
echo "" > coverage.txt
for d in $(go list ./... | grep -v vendor); do
go test -race -coverprofile=profile.out -covermode=atomic "$d"
if [ -f profile.out ]; then
cat profile.out >> coverage.txt
rm profile.out
fi
done

19
vendor/github.com/mattn/go-isatty/isatty_bsd.go generated vendored Normal file
View file

@ -0,0 +1,19 @@
//go:build (darwin || freebsd || openbsd || netbsd || dragonfly || hurd) && !appengine
// +build darwin freebsd openbsd netbsd dragonfly hurd
// +build !appengine
package isatty
import "golang.org/x/sys/unix"
// IsTerminal return true if the file descriptor is terminal.
func IsTerminal(fd uintptr) bool {
_, err := unix.IoctlGetTermios(int(fd), unix.TIOCGETA)
return err == nil
}
// IsCygwinTerminal return true if the file descriptor is a cygwin or msys2
// terminal. This is also always false on this environment.
func IsCygwinTerminal(fd uintptr) bool {
return false
}

16
vendor/github.com/mattn/go-isatty/isatty_others.go generated vendored Normal file
View file

@ -0,0 +1,16 @@
//go:build appengine || js || nacl || wasm
// +build appengine js nacl wasm
package isatty
// IsTerminal returns true if the file descriptor is terminal which
// is always false on js and appengine classic which is a sandboxed PaaS.
func IsTerminal(fd uintptr) bool {
return false
}
// IsCygwinTerminal() return true if the file descriptor is a cygwin or msys2
// terminal. This is also always false on this environment.
func IsCygwinTerminal(fd uintptr) bool {
return false
}

23
vendor/github.com/mattn/go-isatty/isatty_plan9.go generated vendored Normal file
View file

@ -0,0 +1,23 @@
//go:build plan9
// +build plan9
package isatty
import (
"syscall"
)
// IsTerminal returns true if the given file descriptor is a terminal.
func IsTerminal(fd uintptr) bool {
path, err := syscall.Fd2path(int(fd))
if err != nil {
return false
}
return path == "/dev/cons" || path == "/mnt/term/dev/cons"
}
// IsCygwinTerminal return true if the file descriptor is a cygwin or msys2
// terminal. This is also always false on this environment.
func IsCygwinTerminal(fd uintptr) bool {
return false
}

21
vendor/github.com/mattn/go-isatty/isatty_solaris.go generated vendored Normal file
View file

@ -0,0 +1,21 @@
//go:build solaris && !appengine
// +build solaris,!appengine
package isatty
import (
"golang.org/x/sys/unix"
)
// IsTerminal returns true if the given file descriptor is a terminal.
// see: https://src.illumos.org/source/xref/illumos-gate/usr/src/lib/libc/port/gen/isatty.c
func IsTerminal(fd uintptr) bool {
_, err := unix.IoctlGetTermio(int(fd), unix.TCGETA)
return err == nil
}
// IsCygwinTerminal return true if the file descriptor is a cygwin or msys2
// terminal. This is also always false on this environment.
func IsCygwinTerminal(fd uintptr) bool {
return false
}

19
vendor/github.com/mattn/go-isatty/isatty_tcgets.go generated vendored Normal file
View file

@ -0,0 +1,19 @@
//go:build (linux || aix || zos) && !appengine
// +build linux aix zos
// +build !appengine
package isatty
import "golang.org/x/sys/unix"
// IsTerminal return true if the file descriptor is terminal.
func IsTerminal(fd uintptr) bool {
_, err := unix.IoctlGetTermios(int(fd), unix.TCGETS)
return err == nil
}
// IsCygwinTerminal return true if the file descriptor is a cygwin or msys2
// terminal. This is also always false on this environment.
func IsCygwinTerminal(fd uintptr) bool {
return false
}

125
vendor/github.com/mattn/go-isatty/isatty_windows.go generated vendored Normal file
View file

@ -0,0 +1,125 @@
//go:build windows && !appengine
// +build windows,!appengine
package isatty
import (
"errors"
"strings"
"syscall"
"unicode/utf16"
"unsafe"
)
const (
objectNameInfo uintptr = 1
fileNameInfo = 2
fileTypePipe = 3
)
var (
kernel32 = syscall.NewLazyDLL("kernel32.dll")
ntdll = syscall.NewLazyDLL("ntdll.dll")
procGetConsoleMode = kernel32.NewProc("GetConsoleMode")
procGetFileInformationByHandleEx = kernel32.NewProc("GetFileInformationByHandleEx")
procGetFileType = kernel32.NewProc("GetFileType")
procNtQueryObject = ntdll.NewProc("NtQueryObject")
)
func init() {
// Check if GetFileInformationByHandleEx is available.
if procGetFileInformationByHandleEx.Find() != nil {
procGetFileInformationByHandleEx = nil
}
}
// IsTerminal return true if the file descriptor is terminal.
func IsTerminal(fd uintptr) bool {
var st uint32
r, _, e := syscall.Syscall(procGetConsoleMode.Addr(), 2, fd, uintptr(unsafe.Pointer(&st)), 0)
return r != 0 && e == 0
}
// Check pipe name is used for cygwin/msys2 pty.
// Cygwin/MSYS2 PTY has a name like:
// \{cygwin,msys}-XXXXXXXXXXXXXXXX-ptyN-{from,to}-master
func isCygwinPipeName(name string) bool {
token := strings.Split(name, "-")
if len(token) < 5 {
return false
}
if token[0] != `\msys` &&
token[0] != `\cygwin` &&
token[0] != `\Device\NamedPipe\msys` &&
token[0] != `\Device\NamedPipe\cygwin` {
return false
}
if token[1] == "" {
return false
}
if !strings.HasPrefix(token[2], "pty") {
return false
}
if token[3] != `from` && token[3] != `to` {
return false
}
if token[4] != "master" {
return false
}
return true
}
// getFileNameByHandle use the undocomented ntdll NtQueryObject to get file full name from file handler
// since GetFileInformationByHandleEx is not available under windows Vista and still some old fashion
// guys are using Windows XP, this is a workaround for those guys, it will also work on system from
// Windows vista to 10
// see https://stackoverflow.com/a/18792477 for details
func getFileNameByHandle(fd uintptr) (string, error) {
if procNtQueryObject == nil {
return "", errors.New("ntdll.dll: NtQueryObject not supported")
}
var buf [4 + syscall.MAX_PATH]uint16
var result int
r, _, e := syscall.Syscall6(procNtQueryObject.Addr(), 5,
fd, objectNameInfo, uintptr(unsafe.Pointer(&buf)), uintptr(2*len(buf)), uintptr(unsafe.Pointer(&result)), 0)
if r != 0 {
return "", e
}
return string(utf16.Decode(buf[4 : 4+buf[0]/2])), nil
}
// IsCygwinTerminal() return true if the file descriptor is a cygwin or msys2
// terminal.
func IsCygwinTerminal(fd uintptr) bool {
if procGetFileInformationByHandleEx == nil {
name, err := getFileNameByHandle(fd)
if err != nil {
return false
}
return isCygwinPipeName(name)
}
// Cygwin/msys's pty is a pipe.
ft, _, e := syscall.Syscall(procGetFileType.Addr(), 1, fd, 0, 0)
if ft != fileTypePipe || e != 0 {
return false
}
var buf [2 + syscall.MAX_PATH]uint16
r, _, e := syscall.Syscall6(procGetFileInformationByHandleEx.Addr(),
4, fd, fileNameInfo, uintptr(unsafe.Pointer(&buf)),
uintptr(len(buf)*2), 0, 0)
if r == 0 || e != 0 {
return false
}
l := *(*uint32)(unsafe.Pointer(&buf))
return isCygwinPipeName(string(utf16.Decode(buf[2 : 2+l/2])))
}

View file

@ -88,15 +88,15 @@ func parseNumber(input []byte) number {
neg = true
s = s[1:]
size++
if len(s) == 0 {
return number{}
}
// Consume any whitespace or comments between the
// negative sign and the rest of the number
lenBefore := len(s)
s = consume(s, 0)
sep = lenBefore - len(s)
size += sep
if len(s) == 0 {
return number{}
}
}
switch {

View file

@ -51,7 +51,7 @@ import (
// 10. Send out the CL for review and submit it.
const (
Major = 1
Minor = 29
Minor = 30
Patch = 0
PreRelease = ""
)

15
vendor/modules.txt vendored
View file

@ -20,6 +20,8 @@ github.com/felixge/httpsnoop
# github.com/flosch/pongo2 v0.0.0-20200913210552-0d938eb266f3
## explicit; go 1.14
github.com/flosch/pongo2
# github.com/frankban/quicktest v1.14.3
## explicit; go 1.13
# github.com/go-macaroon-bakery/macaroon-bakery/v3 v3.0.1
## explicit; go 1.17
github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery
@ -82,6 +84,8 @@ github.com/juju/errors
# github.com/juju/retry v1.0.0
## explicit; go 1.17
github.com/juju/retry
# github.com/juju/testing v1.0.2
## explicit; go 1.17
# github.com/juju/webbrowser v1.0.0
## explicit; go 1.11
github.com/juju/webbrowser
@ -94,9 +98,9 @@ github.com/kballard/go-shellquote
# github.com/kr/fs v0.1.0
## explicit
github.com/kr/fs
# github.com/kr/pretty v0.3.0
# github.com/kr/pretty v0.3.1
## explicit; go 1.12
# github.com/lxc/lxd v0.0.0-20230310224854-36b345fbd578
# github.com/lxc/lxd v0.0.0-20230325180147-8d608287b0ce
## explicit; go 1.18
github.com/lxc/lxd/client
github.com/lxc/lxd/lxd/device/config
@ -118,6 +122,9 @@ github.com/lxc/lxd/shared/validate
github.com/manifoldco/promptui
github.com/manifoldco/promptui/list
github.com/manifoldco/promptui/screenbuf
# github.com/mattn/go-isatty v0.0.18
## explicit; go 1.15
github.com/mattn/go-isatty
# github.com/mattn/go-runewidth v0.0.14
## explicit; go 1.9
github.com/mattn/go-runewidth
@ -207,8 +214,6 @@ github.com/stretchr/testify/suite
# github.com/teris-io/shortid v0.0.0-20220617161101-71ec9f2aa569
## explicit; go 1.18
github.com/teris-io/shortid
# github.com/xdg-go/stringprep v1.0.3
## explicit; go 1.11
# golang.org/x/crypto v0.7.0
## explicit; go 1.17
golang.org/x/crypto/bcrypt
@ -262,7 +267,7 @@ google.golang.org/appengine/internal/log
google.golang.org/appengine/internal/remote_api
google.golang.org/appengine/internal/urlfetch
google.golang.org/appengine/urlfetch
# google.golang.org/protobuf v1.29.0
# google.golang.org/protobuf v1.30.0
## explicit; go 1.11
google.golang.org/protobuf/encoding/prototext
google.golang.org/protobuf/encoding/protowire