Initial commit

This commit is contained in:
Přemysl Eric Janouch 2017-12-02 22:07:25 +01:00
commit a942e23b39
Signed by: p
GPG Key ID: B715679E3A361BE6
4 changed files with 295 additions and 0 deletions

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/gitea-obs

13
LICENSE Normal file
View File

@ -0,0 +1,13 @@
Copyright (c) 2017, Přemysl Janouch <p.janouch@gmail.com>
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION
OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.

49
README.adoc Normal file
View File

@ -0,0 +1,49 @@
gitea-obs
=========
'gitea-obs' is a webhook endpoint to trigger the services in Open Build Service,
meant as a replacement for the Obs service on GitHub.
Unfortunately someone thought I wasn't being funny with my project names and
blocked my OBS account before I could test it. Therefore it comes as is.
Usage
-----
$ go build
$ mkdir gitea-obs-workdir
$ cd gitea-obs-workdir
$ ../gitea-obs :3000 secret
Then, in Gitea, assuming it's running on the same machine as the program,
use a 'Payload URL' like the following:
http://localhost:3000/?token=TOKEN
Optional arguments:
* 'project' is the OBS project name (when not implied by the token itself)
* 'package' is the OBS package name (when not implied by the token itself)
* 'refs' is a colon-separated list of Go filepath patterns to filter branches,
for example 'refs/heads/*'; defaults to 'refs/heads/master'
* 'obs' specifies a different URL for the OBS instance
The program uses the current working directory as a dispatch queue, and it must
be run in a dedicated directory.
Contributing and Support
------------------------
Use https://git.janouch.name/p/gitea-obs to report any bugs, request features,
or submit pull requests. If you want to discuss this project, or maybe just
hang out with the developer, feel free to join me at irc://irc.janouch.name,
channel #dev.
License
-------
'gitea-obs' is written by Přemysl Janouch <p.janouch@gmail.com>.
You may use the software under the terms of the ISC license, the text of which
is included within the package, or, at your option, you may relicense the work
under the MIT or the Modified BSD License, as listed at the following site:
http://www.gnu.org/licenses/license-list.html

232
main.go Normal file
View File

@ -0,0 +1,232 @@
// Open Build System Gitea trigger webhook
package main
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io/ioutil"
"log"
"net/http"
"net/url"
"os"
"os/signal"
"path"
"path/filepath"
"strings"
"syscall"
"time"
)
var programName = path.Base(os.Args[0])
var secretToken = ""
var apiClient *http.Client = &http.Client{Timeout: 60 * time.Second}
// We're on a relatively short timeout (10s), just queue the events for later
func handler(w http.ResponseWriter, r *http.Request) {
data, err := ioutil.ReadAll(r.Body)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
return
}
// Presence of the header marks the format of hook data
event := r.Header.Get("X-Gitea-Event")
if event == "" {
w.WriteHeader(http.StatusBadRequest)
return
}
// React to new commits being added
if event != "push" {
return
}
buf := new(bytes.Buffer)
if err := json.Indent(buf, data, "", " "); err != nil {
log.Printf("req: %s", err)
w.WriteHeader(http.StatusBadRequest)
return
}
// Mark our files as such, use the filesystem as a simple database
base := programName + "&" + r.URL.RawQuery
if err := func() error {
f, err := os.OpenFile(base+".tmp",
os.O_WRONLY|os.O_CREATE|os.O_TRUNC|os.O_SYNC, 0644)
if err != nil {
return err
}
if _, err := f.Write(buf.Bytes()); err != nil {
f.Close()
return err
}
if err := f.Close(); err != nil {
return err
}
// Ready for processing -- needs to be the last step here
if err := os.Rename(base+".tmp", base); err != nil {
return err
}
return nil
}(); err != nil {
log.Printf("req: %s", err)
w.WriteHeader(http.StatusInternalServerError)
}
}
func process(ctx context.Context, values url.Values, data []byte) error {
pkg := values.Get("package")
project := values.Get("project")
token := values.Get("token")
if token == "" {
return fmt.Errorf("no token given")
}
obs := "https://build.opensuse.org"
if v := values.Get("obs"); v != "" {
obs = v
}
refs := "refs/heads/master"
if v := values.Get("refs"); v != "" {
refs = v
}
// No need for the whole "code.gitea.io/sdk/gitea" package so far
push := struct {
Secret string `json:"secret"`
Ref string `json:"ref"`
}{}
if err := json.Unmarshal(data, &push); err != nil {
return err
}
if push.Secret != secretToken {
return fmt.Errorf("invalid secret: %s", push.Secret)
}
// This roughly matches the behaviour of the GitHub service
matches := false
for _, ref := range strings.Split(refs, ":") {
if m, err := filepath.Match(ref, push.Ref); m {
matches = true
break
} else if err != nil {
return fmt.Errorf("%s: %s", ref, err)
}
}
// Trigger the OBS source service to make it rebuild the package
if matches {
uri := obs + "/trigger/runservice"
if pkg != "" && project != "" {
escape := url.QueryEscape
uri += "?package=" + escape(pkg) + "&project=" + escape(project)
}
req, err := http.NewRequest("POST", uri, nil)
if err != nil {
return err
}
req.Header.Set("Authorization", "Token "+token)
resp, err := apiClient.Do(req.WithContext(ctx))
if err != nil {
return err
}
defer resp.Body.Close()
}
return nil
}
func consume(ctx context.Context) {
list, err := ioutil.ReadDir(".")
if err != nil {
log.Fatalln(err)
}
for _, info := range list {
base := info.Name()
if strings.HasSuffix(base, ".tmp") || !info.Mode().IsRegular() {
continue
}
data, err := ioutil.ReadFile(base)
if err != nil {
log.Println(err)
continue
}
values, err := url.ParseQuery(base)
if err != nil {
log.Printf("%s: %s\n", base, err)
continue
}
if _, ok := values[programName]; !ok {
continue
}
if err := process(ctx, values, data); err != nil {
log.Printf("%s: %s\n", base, err)
continue
}
// XXX minor race condition
if err := os.Remove(base); err != nil {
log.Println(err)
continue
}
}
}
func main() {
if len(os.Args) < 2 || len(os.Args) > 3 {
fmt.Fprintf(os.Stderr, "Usage: %s LISTEN-ADDR [SECRET]\n", os.Args[0])
os.Exit(1)
}
listenAddr := os.Args[1]
if len(os.Args) == 3 {
secretToken = os.Args[2]
}
http.HandleFunc("/", handler)
server := &http.Server{Addr: listenAddr}
// Run the producer
go func() {
if err := server.ListenAndServe(); err != nil &&
err != http.ErrServerClosed {
log.Fatalln(err)
}
}()
// Run the consumer
ctx, ctxCancel := context.WithCancel(context.Background())
consumerFinished := make(chan struct{})
// Process the currently available batch and retry after a few seconds
go func() {
defer close(consumerFinished)
for {
consume(ctx)
select {
case <-time.After(3 * time.Second):
case <-ctx.Done():
return
}
}
}()
// Wait for a termination signal
sigs := make(chan os.Signal, 1)
signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
<-sigs
// Stop the producer gracefully
ctxSd, ctxSdCancel := context.WithTimeout(context.Background(), 5)
if err := server.Shutdown(ctxSd); err != nil {
log.Println(err)
}
ctxSdCancel()
// Stop the consumer gracefully
ctxCancel()
<-consumerFinished
}