mirror of
https://codeberg.org/forgejo/forgejo.git
synced 2026-05-12 22:10:25 +00: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
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue