Implement some more db functions

This commit is contained in:
Gabriel Adrian Samfira 2022-04-27 16:56:28 +00:00
parent 62ba5a5a08
commit 66b46ae0ab
15 changed files with 478 additions and 138 deletions

45
auth/jwt.go Normal file
View file

@ -0,0 +1,45 @@
package auth
import (
"runner-manager/params"
"runner-manager/runner/common"
"time"
"github.com/golang-jwt/jwt"
"github.com/pkg/errors"
)
// InstanceJWTClaims holds JWT claims
type InstanceJWTClaims struct {
ID string `json:"id"`
Name string `json:"name"`
PoolID string `json:"provider_id"`
// Scope is either repository or organization
Scope common.PoolType `json:"scope"`
// Entity is the repo or org name
Entity string `json:"entity"`
jwt.StandardClaims
}
func NewInstanceJWTToken(instance params.Instance, secret, entity string, poolType common.PoolType) (string, error) {
// make TTL configurable?
expireToken := time.Now().Add(3 * time.Hour).Unix()
claims := InstanceJWTClaims{
StandardClaims: jwt.StandardClaims{
ExpiresAt: expireToken,
Issuer: "runner-manager",
},
ID: instance.ID,
Name: instance.Name,
PoolID: instance.PoolID,
Scope: poolType,
Entity: entity,
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
tokenString, err := token.SignedString([]byte(secret))
if err != nil {
return "", errors.Wrap(err, "signing token")
}
return tokenString, nil
}

View file

@ -12,15 +12,53 @@ var CloudConfigTemplate = `#!/bin/bash
set -ex
set -o pipefail
curl -L -o "/home/runner/{{ .FileName }}" "{{ .DownloadURL }}"
mkdir -p /home/runner/actions-runner
tar xf "/home/runner/{{ .FileName }}" -C /home/runner/actions-runner/
chown {{ .RunnerUsername }}:{{ .RunnerGroup }} -R /home/{{ .RunnerUsername }}/actions-runner/
sudo /home/{{ .RunnerUsername }}/actions-runner/bin/installdependencies.sh
sudo -u {{ .RunnerUsername }} -- /home/{{ .RunnerUsername }}/actions-runner/config.sh --unattended --url "{{ .RepoURL }}" --token "{{ .GithubToken }}" --name "{{ .RunnerName }}" --labels "{{ .RunnerLabels }}" --ephemeral
CALLBACK_URL="{{ .CallbackURL }}"
BEARER_TOKEN="{{ .CallbackToken }}"
function call() {
PAYLOAD="$1"
curl -s -X POST -d \'${PAYLOAD}\' -H 'Accept: application/json' -H "Authorization: Bearer ${BEARER_TOKEN}" "${CALLBACK_URL}" || echo "failed to call home: exit code ($?)"
}
function sendStatus() {
MSG="$1"
call '{"status": "installing", "message": "'$MSG'"}'
}
function success() {
MSG="$1"
call '{"status": "active", "message": "'$MSG'"}'
}
function fail() {
MSG="$1"
call '{"status": "failed", "message": "'$MSG'"}'
exit 1
}
sendStatus "downloading tools from {{ .DownloadURL }}"
curl -L -o "/home/runner/{{ .FileName }}" "{{ .DownloadURL }}" || fail "failed to download tools"
mkdir -p /home/runner/actions-runner || fail "failed to create actions-runner folder"
sendStatus "extracting runner"
tar xf "/home/runner/{{ .FileName }}" -C /home/runner/actions-runner/ || fail "failed to extract runner"
chown {{ .RunnerUsername }}:{{ .RunnerGroup }} -R /home/{{ .RunnerUsername }}/actions-runner/ || fail "failed to change owner"
sendStatus "installing dependencies"
cd /home/{{ .RunnerUsername }}/actions-runner
./svc.sh install {{ .RunnerUsername }}
./svc.sh start
sudo ./bin/installdependencies.sh || fail "failed to install dependencies"
sendStatus "configuring runner"
sudo -u {{ .RunnerUsername }} -- ./config.sh --unattended --url "{{ .RepoURL }}" --token "{{ .GithubToken }}" --name "{{ .RunnerName }}" --labels "{{ .RunnerLabels }}" --ephemeral || fail "failed to configure runner"
sendStatus "installing runner service"
./svc.sh install {{ .RunnerUsername }} || fail "failed to install service"
sendStatus "starting service"
./svc.sh start || fail "failed to start service"
success "runner successfully installed"
`
type InstallRunnerParams struct {
@ -32,6 +70,8 @@ type InstallRunnerParams struct {
GithubToken string
RunnerName string
RunnerLabels string
CallbackURL string
CallbackToken string
}
func InstallRunnerScript(params InstallRunnerParams) ([]byte, error) {

View file

@ -98,7 +98,7 @@ func main() {
}
fmt.Println(pool2)
pool3, err := db.FindRepositoryPoolByTags(ctx, repo.ID, []string{"myrunner", "superAwesome3"})
pool3, err := db.FindRepositoryPoolByTags(ctx, repo.ID, []string{"myrunner", "superAwesome2"})
if err != nil {
log.Fatal(err)
}

View file

@ -11,6 +11,7 @@ import (
"time"
"github.com/BurntSushi/toml"
zxcvbn "github.com/nbutton23/zxcvbn-go"
"github.com/pkg/errors"
)
@ -79,23 +80,20 @@ func NewConfig(cfgFile string) (*Config, error) {
if err := config.Validate(); err != nil {
return nil, errors.Wrap(err, "validating config")
}
if config.ConfigDir == "" {
config.ConfigDir = DefaultConfigDir
if config.Default.ConfigDir == "" {
config.Default.ConfigDir = DefaultConfigDir
}
return &config, nil
}
type Config struct {
// ConfigDir is the folder where the runner may save any aditional files
// or configurations it may need. Things like auto-generated SSH keys that
// may be used to access the runner instances.
ConfigDir string `toml:"config_dir,omitempty" json:"config-dir,omitempty"`
Default Default `toml:"default" json:"default"`
APIServer APIServer `toml:"apiserver,omitempty" json:"apiserver,omitempty"`
Database Database `toml:"database,omitempty" json:"database,omitempty"`
Repositories []Repository `toml:"repository,omitempty" json:"repository,omitempty"`
Organizations []Organization `toml:"organization,omitempty" json:"organization,omitempty"`
Providers []Provider `toml:"provider,omitempty" json:"provider,omitempty"`
Github Github `toml:"github,omitempty"`
Github []Github `toml:"github,omitempty"`
// LogFile is the location of the log file.
LogFile string `toml:"log_file,omitempty"`
}
@ -109,8 +107,14 @@ func (c *Config) Validate() error {
return errors.Wrap(err, "validating database config")
}
if err := c.Github.Validate(); err != nil {
return errors.Wrap(err, "validating github config")
if err := c.Default.Validate(); err != nil {
return errors.Wrap(err, "validating default section")
}
for _, gh := range c.Github {
if err := gh.Validate(); err != nil {
return errors.Wrap(err, "validating github config")
}
}
for _, provider := range c.Providers {
@ -164,6 +168,35 @@ func (c *Config) Validate() error {
return nil
}
type Default struct {
// ConfigDir is the folder where the runner may save any aditional files
// or configurations it may need. Things like auto-generated SSH keys that
// may be used to access the runner instances.
ConfigDir string `toml:"config_dir,omitempty" json:"config-dir,omitempty"`
CallbackURL string `toml:"callback_url" json:"callback-url"`
// JWTSecret is used to sign JWT tokens that will be used by instances to
// call home.
JWTSecret string `toml:"jwt_secret" json:"jwt-secret"`
}
func (d *Default) Validate() error {
if d.CallbackURL == "" {
return fmt.Errorf("missing callback_url")
}
if d.JWTSecret == "" {
return fmt.Errorf("missing jwt secret")
}
passwordStenght := zxcvbn.PasswordStrength(d.JWTSecret, nil)
if passwordStenght.Score < 4 {
return fmt.Errorf("jwt_secret is too weak")
}
return nil
}
// Organization represents a Github organization for which we can manage runners.
type Organization struct {
// Name is the name of the organization.
@ -196,6 +229,8 @@ func (o *Organization) String() string {
// Github hold configuration options specific to interacting with github.
// Currently that is just a OAuth2 personal token.
type Github struct {
Name string `toml:"name" json:"name"`
Description string `toml:"description" json:"description"`
OAuth2Token string `toml:"oauth2_token" json:"oauth2-token"`
}

View file

@ -39,6 +39,6 @@ type Store interface {
ListRepoInstances(ctx context.Context, repoID string) ([]params.Instance, error)
ListOrgInstances(ctx context.Context, orgID string) ([]params.Instance, error)
GetInstance(ctx context.Context, poolID string, instanceID string) (params.Instance, error)
GetInstanceByName(ctx context.Context, instanceName string) (params.Instance, error)
// GetInstance(ctx context.Context, poolID string, instanceID string) (params.Instance, error)
GetInstanceByName(ctx context.Context, poolID string, instanceName string) (params.Instance, error)
}

View file

@ -2,6 +2,7 @@ package sql
import (
"runner-manager/config"
"runner-manager/runner/providers/common"
"time"
uuid "github.com/satori/go.uuid"
@ -42,12 +43,15 @@ type Pool struct {
OSType config.OSType
OSArch config.OSArch
Tags []*Tag `gorm:"many2many:pool_tags;"`
Enabled bool
RepoID uuid.UUID
Repository Repository `gorm:"foreignKey:RepoID"`
OrgID uuid.UUID
Organization Organization `gorm:"foreignKey:OrgID"`
Instances []Instance `gorm:"foreignKey:PoolID"`
}
type Repository struct {
@ -77,16 +81,17 @@ type Address struct {
type Instance struct {
Base
Name string `gorm:"uniqueIndex"`
OSType config.OSType
OSArch config.OSArch
OSName string
OSVersion string
Addresses []Address `gorm:"foreignKey:id"`
Status string
RunnerStatus string
CallbackURL string
CallbackToken string
ProviderID string `gorm:"uniqueIndex"`
Name string `gorm:"uniqueIndex"`
OSType config.OSType
OSArch config.OSArch
OSName string
OSVersion string
Addresses []Address `gorm:"foreignKey:id"`
Status common.InstanceStatus
RunnerStatus common.RunnerStatus
CallbackURL string
Pool Pool `gorm:"foreignKey:id"`
PoolID uuid.UUID
Pool Pool `gorm:"foreignKey:PoolID"`
}

View file

@ -72,6 +72,7 @@ func (s *sqlDatabase) sqlToCommonPool(pool Pool) params.Pool {
Flavor: pool.Flavor,
OSArch: pool.OSArch,
OSType: pool.OSType,
Enabled: pool.Enabled,
Tags: make([]params.Tag, len(pool.Tags)),
}
@ -135,7 +136,10 @@ func (s *sqlDatabase) CreateRepository(ctx context.Context, owner, name, webhook
func (s *sqlDatabase) getRepo(ctx context.Context, owner, name string) (Repository, error) {
var repo Repository
q := s.conn.Preload(clause.Associations).Where("name = ? and owner = ?", name, owner).First(&repo)
q := s.conn.Preload(clause.Associations).
Where("name = ? and owner = ?", name, owner).
First(&repo)
if q.Error != nil {
if errors.Is(q.Error, gorm.ErrRecordNotFound) {
return Repository{}, runnerErrors.ErrNotFound
@ -151,7 +155,10 @@ func (s *sqlDatabase) getRepoByID(ctx context.Context, id string) (Repository, e
return Repository{}, errors.Wrap(runnerErrors.NewBadRequestError(""), "parsing id")
}
var repo Repository
q := s.conn.Preload(clause.Associations).Where("id = ?", u).First(&repo)
q := s.conn.Preload(clause.Associations).
Where("id = ?", u).
First(&repo)
if q.Error != nil {
if errors.Is(q.Error, gorm.ErrRecordNotFound) {
return Repository{}, runnerErrors.ErrNotFound
@ -349,6 +356,7 @@ func (s *sqlDatabase) CreateRepositoryPool(ctx context.Context, repoId string, p
OSType: param.OSType,
OSArch: param.OSArch,
RepoID: repo.ID,
Enabled: param.Enabled,
}
tags := []Tag{}
@ -357,9 +365,7 @@ func (s *sqlDatabase) CreateRepositoryPool(ctx context.Context, repoId string, p
if err != nil {
return params.Pool{}, errors.Wrap(err, "fetching tag")
}
fmt.Printf(">>>> Tag name: %s --> ID: %s\n", t.Name, t.ID)
tags = append(tags, t)
// newPool.Tags = append(newPool.Tags, &t)
}
q := s.conn.Create(&newPool)
@ -396,6 +402,7 @@ func (s *sqlDatabase) CreateOrganizationPool(ctx context.Context, orgId string,
Flavor: param.Flavor,
OSType: param.OSType,
OSArch: param.OSArch,
Enabled: param.Enabled,
}
tags := make([]*Tag, len(param.Tags))
@ -454,7 +461,10 @@ func (s *sqlDatabase) getOrgPool(ctx context.Context, orgID, poolID string) (Poo
return Pool{}, fmt.Errorf("invalid pool id")
}
var pool []Pool
err = s.conn.Model(&org).Association("Pools").Find(&pool, "id = ?", u)
err = s.conn.Model(&org).
Association(clause.Associations).
Find(&pool, "id = ?", u)
if err != nil {
return Pool{}, errors.Wrap(err, "fetching pool")
}
@ -465,6 +475,25 @@ func (s *sqlDatabase) getOrgPool(ctx context.Context, orgID, poolID string) (Poo
return pool[0], nil
}
func (s *sqlDatabase) getPoolByID(ctx context.Context, poolID string) (Pool, error) {
u := uuid.Parse(poolID)
if u == nil {
return Pool{}, errors.Wrap(runnerErrors.ErrBadRequest, "parsing id")
}
var pool Pool
q := s.conn.Model(&Pool{}).
Preload(clause.Associations).
Where("id = ?", u).First(&pool)
if q.Error != nil {
if errors.Is(q.Error, gorm.ErrRecordNotFound) {
return Pool{}, runnerErrors.ErrNotFound
}
return Pool{}, errors.Wrap(q.Error, "fetching org from database")
}
return pool, nil
}
func (s *sqlDatabase) GetOrganizationPool(ctx context.Context, orgID, poolID string) (params.Pool, error) {
pool, err := s.getOrgPool(ctx, orgID, poolID)
if err != nil {
@ -503,14 +532,6 @@ func (s *sqlDatabase) DeleteOrganizationPool(ctx context.Context, orgID, poolID
return nil
}
func (s *sqlDatabase) UpdateRepositoryPool(ctx context.Context, repoID, poolID string, param params.UpdatePoolParams) (params.Pool, error) {
return params.Pool{}, nil
}
func (s *sqlDatabase) UpdateOrganizationPool(ctx context.Context, orgID, poolID string, param params.UpdatePoolParams) (params.Pool, error) {
return params.Pool{}, nil
}
func (s *sqlDatabase) findPoolByTags(id, poolType string, tags []string) (params.Pool, error) {
if len(tags) == 0 {
return params.Pool{}, runnerErrors.NewBadRequestError("missing tags")
@ -555,34 +576,213 @@ func (s *sqlDatabase) FindOrganizationPoolByTags(ctx context.Context, orgID stri
return pool, nil
}
func (s *sqlDatabase) CreateInstance(ctx context.Context, poolID string, param params.CreateInstanceParams) (params.Instance, error) {
return params.Instance{}, nil
func (s *sqlDatabase) sqlAddressToParamsAddress(addr Address) params.Address {
return params.Address{
Address: addr.Address,
Type: params.AddressType(addr.Type),
}
}
func (s *sqlDatabase) DeleteInstance(ctx context.Context, poolID string, instanceID string) error {
func (s *sqlDatabase) sqlToParamsInstance(instance Instance) params.Instance {
ret := params.Instance{
ID: instance.ID.String(),
ProviderID: instance.ProviderID,
Name: instance.Name,
OSType: instance.OSType,
OSName: instance.OSName,
OSVersion: instance.OSVersion,
OSArch: instance.OSArch,
Status: instance.Status,
RunnerStatus: instance.RunnerStatus,
PoolID: instance.Pool.ID.String(),
CallbackURL: instance.CallbackURL,
}
for _, addr := range instance.Addresses {
ret.Addresses = append(ret.Addresses, s.sqlAddressToParamsAddress(addr))
}
return ret
}
func (s *sqlDatabase) CreateInstance(ctx context.Context, poolID string, param params.CreateInstanceParams) (params.Instance, error) {
pool, err := s.getPoolByID(ctx, param.Pool)
if err != nil {
return params.Instance{}, errors.Wrap(err, "fetching pool")
}
newInstance := Instance{
Pool: pool,
Name: param.Name,
Status: param.Status,
RunnerStatus: param.RunnerStatus,
OSType: param.OSType,
OSArch: param.OSArch,
CallbackURL: param.CallbackURL,
}
q := s.conn.Create(&newInstance)
if q.Error != nil {
return params.Instance{}, errors.Wrap(q.Error, "creating repository")
}
return s.sqlToParamsInstance(newInstance), nil
}
// func (s *sqlDatabase) GetInstance(ctx context.Context, poolID string, instanceID string) (params.Instance, error) {
// return params.Instance{}, nil
// }
func (s *sqlDatabase) getInstanceByID(ctx context.Context, instanceID string) (Instance, error) {
u := uuid.Parse(instanceID)
if u == nil {
return Instance{}, errors.Wrap(runnerErrors.ErrBadRequest, "parsing id")
}
var instance Instance
q := s.conn.Model(&Instance{}).
Preload(clause.Associations).
Where("id = ?", u).
First(&instance)
if q.Error != nil {
return Instance{}, errors.Wrap(q.Error, "fetching instance")
}
return instance, nil
}
func (s *sqlDatabase) getInstanceByName(ctx context.Context, poolID string, instanceName string) (Instance, error) {
pool, err := s.getPoolByID(ctx, poolID)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return Instance{}, errors.Wrap(runnerErrors.ErrNotFound, "fetching instance")
}
return Instance{}, errors.Wrap(err, "fetching pool")
}
var instance Instance
q := s.conn.Model(&Instance{}).
Preload(clause.Associations).
Where("name = ? and pool_id = ?", instanceName, pool.ID).
First(&instance)
if q.Error != nil {
return Instance{}, errors.Wrap(q.Error, "fetching instance")
}
return instance, nil
}
func (s *sqlDatabase) GetInstanceByName(ctx context.Context, poolID string, instanceName string) (params.Instance, error) {
instance, err := s.getInstanceByName(ctx, poolID, instanceName)
if err != nil {
return params.Instance{}, errors.Wrap(err, "fetching instance")
}
return s.sqlToParamsInstance(instance), nil
}
func (s *sqlDatabase) DeleteInstance(ctx context.Context, poolID string, instanceName string) error {
instance, err := s.getInstanceByName(ctx, poolID, instanceName)
if err != nil {
if errors.Is(err, runnerErrors.ErrNotFound) {
return nil
}
return errors.Wrap(err, "deleting instance")
}
if q := s.conn.Delete(&instance); q.Error != nil {
if errors.Is(q.Error, gorm.ErrRecordNotFound) {
return nil
}
return errors.Wrap(q.Error, "deleting instance")
}
return nil
}
func (s *sqlDatabase) UpdateInstance(ctx context.Context, instanceID string, param params.UpdateInstanceParams) (params.Instance, error) {
return params.Instance{}, nil
instance, err := s.getInstanceByID(ctx, instanceID)
if err != nil {
return params.Instance{}, errors.Wrap(err, "updating instance")
}
if param.ProviderID != "" {
instance.ProviderID = param.ProviderID
}
if param.OSName != "" {
instance.OSName = param.OSName
}
if param.OSVersion != "" {
instance.OSVersion = param.OSVersion
}
if string(param.RunnerStatus) != "" {
instance.RunnerStatus = param.RunnerStatus
}
if string(param.Status) != "" {
instance.Status = param.Status
}
q := s.conn.Save(&instance)
if q.Error != nil {
return params.Instance{}, errors.Wrap(q.Error, "updating instance")
}
if len(param.Addresses) > 0 {
addrs := []Address{}
for _, addr := range param.Addresses {
addrs = append(addrs, Address{
Address: addr.Address,
Type: string(addr.Type),
})
}
if err := s.conn.Model(&instance).Association("Addresses").Replace(addrs); err != nil {
return params.Instance{}, errors.Wrap(err, "updating addresses")
}
}
return s.sqlToParamsInstance(instance), nil
}
func (s *sqlDatabase) ListInstances(ctx context.Context, poolID string) ([]params.Instance, error) {
return []params.Instance{}, nil
pool, err := s.getPoolByID(ctx, poolID)
if err != nil {
return nil, errors.Wrap(err, "fetching pool")
}
ret := make([]params.Instance, len(pool.Instances))
for idx, inst := range pool.Instances {
ret[idx] = s.sqlToParamsInstance(inst)
}
return ret, nil
}
func (s *sqlDatabase) ListRepoInstances(ctx context.Context, repoID string) ([]params.Instance, error) {
return []params.Instance{}, nil
repo, err := s.getRepoByID(ctx, repoID)
if err != nil {
return nil, errors.Wrap(err, "fetching repo")
}
ret := []params.Instance{}
for _, pool := range repo.Pools {
for _, instance := range pool.Instances {
ret = append(ret, s.sqlToParamsInstance(instance))
}
}
return ret, nil
}
func (s *sqlDatabase) ListOrgInstances(ctx context.Context, orgID string) ([]params.Instance, error) {
return []params.Instance{}, nil
org, err := s.getOrgByID(ctx, orgID)
if err != nil {
return nil, errors.Wrap(err, "fetching org")
}
ret := []params.Instance{}
for _, pool := range org.Pools {
for _, instance := range pool.Instances {
ret = append(ret, s.sqlToParamsInstance(instance))
}
}
return ret, nil
}
func (s *sqlDatabase) GetInstance(ctx context.Context, poolID string, instanceID string) (params.Instance, error) {
return params.Instance{}, nil
func (s *sqlDatabase) UpdateRepositoryPool(ctx context.Context, repoID, poolID string, param params.UpdatePoolParams) (params.Pool, error) {
return params.Pool{}, nil
}
func (s *sqlDatabase) GetInstanceByName(ctx context.Context, instanceName string) (params.Instance, error) {
return params.Instance{}, nil
func (s *sqlDatabase) UpdateOrganizationPool(ctx context.Context, orgID, poolID string, param params.UpdatePoolParams) (params.Pool, error) {
return params.Pool{}, nil
}

2
go.mod
View file

@ -4,11 +4,13 @@ go 1.18
require (
github.com/BurntSushi/toml v0.3.1
github.com/golang-jwt/jwt v3.2.2+incompatible
github.com/google/go-github/v43 v43.0.0
github.com/google/uuid v1.3.0
github.com/gorilla/handlers v1.5.1
github.com/gorilla/mux v1.8.0
github.com/lxc/lxd v0.0.0-20220415052741-1170f2806124
github.com/nbutton23/zxcvbn-go v0.0.0-20210217022336-fa2cb2858354
github.com/pborman/uuid v1.2.1
github.com/pkg/errors v0.9.1
github.com/satori/go.uuid v1.2.0

5
go.sum
View file

@ -24,6 +24,8 @@ github.com/go-macaroon-bakery/macaroonpb v1.0.0 h1:It9exBaRMZ9iix1iJ6gwzfwsDE6Ex
github.com/go-macaroon-bakery/macaroonpb v1.0.0/go.mod h1:UzrGOcbiwTXISFP2XDLDPjfhMINZa+fX/7A2lMd31zc=
github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE=
github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY=
github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
@ -97,6 +99,8 @@ github.com/lxc/lxd v0.0.0-20220415052741-1170f2806124 h1:EmjWCASxSUz+ymsEJfiWN3y
github.com/lxc/lxd v0.0.0-20220415052741-1170f2806124/go.mod h1:T4xjj62BmFg1L5JfY2wRyPZtKbBeTFgo/GLwV8DFZ8M=
github.com/mattn/go-sqlite3 v1.14.12 h1:TJ1bhYJPV44phC+IMu1u2K/i5RriLTPe+yc68XDJ1Z0=
github.com/mattn/go-sqlite3 v1.14.12/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
github.com/nbutton23/zxcvbn-go v0.0.0-20210217022336-fa2cb2858354 h1:4kuARK6Y6FxaNu/BnU2OAaLF86eTVhP2hjTB6iMvItA=
github.com/nbutton23/zxcvbn-go v0.0.0-20210217022336-fa2cb2858354/go.mod h1:KSVJerMDfblTH7p5MZaTt+8zaT2iEk3AkVb9PQdZuE8=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
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=
@ -118,6 +122,7 @@ github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdh
github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE=
github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.1.4/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY=

View file

@ -63,8 +63,7 @@ type Instance struct {
PoolID string `json:"pool_id"`
// Do not serialize sensitive info.
CallbackURL string `json:"-"`
CallbackToken string `json:"-"`
CallbackURL string `json:"-"`
}
type BootstrapInstance struct {
@ -107,12 +106,14 @@ type Pool struct {
OSType config.OSType `json:"os_type"`
OSArch config.OSArch `json:"os_arch"`
Tags []Tag `json:"tags"`
Enabled bool `json:"enabled"`
}
type Internal struct {
OAuth2Token string `json:"oauth2"`
ControllerID string `json:"controller_id"`
InstanceCallbackURL string `json:"instance_callback_url"`
JWTSecret string `json:"jwt_secret"`
}
type Repository struct {
@ -143,33 +144,39 @@ type CreatePoolParams struct {
OSType config.OSType `json:"os_type"`
OSArch config.OSArch `json:"os_arch"`
Tags []string `json:"tags"`
Enabled bool `json:"enabled"`
}
/*
Name string `gorm:"uniqueIndex"`
OSType config.OSType
OSArch config.OSArch
OSName string
OSVersion string
Addresses []Address `gorm:"foreignKey:id"`
Status string
RunnerStatus string
CallbackURL string
CallbackToken []byte
Pool Pool `gorm:"foreignKey:id"`
*/
type CreateInstanceParams struct {
Name string
OSType config.OSType
OSArch config.OSArch
Status common.InstanceStatus
RunnerStatus common.RunnerStatus
CallbackURL string
CallbackToken string
Name string
OSType config.OSType
OSArch config.OSArch
Status common.InstanceStatus
RunnerStatus common.RunnerStatus
CallbackURL string
Pool string
}
type UpdatePoolParams struct{}
/*
type Pool struct {
ID string `json:"id"`
ProviderName string `json:"provider_name"`
MaxRunners uint `json:"max_runners"`
MinIdleRunners uint `json:"min_idle_runners"`
Image string `json:"image"`
Flavor string `json:"flavor"`
OSType config.OSType `json:"os_type"`
OSArch config.OSArch `json:"os_arch"`
Tags []Tag `json:"tags"`
Enabled bool `json:"enabled"`
}
*/
type UpdatePoolParams struct {
Tags []Tag `json:"tags"`
Enabled bool `json:"enabled"`
MaxRunners uint `json:"max_runners"`
MinIdleRunners uint `json:"min_idle_runners"`
Image string `json:"image"`
Flavor string `json:"flavor"`
}

View file

@ -2,6 +2,13 @@ package common
import "runner-manager/params"
type PoolType string
const (
RepositoryPool PoolType = "repository"
OrganizationPool PoolType = "organization"
)
type PoolManager interface {
WebhookSecret() string
HandleWorkflowJob(job params.WorkflowJob) error

View file

@ -8,6 +8,7 @@ import (
"sync"
"time"
"runner-manager/auth"
"runner-manager/config"
dbCommon "runner-manager/database/common"
runnerErrors "runner-manager/errors"
@ -145,6 +146,7 @@ func (r *Repository) consolidate() {
}
func (r *Repository) addPendingInstances() {
// TODO: filter instances by status.
instances, err := r.store.ListRepoInstances(r.ctx, r.id)
if err != nil {
log.Printf("failed to fetch instances from store: %s", err)
@ -205,8 +207,6 @@ func (r *Repository) cleanupOrphanedGithubRunners(runners []*github.Runner) erro
}
removeRunner := false
// check locally and delete
// dbInstance, err := r.store.GetInstance()
poolID, err := r.poolIDFromLabels(runner.Labels)
if err != nil {
if !errors.Is(err, runnerErrors.ErrNotFound) {
@ -222,7 +222,7 @@ func (r *Repository) cleanupOrphanedGithubRunners(runners []*github.Runner) erro
continue
}
dbInstance, err := r.store.GetInstance(r.ctx, poolID, *runner.Name)
dbInstance, err := r.store.GetInstanceByName(r.ctx, poolID, *runner.Name)
if err != nil {
if !errors.Is(err, runnerErrors.ErrNotFound) {
return errors.Wrap(err, "fetching instance from DB")
@ -241,33 +241,24 @@ func (r *Repository) cleanupOrphanedGithubRunners(runners []*github.Runner) erro
return fmt.Errorf("unknown provider %s for pool %s", pool.ProviderName, pool.ID)
}
instance, err := provider.GetInstance(r.ctx, dbInstance.Name)
if err != nil {
if !errors.Is(err, runnerErrors.ErrNotFound) {
return errors.Wrap(err, "fetching instance from provider")
}
// instance was manually deleted?
removeRunner = true
} else {
if providerCommon.InstanceStatus(instance.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", instance.Name)
continue
}
//start the instance
if err := provider.Start(r.ctx, instance.ProviderID); err != nil {
return errors.Wrapf(err, "starting instance %s", instance.ProviderID)
}
// we started the instance. Give it a chance to come online
if providerCommon.InstanceStatus(dbInstance.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)
}
// we started the instance. Give it a chance to come online
continue
}
if removeRunner {
if _, err := r.ghcli.Actions.RemoveRunner(r.ctx, r.cfg.Owner, r.cfg.Name, *runner.ID); err != nil {
return errors.Wrap(err, "removing runner")
}
if removeRunner {
if _, err := r.ghcli.Actions.RemoveRunner(r.ctx, r.cfg.Owner, r.cfg.Name, *runner.ID); err != nil {
return errors.Wrap(err, "removing runner")
}
}
}
@ -306,7 +297,7 @@ func (r *Repository) cleanupOrphanedProviderRunners(runners []*github.Runner) er
}
if ok := runnerNames[instance.Name]; !ok {
// Set pending_delete on DB field. Allow consolidate() to remove it.
_, err = r.store.UpdateInstance(r.ctx, instance.Name, params.UpdateInstanceParams{})
_, err = r.store.UpdateInstance(r.ctx, instance.ID, params.UpdateInstanceParams{})
if err != nil {
return errors.Wrap(err, "syncing local state with github")
}
@ -430,12 +421,18 @@ func (r *Repository) addInstanceToProvider(instance params.Instance) error {
return errors.Wrap(err, "creating runner token")
}
entity := fmt.Sprintf("%s/%s", r.cfg.Owner, r.cfg.Name)
jwtToken, err := auth.NewInstanceJWTToken(instance, r.cfg.Internal.JWTSecret, entity, common.RepositoryPool)
if err != nil {
return errors.Wrap(err, "fetching instance jwt token")
}
bootstrapArgs := params.BootstrapInstance{
Tools: r.tools,
RepoURL: r.githubURL(),
GithubRunnerAccessToken: *tk.Token,
CallbackURL: instance.CallbackURL,
InstanceToken: instance.CallbackToken,
InstanceToken: jwtToken,
OSArch: pool.OSArch,
Flavor: pool.Flavor,
Image: pool.Image,
@ -457,11 +454,6 @@ func (r *Repository) addInstanceToProvider(instance params.Instance) error {
// TODO: add function to set runner status to idle when instance calls home on callback url
func (r *Repository) AddRunner(ctx context.Context, poolID string) error {
callbackToken, err := util.GetRandomString(32)
if err != nil {
return errors.Wrap(err, "fetching callbackToken")
}
pool, ok := r.pools[poolID]
if !ok {
return runnerErrors.NewNotFoundError("invalid provider ID")
@ -470,17 +462,16 @@ func (r *Repository) AddRunner(ctx context.Context, poolID string) error {
name := fmt.Sprintf("runner-manager-%s", uuid.New())
createParams := params.CreateInstanceParams{
Name: name,
Pool: poolID,
Status: providerCommon.InstancePendingCreate,
RunnerStatus: providerCommon.RunnerPending,
OSArch: pool.OSArch,
OSType: pool.OSType,
CallbackToken: callbackToken,
CallbackURL: r.cfg.Internal.InstanceCallbackURL,
Name: name,
Pool: poolID,
Status: providerCommon.InstancePendingCreate,
RunnerStatus: providerCommon.RunnerPending,
OSArch: pool.OSArch,
OSType: pool.OSType,
CallbackURL: r.cfg.Internal.InstanceCallbackURL,
}
_, err = r.store.CreateInstance(r.ctx, poolID, createParams)
_, err := r.store.CreateInstance(r.ctx, poolID, createParams)
if err != nil {
return errors.Wrap(err, "creating instance")
}

View file

@ -10,7 +10,9 @@ const (
InstancePendingCreate InstanceStatus = "pending_create"
InstanceStatusUnknown InstanceStatus = "unknown"
RunnerIdle RunnerStatus = "idle"
RunnerPending RunnerStatus = "pending"
RunnerActive RunnerStatus = "active"
RunnerIdle RunnerStatus = "idle"
RunnerPending RunnerStatus = "pending"
RunnerInstalling RunnerStatus = "installing"
RunnerFailed RunnerStatus = "failed"
RunnerActive RunnerStatus = "active"
)

View file

@ -24,16 +24,15 @@ import (
"runner-manager/runner/providers"
"runner-manager/util"
"github.com/google/go-github/v43/github"
"github.com/pkg/errors"
"golang.org/x/crypto/ssh"
)
func NewRunner(ctx context.Context, cfg config.Config) (*Runner, error) {
ghc, err := util.GithubClient(ctx, cfg.Github.OAuth2Token)
if err != nil {
return nil, errors.Wrap(err, "getting github client")
}
// ghc, err := util.GithubClient(ctx, cfg.Github.OAuth2Token)
// if err != nil {
// return nil, errors.Wrap(err, "getting github client")
// }
providers, err := providers.LoadProvidersFromConfig(ctx, cfg, "")
if err != nil {
@ -45,10 +44,10 @@ func NewRunner(ctx context.Context, cfg config.Config) (*Runner, error) {
}
runner := &Runner{
ctx: ctx,
config: cfg,
db: db,
ghc: ghc,
ctx: ctx,
config: cfg,
db: db,
// ghc: ghc,
providers: providers,
}
@ -63,8 +62,8 @@ type Runner struct {
mux sync.Mutex
ctx context.Context
ghc *github.Client
db dbCommon.Store
// ghc *github.Client
db dbCommon.Store
controllerID string
@ -190,7 +189,7 @@ func (r *Runner) DispatchWorkflowJob(hookTargetType, signature string, jobData [
}
func (r *Runner) sshDir() string {
return filepath.Join(r.config.ConfigDir, "ssh")
return filepath.Join(r.config.Default.ConfigDir, "ssh")
}
func (r *Runner) sshKeyPath() string {

View file

@ -147,6 +147,8 @@ func GetCloudConfig(bootstrapParams params.BootstrapInstance, tools github.Runne
RepoURL: bootstrapParams.RepoURL,
RunnerName: runnerName,
RunnerLabels: strings.Join(bootstrapParams.Labels, ","),
CallbackURL: bootstrapParams.CallbackURL,
CallbackToken: bootstrapParams.InstanceToken,
}
installScript, err := cloudconfig.InstallRunnerScript(installRunnerParams)