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