Initial commit
This commit is contained in:
		
							
								
								
									
										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
 | 
			
		||||
}
 | 
			
		||||
		Reference in New Issue
	
	Block a user