mirror of
				https://github.com/go-gitea/gitea.git
				synced 2025-10-29 10:57:44 +09:00 
			
		
		
		
	Use env GITEA_RUNNER_REGISTRATION_TOKEN as global runner token (#32946)
Fix #23703 When Gitea starts, it reads GITEA_RUNNER_REGISTRATION_TOKEN or GITEA_RUNNER_REGISTRATION_TOKEN_FILE to add registration token.
This commit is contained in:
		| @@ -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 { | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
| @@ -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) | ||||
|  | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
| @@ -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) | ||||
| 		}) | ||||
| 	} | ||||
|  | ||||
|   | ||||
| @@ -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) | ||||
| } | ||||
|   | ||||
							
								
								
									
										80
									
								
								services/actions/init_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										80
									
								
								services/actions/init_test.go
									
									
									
									
									
										Normal 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") | ||||
| 	}) | ||||
| } | ||||
| @@ -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> | ||||
|   | ||||
		Reference in New Issue
	
	Block a user