Use env GITEA_RUNNER_REGISTRATION_TOKEN as global runner token (#32946) (#32964)

Backport #32946 by wxiaoguang

Fix #23703

When Gitea starts, it reads GITEA_RUNNER_REGISTRATION_TOKEN
or GITEA_RUNNER_REGISTRATION_TOKEN_FILE to add registration token.

Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
This commit is contained in:
Giteabot 2024-12-24 00:37:11 +08:00 committed by GitHub
parent 0c581106d2
commit 90bd08ceef
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 152 additions and 18 deletions

View File

@ -10,6 +10,7 @@ import (
"code.gitea.io/gitea/models/db"
repo_model "code.gitea.io/gitea/models/repo"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/timeutil"
"code.gitea.io/gitea/modules/util"
)
@ -51,7 +52,7 @@ func GetRunnerToken(ctx context.Context, token string) (*ActionRunnerToken, erro
if err != nil {
return nil, err
} else if !has {
return nil, fmt.Errorf("runner token %q: %w", token, util.ErrNotExist)
return nil, fmt.Errorf(`runner token "%s...": %w`, base.TruncateString(token, 3), util.ErrNotExist)
}
return &runnerToken, nil
}
@ -68,19 +69,15 @@ func UpdateRunnerToken(ctx context.Context, r *ActionRunnerToken, cols ...string
return err
}
// NewRunnerToken creates a new active runner token and invalidate all old tokens
// NewRunnerTokenWithValue creates a new active runner token and invalidate all old tokens
// ownerID will be ignored and treated as 0 if repoID is non-zero.
func NewRunnerToken(ctx context.Context, ownerID, repoID int64) (*ActionRunnerToken, error) {
func NewRunnerTokenWithValue(ctx context.Context, ownerID, repoID int64, token string) (*ActionRunnerToken, error) {
if ownerID != 0 && repoID != 0 {
// It's trying to create a runner token that belongs to a repository, but OwnerID has been set accidentally.
// Remove OwnerID to avoid confusion; it's not worth returning an error here.
ownerID = 0
}
token, err := util.CryptoRandomString(40)
if err != nil {
return nil, err
}
runnerToken := &ActionRunnerToken{
OwnerID: ownerID,
RepoID: repoID,
@ -95,11 +92,19 @@ func NewRunnerToken(ctx context.Context, ownerID, repoID int64) (*ActionRunnerTo
return err
}
_, err = db.GetEngine(ctx).Insert(runnerToken)
_, err := db.GetEngine(ctx).Insert(runnerToken)
return err
})
}
func NewRunnerToken(ctx context.Context, ownerID, repoID int64) (*ActionRunnerToken, error) {
token, err := util.CryptoRandomString(40)
if err != nil {
return nil, err
}
return NewRunnerTokenWithValue(ctx, ownerID, repoID, token)
}
// GetLatestRunnerToken returns the latest runner token
func GetLatestRunnerToken(ctx context.Context, ownerID, repoID int64) (*ActionRunnerToken, error) {
if ownerID != 0 && repoID != 0 {

View File

@ -3722,6 +3722,7 @@ runners.status.active = Active
runners.status.offline = Offline
runners.version = Version
runners.reset_registration_token = Reset registration token
runners.reset_registration_token_confirm = Would you like to invalidate the current token and generate a new one?
runners.reset_registration_token_success = Runner registration token reset successfully
runs.all_workflows = All Workflows

View File

@ -171,7 +171,7 @@ func InitWebInstalled(ctx context.Context) {
auth.Init()
mustInit(svg.Init)
actions_service.Init()
mustInitCtx(ctx, actions_service.Init)
mustInit(repo_service.InitLicenseClassifier)

View File

@ -136,9 +136,8 @@ func RunnerResetRegistrationToken(ctx *context.Context, ownerID, repoID int64, r
ctx.ServerError("ResetRunnerRegistrationToken", err)
return
}
ctx.Flash.Success(ctx.Tr("actions.runners.reset_registration_token_success"))
ctx.Redirect(redirectTo)
ctx.JSONRedirect(redirectTo)
}
// RunnerDeletePost response for deleting a runner

View File

@ -463,7 +463,7 @@ func registerRoutes(m *web.Router) {
m.Combo("/{runnerid}").Get(repo_setting.RunnersEdit).
Post(web.Bind(forms.EditRunnerForm{}), repo_setting.RunnersEditPost)
m.Post("/{runnerid}/delete", repo_setting.RunnerDeletePost)
m.Get("/reset_registration_token", repo_setting.ResetRunnerRegistrationToken)
m.Post("/reset_registration_token", repo_setting.ResetRunnerRegistrationToken)
})
}

View File

@ -4,23 +4,68 @@
package actions
import (
"context"
"errors"
"fmt"
"os"
"strings"
actions_model "code.gitea.io/gitea/models/actions"
"code.gitea.io/gitea/modules/graceful"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/queue"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/util"
notify_service "code.gitea.io/gitea/services/notify"
)
func Init() {
func initGlobalRunnerToken(ctx context.Context) error {
// use the same env name as the runner, for consistency
token := os.Getenv("GITEA_RUNNER_REGISTRATION_TOKEN")
tokenFile := os.Getenv("GITEA_RUNNER_REGISTRATION_TOKEN_FILE")
if token != "" && tokenFile != "" {
return errors.New("both GITEA_RUNNER_REGISTRATION_TOKEN and GITEA_RUNNER_REGISTRATION_TOKEN_FILE are set, only one can be used")
}
if tokenFile != "" {
file, err := os.ReadFile(tokenFile)
if err != nil {
return fmt.Errorf("unable to read GITEA_RUNNER_REGISTRATION_TOKEN_FILE: %w", err)
}
token = strings.TrimSpace(string(file))
}
if token == "" {
return nil
}
if len(token) < 32 {
return errors.New("GITEA_RUNNER_REGISTRATION_TOKEN must be at least 32 random characters")
}
existing, err := actions_model.GetRunnerToken(ctx, token)
if err != nil && !errors.Is(err, util.ErrNotExist) {
return fmt.Errorf("unable to check existing token: %w", err)
}
if existing != nil {
if !existing.IsActive {
log.Warn("The token defined by GITEA_RUNNER_REGISTRATION_TOKEN is already invalidated, please use the latest one from web UI")
}
return nil
}
_, err = actions_model.NewRunnerTokenWithValue(ctx, 0, 0, token)
return err
}
func Init(ctx context.Context) error {
if !setting.Actions.Enabled {
return
return nil
}
jobEmitterQueue = queue.CreateUniqueQueue(graceful.GetManager().ShutdownContext(), "actions_ready_job", jobEmitterQueueHandler)
if jobEmitterQueue == nil {
log.Fatal("Unable to create actions_ready_job queue")
return errors.New("unable to create actions_ready_job queue")
}
go graceful.GetManager().RunWithCancel(jobEmitterQueue)
notify_service.RegisterNotifier(NewNotifier())
return initGlobalRunnerToken(ctx)
}

View File

@ -0,0 +1,80 @@
// Copyright 2024 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package actions
import (
"os"
"testing"
actions_model "code.gitea.io/gitea/models/actions"
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/models/unittest"
"code.gitea.io/gitea/modules/util"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestMain(m *testing.M) {
unittest.MainTest(m, &unittest.TestOptions{
FixtureFiles: []string{"action_runner_token.yml"},
})
os.Exit(m.Run())
}
func TestInitToken(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
t.Run("NoToken", func(t *testing.T) {
_, _ = db.Exec(db.DefaultContext, "DELETE FROM action_runner_token")
t.Setenv("GITEA_RUNNER_REGISTRATION_TOKEN", "")
t.Setenv("GITEA_RUNNER_REGISTRATION_TOKEN_FILE", "")
err := initGlobalRunnerToken(db.DefaultContext)
require.NoError(t, err)
notEmpty, err := db.IsTableNotEmpty(&actions_model.ActionRunnerToken{})
require.NoError(t, err)
assert.False(t, notEmpty)
})
t.Run("EnvToken", func(t *testing.T) {
tokenValue, _ := util.CryptoRandomString(32)
t.Setenv("GITEA_RUNNER_REGISTRATION_TOKEN", tokenValue)
t.Setenv("GITEA_RUNNER_REGISTRATION_TOKEN_FILE", "")
err := initGlobalRunnerToken(db.DefaultContext)
require.NoError(t, err)
token := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRunnerToken{Token: tokenValue})
assert.True(t, token.IsActive)
// init with the same token again, should not create a new token
err = initGlobalRunnerToken(db.DefaultContext)
require.NoError(t, err)
token2 := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRunnerToken{Token: tokenValue})
assert.Equal(t, token.ID, token2.ID)
assert.True(t, token.IsActive)
})
t.Run("EnvFileToken", func(t *testing.T) {
tokenValue, _ := util.CryptoRandomString(32)
f := t.TempDir() + "/token"
_ = os.WriteFile(f, []byte(tokenValue), 0o644)
t.Setenv("GITEA_RUNNER_REGISTRATION_TOKEN", "")
t.Setenv("GITEA_RUNNER_REGISTRATION_TOKEN_FILE", f)
err := initGlobalRunnerToken(db.DefaultContext)
require.NoError(t, err)
token := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRunnerToken{Token: tokenValue})
assert.True(t, token.IsActive)
// if the env token is invalidated by another new token, then it shouldn't be active anymore
_, err = actions_model.NewRunnerToken(db.DefaultContext, 0, 0)
require.NoError(t, err)
token = unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRunnerToken{Token: tokenValue})
assert.False(t, token.IsActive)
})
t.Run("InvalidToken", func(t *testing.T) {
t.Setenv("GITEA_RUNNER_REGISTRATION_TOKEN", "abc")
err := initGlobalRunnerToken(db.DefaultContext)
assert.ErrorContains(t, err, "must be at least")
})
}

