diff --git a/auth/jwt.go b/auth/jwt.go new file mode 100644 index 00000000..932eb7ae --- /dev/null +++ b/auth/jwt.go @@ -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 +} diff --git a/cloudconfig/templates.go b/cloudconfig/templates.go index f0b2d195..a0d38351 100644 --- a/cloudconfig/templates.go +++ b/cloudconfig/templates.go @@ -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) { diff --git a/cmd/runner-manager/dbcreate.go b/cmd/runner-manager/dbcreate.go index 2e28da7a..8a7b6b3a 100644 --- a/cmd/runner-manager/dbcreate.go +++ b/cmd/runner-manager/dbcreate.go @@ -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) } diff --git a/config/config.go b/config/config.go index a96512cb..2d0cf5b7 100644 --- a/config/config.go +++ b/config/config.go @@ -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"` } diff --git a/database/common/common.go b/database/common/common.go index d344e891..5e294073 100644 --- a/database/common/common.go +++ b/database/common/common.go @@ -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) } diff --git a/database/sql/models.go b/database/sql/models.go index 073dcccf..6a87973f 100644 --- a/database/sql/models.go +++ b/database/sql/models.go @@ -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"` } diff --git a/database/sql/sql.go b/database/sql/sql.go index 77c1e2cf..6dd95d27 100644 --- a/database/sql/sql.go +++ b/database/sql/sql.go @@ -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 } diff --git a/go.mod b/go.mod index 2f6f651e..3d642309 100644 --- a/go.mod +++ b/go.mod @@ -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 diff --git a/go.sum b/go.sum index b2133731..8085af31 100644 --- a/go.sum +++ b/go.sum @@ -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= diff --git a/params/params.go b/params/params.go index 5d733245..8b0e11ce 100644 --- a/params/params.go +++ b/params/params.go @@ -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"` +} diff --git a/runner/common/pool.go b/runner/common/pool.go index 2ac301ad..26f1c337 100644 --- a/runner/common/pool.go +++ b/runner/common/pool.go @@ -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 diff --git a/runner/pool/repository.go b/runner/pool/repository.go index 9b630abe..8ec1a246 100644 --- a/runner/pool/repository.go +++ b/runner/pool/repository.go @@ -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") } diff --git a/runner/providers/common/common.go b/runner/providers/common/common.go index 201551f6..90804a38 100644 --- a/runner/providers/common/common.go +++ b/runner/providers/common/common.go @@ -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" ) diff --git a/runner/runner.go b/runner/runner.go index 9906cb2d..93c63a98 100644 --- a/runner/runner.go +++ b/runner/runner.go @@ -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 { diff --git a/util/util.go b/util/util.go index 81e49c87..8c2be2e9 100644 --- a/util/util.go +++ b/util/util.go @@ -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)