mirror of
				https://github.com/go-gitea/gitea.git
				synced 2025-10-31 03:25:11 +01:00 
			
		
		
		
	Actions Runner rest api (#33873)
Implements runner apis based on https://docs.github.com/en/rest/actions/self-hosted-runners?apiVersion=2022-11-28#list-self-hosted-runners-for-an-organization - Add Post endpoints for registration-token, google/go-github revealed this as problem - We should deprecate Get Endpoints, leaving them for compatibility - Get endpoint of admin has api path /admin/runners/registration-token that feels wrong, /admin/actions/runners/registration-token seems more consistent with user/org/repo api - Get Runner Api - List Runner Api - Delete Runner Api - Tests admin / user / org / repo level endpoints Related to #33750 (implements point 1 and 2) Via needs discovered in #32461, this runner api is needed to allow cleanup of runners that are deallocated without user interaction. --------- Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com> Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
This commit is contained in:
		
							parent
							
								
									ba0deab616
								
							
						
					
					
						commit
						21b43fce08
					
				| @ -14,6 +14,7 @@ import ( | ||||
| 	"code.gitea.io/gitea/models/shared/types" | ||||
| 	user_model "code.gitea.io/gitea/models/user" | ||||
| 	"code.gitea.io/gitea/modules/optional" | ||||
| 	"code.gitea.io/gitea/modules/setting" | ||||
| 	"code.gitea.io/gitea/modules/timeutil" | ||||
| 	"code.gitea.io/gitea/modules/translation" | ||||
| 	"code.gitea.io/gitea/modules/util" | ||||
| @ -123,8 +124,15 @@ func (r *ActionRunner) IsOnline() bool { | ||||
| 	return false | ||||
| } | ||||
| 
 | ||||
| // Editable checks if the runner is editable by the user | ||||
| func (r *ActionRunner) Editable(ownerID, repoID int64) bool { | ||||
| // EditableInContext checks if the runner is editable by the "context" owner/repo | ||||
| // ownerID == 0 and repoID == 0 means "admin" context, any runner including global runners could be edited | ||||
| // ownerID == 0 and repoID != 0 means "repo" context, any runner belonging to the given repo could be edited | ||||
| // ownerID != 0 and repoID == 0 means "owner(org/user)" context, any runner belonging to the given user/org could be edited | ||||
| // ownerID != 0 and repoID != 0 means "owner" OR "repo" context, legacy behavior, but we should forbid using it | ||||
| func (r *ActionRunner) EditableInContext(ownerID, repoID int64) bool { | ||||
| 	if ownerID != 0 && repoID != 0 { | ||||
| 		setting.PanicInDevOrTesting("ownerID and repoID should not be both set") | ||||
| 	} | ||||
| 	if ownerID == 0 && repoID == 0 { | ||||
| 		return true | ||||
| 	} | ||||
| @ -168,6 +176,12 @@ func init() { | ||||
| 	db.RegisterModel(&ActionRunner{}) | ||||
| } | ||||
| 
 | ||||
| // FindRunnerOptions | ||||
| // ownerID == 0 and repoID == 0 means any runner including global runners | ||||
| // repoID != 0 and WithAvailable == false means any runner for the given repo | ||||
| // repoID != 0 and WithAvailable == true means any runner for the given repo, parent user/org, and global runners | ||||
| // ownerID != 0 and repoID == 0 and WithAvailable == false means any runner for the given user/org | ||||
| // ownerID != 0 and repoID == 0 and WithAvailable == true means any runner for the given user/org and global runners | ||||
| type FindRunnerOptions struct { | ||||
| 	db.ListOptions | ||||
| 	IDs           []int64 | ||||
|  | ||||
							
								
								
									
										40
									
								
								models/fixtures/action_runner.yml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										40
									
								
								models/fixtures/action_runner.yml
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,40 @@ | ||||
| - | ||||
|   id: 34346 | ||||
|   name: runner_to_be_deleted-user | ||||
|   uuid: 3EF231BD-FBB7-4E4B-9602-E6F28363EF18 | ||||
|   token_hash: 3EF231BD-FBB7-4E4B-9602-E6F28363EF18 | ||||
|   version: "1.0.0" | ||||
|   owner_id: 1 | ||||
|   repo_id: 0 | ||||
|   description: "This runner is going to be deleted" | ||||
|   agent_labels: '["runner_to_be_deleted","linux"]' | ||||
| - | ||||
|   id: 34347 | ||||
|   name: runner_to_be_deleted-org | ||||
|   uuid: 3EF231BD-FBB7-4E4B-9602-E6F28363EF19 | ||||
|   token_hash: 3EF231BD-FBB7-4E4B-9602-E6F28363EF19 | ||||
|   version: "1.0.0" | ||||
|   owner_id: 3 | ||||
|   repo_id: 0 | ||||
|   description: "This runner is going to be deleted" | ||||
|   agent_labels: '["runner_to_be_deleted","linux"]' | ||||
| - | ||||
|   id: 34348 | ||||
|   name: runner_to_be_deleted-repo1 | ||||
|   uuid: 3EF231BD-FBB7-4E4B-9602-E6F28363EF20 | ||||
|   token_hash: 3EF231BD-FBB7-4E4B-9602-E6F28363EF20 | ||||
|   version: "1.0.0" | ||||
|   owner_id: 0 | ||||
|   repo_id: 1 | ||||
|   description: "This runner is going to be deleted" | ||||
|   agent_labels: '["runner_to_be_deleted","linux"]' | ||||
| - | ||||
|   id: 34349 | ||||
|   name: runner_to_be_deleted | ||||
|   uuid: 3EF231BD-FBB7-4E4B-9602-E6F28363EF17 | ||||
|   token_hash: 3EF231BD-FBB7-4E4B-9602-E6F28363EF17 | ||||
|   version: "1.0.0" | ||||
|   owner_id: 0 | ||||
|   repo_id: 0 | ||||
|   description: "This runner is going to be deleted" | ||||
|   agent_labels: '["runner_to_be_deleted","linux"]' | ||||
| @ -133,3 +133,26 @@ type ActionWorkflowJob struct { | ||||
| 	// swagger:strfmt date-time | ||||
| 	CompletedAt time.Time `json:"completed_at,omitempty"` | ||||
| } | ||||
| 
 | ||||
| // ActionRunnerLabel represents a Runner Label | ||||
| type ActionRunnerLabel struct { | ||||
| 	ID   int64  `json:"id"` | ||||
| 	Name string `json:"name"` | ||||
| 	Type string `json:"type"` | ||||
| } | ||||
| 
 | ||||
| // ActionRunner represents a Runner | ||||
| type ActionRunner struct { | ||||
| 	ID        int64                `json:"id"` | ||||
| 	Name      string               `json:"name"` | ||||
| 	Status    string               `json:"status"` | ||||
| 	Busy      bool                 `json:"busy"` | ||||
| 	Ephemeral bool                 `json:"ephemeral"` | ||||
| 	Labels    []*ActionRunnerLabel `json:"labels"` | ||||
| } | ||||
| 
 | ||||
| // ActionRunnersResponse returns Runners | ||||
| type ActionRunnersResponse struct { | ||||
| 	Entries    []*ActionRunner `json:"runners"` | ||||
| 	TotalCount int64           `json:"total_count"` | ||||
| } | ||||
|  | ||||
| @ -24,3 +24,81 @@ func GetRegistrationToken(ctx *context.APIContext) { | ||||
| 
 | ||||
| 	shared.GetRegistrationToken(ctx, 0, 0) | ||||
| } | ||||
| 
 | ||||
| // CreateRegistrationToken returns the token to register global runners | ||||
| func CreateRegistrationToken(ctx *context.APIContext) { | ||||
| 	// swagger:operation POST /admin/actions/runners/registration-token admin adminCreateRunnerRegistrationToken | ||||
| 	// --- | ||||
| 	// summary: Get an global actions runner registration token | ||||
| 	// produces: | ||||
| 	// - application/json | ||||
| 	// parameters: | ||||
| 	// responses: | ||||
| 	//   "200": | ||||
| 	//     "$ref": "#/responses/RegistrationToken" | ||||
| 
 | ||||
| 	shared.GetRegistrationToken(ctx, 0, 0) | ||||
| } | ||||
| 
 | ||||
| // ListRunners get all runners | ||||
| func ListRunners(ctx *context.APIContext) { | ||||
| 	// swagger:operation GET /admin/actions/runners admin getAdminRunners | ||||
| 	// --- | ||||
| 	// summary: Get all runners | ||||
| 	// produces: | ||||
| 	// - application/json | ||||
| 	// responses: | ||||
| 	//   "200": | ||||
| 	//     "$ref": "#/definitions/ActionRunnersResponse" | ||||
| 	//   "400": | ||||
| 	//     "$ref": "#/responses/error" | ||||
| 	//   "404": | ||||
| 	//     "$ref": "#/responses/notFound" | ||||
| 	shared.ListRunners(ctx, 0, 0) | ||||
| } | ||||
| 
 | ||||
| // GetRunner get an global runner | ||||
| func GetRunner(ctx *context.APIContext) { | ||||
| 	// swagger:operation GET /admin/actions/runners/{runner_id} admin getAdminRunner | ||||
| 	// --- | ||||
| 	// summary: Get an global runner | ||||
| 	// produces: | ||||
| 	// - application/json | ||||
| 	// parameters: | ||||
| 	// - name: runner_id | ||||
| 	//   in: path | ||||
| 	//   description: id of the runner | ||||
| 	//   type: string | ||||
| 	//   required: true | ||||
| 	// responses: | ||||
| 	//   "200": | ||||
| 	//     "$ref": "#/definitions/ActionRunner" | ||||
| 	//   "400": | ||||
| 	//     "$ref": "#/responses/error" | ||||
| 	//   "404": | ||||
| 	//     "$ref": "#/responses/notFound" | ||||
| 	shared.GetRunner(ctx, 0, 0, ctx.PathParamInt64("runner_id")) | ||||
| } | ||||
| 
 | ||||
| // DeleteRunner delete an global runner | ||||
| func DeleteRunner(ctx *context.APIContext) { | ||||
| 	// swagger:operation DELETE /admin/actions/runners/{runner_id} admin deleteAdminRunner | ||||
| 	// --- | ||||
| 	// summary: Delete an global runner | ||||
| 	// produces: | ||||
| 	// - application/json | ||||
| 	// parameters: | ||||
| 	// - name: runner_id | ||||
| 	//   in: path | ||||
| 	//   description: id of the runner | ||||
| 	//   type: string | ||||
| 	//   required: true | ||||
| 	// responses: | ||||
| 	//   "204": | ||||
| 	//     description: runner has been deleted | ||||
| 	//   "400": | ||||
| 	//     "$ref": "#/responses/error" | ||||
| 	//   "404": | ||||
| 	//     "$ref": "#/responses/notFound" | ||||
| 	shared.DeleteRunner(ctx, 0, 0, ctx.PathParamInt64("runner_id")) | ||||
| } | ||||
|  | ||||
| @ -912,7 +912,11 @@ func Routes() *web.Router { | ||||
| 			}) | ||||
| 
 | ||||
| 			m.Group("/runners", func() { | ||||
| 				m.Get("", reqToken(), reqChecker, act.ListRunners) | ||||
| 				m.Get("/registration-token", reqToken(), reqChecker, act.GetRegistrationToken) | ||||
| 				m.Post("/registration-token", reqToken(), reqChecker, act.CreateRegistrationToken) | ||||
| 				m.Get("/{runner_id}", reqToken(), reqChecker, act.GetRunner) | ||||
| 				m.Delete("/{runner_id}", reqToken(), reqChecker, act.DeleteRunner) | ||||
| 			}) | ||||
| 		}) | ||||
| 	} | ||||
| @ -1043,7 +1047,11 @@ func Routes() *web.Router { | ||||
| 				}) | ||||
| 
 | ||||
