Project Structure - Core
Core is the main server for Openlane and where the majority of the backend code and business logic is located for the API.
cmd
- server
- cli
Located in cmd/ this is a cobra cli that will start up the main core server. The Taskfile included in the repo provides the common commands but under the hood
the serve command is what is being used
go run main.go serve --debug --pretty
You can use task run-dev to run the server with the default options
Located in cmd/cli/cmd this directory includes all the CRUD operations for the openlane cli. As these follow a pretty similar pattern throughout
all objects, there is a template that allows these files to be generated and only requires the input and output fields to be updated.
If you are developing the cli or related functionality, instead of using the brew installed cli, you can use:
go run --tags cli cmd/cli/main.go [command] [subcommand] <flags>
You can add the following alias to your shell profile to make this easier:
alias ol="go run -tags cli cmd/cli/main.go"
config
Located in config/ this directory contains the generated config and example settings powered by koanf.
If changes are made to the config structure or dependent configs, you'll need to regenerate the config:
task config:generate
The first time you setup the server locally, you'll need to copy the config/config-dev.example.yaml into config/.config.yaml. This file is
in the .gitignore to prevent accidentally committing secrets
internal
Located in internal/ent, this is the meat of the repo, if you are looking for something, it's probably in this directory somewhere. Everything in the internal directory is intended to only be used by packages within this repository, outside usage will be blocked by the go-compiler.
ent
Located in internal/ent this directory contains all the code related to the schemas and the interactions with the database
- structure
- schema
- hooks
- interceptors
- privacy
- mixin
- generated
├── internal
│ ├── ent
│ │ ├── generated
│ │ ├── hooks
│ │ ├── interceptors
│ │ ├── mixin
│ │ ├── privacy
│ │ ├── schema
│ │ ├── templates
│ │ └── validator
This includes all schemas for the database. When generating a new schema, refer to the GraphQL and codegen documentation.
When generating a new schema, a template is used that includes the basic structure of any new schema used in Openlane.
You can use task db:newschema to generate a new schema using the template
package schema
import (
"entgo.io/contrib/entgql"
"entgo.io/ent"
"entgo.io/ent/schema"
"github.com/theopenlane/core/internal/ent/mixin"
)
// Meow defines the meow schema.
type Meow struct {
SchemaFuncs
ent.Schema
}
// SchemaMeow is the name of the meow schema.
const SchemaMeow = "meow"
// Name returns the name of the meow schema.
func (Meow) Name() string {
return SchemaMeow
}
// GetType returns the type of the meow schema.
func (Meow) GetType() any {
return Meow.Type
}
// PluralName returns the plural name of the meow schema.
func (Meow) PluralName() string {
return pluralize.NewClient().Plural(SchemaMeow)
}
// Fields of the Meow
func (Meow) Fields() []ent.Field {
return []ent.Field{
// Fields go here
}
}
// Mixin of the Meow
func (Meow) Mixin() []ent.Mixin {
// getDefaultMixins returns the default mixins for all entities
// see mixingConfig{} for more configuration options
return getDefaultMixins(Meow{})
}
// Edges of the Meow
func (m Meow) Edges() []ent.Edge {
return []ent.Edge{
// Edges go here
// see defaultEdgeToWithPagination(m, Woof{}) and similar functions
// in default.go for helper functions
}
}
// Indexes of the Meow
func (Meow) Indexes() []ent.Index {
return []ent.Index{}
}
// Annotations of the Meow
func (Meow) Annotations() []schema.Annotation {
return []schema.Annotation{
// the AnnotationMixin provides the common annotations for
// to create all the graphQL goodness; if you need the schema only and not the endpoints, use the below annotation instead and set the mixinConfig to `excludeAnnotations: true
// if you do not need the graphql bits
// entgql.Skip(entgql.SkipAll),
// entx.SchemaGenSkip(true),
// entx.QueryGenSkip(true)
// the below annotation adds the entfga policy that will check access to the entity
// remove this annotation (or replace with another policy) if you want checks to be defined
// by another object
// uncomment after the first run
// entfga.SelfAccessChecks(),
}
}
// Hooks of the Meow
func (Meow) Hooks() []ent.Hook {
return []ent.Hook{}
}
// Interceptors of the Meow
func (Meow) Interceptors() []ent.Interceptor {
return []ent.Interceptor{}
}
// Policy of the Meow
func (Meow) Policy() ent.Policy {
// add the new policy here, the default post-policy is to deny all
// so you need to ensure there are rules in place to allow the actions you want
return policy.NewPolicy(
policy.WithMutationRules(
// add mutation rules here, the below is the recommended default
policy.CheckCreateAccess(),
// this needs to be commented out for the first run that had the entfga annotation
// the first run will generate the functions required based on the entfa annotation
// entfga.CheckEditAccess[*generated.{{ $name }}Mutation](),
),
)
}
This contains all hooks written to change the behavior of a mutation. Think of a hook as a middleware that can occur before or after the mutation is executed.
// HookExample is a hook that is used as an example that only happens on `Create` operations
func HookExample() ent.Hook {
return hook.On(func(next ent.Mutator) ent.Mutator {
return hook.ExampleFunc(func(ctx context.Context, m *generated.ExampleMutation) (generated.Value, error) {
// code here will occur before the example mutation is executed
retVal, err := next.Mutate(ctx, m)
if err != nil {
return nil, err
}
// code here will occur after the example mutation is executed
return retVal, err
})
}, ent.OpCreate) // only do the thing on create operations, this can be omitted completely or include multiple operations separated by `|`
}
For more information, refer to the upstream docs
This contains all interceptors written to change the behavior of a query. These are similar to hooks, but instead of acting on a mutation, it acts on a query.
Traversefunctions occur before the query is executedInterceptorfunctions occur after the query is executed
We commonly use these to filter data the user has access to, or log information such as query timing, for example:
func QueryLogger() ent.InterceptFunc {
return func(next ent.Querier) ent.Querier {
return ent.QuerierFunc(func(ctx context.Context, query generated.Query) (ent.Value, error) {
q, err := intercept.NewQuery(query)
if err != nil {
return nil, err
}
start := time.Now()
defer func() {
log.Info().
Str("duration", time.Since(start).String()).
Str("schema", q.Type()).
Msg("query duration")
}()
return next.Query(ctx, query)
})
}
}
For more information, refer to the upstream docs
This includes privacy policies, this is used in conjunction with our FGA implementation
For more information, refer to the upstream docs
Mixins are common fields and functions that are used on multiple schemas, for example, the SoftDeleteMixin is used on all schemas as we want to use soft-deletes
across all our database tables.
not all mixins are here. Due to import cycles, some are located within the schema directory. Others, because they are not unique to this repository are located within the entx repository
All code generated by entc is in this directory and should not be edited manually as changes will be overwritten on the next run of task generate.
graphapi
Located in internal/graphapi this directory contains all the code related to graphapi resolvers,
All graphapi resolvers require authentication, this is handled by the auth middleware and requires no updates to the resolvers themselves.
- structure
- resolvers
- query
- schema
- generated
├── internal
│ ├── graphapi
│ │ ├── clientschema
│ │ ├── generate
│ │ ├── generated
│ │ ├── model
│ │ ├── query
│ │ ├── schema
│ │ └── testdata
The *.resolvers.go files contain the business logic of the resolvers.
These are generated based on the schema and a template, but the generated should be reviewed and updated as needed depending on the use case.
All of the list functions are included in ent.resolvers.go, whereas the CRUD functions are in a file per schema. For example, the group schema
resolvers will be located in group.resolvers.go.
The internal/graphapi/query/simple directory contains queries and mutations used to generate the openlaneclient using gqlgenc
Basic queries are generated as part of the gqlgen process, however, these only include direct fields and no edges. These are not regenerated after each run to preserve manual changes.
The internal/graphapi/schema directory contains the generated graphql schemas.
You can add manually created schemas here for additional functionality. As an example the programextended.graphql contains extended schemas such as:
extend input UpdateProgramInput {
addProgramMembers: [CreateProgramMembershipInput!]
}
extend input ProgramMembershipWhereInput {
programID: String
userID: String
}
Schemas are generated as part of the gqlgen process. These are not regenerated after each run to preserve manual changes with the exception of
ent.graphql which is fully generated.
All the generated functions are included in the internal/graphapi/generated directory, with a file per schema.
The generated models are included in the internal/graphapi/model/gen_model.go.
The files in both of these directories are not intended for manual changes, and instead should only be updated the the generation scripts.
The generate directory contains the config and generation scripts for the graphapi related generation (gqlgen and gqlgenc)
httpserve
- server
- handlers
- routes
- server opts
Located in internal/httpserve this directory contains the main http server and configuration as well as the REST api handlers and routes. Although the
majority of the openlane API is a graphapi, there are several REST routes such as login, password-reset, etc.
├── internal
│ ├── httpserve
│ │ ├── authmanager
│ │ ├── config
│ │ ├── handlers
│ │ ├── route
│ │ ├── server
│ │ └── serveropts
Located in internal/httpserve/handlers this directory contains the REST api handlers. The general use-case should not require a REST endpoint, however, there are several cases where REST handlers have
been implemented such as for login. Handlers should all follow the same pattern of created a Handler and a BindHandler, for openAPI specs.
func (h *Handler) ExampleHandler(ctx echo.Context) error {
// bind the input
in, err := BindAndValidateWithAutoRegistry(ctx, h, openapi.Operation, models.ExampleSuccessRequest, models.ExampleResponse, openapi.Registry)
if err != nil {
return h.InvalidInput(ctx, err, openapi)
}
//
// ... do some stuff ...
//
// return the response
out := &models.ExampleReply{
Reply: rout.Reply{Success: true},
Message: "Example works!",
}
return h.Success(ctx, out, openapi)
}
Located in internal/httpserve/route this directory contains all the http routes for the server.
All handlers must be registered routes with the echo server. Once a handler is created, a route should be added here. These all follow a predictable pattern
func registerExampleHandler(router *Router) (err error) {
config := Config{
Path: "/example",
Method: http.MethodGet,
Name: "Example",
Description: "Get example data",
Tags: []string{"example"},
OperationID: "Example",
Security: handlers.PublicSecurity,
Middlewares: *publicEndpoint,
Handler: router.Handler.ExampleHandler,
}
return router.AddUnversionedHandlerRoute(config)
}
The default middleware (publicEndpoint) is an unauthenticated route. If the REST endpoint should require authentication authenticatedEndpoint should be used instead.
This contains server options for the echo server, including With options for setting up different settings.
pkg
middleware
Located in pkg/middleware this contains many commonly used middleware used by the core server.
These may move to internal if they are dependent on the schema, or to another repo at some point in the future.
├── pkg
│ ├── middleware
│ │ ├── auth
│ │ ├── cachecontrol
│ │ ├── cors
│ │ ├── debug
│ │ ├── mime
│ │ ├── ratelimit
│ │ ├── ratelimiter
│ │ ├── redirect
│ │ ├── secure
│ │ └── transaction
models
Located in pkg/models this includes all the model information for the REST api input and output. This is used to generate our openAPI specs.
All REST endpoints should include an entry in the models package with:
Request- the fields, notingomitemptyif not required, that should be included with thePOSTrequestReply- the fields included in a response from the handlerValidateRequest function - validation function for theRequestExampleSuccessRequest- example request containing valid values for the fieldsExampleSuccessReply- example response that would be returned on a successful request
openlaneclient
Located in pkg/openlaneclient this includes the generated golang API client used to interact with the API. The queries added to internal/graphapi/query/simple
are used as input to gqlgenc for the graphclient. The restclient is manually maintained.