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 }