Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 12 additions & 4 deletions docs/pages/reference/cli/tctl.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -1356,7 +1356,7 @@ $ tctl rm saml/okta
# Delete a local user called "admin":
$ tctl rm users/admin

# Delete a lock
# Delete a lock
$ tctl rm lock/ed7038cb-a3cc-4f59-8063-09553665b773
```

Expand Down Expand Up @@ -1850,12 +1850,18 @@ $ tctl tokens rm [<token>]

Reports diagnostic information.

The diagnostic metrics endpoint must be enabled with `teleport start --diag-addr=<bind-addr>` for `tctl top` to work.
`tctl top` can consume metrics from a HTTP diagnostic endpoint.

```code
$ tctl top [<diag-addr>] [<refresh>]
```

When a specific endpoint is provided, `tctl top` will always attempt to connect to it.
The endpoint should be a valid HTTP URL, corresponding to a matching
diagnostic metrics service configuration such as `teleport start --diag-addr=<bind-addr>`.

When no endpoint is specified, `tctl top` will attempt to connect via the debug UNIX socket endpoint, falling back to localhost.

### Argument

- `[<diag-addr>]` Diagnostic HTTP URL (HTTPS not supported)
Expand All @@ -1867,6 +1873,8 @@ $ tctl top [<diag-addr>] [<refresh>]
$ sudo teleport start --diag-addr=127.0.0.1:3000
# View stats with a refresh period of 5 seconds
$ tctl top http://127.0.0.1:3000 5s
# Use configured defaults
$ tctl top
```

## tctl users add
Expand Down Expand Up @@ -2159,7 +2167,7 @@ the two user filter flags imply that if a user
- was provisioned into Teleport by Okta (from `--user-origin okta`), OR
- has the label values `role=aws-admin` AND `dept=engineering` (from `--user-label "role=aws-admin,dept=engineering"`)

then they will be provisioned into AWS Identity Center by Teleport.
then they will be provisioned into AWS Identity Center by Teleport.

## tctl plugins install okta