| 				m.Group("/runners", func() { | ||||
| 					m.Get("", reqToken(), user.ListRunners) | ||||
| 					m.Get("/registration-token", reqToken(), user.GetRegistrationToken) | ||||
| 					m.Post("/registration-token", reqToken(), user.CreateRegistrationToken) | ||||
| 					m.Get("/{runner_id}", reqToken(), user.GetRunner) | ||||
| 					m.Delete("/{runner_id}", reqToken(), user.DeleteRunner) | ||||
| 				}) | ||||
| 			}) | ||||
| 
 | ||||
| @ -1689,6 +1697,12 @@ func Routes() *web.Router { | ||||
| 					Patch(bind(api.EditHookOption{}), admin.EditHook). | ||||
| 					Delete(admin.DeleteHook) | ||||
| 			}) | ||||
| 			m.Group("/actions/runners", func() { | ||||
| 				m.Get("", admin.ListRunners) | ||||
| 				m.Post("/registration-token", admin.CreateRegistrationToken) | ||||
| 				m.Get("/{runner_id}", admin.GetRunner) | ||||
| 				m.Delete("/{runner_id}", admin.DeleteRunner) | ||||
| 			}) | ||||
| 			m.Group("/runners", func() { | ||||
| 				m.Get("/registration-token", admin.GetRegistrationToken) | ||||
| 			}) | ||||
|  | ||||
| @ -190,6 +190,27 @@ func (Action) GetRegistrationToken(ctx *context.APIContext) { | ||||
| 	shared.GetRegistrationToken(ctx, ctx.Org.Organization.ID, 0) | ||||
| } | ||||
| 
 | ||||
| // https://docs.github.com/en/rest/actions/self-hosted-runners?apiVersion=2022-11-28#create-a-registration-token-for-an-organization | ||||
| // CreateRegistrationToken returns the token to register org runners | ||||
| func (Action) CreateRegistrationToken(ctx *context.APIContext) { | ||||
| 	// swagger:operation POST /orgs/{org}/actions/runners/registration-token organization orgCreateRunnerRegistrationToken | ||||
| 	// --- | ||||
| 	// summary: Get an organization's actions runner registration token | ||||
| 	// produces: | ||||
| 	// - application/json | ||||
| 	// parameters: | ||||
| 	// - name: org | ||||
| 	//   in: path | ||||
| 	//   description: name of the organization | ||||
| 	//   type: string | ||||
| 	//   required: true | ||||
| 	// responses: | ||||
| 	//   "200": | ||||
| 	//     "$ref": "#/responses/RegistrationToken" | ||||
| 
 | ||||
| 	shared.GetRegistrationToken(ctx, ctx.Org.Organization.ID, 0) | ||||
| } | ||||
| 
 | ||||
