From 7902bc382d03461040fb0b080728c06be1265f23 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aleksandar=20=C4=8Cekrli=C4=87?= Date: Fri, 26 Apr 2024 23:00:15 +0200 Subject: [PATCH] Swagger docs for the REST API (#141) * Add (incomplete) swagger docs for the REST API * Add information about execution result * Minor tweaks * Remove unused type * Group models * List required fields * Vanilla code generation * Add the config which generates default models (documenting behavior) * Move swagger file closer to the source of all truth * Add a makefile * Split server, client and models * Explicit type names (no anonymous structs) * Changing the approach again because of some codegen corner cases * Pretty well rounded scheme * Update code references * Remove petstore URL * Switch to using new server code --- api/Makefile | 14 + api/b7s-swagger.yaml | 495 ++++++++++++++++++++++++++++++++ api/client.gen.go | 652 +++++++++++++++++++++++++++++++++++++++++++ api/client.yaml | 8 + api/execute.go | 47 +--- api/execute_test.go | 17 +- api/install.go | 20 +- api/install_test.go | 36 +-- api/models.gen.go | 116 ++++++++ api/models.yaml | 7 + api/result.go | 9 +- api/result_test.go | 8 +- api/server.gen.go | 228 +++++++++++++++ api/server.yaml | 15 + cmd/node/main.go | 29 +- go.mod | 8 + go.sum | 19 ++ 17 files changed, 1628 insertions(+), 100 deletions(-) create mode 100644 api/Makefile create mode 100644 api/b7s-swagger.yaml create mode 100644 api/client.gen.go create mode 100644 api/client.yaml create mode 100644 api/models.gen.go create mode 100644 api/models.yaml create mode 100644 api/server.gen.go create mode 100644 api/server.yaml diff --git a/api/Makefile b/api/Makefile new file mode 100644 index 00000000..63af0ce3 --- /dev/null +++ b/api/Makefile @@ -0,0 +1,14 @@ + + +all: server client models + + +server: + oapi-codegen --config ./server.yaml ./b7s-swagger.yaml + +client: + oapi-codegen --config ./client.yaml ./b7s-swagger.yaml + +models: + oapi-codegen --config ./models.yaml ./b7s-swagger.yaml + diff --git a/api/b7s-swagger.yaml b/api/b7s-swagger.yaml new file mode 100644 index 00000000..71f5a2da --- /dev/null +++ b/api/b7s-swagger.yaml @@ -0,0 +1,495 @@ +openapi: 3.0.3 + +info: + title: B7S Node Rest API - OpenAPI 3.0 + description: |- + This is B7S Head Node Server based on the OpenAPI 3.0 specification. + version: 0.4.9 +externalDocs: + description: Find out more about Swagger + url: http://swagger.io +tags: + - name: functions + description: Everything about Blockless Functions + externalDocs: + description: Find out more + url: https://blockless.network/docs/network + - name: health + description: Verify node health and availability + +paths: + /api/v1/health: + get: + tags: + - health + summary: Check Node health + description: Check Node health + operationId: health + responses: + '200': + description: Node is healthy + content: + application/json: + schema: + $ref: '#/components/schemas/HealthStatus' + + /api/v1/functions/execute: + post: + tags: + - functions + summary: Execute a Blockless Function + description: Execute a Blockless Function + operationId: executeFunction + requestBody: + description: Execute a Blockless Function + content: + application/json: + schema: + $ref: '#/components/schemas/ExecutionRequest' + required: true + responses: + '200': + description: Successful execution + content: + application/json: + schema: + $ref: '#/components/schemas/ExecutionResponse' + '400': + description: Invalid execution request + '500': + description: Internal server error + + /api/v1/functions/requests/result: + post: + tags: + - functions + summary: Get the result of an Execution Request + description: Get the result of an Execution Request + operationId: executionResult + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/FunctionResultRequest' + required: true + responses: + '200': + description: Execution result retrieved + content: + application/json: + schema: + $ref: '#/components/schemas/FunctionResultResponse' + '400': + description: Invalid request + '500': + description: Internal server error + + + /api/v1/functions/install: + post: + tags: + - functions + summary: Install a Blockless Function + description: Install a Blockless Function + operationId: installFunction + requestBody: + description: Install a Blockless Function + content: + application/json: + schema: + $ref: '#/components/schemas/FunctionInstallRequest' + required: true + responses: + '200': + description: Installation request acknowledged + content: + application/json: + schema: + $ref: '#/components/schemas/FunctionInstallResponse' + + +# Schema notes: +# - all fields have a x-go-type-skip-optional-pointer - this is because otherwise all fields which arent required are generated as *string instead of a string +# - all types have a Go name explicitly set - this is to avoid inlined structs in certain scenarios + +components: + schemas: + ExecutionRequest: + required: + - function_id + - method + type: object + x-go-type-skip-optional-pointer: true + properties: + function_id: + description: CID of the function + type: string + example: "bafybeia24v4czavtpjv2co3j54o4a5ztduqcpyyinerjgncx7s2s22s7ea" + x-go-type-skip-optional-pointer: true + method: + type: string + example: hello-world.wasm + description: Name of the WASM file to execute + x-go-type-skip-optional-pointer: true + parameters: + type: array + description: CLI arguments for the Blockless Function + items: + $ref: '#/components/schemas/ExecutionParameter' + example: + - value: --cli-flag1 + - value: value1 + - value: --cli-flag2 + - value: value2 + x-go-type-skip-optional-pointer: true + config: + $ref: '#/components/schemas/ExecutionConfig' + topic: + description: In the scenario where workers form subgroups, you can target a specific subgroup by specifying its identifier + type: string + example: "" + x-go-type-skip-optional-pointer: true + + ExecutionParameter: + type: object + required: + - value + x-go-type-skip-optional-pointer: true + x-go-type: execute.Parameter + x-go-type-import: + path: github.com/blocklessnetwork/b7s/models/execute + properties: + value: + type: string + x-go-type-skip-optional-pointer: true + + ExecutionConfig: + description: Configuration options for the Execution Request + type: object + x-go-type-skip-optional-pointer: true + x-go-type: execute.Config + x-go-type-import: + path: github.com/blocklessnetwork/b7s/models/execute + properties: + runtime: + $ref: '#/components/schemas/RuntimeConfig' + env_vars: + description: Environment variables for the Blockless Function + type: array + x-go-type-skip-optional-pointer: true + items: + $ref: '#/components/schemas/NamedValue' + stdin: + description: Standard Input for the Blockless Function + type: string + example: Standard Input for the Blockless Function + x-go-type-skip-optional-pointer: true + permissions: + description: Permissions for the Execution + type: array + x-go-type-skip-optional-pointer: true + items: + type: string + example: "https://api.example.com" + result_aggregation: + $ref: '#/components/schemas/ResultAggregation' + attributes: + $ref: '#/components/schemas/NodeAttributes' + number_of_nodes: + description: Number of nodes that should execute the Blockless Function + type: integer + example: 1 + x-go-type-skip-optional-pointer: true + timeout: + description: How long should the execution take + type: integer + x-go-type-skip-optional-pointer: true + consensus_algorithm: + description: Which consensus algorithm should be formed for this execution + type: string + example: pbft + x-go-type-skip-optional-pointer: true + threshold: + description: Portion of the nodes that should respond with a result to consider this execution successful + type: number + example: 1.0 + x-go-type-skip-optional-pointer: true + + RuntimeConfig: + description: Configuration options for the Blockless Runtime + type: object + x-go-type-skip-optional-pointer: true + x-go-type: execute.BLSRuntimeConfig + x-go-type-import: + path: github.com/blocklessnetwork/b7s/models/execute + properties: + entry: + type: string + example: hello-world.wasm + x-go-type-skip-optional-pointer: true + run_time: + description: How long should the execution take + type: integer + x-go-type-skip-optional-pointer: true + debug_info: + type: boolean + x-go-type-skip-optional-pointer: true + fuel: + type: integer + x-go-type-skip-optional-pointer: true + memory: + description: Memory limit for this execution + type: integer + x-go-type-skip-optional-pointer: true + logger: + type: string + x-go-type-skip-optional-pointer: true + drivers_root_path: + type: string + x-go-type-skip-optional-pointer: true + + NodeAttributes: + description: Attributes that the executing Node should have + type: object + x-go-type-skip-optional-pointer: true + x-go-type: execute.Attributes + x-go-type-import: + path: github.com/blocklessnetwork/b7s/models/execute + properties: + attestation_required: + description: Is it necessary that the Node attributes are vouched for by an attestor + type: boolean + example: false + x-go-type-skip-optional-pointer: true + values: + allOf: + - description: Attributes that the Node should have + - x-go-type-skip-optional-pointer: true + - $ref: '#/components/schemas/NamedValue' + attestors: + $ref: '#/components/schemas/AttributeAttestors' + + AttributeAttestors: + type: object + description: Require specific attestors as vouchers + x-go-type-skip-optional-pointer: true + x-go-type: execute.AttributeAttestors + x-go-type-import: + path: github.com/blocklessnetwork/b7s/models/execute + properties: + each: + description: LibP2P Peer IDs of each mandatory attestor + type: array + items: + type: string + example: + - 12D3KooWRp3AVk7qtc2Av6xiqgAza1ZouksQaYcS2cvN94kHSCoa + - 12D3KooWRp3AVk7qtc2Av6xiqgAza1ZouksQaYcS2cvN94kHSCob + x-go-type-skip-optional-pointer: true + one_of: + description: LibP2P Peer IDs of attestors where we require at least one + type: array + items: + type: string + example: + - 12D3KooWRp3AVk7qtc2Av6xiqgAza1ZouksQaYcS2cvN94kHSCoa + - 12D3KooWRp3AVk7qtc2Av6xiqgAza1ZouksQaYcS2cvN94kHSCob + x-go-type-skip-optional-pointer: true + + ResultAggregation: + type: object + x-go-type-skip-optional-pointer: true + x-go-type: execute.ResultAggregation + x-go-type-import: + path: github.com/blocklessnetwork/b7s/models/execute + properties: + enable: + type: boolean + x-go-type-skip-optional-pointer: true + type: + type: string + x-go-type-skip-optional-pointer: true + parameters: + $ref: '#/components/schemas/NamedValue' + + ExecutionResponse: + type: object + x-go-type-skip-optional-pointer: true + properties: + code: + description: Status of the execution + type: string + example: "200" + x-go-type-skip-optional-pointer: true + request_id: + description: ID of the Execution Request + type: string + example: b6fbbc5e-1d16-4ea9-b557-51f4a6ab565c + x-go-type-skip-optional-pointer: true + message: + description: If the Execution Request failed, this message might have more info about the error + type: string + x-go-type-skip-optional-pointer: true + results: + $ref: '#/components/schemas/AggregatedResults' + cluster: + $ref: '#/components/schemas/NodeCluster' + + AggregatedResults: + description: List of unique results of the Execution Request + type: array + x-go-type-skip-optional-pointer: true + x-go-type: aggregate.Results + x-go-type-import: + path: github.com/blocklessnetwork/b7s/node/aggregate + items: + $ref: '#/components/schemas/AggregatedResult' + + AggregatedResult: + description: Result of an Execution Request + type: object + x-go-type-skip-optional-pointer: true + x-go-type: aggregate.Result + x-go-type-import: + path: github.com/blocklessnetwork/b7s/node/aggregate + properties: + result: + $ref: '#/components/schemas/ExecutionResult' + frequency: + description: Frequency of this result among all nodes that executed the request + type: number + example: 0.66 + x-go-type-skip-optional-pointer: true + x-go-type: float64 + peers: + description: Libp2p IDs of the Nodes that got this result + type: array + items: + type: string + example: + - 12D3KooWRp3AVk7qtc2Av6xiqgAza1ZouksQaYcS2cvN94kHSCoa + - 12D3KooWRp3AVk7qtc2Av6xiqgAza1ZouksQaYcS2cvN94kHSCob + x-go-type-skip-optional-pointer: true + + ExecutionResult: + description: Actual outputs of the execution, like Standard Output, Standard Error, Exit Code etc.. + type: object + x-go-type-skip-optional-pointer: true + x-go-type: execute.RuntimeOutput + x-go-type-import: + path: github.com/blocklessnetwork/b7s/models/execute + properties: + stdout: + description: Standard Output of the execution + type: string + x-go-type-skip-optional-pointer: true + stderr: + description: Standard Error of the execution + type: string + x-go-type-skip-optional-pointer: true + exit_code: + description: Exit code of the execution + type: string + x-go-type-skip-optional-pointer: true + + NodeCluster: + description: Information about the cluster of nodes that executed this request + type: object + x-go-type-skip-optional-pointer: true + x-go-type: execute.Cluster + x-go-type-import: + path: github.com/blocklessnetwork/b7s/models/execute + properties: + main: + description: LibP2P ID of the Primary node for the cluster + type: string + example: 12D3KooWRp3AVk7qtc2Av6xiqgAza1ZouksQaYcS2cvN94kHSCoa + x-go-type-skip-optional-pointer: true + peers: + description: LibP2P IDs of the Nodes in this cluster + type: array + items: + type: string + example: + - 12D3KooWRp3AVk7qtc2Av6xiqgAza1ZouksQaYcS2cvN94kHSCoa + - 12D3KooWRp3AVk7qtc2Av6xiqgAza1ZouksQaYcS2cvN94kHSCob + - 12D3KooWRp3AVk7qtc2Av6xiqgAza1ZouksQaYcS2cvN94kHSCoc + x-go-type-skip-optional-pointer: true + + NamedValue: + description: A key-value pair + type: object + x-go-type-skip-optional-pointer: true + x-go-type: execute.EnvVar + x-go-type-import: + path: github.com/blocklessnetwork/b7s/models/execute + required: + - name + - value + properties: + name: + type: string + example: name + x-go-type-skip-optional-pointer: true + value: + type: string + example: value + x-go-type-skip-optional-pointer: true + + FunctionInstallRequest: + type: object + required: + - cid + x-go-type-skip-optional-pointer: true + properties: + cid: + description: CID of the function + type: string + example: "bafybeia24v4czavtpjv2co3j54o4a5ztduqcpyyinerjgncx7s2s22s7ea" + x-go-type-skip-optional-pointer: true + uri: + type: string + example: "" + x-go-type-skip-optional-pointer: true + topic: + description: In a scenario where workers form subgroups, you can target a specific subgroup by specifying its identifier + type: string + example: "" + x-go-type-skip-optional-pointer: true + + FunctionInstallResponse: + type: object + x-go-type-skip-optional-pointer: true + properties: + code: + type: string + example: "200" + x-go-type-skip-optional-pointer: true + + FunctionResultRequest: + description: Get the result of an Execution Request, identified by the request ID + type: object + required: + - id + x-go-type-skip-optional-pointer: true + properties: + id: + description: ID of the Execution Request + type: string + example: b6fbbc5e-1d16-4ea9-b557-51f4a6ab565c + x-go-type-skip-optional-pointer: true + + FunctionResultResponse: + description: Result of a past Execution + x-go-type: ExecutionResultResponse + $ref: '#/components/schemas/ExecutionResponse' + + HealthStatus: + type: object + description: Node status + x-go-type-skip-optional-pointer: true + properties: + code: + type: string + example: "200" + x-go-type-skip-optional-pointer: true diff --git a/api/client.gen.go b/api/client.gen.go new file mode 100644 index 00000000..beaa94ba --- /dev/null +++ b/api/client.gen.go @@ -0,0 +1,652 @@ +// Package api provides primitives to interact with the openapi HTTP API. +// +// Code generated by github.com/deepmap/oapi-codegen/v2 version v2.1.0 DO NOT EDIT. +package api + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "io" + "net/http" + "net/url" + "strings" +) + +// RequestEditorFn is the function signature for the RequestEditor callback function +type RequestEditorFn func(ctx context.Context, req *http.Request) error + +// Doer performs HTTP requests. +// +// The standard http.Client implements this interface. +type HttpRequestDoer interface { + Do(req *http.Request) (*http.Response, error) +} + +// Client which conforms to the OpenAPI3 specification for this service. +type Client struct { + // The endpoint of the server conforming to this interface, with scheme, + // https://api.deepmap.com for example. This can contain a path relative + // to the server, such as https://api.deepmap.com/dev-test, and all the + // paths in the swagger spec will be appended to the server. + Server string + + // Doer for performing requests, typically a *http.Client with any + // customized settings, such as certificate chains. + Client HttpRequestDoer + + // A list of callbacks for modifying requests which are generated before sending over + // the network. + RequestEditors []RequestEditorFn +} + +// ClientOption allows setting custom parameters during construction +type ClientOption func(*Client) error + +// Creates a new Client, with reasonable defaults +func NewClient(server string, opts ...ClientOption) (*Client, error) { + // create a client with sane default values + client := Client{ + Server: server, + } + // mutate client and add all optional params + for _, o := range opts { + if err := o(&client); err != nil { + return nil, err + } + } + // ensure the server URL always has a trailing slash + if !strings.HasSuffix(client.Server, "/") { + client.Server += "/" + } + // create httpClient, if not already present + if client.Client == nil { + client.Client = &http.Client{} + } + return &client, nil +} + +// WithHTTPClient allows overriding the default Doer, which is +// automatically created using http.Client. This is useful for tests. +func WithHTTPClient(doer HttpRequestDoer) ClientOption { + return func(c *Client) error { + c.Client = doer + return nil + } +} + +// WithRequestEditorFn allows setting up a callback function, which will be +// called right before sending the request. This can be used to mutate the request. +func WithRequestEditorFn(fn RequestEditorFn) ClientOption { + return func(c *Client) error { + c.RequestEditors = append(c.RequestEditors, fn) + return nil + } +} + +// The interface specification for the client above. +type ClientInterface interface { + // ExecuteFunctionWithBody request with any body + ExecuteFunctionWithBody(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) + + ExecuteFunction(ctx context.Context, body ExecuteFunctionJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) + + // InstallFunctionWithBody request with any body + InstallFunctionWithBody(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) + + InstallFunction(ctx context.Context, body InstallFunctionJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) + + // ExecutionResultWithBody request with any body + ExecutionResultWithBody(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) + + ExecutionResult(ctx context.Context, body ExecutionResultJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) + + // Health request + Health(ctx context.Context, reqEditors ...RequestEditorFn) (*http.Response, error) +} + +func (c *Client) ExecuteFunctionWithBody(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewExecuteFunctionRequestWithBody(c.Server, contentType, body) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + +func (c *Client) ExecuteFunction(ctx context.Context, body ExecuteFunctionJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewExecuteFunctionRequest(c.Server, body) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + +func (c *Client) InstallFunctionWithBody(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewInstallFunctionRequestWithBody(c.Server, contentType, body) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + +func (c *Client) InstallFunction(ctx context.Context, body InstallFunctionJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewInstallFunctionRequest(c.Server, body) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + +func (c *Client) ExecutionResultWithBody(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewExecutionResultRequestWithBody(c.Server, contentType, body) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + +func (c *Client) ExecutionResult(ctx context.Context, body ExecutionResultJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewExecutionResultRequest(c.Server, body) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + +func (c *Client) Health(ctx context.Context, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewHealthRequest(c.Server) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + +// NewExecuteFunctionRequest calls the generic ExecuteFunction builder with application/json body +func NewExecuteFunctionRequest(server string, body ExecuteFunctionJSONRequestBody) (*http.Request, error) { + var bodyReader io.Reader + buf, err := json.Marshal(body) + if err != nil { + return nil, err + } + bodyReader = bytes.NewReader(buf) + return NewExecuteFunctionRequestWithBody(server, "application/json", bodyReader) +} + +// NewExecuteFunctionRequestWithBody generates requests for ExecuteFunction with any type of body +func NewExecuteFunctionRequestWithBody(server string, contentType string, body io.Reader) (*http.Request, error) { + var err error + + serverURL, err := url.Parse(server) + if err != nil { + return nil, err + } + + operationPath := fmt.Sprintf("/api/v1/functions/execute") + if operationPath[0] == '/' { + operationPath = "." + operationPath + } + + queryURL, err := serverURL.Parse(operationPath) + if err != nil { + return nil, err + } + + req, err := http.NewRequest("POST", queryURL.String(), body) + if err != nil { + return nil, err + } + + req.Header.Add("Content-Type", contentType) + + return req, nil +} + +// NewInstallFunctionRequest calls the generic InstallFunction builder with application/json body +func NewInstallFunctionRequest(server string, body InstallFunctionJSONRequestBody) (*http.Request, error) { + var bodyReader io.Reader + buf, err := json.Marshal(body) + if err != nil { + return nil, err + } + bodyReader = bytes.NewReader(buf) + return NewInstallFunctionRequestWithBody(server, "application/json", bodyReader) +} + +// NewInstallFunctionRequestWithBody generates requests for InstallFunction with any type of body +func NewInstallFunctionRequestWithBody(server string, contentType string, body io.Reader) (*http.Request, error) { + var err error + + serverURL, err := url.Parse(server) + if err != nil { + return nil, err + } + + operationPath := fmt.Sprintf("/api/v1/functions/install") + if operationPath[0] == '/' { + operationPath = "." + operationPath + } + + queryURL, err := serverURL.Parse(operationPath) + if err != nil { + return nil, err + } + + req, err := http.NewRequest("POST", queryURL.String(), body) + if err != nil { + return nil, err + } + + req.Header.Add("Content-Type", contentType) + + return req, nil +} + +// NewExecutionResultRequest calls the generic ExecutionResult builder with application/json body +func NewExecutionResultRequest(server string, body ExecutionResultJSONRequestBody) (*http.Request, error) { + var bodyReader io.Reader + buf, err := json.Marshal(body) + if err != nil { + return nil, err + } + bodyReader = bytes.NewReader(buf) + return NewExecutionResultRequestWithBody(server, "application/json", bodyReader) +} + +// NewExecutionResultRequestWithBody generates requests for ExecutionResult with any type of body +func NewExecutionResultRequestWithBody(server string, contentType string, body io.Reader) (*http.Request, error) { + var err error + + serverURL, err := url.Parse(server) + if err != nil { + return nil, err + } + + operationPath := fmt.Sprintf("/api/v1/functions/requests/result") + if operationPath[0] == '/' { + operationPath = "." + operationPath + } + + queryURL, err := serverURL.Parse(operationPath) + if err != nil { + return nil, err + } + + req, err := http.NewRequest("POST", queryURL.String(), body) + if err != nil { + return nil, err + } + + req.Header.Add("Content-Type", contentType) + + return req, nil +} + +// NewHealthRequest generates requests for Health +func NewHealthRequest(server string) (*http.Request, error) { + var err error + + serverURL, err := url.Parse(server) + if err != nil { + return nil, err + } + + operationPath := fmt.Sprintf("/api/v1/health") + if operationPath[0] == '/' { + operationPath = "." + operationPath + } + + queryURL, err := serverURL.Parse(operationPath) + if err != nil { + return nil, err + } + + req, err := http.NewRequest("GET", queryURL.String(), nil) + if err != nil { + return nil, err + } + + return req, nil +} + +func (c *Client) applyEditors(ctx context.Context, req *http.Request, additionalEditors []RequestEditorFn) error { + for _, r := range c.RequestEditors { + if err := r(ctx, req); err != nil { + return err + } + } + for _, r := range additionalEditors { + if err := r(ctx, req); err != nil { + return err + } + } + return nil +} + +// ClientWithResponses builds on ClientInterface to offer response payloads +type ClientWithResponses struct { + ClientInterface +} + +// NewClientWithResponses creates a new ClientWithResponses, which wraps +// Client with return type handling +func NewClientWithResponses(server string, opts ...ClientOption) (*ClientWithResponses, error) { + client, err := NewClient(server, opts...) + if err != nil { + return nil, err + } + return &ClientWithResponses{client}, nil +} + +// WithBaseURL overrides the baseURL. +func WithBaseURL(baseURL string) ClientOption { + return func(c *Client) error { + newBaseURL, err := url.Parse(baseURL) + if err != nil { + return err + } + c.Server = newBaseURL.String() + return nil + } +} + +// ClientWithResponsesInterface is the interface specification for the client with responses above. +type ClientWithResponsesInterface interface { + // ExecuteFunctionWithBodyWithResponse request with any body + ExecuteFunctionWithBodyWithResponse(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*ExecuteFunctionResponse, error) + + ExecuteFunctionWithResponse(ctx context.Context, body ExecuteFunctionJSONRequestBody, reqEditors ...RequestEditorFn) (*ExecuteFunctionResponse, error) + + // InstallFunctionWithBodyWithResponse request with any body + InstallFunctionWithBodyWithResponse(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*InstallFunctionResponse, error) + + InstallFunctionWithResponse(ctx context.Context, body InstallFunctionJSONRequestBody, reqEditors ...RequestEditorFn) (*InstallFunctionResponse, error) + + // ExecutionResultWithBodyWithResponse request with any body + ExecutionResultWithBodyWithResponse(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*ExecutionResultResponse, error) + + ExecutionResultWithResponse(ctx context.Context, body ExecutionResultJSONRequestBody, reqEditors ...RequestEditorFn) (*ExecutionResultResponse, error) + + // HealthWithResponse request + HealthWithResponse(ctx context.Context, reqEditors ...RequestEditorFn) (*HealthResponse, error) +} + +type ExecuteFunctionResponse struct { + Body []byte + HTTPResponse *http.Response + JSON200 *ExecutionResponse +} + +// Status returns HTTPResponse.Status +func (r ExecuteFunctionResponse) Status() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Status + } + return http.StatusText(0) +} + +// StatusCode returns HTTPResponse.StatusCode +func (r ExecuteFunctionResponse) StatusCode() int { + if r.HTTPResponse != nil { + return r.HTTPResponse.StatusCode + } + return 0 +} + +type InstallFunctionResponse struct { + Body []byte + HTTPResponse *http.Response + JSON200 *FunctionInstallResponse +} + +// Status returns HTTPResponse.Status +func (r InstallFunctionResponse) Status() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Status + } + return http.StatusText(0) +} + +// StatusCode returns HTTPResponse.StatusCode +func (r InstallFunctionResponse) StatusCode() int { + if r.HTTPResponse != nil { + return r.HTTPResponse.StatusCode + } + return 0 +} + +type ExecutionResultResponse struct { + Body []byte + HTTPResponse *http.Response + JSON200 *FunctionResultResponse +} + +// Status returns HTTPResponse.Status +func (r ExecutionResultResponse) Status() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Status + } + return http.StatusText(0) +} + +// StatusCode returns HTTPResponse.StatusCode +func (r ExecutionResultResponse) StatusCode() int { + if r.HTTPResponse != nil { + return r.HTTPResponse.StatusCode + } + return 0 +} + +type HealthResponse struct { + Body []byte + HTTPResponse *http.Response + JSON200 *HealthStatus +} + +// Status returns HTTPResponse.Status +func (r HealthResponse) Status() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Status + } + return http.StatusText(0) +} + +// StatusCode returns HTTPResponse.StatusCode +func (r HealthResponse) StatusCode() int { + if r.HTTPResponse != nil { + return r.HTTPResponse.StatusCode + } + return 0 +} + +// ExecuteFunctionWithBodyWithResponse request with arbitrary body returning *ExecuteFunctionResponse +func (c *ClientWithResponses) ExecuteFunctionWithBodyWithResponse(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*ExecuteFunctionResponse, error) { + rsp, err := c.ExecuteFunctionWithBody(ctx, contentType, body, reqEditors...) + if err != nil { + return nil, err + } + return ParseExecuteFunctionResponse(rsp) +} + +func (c *ClientWithResponses) ExecuteFunctionWithResponse(ctx context.Context, body ExecuteFunctionJSONRequestBody, reqEditors ...RequestEditorFn) (*ExecuteFunctionResponse, error) { + rsp, err := c.ExecuteFunction(ctx, body, reqEditors...) + if err != nil { + return nil, err + } + return ParseExecuteFunctionResponse(rsp) +} + +// InstallFunctionWithBodyWithResponse request with arbitrary body returning *InstallFunctionResponse +func (c *ClientWithResponses) InstallFunctionWithBodyWithResponse(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*InstallFunctionResponse, error) { + rsp, err := c.InstallFunctionWithBody(ctx, contentType, body, reqEditors...) + if err != nil { + return nil, err + } + return ParseInstallFunctionResponse(rsp) +} + +func (c *ClientWithResponses) InstallFunctionWithResponse(ctx context.Context, body InstallFunctionJSONRequestBody, reqEditors ...RequestEditorFn) (*InstallFunctionResponse, error) { + rsp, err := c.InstallFunction(ctx, body, reqEditors...) + if err != nil { + return nil, err + } + return ParseInstallFunctionResponse(rsp) +} + +// ExecutionResultWithBodyWithResponse request with arbitrary body returning *ExecutionResultResponse +func (c *ClientWithResponses) ExecutionResultWithBodyWithResponse(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*ExecutionResultResponse, error) { + rsp, err := c.ExecutionResultWithBody(ctx, contentType, body, reqEditors...) + if err != nil { + return nil, err + } + return ParseExecutionResultResponse(rsp) +} + +func (c *ClientWithResponses) ExecutionResultWithResponse(ctx context.Context, body ExecutionResultJSONRequestBody, reqEditors ...RequestEditorFn) (*ExecutionResultResponse, error) { + rsp, err := c.ExecutionResult(ctx, body, reqEditors...) + if err != nil { + return nil, err + } + return ParseExecutionResultResponse(rsp) +} + +// HealthWithResponse request returning *HealthResponse +func (c *ClientWithResponses) HealthWithResponse(ctx context.Context, reqEditors ...RequestEditorFn) (*HealthResponse, error) { + rsp, err := c.Health(ctx, reqEditors...) + if err != nil { + return nil, err + } + return ParseHealthResponse(rsp) +} + +// ParseExecuteFunctionResponse parses an HTTP response from a ExecuteFunctionWithResponse call +func ParseExecuteFunctionResponse(rsp *http.Response) (*ExecuteFunctionResponse, error) { + bodyBytes, err := io.ReadAll(rsp.Body) + defer func() { _ = rsp.Body.Close() }() + if err != nil { + return nil, err + } + + response := &ExecuteFunctionResponse{ + Body: bodyBytes, + HTTPResponse: rsp, + } + + switch { + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: + var dest ExecutionResponse + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON200 = &dest + + } + + return response, nil +} + +// ParseInstallFunctionResponse parses an HTTP response from a InstallFunctionWithResponse call +func ParseInstallFunctionResponse(rsp *http.Response) (*InstallFunctionResponse, error) { + bodyBytes, err := io.ReadAll(rsp.Body) + defer func() { _ = rsp.Body.Close() }() + if err != nil { + return nil, err + } + + response := &InstallFunctionResponse{ + Body: bodyBytes, + HTTPResponse: rsp, + } + + switch { + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: + var dest FunctionInstallResponse + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON200 = &dest + + } + + return response, nil +} + +// ParseExecutionResultResponse parses an HTTP response from a ExecutionResultWithResponse call +func ParseExecutionResultResponse(rsp *http.Response) (*ExecutionResultResponse, error) { + bodyBytes, err := io.ReadAll(rsp.Body) + defer func() { _ = rsp.Body.Close() }() + if err != nil { + return nil, err + } + + response := &ExecutionResultResponse{ + Body: bodyBytes, + HTTPResponse: rsp, + } + + switch { + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: + var dest FunctionResultResponse + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON200 = &dest + + } + + return response, nil +} + +// ParseHealthResponse parses an HTTP response from a HealthWithResponse call +func ParseHealthResponse(rsp *http.Response) (*HealthResponse, error) { + bodyBytes, err := io.ReadAll(rsp.Body) + defer func() { _ = rsp.Body.Close() }() + if err != nil { + return nil, err + } + + response := &HealthResponse{ + Body: bodyBytes, + HTTPResponse: rsp, + } + + switch { + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: + var dest HealthStatus + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON200 = &dest + + } + + return response, nil +} diff --git a/api/client.yaml b/api/client.yaml new file mode 100644 index 00000000..ec4da17f --- /dev/null +++ b/api/client.yaml @@ -0,0 +1,8 @@ +output: client.gen.go + +package: api + +generate: + # Generate client boilerplate + client: true + diff --git a/api/execute.go b/api/execute.go index 595f52a2..5cbf5635 100644 --- a/api/execute.go +++ b/api/execute.go @@ -8,54 +8,37 @@ import ( "github.com/labstack/echo/v4" "github.com/blocklessnetwork/b7s/models/blockless" - "github.com/blocklessnetwork/b7s/models/codes" "github.com/blocklessnetwork/b7s/models/execute" "github.com/blocklessnetwork/b7s/node/aggregate" ) -// ExecuteRequest describes the payload for the REST API request for function execution. -type ExecuteRequest struct { - execute.Request - Topic string `json:"topic,omitempty"` -} - -// ExecuteResponse describes the REST API response for function execution. -type ExecuteResponse struct { - Code codes.Code `json:"code,omitempty"` - RequestID string `json:"request_id,omitempty"` - Message string `json:"message,omitempty"` - Results aggregate.Results `json:"results,omitempty"` - Cluster execute.Cluster `json:"cluster,omitempty"` -} - -// ExecuteResult represents the API representation of a single execution response. -// It is similar to the model in `execute.Result`, except it omits the usage information for now. -type ExecuteResult struct { - Code codes.Code `json:"code,omitempty"` - Result execute.RuntimeOutput `json:"result,omitempty"` - RequestID string `json:"request_id,omitempty"` -} - -// Execute implements the REST API endpoint for function execution. -func (a *API) Execute(ctx echo.Context) error { +// ExecuteFunction implements the REST API endpoint for function execution. +func (a *API) ExecuteFunction(ctx echo.Context) error { // Unpack the API request. - var req ExecuteRequest + var req ExecutionRequest err := ctx.Bind(&req) if err != nil { return echo.NewHTTPError(http.StatusBadRequest, fmt.Errorf("could not unpack request: %w", err)) } + exr := execute.Request{ + Config: req.Config, + FunctionID: req.FunctionId, + Method: req.Method, + Parameters: req.Parameters, + } + // Get the execution result. - code, id, results, cluster, err := a.Node.ExecuteFunction(ctx.Request().Context(), execute.Request(req.Request), req.Topic) + code, id, results, cluster, err := a.Node.ExecuteFunction(ctx.Request().Context(), exr, req.Topic) if err != nil { - a.Log.Warn().Str("function", req.FunctionID).Err(err).Msg("node failed to execute function") + a.Log.Warn().Str("function", req.FunctionId).Err(err).Msg("node failed to execute function") } // Transform the node response format to the one returned by the API. - res := ExecuteResponse{ - Code: code, - RequestID: id, + res := ExecutionResponse{ + Code: string(code), + RequestId: id, Results: aggregate.Aggregate(results), Cluster: cluster, } diff --git a/api/execute_test.go b/api/execute_test.go index 1ce5ad06..e00f8d23 100644 --- a/api/execute_test.go +++ b/api/execute_test.go @@ -51,15 +51,15 @@ func TestAPI_Execute(t *testing.T) { rec, ctx, err := setupRecorder(executeEndpoint, req) require.NoError(t, err) - err = srv.Execute(ctx) + err = srv.ExecuteFunction(ctx) require.NoError(t, err) - var res api.ExecuteResponse + var res api.ExecutionResponse require.NoError(t, json.Unmarshal(rec.Body.Bytes(), &res)) require.Equal(t, http.StatusOK, rec.Result().StatusCode) - require.Equal(t, expectedCode, res.Code) + require.Equal(t, expectedCode.String(), res.Code) require.Len(t, res.Cluster.Peers, 1) require.Equal(t, res.Cluster.Peers, peerIDs) @@ -69,7 +69,7 @@ func TestAPI_Execute(t *testing.T) { require.Equal(t, float64(100), res.Results[0].Frequency) require.Equal(t, peerIDs, res.Results[0].Peers) - require.Equal(t, mocks.GenericUUID.String(), res.RequestID) + require.Equal(t, mocks.GenericUUID.String(), res.RequestId) } func TestAPI_Execute_HandlesErrors(t *testing.T) { @@ -101,15 +101,15 @@ func TestAPI_Execute_HandlesErrors(t *testing.T) { rec, ctx, err := setupRecorder(executeEndpoint, req) require.NoError(t, err) - err = srv.Execute(ctx) + err = srv.ExecuteFunction(ctx) require.NoError(t, err) - var res api.ExecuteResponse + var res api.ExecutionResponse err = json.Unmarshal(rec.Body.Bytes(), &res) require.NoError(t, err) require.Equal(t, http.StatusOK, rec.Result().StatusCode) - require.Equal(t, expectedCode, res.Code) + require.Equal(t, expectedCode.String(), res.Code) require.Len(t, res.Results, 1) require.Equal(t, executionResult.Result, res.Results[0].Result) @@ -121,7 +121,6 @@ func TestAPI_Execute_HandlesErrors(t *testing.T) { func TestAPI_Execute_HandlesMalformedRequests(t *testing.T) { api := setupAPI(t) - _ = api const ( wrongFieldType = ` @@ -180,7 +179,7 @@ func TestAPI_Execute_HandlesMalformedRequests(t *testing.T) { _, ctx, err := setupRecorder(executeEndpoint, test.payload, prepare) require.NoError(t, err) - err = api.Execute(ctx) + err = api.ExecuteFunction(ctx) require.Error(t, err) echoErr, ok := err.(*echo.HTTPError) diff --git a/api/install.go b/api/install.go index 2e49f133..825c369f 100644 --- a/api/install.go +++ b/api/install.go @@ -15,28 +15,16 @@ const ( functionInstallTimeout = 10 * time.Second ) -// InstallFunctionRequest describes the payload for the REST API request for function install. -type InstallFunctionRequest struct { - CID string `json:"cid"` - URI string `json:"uri"` - Subgroup string `json:"subgroup"` -} - -// InstallFunctionResponse describes the REST API response for the function install. -type InstallFunctionResponse struct { - Code string `json:"code"` -} - -func (a *API) Install(ctx echo.Context) error { +func (a *API) InstallFunction(ctx echo.Context) error { // Unpack the API request. - var req InstallFunctionRequest + var req FunctionInstallRequest err := ctx.Bind(&req) if err != nil { return echo.NewHTTPError(http.StatusBadRequest, fmt.Errorf("could not unpack request: %w", err)) } - if req.URI == "" && req.CID == "" { + if req.Uri == "" && req.Cid == "" { return echo.NewHTTPError(http.StatusBadRequest, errors.New("URI or CID are required")) } @@ -47,7 +35,7 @@ func (a *API) Install(ctx echo.Context) error { // Start function install in a separate goroutine and signal when it's done. fnErr := make(chan error) go func() { - err = a.Node.PublishFunctionInstall(reqCtx, req.URI, req.CID, req.Subgroup) + err = a.Node.PublishFunctionInstall(reqCtx, req.Uri, req.Cid, req.Topic) fnErr <- err }() diff --git a/api/install_test.go b/api/install_test.go index 13acd073..3111bf8c 100644 --- a/api/install_test.go +++ b/api/install_test.go @@ -19,9 +19,9 @@ func TestAPI_FunctionInstall(t *testing.T) { t.Run("nominal case", func(t *testing.T) { t.Parallel() - req := api.InstallFunctionRequest{ - URI: "dummy-function-id", - CID: "dummy-cid", + req := api.FunctionInstallRequest{ + Uri: "dummy-function-id", + Cid: "dummy-cid", } srv := setupAPI(t) @@ -29,7 +29,7 @@ func TestAPI_FunctionInstall(t *testing.T) { rec, ctx, err := setupRecorder(installEndpoint, req) require.NoError(t, err) - err = srv.Install(ctx) + err = srv.InstallFunction(ctx) require.NoError(t, err) require.Equal(t, http.StatusOK, rec.Result().StatusCode) @@ -40,9 +40,9 @@ func TestAPI_FunctionInstall_HandlesErrors(t *testing.T) { t.Run("missing URI and CID", func(t *testing.T) { t.Parallel() - req := api.InstallFunctionRequest{ - URI: "", - CID: "", + req := api.FunctionInstallRequest{ + Uri: "", + Cid: "", } srv := setupAPI(t) @@ -50,7 +50,7 @@ func TestAPI_FunctionInstall_HandlesErrors(t *testing.T) { _, ctx, err := setupRecorder(installEndpoint, req) require.NoError(t, err) - err = srv.Install(ctx) + err = srv.InstallFunction(ctx) require.Error(t, err) echoErr, ok := err.(*echo.HTTPError) @@ -72,9 +72,9 @@ func TestAPI_FunctionInstall_HandlesErrors(t *testing.T) { return nil } - req := api.InstallFunctionRequest{ - URI: "dummy-uri", - CID: "dummy-cid", + req := api.FunctionInstallRequest{ + Uri: "dummy-uri", + Cid: "dummy-cid", } srv := api.New(mocks.NoopLogger, node) @@ -82,12 +82,12 @@ func TestAPI_FunctionInstall_HandlesErrors(t *testing.T) { rec, ctx, err := setupRecorder(installEndpoint, req) require.NoError(t, err) - err = srv.Install(ctx) + err = srv.InstallFunction(ctx) require.NoError(t, err) require.Equal(t, http.StatusOK, rec.Result().StatusCode) - var res api.InstallFunctionResponse + var res api.FunctionInstallResponse require.NoError(t, json.Unmarshal(rec.Body.Bytes(), &res)) num, err := strconv.Atoi(res.Code) @@ -105,15 +105,15 @@ func TestAPI_FunctionInstall_HandlesErrors(t *testing.T) { srv := api.New(mocks.NoopLogger, node) - req := api.InstallFunctionRequest{ - URI: "dummy-uri", - CID: "dummy-cid", + req := api.FunctionInstallRequest{ + Uri: "dummy-uri", + Cid: "dummy-cid", } _, ctx, err := setupRecorder(installEndpoint, req) require.NoError(t, err) - err = srv.Install(ctx) + err = srv.InstallFunction(ctx) require.Error(t, err) echoErr, ok := err.(*echo.HTTPError) @@ -181,7 +181,7 @@ func TestAPI_InstallFunction_HandlesMalformedRequests(t *testing.T) { _, ctx, err := setupRecorder(installEndpoint, test.payload, prepare) require.NoError(t, err) - err = srv.Install(ctx) + err = srv.InstallFunction(ctx) require.Error(t, err) echoErr, ok := err.(*echo.HTTPError) diff --git a/api/models.gen.go b/api/models.gen.go new file mode 100644 index 00000000..614c3a52 --- /dev/null +++ b/api/models.gen.go @@ -0,0 +1,116 @@ +// Package api provides primitives to interact with the openapi HTTP API. +// +// Code generated by github.com/deepmap/oapi-codegen/v2 version v2.1.0 DO NOT EDIT. +package api + +import ( + "github.com/blocklessnetwork/b7s/models/execute" + "github.com/blocklessnetwork/b7s/node/aggregate" +) + +// AggregatedResult Result of an Execution Request +type AggregatedResult = aggregate.Result + +// AggregatedResults List of unique results of the Execution Request +type AggregatedResults = aggregate.Results + +// AttributeAttestors Require specific attestors as vouchers +type AttributeAttestors = execute.AttributeAttestors + +// ExecutionConfig Configuration options for the Execution Request +type ExecutionConfig = execute.Config + +// ExecutionParameter defines model for ExecutionParameter. +type ExecutionParameter = execute.Parameter + +// ExecutionRequest defines model for ExecutionRequest. +type ExecutionRequest struct { + // Config Configuration options for the Execution Request + Config ExecutionConfig `json:"config,omitempty"` + + // FunctionId CID of the function + FunctionId string `json:"function_id"` + + // Method Name of the WASM file to execute + Method string `json:"method"` + + // Parameters CLI arguments for the Blockless Function + Parameters []ExecutionParameter `json:"parameters,omitempty"` + + // Topic In the scenario where workers form subgroups, you can target a specific subgroup by specifying its identifier + Topic string `json:"topic,omitempty"` +} + +// ExecutionResponse defines model for ExecutionResponse. +type ExecutionResponse struct { + // Cluster Information about the cluster of nodes that executed this request + Cluster NodeCluster `json:"cluster,omitempty"` + + // Code Status of the execution + Code string `json:"code,omitempty"` + + // Message If the Execution Request failed, this message might have more info about the error + Message string `json:"message,omitempty"` + + // RequestId ID of the Execution Request + RequestId string `json:"request_id,omitempty"` + + // Results List of unique results of the Execution Request + Results AggregatedResults `json:"results,omitempty"` +} + +// ExecutionResult Actual outputs of the execution, like Standard Output, Standard Error, Exit Code etc.. +type ExecutionResult = execute.RuntimeOutput + +// FunctionInstallRequest defines model for FunctionInstallRequest. +type FunctionInstallRequest struct { + // Cid CID of the function + Cid string `json:"cid"` + + // Topic In a scenario where workers form subgroups, you can target a specific subgroup by specifying its identifier + Topic string `json:"topic,omitempty"` + Uri string `json:"uri,omitempty"` +} + +// FunctionInstallResponse defines model for FunctionInstallResponse. +type FunctionInstallResponse struct { + Code string `json:"code,omitempty"` +} + +// FunctionResultRequest Get the result of an Execution Request, identified by the request ID +type FunctionResultRequest struct { + // Id ID of the Execution Request + Id string `json:"id"` +} + +// FunctionResultResponse defines model for FunctionResultResponse. +type FunctionResultResponse = ExecutionResponse + +// HealthStatus Node status +type HealthStatus struct { + Code string `json:"code,omitempty"` +} + +// NamedValue A key-value pair +type NamedValue = execute.EnvVar + +// NodeAttributes Attributes that the executing Node should have +type NodeAttributes = execute.Attributes + +// NodeCluster Information about the cluster of nodes that executed this request +type NodeCluster = execute.Cluster + +// ResultAggregation defines model for ResultAggregation. +type ResultAggregation = execute.ResultAggregation + +// RuntimeConfig Configuration options for the Blockless Runtime +type RuntimeConfig = execute.BLSRuntimeConfig + +// ExecuteFunctionJSONRequestBody defines body for ExecuteFunction for application/json ContentType. +type ExecuteFunctionJSONRequestBody = ExecutionRequest + +// InstallFunctionJSONRequestBody defines body for InstallFunction for application/json ContentType. +type InstallFunctionJSONRequestBody = FunctionInstallRequest + +// ExecutionResultJSONRequestBody defines body for ExecutionResult for application/json ContentType. +type ExecutionResultJSONRequestBody = FunctionResultRequest diff --git a/api/models.yaml b/api/models.yaml new file mode 100644 index 00000000..0e14aa03 --- /dev/null +++ b/api/models.yaml @@ -0,0 +1,7 @@ +output: models.gen.go + +package: api + +generate: + # Generate models + models: true diff --git a/api/result.go b/api/result.go index f0b784e6..9c741818 100644 --- a/api/result.go +++ b/api/result.go @@ -8,22 +8,17 @@ import ( "github.com/labstack/echo/v4" ) -// ExecutionResultRequest describes the payload for the REST API request for execution result. -type ExecutionResultRequest struct { - ID string `json:"id"` -} - // ExecutionResult implements the REST API endpoint for retrieving the result of a function execution. func (a *API) ExecutionResult(ctx echo.Context) error { // Get the request ID. - var request ExecutionResultRequest + var request FunctionResultRequest err := ctx.Bind(&request) if err != nil { return echo.NewHTTPError(http.StatusBadRequest, fmt.Errorf("could not unpack request: %w", err)) } - requestID := request.ID + requestID := request.Id if requestID == "" { return echo.NewHTTPError(http.StatusBadRequest, errors.New("missing request ID")) } diff --git a/api/result_test.go b/api/result_test.go index 7ba3bfb4..827fadcb 100644 --- a/api/result_test.go +++ b/api/result_test.go @@ -19,8 +19,8 @@ func TestAPI_ExecutionResult(t *testing.T) { srv := setupAPI(t) - req := api.ExecutionResultRequest{ - ID: mocks.GenericString, + req := api.FunctionResultRequest{ + Id: mocks.GenericString, } rec, ctx, err := setupRecorder(resultEndpoint, req) @@ -44,8 +44,8 @@ func TestAPI_ExecutionResult(t *testing.T) { srv := api.New(mocks.NoopLogger, node) - req := api.ExecutionResultRequest{ - ID: "dummy-request-id", + req := api.FunctionResultRequest{ + Id: "dummy-request-id", } rec, ctx, err := setupRecorder(resultEndpoint, req) diff --git a/api/server.gen.go b/api/server.gen.go new file mode 100644 index 00000000..5c905080 --- /dev/null +++ b/api/server.gen.go @@ -0,0 +1,228 @@ +// Package api provides primitives to interact with the openapi HTTP API. +// +// Code generated by github.com/deepmap/oapi-codegen/v2 version v2.1.0 DO NOT EDIT. +package api + +import ( + "bytes" + "compress/gzip" + "encoding/base64" + "fmt" + "net/url" + "path" + "strings" + + "github.com/getkin/kin-openapi/openapi3" + "github.com/labstack/echo/v4" +) + +// ServerInterface represents all server handlers. +type ServerInterface interface { + // Execute a Blockless Function + // (POST /api/v1/functions/execute) + ExecuteFunction(ctx echo.Context) error + // Install a Blockless Function + // (POST /api/v1/functions/install) + InstallFunction(ctx echo.Context) error + // Get the result of an Execution Request + // (POST /api/v1/functions/requests/result) + ExecutionResult(ctx echo.Context) error + // Check Node health + // (GET /api/v1/health) + Health(ctx echo.Context) error +} + +// ServerInterfaceWrapper converts echo contexts to parameters. +type ServerInterfaceWrapper struct { + Handler ServerInterface +} + +// ExecuteFunction converts echo context to params. +func (w *ServerInterfaceWrapper) ExecuteFunction(ctx echo.Context) error { + var err error + + // Invoke the callback with all the unmarshaled arguments + err = w.Handler.ExecuteFunction(ctx) + return err +} + +// InstallFunction converts echo context to params. +func (w *ServerInterfaceWrapper) InstallFunction(ctx echo.Context) error { + var err error + + // Invoke the callback with all the unmarshaled arguments + err = w.Handler.InstallFunction(ctx) + return err +} + +// ExecutionResult converts echo context to params. +func (w *ServerInterfaceWrapper) ExecutionResult(ctx echo.Context) error { + var err error + + // Invoke the callback with all the unmarshaled arguments + err = w.Handler.ExecutionResult(ctx) + return err +} + +// Health converts echo context to params. +func (w *ServerInterfaceWrapper) Health(ctx echo.Context) error { + var err error + + // Invoke the callback with all the unmarshaled arguments + err = w.Handler.Health(ctx) + return err +} + +// This is a simple interface which specifies echo.Route addition functions which +// are present on both echo.Echo and echo.Group, since we want to allow using +// either of them for path registration +type EchoRouter interface { + CONNECT(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) *echo.Route + DELETE(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) *echo.Route + GET(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) *echo.Route + HEAD(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) *echo.Route + OPTIONS(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) *echo.Route + PATCH(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) *echo.Route + POST(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) *echo.Route + PUT(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) *echo.Route + TRACE(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) *echo.Route +} + +// RegisterHandlers adds each server route to the EchoRouter. +func RegisterHandlers(router EchoRouter, si ServerInterface) { + RegisterHandlersWithBaseURL(router, si, "") +} + +// Registers handlers, and prepends BaseURL to the paths, so that the paths +// can be served under a prefix. +func RegisterHandlersWithBaseURL(router EchoRouter, si ServerInterface, baseURL string) { + + wrapper := ServerInterfaceWrapper{ + Handler: si, + } + + router.POST(baseURL+"/api/v1/functions/execute", wrapper.ExecuteFunction) + router.POST(baseURL+"/api/v1/functions/install", wrapper.InstallFunction) + router.POST(baseURL+"/api/v1/functions/requests/result", wrapper.ExecutionResult) + router.GET(baseURL+"/api/v1/health", wrapper.Health) + +} + +// Base64 encoded, gzipped, json marshaled Swagger object +var swaggerSpec = []string{ + + "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", + "//9f2L021TUAAA==", +} + +// GetSwagger returns the content of the embedded swagger specification file +// or error if failed to decode +func decodeSpec() ([]byte, error) { + zipped, err := base64.StdEncoding.DecodeString(strings.Join(swaggerSpec, "")) + if err != nil { + return nil, fmt.Errorf("error base64 decoding spec: %w", err) + } + zr, err := gzip.NewReader(bytes.NewReader(zipped)) + if err != nil { + return nil, fmt.Errorf("error decompressing spec: %w", err) + } + var buf bytes.Buffer + _, err = buf.ReadFrom(zr) + if err != nil { + return nil, fmt.Errorf("error decompressing spec: %w", err) + } + + return buf.Bytes(), nil +} + +var rawSpec = decodeSpecCached() + +// a naive cached of a decoded swagger spec +func decodeSpecCached() func() ([]byte, error) { + data, err := decodeSpec() + return func() ([]byte, error) { + return data, err + } +} + +// Constructs a synthetic filesystem for resolving external references when loading openapi specifications. +func PathToRawSpec(pathToFile string) map[string]func() ([]byte, error) { + res := make(map[string]func() ([]byte, error)) + if len(pathToFile) > 0 { + res[pathToFile] = rawSpec + } + + return res +} + +// GetSwagger returns the Swagger specification corresponding to the generated code +// in this file. The external references of Swagger specification are resolved. +// The logic of resolving external references is tightly connected to "import-mapping" feature. +// Externally referenced files must be embedded in the corresponding golang packages. +// Urls can be supported but this task was out of the scope. +func GetSwagger() (swagger *openapi3.T, err error) { + resolvePath := PathToRawSpec("") + + loader := openapi3.NewLoader() + loader.IsExternalRefsAllowed = true + loader.ReadFromURIFunc = func(loader *openapi3.Loader, url *url.URL) ([]byte, error) { + pathToFile := url.String() + pathToFile = path.Clean(pathToFile) + getSpec, ok := resolvePath[pathToFile] + if !ok { + err1 := fmt.Errorf("path not found: %s", pathToFile) + return nil, err1 + } + return getSpec() + } + var specData []byte + specData, err = rawSpec() + if err != nil { + return + } + swagger, err = loader.LoadFromData(specData) + if err != nil { + return + } + return +} diff --git a/api/server.yaml b/api/server.yaml new file mode 100644 index 00000000..dc9f2f26 --- /dev/null +++ b/api/server.yaml @@ -0,0 +1,15 @@ +output: server.gen.go + +package: api + +generate: + # Generate Echo server boilerplate + echo-server: true + + # Generate strict server wrapper - this will parse request bodies and encode responses + strict-server: false + + # Embed swagger spec in the generated code + embedded-spec: true + + diff --git a/cmd/node/main.go b/cmd/node/main.go index 11cddc09..55e76b11 100644 --- a/cmd/node/main.go +++ b/cmd/node/main.go @@ -261,22 +261,11 @@ func run() int { } // Create echo server and iniialize logging. - server := echo.New() - server.HideBanner = true - server.HidePort = true - - elog := lecho.From(log) - server.Logger = elog - server.Use(lecho.Middleware(lecho.Config{Logger: elog})) + server := createEchoServer(log) // Create an API handler. - api := api.New(log, node) - - // Set endpoint handlers. - server.GET("/api/v1/health", api.Health) - server.POST("/api/v1/functions/execute", api.Execute) - server.POST("/api/v1/functions/install", api.Install) - server.POST("/api/v1/functions/requests/result", api.ExecutionResult) + apiHandler := api.New(log, node) + api.RegisterHandlers(server, apiHandler) // Start API in a separate goroutine. go func() { @@ -314,6 +303,18 @@ func run() int { return success } +func createEchoServer(log zerolog.Logger) *echo.Echo { + server := echo.New() + server.HideBanner = true + server.HidePort = true + + elog := lecho.From(log) + server.Logger = elog + server.Use(lecho.Middleware(lecho.Config{Logger: elog})) + + return server +} + func needLimiter(cfg *config.Config) bool { return (cfg.Worker.CPUPercentageLimit > 0 && cfg.Worker.CPUPercentageLimit < 1.0) || cfg.Worker.MemoryLimitKB > 0 } diff --git a/go.mod b/go.mod index 2656e253..aa44d5b6 100644 --- a/go.mod +++ b/go.mod @@ -12,6 +12,7 @@ require ( github.com/containerd/cgroups/v3 v3.0.3 github.com/fatih/camelcase v1.0.0 github.com/fatih/color v1.16.0 + github.com/getkin/kin-openapi v0.124.0 github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 github.com/hashicorp/go-hclog v1.3.0 github.com/hashicorp/raft v1.4.0 @@ -47,6 +48,8 @@ require ( github.com/getsentry/sentry-go v0.26.0 // indirect github.com/go-logr/logr v1.4.1 // indirect github.com/go-logr/stdr v1.2.2 // indirect + github.com/go-openapi/jsonpointer v0.20.2 // indirect + github.com/go-openapi/swag v0.22.8 // indirect github.com/go-viper/mapstructure/v2 v2.0.0-alpha.1 // indirect github.com/golang-jwt/jwt v3.2.2+incompatible // indirect github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd // indirect @@ -54,15 +57,20 @@ require ( github.com/hashicorp/go-immutable-radix v1.3.1 // indirect github.com/hashicorp/go-msgpack v0.5.5 // indirect github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect + github.com/invopop/yaml v0.2.0 // indirect + github.com/josharian/intern v1.0.0 // indirect github.com/knadh/koanf/maps v0.1.1 // indirect github.com/labstack/gommon v0.4.2 // indirect github.com/libp2p/go-libp2p-consensus v0.0.1 // indirect github.com/libp2p/go-libp2p-gostream v0.6.0 // indirect github.com/libp2p/go-libp2p-routing-helpers v0.7.3 // indirect + github.com/mailru/easyjson v0.7.7 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mitchellh/copystructure v1.2.0 // indirect github.com/mitchellh/reflectwalk v1.0.2 // indirect + github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect github.com/onsi/ginkgo/v2 v2.17.1 // indirect + github.com/perimeterx/marshmallow v1.1.5 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/quic-go/qpack v0.4.0 // indirect github.com/quic-go/quic-go v0.42.0 // indirect diff --git a/go.sum b/go.sum index 458aadd7..d001ad5c 100644 --- a/go.sum +++ b/go.sum @@ -114,6 +114,8 @@ github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7z github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= +github.com/getkin/kin-openapi v0.124.0 h1:VSFNMB9C9rTKBnQ/fpyDU8ytMTr4dWI9QovSKj9kz/M= +github.com/getkin/kin-openapi v0.124.0/go.mod h1:wb1aSZA/iWmorQP9KTAS/phLj/t17B5jT7+fS8ed9NM= github.com/getsentry/sentry-go v0.26.0 h1:IX3++sF6/4B5JcevhdZfdKIHfyvMmAq/UnqcyT2H6mA= github.com/getsentry/sentry-go v0.26.0/go.mod h1:lc76E2QywIyW8WuBnwl8Lc4bkmQH4+w1gwTf25trprY= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= @@ -130,9 +132,15 @@ github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ= github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-openapi/jsonpointer v0.20.2 h1:mQc3nmndL8ZBzStEo3JYF8wzmeWffDH4VbXz58sAx6Q= +github.com/go-openapi/jsonpointer v0.20.2/go.mod h1:bHen+N0u1KEO3YlmqOjTT9Adn1RfD91Ar825/PuiRVs= +github.com/go-openapi/swag v0.22.8 h1:/9RjDSQ0vbFR+NyjGMkFTsA1IA0fmhKSThmfGZjicbw= +github.com/go-openapi/swag v0.22.8/go.mod h1:6QT22icPLEqAM/z/TChgb4WAveCHF92+2gF0CNjHpPI= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls= +github.com/go-test/deep v1.0.8 h1:TDsG77qcSprGbC6vTN8OuXp5g+J+b5Pcguhf7Zt61VM= +github.com/go-test/deep v1.0.8/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE= github.com/go-viper/mapstructure/v2 v2.0.0-alpha.1 h1:TQcrn6Wq+sKGkpyPvppOz99zsMBaUOKXq6HSv655U1c= github.com/go-viper/mapstructure/v2 v2.0.0-alpha.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= github.com/go-yaml/yaml v2.1.0+incompatible/go.mod h1:w2MrLa16VYP0jy6N7M5kHaCkaLENm+P+Tv+MfurjSw0= @@ -231,6 +239,8 @@ github.com/hashicorp/raft-boltdb/v2 v2.2.2 h1:rlkPtOllgIcKLxVT4nutqlTH2NRFn+tO1w github.com/hashicorp/raft-boltdb/v2 v2.2.2/go.mod h1:N8YgaZgNJLpZC+h+by7vDu5rzsRgONThTEeUS3zWbfY= github.com/huin/goupnp v1.3.0 h1:UvLUlWDNpoUdYzb2TCn+MuTWtcjXKSza2n6CBdQ0xXc= github.com/huin/goupnp v1.3.0/go.mod h1:gnGPsThkYa7bFi/KWmEysQRf48l2dvR5bxr2OFckNX8= +github.com/invopop/yaml v0.2.0 h1:7zky/qH+O0DwAyoobXUqvVBwgBFRxKoQ/3FjcVpjTMY= +github.com/invopop/yaml v0.2.0/go.mod h1:2XuRLgs/ouIrW3XNzuNj7J3Nvu/Dig5MXvbCEdiBN3Q= github.com/ipfs/boxo v0.17.0 h1:fVXAb12dNbraCX1Cdid5BB6Kl62gVLNVA+e0EYMqAU0= github.com/ipfs/boxo v0.17.0/go.mod h1:pIZgTWdm3k3pLF9Uq6MB8JEcW07UDwNJjlXW1HELW80= github.com/ipfs/go-cid v0.4.1 h1:A/T3qGvxi4kpKWWcPC/PgbvDA2bjVLO7n4UeVwnbs/s= @@ -256,6 +266,8 @@ github.com/jbenet/go-temp-err-catcher v0.1.0/go.mod h1:0kJRvmDZXNMIiJirNPEYfhpPw github.com/jbenet/goprocess v0.1.4 h1:DRGOFReOMqqDNXwW70QkacFW0YN9QnwLV0Vqk+3oU0o= github.com/jbenet/goprocess v0.1.4/go.mod h1:5yspPrukOVuOLORacaBi858NqyClJPQxYZlqdZVfqY4= github.com/jellevandenhooff/dkim v0.0.0-20150330215556-f50fe3d243e1/go.mod h1:E0B/fFc00Y+Rasa88328GlI/XbtyysCtTHZS8h7IrBU= +github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= @@ -339,6 +351,8 @@ github.com/libp2p/go-yamux/v4 v4.0.1 h1:FfDR4S1wj6Bw2Pqbc8Uz7pCxeRBPbwsBbEdfwiCy github.com/libp2p/go-yamux/v4 v4.0.1/go.mod h1:NWjl8ZTLOGlozrXSOZ/HlfG++39iKNnM5wwmtQP1YB4= github.com/lunixbochs/vtclean v1.0.0/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI= github.com/mailru/easyjson v0.0.0-20190312143242-1de009706dbe/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= +github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/marten-seemann/tcp v0.0.0-20210406111302-dfbc87cc63fd h1:br0buuQ854V8u83wA0rVZ8ttrq5CpaPZdvrK0LP2lOk= github.com/marten-seemann/tcp v0.0.0-20210406111302-dfbc87cc63fd/go.mod h1:QuCEs1Nt24+FYQEqAAncTDPJIuGs+LxK1MCiFL25pMU= github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= @@ -374,6 +388,8 @@ github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw= +github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8= github.com/mr-tron/base58 v1.1.2/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc= github.com/mr-tron/base58 v1.2.0 h1:T/HDJBh4ZCPbU39/+c3rRvE0uKBQlU27+QI8LJ4t64o= github.com/mr-tron/base58 v1.2.0/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc= @@ -418,6 +434,8 @@ github.com/pascaldekloe/goe v0.1.0 h1:cBOtyMzM9HTpWjXfbbunk26uA6nG3a8n06Wieeh0Mw github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 h1:onHthvaw9LFnH4t2DcNVpwGmV9E1BkGknEliJkfwQj0= github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58/go.mod h1:DXv8WO4yhMYhSNPKjeNKa5WY9YCIEBRbNzFFPJbWO6Y= +github.com/perimeterx/marshmallow v1.1.5 h1:a2LALqQ1BlHM8PZblsDdidgv1mWi1DgC2UmX50IvK2s= +github.com/perimeterx/marshmallow v1.1.5/go.mod h1:dsXbUu8CRzfYP5a87xpp0xq9S3u0Vchtcl8we9tYaXw= github.com/pingcap/errors v0.11.4 h1:lFuQV/oaUMGcD2tqt+01ROSmJs75VG1ToEOkZIZ4nE4= github.com/pingcap/errors v0.11.4/go.mod h1:Oi8TUi2kEtXXLMJk9l1cGmz20kV3TaQ0usTwv5KuLY8= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= @@ -773,6 +791,7 @@ gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= grpc.go4.org v0.0.0-20170609214715-11d0a25b4919/go.mod h1:77eQGdRu53HpSqPFJFmuJdjuHRquDANNeA4x7B8WQ9o=