mirror of
				https://github.com/go-gitea/gitea.git
				synced 2025-10-31 21:28:11 +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" | 	"code.gitea.io/gitea/models/db" | ||||||
| 	repo_model "code.gitea.io/gitea/models/repo" | 	repo_model "code.gitea.io/gitea/models/repo" | ||||||
| 	user_model "code.gitea.io/gitea/models/user" | 	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/timeutil" | ||||||
| 	"code.gitea.io/gitea/modules/util" | 	"code.gitea.io/gitea/modules/util" | ||||||
| ) | ) | ||||||
| @@ -51,7 +52,7 @@ func GetRunnerToken(ctx context.Context, token string) (*ActionRunnerToken, erro | |||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return nil, err | 		return nil, err | ||||||
| 	} else if !has { | 	} 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 | 	return &runnerToken, nil | ||||||
| } | } | ||||||
| @@ -68,19 +69,15 @@ func UpdateRunnerToken(ctx context.Context, r *ActionRunnerToken, cols ...string | |||||||
| 	return err | 	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. | // 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 { | 	if ownerID != 0 && repoID != 0 { | ||||||
| 		// It's trying to create a runner token that belongs to a repository, but OwnerID has been set accidentally. | 		// 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. | 		// Remove OwnerID to avoid confusion; it's not worth returning an error here. | ||||||
| 		ownerID = 0 | 		ownerID = 0 | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	token, err := util.CryptoRandomString(40) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return nil, err |  | ||||||
| 	} |  | ||||||
| 	runnerToken := &ActionRunnerToken{ | 	runnerToken := &ActionRunnerToken{ | ||||||
| 		OwnerID:  ownerID, | 		OwnerID:  ownerID, | ||||||
| 		RepoID:   repoID, | 		RepoID:   repoID, | ||||||
| @@ -95,11 +92,19 @@ func NewRunnerToken(ctx context.Context, ownerID, repoID int64) (*ActionRunnerTo | |||||||
| 			return err | 			return err | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		_, err = db.GetEngine(ctx).Insert(runnerToken) | 		_, err := db.GetEngine(ctx).Insert(runnerToken) | ||||||
| 		return err | 		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 | // GetLatestRunnerToken returns the latest runner token | ||||||
| func GetLatestRunnerToken(ctx context.Context, ownerID, repoID int64) (*ActionRunnerToken, error) { | func GetLatestRunnerToken(ctx context.Context, ownerID, repoID int64) (*ActionRunnerToken, error) { | ||||||
| 	if ownerID != 0 && repoID != 0 { | 	if ownerID != 0 && repoID != 0 { | ||||||
|   | |||||||
| @@ -3722,6 +3722,7 @@ runners.status.active = Active | |||||||
| runners.status.offline = Offline | runners.status.offline = Offline | ||||||
| runners.version = Version | runners.version = Version | ||||||
| runners.reset_registration_token = Reset registration token | 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 | runners.reset_registration_token_success = Runner registration token reset successfully | ||||||
|  |  | ||||||
| runs.all_workflows = All Workflows | runs.all_workflows = All Workflows | ||||||
|   | |||||||
| @@ -171,7 +171,7 @@ func InitWebInstalled(ctx context.Context) { | |||||||
| 	auth.Init() | 	auth.Init() | ||||||
| 	mustInit(svg.Init) | 	mustInit(svg.Init) | ||||||
|  |  | ||||||
| 	actions_service.Init() | 	mustInitCtx(ctx, actions_service.Init) | ||||||
|  |  | ||||||
| 	mustInit(repo_service.InitLicenseClassifier) | 	mustInit(repo_service.InitLicenseClassifier) | ||||||
|  |  | ||||||
|   | |||||||
| @@ -136,9 +136,8 @@ func RunnerResetRegistrationToken(ctx *context.Context, ownerID, repoID int64, r | |||||||
| 		ctx.ServerError("ResetRunnerRegistrationToken", err) | 		ctx.ServerError("ResetRunnerRegistrationToken", err) | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	ctx.Flash.Success(ctx.Tr("actions.runners.reset_registration_token_success")) | 	ctx.Flash.Success(ctx.Tr("actions.runners.reset_registration_token_success")) | ||||||
| 	ctx.Redirect(redirectTo) | 	ctx.JSONRedirect(redirectTo) | ||||||
| } | } | ||||||
|  |  | ||||||
| // RunnerDeletePost response for deleting a runner | // RunnerDeletePost response for deleting a runner | ||||||
|   | |||||||
| @@ -463,7 +463,7 @@ func registerRoutes(m *web.Router) { | |||||||
| 			m.Combo("/{runnerid}").Get(repo_setting.RunnersEdit). | 			m.Combo("/{runnerid}").Get(repo_setting.RunnersEdit). | ||||||
| 				Post(web.Bind(forms.EditRunnerForm{}), repo_setting.RunnersEditPost) | 				Post(web.Bind(forms.EditRunnerForm{}), repo_setting.RunnersEditPost) | ||||||
| 			m.Post("/{runnerid}/delete", repo_setting.RunnerDeletePost) | 			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 | package actions | ||||||
|  |  | ||||||
| import ( | 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/graceful" | ||||||
| 	"code.gitea.io/gitea/modules/log" | 	"code.gitea.io/gitea/modules/log" | ||||||
| 	"code.gitea.io/gitea/modules/queue" | 	"code.gitea.io/gitea/modules/queue" | ||||||
| 	"code.gitea.io/gitea/modules/setting" | 	"code.gitea.io/gitea/modules/setting" | ||||||
|  | 	"code.gitea.io/gitea/modules/util" | ||||||
| 	notify_service "code.gitea.io/gitea/services/notify" | 	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 { | 	if !setting.Actions.Enabled { | ||||||
| 		return | 		return nil | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	jobEmitterQueue = queue.CreateUniqueQueue(graceful.GetManager().ShutdownContext(), "actions_ready_job", jobEmitterQueueHandler) | 	jobEmitterQueue = queue.CreateUniqueQueue(graceful.GetManager().ShutdownContext(), "actions_ready_job", jobEmitterQueueHandler) | ||||||
| 	if jobEmitterQueue == nil { | 	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) | 	go graceful.GetManager().RunWithCancel(jobEmitterQueue) | ||||||
|  |  | ||||||
| 	notify_service.RegisterNotifier(NewNotifier()) | 	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"> | 	<h4 class="ui top attached header"> | ||||||
| 		{{ctx.Locale.Tr "actions.runners.runner_manage_panel"}} ({{ctx.Locale.Tr "admin.total" .Total}}) | 		{{ctx.Locale.Tr "actions.runners.runner_manage_panel"}} ({{ctx.Locale.Tr "admin.total" .Total}}) | ||||||
| 		<div class="ui right"> | 		<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"> | 				<button class="ui primary tiny button"> | ||||||
| 					{{ctx.Locale.Tr "actions.runners.new"}} | 					{{ctx.Locale.Tr "actions.runners.new"}} | ||||||
| 					{{svg "octicon-triangle-down" 14 "dropdown icon"}} | 					{{svg "octicon-triangle-down" 14 "dropdown icon"}} | ||||||
| @@ -17,14 +17,18 @@ | |||||||
| 						Registration Token | 						Registration Token | ||||||
| 					</div> | 					</div> | ||||||
| 					<div class="ui input"> | 					<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}}"> | 						<button class="ui basic label button" aria-label="{{ctx.Locale.Tr "copy"}}" data-clipboard-text="{{.RegistrationToken}}"> | ||||||
| 							{{svg "octicon-copy" 14}} | 							{{svg "octicon-copy" 14}} | ||||||
| 						</button> | 						</button> | ||||||
| 					</div> | 					</div> | ||||||
| 					<div class="divider"></div> | 					<div class="divider"></div> | ||||||
| 					<div class="item"> | 					<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> | 				</div> | ||||||
| 			</div> | 			</div> | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user