Skip to content

Commit

Permalink
Adding the skeleton for versioned APIs, both servers and clients
Browse files Browse the repository at this point in the history
This patch proposes a generic framework to be able to easily define, maintain
and extend versioned API groups going forward.

The whole idea is documented in `api/README.md`; reviewers should read that
file before reviewing the rest of the PR.

Please note that all `*_generated.go` files in this PR are _meant_ to be
auto-generated by a tool based on gengo, but that tool will only be written
after this patch's approach is accepted and this PR merged; in particular
`*_generated.go` files in this PR should be reviewed.

With integration tests with a dummy API group, across 3 versions, with breaking
changes in conversions from one version to the next.

Also some unit tests where relevant.

Signed-off-by: Jean Rouge <[email protected]>
  • Loading branch information
wk8 committed Sep 11, 2019
1 parent dbb77cb commit 47c7a9a
Show file tree
Hide file tree
Showing 43 changed files with 3,070 additions and 0 deletions.
210 changes: 210 additions & 0 deletions api/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,210 @@
# CSI-proxy's API

## Overview

CSI-proxy's API is a GRPC, versioned API.

The server exposes a number of API groups, all independent of each other. Additionally, each API group has one or more versions. Each version in each group listens for GRPC messages on a Windows named pipe of the form `\\.\\pipe\\csi-proxy-<api_group_name>-<version>` (e.g. `\\.\\pipe\\csi-proxy-filesystem-v2alpha1` or `\\.\\pipe\\csi-proxy-iscsi-v1`).

APIs are defined by protobuf files; each API group should live in its own directory under `api/<api_group_name>` in this repo's root (e.g. `api/iscsi`), and then define each of its version in `api/<api_group_name>/<version>/api.proto` files (e.g. `api/iscsi/v1/api.proto`). Each `proto` file should define exactly one RPC service.

Internally, there is only one internal server `struct` per API group, that handles all the versions for that API group. That server is defined in this repo's `internal/server/<api_group_name>` (e.g. `internal/server/iscsi`) go package. This go package should follow the following pattern:

<a name="serverPkgTree"></a>
```
internal/server/<api_group_name>
├── internal
│   └── types.go
└── server.go
```
where `types.go` should contain the internal types corresponding to the various protobuf types for that API group - these internal structures must be able to represent all the different versions of the API. For example, given a `dummy` API group with two versions defined by the following `proto` files:

`api/dummy/v1alpha1/api.proto`
```proto
syntax = "proto3";
package v1alpha1;
service Dummy {
// ComputeDouble computes the double of the input. Real smart stuff!
rpc ComputeDouble(ComputeDoubleRequest) returns (ComputeDoubleResponse) {}
}
message ComputeDoubleRequest{
int32 input32 = 1;
}
message ComputeDoubleResponse{
int32 response32 = 1;
}
```

and

`api/dummy/v1/api.proto`
```proto
syntax = "proto3";
package v1;
service Dummy {
// ComputeDouble computes the double of the input. Real smart stuff!
rpc ComputeDouble(ComputeDoubleRequest) returns (ComputeDoubleResponse) {}
}
message ComputeDoubleRequest{
// we changed in favor of an int64 field here
int64 input = 2;
}
message ComputeDoubleResponse{
int64 response = 2;
// set to true if the result overflowed
bool overflow = 3;
}
```

