mirror of
https://github.com/goharbor/harbor.git
synced 2024-12-24 09:38:09 +01:00
c729e3b9e0
Signed-off-by: He Weiwei <hweiwei@vmware.com>
312 lines
11 KiB
Markdown
312 lines
11 KiB
Markdown
# swagger
|
|
|
|
In Stratoscale, we really like the idea of API-first services, and we also really like Go.
|
|
We saw the go-swagger library, and thought that most of it can really help us. Generating code from
|
|
swagger files is a big problem with a lot of corner cases, and go-swagger is doing great job.
|
|
|
|
The one thing that we felt missing, is customization of the server to run with our design principles:
|
|
|
|
* Custom `main` function
|
|
* Dependency injection
|
|
* Limited scopes with unit testing.
|
|
|
|
Also:
|
|
|
|
* Adding you functions to the generated `configure_swagger_*.go` seems to be a burden.
|
|
* Lack of Interface that the service implement.
|
|
* Complicated and custom http clients and runtime.
|
|
|
|
Those are the changes that this contributor templates are providing:
|
|
|
|
## Server
|
|
|
|
### The new `restapi` package exposes interfaces
|
|
|
|
* Those interfaces can implemented by the developer and are the business logic of the service.
|
|
* The implementation of those is extensible.
|
|
* The implementation is separated from the generated code.
|
|
|
|
### The `restapi` returns an `http.Handler`
|
|
|
|
The `restapi.Handler` (see [example](./example/restapi/configure_swagger_petstore.go)) function returns
|
|
a standard `http.Handler`
|
|
|
|
* Given objects that implements the business logic, we can create a simple http handler.
|
|
* This handler is standard go http.Handler, so we can now use any other middleware, library, or framework
|
|
that support it.
|
|
* This handler is standard, so we understand it better.
|
|
|
|
## Client
|
|
|
|
* The new client package exposes interfaces, so functions in our code can receive those
|
|
interfaces which can be mocked for testing.
|
|
* The new client has a config that gets an `*url.URL` to customize the endpoint.
|
|
* The new client has a config that gets an `http.RoundTripper` to customize client with libraries, middleware or
|
|
frameworks that support the standard library's objects.
|
|
|
|
# Example Walk-Through
|
|
|
|
In the [example package](https://github.com/Stratoscale/swagger/tree/master/example) you'll find generated code and usage of the pet-store
|
|
[swagger file](./example/swagger.yaml).
|
|
|
|
* The `restapi`, `models` and `client` are auto-generated by the stratoscale/swagger docker file.
|
|
* The `internal` package was manually added and contains the server's business logic.
|
|
* The `main.go` file is the entrypoint and contains initializations and dependency injections of the project.
|
|
|
|
## Server
|
|
|
|
### [restapi](https://github.com/Stratoscale/swagger/tree/master/example/restapi)
|
|
|
|
This package is autogenerated and contains the server routing and parameters parsing.
|
|
|
|
The modified version contains `restapi.PetAPI` and `restapi.StoreAPI` which were auto generated.
|
|
|
|
```go
|
|
// PetAPI
|
|
type PetAPI interface {
|
|
PetCreate(ctx context.Context, params pet.PetCreateParams) middleware.Responder
|
|
PetDelete(ctx context.Context, params pet.PetDeleteParams) middleware.Responder
|
|
PetGet(ctx context.Context, params pet.PetGetParams) middleware.Responder
|
|
PetList(ctx context.Context, params pet.PetListParams) middleware.Responder
|
|
PetUpdate(ctx context.Context, params pet.PetUpdateParams) middleware.Responder
|
|
}
|
|
|
|
//go:generate mockery -name StoreAPI -inpkg
|
|
|
|
// StoreAPI
|
|
type StoreAPI interface {
|
|
InventoryGet(ctx context.Context, params store.InventoryGetParams) middleware.Responder
|
|
OrderCreate(ctx context.Context, params store.OrderCreateParams) middleware.Responder
|
|
// OrderDelete is For valid response try integer IDs with positive integer value. Negative or non-integer values will generate API errors
|
|
OrderDelete(ctx context.Context, params store.OrderDeleteParams) middleware.Responder
|
|
// OrderGet is For valid response try integer IDs with value >= 1 and <= 10. Other values will generated exceptions
|
|
OrderGet(ctx context.Context, params store.OrderGetParams) middleware.Responder
|
|
}
|
|
```
|
|
|
|
Each function matches an `operationId` in the swagger file and they are grouped according to
|
|
the operation `tags`.
|
|
|
|
There is also a `restapi.Config`:
|
|
|
|
```go
|
|
// Config is configuration for Handler
|
|
type Config struct {
|
|
PetAPI
|
|
StoreAPI
|
|
Logger func(string, ...interface{})
|
|
// InnerMiddleware is for the handler executors. These do not apply to the swagger.json document.
|
|
// The middleware executes after routing but before authentication, binding and validation
|
|
InnerMiddleware func(http.Handler) http.Handler
|
|
}
|
|
```
|
|
|
|
This config is auto generated and contains all the declared interfaces above.
|
|
It is used to initiate an http.Handler with the `Handler` function:
|
|
|
|
```go
|
|
// Handler returns an http.Handler given the handler configuration
|
|
// It mounts all the business logic implementers in the right routing.
|
|
func Handler(c Config) (http.Handler, error) {
|
|
...
|
|
```
|
|
|
|
Let's look how we use this generated code to build our server.
|
|
|
|
### [internal](https://github.com/Stratoscale/swagger/tree/master/example/internal)
|
|
|
|
The `internal` package is **not** auto generated and contains the business logic of our server.
|
|
We can see two structs that implements the `restapi.PetAPI` and `restapi.StoreAPI` interfaces,
|
|
needed to make our server work.
|
|
|
|
When adding or removing functions from our REST API, we can just add or remove functions to those
|
|
business logic units. We can also create new logical units when they are added to our REST API.
|
|
|
|
### [main.go](./example/main.go)
|
|
|
|
The main function is pretty straight forward. We initiate our business logic units.
|
|
Then create a config for our rest API. We then create a standard `http.Handler` which we can
|
|
update with middleware, test with `httptest`, or to use with other standard tools.
|
|
The last piece is to run the handler with `http.ListenAndServe` or to use it with an `http.Server` -
|
|
it is all very customizable.
|
|
|
|
```go
|
|
func main() {
|
|
// Initiate business logic implementers.
|
|
// This is the main function, so here the implementers' dependencies can be
|
|
// injected, such as database, parameters from environment variables, or different
|
|
// clients for different APIs.
|
|
p := internal.Pet{}
|
|
s := internal.Store{}
|
|
|
|
// Initiate the http handler, with the objects that are implementing the business logic.
|
|
h, err := restapi.Handler(restapi.Config{
|
|
PetAPI: &p,
|
|
StoreAPI: &s,
|
|
Logger: log.Printf,
|
|
})
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
|
|
// Run the standard http server
|
|
log.Fatal(http.ListenAndServe(":8080", h))
|
|
}
|
|
```
|
|
|
|
## Client
|
|
|
|
The client code is in the [client package](https://github.com/Stratoscale/swagger/tree/master/example/client) and is autogenerated.
|
|
|
|
To create a new client we use the `client.Config` struct:
|
|
|
|
```go
|
|
type Config struct {
|
|
// URL is the base URL of the upstream server
|
|
URL *url.URL
|
|
// Transport is an inner transport for the client
|
|
Transport http.RoundTripper
|
|
}
|
|
```
|
|
|
|
This enables us to use custom server endpoint or custom client middleware. Easily, with the
|
|
standard components, and with any library that accepts them.
|
|
|
|
The client is then generated with the New method:
|
|
|
|
```go
|
|
// New creates a new swagger petstore HTTP client.
|
|
func New(c Config) *SwaggerPetstore { ... }
|
|
```
|
|
|
|
This method returns an object that has two important fields:
|
|
|
|
```go
|
|
type SwaggerPetstore {
|
|
...
|
|
Pet *pet.Client
|
|
Store *store.Client
|
|
}
|
|
```
|
|
|
|
Thos fields are objects, which implements interfaces declared in the [pet](./example/client/pet) and
|
|
[store](./example/client/store) packages:
|
|
|
|
For example:
|
|
|
|
```go
|
|
// API is the interface of the pet client
|
|
type API interface {
|
|
// PetCreate adds a new pet to the store
|
|
PetCreate(ctx context.Context, params *PetCreateParams) (*PetCreateCreated, error)
|
|
// PetDelete deletes a pet
|
|
PetDelete(ctx context.Context, params *PetDeleteParams) (*PetDeleteNoContent, error)
|
|
// PetGet gets pet by it s ID
|
|
PetGet(ctx context.Context, params *PetGetParams) (*PetGetOK, error)
|
|
// PetList lists pets
|
|
PetList(ctx context.Context, params *PetListParams) (*PetListOK, error)
|
|
// PetUpdate updates an existing pet
|
|
PetUpdate(ctx context.Context, params *PetUpdateParams) (*PetUpdateCreated, error)
|
|
}
|
|
```
|
|
|
|
They are very similar to the server interfaces, and can be used by consumers of those APIs
|
|
(instead of using the actual client or the `*Pet` struct)
|
|
|
|
# Authentication
|
|
|
|
Authenticating and policy enforcement of the application is done in several stages, described below.
|
|
|
|
## Define security in swagger.yaml
|
|
|
|
Add to the root of the swagger.yaml the security and security definitions sections.
|
|
|
|
```yaml
|
|
securityDefinitions:
|
|
token:
|
|
type: apiKey
|
|
in: header
|
|
name: Cookie
|
|
|
|
security:
|
|
- token: []
|
|
```
|
|
|
|
The securityDefinitions section defines different security types that your application can handle.
|
|
The supported types by go-swagger are:
|
|
* `apiKey` - token that should be able to processed.
|
|
* `oauth2` - token and scopes that should be processed.
|
|
* and `basic` - user/password that should be processed.
|
|
|
|
Here we defined an apiKey, that is passed through the Cookie header.
|
|
|
|
The `security` section defines the default security enforcement for the application. You can select
|
|
different securityDefinitions, as the keys, and apply "scopes" as the values. Those default definitions
|
|
can be overriden in each route by a section with the same name:
|
|
|
|
```yaml
|
|
paths:
|
|
/pets:
|
|
post:
|
|
[...]
|
|
security:
|
|
- token: [admin]
|
|
```
|
|
|
|
Here we overriden the scope of token in the POST /pets URL so that only admin can use this API.
|
|
|
|
Let's see how we can use this functionality.
|
|
|
|
## Writing Security Handlers
|
|
|
|
Once we created a security definition named "token", a function called "AuthToken" was added to the `restapi.Config`:
|
|
|
|
```go
|
|
type Config struct {
|
|
...
|
|
// AuthToken Applies when the "Cookie" header is set
|
|
AuthToken func(token string) (interface{}, error)
|
|
}
|
|
```
|
|
|
|
This function gets the content of the Cookie header, and should return an `interface{}` and `error`.
|
|
The `interface{}` is the object that should represent the user that performed the request, it should
|
|
be nil to return an unauthorized 401 HTTP response. If the returned `error` is not nil, an HTTP 500,
|
|
internal server error will be returned.
|
|
|
|
The returned object, will be stored in the request context under the `restapi.AuthKey` key.
|
|
|
|
There is another function that we should know about, in the `restapi.Config` struct:
|
|
|
|
```go
|
|
type Config struct {
|
|
...
|
|
// Authorizer is used to authorize a request after the Auth function was called using the "Auth*" functions
|
|
// and the principal was stored in the context in the "AuthKey" context value.
|
|
Authorizer func(*http.Request) error
|
|
}
|
|
```
|
|
|
|
This one is a custom defined function that gets the request and can return an error.
|
|
If the returned error is not nil, and 403 HTTP error will be returned to the client - here the policy
|
|
enforcement comes to place.
|
|
There are two things that this function should be aware of:
|
|
|
|
1. The user - it can retrieve the user information from the context: `ctx.Value(restapi.AuthKey).(MyUserType)`.
|
|
Usually, a server will have a function for extracting this user information and returns a concrete
|
|
type which could be used by all the routes.
|
|
2. The route - it can retrieve the route using the go-swagger function: `middleware.MatchedRouteFrom(*http.Request)`.
|
|
So no need to parse URL and test the request method.
|
|
This route struct contains the route information. If for example, we want to check the scopes that were
|
|
defined for the current route in the swagger.yaml we can use the code below:
|
|
|
|
```go
|
|
for _, auth := range route.Authenticators {
|
|
for scopeName, scopeValues := range auth.Scopes {
|
|
for _, scopeValue := range scopeValues {
|
|
...
|
|
}
|
|
}
|
|
}
|
|
```
|