Skip to content

Commit 90bd08c

Browse files
GiteaBotwxiaoguang
andauthored
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>
1 parent 0c58110 commit 90bd08c

File tree

8 files changed

+152
-18
lines changed

8 files changed

+152
-18
lines changed

models/actions/runner_token.go

+13-8
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
"code.gitea.io/gitea/models/db"
1111
repo_model "code.gitea.io/gitea/models/repo"
1212
user_model "code.gitea.io/gitea/models/user"
13+
"code.gitea.io/gitea/modules/base"
1314
"code.gitea.io/gitea/modules/timeutil"
1415
"code.gitea.io/gitea/modules/util"
1516
)
@@ -51,7 +52,7 @@ func GetRunnerToken(ctx context.Context, token string) (*ActionRunnerToken, erro
5152
if err != nil {
5253
return nil, err
5354
} else if !has {
54-
return nil, fmt.Errorf("runner token %q: %w", token, util.ErrNotExist)
55+
return nil, fmt.Errorf(`runner token "%s...": %w`, base.TruncateString(token, 3), util.ErrNotExist)
5556
}
5657
return &runnerToken, nil
5758
}
@@ -68,19 +69,15 @@ func UpdateRunnerToken(ctx context.Context, r *ActionRunnerToken, cols ...string
6869
return err
6970
}
7071

71-
// NewRunnerToken creates a new active runner token and invalidate all old tokens
72+
// NewRunnerTokenWithValue creates a new active runner token and invalidate all old tokens
7273
// ownerID will be ignored and treated as 0 if repoID is non-zero.
73-
func NewRunnerToken(ctx context.Context, ownerID, repoID int64) (*ActionRunnerToken, error) {
74+
func NewRunnerTokenWithValue(ctx context.Context, ownerID, repoID int64, token string) (*ActionRunnerToken, error) {
7475
if ownerID != 0 && repoID != 0 {
7576
// It's trying to create a runner token that belongs to a repository, but OwnerID has been set accidentally.
7677
// Remove OwnerID to avoid confusion; it's not worth returning an error here.
7778
ownerID = 0
7879
}
7980

80-
token, err := util.CryptoRandomString(40)
81-
if err != nil {
82-
return nil, err
83-
}
8481
runnerToken := &ActionRunnerToken{
8582
OwnerID: ownerID,
8683
RepoID: repoID,
@@ -95,11 +92,19 @@ func NewRunnerToken(ctx context.Context, ownerID, repoID int64) (*ActionRunnerTo
9592
return err
9693
}
9794

98-
_, err = db.GetEngine(ctx).Insert(runnerToken)
95+
_, err := db.GetEngine(ctx).Insert(runnerToken)
9996
return err
10097
})
10198
}
10299

100+
func NewRunnerToken(ctx context.Context, ownerID, repoID int64) (*ActionRunnerToken, error) {
101+
token, err := util.CryptoRandomString(40)
102+
if err != nil {
103+
return nil, err
104+
}
105+
return NewRunnerTokenWithValue(ctx, ownerID, repoID, token)
106+
}
107+
103108
// GetLatestRunnerToken returns the latest runner token
104109
func GetLatestRunnerToken(ctx context.Context, ownerID, repoID int64) (*ActionRunnerToken, error) {
105110
if ownerID != 0 && repoID != 0 {

options/locale/locale_en-US.ini

+1
Original file line numberDiff line numberDiff line change
@@ -3722,6 +3722,7 @@ runners.status.active = Active
37223722
runners.status.offline = Offline
37233723
runners.version = Version
37243724
runners.reset_registration_token = Reset registration token
3725+
runners.reset_registration_token_confirm = Would you like to invalidate the current token and generate a new one?
37253726
runners.reset_registration_token_success = Runner registration token reset successfully
37263727
37273728
runs.all_workflows = All Workflows

routers/init.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -171,7 +171,7 @@ func InitWebInstalled(ctx context.Context) {
171171
auth.Init()
172172
mustInit(svg.Init)
173173

174-
actions_service.Init()
174+
mustInitCtx(ctx, actions_service.Init)
175175

176176
mustInit(repo_service.InitLicenseClassifier)
177177

routers/web/shared/actions/runners.go

+1-2
Original file line numberDiff line numberDiff line change
@@ -136,9 +136,8 @@ func RunnerResetRegistrationToken(ctx *context.Context, ownerID, repoID int64, r
136136
ctx.ServerError("ResetRunnerRegistrationToken", err)
137137
return
138138
}
139-
140139
ctx.Flash.Success(ctx.Tr("actions.runners.reset_registration_token_success"))
141-
ctx.Redirect(redirectTo)
140+
ctx.JSONRedirect(redirectTo)
142141
}
143142

144143
// RunnerDeletePost response for deleting a runner

routers/web/web.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -463,7 +463,7 @@ func registerRoutes(m *web.Router) {
463463
m.Combo("/{runnerid}").Get(repo_setting.RunnersEdit).
464464
Post(web.Bind(forms.EditRunnerForm{}), repo_setting.RunnersEditPost)
465465
m.Post("/{runnerid}/delete", repo_setting.RunnerDeletePost)
466-
m.Get("/reset_registration_token", repo_setting.ResetRunnerRegistrationToken)
466+
m.Post("/reset_registration_token", repo_setting.ResetRunnerRegistrationToken)
467467
})
468468
}
469469

