From f633ec97041c1c29e6ce39dc6bd5b2ee27e2e7ff Mon Sep 17 00:00:00 2001
From: fuxiaohei <fuxiaohei@vip.qq.com>
Date: Mon, 17 Oct 2022 21:21:43 +0800
Subject: [PATCH] feat(runner): create new token for creating new runner

---
 models/bots/runner_token.go      | 32 +++++++++++++++++++++------
 options/locale/locale_en-US.ini  | 26 ++++++++++++++++++++++
 routers/web/admin/runners.go     | 38 +++++++++++++++++++++++++++-----
 routers/web/web.go               |  1 +
 templates/admin/runner/edit.tmpl | 10 ++++-----
 templates/admin/runner/list.tmpl | 33 +++++++++++++++++++++++----
 web_src/less/_runner.less        |  9 ++++++++
 7 files changed, 128 insertions(+), 21 deletions(-)

diff --git a/models/bots/runner_token.go b/models/bots/runner_token.go
index a97d47cd47..097bc79bca 100644
--- a/models/bots/runner_token.go
+++ b/models/bots/runner_token.go
@@ -49,13 +49,6 @@ func init() {
 	db.RegisterModel(new(RunnerToken))
 }
 
-// NewRunnerToken creates new runner token.
-func NewRunnerToken(t *RunnerToken) error {
-	t.Token = base.EncodeSha1(gouuid.New().String())
-	_, err := db.GetEngine(db.DefaultContext).Insert(t)
-	return err
-}
-
 // GetRunnerByToken returns a bot runner via token
 func GetRunnerToken(token string) (*RunnerToken, error) {
 	var runnerToken RunnerToken
@@ -81,3 +74,28 @@ func UpdateRunnerToken(ctx context.Context, r *RunnerToken, cols ...string) (err
 	}
 	return err
 }
+
+// NewRunnerToken creates a new runner token
+func NewRunnerToken(ownerID, repoID int64) (*RunnerToken, error) {
+	runnerToken := &RunnerToken{
+		OwnerID:  ownerID,
+		RepoID:   repoID,
+		IsActive: false,
+		// FIXME: why token is 36 chars?
+		Token: base.EncodeSha1(gouuid.New().String())[:36],
+	}
+	_, err := db.GetEngine(db.DefaultContext).Insert(runnerToken)
+	return runnerToken, err
+}
+
+// GetUnactivatedRunnerToken returns a unactivated runner token
+func GetUnactivatedRunnerToken(ownerID, repoID int64) (*RunnerToken, error) {
+	var runnerToken RunnerToken
+	has, err := db.GetEngine(db.DefaultContext).Where("owner_id=? AND repo_id=? AND is_active=0", ownerID, repoID).OrderBy("id DESC").Get(&runnerToken)
+	if err != nil {
+		return nil, err
+	} else if !has {
+		return nil, ErrRunnerTokenNotExist{}
+	}
+	return &runnerToken, nil
+}
diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini
index 7cfaac0efd..5c9266050f 100644
--- a/options/locale/locale_en-US.ini
+++ b/options/locale/locale_en-US.ini
@@ -3005,6 +3005,32 @@ notices.desc = Description
 notices.op = Op.
 notices.delete_success = The system notices have been deleted.
 
+runners = Runners
+runners.runner_manage_panel = Runners Management
+runners.new = Create new Runner
+runners.new_notice = Show runner installation documentation
+runners.status = Status
+runners.id = ID
+runners.owner_type = Type
+runners.description = Description
+runners.labels = Labels
+runners.latest_online = Last Online Time
+runners.agent_labels = Agent Labels
+runners.custom_labels = Custom Labels
+runners.custom_labels_helper = Custom labels are labels that are not automatically added by the agent. They are added by the administrator. Use comma separated.
+runners.runner_title = Runner
+runners.task_list = Recent jobs on this runner
+runners.edit_runner = Edit Runner
+runners.update_runner = Update Changes
+runners.update_runner_success = Runner updated successfully
+runners.update_runner_failed = Failed to update runner
+runners.delete_runner = Delete this runner
+runners.delete_runner_success = Runner deleted successfully
+runners.delete_runner_failed = Failed to delete runner
+runners.delete_runner_header = Confirm to delete this runner
+runners.delete_runner_notice = If job is running on this runner, it will be terminated and mark as failed. It may break building workflow.
+runners.delete_runner_confirm = Delete this runner
+
 [action]
 create_repo = created repository <a href="%s">%s</a>
 rename_repo = renamed repository from <code>%[1]s</code> to <a href="%[2]s">%[3]s</a>
diff --git a/routers/web/admin/runners.go b/routers/web/admin/runners.go
index cf7bddd291..65ebf5cc23 100644
--- a/routers/web/admin/runners.go
+++ b/routers/web/admin/runners.go
@@ -63,9 +63,26 @@ func Runners(ctx *context.Context) {
 		return
 	}
 
