REST APIs#

This section covers the process of generating REST APIs for a new resource. Let’s continue with the example of the localaccount resource.

OAPI Definitions#

The first step is to define the new resource in the OpenAPI definitions. To define the new resource, do the following:

  1. Go to the OAPI files that are located at api/openapi folder. The resources are defined in the schemas folder.

  2. Create a new file called localaccount.yaml and ensure that the definition looks as follows and contains all the necessary abstractions:

    LocalAccount:
        description: "A local account resource."
        type: object
        required:
            - username
            - sshKey
        properties:
            resourceId:
            type: string
            pattern: "^localaccount-[0-9a-f]{8}$"
            description: "resource ID, generated by the inventory on Create."
            readOnly: true
            username:
            description: "The local account's username."
            type: string
            maxLength: 32
            pattern: "^[a-z][a-z0-9-]{0,31}$"
            sshkey:
            description: "The local account's sshkey."
            type: string
            maxLength: 800
            pattern: "^(ssh-(rsa|dss|ed25519)|ecdsa-sha2-nistp(256|384|521)) ([A-Za-z0-9+/=]+) ?(.*)?$"
            timestamps:
            "$ref": "./common.yaml#/Timestamps"
            readOnly: true
        title: LocalAccount
    

The LocalAccount object is defined with the necessary properties and constraints. They reflect the protobuf definition as closely as possible. Also, remember to register the new resources in _index.yaml file.

The second step is to write the API paths which basically will define the allowed operation on the resource and the attributes/properties that can be modified through the request. For example, the paths allow to specify the readOnly property considering a specific operation.

The paths are defined in the paths folder. Create a new file called localaccount.yaml:

localaccounts:
    get:
        description: "Gets all local account objects."
        parameters:
        ...
    post:
        description: "Create a Local account instance"
        tags:
            - LocalAccount
        requestBody:
            required: true
            content:
                application/json:
                schema:
                    "$ref": "../schemas/_index.yaml#/LocalAccount"
        ...

localaccountId:
    parameters:
        - name: localaccountID
        ...
    get:
        description: "Get a local account object by ID."
        ...
    delete:
        description: "Delete a local account object by ID."
        ...

Before generating the Golang* code, ensure to register the new paths in the edge-infrastructure-manager-openapi.yaml file.

Code and Documentation Generation#

Assuming that the protobuf definition is ready, the next step is to generate the code and the documentation. API Server exposes several make targets that assist the developer journey. Use the following command to generate the code and the documentation:

make generate

After running the command, the generated code and documentation are available in the respective directories. Make sure the following files are updated:

`api/api/openapi/edge-infrastructure-manager-openapi-all.yaml`
`api/pkg/api/v0/edge-infrastructure-manager-openapi-client.gen.go`
`api/pkg/api/v0/edge-infrastructure-manager-openapi-server.gen.go`
`api/pkg/api/v0/edge-infrastructure-manager-openapi-types.gen.go`

API Server Implementation#

Next step is to implement the logic handling the HTTP requests coming for the external clients. First step is to implement a new handler that will receive the HTTP requests and will submit the request to the worker go routines that will deal with the translation of requests in protobuf messages and vice versa.

The handler will be implemented as new file internal/server/handlerlocalaccount.go. Make sure to implement only the necessary hooks. For example, in the following example there will not be an hook to handle PATCH/PUT accordingly to what was defined in the OpenAPI specification.

// (GET /localaccounts).
func (r *restHandlers) GetLocalAccounts(ctx echo.Context, query api.GetLocalAccountParams) error {
    ...
}

// (POST /localaccounts).
func (r *restHandlers) PostLocalAccounts(ctx echo.Context) error {
    ...
}

// GET /localaccount/{localAccountId} - Get a local account by ID
func (r *restHandlers) GetLocalAccountLocalAccountID(ctx echo.Context, localAccountID string) error {
    ...
}

// (DELETE /localaccount/{localAccountId}).
func (r *restHandlers) DeleteLocalAccountLocalAccountID(ctx echo.Context, localAccountID string) error {
    ...
}

The requests submitted by the REST handlers are handled by the corresponding Inventory handler. Each handler is responsible to handle only one resource.

The next step is to register the Inventory handlers and to properly extend the types respectively in the following files internal/worker/handlers/invhandlers/main.go, internal/types/types.go and internal/worker/handlers/invhandlers/params.go.

Now it is the time for the core changes, see one of the files in api/internal/worker/handlers/invhandlers as an example. In general, it is required to implement for each API operation a new function that will handle the request and will deal with the translation of the request in protobuf messages and vice versa.

func (t telemetryLogsGroupHandler) Create(job *types.Job) (*types.Payload, error) {
    ...
}

func (t telemetryLogsGroupHandler) Get(job *types.Job) (*types.Payload, error) {
    ...
}

func (t telemetryLogsGroupHandler) Update(job *types.Job) (*types.Payload, error) {
    err := errors.Errorfc(codes.Unimplemented, "%s operation not supported", job.Operation)
    return nil, err
}

func (t telemetryLogsGroupHandler) Delete(job *types.Job) error {
    ...
}

func (t telemetryLogsGroupHandler) List(job *types.Job) (*types.Payload, error) {
    ...
}

Create unit tests using the testing framework exposed by Inventory. This will allow to test using a real Inventory instance. To further tests corner case make sure to extend the logic of the Inventory mocks. The mocks implementation are located in api/test/utils.

Extending Integration Tests#

End-to-end tests are required to ensure that the new resource is working as expected. The tests are in the folder api/test/client and use auto-generated Golang clients which make HTTP requests to the API server. The tests written in Go are executed using a deployment that contains Edge Infrastructure Manager components and allows to test the new resource in a real environment and exercise the reconciliation of the resource managers.

See the test directory for more details.