Repository: kurator-dev/kurator
Pull request #511 (2023-12-11 11:20:33)

pipeline: update render rbac and remove manifest file
**What type of PR is this?**

/kind cleanup


**What this PR does / why we need it**:

In certain scenarios, the "html/template" may not function properly as it automatically converts punctuation marks

```
            	            	-        print('This is a complete test')
            	            	+        print(&#39;This is a complete test&#39;)
```




Patch:
diff --git a/.github/ISSUE_TEMPLATE/membership-request.md b/.github/ISSUE_TEMPLATE/membership-request.md
new file mode 100644
index 0000000..0b7a6af
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/membership-request.md
@@ -0,0 +1,43 @@
+---
+name: Membership Request
+about: Request to become a Kurator community member
+title: 'REQUEST: New membership for <your name>'
+assignees: ''
+
+---
+
+## GitHub Username
+<!-- your github handle here -->
+
+## Organization you are requesting membership in
+<!-- The organization name here -->
+
+## Requirements
+
+- [ ] I have reviewed the [community membership guidelines]
+- [ ] I have joined in the community [mailing group] and [slack]
+- [ ] I have watched kurator-dev/kurator on GitHub to subscribe updates (My account appears in the [watchers list])
+- [ ] I have enabled [two-factor authentication] on my GitHub account
+- [ ] I am actively contributing to 1 or more Kurator subprojects
+- [ ] I have spoken to my sponsors ahead of this application, and they have agreed to sponsor my application
+
+## Sponsors
+<!--
+  List github handle of your sponsors.
+  Two sponsors is enough, but better from different companies/organizations.
+-->
+
+## List of contributions to the Kurator project
+- PRs reviewed / authored:
+
+- Issues authored:
+
+- Issues responded to:
+
+
+<!-- Please don't edit the following lines -->
+[community membership guidelines]: https://github.com/kurator-dev/kurator/community/membership/community-membership.md
+[mailing group]: https://groups.google.com/g/kuator-dev
+[slack]: https://join.slack.com/t/kurator-hq/shared_invite/zt-1sowqzfnl-Vu1AhxgAjSr1XnaFoogq0A
+[watchers list]: https://github.com/kurator-dev/kurator/watchers
+[two-factor authentication]: https://help.github.com/en/github/authenticating-to-github/about-two-factor-authentication
diff --git a/cmd/kurator/app/app.go b/cmd/kurator/app/app.go
index a2ff9c3..0063c67 100644
--- a/cmd/kurator/app/app.go
+++ b/cmd/kurator/app/app.go
@@ -24,6 +24,7 @@ import (
 
 	"kurator.dev/kurator/cmd/kurator/app/install"
 	"kurator.dev/kurator/cmd/kurator/app/join"
+	"kurator.dev/kurator/cmd/kurator/app/pipeline"
 	"kurator.dev/kurator/cmd/kurator/app/tool"
 	"kurator.dev/kurator/cmd/kurator/app/version"
 	"kurator.dev/kurator/pkg/generic"
@@ -53,6 +54,7 @@ func NewKuratorCommand() *cobra.Command {
 	cmd.AddCommand(install.NewCmd(o))
 	cmd.AddCommand(join.NewCmd(o))
 	cmd.AddCommand(tool.NewCmd(o))
+	cmd.AddCommand(pipeline.NewCmd(o))
 
 	return cmd
 }
diff --git a/cmd/kurator/app/pipeline/execution/execution.go b/cmd/kurator/app/pipeline/execution/execution.go
new file mode 100644
index 0000000..5c23507
--- /dev/null
+++ b/cmd/kurator/app/pipeline/execution/execution.go
@@ -0,0 +1,39 @@
+/*
+Copyright Kurator Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+	http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package execution
+
+import (
+	"github.com/spf13/cobra"
+
+	"kurator.dev/kurator/cmd/kurator/app/pipeline/execution/list"
+	"kurator.dev/kurator/pkg/generic"
+)
+
+func NewCmd(opts *generic.Options) *cobra.Command {
+	executionCmd := &cobra.Command{
+		Use:                   "execution",
+		Short:                 "manage kurator pipeline execution",
+		DisableFlagsInUseLine: true,
+		FParseErrWhitelist: cobra.FParseErrWhitelist{
+			UnknownFlags: true,
+		},
+	}
+
+	executionCmd.AddCommand(list.NewCmd(opts))
+
+	return executionCmd
+}
diff --git a/cmd/kurator/app/pipeline/execution/list/list.go b/cmd/kurator/app/pipeline/execution/list/list.go
new file mode 100644
index 0000000..48b7fa8
--- /dev/null
+++ b/cmd/kurator/app/pipeline/execution/list/list.go
@@ -0,0 +1,68 @@
+/*
+Copyright Kurator Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+	http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package list
+
+import (
+	"fmt"
+
+	"github.com/sirupsen/logrus"
+	"github.com/spf13/cobra"
+
+	"kurator.dev/kurator/pkg/generic"
+	"kurator.dev/kurator/pkg/pipeline/execution/list"
+)
+
+func NewCmd(opts *generic.Options) *cobra.Command {
+	var Args = list.Args{}
+	listCmd := &cobra.Command{
+		Use:     "list",
+		Short:   "list the kurator pipeline execution",
+		Example: getExample(),
+		RunE: func(cmd *cobra.Command, args []string) error {
+			PipelineList, err := list.NewPipelineList(opts, &Args)
+			if err != nil {
+				logrus.Errorf("pipeline init error: %v", err)
+				return fmt.Errorf("pipeline init error: %v", err)
+			}
+
+			logrus.Debugf("start list pipeline obj, Global: %+v ", opts)
+			if err := PipelineList.ListExecute(); err != nil {
+				logrus.Errorf("pipeline execute error: %v", err)
+				return fmt.Errorf("pipeline execute error: %v", err)
+			}
+
+			return nil
+		},
+	}
+
+	listCmd.PersistentFlags().StringVarP(&Args.Namespace, "namespace", "n", "default", "specific namespace")
+	listCmd.PersistentFlags().BoolVarP(&Args.AllNamespaces, "all-namespaces", "A", false, "If true, list the pipelineRuns across all namespaces")
+
+	return listCmd
+}
+
+func getExample() string {
+	return `  # List kurator pipeline objects in the default namespace
+  kurator pipeline execution list
+
+  # List the pipelines in a specific namespace (replace 'example-namespace' with your namespace)
+  kurator pipeline execution list -n example-namespace
+
+  # List the pipelines across all namespaces
+  kurator pipeline execution list -A
+`
+}
diff --git a/cmd/kurator/app/pipeline/pipeline.go b/cmd/kurator/app/pipeline/pipeline.go
new file mode 100644
index 0000000..3255d0f
--- /dev/null
+++ b/cmd/kurator/app/pipeline/pipeline.go
@@ -0,0 +1,39 @@
+/*
+Copyright Kurator Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+	http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package pipeline
+
+import (
+	"github.com/spf13/cobra"
+
+	"kurator.dev/kurator/cmd/kurator/app/pipeline/execution"
+	"kurator.dev/kurator/pkg/generic"
+)
+
+func NewCmd(opts *generic.Options) *cobra.Command {
+	pipelineCmd := &cobra.Command{
+		Use:                   "pipeline",
+		Short:                 "manage kurator pipeline",
+		DisableFlagsInUseLine: true,
+		FParseErrWhitelist: cobra.FParseErrWhitelist{
+			UnknownFlags: true,
+		},
+	}
+
+	pipelineCmd.AddCommand(execution.NewCmd(opts))
+
+	return pipelineCmd
+}
diff --git a/docs/content/en/references/pipeline_v1alpha1_types.html b/docs/content/en/references/pipeline_v1alpha1_types.html
index 783fcd4..58054e8 100644
--- a/docs/content/en/references/pipeline_v1alpha1_types.html
+++ b/docs/content/en/references/pipeline_v1alpha1_types.html
@@ -95,12 +95,16 @@ The tasks will be executed in the order they are listed.</p>
 <td>
 <code>sharedWorkspace</code><br>
 <em>
-string
+<a href="#pipeline.kurator.dev/v1alpha1.VolumeClaimTemplate">
+VolumeClaimTemplate
+</a>
 </em>
 </td>
 <td>
 <em>(Optional)</em>
-<p>SharedWorkspace is the name of the PVC. If not specified, a PVC with the Pipeline&rsquo;s name as prefix will be created by default.
+<p>SharedWorkspace is the config of the PVC where task using
+The PersistentVolumeClaim with this config will be created for each pipeline execution
+it allows the user to specify e.g. size and StorageClass for the volume.
 If not set, Kurator will create a PVC named Pipeline.name using default config</p>
 </td>
 </tr>
@@ -284,12 +288,16 @@ The tasks will be executed in the order they are listed.</p>
 <td>
 <code>sharedWorkspace</code><br>
 <em>
-string
+<a href="#pipeline.kurator.dev/v1alpha1.VolumeClaimTemplate">
+VolumeClaimTemplate
+</a>
 </em>
 </td>
 <td>
 <em>(Optional)</em>
-<p>SharedWorkspace is the name of the PVC. If not specified, a PVC with the Pipeline&rsquo;s name as prefix will be created by default.
+<p>SharedWorkspace is the config of the PVC where task using
+The PersistentVolumeClaim with this config will be created for each pipeline execution
+it allows the user to specify e.g. size and StorageClass for the volume.
 If not set, Kurator will create a PVC named Pipeline.name using default config</p>
 </td>
 </tr>
@@ -472,6 +480,86 @@ The required parameters vary depending on the TaskType chosen.</p>
 (<em>Appears on:</em>
 <a href="#pipeline.kurator.dev/v1alpha1.PredefinedTask">PredefinedTask</a>)
 </p>
+<h3 id="pipeline.kurator.dev/v1alpha1.VolumeClaimTemplate">VolumeClaimTemplate
+</h3>
+<p>
+(<em>Appears on:</em>
+<a href="#pipeline.kurator.dev/v1alpha1.PipelineSpec">PipelineSpec</a>)
+</p>
+<p>VolumeClaimTemplate is the configuration for the volume claim template in pipeline execution.
+For more details, see <a href="https://github.com/kubernetes/api/blob/master/core/v1/types.go">https://github.com/kubernetes/api/blob/master/core/v1/types.go</a></p>
+<div class="md-typeset__scrollwrap">
+<div class="md-typeset__table td-content">
+<table>
+<thead>
+<tr>
+<th>Field</th>
+<th>Description</th>
+</tr>
+</thead>
+<tbody>
+<tr>
+<td>
+<code>accessMode</code><br>
+<em>
+<a href="https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.25/#persistentvolumeaccessmode-v1-core">
+Kubernetes core/v1.PersistentVolumeAccessMode
+</a>
+</em>
+</td>
+<td>
+<p>AccessMode determines the access modes for the volume, e.g., ReadWriteOnce.
+This affects how the volume can be mounted.
+&ldquo;ReadWriteOnce&rdquo; can be mounted in read/write mode to exactly 1 host
+&ldquo;ReadOnlyMany&rdquo; can be mounted in read-only mode to many hosts
+&ldquo;ReadWriteMany&rdquo; can be mounted in read/write mode to many hosts
+&ldquo;ReadWriteOncePod&rdquo; can be mounted in read/write mode to exactly 1 pod, cannot be used in combination with other access modes</p>
+</td>
+</tr>
+<tr>
+<td>
+<code>requestsStorage</code><br>
+<em>
+string
+</em>
+</td>
+<td>
+<p>StorageRequest defines the storage size required for this PVC, e.g., 1Gi, 100Mi.
+It specifies the storage capacity needed as part of ResourceRequirements.</p>
+</td>
+</tr>
+<tr>
+<td>
+<code>storageClassName</code><br>
+<em>
+string
+</em>
+</td>
+<td>
+<p>StorageClassName specifies the StorageClass name to which this persistent volume belongs, e.g., manual.
+It allows the PVC to use the characteristics defined by the StorageClass.</p>
+</td>
+</tr>
+<tr>
+<td>
+<code>volumeMode</code><br>
+<em>
+<a href="https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.25/#persistentvolumemode-v1-core">
+Kubernetes core/v1.PersistentVolumeMode
+</a>
+</em>
+</td>
+<td>
+<p>VolumeMode specifies whether the volume should be used with a formatted filesystem (Filesystem)
+or remain in raw block state (Block). The Filesystem value is implied when not included.
+&ldquo;Block&rdquo;  means the volume will not be formatted with a filesystem and will remain a raw block device.
+&ldquo;Filesystem&rdquo;  means the volume will be or is formatted with a filesystem.</p>
+</td>
+</tr>
+</tbody>
+</table>
+</div>
+</div>
 <div class="admonition note">
 <p class="last">This page was automatically generated with <code>gen-crd-api-reference-docs</code></p>
 </div>
diff --git a/docs/proposals/pipeline/pipeline.md b/docs/proposals/pipeline/pipeline.md
index 085a33a..3bd902a 100644
--- a/docs/proposals/pipeline/pipeline.md
+++ b/docs/proposals/pipeline/pipeline.md
@@ -368,35 +368,55 @@ For more details, see [Tekton signed-provenance-tutorial](https://tekton.dev/doc
 
 #### Kurator Pipeline cli
 
-##### 1. Pipeline Commands
+We are focused on providing the viewing logs of Pipeline executions. Therefore, we have concentrated the CLI's features on this core requirement.
 
-- **`pipeline list`**
-  
-    Display all current Pipelines.
+For viewing information about Pipeline objects, users can directly utilize existing tools like kubectl.
 
-- **`pipeline describe <name>`**
+##### Pipeline Execution Commands
 
-    Show detailed information of a specified Pipeline.
+###### List Commands
 
-- **`pipeline log --lastExecution <name>`**
+- **`kurator pipeline execution list -n`**
 
-    Display the logs of the most recent PipelineExecution triggered by an event for a specified Pipeline.
+This command is used to list all Pipeline execution instances and groups them according to the creator (Pipeline). Each instance will display its creation time and the name of the creator.
 
-##### 2. PipelineExecution Commands
+Here is an example output.
 
-- **`pipeline-execution list [--pipeline=<name>]`**
-  
-    List all PipelineExecution instances. The list includes each execution's event information and timestamp. 
-    
-    If the `--pipeline` parameter is provided, only list PipelineExecutions belonging to a specific Pipeline.
+```console
+-----------------------------------------------------------------------------------
+Execution Name                  | Creation Time          | Namespace        | Pipeline
+-----------------------------------------------------------------------------------
+quick-start-run-frb7l           | 2024-01-02 10:15:30    | kurator-pipeline | quick-start
+quick-start-run-h65wx           | 2024-01-02 11:20:45    | kurator-pipeline | quick-start
+my-app-pipeline-run-a1b2c       | 2024-01-03 14:25:05    | kurator-pipeline | my-app-pipeline
+my-app-pipeline-run-d3e4f       | 2024-01-03 15:55:30    | kurator-pipeline | my-app-pipeline
+my-app-pipeline-run-g6h7i       | 2024-01-03 17:10:00    | kurator-pipeline | my-app-pipeline
+```
 
-- **`pipeline-execution describe <name>`**
+###### Logs Commands
 
-    Show detailed information of a specified PipelineExecution, including task composition, current status.
+- **`kurator pipeline execution logs <execution-name>`**
 
-- **`pipeline-execution logs <name>`**
+This command retrieves and displays the logs for a specified pipeline execution, providing detailed insights into its operational flow.
 
-    Retrieve and display logs of a specified PipelineExecution.
+Here is an example output.
+
+```console
+$ kurator pipeline execution logs quick-start-run-frb7l -n kurator-pipeline
++ '[' true '=' true ]
++ cp /workspace/basic-auth/.git-credentials /home/git/.git-credentials
++ cp /workspace/basic-auth/.gitconfig /home/git/.gitconfig
++ chmod 400 /home/git/.git-credentials
++ chmod 400 /home/git/.gitconfig
+...
+INFO[0158] Running: [/bin/sh -c chown -R app:app ./]    
+INFO[0158] Taking snapshot of full filesystem...        
+INFO[0158] USER app                                     
+INFO[0158] Cmd: USER                                    
+INFO[0158] CMD ["./podinfo"]                            
+INFO[0158] Pushing image to ghcr.io/xieql/kurator-test:0.4.1 
+INFO[0186] Pushed ghcr.io/xieql/kurator-test@sha256:5c8d9af38ab924b7d920de000a161bed8e695769a006ab7a03dd407c041942af 
+```
 
 #### Test Plan
 
diff --git a/manifests/charts/fleet-manager/crds/pipeline.kurator.dev_pipelines.yaml b/manifests/charts/fleet-manager/crds/pipeline.kurator.dev_pipelines.yaml
index 97f6a9c..fe14da7 100644
--- a/manifests/charts/fleet-manager/crds/pipeline.kurator.dev_pipelines.yaml
+++ b/manifests/charts/fleet-manager/crds/pipeline.kurator.dev_pipelines.yaml
@@ -42,11 +42,42 @@ spec:
                   of the pipeline.
                 type: string
               sharedWorkspace:
-                description: SharedWorkspace is the name of the PVC. If not specified,
-                  a PVC with the Pipeline's name as prefix will be created by default.
-                  If not set, Kurator will create a PVC named Pipeline.name using
-                  default config
-                type: string
+                description: SharedWorkspace is the config of the PVC where task using
+                  The PersistentVolumeClaim with this config will be created for each
+                  pipeline execution it allows the user to specify e.g. size and StorageClass
+                  for the volume. If not set, Kurator will create a PVC named Pipeline.name
+                  using default config
+                properties:
+                  accessMode:
+                    description: AccessMode determines the access modes for the volume,
+                      e.g., ReadWriteOnce. This affects how the volume can be mounted.
+                      "ReadWriteOnce" can be mounted in read/write mode to exactly
+                      1 host "ReadOnlyMany" can be mounted in read-only mode to many
+                      hosts "ReadWriteMany" can be mounted in read/write mode to many
+                      hosts "ReadWriteOncePod" can be mounted in read/write mode to
+                      exactly 1 pod, cannot be used in combination with other access
+                      modes
+                    type: string
+                  requestsStorage:
+                    description: StorageRequest defines the storage size required
+                      for this PVC, e.g., 1Gi, 100Mi. It specifies the storage capacity
+                      needed as part of ResourceRequirements.
+                    pattern: ^[0-9]+(\.[0-9]+)?(Gi|Mi)$
+                    type: string
+                  storageClassName:
+                    description: StorageClassName specifies the StorageClass name
+                      to which this persistent volume belongs, e.g., manual. It allows
+                      the PVC to use the characteristics defined by the StorageClass.
+                    type: string
+                  volumeMode:
+                    description: VolumeMode specifies whether the volume should be
+                      used with a formatted filesystem (Filesystem) or remain in raw
+                      block state (Block). The Filesystem value is implied when not
+                      included. "Block"  means the volume will not be formatted with
+                      a filesystem and will remain a raw block device. "Filesystem"  means
+                      the volume will be or is formatted with a filesystem.
+                    type: string
+                type: object
               tasks:
                 description: Tasks is an ordered list of tasks in the pipeline, containing
                   detailed information about each task. The tasks will be executed
diff --git a/pkg/apis/pipeline/v1alpha1/type.go b/pkg/apis/pipeline/v1alpha1/type.go
index d618538..ef54910 100644
--- a/pkg/apis/pipeline/v1alpha1/type.go
+++ b/pkg/apis/pipeline/v1alpha1/type.go
@@ -46,10 +46,39 @@ type PipelineSpec struct {
 	// The tasks will be executed in the order they are listed.
 	Tasks []PipelineTask `json:"tasks"`
 
-	// SharedWorkspace is the name of the PVC. If not specified, a PVC with the Pipeline's name as prefix will be created by default.
+	// SharedWorkspace is the config of the PVC where task using
+	// The PersistentVolumeClaim with this config will be created for each pipeline execution
+	// it allows the user to specify e.g. size and StorageClass for the volume.
 	// If not set, Kurator will create a PVC named Pipeline.name using default config
 	// +optional
-	SharedWorkspace *string `json:"sharedWorkspace,omitempty"`
+	SharedWorkspace *VolumeClaimTemplate `json:"sharedWorkspace,omitempty"`
+}
+
+// VolumeClaimTemplate is the configuration for the volume claim template in pipeline execution.
+// For more details, see https://github.com/kubernetes/api/blob/master/core/v1/types.go
+type VolumeClaimTemplate struct {
+	// AccessMode determines the access modes for the volume, e.g., ReadWriteOnce.
+	// This affects how the volume can be mounted.
+	// "ReadWriteOnce" can be mounted in read/write mode to exactly 1 host
+	// "ReadOnlyMany" can be mounted in read-only mode to many hosts
+	// "ReadWriteMany" can be mounted in read/write mode to many hosts
+	// "ReadWriteOncePod" can be mounted in read/write mode to exactly 1 pod, cannot be used in combination with other access modes
+	AccessMode corev1.PersistentVolumeAccessMode `json:"accessMode,omitempty"`
+
+	// StorageRequest defines the storage size required for this PVC, e.g., 1Gi, 100Mi.
+	// It specifies the storage capacity needed as part of ResourceRequirements.
+	// +kubebuilder:validation:Pattern="^[0-9]+(\\.[0-9]+)?(Gi|Mi)$"
+	StorageRequest string `json:"requestsStorage,omitempty"`
+
+	// StorageClassName specifies the StorageClass name to which this persistent volume belongs, e.g., manual.
+	// It allows the PVC to use the characteristics defined by the StorageClass.
+	StorageClassName string `json:"storageClassName,omitempty"`
+
+	// VolumeMode specifies whether the volume should be used with a formatted filesystem (Filesystem)
+	// or remain in raw block state (Block). The Filesystem value is implied when not included.
+	// "Block"  means the volume will not be formatted with a filesystem and will remain a raw block device.
+	// "Filesystem"  means the volume will be or is formatted with a filesystem.
+	VolumeMode corev1.PersistentVolumeMode `json:"volumeMode,omitempty"`
 }
 
 type PipelineTask struct {
diff --git a/pkg/apis/pipeline/v1alpha1/zz_generated.deepcopy.go b/pkg/apis/pipeline/v1alpha1/zz_generated.deepcopy.go
index 7154abf..7d27231 100644
--- a/pkg/apis/pipeline/v1alpha1/zz_generated.deepcopy.go
+++ b/pkg/apis/pipeline/v1alpha1/zz_generated.deepcopy.go
@@ -133,7 +133,7 @@ func (in *PipelineSpec) DeepCopyInto(out *PipelineSpec) {
 	}
 	if in.SharedWorkspace != nil {
 		in, out := &in.SharedWorkspace, &out.SharedWorkspace
-		*out = new(string)
+		*out = new(VolumeClaimTemplate)
 		**out = **in
 	}
 	return
@@ -218,3 +218,19 @@ func (in *PredefinedTask) DeepCopy() *PredefinedTask {
 	in.DeepCopyInto(out)
 	return out
 }
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *VolumeClaimTemplate) DeepCopyInto(out *VolumeClaimTemplate) {
+	*out = *in
+	return
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new VolumeClaimTemplate.
+func (in *VolumeClaimTemplate) DeepCopy() *VolumeClaimTemplate {
+	if in == nil {
+		return nil
+	}
+	out := new(VolumeClaimTemplate)
+	in.DeepCopyInto(out)
+	return out
+}
diff --git a/pkg/client/client.go b/pkg/client/client.go
index 8a5b594..43fc6d6 100644
--- a/pkg/client/client.go
+++ b/pkg/client/client.go
@@ -23,6 +23,7 @@ import (
 	"net/http"
 	"net/url"
 
+	flaggerapi "github.com/fluxcd/flagger/pkg/apis/flagger/v1beta1"
 	clusterv1alpha1 "github.com/karmada-io/karmada/pkg/apis/cluster/v1alpha1"
 	karmadaclientset "github.com/karmada-io/karmada/pkg/generated/clientset/versioned"
 	promclient "github.com/prometheus-operator/prometheus-operator/pkg/client/versioned"
@@ -30,6 +31,7 @@ import (
 	tektonapi "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1"
 	veleroapi "github.com/vmware-tanzu/velero/pkg/apis/velero/v1"
 	helmclient "helm.sh/helm/v3/pkg/kube"
+	appsv1 "k8s.io/api/apps/v1"
 	corev1 "k8s.io/api/core/v1"
 	crdclientset "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@@ -76,6 +78,15 @@ func NewClient(rest genericclioptions.RESTClientGetter) (*Client, error) {
 	if err := tektonapi.AddToScheme(scheme); err != nil {
 		return nil, fmt.Errorf("failed to add tektonapi to scheme: %v", err)
 	}
+	// add flagger resource
+	if err := flaggerapi.AddToScheme(scheme); err != nil {
+		return nil, fmt.Errorf("failed to add flagger api to scheme: %v", err)
+	}
+	// add appsv1 resource
+	// TODO: add commonly used resources for k8s
+	if err := appsv1.AddToScheme(scheme); err != nil {
+		return nil, fmt.Errorf("failed to add appv1 api to scheme: %v", err)
+	}
 	// create controller-runtime client with scheme
 	ctrlRuntimeClient, err := client.New(c, client.Options{Scheme: scheme})
 	if err != nil {
diff --git a/pkg/fleet-manager/pipeline/render/manifests/rbac/rbac.tpl b/pkg/fleet-manager/pipeline/render/manifests/rbac/rbac.tpl
deleted file mode 100644
index bf36061..0000000
--- a/pkg/fleet-manager/pipeline/render/manifests/rbac/rbac.tpl
+++ /dev/null
@@ -1,33 +0,0 @@
-apiVersion: v1
-kind: ServiceAccount
-metadata:
-  name: "{{ .ServiceAccountName }}"
-  namespace: "{{ .PipelineNamespace }}"
----
-apiVersion: rbac.authorization.k8s.io/v1
-kind: RoleBinding
-metadata:
-  name: "{{ .BroadResourceRoleBindingName }}"
-  namespace: "{{ .PipelineNamespace }}"
-subjects:
-- kind: ServiceAccount
-  name: "{{ .ServiceAccountName }}"
-  namespace: "{{ .PipelineNamespace }}"
-roleRef:
-  apiGroup: rbac.authorization.k8s.io
-  kind: ClusterRole
-  name: tekton-triggers-eventlistener-roles # add role for handle broad-resource, such as eventListener, triggers, configmaps and so on. `tekton-triggers-eventlistener-roles` is provided by Tekton
----
-apiVersion: rbac.authorization.k8s.io/v1
-kind: RoleBinding
-metadata:
-  name: "{{ .SecretRoleBindingName }}"
-  namespace: "{{ .PipelineNamespace }}"
-subjects:
-- kind: ServiceAccount
-  name: "{{ .ServiceAccountName }}"
-  namespace: "{{ .PipelineNamespace }}"
-roleRef:
-  apiGroup: rbac.authorization.k8s.io
-  kind: ClusterRole
-  name: tekton-triggers-eventlistener-clusterroles # add role for handle secret, clustertriggerbinding and clusterinterceptors. `tekton-triggers-eventlistener-clusterroles` is provided by Tekton
diff --git a/pkg/fleet-manager/pipeline/render/rbac.go b/pkg/fleet-manager/pipeline/render/rbac.go
index bb62eaa..bdf9bb7 100644
--- a/pkg/fleet-manager/pipeline/render/rbac.go
+++ b/pkg/fleet-manager/pipeline/render/rbac.go
@@ -18,15 +18,12 @@ package render
 
 import (
 	"fmt"
-	"io/fs"
+
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 )
 
 const (
-	// RBACTemplateFileName is the name of the RBAC template file.
-	RBACTemplateFileName = "rbac.tpl"
-	RBACTemplateName     = "pipeline rbac template"
-	SecretSuffix         = "-secret"
-	BroadResourceSuffix  = "-broad-resource"
+	RBACTemplateName = "pipeline rbac template"
 )
 
 // RBACConfig contains the configuration data required for the RBAC template.
@@ -34,27 +31,77 @@ const (
 type RBACConfig struct {
 	PipelineName      string // Name of the pipeline.
 	PipelineNamespace string // Kubernetes namespace where the pipeline is deployed.
+	OwnerReference    *metav1.OwnerReference
 }
 
-// ServiceAccountName generates the service account name using the pipeline name and namespace.
+// ServiceAccountName generates the service account name using the pipeline name.
 func (rbac RBACConfig) ServiceAccountName() string {
 	return rbac.PipelineName
 }
 
-// RoleBindingName generates the role binding name using the service account name.
-func (rbac RBACConfig) BroadResourceRoleBindingName() string {
-	return rbac.ServiceAccountName() + BroadResourceSuffix
-}
-
-// ClusterRoleBindingName generates the cluster role binding name using the service account name.
-func (rbac RBACConfig) SecretRoleBindingName() string {
-	return rbac.ServiceAccountName() + SecretSuffix
-}
-
-// renderRBAC renders the RBAC configuration using a specified template.
-func renderRBAC(fsys fs.FS, cfg RBACConfig) ([]byte, error) {
+// RenderRBAC renders the RBAC configuration using a specified template.
+func RenderRBAC(cfg RBACConfig) ([]byte, error) {
 	if cfg.PipelineName == "" || cfg.PipelineNamespace == "" {
 		return nil, fmt.Errorf("invalid RBACConfig: PipelineName and PipelineNamespace must not be empty")
 	}
-	return renderPipelineTemplate(fsys, RBACTemplateFileName, RBACTemplateName, cfg)
+	return renderTemplate(RBACTemplateContent, RBACTemplateName, cfg)
 }
+
+const RBACTemplateContent = `apiVersion: v1
+kind: ServiceAccount
+metadata:
+  name: "{{ .ServiceAccountName }}"
+  namespace: "{{ .PipelineNamespace }}"
+{{- if .OwnerReference }}
+  ownerReferences:
+  - apiVersion: "{{ .OwnerReference.APIVersion }}"
+    kind: "{{ .OwnerReference.Kind }}"
+    name: "{{ .OwnerReference.Name }}"
+    uid: "{{ .OwnerReference.UID }}"
+{{- end }}
+secrets:
+  - name: "chain-credentials"
+    namespace: "{{ .PipelineNamespace }}"
+---
+apiVersion: rbac.authorization.k8s.io/v1
+kind: RoleBinding
+metadata:
+  name: "{{ .PipelineName }}"
+  namespace: "{{ .PipelineNamespace }}"
+{{- if .OwnerReference }}
+  ownerReferences:
+  - apiVersion: "{{ .OwnerReference.APIVersion }}"
+    kind: "{{ .OwnerReference.Kind }}"
+    name: "{{ .OwnerReference.Name }}"
+    uid: "{{ .OwnerReference.UID }}"
+{{- end }}
+subjects:
+- kind: ServiceAccount
+  name: "{{ .ServiceAccountName }}"
+  namespace: "{{ .PipelineNamespace }}"
+roleRef:
+  apiGroup: rbac.authorization.k8s.io
+  kind: ClusterRole
+  name: tekton-triggers-eventlistener-roles # add role for handle broad-resource, such as eventListener, triggers, configmaps and so on. tekton-triggers-eventlistener-roles is provided by Tekton
+---
+apiVersion: rbac.authorization.k8s.io/v1
+kind: ClusterRoleBinding
+metadata:
+  name: "{{ .PipelineName }}"
+  namespace: "{{ .PipelineNamespace }}"
+{{- if .OwnerReference }}
+  ownerReferences:
+  - apiVersion: "{{ .OwnerReference.APIVersion }}"
+    kind: "{{ .OwnerReference.Kind }}"
+    name: "{{ .OwnerReference.Name }}"
+    uid: "{{ .OwnerReference.UID }}"
+{{- end }}
+subjects:
+- kind: ServiceAccount
+  name: "{{ .ServiceAccountName }}"
+  namespace: "{{ .PipelineNamespace }}"
+roleRef:
+  apiGroup: rbac.authorization.k8s.io
+  kind: ClusterRole
+  name: tekton-triggers-eventlistener-clusterroles # add role for handle secret, clustertriggerbinding and clusterinterceptors. tekton-triggers-eventlistener-clusterroles is provided by Tekton
+`
diff --git a/pkg/fleet-manager/pipeline/render/util.go b/pkg/fleet-manager/pipeline/render/util.go
index d5f410c..74d4b66 100644
--- a/pkg/fleet-manager/pipeline/render/util.go
+++ b/pkg/fleet-manager/pipeline/render/util.go
@@ -18,23 +18,15 @@ package render
 
 import (
 	"bytes"
-	"html/template"
-	"io/fs"
+	"text/template"
 
 	"github.com/Masterminds/sprig/v3"
 	"sigs.k8s.io/yaml"
 )
 
-// renderPipelineTemplate reads, parses, and renders a template file using the provided configuration data.
-func renderPipelineTemplate(fsys fs.FS, tplFileName, tplName string, cfg interface{}) ([]byte, error) {
-	out, err := fs.ReadFile(fsys, tplFileName)
-	if err != nil {
-		return nil, err
-	}
-
-	t := template.New(tplName)
-
-	tpl, err := t.Funcs(funMap()).Parse(string(out))
+// renderTemplate reads, parses, and renders a template file using the provided configuration data.
+func renderTemplate(tplFileString, tplName string, cfg interface{}) ([]byte, error) {
+	tpl, err := template.New(tplName).Funcs(funMap()).Parse(tplFileString)
 	if err != nil {
 		return nil, err
 	}
diff --git a/pkg/pipeline/execution/list/list.go b/pkg/pipeline/execution/list/list.go
new file mode 100644
index 0000000..63119b2
--- /dev/null
+++ b/pkg/pipeline/execution/list/list.go
@@ -0,0 +1,130 @@
+/*
+Copyright Kurator Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+	http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package list
+
+import (
+	"context"
+	"fmt"
+	"sort"
+
+	"github.com/sirupsen/logrus"
+	tektonapi "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	ctrlclient "sigs.k8s.io/controller-runtime/pkg/client"
+
+	"kurator.dev/kurator/pkg/client"
+	"kurator.dev/kurator/pkg/generic"
+)
+
+// pipelineList is the structure used for listing pipeline objects.
+type pipelineList struct {
+	*client.Client
+	args    *Args
+	options *generic.Options
+}
+
+// Args holds the arguments for listing pipeline runs.
+type Args struct {
+	Namespace     string // Specific namespace to list pipeline runs.
+	AllNamespaces bool   // Flag to list pipeline runs across all namespaces.
+}
+
+// NewPipelineList creates a new pipelineList instance.
+func NewPipelineList(opts *generic.Options, args *Args) (*pipelineList, error) {
+	pList := &pipelineList{
+		options: opts,
+		args:    args,
+	}
+	rest := opts.RESTClientGetter()
+	c, err := client.NewClient(rest)
+	if err != nil {
+		return nil, err
+	}
+	pList.Client = c
+	return pList, nil
+}
+
+// PipelineRunValue represents a single pipeline run.
+type PipelineRunValue struct {
+	Name              string
+	CreationTimestamp metav1.Time
+	Namespace         string
+	CreatorPipeline   string
+}
+
+// ListExecute fetches and displays a formatted list of PipelineRuns.
+func (p *pipelineList) ListExecute() error {
+	listOpts := &ctrlclient.ListOptions{}
+
+	// Apply namespace filter if AllNamespaces flag is not set.
+	if !p.args.AllNamespaces {
+		listOpts.Namespace = p.args.Namespace
+	}
+
+	pipelineRunList := &tektonapi.PipelineRunList{}
+	if err := p.CtrlRuntimeClient().List(context.Background(), pipelineRunList, listOpts); err != nil {
+		logrus.Errorf("failed to get PipelineRunList, %v", err)
+		return err
+	}
+
+	// Transform pipelineRunList items to PipelineRunValue instances.
+	var valueList []PipelineRunValue
+	for _, tr := range pipelineRunList.Items {
+		valueList = append(valueList, PipelineRunValue{
+			Name:              tr.Name,
+			CreationTimestamp: tr.CreationTimestamp,
+			Namespace:         tr.Namespace,
+			CreatorPipeline:   tr.Spec.PipelineRef.Name,
+		})
+	}
+
+	// Group and sort pipeline runs for display.
+	groupedRuns := GroupAndSortPipelineRuns(valueList)
+
+	fmt.Println("------------------------------------- Pipeline Execution -----------------------------")
+	fmt.Println("  Execution Name          |   Creation Time     |   Namespace      | Creator Pipeline")
+	fmt.Println("--------------------------------------------------------------------------------------")
+
+	for _, runs := range groupedRuns {
+		for _, tr := range runs {
+			fmt.Printf("%-25s | %-20s | %-16s | %s\n",
+				tr.Name,
+				tr.CreationTimestamp.Time.Format("2006-01-02 15:04:05"),
+				tr.Namespace,
+				tr.CreatorPipeline)
+		}
+	}
+
+	return nil
+}
+
+// GroupAndSortPipelineRuns organizes PipelineRunValues by CreatorPipeline and orders them by CreationTimestamp within each group.
+func GroupAndSortPipelineRuns(runs []PipelineRunValue) map[string][]PipelineRunValue {
+	groups := make(map[string][]PipelineRunValue)
+	for _, run := range runs {
+		groups[run.CreatorPipeline] = append(groups[run.CreatorPipeline], run)
+	}
+
+	// Sort each group by creation timestamp.
+	for _, group := range groups {
+		sort.Slice(group, func(i, j int) bool {
+			return group[i].CreationTimestamp.Time.Before(group[j].CreationTimestamp.Time)
+		})
+	}
+
+	return groups
+}


Test patch:
diff --git a/pkg/fleet-manager/pipeline/render/rbac_test.go b/pkg/fleet-manager/pipeline/render/rbac_test.go
index b8489ae..97a17dc 100644
--- a/pkg/fleet-manager/pipeline/render/rbac_test.go
+++ b/pkg/fleet-manager/pipeline/render/rbac_test.go
@@ -21,15 +21,11 @@ import (
 	"testing"
 
 	"github.com/stretchr/testify/assert"
-
-	"kurator.dev/kurator/pkg/fleet-manager/manifests"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 )
 
-var manifestFS = manifests.BuiltinOrDir("manifests/rbac/")
-
-const expectedRBACFilePath = "testdata/rbac/"
-
 func TestRenderRBAC(t *testing.T) {
+	const expectedRBACFilePath = "testdata/rbac/"
 	// Define test cases including both valid and error scenarios.
 	cases := []struct {
 		name         string
@@ -63,30 +59,30 @@ func TestRenderRBAC(t *testing.T) {
 			expectError: true,
 		},
 		{
-			name: "invalid file system path",
+			name: "configuration with OwnerReference",
 			cfg: RBACConfig{
-				PipelineName:      "example",
+				PipelineName:      "example-with-owner",
 				PipelineNamespace: "default",
+				OwnerReference: &metav1.OwnerReference{
+					APIVersion: "v1",
+					Kind:       "Deployment",
+					Name:       "example-deployment",
+					UID:        "12345678-1234-1234-1234-123456789abc",
+				},
 			},
-			expectError: true,
+			expectError:  false,
+			expectedFile: "with-owner.yaml",
 		},
 	}
 
 	for _, tc := range cases {
 		t.Run(tc.name, func(t *testing.T) {
-			fs := manifestFS
-			// Use an invalid file system for the relevant test case.
-			if tc.name == "invalid file system path" {
-				fs = manifests.BuiltinOrDir("invalid-path")
-			}
-
-			result, err := renderRBAC(fs, tc.cfg)
+			result, err := RenderRBAC(tc.cfg)
 
 			if tc.expectError {
 				assert.Error(t, err)
 			} else {
 				assert.NoError(t, err)
-
 				expected, err := os.ReadFile(expectedRBACFilePath + tc.expectedFile)
 				assert.NoError(t, err)
 				assert.Equal(t, string(expected), string(result))
diff --git a/pkg/fleet-manager/pipeline/render/testdata/rbac/default-example.yaml b/pkg/fleet-manager/pipeline/render/testdata/rbac/default-example.yaml
index 97681dc..94d7122 100644
--- a/pkg/fleet-manager/pipeline/render/testdata/rbac/default-example.yaml
+++ b/pkg/fleet-manager/pipeline/render/testdata/rbac/default-example.yaml
@@ -3,11 +3,14 @@ kind: ServiceAccount
 metadata:
   name: "example"
   namespace: "default"
+secrets:
+  - name: "chain-credentials"
+    namespace: "default"
 ---
 apiVersion: rbac.authorization.k8s.io/v1
 kind: RoleBinding
 metadata:
-  name: "example-broad-resource"
+  name: "example"
   namespace: "default"
 subjects:
 - kind: ServiceAccount
@@ -16,12 +19,12 @@ subjects:
 roleRef:
   apiGroup: rbac.authorization.k8s.io
   kind: ClusterRole
-  name: tekton-triggers-eventlistener-roles # add role for handle broad-resource, such as eventListener, triggers, configmaps and so on. `tekton-triggers-eventlistener-roles` is provided by Tekton
+  name: tekton-triggers-eventlistener-roles # add role for handle broad-resource, such as eventListener, triggers, configmaps and so on. tekton-triggers-eventlistener-roles is provided by Tekton
 ---
 apiVersion: rbac.authorization.k8s.io/v1
-kind: RoleBinding
+kind: ClusterRoleBinding
 metadata:
-  name: "example-secret"
+  name: "example"
   namespace: "default"
 subjects:
 - kind: ServiceAccount
@@ -30,4 +33,4 @@ subjects:
 roleRef:
   apiGroup: rbac.authorization.k8s.io
   kind: ClusterRole
-  name: tekton-triggers-eventlistener-clusterroles # add role for handle secret, clustertriggerbinding and clusterinterceptors. `tekton-triggers-eventlistener-clusterroles` is provided by Tekton
+  name: tekton-triggers-eventlistener-clusterroles # add role for handle secret, clustertriggerbinding and clusterinterceptors. tekton-triggers-eventlistener-clusterroles is provided by Tekton
diff --git a/pkg/fleet-manager/pipeline/render/testdata/rbac/with-owner.yaml b/pkg/fleet-manager/pipeline/render/testdata/rbac/with-owner.yaml
new file mode 100644
index 0000000..9f65d41
--- /dev/null
+++ b/pkg/fleet-manager/pipeline/render/testdata/rbac/with-owner.yaml
@@ -0,0 +1,51 @@
+apiVersion: v1
+kind: ServiceAccount
+metadata:
+  name: "example-with-owner"
+  namespace: "default"
+  ownerReferences:
+  - apiVersion: "v1"
+    kind: "Deployment"
+    name: "example-deployment"
+    uid: "12345678-1234-1234-1234-123456789abc"
+secrets:
+  - name: "chain-credentials"
+    namespace: "default"
+---
+apiVersion: rbac.authorization.k8s.io/v1
+kind: RoleBinding
+metadata:
+  name: "example-with-owner"
+  namespace: "default"
+  ownerReferences:
+  - apiVersion: "v1"
+    kind: "Deployment"
+    name: "example-deployment"
+    uid: "12345678-1234-1234-1234-123456789abc"
+subjects:
+- kind: ServiceAccount
+  name: "example-with-owner"
+  namespace: "default"
+roleRef:
+  apiGroup: rbac.authorization.k8s.io
+  kind: ClusterRole
+  name: tekton-triggers-eventlistener-roles # add role for handle broad-resource, such as eventListener, triggers, configmaps and so on. tekton-triggers-eventlistener-roles is provided by Tekton
+---
+apiVersion: rbac.authorization.k8s.io/v1
+kind: ClusterRoleBinding
+metadata:
+  name: "example-with-owner"
+  namespace: "default"
+  ownerReferences:
+  - apiVersion: "v1"
+    kind: "Deployment"
+    name: "example-deployment"
+    uid: "12345678-1234-1234-1234-123456789abc"
+subjects:
+- kind: ServiceAccount
+  name: "example-with-owner"
+  namespace: "default"
+roleRef:
+  apiGroup: rbac.authorization.k8s.io
+  kind: ClusterRole
+  name: tekton-triggers-eventlistener-clusterroles # add role for handle secret, clustertriggerbinding and clusterinterceptors. tekton-triggers-eventlistener-clusterroles is provided by Tekton
diff --git a/pkg/pipeline/execution/list/list_test.go b/pkg/pipeline/execution/list/list_test.go
new file mode 100644
index 0000000..3670e14
--- /dev/null
+++ b/pkg/pipeline/execution/list/list_test.go
@@ -0,0 +1,89 @@
+/*
+Copyright Kurator Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+	http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package list
+
+import (
+	"testing"
+	"time"
+
+	"github.com/stretchr/testify/assert"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+)
+
+// TestGroupAndSortPipelineRuns tests the GroupAndSortPipelineRuns function.
+func TestGroupAndSortPipelineRuns(t *testing.T) {
+	tests := []struct {
+		name     string
+		runs     []PipelineRunValue
+		expected map[string][]PipelineRunValue
+	}{
+		{
+			name: "Group and sort pipeline runs",
+			runs: []PipelineRunValue{
+				{
+					Name:              "run1",
+					CreationTimestamp: metav1.Time{Time: time.Date(2024, 01, 02, 10, 00, 00, 00, time.UTC)},
+					Namespace:         "ns1",
+					CreatorPipeline:   "pipeline1",
+				},
+				{
+					Name:              "run2",
+					CreationTimestamp: metav1.Time{Time: time.Date(2024, 01, 02, 12, 00, 00, 00, time.UTC)},
+					Namespace:         "ns2",
+					CreatorPipeline:   "pipeline2",
+				},
+				{
+					Name:              "run3",
+					CreationTimestamp: metav1.Time{Time: time.Date(2024, 01, 02, 11, 00, 00, 00, time.UTC)},
+					Namespace:         "ns1",
+					CreatorPipeline:   "pipeline1",
+				},
+			},
+			expected: map[string][]PipelineRunValue{
+				"pipeline1": {
+					{
+						Name:              "run1",
+						CreationTimestamp: metav1.Time{Time: time.Date(2024, 01, 02, 10, 00, 00, 00, time.UTC)},
+						Namespace:         "ns1",
+						CreatorPipeline:   "pipeline1",
+					},
+					{
+						Name:              "run3",
+						CreationTimestamp: metav1.Time{Time: time.Date(2024, 01, 02, 11, 00, 00, 00, time.UTC)},
+						Namespace:         "ns1",
+						CreatorPipeline:   "pipeline1",
+					},
+				},
+				"pipeline2": {
+					{
+						Name:              "run2",
+						CreationTimestamp: metav1.Time{Time: time.Date(2024, 01, 02, 12, 00, 00, 00, time.UTC)},
+						Namespace:         "ns2",
+						CreatorPipeline:   "pipeline2",
+					},
+				},
+			},
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			result := GroupAndSortPipelineRuns(tt.runs)
+			assert.Equal(t, tt.expected, result)
+		})
+	}
+}