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  }