feat: add agentapi endpoint to report connections for audit (#16507)

This change adds a new `ReportConnection` endpoint to the `agentapi`.

The protocol version was bumped previously, so it has been omitted here.

This allows the agent to report connection events, for example when the
user connects to the workspace via SSH or VS Code.

Updates #15139
This commit is contained in:
Mathias Fredriksson
2025-02-20 14:52:01 +02:00
committed by GitHub
parent dedc32fb1a
commit b07b33ec9d
16 changed files with 1488 additions and 709 deletions

View File

@ -372,7 +372,6 @@ func (a *agent) collectMetadata(ctx context.Context, md codersdk.WorkspaceAgentM
// Important: if the command times out, we may see a misleading error like
// "exit status 1", so it's important to include the context error.
err = errors.Join(err, ctx.Err())
if err != nil {
result.Error = fmt.Sprintf("run cmd: %+v", err)
}

View File

@ -15,6 +15,7 @@ import (
"golang.org/x/exp/slices"
"golang.org/x/xerrors"
"google.golang.org/protobuf/types/known/durationpb"
"google.golang.org/protobuf/types/known/emptypb"
"storj.io/drpc/drpcmux"
"storj.io/drpc/drpcserver"
"tailscale.com/tailcfg"
@ -170,6 +171,7 @@ type FakeAgentAPI struct {
lifecycleStates []codersdk.WorkspaceAgentLifecycle
metadata map[string]agentsdk.Metadata
timings []*agentproto.Timing
connections []*agentproto.Connection
getAnnouncementBannersFunc func() ([]codersdk.BannerConfig, error)
getResourcesMonitoringConfigurationFunc func() (*agentproto.GetResourcesMonitoringConfigurationResponse, error)
@ -338,12 +340,20 @@ func (f *FakeAgentAPI) BatchCreateLogs(ctx context.Context, req *agentproto.Batc
func (f *FakeAgentAPI) ScriptCompleted(_ context.Context, req *agentproto.WorkspaceAgentScriptCompletedRequest) (*agentproto.WorkspaceAgentScriptCompletedResponse, error) {
f.Lock()
f.timings = append(f.timings, req.Timing)
f.timings = append(f.timings, req.GetTiming())
f.Unlock()
return &agentproto.WorkspaceAgentScriptCompletedResponse{}, nil
}
func (f *FakeAgentAPI) ReportConnection(_ context.Context, req *agentproto.ReportConnectionRequest) (*emptypb.Empty, error) {
f.Lock()
f.connections = append(f.connections, req.GetConnection())
f.Unlock()
return &emptypb.Empty{}, nil
}
func NewFakeAgentAPI(t testing.TB, logger slog.Logger, manifest *agentproto.Manifest, statsCh chan *agentproto.Stats) *FakeAgentAPI {
return &FakeAgentAPI{
t: t,

File diff suppressed because it is too large Load Diff

View File

@ -6,6 +6,7 @@ package coder.agent.v2;
import "tailnet/proto/tailnet.proto";
import "google/protobuf/timestamp.proto";
import "google/protobuf/duration.proto";
import "google/protobuf/empty.proto";
message WorkspaceApp {
bytes id = 1;
@ -340,6 +341,33 @@ message PushResourcesMonitoringUsageRequest {
message PushResourcesMonitoringUsageResponse {
}
message Connection {
enum Action {
ACTION_UNSPECIFIED = 0;
CONNECT = 1;
DISCONNECT = 2;
}
enum Type {
TYPE_UNSPECIFIED = 0;
SSH = 1;
VSCODE = 2;
JETBRAINS = 3;
RECONNECTING_PTY = 4;
}
bytes id = 1;
Action action = 2;
Type type = 3;
google.protobuf.Timestamp timestamp = 4;
string ip = 5;
int32 status_code = 6;
optional string reason = 7;
}
message ReportConnectionRequest {
Connection connection = 1;
}
service Agent {
rpc GetManifest(GetManifestRequest) returns (Manifest);
rpc GetServiceBanner(GetServiceBannerRequest) returns (ServiceBanner);
@ -353,4 +381,5 @@ service Agent {
rpc ScriptCompleted(WorkspaceAgentScriptCompletedRequest) returns (WorkspaceAgentScriptCompletedResponse);
rpc GetResourcesMonitoringConfiguration(GetResourcesMonitoringConfigurationRequest) returns (GetResourcesMonitoringConfigurationResponse);
rpc PushResourcesMonitoringUsage(PushResourcesMonitoringUsageRequest) returns (PushResourcesMonitoringUsageResponse);
rpc ReportConnection(ReportConnectionRequest) returns (google.protobuf.Empty);
}

View File

@ -9,6 +9,7 @@ import (
errors "errors"
protojson "google.golang.org/protobuf/encoding/protojson"
proto "google.golang.org/protobuf/proto"
emptypb "google.golang.org/protobuf/types/known/emptypb"
drpc "storj.io/drpc"
drpcerr "storj.io/drpc/drpcerr"
)
@ -50,6 +51,7 @@ type DRPCAgentClient interface {
ScriptCompleted(ctx context.Context, in *WorkspaceAgentScriptCompletedRequest) (*WorkspaceAgentScriptCompletedResponse, error)
GetResourcesMonitoringConfiguration(ctx context.Context, in *GetResourcesMonitoringConfigurationRequest) (*GetResourcesMonitoringConfigurationResponse, error)
PushResourcesMonitoringUsage(ctx context.Context, in *PushResourcesMonitoringUsageRequest) (*PushResourcesMonitoringUsageResponse, error)
ReportConnection(ctx context.Context, in *ReportConnectionRequest) (*emptypb.Empty, error)
}
type drpcAgentClient struct {
@ -170,6 +172,15 @@ func (c *drpcAgentClient) PushResourcesMonitoringUsage(ctx context.Context, in *
return out, nil
}
func (c *drpcAgentClient) ReportConnection(ctx context.Context, in *ReportConnectionRequest) (*emptypb.Empty, error) {
out := new(emptypb.Empty)
err := c.cc.Invoke(ctx, "/coder.agent.v2.Agent/ReportConnection", drpcEncoding_File_agent_proto_agent_proto{}, in, out)
if err != nil {
return nil, err
}
return out, nil
}
type DRPCAgentServer interface {
GetManifest(context.Context, *GetManifestRequest) (*Manifest, error)
GetServiceBanner(context.Context, *GetServiceBannerRequest) (*ServiceBanner, error)
@ -183,6 +194,7 @@ type DRPCAgentServer interface {
ScriptCompleted(context.Context, *WorkspaceAgentScriptCompletedRequest) (*WorkspaceAgentScriptCompletedResponse, error)
GetResourcesMonitoringConfiguration(context.Context, *GetResourcesMonitoringConfigurationRequest) (*GetResourcesMonitoringConfigurationResponse, error)
PushResourcesMonitoringUsage(context.Context, *PushResourcesMonitoringUsageRequest) (*PushResourcesMonitoringUsageResponse, error)
ReportConnection(context.Context, *ReportConnectionRequest) (*emptypb.Empty, error)
}
type DRPCAgentUnimplementedServer struct{}
@ -235,9 +247,13 @@ func (s *DRPCAgentUnimplementedServer) PushResourcesMonitoringUsage(context.Cont
return nil, drpcerr.WithCode(errors.New("Unimplemented"), drpcerr.Unimplemented)
}
func (s *DRPCAgentUnimplementedServer) ReportConnection(context.Context, *ReportConnectionRequest) (*emptypb.Empty, error) {
return nil, drpcerr.WithCode(errors.New("Unimplemented"), drpcerr.Unimplemented)
}
type DRPCAgentDescription struct{}
func (DRPCAgentDescription) NumMethods() int { return 12 }
func (DRPCAgentDescription) NumMethods() int { return 13 }
func (DRPCAgentDescription) Method(n int) (string, drpc.Encoding, drpc.Receiver, interface{}, bool) {
switch n {
@ -349,6 +365,15 @@ func (DRPCAgentDescription) Method(n int) (string, drpc.Encoding, drpc.Receiver,
in1.(*PushResourcesMonitoringUsageRequest),
)
}, DRPCAgentServer.PushResourcesMonitoringUsage, true
case 12:
return "/coder.agent.v2.Agent/ReportConnection", drpcEncoding_File_agent_proto_agent_proto{},
func(srv interface{}, ctx context.Context, in1, in2 interface{}) (drpc.Message, error) {
return srv.(DRPCAgentServer).
ReportConnection(
ctx,
in1.(*ReportConnectionRequest),
)
}, DRPCAgentServer.ReportConnection, true
default:
return "", nil, nil, nil, false
}
@ -549,3 +574,19 @@ func (x *drpcAgent_PushResourcesMonitoringUsageStream) SendAndClose(m *PushResou
}
return x.CloseSend()
}
type DRPCAgent_ReportConnectionStream interface {
drpc.Stream
SendAndClose(*emptypb.Empty) error
}
type drpcAgent_ReportConnectionStream struct {
drpc.Stream
}
func (x *drpcAgent_ReportConnectionStream) SendAndClose(m *emptypb.Empty) error {
if err := x.MsgSend(m, drpcEncoding_File_agent_proto_agent_proto{}); err != nil {
return err
}
return x.CloseSend()
}

View File

@ -3,6 +3,7 @@ package proto
import (
"context"
emptypb "google.golang.org/protobuf/types/known/emptypb"
"storj.io/drpc"
)
@ -41,10 +42,11 @@ type DRPCAgentClient23 interface {
ScriptCompleted(ctx context.Context, in *WorkspaceAgentScriptCompletedRequest) (*WorkspaceAgentScriptCompletedResponse, error)
}
// DRPCAgentClient24 is the Agent API at v2.4. It adds the GetResourcesMonitoringConfiguration and
// PushResourcesMonitoringUsage RPCs. Compatible with Coder v2.19+
// DRPCAgentClient24 is the Agent API at v2.4. It adds the GetResourcesMonitoringConfiguration,
// PushResourcesMonitoringUsage and ReportConnection RPCs. Compatible with Coder v2.19+
type DRPCAgentClient24 interface {
DRPCAgentClient23
GetResourcesMonitoringConfiguration(ctx context.Context, in *GetResourcesMonitoringConfigurationRequest) (*GetResourcesMonitoringConfigurationResponse, error)
PushResourcesMonitoringUsage(ctx context.Context, in *PushResourcesMonitoringUsageRequest) (*PushResourcesMonitoringUsageResponse, error)
ReportConnection(ctx context.Context, in *ReportConnectionRequest) (*emptypb.Empty, error)
}

View File

@ -19,6 +19,7 @@ import (
agentproto "github.com/coder/coder/v2/agent/proto"
"github.com/coder/coder/v2/coderd/agentapi/resourcesmonitor"
"github.com/coder/coder/v2/coderd/appearance"
"github.com/coder/coder/v2/coderd/audit"
"github.com/coder/coder/v2/coderd/database"
"github.com/coder/coder/v2/coderd/database/pubsub"
"github.com/coder/coder/v2/coderd/externalauth"
@ -48,6 +49,7 @@ type API struct {
*ResourcesMonitoringAPI
*LogsAPI
*ScriptsAPI
*AuditAPI
*tailnet.DRPCService
mu sync.Mutex
@ -66,6 +68,7 @@ type Options struct {
Database database.Store
NotificationsEnqueuer notifications.Enqueuer
Pubsub pubsub.Pubsub
Auditor *atomic.Pointer[audit.Auditor]
DerpMapFn func() *tailcfg.DERPMap
TailnetCoordinator *atomic.Pointer[tailnet.Coordinator]
StatsReporter *workspacestats.Reporter
@ -174,6 +177,13 @@ func New(opts Options) *API {
Database: opts.Database,
}
api.AuditAPI = &AuditAPI{
AgentFn: api.agent,
Auditor: opts.Auditor,
Database: opts.Database,
Log: opts.Log,
}
api.DRPCService = &tailnet.DRPCService{
CoordPtr: opts.TailnetCoordinator,
Logger: opts.Log,

105
coderd/agentapi/audit.go Normal file
View File

@ -0,0 +1,105 @@
package agentapi
import (
"context"
"encoding/json"
"strconv"
"sync/atomic"
"github.com/google/uuid"
"golang.org/x/xerrors"
"google.golang.org/protobuf/types/known/emptypb"
"cdr.dev/slog"
agentproto "github.com/coder/coder/v2/agent/proto"
"github.com/coder/coder/v2/coderd/audit"
"github.com/coder/coder/v2/coderd/database"
"github.com/coder/coder/v2/coderd/database/db2sdk"
"github.com/coder/coder/v2/codersdk/agentsdk"
)
type AuditAPI struct {
AgentFn func(context.Context) (database.WorkspaceAgent, error)
Auditor *atomic.Pointer[audit.Auditor]
Database database.Store
Log slog.Logger
}
func (a *AuditAPI) ReportConnection(ctx context.Context, req *agentproto.ReportConnectionRequest) (*emptypb.Empty, error) {
// We will use connection ID as request ID, typically this is the
// SSH session ID as reported by the agent.
connectionID, err := uuid.FromBytes(req.GetConnection().GetId())
if err != nil {
return nil, xerrors.Errorf("connection id from bytes: %w", err)
}
action, err := db2sdk.AuditActionFromAgentProtoConnectionAction(req.GetConnection().GetAction())
if err != nil {
return nil, err
}
connectionType, err := agentsdk.ConnectionTypeFromProto(req.GetConnection().GetType())
if err != nil {
return nil, err
}
// Fetch contextual data for this audit event.
workspaceAgent, err := a.AgentFn(ctx)
if err != nil {
return nil, xerrors.Errorf("get agent: %w", err)
}
workspace, err := a.Database.GetWorkspaceByAgentID(ctx, workspaceAgent.ID)
if err != nil {
return nil, xerrors.Errorf("get workspace by agent id: %w", err)
}
build, err := a.Database.GetLatestWorkspaceBuildByWorkspaceID(ctx, workspace.ID)
if err != nil {
return nil, xerrors.Errorf("get latest workspace build by workspace id: %w", err)
}
// We pass the below information to the Auditor so that it
// can form a friendly string for the user to view in the UI.
type additionalFields struct {
audit.AdditionalFields
ConnectionType agentsdk.ConnectionType `json:"connection_type"`
Reason string `json:"reason,omitempty"`
}
resourceInfo := additionalFields{
AdditionalFields: audit.AdditionalFields{
WorkspaceID: workspace.ID,
WorkspaceName: workspace.Name,
WorkspaceOwner: workspace.OwnerUsername,
BuildNumber: strconv.FormatInt(int64(build.BuildNumber), 10),
BuildReason: database.BuildReason(string(build.Reason)),
},
ConnectionType: connectionType,
Reason: req.GetConnection().GetReason(),
}
riBytes, err := json.Marshal(resourceInfo)
if err != nil {
a.Log.Error(ctx, "marshal resource info for agent connection failed", slog.Error(err))
riBytes = []byte("{}")
}
audit.BackgroundAudit(ctx, &audit.BackgroundAuditParams[database.WorkspaceAgent]{
Audit: *a.Auditor.Load(),
Log: a.Log,
Time: req.GetConnection().GetTimestamp().AsTime(),
OrganizationID: workspace.OrganizationID,
RequestID: connectionID,
Action: action,
New: workspaceAgent,
Old: workspaceAgent,
IP: req.GetConnection().GetIp(),
Status: int(req.GetConnection().GetStatusCode()),
AdditionalFields: riBytes,
// It's not possible to tell which user connected. Once we have
// the capability, this may be reported by the agent.
UserID: uuid.Nil,
})
return &emptypb.Empty{}, nil
}

View File

@ -0,0 +1,179 @@
package agentapi_test
import (
"context"
"encoding/json"
"net"
"sync/atomic"
"testing"
"time"
"github.com/google/uuid"
"github.com/sqlc-dev/pqtype"
"github.com/stretchr/testify/require"
"go.uber.org/mock/gomock"
"google.golang.org/protobuf/types/known/timestamppb"
agentproto "github.com/coder/coder/v2/agent/proto"
"github.com/coder/coder/v2/coderd/agentapi"
"github.com/coder/coder/v2/coderd/audit"
"github.com/coder/coder/v2/coderd/database"
"github.com/coder/coder/v2/coderd/database/db2sdk"
"github.com/coder/coder/v2/coderd/database/dbmock"
"github.com/coder/coder/v2/coderd/database/dbtime"
"github.com/coder/coder/v2/codersdk/agentsdk"
)
func TestAuditReport(t *testing.T) {
t.Parallel()
var (
owner = database.User{
ID: uuid.New(),
Username: "cool-user",
}
workspace = database.Workspace{
ID: uuid.New(),
OrganizationID: uuid.New(),
OwnerID: owner.ID,
Name: "cool-workspace",
}
build = database.WorkspaceBuild{
ID: uuid.New(),
WorkspaceID: workspace.ID,
}
agent = database.WorkspaceAgent{
ID: uuid.New(),
}
)
tests := []struct {
name string
id uuid.UUID
action *agentproto.Connection_Action
typ *agentproto.Connection_Type
time time.Time
ip string
status int32
reason string
}{
{
name: "SSH Connect",
id: uuid.New(),
action: agentproto.Connection_CONNECT.Enum(),
typ: agentproto.Connection_SSH.Enum(),
time: time.Now(),
ip: "127.0.0.1",
status: 200,
},
{
name: "VS Code Connect",
id: uuid.New(),
action: agentproto.Connection_CONNECT.Enum(),
typ: agentproto.Connection_VSCODE.Enum(),
time: time.Now(),
ip: "8.8.8.8",
},
{
name: "JetBrains Connect",
id: uuid.New(),
action: agentproto.Connection_CONNECT.Enum(),
typ: agentproto.Connection_JETBRAINS.Enum(),
time: time.Now(),
},
{
name: "Reconnecting PTY Connect",
id: uuid.New(),
action: agentproto.Connection_CONNECT.Enum(),
typ: agentproto.Connection_RECONNECTING_PTY.Enum(),
time: time.Now(),
},
{
name: "SSH Disconnect",
id: uuid.New(),
action: agentproto.Connection_DISCONNECT.Enum(),
typ: agentproto.Connection_SSH.Enum(),
time: time.Now(),
},
{
name: "SSH Disconnect",
id: uuid.New(),
action: agentproto.Connection_DISCONNECT.Enum(),
typ: agentproto.Connection_SSH.Enum(),
time: time.Now(),
status: 500,
reason: "because error says so",
},
}
//nolint:paralleltest // No longer necessary to reinitialise the variable tt.
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
mAudit := audit.NewMock()
mDB := dbmock.NewMockStore(gomock.NewController(t))
mDB.EXPECT().GetWorkspaceByAgentID(gomock.Any(), agent.ID).Return(workspace, nil)
mDB.EXPECT().GetLatestWorkspaceBuildByWorkspaceID(gomock.Any(), workspace.ID).Return(build, nil)
api := &agentapi.AuditAPI{
Auditor: asAtomicPointer[audit.Auditor](mAudit),
Database: mDB,
AgentFn: func(context.Context) (database.WorkspaceAgent, error) {
return agent, nil
},
}
api.ReportConnection(context.Background(), &agentproto.ReportConnectionRequest{
Connection: &agentproto.Connection{
Id: tt.id[:],
Action: *tt.action,
Type: *tt.typ,
Timestamp: timestamppb.New(tt.time),
Ip: tt.ip,
StatusCode: tt.status,
Reason: &tt.reason,
},
})
mAudit.Contains(t, database.AuditLog{
Time: dbtime.Time(tt.time).In(time.UTC),
Action: agentProtoConnectionActionToAudit(t, *tt.action),
OrganizationID: workspace.OrganizationID,
UserID: uuid.Nil,
RequestID: tt.id,
ResourceType: database.ResourceTypeWorkspaceAgent,
ResourceID: agent.ID,
ResourceTarget: agent.Name,
Ip: pqtype.Inet{Valid: true, IPNet: net.IPNet{IP: net.ParseIP(tt.ip), Mask: net.CIDRMask(32, 32)}},
StatusCode: tt.status,
})
// Check some additional fields.
var m map[string]any
err := json.Unmarshal(mAudit.AuditLogs()[0].AdditionalFields, &m)
require.NoError(t, err)
require.Equal(t, string(agentProtoConnectionTypeToSDK(t, *tt.typ)), m["connection_type"].(string))
if tt.reason != "" {
require.Equal(t, tt.reason, m["reason"])
}
})
}
}
func agentProtoConnectionActionToAudit(t *testing.T, action agentproto.Connection_Action) database.AuditAction {
a, err := db2sdk.AuditActionFromAgentProtoConnectionAction(action)
require.NoError(t, err)
return a
}
func agentProtoConnectionTypeToSDK(t *testing.T, typ agentproto.Connection_Type) agentsdk.ConnectionType {
action, err := agentsdk.ConnectionTypeFromProto(typ)
require.NoError(t, err)
return action
}
func asAtomicPointer[T any](v T) *atomic.Pointer[T] {
var p atomic.Pointer[T]
p.Store(&v)
return &p
}

View File

@ -9,6 +9,7 @@ import (
"net"
"net/http"
"strconv"
"time"
"github.com/google/uuid"
"github.com/sqlc-dev/pqtype"
@ -65,6 +66,7 @@ type BackgroundAuditParams[T Auditable] struct {
UserID uuid.UUID
RequestID uuid.UUID
Time time.Time
Status int
Action database.AuditAction
OrganizationID uuid.UUID
@ -461,13 +463,19 @@ func BackgroundAudit[T Auditable](ctx context.Context, p *BackgroundAuditParams[
diffRaw = []byte("{}")
}
if p.Time.IsZero() {
p.Time = dbtime.Now()
} else {
// NOTE(mafredri): dbtime.Time does not currently enforce UTC.
p.Time = dbtime.Time(p.Time.In(time.UTC))
}
if p.AdditionalFields == nil {
p.AdditionalFields = json.RawMessage("{}")
}
auditLog := database.AuditLog{
ID: uuid.New(),
Time: dbtime.Now(),
Time: p.Time,
UserID: p.UserID,
OrganizationID: requireOrgID[T](ctx, p.OrganizationID, p.Log),
Ip: ip,

View File

@ -15,6 +15,7 @@ import (
"golang.org/x/xerrors"
"tailscale.com/tailcfg"
agentproto "github.com/coder/coder/v2/agent/proto"
"github.com/coder/coder/v2/coderd/database"
"github.com/coder/coder/v2/coderd/rbac"
"github.com/coder/coder/v2/coderd/rbac/policy"
@ -705,3 +706,26 @@ func TemplateRoleActions(role codersdk.TemplateRole) []policy.Action {
}
return []policy.Action{}
}
func AuditActionFromAgentProtoConnectionAction(action agentproto.Connection_Action) (database.AuditAction, error) {
switch action {
case agentproto.Connection_CONNECT:
return database.AuditActionConnect, nil
case agentproto.Connection_DISCONNECT:
return database.AuditActionDisconnect, nil
default:
// Also Connection_ACTION_UNSPECIFIED, no mapping.
return "", xerrors.Errorf("unknown agent connection action %q", action)
}
}
func AgentProtoConnectionActionToAuditAction(action database.AuditAction) (agentproto.Connection_Action, error) {
switch action {
case database.AuditActionConnect:
return agentproto.Connection_CONNECT, nil
case database.AuditActionDisconnect:
return agentproto.Connection_DISCONNECT, nil
default:
return agentproto.Connection_ACTION_UNSPECIFIED, xerrors.Errorf("unknown agent connection action %q", action)
}
}

View File

@ -147,6 +147,7 @@ func (api *API) workspaceAgentRPC(rw http.ResponseWriter, r *http.Request) {
Database: api.Database,
NotificationsEnqueuer: api.NotificationsEnqueuer,
Pubsub: api.Pubsub,
Auditor: &api.Auditor,
DerpMapFn: api.DERPMap,
TailnetCoordinator: &api.TailnetCoordinator,
AppearanceFetcher: &api.AppearanceFetcher,

View File

@ -34,6 +34,18 @@ import (
// log-source. This should be removed in the future.
var ExternalLogSourceID = uuid.MustParse("3b579bf4-1ed8-4b99-87a8-e9a1e3410410")
// ConnectionType is the type of connection that the agent is receiving.
type ConnectionType string
// Connection type enums.
const (
ConnectionTypeUnspecified ConnectionType = "Unspecified"
ConnectionTypeSSH ConnectionType = "SSH"
ConnectionTypeVSCode ConnectionType = "VS Code"
ConnectionTypeJetBrains ConnectionType = "JetBrains"
ConnectionTypeReconnectingPTY ConnectionType = "Web Terminal"
)
// New returns a client that is used to interact with the
// Coder API from a workspace agent.
func New(serverURL *url.URL) *Client {

View File

@ -390,3 +390,37 @@ func ProtoFromLifecycleState(s codersdk.WorkspaceAgentLifecycle) (proto.Lifecycl
}
return proto.Lifecycle_State(caps), nil
}
func ConnectionTypeFromProto(typ proto.Connection_Type) (ConnectionType, error) {
switch typ {
case proto.Connection_TYPE_UNSPECIFIED:
return ConnectionTypeUnspecified, nil
case proto.Connection_SSH:
return ConnectionTypeSSH, nil
case proto.Connection_VSCODE:
return ConnectionTypeVSCode, nil
case proto.Connection_JETBRAINS:
return ConnectionTypeJetBrains, nil
case proto.Connection_RECONNECTING_PTY:
return ConnectionTypeReconnectingPTY, nil
default:
return "", xerrors.Errorf("unknown connection type %q", typ)
}
}
func ProtoFromConnectionType(typ ConnectionType) (proto.Connection_Type, error) {
switch typ {
case ConnectionTypeUnspecified:
return proto.Connection_TYPE_UNSPECIFIED, nil
case ConnectionTypeSSH:
return proto.Connection_SSH, nil
case ConnectionTypeVSCode:
return proto.Connection_VSCODE, nil
case ConnectionTypeJetBrains:
return proto.Connection_JETBRAINS, nil
case ConnectionTypeReconnectingPTY:
return proto.Connection_RECONNECTING_PTY, nil
default:
return 0, xerrors.Errorf("unknown connection type %q", typ)
}
}

View File

@ -36,7 +36,7 @@ type DRPCTailnetClient23 interface {
}
// DRPCTailnetClient24 is the Tailnet API at v2.4. It is functionally identical to 2.3, because the
// change was to the Agent API (ResourcesMonitoring methods).
// change was to the Agent API (ResourcesMonitoring and ReportConnection methods).
type DRPCTailnetClient24 interface {
DRPCTailnetClient23
}

View File

@ -43,6 +43,8 @@ import (
// - Shipped in Coder v2.{{placeholder}} // TODO Vincent: Replace with the correct version
// - Added support for GetResourcesMonitoringConfiguration and
// PushResourcesMonitoringUsage RPCs on the Agent API.
// - Added support for reporting connection events for auditing via the
// ReportConnection RPC on the Agent API.
const (
CurrentMajor = 2
CurrentMinor = 4