Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Auto configure new repo (only with GH app) #890

Merged
merged 2 commits into from
Oct 28, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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"]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a bit annoying thing to give! I wish we could separate those capaibilities depending of feature being enabled or not...

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah it is possible to do through operator, if enabled then update the controller role.
not possible directly with PAC I think 🤔
but gets complicated

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is it complicated with operator or when doing it with PAC,

I wonder if we should just disable it by default, document it and let the operator handles it?

I don't think everyone would be using this feature....

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

with operator it will be easy actually depending on what user configure we an update the role

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

- 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")
sm43 marked this conversation as resolved.
Show resolved Hide resolved
return
}
l.writeResponse(response, http.StatusOK, "skipped event")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this would look weird in log can you write which event has been skipped?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we don't log this here, this is api response
we log here

logger.Infof("github: repository event \"%v\" is not supported", repoEvent.GetAction())

for skipped repo events

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)
chmouel marked this conversation as resolved.
Show resolved Hide resolved
}

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
chmouel marked this conversation as resolved.
Show resolved Hide resolved
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" {
chmouel marked this conversation as resolved.
Show resolved Hide resolved
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())
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

GetRepoOwnerSplitter try to handle weird edge case for gitlab or other vcs naming, perhaps you can just use splitGithubURL which should be more robust for github

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

GetRepoOwnerSplitter is doing a lot more things .. and it error out on normal git url like https://github.com/openshift-pipelines/pipelines-as-code GetRepoOwnerSplitted just split and return owner and repo which is fine I think here.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

errors out here

return "", "", "", "", fmt.Errorf("URL %s does not seem to be a proper provider url: %w", uri, err)

not sure why that condition is <= 🤔 any reason to have =

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it's at least org/repo/path, == would be no subpath too

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