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