Replace supervisor with agentrun worker service
Move agent-run orchestration from the legacy supervisor path into the new agentrun worker/service package and wire it through coredata, server, policies, and GraphQL resolvers. This consolidates run lifecycle handling around lease-aware workers and aligns API surface with the new agent-run domain model so reviewers can follow one coherent execution path. Signed-off-by: Bryan Frimin <bryan@probo.com>
This commit is contained in:
@@ -26,6 +26,7 @@ import (
|
||||
"go.gearno.de/kit/httpserver"
|
||||
"go.gearno.de/kit/log"
|
||||
"go.probo.inc/probo/pkg/accessreview"
|
||||
"go.probo.inc/probo/pkg/agentrun"
|
||||
"go.probo.inc/probo/pkg/baseurl"
|
||||
"go.probo.inc/probo/pkg/connector"
|
||||
"go.probo.inc/probo/pkg/connector/provider"
|
||||
@@ -60,6 +61,7 @@ type (
|
||||
Trust *trust.Service
|
||||
ESign *esign.Service
|
||||
AccessReview *accessreview.Service
|
||||
AgentRun *agentrun.Service
|
||||
Slack *slack.Service
|
||||
Mailman *mailman.Service
|
||||
CookieBanner *cookiebanner.Service
|
||||
@@ -189,6 +191,7 @@ func NewServer(cfg Config) (*Server, error) {
|
||||
cfg.IAM,
|
||||
cfg.ESign,
|
||||
cfg.AccessReview,
|
||||
cfg.AgentRun,
|
||||
cfg.Mailman,
|
||||
cfg.CookieBanner,
|
||||
cfg.Cookie,
|
||||
|
||||
82
pkg/server/api/console/v1/agent_run_resolvers.go
Normal file
82
pkg/server/api/console/v1/agent_run_resolvers.go
Normal file
@@ -0,0 +1,82 @@
|
||||
package console_v1
|
||||
|
||||
// This file will be automatically regenerated based on the schema, any resolver
|
||||
// implementations
|
||||
// will be copied through when generating and any unknown code will be moved to the end.
|
||||
// Code generated by github.com/99designs/gqlgen version v0.17.90
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/vikstrous/dataloadgen"
|
||||
"go.gearno.de/kit/log"
|
||||
"go.probo.inc/probo/pkg/coredata"
|
||||
"go.probo.inc/probo/pkg/probo"
|
||||
"go.probo.inc/probo/pkg/server/api/console/v1/dataloader"
|
||||
"go.probo.inc/probo/pkg/server/api/console/v1/schema"
|
||||
"go.probo.inc/probo/pkg/server/api/console/v1/types"
|
||||
"go.probo.inc/probo/pkg/server/gqlutils"
|
||||
)
|
||||
|
||||
// Organization is the resolver for the organization field.
|
||||
func (r *agentRunResolver) Organization(ctx context.Context, obj *types.AgentRun) (*types.Organization, error) {
|
||||
if _, err := r.authorize(ctx, obj.ID, probo.ActionOrganizationGet); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
loaders := dataloader.FromContext(ctx)
|
||||
|
||||
organization, err := loaders.Organization.Load(ctx, obj.Organization.ID)
|
||||
if err != nil {
|
||||
if errors.Is(err, coredata.ErrResourceNotFound) || errors.Is(err, dataloadgen.ErrNotFound) {
|
||||
return nil, gqlutils.NotFound(ctx, err)
|
||||
}
|
||||
|
||||
r.logger.ErrorCtx(ctx, "cannot load organization", log.Error(err))
|
||||
|
||||
return nil, gqlutils.Internal(ctx)
|
||||
}
|
||||
|
||||
return types.NewOrganization(organization), nil
|
||||
}
|
||||
|
||||
// Permission is the resolver for the permission field.
|
||||
func (r *agentRunResolver) Permission(ctx context.Context, obj *types.AgentRun, action string) (bool, error) {
|
||||
return r.Resolver.Permission(ctx, obj, action)
|
||||
}
|
||||
|
||||
// TotalCount is the resolver for the totalCount field.
|
||||
func (r *agentRunConnectionResolver) TotalCount(ctx context.Context, obj *types.AgentRunConnection) (int, error) {
|
||||
scope, err := r.authorize(ctx, obj.ParentID, probo.ActionAgentRunList)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
switch obj.Resolver.(type) {
|
||||
case *organizationResolver:
|
||||
count, err := r.agentRun.CountForOrganizationID(ctx, scope, obj.ParentID)
|
||||
if err != nil {
|
||||
r.logger.ErrorCtx(ctx, "cannot count agent runs", log.Error(err))
|
||||
return 0, gqlutils.Internal(ctx)
|
||||
}
|
||||
|
||||
return count, nil
|
||||
}
|
||||
|
||||
r.logger.ErrorCtx(ctx, "unsupported resolver for agent run connection", log.String("resolver", fmt.Sprintf("%T", obj.Resolver)))
|
||||
|
||||
return 0, gqlutils.Internal(ctx)
|
||||
}
|
||||
|
||||
// AgentRun returns schema.AgentRunResolver implementation.
|
||||
func (r *Resolver) AgentRun() schema.AgentRunResolver { return &agentRunResolver{r} }
|
||||
|
||||
// AgentRunConnection returns schema.AgentRunConnectionResolver implementation.
|
||||
func (r *Resolver) AgentRunConnection() schema.AgentRunConnectionResolver {
|
||||
return &agentRunConnectionResolver{r}
|
||||
}
|
||||
|
||||
type agentRunResolver struct{ *Resolver }
|
||||
type agentRunConnectionResolver struct{ *Resolver }
|
||||
@@ -369,6 +369,16 @@ func (r *queryResolver) Node(ctx context.Context, id gid.GID) (types.Node, error
|
||||
|
||||
return types.NewWebhookSubscription(wc), nil
|
||||
}
|
||||
case coredata.AgentRunEntityType:
|
||||
action = probo.ActionAgentRunGet
|
||||
loadNode = func(ctx context.Context, scope *coredata.Scope, id gid.GID) (types.Node, error) {
|
||||
run, err := r.agentRun.Get(ctx, scope, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return types.NewAgentRun(run), nil
|
||||
}
|
||||
case coredata.AccessReviewCampaignEntityType:
|
||||
action = probo.ActionAccessReviewCampaignGet
|
||||
loadNode = func(ctx context.Context, scope *coredata.Scope, id gid.GID) (types.Node, error) {
|
||||
|
||||
60
pkg/server/api/console/v1/graphql/agent_run.graphql
Normal file
60
pkg/server/api/console/v1/graphql/agent_run.graphql
Normal file
@@ -0,0 +1,60 @@
|
||||
enum AgentRunStatus
|
||||
@goModel(model: "go.probo.inc/probo/pkg/coredata.AgentRunStatus") {
|
||||
PENDING
|
||||
@goEnum(value: "go.probo.inc/probo/pkg/coredata.AgentRunStatusPending")
|
||||
RUNNING
|
||||
@goEnum(value: "go.probo.inc/probo/pkg/coredata.AgentRunStatusRunning")
|
||||
SUSPENDED
|
||||
@goEnum(value: "go.probo.inc/probo/pkg/coredata.AgentRunStatusSuspended")
|
||||
AWAITING_APPROVAL
|
||||
@goEnum(
|
||||
value: "go.probo.inc/probo/pkg/coredata.AgentRunStatusAwaitingApproval"
|
||||
)
|
||||
COMPLETED
|
||||
@goEnum(value: "go.probo.inc/probo/pkg/coredata.AgentRunStatusCompleted")
|
||||
FAILED
|
||||
@goEnum(value: "go.probo.inc/probo/pkg/coredata.AgentRunStatusFailed")
|
||||
}
|
||||
|
||||
enum AgentRunOrderField
|
||||
@goModel(model: "go.probo.inc/probo/pkg/coredata.AgentRunOrderField") {
|
||||
CREATED_AT
|
||||
@goEnum(
|
||||
value: "go.probo.inc/probo/pkg/coredata.AgentRunOrderFieldCreatedAt"
|
||||
)
|
||||
}
|
||||
|
||||
input AgentRunOrder
|
||||
@goModel(
|
||||
model: "go.probo.inc/probo/pkg/server/api/console/v1/types.AgentRunOrderBy"
|
||||
) {
|
||||
direction: OrderDirection!
|
||||
field: AgentRunOrderField!
|
||||
}
|
||||
|
||||
type AgentRun implements Node {
|
||||
id: ID!
|
||||
organization: Organization! @goField(forceResolver: true)
|
||||
agentName: String!
|
||||
status: AgentRunStatus!
|
||||
errorMessage: String
|
||||
startedAt: Datetime
|
||||
createdAt: Datetime!
|
||||
updatedAt: Datetime!
|
||||
|
||||
permission(action: String!): Boolean! @goField(forceResolver: true)
|
||||
}
|
||||
|
||||
type AgentRunConnection
|
||||
@goModel(
|
||||
model: "go.probo.inc/probo/pkg/server/api/console/v1/types.AgentRunConnection"
|
||||
) {
|
||||
totalCount: Int! @goField(forceResolver: true)
|
||||
edges: [AgentRunEdge!]!
|
||||
pageInfo: PageInfo!
|
||||
}
|
||||
|
||||
type AgentRunEdge {
|
||||
cursor: CursorKey!
|
||||
node: AgentRun!
|
||||
}
|
||||
@@ -311,6 +311,14 @@ type Organization implements Node {
|
||||
orderBy: TaskOrder
|
||||
): TaskConnection! @goField(forceResolver: true)
|
||||
|
||||
agentRuns(
|
||||
first: Int
|
||||
after: CursorKey
|
||||
last: Int
|
||||
before: CursorKey
|
||||
orderBy: AgentRunOrder
|
||||
): AgentRunConnection! @goField(forceResolver: true)
|
||||
|
||||
trustCenter: TrustCenter @goField(forceResolver: true)
|
||||
customDomain: CustomDomain @goField(forceResolver: true)
|
||||
trustCenterFiles(
|
||||
|
||||
@@ -19,6 +19,7 @@ import (
|
||||
|
||||
"go.gearno.de/kit/log"
|
||||
"go.probo.inc/probo/pkg/accessreview"
|
||||
"go.probo.inc/probo/pkg/agentrun"
|
||||
"go.probo.inc/probo/pkg/connector"
|
||||
"go.probo.inc/probo/pkg/connector/provider"
|
||||
"go.probo.inc/probo/pkg/cookiebanner"
|
||||
@@ -39,6 +40,7 @@ func NewGraphQLHandler(
|
||||
proboSvc *probo.Service,
|
||||
esignSvc *esign.Service,
|
||||
accessReviewSvc *accessreview.Service,
|
||||
agentRunSvc *agentrun.Service,
|
||||
mailmanSvc *mailman.Service,
|
||||
cookieBannerSvc *cookiebanner.Service,
|
||||
connectorRegistry *connector.ConnectorRegistry,
|
||||
@@ -56,6 +58,7 @@ func NewGraphQLHandler(
|
||||
iam: iamSvc,
|
||||
esign: esignSvc,
|
||||
accessReview: accessReviewSvc,
|
||||
agentRun: agentRunSvc,
|
||||
mailman: mailmanSvc,
|
||||
cookieBanner: cookieBannerSvc,
|
||||
connectorRegistry: connectorRegistry,
|
||||
|
||||
@@ -1173,6 +1173,36 @@ func (r *organizationResolver) Tasks(ctx context.Context, obj *types.Organizatio
|
||||
return types.NewTaskConnection(page, r, obj.ID), nil
|
||||
}
|
||||
|
||||
// AgentRuns is the resolver for the agentRuns field.
|
||||
func (r *organizationResolver) AgentRuns(ctx context.Context, obj *types.Organization, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.AgentRunOrderBy) (*types.AgentRunConnection, error) {
|
||||
scope, err := r.authorize(ctx, obj.ID, probo.ActionAgentRunList)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
pageOrderBy := page.OrderBy[coredata.AgentRunOrderField]{
|
||||
Field: coredata.AgentRunOrderFieldCreatedAt,
|
||||
Direction: page.OrderDirectionDesc,
|
||||
}
|
||||
|
||||
if orderBy != nil {
|
||||
pageOrderBy = page.OrderBy[coredata.AgentRunOrderField]{
|
||||
Field: orderBy.Field,
|
||||
Direction: orderBy.Direction,
|
||||
}
|
||||
}
|
||||
|
||||
cursor := types.NewCursor(first, after, last, before, pageOrderBy)
|
||||
|
||||
page, err := r.agentRun.ListForOrganizationID(ctx, scope, obj.ID, cursor)
|
||||
if err != nil {
|
||||
r.logger.ErrorCtx(ctx, "cannot list organization agent runs", log.Error(err))
|
||||
return nil, gqlutils.Internal(ctx)
|
||||
}
|
||||
|
||||
return types.NewAgentRunConnection(page, r, obj.ID), nil
|
||||
}
|
||||
|
||||
// TrustCenter is the resolver for the trustCenter field.
|
||||
func (r *organizationResolver) TrustCenter(ctx context.Context, obj *types.Organization) (*types.TrustCenter, error) {
|
||||
scope, err := r.authorize(ctx, obj.ID, probo.ActionTrustCenterGet)
|
||||
|
||||
@@ -27,6 +27,7 @@ import (
|
||||
"go.gearno.de/kit/httpserver"
|
||||
"go.gearno.de/kit/log"
|
||||
"go.probo.inc/probo/pkg/accessreview"
|
||||
"go.probo.inc/probo/pkg/agentrun"
|
||||
"go.probo.inc/probo/pkg/baseurl"
|
||||
"go.probo.inc/probo/pkg/connector"
|
||||
"go.probo.inc/probo/pkg/connector/provider"
|
||||
@@ -55,6 +56,7 @@ type (
|
||||
iam *iam.Service
|
||||
esign *esign.Service
|
||||
accessReview *accessreview.Service
|
||||
agentRun *agentrun.Service
|
||||
mailman *mailman.Service
|
||||
cookieBanner *cookiebanner.Service
|
||||
connectorRegistry *connector.ConnectorRegistry
|
||||
@@ -72,6 +74,7 @@ func NewMux(
|
||||
iamSvc *iam.Service,
|
||||
esignSvc *esign.Service,
|
||||
accessReviewSvc *accessreview.Service,
|
||||
agentRunSvc *agentrun.Service,
|
||||
mailmanSvc *mailman.Service,
|
||||
cookieBannerSvc *cookiebanner.Service,
|
||||
cookieConfig securecookie.Config,
|
||||
@@ -92,6 +95,7 @@ func NewMux(
|
||||
proboSvc,
|
||||
esignSvc,
|
||||
accessReviewSvc,
|
||||
agentRunSvc,
|
||||
mailmanSvc,
|
||||
cookieBannerSvc,
|
||||
connectorRegistry,
|
||||
|
||||
76
pkg/server/api/console/v1/types/agent_run.go
Normal file
76
pkg/server/api/console/v1/types/agent_run.go
Normal file
@@ -0,0 +1,76 @@
|
||||
// Copyright (c) 2026 Probo Inc <hello@getprobo.com>.
|
||||
//
|
||||
// Permission to use, copy, modify, and/or distribute this software for any
|
||||
// purpose with or without fee is hereby granted, provided that the above
|
||||
// copyright notice and this permission notice appear in all copies.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
||||
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
||||
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
||||
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
||||
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
||||
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
||||
// PERFORMANCE OF THIS SOFTWARE.
|
||||
|
||||
package types
|
||||
|
||||
import (
|
||||
"go.probo.inc/probo/pkg/coredata"
|
||||
"go.probo.inc/probo/pkg/gid"
|
||||
"go.probo.inc/probo/pkg/page"
|
||||
)
|
||||
|
||||
type (
|
||||
AgentRunOrderBy OrderBy[coredata.AgentRunOrderField]
|
||||
|
||||
AgentRunConnection struct {
|
||||
TotalCount int
|
||||
Edges []*AgentRunEdge
|
||||
PageInfo PageInfo
|
||||
|
||||
Resolver any
|
||||
ParentID gid.GID
|
||||
}
|
||||
)
|
||||
|
||||
func NewAgentRunConnection(
|
||||
p *page.Page[*coredata.AgentRun, coredata.AgentRunOrderField],
|
||||
parentType any,
|
||||
parentID gid.GID,
|
||||
) *AgentRunConnection {
|
||||
var edges = make([]*AgentRunEdge, len(p.Data))
|
||||
|
||||
for i := range edges {
|
||||
edges[i] = NewAgentRunEdge(p.Data[i], p.Cursor.OrderBy.Field)
|
||||
}
|
||||
|
||||
return &AgentRunConnection{
|
||||
Edges: edges,
|
||||
PageInfo: *NewPageInfo(p),
|
||||
|
||||
Resolver: parentType,
|
||||
ParentID: parentID,
|
||||
}
|
||||
}
|
||||
|
||||
func NewAgentRunEdge(run *coredata.AgentRun, orderBy coredata.AgentRunOrderField) *AgentRunEdge {
|
||||
return &AgentRunEdge{
|
||||
Cursor: run.CursorKey(orderBy),
|
||||
Node: NewAgentRun(run),
|
||||
}
|
||||
}
|
||||
|
||||
func NewAgentRun(run *coredata.AgentRun) *AgentRun {
|
||||
return &AgentRun{
|
||||
ID: run.ID,
|
||||
Organization: &Organization{
|
||||
ID: run.OrganizationID,
|
||||
},
|
||||
AgentName: run.StartAgentName,
|
||||
Status: run.Status,
|
||||
ErrorMessage: run.ErrorMessage,
|
||||
StartedAt: run.StartedAt,
|
||||
CreatedAt: run.CreatedAt,
|
||||
UpdatedAt: run.UpdatedAt,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user