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