From 4a4b6fd1854877e26101c0c55dd68b7c38a303d4 Mon Sep 17 00:00:00 2001 From: restitux Date: Sat, 14 Jan 2023 06:02:17 -0700 Subject: [PATCH] Add basic graphql api --- admin_api/admin_api.go | 214 +++++++++++++++++++++++++++++++++++++++++ database/db.go | 34 ++++--- database/func.go | 124 ++++++++++++++++++++++++ database/types.go | 50 ++++++++++ go.mod | 2 + go.sum | 4 + listen/listen.go | 25 +++-- main.go | 7 +- 8 files changed, 435 insertions(+), 25 deletions(-) create mode 100644 admin_api/admin_api.go create mode 100644 database/func.go create mode 100644 database/types.go diff --git a/admin_api/admin_api.go b/admin_api/admin_api.go new file mode 100644 index 0000000..248c4a3 --- /dev/null +++ b/admin_api/admin_api.go @@ -0,0 +1,214 @@ +package admin_api + +import ( + "fmt" + "net/http" + + "git.ohea.xyz/cursorius/server/database" + "github.com/google/uuid" + "github.com/graphql-go/graphql" + "github.com/graphql-go/handler" +) + +func createSchema(db database.Database) (graphql.Schema, error) { + webhookType := graphql.NewObject(graphql.ObjectConfig{ + Name: "Webhook", + Description: "A webhook for triggering pipelines", + Fields: graphql.Fields{ + "id": &graphql.Field{ + Type: graphql.NewNonNull(graphql.String), + Description: "The id of the webhook.", + Resolve: func(p graphql.ResolveParams) (interface{}, error) { + if webhook, ok := p.Source.(database.Webhook); ok { + return webhook.Id, nil + } + return nil, nil + }, + }, + "serverType": &graphql.Field{ + Type: graphql.NewNonNull(graphql.String), + Description: "The format of the webhook.", + Resolve: func(p graphql.ResolveParams) (interface{}, error) { + if webhook, ok := p.Source.(database.Webhook); ok { + return webhook.ServerType, nil + } + return nil, nil + }, + }, + "secret": &graphql.Field{ + Type: graphql.NewNonNull(graphql.String), + Description: "The secret used to validate the webhook.", + Resolve: func(p graphql.ResolveParams) (interface{}, error) { + if webhook, ok := p.Source.(database.Webhook); ok { + return webhook.Secret, nil + } + return nil, nil + }, + }, + }, + }) + + pipelineType := graphql.NewObject(graphql.ObjectConfig{ + Name: "Pipeline", + Description: "A pipeline for running ci jobs", + Fields: graphql.Fields{ + "id": &graphql.Field{ + Type: graphql.NewNonNull(graphql.String), + Description: "The id of the pipeline.", + Resolve: func(p graphql.ResolveParams) (interface{}, error) { + if pipeline, ok := p.Source.(database.Pipeline); ok { + return pipeline.Id, nil + } + return nil, nil + }, + }, + "name": &graphql.Field{ + Type: graphql.NewNonNull(graphql.String), + Description: "The name of the pipeline.", + Resolve: func(p graphql.ResolveParams) (interface{}, error) { + if pipeline, ok := p.Source.(database.Pipeline); ok { + return pipeline.Name, nil + } + return nil, nil + }, + }, + "url": &graphql.Field{ + Type: graphql.NewNonNull(graphql.String), + Description: "The url of the pipeline.", + Resolve: func(p graphql.ResolveParams) (interface{}, error) { + if pipeline, ok := p.Source.(database.Pipeline); ok { + return pipeline.Url, nil + } + return nil, nil + }, + }, + "pollInterval": &graphql.Field{ + Type: graphql.NewNonNull(graphql.Int), + Description: "The polling interval for the pipeline.", + Resolve: func(p graphql.ResolveParams) (interface{}, error) { + if pipeline, ok := p.Source.(database.Pipeline); ok { + return pipeline.PollInterval, nil + } + return nil, nil + }, + }, + "createWebhook": &graphql.Field{ + Type: webhookType, + Description: "Create a new webhook", + Args: graphql.FieldConfigArgument{ + "type": &graphql.ArgumentConfig{ + Type: graphql.NewNonNull(graphql.String), + }, + }, + Resolve: func(params graphql.ResolveParams) (interface{}, error) { + webhook, err := db.CreateWebhook( + database.WebhookSender(params.Args["type"].(string)), + params.Source.(database.Pipeline).Id, + ) + if err != nil { + return nil, err + } + return webhook, nil + }, + }, + "webhooks": &graphql.Field{ + Type: graphql.NewNonNull(graphql.NewList(graphql.NewNonNull(webhookType))), + Description: "The list of webhooks for the pipeline.", + Resolve: func(p graphql.ResolveParams) (interface{}, error) { + if pipeline, ok := p.Source.(database.Pipeline); ok { + return db.GetWebhooksForPipeline(pipeline.Id) + } + return []database.Webhook{}, nil + }, + }, + }, + }) + + queryType := graphql.NewObject(graphql.ObjectConfig{ + Name: "Query", + Fields: graphql.Fields{ + "Pipeline": &graphql.Field{ + Type: pipelineType, + Args: graphql.FieldConfigArgument{ + "id": &graphql.ArgumentConfig{ + Type: graphql.NewNonNull(graphql.String), + Description: "The id of the requested pipeline.", + }, + }, + Resolve: func(p graphql.ResolveParams) (interface{}, error) { + id, err := uuid.Parse(p.Args["id"].(string)) + if err != nil { + return nil, err + } + return db.GetPipelineById(id) + }, + }, + }, + }) + + mutationType := graphql.NewObject(graphql.ObjectConfig{ + Name: "Mutation", + Fields: graphql.Fields{ + "createPipeline": &graphql.Field{ + Type: pipelineType, + Description: "Create a new pipeline", + Args: graphql.FieldConfigArgument{ + "name": &graphql.ArgumentConfig{ + Type: graphql.NewNonNull(graphql.String), + }, + "url": &graphql.ArgumentConfig{ + Type: graphql.NewNonNull(graphql.String), + }, + "pollInterval": &graphql.ArgumentConfig{ + Type: graphql.Int, + }, + }, + Resolve: func(params graphql.ResolveParams) (interface{}, error) { + var interval int + if intervalVal, ok := params.Args["pollInterval"]; ok { + interval = intervalVal.(int) + } else { + interval = 0 + } + pipeline, err := db.CreatePipeline( + params.Args["name"].(string), + params.Args["url"].(string), + interval, + ) + if err != nil { + return nil, err + } + return pipeline, nil + }, + }, + }, + }) + + schema, err := graphql.NewSchema(graphql.SchemaConfig{ + Query: queryType, + Mutation: mutationType, + }) + if err != nil { + return schema, fmt.Errorf("Could not create schema: %w", err) + } + + return schema, nil +} + +func CreateHandler(db database.Database, mux *http.ServeMux) error { + + schema, err := createSchema(db) + if err != nil { + return err + } + + h := handler.New(&handler.Config{ + Schema: &schema, + Pretty: true, + GraphiQL: true, + }) + + mux.Handle("/graphql", h) + + return nil +} diff --git a/database/db.go b/database/db.go index 92d0b1f..7df2cac 100644 --- a/database/db.go +++ b/database/db.go @@ -18,7 +18,7 @@ type Database struct { Conn *pgx.Conn } -func LaunchDB(conf config.DBConfig) error { +func LaunchDB(conf config.DBConfig) (Database, error) { dbURL := fmt.Sprintf( "postgres://%v:%v@%v:%v/%v", @@ -50,7 +50,7 @@ func LaunchDB(conf config.DBConfig) error { } if err != nil { - return fmt.Errorf("Could not open database: %w", err) + return db, fmt.Errorf("Could not open database: %w", err) } log.Infof("Database connected sucessfully!") @@ -64,7 +64,7 @@ SELECT EXISTS ( var versionTableExists bool err = db.Conn.QueryRow(context.Background(), versionTableExistsQuery).Scan(&versionTableExists) if err != nil { - return fmt.Errorf("Could not check if database was initalized: %w", err) + return db, fmt.Errorf("Could not check if database was initalized: %w", err) } if versionTableExists { @@ -73,31 +73,33 @@ SELECT EXISTS ( log.Info("New database found, initializing....") err = initDB(db.Conn) if err != nil { - return fmt.Errorf("Could not initalize database: %w", err) + return db, fmt.Errorf("Could not initalize database: %w", err) } } - return nil + return db, nil } func initDB(conn *pgx.Conn) error { createTablesQuery := ` +CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; + CREATE TABLE version ( version INT NOT NULL ); CREATE TABLE pipelines ( - id SERIAL PRIMARY KEY, + id UUID PRIMARY KEY, name TEXT NOT NULL, url TEXT NOT NULL, poll_interval INTEGER ); CREATE TABLE webhooks ( - id SERIAL PRIMARY KEY, - type TEXT, - secret TEXT, - pipeline INTEGER, + id UUID PRIMARY KEY, + server_type TEXT, + secret TEXT, + pipeline UUID, CONSTRAINT fk_pipeline FOREIGN KEY(pipeline) @@ -105,14 +107,14 @@ CREATE TABLE webhooks ( ); CREATE TABLE runners ( - id SERIAL PRIMARY KEY, + id UUID PRIMARY KEY, name TEXT, secret TEXT ); CREATE TABLE runs ( - id SERIAL PRIMARY KEY, - pipeline SERIAL, + id UUID PRIMARY KEY, + pipeline UUID, result BOOLEAN NOT NULL, CONSTRAINT fk_pipeline @@ -121,10 +123,10 @@ CREATE TABLE runs ( ); CREATE TABLE command_executions ( - id SERIAL PRIMARY KEY, - run_id SERIAL, + id UUID PRIMARY KEY, + run_id UUID, command TEXT, - return_code TEXT, + return_code INT, stdout TEXT, stderr TEXT, start_time TIMESTAMP, diff --git a/database/func.go b/database/func.go new file mode 100644 index 0000000..e928418 --- /dev/null +++ b/database/func.go @@ -0,0 +1,124 @@ +package database + +import ( + "context" + "fmt" + + "github.com/google/uuid" +) + +func (d *Database) GetPipelineById(id uuid.UUID) (Pipeline, error) { + query := ` +SELECT name, url, poll_interval +FROM pipelines +WHERE id=$1;` + + pipeline := Pipeline{ + Id: id, + } + + err := d.Conn.QueryRow(context.Background(), query, id).Scan(&pipeline.Name, &pipeline.Url, &pipeline.PollInterval) + if err != nil { + return pipeline, fmt.Errorf("Could not query database for pipeline with id %v: %w", id.String(), err) + } + + return pipeline, nil +} + +func (d *Database) CreatePipeline(name string, url string, pollInterval int) (Pipeline, error) { + query := ` +INSERT INTO pipelines (id, name, url, poll_interval) +VALUES (uuid_generate_v4(), $1, $2, $3) +RETURNING id, name, url, poll_interval;` + + pipeline := Pipeline{} + var idStr string + err := d.Conn.QueryRow(context.Background(), query, name, url, pollInterval).Scan(&idStr, &pipeline.Name, &pipeline.Url, &pipeline.PollInterval) + if err != nil { + return pipeline, err + } + + id, err := uuid.Parse(idStr) + if err != nil { + return pipeline, err + } + + pipeline.Id = id + + return pipeline, nil +} + +func (db *Database) GetWebhooksForPipeline(id uuid.UUID) ([]Webhook, error) { + query := ` +SELECT id, server_type, secret +FROM webhooks +WHERE pipeline=$1;` + + webhooks := make([]Webhook, 0) + + rows, err := db.Conn.Query(context.Background(), query, id) + if err != nil { + log.Fatal(err) + } + defer rows.Close() + + for rows.Next() { + var webhook Webhook + var idStr string + if err := rows.Scan(&idStr, &webhook.ServerType, &webhook.Secret); err != nil { + return webhooks, err + } + + webhook.Id, err = uuid.Parse(idStr) + if err != nil { + return webhooks, err + } + webhooks = append(webhooks, webhook) + } + + return webhooks, nil +} + +func (d *Database) GetWebhookById(id uuid.UUID) (Webhook, error) { + query := ` +SELECT server_type, secret, pipeline +FROM webhooks +WHERE id=$1;` + + webhook := Webhook{ + Id: id, + } + + err := d.Conn.QueryRow(context.Background(), query, id).Scan(&webhook.ServerType, &webhook.Secret, &webhook.Pipeline) + if err != nil { + return webhook, fmt.Errorf("Could not query database for webhook with id %v: %w", id.String(), err) + } + + return webhook, nil +} + +func (d *Database) CreateWebhook(serverType WebhookSender, pipelineId uuid.UUID) (Webhook, error) { + + //WITH secret_val as (select substr(md5(random()::text), 0, 50)), + + query := ` +INSERT INTO webhooks (id, server_type, secret, pipeline) +VALUES (uuid_generate_v4(), $1, (select substr(md5(random()::text), 0, 50)), $2) +RETURNING id, server_type, secret, pipeline;` + + webhook := Webhook{} + var idStr string + err := d.Conn.QueryRow(context.Background(), query, string(serverType), pipelineId).Scan(&idStr, &webhook.ServerType, &webhook.Secret, &webhook.Pipeline) + if err != nil { + return webhook, err + } + + id, err := uuid.Parse(idStr) + if err != nil { + return webhook, err + } + + webhook.Id = id + + return webhook, nil +} diff --git a/database/types.go b/database/types.go new file mode 100644 index 0000000..6766c8c --- /dev/null +++ b/database/types.go @@ -0,0 +1,50 @@ +package database + +import ( + "time" + + "github.com/google/uuid" +) + +type Pipeline struct { + Id uuid.UUID + Name string + Url string + PollInterval int +} + +type WebhookSender string + +const ( + Gitea WebhookSender = "gitea" +) + +type Webhook struct { + Id uuid.UUID + ServerType WebhookSender + Secret string + Pipeline uuid.UUID +} + +type Runner struct { + Id uuid.UUID + Name string + Secret string +} + +type Run struct { + Id uuid.UUID + Pipeline uuid.UUID + Result bool +} + +type CommandExecution struct { + Id uuid.UUID + RunId uuid.UUID + Command string + ReturnCode int + Stdout string + Stderr string + StartTime time.Time + EndTime time.Time +} diff --git a/go.mod b/go.mod index 656a900..25d6425 100644 --- a/go.mod +++ b/go.mod @@ -11,6 +11,8 @@ require ( github.com/go-git/go-git/v5 v5.4.3-0.20220529141257-bc1f419cebcf github.com/go-playground/webhooks/v6 v6.0.1 github.com/google/uuid v1.3.0 + github.com/graphql-go/graphql v0.8.0 + github.com/graphql-go/handler v0.2.3 github.com/jackc/pgx/v5 v5.2.0 github.com/op/go-logging v0.0.0-20160315200505-970db520ece7 golang.org/x/net v0.2.0 diff --git a/go.sum b/go.sum index b094f8c..27aa077 100644 --- a/go.sum +++ b/go.sum @@ -709,6 +709,10 @@ github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoA github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/graphql-go/graphql v0.8.0 h1:JHRQMeQjofwqVvGwYnr8JnPTY0AxgVy1HpHSGPLdH0I= +github.com/graphql-go/graphql v0.8.0/go.mod h1:nKiHzRM0qopJEwCITUuIsxk9PlVlwIiiI8pnJEhordQ= +github.com/graphql-go/handler v0.2.3 h1:CANh8WPnl5M9uA25c2GBhPqJhE53Fg0Iue/fRNla71E= +github.com/graphql-go/handler v0.2.3/go.mod h1:leLF6RpV5uZMN1CdImAxuiayrYYhOk33bZciaUGaXeU= github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= github.com/grpc-ecosystem/go-grpc-middleware v1.0.1-0.20190118093823-f849b5445de4/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= github.com/grpc-ecosystem/go-grpc-middleware v1.2.2/go.mod h1:EaizFBKfUKtMIF5iaDEhniwNedqGo9FuLFzppDr3uwI= diff --git a/listen/listen.go b/listen/listen.go index a6a48a0..0aa9841 100644 --- a/listen/listen.go +++ b/listen/listen.go @@ -4,7 +4,9 @@ import ( "fmt" "net/http" + "git.ohea.xyz/cursorius/server/admin_api" "git.ohea.xyz/cursorius/server/config" + "git.ohea.xyz/cursorius/server/database" "git.ohea.xyz/cursorius/server/pipeline_api" "git.ohea.xyz/cursorius/server/runnermanager" "git.ohea.xyz/cursorius/server/webhook" @@ -19,14 +21,20 @@ var log = logging.MustGetLogger("cursorius-server") func setupHTTPServer( mux *http.ServeMux, conf config.Config, + db database.Database, registerCh chan runnermanager.RunnerRegistration, getRunnerCh chan runnermanager.GetRunnerRequest, -) { +) error { webhook.CreateWebhookHandler(conf, mux) pipeline_api.CreateHandler(getRunnerCh, mux) + err := admin_api.CreateHandler(db, mux) + if err != nil { + return fmt.Errorf("Could not create admin api handler: %w", err) + } + mux.HandleFunc("/runner", func(w http.ResponseWriter, r *http.Request) { conn, err := websocket.Accept(w, r, nil) if err != nil { @@ -35,6 +43,7 @@ func setupHTTPServer( } go runnermanager.RegisterRunner(conn, registerCh) }) + return nil } func Listen( @@ -42,22 +51,26 @@ func Listen( address string, port int, conf config.Config, + db database.Database, registerCh chan runnermanager.RunnerRegistration, getRunnerCh chan runnermanager.GetRunnerRequest, -) { +) error { - setupHTTPServer( + err := setupHTTPServer( mux, conf, + db, registerCh, getRunnerCh, ) + if err != nil { + return fmt.Errorf("Could not setup http endpoints: %w", err) + } connect_string := fmt.Sprintf("%v:%v", address, port) log.Noticef("Launching HTTP server on %v\n", connect_string) - log.Fatal(http.ListenAndServe( + return http.ListenAndServe( connect_string, h2c.NewHandler(mux, &http2.Server{}), - )) - + ) } diff --git a/main.go b/main.go index c99ab43..20d7e34 100644 --- a/main.go +++ b/main.go @@ -34,7 +34,7 @@ func main() { return } - err = database.LaunchDB(configData.Config.DBConfig) + db, err := database.LaunchDB(configData.Config.DBConfig) if err != nil { log.Errorf("Could not launch db: %v", err) return @@ -50,12 +50,13 @@ func main() { mux := http.NewServeMux() - listen.Listen( + log.Fatal(listen.Listen( mux, configData.Config.Address, configData.Config.Port, configData.Config, + db, registerCh, getRunnerCh, - ) + )) }