diff --git a/cloudconfig/templates.go b/cloudconfig/templates.go index b2815c59..2518b7c0 100644 --- a/cloudconfig/templates.go +++ b/cloudconfig/templates.go @@ -63,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 }}" @@ -81,7 +90,7 @@ 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" @@ -274,6 +283,7 @@ function Invoke-GarmFailure() { $PEMData = @" {{.CABundle}} "@ +$GHRunnerGroup = "{{.GitHubRunnerGroup}}" function Install-Runner() { $CallbackURL="{{.CallbackURL}}" @@ -311,10 +321,13 @@ function Install-Runner() { 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 --name "{{ .RunnerName }}" --labels "{{ .RunnerLabels }}" --ephemeral --runasservice + ./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) @@ -339,6 +352,7 @@ type InstallRunnerParams struct { CallbackToken string TempDownloadToken string CABundle string + GitHubRunnerGroup string } func InstallRunnerScript(installParams InstallRunnerParams, osType params.OSType) ([]byte, error) { diff --git a/cmd/garm-cli/cmd/pool.go b/cmd/garm-cli/cmd/pool.go index 27261da2..7838f280 100644 --- a/cmd/garm-cli/cmd/pool.go +++ b/cmd/garm-cli/cmd/pool.go @@ -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.") diff --git a/database/sql/instances.go b/database/sql/instances.go index 30587809..ab83763c 100644 --- a/database/sql/instances.go +++ b/database/sql/instances.go @@ -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 { diff --git a/database/sql/models.go b/database/sql/models.go index 3c6b1f85..a46f0e54 100644 --- a/database/sql/models.go +++ b/database/sql/models.go @@ -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"` diff --git a/database/sql/pools_test.go b/database/sql/pools_test.go index de06e126..69039813 100644 --- a/database/sql/pools_test.go +++ b/database/sql/pools_test.go @@ -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()) diff --git a/database/sql/util.go b/database/sql/util.go index d9efc5ad..31aa8ba3 100644 --- a/database/sql/util.go +++ b/database/sql/util.go @@ -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") } diff --git a/params/params.go b/params/params.go index ee43e4b9..ece5b52b 100644 --- a/params/params.go +++ b/params/params.go @@ -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,14 +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"` - OSType OSType `json:"os_type"` - 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 { @@ -202,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 { diff --git a/params/requests.go b/params/requests.go index ce7bebd4..c7c50eb5 100644 --- a/params/requests.go +++ b/params/requests.go @@ -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 { diff --git a/runner/pool/pool.go b/runner/pool/pool.go index 9029948c..2e47a190 100644 --- a/runner/pool/pool.go +++ b/runner/pool/pool.go @@ -417,14 +417,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,20 +604,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, - OSType: pool.OSType, - 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 diff --git a/util/util.go b/util/util.go index 76a36e90..0bf283eb 100644 --- a/util/util.go +++ b/util/util.go @@ -251,6 +251,7 @@ 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)