reestructuración masiva + gitea-ci
This commit is contained in:
parent
07d276b2a3
commit
c575551d2e
14 changed files with 677 additions and 24 deletions
2
cli.go
2
cli.go
|
@ -11,7 +11,7 @@ import (
|
|||
func main() {
|
||||
config, err := readConfig()
|
||||
must(err)
|
||||
must(runner.Run(config, "rootfs/", "cache/"))
|
||||
must(runner.Run(config, "rootfs/", "cache/", ".", nil))
|
||||
}
|
||||
|
||||
func must(err error, where ...string) {
|
||||
|
|
56
gitea-ci/gitea/commit_status.go
Normal file
56
gitea-ci/gitea/commit_status.go
Normal file
|
@ -0,0 +1,56 @@
|
|||
package gitea
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"path"
|
||||
)
|
||||
|
||||
type CommitStatus struct {
|
||||
Context string `json:"context"`
|
||||
Description string `json:"description"`
|
||||
// State holds the state of a CommitStatus
|
||||
// It can be "pending", "success", "error", "failure", and "warning"
|
||||
State string `json:"state"`
|
||||
TargetUrl string `json:"target_url"`
|
||||
}
|
||||
|
||||
func (g Gitea) CreateCommitStatus(repo, commit string, status CommitStatus) (err error) {
|
||||
ur, err := url.Parse(g.Url)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
ur.Path = path.Join("/api/v1/repos/", repo, "/statuses/", commit)
|
||||
|
||||
payload, err := json.Marshal(status)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
req, err := http.NewRequest("POST", ur.String(), bytes.NewReader(payload))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
g.setAuth(req)
|
||||
|
||||
res, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if res.StatusCode != http.StatusCreated {
|
||||
byt, err := io.ReadAll(res.Body)
|
||||
log.Printf("%s %v", byt, err)
|
||||
return errors.New("not 200")
|
||||
}
|
||||
|
||||
// err = json.NewDecoder(res.Body).Decode(&u)
|
||||
return
|
||||
}
|
52
gitea-ci/gitea/gitea.go
Normal file
52
gitea-ci/gitea/gitea.go
Normal file
|
@ -0,0 +1,52 @@
|
|||
package gitea
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/url"
|
||||
)
|
||||
|
||||
type User struct {
|
||||
FullName string `json:"full_name"`
|
||||
Login string `json:"login"`
|
||||
}
|
||||
|
||||
type Gitea struct {
|
||||
Url string
|
||||
Username string
|
||||
Password string
|
||||
}
|
||||
|
||||
func (g Gitea) setAuth(req *http.Request) {
|
||||
req.Header.Set("Authorization", "Basic "+base64.StdEncoding.EncodeToString([]byte(g.Username+":"+g.Password)))
|
||||
|
||||
}
|
||||
|
||||
func (g Gitea) GetUser() (u User, err error) {
|
||||
ur, err := url.Parse(g.Url)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
ur.Path = "/api/v1/user"
|
||||
req, err := http.NewRequest("GET", ur.String(), nil)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
g.setAuth(req)
|
||||
res, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if res.StatusCode != http.StatusOK {
|
||||
byt, err := io.ReadAll(res.Body)
|
||||
log.Printf("%s %v", byt, err)
|
||||
return u, errors.New("not 200")
|
||||
}
|
||||
|
||||
err = json.NewDecoder(res.Body).Decode(&u)
|
||||
return
|
||||
}
|
15
gitea-ci/go.mod
Normal file
15
gitea-ci/go.mod
Normal file
|
@ -0,0 +1,15 @@
|
|||
module gitea.nulo.in/Nulo/repro-run/gitea-ci
|
||||
|
||||
go 1.19
|
||||
|
||||
require (
|
||||
gitea.nulo.in/Nulo/repro-run v0.0.0-20230122131519-07d276b2a3b9
|
||||
golang.org/x/exp v0.0.0-20230118134722-a68e582fa157
|
||||
nhooyr.io/websocket v1.8.7
|
||||
)
|
||||
|
||||
require github.com/klauspost/compress v1.10.3 // indirect
|
||||
|
||||
replace gitea.nulo.in/Nulo/repro-run/gitea-ci/gitea => ./gitea
|
||||
|
||||
replace gitea.nulo.in/Nulo/repro-run => ../
|
63
gitea-ci/go.sum
Normal file
63
gitea-ci/go.sum
Normal file
|
@ -0,0 +1,63 @@
|
|||
gitea.nulo.in/Nulo/repro-run v0.0.0-20230122131519-07d276b2a3b9 h1:gtm7Z68e/LBFza03lIWZmN8oD/ZGSlkSPdeBlJeSdV0=
|
||||
gitea.nulo.in/Nulo/repro-run v0.0.0-20230122131519-07d276b2a3b9/go.mod h1:i3bNu9aTQZh6vFjPP+LzC46o9c+Ggohivw2VA8voVgY=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
|
||||
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
|
||||
github.com/gin-gonic/gin v1.6.3 h1:ahKqKTFpO5KTPHxWZjEdPScmYaGtLo8Y4DMHoEsnp14=
|
||||
github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M=
|
||||
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||
github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q=
|
||||
github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=
|
||||
github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no=
|
||||
github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA=
|
||||
github.com/go-playground/validator/v10 v10.2.0 h1:KgJ0snyC2R9VXYN2rneOtQcw5aHQB1Vv0sFl1UcHBOY=
|
||||
github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI=
|
||||
github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee h1:s+21KNqlpePfkah2I+gwHF8xmJWRjooY+5248k6m4A0=
|
||||
github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo=
|
||||
github.com/gobwas/pool v0.2.0 h1:QEmUOlnSjWtnpRGHF3SauEiOsy82Cup83Vf2LcMlnc8=
|
||||
github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
|
||||
github.com/gobwas/ws v1.0.2 h1:CoAavW/wd/kulfZmSIBt6p24n4j7tHgNVCjsfHVNUbo=
|
||||
github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM=
|
||||
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
|
||||
github.com/golang/protobuf v1.3.5 h1:F768QJ1E9tib+q5Sc8MkdJi1RxLTbRcTf8LJV56aRls=
|
||||
github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
|
||||
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/gorilla/websocket v1.4.1 h1:q7AeDBpnBk8AogcD4DSag/Ukw/KV+YhzLj2bP5HvKCM=
|
||||
github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/json-iterator/go v1.1.9 h1:9yzud/Ht36ygwatGx56VwCZtlI/2AD15T1X2sjSuGns=
|
||||
github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||
github.com/klauspost/compress v1.10.3 h1:OP96hzwJVBIHYU52pVTI6CczrxPvrGfgqF9N5eTO0Q8=
|
||||
github.com/klauspost/compress v1.10.3/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
|
||||
github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y=
|
||||
github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
|
||||
github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
|
||||
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 h1:Esafd1046DLDQ0W1YjYsBW+p8U2u7vzgW2SQVmlNazg=
|
||||
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo=
|
||||
github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
|
||||
github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs=
|
||||
github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
|
||||
golang.org/x/exp v0.0.0-20230118134722-a68e582fa157 h1:fiNkyhJPUvxbRPbCqY/D9qdjmPzfHcpK3P4bM4gioSY=
|
||||
golang.org/x/exp v0.0.0-20230118134722-a68e582fa157/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc=
|
||||
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.1.0 h1:kunALQeHf1/185U1i0GOB/fy1IPRDDpuoOOqRReG57U=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
|
||||
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
nhooyr.io/websocket v1.8.7 h1:usjR2uOr/zjjkVMy0lW+PPohFok7PCow5sDjLgX4P4g=
|
||||
nhooyr.io/websocket v1.8.7/go.mod h1:B70DZP8IakI65RVQ51MsWP/8jndNma26DVA/nFSCgW0=
|
12
gitea-ci/index.html
Normal file
12
gitea-ci/index.html
Normal file
|
@ -0,0 +1,12 @@
|
|||
<!DOCTYPE html>
|
||||
<meta charset="utf-8" />
|
||||
<pre></pre>
|
||||
<script>
|
||||
const el = document.querySelector("pre");
|
||||
const id = location.hash.slice(1);
|
||||
const ws = new WebSocket("ws://localhost:8080/logs/socket/" + id);
|
||||
window.ws = ws;
|
||||
ws.addEventListener("message", async (event) => {
|
||||
el.append(await event.data.text());
|
||||
});
|
||||
</script>
|
58
gitea-ci/log.go
Normal file
58
gitea-ci/log.go
Normal file
|
@ -0,0 +1,58 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"nhooyr.io/websocket"
|
||||
)
|
||||
|
||||
type logWebsocket struct {
|
||||
}
|
||||
|
||||
func (l logWebsocket) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
parts := strings.Split(r.URL.Path[1:], "/")
|
||||
run := runss.getRun(parts[2])
|
||||
if run == nil {
|
||||
http.Error(w, "log not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
c, err := websocket.Accept(w, r, nil)
|
||||
if err != nil {
|
||||
http.Error(w, "this a websocket path", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
defer c.Close(websocket.StatusInternalError, "the sky is falling")
|
||||
|
||||
ch := make(chan *[]byte)
|
||||
err = run.addWriter(conn{ch: ch})
|
||||
if err != nil {
|
||||
log.Println("addWriter", err)
|
||||
http.Error(w, "error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
for payload := range ch {
|
||||
if payload == nil {
|
||||
break
|
||||
} else {
|
||||
c.Write(context.Background(), websocket.MessageBinary, *payload)
|
||||
}
|
||||
}
|
||||
c.Close(websocket.StatusNormalClosure, "")
|
||||
}
|
||||
|
||||
type conn struct {
|
||||
ch chan *[]byte
|
||||
}
|
||||
|
||||
func (c conn) Write(payload []byte) (n int, err error) {
|
||||
c.ch <- &payload
|
||||
return len(payload), nil
|
||||
}
|
||||
func (c conn) Close() error {
|
||||
c.ch <- nil
|
||||
return nil
|
||||
}
|
63
gitea-ci/main.go
Normal file
63
gitea-ci/main.go
Normal file
|
@ -0,0 +1,63 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"gitea.nulo.in/Nulo/repro-run/gitea-ci/gitea"
|
||||
)
|
||||
|
||||
func main() {
|
||||
config := parseConfig()
|
||||
|
||||
g := gitea.Gitea{
|
||||
Url: config.giteaUrl,
|
||||
Username: config.giteaUsername,
|
||||
Password: config.giteaPassword,
|
||||
}
|
||||
|
||||
u, err := g.GetUser()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
log.Printf("Logged in as @%s", u.Login)
|
||||
|
||||
http.Handle("/webhook", webhook{config: config, gitea: g})
|
||||
http.Handle("/logs/socket/", logWebsocket{})
|
||||
http.Handle("/", http.FileServer(http.Dir(".")))
|
||||
log.Fatal(http.ListenAndServe(":8080", nil))
|
||||
}
|
||||
|
||||
type config struct {
|
||||
allowedUsers []string
|
||||
giteaWebhookSecret string
|
||||
giteaUrl string
|
||||
giteaUsername string
|
||||
giteaPassword string
|
||||
}
|
||||
|
||||
func parseConfig() (c config) {
|
||||
var allowedUsersStr string
|
||||
if allowedUsersStr = os.Getenv("ALLOWED_REPOS"); len(allowedUsersStr) == 0 {
|
||||
log.Fatal("ALLOWED_REPOS is nil")
|
||||
}
|
||||
c.allowedUsers = strings.Split(allowedUsersStr, ",")
|
||||
|
||||
if c.giteaWebhookSecret = os.Getenv("GITEA_WEBHOOK_SECRET"); len(c.giteaWebhookSecret) == 0 {
|
||||
log.Fatal("GITEA_WEBHOOK_SECRET is nil")
|
||||
}
|
||||
|
||||
if c.giteaUrl = os.Getenv("GITEA_URL"); len(c.giteaUrl) == 0 {
|
||||
log.Fatal("GITEA_URL is nil")
|
||||
}
|
||||
if c.giteaUsername = os.Getenv("GITEA_USERNAME"); len(c.giteaUsername) == 0 {
|
||||
log.Fatal("GITEA_USERNAME is nil")
|
||||
}
|
||||
if c.giteaPassword = os.Getenv("GITEA_PASSWORD"); len(c.giteaPassword) == 0 {
|
||||
log.Fatal("GITEA_PASSWORD is nil")
|
||||
}
|
||||
|
||||
return
|
||||
}
|
280
gitea-ci/webhook.go
Normal file
280
gitea-ci/webhook.go
Normal file
|
@ -0,0 +1,280 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"crypto/hmac"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path"
|
||||
"sync"
|
||||
|
||||
"gitea.nulo.in/Nulo/repro-run/gitea-ci/gitea"
|
||||
"gitea.nulo.in/Nulo/repro-run/runner"
|
||||
"golang.org/x/exp/slices"
|
||||
)
|
||||
|
||||
type run struct {
|
||||
mutex sync.Mutex
|
||||
previous []byte
|
||||
writers []io.WriteCloser
|
||||
}
|
||||
|
||||
func (r *run) Write(p []byte) (n int, err error) {
|
||||
log.Println(string(p))
|
||||
r.mutex.Lock()
|
||||
defer r.mutex.Unlock()
|
||||
r.previous = append(r.previous, p...)
|
||||
|
||||
var writers []io.Writer
|
||||
for _, w := range r.writers {
|
||||
writers = append(writers, w)
|
||||
}
|
||||
|
||||
n, err = io.MultiWriter(writers...).Write(slices.Clone(p))
|
||||
return len(p), nil
|
||||
}
|
||||
func (r *run) addWriter(w io.WriteCloser) (err error) {
|
||||
r.mutex.Lock()
|
||||
defer r.mutex.Unlock()
|
||||
|
||||
go w.Write(slices.Clone(r.previous))
|
||||
r.writers = append(r.writers, w)
|
||||
return
|
||||
}
|
||||
func (r *run) finishRun() {
|
||||
r.mutex.Lock()
|
||||
defer r.mutex.Unlock()
|
||||
for _, c := range r.writers {
|
||||
c.Close()
|
||||
}
|
||||
}
|
||||
|
||||
type runs struct {
|
||||
things map[string]*run
|
||||
mutex sync.Mutex
|
||||
}
|
||||
|
||||
func (r *runs) newRun(id string) *run {
|
||||
r.mutex.Lock()
|
||||
defer r.mutex.Unlock()
|
||||
|
||||
_, found := r.things[id]
|
||||
if found {
|
||||
log.Fatalf("run %s already exists", id)
|
||||
}
|
||||
|
||||
rr := &run{}
|
||||
r.things[id] = rr
|
||||
return rr
|
||||
}
|
||||
func (r *runs) getRun(id string) *run {
|
||||
r.mutex.Lock()
|
||||
defer r.mutex.Unlock()
|
||||
return r.things[id]
|
||||
}
|
||||
|
||||
var runss = runs{
|
||||
things: make(map[string]*run),
|
||||
}
|
||||
|
||||
var wgs map[string]*sync.WaitGroup = make(map[string]*sync.WaitGroup)
|
||||
var wgsMutex sync.Mutex
|
||||
|
||||
type webhook struct {
|
||||
config config
|
||||
gitea gitea.Gitea
|
||||
}
|
||||
|
||||
func (h webhook) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
||||
if req.Method != "POST" {
|
||||
http.Error(w, "not POST", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
payload, err := io.ReadAll(req.Body)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// https://github.com/go-gitea/gitea/blob/0f4e1b9ac66b8ffa0083a5a2516e4710393bb0da/services/webhook/deliver.go#L112
|
||||
sig256 := hmac.New(sha256.New, []byte(h.config.giteaWebhookSecret))
|
||||
_, err = sig256.Write(payload)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
http.Error(w, "error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
sig := hex.EncodeToString(sig256.Sum(nil))
|
||||
if sig != req.Header.Get("X-Gitea-Signature") {
|
||||
http.Error(w, "HMAC doesn't match", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
event := req.Header.Get("X-Gitea-Event")
|
||||
if event != "push" {
|
||||
log.Printf("event not push")
|
||||
return
|
||||
}
|
||||
|
||||
var hook recWebhook
|
||||
err = json.Unmarshal(payload, &hook)
|
||||
if err != nil {
|
||||
http.Error(w, "invalid JSON", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if !slices.Contains(h.config.allowedUsers, hook.Repository.Owner.Username) {
|
||||
log.Printf("username %s not allowed", hook.Repository.Owner.Username)
|
||||
return
|
||||
}
|
||||
|
||||
var runnerConfig *runner.Config
|
||||
if runnerConfig = h.getConfig(hook, w); runnerConfig == nil {
|
||||
return
|
||||
}
|
||||
|
||||
runId := req.Header.Get("X-Gitea-Delivery")
|
||||
// TODO: make dynamic
|
||||
runUrl := "http://localhost:8080/#" + runId
|
||||
log.Printf("New run: %s", runUrl)
|
||||
run := runss.newRun(runId)
|
||||
defer run.finishRun()
|
||||
|
||||
wgsMutex.Lock()
|
||||
wg := wgs[hook.Repository.FullName]
|
||||
if wg == nil {
|
||||
wg = &sync.WaitGroup{}
|
||||
wgs[hook.Repository.FullName] = wg
|
||||
}
|
||||
wgsMutex.Unlock()
|
||||
wg.Wait()
|
||||
wg.Add(1)
|
||||
defer wg.Done()
|
||||
|
||||
dir, err := os.MkdirTemp("", "repro-run-gitea-ci-")
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
http.Error(w, "error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
defer os.RemoveAll(dir)
|
||||
|
||||
cache := path.Join("cache/", hook.Repository.FullName)
|
||||
if err = os.MkdirAll(cache, 0700); err != nil {
|
||||
log.Println(err)
|
||||
http.Error(w, "error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
rootfs := path.Join(dir, "rootfs")
|
||||
src := path.Join(dir, "src")
|
||||
if err = os.MkdirAll(rootfs, 0700); err != nil {
|
||||
log.Println(err)
|
||||
http.Error(w, "error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
if err = os.MkdirAll(src, 0700); err != nil {
|
||||
log.Println(err)
|
||||
http.Error(w, "error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
if err = os.MkdirAll(src, 0700); err != nil {
|
||||
log.Println(err)
|
||||
http.Error(w, "error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
var cmd *exec.Cmd
|
||||
if _, err = os.ReadDir(path.Join(src, ".git")); os.IsExist(err) {
|
||||
cmd = exec.Command("git", "pull")
|
||||
cmd.Dir = src
|
||||
} else {
|
||||
cmd = exec.Command("git", "clone", hook.Repository.CloneUrl, src)
|
||||
}
|
||||
if err = cmd.Run(); err != nil {
|
||||
log.Println(err)
|
||||
http.Error(w, "error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
err = h.gitea.CreateCommitStatus(hook.Repository.FullName, hook.AfterCommit, gitea.CommitStatus{
|
||||
Context: "repro-run",
|
||||
Description: "Corre repro-run.json",
|
||||
State: "pending",
|
||||
TargetUrl: runUrl,
|
||||
})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
err = runner.Run(*runnerConfig, rootfs, cache, src, run)
|
||||
state := "success"
|
||||
if err != nil {
|
||||
log.Println("runner.Run", err)
|
||||
state = "failure"
|
||||
}
|
||||
err = h.gitea.CreateCommitStatus(hook.Repository.FullName, hook.AfterCommit, gitea.CommitStatus{
|
||||
Context: "repro-run",
|
||||
Description: "Corre repro-run.json",
|
||||
State: state,
|
||||
TargetUrl: runUrl,
|
||||
})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func (h webhook) getConfig(hook recWebhook, w http.ResponseWriter) (c *runner.Config) {
|
||||
u, err := url.Parse(h.config.giteaUrl)
|
||||
if err != nil {
|
||||
log.Println("parsing gitea_url", err)
|
||||
http.Error(w, "error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
u.Path = path.Join("/api/v1/repos", hook.Repository.Owner.Username, hook.Repository.Name, "raw", "repro-run.json")
|
||||
q := u.Query()
|
||||
q.Add("ref", hook.AfterCommit)
|
||||
u.RawQuery = q.Encode()
|
||||
|
||||
res, err := http.Get(u.String())
|
||||
if err != nil {
|
||||
log.Println("getting repro-run.json", err)
|
||||
http.Error(w, "error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
if res.StatusCode == http.StatusNotFound {
|
||||
http.Error(w, "no repro-run.json", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if err = json.NewDecoder(res.Body).Decode(&c); err != nil {
|
||||
log.Println(err)
|
||||
http.Error(w, "invalid repro-run.json", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
type recWebhook struct {
|
||||
Repository recRepository `json:"repository"`
|
||||
AfterCommit string `json:"after"`
|
||||
}
|
||||
|
||||
type recRepository struct {
|
||||
Id uint64 `json:"id"`
|
||||
Owner recUser `json:"owner"`
|
||||
CloneUrl string `json:"clone_url"`
|
||||
FullName string `json:"full_name"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
type recUser struct {
|
||||
Username string `json:"username"`
|
||||
}
|
7
go.mod
7
go.mod
|
@ -1,3 +1,10 @@
|
|||
module gitea.nulo.in/Nulo/repro-run
|
||||
|
||||
go 1.19
|
||||
|
||||
require golang.org/x/exp v0.0.0-20230118134722-a68e582fa157
|
||||
|
||||
require (
|
||||
github.com/klauspost/compress v1.10.3 // indirect
|
||||
nhooyr.io/websocket v1.8.7 // indirect
|
||||
)
|
||||
|
|
41
go.sum
Normal file
41
go.sum
Normal file
|
@ -0,0 +1,41 @@
|
|||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
|
||||
github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M=
|
||||
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||
github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=
|
||||
github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA=
|
||||
github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI=
|
||||
github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo=
|
||||
github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
|
||||
github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM=
|
||||
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
|
||||
github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
|
||||
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||
github.com/klauspost/compress v1.10.3 h1:OP96hzwJVBIHYU52pVTI6CczrxPvrGfgqF9N5eTO0Q8=
|
||||
github.com/klauspost/compress v1.10.3/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
|
||||
github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
|
||||
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
|
||||
github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
|
||||
golang.org/x/exp v0.0.0-20230118134722-a68e582fa157 h1:fiNkyhJPUvxbRPbCqY/D9qdjmPzfHcpK3P4bM4gioSY=
|
||||
golang.org/x/exp v0.0.0-20230118134722-a68e582fa157/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc=
|
||||
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
nhooyr.io/websocket v1.8.7 h1:usjR2uOr/zjjkVMy0lW+PPohFok7PCow5sDjLgX4P4g=
|
||||
nhooyr.io/websocket v1.8.7/go.mod h1:B70DZP8IakI65RVQ51MsWP/8jndNma26DVA/nFSCgW0=
|
3
go.work
Normal file
3
go.work
Normal file
|
@ -0,0 +1,3 @@
|
|||
go 1.19
|
||||
|
||||
use ./gitea-ci
|
|
@ -1,6 +0,0 @@
|
|||
package runner
|
||||
|
||||
type Config struct {
|
||||
Command string
|
||||
Cache []string
|
||||
}
|
|
@ -14,21 +14,21 @@ import (
|
|||
//go:embed alpine/keys
|
||||
var alpineKeys embed.FS
|
||||
|
||||
func Run(config Config, rootfs string, cache string) (err error) {
|
||||
if err = os.RemoveAll("rootfs/"); err != nil {
|
||||
func Run(config Config, rootfs string, cache string, src string, w io.Writer) (err error) {
|
||||
if err = os.RemoveAll(rootfs); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if err = os.MkdirAll("rootfs/etc/apk/keys", 0700); err != nil {
|
||||
if err = os.MkdirAll(path.Join(rootfs, "etc/apk/keys"), 0700); err != nil {
|
||||
return
|
||||
}
|
||||
if err = os.WriteFile("rootfs/etc/apk/repositories", []byte(
|
||||
if err = os.WriteFile(path.Join(rootfs, "etc/apk/repositories"), []byte(
|
||||
`https://dl-cdn.alpinelinux.org/alpine/v3.17/main
|
||||
https://dl-cdn.alpinelinux.org/alpine/v3.17/community`), 0600); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if err = os.WriteFile("rootfs/etc/resolv.conf", []byte(`nameserver 8.8.8.8`), 0644); err != nil {
|
||||
if err = os.WriteFile(path.Join(rootfs, "etc/resolv.conf"), []byte(`nameserver 8.8.8.8`), 0644); err != nil {
|
||||
return
|
||||
}
|
||||
keys, err := alpineKeys.ReadDir("alpine/keys")
|
||||
|
@ -36,7 +36,7 @@ https://dl-cdn.alpinelinux.org/alpine/v3.17/community`), 0600); err != nil {
|
|||
return
|
||||
}
|
||||
for _, entry := range keys {
|
||||
o, err := os.Create(path.Join("rootfs/etc/apk/keys/", entry.Name()))
|
||||
o, err := os.Create(path.Join(rootfs, "etc/apk/keys/", entry.Name()))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -49,17 +49,16 @@ https://dl-cdn.alpinelinux.org/alpine/v3.17/community`), 0600); err != nil {
|
|||
}
|
||||
}
|
||||
|
||||
if err = os.MkdirAll("cache/_var_cache_apk", 0700); err != nil {
|
||||
if err = os.MkdirAll(path.Join(cache, "_var_cache_apk"), 0700); err != nil {
|
||||
return
|
||||
}
|
||||
os.Remove("rootfs/etc/apk/cache")
|
||||
if err = os.Symlink("/var/cache/apk", "rootfs/etc/apk/cache"); err != nil {
|
||||
if err = os.Symlink("/var/cache/apk", path.Join(rootfs, "etc/apk/cache")); err != nil {
|
||||
return
|
||||
}
|
||||
cmd := exec.Command("apk", "add",
|
||||
"--root", "rootfs",
|
||||
"--root", rootfs,
|
||||
"--initdb",
|
||||
"--cache-dir", "../cache/_var_cache_apk",
|
||||
"--cache-dir", path.Join(cache, "_var_cache_apk"),
|
||||
"apk-tools")
|
||||
if err = cmd.Run(); err != nil {
|
||||
return fmt.Errorf("apk add apk-tools: %w", err)
|
||||
|
@ -68,7 +67,7 @@ https://dl-cdn.alpinelinux.org/alpine/v3.17/community`), 0600); err != nil {
|
|||
cached := append(config.Cache, "/var/cache/apk")
|
||||
var cachedParams []string
|
||||
for _, c := range cached {
|
||||
cacheDir := path.Join("./cache", strings.ReplaceAll(c, "/", "_"))
|
||||
cacheDir := path.Join(cache, strings.ReplaceAll(c, "/", "_"))
|
||||
if err = os.MkdirAll(cacheDir, 0700); err != nil {
|
||||
return
|
||||
}
|
||||
|
@ -83,8 +82,8 @@ https://dl-cdn.alpinelinux.org/alpine/v3.17/community`), 0600); err != nil {
|
|||
log.Println("/work = " + tmp)
|
||||
|
||||
params := append(
|
||||
[]string{"--bind", "./rootfs", "/",
|
||||
"--ro-bind", ".", "/src",
|
||||
[]string{"--bind", rootfs, "/",
|
||||
"--ro-bind", src, "/src",
|
||||
"--dev-bind", "/dev", "/dev",
|
||||
"--bind", tmp, "/work",
|
||||
"--unshare-user", "--uid", "1000", "--gid", "1000",
|
||||
|
@ -108,12 +107,22 @@ https://dl-cdn.alpinelinux.org/alpine/v3.17/community`), 0600); err != nil {
|
|||
}
|
||||
|
||||
cmd = exec.Command("bwrap", append(params, strings.Split(config.Command, " ")...)...)
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stdin = os.Stdin
|
||||
cmd.Stderr = os.Stderr
|
||||
// cmd.Stdin = os.Stdin
|
||||
if w == nil {
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
} else {
|
||||
cmd.Stdout = w
|
||||
cmd.Stderr = w
|
||||
}
|
||||
if err = cmd.Run(); err != nil {
|
||||
return fmt.Errorf("running Command: %w", err)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
Command string
|
||||
Cache []string
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue