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 }