-
Notifications
You must be signed in to change notification settings - Fork 1.8k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add a guide to using the API for auto-discovery #22817
Conversation
Roles: types.SystemRoles{types.RoleApp}, | ||
}) | ||
|
||
if err := t.teleportClient.CreateToken(ctx, tok); err != nil { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This example might be simpler if you registered dynamic apps rather than started new app services and generated new join tokens.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wanted to demonstrate the token method in this guide somehow so users could see an example, but it makes sense to advise using a dynamic approach for simplicity. I've moved the Details
box about the dynamic approach higher in this section and changed the first paragraph after the code snippet to make the dynamic approach more prominent.
@alexfornuto This is ready for copy review |
@ptgott do you wanna resolve the conflict with |
9967ae4
to
8b3507f
Compare
method: | ||
|
||
```go | ||
func (c *Client) CreateApp(ctx context.Context, app types.Application) error |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would this line replace func (t *tokenDemoApp) createAppToken(ctx context.Context) (string, error) {
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The current app creates a token and launches an App Service instance with it. If you use CreateApp
, you don't need to create a token or launch a new App Service instance, but you do need to have an App Service instance running already.
``` | ||
|
||
For this to work, you must have an instance of the Application Service already | ||
running. Your client application must also have the following permissions: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
By "client application must have" are you referring to the spec of the register-apps
role?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've changed this to "Your API client application's Teleport user must also have the following permissions:"
create containers based on it: | ||
|
||
```code | ||
$ docker image pull (=teleport.latest_oss_docker_image=) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would Docker not pull the image as needed? Below when we spin up containers based on the rabbitmq:3-management
image it does. Should we pull that image here as well to get it out of the way?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It didn't when I ran the demo app. I think the docker
CLI bundles some actions together that the API client library separates. docker run
pulls a container, creates it, and executes it, whereas these are separate Docker API client function calls. I thought it would make sense to pull the container manually, since it's a quick step that you only need to do once.
$ for i in {1..3}; do docker run -d rabbitmq:3-management; done; | ||
``` | ||
|
||
The terminal where you ran the application should show output similar to the |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is where my testing fails. I'm on macOS, and running the rabbit containers:
❯ docker container ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
e20bbe8e9dea rabbitmq:3-management "docker-entrypoint.s…" 2 minutes ago Up 2 minutes 4369/tcp, 5671-5672/tcp, 15671-15672/tcp, 15691-15692/tcp, 25672/tcp naughty_shtern
fbca4f93ba67 rabbitmq:3-management "docker-entrypoint.s…" 2 minutes ago Up 2 minutes 4369/tcp, 5671-5672/tcp, 15671-15672/tcp, 15691-15692/tcp, 25672/tcp affectionate_dubinsky
0f3a961dd928 rabbitmq:3-management "docker-entrypoint.s…" 2 minutes ago Up 2 minutes 4369/tcp, 5671-5672/tcp, 15671-15672/tcp, 15691-15692/tcp, 25672/tcp cranky_wright
But the go program is not responding:
❯ go run main.go
Starting the application
Connected to Teleport
Connected to the Docker daemon
And tsh apps ls
doesn't list any new applications. I tried restarting the go client while the containers were still running, and then restarting the containers while the go client was running. I've also waited up to five minutes after creating the containers for the client app to do something.
Here's my completed main.go, to check for PEBKAC errors:
main.go
package main
import (
"context"
"crypto/rand"
"encoding/hex"
"fmt"
"net"
"net/url"
"strings"
"time"
dtypes "github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/filters"
"github.com/docker/docker/api/types/strslice"
docker "github.com/docker/docker/client"
teleport "github.com/gravitational/teleport/api/client"
"github.com/gravitational/teleport/api/client/proto"
"github.com/gravitational/teleport/api/types"
"github.com/gravitational/trace"
"google.golang.org/grpc"
)
const (
// Assign proxyAddr to the host and port of your Teleport Proxy Service instance
proxyAddr string = "REDACTED:443"
teleportImage string = "public.ecr.aws/gravitational/teleport:12.1.1"
initTimeout = time.Duration(30) * time.Second
updateInterval = time.Duration(5) * time.Second
tokenTTL = time.Duration(5) * time.Minute
networkName string = "bridge"
managementPort string = "15672"
tokenLenBytes = 16
rabbitMQImage string = "rabbitmq:3-management"
)
type tokenDemoApp struct {
dockerClient *docker.Client
teleportClient *teleport.Client
}
func (t *tokenDemoApp) listRegisteredAppURLs(ctx context.Context) (map[string]types.AppServer, error) {
m := make(map[string]types.AppServer)
for {
req := proto.ListResourcesRequest{
ResourceType: "app_server",
Limit: 10,
}
resp, err := t.teleportClient.ListResources(
ctx,
req,
)
if err != nil {
return nil, trace.Wrap(err)
}
for _, r := range resp.Resources {
if p, ok := r.(types.AppServer); ok {
m[p.GetApp().GetURI()] = p
}
}
// No more pages to request
if resp.NextKey == "" {
break
}
req.StartKey = resp.NextKey
}
return m, nil
}
func (t *tokenDemoApp) listAppContainerURLs(ctx context.Context, image string) (map[string]struct{}, error) {
c, err := t.dockerClient.ContainerList(ctx, dtypes.ContainerListOptions{
Filters: filters.NewArgs(filters.KeyValuePair{
Key: "ancestor",
Value: image,
}),
})
if err != nil {
return nil, trace.Wrap(err)
}
l := make(map[string]struct{})
for _, r := range c {
b, ok := r.NetworkSettings.Networks[networkName]
// Not connected to the chosen network, so skip it
if !ok {
continue
}
u, err := url.Parse("http://" + net.JoinHostPort(
b.IPAddress,
managementPort,
))
if err != nil {
return nil, trace.Wrap(err)
}
l[u.String()] = struct{}{}
}
return l, nil
}
func cryptoRandomHex(len int) (string, error) {
randomBytes := make([]byte, len)
if _, err := rand.Read(randomBytes); err != nil {
return "", trace.Wrap(err)
}
return hex.EncodeToString(randomBytes), nil
}
func (t *tokenDemoApp) createAppToken(ctx context.Context) (string, error) {
n, err := cryptoRandomHex(tokenLenBytes)
if err != nil {
return "", trace.Wrap(err)
}
tok, err := types.NewProvisionTokenFromSpec(
n,
time.Now().Add(tokenTTL),
types.ProvisionTokenSpecV2{
Roles: types.SystemRoles{types.RoleApp},
})
if err := t.teleportClient.CreateToken(ctx, tok); err != nil {
return "", trace.Wrap(err)
}
return n, nil
}
func (t *tokenDemoApp) startApplicationServiceContainer(
ctx context.Context,
token string,
u url.URL,
) error {
name := strings.ReplaceAll(u.Hostname(), ".", "-")
resp, err := t.dockerClient.ContainerCreate(
ctx,
&container.Config{
Image: teleportImage,
Entrypoint: strslice.StrSlice{
"/usr/bin/dumb-init",
"teleport",
"start",
"--roles=app",
"--auth-server=" + proxyAddr,
"--token=" + token,
"--app-name=rabbitmq-" + name,
"--app-uri=" + u.String(),
},
},
nil,
nil,
nil,
"",
)
if err != nil {
return trace.Wrap(err)
}
err = t.dockerClient.ContainerStart(
ctx,
resp.ID,
dtypes.ContainerStartOptions{},
)
if err != nil {
return trace.Wrap(err)
}
return nil
}
func (t *tokenDemoApp) pruneAppServiceInstance(ctx context.Context, p types.AppServer) error {
host := p.GetHostname()
if err := t.teleportClient.DeleteApplicationServer(
ctx,
p.GetNamespace(),
p.GetHostID(),
p.GetName(),
); err != nil {
return trace.Wrap(err)
}
fmt.Println("Deleted unnecessary Application Service record:", p.GetName())
// Don't check errors when removing the container, since it may already
// have been removed.
t.dockerClient.ContainerStop(ctx, host, container.StopOptions{})
t.dockerClient.ContainerRemove(ctx, host, dtypes.ContainerRemoveOptions{})
fmt.Println("Deleted unnecessary Application Service container:", host)
return nil
}
func (t *tokenDemoApp) reconcileApps() error {
ctx := context.Background()
apps, err := t.listRegisteredAppURLs(ctx)
if err != nil {
return trace.Wrap(err)
}
urls, err := t.listAppContainerURLs(ctx, rabbitMQImage)
if err != nil {
return trace.Wrap(err)
}
for u, _ := range urls {
if _, ok := apps[u]; ok {
continue
}
tok, err := t.createAppToken(ctx)
if err != nil {
return trace.Wrap(err)
}
fmt.Println("Created a new application token for URL: " + u)
r, err := url.Parse(u)
if err != nil {
return trace.Wrap(err)
}
err = t.startApplicationServiceContainer(ctx, tok, *r)
if err != nil {
return trace.Wrap(err)
}
fmt.Println("Started an Application Service container to proxy URL: " + u)
}
for a, p := range apps {
_, ok := urls[a]
if ok {
continue
}
if err := t.pruneAppServiceInstance(ctx, p); err != nil {
return trace.Wrap(err)
}
}
return nil
}
func newTokenDemoApp() *tokenDemoApp {
ctx := context.Background()
ctx, cancel := context.WithTimeout(ctx, initTimeout)
defer cancel()
creds := teleport.LoadIdentityFile("auth.pem")
t, err := teleport.New(ctx, teleport.Config{
Addrs: []string{proxyAddr},
Credentials: []teleport.Credentials{creds},
DialOpts: []grpc.DialOption{
grpc.WithReturnConnectionError(),
},
})
if err != nil {
panic(err)
}
fmt.Println("Connected to Teleport")
d, err := docker.NewClientWithOpts(
docker.WithAPIVersionNegotiation(),
)
if err != nil {
panic(err)
}
fmt.Println("Connected to the Docker daemon")
return &tokenDemoApp{
teleportClient: t,
dockerClient: d,
}
}
func main() {
fmt.Println("Starting the application")
app := newTokenDemoApp()
k := time.NewTicker(updateInterval)
defer k.Stop()
for {
<-k.C
if err := app.reconcileApps(); err != nil {
panic(err)
}
}
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm going to push a commit that includes the whole program in the examples
directory so there's no risk of accidentally pasting the wrong thing (I don't think that that's the problem, but just in case...).
It works on my machine™, so there must be some difference between our environments that's coming into play.
One thing you could do is add some Println
calls to *tokenDemoApp.reconcileApps
to see if the app is listing containers/registered apps as expected:
func (t *tokenDemoApp) reconcileApps() error {
ctx := context.Background()
apps, err := t.listRegisteredAppURLs(ctx)
if err != nil {
return trace.Wrap(err)
}
+ fmt.Println("registered apps", apps)
urls, err := t.listAppContainerURLs(ctx, rabbitMQImage)
if err != nil {
return trace.Wrap(err)
}
+ fmt.Println("app container URLs:", urls)
for u, _ := range urls {
if _, ok := apps[u]; ok {
continue
}
tok, err := t.createAppToken(ctx)
if err != nil {
return trace.Wrap(err)
}
fmt.Println("Created a new application token for URL: " + u)
r, err := url.Parse(u)
if err != nil {
return trace.Wrap(err)
}
err = t.startApplicationServiceContainer(ctx, tok, *r)
if err != nil {
return trace.Wrap(err)
}
fmt.Println("Started an Application Service container to proxy URL: " + u)
}
for a, p := range apps {
_, ok := urls[a]
if ok {
continue
}
if err := t.pruneAppServiceInstance(ctx, p); err != nil {
return trace.Wrap(err)
}
}
return nil
}
Are the containers connected to the default bridge network?
docker network inspect bridge | jq '.[0].Containers'
If not, the app also won't be able to find them, since it only keeps track of container URLs on the default bridge network.
In any case, thanks for testing this out!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Some additional copy edits.
docs/pages/api/introduction.mdx
Outdated
- Integrate with external tools, e.g., to write an [Access Request | ||
plugin](../access-controls/access-request-plugins/index.mdx). Teleport | ||
maintains Access Request plugins for tools like Slack, Jira, and Mattermost. | ||
- Writing a program/bot to manage Access Requests automatically, based on your |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
To match tense:
- Writing a program/bot to manage Access Requests automatically, based on your | |
- Write a program/bot to manage Access Requests automatically, based on your |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍
I've also deleted this particular line (from the original page), since we don't recommend approving/denying Access Requests automatically (and recommend displaying links to the Teleport Web UI to do this).
@ptgott - this PR will require admin approval to merge due to its size. Consider breaking it up into a series smaller changes. |
@alexfornuto Thanks for the review! I've responded to your feedback and also pushed a commit to incorporate some feedback from Zac in this PR that also applies to this one. |
See #19716 Adding resources is a popular use of Teleport's API, so I have added a guide to using the API for syncing resources to a service discovery API using an example of a local Docker setup.
This way, users can have a compilable example before they start working through the guide.
364a247
to
c77582e
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just a couple more suggestions.
* Add a guide to using the API for auto-discovery See #19716 Adding resources is a popular use of Teleport's API, so I have added a guide to using the API for syncing resources to a service discovery API using an example of a local Docker setup. * Respond to zmb3 feedback * Use the examples directory for the code This way, users can have a compilable example before they start working through the guide. * Respond to alexfornuto feedback * Respond to PR feedback * Run make fix-license
* Add a guide to using the API for auto-discovery See #19716 Adding resources is a popular use of Teleport's API, so I have added a guide to using the API for syncing resources to a service discovery API using an example of a local Docker setup. * Respond to zmb3 feedback * Use the examples directory for the code This way, users can have a compilable example before they start working through the guide. * Respond to alexfornuto feedback * Respond to PR feedback * Run make fix-license
See #19716
Adding resources is a popular use of Teleport's API, so I have added a guide to using the API for syncing resources to a service discovery API using an example of a local Docker setup.