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" "github.com/op/go-logging" ) var log = logging.MustGetLogger("cursorius-server") func createSchema(db database.Database) (graphql.Schema, error) { secretType := graphql.NewObject(graphql.ObjectConfig{ Name: "Secret", Description: "A secret available for use inside of a pipeline.", Fields: graphql.Fields{ "id": &graphql.Field{ Type: graphql.NewNonNull(graphql.String), Description: "The id of the secret.", Resolve: func(p graphql.ResolveParams) (interface{}, error) { if secret, ok := p.Source.(database.Secret); ok { return secret.Id, nil } return nil, nil }, }, "name": &graphql.Field{ Type: graphql.NewNonNull(graphql.String), Description: "The name of the secret.", Resolve: func(p graphql.ResolveParams) (interface{}, error) { if secret, ok := p.Source.(database.Secret); ok { return secret.Name, nil } return nil, nil }, }, "secret": &graphql.Field{ Type: graphql.NewNonNull(graphql.String), Description: "The secret.", Resolve: func(p graphql.ResolveParams) (interface{}, error) { if secret, ok := p.Source.(database.Secret); ok { return secret.Secret, nil } return nil, nil }, }, }, }) cloneCredentialType := graphql.NewObject(graphql.ObjectConfig{ Name: "CloneCredential", Description: "A credential for authenticating with the pipeline source host.", Fields: graphql.Fields{ "id": &graphql.Field{ Type: graphql.NewNonNull(graphql.String), Description: "The id of the credential.", Resolve: func(p graphql.ResolveParams) (interface{}, error) { if credential, ok := p.Source.(database.CloneCredential); ok { return credential.Id, nil } return nil, nil }, }, "name": &graphql.Field{ Type: graphql.NewNonNull(graphql.String), Description: "The name of the credential.", Resolve: func(p graphql.ResolveParams) (interface{}, error) { if credential, ok := p.Source.(database.CloneCredential); ok { return credential.Name, nil } return nil, nil }, }, "type": &graphql.Field{ Type: graphql.NewNonNull(graphql.String), Description: "The credential type.", Resolve: func(p graphql.ResolveParams) (interface{}, error) { if credential, ok := p.Source.(database.CloneCredential); ok { return credential.Type, nil } return nil, nil }, }, "username": &graphql.Field{ Type: graphql.NewNonNull(graphql.String), Description: "The username to user with the credential.", Resolve: func(p graphql.ResolveParams) (interface{}, error) { if credential, ok := p.Source.(database.CloneCredential); ok { return credential.Username, nil } return nil, nil }, }, "secret": &graphql.Field{ Type: graphql.NewNonNull(graphql.String), Description: "The secret for the credential.", Resolve: func(p graphql.ResolveParams) (interface{}, error) { if credential, ok := p.Source.(database.CloneCredential); ok { return credential.Secret, nil } return nil, nil }, }, }, }) 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 }, }, }, }) runType := graphql.NewObject(graphql.ObjectConfig{ Name: "Run", Description: "A pipeline run", Fields: graphql.Fields{ "id": &graphql.Field{ Type: graphql.NewNonNull(graphql.String), Description: "The id of the run.", Resolve: func(p graphql.ResolveParams) (interface{}, error) { if run, ok := p.Source.(database.Run); ok { return run.Id, nil } return nil, nil }, }, "progress": &graphql.Field{ Type: graphql.Boolean, Description: "The progress status of the run.", Resolve: func(p graphql.ResolveParams) (interface{}, error) { if run, ok := p.Source.(database.Run); ok { return run.InProgress, nil } return nil, nil }, }, "result": &graphql.Field{ // TODO: handle bigint properly here Type: graphql.Float, Description: "The result of the run.", Resolve: func(p graphql.ResolveParams) (interface{}, error) { if run, ok := p.Source.(database.Run); ok { return run.Result, nil } return nil, nil }, }, "stdout": &graphql.Field{ Type: graphql.String, Description: "The stdout used to validate the run.", Resolve: func(p graphql.ResolveParams) (interface{}, error) { if run, ok := p.Source.(database.Run); ok { return run.Stdout, nil } return nil, nil }, }, "stderr": &graphql.Field{ Type: graphql.String, Description: "The stderr used to validate the run.", Resolve: func(p graphql.ResolveParams) (interface{}, error) { if run, ok := p.Source.(database.Run); ok { return run.Stderr, 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 }, }, "cloneCredential": &graphql.Field{ Type: cloneCredentialType, Description: "The configured credential for cloning the pipeline source.", Resolve: func(p graphql.ResolveParams) (interface{}, error) { if pipeline, ok := p.Source.(database.Pipeline); ok { if pipeline.CloneCredential != nil { return db.GetCloneCredentialById(*pipeline.CloneCredential) } } return nil, nil }, }, "secrets": &graphql.Field{ Type: graphql.NewNonNull(graphql.NewList(graphql.NewNonNull(secretType))), Description: "The list of secrets for the pipeline.", Resolve: func(p graphql.ResolveParams) (interface{}, error) { if pipeline, ok := p.Source.(database.Pipeline); ok { return db.GetSecretsForPipeline(pipeline.Id) } return []database.Secret{}, 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 }, }, "runs": &graphql.Field{ Type: graphql.NewNonNull(graphql.NewList(graphql.NewNonNull(runType))), Description: "The list of runs for the pipeline.", Resolve: func(p graphql.ResolveParams) (interface{}, error) { if pipeline, ok := p.Source.(database.Pipeline); ok { return db.GetRunsForPipeline(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) }, }, "Pipelines": &graphql.Field{ Type: graphql.NewNonNull(graphql.NewList(pipelineType)), Args: graphql.FieldConfigArgument{}, Resolve: func(p graphql.ResolveParams) (interface{}, error) { return db.GetPipelines() }, }, "CloneCredential": &graphql.Field{ Type: cloneCredentialType, Args: graphql.FieldConfigArgument{ "id": &graphql.ArgumentConfig{ Type: graphql.NewNonNull(graphql.String), Description: "The id of the requested credential.", }, }, Resolve: func(p graphql.ResolveParams) (interface{}, error) { id, err := uuid.Parse(p.Args["id"].(string)) if err != nil { return nil, err } return db.GetCloneCredentialById(id) }, }, "CloneCredentials": &graphql.Field{ Type: graphql.NewNonNull(graphql.NewList(cloneCredentialType)), Args: graphql.FieldConfigArgument{}, Resolve: func(p graphql.ResolveParams) (interface{}, error) { return db.GetCredentials() }, }, "Secrets": &graphql.Field{ Type: graphql.NewNonNull(graphql.NewList(secretType)), Args: graphql.FieldConfigArgument{}, Resolve: func(p graphql.ResolveParams) (interface{}, error) { return db.GetSecrets() }, }, }, }) 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, }, "cloneCredentialId": &graphql.ArgumentConfig{ Type: graphql.String, }, }, Resolve: func(params graphql.ResolveParams) (interface{}, error) { var interval int if intervalVal, ok := params.Args["pollInterval"]; ok { interval = intervalVal.(int) } else { interval = 0 } var credential *uuid.UUID if credentialVal, ok := params.Args["cloneCredentialId"]; ok { id, err := uuid.Parse(credentialVal.(string)) if err != nil { return nil, err } credential = &id } else { credential = nil } pipeline, err := db.CreatePipeline( params.Args["name"].(string), params.Args["url"].(string), interval, credential, ) if err != nil { return nil, err } return pipeline, nil }, }, "createWebhook": &graphql.Field{ Type: webhookType, Description: "Create a new webhook", Args: graphql.FieldConfigArgument{ "type": &graphql.ArgumentConfig{ Type: graphql.NewNonNull(graphql.String), }, "pipelineId": &graphql.ArgumentConfig{ Type: graphql.NewNonNull(graphql.String), }, }, Resolve: func(params graphql.ResolveParams) (interface{}, error) { id, err := uuid.Parse(params.Args["id"].(string)) if err != nil { return nil, err } webhook, err := db.CreateWebhook( database.WebhookSender(params.Args["type"].(string)), id, ) if err != nil { return nil, err } return webhook, nil }, }, "createCloneCredential": &graphql.Field{ Type: cloneCredentialType, Description: "Create a new CloneCredential", Args: graphql.FieldConfigArgument{ "name": &graphql.ArgumentConfig{ Type: graphql.NewNonNull(graphql.String), }, "type": &graphql.ArgumentConfig{ Type: graphql.NewNonNull(graphql.String), }, "username": &graphql.ArgumentConfig{ Type: graphql.NewNonNull(graphql.String), }, "secret": &graphql.ArgumentConfig{ Type: graphql.NewNonNull(graphql.String), }, }, Resolve: func(params graphql.ResolveParams) (interface{}, error) { credential, err := db.CreateCredential( params.Args["name"].(string), database.CloneCredentialType(params.Args["type"].(string)), params.Args["username"].(string), params.Args["secret"].(string), ) if err != nil { return nil, err } return credential, nil }, }, "createSecret": &graphql.Field{ Type: secretType, Description: "Create a new secret", Args: graphql.FieldConfigArgument{ "name": &graphql.ArgumentConfig{ Type: graphql.NewNonNull(graphql.String), }, "secret": &graphql.ArgumentConfig{ Type: graphql.NewNonNull(graphql.String), }, }, Resolve: func(params graphql.ResolveParams) (interface{}, error) { secret, err := db.CreateSecret( params.Args["name"].(string), params.Args["secret"].(string), ) if err != nil { return nil, err } return secret, nil }, }, "setPipelineCloneCredential": &graphql.Field{ Type: pipelineType, Description: "Set the CloneCredential used by a pipeline to clone the source repo", Args: graphql.FieldConfigArgument{ "cloneCredentialId": &graphql.ArgumentConfig{ Type: graphql.String, }, "pipelineId": &graphql.ArgumentConfig{ Type: graphql.NewNonNull(graphql.String), }, }, Resolve: func(params graphql.ResolveParams) (interface{}, error) { pipelineId, err := uuid.Parse(params.Args["pipelineId"].(string)) if err != nil { return nil, err } if cloneCredentialIdVal, ok := params.Args["cloneCredentialId"]; ok { cloneCredentialId, err := uuid.Parse(cloneCredentialIdVal.(string)) if err != nil { return nil, err } pipeline, err := db.SetPipelineCloneCredential(pipelineId, &cloneCredentialId) if err != nil { return nil, err } return pipeline, nil } else { pipeline, err := db.SetPipelineCloneCredential(pipelineId, nil) if err != nil { return nil, err } return pipeline, nil } }, }, "addSecretToPipeline": &graphql.Field{ Type: pipelineType, Description: "Allow a secret to be accessed by a pipeline.", Args: graphql.FieldConfigArgument{ "secretId": &graphql.ArgumentConfig{ Type: graphql.String, }, "pipelineId": &graphql.ArgumentConfig{ Type: graphql.NewNonNull(graphql.String), }, }, Resolve: func(params graphql.ResolveParams) (interface{}, error) { secretId, err := uuid.Parse(params.Args["secretId"].(string)) if err != nil { return nil, err } pipelineId, err := uuid.Parse(params.Args["pipelineId"].(string)) if err != nil { return nil, err } err = db.AssignSecretToPipeline(pipelineId, secretId) if err != nil { return nil, err } pipeline, err := db.GetPipelineById(pipelineId) if err != nil { return nil, err } return pipeline, nil }, }, "removeSecretFromPipeline": &graphql.Field{ Type: pipelineType, Description: "Remove a pipeline's access to a secret.", Args: graphql.FieldConfigArgument{ "secretId": &graphql.ArgumentConfig{ Type: graphql.String, }, "pipelineId": &graphql.ArgumentConfig{ Type: graphql.NewNonNull(graphql.String), }, }, Resolve: func(params graphql.ResolveParams) (interface{}, error) { secretId, err := uuid.Parse(params.Args["secretId"].(string)) if err != nil { return nil, err } pipelineId, err := uuid.Parse(params.Args["pipelineId"].(string)) if err != nil { return nil, err } err = db.RemoveSecretFromPipeline(pipelineId, secretId) if err != nil { return nil, err } pipeline, err := db.GetPipelineById(pipelineId) 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 }