services/actions/init.go

+48-3
Original file line numberDiff line numberDiff line change
@@ -4,23 +4,68 @@
44
package actions
55

66
import (
7+
"context"
8+
"errors"
9+
"fmt"
10+
"os"
11+
"strings"
12+
13+
actions_model "code.gitea.io/gitea/models/actions"
714
"code.gitea.io/gitea/modules/graceful"
815
"code.gitea.io/gitea/modules/log"
916
"code.gitea.io/gitea/modules/queue"
1017
"code.gitea.io/gitea/modules/setting"
18+
"code.gitea.io/gitea/modules/util"
1119
notify_service "code.gitea.io/gitea/services/notify"
1220
)
1321

14-
func Init() {
22+
func initGlobalRunnerToken(ctx context.Context) error {
23+
// use the same env name as the runner, for consistency
24+
token := os.Getenv("GITEA_RUNNER_REGISTRATION_TOKEN")
25+
tokenFile := os.Getenv("GITEA_RUNNER_REGISTRATION_TOKEN_FILE")
26+
if token != "" && tokenFile != "" {
27+
return errors.New("both GITEA_RUNNER_REGISTRATION_TOKEN and GITEA_RUNNER_REGISTRATION_TOKEN_FILE are set, only one can be used")
28+
}
29+
if tokenFile != "" {
30+
file, err := os.ReadFile(tokenFile)
31+
if err != nil {
32+
return fmt.Errorf("unable to read GITEA_RUNNER_REGISTRATION_TOKEN_FILE: %w", err)
33+
}
34+
token = strings.TrimSpace(string(file))
35+
}
36+
if token == "" {
37+
return nil
38+
}
39+
40+
if len(token) < 32 {
41+
return errors.New("GITEA_RUNNER_REGISTRATION_TOKEN must be at least 32 random characters")
42+
}
43+
44+
existing, err := actions_model.GetRunnerToken(ctx, token)
45+
if err != nil && !errors.Is(err, util.ErrNotExist) {
46+
return fmt.Errorf("unable to check existing token: %w", err)
47+
}
48+
if existing != nil {
49+
if !existing.IsActive {
50+
log.Warn("The token defined by GITEA_RUNNER_REGISTRATION_TOKEN is already invalidated, please use the latest one from web UI")
51+
}
52+
return nil
53+
}
54+
_, err = actions_model.NewRunnerTokenWithValue(ctx, 0, 0, token)
55+
return err
56+
}
57+
58+
func Init(ctx context.Context) error {
1559
if !setting.Actions.Enabled {
16-
return
60+
return nil
1761
}
1862

1963
jobEmitterQueue = queue.CreateUniqueQueue(graceful.GetManager().ShutdownContext(), "actions_ready_job", jobEmitterQueueHandler)
2064
if jobEmitterQueue == nil {
21-
log.Fatal("Unable to create actions_ready_job queue")
65+
return errors.New("unable to create actions_ready_job queue")
2266
}
2367
go graceful.GetManager().RunWithCancel(jobEmitterQueue)
2468

2569
notify_service.RegisterNotifier(NewNotifier())
70+
return initGlobalRunnerToken(ctx)
2671
}

services/actions/init_test.go

+80
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
// Copyright 2024 The Gitea Authors. All rights reserved.
2+
// SPDX-License-Identifier: MIT
3+
4+
package actions
5+
6+
import (
7+
"os"
8+
"testing"
9+
10+
actions_model "code.gitea.io/gitea/models/actions"
11+
"code.gitea.io/gitea/models/db"
12+
"code.gitea.io/gitea/models/unittest"
13+
"code.gitea.io/gitea/modules/util"
14+
15+
"github.com/stretchr/testify/assert"
16+
"github.com/stretchr/testify/require"
17+
)
18+
19+
func TestMain(m *testing.M) {
20+
unittest.MainTest(m, &unittest.TestOptions{
21+
FixtureFiles: []string{"action_runner_token.yml"},
22+
})
23+
os.Exit(m.Run())
24+
}
25+
26+
func TestInitToken(t *testing.T) {
27+
assert.NoError(t, unittest.PrepareTestDatabase())
28+
29+
t.Run("NoToken", func(t *testing.T) {
30+
_, _ = db.Exec(db.DefaultContext, "DELETE FROM action_runner_token")
31+
t.Setenv("GITEA_RUNNER_REGISTRATION_TOKEN", "")
32+
t.Setenv("GITEA_RUNNER_REGISTRATION_TOKEN_FILE", "")
33+
err := initGlobalRunnerToken(db.DefaultContext)
34+
require.NoError(t, err)
35+
notEmpty, err := db.IsTableNotEmpty(&actions_model.ActionRunnerToken{})
36+
require.NoError(t, err)
37+
assert.False(t, notEmpty)
38+
})
39+
40+
t.Run("EnvToken", func(t *testing.T) {
41+
tokenValue, _ := util.CryptoRandomString(32)
42+
t.Setenv("GITEA_RUNNER_REGISTRATION_TOKEN", tokenValue)
43+
t.Setenv("GITEA_RUNNER_REGISTRATION_TOKEN_FILE", "")
44+
err := initGlobalRunnerToken(db.DefaultContext)
45+
require.NoError(t, err)
46+
token := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRunnerToken{Token: tokenValue})
47+
assert.True(t, token.IsActive)
48+
49+
// init with the same token again, should not create a new token
50+
err = initGlobalRunnerToken(db.DefaultContext)
51+
require.NoError(t, err)
52+
token2 := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRunnerToken{Token: tokenValue})
53+
assert.Equal(t, token.ID, token2.ID)
54+
assert.True(t, token.IsActive)
55+
})
56+
57+
t.Run("EnvFileToken", func(t *testing.T) {
58+
tokenValue, _ := util.CryptoRandomString(32)
59+
f := t.TempDir() + "/token"
60+
_ = os.WriteFile(f, []byte(tokenValue), 0o644)
61+
t.Setenv("GITEA_RUNNER_REGISTRATION_TOKEN", "")
62+
t.Setenv("GITEA_RUNNER_REGISTRATION_TOKEN_FILE", f)
63+
err := initGlobalRunnerToken(db.DefaultContext)
64+
require.NoError(t, err)
65+
token := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRunnerToken{Token: tokenValue})
66+
assert.True(t, token.IsActive)
67+
68+
// if the env token is invalidated by another new token, then it shouldn't be active anymore
69+
_, err = actions_model.NewRunnerToken(db.DefaultContext, 0, 0)
70+
require.NoError(t, err)
71+
token = unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRunnerToken{Token: tokenValue})
72+
assert.False(t, token.IsActive)
73+
})
74+
75+
t.Run("InvalidToken", func(t *testing.T) {
76+
t.Setenv("GITEA_RUNNER_REGISTRATION_TOKEN", "abc")
77+
err := initGlobalRunnerToken(db.DefaultContext)
78+
assert.ErrorContains(t, err, "must be at least")
79+
})
80+
}

