Co-authored-by: Gus Luxton <gus@gravitational.com>
6.9 KiB
authors | state |
---|---|
Alexander Klizhentas (sasha@gravitational.com), Brian Joerger (bjoerger@gravitational.com) | draft |
RFD 10 - API and Client Libraries
What
- Unify Golang Client experience.
- Automatically generate API documentation using protoc-gen-doc.
Why
There are several problems with Go client libraries:
- Go library example pulls entire teleport dependency including
lib/auth
andlib/services
. - There is no separate client library with guarantees of compatibility with specific version of Teleport. See examples here.
- It's impossible to generate library in any other language other than Go due to unclear API surface.
- Some client logic is residing in plugins, while other is in
lib/auth
. - Code in
lib/auth
uses some concepts unfamiliar to Go users:
// connectClient establishes a gRPC connection to an auth server.
func connectClient() (*auth.Client, error) {
tlsConfig, err := LoadTLSConfig("certs/api-admin.crt", "certs/api-admin.key", "certs/api-admin.cas")
if err != nil {
return nil, fmt.Errorf("Failed to setup TLS config: %v", err)
}
// must parse addr list panics and is not necessary
authServerAddr := utils.MustParseAddrList("127.0.0.1:3025")
clientConfig := auth.ClientConfig{Addrs: authServerAddr, TLS: tlsConfig}
// TLS client is the only client
return auth.NewTLSClient(clientConfig)
Details
Approach
Keep Teleport's API layer unified, avoid having internal API vs external API layer. The access to the API surface is limited by role based access control.
Avoid large refactors, instead use Go's type aliases to move things around while keeping things backwards compatible.
// Preserve full backwards compatibility
type services.Role = types.Role
type auth.Service = proto.Service
Move protobuf types and services and generate docs
Point protobuf generators to generate types to another folder and use a system of go submodules.
Use protoc-gen-doc to generate gRPC API markdown documentation during the protoc build process. This will require us to improve our proto files with user-focused comments.
$ ls github.com/gravitational/teleport/api
# root folder with API specs and is a go submodule
api/
# open API spec used for swagger, see for example K8s Swagger spec
# https://github.com/kubernetes/kubernetes/tree/master/api/openapi-spec
# These docs can be copied to our doc engine before each doc release, optionally with custom templates.
docs/
types.md
wrappers.md
client.md
# move generated services types here, while keeping the original proto files
# in place in lib/services/services.proto
types/
types.pb.go
wrappers.pb.go
# generate auth service lib/auth/proto here
client/
authservice.pb.go
Move client implementation to api/client
Move relevant lib/client.*Client code to api/client
.
Track dependencies, make sure that new api/client
module has no dependencies
on the outside modules anywhere in lib
.
Track dependencies
Track dependencies to be limited to:
gravitational/roundtrip
,gravitational/trace
- grpc google package and its dependencies
- protobuf generated code
- Keep some auth service specific code like
utils.Addr
out of theapi/client
Go native user experience
Achieve the user experience in Client with a simplified client constructor. This should also transparently handle dialing via a proxy.
import (
"github.com/gravitational/teleport/api/client"
)
func main() {
// TLS client is the only TLS client supported in teleport
client, err := client.New(client.Config{
Addrs: []string{"localhost:3025"},
// ContextDialer is optional context dialer
ContextDialer: net.DialContext,
// direct TLS credentials config
Credentials: client.TLSCreds(tls.Config)
})
...
defer client.Close()
ping, err := client.Ping()
...
fmt.Printf("Ping: %v\n", ping.Version)
}
$ go mod init
$ go run main.go
Credential providers
Support multiple credential providers with chaining
- client.TLSCreds(*tls.Config) loads creds from TLS config
- client.PathCreds("path") loads mounted creds from path, detects reloads and updates the grpc transport
Support convenience method that supports and understands ~/.tsh/profile
,
so users can try testing using credentials they got from tsh login
// for testing, use client spec that loads dialer, and credentials from profile
cfg, err := client.LoadTSHConfig()
// cfg.ContextDialer sets up dialer using tunnel dialer in tctl
// TLS creds are loaded from key store, similar how tctl does it in 5.0
client, err := client.New(cfg)
$ tsh login
# try client
$ go run main.go
Auto version checks
Client should verify that its implementation is compatible with
Teleport's server version by calling Ping
and checking the version
similar to tsh logic.
Move Access requests code into client
Move access
workflows code into client
subpackage with new name workflows
:
https://github.com/gravitational/teleport-plugins/tree/master/access
Instead of a separate client, workflows
becomes a subpackage of the api client:
import (
"github.com/gravitational/teleport/api/client"
"github.com/gravitational/teleport/api/client/workflows"
)
func main() {
client, err := client.New(...)
watcher, err := workflows.NewWatcher(ctx, client, workflows.Watch{State: ...})
...
defer watcher.Close()
Provide simplified high level access hooks
Access hooks are too low level, add simple handler similar to http server design with a router:
func myTeamRequest(ctx context.Context, req access.Request) (access.Request, error) {
return req, trace.AccessDenied("access denied")
}
func main() {
router := access.Router()
// handle access requests from my team only
router.HandleFunc(router.Match{Traits: Trait("team", "myteam")}
srv := access.Server{
Client: clt,
Handler: router,
}
Improve API documentation user experience
Integrate protoc-gen-doc generated docs into mkdocs` using mkdocs plugin.
Add Python Client
Generate a python
version of the Teleport client as a proof of concept. This could be any language, and the choice to use python
first is arbitrary.
ls github.com/gravitational/teleport
api/
python/
$ pip install teleport-client
import teleport
client = teleport.client(addr=...)
client.tokens()
Port router code
@access.handle(clt, path=access.trait("team")):
def handle(req):
if req.user == "bob":
raise access.Denied("access denied")
Update docs to include samples in both languages, Python and Go.