Skip to content

Commit

Permalink
Auto configuring newly created GitHub Repos with GH App
Browse files Browse the repository at this point in the history
supports template for generating namespace for repos

Signed-off-by: Shivam Mukhade <[email protected]>
  • Loading branch information
Shivam Mukhade committed Oct 26, 2022
1 parent 75b3932 commit 08fecbd
Show file tree
Hide file tree
Showing 10 changed files with 380 additions and 9 deletions.
5 changes: 4 additions & 1 deletion config/201-controller-role.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -62,12 +62,15 @@ metadata:
app.kubernetes.io/instance: default
app.kubernetes.io/part-of: pipelines-as-code
rules:
- apiGroups: [""]
resources: ["namespaces"]
verbs: ["create"]
- apiGroups: [""]
resources: ["secrets"]
verbs: ["get", "create"]
- apiGroups: ["pipelinesascode.tekton.dev"]
resources: ["repositories"]
verbs: ["list"]
verbs: ["create", "list"]
- apiGroups: ["tekton.dev"]
resources: ["pipelineruns"]
verbs: ["get", "create", "patch"]
Expand Down
10 changes: 10 additions & 0 deletions config/302-pac-configmap.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,16 @@ data:
# if defined then applies to all pipelineRun who doesn't have max-keep-runs annotation
default-max-keep-runs: ""

# Whether to auto configure newly created repositories, this will create a new namespace
# and repository CR, supported only with GitHub App
auto-configure-new-github-repo: "false"

# add a template to generate name for namespace for your auto configured github repo
# supported fields are repo_owner, repo_name
# eg. if defined as `{{repo_owner}}-{{repo_name}}-ci`, then namespace generated for repository
# https://github.com/owner/repo will be `owner-repo-ci`
auto-configure-repo-namespace-template: ""

kind: ConfigMap
metadata:
name: pipelines-as-code
Expand Down
24 changes: 23 additions & 1 deletion docs/content/docs/install/settings.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,9 +60,31 @@ There is a few things you can configure through the configmap

This allows user to define a default limit for max-keep-run value. If defined then it's applied to all the pipelineRun
which do not have `max-keep-runs` annotation.

* `auto-configure-new-github-repo`

Let you autoconfigure newly created GitHub repositories. On creation of a new repository, Pipelines As Code will set up a namespace
for your repository and create a Repository CR.

This feature is disabled by default and is only supported with GitHub App.

## Pipelines-As-Code Info
****NOTE****: If you have a GitHub App already setup then verify if `Repository` event is subscribed.

* `auto-configure-repo-namespace-template`

If `auto-configure-new-github-repo` is enabled then you can provide a template for generating the namespace for your new repository.
By default, the namespace will be generated using this format `{{repo_name}}-pipelines`.

You can override the default using the following variables

* `{{repo_owner}}`: The repository owner.
* `{{repo_name}}`: The repository name.

for example. if the template is defined as `{{repo_owner}}-{{repo_name}}-ci`, then the namespace generated for repository
`https://github.com/owner/repo` will be `owner-repo-ci`

## Pipelines-As-Code Info

There are a settings exposed through a configmap which any authenticated user can access to know about
Pipeline as Code.

Expand Down
21 changes: 19 additions & 2 deletions pkg/adapter/adapter.go
Original file line number Diff line number Diff line change
Expand Up @@ -106,8 +106,6 @@ func (l listener) handleEvent() http.HandlerFunc {
return
}

// read request Body

