feat(coderd): add support for external agents to API's and provisioner (#19286)

This pull request introduces support for external workspace management, allowing users to register and manage workspaces that are provisioned and managed outside of the Coder.

Depends on: https://github.com/coder/terraform-provider-coder/pull/424

* GET /api/v2/init-script - Gets the agent initialization script
  * By default, it returns a script for Linux (amd64), but with query parameters (os and arch) you can get the init script for different platforms
* GET /api/v2/workspaces/{workspace}/external-agent/{agent}/credentials - Gets credentials for an external agent **(enterprise)**
* Updated queries to filter workspaces/templates by the has_external_agent field
This commit is contained in:
Kacper Sawicki
2025-08-19 10:41:33 +02:00
committed by GitHub
parent f085c37af3
commit 9edceef0bf
53 changed files with 1616 additions and 98 deletions
+49 -37
View File
@@ -506,6 +506,15 @@ func New(ctx context.Context, options *Options) (_ *API, err error) {
apiKeyMiddleware,
httpmw.ExtractNotificationTemplateParam(options.Database),
).Put("/notifications/templates/{notification_template}/method", api.updateNotificationTemplateMethod)
r.Route("/workspaces/{workspace}/external-agent", func(r chi.Router) {
r.Use(
apiKeyMiddleware,
httpmw.ExtractWorkspaceParam(options.Database),
api.RequireFeatureMW(codersdk.FeatureWorkspaceExternalAgent),
)
r.Get("/{agent}/credentials", api.workspaceExternalAgentCredentials)
})
})
if len(options.SCIMAPIKey) != 0 {
@@ -920,17 +929,9 @@ func (api *API) updateEntitlements(ctx context.Context) error {
}
reloadedEntitlements.Features[codersdk.FeatureExternalTokenEncryption] = featureExternalTokenEncryption
// If there's a license installed, we will use the enterprise build
// limit checker.
// This checker currently only enforces the managed agent limit.
if reloadedEntitlements.HasLicense {
var checker wsbuilder.UsageChecker = api
api.AGPL.BuildUsageChecker.Store(&checker)
} else {
// Don't check any usage, just like AGPL.
var checker wsbuilder.UsageChecker = wsbuilder.NoopUsageChecker{}
api.AGPL.BuildUsageChecker.Store(&checker)
}
// Always use the enterprise usage checker
var checker wsbuilder.UsageChecker = api
api.AGPL.BuildUsageChecker.Store(&checker)
return reloadedEntitlements, nil
})
@@ -939,9 +940,17 @@ func (api *API) updateEntitlements(ctx context.Context) error {
var _ wsbuilder.UsageChecker = &API{}
func (api *API) CheckBuildUsage(ctx context.Context, store database.Store, templateVersion *database.TemplateVersion) (wsbuilder.UsageCheckResponse, error) {
// We assume that if this function is called, a valid license is installed.
// When there are no licenses installed, a noop usage checker is used
// instead.
// If the template version has an external agent, we need to check that the
// license is entitled to this feature.
if templateVersion.HasExternalAgent.Valid && templateVersion.HasExternalAgent.Bool {
feature, ok := api.Entitlements.Feature(codersdk.FeatureWorkspaceExternalAgent)
if !ok || !feature.Enabled {
return wsbuilder.UsageCheckResponse{
Permitted: false,
Message: "You have a template which uses external agents but your license is not entitled to this feature. You will be unable to create new workspaces from these templates.",
}, nil
}
}
// If the template version doesn't have an AI task, we don't need to check
// usage.
@@ -951,32 +960,35 @@ func (api *API) CheckBuildUsage(ctx context.Context, store database.Store, templ
}, nil
}
// Otherwise, we need to check that we haven't breached the managed agent
// When unlicensed, we need to check that we haven't breached the managed agent
// limit.
managedAgentLimit, ok := api.Entitlements.Feature(codersdk.FeatureManagedAgentLimit)
if !ok || !managedAgentLimit.Enabled || managedAgentLimit.Limit == nil || managedAgentLimit.UsagePeriod == nil {
return wsbuilder.UsageCheckResponse{
Permitted: false,
Message: "Your license is not entitled to managed agents. Please contact sales to continue using managed agents.",
}, nil
}
// Unlicensed deployments are allowed to use unlimited managed agents.
if api.Entitlements.HasLicense() {
managedAgentLimit, ok := api.Entitlements.Feature(codersdk.FeatureManagedAgentLimit)
if !ok || !managedAgentLimit.Enabled || managedAgentLimit.Limit == nil || managedAgentLimit.UsagePeriod == nil {
return wsbuilder.UsageCheckResponse{
Permitted: false,
Message: "Your license is not entitled to managed agents. Please contact sales to continue using managed agents.",
}, nil
}
// This check is intentionally not committed to the database. It's fine if
// it's not 100% accurate or allows for minor breaches due to build races.
// nolint:gocritic // Requires permission to read all workspaces to read managed agent count.
managedAgentCount, err := store.GetManagedAgentCount(agpldbauthz.AsSystemRestricted(ctx), database.GetManagedAgentCountParams{
StartTime: managedAgentLimit.UsagePeriod.Start,
EndTime: managedAgentLimit.UsagePeriod.End,
})
if err != nil {
return wsbuilder.UsageCheckResponse{}, xerrors.Errorf("get managed agent count: %w", err)
}
// This check is intentionally not committed to the database. It's fine if
// it's not 100% accurate or allows for minor breaches due to build races.
// nolint:gocritic // Requires permission to read all workspaces to read managed agent count.
managedAgentCount, err := store.GetManagedAgentCount(agpldbauthz.AsSystemRestricted(ctx), database.GetManagedAgentCountParams{
StartTime: managedAgentLimit.UsagePeriod.Start,
EndTime: managedAgentLimit.UsagePeriod.End,
})
if err != nil {
return wsbuilder.UsageCheckResponse{}, xerrors.Errorf("get managed agent count: %w", err)
}
if managedAgentCount >= *managedAgentLimit.Limit {
return wsbuilder.UsageCheckResponse{
Permitted: false,
Message: "You have breached the managed agent limit in your license. Please contact sales to continue using managed agents.",
}, nil
if managedAgentCount >= *managedAgentLimit.Limit {
return wsbuilder.UsageCheckResponse{
Permitted: false,
Message: "You have breached the managed agent limit in your license. Please contact sales to continue using managed agents.",
}, nil
}
}
return wsbuilder.UsageCheckResponse{