+	// ownid=0,repo_id=0,means this token is used for global
+	var token *bots_model.RunnerToken
+	token, err = bots_model.GetUnactivatedRunnerToken(0, 0)
+	if _, ok := err.(bots_model.ErrRunnerTokenNotExist); ok {
+		token, err = bots_model.NewRunnerToken(0, 0)
+		if err != nil {
+			ctx.ServerError("CreateRunnerToken", err)
+			return
+		}
+	} else {
+		if err != nil {
+			ctx.ServerError("GetUnactivatedRunnerToken", err)
+			return
+		}
+	}
+
 	ctx.Data["Keyword"] = opts.Filter
 	ctx.Data["Runners"] = runners
 	ctx.Data["Total"] = count
+	ctx.Data["RegistrationToken"] = token.Token
 
 	pager := context.NewPagination(int(count), opts.PageSize, opts.Page, 5)
 	ctx.Data["Page"] = pager
@@ -75,7 +92,7 @@ func Runners(ctx *context.Context) {
 
 // EditRunner show editing runner page
 func EditRunner(ctx *context.Context) {
-	ctx.Data["Title"] = ctx.Tr("admin.runners.edit")
+	ctx.Data["Title"] = ctx.Tr("admin.runners.edit_runner")
 	ctx.Data["PageIsAdmin"] = true
 	ctx.Data["PageIsAdminRunners"] = true
 
@@ -107,7 +124,7 @@ func EditRunnerPost(ctx *context.Context) {
 	err = bots_model.UpdateRunner(ctx, runner, "description", "custom_labels")
 	if err != nil {
 		log.Warn("EditRunnerPost.UpdateRunner failed: %v, url: %s", err, ctx.Req.URL)
-		ctx.Flash.Warning(ctx.Tr("admin.runners.edit_failed"))
+		ctx.Flash.Warning(ctx.Tr("admin.runners.update_runner_failed"))
 		ctx.Redirect(setting.AppSubURL + "/admin/runners/" + url.PathEscape(ctx.Params(":runnerid")))
 		return
 	}
@@ -118,7 +135,7 @@ func EditRunnerPost(ctx *context.Context) {
 
 	log.Debug("EditRunnerPost success: %s", ctx.Req.URL)
 
-	ctx.Flash.Success(ctx.Tr("admin.runners.edit_success"))
+	ctx.Flash.Success(ctx.Tr("admin.runners.update_runner_success"))
 	ctx.Redirect(setting.AppSubURL + "/admin/runners/" + url.PathEscape(ctx.Params(":runnerid")))
 }
 
@@ -134,14 +151,25 @@ func DeleteRunnerPost(ctx *context.Context) {
 	err = bots_model.DeleteRunner(ctx, runner)
 	if err != nil {
 		log.Warn("DeleteRunnerPost.UpdateRunner failed: %v, url: %s", err, ctx.Req.URL)
-		ctx.Flash.Warning(ctx.Tr("admin.runners.delete_failed"))
+		ctx.Flash.Warning(ctx.Tr("admin.runners.delete_runner_failed"))
 		ctx.Redirect(setting.AppSubURL + "/admin/runners/" + url.PathEscape(ctx.Params(":runnerid")))
 		return
 	}
 
 	log.Info("DeleteRunnerPost success: %s", ctx.Req.URL)
 
-	ctx.Flash.Success(ctx.Tr("admin.runners.deletion_success"))
+	ctx.Flash.Success(ctx.Tr("admin.runners.delete_runner_success"))
+	ctx.Redirect(setting.AppSubURL + "/admin/runners/")
+}
+
+func ResetRunnerRegistrationToken(ctx *context.Context) {
+	_, err := bots_model.NewRunnerToken(0, 0)
+	if err != nil {
+		ctx.ServerError("ResetRunnerRegistrationToken", err)
+		return
+	}
+
+	ctx.Flash.Success(ctx.Tr("admin.runners.reset_registration_token_success"))
 	ctx.Redirect(setting.AppSubURL + "/admin/runners/")
 }
 
diff --git a/routers/web/web.go b/routers/web/web.go
index b8e72d14eb..2dbaf26538 100644
--- a/routers/web/web.go
+++ b/routers/web/web.go
@@ -627,6 +627,7 @@ func RegisterRoutes(m *web.Route) {
 
 		m.Group("/runners", func() {
 			m.Get("", admin.Runners)
+			m.Get("/reset_registration_token", admin.ResetRunnerRegistrationToken)
 			m.Combo("/{runnerid}").Get(admin.EditRunner).Post(bindIgnErr(forms.AdminEditRunnerForm{}), admin.EditRunnerPost)
 			m.Post("/{runnerid}/delete", admin.DeleteRunnerPost)
 		})
diff --git a/templates/admin/runner/edit.tmpl b/templates/admin/runner/edit.tmpl
index 023114185d..6a46eed8f4 100644
--- a/templates/admin/runner/edit.tmpl
+++ b/templates/admin/runner/edit.tmpl
@@ -16,7 +16,7 @@
 						<span class="runner-status-{{.Runner.StatusType}}">{{.Runner.StatusType}}</span>
 					</div>
 					<div class="field dib">
-						<label>{{.locale.Tr "admin.runners.last_online"}}</label>
+						<label>{{.locale.Tr "admin.runners.latest_online"}}</label>
 						<span>{{TimeSinceUnix .Runner.LastOnline $.locale}}</span>
 					</div>
 					<div class="field dib">
@@ -56,7 +56,7 @@
 		</div>
 
 		<h4 class="ui top attached header">
-			{{.locale.Tr "admin.runner.task_list"}}
+			{{.locale.Tr "admin.runners.task_list"}}
 		</h4>
 		<div class="ui attached segment">
 			Comming soon
@@ -66,18 +66,18 @@
 
 <div class="ui small modal" id="runner-delete-modal">
 	<div class="header">
-		{{.locale.Tr "admin.runners.delete_header"}}
+		{{.locale.Tr "admin.runners.delete_runner_header"}}
 	</div>
 	<div class="content">
 		<div class="ui warning message text left">
-			{{.locale.Tr "admin.runnners.delete_notice" | Safe}}
+			{{.locale.Tr "admin.runners.delete_runner_notice" | Safe}}
 		</div>
 		<form class="ui form" action="{{.Link}}/delete" method="post">
 			{{.CsrfTokenHtml}}
 			<input type="hidden" name="action" value="delete">
 			<div class="text right actions">
 				<div class="ui cancel button">{{.locale.Tr "settings.cancel"}}</div>
-				<button class="ui red button">{{.locale.Tr "admin.runners.delete_confirm"}}</button>
+				<button class="ui red button">{{.locale.Tr "admin.runners.delete_runner_confirm"}}</button>
 			</div>
 		</form>
 	</div>
diff --git a/templates/admin/runner/list.tmpl b/templates/admin/runner/list.tmpl
index 0752cf6506..84cf11801f 100644
--- a/templates/admin/runner/list.tmpl
+++ b/templates/admin/runner/list.tmpl
@@ -6,7 +6,32 @@
 		<h4 class="ui top attached header">
 			{{.locale.Tr "admin.runners.runner_manage_panel"}} ({{.locale.Tr "admin.total" .Total}})
 			<div class="ui right">
-				<a class="ui primary tiny button" href="{{AppSubUrl}}/admin/runners/new">{{.locale.Tr "admin.runners.new"}}</a>
+				<!--<a class="ui primary tiny button" href="{{AppSubUrl}}/admin/runners/new">{{.locale.Tr "admin.runners.new"}}</a>-->
+
+				<div class="ui top right pointing dropdown button primary" id="runner-new">
+					<span class="text runner-new-text">{{.locale.Tr "admin.runners.new"}}</span>
+					<div class="menu runner-new-menu">
+						<div class="item">
+							<a href="#">{{.locale.Tr "admin.runners.new_notice"}}</a>
+						</div>
+						<div class="divider"></div>
+						<div class="header">
+							Registration Token
+						</div>
+						<div class="ui input">
+							<input type="text" value="{{.RegistrationToken}}">
+							<div class="ui basic label button" data-clipboard-text="{{.RegistrationToken}}">
+								{{svg "octicon-copy" 14}}
+							</div>
+						</div>
+						<div class="divider"></div>
+
+						<div class="item">
+							<a href="{{$.Link}}/reset_registration_token">Reset registration token</a>
+						</div>
+					</div>
+				</div>
+
 			</div>
 		</h4>
 		<div class="ui attached segment">
@@ -26,9 +51,9 @@
 						<th data-sortt-asc="alphabetically">
 							{{.locale.Tr "admin.runners.id"}}
 						</th>
-						<th>{{.locale.Tr "admin.runners.own_type"}}</th>
-						<th>{{.locale.Tr "admin.runners.tags"}}</th>
-						<th>{{.locale.Tr "admin.runners.latest_contact"}}</th>
+						<th>{{.locale.Tr "admin.runners.owner_type"}}</th>
+						<th>{{.locale.Tr "admin.runners.labels"}}</th>
+						<th>{{.locale.Tr "admin.runners.latest_online"}}</th>
 						<th></th>
 					</tr>
 				</thead>
diff --git a/web_src/less/_runner.less b/web_src/less/_runner.less
index dacbe94f9e..f6b1a99ec3 100644
--- a/web_src/less/_runner.less
+++ b/web_src/less/_runner.less
@@ -15,4 +15,13 @@
     background-color: var(--color-green);
     color: var(--color-white);
   }
+  .runner-new-text{
+    color: var(--color-white)
+  }
+  #runner-new:hover .runner-new-text{
+    color: var(--color-white) !important;
+  }
+  .runner-new-menu{
+    width: 300px;
+  }
 }