mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
feat(coderd/agentapi): allow inserting apps for sub agents (#18129)
Allow creating workspace apps for a sub agent when the agent is being created with `CreateSubAgent` in the agent api.
This commit is contained in:
+127
-2
@@ -2,6 +2,9 @@ package agentapi
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/sqlc-dev/pqtype"
|
||||
@@ -11,6 +14,7 @@ import (
|
||||
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/codersdk"
|
||||
"github.com/coder/coder/v2/provisioner"
|
||||
"github.com/coder/quartz"
|
||||
)
|
||||
@@ -37,10 +41,16 @@ func (a *SubAgentAPI) CreateSubAgent(ctx context.Context, req *agentproto.Create
|
||||
|
||||
agentName := req.Name
|
||||
if agentName == "" {
|
||||
return nil, xerrors.Errorf("agent name cannot be empty")
|
||||
return nil, codersdk.ValidationError{
|
||||
Field: "name",
|
||||
Detail: "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())
|
||||
return nil, codersdk.ValidationError{
|
||||
Field: "name",
|
||||
Detail: fmt.Sprintf("agent name %q does not match regex %q", agentName, provisioner.AgentNameRegex),
|
||||
}
|
||||
}
|
||||
|
||||
createdAt := a.Clock.Now()
|
||||
@@ -71,12 +81,127 @@ func (a *SubAgentAPI) CreateSubAgent(ctx context.Context, req *agentproto.Create
|
||||
return nil, xerrors.Errorf("insert sub agent: %w", err)
|
||||
}
|
||||
|
||||
var appCreationErrors []*agentproto.CreateSubAgentResponse_AppCreationError
|
||||
appSlugs := make(map[string]struct{})
|
||||
|
||||
for i, app := range req.Apps {
|
||||
err := func() error {
|
||||
slug := app.Slug
|
||||
if slug == "" {
|
||||
return codersdk.ValidationError{
|
||||
Field: "slug",
|
||||
Detail: "must not be empty",
|
||||
}
|
||||
}
|
||||
if !provisioner.AppSlugRegex.MatchString(slug) {
|
||||
return codersdk.ValidationError{
|
||||
Field: "slug",
|
||||
Detail: fmt.Sprintf("%q does not match regex %q", slug, provisioner.AppSlugRegex),
|
||||
}
|
||||
}
|
||||
if _, exists := appSlugs[slug]; exists {
|
||||
return codersdk.ValidationError{
|
||||
Field: "slug",
|
||||
Detail: fmt.Sprintf("%q is already in use", slug),
|
||||
}
|
||||
}
|
||||
appSlugs[slug] = struct{}{}
|
||||
|
||||
health := database.WorkspaceAppHealthDisabled
|
||||
if app.Healthcheck == nil {
|
||||
app.Healthcheck = &agentproto.CreateSubAgentRequest_App_Healthcheck{}
|
||||
}
|
||||
if app.Healthcheck.Url != "" {
|
||||
health = database.WorkspaceAppHealthInitializing
|
||||
}
|
||||
|
||||
var sharingLevel database.AppSharingLevel
|
||||
switch app.GetShare() {
|
||||
case agentproto.CreateSubAgentRequest_App_OWNER:
|
||||
sharingLevel = database.AppSharingLevelOwner
|
||||
case agentproto.CreateSubAgentRequest_App_AUTHENTICATED:
|
||||
sharingLevel = database.AppSharingLevelAuthenticated
|
||||
case agentproto.CreateSubAgentRequest_App_PUBLIC:
|
||||
sharingLevel = database.AppSharingLevelPublic
|
||||
default:
|
||||
return codersdk.ValidationError{
|
||||
Field: "share",
|
||||
Detail: fmt.Sprintf("%q is not a valid app sharing level", app.GetShare()),
|
||||
}
|
||||
}
|
||||
|
||||
var openIn database.WorkspaceAppOpenIn
|
||||
switch app.GetOpenIn() {
|
||||
case agentproto.CreateSubAgentRequest_App_SLIM_WINDOW:
|
||||
openIn = database.WorkspaceAppOpenInSlimWindow
|
||||
case agentproto.CreateSubAgentRequest_App_TAB:
|
||||
openIn = database.WorkspaceAppOpenInTab
|
||||
default:
|
||||
return codersdk.ValidationError{
|
||||
Field: "open_in",
|
||||
Detail: fmt.Sprintf("%q is not an open in setting", app.GetOpenIn()),
|
||||
}
|
||||
}
|
||||
|
||||
_, err := a.Database.InsertWorkspaceApp(ctx, database.InsertWorkspaceAppParams{
|
||||
ID: uuid.New(),
|
||||
CreatedAt: createdAt,
|
||||
AgentID: subAgent.ID,
|
||||
Slug: app.Slug,
|
||||
DisplayName: app.GetDisplayName(),
|
||||
Icon: app.GetIcon(),
|
||||
Command: sql.NullString{
|
||||
Valid: app.GetCommand() != "",
|
||||
String: app.GetCommand(),
|
||||
},
|
||||
Url: sql.NullString{
|
||||
Valid: app.GetUrl() != "",
|
||||
String: app.GetUrl(),
|
||||
},
|
||||
External: app.GetExternal(),
|
||||
Subdomain: app.GetSubdomain(),
|
||||
SharingLevel: sharingLevel,
|
||||
HealthcheckUrl: app.Healthcheck.Url,
|
||||
HealthcheckInterval: app.Healthcheck.Interval,
|
||||
HealthcheckThreshold: app.Healthcheck.Threshold,
|
||||
Health: health,
|
||||
DisplayOrder: app.GetOrder(),
|
||||
Hidden: app.GetHidden(),
|
||||
OpenIn: openIn,
|
||||
DisplayGroup: sql.NullString{
|
||||
Valid: app.GetGroup() != "",
|
||||
String: app.GetGroup(),
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return xerrors.Errorf("insert workspace app: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}()
|
||||
if err != nil {
|
||||
appErr := &agentproto.CreateSubAgentResponse_AppCreationError{
|
||||
Index: int32(i), //nolint:gosec // This would only overflow if we created 2 billion apps.
|
||||
Error: err.Error(),
|
||||
}
|
||||
|
||||
var validationErr codersdk.ValidationError
|
||||
if errors.As(err, &validationErr) {
|
||||
appErr.Field = &validationErr.Field
|
||||
appErr.Error = validationErr.Detail
|
||||
}
|
||||
|
||||
appCreationErrors = append(appCreationErrors, appErr)
|
||||
}
|
||||
}
|
||||
|
||||
return &agentproto.CreateSubAgentResponse{
|
||||
Agent: &agentproto.SubAgent{
|
||||
Name: subAgent.Name,
|
||||
Id: subAgent.ID[:],
|
||||
AuthToken: subAgent.AuthToken[:],
|
||||
},
|
||||
AppCreationErrors: appCreationErrors,
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user