then `internal/server/dummy/internal/types.go` could look something like:
```go
type ComputeDoubleRequest struct {
Input int64
}

type ComputeDoubleResponse struct {
Response int64
Overflow bool
}
```
and then the API group's server (`internal/server/dummy/server.go`) needs to define the callbacks to handle requests for all API versions, e.g.:
```go
type Server struct{}

func (s *Server) ComputeDouble(ctx context.Context, request *internal.ComputeDoubleRequest, version apiversion.Version) (*internal.ComputeDoubleResponse, error) {
in := request.Input64
out := 2 * in

response := &internal.ComputeDoubleResponse{}

if sign(in) != sign(out) {
// overflow
response.Overflow = true
} else {
response.Response = out
}

return response, nil
}

func sign(x int64) int {
switch {
case x > 0:
return 1
case x < 0:
return -1
default:
return 0
}
}
```
All the boilerplate code to:
* add a named pipe to the server for each version of the API group, listening for its version's requests, and replying with its version's responses
* convert versioned requests to internal representations
* and then internal responses back to versioned responses
* create clients to talk to that API group & versioned
is then generated automatically using [gengo](https://github.com/kubernetes/gengo).

The only caveat is that when conversions cannot be made trivially (e.g. when fields from internal and versioned `struct`s have different types), API devs need to define conversion functions. They can do that by creating an (otherwise optional) `internal/server/<api_group_name>/internal/<version>/conversion.go` file, containing functions of the form `func convert_pb_<Type>_To_internal_<Type>(in *pb.<Type>, out *internal.<Type>) error` or `func convert_internal_<Type>_To_pb_<Type>(in *internal.<Type>, out *pb.<Type>) error`; for example, in our `dummy` example above, we need to define a conversion function to account for the different fields in requests and responses from `v1alpha1` to `v1`; so `internal/server/dummy/internal/v1alpha1/conversion.go` could look like:
```go
func convert_pb_ComputeDoubleRequest_To_internal_ComputeDoubleRequest(in *pb.ComputeDoubleRequest, out *internal.ComputeDoubleRequest) error {
out.Input64 = int64(in.Input32)
return nil
}

func convert_internal_ComputeDoubleResponse_To_pb_ComputeDoubleResponse(in *internal.ComputeDoubleResponse, out *pb.ComputeDoubleResponse) error {
i := in.Response
if i > math.MaxInt32 || i < math.MinInt64 {
// overflow
out.Overflow = true
} else {
out.Response32 = int32(i)
}
return nil
}
```

## How to change the API

### How to add a new API group

Simply create a new `api/<api_group_name>/<version>/api.proto` file, defining your new service; then generate the Go protobuf code, and run the CSI-proxy generator to generate all the boilerplate code.

FIXME: add more details on which commands to run, and which files to edit when done generating.

### How to make changes to an existing API group & version

Existing APIs changes are only acceptable as long as they are backward compatible; where compatibility is understood here much [as k8s API guidelines define it](https://github.com/kubernetes/community/blob/master/contributors/devel/sig-architecture/api_changes.md#on-compatibility):
1. Any API call (e.g. a structure POSTed to a REST endpoint) that succeeded
before your change must succeed after your change.
2. Any API call that does not use your change must behave the same as it did
before your change.
3. Any API call that uses your change must not cause problems (e.g. crash or
degrade behavior) when issued against an API servers that do not include your
change.
versions and back) with no loss of information.
4. Existing clients need not be aware of your change in order for them to
continue to function as they did previously, even when your change is in use.

The main difference with the k8s API's definition of compatibility here is that it is not required to be able round-trip a change (convert to different API versions and back) with no loss of information, since contrary to k8s' API we don't need to store API objects here.

As long as a change abides by the compatibility rules outlined above, one can simply amend the `proto` file defining that API version, and then regenerate all the Go boilerplate code.

### How to add a new version to an existing API group

Any change that cannot be done in a compatible way requires creating a new API version.

Steps to add a new API version:
1. define it its own new `api.proto` file
2. generate the Go protobuf code
3. update the API group's internal representations (in its `types.go` file) to be able to represent all of the group's version (the new and the old ones)
3. add any needed conversion functions for all existing versions of this API group to account for the changes made at the previous step
4. re-generate all of the Go boilerplate code
5. now you can change the API group's server to add your new feature!

### Detailed breakdown of generated files

This section details how `csi-proxy-gen` works, and what files it generates; `csi-proxy-gen` is built on top of [gengo](https://github.com/kubernetes/gengo), and re-uses part of [k8s' code-generator](https://github.com/kubernetes/code-generator), notably to generate conversion functions.

First, it looks for all API group definitions, which are either subdirectories of `api/`, or any go package that contains a `doc.go` file containing a `// +csi-proxy-gen` comment.

Then for each API group it finds:
1. it iterates through each version subpackage, and in each looks for the `<ApiGroupName>Server` interface, and compiles the list of callbacks that the group's `Server` needs to implement as well as the list of top-level `struct`s (`*Request`s and `*Response`s)
2. it looks for an existing `internal/server/<api_group_name>/internal/types.go` file:
* if it exists, it checks that it contains all the expected top-level `struct`s from the previous step
* if it doesn't exist, _and_ the API group only defines one version, it auto-generates one that simply copies the protobuf `struct`s (from the previous step) - this is meant to make it easy to bootstrap a new API group
3. it generates the `internal/server/<api_group_name>/internal/types_generated.go` file, using the list of callbacks from the first step above
4. if `internal/server/<api_group_name>/server.go` doesn't exist, it generates a skeleton for it - this, too, is meant to make it easy to bootstrap new API groups
5. then for each version of the API:
1. it looks for an existing `internal/server/<api_group_name>/internal/<version>/conversion.go`, generates an empty one if it doesn't exist; then looks for existing conversion functions
2. it generates missing conversion functions to `internal/server/<api_group_name>/internal/<version>/conversion_generated.go`
3. it generates `internal/server/<api_group_name>/internal/<version>/server_generated.go`
6. it generates `internal/server/<api_group_name>/internal/api_group_generated.go` to list all the versioned servers it's just created
7. and finally, it generates `client/<api_group_name>/<version>/client_generated.go`

When `csi-proxy-gen` has successfully run to completion, [our example API group's go package from earlier](#serverPkgTree) will look something like:
```
internal/server/<api_group_name>
├── api_group_generated.go
├── internal
│   ├── types.go
│   ├── types_generated.go
│   ├── v1
│   │   ├── conversion.go
│   │   ├── conversion_generated.go
│   │   └── server_generated.go
│   └── v1alpha1
│      ├── conversion.go
│      ├── conversion_generated.go
│   └── server_generated.go
└── server.go
```
98 changes: 98 additions & 0 deletions api/errors.pb.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

15 changes: 15 additions & 0 deletions api/errors.proto
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
syntax = "proto3";

package api;

// CommandError details errors yielded by cmdlet calls.
message CmdletError {
// Name of the cmdlet that errored out.
string cmdlet_name = 1;

// Error code that got returned.
uint32 code = 2;

// Human-readable error message - can be empty.
string message = 3;
}
Loading

0 comments on commit 47c7a9a

Please sign in to comment.