templates/shared/actions/runner_list.tmpl

+7-3
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
<h4 class="ui top attached header">
44
{{ctx.Locale.Tr "actions.runners.runner_manage_panel"}} ({{ctx.Locale.Tr "admin.total" .Total}})
55
<div class="ui right">
6-
<div class="ui top right pointing dropdown">
6+
<div class="ui top right pointing dropdown jump">
77
<button class="ui primary tiny button">
88
{{ctx.Locale.Tr "actions.runners.new"}}
99
{{svg "octicon-triangle-down" 14 "dropdown icon"}}
@@ -17,14 +17,18 @@
1717
Registration Token
1818
</div>
1919
<div class="ui input">
20-
<input type="text" value="{{.RegistrationToken}}">
20+
<input type="text" value="{{.RegistrationToken}}" readonly>
2121
<button class="ui basic label button" aria-label="{{ctx.Locale.Tr "copy"}}" data-clipboard-text="{{.RegistrationToken}}">
2222
{{svg "octicon-copy" 14}}
2323
</button>
2424
</div>
2525
<div class="divider"></div>
2626
<div class="item">
27-
<a href="{{$.Link}}/reset_registration_token">{{ctx.Locale.Tr "actions.runners.reset_registration_token"}}</a>
27+
<a class="link-action" data-url="{{$.Link}}/reset_registration_token"
28+
data-modal-confirm="{{ctx.Locale.Tr "actions.runners.reset_registration_token_confirm"}}"
29+
>
30+
{{ctx.Locale.Tr "actions.runners.reset_registration_token"}}
31+
</a>
2832
</div>
2933
</div>
3034
</div>

0 commit comments

Comments
 (0)