Skip to content

Commit 4032212

Browse files
committed
Initial web inspection endpoints
1 parent 80e414a commit 4032212

File tree

15 files changed

+248
-66
lines changed

15 files changed

+248
-66
lines changed

.vscode/launch.json

+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
{
2+
// Use IntelliSense to learn about possible attributes.
3+
// Hover to view descriptions of existing attributes.
4+
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
5+
"version": "0.2.0",
6+
"configurations": [
7+
{
8+
"name": "Launch Package",
9+
"type": "go",
10+
"request": "launch",
11+
"mode": "auto",
12+
"program": "."
13+
}
14+
]
15+
}

README.md

+3-3
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# git webhook receiver
22

3-
Small service for listening for the incoming webhhok HTTP-post from a git
3+
Small service for listening for the incoming webhook HTTP-posts from a git
44
provider (gitea, github, gitlab) for one or many projects and running
55
a script/program on a matching webhook event. Supports request authorization.
66

@@ -17,8 +17,8 @@ In a nutshell:
1717
corresponding build scripts/actions (either as a standalone script
1818
or [crossplatform inline scripts](#inline-scripts-and-standalone-scripts))
1919
- set webhooks for those repo in their git services (github, gitea, gitlab,
20-
etc.) to post to {YOUR_HOST}/{PROJECT_NAME}
21-
- start the service, it will listen for the webhook posts and runs the actions
20+
etc.) to post to {YOUR_HOST}/projects/{PROJECT_NAME}
21+
- start the service, it will listen for the webhook posts and run the actions
2222
you described in the config (building your projects or whatever) when those
2323
webhooks are fired from git
2424

config.example.yml

+1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
host: example.com # defaults to localhost
33
port: 9090 # defaults to 9090
44
log_level: "debug" # defaults to 'info', other options are 'warn' and 'error'
5+
web_admin: true # Enable web-interface to retrieve pipelines and logs
56
# SSL config: (optional, but recommended, unless you're using a reverse proxy)
67
ssl:
78
cert_file_path: "./your/certfile/path/fullchain.pem"

internal/actionDb/ActionDb.go

+13-33
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,7 @@ func (d ActionDb) GetPipelineRecord(pipeId string) (PipeLineRecord, error) {
113113
type PipeStatus int
114114

115115
const (
116+
PipeStatusAny PipeStatus = 0
116117
PipeStatusOk PipeStatus = 1
117118
PipeStatusError PipeStatus = 2
118119
PipeStatusPending PipeStatus = 3
@@ -178,38 +179,17 @@ FROM
178179
return records, err
179180
}
180181

181-
type filterJoiner struct {
182-
HasFilters bool
183-
qb strings.Builder
184-
args []interface{}
185-
}
186-
187-
func (fj *filterJoiner) AddLikeFilter(value string, columnName string) {
188-
if value == "" {
189-
return
190-
}
191-
fj.checkHasFilter()
192-
fj.qb.WriteString("`" + columnName + "` LIKE ?\n")
193-
fj.args = append(fj.args, "%"+value+"%")
194-
}
195-
196-
func (fj *filterJoiner) AddFilter(filter string) {
197-
fj.checkHasFilter()
198-
fj.qb.WriteString(filter)
199-
}
200-
201-
func (fj *filterJoiner) checkHasFilter() {
202-
if !fj.HasFilters {
203-
fj.HasFilters = true
204-
} else {
205-
fj.qb.WriteString(" AND ")
182+
func ParsePipelineStatus(status string) (PipeStatus, error) {
183+
switch status {
184+
case "ok":
185+
return PipeStatusOk, nil
186+
case "error":
187+
return PipeStatusError, nil
188+
case "pending":
189+
return PipeStatusPending, nil
190+
case "any":
191+
return PipeStatusAny, nil
192+
default:
193+
return PipeStatusAny, fmt.Errorf("unknown pipe status: '%s'", status)
206194
}
207195
}
208-
209-
func (fj *filterJoiner) String() string {
210-
return fj.qb.String()
211-
}
212-
213-
func (fj *filterJoiner) Args() []interface{} {
214-
return fj.args
215-
}

internal/actionDb/filterJoiner.go

+39
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
package actiondb
2+
3+
import "strings"
4+
5+
type filterJoiner struct {
6+
HasFilters bool
7+
qb strings.Builder
8+
args []interface{}
9+
}
10+
11+
func (fj *filterJoiner) AddLikeFilter(value string, columnName string) {
12+
if value == "" {
13+
return
14+
}
15+
fj.checkHasFilter()
16+
fj.qb.WriteString("`" + columnName + "` LIKE ?\n")
17+
fj.args = append(fj.args, "%"+value+"%")
18+
}
19+
20+
func (fj *filterJoiner) AddFilter(filter string) {
21+
fj.checkHasFilter()
22+
fj.qb.WriteString(filter)
23+
}
24+
25+
func (fj *filterJoiner) checkHasFilter() {
26+
if !fj.HasFilters {
27+
fj.HasFilters = true
28+
} else {
29+
fj.qb.WriteString(" AND ")
30+
}
31+
}
32+
33+
func (fj *filterJoiner) String() string {
34+
return fj.qb.String()
35+
}
36+
37+
func (fj *filterJoiner) Args() []interface{} {
38+
return fj.args
39+
}

internal/cmd/ListPipeline.go

+1-9
Original file line numberDiff line numberDiff line change
@@ -41,15 +41,7 @@ func ListPipelines(cfg config.Config, args ListPipelinesArgs) {
4141
Project: args.Project,
4242
DeliveryId: args.DeliveryId,
4343
}
44-
45-
switch args.Status {
46-
case "ok":
47-
query.Status = actiondb.PipeStatusOk
48-
case "error":
49-
query.Status = actiondb.PipeStatusError
50-
case "pending":
51-
query.Status = actiondb.PipeStatusPending
52-
}
44+
query.Status, _ = actiondb.ParsePipelineStatus(args.Status)
5345

5446
pipeLines, err := dbActions.ListPipelineRecords(query)
5547
if err != nil {

internal/cmd/Logs.go

+3-9
Original file line numberDiff line numberDiff line change
@@ -47,15 +47,9 @@ func Logs(cfg config.Config, args LogsArgs) {
4747

4848
query.Levels = make([]int, 0)
4949
for _, lvl := range args.Levels {
50-
switch lvl {
51-
case "debug":
52-
query.Levels = append(query.Levels, int(slog.LevelDebug))
53-
case "info":
54-
query.Levels = append(query.Levels, int(slog.LevelInfo))
55-
case "warn":
56-
query.Levels = append(query.Levels, int(slog.LevelWarn))
57-
case "error":
58-
query.Levels = append(query.Levels, int(slog.LevelError))
50+
l, err := logsDb.ParseLogLevel(lvl)
51+
if err == nil {
52+
query.Levels = append(query.Levels, l)
5953
}
6054
}
6155

internal/cmd/Serve.go

+23-9
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@ import (
1414
"github.com/religiosa1/git-webhook-receiver/internal/ActionRunner"
1515
actiondb "github.com/religiosa1/git-webhook-receiver/internal/actionDb"
1616
"github.com/religiosa1/git-webhook-receiver/internal/config"
17-
"github.com/religiosa1/git-webhook-receiver/internal/http/handlers"
17+
"github.com/religiosa1/git-webhook-receiver/internal/http/admin"
18+
handlers "github.com/religiosa1/git-webhook-receiver/internal/http/webhook_handlers"
1819
"github.com/religiosa1/git-webhook-receiver/internal/logger"
1920
"github.com/religiosa1/git-webhook-receiver/internal/logsDb"
2021
"github.com/religiosa1/git-webhook-receiver/internal/whreceiver"
@@ -60,11 +61,28 @@ func Serve(cfg config.Config) {
6061

6162
//==========================================================================
6263
// HTTP-Server
63-
srv, err := createServer(actionRunner.Chan(), cfg, logger)
64+
mux, err := createProjectsMux(actionRunner.Chan(), cfg, logger)
6465
if err != nil {
6566
logger.Error("Error creating the server", slog.Any("error", err))
6667
os.Exit(ExitReadConfig)
6768
}
69+
if cfg.WebAdmin {
70+
if dbActions != nil {
71+
logger.Debug("Web admin enabled for pipelines")
72+
mux.HandleFunc("GET /pipelines", admin.ListPipelines(dbActions, logger))
73+
mux.HandleFunc("GET /pipelines/{pipeId}", admin.GetPipeline(dbActions, logger))
74+
mux.HandleFunc("GET /pipelines/{pipeId}/output", admin.GetPipelineOutput(dbActions, logger))
75+
}
76+
if dbLogs != nil {
77+
logger.Debug("Web admin enabled for logs")
78+
mux.HandleFunc("GET /logs", admin.GetLogs(dbLogs, logger))
79+
}
80+
}
81+
82+
srv := &http.Server{
83+
Addr: fmt.Sprintf("%s:%d", cfg.Host, cfg.Port),
84+
Handler: mux,
85+
}
6886

6987
srvCtx, srcCancel := context.WithCancel(context.Background())
7088
defer srcCancel()
@@ -140,7 +158,7 @@ func runServer(ctx context.Context, srv *http.Server, sslConfig config.SslConfig
140158
return err
141159
}
142160

143-
func createServer(actionsCh chan ActionRunner.ActionArgs, cfg config.Config, logger *slog.Logger) (*http.Server, error) {
161+
func createProjectsMux(actionsCh chan ActionRunner.ActionArgs, cfg config.Config, logger *slog.Logger) (*http.ServeMux, error) {
144162
mux := http.NewServeMux()
145163
for projectName, project := range cfg.Projects {
146164
receiver := whreceiver.New(project)
@@ -158,7 +176,7 @@ func createServer(actionsCh chan ActionRunner.ActionArgs, cfg config.Config, log
158176

159177
projectLogger := logger.With(slog.String("project", projectName))
160178
mux.HandleFunc(
161-
fmt.Sprintf("POST /%s", projectName),
179+
fmt.Sprintf("POST /projects/%s", projectName),
162180
handlers.HandleWebhookPost(actionsCh, projectLogger, cfg, projectName, project, receiver),
163181
)
164182
logger.Debug("Registered project",
@@ -167,11 +185,7 @@ func createServer(actionsCh chan ActionRunner.ActionArgs, cfg config.Config, log
167185
slog.String("repo", project.Repo),
168186
)
169187
}
170-
srv := &http.Server{
171-
Addr: fmt.Sprintf("%s:%d", cfg.Host, cfg.Port),
172-
Handler: mux,
173-
}
174-
return srv, nil
188+
return mux, nil
175189
}
176190

177191
type ErrShutdown struct {

internal/config/config.go

+1
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import (
1212
type Config struct {
1313
Host string `yaml:"host" env:"HOST" env-default:"localhost"`
1414
Port int16 `yaml:"port" env:"PORT" env-default:"9090"`
15+
WebAdmin bool `yaml:"web_admin" env:"WEB_ADMIN" env-default:"true"`
1516
LogLevel string `yaml:"log_level" env:"LOG_LEVEL" env-default:"info"`
1617
LogsDbFile string `yaml:"logs_db_file" env:"LOGS_DB_FILE"`
1718
ActionsDbFile string `yaml:"actions_db_file" env:"ACTIONS_DB_FILE" env-default:"actions.sqlite3"`

internal/http/admin/logs.go

+58
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
package admin
2+
3+
import (
4+
"encoding/json"
5+
"log/slog"
6+
"net/http"
7+
"strconv"
8+
9+
"github.com/religiosa1/git-webhook-receiver/internal/logsDb"
10+
)
11+
12+
func GetLogs(db *logsDb.LogsDb, logger *slog.Logger) http.HandlerFunc {
13+
return func(w http.ResponseWriter, req *http.Request) {
14+
w.Header().Set("Content-Type", "application/json")
15+
queryParams := req.URL.Query()
16+
17+
offset, _ := strconv.Atoi(queryParams.Get("offset"))
18+
limit, _ := strconv.Atoi(queryParams.Get("limit"))
19+
20+
cursorId, _ := strconv.ParseInt(queryParams.Get("cursorId"), 10, 64)
21+
cursorTs, _ := strconv.ParseInt(queryParams.Get("cursorTs"), 10, 64)
22+
23+
query := logsDb.GetEntryFilteredQuery{
24+
GetEntryQuery: logsDb.GetEntryQuery{
25+
CursorId: cursorId,
26+
CursorTs: cursorTs,
27+
PageSize: limit,
28+
},
29+
Levels: parseLevels(queryParams["level"]),
30+
Project: queryParams.Get("project"),
31+
DeliveryId: queryParams.Get("deliveryId"),
32+
PipeId: queryParams.Get("pipeId"),
33+
Message: queryParams.Get("message"),
34+
Offset: offset,
35+
}
36+
37+
logs, err := db.GetEntryFiltered(query)
38+
if err != nil {
39+
logger.Error("Error processing GetLogs request", slog.Any("error", err))
40+
w.WriteHeader(500)
41+
w.Write([]byte(err.Error()))
42+
return
43+
}
44+
45+
json.NewEncoder(w).Encode(logs)
46+
}
47+
}
48+
49+
func parseLevels(levels []string) []int {
50+
result := make([]int, 0)
51+
for _, lvl := range levels {
52+
l, err := logsDb.ParseLogLevel(lvl)
53+
if err == nil {
54+
result = append(result, l)
55+
}
56+
}
57+
return result
58+
}

internal/http/admin/pipelines.go

+72
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
package admin
2+
3+
import (
4+
"database/sql"
5+
"encoding/json"
6+
"log/slog"
7+
"net/http"
8+
"strconv"
9+
10+
actiondb "github.com/religiosa1/git-webhook-receiver/internal/actionDb"
11+
)
12+
13+
func ListPipelines(db *actiondb.ActionDb, logger *slog.Logger) http.HandlerFunc {
14+
return func(w http.ResponseWriter, req *http.Request) {
15+
queryParams := req.URL.Query()
16+
17+
offset, _ := strconv.Atoi(queryParams.Get("offset"))
18+
limit, _ := strconv.Atoi(queryParams.Get("limit"))
19+
20+
query := actiondb.ListPipelineRecordsQuery{
21+
Offset: offset,
22+
Limit: limit,
23+
Project: queryParams.Get("project"),
24+
DeliveryId: queryParams.Get("deliveryId"),
25+
}
26+
query.Status, _ = actiondb.ParsePipelineStatus(queryParams.Get("status"))
27+
28+
records, err := db.ListPipelineRecords(query)
29+
if err != nil {
30+
w.WriteHeader(500)
31+
return
32+
}
33+
w.Header().Set("Content-Type", "application/json")
34+
json.NewEncoder(w).Encode(records)
35+
}
36+
}
37+
38+
func GetPipeline(db *actiondb.ActionDb, logger *slog.Logger) http.HandlerFunc {
39+
return func(w http.ResponseWriter, req *http.Request) {
40+
pipeId := req.PathValue("pipeId")
41+
record, err := db.GetPipelineRecord(pipeId)
42+
if err == sql.ErrNoRows {
43+
w.WriteHeader(404)
44+
return
45+
} else if err != nil {
46+
logger.Error("Error processing GetPipeline request", slog.Any("error", err))
47+
w.WriteHeader(500)
48+
w.Write([]byte(err.Error()))
49+
return
50+
}
51+
w.Header().Set("Content-Type", "application/json")
52+
json.NewEncoder(w).Encode(record)
53+
}
54+
}
55+
56+
func GetPipelineOutput(db *actiondb.ActionDb, logger *slog.Logger) http.HandlerFunc {
57+
return func(w http.ResponseWriter, req *http.Request) {
58+
pipeId := req.PathValue("pipeId")
59+
record, err := db.GetPipelineRecord(pipeId)
60+
if err == sql.ErrNoRows {
61+
w.WriteHeader(404)
62+
return
63+
} else if err != nil {
64+
logger.Error("Error processing GetPipelineOutput request", slog.Any("error", err))
65+
w.WriteHeader(500)
66+
w.Write([]byte(err.Error()))
67+
return
68+
}
69+
w.Header().Set("Content-Type", "text/plain")
70+
w.Write([]byte(record.Output.String))
71+
}
72+
}

internal/http/handlers/webhookPost.go renamed to internal/http/webhook_handlers/webhookPost.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
package handlers
1+
package webhookhandlers
22

33
import (
44
"encoding/json"

0 commit comments

Comments
 (0)