code.gitea.io/gitea@v1.21.7/models/actions/runner.go (about)

     1  // Copyright 2021 The Gitea Authors. All rights reserved.
     2  // SPDX-License-Identifier: MIT
     3  
     4  package actions
     5  
     6  import (
     7  	"context"
     8  	"fmt"
     9  	"strings"
    10  	"time"
    11  
    12  	"code.gitea.io/gitea/models/db"
    13  	repo_model "code.gitea.io/gitea/models/repo"
    14  	"code.gitea.io/gitea/models/shared/types"
    15  	user_model "code.gitea.io/gitea/models/user"
    16  	"code.gitea.io/gitea/modules/timeutil"
    17  	"code.gitea.io/gitea/modules/translation"
    18  	"code.gitea.io/gitea/modules/util"
    19  
    20  	runnerv1 "code.gitea.io/actions-proto-go/runner/v1"
    21  	"xorm.io/builder"
    22  )
    23  
    24  // ActionRunner represents runner machines
    25  type ActionRunner struct {
    26  	ID          int64
    27  	UUID        string                 `xorm:"CHAR(36) UNIQUE"`
    28  	Name        string                 `xorm:"VARCHAR(255)"`
    29  	Version     string                 `xorm:"VARCHAR(64)"`
    30  	OwnerID     int64                  `xorm:"index"` // org level runner, 0 means system
    31  	Owner       *user_model.User       `xorm:"-"`
    32  	RepoID      int64                  `xorm:"index"` // repo level runner, if OwnerID also is zero, then it's a global
    33  	Repo        *repo_model.Repository `xorm:"-"`
    34  	Description string                 `xorm:"TEXT"`
    35  	Base        int                    // 0 native 1 docker 2 virtual machine
    36  	RepoRange   string                 // glob match which repositories could use this runner
    37  
    38  	Token     string `xorm:"-"`
    39  	TokenHash string `xorm:"UNIQUE"` // sha256 of token
    40  	TokenSalt string
    41  	// TokenLastEight string `xorm:"token_last_eight"` // it's unnecessary because we don't find runners by token
    42  
    43  	LastOnline timeutil.TimeStamp `xorm:"index"`
    44  	LastActive timeutil.TimeStamp `xorm:"index"`
    45  
    46  	// Store labels defined in state file (default: .runner file) of `act_runner`
    47  	AgentLabels []string `xorm:"TEXT"`
    48  
    49  	Created timeutil.TimeStamp `xorm:"created"`
    50  	Updated timeutil.TimeStamp `xorm:"updated"`
    51  	Deleted timeutil.TimeStamp `xorm:"deleted"`
    52  }
    53  
    54  const (
    55  	RunnerOfflineTime = time.Minute
    56  	RunnerIdleTime    = 10 * time.Second
    57  )
    58  
    59  // BelongsToOwnerName before calling, should guarantee that all attributes are loaded
    60  func (r *ActionRunner) BelongsToOwnerName() string {
    61  	if r.RepoID != 0 {
    62  		return r.Repo.FullName()
    63  	}
    64  	if r.OwnerID != 0 {
    65  		return r.Owner.Name
    66  	}
    67  	return ""
    68  }
    69  
    70  func (r *ActionRunner) BelongsToOwnerType() types.OwnerType {
    71  	if r.RepoID != 0 {
    72  		return types.OwnerTypeRepository
    73  	}
    74  	if r.OwnerID != 0 {
    75  		if r.Owner.Type == user_model.UserTypeOrganization {
    76  			return types.OwnerTypeOrganization
    77  		} else if r.Owner.Type == user_model.UserTypeIndividual {
    78  			return types.OwnerTypeIndividual
    79  		}
    80  	}
    81  	return types.OwnerTypeSystemGlobal
    82  }
    83  
    84  // if the logic here changed, you should also modify FindRunnerOptions.ToCond
    85  func (r *ActionRunner) Status() runnerv1.RunnerStatus {
    86  	if time.Since(r.LastOnline.AsTime()) > RunnerOfflineTime {
    87  		return runnerv1.RunnerStatus_RUNNER_STATUS_OFFLINE
    88  	}
    89  	if time.Since(r.LastActive.AsTime()) > RunnerIdleTime {
    90  		return runnerv1.RunnerStatus_RUNNER_STATUS_IDLE
    91  	}
    92  	return runnerv1.RunnerStatus_RUNNER_STATUS_ACTIVE
    93  }
    94  
    95  func (r *ActionRunner) StatusName() string {
    96  	return strings.ToLower(strings.TrimPrefix(r.Status().String(), "RUNNER_STATUS_"))
    97  }
    98  
    99  func (r *ActionRunner) StatusLocaleName(lang translation.Locale) string {
   100  	return lang.Tr("actions.runners.status." + r.StatusName())
   101  }
   102  
   103  func (r *ActionRunner) IsOnline() bool {
   104  	status := r.Status()
   105  	if status == runnerv1.RunnerStatus_RUNNER_STATUS_IDLE || status == runnerv1.RunnerStatus_RUNNER_STATUS_ACTIVE {
   106  		return true
   107  	}
   108  	return false
   109  }
   110  
   111  // Editable checks if the runner is editable by the user
   112  func (r *ActionRunner) Editable(ownerID, repoID int64) bool {
   113  	if ownerID == 0 && repoID == 0 {
   114  		return true
   115  	}
   116  	if ownerID > 0 && r.OwnerID == ownerID {
   117  		return true
   118  	}
   119  	return repoID > 0 && r.RepoID == repoID
   120  }
   121  
   122  // LoadAttributes loads the attributes of the runner
   123  func (r *ActionRunner) LoadAttributes(ctx context.Context) error {
   124  	if r.OwnerID > 0 {
   125  		var user user_model.User
   126  		has, err := db.GetEngine(ctx).ID(r.OwnerID).Get(&user)
   127  		if err != nil {
   128  			return err
   129  		}
   130  		if has {
   131  			r.Owner = &user
   132  		}
   133  	}
   134  	if r.RepoID > 0 {
   135  		var repo repo_model.Repository
   136  		has, err := db.GetEngine(ctx).ID(r.RepoID).Get(&repo)
   137  		if err != nil {
   138  			return err
   139  		}
   140  		if has {
   141  			r.Repo = &repo
   142  		}
   143  	}
   144  	return nil
   145  }
   146  
   147  func (r *ActionRunner) GenerateToken() (err error) {
   148  	r.Token, r.TokenSalt, r.TokenHash, _, err = generateSaltedToken()
   149  	return err
   150  }
   151  
   152  func init() {
   153  	db.RegisterModel(&ActionRunner{})
   154  }
   155  
   156  type FindRunnerOptions struct {
   157  	db.ListOptions
   158  	RepoID        int64
   159  	OwnerID       int64
   160  	Sort          string
   161  	Filter        string
   162  	IsOnline      util.OptionalBool
   163  	WithAvailable bool // not only runners belong to, but also runners can be used
   164  }
   165  
   166  func (opts FindRunnerOptions) toCond() builder.Cond {
   167  	cond := builder.NewCond()
   168  
   169  	if opts.RepoID > 0 {
   170  		c := builder.NewCond().And(builder.Eq{"repo_id": opts.RepoID})
   171  		if opts.WithAvailable {
   172  			c = c.Or(builder.Eq{"owner_id": builder.Select("owner_id").From("repository").Where(builder.Eq{"id": opts.RepoID})})
   173  			c = c.Or(builder.Eq{"repo_id": 0, "owner_id": 0})
   174  		}
   175  		cond = cond.And(c)
   176  	}
   177  	if opts.OwnerID > 0 {
   178  		c := builder.NewCond().And(builder.Eq{"owner_id": opts.OwnerID})
   179  		if opts.WithAvailable {
   180  			c = c.Or(builder.Eq{"repo_id": 0, "owner_id": 0})
   181  		}
   182  		cond = cond.And(c)
   183  	}
   184  
   185  	if opts.Filter != "" {
   186  		cond = cond.And(builder.Like{"name", opts.Filter})
   187  	}
   188  
   189  	if opts.IsOnline.IsTrue() {
   190  		cond = cond.And(builder.Gt{"last_online": time.Now().Add(-RunnerOfflineTime).Unix()})
   191  	} else if opts.IsOnline.IsFalse() {
   192  		cond = cond.And(builder.Lte{"last_online": time.Now().Add(-RunnerOfflineTime).Unix()})
   193  	}
   194  	return cond
   195  }
   196  
   197  func (opts FindRunnerOptions) toOrder() string {
   198  	switch opts.Sort {
   199  	case "online":
   200  		return "last_online DESC"
   201  	case "offline":
   202  		return "last_online ASC"
   203  	case "alphabetically":
   204  		return "name ASC"
   205  	case "reversealphabetically":
   206  		return "name DESC"
   207  	case "newest":
   208  		return "id DESC"
   209  	case "oldest":
   210  		return "id ASC"
   211  	}
   212  	return "last_online DESC"
   213  }
   214  
   215  func CountRunners(ctx context.Context, opts FindRunnerOptions) (int64, error) {
   216  	return db.GetEngine(ctx).
   217  		Where(opts.toCond()).
   218  		Count(ActionRunner{})
   219  }
   220  
   221  func FindRunners(ctx context.Context, opts FindRunnerOptions) (runners RunnerList, err error) {
   222  	sess := db.GetEngine(ctx).
   223  		Where(opts.toCond()).
   224  		OrderBy(opts.toOrder())
   225  	if opts.Page > 0 {
   226  		sess.Limit(opts.PageSize, (opts.Page-1)*opts.PageSize)
   227  	}
   228  	return runners, sess.Find(&runners)
   229  }
   230  
   231  // GetRunnerByUUID returns a runner via uuid
   232  func GetRunnerByUUID(ctx context.Context, uuid string) (*ActionRunner, error) {
   233  	var runner ActionRunner
   234  	has, err := db.GetEngine(ctx).Where("uuid=?", uuid).Get(&runner)
   235  	if err != nil {
   236  		return nil, err
   237  	} else if !has {
   238  		return nil, fmt.Errorf("runner with uuid %s: %w", uuid, util.ErrNotExist)
   239  	}
   240  	return &runner, nil
   241  }
   242  
   243  // GetRunnerByID returns a runner via id
   244  func GetRunnerByID(ctx context.Context, id int64) (*ActionRunner, error) {
   245  	var runner ActionRunner
   246  	has, err := db.GetEngine(ctx).Where("id=?", id).Get(&runner)
   247  	if err != nil {
   248  		return nil, err
   249  	} else if !has {
   250  		return nil, fmt.Errorf("runner with id %d: %w", id, util.ErrNotExist)
   251  	}
   252  	return &runner, nil
   253  }
   254  
   255  // UpdateRunner updates runner's information.
   256  func UpdateRunner(ctx context.Context, r *ActionRunner, cols ...string) error {
   257  	e := db.GetEngine(ctx)
   258  	var err error
   259  	if len(cols) == 0 {
   260  		_, err = e.ID(r.ID).AllCols().Update(r)
   261  	} else {
   262  		_, err = e.ID(r.ID).Cols(cols...).Update(r)
   263  	}
   264  	return err
   265  }
   266  
   267  // DeleteRunner deletes a runner by given ID.
   268  func DeleteRunner(ctx context.Context, id int64) error {
   269  	if _, err := GetRunnerByID(ctx, id); err != nil {
   270  		return err
   271  	}
   272  
   273  	_, err := db.GetEngine(ctx).Delete(&ActionRunner{ID: id})
   274  	return err
   275  }
   276  
   277  // CreateRunner creates new runner.
   278  func CreateRunner(ctx context.Context, t *ActionRunner) error {
   279  	_, err := db.GetEngine(ctx).Insert(t)
   280  	return err
   281  }
   282  
   283  func CountRunnersWithoutBelongingOwner(ctx context.Context) (int64, error) {
   284  	// Only affect action runners were a owner ID is set, as actions runners
   285  	// could also be created on a repository.
   286  	return db.GetEngine(ctx).Table("action_runner").
   287  		Join("LEFT", "user", "`action_runner`.owner_id = `user`.id").
   288  		Where("`action_runner`.owner_id != ?", 0).
   289  		And(builder.IsNull{"`user`.id"}).
   290  		Count(new(ActionRunner))
   291  }
   292  
   293  func FixRunnersWithoutBelongingOwner(ctx context.Context) (int64, error) {
   294  	subQuery := builder.Select("`action_runner`.id").
   295  		From("`action_runner`").
   296  		Join("LEFT", "user", "`action_runner`.owner_id = `user`.id").
   297  		Where(builder.Neq{"`action_runner`.owner_id": 0}).
   298  		And(builder.IsNull{"`user`.id"})
   299  	b := builder.Delete(builder.In("id", subQuery)).From("`action_runner`")
   300  	res, err := db.GetEngine(ctx).Exec(b)
   301  	if err != nil {
   302  		return 0, err
   303  	}
   304  	return res.RowsAffected()
   305  }