diff options
Diffstat (limited to 'models/actions/runner.go')
-rw-r--r-- | models/actions/runner.go | 362 |
1 files changed, 362 insertions, 0 deletions
diff --git a/models/actions/runner.go b/models/actions/runner.go new file mode 100644 index 0000000..175f211 --- /dev/null +++ b/models/actions/runner.go @@ -0,0 +1,362 @@ +// Copyright 2021 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package actions + +import ( + "context" + "encoding/binary" + "encoding/hex" + "fmt" + "strings" + "time" + + auth_model "code.gitea.io/gitea/models/auth" + "code.gitea.io/gitea/models/db" + repo_model "code.gitea.io/gitea/models/repo" + "code.gitea.io/gitea/models/shared/types" + user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/optional" + "code.gitea.io/gitea/modules/timeutil" + "code.gitea.io/gitea/modules/translation" + "code.gitea.io/gitea/modules/util" + + runnerv1 "code.gitea.io/actions-proto-go/runner/v1" + "xorm.io/builder" +) + +// ActionRunner represents runner machines +// +// It can be: +// 1. global runner, OwnerID is 0 and RepoID is 0 +// 2. org/user level runner, OwnerID is org/user ID and RepoID is 0 +// 3. repo level runner, OwnerID is 0 and RepoID is repo ID +// +// Please note that it's not acceptable to have both OwnerID and RepoID to be non-zero, +// or it will be complicated to find runners belonging to a specific owner. +// For example, conditions like `OwnerID = 1` will also return runner {OwnerID: 1, RepoID: 1}, +// but it's a repo level runner, not an org/user level runner. +// To avoid this, make it clear with {OwnerID: 0, RepoID: 1} for repo level runners. +type ActionRunner struct { + ID int64 + UUID string `xorm:"CHAR(36) UNIQUE"` + Name string `xorm:"VARCHAR(255)"` + Version string `xorm:"VARCHAR(64)"` + OwnerID int64 `xorm:"index"` + Owner *user_model.User `xorm:"-"` + RepoID int64 `xorm:"index"` + Repo *repo_model.Repository `xorm:"-"` + Description string `xorm:"TEXT"` + Base int // 0 native 1 docker 2 virtual machine + RepoRange string // glob match which repositories could use this runner + + Token string `xorm:"-"` + TokenHash string `xorm:"UNIQUE"` // sha256 of token + TokenSalt string + // TokenLastEight string `xorm:"token_last_eight"` // it's unnecessary because we don't find runners by token + + LastOnline timeutil.TimeStamp `xorm:"index"` + LastActive timeutil.TimeStamp `xorm:"index"` + + // Store labels defined in state file (default: .runner file) of `act_runner` + AgentLabels []string `xorm:"TEXT"` + + Created timeutil.TimeStamp `xorm:"created"` + Updated timeutil.TimeStamp `xorm:"updated"` + Deleted timeutil.TimeStamp `xorm:"deleted"` +} + +const ( + RunnerOfflineTime = time.Minute + RunnerIdleTime = 10 * time.Second +) + +// BelongsToOwnerName before calling, should guarantee that all attributes are loaded +func (r *ActionRunner) BelongsToOwnerName() string { + if r.RepoID != 0 { + return r.Repo.FullName() + } + if r.OwnerID != 0 { + return r.Owner.Name + } + return "" +} + +func (r *ActionRunner) BelongsToOwnerType() types.OwnerType { + if r.RepoID != 0 { + return types.OwnerTypeRepository + } + if r.OwnerID != 0 { + if r.Owner.Type == user_model.UserTypeOrganization { + return types.OwnerTypeOrganization + } else if r.Owner.Type == user_model.UserTypeIndividual { + return types.OwnerTypeIndividual + } + } + return types.OwnerTypeSystemGlobal +} + +// if the logic here changed, you should also modify FindRunnerOptions.ToCond +func (r *ActionRunner) Status() runnerv1.RunnerStatus { + if time.Since(r.LastOnline.AsTime()) > RunnerOfflineTime { + return runnerv1.RunnerStatus_RUNNER_STATUS_OFFLINE + } + if time.Since(r.LastActive.AsTime()) > RunnerIdleTime { + return runnerv1.RunnerStatus_RUNNER_STATUS_IDLE + } + return runnerv1.RunnerStatus_RUNNER_STATUS_ACTIVE +} + +func (r *ActionRunner) StatusName() string { + return strings.ToLower(strings.TrimPrefix(r.Status().String(), "RUNNER_STATUS_")) +} + +func (r *ActionRunner) StatusLocaleName(lang translation.Locale) string { + return lang.TrString("actions.runners.status." + r.StatusName()) +} + +func (r *ActionRunner) IsOnline() bool { + status := r.Status() + if status == runnerv1.RunnerStatus_RUNNER_STATUS_IDLE || status == runnerv1.RunnerStatus_RUNNER_STATUS_ACTIVE { + return true + } + return false +} + +// Editable checks if the runner is editable by the user +func (r *ActionRunner) Editable(ownerID, repoID int64) bool { + if ownerID == 0 && repoID == 0 { + return true + } + if ownerID > 0 && r.OwnerID == ownerID { + return true + } + return repoID > 0 && r.RepoID == repoID +} + +// LoadAttributes loads the attributes of the runner +func (r *ActionRunner) LoadAttributes(ctx context.Context) error { + if r.OwnerID > 0 { + var user user_model.User + has, err := db.GetEngine(ctx).ID(r.OwnerID).Get(&user) + if err != nil { + return err + } + if has { + r.Owner = &user + } + } + if r.RepoID > 0 { + var repo repo_model.Repository + has, err := db.GetEngine(ctx).ID(r.RepoID).Get(&repo) + if err != nil { + return err + } + if has { + r.Repo = &repo + } + } + return nil +} + +func (r *ActionRunner) GenerateToken() (err error) { + r.Token, r.TokenSalt, r.TokenHash, _, err = generateSaltedToken() + return err +} + +// UpdateSecret updates the hash based on the specified token. It does not +// ensure that the runner's UUID matches the first 16 bytes of the token. +func (r *ActionRunner) UpdateSecret(token string) error { + saltBytes, err := util.CryptoRandomBytes(16) + if err != nil { + return fmt.Errorf("CryptoRandomBytes %v", err) + } + + salt := hex.EncodeToString(saltBytes) + + r.Token = token + r.TokenSalt = salt + r.TokenHash = auth_model.HashToken(token, salt) + return nil +} + +func init() { + db.RegisterModel(&ActionRunner{}) +} + +type FindRunnerOptions struct { + db.ListOptions + RepoID int64 + OwnerID int64 // it will be ignored if RepoID is set + Sort string + Filter string + IsOnline optional.Option[bool] + WithAvailable bool // not only runners belong to, but also runners can be used +} + +func (opts FindRunnerOptions) ToConds() builder.Cond { + cond := builder.NewCond() + + if opts.RepoID > 0 { + c := builder.NewCond().And(builder.Eq{"repo_id": opts.RepoID}) + if opts.WithAvailable { + c = c.Or(builder.Eq{"owner_id": builder.Select("owner_id").From("repository").Where(builder.Eq{"id": opts.RepoID})}) + c = c.Or(builder.Eq{"repo_id": 0, "owner_id": 0}) + } + cond = cond.And(c) + } else if opts.OwnerID > 0 { // OwnerID is ignored if RepoID is set + c := builder.NewCond().And(builder.Eq{"owner_id": opts.OwnerID}) + if opts.WithAvailable { + c = c.Or(builder.Eq{"repo_id": 0, "owner_id": 0}) + } + cond = cond.And(c) + } + + if opts.Filter != "" { + cond = cond.And(builder.Like{"name", opts.Filter}) + } + + if opts.IsOnline.Has() { + if opts.IsOnline.Value() { + cond = cond.And(builder.Gt{"last_online": time.Now().Add(-RunnerOfflineTime).Unix()}) + } else { + cond = cond.And(builder.Lte{"last_online": time.Now().Add(-RunnerOfflineTime).Unix()}) + } + } + return cond +} + +func (opts FindRunnerOptions) ToOrders() string { + switch opts.Sort { + case "online": + return "last_online DESC" + case "offline": + return "last_online ASC" + case "alphabetically": + return "name ASC" + case "reversealphabetically": + return "name DESC" + case "newest": + return "id DESC" + case "oldest": + return "id ASC" + } + return "last_online DESC" +} + +// GetRunnerByUUID returns a runner via uuid +func GetRunnerByUUID(ctx context.Context, uuid string) (*ActionRunner, error) { + var runner ActionRunner + has, err := db.GetEngine(ctx).Where("uuid=?", uuid).Get(&runner) + if err != nil { + return nil, err + } else if !has { + return nil, fmt.Errorf("runner with uuid %s: %w", uuid, util.ErrNotExist) + } + return &runner, nil +} + +// GetRunnerByID returns a runner via id +func GetRunnerByID(ctx context.Context, id int64) (*ActionRunner, error) { + var runner ActionRunner + has, err := db.GetEngine(ctx).Where("id=?", id).Get(&runner) + if err != nil { + return nil, err + } else if !has { + return nil, fmt.Errorf("runner with id %d: %w", id, util.ErrNotExist) + } + return &runner, nil +} + +// UpdateRunner updates runner's information. +func UpdateRunner(ctx context.Context, r *ActionRunner, cols ...string) error { + e := db.GetEngine(ctx) + var err error + if len(cols) == 0 { + _, err = e.ID(r.ID).AllCols().Update(r) + } else { + _, err = e.ID(r.ID).Cols(cols...).Update(r) + } + return err +} + +// DeleteRunner deletes a runner by given ID. +func DeleteRunner(ctx context.Context, id int64) error { + runner, err := GetRunnerByID(ctx, id) + if err != nil { + return err + } + + // Replace the UUID, which was either based on the secret's first 16 bytes or an UUIDv4, + // with a sequence of 8 0xff bytes followed by the little-endian version of the record's + // identifier. This will prevent the deleted record's identifier from colliding with any + // new record. + b := make([]byte, 8) + binary.LittleEndian.PutUint64(b, uint64(id)) + runner.UUID = fmt.Sprintf("ffffffff-ffff-ffff-%.2x%.2x-%.2x%.2x%.2x%.2x%.2x%.2x", + b[0], b[1], b[2], b[3], b[4], b[5], b[6], b[7]) + + err = UpdateRunner(ctx, runner, "UUID") + if err != nil { + return err + } + + _, err = db.DeleteByID[ActionRunner](ctx, id) + return err +} + +// CreateRunner creates new runner. +func CreateRunner(ctx context.Context, t *ActionRunner) error { + if t.OwnerID != 0 && t.RepoID != 0 { + // It's trying to create a runner that belongs to a repository, but OwnerID has been set accidentally. + // Remove OwnerID to avoid confusion; it's not worth returning an error here. + t.OwnerID = 0 + } + return db.Insert(ctx, t) +} + +func CountRunnersWithoutBelongingOwner(ctx context.Context) (int64, error) { + // Only affect action runners were a owner ID is set, as actions runners + // could also be created on a repository. + return db.GetEngine(ctx).Table("action_runner"). + Join("LEFT", "`user`", "`action_runner`.owner_id = `user`.id"). + Where("`action_runner`.owner_id != ?", 0). + And(builder.IsNull{"`user`.id"}). + Count(new(ActionRunner)) +} + +func FixRunnersWithoutBelongingOwner(ctx context.Context) (int64, error) { + subQuery := builder.Select("`action_runner`.id"). + From("`action_runner`"). + Join("LEFT", "`user`", "`action_runner`.owner_id = `user`.id"). + Where(builder.Neq{"`action_runner`.owner_id": 0}). + And(builder.IsNull{"`user`.id"}) + b := builder.Delete(builder.In("id", subQuery)).From("`action_runner`") + res, err := db.GetEngine(ctx).Exec(b) + if err != nil { + return 0, err + } + return res.RowsAffected() +} + +func CountRunnersWithoutBelongingRepo(ctx context.Context) (int64, error) { + return db.GetEngine(ctx).Table("action_runner"). + Join("LEFT", "`repository`", "`action_runner`.repo_id = `repository`.id"). + Where("`action_runner`.repo_id != ?", 0). + And(builder.IsNull{"`repository`.id"}). + Count(new(ActionRunner)) +} + +func FixRunnersWithoutBelongingRepo(ctx context.Context) (int64, error) { + subQuery := builder.Select("`action_runner`.id"). + From("`action_runner`"). + Join("LEFT", "`repository`", "`action_runner`.repo_id = `repository`.id"). + Where(builder.Neq{"`action_runner`.repo_id": 0}). + And(builder.IsNull{"`repository`.id"}) + b := builder.Delete(builder.In("id", subQuery)).From("`action_runner`") + res, err := db.GetEngine(ctx).Exec(b) + if err != nil { + return 0, err + } + return res.RowsAffected() +} |