Skip to content

Commit 0edaa50

Browse files
chmouelsavitaashture
authored andcommitted
Add support of remote task on remote Pipeline
We now support remote tasks on remote Pipeline, allowing to share a remote Pipeline across multiple repositories. User can override tasks from the remote pipeline by adding a task with the same name. Signed-off-by: Chmouel Boudjnah <[email protected]>
1 parent d81b08e commit 0edaa50

File tree

10 files changed

+666
-110
lines changed

10 files changed

+666
-110
lines changed

docs/content/docs/guide/resolver.md

Lines changed: 71 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ command to learn on how to use it.
4646
location with annotations on PipelineRun.
4747

4848
If the resolver sees a PipelineRun referencing a remote task or a Pipeline in
49-
a PipelineRun or a PipelineSpec it will automatically inlines them.
49+
a PipelineRun or a PipelineSpec it will automatically inline them.
5050

5151
If multiple annotations reference the same task name the resolver will pick the
5252
first one fetched from the annotations.
@@ -128,18 +128,19 @@ will fetch the task directly from that remote URL :
128128
### Remote HTTP URL from a private GitHub repository
129129

130130
If you are using `GitHub` and If the remote task URL uses the same host as where
131-
the repo CRD is, PAC will use the GitHub token and fetch the URL using the
131+
the repository CRD is, PAC will use the GitHub token and fetch the URL using the
132132
GitHub API.
133133

134-
For example if you have a repo URL looking like this :
134+
For example if you have a repository URL looking like this :
135135

136136
<https://github.com/organization/repository>
137137

138138
and the remote HTTP URLs is a referenced GitHub "blob" URL:
139139

140140
<https://github.com/organization/repository/blob/mainbranch/path/file>
141141

142-
if the remote HTTP url has a slash (/) in the branch name you will need to html encode with the `%2F` character, eg:
142+
if the remote HTTP URL has a slash (/) in the branch name you will need to HTML
143+
encode with the `%2F` character, example:
143144

144145
<https://github.com/organization/repository/blob/feature%2Fmainbranch/path/file>
145146

@@ -150,14 +151,14 @@ GitHub app token are scoped to the owner or organization where the repository is
150151
If you are using the GitHub webhook method you are able to fetch any private or
151152
public repositories on any organization where the personal token is allowed.
152153

153-
There is settings you can set in the pac `Configmap` to control that behavior, see the
154+
There is settings you can set in the pac `Configmap` to control that behaviour, see the
154155
`secret-github-app-token-scoped` and `secret-github-app-scope-extra-repos` settings in the
155156
[settings documentation](/docs/install/settings).
156157

157158
### Tasks or Pipelines inside the repository
158159

159160
Additionally, you can as well have a reference to a task or pipeline from a YAML file inside
160-
your repo if you specify the relative path to it, for example :
161+
your repository if you specify the relative path to it, for example :
161162