View File

@ -3,7 +3,7 @@
<h4 class="ui top attached header">
{{ctx.Locale.Tr "actions.runners.runner_manage_panel"}} ({{ctx.Locale.Tr "admin.total" .Total}})
<div class="ui right">
<div class="ui top right pointing dropdown">
<div class="ui top right pointing dropdown jump">
<button class="ui primary tiny button">
{{ctx.Locale.Tr "actions.runners.new"}}
{{svg "octicon-triangle-down" 14 "dropdown icon"}}
@ -17,14 +17,18 @@
Registration Token
</div>
<div class="ui input">
<input type="text" value="{{.RegistrationToken}}">
<input type="text" value="{{.RegistrationToken}}" readonly>
<button class="ui basic label button" aria-label="{{ctx.Locale.Tr "copy"}}" data-clipboard-text="{{.RegistrationToken}}">
{{svg "octicon-copy" 14}}
</button>
</div>
<div class="divider"></div>
<div class="item">
<a href="{{$.Link}}/reset_registration_token">{{ctx.Locale.Tr "actions.runners.reset_registration_token"}}</a>
<a class="link-action" data-url="{{$.Link}}/reset_registration_token"
data-modal-confirm="{{ctx.Locale.Tr "actions.runners.reset_registration_token_confirm"}}"
>
{{ctx.Locale.Tr "actions.runners.reset_registration_token"}}
</a>
</div>
</div>
</div>