mirror of
https://codeberg.org/forgejo/forgejo.git
synced 2026-02-18 21:27:49 -05:00
feat: implement ephemeral runners (#9962)
As described in [this comment](https://gitea.com/gitea/act_runner/issues/19#issuecomment-739221) one-job runners are not secure when running in host mode. We implemented a routine preventing runner tokens from receiving a second job in order to render a potentially compromised token useless. Also we implemented a routine that removes finished runners as soon as possible. Big thanks to [ChristopherHX](https://github.com/ChristopherHX) who did all the work for gitea! Rel: #9407 ## Checklist The [contributor guide](https://forgejo.org/docs/next/contributor/) contains information that will be helpful to first time contributors. There also are a few [conditions for merging Pull Requests in Forgejo repositories](https://codeberg.org/forgejo/governance/src/branch/main/PullRequestsAgreement.md). You are also welcome to join the [Forgejo development chatroom](https://matrix.to/#/#forgejo-development:matrix.org). ### Tests - I added test coverage for Go changes... - [ ] in their respective `*_test.go` for unit tests. - [x] in the `tests/integration` directory if it involves interactions with a live Forgejo server. - I added test coverage for JavaScript changes... - [ ] in `web_src/js/*.test.js` if it can be unit tested. - [ ] in `tests/e2e/*.test.e2e.js` if it requires interactions with a live Forgejo server (see also the [developer guide for JavaScript testing](https://codeberg.org/forgejo/forgejo/src/branch/forgejo/tests/e2e/README.md#end-to-end-tests)). ### Documentation - [ ] I created a pull request [to the documentation](https://codeberg.org/forgejo/docs) to explain to Forgejo users how to use this change. - [ ] I did not document these changes and I do not expect someone else to do it. ### Release notes - [ ] I do not want this change to show in the release notes. - [ ] I want the title to show in the release notes with a link to this pull request. - [ ] I want the content of the `release-notes/<pull request number>.md` to be be used for the release notes instead of the title. Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/9962 Reviewed-by: Andreas Ahlenstorf <aahlenst@noreply.codeberg.org> Reviewed-by: Mathieu Fenniak <mfenniak@noreply.codeberg.org> Co-authored-by: Manuel Ganter <manuel.ganter@think-ahead.tech> Co-committed-by: Manuel Ganter <manuel.ganter@think-ahead.tech>
This commit is contained in:
parent
b085be779c
commit
5b6bbabd74
33 changed files with 1130 additions and 41 deletions
|
|
@ -102,6 +102,11 @@ func SubcmdActionsRegister(ctx context.Context) *cli.Command {
|
|||
Value: "",
|
||||
Usage: "version of the runner (not required since v1.21)",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "ephemeral",
|
||||
Value: false,
|
||||
Usage: "instruct Forgejo to permanently unregister this runner after it has run one job",
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
@ -172,6 +177,7 @@ func RunRegister(ctx context.Context, cli *cli.Command) error {
|
|||
scope := cli.String("scope")
|
||||
name := cli.String("name")
|
||||
version := cli.String("version")
|
||||
ephemeral := cli.Bool("ephemeral")
|
||||
labels, err := getLabels(cli)
|
||||
if err != nil {
|
||||
return err
|
||||
|
|
@ -199,7 +205,7 @@ func RunRegister(ctx context.Context, cli *cli.Command) error {
|
|||
return err
|
||||
}
|
||||
|
||||
runner, err := actions_model.RegisterRunner(ctx, owner, repo, secret, labels, name, version)
|
||||
runner, err := actions_model.RegisterRunner(ctx, owner, repo, secret, labels, name, version, ephemeral)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error while registering runner: %v", err)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ import (
|
|||
gouuid "github.com/google/uuid"
|
||||
)
|
||||
|
||||
func RegisterRunner(ctx context.Context, ownerID, repoID int64, token string, labels *[]string, name, version string) (*ActionRunner, error) {
|
||||
func RegisterRunner(ctx context.Context, ownerID, repoID int64, token string, labels *[]string, name, version string, ephemeral bool) (*ActionRunner, error) {
|
||||
uuid, err := gouuid.FromBytes([]byte(token[:16]))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("gouuid.FromBytes %v", err)
|
||||
|
|
@ -60,11 +60,12 @@ func RegisterRunner(ctx context.Context, ownerID, repoID int64, token string, la
|
|||
//
|
||||
name, _ = util.SplitStringAtByteN(name, 255)
|
||||
|
||||
cols := []string{"name", "owner_id", "repo_id", "version"}
|
||||
cols := []string{"name", "owner_id", "repo_id", "version", "ephemeral"}
|
||||
runner.Name = name
|
||||
runner.OwnerID = ownerID
|
||||
runner.RepoID = repoID
|
||||
runner.Version = version
|
||||
runner.Ephemeral = ephemeral
|
||||
if labels != nil {
|
||||
runner.AgentLabels = *labels
|
||||
cols = append(cols, "agent_labels")
|
||||
|
|
|
|||
|
|
@ -22,9 +22,11 @@ func TestActions_RegisterRunner_Token(t *testing.T) {
|
|||
labels := []string{}
|
||||
name := "runner"
|
||||
version := "v1.2.3"
|
||||
runner, err := RegisterRunner(db.DefaultContext, ownerID, repoID, token, &labels, name, version)
|
||||
ephemeral := true
|
||||
runner, err := RegisterRunner(db.DefaultContext, ownerID, repoID, token, &labels, name, version, ephemeral)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, name, runner.Name)
|
||||
assert.True(t, runner.Ephemeral)
|
||||
|
||||
assert.Equal(t, 1, subtle.ConstantTimeCompare([]byte(runner.TokenHash), []byte(auth_model.HashToken(token, runner.TokenSalt))), "the token cannot be verified with the same method as routers/api/actions/runner/interceptor.go as of 8228751c55d6a4263f0fec2932ca16181c09c97d")
|
||||
}
|
||||
|
|
@ -44,7 +46,7 @@ func TestActions_RegisterRunner_TokenUpdate(t *testing.T) {
|
|||
"the initial token should match the runner's secret",
|
||||
)
|
||||
|
||||
RegisterRunner(db.DefaultContext, before.OwnerID, before.RepoID, newToken, nil, before.Name, before.Version)
|
||||
RegisterRunner(db.DefaultContext, before.OwnerID, before.RepoID, newToken, nil, before.Name, before.Version, false)
|
||||
|
||||
after := unittest.AssertExistsAndLoadBean(t, &ActionRunner{ID: recordID})
|
||||
|
||||
|
|
@ -66,10 +68,11 @@ func TestActions_RegisterRunner_CreateWithLabels(t *testing.T) {
|
|||
token := "0123456789012345678901234567890123456789"
|
||||
name := "runner"
|
||||
version := "v1.2.3"
|
||||
ephemeral := true
|
||||
labels := []string{"woop", "doop"}
|
||||
labelsCopy := labels // labels may be affected by the tested function so we copy them
|
||||
|
||||
runner, err := RegisterRunner(db.DefaultContext, ownerID, repoID, token, &labels, name, version)
|
||||
runner, err := RegisterRunner(db.DefaultContext, ownerID, repoID, token, &labels, name, version, ephemeral)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Check that the returned record has been updated, except for the labels
|
||||
|
|
@ -78,6 +81,7 @@ func TestActions_RegisterRunner_CreateWithLabels(t *testing.T) {
|
|||
assert.Equal(t, name, runner.Name)
|
||||
assert.Equal(t, version, runner.Version)
|
||||
assert.Equal(t, labelsCopy, runner.AgentLabels)
|
||||
assert.Equal(t, ephemeral, runner.Ephemeral)
|
||||
|
||||
// Check that whatever is in the DB has been updated, except for the labels
|
||||
after := unittest.AssertExistsAndLoadBean(t, &ActionRunner{ID: runner.ID})
|
||||
|
|
@ -86,6 +90,7 @@ func TestActions_RegisterRunner_CreateWithLabels(t *testing.T) {
|
|||
assert.Equal(t, name, after.Name)
|
||||
assert.Equal(t, version, after.Version)
|
||||
assert.Equal(t, labelsCopy, after.AgentLabels)
|
||||
assert.Equal(t, ephemeral, after.Ephemeral)
|
||||
}
|
||||
|
||||
func TestActions_RegisterRunner_CreateWithoutLabels(t *testing.T) {
|
||||
|
|
@ -95,8 +100,9 @@ func TestActions_RegisterRunner_CreateWithoutLabels(t *testing.T) {
|
|||
token := "0123456789012345678901234567890123456789"
|
||||
name := "runner"
|
||||
version := "v1.2.3"
|
||||
ephemeral := true
|
||||
|
||||
runner, err := RegisterRunner(db.DefaultContext, ownerID, repoID, token, nil, name, version)
|
||||
runner, err := RegisterRunner(db.DefaultContext, ownerID, repoID, token, nil, name, version, ephemeral)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Check that the returned record has been updated, except for the labels
|
||||
|
|
@ -105,6 +111,7 @@ func TestActions_RegisterRunner_CreateWithoutLabels(t *testing.T) {
|
|||
assert.Equal(t, name, runner.Name)
|
||||
assert.Equal(t, version, runner.Version)
|
||||
assert.Equal(t, []string{}, runner.AgentLabels)
|
||||
assert.Equal(t, ephemeral, runner.Ephemeral)
|
||||
|
||||
// Check that whatever is in the DB has been updated, except for the labels
|
||||
after := unittest.AssertExistsAndLoadBean(t, &ActionRunner{ID: runner.ID})
|
||||
|
|
@ -113,6 +120,7 @@ func TestActions_RegisterRunner_CreateWithoutLabels(t *testing.T) {
|
|||
assert.Equal(t, name, after.Name)
|
||||
assert.Equal(t, version, after.Version)
|
||||
assert.Equal(t, []string{}, after.AgentLabels)
|
||||
assert.Equal(t, ephemeral, after.Ephemeral)
|
||||
}
|
||||
|
||||
func TestActions_RegisterRunner_UpdateWithLabels(t *testing.T) {
|
||||
|
|
@ -125,10 +133,11 @@ func TestActions_RegisterRunner_UpdateWithLabels(t *testing.T) {
|
|||
newRepoID := int64(1)
|
||||
newName := "rennur"
|
||||
newVersion := "v4.5.6"
|
||||
ephemeral := true
|
||||
newLabels := []string{"warp", "darp"}
|
||||
labelsCopy := newLabels // labels may be affected by the tested function so we copy them
|
||||
|
||||
runner, err := RegisterRunner(db.DefaultContext, newOwnerID, newRepoID, token, &newLabels, newName, newVersion)
|
||||
runner, err := RegisterRunner(db.DefaultContext, newOwnerID, newRepoID, token, &newLabels, newName, newVersion, ephemeral)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Check that the returned record has been updated
|
||||
|
|
@ -137,6 +146,7 @@ func TestActions_RegisterRunner_UpdateWithLabels(t *testing.T) {
|
|||
assert.Equal(t, newName, runner.Name)
|
||||
assert.Equal(t, newVersion, runner.Version)
|
||||
assert.Equal(t, labelsCopy, runner.AgentLabels)
|
||||
assert.Equal(t, ephemeral, runner.Ephemeral)
|
||||
|
||||
// Check that whatever is in the DB has been updated
|
||||
after := unittest.AssertExistsAndLoadBean(t, &ActionRunner{ID: recordID})
|
||||
|
|
@ -145,6 +155,7 @@ func TestActions_RegisterRunner_UpdateWithLabels(t *testing.T) {
|
|||
assert.Equal(t, newName, after.Name)
|
||||
assert.Equal(t, newVersion, after.Version)
|
||||
assert.Equal(t, labelsCopy, after.AgentLabels)
|
||||
assert.Equal(t, ephemeral, after.Ephemeral)
|
||||
}
|
||||
|
||||
func TestActions_RegisterRunner_UpdateWithoutLabels(t *testing.T) {
|
||||
|
|
@ -157,8 +168,9 @@ func TestActions_RegisterRunner_UpdateWithoutLabels(t *testing.T) {
|
|||
newRepoID := int64(1)
|
||||
newName := "rennur"
|
||||
newVersion := "v4.5.6"
|
||||
ephemeral := true
|
||||
|
||||
runner, err := RegisterRunner(db.DefaultContext, newOwnerID, newRepoID, token, nil, newName, newVersion)
|
||||
runner, err := RegisterRunner(db.DefaultContext, newOwnerID, newRepoID, token, nil, newName, newVersion, ephemeral)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Check that the returned record has been updated, except for the labels
|
||||
|
|
@ -167,6 +179,7 @@ func TestActions_RegisterRunner_UpdateWithoutLabels(t *testing.T) {
|
|||
assert.Equal(t, newName, runner.Name)
|
||||
assert.Equal(t, newVersion, runner.Version)
|
||||
assert.Equal(t, before.AgentLabels, runner.AgentLabels)
|
||||
assert.Equal(t, ephemeral, runner.Ephemeral)
|
||||
|
||||
// Check that whatever is in the DB has been updated, except for the labels
|
||||
after := unittest.AssertExistsAndLoadBean(t, &ActionRunner{ID: recordID})
|
||||
|
|
@ -175,4 +188,5 @@ func TestActions_RegisterRunner_UpdateWithoutLabels(t *testing.T) {
|
|||
assert.Equal(t, newName, after.Name)
|
||||
assert.Equal(t, newVersion, after.Version)
|
||||
assert.Equal(t, before.AgentLabels, after.AgentLabels)
|
||||
assert.Equal(t, ephemeral, after.Ephemeral)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -61,6 +61,8 @@ type ActionRunner struct {
|
|||
|
||||
// Store labels defined in state file (default: .runner file) of `act_runner`
|
||||
AgentLabels []string `xorm:"TEXT"`
|
||||
// Store if this is a runner that only ever get one single job assigned
|
||||
Ephemeral bool `xorm:"ephemeral NOT NULL DEFAULT false"`
|
||||
|
||||
Created timeutil.TimeStamp `xorm:"created"`
|
||||
Updated timeutil.TimeStamp `xorm:"updated"`
|
||||
|
|
|
|||
|
|
@ -174,6 +174,10 @@ func GetTaskByID(ctx context.Context, id int64) (*ActionTask, error) {
|
|||
return &task, nil
|
||||
}
|
||||
|
||||
func HasTaskForRunner(ctx context.Context, runnerID int64) (bool, error) {
|
||||
return db.GetEngine(ctx).Where("runner_id = ?", runnerID).Exist(&ActionTask{})
|
||||
}
|
||||
|
||||
func GetTaskByJobAttempt(ctx context.Context, jobID, attempt int64) (*ActionTask, error) {
|
||||
var task ActionTask
|
||||
has, err := db.GetEngine(ctx).Where("job_id=?", jobID).Where("attempt=?", attempt).Get(&task)
|
||||
|
|
|
|||
24
models/forgejo_migrations/v15b_add-ephemeral_runner.go
Normal file
24
models/forgejo_migrations/v15b_add-ephemeral_runner.go
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
// Copyright 2025 The Forgejo Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package forgejo_migrations
|
||||
|
||||
import (
|
||||
"xorm.io/xorm"
|
||||
)
|
||||
|
||||
func init() {
|
||||
registerMigration(&Migration{
|
||||
Description: "add ephemeral to action_runner",
|
||||
Upgrade: addRunnerEphemeralField,
|
||||
})
|
||||
}
|
||||
|
||||
func addRunnerEphemeralField(x *xorm.Engine) error {
|
||||
type ActionRunner struct {
|
||||
Ephemeral bool `xorm:"ephemeral NOT NULL DEFAULT false"`
|
||||
}
|
||||
|
||||
_, err := x.SyncWithOptions(xorm.SyncOptions{IgnoreDropIndices: true}, new(ActionRunner))
|
||||
return err
|
||||
}
|
||||
|
|
@ -80,4 +80,6 @@ type ActionRunner struct {
|
|||
Labels []string `json:"labels"`
|
||||
// Description provides optional details about this runner.
|
||||
Description string `json:"description"`
|
||||
// Indicates if runner is ephemeral runner
|
||||
Ephemeral bool `json:"ephemeral"`
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,12 +15,18 @@ type RegisterRunnerOptions struct {
|
|||
//
|
||||
// required: false
|
||||
Description string `json:"description"`
|
||||
|
||||
// Register as ephemeral runner https://forgejo.org/docs/latest/admin/actions/security/#ephemeral-runner
|
||||
//
|
||||
// required: false
|
||||
Ephemeral bool `json:"ephemeral"`
|
||||
}
|
||||
|
||||
// RegisterRunnerResponse contains the details of the just registered runner.
|
||||
// swagger:model
|
||||
type RegisterRunnerResponse struct {
|
||||
ID int64 `json:"id" binding:"Required"`
|
||||
UUID string `json:"uuid" binding:"Required"`
|
||||
Token string `json:"token" binding:"Required"`
|
||||
ID int64 `json:"id" binding:"Required"`
|
||||
UUID string `json:"uuid" binding:"Required"`
|
||||
Token string `json:"token" binding:"Required"`
|
||||
Ephemeral bool `json:"ephemeral" binding:"Required"`
|
||||
}
|
||||
|
|
|
|||
|
|
@ -79,6 +79,7 @@ func (s *Service) Register(
|
|||
RepoID: runnerToken.RepoID,
|
||||
Version: req.Msg.Version,
|
||||
AgentLabels: labels,
|
||||
Ephemeral: req.Msg.Ephemeral,
|
||||
}
|
||||
runner.GenerateToken()
|
||||
|
||||
|
|
@ -95,12 +96,13 @@ func (s *Service) Register(
|
|||
|
||||
res := connect.NewResponse(&runnerv1.RegisterResponse{
|
||||
Runner: &runnerv1.Runner{
|
||||
Id: runner.ID,
|
||||
Uuid: runner.UUID,
|
||||
Token: runner.Token,
|
||||
Name: runner.Name,
|
||||
Version: runner.Version,
|
||||
Labels: runner.AgentLabels,
|
||||
Id: runner.ID,
|
||||
Uuid: runner.UUID,
|
||||
Token: runner.Token,
|
||||
Name: runner.Name,
|
||||
Version: runner.Version,
|
||||
Labels: runner.AgentLabels,
|
||||
Ephemeral: runner.Ephemeral,
|
||||
},
|
||||
})
|
||||
|
||||
|
|
@ -120,12 +122,13 @@ func (s *Service) Declare(
|
|||
|
||||
return connect.NewResponse(&runnerv1.DeclareResponse{
|
||||
Runner: &runnerv1.Runner{
|
||||
Id: runner.ID,
|
||||
Uuid: runner.UUID,
|
||||
Token: runner.Token,
|
||||
Name: runner.Name,
|
||||
Version: runner.Version,
|
||||
Labels: runner.AgentLabels,
|
||||
Id: runner.ID,
|
||||
Uuid: runner.UUID,
|
||||
Token: runner.Token,
|
||||
Name: runner.Name,
|
||||
Version: runner.Version,
|
||||
Labels: runner.AgentLabels,
|
||||
Ephemeral: runner.Ephemeral,
|
||||
},
|
||||
}), nil
|
||||
}
|
||||
|
|
@ -251,6 +254,14 @@ func (s *Service) UpdateTask(
|
|||
return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("fail to increase task version: %w", err))
|
||||
}
|
||||
}
|
||||
|
||||
if runner.Ephemeral {
|
||||
err := actions_model.DeleteRunner(ctx, runner)
|
||||
if err != nil {
|
||||
log.Error("failed to delete ephemeral runner %v, %w", task.RunnerID, err)
|
||||
return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("failed to delete ephemeral runner %v, %w", task.RunnerID, err))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return connect.NewResponse(&runnerv1.UpdateTaskResponse{
|
||||
|
|
|
|||
|
|
@ -162,6 +162,7 @@ func RegisterRunner(ctx *context.APIContext, ownerID, repoID int64) {
|
|||
OwnerID: ownerID,
|
||||
RepoID: repoID,
|
||||
Description: options.Description,
|
||||
Ephemeral: options.Ephemeral,
|
||||
}
|
||||
runner.GenerateToken()
|
||||
if err := actions_model.CreateRunner(ctx, runner); err != nil {
|
||||
|
|
@ -169,9 +170,10 @@ func RegisterRunner(ctx *context.APIContext, ownerID, repoID int64) {
|
|||
}
|
||||
|
||||
response := &structs.RegisterRunnerResponse{
|
||||
ID: runner.ID,
|
||||
UUID: runner.UUID,
|
||||
Token: runner.Token,
|
||||
ID: runner.ID,
|
||||
UUID: runner.UUID,
|
||||
Token: runner.Token,
|
||||
Ephemeral: runner.Ephemeral,
|
||||
}
|
||||
ctx.JSON(http.StatusCreated, response)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,23 +11,31 @@ import (
|
|||
"time"
|
||||
|
||||
actions_model "forgejo.org/models/actions"
|
||||
"forgejo.org/models/db"
|
||||
actions_module "forgejo.org/modules/actions"
|
||||
"forgejo.org/modules/log"
|
||||
"forgejo.org/modules/setting"
|
||||
"forgejo.org/modules/storage"
|
||||
"forgejo.org/modules/timeutil"
|
||||
|
||||
"xorm.io/builder"
|
||||
)
|
||||
|
||||
// Cleanup removes expired actions logs, data and artifacts
|
||||
// Cleanup removes expired actions logs, data, artifacts and used ephemeral runners
|
||||
func Cleanup(ctx context.Context) error {
|
||||
// clean up expired artifacts
|
||||
if err := CleanupArtifacts(ctx); err != nil {
|
||||
return fmt.Errorf("cleanup artifacts: %w", err)
|
||||
return fmt.Errorf("failed to clean up artifacts: %w", err)
|
||||
}
|
||||
|
||||
// clean up old logs
|
||||
if err := CleanupLogs(ctx); err != nil {
|
||||
return fmt.Errorf("cleanup logs: %w", err)
|
||||
return fmt.Errorf("failed to clean up logs: %w", err)
|
||||
}
|
||||
|
||||
// clean up old ephemeral runners
|
||||
if err := CleanupEphemeralRunners(ctx); err != nil {
|
||||
return fmt.Errorf("failed to clean up old ephemeral runners: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
|
|
@ -127,6 +135,50 @@ func CleanupLogs(ctx context.Context) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// CleanupEphemeralRunners removes used ephemeral runners which are no longer able to process jobs
|
||||
func CleanupEphemeralRunners(ctx context.Context) error {
|
||||
var ids []int
|
||||
err := db.GetEngine(ctx).
|
||||
Table("`action_runner`").
|
||||
Select("DISTINCT `action_runner`.id").
|
||||
Join("INNER", "`action_task`", "`action_task`.`runner_id` = `action_runner`.`id`").
|
||||
Where(builder.Eq{"`action_runner`.`ephemeral`": true}).
|
||||
And(builder.In("`action_task`.`status`", actions_model.DoneStatuses())).
|
||||
Find(&ids)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to find ephemeral runners: %w", err)
|
||||
}
|
||||
|
||||
res, err := db.GetEngine(ctx).
|
||||
In("id", ids).
|
||||
Delete(&actions_model.ActionRunner{})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to delete ephemeral runners: %w", err)
|
||||
}
|
||||
|
||||
log.Info("Removed %d ephemeral runners", res)
|
||||
return nil
|
||||
}
|
||||
|
||||
// CleanupEphemeralRunnersByPickedTaskOfRepo removes all ephemeral runners that have active/finished tasks on the given repository
|
||||
func CleanupEphemeralRunnersByPickedTaskOfRepo(ctx context.Context, repoID int64) error {
|
||||
subQuery := builder.Select("`action_runner`.id").
|
||||
From(builder.Select("*").From("`action_runner`"), "`action_runner`"). // mysql needs this redundant subquery
|
||||
Join("INNER", "`action_task`", "`action_task`.`runner_id` = `action_runner`.`id`").
|
||||
Where(builder.And(builder.Eq{"`action_runner`.`ephemeral`": true}, builder.Eq{"`action_task`.`repo_id`": repoID}))
|
||||
b := builder.Delete(builder.In("id", subQuery)).From("`action_runner`")
|
||||
res, err := db.GetEngine(ctx).Exec(b)
|
||||
if err != nil {
|
||||
return fmt.Errorf("find runners: %w", err)
|
||||
}
|
||||
affected, err := res.RowsAffected()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
log.Info("Removed %d runners", affected)
|
||||
return nil
|
||||
}
|
||||
|
||||
// CleanupOfflineRunners removes offline runners
|
||||
func CleanupOfflineRunners(ctx context.Context, duration time.Duration, globalOnly bool) error {
|
||||
olderThan := timeutil.TimeStampNow().AddDuration(-duration)
|
||||
|
|
|
|||
|
|
@ -29,3 +29,78 @@ func TestCleanup(t *testing.T) {
|
|||
assert.Nil(t, task.LogIndexes)
|
||||
})
|
||||
}
|
||||
|
||||
func TestCleanupEphemeralRunners(t *testing.T) {
|
||||
require.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
t.Run("Deletes ephemeral runner with successful task", func(t *testing.T) {
|
||||
unittest.AssertSuccessfulInsert(t, &actions_model.ActionRunner{ID: 2001, UUID: "2001-uuid", TokenHash: "2001-hash", Ephemeral: true})
|
||||
unittest.AssertSuccessfulInsert(t, &actions_model.ActionTask{ID: 2001, RunnerID: 2001, TokenHash: "task-2001-hash", Status: actions_model.StatusSuccess})
|
||||
|
||||
require.NoError(t, CleanupEphemeralRunners(db.DefaultContext))
|
||||
|
||||
unittest.AssertNotExistsBean(t, &actions_model.ActionRunner{ID: 2001})
|
||||
})
|
||||
|
||||
t.Run("Deletes ephemeral runner with failed task", func(t *testing.T) {
|
||||
unittest.AssertSuccessfulInsert(t, &actions_model.ActionRunner{ID: 2002, UUID: "2002-uuid", TokenHash: "2002-hash", Ephemeral: true})
|
||||
unittest.AssertSuccessfulInsert(t, &actions_model.ActionTask{ID: 2002, RunnerID: 2002, TokenHash: "task-2002-hash", Status: actions_model.StatusFailure})
|
||||
|
||||
require.NoError(t, CleanupEphemeralRunners(db.DefaultContext))
|
||||
|
||||
unittest.AssertNotExistsBean(t, &actions_model.ActionRunner{ID: 2002})
|
||||
})
|
||||
|
||||
t.Run("Deletes ephemeral runner with cancelled task", func(t *testing.T) {
|
||||
unittest.AssertSuccessfulInsert(t, &actions_model.ActionRunner{ID: 2003, UUID: "2003-uuid", TokenHash: "2003-hash", Ephemeral: true})
|
||||
unittest.AssertSuccessfulInsert(t, &actions_model.ActionTask{ID: 2003, RunnerID: 2003, TokenHash: "task-2003-hash", Status: actions_model.StatusCancelled})
|
||||
|
||||
require.NoError(t, CleanupEphemeralRunners(db.DefaultContext))
|
||||
|
||||
unittest.AssertNotExistsBean(t, &actions_model.ActionRunner{ID: 2003})
|
||||
})
|
||||
|
||||
t.Run("Deletes ephemeral runner with skipped task", func(t *testing.T) {
|
||||
unittest.AssertSuccessfulInsert(t, &actions_model.ActionRunner{ID: 2004, UUID: "2004-uuid", TokenHash: "2004-hash", Ephemeral: true})
|
||||
unittest.AssertSuccessfulInsert(t, &actions_model.ActionTask{ID: 2004, RunnerID: 2004, TokenHash: "task-2004-hash", Status: actions_model.StatusSkipped})
|
||||
|
||||
require.NoError(t, CleanupEphemeralRunners(db.DefaultContext))
|
||||
|
||||
unittest.AssertNotExistsBean(t, &actions_model.ActionRunner{ID: 2004})
|
||||
})
|
||||
|
||||
t.Run("Keeps ephemeral runner with running task", func(t *testing.T) {
|
||||
unittest.AssertSuccessfulInsert(t, &actions_model.ActionRunner{ID: 2005, UUID: "2005-uuid", TokenHash: "2005-hash", Ephemeral: true})
|
||||
unittest.AssertSuccessfulInsert(t, &actions_model.ActionTask{ID: 2005, RunnerID: 2005, TokenHash: "task-2005-hash", Status: actions_model.StatusRunning})
|
||||
|
||||
require.NoError(t, CleanupEphemeralRunners(db.DefaultContext))
|
||||
|
||||
unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRunner{ID: 2005})
|
||||
})
|
||||
|
||||
t.Run("Keeps ephemeral runner with waiting task", func(t *testing.T) {
|
||||
unittest.AssertSuccessfulInsert(t, &actions_model.ActionRunner{ID: 2006, UUID: "2006-uuid", TokenHash: "2006-hash", Ephemeral: true})
|
||||
unittest.AssertSuccessfulInsert(t, &actions_model.ActionTask{ID: 2006, RunnerID: 2006, TokenHash: "task-2006-hash", Status: actions_model.StatusWaiting})
|
||||
|
||||
require.NoError(t, CleanupEphemeralRunners(db.DefaultContext))
|
||||
|
||||
unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRunner{ID: 2006})
|
||||
})
|
||||
|
||||
t.Run("Keeps ephemeral runner with no tasks", func(t *testing.T) {
|
||||
unittest.AssertSuccessfulInsert(t, &actions_model.ActionRunner{ID: 2007, UUID: "2007-uuid", TokenHash: "2007-hash", Ephemeral: true})
|
||||
|
||||
require.NoError(t, CleanupEphemeralRunners(db.DefaultContext))
|
||||
|
||||
unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRunner{ID: 2007})
|
||||
})
|
||||
|
||||
t.Run("Keeps non-ephemeral runner with completed task", func(t *testing.T) {
|
||||
unittest.AssertSuccessfulInsert(t, &actions_model.ActionRunner{ID: 2008, UUID: "2008-uuid", TokenHash: "2008-hash", Ephemeral: false})
|
||||
unittest.AssertSuccessfulInsert(t, &actions_model.ActionTask{ID: 2008, RunnerID: 2008, TokenHash: "task-2008-hash", Status: actions_model.StatusSuccess})
|
||||
|
||||
require.NoError(t, CleanupEphemeralRunners(db.DefaultContext))
|
||||
|
||||
unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRunner{ID: 2008})
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,6 +26,19 @@ func PickTask(ctx context.Context, runner *actions_model.ActionRunner) (*runnerv
|
|||
job *actions_model.ActionRunJob
|
||||
)
|
||||
|
||||
if runner.Ephemeral {
|
||||
hasRunnerAssignedTask, err := actions_model.HasTaskForRunner(ctx, runner.ID)
|
||||
// Let the runner retry the request, do not allow to proceed
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
|
||||
// if runner has task, dont assign new task
|
||||
if hasRunnerAssignedTask {
|
||||
return nil, false, nil
|
||||
}
|
||||
}
|
||||
|
||||
if err := db.WithTx(ctx, func(ctx context.Context) error {
|
||||
t, ok, err := actions_model.CreateTaskForRunner(ctx, runner)
|
||||
if err != nil {
|
||||
|
|
@ -166,6 +179,18 @@ func StopTask(ctx context.Context, taskID int64, status actions_model.Status) er
|
|||
return err
|
||||
}
|
||||
|
||||
runner := &actions_model.ActionRunner{}
|
||||
if _, err := e.ID(task.RunnerID).Get(runner); err != nil {
|
||||
return fmt.Errorf("failed to find runner assigned to task")
|
||||
}
|
||||
|
||||
if runner.Ephemeral {
|
||||
err := actions_model.DeleteRunner(ctx, runner)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to remove ephemeral runner from stopped task: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
if err := task.LoadAttributes(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
|
|||
|
|
@ -547,6 +547,7 @@ func ToActionRunner(runner *actions_model.ActionRunner) (api.ActionRunner, error
|
|||
Version: runner.Version,
|
||||
Status: status.String(),
|
||||
Labels: runner.AgentLabels,
|
||||
Ephemeral: runner.Ephemeral,
|
||||
}
|
||||
|
||||
return actionRunner, nil
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ import (
|
|||
"forgejo.org/modules/log"
|
||||
"forgejo.org/modules/setting"
|
||||
"forgejo.org/modules/storage"
|
||||
actions_service "forgejo.org/services/actions"
|
||||
federation_service "forgejo.org/services/federation"
|
||||
|
||||
"xorm.io/builder"
|
||||
|
|
@ -145,6 +146,13 @@ func DeleteRepositoryDirectly(ctx context.Context, doer *user_model.User, repoID
|
|||
}
|
||||
}
|
||||
|
||||
// CleanupEphemeralRunnersByPickedTaskOfRepo deletes ephemeral global/org/user that have started any task of this repo, as they cannot pick a second task
|
||||
// This method will delete affected ephemeral global/org/user runners
|
||||
// &actions_model.ActionRunner{RepoID: repoID} does only handle ephemeral repository runners
|
||||
if err := actions_service.CleanupEphemeralRunnersByPickedTaskOfRepo(ctx, repoID); err != nil {
|
||||
return fmt.Errorf("cleanupEphemeralRunners: %w", err)
|
||||
}
|
||||
|
||||
if err := db.DeleteBeans(ctx,
|
||||
&access_model.Access{RepoID: repo.ID},
|
||||
&activities_model.Action{RepoID: repo.ID},
|
||||
|
|
|
|||
14
templates/swagger/v1_json.tmpl
generated
14
templates/swagger/v1_json.tmpl
generated
|
|
@ -22371,6 +22371,11 @@
|
|||
"type": "string",
|
||||
"x-go-name": "Description"
|
||||
},
|
||||
"ephemeral": {
|
||||
"description": "Indicates if runner is ephemeral runner",
|
||||
"type": "boolean",
|
||||
"x-go-name": "Ephemeral"
|
||||
},
|
||||
"id": {
|
||||
"description": "ID uniquely identifies this runner.",
|
||||
"type": "integer",
|
||||
|
|
@ -28516,6 +28521,11 @@
|
|||
"type": "string",
|
||||
"x-go-name": "Description"
|
||||
},
|
||||
"ephemeral": {
|
||||
"description": "Register as ephemeral runner https://forgejo.org/docs/latest/admin/actions/security/#ephemeral-runner",
|
||||
"type": "boolean",
|
||||
"x-go-name": "Ephemeral"
|
||||
},
|
||||
"name": {
|
||||
"description": "Name of the runner to register. The name of the runner does not have to be unique.",
|
||||
"type": "string",
|
||||
|
|
@ -28528,6 +28538,10 @@
|
|||
"type": "object",
|
||||
"title": "RegisterRunnerResponse contains the details of the just registered runner.",
|
||||
"properties": {
|
||||
"ephemeral": {
|
||||
"type": "boolean",
|
||||
"x-go-name": "Ephemeral"
|
||||
},
|
||||
"id": {
|
||||
"type": "integer",
|
||||
"format": "int64",
|
||||
|
|
|
|||
|
|
@ -22,8 +22,10 @@ import (
|
|||
"forgejo.org/modules/json"
|
||||
"forgejo.org/modules/setting"
|
||||
api "forgejo.org/modules/structs"
|
||||
actions_service "forgejo.org/services/actions"
|
||||
|
||||
runnerv1 "code.forgejo.org/forgejo/actions-proto/runner/v1"
|
||||
"connectrpc.com/connect"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
|
@ -606,6 +608,128 @@ jobs:
|
|||
})
|
||||
}
|
||||
|
||||
func TestActionsEphemeral(t *testing.T) {
|
||||
if !setting.Database.Type.IsSQLite3() {
|
||||
t.Skip()
|
||||
}
|
||||
|
||||
onApplicationRun(t, func(t *testing.T, u *url.URL) {
|
||||
user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
||||
user2Session := loginUser(t, user2.Name)
|
||||
user2Token := getTokenForLoggedInUser(t, user2Session, auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser)
|
||||
|
||||
apiBaseRepo := createActionsTestRepo(t, user2Token, "actions-gitea-context", false)
|
||||
baseRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: apiBaseRepo.ID})
|
||||
user2APICtx := NewAPITestContext(t, baseRepo.OwnerName, baseRepo.Name, auth_model.AccessTokenScopeWriteRepository)
|
||||
|
||||
runner := newMockRunner()
|
||||
runner.registerAsEphemeralRepoRunner(t, baseRepo.OwnerName, baseRepo.Name, "mock-runner", []string{"ubuntu-latest"})
|
||||
|
||||
// verify CleanupEphemeralRunners does not remove this runner
|
||||
err := actions_service.CleanupEphemeralRunners(t.Context())
|
||||
require.NoError(t, err)
|
||||
|
||||
// init the workflow
|
||||
wfTreePath := ".gitea/workflows/pull.yml"
|
||||
wfFileContent := `name: Pull Request
|
||||
on: pull_request
|
||||
jobs:
|
||||
wf1-job:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- run: echo 'test the pull'
|
||||
wf2-job:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- run: echo 'test the pull'
|
||||
`
|
||||
opts := getWorkflowCreateFileOptions(user2, baseRepo.DefaultBranch, fmt.Sprintf("create %s", wfTreePath), wfFileContent)
|
||||
createWorkflowFile(t, user2Token, baseRepo.OwnerName, baseRepo.Name, wfTreePath, opts)
|
||||
// user2 creates a pull request
|
||||
doAPICreateFile(user2APICtx, "user2-patch.txt", &api.CreateFileOptions{
|
||||
FileOptions: api.FileOptions{
|
||||
NewBranchName: "user2/patch-1",
|
||||
Message: "create user2-patch.txt",
|
||||
Author: api.Identity{
|
||||
Name: user2.Name,
|
||||
Email: user2.Email,
|
||||
},
|
||||
Committer: api.Identity{
|
||||
Name: user2.Name,
|
||||
Email: user2.Email,
|
||||
},
|
||||
Dates: api.CommitDateOptions{
|
||||
Author: time.Now(),
|
||||
Committer: time.Now(),
|
||||
},
|
||||
},
|
||||
ContentBase64: base64.StdEncoding.EncodeToString([]byte("user2-fix")),
|
||||
})(t)
|
||||
_, err = doAPICreatePullRequest(user2APICtx, baseRepo.OwnerName, baseRepo.Name, baseRepo.DefaultBranch, "user2/patch-1")(t)
|
||||
require.NoError(t, err)
|
||||
task := runner.fetchTask(t)
|
||||
actionTask := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionTask{ID: task.Id})
|
||||
actionRunJob := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRunJob{ID: actionTask.JobID})
|
||||
actionRun := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{ID: actionRunJob.RunID})
|
||||
require.NoError(t, actionRun.LoadAttributes(t.Context()))
|
||||
|
||||
runEvent := map[string]any{}
|
||||
require.NoError(t, json.Unmarshal([]byte(actionRun.EventPayload), &runEvent))
|
||||
|
||||
// verify CleanupEphemeralRunners does not remove this runner
|
||||
err = actions_service.CleanupEphemeralRunners(t.Context())
|
||||
require.NoError(t, err)
|
||||
|
||||
resp, err := runner.client.runnerServiceClient.FetchTask(t.Context(), connect.NewRequest(&runnerv1.FetchTaskRequest{
|
||||
TasksVersion: 0,
|
||||
}))
|
||||
require.NoError(t, err)
|
||||
assert.Nil(t, resp.Msg.Task)
|
||||
|
||||
// verify CleanupEphemeralRunners does not remove this runner
|
||||
err = actions_service.CleanupEphemeralRunners(t.Context())
|
||||
require.NoError(t, err)
|
||||
|
||||
runner.client.runnerServiceClient.UpdateTask(t.Context(), connect.NewRequest(&runnerv1.UpdateTaskRequest{
|
||||
State: &runnerv1.TaskState{
|
||||
Id: actionTask.ID,
|
||||
Result: runnerv1.Result_RESULT_SUCCESS,
|
||||
},
|
||||
}))
|
||||
resp, err = runner.client.runnerServiceClient.FetchTask(t.Context(), connect.NewRequest(&runnerv1.FetchTaskRequest{
|
||||
TasksVersion: 0,
|
||||
}))
|
||||
require.Error(t, err)
|
||||
assert.Nil(t, resp)
|
||||
|
||||
// create an runner that picks a job and get force cancelled
|
||||
runnerToBeRemoved := newMockRunner()
|
||||
runnerToBeRemoved.registerAsEphemeralRepoRunner(t, baseRepo.OwnerName, baseRepo.Name, "mock-runner-to-be-removed", []string{"ubuntu-latest"})
|
||||
|
||||
taskToStopAPIObj := runnerToBeRemoved.fetchTask(t)
|
||||
|
||||
taskToStop := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionTask{ID: taskToStopAPIObj.Id})
|
||||
|
||||
// verify CleanupEphemeralRunners does not remove the custom crafted runner
|
||||
err = actions_service.CleanupEphemeralRunners(t.Context())
|
||||
require.NoError(t, err)
|
||||
|
||||
runnerToRemove := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRunner{ID: taskToStop.RunnerID})
|
||||
|
||||
err = actions_service.StopTask(t.Context(), taskToStop.ID, actions_model.StatusFailure)
|
||||
require.NoError(t, err)
|
||||
|
||||
// verify CleanupEphemeralRunners does remove the custom crafted runner
|
||||
err = actions_service.CleanupEphemeralRunners(t.Context())
|
||||
require.NoError(t, err)
|
||||
|
||||
unittest.AssertNotExistsBean(t, &actions_model.ActionRunner{ID: runnerToRemove.ID})
|
||||
|
||||
// this cleanup is required to allow further tests to pass
|
||||
doAPIDeleteRepository(user2APICtx)(t)
|
||||
})
|
||||
}
|
||||
|
||||
func createActionsTestRepo(t *testing.T, authToken, repoName string, isPrivate bool) *api.Repository {
|
||||
req := NewRequestWithJSON(t, "POST", "/api/v1/user/repos", &api.CreateRepoOption{
|
||||
Name: repoName,
|
||||
|
|
|
|||
|
|
@ -72,10 +72,24 @@ func (r *mockRunner) doPing(t *testing.T) {
|
|||
func (r *mockRunner) doRegister(t *testing.T, name, token string, labels []string) {
|
||||
r.doPing(t)
|
||||
resp, err := r.client.runnerServiceClient.Register(t.Context(), connect.NewRequest(&runnerv1.RegisterRequest{
|
||||
Name: name,
|
||||
Token: token,
|
||||
Version: "mock-runner-version",
|
||||
Labels: labels,
|
||||
Name: name,
|
||||
Token: token,
|
||||
Version: "mock-runner-version",
|
||||
Labels: labels,
|
||||
Ephemeral: false,
|
||||
}))
|
||||
require.NoError(t, err)
|
||||
r.client = newMockRunnerClient(resp.Msg.Runner.Uuid, resp.Msg.Runner.Token)
|
||||
}
|
||||
|
||||
func (r *mockRunner) doRegisterEphemeral(t *testing.T, name, token string, labels []string) {
|
||||
r.doPing(t)
|
||||
resp, err := r.client.runnerServiceClient.Register(t.Context(), connect.NewRequest(&runnerv1.RegisterRequest{
|
||||
Name: name,
|
||||
Token: token,
|
||||
Version: "mock-runner-version",
|
||||
Labels: labels,
|
||||
Ephemeral: true,
|
||||
}))
|
||||
require.NoError(t, err)
|
||||
r.client = newMockRunnerClient(resp.Msg.Runner.Uuid, resp.Msg.Runner.Token)
|
||||
|
|
@ -96,6 +110,21 @@ func (r *mockRunner) registerAsRepoRunner(t *testing.T, ownerName, repoName, run
|
|||
r.doRegister(t, runnerName, registrationToken.Token, labels)
|
||||
}
|
||||
|
||||
func (r *mockRunner) registerAsEphemeralRepoRunner(t *testing.T, ownerName, repoName, runnerName string, labels []string) {
|
||||
if !setting.Database.Type.IsSQLite3() {
|
||||
assert.FailNow(t, "registering a mock runner when using a database other than SQLite leaves leftovers")
|
||||
}
|
||||
session := loginUser(t, ownerName)
|
||||
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository)
|
||||
req := NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/%s/%s/actions/runners/registration-token", ownerName, repoName)).AddTokenAuth(token)
|
||||
resp := MakeRequest(t, req, http.StatusOK)
|
||||
var registrationToken struct {
|
||||
Token string `json:"token"`
|
||||
}
|
||||
DecodeJSON(t, resp, ®istrationToken)
|
||||
r.doRegisterEphemeral(t, runnerName, registrationToken.Token, labels)
|
||||
}
|
||||
|
||||
func (r *mockRunner) maybeFetchTask(t *testing.T) *runnerv1.Task {
|
||||
resp, err := r.client.runnerServiceClient.FetchTask(t.Context(), connect.NewRequest(&runnerv1.FetchTaskRequest{
|
||||
TasksVersion: r.lastTasksVersion,
|
||||
|
|
|
|||
|
|
@ -226,12 +226,25 @@ func TestAPIAdminActionsRunnerOperations(t *testing.T) {
|
|||
Labels: []string{"fedora"},
|
||||
Status: "offline",
|
||||
}
|
||||
runnerFive := &api.ActionRunner{
|
||||
ID: 130795,
|
||||
UUID: "16ca1a5c-8024-41f1-be31-e55830263cc6",
|
||||
Name: "runner-5-ephemeral",
|
||||
Version: "1.0.0",
|
||||
OwnerID: 0,
|
||||
RepoID: 0,
|
||||
Description: "An ephemeral runner",
|
||||
Labels: []string{"ephemeral-label"},
|
||||
Status: "offline",
|
||||
Ephemeral: true,
|
||||
}
|
||||
|
||||
// There are more runners in the result that originate from the global fixtures. The test ignores them to limit
|
||||
// the impact of unrelated changes.
|
||||
assert.Contains(t, runners, runnerOne)
|
||||
assert.Contains(t, runners, runnerTwo)
|
||||
assert.Contains(t, runners, runnerThree)
|
||||
assert.Contains(t, runners, runnerFive)
|
||||
})
|
||||
|
||||
t.Run("Get runners paginated", func(t *testing.T) {
|
||||
|
|
@ -293,6 +306,30 @@ func TestAPIAdminActionsRunnerOperations(t *testing.T) {
|
|||
assert.Equal(t, runnerFour, runner)
|
||||
})
|
||||
|
||||
t.Run("Get ephemeral runner", func(t *testing.T) {
|
||||
request := NewRequest(t, "GET", "/api/v1/admin/actions/runners/130795")
|
||||
request.AddTokenAuth(readToken)
|
||||
response := MakeRequest(t, request, http.StatusOK)
|
||||
|
||||
var runner *api.ActionRunner
|
||||
DecodeJSON(t, response, &runner)
|
||||
|
||||
expectedRunner := &api.ActionRunner{
|
||||
ID: 130795,
|
||||
UUID: "16ca1a5c-8024-41f1-be31-e55830263cc6",
|
||||
Name: "runner-5-ephemeral",
|
||||
Version: "1.0.0",
|
||||
OwnerID: 0,
|
||||
RepoID: 0,
|
||||
Description: "An ephemeral runner",
|
||||
Labels: []string{"ephemeral-label"},
|
||||
Status: "offline",
|
||||
Ephemeral: true,
|
||||
}
|
||||
|
||||
assert.Equal(t, expectedRunner, runner)
|
||||
})
|
||||
|
||||
t.Run("Delete global runner", func(t *testing.T) {
|
||||
url := "/api/v1/admin/actions/runners/130791"
|
||||
|
||||
|
|
@ -339,6 +376,7 @@ func TestAPIAdminActionsRunnerOperations(t *testing.T) {
|
|||
assert.Positive(t, registerRunnerResponse.ID)
|
||||
assert.Equal(t, gouuid.Version(4), gouuid.MustParse(registerRunnerResponse.UUID).Version())
|
||||
assert.Regexp(t, "(?i)^[0-9a-f]{40}$", registerRunnerResponse.Token)
|
||||
assert.False(t, registerRunnerResponse.Ephemeral)
|
||||
|
||||
registeredRunner := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRunner{UUID: registerRunnerResponse.UUID})
|
||||
assert.Equal(t, registerRunnerResponse.ID, registeredRunner.ID)
|
||||
|
|
@ -351,6 +389,24 @@ func TestAPIAdminActionsRunnerOperations(t *testing.T) {
|
|||
assert.Empty(t, registeredRunner.Version)
|
||||
assert.NotEmpty(t, registeredRunner.TokenHash)
|
||||
assert.NotEmpty(t, registeredRunner.TokenSalt)
|
||||
assert.False(t, registeredRunner.Ephemeral)
|
||||
})
|
||||
|
||||
t.Run("Register ephemeral runner", func(t *testing.T) {
|
||||
options := api.RegisterRunnerOptions{Name: "ephemeral-runner", Description: "Ephemeral runner", Ephemeral: true}
|
||||
|
||||
request := NewRequestWithJSON(t, "POST", "/api/v1/admin/actions/runners", options)
|
||||
request.AddTokenAuth(writeToken)
|
||||
response := MakeRequest(t, request, http.StatusCreated)
|
||||
|
||||
var registerRunnerResponse *api.RegisterRunnerResponse
|
||||
DecodeJSON(t, response, ®isterRunnerResponse)
|
||||
|
||||
assert.True(t, registerRunnerResponse.Ephemeral)
|
||||
|
||||
registeredRunner := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRunner{UUID: registerRunnerResponse.UUID})
|
||||
assert.Equal(t, registerRunnerResponse.UUID, registeredRunner.UUID)
|
||||
assert.True(t, registeredRunner.Ephemeral)
|
||||
})
|
||||
|
||||
t.Run("Runner registration does not update runner with identical name", func(t *testing.T) {
|
||||
|
|
|
|||
|
|
@ -117,7 +117,7 @@ func TestAPIOrgActionsRunnerOperations(t *testing.T) {
|
|||
request.AddTokenAuth(readToken)
|
||||
response := MakeRequest(t, request, http.StatusOK)
|
||||
|
||||
assert.Equal(t, "2", response.Header().Get("X-Total-Count"))
|
||||
assert.Equal(t, "3", response.Header().Get("X-Total-Count"))
|
||||
|
||||
var runners []*api.ActionRunner
|
||||
DecodeJSON(t, response, &runners)
|
||||
|
|
@ -144,8 +144,20 @@ func TestAPIOrgActionsRunnerOperations(t *testing.T) {
|
|||
Labels: []string{"fedora"},
|
||||
Status: "offline",
|
||||
}
|
||||
runnerFive := &api.ActionRunner{
|
||||
ID: 655695,
|
||||
UUID: "0851ed0a-f0af-4a01-9b98-fc9bf9c1d332",
|
||||
Name: "runner-5-ephemeral",
|
||||
Version: "1.0.0",
|
||||
OwnerID: 3,
|
||||
RepoID: 0,
|
||||
Description: "An ephemeral runner",
|
||||
Labels: []string{"ephemeral-label"},
|
||||
Status: "offline",
|
||||
Ephemeral: true,
|
||||
}
|
||||
|
||||
assert.ElementsMatch(t, []*api.ActionRunner{runnerOne, runnerThree}, runners)
|
||||
assert.ElementsMatch(t, []*api.ActionRunner{runnerOne, runnerThree, runnerFive}, runners)
|
||||
})
|
||||
|
||||
t.Run("Get runners paginated", func(t *testing.T) {
|
||||
|
|
@ -184,6 +196,30 @@ func TestAPIOrgActionsRunnerOperations(t *testing.T) {
|
|||
assert.Equal(t, runnerOne, runner)
|
||||
})
|
||||
|
||||
t.Run("Get ephemeral runner", func(t *testing.T) {
|
||||
request := NewRequest(t, "GET", "/api/v1/orgs/org3/actions/runners/655695")
|
||||
request.AddTokenAuth(readToken)
|
||||
response := MakeRequest(t, request, http.StatusOK)
|
||||
|
||||
var runner *api.ActionRunner
|
||||
DecodeJSON(t, response, &runner)
|
||||
|
||||
expectedRunner := &api.ActionRunner{
|
||||
ID: 655695,
|
||||
UUID: "0851ed0a-f0af-4a01-9b98-fc9bf9c1d332",
|
||||
Name: "runner-5-ephemeral",
|
||||
Version: "1.0.0",
|
||||
OwnerID: 3,
|
||||
RepoID: 0,
|
||||
Description: "An ephemeral runner",
|
||||
Labels: []string{"ephemeral-label"},
|
||||
Status: "offline",
|
||||
Ephemeral: true,
|
||||
}
|
||||
|
||||
assert.Equal(t, expectedRunner, runner)
|
||||
})
|
||||
|
||||
t.Run("Delete runner", func(t *testing.T) {
|
||||
url := "/api/v1/orgs/org3/actions/runners/655691"
|
||||
|
||||
|
|
@ -214,6 +250,7 @@ func TestAPIOrgActionsRunnerOperations(t *testing.T) {
|
|||
assert.Positive(t, registerRunnerResponse.ID)
|
||||
assert.Equal(t, gouuid.Version(4), gouuid.MustParse(registerRunnerResponse.UUID).Version())
|
||||
assert.Regexp(t, "(?i)^[0-9a-f]{40}$", registerRunnerResponse.Token)
|
||||
assert.False(t, registerRunnerResponse.Ephemeral)
|
||||
|
||||
registeredRunner := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRunner{UUID: registerRunnerResponse.UUID})
|
||||
assert.Equal(t, registerRunnerResponse.ID, registeredRunner.ID)
|
||||
|
|
@ -226,6 +263,24 @@ func TestAPIOrgActionsRunnerOperations(t *testing.T) {
|
|||
assert.Empty(t, registeredRunner.Version)
|
||||
assert.NotEmpty(t, registeredRunner.TokenHash)
|
||||
assert.NotEmpty(t, registeredRunner.TokenSalt)
|
||||
assert.False(t, registeredRunner.Ephemeral)
|
||||
})
|
||||
|
||||
t.Run("Register ephemeral runner", func(t *testing.T) {
|
||||
options := api.RegisterRunnerOptions{Name: "ephemeral-runner", Description: "Ephemeral runner", Ephemeral: true}
|
||||
|
||||
request := NewRequestWithJSON(t, "POST", "/api/v1/orgs/org3/actions/runners", options)
|
||||
request.AddTokenAuth(writeToken)
|
||||
response := MakeRequest(t, request, http.StatusCreated)
|
||||
|
||||
var registerRunnerResponse *api.RegisterRunnerResponse
|
||||
DecodeJSON(t, response, ®isterRunnerResponse)
|
||||
|
||||
assert.True(t, registerRunnerResponse.Ephemeral)
|
||||
|
||||
registeredRunner := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRunner{UUID: registerRunnerResponse.UUID})
|
||||
assert.Equal(t, registerRunnerResponse.UUID, registeredRunner.UUID)
|
||||
assert.True(t, registeredRunner.Ephemeral)
|
||||
})
|
||||
|
||||
t.Run("Runner registration does not update runner with identical name", func(t *testing.T) {
|
||||
|
|
|
|||
|
|
@ -398,7 +398,7 @@ func TestAPIRepoActionsRunnerOperations(t *testing.T) {
|
|||
request.AddTokenAuth(readToken)
|
||||
response := MakeRequest(t, request, http.StatusOK)
|
||||
|
||||
assert.Equal(t, "2", response.Header().Get("X-Total-Count"))
|
||||
assert.Equal(t, "3", response.Header().Get("X-Total-Count"))
|
||||
|
||||
var runners []*api.ActionRunner
|
||||
DecodeJSON(t, response, &runners)
|
||||
|
|
@ -425,8 +425,20 @@ func TestAPIRepoActionsRunnerOperations(t *testing.T) {
|
|||
Labels: []string{"fedora"},
|
||||
Status: "offline",
|
||||
}
|
||||
runnerFive := &api.ActionRunner{
|
||||
ID: 899255,
|
||||
UUID: "96639646-67b2-4bcb-9142-fde1ab8498cf",
|
||||
Name: "runner-5-repository-ephemeral",
|
||||
Version: "1.0.0",
|
||||
OwnerID: 0,
|
||||
RepoID: 62,
|
||||
Description: "An ephemeral runner",
|
||||
Labels: []string{"ephemeral-label"},
|
||||
Status: "offline",
|
||||
Ephemeral: true,
|
||||
}
|
||||
|
||||
assert.ElementsMatch(t, []*api.ActionRunner{runnerOne, runnerThree}, runners)
|
||||
assert.ElementsMatch(t, []*api.ActionRunner{runnerOne, runnerThree, runnerFive}, runners)
|
||||
})
|
||||
|
||||
t.Run("Get runners paginated", func(t *testing.T) {
|
||||
|
|
@ -465,6 +477,30 @@ func TestAPIRepoActionsRunnerOperations(t *testing.T) {
|
|||
assert.Equal(t, runnerOne, runner)
|
||||
})
|
||||
|
||||
t.Run("Get ephemeral runner", func(t *testing.T) {
|
||||
request := NewRequest(t, "GET", "/api/v1/repos/user2/test_workflows/actions/runners/899255")
|
||||
request.AddTokenAuth(readToken)
|
||||
response := MakeRequest(t, request, http.StatusOK)
|
||||
|
||||
var runner *api.ActionRunner
|
||||
DecodeJSON(t, response, &runner)
|
||||
|
||||
expectedRunner := &api.ActionRunner{
|
||||
ID: 899255,
|
||||
UUID: "96639646-67b2-4bcb-9142-fde1ab8498cf",
|
||||
Name: "runner-5-repository-ephemeral",
|
||||
Version: "1.0.0",
|
||||
OwnerID: 0,
|
||||
RepoID: 62,
|
||||
Description: "An ephemeral runner",
|
||||
Labels: []string{"ephemeral-label"},
|
||||
Status: "offline",
|
||||
Ephemeral: true,
|
||||
}
|
||||
|
||||
assert.Equal(t, expectedRunner, runner)
|
||||
})
|
||||
|
||||
t.Run("Delete runner", func(t *testing.T) {
|
||||
url := "/api/v1/repos/user2/test_workflows/actions/runners/899253"
|
||||
|
||||
|
|
@ -496,6 +532,7 @@ func TestAPIRepoActionsRunnerOperations(t *testing.T) {
|
|||
assert.Positive(t, registerRunnerResponse.ID)
|
||||
assert.Equal(t, gouuid.Version(4), gouuid.MustParse(registerRunnerResponse.UUID).Version())
|
||||
assert.Regexp(t, "(?i)^[0-9a-f]{40}$", registerRunnerResponse.Token)
|
||||
assert.False(t, registerRunnerResponse.Ephemeral)
|
||||
|
||||
registeredRunner := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRunner{UUID: registerRunnerResponse.UUID})
|
||||
assert.Equal(t, registerRunnerResponse.ID, registeredRunner.ID)
|
||||
|
|
@ -508,6 +545,25 @@ func TestAPIRepoActionsRunnerOperations(t *testing.T) {
|
|||
assert.Empty(t, registeredRunner.Version)
|
||||
assert.NotEmpty(t, registeredRunner.TokenHash)
|
||||
assert.NotEmpty(t, registeredRunner.TokenSalt)
|
||||
assert.False(t, registeredRunner.Ephemeral)
|
||||
})
|
||||
|
||||
t.Run("Register ephemeral runner", func(t *testing.T) {
|
||||
options := api.RegisterRunnerOptions{Name: "ephemeral-runner", Description: "Ephemeral runner", Ephemeral: true}
|
||||
|
||||
requestURL := fmt.Sprintf("/api/v1/repos/%s/%s/actions/runners", repo1.OwnerName, repo1.Name)
|
||||
request := NewRequestWithJSON(t, "POST", requestURL, options)
|
||||
request.AddTokenAuth(writeToken)
|
||||
response := MakeRequest(t, request, http.StatusCreated)
|
||||
|
||||
var registerRunnerResponse *api.RegisterRunnerResponse
|
||||
DecodeJSON(t, response, ®isterRunnerResponse)
|
||||
|
||||
assert.True(t, registerRunnerResponse.Ephemeral)
|
||||
|
||||
registeredRunner := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRunner{UUID: registerRunnerResponse.UUID})
|
||||
assert.Equal(t, registerRunnerResponse.UUID, registeredRunner.UUID)
|
||||
assert.True(t, registeredRunner.Ephemeral)
|
||||
})
|
||||
|
||||
t.Run("Runner registration does not update runner with identical name", func(t *testing.T) {
|
||||
|
|
|
|||
|
|
@ -115,7 +115,7 @@ func TestAPIUserActionsRunnerOperations(t *testing.T) {
|
|||
request.AddTokenAuth(readToken)
|
||||
response := MakeRequest(t, request, http.StatusOK)
|
||||
|
||||
assert.Equal(t, "2", response.Header().Get("X-Total-Count"))
|
||||
assert.Equal(t, "3", response.Header().Get("X-Total-Count"))
|
||||
|
||||
var runners []*api.ActionRunner
|
||||
DecodeJSON(t, response, &runners)
|
||||
|
|
@ -142,8 +142,20 @@ func TestAPIUserActionsRunnerOperations(t *testing.T) {
|
|||
Labels: []string{"fedora"},
|
||||
Status: "offline",
|
||||
}
|
||||
runnerFive := &api.ActionRunner{
|
||||
ID: 71305,
|
||||
UUID: "3ca04a95-3e75-4e48-8b7a-63427ebcf3b8",
|
||||
Name: "runner-5-user-ephemeral",
|
||||
Version: "1.0.0",
|
||||
OwnerID: 2,
|
||||
RepoID: 0,
|
||||
Description: "An ephemeral runner",
|
||||
Labels: []string{"ephemeral-label"},
|
||||
Status: "offline",
|
||||
Ephemeral: true,
|
||||
}
|
||||
|
||||
assert.ElementsMatch(t, []*api.ActionRunner{runnerOne, runnerThree}, runners)
|
||||
assert.ElementsMatch(t, []*api.ActionRunner{runnerOne, runnerThree, runnerFive}, runners)
|
||||
})
|
||||
|
||||
t.Run("Get runners paginated", func(t *testing.T) {
|
||||
|
|
@ -182,6 +194,30 @@ func TestAPIUserActionsRunnerOperations(t *testing.T) {
|
|||
assert.Equal(t, runnerThree, runner)
|
||||
})
|
||||
|
||||
t.Run("Get ephemeral runner", func(t *testing.T) {
|
||||
request := NewRequest(t, "GET", "/api/v1/user/actions/runners/71305")
|
||||
request.AddTokenAuth(readToken)
|
||||
response := MakeRequest(t, request, http.StatusOK)
|
||||
|
||||
var runner *api.ActionRunner
|
||||
DecodeJSON(t, response, &runner)
|
||||
|
||||
expectedRunner := &api.ActionRunner{
|
||||
ID: 71305,
|
||||
UUID: "3ca04a95-3e75-4e48-8b7a-63427ebcf3b8",
|
||||
Name: "runner-5-user-ephemeral",
|
||||
Version: "1.0.0",
|
||||
OwnerID: 2,
|
||||
RepoID: 0,
|
||||
Description: "An ephemeral runner",
|
||||
Labels: []string{"ephemeral-label"},
|
||||
Status: "offline",
|
||||
Ephemeral: true,
|
||||
}
|
||||
|
||||
assert.Equal(t, expectedRunner, runner)
|
||||
})
|
||||
|
||||
t.Run("Delete runner", func(t *testing.T) {
|
||||
url := "/api/v1/user/actions/runners/71303"
|
||||
|
||||
|
|
@ -212,6 +248,7 @@ func TestAPIUserActionsRunnerOperations(t *testing.T) {
|
|||
assert.Positive(t, registerRunnerResponse.ID)
|
||||
assert.Equal(t, gouuid.Version(4), gouuid.MustParse(registerRunnerResponse.UUID).Version())
|
||||
assert.Regexp(t, "(?i)^[0-9a-f]{40}$", registerRunnerResponse.Token)
|
||||
assert.False(t, registerRunnerResponse.Ephemeral)
|
||||
|
||||
registeredRunner := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRunner{UUID: registerRunnerResponse.UUID})
|
||||
assert.Equal(t, registerRunnerResponse.ID, registeredRunner.ID)
|
||||
|
|
@ -224,6 +261,24 @@ func TestAPIUserActionsRunnerOperations(t *testing.T) {
|
|||
assert.Empty(t, registeredRunner.Version)
|
||||
assert.NotEmpty(t, registeredRunner.TokenHash)
|
||||
assert.NotEmpty(t, registeredRunner.TokenSalt)
|
||||
assert.False(t, registeredRunner.Ephemeral)
|
||||
})
|
||||
|
||||
t.Run("Register ephemeral runner", func(t *testing.T) {
|
||||
options := api.RegisterRunnerOptions{Name: "ephemeral-runner", Description: "Ephemeral runner", Ephemeral: true}
|
||||
|
||||
request := NewRequestWithJSON(t, "POST", "/api/v1/user/actions/runners", options)
|
||||
request.AddTokenAuth(writeToken)
|
||||
response := MakeRequest(t, request, http.StatusCreated)
|
||||
|
||||
var registerRunnerResponse *api.RegisterRunnerResponse
|
||||
DecodeJSON(t, response, ®isterRunnerResponse)
|
||||
|
||||
assert.True(t, registerRunnerResponse.Ephemeral)
|
||||
|
||||
registeredRunner := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRunner{UUID: registerRunnerResponse.UUID})
|
||||
assert.Equal(t, registerRunnerResponse.UUID, registeredRunner.UUID)
|
||||
assert.True(t, registeredRunner.Ephemeral)
|
||||
})
|
||||
|
||||
t.Run("Runner registration does not update runner with identical name", func(t *testing.T) {
|
||||
|
|
|
|||
170
tests/integration/ephemeral_actions_runner_deletion_test.go
Normal file
170
tests/integration/ephemeral_actions_runner_deletion_test.go
Normal file
|
|
@ -0,0 +1,170 @@
|
|||
// Copyright 2025 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package integration
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/url"
|
||||
"testing"
|
||||
|
||||
actions_model "forgejo.org/models/actions"
|
||||
org_model "forgejo.org/models/organization"
|
||||
"forgejo.org/models/unittest"
|
||||
user_model "forgejo.org/models/user"
|
||||
"forgejo.org/modules/setting"
|
||||
"forgejo.org/modules/util"
|
||||
actions_service "forgejo.org/services/actions"
|
||||
org_service "forgejo.org/services/org"
|
||||
repo_service "forgejo.org/services/repository"
|
||||
user_service "forgejo.org/services/user"
|
||||
|
||||
runnerv1 "code.forgejo.org/forgejo/actions-proto/runner/v1"
|
||||
"connectrpc.com/connect"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// Test that the ephemeral runner is deleted when the task is finished
|
||||
func TestEphemeralRunnerDeletionByTaskCompletion(t *testing.T) {
|
||||
if !setting.Database.Type.IsSQLite3() {
|
||||
t.Skip()
|
||||
}
|
||||
|
||||
defer unittest.OverrideFixtures("tests/integration/fixtures/TestEphemeralRunner")()
|
||||
|
||||
onApplicationRun(t, func(t *testing.T, u *url.URL) {
|
||||
// Verify runner exists before the test
|
||||
runner, err := actions_model.GetRunnerByID(context.Background(), 10000008)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, runner)
|
||||
require.True(t, runner.Ephemeral, "runner should be ephemeral")
|
||||
|
||||
// Verify task exists and is running
|
||||
task := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionTask{ID: 10054})
|
||||
assert.Equal(t, actions_model.StatusRunning, task.Status)
|
||||
assert.Equal(t, int64(10000008), task.RunnerID)
|
||||
|
||||
// Token can be found in models/fixtures/action_runner.yml with id: 10000008
|
||||
runnerClient := newMockRunnerClient(
|
||||
runner.UUID,
|
||||
"mysuuupersecrettoekn",
|
||||
)
|
||||
|
||||
// Finish the Task
|
||||
resp, err := runnerClient.runnerServiceClient.UpdateTask(
|
||||
context.Background(),
|
||||
connect.NewRequest(&runnerv1.UpdateTaskRequest{
|
||||
State: &runnerv1.TaskState{
|
||||
Id: task.ID,
|
||||
Result: runnerv1.Result_RESULT_SUCCESS,
|
||||
},
|
||||
}),
|
||||
)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, resp)
|
||||
assert.Equal(t, runnerv1.Result_RESULT_SUCCESS, resp.Msg.State.Result)
|
||||
|
||||
// Expect the ephemeral runner has been deleted
|
||||
_, err = actions_model.GetRunnerByID(context.Background(), 10000008)
|
||||
assert.ErrorIs(t, err, util.ErrNotExist, "ephemeral runner should be deleted after task completion")
|
||||
})
|
||||
}
|
||||
|
||||
func TestEphemeralRunnerDeletedByTaskZombieCleanup(t *testing.T) {
|
||||
if !setting.Database.Type.IsSQLite3() {
|
||||
t.Skip()
|
||||
}
|
||||
|
||||
defer unittest.OverrideFixtures("tests/integration/fixtures/TestEphemeralRunner")()
|
||||
|
||||
onApplicationRun(t, func(t *testing.T, u *url.URL) {
|
||||
// Verify runner exists before the test
|
||||
runner, err := actions_model.GetRunnerByID(context.Background(), 10000011)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, runner)
|
||||
require.True(t, runner.Ephemeral, "runner should be ephemeral")
|
||||
|
||||
// Verify zombie task exists and is running
|
||||
task := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionTask{ID: 10055})
|
||||
assert.Equal(t, actions_model.StatusRunning, task.Status)
|
||||
assert.Equal(t, int64(10000011), task.RunnerID)
|
||||
|
||||
// Run zombie task cleanup
|
||||
err = actions_service.StopZombieTasks(context.Background())
|
||||
require.NoError(t, err)
|
||||
|
||||
// Expect the ephemeral runner has been deleted
|
||||
_, err = actions_model.GetRunnerByID(context.Background(), 10000011)
|
||||
assert.ErrorIs(t, err, util.ErrNotExist, "ephemeral runner should be deleted after zombie task cleanup")
|
||||
})
|
||||
}
|
||||
|
||||
func TestEphemeralRunnerDeletionOnRepositoryDeletion(t *testing.T) {
|
||||
if !setting.Database.Type.IsSQLite3() {
|
||||
t.Skip()
|
||||
}
|
||||
|
||||
defer unittest.OverrideFixtures("tests/integration/fixtures/TestEphemeralRunner")()
|
||||
|
||||
onApplicationRun(t, func(t *testing.T, u *url.URL) {
|
||||
runner, err := actions_model.GetRunnerByID(t.Context(), 10000008)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, int64(0), runner.OwnerID, "runner should not start in user scope")
|
||||
assert.NotEqual(t, int64(0), runner.RepoID, "runner should start in repo scope")
|
||||
|
||||
task := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionTask{ID: 10054})
|
||||
assert.Equal(t, actions_model.StatusRunning, task.Status)
|
||||
|
||||
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 5})
|
||||
err = repo_service.DeleteRepositoryDirectly(t.Context(), user, task.RepoID, true)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = actions_model.GetRunnerByID(t.Context(), 10000008)
|
||||
assert.ErrorIs(t, err, util.ErrNotExist)
|
||||
})
|
||||
}
|
||||
|
||||
// Test that the ephemeral runner is deleted when a user is deleted
|
||||
func TestEphemeralRunnerDeletionOnUserDeletion(t *testing.T) {
|
||||
if !setting.Database.Type.IsSQLite3() {
|
||||
t.Skip()
|
||||
}
|
||||
|
||||
defer unittest.OverrideFixtures("tests/integration/fixtures/TestEphemeralRunner")()
|
||||
|
||||
onApplicationRun(t, func(t *testing.T, u *url.URL) {
|
||||
runner, err := actions_model.GetRunnerByID(t.Context(), 10000012)
|
||||
require.NoError(t, err)
|
||||
assert.NotEqual(t, int64(0), runner.OwnerID, "runner should start in user scope")
|
||||
assert.Equal(t, int64(0), runner.RepoID, "runner should not start in repo scope")
|
||||
|
||||
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
||||
err = user_service.DeleteUser(t.Context(), user, true)
|
||||
require.NoError(t, err)
|
||||
|
||||
unittest.AssertNotExistsBean(t, runner)
|
||||
})
|
||||
}
|
||||
|
||||
// Test that the ephemeral runner is deleted when an organization is deleted
|
||||
func TestEphemeralRunnerDeletionOnOrgDeletion(t *testing.T) {
|
||||
if !setting.Database.Type.IsSQLite3() {
|
||||
t.Skip()
|
||||
}
|
||||
|
||||
defer unittest.OverrideFixtures("tests/integration/fixtures/TestEphemeralRunner")()
|
||||
|
||||
onApplicationRun(t, func(t *testing.T, u *url.URL) {
|
||||
runner, err := actions_model.GetRunnerByID(t.Context(), 10000013)
|
||||
require.NoError(t, err)
|
||||
assert.NotEqual(t, int64(0), runner.OwnerID, "runner should start in org scope")
|
||||
assert.Equal(t, int64(0), runner.RepoID, "runner should not start in repo scope")
|
||||
|
||||
org := unittest.AssertExistsAndLoadBean(t, &org_model.Organization{ID: runner.OwnerID})
|
||||
err = org_service.DeleteOrganization(t.Context(), org, true)
|
||||
require.NoError(t, err)
|
||||
|
||||
unittest.AssertNotExistsBean(t, runner)
|
||||
})
|
||||
}
|
||||
|
|
@ -33,4 +33,14 @@
|
|||
repo_id: 62
|
||||
description: ""
|
||||
agent_labels: ["nixos"]
|
||||
deleted: 0
|
||||
deleted: 0
|
||||
- id: 130795
|
||||
uuid: "16ca1a5c-8024-41f1-be31-e55830263cc6"
|
||||
name: "runner-5-ephemeral"
|
||||
version: "1.0.0"
|
||||
owner_id: 0
|
||||
repo_id: 0
|
||||
description: "An ephemeral runner"
|
||||
agent_labels: ["ephemeral-label"]
|
||||
ephemeral: true
|
||||
deleted: 0
|
||||
|
|
|
|||
|
|
@ -34,3 +34,13 @@
|
|||
description: ""
|
||||
agent_labels: []
|
||||
deleted: 0
|
||||
- id: 655695
|
||||
uuid: "0851ed0a-f0af-4a01-9b98-fc9bf9c1d332"
|
||||
name: "runner-5-ephemeral"
|
||||
version: "1.0.0"
|
||||
owner_id: 3
|
||||
repo_id: 0
|
||||
description: "An ephemeral runner"
|
||||
agent_labels: ["ephemeral-label"]
|
||||
ephemeral: true
|
||||
deleted: 0
|
||||
|
|
|
|||
|
|
@ -34,3 +34,13 @@
|
|||
description: ""
|
||||
agent_labels: []
|
||||
deleted: 0
|
||||
- id: 899255
|
||||
uuid: "96639646-67b2-4bcb-9142-fde1ab8498cf"
|
||||
name: "runner-5-repository-ephemeral"
|
||||
version: "1.0.0"
|
||||
owner_id: 0
|
||||
repo_id: 62
|
||||
description: "An ephemeral runner"
|
||||
agent_labels: ["ephemeral-label"]
|
||||
ephemeral: true
|
||||
deleted: 0
|
||||
|
|
|
|||
|
|
@ -34,3 +34,13 @@
|
|||
description: ""
|
||||
agent_labels: []
|
||||
deleted: 0
|
||||
- id: 71305
|
||||
uuid: "3ca04a95-3e75-4e48-8b7a-63427ebcf3b8"
|
||||
name: "runner-5-user-ephemeral"
|
||||
version: "1.0.0"
|
||||
owner_id: 2
|
||||
repo_id: 0
|
||||
description: "An ephemeral runner"
|
||||
agent_labels: ["ephemeral-label"]
|
||||
ephemeral: true
|
||||
deleted: 0
|
||||
|
|
|
|||
|
|
@ -0,0 +1,61 @@
|
|||
-
|
||||
id: 10895
|
||||
title: "running workflow_dispatch run"
|
||||
repo_id: 64
|
||||
owner_id: 3
|
||||
workflow_id: "running.yaml"
|
||||
index: 3
|
||||
trigger_user_id: 2
|
||||
ref: "refs/heads/main"
|
||||
commit_sha: "c2d72f548424103f01ee1dc02889c1e2bff816b0"
|
||||
event: "workflow_dispatch"
|
||||
trigger_event: "workflow_dispatch"
|
||||
is_fork_pull_request: 0
|
||||
status: 6 # running
|
||||
started: 1683636528
|
||||
created: 1683636108
|
||||
updated: 1683636626
|
||||
need_approval: 0
|
||||
approved_by: 0
|
||||
event_payload: '{}'
|
||||
|
||||
-
|
||||
id: 10896
|
||||
title: "running workflow_dispatch run"
|
||||
repo_id: 64
|
||||
owner_id: 3
|
||||
workflow_id: "running.yaml"
|
||||
index: 4
|
||||
trigger_user_id: 2
|
||||
ref: "refs/heads/main"
|
||||
commit_sha: "c2d72f548424103f01ee1dc02889c1e2bff816b0"
|
||||
event: "workflow_dispatch"
|
||||
trigger_event: "workflow_dispatch"
|
||||
is_fork_pull_request: 0
|
||||
status: 6 # running
|
||||
started: 1683636528
|
||||
created: 1683636108
|
||||
updated: 1683636626
|
||||
need_approval: 0
|
||||
approved_by: 0
|
||||
event_payload: '{}'
|
||||
|
||||
-
|
||||
id: 10897
|
||||
title: "waiting workflow for user runner scope test"
|
||||
repo_id: 64
|
||||
owner_id: 10
|
||||
workflow_id: "waiting.yaml"
|
||||
index: 5
|
||||
trigger_user_id: 10
|
||||
ref: "refs/heads/main"
|
||||
commit_sha: "c2d72f548424103f01ee1dc02889c1e2bff816b0"
|
||||
event: "push"
|
||||
is_fork_pull_request: 0
|
||||
status: 5 # waiting
|
||||
started: 0
|
||||
created: 1683636108
|
||||
updated: 1683636108
|
||||
need_approval: 0
|
||||
approved_by: 0
|
||||
event_payload: '{"head_commit":{"id":"c2d72f548424103f01ee1dc02889c1e2bff816b0"}}'
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
-
|
||||
id: 10398
|
||||
run_id: 10895
|
||||
repo_id: 64
|
||||
owner_id: 3
|
||||
commit_sha: c2d72f548424103f01ee1dc02889c1e2bff816b0
|
||||
is_fork_pull_request: 0
|
||||
name: job_3
|
||||
attempt: 1
|
||||
job_id: job_3
|
||||
runs_on: '["fedora"]'
|
||||
task_id: 10054
|
||||
status: 6
|
||||
started: 1683636528
|
||||
stopped: 1683636626
|
||||
-
|
||||
id: 10399
|
||||
run_id: 10896
|
||||
repo_id: 64
|
||||
owner_id: 3
|
||||
commit_sha: c2d72f548424103f01ee1dc02889c1e2bff816b0
|
||||
is_fork_pull_request: 0
|
||||
name: job_3
|
||||
runs_on: '["fedora"]'
|
||||
attempt: 1
|
||||
job_id: job_3
|
||||
task_id: 10055
|
||||
status: 6
|
||||
started: 1683636528
|
||||
stopped: 1683636626
|
||||
-
|
||||
id: 10401
|
||||
run_id: 10897
|
||||
repo_id: 64
|
||||
owner_id: 10
|
||||
commit_sha: c2d72f548424103f01ee1dc02889c1e2bff816b0
|
||||
is_fork_pull_request: 0
|
||||
name: job_for_ephemeral_runner_test
|
||||
attempt: 0
|
||||
job_id: job_ephemeral_test
|
||||
task_id: 0
|
||||
status: 5
|
||||
runs_on: '["job_for_org_runner_scope_test"]'
|
||||
workflow_payload: '{"jobs":{"job_ephemeral_test":{"runs-on":["job_for_org_runner_scope_test"],"steps":[{"run":"echo test"}]}}}'
|
||||
started: 0
|
||||
stopped: 0
|
||||
|
|
@ -0,0 +1,75 @@
|
|||
-
|
||||
id: 10000008
|
||||
name: ephemeral_runner_to_be_deleted
|
||||
uuid: 3FF231BD-FBB7-4E4B-9602-E6F28363EF20
|
||||
token_hash: e379fa0089b8829085497fd5231f170f4e5ab44fa3dc7c4e2b5b5ce72e01fab2930d9e2ea3a65183f2db93c00e3a3dc9cae2
|
||||
token_salt: saltysaltsalt
|
||||
# token: mysuuupersecrettoekn
|
||||
ephemeral: true
|
||||
version: "1.0.0"
|
||||
owner_id: 0
|
||||
repo_id: 64
|
||||
description: "This runner is going to be deleted"
|
||||
agent_labels: '["job_for_org_runner_scope_test"]'
|
||||
- # ephemeral org runner for scope change test
|
||||
id: 10000010
|
||||
name: ephemeral_org_runner_for_scope_test
|
||||
uuid: a1b2c3d4-e5f6-4a5b-8c9d-0e1f2a3b4c5d
|
||||
token_hash: c0ff52ff7e163e2bd7ed8654858d4532b7852f767347c413b50deed646886e3255d56f232785cab6bdc1eb667707eaa1e70e
|
||||
token_salt: saltysaltsalt
|
||||
# token: 379fa0089b8829085497fd5231f170f4e
|
||||
ephemeral: true
|
||||
version: "1.0.0"
|
||||
owner_id: 10
|
||||
repo_id: 0
|
||||
description: "Ephemeral org runner for testing scope change"
|
||||
agent_labels: '["job_for_org_runner_scope_test"]'
|
||||
created: 1716104432
|
||||
updated: 1716104432
|
||||
deleted: ~
|
||||
- # ephemeral runner for zombie task test
|
||||
id: 10000011
|
||||
name: ephemeral_runner_for_zombie_test
|
||||
uuid: b2c3d4e5-f6a7-5b6c-9d0e-1f2a3b4c5d6e
|
||||
token_hash: notvalid
|
||||
token_salt: notvalid
|
||||
ephemeral: true
|
||||
version: "1.0.0"
|
||||
owner_id: 0
|
||||
repo_id: 64
|
||||
description: "Ephemeral runner for testing zombie cleanup"
|
||||
agent_labels: '["zombie-test"]'
|
||||
created: 1716104432
|
||||
updated: 1716104432
|
||||
deleted: ~
|
||||
- # ephemeral user-scoped runner for user deletion test
|
||||
id: 10000012
|
||||
name: ephemeral_user_runner_for_deletion_test
|
||||
uuid: c3d4e5f6-a7b8-6c7d-0e1f-2a3b4c5d6e7f
|
||||
token_hash: f480ga1190c9930196508ge6342g271g5f6bc56c878458d524c61feff757f8746166e67c343896dbc7cde2fc778808fbb2f81
|
||||
token_salt: saltysaltsalt
|
||||
# token: userdeletiontoken123456
|
||||
ephemeral: true
|
||||
version: "1.0.0"
|
||||
owner_id: 2
|
||||
repo_id: 0
|
||||
description: "Ephemeral user runner for testing user deletion"
|
||||
agent_labels: '["user-deletion-test"]'
|
||||
created: 1716104432
|
||||
updated: 1716104432
|
||||
deleted: ~
|
||||
- id: 10000013
|
||||
name: ephemeral_org_runner_for_deletion_test
|
||||
uuid: d4e5f6a7-b8c9-7d8e-1f2a-3b4c5d6e7f8a
|
||||
token_hash: fix-the-hash-if-you-want-to-use-runner-api
|
||||
token_salt: saltysaltsalt
|
||||
# token: orgdeletiontoken123456
|
||||
ephemeral: true
|
||||
version: "1.0.0"
|
||||
owner_id: 3
|
||||
repo_id: 0
|
||||
description: "Ephemeral org runner for testing org deletion"
|
||||
agent_labels: '["org-deletion-test"]'
|
||||
created: 1716104432
|
||||
updated: 1716104432
|
||||
deleted: ~
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
-
|
||||
id: 10054
|
||||
job_id: 10398
|
||||
attempt: 1
|
||||
runner_id: 10000008
|
||||
status: 6 # running
|
||||
started: 1683636528
|
||||
stopped: 1683636626
|
||||
repo_id: 64
|
||||
owner_id: 3
|
||||
commit_sha: c2d72f548424103f01ee1dc02889c1e2bff816b0
|
||||
is_fork_pull_request: 0
|
||||
token_hash: f8d3962425466b6709b9ac51446f93260c54afe8e7b6d3686e34f991fb8a8953822b0deed86fe41a103f34bc48dbc4784222
|
||||
token_salt: ffffffffff
|
||||
token_last_eight: ffffffff
|
||||
log_filename: artifact-test2/2f/47.log
|
||||
log_in_storage: 1
|
||||
log_length: 707
|
||||
log_size: 90179
|
||||
log_expired: 0
|
||||
-
|
||||
id: 10055
|
||||
job_id: 10399
|
||||
attempt: 1
|
||||
runner_id: 10000011
|
||||
status: 6 # running
|
||||
started: 946684810
|
||||
stopped: 0
|
||||
repo_id: 64
|
||||
owner_id: 3
|
||||
commit_sha: c2d72f548424103f01ee1dc02889c1e2bff816b0
|
||||
is_fork_pull_request: 0
|
||||
token_hash: a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0
|
||||
token_salt: zombietask
|
||||
token_last_eight: zombietk
|
||||
log_filename: zombie-task/55.log
|
||||
log_in_storage: 0
|
||||
log_length: 100
|
||||
log_size: 1000
|
||||
log_expired: 0
|
||||
updated: 946684810
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
-
|
||||
id: 121
|
||||
repo_id: 64
|
||||
type: 10
|
||||
config: "{}"
|
||||
created_unix: 946684810
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
|
||||
-
|
||||
id: 64
|
||||
owner_id: 10
|
||||
owner_name: user10
|
||||
lower_name: test_ephemeral_runner
|
||||
name: test_ephemeral_runner
|
||||
default_branch: main
|
||||
num_watches: 0
|
||||
num_stars: 0
|
||||
num_forks: 0
|
||||
num_milestones: 0
|
||||
num_closed_milestones: 0
|
||||
num_projects: 0
|
||||
num_closed_projects: 0
|
||||
is_private: false
|
||||
is_empty: false
|
||||
is_archived: false
|
||||
is_mirror: false
|
||||
status: 0
|
||||
is_fork: false
|
||||
fork_id: 0
|
||||
is_template: false
|
||||
template_id: 0
|
||||
size: 0
|
||||
is_fsck_enabled: true
|
||||
close_issues_via_commit_in_any_branch: false
|
||||
topics: '[]'
|
||||
Loading…
Reference in a new issue