162163
```yaml
163164
pipelinesascode.tekton.dev/task: "[share/tasks/git-clone.yaml]"
@@ -174,18 +175,77 @@ If the object fetched cannot be parsed as a Tekton `Task` it will error out.
174175

175176
## Remote Pipeline annotations
176177

177-
Remote Pipeline can be referenced by annotation, this allows you to share your Pipeline definition across.
178+
Remote Pipeline can be referenced by annotation, allowing you to share a Pipeline across multiple repositories.
178179

179-
Only one Pipeline is allowed in annotation.
180+
Only one Pipeline is allowed on the `PipelineRun` annotation.
180181

181-
An annotation to a remote pipeline looks like this :
182+
An annotation to a remote pipeline looks like this, using a remote URL:
182183

183184
```yaml
184185
pipelinesascode.tekton.dev/pipeline: "https://git.provider/raw/pipeline.yaml
185186
```
186187

187-
It supports remote URL and files inside the same Git repository.
188+
or from a relative path inside the repository:
189+
190+
```yaml
191+
pipelinesascode.tekton.dev/pipeline: "./tasks/pipeline.yaml
192+
```
193+
194+
Fetching `Pipelines` from the [Tekton Hub](https://hub.tekton.dev) is not currently supported.
195+
196+
### Overriding tasks from a remote pipeline on a PipelineRun
197+
198+
Remote task annotations on the remote pipeline are supported. No other
199+
annotations like `on-target-branch`, `on-event` or `on-cel-expression` are
200+
supported.
201+
202+
If a user wants to override one of the tasks from the remote pipeline, they can do
203+
so by adding a task in the annotations that has the same name In their `PipelineRun` annotations.
204+
205+
For example if the user PipelineRun contains those annotations:
206+
207+
```yaml
208+
kind: PipelineRun
209+
metadata:
210+
annotations:
211+
pipelinesascode.tekton.dev/pipeline: "https://git.provider/raw/pipeline.yaml
212+
pipelinesascode.tekton.dev/task: "./my-git-clone-task.yaml
213+
```
214+
215+
and the Pipeline referenced by the `pipelinesascode.tekton.dev/pipeline` annotation
216+
in "<https://git.provider/raw/pipeline.yaml>" contains those annotations:
217+
218+
```yaml
219+
kind: Pipeline
220+
metadata:
221+
annotations:
222+
pipelinesascode.tekton.dev/task: "git-clone"
223+
```
224+
225+
In this case if the `my-git-clone-task.yaml` file in the root directory is a
226+
task named `git-clone` it will be used instead of the `git-clone` on the remote
227+
pipeline that is coming from the Tekon Hub.
188228

189229
{{< hint info >}}
190-
[Tekton Hub](https://hub.tekton.dev) doesn't currently have support for `Pipeline`.
230+
Task overriding is only supported for tasks that are referenced by a `taskRef`
231+
to a `Name`, no override is done on `Tasks` embedded with a `taskSpec`. See
232+
[Tekton documentation](https://tekton.dev/docs/pipelines/pipelines/#adding-tasks-to-the-pipeline) for the differences between `taskRef` and `taskSpec`:
191233
{{< /hint >}}
234+
235+
### Tasks or Pipelines Precedence
236+
237+
From where tasks or pipelines of the same name takes precedence?
238+
239+
for remote Tasks, when you have a `taskRef` on a task name, pac will try to find the task in this order:
240+
241+
1. A task matched from the PipelineRun annotations
242+
2. A task matched from the remote Pipeline annotations
243+
3. A task matched fetched from the Tekton directory
244+
(the tasks from the `.tekton` directory and its subdirs are automatically included)
245+
246+
for remote Pipelines referenced on a `pipelineRef`, pac will try to match a
247+
pipeline in this order:
248+
249+
1. Pipeline from the PipelineRun annotations
250+
2. Pipeline from the Tekton directory (pipelines are automatically fetched from
251+
the `.tekton` directory and subdirs)

pkg/action/patch_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ func TestPatchPipelineRun(t *testing.T) {
2222
observer, _ := zapobserver.New(zap.InfoLevel)
2323
logger := zap.New(observer).Sugar()
2424

25-
testPR := tektontest.MakePR("namespace", "force-me", []pipelinev1.ChildStatusReference{
25+
testPR := tektontest.MakePRStatus("namespace", "force-me", []pipelinev1.ChildStatusReference{
2626
tektontest.MakeChildStatusReference("first"),
2727
tektontest.MakeChildStatusReference("last"),
2828
tektontest.MakeChildStatusReference("middle"),

pkg/matcher/annotation_tasks_install.go

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -216,7 +216,7 @@ func (rt RemoteTasks) GetTaskFromAnnotations(ctx context.Context, annotations ma
216216

217217
// GetPipelineFromAnnotations Get pipeline remotely if they are on Annotations
218218
// TODO: merge in a generic between the two
219-
func (rt RemoteTasks) GetPipelineFromAnnotations(ctx context.Context, annotations map[string]string) ([]*tektonv1.Pipeline, error) {
219+
func (rt RemoteTasks) GetPipelineFromAnnotations(ctx context.Context, annotations map[string]string) (*tektonv1.Pipeline, error) {
220220
ret := []*tektonv1.Pipeline{}
221221
pipelinesAnnotation, err := grabValuesFromAnnotations(annotations, pipelineAnnotationsRegexp)
222222
if err != nil {
@@ -225,6 +225,9 @@ func (rt RemoteTasks) GetPipelineFromAnnotations(ctx context.Context, annotation
225225
if len(pipelinesAnnotation) > 1 {
226226
return nil, fmt.Errorf("only one pipeline is allowed on remote resolution, we have received multiple of them: %+v", pipelinesAnnotation)
227227
}
228+
if len(pipelinesAnnotation) == 0 {
229+
return nil, nil
230+
}
228231
for _, v := range pipelinesAnnotation {
229232
data, err := rt.getRemote(ctx, v, false)
230233
if err != nil {
@@ -239,7 +242,7 @@ func (rt RemoteTasks) GetPipelineFromAnnotations(ctx context.Context, annotation
239242
}
240243
ret = append(ret, pipeline)
241244
}
242-
return ret, nil
245+
return ret[0], nil
243246
}
244247

245248
// getTaskFromLocalFS get task locally if file exist

pkg/matcher/annotation_tasks_install_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -463,10 +463,10 @@ func TestGetPipelineFromAnnotations(t *testing.T) {
463463
assert.Assert(t, len(fakelog.FilterMessageSnippet(tt.wantLog).TakeAll()) > 0, "could not find log message: got ", fakelog)
464464
}
465465
assert.NilError(t, err)
466-
assert.Assert(t, len(got) > 0, "GetPipelineFromAnnotations() error no pipelines has been processed")
466+
assert.Assert(t, got != nil, "GetPipelineFromAnnotations() error no pipelines has been processed")
467467

468468
if tt.gotPipelineName != "" {
469-
assert.Equal(t, tt.gotPipelineName, got[0].GetName())
469+
assert.Equal(t, tt.gotPipelineName, got.GetName())
470470
}
471471
})
472472
}

pkg/pipelineascode/pipelineascode_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -517,7 +517,7 @@ func TestRun(t *testing.T) {
517517
},
518518
Repositories: tt.repositories,
519519
PipelineRuns: []*pipelinev1.PipelineRun{
520-
tektontest.MakePR("namespace", "force-me", []pipelinev1.ChildStatusReference{
520+
tektontest.MakePRStatus("namespace", "force-me", []pipelinev1.ChildStatusReference{
521521
tektontest.MakeChildStatusReference("first"),
522522
tektontest.MakeChildStatusReference("last"),
523523
tektontest.MakeChildStatusReference("middle"),

pkg/reconciler/controller_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ func TestCheckStateAndEnqueue(t *testing.T) {
3333
})
3434

3535
// Create a new PipelineRun object with the "started" state label.
36-
testPR := tektontest.MakePR("namespace", "force-me", []pipelinev1.ChildStatusReference{
36+
testPR := tektontest.MakePRStatus("namespace", "force-me", []pipelinev1.ChildStatusReference{
3737
tektontest.MakeChildStatusReference("first"),
3838
tektontest.MakeChildStatusReference("last"),
3939
tektontest.MakeChildStatusReference("middle"),

pkg/resolve/remote.go

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
package resolve
2+
3+
import (
4+
"context"
5+
"fmt"
6+
7+
"github.com/openshift-pipelines/pipelines-as-code/pkg/matcher"
8+
)
9+
10+
type NamedItem interface {
11+
GetName() string
12+
}
13+
14+
func alreadySeen[T NamedItem](items []T, item T) bool {
15+
for _, value := range items {
16+
if value.GetName() == item.GetName() {
17+
return true
18+
}
19+
}
20+
return false
21+
}
22+
23+
// getRemotes will get remote tasks or Pipelines from annotations.
24+
//
25+
// It already has some tasks or pipeline coming from the tekton directory stored in [types]
26+
//
27+
// The precedence logic for tasks is in this order:
28+
//
29+
// * Tasks from the PipelineRun annotations
30+
// * Tasks from the Pipeline annotations
31+
// * Tasks from the Tekton directory
32+
//
33+
// The precedence logic for Pipeline is first from PipelineRun annotations and
34+
// then from Tekton directory
35+
func getRemotes(ctx context.Context, rt *matcher.RemoteTasks, types TektonTypes) (TektonTypes, error) {
36+
remoteType := &TektonTypes{}
37+
for _, pipelinerun := range types.PipelineRuns {
38+
if len(pipelinerun.GetObjectMeta().GetAnnotations()) == 0 {
39+
continue
40+
}
41+
42+
// get first all the tasks from the pipelinerun annotations
43+
remoteTasks, err := rt.GetTaskFromAnnotations(ctx, pipelinerun.GetObjectMeta().GetAnnotations())
44+
if err != nil {
45+
return TektonTypes{}, fmt.Errorf("error getting remote task from pipelinerun annotations: %w", err)
46+
}
47+
48+
for _, task := range remoteTasks {
49+
if alreadySeen(remoteType.Tasks, task) {
50+
rt.Logger.Infof("skipping duplicated task %s in annotations on pipelinerun %s", task.GetName(), pipelinerun.GetName())
51+
continue
52+
}
53+
remoteType.Tasks = append(remoteType.Tasks, task)
54+
}
55+
56+
// get the pipeline from the remote annotation if any
57+
remotePipeline, err := rt.GetPipelineFromAnnotations(ctx, pipelinerun.GetObjectMeta().GetAnnotations())
58+
if err != nil {
59+
return TektonTypes{}, fmt.Errorf("error getting remote pipeline from pipelinerun annotation: %w", err)
60+
}
61+
62+
if remotePipeline != nil {
63+
remoteType.Pipelines = append(remoteType.Pipelines, remotePipeline)
64+
}
65+
}
66+
67+
// grab the tasks from the remote pipeline
68+
for _, pipeline := range remoteType.Pipelines {
69+
if pipeline.GetObjectMeta().GetAnnotations() == nil {
70+
continue
71+
}
72+
remoteTasks, err := rt.GetTaskFromAnnotations(ctx, pipeline.GetObjectMeta().GetAnnotations())
73+
if err != nil {
74+
return TektonTypes{}, fmt.Errorf("error getting remote tasks from remote pipeline %s: %w", pipeline.GetName(), err)
75+
}
76+
77+
for _, remoteTask := range remoteTasks {
78+
if alreadySeen(remoteType.Tasks, remoteTask) {
79+
rt.Logger.Infof("skipping remote task %s from remote pipeline %s as already defined in pipelinerun", remoteTask.GetName(), pipeline.GetName())
80+
continue
81+
}
82+
remoteType.Tasks = append(remoteType.Tasks, remoteTask)
83+
}
84+
}
85+
86+
ret := TektonTypes{
87+
PipelineRuns: types.PipelineRuns,
88+
}
89+
// first get the remote types and then the local ones so remote takes precedence
90+
for _, task := range append(remoteType.Tasks, types.Tasks...) {
91+
if alreadySeen(ret.Tasks, task) {
92+
rt.Logger.Infof("overriding task %s coming from tekton directory by an annotation task on the pipeline or pipelinerun", task.GetName())
93+
continue
94+
}
95+
ret.Tasks = append(ret.Tasks, task)
96+
}
97+
for _, remotePipeline := range append(remoteType.Pipelines, types.Pipelines...) {
98+
if alreadySeen(ret.Pipelines, remotePipeline) {
99+
rt.Logger.Infof("overriding pipeline %s coming from tekton directory by the annotation pipelinerun", remotePipeline.GetName())
100+
continue
101+
}
102+
ret.Pipelines = append(ret.Pipelines, remotePipeline)
103+
}
104+
return ret, nil
105+
}

0 commit comments

Comments
 (0)