Skip to content

Commit

Permalink
Let user define pipeline provenance definition
Browse files Browse the repository at this point in the history
By default on a `Push` or a `Pull Request`, Pipelines as Code will fetch the
PipelineRun definition from the branch of where the event has been triggered.

This behavior can be changed by setting the setting `pipelinerun_provenance`.
The setting currently accept two values:

- `source`: The default behavior, the PipelineRun definition will be fetched
  from the branch of where the event has been triggered.
- `default_branch`: The PipelineRun definition will be fetched from the default
  branch of the repository as configured on the git platform. For example
  `main`, `master`, or `trunk`.

Example:

This configuration specifies a repository named my-repo with a URL of
<https://github.com/my-org/my-repo>. It also sets the `pipelinerun_provenance`
setting to `default_branch`, which means that the PipelineRun definition will be
fetched from the default branch of the repository.

```yaml
apiVersion: "pipelinesascode.tekton.dev/v1alpha1"
kind: Repository
metadata:
  name: my-repo
spec:
  url: "https://github.com/owner/repo"
  settings:
    pipelinerun_provenance: "default_branch"
```

SRVKP: https://issues.redhat.com/browse/SRVKP-2896

Letting the user specify the provenance of the PipelineRun definition to default
branch is another layer of security. It ensures that only the one who has the
right to merge commit to the default branch can change the PipelineRun and have
access to the infrastrucutre.

Signed-off-by: Chmouel Boudjnah <chmouel@redhat.com>
  • Loading branch information
chmouel authored and savitaashture committed May 2, 2023
1 parent a45a317 commit 28f7790
Show file tree
Hide file tree
Showing 24 changed files with 356 additions and 91 deletions.
6 changes: 6 additions & 0 deletions config/300-repositories.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,12 @@ spec:
items:
description: list of repositories where Github token can be scoped
type: string
pipelinerun_provenance:
description: From where the PipelineRun definitions will be coming from
type: string
enum:
- source
- default_branch
concurrency_limit:
description: Number of maximum pipelinerun running at any moment
type: integer
Expand Down
41 changes: 40 additions & 1 deletion docs/content/docs/guide/repositorycrd.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ specific branch and event like a `push` or `pull_request`. It wil start the
`PipelineRun` where the `Repository` CR has been created. You can only start the
`PipelineRun` in the namespace where the Repository CR is located.

## Additional Repository CR Security
## Setting PipelineRun definition source

An additional layer of security can be added by using a PipelineRun annotation
to explicitly target a specific namespace. However, a Repository CRD must still
Expand Down Expand Up @@ -72,6 +72,45 @@ same URL. However, only the oldest created Repository CRD will be matched,
unless you use the `target-namespace` annotation.
{{< /hint >}}

### PipelineRun definition provenance

By default on a `Push` or a `Pull Request`, Pipelines as Code will fetch the
PipelineRun definition from the branch of where the event has been triggered.

This behavior can be changed by setting the setting `pipelinerun_provenance`.
The setting currently accept two values:

- `source`: The default behavior, the PipelineRun definition will be fetched
from the branch of where the event has been triggered.
- `default_branch`: The PipelineRun definition will be fetched from the default
branch of the repository as configured on the git platform. For example
`main`, `master`, or `trunk`.

Example:

This configuration specifies a repository named my-repo with a URL of
<https://github.com/my-org/my-repo>. It also sets the `pipelinerun_provenance`
setting to `default_branch`, which means that the PipelineRun definition will be
fetched from the default branch of the repository.

```yaml
apiVersion: "pipelinesascode.tekton.dev/v1alpha1"
kind: Repository
metadata:
name: my-repo
spec:
url: "https://github.com/owner/repo"
settings:
pipelinerun_provenance: "default_branch"
```

{{< hint info >}}
Letting the user specify the provenance of the PipelineRun definition to default
branch is another layer of security. It ensures that only the one who has the
right to merge commit to the default branch can change the PipelineRun and have
access to the infrastrucutre.
{{< /hint >}}

## Concurrency

