Skip to content

Commit

Permalink
[release-0.7] feat(svn): support svn post-commit hook (#607)
Browse files Browse the repository at this point in the history
* feat(svn): support svn post-commit hook

* docs(svn): add docs for using svn post-commit hook
  • Loading branch information
caicloud-bot committed Nov 19, 2018
1 parent 9d4c592 commit 00b8ed4
Show file tree
Hide file tree
Showing 15 changed files with 363 additions and 36 deletions.
1 change: 1 addition & 0 deletions build/server/Dockerfile
Expand Up @@ -3,6 +3,7 @@ FROM alpine:3.6
WORKDIR /root

RUN apk update && apk add ca-certificates && \
apk add --no-cache subversion && \
apk add tzdata && \
ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime && \
echo "Asia/Shanghai" > /etc/timezone
Expand Down
57 changes: 57 additions & 0 deletions docs/api/v1/api.md
Expand Up @@ -145,6 +145,7 @@
| --- | --- | --- |
| Create | POST `/api/v1/pipelines/{pipelineid}/githubwebhook` | WIP, [link](#github-webhook) |
| Create | POST `/api/v1/pipelines/{pipelineid}/gitlabwebhook` | WIP, [link](#gitlab-webhook) |
| Create | POST `/api/v1/subversion/{svnrepoid}/postcommithook` | [link](#svn-hooks) |

### Stats API

Expand Down Expand Up @@ -457,6 +458,9 @@ Success:
"stages": ["string", ...],
"comments": ["string", ...]
},
"postCommit":{
"stages": ["string", ...]
}
}
},
"build": {
Expand Down Expand Up @@ -1295,6 +1299,59 @@ Success:
200 OK
```

### SVN hooks

Trigger pipeline by SVN post-commit hooks.

**Request**

URL: `POST /api/v1/subversion/{svnrepoid}/postcommithook`

Header:
```
Content-Type:text/plain;charset=UTF-8
```

Query:
```
revision: 27 // {revision-id}
```

Body:
Output of `svnlook changed --revision $REV $REPOS`, for example:
```
U cyclone/test.go
U cyclone/README.md
```

**Response**

Success:

```
200 OK
```

To make post-commit hooks effective, you should [config your svn repository](#Config-your-svn-repository).

#### Config your svn repository
You can set up a post commit hook so the Subversion repository can notify cyclone whenever a change is made to that repository. To do this, put the following script in your post-commit file (in the $REPOSITORY/hooks directory):
```
REPOS="$1"
REV="$2"
TXN_NAME="$3"
UUID=`svnlook uuid $REPOS`
/usr/bin/curl --request POST --header "Content-Type:text/plain;charset=UTF-8" \
--data "`svnlook changed --revision $REV $REPOS`" \
{cyclone-server-address}/api/v1/subversion/$UUID/postcommithook?revision=$REV
```

Notes:

Replace `{cyclone-server-address}` by the actural cyclone server address value.

### PipelineStatusStatsObject

```
Expand Down
33 changes: 33 additions & 0 deletions pkg/api/types.go
Expand Up @@ -304,6 +304,7 @@ type AutoTrigger struct {

// SCMTrigger represents the auto trigger strategy from SCM.
type SCMTrigger struct {
PostCommit *PostCommitTrigger `bson:"postCommit,omitempty" json:"postCommit,omitempty" description:"post commit trigger strategy"`
Push *PushTrigger `bson:"push,omitempty" json:"push,omitempty" description:"push trigger strategy"`
TagRelease *TagReleaseTrigger `bson:"tagRelease,omitempty" json:"tagRelease,omitempty" description:"commit trigger strategy"`
PullRequest *PullRequestTrigger `bson:"pullRequest,omitempty" json:"pullRequest,omitempty" description:"pull request trigger strategy"`
Expand Down Expand Up @@ -339,6 +340,32 @@ type PushTrigger struct {
Branches []string `bson:"branches" json:"branches" description:"branches with new commit to trigger"`
}

// PostCommitTrigger represents the SCM auto trigger from SVN post_commit.
type PostCommitTrigger struct {
GeneralTrigger `bson:",inline"`

RepoInfo *RepoInfo `bson:"repoInfo" json:"repoInfo" description:"svn repository information"`
}

// RepoInfo contains svn repository information, id and root-url.
type RepoInfo struct {
// ID represents SVN repository UUID, this ID is retrieved by cyclone-server by
//
// 'svn info --show-item repos-uuid --username {user} --password {password} --non-interactive
// --trust-server-cert-failures unknown-ca,cn-mismatch,expired,not-yet-valid,other
// --no-auth-cache {remote-svn-address}'
//
ID string `bson:"id" json:"id" description:"svn repository UUID"`

// RootURL represents SVN repository root url, this root is retrieved by cyclone-server by
//
// 'svn info --show-item repos-root-url --username {user} --password {password} --non-interactive
// --trust-server-cert-failures unknown-ca,cn-mismatch,expired,not-yet-valid,other
// --no-auth-cache {remote-svn-address}'
//
RootURL string `bson:"rootURL" json:"rootURL" description:"svn repository root url"`
}

// TagReleaseTrigger represents the SCM auto trigger from tag release.
type TagReleaseTrigger struct {
GeneralTrigger `bson:",inline"`
Expand Down Expand Up @@ -701,6 +728,12 @@ const (
TriggerSCM string = "webhook"
TriggerCron string = "timer"

// Fixme, this is a litter tricky.
// SVNPostCommitRefPrefix is a flag used by svn code checkout;
// If 'ref' with this prefix, we will checkout code frome a specific revision,
// otherwise, apped ref to clone url, then do checkout.
SVNPostCommitRefPrefix string = "hook-post-commit-"
TriggerSVNHookPostCommit string = "hook-post-commit"
TriggerWebhookPush string = "webhook-push"
TriggerWebhookTagRelease string = "webhook-tag-release"
TriggerWebhookPullRequest string = "webhook-pull-request"
Expand Down
26 changes: 24 additions & 2 deletions pkg/api/v1/descriptor/webhook.go
Expand Up @@ -14,7 +14,7 @@ func init() {
var webhooks = []definition.Descriptor{
{
Path: "/pipelines/{pipelineid}/githubwebhook",
Description: "Cloud API",
Description: "GitHub webhook API",
Definitions: []definition.Definition{
{
Method: definition.Create,
Expand All @@ -32,7 +32,7 @@ var webhooks = []definition.Descriptor{
},
{
Path: "/pipelines/{pipelineid}/gitlabwebhook",
Description: "Cloud API",
Description: "GitLab webhook API",
Definitions: []definition.Definition{
{
Method: definition.Create,
Expand All @@ -48,4 +48,26 @@ var webhooks = []definition.Descriptor{
},
},
},
{
Path: "/subversion/{svnrepoid}/postcommithook",
Description: "SVN hooks API",
Definitions: []definition.Definition{
{
Method: definition.Create,
Function: handler.HandleSVNHooks,
Description: "Trigger the pipeline by svn hooks",
Parameters: []definition.Parameter{
{
Source: definition.Path,
Name: httputil.SVNRepoIDPathParameterName,
},
{
Source: definition.Query,
Name: httputil.SVNRevisionQueryParameterName,
},
},
Results: []definition.Result{definition.ErrorResult()},
},
},
},
}
93 changes: 93 additions & 0 deletions pkg/api/v1/handler/webhook.go
Expand Up @@ -31,6 +31,7 @@ import (
"github.com/caicloud/cyclone/pkg/scm"
contextutil "github.com/caicloud/cyclone/pkg/util/context"
gitlabuitl "github.com/caicloud/cyclone/pkg/util/gitlab"
"github.com/caicloud/cyclone/pkg/util/http/errors"
)

const (
Expand Down Expand Up @@ -427,3 +428,95 @@ func getGitHubLastCommitID(number int, pipeline *api.Pipeline) (string, error) {

return sha, nil
}

// HandleSVNHooks handles SVN post_commit hooks.
// 1. Find svn pipelines with the repoid;
// 2. Then filter svn pipelines by file path that this commit contains;
// 3. Trigger these pipelines
func HandleSVNHooks(ctx context.Context, repoid, revision string) error {
//response := webhookResponse{}

request := contextutil.GetHttpRequest(ctx)
payload, err := ioutil.ReadAll(request.Body)
if err != nil {
return errors.ErrorUnknownInternal.Error("Fail to read the request body")
}

pipelines, err := pipelineManager.FindSVNHooksPipelines(repoid)
if err != nil {
return err
}

files := getSVNChangedFiles(string(payload))

// Record the id of the pipeline that has been triggered,
// prevent from the same pipeline triggered again.
triggeredPipelines := map[string]struct{}{}
for _, pipeline := range pipelines {
url := pipeline.Build.Stages.CodeCheckout.MainRepo.SVN.Url
repoinfo := pipeline.AutoTrigger.SCMTrigger.PostCommit.RepoInfo

if pipeline.AutoTrigger == nil || pipeline.AutoTrigger.SCMTrigger == nil || pipeline.AutoTrigger.SCMTrigger.PostCommit == nil {
continue
}
pc := pipeline.AutoTrigger.SCMTrigger.PostCommit

for _, file := range files {
fullPath := repoinfo.RootURL + "/" + file
_, isAlreadyTriggered := triggeredPipelines[pipeline.ID]
// Changed file's full path contains pipeline main repo url
// and the pipeline has not been triggered
if strings.Contains(fullPath, url) && !isAlreadyTriggered {
triggeredPipelines[pipeline.ID] = struct{}{}
log.Info("SVN hooks triggered pipeline: %s, id: %s", pipeline.Name, pipeline.ID)
// Trigger the pipeline
errt := triggerSVNPipelines(pc, pipeline, revision)
if errt != nil {
log.Error("svn hook trigger pipeline failed as %v", errt)
}
}
}
}
return nil
}

// getSVNChangedFiles gets svn changed file frome message.
// eg:
// input message:`
// U cyclone/README.txt
// U cyclone/test.go
// `
// output will be: [cyclone/README.txt, cyclone/test.go]
func getSVNChangedFiles(message string) []string {
fs := []string{}
lineinfos := strings.Split(message, "\n")
for _, lineinfo := range lineinfos {
words := strings.Fields(lineinfo)
if len(words) == 2 {
fs = append(fs, words[1])
}

}

return fs
}

func triggerSVNPipelines(trigger *api.PostCommitTrigger, pipeline api.Pipeline, revision string) error {
name := pipeline.Name + "-hook-revision-" + revision
pipelineRecord := &api.PipelineRecord{
Name: name,
PipelineID: pipeline.ID,
PerformParams: &api.PipelinePerformParams{
Name: name,
Ref: api.SVNPostCommitRefPrefix + revision,
Stages: trigger.Stages,
},
Trigger: api.TriggerSVNHookPostCommit,
}
if _, err := pipelineRecordManager.CreatePipelineRecord(pipelineRecord); err != nil {
return err
}

return nil

}
21 changes: 20 additions & 1 deletion pkg/api/v1/handler/webhook_test.go
Expand Up @@ -17,11 +17,30 @@ func TestSplitStatusesURL(t *testing.T) {
for d, tc := range testCases {
ref, err := extractCommitSha(tc.url)
if err != nil {
t.Error("%s failed as error Expect error to be nil")
t.Errorf("%s failed as error %v Expect error to be nil", d, err)
}

if ref != tc.ref {
t.Errorf("%s failed as error : Expect result %s equals to %s", d, ref, tc.ref)
}
}
}

func TestGetSVNChangedFiles(t *testing.T) {
var m string

m = `svnlook: warning: cannot set LC_CTYPE locale
svnlook: warning: environment variable LC_CTYPE is UTF-8
svnlook: warning: please check that your locale name is correct
U cyclone/test.go
U cyclone/README.md
`
s := struct{}{}
expect := map[string]struct{}{"cyclone/test.go": s, "cyclone/README.md": s}
fs := getSVNChangedFiles(m)
for _, f := range fs {
if _, ok := expect[f]; !ok {
t.Errorf("%v not exist in expect map:%+v", f, expect)
}
}
}
5 changes: 5 additions & 0 deletions pkg/scm/provider/github/github.go
Expand Up @@ -30,6 +30,7 @@ import (
"github.com/caicloud/cyclone/pkg/api"
"github.com/caicloud/cyclone/pkg/scm"
"github.com/caicloud/cyclone/pkg/scm/provider"
"github.com/caicloud/cyclone/pkg/util/http/errors"
)

func init() {
Expand Down Expand Up @@ -480,3 +481,7 @@ func (g *Github) GetPullRequestSHA(repoURL string, number int) (string, error) {

return *pr.Head.SHA, nil
}

func (g *Github) RetrieveRepoInfo() (*api.RepoInfo, error) {
return nil, errors.ErrorNotImplemented.Error("retrive GitHub repo id")
}
4 changes: 4 additions & 0 deletions pkg/scm/provider/gitlab/gitlabv3.go
Expand Up @@ -239,3 +239,7 @@ func (g *GitlabV3) CreateStatus(recordStatus api.Status, targetURL, repoURL, com
func (g *GitlabV3) GetPullRequestSHA(repoURL string, number int) (string, error) {
return "", errors.ErrorNotImplemented.Error("get pull request sha")
}

func (g *GitlabV3) RetrieveRepoInfo() (*api.RepoInfo, error) {
return nil, errors.ErrorNotImplemented.Error("retrive GitLab repo id")
}
4 changes: 4 additions & 0 deletions pkg/scm/provider/gitlab/gitlabv4.go
Expand Up @@ -254,3 +254,7 @@ func (g *GitlabV4) CreateStatus(recordStatus api.Status, targetURL, repoURL, com
func (g *GitlabV4) GetPullRequestSHA(repoURL string, number int) (string, error) {
return "", errors.ErrorNotImplemented.Error("get pull request sha")
}

func (g *GitlabV4) RetrieveRepoInfo() (*api.RepoInfo, error) {
return nil, errors.ErrorNotImplemented.Error("retrive GitLab repo id")
}

0 comments on commit 00b8ed4

Please sign in to comment.