mirror of
				https://github.com/go-gitea/gitea
				synced 2025-11-03 21:08:25 +00: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