`concurrency_limit` allows you to define the maximum number of PipelineRuns running at any time for a Repository.
Expand Down
2 changes: 1 addition & 1 deletion hack/dev/kind/install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -250,4 +250,4 @@ while getopts "RGgpcrb" o; do
done
shift $((OPTIND-1))

main
main
1 change: 1 addition & 0 deletions pkg/apis/pipelinesascode/v1alpha1/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ type RepositorySpec struct {

type Settings struct {
GithubAppTokenScopeRepos []string `json:"github_app_token_scope_repos,omitempty"`
PipelineRunProvenance string `json:"pipelinerun_provenance,omitempty"`
}

type Params struct {
Expand Down
6 changes: 5 additions & 1 deletion pkg/pipelineascode/match.go
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,11 @@ is that what you want? make sure you use -n when generating the secret, eg: echo

// getPipelineRunsFromRepo fetches pipelineruns from git repository and prepare them for creation
func (p *PacRun) getPipelineRunsFromRepo(ctx context.Context, repo *v1alpha1.Repository) ([]matcher.Match, error) {
rawTemplates, err := p.vcx.GetTektonDir(ctx, p.event, tektonDir)
provenance := "source"
if repo.Spec.Settings != nil && repo.Spec.Settings.PipelineRunProvenance != "" {
provenance = repo.Spec.Settings.PipelineRunProvenance
}
rawTemplates, err := p.vcx.GetTektonDir(ctx, p.event, tektonDir, provenance)
if err != nil || rawTemplates == "" {
msg := fmt.Sprintf("cannot locate templates in %s/ directory for this repository in %s", tektonDir, p.event.HeadBranch)
if err != nil {
Expand Down
2 changes: 1 addition & 1 deletion pkg/provider/bitbucketcloud/acl_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,7 @@ func TestIsAllowed(t *testing.T) {

bbcloudtest.MuxOrgMember(t, mux, tt.event, tt.fields.workspaceMembers)
bbcloudtest.MuxComments(t, mux, tt.event, tt.fields.comments)
bbcloudtest.MuxFiles(t, mux, tt.event, tt.fields.filescontents)
bbcloudtest.MuxFiles(t, mux, tt.event, tt.fields.filescontents, "")

v := &Provider{Client: bbclient}

Expand Down
34 changes: 26 additions & 8 deletions pkg/provider/bitbucketcloud/bitbucket.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ type Provider struct {
Logger *zap.SugaredLogger
Token, APIURL *string
Username *string
provenance string
}

// GetTaskURI TODO: Implement ME
Expand Down Expand Up @@ -115,7 +116,8 @@ func (v *Provider) CreateStatus(_ context.Context, _ versioned.Interface, event
return nil
}

func (v *Provider) GetTektonDir(_ context.Context, event *info.Event, path string) (string, error) {
func (v *Provider) GetTektonDir(_ context.Context, event *info.Event, path, provenance string) (string, error) {
v.provenance = provenance
repositoryFiles, err := v.getDir(event, path)
if err != nil {
return "", err
Expand All @@ -125,10 +127,18 @@ func (v *Provider) GetTektonDir(_ context.Context, event *info.Event, path strin
}

func (v *Provider) getDir(event *info.Event, path string) ([]bitbucket.RepositoryFile, error) {
// default set provenance from the SHA
revision := event.SHA
if v.provenance == "default_branch" {
revision = event.DefaultBranch
v.Logger.Infof("Using PipelineRun definition from default_branch: %s", event.DefaultBranch)
} else {
v.Logger.Infof("Using PipelineRun definition from source pull request SHA: %s", event.SHA)
}
repoFileOpts := &bitbucket.RepositoryFilesOptions{
Owner: event.Organization,
RepoSlug: event.Repository,
Ref: event.SHA,
Ref: revision,
Path: path,
}

Expand All @@ -139,8 +149,12 @@ func (v *Provider) getDir(event *info.Event, path string) ([]bitbucket.Repositor
return repositoryFiles, nil
}

func (v *Provider) GetFileInsideRepo(_ context.Context, runevent *info.Event, path, _ string) (string, error) {
return v.getBlob(runevent, runevent.SHA, path)
func (v *Provider) GetFileInsideRepo(_ context.Context, event *info.Event, path, _ string) (string, error) {
revision := event.SHA
if v.provenance == "default_branch" {
revision = event.DefaultBranch
}
return v.getBlob(event, revision, path)
}

func (v *Provider) SetClient(_ context.Context, _ *params.Run, event *info.Event) error {
Expand Down Expand Up @@ -203,16 +217,20 @@ func (v *Provider) GetCommitInfo(_ context.Context, event *info.Event) error {
return nil
}

func (v *Provider) concatAllYamlFiles(objects []bitbucket.RepositoryFile, runevent *info.Event) (string, error) {
func (v *Provider) concatAllYamlFiles(objects []bitbucket.RepositoryFile, event *info.Event) (string, error) {
var allTemplates string

revision := event.SHA
if v.provenance == "default_branch" {
revision = event.DefaultBranch
}
for _, value := range objects {
if value.Type == "commit_directory" {
objects, err := v.getDir(runevent, value.Path)
objects, err := v.getDir(event, value.Path)
if err != nil {
return "", err
}
subdirdata, err := v.concatAllYamlFiles(objects, runevent)
subdirdata, err := v.concatAllYamlFiles(objects, event)
if err != nil {
return "", err
}
Expand All @@ -222,7 +240,7 @@ func (v *Provider) concatAllYamlFiles(objects []bitbucket.RepositoryFile, runeve
allTemplates += fmt.Sprintf("\n%s\n", subdirdata)
} else if strings.HasSuffix(value.Path, ".yaml") ||
strings.HasSuffix(value.Path, ".yml") {
data, err := v.getBlob(runevent, runevent.SHA, value.Path)
data, err := v.getBlob(event, revision, value.Path)
if err != nil {
return "", err
}
Expand Down
46 changes: 33 additions & 13 deletions pkg/provider/bitbucketcloud/bitbucket_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import (
"github.com/openshift-pipelines/pipelines-as-code/pkg/provider"
bbcloudtest "github.com/openshift-pipelines/pipelines-as-code/pkg/provider/bitbucketcloud/test"
"github.com/openshift-pipelines/pipelines-as-code/pkg/provider/bitbucketcloud/types"
"go.uber.org/zap"
zapobserver "go.uber.org/zap/zaptest/observer"
"gotest.tools/v3/assert"
rtesting "knative.dev/pkg/reconciler/testing"
)
Expand All @@ -22,18 +24,29 @@ func TestGetConfig(t *testing.T) {

func TestGetTektonDir(t *testing.T) {
tests := []struct {
name string
event *info.Event
testDirPath string
contentContains string
wantErr bool
removeSuffix bool
name string
event *info.Event
testDirPath string
contentContains string
wantErr bool
removeSuffix bool
provenance string
filterMessageSnippet string
}{
{
name: "Get Tekton Directory",
event: bbcloudtest.MakeEvent(nil),
testDirPath: "../../pipelineascode/testdata/pull_request/.tekton",
contentContains: "kind: PipelineRun",
name: "Get Tekton Directory",
event: bbcloudtest.MakeEvent(nil),
testDirPath: "../../pipelineascode/testdata/pull_request/.tekton",
contentContains: "kind: PipelineRun",
filterMessageSnippet: "Using PipelineRun definition from source pull request SHA",
},
{
name: "Get Tekton Directory Mainbranch",
event: bbcloudtest.MakeEvent(&info.Event{DefaultBranch: "main"}),
testDirPath: "../../pipelineascode/testdata/pull_request/.tekton",
contentContains: "kind: PipelineRun",
provenance: "default_branch",
filterMessageSnippet: "Using PipelineRun definition from default_branch: main",
},
{
name: "Get Tekton Directory and subdirectory",
Expand All @@ -50,12 +63,14 @@ func TestGetTektonDir(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
observer, exporter := zapobserver.New(zap.InfoLevel)
fakelogger := zap.New(observer).Sugar()
ctx, _ := rtesting.SetupFakeContext(t)
bbclient, mux, tearDown := bbcloudtest.SetupBBCloudClient(t)
defer tearDown()
v := &Provider{Client: bbclient}
bbcloudtest.MuxDirContent(t, mux, tt.event, tt.testDirPath)
content, err := v.GetTektonDir(ctx, tt.event, ".tekton")
v := &Provider{Logger: fakelogger, Client: bbclient}
bbcloudtest.MuxDirContent(t, mux, tt.event, tt.testDirPath, tt.provenance)
content, err := v.GetTektonDir(ctx, tt.event, ".tekton", tt.provenance)
if tt.wantErr {
assert.Assert(t, err != nil, "GetTektonDir() error = %v, wantErr %v", err, tt.wantErr)
return
Expand All @@ -65,6 +80,11 @@ func TestGetTektonDir(t *testing.T) {
return
}
assert.Assert(t, strings.Contains(content, tt.contentContains), "content %s doesn't have %s", content, tt.contentContains)

if tt.filterMessageSnippet != "" {
gotcha := exporter.FilterMessageSnippet(tt.filterMessageSnippet)
assert.Assert(t, gotcha.Len() > 0, "expected to find %s in logs, found %v", tt.filterMessageSnippet, exporter.All())
}
})
}
}
Expand Down
24 changes: 16 additions & 8 deletions pkg/provider/bitbucketcloud/test/bbcloudtest.go
Original file line number Diff line number Diff line change
Expand Up @@ -84,11 +84,15 @@ func MuxOrgMember(t *testing.T, mux *http.ServeMux, event *info.Event, members [
})
}

func MuxFiles(t *testing.T, mux *http.ServeMux, event *info.Event, filescontents map[string]string) {
func MuxFiles(t *testing.T, mux *http.ServeMux, event *info.Event, filescontents map[string]string, provenance string) {
t.Helper()

sha := event.SHA
if provenance == "default_branch" {
sha = event.DefaultBranch
}
for key := range filescontents {
target := fmt.Sprintf("/repositories/%s/%s/src/%s", event.Organization, event.Repository, event.SHA)
target := fmt.Sprintf("/repositories/%s/%s/src/%s", event.Organization, event.Repository, sha)
mux.HandleFunc(target+"/"+key, func(rw http.ResponseWriter, r *http.Request) {
s := strings.ReplaceAll(r.URL.String(), target, "")
s = strings.TrimPrefix(s, "/")
Expand All @@ -97,11 +101,15 @@ func MuxFiles(t *testing.T, mux *http.ServeMux, event *info.Event, filescontents
}
}

func MuxListDirFiles(t *testing.T, mux *http.ServeMux, event *info.Event, dirs map[string][]bitbucket.RepositoryFile) {
func MuxListDirFiles(t *testing.T, mux *http.ServeMux, event *info.Event, dirs map[string][]bitbucket.RepositoryFile, provenance string) {
t.Helper()
sha := event.SHA
if provenance == "default_branch" {
sha = event.DefaultBranch
}

for key, value := range dirs {
urlp := "/repositories/" + event.Organization + "/" + event.Repository + "/src/" + event.SHA + "/" + key + "/"
urlp := "/repositories/" + event.Organization + "/" + event.Repository + "/src/" + sha + "/" + key + "/"
mux.HandleFunc(urlp, func(rw http.ResponseWriter, r *http.Request) {
dircontents := map[string][]bitbucket.RepositoryFile{
"values": value,
Expand Down Expand Up @@ -181,7 +189,7 @@ func MuxCreateComment(t *testing.T, mux *http.ServeMux, event *info.Event, expec
})
}

func MuxDirContent(t *testing.T, mux *http.ServeMux, event *info.Event, testdir string) {
func MuxDirContent(t *testing.T, mux *http.ServeMux, event *info.Event, testdir, provenance string) {
t.Helper()
files, err := os.ReadDir(testdir)
assert.NilError(t, err)
Expand All @@ -201,7 +209,7 @@ func MuxDirContent(t *testing.T, mux *http.ServeMux, event *info.Event, testdir
Type: btype,
})

MuxDirContent(t, mux, event, fpath)
MuxDirContent(t, mux, event, fpath, provenance)
} else {
btype := "file"
brfiles[relativenamedir] = append(brfiles[relativenamedir], bitbucket.RepositoryFile{
Expand All @@ -213,8 +221,8 @@ func MuxDirContent(t *testing.T, mux *http.ServeMux, event *info.Event, testdir
filecontents[relativename] = string(content)
}
}
MuxListDirFiles(t, mux, event, brfiles)
MuxFiles(t, mux, event, filecontents)
MuxListDirFiles(t, mux, event, brfiles, provenance)
MuxFiles(t, mux, event, filecontents, provenance)
}

func MakePREvent(accountid, nickname, sha, comment string) types.PullRequestEvent {
Expand Down
14 changes: 12 additions & 2 deletions pkg/provider/bitbucketserver/bitbucketserver.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ type Provider struct {
defaultBranchLatestCommit string
pullRequestNumber int
apiURL string
provenance string
projectKey string
}

Expand Down Expand Up @@ -153,9 +154,18 @@ func (v *Provider) getRaw(runevent *info.Event, revision, path string) (string,
return string(resp.Payload), nil
}

func (v *Provider) GetTektonDir(_ context.Context, event *info.Event, path string) (string, error) {
func (v *Provider) GetTektonDir(_ context.Context, event *info.Event, path, provenance string) (string, error) {
v.provenance = provenance
allValues, err := paginate(func(nextPage int) (*bbv1.APIResponse, error) {
localVarOptionals := map[string]interface{}{"at": event.SHA}
// according to the docs, if no at parameters is specified it will default to the default branch
// cf: https://docs.atlassian.com/bitbucket-server/rest/4.1.0/bitbucket-rest.html#idp2425664
localVarOptionals := map[string]interface{}{}
if v.provenance == "source" {
localVarOptionals = map[string]interface{}{"at": event.SHA}
v.Logger.Infof("Using PipelineRun definition from source pull request SHA: %s", event.SHA)
} else {
v.Logger.Infof("Using PipelineRun definition from default_branch: %s", event.DefaultBranch)
}
if nextPage != 0 {
localVarOptionals["start"] = nextPage
}
Expand Down
8 changes: 6 additions & 2 deletions pkg/provider/bitbucketserver/bitbucketserver_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ import (
"github.com/openshift-pipelines/pipelines-as-code/pkg/params/settings"
"github.com/openshift-pipelines/pipelines-as-code/pkg/provider"
bbtest "github.com/openshift-pipelines/pipelines-as-code/pkg/provider/bitbucketserver/test"
"go.uber.org/zap"
zapobserver "go.uber.org/zap/zaptest/observer"
"gotest.tools/v3/assert"
rtesting "knative.dev/pkg/reconciler/testing"
)
Expand Down Expand Up @@ -51,12 +53,14 @@ func TestGetTektonDir(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
observer, _ := zapobserver.New(zap.InfoLevel)
logger := zap.New(observer).Sugar()
ctx, _ := rtesting.SetupFakeContext(t)
client, mux, tearDown := bbtest.SetupBBServerClient(ctx)
defer tearDown()
v := &Provider{Client: client, projectKey: tt.event.Organization}
v := &Provider{Logger: logger, Client: client, projectKey: tt.event.Organization}
bbtest.MuxDirContent(t, mux, tt.event, tt.testDirPath, tt.path)
content, err := v.GetTektonDir(ctx, tt.event, tt.path)
content, err := v.GetTektonDir(ctx, tt.event, tt.path, "")
if tt.wantErr {
assert.Assert(t, err != nil,
"GetTektonDir() error = %v, wantErr %v", err, tt.wantErr)
Expand Down

0 comments on commit 28f7790

Please sign in to comment.