code.gitea.io/gitea@v1.21.7/routers/api/actions/runner/runner.go (about)

     1  // Copyright 2022 The Gitea Authors. All rights reserved.
     2  // SPDX-License-Identifier: MIT
     3  
     4  package runner
     5  
     6  import (
     7  	"context"
     8  	"errors"
     9  	"net/http"
    10  
    11  	actions_model "code.gitea.io/gitea/models/actions"
    12  	"code.gitea.io/gitea/modules/actions"
    13  	"code.gitea.io/gitea/modules/log"
    14  	"code.gitea.io/gitea/modules/util"
    15  	actions_service "code.gitea.io/gitea/services/actions"
    16  
    17  	runnerv1 "code.gitea.io/actions-proto-go/runner/v1"
    18  	"code.gitea.io/actions-proto-go/runner/v1/runnerv1connect"
    19  	"github.com/bufbuild/connect-go"
    20  	gouuid "github.com/google/uuid"
    21  	"google.golang.org/grpc/codes"
    22  	"google.golang.org/grpc/status"
    23  )
    24  
    25  func NewRunnerServiceHandler() (string, http.Handler) {
    26  	return runnerv1connect.NewRunnerServiceHandler(
    27  		&Service{},
    28  		connect.WithCompressMinBytes(1024),
    29  		withRunner,
    30  	)
    31  }
    32  
    33  var _ runnerv1connect.RunnerServiceClient = (*Service)(nil)
    34  
    35  type Service struct {
    36  	runnerv1connect.UnimplementedRunnerServiceHandler
    37  }
    38  
    39  // Register for new runner.
    40  func (s *Service) Register(
    41  	ctx context.Context,
    42  	req *connect.Request[runnerv1.RegisterRequest],
    43  ) (*connect.Response[runnerv1.RegisterResponse], error) {
    44  	if req.Msg.Token == "" || req.Msg.Name == "" {
    45  		return nil, errors.New("missing runner token, name")
    46  	}
    47  
    48  	runnerToken, err := actions_model.GetRunnerToken(ctx, req.Msg.Token)
    49  	if err != nil {
    50  		return nil, errors.New("runner registration token not found")
    51  	}
    52  
    53  	if !runnerToken.IsActive {
    54  		return nil, errors.New("runner registration token has been invalidated, please use the latest one")
    55  	}
    56  
    57  	labels := req.Msg.Labels
    58  	// TODO: agent_labels should be removed from pb after Gitea 1.20 released.
    59  	// Old version runner's agent_labels slice is not empty and labels slice is empty.
    60  	// And due to compatibility with older versions, it is temporarily marked as Deprecated in pb, so use `//nolint` here.
    61  	if len(req.Msg.AgentLabels) > 0 && len(req.Msg.Labels) == 0 { //nolint:staticcheck
    62  		labels = req.Msg.AgentLabels //nolint:staticcheck
    63  	}
    64  
    65  	// create new runner
    66  	name, _ := util.SplitStringAtByteN(req.Msg.Name, 255)
    67  	runner := &actions_model.ActionRunner{
    68  		UUID:        gouuid.New().String(),
    69  		Name:        name,
    70  		OwnerID:     runnerToken.OwnerID,
    71  		RepoID:      runnerToken.RepoID,
    72  		Version:     req.Msg.Version,
    73  		AgentLabels: labels,
    74  	}
    75  	if err := runner.GenerateToken(); err != nil {
    76  		return nil, errors.New("can't generate token")
    77  	}
    78  
    79  	// create new runner
    80  	if err := actions_model.CreateRunner(ctx, runner); err != nil {
    81  		return nil, errors.New("can't create new runner")
    82  	}
    83  
    84  	// update token status
    85  	runnerToken.IsActive = true
    86  	if err := actions_model.UpdateRunnerToken(ctx, runnerToken, "is_active"); err != nil {
    87  		return nil, errors.New("can't update runner token status")
    88  	}
    89  
    90  	res := connect.NewResponse(&runnerv1.RegisterResponse{
    91  		Runner: &runnerv1.Runner{
    92  			Id:      runner.ID,
    93  			Uuid:    runner.UUID,
    94  			Token:   runner.Token,
    95  			Name:    runner.Name,
    96  			Version: runner.Version,
    97  			Labels:  runner.AgentLabels,
    98  		},
    99  	})
   100  
   101  	return res, nil
   102  }
   103  
   104  func (s *Service) Declare(
   105  	ctx context.Context,
   106  	req *connect.Request[runnerv1.DeclareRequest],
   107  ) (*connect.Response[runnerv1.DeclareResponse], error) {
   108  	runner := GetRunner(ctx)
   109  	runner.AgentLabels = req.Msg.Labels
   110  	runner.Version = req.Msg.Version
   111  	if err := actions_model.UpdateRunner(ctx, runner, "agent_labels", "version"); err != nil {
   112  		return nil, status.Errorf(codes.Internal, "update runner: %v", err)
   113  	}
   114  
   115  	return connect.NewResponse(&runnerv1.DeclareResponse{
   116  		Runner: &runnerv1.Runner{
   117  			Id:      runner.ID,
   118  			Uuid:    runner.UUID,
   119  			Token:   runner.Token,
   120  			Name:    runner.Name,
   121  			Version: runner.Version,
   122  			Labels:  runner.AgentLabels,
   123  		},
   124  	}), nil
   125  }
   126  
   127  // FetchTask assigns a task to the runner
   128  func (s *Service) FetchTask(
   129  	ctx context.Context,
   130  	req *connect.Request[runnerv1.FetchTaskRequest],
   131  ) (*connect.Response[runnerv1.FetchTaskResponse], error) {
   132  	runner := GetRunner(ctx)
   133  
   134  	var task *runnerv1.Task
   135  	tasksVersion := req.Msg.TasksVersion // task version from runner
   136  	latestVersion, err := actions_model.GetTasksVersionByScope(ctx, runner.OwnerID, runner.RepoID)
   137  	if err != nil {
   138  		return nil, status.Errorf(codes.Internal, "query tasks version failed: %v", err)
   139  	} else if latestVersion == 0 {
   140  		if err := actions_model.IncreaseTaskVersion(ctx, runner.OwnerID, runner.RepoID); err != nil {
   141  			return nil, status.Errorf(codes.Internal, "fail to increase task version: %v", err)
   142  		}
   143  		// if we don't increase the value of `latestVersion` here,
   144  		// the response of FetchTask will return tasksVersion as zero.
   145  		// and the runner will treat it as an old version of Gitea.
   146  		latestVersion++
   147  	}
   148  
   149  	if tasksVersion != latestVersion {
   150  		// if the task version in request is not equal to the version in db,
   151  		// it means there may still be some tasks not be assgined.
   152  		// try to pick a task for the runner that send the request.
   153  		if t, ok, err := pickTask(ctx, runner); err != nil {
   154  			log.Error("pick task failed: %v", err)
   155  			return nil, status.Errorf(codes.Internal, "pick task: %v", err)
   156  		} else if ok {
   157  			task = t
   158  		}
   159  	}
   160  	res := connect.NewResponse(&runnerv1.FetchTaskResponse{
   161  		Task:         task,
   162  		TasksVersion: latestVersion,
   163  	})
   164  	return res, nil
   165  }
   166  
   167  // UpdateTask updates the task status.
   168  func (s *Service) UpdateTask(
   169  	ctx context.Context,
   170  	req *connect.Request[runnerv1.UpdateTaskRequest],
   171  ) (*connect.Response[runnerv1.UpdateTaskResponse], error) {
   172  	task, err := actions_model.UpdateTaskByState(ctx, req.Msg.State)
   173  	if err != nil {
   174  		return nil, status.Errorf(codes.Internal, "update task: %v", err)
   175  	}
   176  
   177  	for k, v := range req.Msg.Outputs {
   178  		if len(k) > 255 {
   179  			log.Warn("Ignore the output of task %d because the key is too long: %q", task.ID, k)
   180  			continue
   181  		}
   182  		// The value can be a maximum of 1 MB
   183  		if l := len(v); l > 1024*1024 {
   184  			log.Warn("Ignore the output %q of task %d because the value is too long: %v", k, task.ID, l)
   185  			continue
   186  		}
   187  		// There's another limitation on GitHub that the total of all outputs in a workflow run can be a maximum of 50 MB.
   188  		// We don't check the total size here because it's not easy to do, and it doesn't really worth it.
   189  		// See https://docs.github.com/en/actions/using-jobs/defining-outputs-for-jobs
   190  
   191  		if err := actions_model.InsertTaskOutputIfNotExist(ctx, task.ID, k, v); err != nil {
   192  			log.Warn("Failed to insert the output %q of task %d: %v", k, task.ID, err)
   193  			// It's ok not to return errors, the runner will resend the outputs.
   194  		}
   195  	}
   196  	sentOutputs, err := actions_model.FindTaskOutputKeyByTaskID(ctx, task.ID)
   197  	if err != nil {
   198  		log.Warn("Failed to find the sent outputs of task %d: %v", task.ID, err)
   199  		// It's not to return errors, it can be handled when the runner resends sent outputs.
   200  	}
   201  
   202  	if err := task.LoadJob(ctx); err != nil {
   203  		return nil, status.Errorf(codes.Internal, "load job: %v", err)
   204  	}
   205  	if err := task.Job.LoadRun(ctx); err != nil {
   206  		return nil, status.Errorf(codes.Internal, "load run: %v", err)
   207  	}
   208  
   209  	// don't create commit status for cron job
   210  	if task.Job.Run.ScheduleID == 0 {
   211  		actions_service.CreateCommitStatus(ctx, task.Job)
   212  	}
   213  
   214  	if req.Msg.State.Result != runnerv1.Result_RESULT_UNSPECIFIED {
   215  		if err := actions_service.EmitJobsIfReady(task.Job.RunID); err != nil {
   216  			log.Error("Emit ready jobs of run %d: %v", task.Job.RunID, err)
   217  		}
   218  	}
   219  
   220  	return connect.NewResponse(&runnerv1.UpdateTaskResponse{
   221  		State: &runnerv1.TaskState{
   222  			Id:     req.Msg.State.Id,
   223  			Result: task.Status.AsResult(),
   224  		},
   225  		SentOutputs: sentOutputs,
   226  	}), nil
   227  }
   228  
   229  // UpdateLog uploads log of the task.
   230  func (s *Service) UpdateLog(
   231  	ctx context.Context,
   232  	req *connect.Request[runnerv1.UpdateLogRequest],
   233  ) (*connect.Response[runnerv1.UpdateLogResponse], error) {
   234  	res := connect.NewResponse(&runnerv1.UpdateLogResponse{})
   235  
   236  	task, err := actions_model.GetTaskByID(ctx, req.Msg.TaskId)
   237  	if err != nil {
   238  		return nil, status.Errorf(codes.Internal, "get task: %v", err)
   239  	}
   240  	ack := task.LogLength
   241  
   242  	if len(req.Msg.Rows) == 0 || req.Msg.Index > ack || int64(len(req.Msg.Rows))+req.Msg.Index <= ack {
   243  		res.Msg.AckIndex = ack
   244  		return res, nil
   245  	}
   246  
   247  	if task.LogInStorage {
   248  		return nil, status.Errorf(codes.AlreadyExists, "log file has been archived")
   249  	}
   250  
   251  	rows := req.Msg.Rows[ack-req.Msg.Index:]
   252  	ns, err := actions.WriteLogs(ctx, task.LogFilename, task.LogSize, rows)
   253  	if err != nil {
   254  		return nil, status.Errorf(codes.Internal, "write logs: %v", err)
   255  	}
   256  	task.LogLength += int64(len(rows))
   257  	for _, n := range ns {
   258  		task.LogIndexes = append(task.LogIndexes, task.LogSize)
   259  		task.LogSize += int64(n)
   260  	}
   261  
   262  	res.Msg.AckIndex = task.LogLength
   263  
   264  	var remove func()
   265  	if req.Msg.NoMore {
   266  		task.LogInStorage = true
   267  		remove, err = actions.TransferLogs(ctx, task.LogFilename)
   268  		if err != nil {
   269  			return nil, status.Errorf(codes.Internal, "transfer logs: %v", err)
   270  		}
   271  	}
   272  
   273  	if err := actions_model.UpdateTask(ctx, task, "log_indexes", "log_length", "log_size", "log_in_storage"); err != nil {
   274  		return nil, status.Errorf(codes.Internal, "update task: %v", err)
   275  	}
   276  	if remove != nil {
   277  		remove()
   278  	}
   279  
   280  	return res, nil
   281  }