Expand All @@ -2176,7 +2184,7 @@ Install the Okta integration.
| `--api-token` | none | string | Optional. Okta API token for the plugin to use. |
| `--[no-]scim` | `--no-scim` | boolean | Optional. Enable SCIM Okta integration. |
| `--[no-]users-sync` | `--users-sync` | none | Optional. Enable user synchronization. |
| `-o`, `--owner` | none | string | Optional. Add a default owner for synced Access Lists. |
| `-o`, `--owner` | none | string | Optional. Add a default owner for synced Access Lists. |
| `--[no-]accesslist-sync` | `--accesslist-sync` | none | Optional. Enable or disable group to Access List synchronization. |
| `--[no-]appgroup-sync` | `--appgroup-sync` | none | Optional. Enable or disable Okta Applications and Groups sync. |
| `-g`, `--group-filter` | none | string | Optional. Add a group filter. Supports globbing by default. Enclose in `^pattern$` for full regex support. |
Expand Down
6 changes: 2 additions & 4 deletions lib/autoupdate/agent/updater.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,9 +66,7 @@ const (
// reservedFreeDisk is the minimum required free space left on disk during downloads.
// TODO(sclevine): This value is arbitrary and could be replaced by, e.g., min(1%, 200mb) in the future
// to account for a range of disk sizes.
reservedFreeDisk = 10_000_000
// debugSocketFileName is the name of Teleport's debug socket in the data dir.
debugSocketFileName = "debug.sock" // 10 MB
Comment thread
okraport marked this conversation as resolved.
reservedFreeDisk = 10_000_000 // 10 MB
// requiredUmask must be set before this package can be used.
// Use syscall.Umask to set when no other goroutines are running.
requiredUmask = 0o022
Expand Down Expand Up @@ -125,7 +123,7 @@ func NewLocalUpdater(cfg LocalUpdaterConfig, ns *Namespace) (*Updater, error) {
cfg.SystemDir = packageSystemDir
}
validator := Validator{Log: cfg.Log}
debugClient := debug.NewClient(filepath.Join(ns.dataDir, debugSocketFileName))
debugClient := debug.NewClient(ns.dataDir)
return &Updater{
Log: cfg.Log,
Pool: certPool,
Expand Down
32 changes: 30 additions & 2 deletions lib/client/debug/debug.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,14 @@ import (
"net"
"net/http"
"net/url"
"path/filepath"
"strconv"

"github.com/gravitational/trace"
dto "github.com/prometheus/client_model/go"
"github.com/prometheus/common/expfmt"

"github.com/gravitational/teleport"
apidefaults "github.com/gravitational/teleport/api/defaults"
)

Expand All @@ -47,11 +51,13 @@ var SupportedProfiles = map[string]struct{}{

// Client represents the debug service client.
type Client struct {
clt *http.Client
clt *http.Client
socketPath string
}

// NewClient generates a new debug service client.
func NewClient(socketPath string) *Client {
func NewClient(dataDir string) *Client {
socketPath := filepath.Join(dataDir, teleport.DebugServiceSocketName)
return &Client{
clt: &http.Client{
Timeout: apidefaults.DefaultIOTimeout,
Expand All @@ -66,9 +72,15 @@ func NewClient(socketPath string) *Client {
return trace.Errorf("redirect via socket not allowed")
},
},
socketPath: socketPath,
}
}

// SocketPath returns the absolute path to the UNIX socket that the debug service is exposed on.
func (c *Client) SocketPath() string {
Comment thread
okraport marked this conversation as resolved.
return c.socketPath
}

// SetLogLevel changes the application's log level and a change status message.
func (c *Client) SetLogLevel(ctx context.Context, level string) (string, error) {
resp, err := c.do(ctx, http.MethodPut, url.URL{Path: "/log-level"}, []byte(level))
Expand Down Expand Up @@ -173,6 +185,22 @@ func (c *Client) GetReadiness(ctx context.Context) (Readiness, error) {
return ready, nil
}

// GetMetrics returns prometheus metrics as a map keyed by metric name.
func (c *Client) GetMetrics(ctx context.Context) (map[string]*dto.MetricFamily, error) {
resp, err := c.do(ctx, http.MethodGet, url.URL{Path: "/metrics"}, nil)
if err != nil {
return nil, trace.Wrap(err)
}
defer resp.Body.Close()
var parser expfmt.TextParser
metrics, err := parser.TextToMetricFamilies(resp.Body)
if err != nil {
return nil, trace.Wrap(err)
}

return metrics, nil
}

func (c *Client) do(ctx context.Context, method string, u url.URL, body []byte) (*http.Response, error) {
u.Scheme = "http"
u.Host = "debug"
Expand Down
2 changes: 1 addition & 1 deletion lib/client/debug/debug_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -206,7 +206,7 @@ func newSocketMockService(t *testing.T, status int, contents []byte) (string, fu
}()

t.Cleanup(func() { srv.Shutdown(context.Background()) })
return socketPath, func() []string {
return socketDir, func() []string {
srv.Shutdown(context.Background())
return requests
}
Expand Down
102 changes: 102 additions & 0 deletions tool/tctl/common/top/client/diag/client.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
// Teleport
// Copyright (C) 2025 Gravitational, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.

package diag

import (
"context"
"net"
"net/http"
"net/url"

"github.com/gravitational/trace"
dto "github.com/prometheus/client_model/go"
"github.com/prometheus/common/expfmt"

"github.com/gravitational/teleport/lib/defaults"
)

// Client is a wrapper around [*http.Client] that provides
// helpers for fetching metrics from the diagnostic endpoint of Teleport.
type Client struct {
endpoint string
clt *http.Client
}

// parseAddress takes a string address and attempts to parse it into a valid URL.
// The input can either be a valid string URL or a <host>:<port> pair.
func parseAddress(addr string) (*url.URL, error) {
u, err := url.Parse(addr)

if err != nil || u.Scheme == "" || u.Host == "" {
// Attempt to parse the input as a host:port tuple instead.
_, _, err = net.SplitHostPort(addr)
if err != nil {
return nil, trace.Errorf("address %s is neither a valid URL nor <host>:<port>", addr)
}

u = &url.URL{
Scheme: "http",
Host: addr,
}
}

return u, nil
}

// NewClient creates a new Client for a given address.
func NewClient(addr string) (*Client, error) {
clt, err := defaults.HTTPClient()
if err != nil {
return nil, trace.Wrap(err)
}

u, err := parseAddress(addr)
if err != nil {
return nil, trace.Wrap(err)
}

if u.Scheme != "http" {
return nil, trace.Errorf("unsupported scheme: %s, please provide a http address", u.Scheme)
}

return &Client{
endpoint: u.JoinPath("metrics").String(),
clt: clt,
}, nil
}

// GetMetrics returns prometheus metrics as a map keyed by metric name.
func (c *Client) GetMetrics(ctx context.Context) (map[string]*dto.MetricFamily, error) {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, c.endpoint, http.NoBody)
if err != nil {
return nil, trace.Wrap(err)
}

resp, err := c.clt.Do(req)
if err != nil {
return nil, trace.Wrap(err)
}
defer resp.Body.Close()

var parser expfmt.TextParser
metrics, err := parser.TextToMetricFamilies(resp.Body)
if err != nil {
return nil, trace.Wrap(err)
}

return metrics, nil
}
87 changes: 87 additions & 0 deletions tool/tctl/common/top/client/diag/client_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
/*
* Teleport
* Copyright (C) 2025 Gravitational, Inc.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/

package diag

import (
"net/url"
"testing"

"github.com/stretchr/testify/require"
)

func TestClientAddressParsing(t *testing.T) {
t.Parallel()

testCases := []struct {
addr string
url *url.URL
}{
{
addr: "http://127.0.0.1:3000",
url: &url.URL{
Scheme: "http",
Host: "127.0.0.1:3000",
},
},
{
addr: "http://localhost:3000",
url: &url.URL{
Scheme: "http",
Host: "localhost:3000",
},
},
{
addr: "localhost:3000",
url: &url.URL{
Scheme: "http",
Host: "localhost:3000",
},
},
{
addr: "127.0.0.1:3000",
url: &url.URL{
Scheme: "http",
Host: "127.0.0.1:3000",
},
},
{
addr: "badurl:300:9:1",
},
{
addr: "http//badurl",
},
{
addr: "/var/lib/file.sock",
},
}

for _, tc := range testCases {
t.Run(tc.addr, func(t *testing.T) {
u, err := parseAddress(tc.addr)
if tc.url == nil {
Comment thread
okraport marked this conversation as resolved.
require.Error(t, err)
require.Nil(t, u)
} else {
require.NoError(t, err)
require.Equal(t, tc.url.Host, u.Host)
require.Equal(t, tc.url.Scheme, u.Scheme)
}
})
}
}
Loading
Loading