mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
feat: add agent metadata (#6614)
This commit is contained in:
@@ -65,37 +65,56 @@ func (c *Client) GitSSHKey(ctx context.Context) (GitSSHKey, error) {
|
||||
return gitSSHKey, json.NewDecoder(res.Body).Decode(&gitSSHKey)
|
||||
}
|
||||
|
||||
type Metadata struct {
|
||||
// In the future, we may want to support sending back multiple values for
|
||||
// performance.
|
||||
type PostMetadataRequest = codersdk.WorkspaceAgentMetadataResult
|
||||
|
||||
func (c *Client) PostMetadata(ctx context.Context, key string, req PostMetadataRequest) error {
|
||||
res, err := c.SDK.Request(ctx, http.MethodPost, "/api/v2/workspaceagents/me/metadata/"+key, req)
|
||||
if err != nil {
|
||||
return xerrors.Errorf("execute request: %w", err)
|
||||
}
|
||||
defer res.Body.Close()
|
||||
|
||||
if res.StatusCode != http.StatusNoContent {
|
||||
return codersdk.ReadBodyAsError(res)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type Manifest struct {
|
||||
// GitAuthConfigs stores the number of Git configurations
|
||||
// the Coder deployment has. If this number is >0, we
|
||||
// set up special configuration in the workspace.
|
||||
GitAuthConfigs int `json:"git_auth_configs"`
|
||||
VSCodePortProxyURI string `json:"vscode_port_proxy_uri"`
|
||||
Apps []codersdk.WorkspaceApp `json:"apps"`
|
||||
DERPMap *tailcfg.DERPMap `json:"derpmap"`
|
||||
EnvironmentVariables map[string]string `json:"environment_variables"`
|
||||
StartupScript string `json:"startup_script"`
|
||||
StartupScriptTimeout time.Duration `json:"startup_script_timeout"`
|
||||
Directory string `json:"directory"`
|
||||
MOTDFile string `json:"motd_file"`
|
||||
ShutdownScript string `json:"shutdown_script"`
|
||||
ShutdownScriptTimeout time.Duration `json:"shutdown_script_timeout"`
|
||||
GitAuthConfigs int `json:"git_auth_configs"`
|
||||
VSCodePortProxyURI string `json:"vscode_port_proxy_uri"`
|
||||
Apps []codersdk.WorkspaceApp `json:"apps"`
|
||||
DERPMap *tailcfg.DERPMap `json:"derpmap"`
|
||||
EnvironmentVariables map[string]string `json:"environment_variables"`
|
||||
StartupScript string `json:"startup_script"`
|
||||
StartupScriptTimeout time.Duration `json:"startup_script_timeout"`
|
||||
Directory string `json:"directory"`
|
||||
MOTDFile string `json:"motd_file"`
|
||||
ShutdownScript string `json:"shutdown_script"`
|
||||
ShutdownScriptTimeout time.Duration `json:"shutdown_script_timeout"`
|
||||
Metadata []codersdk.WorkspaceAgentMetadataDescription `json:"metadata"`
|
||||
}
|
||||
|
||||
// Metadata fetches metadata for the currently authenticated workspace agent.
|
||||
func (c *Client) Metadata(ctx context.Context) (Metadata, error) {
|
||||
res, err := c.SDK.Request(ctx, http.MethodGet, "/api/v2/workspaceagents/me/metadata", nil)
|
||||
// Manifest fetches manifest for the currently authenticated workspace agent.
|
||||
func (c *Client) Manifest(ctx context.Context) (Manifest, error) {
|
||||
res, err := c.SDK.Request(ctx, http.MethodGet, "/api/v2/workspaceagents/me/manifest", nil)
|
||||
if err != nil {
|
||||
return Metadata{}, err
|
||||
return Manifest{}, err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
if res.StatusCode != http.StatusOK {
|
||||
return Metadata{}, codersdk.ReadBodyAsError(res)
|
||||
return Manifest{}, codersdk.ReadBodyAsError(res)
|
||||
}
|
||||
var agentMeta Metadata
|
||||
var agentMeta Manifest
|
||||
err = json.NewDecoder(res.Body).Decode(&agentMeta)
|
||||
if err != nil {
|
||||
return Metadata{}, err
|
||||
return Manifest{}, err
|
||||
}
|
||||
accessingPort := c.SDK.URL.Port()
|
||||
if accessingPort == "" {
|
||||
@@ -106,14 +125,14 @@ func (c *Client) Metadata(ctx context.Context) (Metadata, error) {
|
||||
}
|
||||
accessPort, err := strconv.Atoi(accessingPort)
|
||||
if err != nil {
|
||||
return Metadata{}, xerrors.Errorf("convert accessing port %q: %w", accessingPort, err)
|
||||
return Manifest{}, xerrors.Errorf("convert accessing port %q: %w", accessingPort, err)
|
||||
}
|
||||
// Agents can provide an arbitrary access URL that may be different
|
||||
// that the globally configured one. This breaks the built-in DERP,
|
||||
// which would continue to reference the global access URL.
|
||||
//
|
||||
// This converts all built-in DERPs to use the access URL that the
|
||||
// metadata request was performed with.
|
||||
// manifest request was performed with.
|
||||
for _, region := range agentMeta.DERPMap.Regions {
|
||||
if !region.EmbeddedRelay {
|
||||
continue
|
||||
|
||||
@@ -19,6 +19,7 @@ import (
|
||||
"tailscale.com/tailcfg"
|
||||
|
||||
"cdr.dev/slog"
|
||||
"github.com/coder/coder/coderd/tracing"
|
||||
"github.com/coder/coder/tailnet"
|
||||
"github.com/coder/retry"
|
||||
)
|
||||
@@ -75,6 +76,31 @@ var WorkspaceAgentLifecycleOrder = []WorkspaceAgentLifecycle{
|
||||
WorkspaceAgentLifecycleOff,
|
||||
}
|
||||
|
||||
type WorkspaceAgentMetadataResult struct {
|
||||
CollectedAt time.Time `json:"collected_at" format:"date-time"`
|
||||
// Age is the number of seconds since the metadata was collected.
|
||||
// It is provided in addition to CollectedAt to protect against clock skew.
|
||||
Age int64 `json:"age"`
|
||||
Value string `json:"value"`
|
||||
Error string `json:"error"`
|
||||
}
|
||||
|
||||
// WorkspaceAgentMetadataDescription is a description of dynamic metadata the agent should report
|
||||
// back to coderd. It is provided via the `metadata` list in the `coder_agent`
|
||||
// block.
|
||||
type WorkspaceAgentMetadataDescription struct {
|
||||
DisplayName string `json:"display_name"`
|
||||
Key string `json:"key"`
|
||||
Script string `json:"script"`
|
||||
Interval int64 `json:"interval"`
|
||||
Timeout int64 `json:"timeout"`
|
||||
}
|
||||
|
||||
type WorkspaceAgentMetadata struct {
|
||||
Result WorkspaceAgentMetadataResult `json:"result"`
|
||||
Description WorkspaceAgentMetadataDescription `json:"description"`
|
||||
}
|
||||
|
||||
type WorkspaceAgent struct {
|
||||
ID uuid.UUID `json:"id" format:"uuid"`
|
||||
CreatedAt time.Time `json:"created_at" format:"date-time"`
|
||||
@@ -258,6 +284,75 @@ func (c *Client) DialWorkspaceAgent(ctx context.Context, agentID uuid.UUID, opti
|
||||
return agentConn, nil
|
||||
}
|
||||
|
||||
// WatchWorkspaceAgentMetadata watches the metadata of a workspace agent.
|
||||
// The returned channel will be closed when the context is canceled. Exactly
|
||||
// one error will be sent on the error channel. The metadata channel is never closed.
|
||||
func (c *Client) WatchWorkspaceAgentMetadata(ctx context.Context, id uuid.UUID) (<-chan []WorkspaceAgentMetadata, <-chan error) {
|
||||
ctx, span := tracing.StartSpan(ctx)
|
||||
defer span.End()
|
||||
|
||||
metadataChan := make(chan []WorkspaceAgentMetadata, 256)
|
||||
|
||||
watch := func() error {
|
||||
res, err := c.Request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/workspaceagents/%s/watch-metadata", id), nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if res.StatusCode != http.StatusOK {
|
||||
return ReadBodyAsError(res)
|
||||
}
|
||||
|
||||
nextEvent := ServerSentEventReader(ctx, res.Body)
|
||||
defer res.Body.Close()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
||||
sse, err := nextEvent()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
b, ok := sse.Data.([]byte)
|
||||
if !ok {
|
||||
return xerrors.Errorf("unexpected data type: %T", sse.Data)
|
||||
}
|
||||
|
||||
switch sse.Type {
|
||||
case ServerSentEventTypeData:
|
||||
var met []WorkspaceAgentMetadata
|
||||
err = json.Unmarshal(b, &met)
|
||||
if err != nil {
|
||||
return xerrors.Errorf("unmarshal metadata: %w", err)
|
||||
}
|
||||
metadataChan <- met
|
||||
case ServerSentEventTypeError:
|
||||
var r Response
|
||||
err = json.Unmarshal(b, &r)
|
||||
if err != nil {
|
||||
return xerrors.Errorf("unmarshal error: %w", err)
|
||||
}
|
||||
return xerrors.Errorf("%+v", r)
|
||||
default:
|
||||
return xerrors.Errorf("unexpected event type: %s", sse.Type)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
errorChan := make(chan error, 1)
|
||||
go func() {
|
||||
defer close(errorChan)
|
||||
errorChan <- watch()
|
||||
}()
|
||||
|
||||
return metadataChan, errorChan
|
||||
}
|
||||
|
||||
// WorkspaceAgent returns an agent by ID.
|
||||
func (c *Client) WorkspaceAgent(ctx context.Context, id uuid.UUID) (WorkspaceAgent, error) {
|
||||
res, err := c.Request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/workspaceagents/%s", id), nil)
|
||||
|
||||
@@ -25,7 +25,7 @@ func TestWorkspaceAgentMetadata(t *testing.T) {
|
||||
// This test ensures that the DERP map returned properly
|
||||
// mutates built-in DERPs with the client access URL.
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
httpapi.Write(context.Background(), w, http.StatusOK, agentsdk.Metadata{
|
||||
httpapi.Write(context.Background(), w, http.StatusOK, agentsdk.Manifest{
|
||||
DERPMap: &tailcfg.DERPMap{
|
||||
Regions: map[int]*tailcfg.DERPRegion{
|
||||
1: {
|
||||
@@ -43,9 +43,9 @@ func TestWorkspaceAgentMetadata(t *testing.T) {
|
||||
parsed, err := url.Parse(srv.URL)
|
||||
require.NoError(t, err)
|
||||
client := agentsdk.New(parsed)
|
||||
metadata, err := client.Metadata(context.Background())
|
||||
manifest, err := client.Manifest(context.Background())
|
||||
require.NoError(t, err)
|
||||
region := metadata.DERPMap.Regions[1]
|
||||
region := manifest.DERPMap.Regions[1]
|
||||
require.True(t, region.EmbeddedRelay)
|
||||
require.Len(t, region.Nodes, 1)
|
||||
node := region.Nodes[0]
|
||||
|
||||
Reference in New Issue
Block a user