| // ListVariables list org-level variables | ||||
| func (Action) ListVariables(ctx *context.APIContext) { | ||||
| 	// swagger:operation GET /orgs/{org}/actions/variables organization getOrgVariablesList | ||||
| @ -470,6 +491,85 @@ func (Action) UpdateVariable(ctx *context.APIContext) { | ||||
| 	ctx.Status(http.StatusNoContent) | ||||
| } | ||||
| 
 | ||||
| // ListRunners get org-level runners | ||||
| func (Action) ListRunners(ctx *context.APIContext) { | ||||
| 	// swagger:operation GET /orgs/{org}/actions/runners organization getOrgRunners | ||||
| 	// --- | ||||
| 	// summary: Get org-level runners | ||||
| 	// produces: | ||||
| 	// - application/json | ||||
| 	// parameters: | ||||
| 	// - name: org | ||||
| 	//   in: path | ||||
| 	//   description: name of the organization | ||||
| 	//   type: string | ||||
| 	//   required: true | ||||
| 	// responses: | ||||
| 	//   "200": | ||||
| 	//     "$ref": "#/definitions/ActionRunnersResponse" | ||||
| 	//   "400": | ||||
| 	//     "$ref": "#/responses/error" | ||||
| 	//   "404": | ||||
| 	//     "$ref": "#/responses/notFound" | ||||
| 	shared.ListRunners(ctx, ctx.Org.Organization.ID, 0) | ||||
| } | ||||
| 
 | ||||
| // GetRunner get an org-level runner | ||||
| func (Action) GetRunner(ctx *context.APIContext) { | ||||
| 	// swagger:operation GET /orgs/{org}/actions/runners/{runner_id} organization getOrgRunner | ||||
| 	// --- | ||||
| 	// summary: Get an org-level runner | ||||
| 	// produces: | ||||
| 	// - application/json | ||||
| 	// parameters: | ||||
| 	// - name: org | ||||
| 	//   in: path | ||||
| 	//   description: name of the organization | ||||
| 	//   type: string | ||||
| 	//   required: true | ||||
| 	// - name: runner_id | ||||
| 	//   in: path | ||||
| 	//   description: id of the runner | ||||
| 	//   type: string | ||||
| 	//   required: true | ||||
| 	// responses: | ||||
| 	//   "200": | ||||
| 	//     "$ref": "#/definitions/ActionRunner" | ||||
| 	//   "400": | ||||
| 	//     "$ref": "#/responses/error" | ||||
| 	//   "404": | ||||
| 	//     "$ref": "#/responses/notFound" | ||||
| 	shared.GetRunner(ctx, ctx.Org.Organization.ID, 0, ctx.PathParamInt64("runner_id")) | ||||
| } | ||||
| 
 | ||||
| // DeleteRunner delete an org-level runner | ||||
| func (Action) DeleteRunner(ctx *context.APIContext) { | ||||
| 	// swagger:operation DELETE /orgs/{org}/actions/runners/{runner_id} organization deleteOrgRunner | ||||
| 	// --- | ||||
| 	// summary: Delete an org-level runner | ||||
| 	// produces: | ||||
| 	// - application/json | ||||
| 	// parameters: | ||||
| 	// - name: org | ||||
| 	//   in: path | ||||
| 	//   description: name of the organization | ||||
| 	//   type: string | ||||
| 	//   required: true | ||||
| 	// - name: runner_id | ||||
| 	//   in: path | ||||
| 	//   description: id of the runner | ||||
| 	//   type: string | ||||
| 	//   required: true | ||||
| 	// responses: | ||||
| 	//   "204": | ||||
| 	//     description: runner has been deleted | ||||
| 	//   "400": | ||||
| 	//     "$ref": "#/responses/error" | ||||
| 	//   "404": | ||||
| 	//     "$ref": "#/responses/notFound" | ||||
| 	shared.DeleteRunner(ctx, ctx.Org.Organization.ID, 0, ctx.PathParamInt64("runner_id")) | ||||
| } | ||||
| 
 | ||||
| var _ actions_service.API = new(Action) | ||||
| 
 | ||||
| // Action implements actions_service.API | ||||
|  | ||||
| @ -183,7 +183,7 @@ func (Action) DeleteSecret(ctx *context.APIContext) { | ||||
| 	//   required: true | ||||
| 	// responses: | ||||
| 	//   "204": | ||||
| 	//     description: delete one secret of the organization | ||||
| 	//     description: delete one secret of the repository | ||||
| 	//   "400": | ||||
| 	//     "$ref": "#/responses/error" | ||||
| 	//   "404": | ||||
| @ -531,6 +531,125 @@ func (Action) GetRegistrationToken(ctx *context.APIContext) { | ||||
| 	shared.GetRegistrationToken(ctx, 0, ctx.Repo.Repository.ID) | ||||
| } | ||||
| 
 | ||||
| // CreateRegistrationToken returns the token to register repo runners | ||||
| func (Action) CreateRegistrationToken(ctx *context.APIContext) { | ||||
| 	// swagger:operation POST /repos/{owner}/{repo}/actions/runners/registration-token repository repoCreateRunnerRegistrationToken | ||||
| 	// --- | ||||
| 	// summary: Get a repository's actions runner registration token | ||||
| 	// produces: | ||||
| 	// - application/json | ||||
| 	// parameters: | ||||
| 	// - name: owner | ||||
| 	//   in: path | ||||
| 	//   description: owner of the repo | ||||
| 	//   type: string | ||||
| 	//   required: true | ||||
| 	// - name: repo | ||||
| 	//   in: path | ||||
| 	//   description: name of the repo | ||||
| 	//   type: string | ||||
| 	//   required: true | ||||
| 	// responses: | ||||
| 	//   "200": | ||||
| 	//     "$ref": "#/responses/RegistrationToken" | ||||
| 
 | ||||
| 	shared.GetRegistrationToken(ctx, 0, ctx.Repo.Repository.ID) | ||||
| } | ||||
| 
 | ||||
| // ListRunners get repo-level runners | ||||
| func (Action) ListRunners(ctx *context.APIContext) { | ||||
| 	// swagger:operation GET /repos/{owner}/{repo}/actions/runners repository getRepoRunners | ||||
| 	// --- | ||||
| 	// summary: Get repo-level runners | ||||
| 	// produces: | ||||
| 	// - application/json | ||||
| 	// parameters: | ||||
| 	// - name: owner | ||||
| 	//   in: path | ||||
| 	//   description: owner of the repo | ||||
| 	//   type: string | ||||
| 	//   required: true | ||||
| 	// - name: repo | ||||
| 	//   in: path | ||||
| 	//   description: name of the repo | ||||
| 	//   type: string | ||||
| 	//   required: true | ||||
| 	// responses: | ||||
| 	//   "200": | ||||
| 	//     "$ref": "#/definitions/ActionRunnersResponse" | ||||
| 	//   "400": | ||||
| 	//     "$ref": "#/responses/error" | ||||
| 	//   "404": | ||||
| 	//     "$ref": "#/responses/notFound" | ||||
| 	shared.ListRunners(ctx, 0, ctx.Repo.Repository.ID) | ||||
| } | ||||
| 
 | ||||
| // GetRunner get an repo-level runner | ||||
| func (Action) GetRunner(ctx *context.APIContext) { | ||||
| 	// swagger:operation GET /repos/{owner}/{repo}/actions/runners/{runner_id} repository getRepoRunner | ||||
| 	// --- | ||||
| 	// summary: Get an repo-level runner | ||||
| 	// produces: | ||||
| 	// - application/json | ||||
| 	// parameters: | ||||
| 	// - name: owner | ||||
| 	//   in: path | ||||
| 	//   description: owner of the repo | ||||
| 	//   type: string | ||||
| 	//   required: true | ||||
| 	// - name: repo | ||||
| 	//   in: path | ||||
| 	//   description: name of the repo | ||||
| 	//   type: string | ||||
| 	//   required: true | ||||
| 	// - name: runner_id | ||||
| 	//   in: path | ||||
| 	//   description: id of the runner | ||||
| 	//   type: string | ||||
| 	//   required: true | ||||
| 	// responses: | ||||
| 	//   "200": | ||||
| 	//     "$ref": "#/definitions/ActionRunner" | ||||
| 	//   "400": | ||||
| 	//     "$ref": "#/responses/error" | ||||
| 	//   "404": | ||||
| 	//     "$ref": "#/responses/notFound" | ||||
| 	shared.GetRunner(ctx, 0, ctx.Repo.Repository.ID, ctx.PathParamInt64("runner_id")) | ||||
| } | ||||
| 
 | ||||
| // DeleteRunner delete an repo-level runner | ||||
| func (Action) DeleteRunner(ctx *context.APIContext) { | ||||
| 	// swagger:operation DELETE /repos/{owner}/{repo}/actions/runners/{runner_id} repository deleteRepoRunner | ||||
| 	// --- | ||||
| 	// summary: Delete an repo-level runner | ||||
| 	// produces: | ||||
| 	// - application/json | ||||
| 	// parameters: | ||||
| 	// - name: owner | ||||
| 	//   in: path | ||||
| 	//   description: owner of the repo | ||||
| 	//   type: string | ||||
| 	//   required: true | ||||
| 	// - name: repo | ||||
| 	//   in: path | ||||
| 	//   description: name of the repo | ||||
| 	//   type: string | ||||
| 	//   required: true | ||||
| 	// - name: runner_id | ||||
| 	//   in: path | ||||
| 	//   description: id of the runner | ||||
| 	//   type: string | ||||
| 	//   required: true | ||||
| 	// responses: | ||||
| 	//   "204": | ||||
| 	//     description: runner has been deleted | ||||
| 	//   "400": | ||||
| 	//     "$ref": "#/responses/error" | ||||
| 	//   "404": | ||||
| 	//     "$ref": "#/responses/notFound" | ||||
| 	shared.DeleteRunner(ctx, 0, ctx.Repo.Repository.ID, ctx.PathParamInt64("runner_id")) | ||||
| } | ||||
| 
 | ||||
| var _ actions_service.API = new(Action) | ||||
| 
 | ||||
| // Action implements actions_service.API | ||||
|  | ||||
| @ -8,8 +8,13 @@ import ( | ||||
| 	"net/http" | ||||
| 
 | ||||
| 	actions_model "code.gitea.io/gitea/models/actions" | ||||
| 	"code.gitea.io/gitea/models/db" | ||||
| 	"code.gitea.io/gitea/modules/setting" | ||||
| 	api "code.gitea.io/gitea/modules/structs" | ||||
| 	"code.gitea.io/gitea/modules/util" | ||||
| 	"code.gitea.io/gitea/routers/api/v1/utils" | ||||
| 	"code.gitea.io/gitea/services/context" | ||||
| 	"code.gitea.io/gitea/services/convert" | ||||
| ) | ||||
| 
 | ||||
| // RegistrationToken is response related to registration token | ||||
| @ -30,3 +35,84 @@ func GetRegistrationToken(ctx *context.APIContext, ownerID, repoID int64) { | ||||
| 
 | ||||
| 	ctx.JSON(http.StatusOK, RegistrationToken{Token: token.Token}) | ||||
| } | ||||
| 
 | ||||
| // ListRunners lists runners for api route validated ownerID and repoID | ||||
| // ownerID == 0 and repoID == 0 means all runners including global runners, does not appear in sql where clause | ||||
| // ownerID == 0 and repoID != 0 means all runners for the given repo | ||||
| // ownerID != 0 and repoID == 0 means all runners for the given user/org | ||||
| // ownerID != 0 and repoID != 0 undefined behavior | ||||
| // Access rights are checked at the API route level | ||||
| func ListRunners(ctx *context.APIContext, ownerID, repoID int64) { | ||||
| 	if ownerID != 0 && repoID != 0 { | ||||
| 		setting.PanicInDevOrTesting("ownerID and repoID should not be both set") | ||||
| 	} | ||||
| 	runners, total, err := db.FindAndCount[actions_model.ActionRunner](ctx, &actions_model.FindRunnerOptions{ | ||||
| 		OwnerID:     ownerID, | ||||
| 		RepoID:      repoID, | ||||
| 		ListOptions: utils.GetListOptions(ctx), | ||||
| 	}) | ||||
| 	if err != nil { | ||||
| 		ctx.APIErrorInternal(err) | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	res := new(api.ActionRunnersResponse) | ||||
| 	res.TotalCount = total | ||||
| 
 | ||||
| 	res.Entries = make([]*api.ActionRunner, len(runners)) | ||||
| 	for i, runner := range runners { | ||||
| 		res.Entries[i] = convert.ToActionRunner(ctx, runner) | ||||
| 	} | ||||
| 
 | ||||
| 	ctx.JSON(http.StatusOK, &res) | ||||
| } | ||||
| 
 | ||||
| // GetRunner get the runner for api route validated ownerID and repoID | ||||
| // ownerID == 0 and repoID == 0 means any runner including global runners | ||||
| // ownerID == 0 and repoID != 0 means any runner for the given repo | ||||
| // ownerID != 0 and repoID == 0 means any runner for the given user/org | ||||
| // ownerID != 0 and repoID != 0 undefined behavior | ||||
| // Access rights are checked at the API route level | ||||
| func GetRunner(ctx *context.APIContext, ownerID, repoID, runnerID int64) { | ||||
| 	if ownerID != 0 && repoID != 0 { | ||||
| 		setting.PanicInDevOrTesting("ownerID and repoID should not be both set") | ||||
| 	} | ||||
| 	runner, err := actions_model.GetRunnerByID(ctx, runnerID) | ||||
| 	if err != nil { | ||||
| 		ctx.APIErrorNotFound(err) | ||||
| 		return | ||||
| 	} | ||||
| 	if !runner.EditableInContext(ownerID, repoID) { | ||||
| 		ctx.APIErrorNotFound("No permission to get this runner") | ||||
| 		return | ||||
| 	} | ||||
| 	ctx.JSON(http.StatusOK, convert.ToActionRunner(ctx, runner)) | ||||
| } | ||||
| 
 | ||||
| // DeleteRunner deletes the runner for api route validated ownerID and repoID | ||||
| // ownerID == 0 and repoID == 0 means any runner including global runners | ||||
| // ownerID == 0 and repoID != 0 means any runner for the given repo | ||||
| // ownerID != 0 and repoID == 0 means any runner for the given user/org | ||||
| // ownerID != 0 and repoID != 0 undefined behavior | ||||
| // Access rights are checked at the API route level | ||||
| func DeleteRunner(ctx *context.APIContext, ownerID, repoID, runnerID int64) { | ||||
| 	if ownerID != 0 && repoID != 0 { | ||||
| 		setting.PanicInDevOrTesting("ownerID and repoID should not be both set") | ||||
| 	} | ||||
| 	runner, err := actions_model.GetRunnerByID(ctx, runnerID) | ||||
| 	if err != nil { | ||||
| 		ctx.APIErrorInternal(err) | ||||
| 		return | ||||
| 	} | ||||
| 	if !runner.EditableInContext(ownerID, repoID) { | ||||
| 		ctx.APIErrorNotFound("No permission to delete this runner") | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	err = actions_model.DeleteRunner(ctx, runner.ID) | ||||
| 	if err != nil { | ||||
| 		ctx.APIErrorInternal(err) | ||||
| 		return | ||||
| 	} | ||||
| 	ctx.Status(http.StatusNoContent) | ||||
| } | ||||
|  | ||||
| @ -457,6 +457,20 @@ type swaggerRepoArtifact struct { | ||||
| 	Body api.ActionArtifact `json:"body"` | ||||
| } | ||||
| 
 | ||||
| // RunnerList | ||||
| // swagger:response RunnerList | ||||
| type swaggerRunnerList struct { | ||||
| 	// in:body | ||||
| 	Body api.ActionRunnersResponse `json:"body"` | ||||
| } | ||||
| 
 | ||||
| // Runner | ||||
| // swagger:response Runner | ||||
| type swaggerRunner struct { | ||||
| 	// in:body | ||||
| 	Body api.ActionRunner `json:"body"` | ||||
| } | ||||
| 
 | ||||
| // swagger:response Compare | ||||
| type swaggerCompare struct { | ||||
| 	// in:body | ||||
|  | ||||
| @ -24,3 +24,81 @@ func GetRegistrationToken(ctx *context.APIContext) { | ||||
| 
 | ||||
| 	shared.GetRegistrationToken(ctx, ctx.Doer.ID, 0) | ||||
| } | ||||
| 
 | ||||
| // CreateRegistrationToken returns the token to register user runners | ||||
| func CreateRegistrationToken(ctx *context.APIContext) { | ||||
| 	// swagger:operation POST /user/actions/runners/registration-token user userCreateRunnerRegistrationToken | ||||
| 	// --- | ||||
| 	// summary: Get an user's actions runner registration token | ||||
| 	// produces: | ||||
| 	// - application/json | ||||
| 	// parameters: | ||||
| 	// responses: | ||||
| 	//   "200": | ||||
| 	//     "$ref": "#/responses/RegistrationToken" | ||||
| 
 | ||||
| 	shared.GetRegistrationToken(ctx, ctx.Doer.ID, 0) | ||||
| } | ||||
| 
 | ||||
| // ListRunners get user-level runners | ||||
| func ListRunners(ctx *context.APIContext) { | ||||
| 	// swagger:operation GET /user/actions/runners user getUserRunners | ||||
| 	// --- | ||||
| 	// summary: Get user-level runners | ||||
| 	// produces: | ||||
| 	// - application/json | ||||
| 	// responses: | ||||
| 	//   "200": | ||||
| 	//     "$ref": "#/definitions/ActionRunnersResponse" | ||||
| 	//   "400": | ||||
| 	//     "$ref": "#/responses/error" | ||||
| 	//   "404": | ||||
| 	//     "$ref": "#/responses/notFound" | ||||
| 	shared.ListRunners(ctx, ctx.Doer.ID, 0) | ||||
| } | ||||
| 
 | ||||
| // GetRunner get an user-level runner | ||||
| func GetRunner(ctx *context.APIContext) { | ||||
| 	// swagger:operation GET /user/actions/runners/{runner_id} user getUserRunner | ||||
| 	// --- | ||||
| 	// summary: Get an user-level runner | ||||
| 	// produces: | ||||
| 	// - application/json | ||||
| 	// parameters: | ||||
| 	// - name: runner_id | ||||
| 	//   in: path | ||||
| 	//   description: id of the runner | ||||
| 	//   type: string | ||||
| 	//   required: true | ||||
| 	// responses: | ||||
| 	//   "200": | ||||
| 	//     "$ref": "#/definitions/ActionRunner" | ||||
| 	//   "400": | ||||
| 	//     "$ref": "#/responses/error" | ||||
| 	//   "404": | ||||
| 	//     "$ref": "#/responses/notFound" | ||||
| 	shared.GetRunner(ctx, ctx.Doer.ID, 0, ctx.PathParamInt64("runner_id")) | ||||
| } | ||||
| 
 | ||||
| // DeleteRunner delete an user-level runner | ||||
| func DeleteRunner(ctx *context.APIContext) { | ||||
| 	// swagger:operation DELETE /user/actions/runners/{runner_id} user deleteUserRunner | ||||
| 	// --- | ||||
| 	// summary: Delete an user-level runner | ||||
| 	// produces: | ||||
| 	// - application/json | ||||
| 	// parameters: | ||||
| 	// - name: runner_id | ||||
| 	//   in: path | ||||
| 	//   description: id of the runner | ||||
| 	//   type: string | ||||
| 	//   required: true | ||||
| 	// responses: | ||||
| 	//   "204": | ||||
| 	//     description: runner has been deleted | ||||
| 	//   "400": | ||||
| 	//     "$ref": "#/responses/error" | ||||
| 	//   "404": | ||||
| 	//     "$ref": "#/responses/notFound" | ||||
| 	shared.DeleteRunner(ctx, ctx.Doer.ID, 0, ctx.PathParamInt64("runner_id")) | ||||
| } | ||||
|  | ||||
| @ -197,7 +197,7 @@ func RunnersEdit(ctx *context.Context) { | ||||
| 		ctx.ServerError("LoadAttributes", err) | ||||
| 		return | ||||
| 	} | ||||
| 	if !runner.Editable(ownerID, repoID) { | ||||
| 	if !runner.EditableInContext(ownerID, repoID) { | ||||
| 		err = errors.New("no permission to edit this runner") | ||||
| 		ctx.NotFound(err) | ||||
| 		return | ||||
| @ -250,7 +250,7 @@ func RunnersEditPost(ctx *context.Context) { | ||||
| 		ctx.ServerError("RunnerDetailsEditPost.GetRunnerByID", err) | ||||
| 		return | ||||
| 	} | ||||
| 	if !runner.Editable(ownerID, repoID) { | ||||
| 	if !runner.EditableInContext(ownerID, repoID) { | ||||
| 		ctx.NotFound(util.NewPermissionDeniedErrorf("no permission to edit this runner")) | ||||
| 		return | ||||
| 	} | ||||
| @ -304,7 +304,7 @@ func RunnerDeletePost(ctx *context.Context) { | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	if !runner.Editable(rCtx.OwnerID, rCtx.RepoID) { | ||||
| 	if !runner.EditableInContext(rCtx.OwnerID, rCtx.RepoID) { | ||||
| 		ctx.NotFound(util.NewPermissionDeniedErrorf("no permission to delete this runner")) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| @ -25,4 +25,12 @@ type API interface { | ||||
| 	UpdateVariable(*context.APIContext) | ||||
| 	// GetRegistrationToken get registration token | ||||
| 	GetRegistrationToken(*context.APIContext) | ||||
| 	// CreateRegistrationToken get registration token | ||||
| 	CreateRegistrationToken(*context.APIContext) | ||||
| 	// ListRunners list runners | ||||
| 	ListRunners(*context.APIContext) | ||||
| 	// GetRunner get a runner | ||||
| 	GetRunner(*context.APIContext) | ||||
| 	// DeleteRunner delete runner | ||||
| 	DeleteRunner(*context.APIContext) | ||||
| } | ||||
|  | ||||
| @ -30,6 +30,8 @@ import ( | ||||
| 	"code.gitea.io/gitea/modules/util" | ||||
| 	asymkey_service "code.gitea.io/gitea/services/asymkey" | ||||
| 	"code.gitea.io/gitea/services/gitdiff" | ||||
| 
 | ||||
| 	runnerv1 "code.gitea.io/actions-proto-go/runner/v1" | ||||
| ) | ||||
| 
 | ||||
| // ToEmail convert models.EmailAddress to api.Email | ||||
| @ -252,6 +254,30 @@ func ToActionArtifact(repo *repo_model.Repository, art *actions_model.ActionArti | ||||
| 	}, nil | ||||
| } | ||||
| 
 | ||||
| func ToActionRunner(ctx context.Context, runner *actions_model.ActionRunner) *api.ActionRunner { | ||||
| 	status := runner.Status() | ||||
| 	apiStatus := "offline" | ||||
| 	if runner.IsOnline() { | ||||
| 		apiStatus = "online" | ||||
| 	} | ||||
| 	labels := make([]*api.ActionRunnerLabel, len(runner.AgentLabels)) | ||||
| 	for i, label := range runner.AgentLabels { | ||||
| 		labels[i] = &api.ActionRunnerLabel{ | ||||
| 			ID:   int64(i), | ||||
| 			Name: label, | ||||
| 			Type: "custom", | ||||
| 		} | ||||
| 	} | ||||
| 	return &api.ActionRunner{ | ||||
| 		ID:        runner.ID, | ||||
| 		Name:      runner.Name, | ||||
| 		Status:    apiStatus, | ||||
| 		Busy:      status == runnerv1.RunnerStatus_RUNNER_STATUS_ACTIVE, | ||||
| 		Ephemeral: runner.Ephemeral, | ||||
| 		Labels:    labels, | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| // ToVerification convert a git.Commit.Signature to an api.PayloadCommitVerification | ||||
| func ToVerification(ctx context.Context, c *git.Commit) *api.PayloadCommitVerification { | ||||
| 	verif := asymkey_service.ParseCommitWithSignature(ctx, c) | ||||
|  | ||||
							
								
								
									
										582
									
								
								templates/swagger/v1_json.tmpl
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										582
									
								
								templates/swagger/v1_json.tmpl
									
									
									
										generated
									
									
									
								
							| @ -75,6 +75,108 @@ | ||||
|         } | ||||
|       } | ||||
|     }, | ||||
|     "/admin/actions/runners": { | ||||
|       "get": { | ||||
|         "produces": [ | ||||
|           "application/json" | ||||
|         ], | ||||
|         "tags": [ | ||||
|           "admin" | ||||
|         ], | ||||
|         "summary": "Get all runners", | ||||
|         "operationId": "getAdminRunners", | ||||
|         "responses": { | ||||
|           "200": { | ||||
|             "$ref": "#/definitions/ActionRunnersResponse" | ||||
|           }, | ||||
|           "400": { | ||||
|             "$ref": "#/responses/error" | ||||
|           }, | ||||
|           "404": { | ||||
|             "$ref": "#/responses/notFound" | ||||
|           } | ||||
|         } | ||||
|       } | ||||
|     }, | ||||
|     "/admin/actions/runners/registration-token": { | ||||
|       "post": { | ||||
|         "produces": [ | ||||
|           "application/json" | ||||
|         ], | ||||
|         "tags": [ | ||||
|           "admin" | ||||
|         ], | ||||
|         "summary": "Get an global actions runner registration token", | ||||
|         "operationId": "adminCreateRunnerRegistrationToken", | ||||
|         "responses": { | ||||
|           "200": { | ||||
|             "$ref": "#/responses/RegistrationToken" | ||||
|           } | ||||
|         } | ||||
|       } | ||||
|     }, | ||||
|     "/admin/actions/runners/{runner_id}": { | ||||
|       "get": { | ||||
|         "produces": [ | ||||
|           "application/json" | ||||
|         ], | ||||
|         "tags": [ | ||||
|           "admin" | ||||
|         ], | ||||
|         "summary": "Get an global runner", | ||||
|         "operationId": "getAdminRunner", | ||||
|         "parameters": [ | ||||
|           { | ||||
|             "type": "string", | ||||
|             "description": "id of the runner", | ||||
|             "name": "runner_id", | ||||
|             "in": "path", | ||||
|             "required": true | ||||
|           } | ||||
|         ], | ||||
|         "responses": { | ||||
|           "200": { | ||||
|             "$ref": "#/definitions/ActionRunner" | ||||
|           }, | ||||
|           "400": { | ||||
|             "$ref": "#/responses/error" | ||||
|           }, | ||||
|           "404": { | ||||
|             "$ref": "#/responses/notFound" | ||||
|           } | ||||
|         } | ||||
|       }, | ||||
|       "delete": { | ||||
|         "produces": [ | ||||
|           "application/json" | ||||
|         ], | ||||
|         "tags": [ | ||||
|           "admin" | ||||
|         ], | ||||
|         "summary": "Delete an global runner", | ||||
|         "operationId": "deleteAdminRunner", | ||||
|         "parameters": [ | ||||
|           { | ||||
|             "type": "string", | ||||
|             "description": "id of the runner", | ||||
|             "name": "runner_id", | ||||
|             "in": "path", | ||||
|             "required": true | ||||
|           } | ||||
|         ], | ||||
|         "responses": { | ||||
|           "204": { | ||||
|             "description": "runner has been deleted" | ||||
|           }, | ||||
|           "400": { | ||||
|             "$ref": "#/responses/error" | ||||
|           }, | ||||
|           "404": { | ||||
|             "$ref": "#/responses/notFound" | ||||
|           } | ||||
|         } | ||||
|       } | ||||
|     }, | ||||
|     "/admin/cron": { | ||||
|       "get": { | ||||
|         "produces": [ | ||||
| @ -1697,6 +1799,38 @@ | ||||
|         } | ||||
|       } | ||||
|     }, | ||||
|     "/orgs/{org}/actions/runners": { | ||||
|       "get": { | ||||
|         "produces": [ | ||||
|           "application/json" | ||||
|         ], | ||||
|         "tags": [ | ||||
|           "organization" | ||||
|         ], | ||||
|         "summary": "Get org-level runners", | ||||
|         "operationId": "getOrgRunners", | ||||
|         "parameters": [ | ||||
|           { | ||||
|             "type": "string", | ||||
|             "description": "name of the organization", | ||||
|             "name": "org", | ||||
|             "in": "path", | ||||
|             "required": true | ||||
|           } | ||||
|         ], | ||||
|         "responses": { | ||||
|           "200": { | ||||
|             "$ref": "#/definitions/ActionRunnersResponse" | ||||
|           }, | ||||
|           "400": { | ||||
|             "$ref": "#/responses/error" | ||||
|           }, | ||||
|           "404": { | ||||
|             "$ref": "#/responses/notFound" | ||||
|           } | ||||
|         } | ||||
|       } | ||||
|     }, | ||||
|     "/orgs/{org}/actions/runners/registration-token": { | ||||
|       "get": { | ||||
|         "produces": [ | ||||
| @ -1721,6 +1855,106 @@ | ||||
|             "$ref": "#/responses/RegistrationToken" | ||||
|           } | ||||
|         } | ||||
|       }, | ||||
|       "post": { | ||||
|         "produces": [ | ||||
|           "application/json" | ||||
|         ], | ||||
|         "tags": [ | ||||
|           "organization" | ||||
|         ], | ||||
|         "summary": "Get an organization's actions runner registration token", | ||||
|         "operationId": "orgCreateRunnerRegistrationToken", | ||||
|         "parameters": [ | ||||
|           { | ||||
|             "type": "string", | ||||
|             "description": "name of the organization", | ||||
|             "name": "org", | ||||
|             "in": "path", | ||||
|             "required": true | ||||
|           } | ||||
|         ], | ||||
|         "responses": { | ||||
|           "200": { | ||||
|             "$ref": "#/responses/RegistrationToken" | ||||
|           } | ||||
|         } | ||||
|       } | ||||
|     }, | ||||
|     "/orgs/{org}/actions/runners/{runner_id}": { | ||||
|       "get": { | ||||
|         "produces": [ | ||||
|           "application/json" | ||||
|         ], | ||||
|         "tags": [ | ||||
|           "organization" | ||||
|         ], | ||||
|         "summary": "Get an org-level runner", | ||||
|         "operationId": "getOrgRunner", | ||||
|         "parameters": [ | ||||
|           { | ||||
|             "type": "string", | ||||
|             "description": "name of the organization", | ||||
|             "name": "org", | ||||
|             "in": "path", | ||||
|             "required": true | ||||
|           }, | ||||
|           { | ||||
|             "type": "string", | ||||
|             "description": "id of the runner", | ||||
|             "name": "runner_id", | ||||
|             "in": "path", | ||||
|             "required": true | ||||
|           } | ||||
|         ], | ||||
|         "responses": { | ||||
|           "200": { | ||||
|             "$ref": "#/definitions/ActionRunner" | ||||
|           }, | ||||
|           "400": { | ||||
|             "$ref": "#/responses/error" | ||||
|           }, | ||||
|           "404": { | ||||
|             "$ref": "#/responses/notFound" | ||||
|           } | ||||
|         } | ||||
|       }, | ||||
|       "delete": { | ||||
|         "produces": [ | ||||
|           "application/json" | ||||
|         ], | ||||
|         "tags": [ | ||||
|           "organization" | ||||
|         ], | ||||
|         "summary": "Delete an org-level runner", | ||||
|         "operationId": "deleteOrgRunner", | ||||
|         "parameters": [ | ||||
|           { | ||||
|             "type": "string", | ||||
|             "description": "name of the organization", | ||||
|             "name": "org", | ||||
|             "in": "path", | ||||
|             "required": true | ||||
|           }, | ||||
|           { | ||||
|             "type": "string", | ||||
|             "description": "id of the runner", | ||||
|             "name": "runner_id", | ||||
|             "in": "path", | ||||
|             "required": true | ||||
|           } | ||||
|         ], | ||||
|         "responses": { | ||||
|           "204": { | ||||
|             "description": "runner has been deleted" | ||||
|           }, | ||||
|           "400": { | ||||
|             "$ref": "#/responses/error" | ||||
|           }, | ||||
|           "404": { | ||||
|             "$ref": "#/responses/notFound" | ||||
|           } | ||||
|         } | ||||
|       } | ||||
|     }, | ||||
|     "/orgs/{org}/actions/secrets": { | ||||
| @ -4331,6 +4565,45 @@ | ||||
|         } | ||||
|       } | ||||
|     }, | ||||
|     "/repos/{owner}/{repo}/actions/runners": { | ||||
|       "get": { | ||||
|         "produces": [ | ||||
|           "application/json" | ||||
|         ], | ||||
|         "tags": [ | ||||
|           "repository" | ||||
|         ], | ||||
|         "summary": "Get repo-level runners", | ||||
|         "operationId": "getRepoRunners", | ||||
|         "parameters": [ | ||||
|           { | ||||
|             "type": "string", | ||||
|             "description": "owner of the repo", | ||||
|             "name": "owner", | ||||
|             "in": "path", | ||||
|             "required": true | ||||
|           }, | ||||
|           { | ||||
|             "type": "string", | ||||
|             "description": "name of the repo", | ||||
|             "name": "repo", | ||||
|             "in": "path", | ||||
|             "required": true | ||||
|           } | ||||
|         ], | ||||
|         "responses": { | ||||
|           "200": { | ||||
|             "$ref": "#/definitions/ActionRunnersResponse" | ||||
|           }, | ||||
|           "400": { | ||||
|             "$ref": "#/responses/error" | ||||
|           }, | ||||
|           "404": { | ||||
|             "$ref": "#/responses/notFound" | ||||
|           } | ||||
|         } | ||||
|       } | ||||
|     }, | ||||
|     "/repos/{owner}/{repo}/actions/runners/registration-token": { | ||||
|       "get": { | ||||
|         "produces": [ | ||||
| @ -4362,6 +4635,127 @@ | ||||
|             "$ref": "#/responses/RegistrationToken" | ||||
|           } | ||||
|         } | ||||
|       }, | ||||
|       "post": { | ||||
|         "produces": [ | ||||
|           "application/json" | ||||
|         ], | ||||
|         "tags": [ | ||||
|           "repository" | ||||
|         ], | ||||
|         "summary": "Get a repository's actions runner registration token", | ||||
|         "operationId": "repoCreateRunnerRegistrationToken", | ||||
|         "parameters": [ | ||||
|           { | ||||
|             "type": "string", | ||||
|             "description": "owner of the repo", | ||||
|             "name": "owner", | ||||
|             "in": "path", | ||||
|             "required": true | ||||
|           }, | ||||
|           { | ||||
|             "type": "string", | ||||
|             "description": "name of the repo", | ||||
|             "name": "repo", | ||||
|             "in": "path", | ||||
|             "required": true | ||||
|           } | ||||
|         ], | ||||
|         "responses": { | ||||
|           "200": { | ||||
|             "$ref": "#/responses/RegistrationToken" | ||||
|           } | ||||
|         } | ||||
|       } | ||||
|     }, | ||||
|     "/repos/{owner}/{repo}/actions/runners/{runner_id}": { | ||||
|       "get": { | ||||
|         "produces": [ | ||||
|           "application/json" | ||||
|         ], | ||||
|         "tags": [ | ||||
|           "repository" | ||||
|         ], | ||||
|         "summary": "Get an repo-level runner", | ||||
|         "operationId": "getRepoRunner", | ||||
|         "parameters": [ | ||||
|           { | ||||
|             "type": "string", | ||||
|             "description": "owner of the repo", | ||||
|             "name": "owner", | ||||
|             "in": "path", | ||||
|             "required": true | ||||
|           }, | ||||
|           { | ||||
|             "type": "string", | ||||
|             "description": "name of the repo", | ||||
|             "name": "repo", | ||||
|             "in": "path", | ||||
|             "required": true | ||||
|           }, | ||||
|           { | ||||
|             "type": "string", | ||||
|             "description": "id of the runner", | ||||
|             "name": "runner_id", | ||||
|             "in": "path", | ||||
|             "required": true | ||||
|           } | ||||
|         ], | ||||
|         "responses": { | ||||
|           "200": { | ||||
|             "$ref": "#/definitions/ActionRunner" | ||||
|           }, | ||||
|           "400": { | ||||
|             "$ref": "#/responses/error" | ||||
|           }, | ||||
|           "404": { | ||||
|             "$ref": "#/responses/notFound" | ||||
|           } | ||||
|         } | ||||
|       }, | ||||
|       "delete": { | ||||
|         "produces": [ | ||||
|           "application/json" | ||||
|         ], | ||||
|         "tags": [ | ||||
|           "repository" | ||||
|         ], | ||||
|         "summary": "Delete an repo-level runner", | ||||
|         "operationId": "deleteRepoRunner", | ||||
|         "parameters": [ | ||||
|           { | ||||
|             "type": "string", | ||||
|             "description": "owner of the repo", | ||||
|             "name": "owner", | ||||
|             "in": "path", | ||||
|             "required": true | ||||
|           }, | ||||
|           { | ||||
|             "type": "string", | ||||
|             "description": "name of the repo", | ||||
|             "name": "repo", | ||||
|             "in": "path", | ||||
|             "required": true | ||||
|           }, | ||||
|           { | ||||
|             "type": "string", | ||||
|             "description": "id of the runner", | ||||
|             "name": "runner_id", | ||||
|             "in": "path", | ||||
|             "required": true | ||||
|           } | ||||
|         ], | ||||
|         "responses": { | ||||
|           "204": { | ||||
|             "description": "runner has been deleted" | ||||
|           }, | ||||
|           "400": { | ||||
|             "$ref": "#/responses/error" | ||||
|           }, | ||||
|           "404": { | ||||
|             "$ref": "#/responses/notFound" | ||||
|           } | ||||
|         } | ||||
|       } | ||||
|     }, | ||||
|     "/repos/{owner}/{repo}/actions/runs/{run}/artifacts": { | ||||
| @ -4559,7 +4953,7 @@ | ||||
|         ], | ||||
|         "responses": { | ||||
|           "204": { | ||||
|             "description": "delete one secret of the organization" | ||||
|             "description": "delete one secret of the repository" | ||||
|           }, | ||||
|           "400": { | ||||
|             "$ref": "#/responses/error" | ||||
| @ -16869,6 +17263,29 @@ | ||||
|         } | ||||
|       } | ||||
|     }, | ||||
|     "/user/actions/runners": { | ||||
|       "get": { | ||||
|         "produces": [ | ||||
|           "application/json" | ||||
|         ], | ||||
|         "tags": [ | ||||
|           "user" | ||||
|         ], | ||||
|         "summary": "Get user-level runners", | ||||
|         "operationId": "getUserRunners", | ||||
|         "responses": { | ||||
|           "200": { | ||||
|             "$ref": "#/definitions/ActionRunnersResponse" | ||||
|           }, | ||||
|           "400": { | ||||
|             "$ref": "#/responses/error" | ||||
|           }, | ||||
|           "404": { | ||||
|             "$ref": "#/responses/notFound" | ||||
|           } | ||||
|         } | ||||
|       } | ||||
|     }, | ||||
|     "/user/actions/runners/registration-token": { | ||||
|       "get": { | ||||
|         "produces": [ | ||||
| @ -16884,6 +17301,83 @@ | ||||
|             "$ref": "#/responses/RegistrationToken" | ||||
|           } | ||||
|         } | ||||
|       }, | ||||
|       "post": { | ||||
|         "produces": [ | ||||
|           "application/json" | ||||
|         ], | ||||
|         "tags": [ | ||||
|           "user" | ||||
|         ], | ||||
|         "summary": "Get an user's actions runner registration token", | ||||
|         "operationId": "userCreateRunnerRegistrationToken", | ||||
|         "responses": { | ||||
|           "200": { | ||||
|             "$ref": "#/responses/RegistrationToken" | ||||
|           } | ||||
|         } | ||||
|       } | ||||
|     }, | ||||
|     "/user/actions/runners/{runner_id}": { | ||||
|       "get": { | ||||
|         "produces": [ | ||||
|           "application/json" | ||||
|         ], | ||||
|         "tags": [ | ||||
|           "user" | ||||
|         ], | ||||
|         "summary": "Get an user-level runner", | ||||
|         "operationId": "getUserRunner", | ||||
|         "parameters": [ | ||||
|           { | ||||
|             "type": "string", | ||||
|             "description": "id of the runner", | ||||
|             "name": "runner_id", | ||||
|             "in": "path", | ||||
|             "required": true | ||||
|           } | ||||
|         ], | ||||
|         "responses": { | ||||
|           "200": { | ||||
|             "$ref": "#/definitions/ActionRunner" | ||||
|           }, | ||||
|           "400": { | ||||
|             "$ref": "#/responses/error" | ||||
|           }, | ||||
|           "404": { | ||||
|             "$ref": "#/responses/notFound" | ||||
|           } | ||||
|         } | ||||
|       }, | ||||
|       "delete": { | ||||
|         "produces": [ | ||||
|           "application/json" | ||||
|         ], | ||||
|         "tags": [ | ||||
|           "user" | ||||
|         ], | ||||
|         "summary": "Delete an user-level runner", | ||||
|         "operationId": "deleteUserRunner", | ||||
|         "parameters": [ | ||||
|           { | ||||
|             "type": "string", | ||||
|             "description": "id of the runner", | ||||
|             "name": "runner_id", | ||||
|             "in": "path", | ||||
|             "required": true | ||||
|           } | ||||
|         ], | ||||
|         "responses": { | ||||
|           "204": { | ||||
|             "description": "runner has been deleted" | ||||
|           }, | ||||
|           "400": { | ||||
|             "$ref": "#/responses/error" | ||||
|           }, | ||||
|           "404": { | ||||
|             "$ref": "#/responses/notFound" | ||||
|           } | ||||
|         } | ||||
|       } | ||||
|     }, | ||||
|     "/user/actions/secrets/{secretname}": { | ||||
| @ -19377,6 +19871,80 @@ | ||||
|       }, | ||||
|       "x-go-package": "code.gitea.io/gitea/modules/structs" | ||||
|     }, | ||||
|     "ActionRunner": { | ||||
|       "description": "ActionRunner represents a Runner", | ||||
|       "type": "object", | ||||
|       "properties": { | ||||
|         "busy": { | ||||
|           "type": "boolean", | ||||
|           "x-go-name": "Busy" | ||||
|         }, | ||||
|         "ephemeral": { | ||||
|           "type": "boolean", | ||||
|           "x-go-name": "Ephemeral" | ||||
|         }, | ||||
|         "id": { | ||||
|           "type": "integer", | ||||
|           "format": "int64", | ||||
|           "x-go-name": "ID" | ||||
|         }, | ||||
|         "labels": { | ||||
|           "type": "array", | ||||
|           "items": { | ||||
|             "$ref": "#/definitions/ActionRunnerLabel" | ||||
|           }, | ||||
|           "x-go-name": "Labels" | ||||
|         }, | ||||
|         "name": { | ||||
|           "type": "string", | ||||
|           "x-go-name": "Name" | ||||
|         }, | ||||
|         "status": { | ||||
|           "type": "string", | ||||
|           "x-go-name": "Status" | ||||
|         } | ||||
|       }, | ||||
|       "x-go-package": "code.gitea.io/gitea/modules/structs" | ||||
|     }, | ||||
|     "ActionRunnerLabel": { | ||||
|       "description": "ActionRunnerLabel represents a Runner Label", | ||||
|       "type": "object", | ||||
|       "properties": { | ||||
|         "id": { | ||||
|           "type": "integer", | ||||
|           "format": "int64", | ||||
|           "x-go-name": "ID" | ||||
|         }, | ||||
|         "name": { | ||||
|           "type": "string", | ||||
|           "x-go-name": "Name" | ||||
|         }, | ||||
|         "type": { | ||||
|           "type": "string", | ||||
|           "x-go-name": "Type" | ||||
|         } | ||||
|       }, | ||||
|       "x-go-package": "code.gitea.io/gitea/modules/structs" | ||||
|     }, | ||||
|     "ActionRunnersResponse": { | ||||
|       "description": "ActionRunnersResponse returns Runners", | ||||
|       "type": "object", | ||||
|       "properties": { | ||||
|         "runners": { | ||||
|           "type": "array", | ||||
|           "items": { | ||||
|             "$ref": "#/definitions/ActionRunner" | ||||
|           }, | ||||
|           "x-go-name": "Entries" | ||||
|         }, | ||||
|         "total_count": { | ||||
|           "type": "integer", | ||||
|           "format": "int64", | ||||
|           "x-go-name": "TotalCount" | ||||
|         } | ||||
|       }, | ||||
|       "x-go-package": "code.gitea.io/gitea/modules/structs" | ||||
|     }, | ||||
|     "ActionTask": { | ||||
|       "description": "ActionTask represents a ActionTask", | ||||
|       "type": "object", | ||||
| @ -27409,6 +27977,18 @@ | ||||
|         } | ||||
|       } | ||||
|     }, | ||||
|     "Runner": { | ||||
|       "description": "Runner", | ||||
|       "schema": { | ||||
|         "$ref": "#/definitions/ActionRunner" | ||||
|       } | ||||
|     }, | ||||
|     "RunnerList": { | ||||
|       "description": "RunnerList", | ||||
|       "schema": { | ||||
|         "$ref": "#/definitions/ActionRunnersResponse" | ||||
|       } | ||||
|     }, | ||||
|     "SearchResults": { | ||||
|       "description": "SearchResults", | ||||
|       "schema": { | ||||
|  | ||||
							
								
								
									
										332
									
								
								tests/integration/api_actions_runner_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										332
									
								
								tests/integration/api_actions_runner_test.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,332 @@ | ||||
| // Copyright 2025 The Gitea Authors. All rights reserved. | ||||
| // SPDX-License-Identifier: MIT | ||||
| 
 | ||||
| package integration | ||||
| 
 | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"net/http" | ||||
| 	"slices" | ||||
| 	"testing" | ||||
| 
 | ||||
| 	auth_model "code.gitea.io/gitea/models/auth" | ||||
| 	api "code.gitea.io/gitea/modules/structs" | ||||
| 	"code.gitea.io/gitea/tests" | ||||
| 
 | ||||
| 	"github.com/stretchr/testify/assert" | ||||
| 	"github.com/stretchr/testify/require" | ||||
| ) | ||||
| 
 | ||||
| func TestAPIActionsRunner(t *testing.T) { | ||||
| 	t.Run("AdminRunner", testActionsRunnerAdmin) | ||||
| 	t.Run("UserRunner", testActionsRunnerUser) | ||||
| 	t.Run("OwnerRunner", testActionsRunnerOwner) | ||||
| 	t.Run("RepoRunner", testActionsRunnerRepo) | ||||
| } | ||||
| 
 | ||||
| func testActionsRunnerAdmin(t *testing.T) { | ||||
| 	defer tests.PrepareTestEnv(t)() | ||||
| 	adminUsername := "user1" | ||||
| 	token := getUserToken(t, adminUsername, auth_model.AccessTokenScopeWriteAdmin) | ||||
| 	req := NewRequest(t, "POST", "/api/v1/admin/actions/runners/registration-token").AddTokenAuth(token) | ||||
| 	tokenResp := MakeRequest(t, req, http.StatusOK) | ||||
| 	var registrationToken struct { | ||||
| 		Token string `json:"token"` | ||||
| 	} | ||||
| 	DecodeJSON(t, tokenResp, ®istrationToken) | ||||
| 	assert.NotEmpty(t, registrationToken.Token) | ||||
| 
 | ||||
| 	req = NewRequest(t, "GET", "/api/v1/admin/actions/runners").AddTokenAuth(token) | ||||
| 	runnerListResp := MakeRequest(t, req, http.StatusOK) | ||||
| 	runnerList := api.ActionRunnersResponse{} | ||||
| 	DecodeJSON(t, runnerListResp, &runnerList) | ||||
| 
 | ||||
| 	assert.Len(t, runnerList.Entries, 4) | ||||
| 
 | ||||
| 	idx := slices.IndexFunc(runnerList.Entries, func(e *api.ActionRunner) bool { return e.ID == 34349 }) | ||||
| 	require.NotEqual(t, -1, idx) | ||||
| 	expectedRunner := runnerList.Entries[idx] | ||||
| 	assert.Equal(t, "runner_to_be_deleted", expectedRunner.Name) | ||||
| 	assert.False(t, expectedRunner.Ephemeral) | ||||
| 	assert.Len(t, expectedRunner.Labels, 2) | ||||
| 	assert.Equal(t, "runner_to_be_deleted", expectedRunner.Labels[0].Name) | ||||
| 	assert.Equal(t, "linux", expectedRunner.Labels[1].Name) | ||||
| 
 | ||||
| 	// Verify all returned runners can be requested and deleted | ||||
| 	for _, runnerEntry := range runnerList.Entries { | ||||
| 		// Verify get the runner by id | ||||
| 		req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/admin/actions/runners/%d", runnerEntry.ID)).AddTokenAuth(token) | ||||
| 		runnerResp := MakeRequest(t, req, http.StatusOK) | ||||
| 
 | ||||
| 		runner := api.ActionRunner{} | ||||
| 		DecodeJSON(t, runnerResp, &runner) | ||||
| 
 | ||||
| 		assert.Equal(t, runnerEntry.Name, runner.Name) | ||||
| 		assert.Equal(t, runnerEntry.ID, runner.ID) | ||||
| 		assert.Equal(t, runnerEntry.Ephemeral, runner.Ephemeral) | ||||
| 		assert.ElementsMatch(t, runnerEntry.Labels, runner.Labels) | ||||
| 
 | ||||
| 		req = NewRequest(t, "DELETE", fmt.Sprintf("/api/v1/admin/actions/runners/%d", runnerEntry.ID)).AddTokenAuth(token) | ||||
| 		MakeRequest(t, req, http.StatusNoContent) | ||||
| 
 | ||||
| 		// Verify runner deletion | ||||
| 		req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/admin/actions/runners/%d", runnerEntry.ID)).AddTokenAuth(token) | ||||
| 		MakeRequest(t, req, http.StatusNotFound) | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func testActionsRunnerUser(t *testing.T) { | ||||
| 	defer tests.PrepareTestEnv(t)() | ||||
| 	userUsername := "user1" | ||||
| 	token := getUserToken(t, userUsername, auth_model.AccessTokenScopeWriteUser) | ||||
| 	req := NewRequest(t, "POST", "/api/v1/user/actions/runners/registration-token").AddTokenAuth(token) | ||||
| 	tokenResp := MakeRequest(t, req, http.StatusOK) | ||||
| 	var registrationToken struct { | ||||
| 		Token string `json:"token"` | ||||
| 	} | ||||
| 	DecodeJSON(t, tokenResp, ®istrationToken) | ||||
| 	assert.NotEmpty(t, registrationToken.Token) | ||||
| 
 | ||||
| 	req = NewRequest(t, "GET", "/api/v1/user/actions/runners").AddTokenAuth(token) | ||||
| 	runnerListResp := MakeRequest(t, req, http.StatusOK) | ||||
| 	runnerList := api.ActionRunnersResponse{} | ||||
| 	DecodeJSON(t, runnerListResp, &runnerList) | ||||
| 
 | ||||
| 	assert.Len(t, runnerList.Entries, 1) | ||||
| 	assert.Equal(t, "runner_to_be_deleted-user", runnerList.Entries[0].Name) | ||||
| 	assert.Equal(t, int64(34346), runnerList.Entries[0].ID) | ||||
| 	assert.False(t, runnerList.Entries[0].Ephemeral) | ||||
| 	assert.Len(t, runnerList.Entries[0].Labels, 2) | ||||
| 	assert.Equal(t, "runner_to_be_deleted", runnerList.Entries[0].Labels[0].Name) | ||||
| 	assert.Equal(t, "linux", runnerList.Entries[0].Labels[1].Name) | ||||
| 
 | ||||
| 	// Verify get the runner by id | ||||
| 	req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/user/actions/runners/%d", runnerList.Entries[0].ID)).AddTokenAuth(token) | ||||
| 	runnerResp := MakeRequest(t, req, http.StatusOK) | ||||
| 
 | ||||
| 	runner := api.ActionRunner{} | ||||
| 	DecodeJSON(t, runnerResp, &runner) | ||||
| 
 | ||||
| 	assert.Equal(t, "runner_to_be_deleted-user", runner.Name) | ||||
| 	assert.Equal(t, int64(34346), runner.ID) | ||||
| 	assert.False(t, runner.Ephemeral) | ||||
| 	assert.Len(t, runner.Labels, 2) | ||||
| 	assert.Equal(t, "runner_to_be_deleted", runner.Labels[0].Name) | ||||
| 	assert.Equal(t, "linux", runner.Labels[1].Name) | ||||
| 
 | ||||
| 	// Verify delete the runner by id | ||||
| 	req = NewRequest(t, "DELETE", fmt.Sprintf("/api/v1/user/actions/runners/%d", runnerList.Entries[0].ID)).AddTokenAuth(token) | ||||
| 	MakeRequest(t, req, http.StatusNoContent) | ||||
| 
 | ||||
| 	// Verify runner deletion | ||||
| 	req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/user/actions/runners/%d", runnerList.Entries[0].ID)).AddTokenAuth(token) | ||||
| 	MakeRequest(t, req, http.StatusNotFound) | ||||
| } | ||||
| 
 | ||||
| func testActionsRunnerOwner(t *testing.T) { | ||||
| 	defer tests.PrepareTestEnv(t)() | ||||
| 
 | ||||
| 	t.Run("GetRunner", func(t *testing.T) { | ||||
| 		userUsername := "user2" | ||||
| 		token := getUserToken(t, userUsername, auth_model.AccessTokenScopeReadOrganization) | ||||
| 		// Verify get the runner by id with read scope | ||||
| 		req := NewRequest(t, "GET", fmt.Sprintf("/api/v1/orgs/org3/actions/runners/%d", 34347)).AddTokenAuth(token) | ||||
| 		runnerResp := MakeRequest(t, req, http.StatusOK) | ||||
| 
 | ||||
| 		runner := api.ActionRunner{} | ||||
| 		DecodeJSON(t, runnerResp, &runner) | ||||
| 
 | ||||
| 		assert.Equal(t, "runner_to_be_deleted-org", runner.Name) | ||||
| 		assert.Equal(t, int64(34347), runner.ID) | ||||
| 		assert.False(t, runner.Ephemeral) | ||||
| 		assert.Len(t, runner.Labels, 2) | ||||
| 		assert.Equal(t, "runner_to_be_deleted", runner.Labels[0].Name) | ||||
| 		assert.Equal(t, "linux", runner.Labels[1].Name) | ||||
| 	}) | ||||
| 
 | ||||
| 	t.Run("Access", func(t *testing.T) { | ||||
| 		userUsername := "user2" | ||||
| 		token := getUserToken(t, userUsername, auth_model.AccessTokenScopeWriteOrganization) | ||||
| 		req := NewRequest(t, "POST", "/api/v1/orgs/org3/actions/runners/registration-token").AddTokenAuth(token) | ||||
| 		tokenResp := MakeRequest(t, req, http.StatusOK) | ||||
| 		var registrationToken struct { | ||||
| 			Token string `json:"token"` | ||||
| 		} | ||||
| 		DecodeJSON(t, tokenResp, ®istrationToken) | ||||
| 		assert.NotEmpty(t, registrationToken.Token) | ||||
| 
 | ||||
| 		req = NewRequest(t, "GET", "/api/v1/orgs/org3/actions/runners").AddTokenAuth(token) | ||||
| 		runnerListResp := MakeRequest(t, req, http.StatusOK) | ||||
| 		runnerList := api.ActionRunnersResponse{} | ||||
| 		DecodeJSON(t, runnerListResp, &runnerList) | ||||
| 
 | ||||
| 		assert.Len(t, runnerList.Entries, 1) | ||||
| 		assert.Equal(t, "runner_to_be_deleted-org", runnerList.Entries[0].Name) | ||||
| 		assert.Equal(t, int64(34347), runnerList.Entries[0].ID) | ||||
| 		assert.False(t, runnerList.Entries[0].Ephemeral) | ||||
| 		assert.Len(t, runnerList.Entries[0].Labels, 2) | ||||
| 		assert.Equal(t, "runner_to_be_deleted", runnerList.Entries[0].Labels[0].Name) | ||||
| 		assert.Equal(t, "linux", runnerList.Entries[0].Labels[1].Name) | ||||
| 
 | ||||
| 		// Verify get the runner by id | ||||
| 		req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/orgs/org3/actions/runners/%d", runnerList.Entries[0].ID)).AddTokenAuth(token) | ||||
| 		runnerResp := MakeRequest(t, req, http.StatusOK) | ||||
| 
 | ||||
| 		runner := api.ActionRunner{} | ||||
| 		DecodeJSON(t, runnerResp, &runner) | ||||
| 
 | ||||
| 		assert.Equal(t, "runner_to_be_deleted-org", runner.Name) | ||||
| 		assert.Equal(t, int64(34347), runner.ID) | ||||
| 		assert.False(t, runner.Ephemeral) | ||||
| 		assert.Len(t, runner.Labels, 2) | ||||
| 		assert.Equal(t, "runner_to_be_deleted", runner.Labels[0].Name) | ||||
| 		assert.Equal(t, "linux", runner.Labels[1].Name) | ||||
| 
 | ||||
| 		// Verify delete the runner by id | ||||
| 		req = NewRequest(t, "DELETE", fmt.Sprintf("/api/v1/orgs/org3/actions/runners/%d", runnerList.Entries[0].ID)).AddTokenAuth(token) | ||||
| 		MakeRequest(t, req, http.StatusNoContent) | ||||
| 
 | ||||
| 		// Verify runner deletion | ||||
| 		req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/orgs/org3/actions/runners/%d", runnerList.Entries[0].ID)).AddTokenAuth(token) | ||||
| 		MakeRequest(t, req, http.StatusNotFound) | ||||
| 	}) | ||||
| 
 | ||||
| 	t.Run("DeleteReadScopeForbidden", func(t *testing.T) { | ||||
| 		userUsername := "user2" | ||||
| 		token := getUserToken(t, userUsername, auth_model.AccessTokenScopeReadOrganization) | ||||
| 
 | ||||
| 		// Verify delete the runner by id is forbidden with read scope | ||||
| 		req := NewRequest(t, "DELETE", fmt.Sprintf("/api/v1/orgs/org3/actions/runners/%d", 34347)).AddTokenAuth(token) | ||||
| 		MakeRequest(t, req, http.StatusForbidden) | ||||
| 	}) | ||||
| 
 | ||||
| 	t.Run("GetRepoScopeForbidden", func(t *testing.T) { | ||||
| 		userUsername := "user2" | ||||
| 		token := getUserToken(t, userUsername, auth_model.AccessTokenScopeReadRepository) | ||||
| 		// Verify get the runner by id with read scope | ||||
| 		req := NewRequest(t, "GET", fmt.Sprintf("/api/v1/orgs/org3/actions/runners/%d", 34347)).AddTokenAuth(token) | ||||
| 		MakeRequest(t, req, http.StatusForbidden) | ||||
| 	}) | ||||
| 
 | ||||
| 	t.Run("GetAdminRunner", func(t *testing.T) { | ||||
| 		userUsername := "user2" | ||||
| 		token := getUserToken(t, userUsername, auth_model.AccessTokenScopeReadOrganization) | ||||
| 		// Verify get a runner by id of different entity is not found | ||||
| 		// runner.EditableInContext(ownerID, repoID) false | ||||
| 		req := NewRequest(t, "GET", fmt.Sprintf("/api/v1/orgs/org3/actions/runners/%d", 34349)).AddTokenAuth(token) | ||||
| 		MakeRequest(t, req, http.StatusNotFound) | ||||
| 	}) | ||||
| 
 | ||||
| 	t.Run("DeleteAdminRunner", func(t *testing.T) { | ||||
| 		userUsername := "user2" | ||||
| 		token := getUserToken(t, userUsername, auth_model.AccessTokenScopeWriteOrganization) | ||||
| 		// Verify delete a runner by id of different entity is not found | ||||
| 		// runner.EditableInContext(ownerID, repoID) false | ||||
| 		req := NewRequest(t, "DELETE", fmt.Sprintf("/api/v1/orgs/org3/actions/runners/%d", 34349)).AddTokenAuth(token) | ||||
| 		MakeRequest(t, req, http.StatusNotFound) | ||||
| 	}) | ||||
| } | ||||
| 
 | ||||
| func testActionsRunnerRepo(t *testing.T) { | ||||
| 	defer tests.PrepareTestEnv(t)() | ||||
| 
 | ||||
| 	t.Run("GetRunner", func(t *testing.T) { | ||||
| 		userUsername := "user2" | ||||
| 		token := getUserToken(t, userUsername, auth_model.AccessTokenScopeReadRepository) | ||||
| 		// Verify get the runner by id with read scope | ||||
| 		req := NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/user2/repo1/actions/runners/%d", 34348)).AddTokenAuth(token) | ||||
| 		runnerResp := MakeRequest(t, req, http.StatusOK) | ||||
| 
 | ||||
| 		runner := api.ActionRunner{} | ||||
| 		DecodeJSON(t, runnerResp, &runner) | ||||
| 
 | ||||
| 		assert.Equal(t, "runner_to_be_deleted-repo1", runner.Name) | ||||
| 		assert.Equal(t, int64(34348), runner.ID) | ||||
| 		assert.False(t, runner.Ephemeral) | ||||
| 		assert.Len(t, runner.Labels, 2) | ||||
| 		assert.Equal(t, "runner_to_be_deleted", runner.Labels[0].Name) | ||||
| 		assert.Equal(t, "linux", runner.Labels[1].Name) | ||||
| 	}) | ||||
| 
 | ||||
| 	t.Run("Access", func(t *testing.T) { | ||||
| 		userUsername := "user2" | ||||
| 		token := getUserToken(t, userUsername, auth_model.AccessTokenScopeWriteRepository) | ||||
| 		req := NewRequest(t, "POST", "/api/v1/repos/user2/repo1/actions/runners/registration-token").AddTokenAuth(token) | ||||
| 		tokenResp := MakeRequest(t, req, http.StatusOK) | ||||
| 		var registrationToken struct { | ||||
| 			Token string `json:"token"` | ||||
| 		} | ||||
| 		DecodeJSON(t, tokenResp, ®istrationToken) | ||||
| 		assert.NotEmpty(t, registrationToken.Token) | ||||
| 
 | ||||
| 		req = NewRequest(t, "GET", "/api/v1/repos/user2/repo1/actions/runners").AddTokenAuth(token) | ||||
| 		runnerListResp := MakeRequest(t, req, http.StatusOK) | ||||
| 		runnerList := api.ActionRunnersResponse{} | ||||
| 		DecodeJSON(t, runnerListResp, &runnerList) | ||||
| 
 | ||||
| 		assert.Len(t, runnerList.Entries, 1) | ||||
| 		assert.Equal(t, "runner_to_be_deleted-repo1", runnerList.Entries[0].Name) | ||||
| 		assert.Equal(t, int64(34348), runnerList.Entries[0].ID) | ||||
| 		assert.False(t, runnerList.Entries[0].Ephemeral) | ||||
| 		assert.Len(t, runnerList.Entries[0].Labels, 2) | ||||
| 		assert.Equal(t, "runner_to_be_deleted", runnerList.Entries[0].Labels[0].Name) | ||||
| 		assert.Equal(t, "linux", runnerList.Entries[0].Labels[1].Name) | ||||
| 
 | ||||
| 		// Verify get the runner by id | ||||
| 		req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/user2/repo1/actions/runners/%d", runnerList.Entries[0].ID)).AddTokenAuth(token) | ||||
| 		runnerResp := MakeRequest(t, req, http.StatusOK) | ||||
| 
 | ||||
| 		runner := api.ActionRunner{} | ||||
| 		DecodeJSON(t, runnerResp, &runner) | ||||
| 
 | ||||
| 		assert.Equal(t, "runner_to_be_deleted-repo1", runner.Name) | ||||
| 		assert.Equal(t, int64(34348), runner.ID) | ||||
| 		assert.False(t, runner.Ephemeral) | ||||
| 		assert.Len(t, runner.Labels, 2) | ||||
| 		assert.Equal(t, "runner_to_be_deleted", runner.Labels[0].Name) | ||||
| 		assert.Equal(t, "linux", runner.Labels[1].Name) | ||||
| 
 | ||||
| 		// Verify delete the runner by id | ||||
| 		req = NewRequest(t, "DELETE", fmt.Sprintf("/api/v1/repos/user2/repo1/actions/runners/%d", runnerList.Entries[0].ID)).AddTokenAuth(token) | ||||
| 		MakeRequest(t, req, http.StatusNoContent) | ||||
| 
 | ||||
| 		// Verify runner deletion | ||||
| 		req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/user2/repo1/actions/runners/%d", runnerList.Entries[0].ID)).AddTokenAuth(token) | ||||
| 		MakeRequest(t, req, http.StatusNotFound) | ||||
| 	}) | ||||
| 
 | ||||
| 	t.Run("DeleteReadScopeForbidden", func(t *testing.T) { | ||||
| 		userUsername := "user2" | ||||
| 		token := getUserToken(t, userUsername, auth_model.AccessTokenScopeReadRepository) | ||||
| 
 | ||||
| 		// Verify delete the runner by id is forbidden with read scope | ||||
| 		req := NewRequest(t, "DELETE", fmt.Sprintf("/api/v1/repos/user2/repo1/actions/runners/%d", 34348)).AddTokenAuth(token) | ||||
| 		MakeRequest(t, req, http.StatusForbidden) | ||||
| 	}) | ||||
| 
 | ||||
| 	t.Run("GetOrganizationScopeForbidden", func(t *testing.T) { | ||||
| 		userUsername := "user2" | ||||
| 		token := getUserToken(t, userUsername, auth_model.AccessTokenScopeReadOrganization) | ||||
| 		// Verify get the runner by id with read scope | ||||
| 		req := NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/user2/repo1/actions/runners/%d", 34348)).AddTokenAuth(token) | ||||
| 		MakeRequest(t, req, http.StatusForbidden) | ||||
| 	}) | ||||
| 
 | ||||
| 	t.Run("GetAdminRunnerNotFound", func(t *testing.T) { | ||||
| 		userUsername := "user2" | ||||
| 		token := getUserToken(t, userUsername, auth_model.AccessTokenScopeReadRepository) | ||||
| 		// Verify get a runner by id of different entity is not found | ||||
| 		// runner.EditableInContext(ownerID, repoID) false | ||||
| 		req := NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/user2/repo1/actions/runners/%d", 34349)).AddTokenAuth(token) | ||||
| 		MakeRequest(t, req, http.StatusNotFound) | ||||
| 	}) | ||||
| 
 | ||||
| 	t.Run("DeleteAdminRunnerNotFound", func(t *testing.T) { | ||||
| 		userUsername := "user2" | ||||
| 		token := getUserToken(t, userUsername, auth_model.AccessTokenScopeWriteRepository) | ||||
| 		// Verify delete a runner by id of different entity is not found | ||||
| 		// runner.EditableInContext(ownerID, repoID) false | ||||
| 		req := NewRequest(t, "DELETE", fmt.Sprintf("/api/v1/repos/user2/repo1/actions/runners/%d", 34349)).AddTokenAuth(token) | ||||
| 		MakeRequest(t, req, http.StatusNotFound) | ||||
| 	}) | ||||
| } | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user