355 lines · 10.7 KB · Go
RawDownload
// 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 "forgejo.org/models/auth"	"forgejo.org/models/db"	repo_model "forgejo.org/models/repo"	"forgejo.org/models/shared/types"	user_model "forgejo.org/models/user"	"forgejo.org/modules/optional"	"forgejo.org/modules/timeutil"	"forgejo.org/modules/translation"	"forgejo.org/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 loadedfunc (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 {		switch r.Owner.Type {		case user_model.UserTypeOrganization:			return types.OwnerTypeOrganization		case user_model.UserTypeIndividual:			return types.OwnerTypeIndividual		}	}	return types.OwnerTypeSystemGlobal} // if the logic here changed, you should also modify FindRunnerOptions.ToCondfunc (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 userfunc (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 runnerfunc (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 {	salt := hex.EncodeToString(util.CryptoRandomBytes(16)) 	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 uuidfunc 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 idfunc 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)	r.Name, _ = util.SplitStringAtByteN(r.Name, 255)	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, r *ActionRunner) error {	// 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(r.ID))	r.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, r, "UUID")	if err != nil {		return err	} 	_, err = db.DeleteByID[ActionRunner](ctx, r.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	}	t.Name, _ = util.SplitStringAtByteN(t.Name, 255)	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()}