feat: add agent metadata (#6614)

This commit is contained in:
Ammar Bandukwala
2023-03-31 15:26:19 -05:00
committed by GitHub
parent c191692751
commit ca4fa81570
62 changed files with 3139 additions and 727 deletions
+40 -21
View File
@@ -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
+95
View File
@@ -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)
+3 -3
View File
@@ -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]