feat(coderd/agentapi): implement sub agent api (#17823)
Closes https://github.com/coder/internal/issues/619 Implement the `coderd` side of the AgentAPI for the upcoming dev-container agents work. `agent/agenttest/client.go` is left unimplemented for a future PR working to implement the agent side of this feature.
This commit is contained in:
+3
-3
@@ -95,8 +95,8 @@ type Options struct {
|
||||
}
|
||||
|
||||
type Client interface {
|
||||
ConnectRPC25(ctx context.Context) (
|
||||
proto.DRPCAgentClient25, tailnetproto.DRPCTailnetClient25, error,
|
||||
ConnectRPC26(ctx context.Context) (
|
||||
proto.DRPCAgentClient26, tailnetproto.DRPCTailnetClient26, error,
|
||||
)
|
||||
RewriteDERPMap(derpMap *tailcfg.DERPMap)
|
||||
}
|
||||
@@ -908,7 +908,7 @@ func (a *agent) run() (retErr error) {
|
||||
a.sessionToken.Store(&sessionToken)
|
||||
|
||||
// ConnectRPC returns the dRPC connection we use for the Agent and Tailnet v2+ APIs
|
||||
aAPI, tAPI, err := a.client.ConnectRPC25(a.hardCtx)
|
||||
aAPI, tAPI, err := a.client.ConnectRPC26(a.hardCtx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -98,8 +98,8 @@ func (c *Client) Close() {
|
||||
c.derpMapOnce.Do(func() { close(c.derpMapUpdates) })
|
||||
}
|
||||
|
||||
func (c *Client) ConnectRPC25(ctx context.Context) (
|
||||
agentproto.DRPCAgentClient25, proto.DRPCTailnetClient25, error,
|
||||
func (c *Client) ConnectRPC26(ctx context.Context) (
|
||||
agentproto.DRPCAgentClient26, proto.DRPCTailnetClient26, error,
|
||||
) {
|
||||
conn, lis := drpcsdk.MemTransportPipe()
|
||||
c.LastWorkspaceAgent = func() {
|
||||
@@ -365,6 +365,18 @@ func (f *FakeAgentAPI) GetConnectionReports() []*agentproto.ReportConnectionRequ
|
||||
return slices.Clone(f.connectionReports)
|
||||
}
|
||||
|
||||
func (*FakeAgentAPI) CreateSubAgent(_ context.Context, _ *agentproto.CreateSubAgentRequest) (*agentproto.CreateSubAgentResponse, error) {
|
||||
panic("unimplemented")
|
||||
}
|
||||
|
||||
func (*FakeAgentAPI) DeleteSubAgent(_ context.Context, _ *agentproto.DeleteSubAgentRequest) (*agentproto.DeleteSubAgentResponse, error) {
|
||||
panic("unimplemented")
|
||||
}
|
||||
|
||||
func (*FakeAgentAPI) ListSubAgents(_ context.Context, _ *agentproto.ListSubAgentsRequest) (*agentproto.ListSubAgentsResponse, error) {
|
||||
panic("unimplemented")
|
||||
}
|
||||
|
||||
func NewFakeAgentAPI(t testing.TB, logger slog.Logger, manifest *agentproto.Manifest, statsCh chan *agentproto.Stats) *FakeAgentAPI {
|
||||
return &FakeAgentAPI{
|
||||
t: t,
|
||||
|
||||
+733
-235
File diff suppressed because it is too large
Load Diff
@@ -377,6 +377,35 @@ message ReportConnectionRequest {
|
||||
Connection connection = 1;
|
||||
}
|
||||
|
||||
message SubAgent {
|
||||
string name = 1;
|
||||
bytes id = 2;
|
||||
bytes auth_token = 3;
|
||||
}
|
||||
|
||||
message CreateSubAgentRequest {
|
||||
string name = 1;
|
||||
string directory = 2;
|
||||
string architecture = 3;
|
||||
string operating_system = 4;
|
||||
}
|
||||
|
||||
message CreateSubAgentResponse {
|
||||
SubAgent agent = 1;
|
||||
}
|
||||
|
||||
message DeleteSubAgentRequest {
|
||||
bytes id = 1;
|
||||
}
|
||||
|
||||
message DeleteSubAgentResponse {}
|
||||
|
||||
message ListSubAgentsRequest {}
|
||||
|
||||
message ListSubAgentsResponse {
|
||||
repeated SubAgent agents = 1;
|
||||
}
|
||||
|
||||
service Agent {
|
||||
rpc GetManifest(GetManifestRequest) returns (Manifest);
|
||||
rpc GetServiceBanner(GetServiceBannerRequest) returns (ServiceBanner);
|
||||
@@ -391,4 +420,7 @@ service Agent {
|
||||
rpc GetResourcesMonitoringConfiguration(GetResourcesMonitoringConfigurationRequest) returns (GetResourcesMonitoringConfigurationResponse);
|
||||
rpc PushResourcesMonitoringUsage(PushResourcesMonitoringUsageRequest) returns (PushResourcesMonitoringUsageResponse);
|
||||
rpc ReportConnection(ReportConnectionRequest) returns (google.protobuf.Empty);
|
||||
rpc CreateSubAgent(CreateSubAgentRequest) returns (CreateSubAgentResponse);
|
||||
rpc DeleteSubAgent(DeleteSubAgentRequest) returns (DeleteSubAgentResponse);
|
||||
rpc ListSubAgents(ListSubAgentsRequest) returns (ListSubAgentsResponse);
|
||||
}
|
||||
|
||||
@@ -52,6 +52,9 @@ type DRPCAgentClient interface {
|
||||
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)
|
||||
CreateSubAgent(ctx context.Context, in *CreateSubAgentRequest) (*CreateSubAgentResponse, error)
|
||||
DeleteSubAgent(ctx context.Context, in *DeleteSubAgentRequest) (*DeleteSubAgentResponse, error)
|
||||
ListSubAgents(ctx context.Context, in *ListSubAgentsRequest) (*ListSubAgentsResponse, error)
|
||||
}
|
||||
|
||||
type drpcAgentClient struct {
|
||||
@@ -181,6 +184,33 @@ func (c *drpcAgentClient) ReportConnection(ctx context.Context, in *ReportConnec
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *drpcAgentClient) CreateSubAgent(ctx context.Context, in *CreateSubAgentRequest) (*CreateSubAgentResponse, error) {
|
||||
out := new(CreateSubAgentResponse)
|
||||
err := c.cc.Invoke(ctx, "/coder.agent.v2.Agent/CreateSubAgent", drpcEncoding_File_agent_proto_agent_proto{}, in, out)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *drpcAgentClient) DeleteSubAgent(ctx context.Context, in *DeleteSubAgentRequest) (*DeleteSubAgentResponse, error) {
|
||||
out := new(DeleteSubAgentResponse)
|
||||
err := c.cc.Invoke(ctx, "/coder.agent.v2.Agent/DeleteSubAgent", drpcEncoding_File_agent_proto_agent_proto{}, in, out)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *drpcAgentClient) ListSubAgents(ctx context.Context, in *ListSubAgentsRequest) (*ListSubAgentsResponse, error) {
|
||||
out := new(ListSubAgentsResponse)
|
||||
err := c.cc.Invoke(ctx, "/coder.agent.v2.Agent/ListSubAgents", 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)
|
||||
@@ -195,6 +225,9 @@ type DRPCAgentServer interface {
|
||||
GetResourcesMonitoringConfiguration(context.Context, *GetResourcesMonitoringConfigurationRequest) (*GetResourcesMonitoringConfigurationResponse, error)
|
||||
PushResourcesMonitoringUsage(context.Context, *PushResourcesMonitoringUsageRequest) (*PushResourcesMonitoringUsageResponse, error)
|
||||
ReportConnection(context.Context, *ReportConnectionRequest) (*emptypb.Empty, error)
|
||||
CreateSubAgent(context.Context, *CreateSubAgentRequest) (*CreateSubAgentResponse, error)
|
||||
DeleteSubAgent(context.Context, *DeleteSubAgentRequest) (*DeleteSubAgentResponse, error)
|
||||
ListSubAgents(context.Context, *ListSubAgentsRequest) (*ListSubAgentsResponse, error)
|
||||
}
|
||||
|
||||
type DRPCAgentUnimplementedServer struct{}
|
||||
@@ -251,9 +284,21 @@ func (s *DRPCAgentUnimplementedServer) ReportConnection(context.Context, *Report
|
||||
return nil, drpcerr.WithCode(errors.New("Unimplemented"), drpcerr.Unimplemented)
|
||||
}
|
||||
|
||||
func (s *DRPCAgentUnimplementedServer) CreateSubAgent(context.Context, *CreateSubAgentRequest) (*CreateSubAgentResponse, error) {
|
||||
return nil, drpcerr.WithCode(errors.New("Unimplemented"), drpcerr.Unimplemented)
|
||||
}
|
||||
|
||||
func (s *DRPCAgentUnimplementedServer) DeleteSubAgent(context.Context, *DeleteSubAgentRequest) (*DeleteSubAgentResponse, error) {
|
||||
return nil, drpcerr.WithCode(errors.New("Unimplemented"), drpcerr.Unimplemented)
|
||||
}
|
||||
|
||||
func (s *DRPCAgentUnimplementedServer) ListSubAgents(context.Context, *ListSubAgentsRequest) (*ListSubAgentsResponse, error) {
|
||||
return nil, drpcerr.WithCode(errors.New("Unimplemented"), drpcerr.Unimplemented)
|
||||
}
|
||||
|
||||
type DRPCAgentDescription struct{}
|
||||
|
||||
func (DRPCAgentDescription) NumMethods() int { return 13 }
|
||||
func (DRPCAgentDescription) NumMethods() int { return 16 }
|
||||
|
||||
func (DRPCAgentDescription) Method(n int) (string, drpc.Encoding, drpc.Receiver, interface{}, bool) {
|
||||
switch n {
|
||||
@@ -374,6 +419,33 @@ func (DRPCAgentDescription) Method(n int) (string, drpc.Encoding, drpc.Receiver,
|
||||
in1.(*ReportConnectionRequest),
|
||||
)
|
||||
}, DRPCAgentServer.ReportConnection, true
|
||||
case 13:
|
||||
return "/coder.agent.v2.Agent/CreateSubAgent", drpcEncoding_File_agent_proto_agent_proto{},
|
||||
func(srv interface{}, ctx context.Context, in1, in2 interface{}) (drpc.Message, error) {
|
||||
return srv.(DRPCAgentServer).
|
||||
CreateSubAgent(
|
||||
ctx,
|
||||
in1.(*CreateSubAgentRequest),
|
||||
)
|
||||
}, DRPCAgentServer.CreateSubAgent, true
|
||||
case 14:
|
||||
return "/coder.agent.v2.Agent/DeleteSubAgent", drpcEncoding_File_agent_proto_agent_proto{},
|
||||
func(srv interface{}, ctx context.Context, in1, in2 interface{}) (drpc.Message, error) {
|
||||
return srv.(DRPCAgentServer).
|
||||
DeleteSubAgent(
|
||||
ctx,
|
||||
in1.(*DeleteSubAgentRequest),
|
||||
)
|
||||
}, DRPCAgentServer.DeleteSubAgent, true
|
||||
case 15:
|
||||
return "/coder.agent.v2.Agent/ListSubAgents", drpcEncoding_File_agent_proto_agent_proto{},
|
||||
func(srv interface{}, ctx context.Context, in1, in2 interface{}) (drpc.Message, error) {
|
||||
return srv.(DRPCAgentServer).
|
||||
ListSubAgents(
|
||||
ctx,
|
||||
in1.(*ListSubAgentsRequest),
|
||||
)
|
||||
}, DRPCAgentServer.ListSubAgents, true
|
||||
default:
|
||||
return "", nil, nil, nil, false
|
||||
}
|
||||
@@ -590,3 +662,51 @@ func (x *drpcAgent_ReportConnectionStream) SendAndClose(m *emptypb.Empty) error
|
||||
}
|
||||
return x.CloseSend()
|
||||
}
|
||||
|
||||
type DRPCAgent_CreateSubAgentStream interface {
|
||||
drpc.Stream
|
||||
SendAndClose(*CreateSubAgentResponse) error
|
||||
}
|
||||
|
||||
type drpcAgent_CreateSubAgentStream struct {
|
||||
drpc.Stream
|
||||
}
|
||||
|
||||
func (x *drpcAgent_CreateSubAgentStream) SendAndClose(m *CreateSubAgentResponse) error {
|
||||
if err := x.MsgSend(m, drpcEncoding_File_agent_proto_agent_proto{}); err != nil {
|
||||
return err
|
||||
}
|
||||
return x.CloseSend()
|
||||
}
|
||||
|
||||
type DRPCAgent_DeleteSubAgentStream interface {
|
||||
drpc.Stream
|
||||
SendAndClose(*DeleteSubAgentResponse) error
|
||||
}
|
||||
|
||||
type drpcAgent_DeleteSubAgentStream struct {
|
||||
drpc.Stream
|
||||
}
|
||||
|
||||
func (x *drpcAgent_DeleteSubAgentStream) SendAndClose(m *DeleteSubAgentResponse) error {
|
||||
if err := x.MsgSend(m, drpcEncoding_File_agent_proto_agent_proto{}); err != nil {
|
||||
return err
|
||||
}
|
||||
return x.CloseSend()
|
||||
}
|
||||
|
||||
type DRPCAgent_ListSubAgentsStream interface {
|
||||
drpc.Stream
|
||||
SendAndClose(*ListSubAgentsResponse) error
|
||||
}
|
||||
|
||||
type drpcAgent_ListSubAgentsStream struct {
|
||||
drpc.Stream
|
||||
}
|
||||
|
||||
func (x *drpcAgent_ListSubAgentsStream) SendAndClose(m *ListSubAgentsResponse) error {
|
||||
if err := x.MsgSend(m, drpcEncoding_File_agent_proto_agent_proto{}); err != nil {
|
||||
return err
|
||||
}
|
||||
return x.CloseSend()
|
||||
}
|
||||
|
||||
@@ -51,7 +51,17 @@ type DRPCAgentClient24 interface {
|
||||
ReportConnection(ctx context.Context, in *ReportConnectionRequest) (*emptypb.Empty, error)
|
||||
}
|
||||
|
||||
// DRPCAgentClient25 is the Agent API at v2.5.
|
||||
// DRPCAgentClient25 is the Agent API at v2.5. It adds a ParentId field to the
|
||||
// agent manifest response. Compatible with Coder v2.23+
|
||||
type DRPCAgentClient25 interface {
|
||||
DRPCAgentClient24
|
||||
}
|
||||
|
||||
// DRPCAgentClient26 is the Agent API at v2.6. It adds the CreateSubAgent,
|
||||
// DeleteSubAgent and ListSubAgents RPCs. Compatible with Coder v2.24+
|
||||
type DRPCAgentClient26 interface {
|
||||
DRPCAgentClient25
|
||||
CreateSubAgent(ctx context.Context, in *CreateSubAgentRequest) (*CreateSubAgentResponse, error)
|
||||
DeleteSubAgent(ctx context.Context, in *DeleteSubAgentRequest) (*DeleteSubAgentResponse, error)
|
||||
ListSubAgents(ctx context.Context, in *ListSubAgentsRequest) (*ListSubAgentsResponse, error)
|
||||
}
|
||||
|
||||
+15
-3
@@ -51,6 +51,7 @@ type API struct {
|
||||
*LogsAPI
|
||||
*ScriptsAPI
|
||||
*AuditAPI
|
||||
*SubAgentAPI
|
||||
*tailnet.DRPCService
|
||||
|
||||
mu sync.Mutex
|
||||
@@ -59,9 +60,10 @@ type API struct {
|
||||
var _ agentproto.DRPCAgentServer = &API{}
|
||||
|
||||
type Options struct {
|
||||
AgentID uuid.UUID
|
||||
OwnerID uuid.UUID
|
||||
WorkspaceID uuid.UUID
|
||||
AgentID uuid.UUID
|
||||
OwnerID uuid.UUID
|
||||
WorkspaceID uuid.UUID
|
||||
OrganizationID uuid.UUID
|
||||
|
||||
Ctx context.Context
|
||||
Log slog.Logger
|
||||
@@ -193,6 +195,16 @@ func New(opts Options) *API {
|
||||
NetworkTelemetryHandler: opts.NetworkTelemetryHandler,
|
||||
}
|
||||
|
||||
api.SubAgentAPI = &SubAgentAPI{
|
||||
OwnerID: opts.OwnerID,
|
||||
OrganizationID: opts.OrganizationID,
|
||||
AgentID: opts.AgentID,
|
||||
AgentFn: api.agent,
|
||||
Log: opts.Log,
|
||||
Clock: opts.Clock,
|
||||
Database: opts.Database,
|
||||
}
|
||||
|
||||
return api
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,119 @@
|
||||
package agentapi
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/sqlc-dev/pqtype"
|
||||
"golang.org/x/xerrors"
|
||||
|
||||
"cdr.dev/slog"
|
||||
agentproto "github.com/coder/coder/v2/agent/proto"
|
||||
"github.com/coder/coder/v2/coderd/database"
|
||||
"github.com/coder/coder/v2/coderd/database/dbauthz"
|
||||
"github.com/coder/coder/v2/provisioner"
|
||||
"github.com/coder/quartz"
|
||||
)
|
||||
|
||||
type SubAgentAPI struct {
|
||||
OwnerID uuid.UUID
|
||||
OrganizationID uuid.UUID
|
||||
AgentID uuid.UUID
|
||||
AgentFn func(context.Context) (database.WorkspaceAgent, error)
|
||||
|
||||
Log slog.Logger
|
||||
Clock quartz.Clock
|
||||
Database database.Store
|
||||
}
|
||||
|
||||
func (a *SubAgentAPI) CreateSubAgent(ctx context.Context, req *agentproto.CreateSubAgentRequest) (*agentproto.CreateSubAgentResponse, error) {
|
||||
//nolint:gocritic // This gives us only the permissions required to do the job.
|
||||
ctx = dbauthz.AsSubAgentAPI(ctx, a.OrganizationID, a.OwnerID)
|
||||
|
||||
parentAgent, err := a.AgentFn(ctx)
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("get parent agent: %w", err)
|
||||
}
|
||||
|
||||
agentName := req.Name
|
||||
if agentName == "" {
|
||||
return nil, xerrors.Errorf("agent name cannot be empty")
|
||||
}
|
||||
if !provisioner.AgentNameRegex.MatchString(agentName) {
|
||||
return nil, xerrors.Errorf("agent name %q does not match regex %q", agentName, provisioner.AgentNameRegex.String())
|
||||
}
|
||||
|
||||
createdAt := a.Clock.Now()
|
||||
|
||||
subAgent, err := a.Database.InsertWorkspaceAgent(ctx, database.InsertWorkspaceAgentParams{
|
||||
ID: uuid.New(),
|
||||
ParentID: uuid.NullUUID{Valid: true, UUID: parentAgent.ID},
|
||||
CreatedAt: createdAt,
|
||||
UpdatedAt: createdAt,
|
||||
Name: agentName,
|
||||
ResourceID: parentAgent.ResourceID,
|
||||
AuthToken: uuid.New(),
|
||||
AuthInstanceID: parentAgent.AuthInstanceID,
|
||||
Architecture: req.Architecture,
|
||||
EnvironmentVariables: pqtype.NullRawMessage{},
|
||||
OperatingSystem: req.OperatingSystem,
|
||||
Directory: req.Directory,
|
||||
InstanceMetadata: pqtype.NullRawMessage{},
|
||||
ResourceMetadata: pqtype.NullRawMessage{},
|
||||
ConnectionTimeoutSeconds: parentAgent.ConnectionTimeoutSeconds,
|
||||
TroubleshootingURL: parentAgent.TroubleshootingURL,
|
||||
MOTDFile: "",
|
||||
DisplayApps: []database.DisplayApp{},
|
||||
DisplayOrder: 0,
|
||||
APIKeyScope: parentAgent.APIKeyScope,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("insert sub agent: %w", err)
|
||||
}
|
||||
|
||||
return &agentproto.CreateSubAgentResponse{
|
||||
Agent: &agentproto.SubAgent{
|
||||
Name: subAgent.Name,
|
||||
Id: subAgent.ID[:],
|
||||
AuthToken: subAgent.AuthToken[:],
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (a *SubAgentAPI) DeleteSubAgent(ctx context.Context, req *agentproto.DeleteSubAgentRequest) (*agentproto.DeleteSubAgentResponse, error) {
|
||||
//nolint:gocritic // This gives us only the permissions required to do the job.
|
||||
ctx = dbauthz.AsSubAgentAPI(ctx, a.OrganizationID, a.OwnerID)
|
||||
|
||||
subAgentID, err := uuid.FromBytes(req.Id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := a.Database.DeleteWorkspaceSubAgentByID(ctx, subAgentID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &agentproto.DeleteSubAgentResponse{}, nil
|
||||
}
|
||||
|
||||
func (a *SubAgentAPI) ListSubAgents(ctx context.Context, _ *agentproto.ListSubAgentsRequest) (*agentproto.ListSubAgentsResponse, error) {
|
||||
//nolint:gocritic // This gives us only the permissions required to do the job.
|
||||
ctx = dbauthz.AsSubAgentAPI(ctx, a.OrganizationID, a.OwnerID)
|
||||
|
||||
workspaceAgents, err := a.Database.GetWorkspaceAgentsByParentID(ctx, a.AgentID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
agents := make([]*agentproto.SubAgent, len(workspaceAgents))
|
||||
|
||||
for i, agent := range workspaceAgents {
|
||||
agents[i] = &agentproto.SubAgent{
|
||||
Name: agent.Name,
|
||||
Id: agent.ID[:],
|
||||
AuthToken: agent.AuthToken[:],
|
||||
}
|
||||
}
|
||||
|
||||
return &agentproto.ListSubAgentsResponse{Agents: agents}, nil
|
||||
}
|
||||
@@ -0,0 +1,412 @@
|
||||
package agentapi_test
|
||||
|
||||
import (
|
||||
"cmp"
|
||||
"context"
|
||||
"database/sql"
|
||||
"slices"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"cdr.dev/slog"
|
||||
"github.com/coder/coder/v2/agent/proto"
|
||||
"github.com/coder/coder/v2/coderd/agentapi"
|
||||
"github.com/coder/coder/v2/coderd/database"
|
||||
"github.com/coder/coder/v2/coderd/database/dbauthz"
|
||||
"github.com/coder/coder/v2/coderd/database/dbgen"
|
||||
"github.com/coder/coder/v2/coderd/database/dbtestutil"
|
||||
"github.com/coder/coder/v2/coderd/rbac"
|
||||
"github.com/coder/coder/v2/testutil"
|
||||
"github.com/coder/quartz"
|
||||
)
|
||||
|
||||
func TestSubAgentAPI(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
newDatabaseWithOrg := func(t *testing.T) (database.Store, database.Organization) {
|
||||
db, _ := dbtestutil.NewDB(t)
|
||||
org := dbgen.Organization(t, db, database.Organization{})
|
||||
return db, org
|
||||
}
|
||||
|
||||
newUserWithWorkspaceAgent := func(t *testing.T, db database.Store, org database.Organization) (database.User, database.WorkspaceAgent) {
|
||||
user := dbgen.User(t, db, database.User{})
|
||||
template := dbgen.Template(t, db, database.Template{
|
||||
OrganizationID: org.ID,
|
||||
CreatedBy: user.ID,
|
||||
})
|
||||
templateVersion := dbgen.TemplateVersion(t, db, database.TemplateVersion{
|
||||
TemplateID: uuid.NullUUID{Valid: true, UUID: template.ID},
|
||||
OrganizationID: org.ID,
|
||||
CreatedBy: user.ID,
|
||||
})
|
||||
workspace := dbgen.Workspace(t, db, database.WorkspaceTable{
|
||||
OrganizationID: org.ID,
|
||||
TemplateID: template.ID,
|
||||
OwnerID: user.ID,
|
||||
})
|
||||
job := dbgen.ProvisionerJob(t, db, nil, database.ProvisionerJob{
|
||||
Type: database.ProvisionerJobTypeWorkspaceBuild,
|
||||
OrganizationID: org.ID,
|
||||
})
|
||||
build := dbgen.WorkspaceBuild(t, db, database.WorkspaceBuild{
|
||||
JobID: job.ID,
|
||||
WorkspaceID: workspace.ID,
|
||||
TemplateVersionID: templateVersion.ID,
|
||||
})
|
||||
resource := dbgen.WorkspaceResource(t, db, database.WorkspaceResource{
|
||||
JobID: build.JobID,
|
||||
})
|
||||
agent := dbgen.WorkspaceAgent(t, db, database.WorkspaceAgent{
|
||||
ResourceID: resource.ID,
|
||||
})
|
||||
|
||||
return user, agent
|
||||
}
|
||||
|
||||
newAgentAPI := func(t *testing.T, logger slog.Logger, db database.Store, clock quartz.Clock, user database.User, org database.Organization, agent database.WorkspaceAgent) *agentapi.SubAgentAPI {
|
||||
auth := rbac.NewStrictCachingAuthorizer(prometheus.NewRegistry())
|
||||
|
||||
accessControlStore := &atomic.Pointer[dbauthz.AccessControlStore]{}
|
||||
var acs dbauthz.AccessControlStore = dbauthz.AGPLTemplateAccessControlStore{}
|
||||
accessControlStore.Store(&acs)
|
||||
|
||||
return &agentapi.SubAgentAPI{
|
||||
OwnerID: user.ID,
|
||||
OrganizationID: org.ID,
|
||||
AgentID: agent.ID,
|
||||
AgentFn: func(context.Context) (database.WorkspaceAgent, error) {
|
||||
return agent, nil
|
||||
},
|
||||
Clock: clock,
|
||||
Database: dbauthz.New(db, auth, logger, accessControlStore),
|
||||
}
|
||||
}
|
||||
|
||||
t.Run("CreateSubAgent", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
agentName string
|
||||
agentDir string
|
||||
agentArch string
|
||||
agentOS string
|
||||
shouldErr bool
|
||||
}{
|
||||
{
|
||||
name: "Ok",
|
||||
agentName: "some-child-agent",
|
||||
agentDir: "/workspaces/wibble",
|
||||
agentArch: "amd64",
|
||||
agentOS: "linux",
|
||||
},
|
||||
{
|
||||
name: "NameWithUnderscore",
|
||||
agentName: "some_child_agent",
|
||||
agentDir: "/workspaces/wibble",
|
||||
agentArch: "amd64",
|
||||
agentOS: "linux",
|
||||
shouldErr: true,
|
||||
},
|
||||
{
|
||||
name: "EmptyName",
|
||||
agentName: "",
|
||||
agentDir: "/workspaces/wibble",
|
||||
agentArch: "amd64",
|
||||
agentOS: "linux",
|
||||
shouldErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
log := testutil.Logger(t)
|
||||
ctx := testutil.Context(t, testutil.WaitShort)
|
||||
clock := quartz.NewMock(t)
|
||||
|
||||
db, org := newDatabaseWithOrg(t)
|
||||
user, agent := newUserWithWorkspaceAgent(t, db, org)
|
||||
api := newAgentAPI(t, log, db, clock, user, org, agent)
|
||||
|
||||
createResp, err := api.CreateSubAgent(ctx, &proto.CreateSubAgentRequest{
|
||||
Name: tt.agentName,
|
||||
Directory: tt.agentDir,
|
||||
Architecture: tt.agentArch,
|
||||
OperatingSystem: tt.agentOS,
|
||||
})
|
||||
if tt.shouldErr {
|
||||
require.Error(t, err)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
|
||||
require.NotNil(t, createResp.Agent)
|
||||
|
||||
agentID, err := uuid.FromBytes(createResp.Agent.Id)
|
||||
require.NoError(t, err)
|
||||
|
||||
agent, err := api.Database.GetWorkspaceAgentByID(dbauthz.AsSystemRestricted(ctx), agentID) //nolint:gocritic // this is a test.
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, tt.agentName, agent.Name)
|
||||
assert.Equal(t, tt.agentDir, agent.Directory)
|
||||
assert.Equal(t, tt.agentArch, agent.Architecture)
|
||||
assert.Equal(t, tt.agentOS, agent.OperatingSystem)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("DeleteSubAgent", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
t.Run("WhenOnlyOne", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
log := testutil.Logger(t)
|
||||
ctx := testutil.Context(t, testutil.WaitShort)
|
||||
clock := quartz.NewMock(t)
|
||||
|
||||
db, org := newDatabaseWithOrg(t)
|
||||
user, agent := newUserWithWorkspaceAgent(t, db, org)
|
||||
api := newAgentAPI(t, log, db, clock, user, org, agent)
|
||||
|
||||
// Given: A sub agent.
|
||||
childAgent := dbgen.WorkspaceAgent(t, db, database.WorkspaceAgent{
|
||||
ParentID: uuid.NullUUID{Valid: true, UUID: agent.ID},
|
||||
ResourceID: agent.ResourceID,
|
||||
Name: "some-child-agent",
|
||||
Directory: "/workspaces/wibble",
|
||||
Architecture: "amd64",
|
||||
OperatingSystem: "linux",
|
||||
})
|
||||
|
||||
// When: We delete the sub agent.
|
||||
_, err := api.DeleteSubAgent(ctx, &proto.DeleteSubAgentRequest{
|
||||
Id: childAgent.ID[:],
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Then: It is deleted.
|
||||
_, err = db.GetWorkspaceAgentByID(dbauthz.AsSystemRestricted(ctx), childAgent.ID) //nolint:gocritic // this is a test.
|
||||
require.ErrorIs(t, err, sql.ErrNoRows)
|
||||
})
|
||||
|
||||
t.Run("WhenOneOfMany", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
log := testutil.Logger(t)
|
||||
ctx := testutil.Context(t, testutil.WaitShort)
|
||||
clock := quartz.NewMock(t)
|
||||
|
||||
db, org := newDatabaseWithOrg(t)
|
||||
user, agent := newUserWithWorkspaceAgent(t, db, org)
|
||||
api := newAgentAPI(t, log, db, clock, user, org, agent)
|
||||
|
||||
// Given: Multiple sub agents.
|
||||
childAgentOne := dbgen.WorkspaceAgent(t, db, database.WorkspaceAgent{
|
||||
ParentID: uuid.NullUUID{Valid: true, UUID: agent.ID},
|
||||
ResourceID: agent.ResourceID,
|
||||
Name: "child-agent-one",
|
||||
Directory: "/workspaces/wibble",
|
||||
Architecture: "amd64",
|
||||
OperatingSystem: "linux",
|
||||
})
|
||||
|
||||
childAgentTwo := dbgen.WorkspaceAgent(t, db, database.WorkspaceAgent{
|
||||
ParentID: uuid.NullUUID{Valid: true, UUID: agent.ID},
|
||||
ResourceID: agent.ResourceID,
|
||||
Name: "child-agent-two",
|
||||
Directory: "/workspaces/wobble",
|
||||
Architecture: "amd64",
|
||||
OperatingSystem: "linux",
|
||||
})
|
||||
|
||||
// When: We delete one of the sub agents.
|
||||
_, err := api.DeleteSubAgent(ctx, &proto.DeleteSubAgentRequest{
|
||||
Id: childAgentOne.ID[:],
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Then: The correct one is deleted.
|
||||
_, err = api.Database.GetWorkspaceAgentByID(dbauthz.AsSystemRestricted(ctx), childAgentOne.ID) //nolint:gocritic // this is a test.
|
||||
require.ErrorIs(t, err, sql.ErrNoRows)
|
||||
|
||||
_, err = api.Database.GetWorkspaceAgentByID(dbauthz.AsSystemRestricted(ctx), childAgentTwo.ID) //nolint:gocritic // this is a test.
|
||||
require.NoError(t, err)
|
||||
})
|
||||
|
||||
t.Run("CannotDeleteOtherAgentsChild", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
log := testutil.Logger(t)
|
||||
ctx := testutil.Context(t, testutil.WaitShort)
|
||||
clock := quartz.NewMock(t)
|
||||
|
||||
db, org := newDatabaseWithOrg(t)
|
||||
|
||||
userOne, agentOne := newUserWithWorkspaceAgent(t, db, org)
|
||||
_ = newAgentAPI(t, log, db, clock, userOne, org, agentOne)
|
||||
|
||||
userTwo, agentTwo := newUserWithWorkspaceAgent(t, db, org)
|
||||
apiTwo := newAgentAPI(t, log, db, clock, userTwo, org, agentTwo)
|
||||
|
||||
// Given: Both workspaces have child agents
|
||||
childAgentOne := dbgen.WorkspaceAgent(t, db, database.WorkspaceAgent{
|
||||
ParentID: uuid.NullUUID{Valid: true, UUID: agentOne.ID},
|
||||
ResourceID: agentOne.ResourceID,
|
||||
Name: "child-agent-one",
|
||||
Directory: "/workspaces/wibble",
|
||||
Architecture: "amd64",
|
||||
OperatingSystem: "linux",
|
||||
})
|
||||
|
||||
// When: An agent API attempts to delete an agent it doesn't own
|
||||
_, err := apiTwo.DeleteSubAgent(ctx, &proto.DeleteSubAgentRequest{
|
||||
Id: childAgentOne.ID[:],
|
||||
})
|
||||
|
||||
// Then: We expect it to fail and for the agent to still exist.
|
||||
var notAuthorizedError dbauthz.NotAuthorizedError
|
||||
require.ErrorAs(t, err, ¬AuthorizedError)
|
||||
|
||||
_, err = db.GetWorkspaceAgentByID(dbauthz.AsSystemRestricted(ctx), childAgentOne.ID) //nolint:gocritic // this is a test.
|
||||
require.NoError(t, err)
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("ListSubAgents", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
t.Run("Empty", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
log := testutil.Logger(t)
|
||||
ctx := testutil.Context(t, testutil.WaitShort)
|
||||
clock := quartz.NewMock(t)
|
||||
|
||||
db, org := newDatabaseWithOrg(t)
|
||||
user, agent := newUserWithWorkspaceAgent(t, db, org)
|
||||
api := newAgentAPI(t, log, db, clock, user, org, agent)
|
||||
|
||||
// When: We list sub agents with no children
|
||||
listResp, err := api.ListSubAgents(ctx, &proto.ListSubAgentsRequest{})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Then: We expect an empty list
|
||||
require.Empty(t, listResp.Agents)
|
||||
})
|
||||
|
||||
t.Run("Ok", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
log := testutil.Logger(t)
|
||||
ctx := testutil.Context(t, testutil.WaitShort)
|
||||
clock := quartz.NewMock(t)
|
||||
|
||||
db, org := newDatabaseWithOrg(t)
|
||||
user, agent := newUserWithWorkspaceAgent(t, db, org)
|
||||
api := newAgentAPI(t, log, db, clock, user, org, agent)
|
||||
|
||||
// Given: Multiple sub agents.
|
||||
childAgentOne := dbgen.WorkspaceAgent(t, db, database.WorkspaceAgent{
|
||||
ParentID: uuid.NullUUID{Valid: true, UUID: agent.ID},
|
||||
ResourceID: agent.ResourceID,
|
||||
Name: "child-agent-one",
|
||||
Directory: "/workspaces/wibble",
|
||||
Architecture: "amd64",
|
||||
OperatingSystem: "linux",
|
||||
})
|
||||
|
||||
childAgentTwo := dbgen.WorkspaceAgent(t, db, database.WorkspaceAgent{
|
||||
ParentID: uuid.NullUUID{Valid: true, UUID: agent.ID},
|
||||
ResourceID: agent.ResourceID,
|
||||
Name: "child-agent-two",
|
||||
Directory: "/workspaces/wobble",
|
||||
Architecture: "amd64",
|
||||
OperatingSystem: "linux",
|
||||
})
|
||||
|
||||
childAgents := []database.WorkspaceAgent{childAgentOne, childAgentTwo}
|
||||
slices.SortFunc(childAgents, func(a, b database.WorkspaceAgent) int {
|
||||
return cmp.Compare(a.ID.String(), b.ID.String())
|
||||
})
|
||||
|
||||
// When: We list the sub agents.
|
||||
listResp, err := api.ListSubAgents(ctx, &proto.ListSubAgentsRequest{}) //nolint:gocritic // this is a test.
|
||||
require.NoError(t, err)
|
||||
|
||||
listedChildAgents := listResp.Agents
|
||||
slices.SortFunc(listedChildAgents, func(a, b *proto.SubAgent) int {
|
||||
return cmp.Compare(string(a.Id), string(b.Id))
|
||||
})
|
||||
|
||||
// Then: We expect to see all the agents listed.
|
||||
require.Len(t, listedChildAgents, len(childAgents))
|
||||
for i, listedAgent := range listedChildAgents {
|
||||
require.Equal(t, childAgents[i].ID[:], listedAgent.Id)
|
||||
require.Equal(t, childAgents[i].Name, listedAgent.Name)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("DoesNotListOtherAgentsChildren", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
log := testutil.Logger(t)
|
||||
ctx := testutil.Context(t, testutil.WaitShort)
|
||||
clock := quartz.NewMock(t)
|
||||
|
||||
db, org := newDatabaseWithOrg(t)
|
||||
|
||||
// Create two users with their respective agents
|
||||
userOne, agentOne := newUserWithWorkspaceAgent(t, db, org)
|
||||
apiOne := newAgentAPI(t, log, db, clock, userOne, org, agentOne)
|
||||
|
||||
userTwo, agentTwo := newUserWithWorkspaceAgent(t, db, org)
|
||||
apiTwo := newAgentAPI(t, log, db, clock, userTwo, org, agentTwo)
|
||||
|
||||
// Given: Both parent agents have child agents
|
||||
childAgentOne := dbgen.WorkspaceAgent(t, db, database.WorkspaceAgent{
|
||||
ParentID: uuid.NullUUID{Valid: true, UUID: agentOne.ID},
|
||||
ResourceID: agentOne.ResourceID,
|
||||
Name: "agent-one-child",
|
||||
Directory: "/workspaces/wibble",
|
||||
Architecture: "amd64",
|
||||
OperatingSystem: "linux",
|
||||
})
|
||||
|
||||
childAgentTwo := dbgen.WorkspaceAgent(t, db, database.WorkspaceAgent{
|
||||
ParentID: uuid.NullUUID{Valid: true, UUID: agentTwo.ID},
|
||||
ResourceID: agentTwo.ResourceID,
|
||||
Name: "agent-two-child",
|
||||
Directory: "/workspaces/wobble",
|
||||
Architecture: "amd64",
|
||||
OperatingSystem: "linux",
|
||||
})
|
||||
|
||||
// When: We list the sub agents for the first user
|
||||
listRespOne, err := apiOne.ListSubAgents(ctx, &proto.ListSubAgentsRequest{})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Then: We should only see the first user's child agent
|
||||
require.Len(t, listRespOne.Agents, 1)
|
||||
require.Equal(t, childAgentOne.ID[:], listRespOne.Agents[0].Id)
|
||||
require.Equal(t, childAgentOne.Name, listRespOne.Agents[0].Name)
|
||||
|
||||
// When: We list the sub agents for the second user
|
||||
listRespTwo, err := apiTwo.ListSubAgents(ctx, &proto.ListSubAgentsRequest{})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Then: We should only see the second user's child agent
|
||||
require.Len(t, listRespTwo.Agents, 1)
|
||||
require.Equal(t, childAgentTwo.ID[:], listRespTwo.Agents[0].Id)
|
||||
require.Equal(t, childAgentTwo.Name, listRespTwo.Agents[0].Name)
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -319,6 +319,28 @@ var (
|
||||
Scope: rbac.ScopeAll,
|
||||
}.WithCachedASTValue()
|
||||
|
||||
subjectSubAgentAPI = func(userID uuid.UUID, orgID uuid.UUID) rbac.Subject {
|
||||
return rbac.Subject{
|
||||
Type: rbac.SubjectTypeSubAgentAPI,
|
||||
FriendlyName: "Sub Agent API",
|
||||
ID: userID.String(),
|
||||
Roles: rbac.Roles([]rbac.Role{
|
||||
{
|
||||
Identifier: rbac.RoleIdentifier{Name: "subagentapi"},
|
||||
DisplayName: "Sub Agent API",
|
||||
Site: []rbac.Permission{},
|
||||
Org: map[string][]rbac.Permission{
|
||||
orgID.String(): {},
|
||||
},
|
||||
User: rbac.Permissions(map[string][]policy.Action{
|
||||
rbac.ResourceWorkspace.Type: {policy.ActionRead, policy.ActionCreateAgent, policy.ActionDeleteAgent},
|
||||
}),
|
||||
},
|
||||
}),
|
||||
Scope: rbac.ScopeAll,
|
||||
}.WithCachedASTValue()
|
||||
}
|
||||
|
||||
subjectSystemRestricted = rbac.Subject{
|
||||
Type: rbac.SubjectTypeSystemRestricted,
|
||||
FriendlyName: "System",
|
||||
@@ -437,6 +459,12 @@ func AsResourceMonitor(ctx context.Context) context.Context {
|
||||
return As(ctx, subjectResourceMonitor)
|
||||
}
|
||||
|
||||
// AsSubAgentAPI returns a context with an actor that has permissions required for
|
||||
// handling the lifecycle of sub agents.
|
||||
func AsSubAgentAPI(ctx context.Context, orgID uuid.UUID, userID uuid.UUID) context.Context {
|
||||
return As(ctx, subjectSubAgentAPI(userID, orgID))
|
||||
}
|
||||
|
||||
// AsSystemRestricted returns a context with an actor that has permissions
|
||||
// required for various system operations (login, logout, metrics cache).
|
||||
func AsSystemRestricted(ctx context.Context) context.Context {
|
||||
@@ -1509,6 +1537,19 @@ func (q *querier) DeleteWorkspaceAgentPortSharesByTemplate(ctx context.Context,
|
||||
return q.db.DeleteWorkspaceAgentPortSharesByTemplate(ctx, templateID)
|
||||
}
|
||||
|
||||
func (q *querier) DeleteWorkspaceSubAgentByID(ctx context.Context, id uuid.UUID) error {
|
||||
workspace, err := q.db.GetWorkspaceByAgentID(ctx, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := q.authorizeContext(ctx, policy.ActionDeleteAgent, workspace); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return q.db.DeleteWorkspaceSubAgentByID(ctx, id)
|
||||
}
|
||||
|
||||
func (q *querier) DisableForeignKeysAndTriggers(ctx context.Context) error {
|
||||
if !testing.Testing() {
|
||||
return xerrors.Errorf("DisableForeignKeysAndTriggers is only allowed in tests")
|
||||
@@ -3038,6 +3079,19 @@ func (q *querier) GetWorkspaceAgentUsageStatsAndLabels(ctx context.Context, crea
|
||||
return q.db.GetWorkspaceAgentUsageStatsAndLabels(ctx, createdAt)
|
||||
}
|
||||
|
||||
func (q *querier) GetWorkspaceAgentsByParentID(ctx context.Context, parentID uuid.UUID) ([]database.WorkspaceAgent, error) {
|
||||
workspace, err := q.db.GetWorkspaceByAgentID(ctx, parentID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := q.authorizeContext(ctx, policy.ActionRead, workspace); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return q.db.GetWorkspaceAgentsByParentID(ctx, parentID)
|
||||
}
|
||||
|
||||
// GetWorkspaceAgentsByResourceIDs
|
||||
// The workspace/job is already fetched.
|
||||
func (q *querier) GetWorkspaceAgentsByResourceIDs(ctx context.Context, ids []uuid.UUID) ([]database.WorkspaceAgent, error) {
|
||||
|
||||
@@ -4033,6 +4033,44 @@ func (s *MethodTestSuite) TestSystemFunctions() {
|
||||
Asserts(rbac.ResourceProvisionerJobs.InOrg(o.ID), policy.ActionRead).
|
||||
Returns(slice.New(a, b))
|
||||
}))
|
||||
s.Run("DeleteWorkspaceSubAgentByID", s.Subtest(func(db database.Store, check *expects) {
|
||||
_ = dbgen.User(s.T(), db, database.User{})
|
||||
u := dbgen.User(s.T(), db, database.User{})
|
||||
o := dbgen.Organization(s.T(), db, database.Organization{})
|
||||
j := dbgen.ProvisionerJob(s.T(), db, nil, database.ProvisionerJob{Type: database.ProvisionerJobTypeWorkspaceBuild})
|
||||
tpl := dbgen.Template(s.T(), db, database.Template{CreatedBy: u.ID, OrganizationID: o.ID})
|
||||
tv := dbgen.TemplateVersion(s.T(), db, database.TemplateVersion{
|
||||
TemplateID: uuid.NullUUID{UUID: tpl.ID, Valid: true},
|
||||
JobID: j.ID,
|
||||
OrganizationID: o.ID,
|
||||
CreatedBy: u.ID,
|
||||
})
|
||||
ws := dbgen.Workspace(s.T(), db, database.WorkspaceTable{OwnerID: u.ID, TemplateID: tpl.ID, OrganizationID: o.ID})
|
||||
_ = dbgen.WorkspaceBuild(s.T(), db, database.WorkspaceBuild{WorkspaceID: ws.ID, JobID: j.ID, TemplateVersionID: tv.ID})
|
||||
res := dbgen.WorkspaceResource(s.T(), db, database.WorkspaceResource{JobID: j.ID})
|
||||
agent := dbgen.WorkspaceAgent(s.T(), db, database.WorkspaceAgent{ResourceID: res.ID})
|
||||
_ = dbgen.WorkspaceAgent(s.T(), db, database.WorkspaceAgent{ResourceID: res.ID, ParentID: uuid.NullUUID{Valid: true, UUID: agent.ID}})
|
||||
check.Args(agent.ID).Asserts(ws, policy.ActionDeleteAgent)
|
||||
}))
|
||||
s.Run("GetWorkspaceAgentsByParentID", s.Subtest(func(db database.Store, check *expects) {
|
||||
_ = dbgen.User(s.T(), db, database.User{})
|
||||
u := dbgen.User(s.T(), db, database.User{})
|
||||
o := dbgen.Organization(s.T(), db, database.Organization{})
|
||||
j := dbgen.ProvisionerJob(s.T(), db, nil, database.ProvisionerJob{Type: database.ProvisionerJobTypeWorkspaceBuild})
|
||||
tpl := dbgen.Template(s.T(), db, database.Template{CreatedBy: u.ID, OrganizationID: o.ID})
|
||||
tv := dbgen.TemplateVersion(s.T(), db, database.TemplateVersion{
|
||||
TemplateID: uuid.NullUUID{UUID: tpl.ID, Valid: true},
|
||||
JobID: j.ID,
|
||||
OrganizationID: o.ID,
|
||||
CreatedBy: u.ID,
|
||||
})
|
||||
ws := dbgen.Workspace(s.T(), db, database.WorkspaceTable{OwnerID: u.ID, TemplateID: tpl.ID, OrganizationID: o.ID})
|
||||
_ = dbgen.WorkspaceBuild(s.T(), db, database.WorkspaceBuild{WorkspaceID: ws.ID, JobID: j.ID, TemplateVersionID: tv.ID})
|
||||
res := dbgen.WorkspaceResource(s.T(), db, database.WorkspaceResource{JobID: j.ID})
|
||||
agent := dbgen.WorkspaceAgent(s.T(), db, database.WorkspaceAgent{ResourceID: res.ID})
|
||||
_ = dbgen.WorkspaceAgent(s.T(), db, database.WorkspaceAgent{ResourceID: res.ID, ParentID: uuid.NullUUID{Valid: true, UUID: agent.ID}})
|
||||
check.Args(agent.ID).Asserts(ws, policy.ActionRead)
|
||||
}))
|
||||
s.Run("InsertWorkspaceAgent", s.Subtest(func(db database.Store, check *expects) {
|
||||
u := dbgen.User(s.T(), db, database.User{})
|
||||
o := dbgen.Organization(s.T(), db, database.Organization{})
|
||||
|
||||
@@ -2543,6 +2543,20 @@ func (q *FakeQuerier) DeleteWorkspaceAgentPortSharesByTemplate(_ context.Context
|
||||
return nil
|
||||
}
|
||||
|
||||
func (q *FakeQuerier) DeleteWorkspaceSubAgentByID(ctx context.Context, id uuid.UUID) error {
|
||||
q.mutex.Lock()
|
||||
defer q.mutex.Unlock()
|
||||
|
||||
for i, agent := range q.workspaceAgents {
|
||||
if agent.ID == id && agent.ParentID.Valid {
|
||||
q.workspaceAgents = slices.Delete(q.workspaceAgents, i, i+1)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (*FakeQuerier) DisableForeignKeysAndTriggers(_ context.Context) error {
|
||||
// This is a no-op in the in-memory database.
|
||||
return nil
|
||||
@@ -7679,6 +7693,22 @@ func (q *FakeQuerier) GetWorkspaceAgentUsageStatsAndLabels(_ context.Context, cr
|
||||
return stats, nil
|
||||
}
|
||||
|
||||
func (q *FakeQuerier) GetWorkspaceAgentsByParentID(ctx context.Context, parentID uuid.UUID) ([]database.WorkspaceAgent, error) {
|
||||
q.mutex.RLock()
|
||||
defer q.mutex.RUnlock()
|
||||
|
||||
workspaceAgents := make([]database.WorkspaceAgent, 0)
|
||||
for _, agent := range q.workspaceAgents {
|
||||
if !agent.ParentID.Valid || agent.ParentID.UUID != parentID {
|
||||
continue
|
||||
}
|
||||
|
||||
workspaceAgents = append(workspaceAgents, agent)
|
||||
}
|
||||
|
||||
return workspaceAgents, nil
|
||||
}
|
||||
|
||||
func (q *FakeQuerier) GetWorkspaceAgentsByResourceIDs(ctx context.Context, resourceIDs []uuid.UUID) ([]database.WorkspaceAgent, error) {
|
||||
q.mutex.RLock()
|
||||
defer q.mutex.RUnlock()
|
||||
|
||||
@@ -466,6 +466,13 @@ func (m queryMetricsStore) DeleteWorkspaceAgentPortSharesByTemplate(ctx context.
|
||||
return r0
|
||||
}
|
||||
|
||||
func (m queryMetricsStore) DeleteWorkspaceSubAgentByID(ctx context.Context, id uuid.UUID) error {
|
||||
start := time.Now()
|
||||
r0 := m.s.DeleteWorkspaceSubAgentByID(ctx, id)
|
||||
m.queryLatencies.WithLabelValues("DeleteWorkspaceSubAgentByID").Observe(time.Since(start).Seconds())
|
||||
return r0
|
||||
}
|
||||
|
||||
func (m queryMetricsStore) DisableForeignKeysAndTriggers(ctx context.Context) error {
|
||||
start := time.Now()
|
||||
r0 := m.s.DisableForeignKeysAndTriggers(ctx)
|
||||
@@ -1761,6 +1768,13 @@ func (m queryMetricsStore) GetWorkspaceAgentUsageStatsAndLabels(ctx context.Cont
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
func (m queryMetricsStore) GetWorkspaceAgentsByParentID(ctx context.Context, dollar_1 uuid.UUID) ([]database.WorkspaceAgent, error) {
|
||||
start := time.Now()
|
||||
r0, r1 := m.s.GetWorkspaceAgentsByParentID(ctx, dollar_1)
|
||||
m.queryLatencies.WithLabelValues("GetWorkspaceAgentsByParentID").Observe(time.Since(start).Seconds())
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
func (m queryMetricsStore) GetWorkspaceAgentsByResourceIDs(ctx context.Context, ids []uuid.UUID) ([]database.WorkspaceAgent, error) {
|
||||
start := time.Now()
|
||||
agents, err := m.s.GetWorkspaceAgentsByResourceIDs(ctx, ids)
|
||||
|
||||
@@ -816,6 +816,20 @@ func (mr *MockStoreMockRecorder) DeleteWorkspaceAgentPortSharesByTemplate(ctx, t
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteWorkspaceAgentPortSharesByTemplate", reflect.TypeOf((*MockStore)(nil).DeleteWorkspaceAgentPortSharesByTemplate), ctx, templateID)
|
||||
}
|
||||
|
||||
// DeleteWorkspaceSubAgentByID mocks base method.
|
||||
func (m *MockStore) DeleteWorkspaceSubAgentByID(ctx context.Context, id uuid.UUID) error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "DeleteWorkspaceSubAgentByID", ctx, id)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// DeleteWorkspaceSubAgentByID indicates an expected call of DeleteWorkspaceSubAgentByID.
|
||||
func (mr *MockStoreMockRecorder) DeleteWorkspaceSubAgentByID(ctx, id any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteWorkspaceSubAgentByID", reflect.TypeOf((*MockStore)(nil).DeleteWorkspaceSubAgentByID), ctx, id)
|
||||
}
|
||||
|
||||
// DisableForeignKeysAndTriggers mocks base method.
|
||||
func (m *MockStore) DisableForeignKeysAndTriggers(ctx context.Context) error {
|
||||
m.ctrl.T.Helper()
|
||||
@@ -3693,6 +3707,21 @@ func (mr *MockStoreMockRecorder) GetWorkspaceAgentUsageStatsAndLabels(ctx, creat
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetWorkspaceAgentUsageStatsAndLabels", reflect.TypeOf((*MockStore)(nil).GetWorkspaceAgentUsageStatsAndLabels), ctx, createdAt)
|
||||
}
|
||||
|
||||
// GetWorkspaceAgentsByParentID mocks base method.
|
||||
func (m *MockStore) GetWorkspaceAgentsByParentID(ctx context.Context, parentID uuid.UUID) ([]database.WorkspaceAgent, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "GetWorkspaceAgentsByParentID", ctx, parentID)
|
||||
ret0, _ := ret[0].([]database.WorkspaceAgent)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// GetWorkspaceAgentsByParentID indicates an expected call of GetWorkspaceAgentsByParentID.
|
||||
func (mr *MockStoreMockRecorder) GetWorkspaceAgentsByParentID(ctx, parentID any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetWorkspaceAgentsByParentID", reflect.TypeOf((*MockStore)(nil).GetWorkspaceAgentsByParentID), ctx, parentID)
|
||||
}
|
||||
|
||||
// GetWorkspaceAgentsByResourceIDs mocks base method.
|
||||
func (m *MockStore) GetWorkspaceAgentsByResourceIDs(ctx context.Context, ids []uuid.UUID) ([]database.WorkspaceAgent, error) {
|
||||
m.ctrl.T.Helper()
|
||||
|
||||
@@ -118,6 +118,7 @@ type sqlcQuerier interface {
|
||||
DeleteWebpushSubscriptions(ctx context.Context, ids []uuid.UUID) error
|
||||
DeleteWorkspaceAgentPortShare(ctx context.Context, arg DeleteWorkspaceAgentPortShareParams) error
|
||||
DeleteWorkspaceAgentPortSharesByTemplate(ctx context.Context, templateID uuid.UUID) error
|
||||
DeleteWorkspaceSubAgentByID(ctx context.Context, id uuid.UUID) error
|
||||
// Disable foreign keys and triggers for all tables.
|
||||
// Deprecated: disable foreign keys was created to aid in migrating off
|
||||
// of the test-only in-memory database. Do not use this in new code.
|
||||
@@ -412,6 +413,7 @@ type sqlcQuerier interface {
|
||||
// `minute_buckets` could return 0 rows if there are no usage stats since `created_at`.
|
||||
GetWorkspaceAgentUsageStats(ctx context.Context, createdAt time.Time) ([]GetWorkspaceAgentUsageStatsRow, error)
|
||||
GetWorkspaceAgentUsageStatsAndLabels(ctx context.Context, createdAt time.Time) ([]GetWorkspaceAgentUsageStatsAndLabelsRow, error)
|
||||
GetWorkspaceAgentsByParentID(ctx context.Context, parentID uuid.UUID) ([]WorkspaceAgent, error)
|
||||
GetWorkspaceAgentsByResourceIDs(ctx context.Context, ids []uuid.UUID) ([]WorkspaceAgent, error)
|
||||
GetWorkspaceAgentsByWorkspaceAndBuildNumber(ctx context.Context, arg GetWorkspaceAgentsByWorkspaceAndBuildNumberParams) ([]WorkspaceAgent, error)
|
||||
GetWorkspaceAgentsCreatedAfter(ctx context.Context, createdAt time.Time) ([]WorkspaceAgent, error)
|
||||
|
||||
@@ -4954,6 +4954,37 @@ func TestWorkspaceAgentNameUniqueTrigger(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestGetWorkspaceAgentsByParentID(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
t.Run("NilParentDoesNotReturnAllParentAgents", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ctx := testutil.Context(t, testutil.WaitShort)
|
||||
|
||||
// Given: A workspace agent
|
||||
db, _ := dbtestutil.NewDB(t)
|
||||
org := dbgen.Organization(t, db, database.Organization{})
|
||||
job := dbgen.ProvisionerJob(t, db, nil, database.ProvisionerJob{
|
||||
Type: database.ProvisionerJobTypeTemplateVersionImport,
|
||||
OrganizationID: org.ID,
|
||||
})
|
||||
resource := dbgen.WorkspaceResource(t, db, database.WorkspaceResource{
|
||||
JobID: job.ID,
|
||||
})
|
||||
_ = dbgen.WorkspaceAgent(t, db, database.WorkspaceAgent{
|
||||
ResourceID: resource.ID,
|
||||
})
|
||||
|
||||
// When: We attempt to select agents with a null parent id
|
||||
agents, err := db.GetWorkspaceAgentsByParentID(ctx, uuid.Nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Then: We expect to see no agents.
|
||||
require.Len(t, agents, 0)
|
||||
})
|
||||
}
|
||||
|
||||
func requireUsersMatch(t testing.TB, expected []database.User, found []database.GetUsersRow, msg string) {
|
||||
t.Helper()
|
||||
require.ElementsMatch(t, expected, database.ConvertUserRows(found), msg)
|
||||
|
||||
@@ -14159,6 +14159,15 @@ func (q *sqlQuerier) DeleteOldWorkspaceAgentLogs(ctx context.Context, threshold
|
||||
return err
|
||||
}
|
||||
|
||||
const deleteWorkspaceSubAgentByID = `-- name: DeleteWorkspaceSubAgentByID :exec
|
||||
DELETE FROM workspace_agents WHERE id = $1 AND parent_id IS NOT NULL
|
||||
`
|
||||
|
||||
func (q *sqlQuerier) DeleteWorkspaceSubAgentByID(ctx context.Context, id uuid.UUID) error {
|
||||
_, err := q.db.ExecContext(ctx, deleteWorkspaceSubAgentByID, id)
|
||||
return err
|
||||
}
|
||||
|
||||
const getWorkspaceAgentAndLatestBuildByAuthToken = `-- name: GetWorkspaceAgentAndLatestBuildByAuthToken :one
|
||||
SELECT
|
||||
workspaces.id, workspaces.created_at, workspaces.updated_at, workspaces.owner_id, workspaces.organization_id, workspaces.template_id, workspaces.deleted, workspaces.name, workspaces.autostart_schedule, workspaces.ttl, workspaces.last_used_at, workspaces.dormant_at, workspaces.deleting_at, workspaces.automatic_updates, workspaces.favorite, workspaces.next_start_at,
|
||||
@@ -14591,6 +14600,67 @@ func (q *sqlQuerier) GetWorkspaceAgentScriptTimingsByBuildID(ctx context.Context
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const getWorkspaceAgentsByParentID = `-- name: GetWorkspaceAgentsByParentID :many
|
||||
SELECT id, created_at, updated_at, name, first_connected_at, last_connected_at, disconnected_at, resource_id, auth_token, auth_instance_id, architecture, environment_variables, operating_system, instance_metadata, resource_metadata, directory, version, last_connected_replica_id, connection_timeout_seconds, troubleshooting_url, motd_file, lifecycle_state, expanded_directory, logs_length, logs_overflowed, started_at, ready_at, subsystems, display_apps, api_version, display_order, parent_id, api_key_scope FROM workspace_agents WHERE parent_id = $1::uuid
|
||||
`
|
||||
|
||||
func (q *sqlQuerier) GetWorkspaceAgentsByParentID(ctx context.Context, parentID uuid.UUID) ([]WorkspaceAgent, error) {
|
||||
rows, err := q.db.QueryContext(ctx, getWorkspaceAgentsByParentID, parentID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []WorkspaceAgent
|
||||
for rows.Next() {
|
||||
var i WorkspaceAgent
|
||||
if err := rows.Scan(
|
||||
&i.ID,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
&i.Name,
|
||||
&i.FirstConnectedAt,
|
||||
&i.LastConnectedAt,
|
||||
&i.DisconnectedAt,
|
||||
&i.ResourceID,
|
||||
&i.AuthToken,
|
||||
&i.AuthInstanceID,
|
||||
&i.Architecture,
|
||||
&i.EnvironmentVariables,
|
||||
&i.OperatingSystem,
|
||||
&i.InstanceMetadata,
|
||||
&i.ResourceMetadata,
|
||||
&i.Directory,
|
||||
&i.Version,
|
||||
&i.LastConnectedReplicaID,
|
||||
&i.ConnectionTimeoutSeconds,
|
||||
&i.TroubleshootingURL,
|
||||
&i.MOTDFile,
|
||||
&i.LifecycleState,
|
||||
&i.ExpandedDirectory,
|
||||
&i.LogsLength,
|
||||
&i.LogsOverflowed,
|
||||
&i.StartedAt,
|
||||
&i.ReadyAt,
|
||||
pq.Array(&i.Subsystems),
|
||||
pq.Array(&i.DisplayApps),
|
||||
&i.APIVersion,
|
||||
&i.DisplayOrder,
|
||||
&i.ParentID,
|
||||
&i.APIKeyScope,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Close(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const getWorkspaceAgentsByResourceIDs = `-- name: GetWorkspaceAgentsByResourceIDs :many
|
||||
SELECT
|
||||
id, created_at, updated_at, name, first_connected_at, last_connected_at, disconnected_at, resource_id, auth_token, auth_instance_id, architecture, environment_variables, operating_system, instance_metadata, resource_metadata, directory, version, last_connected_replica_id, connection_timeout_seconds, troubleshooting_url, motd_file, lifecycle_state, expanded_directory, logs_length, logs_overflowed, started_at, ready_at, subsystems, display_apps, api_version, display_order, parent_id, api_key_scope
|
||||
|
||||
@@ -330,3 +330,9 @@ INNER JOIN workspace_resources ON workspace_resources.id = workspace_agents.reso
|
||||
INNER JOIN workspace_builds ON workspace_builds.job_id = workspace_resources.job_id
|
||||
WHERE workspace_builds.id = $1
|
||||
ORDER BY workspace_agent_script_timings.script_id, workspace_agent_script_timings.started_at;
|
||||
|
||||
-- name: GetWorkspaceAgentsByParentID :many
|
||||
SELECT * FROM workspace_agents WHERE parent_id = @parent_id::uuid;
|
||||
|
||||
-- name: DeleteWorkspaceSubAgentByID :exec
|
||||
DELETE FROM workspace_agents WHERE id = $1 AND parent_id IS NOT NULL;
|
||||
|
||||
@@ -135,6 +135,7 @@ var actorLogOrder = []rbac.SubjectType{
|
||||
rbac.SubjectTypeJobReaper,
|
||||
rbac.SubjectTypeNotifier,
|
||||
rbac.SubjectTypePrebuildsOrchestrator,
|
||||
rbac.SubjectTypeSubAgentAPI,
|
||||
rbac.SubjectTypeProvisionerd,
|
||||
rbac.SubjectTypeResourceMonitor,
|
||||
rbac.SubjectTypeSystemReadProvisionerDaemons,
|
||||
|
||||
@@ -73,6 +73,7 @@ const (
|
||||
SubjectTypeSystemReadProvisionerDaemons SubjectType = "system_read_provisioner_daemons"
|
||||
SubjectTypeSystemRestricted SubjectType = "system_restricted"
|
||||
SubjectTypeNotifier SubjectType = "notifier"
|
||||
SubjectTypeSubAgentAPI SubjectType = "sub_agent_api"
|
||||
)
|
||||
|
||||
// Subject is a struct that contains all the elements of a subject in an rbac
|
||||
|
||||
@@ -2613,7 +2613,7 @@ func requireGetManifest(ctx context.Context, t testing.TB, aAPI agentproto.DRPCA
|
||||
}
|
||||
|
||||
func postStartup(ctx context.Context, t testing.TB, client agent.Client, startup *agentproto.Startup) error {
|
||||
aAPI, _, err := client.ConnectRPC25(ctx)
|
||||
aAPI, _, err := client.ConnectRPC26(ctx)
|
||||
require.NoError(t, err)
|
||||
defer func() {
|
||||
cErr := aAPI.DRPCConn().Close()
|
||||
|
||||
@@ -128,9 +128,10 @@ func (api *API) workspaceAgentRPC(rw http.ResponseWriter, r *http.Request) {
|
||||
defer monitor.close()
|
||||
|
||||
agentAPI := agentapi.New(agentapi.Options{
|
||||
AgentID: workspaceAgent.ID,
|
||||
OwnerID: workspace.OwnerID,
|
||||
WorkspaceID: workspace.ID,
|
||||
AgentID: workspaceAgent.ID,
|
||||
OwnerID: workspace.OwnerID,
|
||||
WorkspaceID: workspace.ID,
|
||||
OrganizationID: workspace.OrganizationID,
|
||||
|
||||
Ctx: api.ctx,
|
||||
Log: logger,
|
||||
|
||||
@@ -258,7 +258,7 @@ func (c *Client) ConnectRPC24(ctx context.Context) (
|
||||
}
|
||||
|
||||
// ConnectRPC25 returns a dRPC client to the Agent API v2.5. It is useful when you want to be
|
||||
// maximally compatible with Coderd Release Versions from 2.xx+ // TODO(DanielleMaywood): Update version
|
||||
// maximally compatible with Coderd Release Versions from 2.23+
|
||||
func (c *Client) ConnectRPC25(ctx context.Context) (
|
||||
proto.DRPCAgentClient25, tailnetproto.DRPCTailnetClient25, error,
|
||||
) {
|
||||
@@ -269,6 +269,18 @@ func (c *Client) ConnectRPC25(ctx context.Context) (
|
||||
return proto.NewDRPCAgentClient(conn), tailnetproto.NewDRPCTailnetClient(conn), nil
|
||||
}
|
||||
|
||||
// ConnectRPC25 returns a dRPC client to the Agent API v2.5. It is useful when you want to be
|
||||
// maximally compatible with Coderd Release Versions from 2.24+
|
||||
func (c *Client) ConnectRPC26(ctx context.Context) (
|
||||
proto.DRPCAgentClient26, tailnetproto.DRPCTailnetClient26, error,
|
||||
) {
|
||||
conn, err := c.connectRPCVersion(ctx, apiversion.New(2, 6))
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return proto.NewDRPCAgentClient(conn), tailnetproto.NewDRPCTailnetClient(conn), nil
|
||||
}
|
||||
|
||||
// ConnectRPC connects to the workspace agent API and tailnet API
|
||||
func (c *Client) ConnectRPC(ctx context.Context) (drpc.Conn, error) {
|
||||
return c.connectRPCVersion(ctx, proto.CurrentVersion)
|
||||
|
||||
@@ -45,3 +45,8 @@ type DRPCTailnetClient24 interface {
|
||||
type DRPCTailnetClient25 interface {
|
||||
DRPCTailnetClient24
|
||||
}
|
||||
|
||||
// DRPCTailnetClient26 is the Tailnet API at v2.6.
|
||||
type DRPCTailnetClient26 interface {
|
||||
DRPCTailnetClient25
|
||||
}
|
||||
|
||||
@@ -47,11 +47,17 @@ import (
|
||||
// ReportConnection RPC on the Agent API.
|
||||
//
|
||||
// API v2.5:
|
||||
// - Shipped in Coder v2.xx.x // TODO(DanielleMaywood): Update version
|
||||
// - Shipped in Coder v2.23.0
|
||||
// - Added `ParentId` to the agent manifest.
|
||||
//
|
||||
// API v2.6:
|
||||
// - Shipped in Coder v2.24.0
|
||||
// - Added support for CreateSubAgent RPC on the Agent API.
|
||||
// - Added support for DeleteSubAgent RPC on the Agent API.
|
||||
// - Added support for ListSubAgents RPC on the Agent API.
|
||||
const (
|
||||
CurrentMajor = 2
|
||||
CurrentMinor = 5
|
||||
CurrentMinor = 6
|
||||
)
|
||||
|
||||
var CurrentVersion = apiversion.New(CurrentMajor, CurrentMinor)
|
||||
|
||||
Reference in New Issue
Block a user