// event body
payload, err := io.ReadAll(request.Body)
if err != nil {
Expand All @@ -128,6 +126,25 @@ func (l listener) handleEvent() http.HandlerFunc {
var logger *zap.SugaredLogger

l.event = info.NewEvent()

// if repository auto configuration is enabled then check if its a valid event
if l.run.Info.Pac.AutoConfigureNewRepo {
detected, configuring, err := github.ConfigureRepository(ctx, l.run, request, string(payload), l.logger)
if detected {
if configuring && err == nil {
l.writeResponse(response, http.StatusCreated, "configured")
return
}
if configuring && err != nil {
l.logger.Errorf("repository auto-configure has failed, err: %v", err)
l.writeResponse(response, http.StatusOK, "failed to configure")
return
}
l.writeResponse(response, http.StatusOK, "skipped event")
return
}
}

isIncoming, targettedRepo, err := l.detectIncoming(ctx, request, payload)
if err != nil {
l.logger.Errorf("error processing incoming webhook: %v", err)
Expand Down
4 changes: 4 additions & 0 deletions pkg/adapter/adapter_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"github.com/google/go-github/v47/github"
"github.com/openshift-pipelines/pipelines-as-code/pkg/params"
"github.com/openshift-pipelines/pipelines-as-code/pkg/params/clients"
"github.com/openshift-pipelines/pipelines-as-code/pkg/params/info"
testclient "github.com/openshift-pipelines/pipelines-as-code/pkg/test/clients"
"go.uber.org/zap"
zapobserver "go.uber.org/zap/zaptest/observer"
Expand All @@ -33,6 +34,9 @@ func TestHandleEvent(t *testing.T) {
Clients: clients.Clients{
PipelineAsCode: cs.PipelineAsCode,
},
Info: info.Info{
Pac: &info.PacOpts{AutoConfigureNewRepo: false},
},
},
logger: getLogger(),
}
Expand Down
4 changes: 4 additions & 0 deletions pkg/params/info/pac.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ type PacOpts struct {
// bitbucket cloud specific fields
BitbucketCloudCheckSourceIP bool
BitbucketCloudAdditionalSourceIP string

// GitHub App specific
AutoConfigureNewRepo bool
AutoConfigureRepoNamespaceTemplate string
}

func (p *PacOpts) AddFlags(cmd *cobra.Command) error {
Expand Down
8 changes: 8 additions & 0 deletions pkg/params/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,14 @@ func (r *Run) UpdatePACInfo(ctx context.Context) error {
}
}

if autoConfigure, ok := cfg.Data["auto-configure-new-github-repo"]; ok {
r.Info.Pac.AutoConfigureNewRepo = StringToBool(autoConfigure)
}

if nsTemplate, ok := cfg.Data["auto-configure-repo-namespace-template"]; ok {
r.Info.Pac.AutoConfigureRepoNamespaceTemplate = nsTemplate
}

return nil
}

Expand Down
11 changes: 6 additions & 5 deletions pkg/provider/github/github.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,12 @@ const (
)

type Provider struct {
Client *github.Client
Logger *zap.SugaredLogger
Token, APIURL *string
ApplicationID *int64
providerName string
Client *github.Client
Logger *zap.SugaredLogger
Token, APIURL *string
ApplicationID *int64
providerName string
AutoConfigureNewRepos bool
}

// splitGithubURL Take a Github url and split it with org/repo path ref, supports rawURL
Expand Down
112 changes: 112 additions & 0 deletions pkg/provider/github/repository.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
package github

import (
"context"
"encoding/json"
"fmt"
"net/http"

"github.com/google/go-github/v47/github"
"github.com/openshift-pipelines/pipelines-as-code/pkg/apis/pipelinesascode/v1alpha1"
"github.com/openshift-pipelines/pipelines-as-code/pkg/formatting"
"github.com/openshift-pipelines/pipelines-as-code/pkg/params"
"github.com/openshift-pipelines/pipelines-as-code/pkg/params/clients"
"github.com/openshift-pipelines/pipelines-as-code/pkg/templates"
"go.uber.org/zap"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

const defaultNsTemplate = "%v-pipelines"

func ConfigureRepository(ctx context.Context, run *params.Run, req *http.Request, payload string, logger *zap.SugaredLogger) (bool, bool, error) {
// gitea set x-github-event too, so skip it for the gitea driver
if h := req.Header.Get("X-Gitea-Event-Type"); h != "" {
return false, false, nil
}
event := req.Header.Get("X-Github-Event")
if event != "repository" {
return false, false, nil
}

eventInt, err := github.ParseWebHook(event, []byte(payload))
if err != nil {
return true, false, err
}
_ = json.Unmarshal([]byte(payload), &eventInt)
repoEvent, _ := eventInt.(*github.RepositoryEvent)

if repoEvent.GetAction() != "created" {
logger.Infof("github: repository event \"%v\" is not supported", repoEvent.GetAction())
return true, false, nil
}

logger.Infof("github: configuring repository cr for repo: %v", repoEvent.Repo.GetHTMLURL())
if err := createRepository(ctx, run.Info.Pac.AutoConfigureRepoNamespaceTemplate, run.Clients, repoEvent, logger); err != nil {
logger.Errorf("failed repository creation: %v", err)
return true, true, err
}

return true, true, nil
}

func createRepository(ctx context.Context, nsTemplate string, clients clients.Clients, gitEvent *github.RepositoryEvent, logger *zap.SugaredLogger) error {
repoNsName, err := generateNamespaceName(nsTemplate, gitEvent)
if err != nil {
return fmt.Errorf("failed to generate namespace for repo: %w", err)
}

logger.Info("github: generated namespace name: ", repoNsName)

// create namespace
repoNs := &corev1.Namespace{
ObjectMeta: metav1.ObjectMeta{
Name: repoNsName,
},
}
repoNs, err = clients.Kube.CoreV1().Namespaces().Create(ctx, repoNs, metav1.CreateOptions{})
if err != nil && !errors.IsAlreadyExists(err) {
return fmt.Errorf("failed to create namespace %v: %w", repoNs.Name, err)
}

if errors.IsAlreadyExists(err) {
logger.Infof("github: namespace %v already exists, creating repository", repoNsName)
} else {
logger.Info("github: created repository namespace: ", repoNs.Name)
}

// create repository
repo := &v1alpha1.Repository{
ObjectMeta: metav1.ObjectMeta{
Name: repoNsName,
Namespace: repoNsName,
},
Spec: v1alpha1.RepositorySpec{
URL: gitEvent.Repo.GetHTMLURL(),
},
}
repo, err = clients.PipelineAsCode.PipelinesascodeV1alpha1().Repositories(repoNsName).Create(ctx, repo, metav1.CreateOptions{})
if err != nil {
return fmt.Errorf("failed to create repository for repo: %v: %w", gitEvent.Repo.GetHTMLURL(), err)
}
logger.Infof("github: repository created: %s/%s ", repo.Namespace, repo.Name)
return nil
}

func generateNamespaceName(nsTemplate string, gitEvent *github.RepositoryEvent) (string, error) {
repoOwner, repoName, err := formatting.GetRepoOwnerSplitted(gitEvent.Repo.GetHTMLURL())
if err != nil {
return "", fmt.Errorf("failed to parse git repo url: %w", err)
}

if nsTemplate == "" {
return fmt.Sprintf(defaultNsTemplate, repoName), nil
}

maptemplate := map[string]string{
"repo_owner": repoOwner,
"repo_name": repoName,
}
return templates.ReplacePlaceHoldersVariables(nsTemplate, maptemplate), nil
}
Loading

0 comments on commit 08fecbd

Please sign in to comment.