code.gitea.io/gitea@v1.22.3/models/actions/task.go (about) 1 // Copyright 2022 The Gitea Authors. All rights reserved. 2 // SPDX-License-Identifier: MIT 3 4 package actions 5 6 import ( 7 "context" 8 "crypto/subtle" 9 "fmt" 10 "time" 11 12 auth_model "code.gitea.io/gitea/models/auth" 13 "code.gitea.io/gitea/models/db" 14 "code.gitea.io/gitea/models/unit" 15 "code.gitea.io/gitea/modules/container" 16 "code.gitea.io/gitea/modules/log" 17 "code.gitea.io/gitea/modules/setting" 18 "code.gitea.io/gitea/modules/timeutil" 19 "code.gitea.io/gitea/modules/util" 20 21 runnerv1 "code.gitea.io/actions-proto-go/runner/v1" 22 lru "github.com/hashicorp/golang-lru/v2" 23 "github.com/nektos/act/pkg/jobparser" 24 "google.golang.org/protobuf/types/known/timestamppb" 25 "xorm.io/builder" 26 ) 27 28 // ActionTask represents a distribution of job 29 type ActionTask struct { 30 ID int64 31 JobID int64 32 Job *ActionRunJob `xorm:"-"` 33 Steps []*ActionTaskStep `xorm:"-"` 34 Attempt int64 35 RunnerID int64 `xorm:"index"` 36 Status Status `xorm:"index"` 37 Started timeutil.TimeStamp `xorm:"index"` 38 Stopped timeutil.TimeStamp 39 40 RepoID int64 `xorm:"index"` 41 OwnerID int64 `xorm:"index"` 42 CommitSHA string `xorm:"index"` 43 IsForkPullRequest bool 44 45 Token string `xorm:"-"` 46 TokenHash string `xorm:"UNIQUE"` // sha256 of token 47 TokenSalt string 48 TokenLastEight string `xorm:"index token_last_eight"` 49 50 LogFilename string // file name of log 51 LogInStorage bool // read log from database or from storage 52 LogLength int64 // lines count 53 LogSize int64 // blob size 54 LogIndexes LogIndexes `xorm:"LONGBLOB"` // line number to offset 55 LogExpired bool // files that are too old will be deleted 56 57 Created timeutil.TimeStamp `xorm:"created"` 58 Updated timeutil.TimeStamp `xorm:"updated index"` 59 } 60 61 var successfulTokenTaskCache *lru.Cache[string, any] 62 63 func init() { 64 db.RegisterModel(new(ActionTask), func() error { 65 if setting.SuccessfulTokensCacheSize > 0 { 66 var err error 67 successfulTokenTaskCache, err = lru.New[string, any](setting.SuccessfulTokensCacheSize) 68 if err != nil { 69 return fmt.Errorf("unable to allocate Task cache: %v", err) 70 } 71 } else { 72 successfulTokenTaskCache = nil 73 } 74 return nil 75 }) 76 } 77 78 func (task *ActionTask) Duration() time.Duration { 79 return calculateDuration(task.Started, task.Stopped, task.Status) 80 } 81 82 func (task *ActionTask) IsStopped() bool { 83 return task.Stopped > 0 84 } 85 86 func (task *ActionTask) GetRunLink() string { 87 if task.Job == nil || task.Job.Run == nil { 88 return "" 89 } 90 return task.Job.Run.Link() 91 } 92 93 func (task *ActionTask) GetCommitLink() string { 94 if task.Job == nil || task.Job.Run == nil || task.Job.Run.Repo == nil { 95 return "" 96 } 97 return task.Job.Run.Repo.CommitLink(task.CommitSHA) 98 } 99 100 func (task *ActionTask) GetRepoName() string { 101 if task.Job == nil || task.Job.Run == nil || task.Job.Run.Repo == nil { 102 return "" 103 } 104 return task.Job.Run.Repo.FullName() 105 } 106 107 func (task *ActionTask) GetRepoLink() string { 108 if task.Job == nil || task.Job.Run == nil || task.Job.Run.Repo == nil { 109 return "" 110 } 111 return task.Job.Run.Repo.Link() 112 } 113 114 func (task *ActionTask) LoadJob(ctx context.Context) error { 115 if task.Job == nil { 116 job, err := GetRunJobByID(ctx, task.JobID) 117 if err != nil { 118 return err 119 } 120 task.Job = job 121 } 122 return nil 123 } 124 125 // LoadAttributes load Job Steps if not loaded 126 func (task *ActionTask) LoadAttributes(ctx context.Context) error { 127 if task == nil { 128 return nil 129 } 130 if err := task.LoadJob(ctx); err != nil { 131 return err 132 } 133 134 if err := task.Job.LoadAttributes(ctx); err != nil { 135 return err 136 } 137 138 if task.Steps == nil { // be careful, an empty slice (not nil) also means loaded 139 steps, err := GetTaskStepsByTaskID(ctx, task.ID) 140 if err != nil { 141 return err 142 } 143 task.Steps = steps 144 } 145 146 return nil 147 } 148 149 func (task *ActionTask) GenerateToken() (err error) { 150 task.Token, task.TokenSalt, task.TokenHash, task.TokenLastEight, err = generateSaltedToken() 151 return err 152 } 153 154 func GetTaskByID(ctx context.Context, id int64) (*ActionTask, error) { 155 var task ActionTask 156 has, err := db.GetEngine(ctx).Where("id=?", id).Get(&task) 157 if err != nil { 158 return nil, err 159 } else if !has { 160 return nil, fmt.Errorf("task with id %d: %w", id, util.ErrNotExist) 161 } 162 163 return &task, nil 164 } 165 166 func GetRunningTaskByToken(ctx context.Context, token string) (*ActionTask, error) { 167 errNotExist := fmt.Errorf("task with token %q: %w", token, util.ErrNotExist) 168 if token == "" { 169 return nil, errNotExist 170 } 171 // A token is defined as being SHA1 sum these are 40 hexadecimal bytes long 172 if len(token) != 40 { 173 return nil, errNotExist 174 } 175 for _, x := range []byte(token) { 176 if x < '0' || (x > '9' && x < 'a') || x > 'f' { 177 return nil, errNotExist 178 } 179 } 180 181 lastEight := token[len(token)-8:] 182 183 if id := getTaskIDFromCache(token); id > 0 { 184 task := &ActionTask{ 185 TokenLastEight: lastEight, 186 } 187 // Re-get the task from the db in case it has been deleted in the intervening period 188 has, err := db.GetEngine(ctx).ID(id).Get(task) 189 if err != nil { 190 return nil, err 191 } 192 if has { 193 return task, nil 194 } 195 successfulTokenTaskCache.Remove(token) 196 } 197 198 var tasks []*ActionTask 199 err := db.GetEngine(ctx).Where("token_last_eight = ? AND status = ?", lastEight, StatusRunning).Find(&tasks) 200 if err != nil { 201 return nil, err 202 } else if len(tasks) == 0 { 203 return nil, errNotExist 204 } 205 206 for _, t := range tasks { 207 tempHash := auth_model.HashToken(token, t.TokenSalt) 208 if subtle.ConstantTimeCompare([]byte(t.TokenHash), []byte(tempHash)) == 1 { 209 if successfulTokenTaskCache != nil { 210 successfulTokenTaskCache.Add(token, t.ID) 211 } 212 return t, nil 213 } 214 } 215 return nil, errNotExist 216 } 217 218 func CreateTaskForRunner(ctx context.Context, runner *ActionRunner) (*ActionTask, bool, error) { 219 ctx, committer, err := db.TxContext(ctx) 220 if err != nil { 221 return nil, false, err 222 } 223 defer committer.Close() 224 225 e := db.GetEngine(ctx) 226 227 jobCond := builder.NewCond() 228 if runner.RepoID != 0 { 229 jobCond = builder.Eq{"repo_id": runner.RepoID} 230 } else if runner.OwnerID != 0 { 231 jobCond = builder.In("repo_id", builder.Select("`repository`.id").From("repository"). 232 Join("INNER", "repo_unit", "`repository`.id = `repo_unit`.repo_id"). 233 Where(builder.Eq{"`repository`.owner_id": runner.OwnerID, "`repo_unit`.type": unit.TypeActions})) 234 } 235 if jobCond.IsValid() { 236 jobCond = builder.In("run_id", builder.Select("id").From("action_run").Where(jobCond)) 237 } 238 239 var jobs []*ActionRunJob 240 if err := e.Where("task_id=? AND status=?", 0, StatusWaiting).And(jobCond).Asc("updated", "id").Find(&jobs); err != nil { 241 return nil, false, err 242 } 243 244 // TODO: a more efficient way to filter labels 245 var job *ActionRunJob 246 log.Trace("runner labels: %v", runner.AgentLabels) 247 for _, v := range jobs { 248 if isSubset(runner.AgentLabels, v.RunsOn) { 249 job = v 250 break 251 } 252 } 253 if job == nil { 254 return nil, false, nil 255 } 256 if err := job.LoadAttributes(ctx); err != nil { 257 return nil, false, err 258 } 259 260 now := timeutil.TimeStampNow() 261 job.Attempt++ 262 job.Started = now 263 job.Status = StatusRunning 264 265 task := &ActionTask{ 266 JobID: job.ID, 267 Attempt: job.Attempt, 268 RunnerID: runner.ID, 269 Started: now, 270 Status: StatusRunning, 271 RepoID: job.RepoID, 272 OwnerID: job.OwnerID, 273 CommitSHA: job.CommitSHA, 274 IsForkPullRequest: job.IsForkPullRequest, 275 } 276 if err := task.GenerateToken(); err != nil { 277 return nil, false, err 278 } 279 280 var workflowJob *jobparser.Job 281 if gots, err := jobparser.Parse(job.WorkflowPayload); err != nil { 282 return nil, false, fmt.Errorf("parse workflow of job %d: %w", job.ID, err) 283 } else if len(gots) != 1 { 284 return nil, false, fmt.Errorf("workflow of job %d: not single workflow", job.ID) 285 } else { //nolint:revive 286 _, workflowJob = gots[0].Job() 287 } 288 289 if _, err := e.Insert(task); err != nil { 290 return nil, false, err 291 } 292 293 task.LogFilename = logFileName(job.Run.Repo.FullName(), task.ID) 294 if err := UpdateTask(ctx, task, "log_filename"); err != nil { 295 return nil, false, err 296 } 297 298 if len(workflowJob.Steps) > 0 { 299 steps := make([]*ActionTaskStep, len(workflowJob.Steps)) 300 for i, v := range workflowJob.Steps { 301 name, _ := util.SplitStringAtByteN(v.String(), 255) 302 steps[i] = &ActionTaskStep{ 303 Name: name, 304 TaskID: task.ID, 305 Index: int64(i), 306 RepoID: task.RepoID, 307 Status: StatusWaiting, 308 } 309 } 310 if _, err := e.Insert(steps); err != nil { 311 return nil, false, err 312 } 313 task.Steps = steps 314 } 315 316 job.TaskID = task.ID 317 if n, err := UpdateRunJob(ctx, job, builder.Eq{"task_id": 0}); err != nil { 318 return nil, false, err 319 } else if n != 1 { 320 return nil, false, nil 321 } 322 323 task.Job = job 324 325 if err := committer.Commit(); err != nil { 326 return nil, false, err 327 } 328 329 return task, true, nil 330 } 331 332 func UpdateTask(ctx context.Context, task *ActionTask, cols ...string) error { 333 sess := db.GetEngine(ctx).ID(task.ID) 334 if len(cols) > 0 { 335 sess.Cols(cols...) 336 } 337 _, err := sess.Update(task) 338 return err 339 } 340 341 // UpdateTaskByState updates the task by the state. 342 // It will always update the task if the state is not final, even there is no change. 343 // So it will update ActionTask.Updated to avoid the task being judged as a zombie task. 344 func UpdateTaskByState(ctx context.Context, state *runnerv1.TaskState) (*ActionTask, error) { 345 stepStates := map[int64]*runnerv1.StepState{} 346 for _, v := range state.Steps { 347 stepStates[v.Id] = v 348 } 349 350 ctx, committer, err := db.TxContext(ctx) 351 if err != nil { 352 return nil, err 353 } 354 defer committer.Close() 355 356 e := db.GetEngine(ctx) 357 358 task := &ActionTask{} 359 if has, err := e.ID(state.Id).Get(task); err != nil { 360 return nil, err 361 } else if !has { 362 return nil, util.ErrNotExist 363 } 364 365 if task.Status.IsDone() { 366 // the state is final, do nothing 367 return task, nil 368 } 369 370 // state.Result is not unspecified means the task is finished 371 if state.Result != runnerv1.Result_RESULT_UNSPECIFIED { 372 task.Status = Status(state.Result) 373 task.Stopped = timeutil.TimeStamp(state.StoppedAt.AsTime().Unix()) 374 if err := UpdateTask(ctx, task, "status", "stopped"); err != nil { 375 return nil, err 376 } 377 if _, err := UpdateRunJob(ctx, &ActionRunJob{ 378 ID: task.JobID, 379 Status: task.Status, 380 Stopped: task.Stopped, 381 }, nil); err != nil { 382 return nil, err 383 } 384 } else { 385 // Force update ActionTask.Updated to avoid the task being judged as a zombie task 386 task.Updated = timeutil.TimeStampNow() 387 if err := UpdateTask(ctx, task, "updated"); err != nil { 388 return nil, err 389 } 390 } 391 392 if err := task.LoadAttributes(ctx); err != nil { 393 return nil, err 394 } 395 396 for _, step := range task.Steps { 397 var result runnerv1.Result 398 if v, ok := stepStates[step.Index]; ok { 399 result = v.Result 400 step.LogIndex = v.LogIndex 401 step.LogLength = v.LogLength 402 step.Started = convertTimestamp(v.StartedAt) 403 step.Stopped = convertTimestamp(v.StoppedAt) 404 } 405 if result != runnerv1.Result_RESULT_UNSPECIFIED { 406 step.Status = Status(result) 407 } else if step.Started != 0 { 408 step.Status = StatusRunning 409 } 410 if _, err := e.ID(step.ID).Update(step); err != nil { 411 return nil, err 412 } 413 } 414 415 if err := committer.Commit(); err != nil { 416 return nil, err 417 } 418 419 return task, nil 420 } 421 422 func StopTask(ctx context.Context, taskID int64, status Status) error { 423 if !status.IsDone() { 424 return fmt.Errorf("cannot stop task with status %v", status) 425 } 426 e := db.GetEngine(ctx) 427 428 task := &ActionTask{} 429 if has, err := e.ID(taskID).Get(task); err != nil { 430 return err 431 } else if !has { 432 return util.ErrNotExist 433 } 434 if task.Status.IsDone() { 435 return nil 436 } 437 438 now := timeutil.TimeStampNow() 439 task.Status = status 440 task.Stopped = now 441 if _, err := UpdateRunJob(ctx, &ActionRunJob{ 442 ID: task.JobID, 443 Status: task.Status, 444 Stopped: task.Stopped, 445 }, nil); err != nil { 446 return err 447 } 448 449 if err := UpdateTask(ctx, task, "status", "stopped"); err != nil { 450 return err 451 } 452 453 if err := task.LoadAttributes(ctx); err != nil { 454 return err 455 } 456 457 for _, step := range task.Steps { 458 if !step.Status.IsDone() { 459 step.Status = status 460 if step.Started == 0 { 461 step.Started = now 462 } 463 step.Stopped = now 464 } 465 if _, err := e.ID(step.ID).Update(step); err != nil { 466 return err 467 } 468 } 469 470 return nil 471 } 472 473 func isSubset(set, subset []string) bool { 474 m := make(container.Set[string], len(set)) 475 for _, v := range set { 476 m.Add(v) 477 } 478 479 for _, v := range subset { 480 if !m.Contains(v) { 481 return false 482 } 483 } 484 return true 485 } 486 487 func convertTimestamp(timestamp *timestamppb.Timestamp) timeutil.TimeStamp { 488 if timestamp.GetSeconds() == 0 && timestamp.GetNanos() == 0 { 489 return timeutil.TimeStamp(0) 490 } 491 return timeutil.TimeStamp(timestamp.AsTime().Unix()) 492 } 493 494 func logFileName(repoFullName string, taskID int64) string { 495 return fmt.Sprintf("%s/%02x/%d.log", repoFullName, taskID%256, taskID) 496 } 497 498 func getTaskIDFromCache(token string) int64 { 499 if successfulTokenTaskCache == nil { 500 return 0 501 } 502 tInterface, ok := successfulTokenTaskCache.Get(token) 503 if !ok { 504 return 0 505 } 506 t, ok := tInterface.(int64) 507 if !ok { 508 return 0 509 } 510 return t 511 }