mirror of
https://git.janouch.name/p/gitea-obs
synced 2024-11-22 21:11:58 +01:00
Initial commit
This commit is contained in:
commit
a942e23b39
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
/gitea-obs
|
13
LICENSE
Normal file
13
LICENSE
Normal 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
49
README.adoc
Normal 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
232
main.go
Normal 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
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user