Repository: kubesphere/ks-devops
Pull request #384 (2021-11-26 15:34:38)

Fix that the controller did not use creator's token to trigger a Jenkins job
### What dose this PR?

1. Add `devops.kubesphere.io/creator` into annotations of PipelineRun
2. Trigger PipelineRun with specific token which is issued by `devops.kubesphere.io/creator`

### Why do we need it?

At present, if we trigger a Pipeline using non-admin account, we will get message `Started by admin` always.

### Steps to test

Docker images for test:

```bash
johnniang/devops-apiserver:dev-v3.2.1-rc.1-2debd81
johnniang/devops-controller:dev-v3.2.1-rc.1-2debd81
```
1. Create a Workspace, DevOps Project, Pipeline using admin account
1. Create another account tester and invite him/her into that Workspace and DevOps Project
1. Trigger that Pipeline using tester account
1. See the `Last Message` column

![image](https://user-images.githubusercontent.com/16865714/143803721-bad38141-8d49-4c32-9328-5bc138b9ac40.png)

/kind bug
/milestone v3.2
/cc @kubesphere/sig-devops 

ks-ci-bot: @JohnNiang: You must be a member of the [kubesphere/milestone-maintainers](https://github.com/orgs/kubesphere/teams/milestone-maintainers/members) GitHub team to set the milestone. If you believe you should be able to issue the /milestone command, please contact your  and have them propose you as an additional delegate for this responsibility.

<details>

In response to [this](https://github.com/kubesphere/ks-devops/pull/384):

>### What dose this PR?
>
>1. Add `devops.kubesphere.io/creator` into annotations of PipelineRun
>2. Trigger PipelineRun with specific token which is issued by `devops.kubesphere.io/creator`
>
>### Why do we need it?
>
>At present, if we trigger a Pipeline using non-admin account, we will get message `Started by admin` always.
>
>### Steps to test
>
>Docker images for test:
>
>```bash
>johnniang/devops-controller:dev-v3.2.1-rc.1-4deb5d9
>johnniang/devops-apiserver:dev-v3.2.1-rc.1-4deb5d9
>```
>1. Create a Workspace, DevOps Project, Pipeline using admin account
>1. Create another account tester and invite him/her into that Workspace and DevOps Project
>1. Trigger that Pipeline using tester account
>2. See the `Last Message` column
>
>/kind bug
>/milestone v3.2
>/cc @kubesphere/sig-devops 


Instructions for interacting with me using PR comments are available [here](https://git.k8s.io/community/contributors/guide/pull-requests.md).  If you have questions or suggestions related to my behavior, please file an issue against the [kubernetes/test-infra](https://github.com/kubernetes/test-infra/issues/new?title=Prow%20issue:) repository.
</details>
ks-ci-bot: [APPROVALNOTIFIER] This PR is **NOT APPROVED**

This pull-request has been approved by: *<a href="https://github.com/kubesphere/ks-devops/pull/384#" title="Author self-approved">JohnNiang</a>*
To complete the [pull request process](https://git.k8s.io/community/contributors/guide/owners.md#the-code-review-process), please assign **linuxsuren** after the PR has been reviewed.
You can assign the PR to them by writing `/assign @linuxsuren` in a comment when ready.

The full list of commands accepted by this bot can be found [here](https://go.k8s.io/bot-commands?repo=kubesphere%2Fks-devops).

<details open>
Needs approval from an approver in each of these files:

- **[OWNERS](https://github.com/kubesphere/ks-devops/blob/master/OWNERS)**

Approvers can indicate their approval by writing `/approve` in a comment
Approvers can cancel approval by writing `/approve cancel` in a comment
</details>
<!-- META={"approvers":["linuxsuren"]} -->
codecov[bot]: # [Codecov](https://codecov.io/gh/kubesphere/ks-devops/pull/384?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubesphere) Report
> Merging [#384](https://codecov.io/gh/kubesphere/ks-devops/pull/384?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubesphere) (4deb5d9) into [master](https://codecov.io/gh/kubesphere/ks-devops/commit/ba24eff7b4f1f073274edb7d1a2ba14371a327dc?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubesphere) (ba24eff) will **increase** coverage by `0.03%`.
> The diff coverage is `37.50%`.

[![Impacted file tree graph](https://codecov.io/gh/kubesphere/ks-devops/pull/384/graphs/tree.svg?width=650&height=150&src=pr&token=qSK4RBbyNg&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubesphere)](https://codecov.io/gh/kubesphere/ks-devops/pull/384?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubesphere)

```diff
@@            Coverage Diff            @@
##           master    #384      +/-   ##
=========================================
+ Coverage    7.72%   7.76%   +0.03%     
=========================================
  Files          96      96              
  Lines       21582   21605      +23     
=========================================
+ Hits         1668    1677       +9     
- Misses      19791   19803      +12     
- Partials      123     125       +2     
```

| Flag | Coverage Δ | |
|---|---|---|
| unittests | `7.76% <37.50%> (+0.03%)` | :arrow_up: |

Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubesphere#carryforward-flags-in-the-pull-request-comment) to find out more.

| [Impacted Files](https://codecov.io/gh/kubesphere/ks-devops/pull/384?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubesphere) | Coverage Δ | |
|---|---|---|
| [pkg/api/devops/v1alpha3/groupversion\_info.go](https://codecov.io/gh/kubesphere/ks-devops/pull/384/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubesphere#diff-cGtnL2FwaS9kZXZvcHMvdjFhbHBoYTMvZ3JvdXB2ZXJzaW9uX2luZm8uZ28=) | `0.00% <ø> (ø)` | |
| [pkg/kapis/devops/v1alpha3/pipelinerun/handler.go](https://codecov.io/gh/kubesphere/ks-devops/pull/384/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubesphere#diff-cGtnL2thcGlzL2Rldm9wcy92MWFscGhhMy9waXBlbGluZXJ1bi9oYW5kbGVyLmdv) | `0.00% <0.00%> (ø)` | |
| [...lers/jenkins/pipelinerun/pipelinerun\_controller.go](https://codecov.io/gh/kubesphere/ks-devops/pull/384/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubesphere#diff-Y29udHJvbGxlcnMvamVua2lucy9waXBlbGluZXJ1bi9waXBlbGluZXJ1bl9jb250cm9sbGVyLmdv) | `16.47% <52.94%> (+4.13%)` | :arrow_up: |
| [controllers/s2irun/s2irun\_controller.go](https://codecov.io/gh/kubesphere/ks-devops/pull/384/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubesphere#diff-Y29udHJvbGxlcnMvczJpcnVuL3MyaXJ1bl9jb250cm9sbGVyLmdv) | `33.57% <0.00%> (-3.65%)` | :arrow_down: |
| [...trollers/devopsproject/devopsproject\_controller.go](https://codecov.io/gh/kubesphere/ks-devops/pull/384/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubesphere#diff-Y29udHJvbGxlcnMvZGV2b3BzcHJvamVjdC9kZXZvcHNwcm9qZWN0X2NvbnRyb2xsZXIuZ28=) | `40.62% <0.00%> (-2.61%)` | :arrow_down: |
| [...rs/devopscredential/devopscredential\_controller.go](https://codecov.io/gh/kubesphere/ks-devops/pull/384/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubesphere#diff-Y29udHJvbGxlcnMvZGV2b3BzY3JlZGVudGlhbC9kZXZvcHNjcmVkZW50aWFsX2NvbnRyb2xsZXIuZ28=) | `36.48% <0.00%> (+3.37%)` | :arrow_up: |
| [controllers/pipeline/pipeline\_controller.go](https://codecov.io/gh/kubesphere/ks-devops/pull/384/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubesphere#diff-Y29udHJvbGxlcnMvcGlwZWxpbmUvcGlwZWxpbmVfY29udHJvbGxlci5nbw==) | `41.91% <0.00%> (+3.67%)` | :arrow_up: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/kubesphere/ks-devops/pull/384?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubesphere).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubesphere)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/kubesphere/ks-devops/pull/384?src=pr&el=footer&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubesphere). Last update [ba24eff...4deb5d9](https://codecov.io/gh/kubesphere/ks-devops/pull/384?src=pr&el=lastupdated&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubesphere). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubesphere).

LinuxSuRen: /cherrypick release-3.2
ks-ci-bot: @LinuxSuRen: once the present PR merges, I will cherry-pick it on top of release-3.2 in a new PR and assign it to you.

<details>

In response to [this](https://github.com/kubesphere/ks-devops/pull/384#issuecomment-980144887):

>/cherrypick release-3.2


Instructions for interacting with me using PR comments are available [here](https://git.k8s.io/community/contributors/guide/pull-requests.md).  If you have questions or suggestions related to my behavior, please file an issue against the [kubernetes/test-infra](https://github.com/kubernetes/test-infra/issues/new?title=Prow%20issue:) repository.
</details>
JohnNiang: /label tide/merge-method-squash
JohnNiang: /retest Test
ks-ci-bot: @JohnNiang: No presubmit jobs available for kubesphere/ks-devops@master

<details>

In response to [this](https://github.com/kubesphere/ks-devops/pull/384#issuecomment-981259103):

>/retest Test


Instructions for interacting with me using PR comments are available [here](https://git.k8s.io/community/contributors/guide/pull-requests.md).  If you have questions or suggestions related to my behavior, please file an issue against the [kubernetes/test-infra](https://github.com/kubernetes/test-infra/issues/new?title=Prow%20issue:) repository.
</details>
JohnNiang: /test ?
ks-ci-bot: @JohnNiang: No presubmit jobs available for kubesphere/ks-devops@master

<details>

In response to [this](https://github.com/kubesphere/ks-devops/pull/384#issuecomment-981260086):

>/test ?


Instructions for interacting with me using PR comments are available [here](https://git.k8s.io/community/contributors/guide/pull-requests.md).  If you have questions or suggestions related to my behavior, please file an issue against the [kubernetes/test-infra](https://github.com/kubernetes/test-infra/issues/new?title=Prow%20issue:) repository.
</details>
JohnNiang: I've run `make test` in my local environment many times, and it always passes.

![image](https://user-images.githubusercontent.com/16865714/143804958-07b45218-2052-4e5e-96c6-3c5b94438add.png)

I'm going to re-run the workflow by closing and reopening this PR.

JohnNiang: /ping @kubesphere/sig-devops 
LinuxSuRen: hi @JohnNiang , The real problem is that the controller didn't use the correct user to trigger a Jenkins job.

`showing wrong message "started by admin" while triggering by non-admin user` describes a UI issue.

So, I suggest you provide a more accurate title.
JohnNiang: > hi @JohnNiang , The real problem is that the controller didn't use the correct user to trigger a Jenkins job.
> 
> `showing wrong message "started by admin" while triggering by non-admin user` describes a UI issue.
> 
> So, I suggest you provide a more accurate title.

Thanks for your suggestion!

/retitle Enable the controller to trigger a Jenkins job with the token of the creator
ks-ci-bot: [APPROVALNOTIFIER] This PR is **APPROVED**

This pull-request has been approved by: *<a href="https://github.com/kubesphere/ks-devops/pull/384#" title="Author self-approved">JohnNiang</a>*, *<a href="https://github.com/kubesphere/ks-devops/pull/384#pullrequestreview-817787779" title="Approved">LinuxSuRen</a>*

The full list of commands accepted by this bot can be found [here](https://go.k8s.io/bot-commands?repo=kubesphere%2Fks-devops).

The pull request process is described [here](https://git.k8s.io/community/contributors/guide/owners.md#the-code-review-process)

<details >
Needs approval from an approver in each of these files:

- ~~[OWNERS](https://github.com/kubesphere/ks-devops/blob/master/OWNERS)~~ [LinuxSuRen]

Approvers can indicate their approval by writing `/approve` in a comment
Approvers can cancel approval by writing `/approve cancel` in a comment
</details>
<!-- META={"approvers":[]} -->

Patch:
diff --git a/.golangci.yml b/.golangci.yml
index 46401d3..0a4166a 100644
--- a/.golangci.yml
+++ b/.golangci.yml
@@ -36,8 +36,8 @@ run:
     - pkg/api/types.go
     - pkg/apiserver/apiserver.go
     - pkg/apiserver/authentication/oauth/oauth_options.go
-    - pkg/apiserver/authentication/token/issuer.go
-    - pkg/apiserver/authentication/token/jwt.go
+    - pkg/jwt/token/issuer.go
+    - pkg/jwt/token/jwt.go
     - pkg/apiserver/filters/requestinfo.go
     - pkg/apiserver/query/types.go
     - pkg/apiserver/request/context.go
diff --git a/cmd/controller/app/controllers.go b/cmd/controller/app/controllers.go
index 55dbbb5..08b7618 100644
--- a/cmd/controller/app/controllers.go
+++ b/cmd/controller/app/controllers.go
@@ -17,6 +17,7 @@ limitations under the License.
 package app
 
 import (
+	"kubesphere.io/devops/pkg/jwt/token"
 	"kubesphere.io/devops/pkg/server/errors"
 
 	"github.com/jenkins-zh/jenkins-client/pkg/core"
@@ -44,12 +45,14 @@ func addControllers(mgr manager.Manager, client k8s.Client, informerFactory info
 		return errors.New("devopsClient should not be nil")
 	}
 
+	tokenIssuer := token.NewTokenIssuer(s.JWTOptions.Secret, s.JWTOptions.MaximumClockSkew)
 	// add PipelineRun controller
 	if err := (&pipelinerun.Reconciler{
 		Client:       mgr.GetClient(),
 		Scheme:       mgr.GetScheme(),
 		DevOpsClient: devopsClient,
 		JenkinsCore:  jenkinsCore,
+		TokenIssuer:  tokenIssuer,
 	}).SetupWithManager(mgr); err != nil {
 		klog.Errorf("unable to create pipelinerun-controller, err: %v", err)
 		return err
diff --git a/cmd/controller/app/options/jwt.go b/cmd/controller/app/options/jwt.go
new file mode 100644
index 0000000..4c17d66
--- /dev/null
+++ b/cmd/controller/app/options/jwt.go
@@ -0,0 +1,11 @@
+package options
+
+import "time"
+
+// JWTOptions contain some options of JWT, such as secret and clock skew.
+type JWTOptions struct {
+	// MaximumClockSkew indicates token verification maximum time difference.
+	MaximumClockSkew time.Duration
+	// Secret is used to sign JWT token.
+	Secret string
+}
diff --git a/cmd/controller/app/options/options.go b/cmd/controller/app/options/options.go
index b1efcb7..861bc1a 100644
--- a/cmd/controller/app/options/options.go
+++ b/cmd/controller/app/options/options.go
@@ -41,6 +41,7 @@ type DevOpsControllerManagerOptions struct {
 	WebhookCertDir    string
 	S3Options         *s3.Options
 	FeatureOptions    *FeatureOptions
+	JWTOptions        *JWTOptions
 
 	// KubeSphere is using sigs.k8s.io/application as fundamental object to implement Application Management.
 	// There are other projects also built on sigs.k8s.io/application, when KubeSphere installed along side
diff --git a/cmd/controller/app/server.go b/cmd/controller/app/server.go
index f66c07f..ec48df3 100644
--- a/cmd/controller/app/server.go
+++ b/cmd/controller/app/server.go
@@ -18,6 +18,7 @@ package app
 
 import (
 	"fmt"
+
 	"github.com/jenkins-zh/jenkins-client/pkg/core"
 	utilerrors "k8s.io/apimachinery/pkg/util/errors"
 	"kubesphere.io/devops/cmd/controller/app/options"
@@ -51,10 +52,14 @@ func NewControllerManagerCommand() *cobra.Command {
 			KubernetesOptions: conf.KubernetesOptions,
 			JenkinsOptions:    conf.JenkinsOptions,
 			S3Options:         conf.S3Options,
-			FeatureOptions:    s.FeatureOptions,
-			LeaderElection:    s.LeaderElection,
-			LeaderElect:       s.LeaderElect,
-			WebhookCertDir:    s.WebhookCertDir,
+			JWTOptions: &options.JWTOptions{
+				Secret:           conf.AuthenticationOptions.JwtSecret,
+				MaximumClockSkew: conf.AuthenticationOptions.MaximumClockSkew,
+			},
+			FeatureOptions: s.FeatureOptions,
+			LeaderElection: s.LeaderElection,
+			LeaderElect:    s.LeaderElect,
+			WebhookCertDir: s.WebhookCertDir,
 		}
 	} else {
 		klog.Fatal("Failed to load configuration from disk", err)
diff --git a/cmd/tools/jwt/app/jwt_root.go b/cmd/tools/jwt/app/jwt_root.go
index 9dfe799..3bc5b23 100644
--- a/cmd/tools/jwt/app/jwt_root.go
+++ b/cmd/tools/jwt/app/jwt_root.go
@@ -3,15 +3,16 @@ package app
 import (
 	"context"
 	"fmt"
+	"math/rand"
+	"strings"
+	"time"
+
 	"github.com/spf13/cobra"
 	"gopkg.in/yaml.v2"
 	"k8s.io/apiserver/pkg/authentication/user"
 	"k8s.io/client-go/kubernetes"
-	"kubesphere.io/devops/pkg/apiserver/authentication/token"
 	"kubesphere.io/devops/pkg/config"
-	"math/rand"
-	"strings"
-	"time"
+	"kubesphere.io/devops/pkg/jwt/token"
 )
 
 // NewCmd creates a root command for jwt
diff --git a/controllers/jenkins/pipelinerun/pipelinerun_controller.go b/controllers/jenkins/pipelinerun/pipelinerun_controller.go
index 1d69581..cabf7d6 100644
--- a/controllers/jenkins/pipelinerun/pipelinerun_controller.go
+++ b/controllers/jenkins/pipelinerun/pipelinerun_controller.go
@@ -31,16 +31,21 @@ import (
 	v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/runtime"
 	"k8s.io/apimachinery/pkg/util/validation"
+	"k8s.io/apiserver/pkg/authentication/user"
 	"k8s.io/client-go/tools/record"
 	"k8s.io/client-go/util/retry"
 	"k8s.io/klog"
 	"kubesphere.io/devops/pkg/api/devops/v1alpha3"
 	devopsClient "kubesphere.io/devops/pkg/client/devops"
+	"kubesphere.io/devops/pkg/jwt/token"
 	"kubesphere.io/devops/pkg/utils/sliceutil"
 	ctrl "sigs.k8s.io/controller-runtime"
 	"sigs.k8s.io/controller-runtime/pkg/client"
 )
 
+// tokenExpireIn indicates that the temporary token issued by controller will be expired in some time.
+const tokenExpireIn time.Duration = 5 * time.Minute
+
 // Reconciler reconciles a PipelineRun object
 type Reconciler struct {
 	client.Client
@@ -48,6 +53,7 @@ type Reconciler struct {
 	Scheme       *runtime.Scheme
 	DevOpsClient devopsClient.Interface
 	JenkinsCore  core.JenkinsCore
+	TokenIssuer  token.Issuer
 	recorder     record.EventRecorder
 }
 
@@ -172,11 +178,19 @@ func (r *Reconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) {
 		return ctrl.Result{RequeueAfter: 3 * time.Second}, nil
 	}
 
+	// get or create JenkinsCore if the PipelineRun has creator annotation
+	jenkinsCore, err := r.getOrCreateJenkinsCore(pipelineRunCopied.GetAnnotations())
+	if err != nil {
+		r.recorder.Eventf(pipelineRunCopied, corev1.EventTypeWarning, v1alpha3.TriggerFailed, "Failed to trigger PipelineRun %s, and error was %v", req.NamespacedName, err)
+		return ctrl.Result{}, err
+	}
+	// create trigger handler
+	triggerHandler := &jenkinsHandler{jenkinsCore}
 	// first run
-	jobRun, err := jHandler.triggerJenkinsJob(namespaceName, pipelineName, &pipelineRunCopied.Spec)
+	jobRun, err := triggerHandler.triggerJenkinsJob(namespaceName, pipelineName, &pipelineRunCopied.Spec)
 	if err != nil {
 		log.Error(err, "unable to run pipeline", "namespace", namespaceName, "pipeline", pipeline.Name)
-		r.recorder.Eventf(pipelineRunCopied, corev1.EventTypeWarning, v1alpha3.TriggerFailed, "Failed to trigger PipelineRun %s, and error was %s", req.NamespacedName, err)
+		r.recorder.Eventf(pipelineRunCopied, corev1.EventTypeWarning, v1alpha3.TriggerFailed, "Failed to trigger PipelineRun %s, and error was %v", req.NamespacedName, err)
 		return ctrl.Result{}, err
 	}
 	// check if there is still a same PipelineRun
@@ -309,6 +323,24 @@ func (r *Reconciler) makePipelineRunOrphan(ctx context.Context, pr *v1alpha3.Pip
 	return r.updateStatus(ctx, &pr.Status, client.ObjectKey{Namespace: pr.Namespace, Name: pr.Name})
 }
 
+func (r *Reconciler) getOrCreateJenkinsCore(annotations map[string]string) (*core.JenkinsCore, error) {
+	creator, ok := annotations[v1alpha3.PipelineRunCreatorAnnoKey]
+	if !ok || creator == "" {
+		return &r.JenkinsCore, nil
+	}
+	// create a new JenkinsCore for current creator
+	accessToken, err := r.TokenIssuer.IssueTo(&user.DefaultInfo{Name: creator}, token.AccessToken, tokenExpireIn)
+	if err != nil {
+		return nil, fmt.Errorf("failed to issue access token for creator %s, error was %v", creator, err)
+	}
+	jenkinsCore := &core.JenkinsCore{
+		URL:      r.JenkinsCore.URL,
+		UserName: creator,
+		Token:    accessToken,
+	}
+	return jenkinsCore, nil
+}
+
 // SetupWithManager sets up the controller with the Manager.
 func (r *Reconciler) SetupWithManager(mgr ctrl.Manager) error {
 	// the name should obey Kubernetes naming convention: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/
diff --git a/pkg/api/devops/v1alpha3/groupversion_info.go b/pkg/api/devops/v1alpha3/groupversion_info.go
index 943c191..4336187 100644
--- a/pkg/api/devops/v1alpha3/groupversion_info.go
+++ b/pkg/api/devops/v1alpha3/groupversion_info.go
@@ -37,6 +37,8 @@ const (
 	PipelineNameLabelKey = GroupName + "/pipeline"
 	// SCMRefNameLabelKey is label key of SCM reference name.
 	SCMRefNameLabelKey = GroupName + "/scm-ref-name"
+	// PipelineRunCreatorAnnoKey is annotation key of PipelineRun's creator
+	PipelineRunCreatorAnnoKey = GroupName + "/creator"
 )
 
 var (
diff --git a/pkg/apiserver/authentication/authenticators/bearertoken/bearertoken.go b/pkg/apiserver/authentication/authenticators/bearertoken/bearertoken.go
index 03430d5..b68ea0c 100644
--- a/pkg/apiserver/authentication/authenticators/bearertoken/bearertoken.go
+++ b/pkg/apiserver/authentication/authenticators/bearertoken/bearertoken.go
@@ -2,11 +2,12 @@ package bearertoken
 
 import (
 	"context"
-	"k8s.io/apiserver/pkg/authentication/user"
 	"time"
 
+	"k8s.io/apiserver/pkg/authentication/user"
+
 	"k8s.io/apiserver/pkg/authentication/authenticator"
-	jwt "kubesphere.io/devops/pkg/apiserver/authentication/token"
+	jwt "kubesphere.io/devops/pkg/jwt/token"
 )
 
 // tokenAuthenticator implements an simple auth which only check the format of target JWT token
diff --git a/pkg/apiserver/authentication/token/issuer.go b/pkg/jwt/token/issuer.go
similarity index 100%
rename from pkg/apiserver/authentication/token/issuer.go
rename to pkg/jwt/token/issuer.go
diff --git a/pkg/apiserver/authentication/token/jwt.go b/pkg/jwt/token/jwt.go
similarity index 99%
rename from pkg/apiserver/authentication/token/jwt.go
rename to pkg/jwt/token/jwt.go
index f62b1b1..b4aaeef 100644
--- a/pkg/apiserver/authentication/token/jwt.go
+++ b/pkg/jwt/token/jwt.go
@@ -18,9 +18,10 @@ package token
 
 import (
 	"fmt"
-	"kubesphere.io/devops/pkg/server/errors"
 	"time"
 
+	"kubesphere.io/devops/pkg/server/errors"
+
 	"github.com/form3tech-oss/jwt-go"
 	"k8s.io/apiserver/pkg/authentication/user"
 	"k8s.io/klog"
diff --git a/pkg/apiserver/authentication/token/jwt_test.go b/pkg/jwt/token/jwt_test.go
similarity index 100%
rename from pkg/apiserver/authentication/token/jwt_test.go
rename to pkg/jwt/token/jwt_test.go
diff --git a/pkg/kapis/devops/v1alpha3/pipelinerun/handler.go b/pkg/kapis/devops/v1alpha3/pipelinerun/handler.go
index 46ac021..cc47d90 100644
--- a/pkg/kapis/devops/v1alpha3/pipelinerun/handler.go
+++ b/pkg/kapis/devops/v1alpha3/pipelinerun/handler.go
@@ -3,6 +3,7 @@ package pipelinerun
 import (
 	"context"
 	"encoding/json"
+	"fmt"
 	"io"
 	"strconv"
 
@@ -12,6 +13,7 @@ import (
 	"kubesphere.io/devops/pkg/api"
 	"kubesphere.io/devops/pkg/api/devops/v1alpha3"
 	"kubesphere.io/devops/pkg/apiserver/query"
+	apiserverrequest "kubesphere.io/devops/pkg/apiserver/request"
 	"kubesphere.io/devops/pkg/client/devops"
 	"kubesphere.io/devops/pkg/models/pipelinerun"
 	resourcesV1alpha3 "kubesphere.io/devops/pkg/models/resources/v1alpha3"
@@ -130,8 +132,19 @@ func (h *apiHandler) createPipelineRun(request *restful.Request, response *restf
 		return
 	}
 
+	// get current login user from request context
+	user, ok := apiserverrequest.UserFrom(request.Request.Context())
+	if !ok || user == nil {
+		// should never happen
+		err := fmt.Errorf("unauthenticated user entered to create PipelineRun for Pipeline '%s/%s'", nsName, pipName)
+		api.HandleUnauthorized(response, request, err)
+		return
+	}
 	// create PipelineRun
 	pr := CreatePipelineRun(&pipeline, &payload, scm)
+	if user != nil && user.GetName() != "" {
+		pr.GetAnnotations()[v1alpha3.PipelineRunCreatorAnnoKey] = user.GetName()
+	}
 	if err := h.client.Create(context.Background(), pr); err != nil {
 		api.HandleError(request, response, err)
 		return
diff --git a/pkg/models/auth/token.go b/pkg/models/auth/token.go
index cd8806f..a9bfd24 100644
--- a/pkg/models/auth/token.go
+++ b/pkg/models/auth/token.go
@@ -28,8 +28,8 @@ import (
 
 	"kubesphere.io/devops/pkg/apiserver/authentication/oauth"
 	authoptions "kubesphere.io/devops/pkg/apiserver/authentication/options"
-	"kubesphere.io/devops/pkg/apiserver/authentication/token"
 	"kubesphere.io/devops/pkg/client/cache"
+	"kubesphere.io/devops/pkg/jwt/token"
 )
 
 type TokenManagementInterface interface {


Test patch:
diff --git a/controllers/jenkins/pipelinerun/pipelinerun_controller_test.go b/controllers/jenkins/pipelinerun/pipelinerun_controller_test.go
index f7ad8cd..633fe07 100644
--- a/controllers/jenkins/pipelinerun/pipelinerun_controller_test.go
+++ b/controllers/jenkins/pipelinerun/pipelinerun_controller_test.go
@@ -1,14 +1,19 @@
 package pipelinerun
 
 import (
+	"reflect"
 	"testing"
 
+	"github.com/jenkins-zh/jenkins-client/pkg/core"
 	"github.com/jenkins-zh/jenkins-client/pkg/job"
 	. "github.com/onsi/ginkgo"
 	. "github.com/onsi/gomega"
+	"github.com/stretchr/testify/assert"
 	v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apiserver/pkg/authentication/user"
 	"kubesphere.io/devops/pkg/api/devops/v1alpha3"
 	"kubesphere.io/devops/pkg/client/clientset/versioned/scheme"
+	"kubesphere.io/devops/pkg/jwt/token"
 	"sigs.k8s.io/controller-runtime/pkg/client"
 
 	// nolint
@@ -257,3 +262,84 @@ var _ = Describe("TestReconciler_hasSamePipelineRun", func() {
 		})
 	})
 })
+
+func TestReconciler_getOrCreateJenkinsCoreIfHasCreator(t *testing.T) {
+	defaultJenkinsCore := &core.JenkinsCore{
+		URL:      "https://devops.com",
+		UserName: "admin",
+		Token:    "fake-token",
+	}
+	tokenIssuer := token.NewTokenIssuer("test-secret", 0)
+	accessToken, err := tokenIssuer.IssueTo(&user.DefaultInfo{Name: "tester"}, token.AccessToken, tokenExpireIn)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	type fields struct {
+		JenkinsCore core.JenkinsCore
+		TokenIssuer token.Issuer
+	}
+	type args struct {
+		annotations map[string]string
+	}
+	tests := []struct {
+		name      string
+		fields    fields
+		args      args
+		want      *core.JenkinsCore
+		assertion func(*core.JenkinsCore)
+		wantErr   bool
+	}{{
+		name: "Empty annotations",
+		fields: fields{
+			JenkinsCore: *defaultJenkinsCore,
+			TokenIssuer: tokenIssuer,
+		},
+		want: defaultJenkinsCore,
+	}, {
+		name: "Has creator in annotations",
+		fields: fields{
+			JenkinsCore: *defaultJenkinsCore,
+			TokenIssuer: tokenIssuer,
+		},
+		args: args{
+			annotations: map[string]string{
+				v1alpha3.PipelineRunCreatorAnnoKey: "tester",
+			},
+		},
+		want: &core.JenkinsCore{
+			URL:      defaultJenkinsCore.URL,
+			UserName: "tester",
+			Token:    accessToken,
+		},
+		assertion: func(jenkinsCore *core.JenkinsCore) {
+			assert.Equal(t, "tester", jenkinsCore.UserName)
+			assert.Equal(t, defaultJenkinsCore.URL, jenkinsCore.URL)
+			userInfo, tokenType, err := tokenIssuer.Verify(jenkinsCore.Token)
+			assert.Equal(t, &user.DefaultInfo{Name: "tester"}, userInfo)
+			assert.Equal(t, token.AccessToken, tokenType)
+			assert.Nil(t, err)
+		},
+	},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			r := &Reconciler{
+				JenkinsCore: tt.fields.JenkinsCore,
+				TokenIssuer: tt.fields.TokenIssuer,
+			}
+			got, err := r.getOrCreateJenkinsCore(tt.args.annotations)
+			if (err != nil) != tt.wantErr {
+				t.Errorf("Reconciler.getOrCreateJenkinsCoreIfHasCreator() error = %v, wantErr %v", err, tt.wantErr)
+				return
+			}
+			if tt.assertion != nil {
+				tt.assertion(got)
+				return
+			}
+			if !reflect.DeepEqual(got, tt.want) {
+				t.Errorf("Reconciler.getOrCreateJenkinsCoreIfHasCreator() = %v, want %v", got, tt.want)
+			}
+		})
+	}
+}