mirror of
https://github.com/coder/coder.git
synced 2026-06-02 20:48:20 +00:00
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:
Generated
+93
-1
@@ -1280,6 +1280,39 @@ const docTemplate = `{
|
||||
}
|
||||
}
|
||||
},
|
||||
"/init-script/{os}/{arch}": {
|
||||
"get": {
|
||||
"produces": [
|
||||
"text/plain"
|
||||
],
|
||||
"tags": [
|
||||
"InitScript"
|
||||
],
|
||||
"summary": "Get agent init script",
|
||||
"operationId": "get-agent-init-script",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Operating system",
|
||||
"name": "os",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Architecture",
|
||||
"name": "arch",
|
||||
"in": "path",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Success"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/insights/daus": {
|
||||
"get": {
|
||||
"security": [
|
||||
@@ -9835,7 +9868,7 @@ const docTemplate = `{
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Search query in the format ` + "`" + `key:value` + "`" + `. Available keys are: owner, template, name, status, has-agent, dormant, last_used_after, last_used_before, has-ai-task.",
|
||||
"description": "Search query in the format ` + "`" + `key:value` + "`" + `. Available keys are: owner, template, name, status, has-agent, dormant, last_used_after, last_used_before, has-ai-task, has_external_agent.",
|
||||
"name": "q",
|
||||
"in": "query"
|
||||
},
|
||||
@@ -10271,6 +10304,48 @@ const docTemplate = `{
|
||||
}
|
||||
}
|
||||
},
|
||||
"/workspaces/{workspace}/external-agent/{agent}/credentials": {
|
||||
"get": {
|
||||
"security": [
|
||||
{
|
||||
"CoderSessionToken": []
|
||||
}
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"Enterprise"
|
||||
],
|
||||
"summary": "Get workspace external agent credentials",
|
||||
"operationId": "get-workspace-external-agent-credentials",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"format": "uuid",
|
||||
"description": "Workspace ID",
|
||||
"name": "workspace",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Agent name",
|
||||
"name": "agent",
|
||||
"in": "path",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/codersdk.ExternalAgentCredentials"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/workspaces/{workspace}/favorite": {
|
||||
"put": {
|
||||
"security": [
|
||||
@@ -12901,6 +12976,17 @@ const docTemplate = `{
|
||||
"ExperimentWorkspaceSharing"
|
||||
]
|
||||
},
|
||||
"codersdk.ExternalAgentCredentials": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"agent_token": {
|
||||
"type": "string"
|
||||
},
|
||||
"command": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"codersdk.ExternalAuth": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -16816,6 +16902,9 @@ const docTemplate = `{
|
||||
"created_by": {
|
||||
"$ref": "#/definitions/codersdk.MinimalUser"
|
||||
},
|
||||
"has_external_agent": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"id": {
|
||||
"type": "string",
|
||||
"format": "uuid"
|
||||
@@ -18675,6 +18764,9 @@ const docTemplate = `{
|
||||
"has_ai_task": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"has_external_agent": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"id": {
|
||||
"type": "string",
|
||||
"format": "uuid"
|
||||
|
||||
Generated
+85
-1
@@ -1108,6 +1108,35 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/init-script/{os}/{arch}": {
|
||||
"get": {
|
||||
"produces": ["text/plain"],
|
||||
"tags": ["InitScript"],
|
||||
"summary": "Get agent init script",
|
||||
"operationId": "get-agent-init-script",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Operating system",
|
||||
"name": "os",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Architecture",
|
||||
"name": "arch",
|
||||
"in": "path",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Success"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/insights/daus": {
|
||||
"get": {
|
||||
"security": [
|
||||
@@ -8693,7 +8722,7 @@
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Search query in the format `key:value`. Available keys are: owner, template, name, status, has-agent, dormant, last_used_after, last_used_before, has-ai-task.",
|
||||
"description": "Search query in the format `key:value`. Available keys are: owner, template, name, status, has-agent, dormant, last_used_after, last_used_before, has-ai-task, has_external_agent.",
|
||||
"name": "q",
|
||||
"in": "query"
|
||||
},
|
||||
@@ -9085,6 +9114,44 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/workspaces/{workspace}/external-agent/{agent}/credentials": {
|
||||
"get": {
|
||||
"security": [
|
||||
{
|
||||
"CoderSessionToken": []
|
||||
}
|
||||
],
|
||||
"produces": ["application/json"],
|
||||
"tags": ["Enterprise"],
|
||||
"summary": "Get workspace external agent credentials",
|
||||
"operationId": "get-workspace-external-agent-credentials",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"format": "uuid",
|
||||
"description": "Workspace ID",
|
||||
"name": "workspace",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Agent name",
|
||||
"name": "agent",
|
||||
"in": "path",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/codersdk.ExternalAgentCredentials"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/workspaces/{workspace}/favorite": {
|
||||
"put": {
|
||||
"security": [
|
||||
@@ -11563,6 +11630,17 @@
|
||||
"ExperimentWorkspaceSharing"
|
||||
]
|
||||
},
|
||||
"codersdk.ExternalAgentCredentials": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"agent_token": {
|
||||
"type": "string"
|
||||
},
|
||||
"command": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"codersdk.ExternalAuth": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -15337,6 +15415,9 @@
|
||||
"created_by": {
|
||||
"$ref": "#/definitions/codersdk.MinimalUser"
|
||||
},
|
||||
"has_external_agent": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"id": {
|
||||
"type": "string",
|
||||
"format": "uuid"
|
||||
@@ -17079,6 +17160,9 @@
|
||||
"has_ai_task": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"has_external_agent": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"id": {
|
||||
"type": "string",
|
||||
"format": "uuid"
|
||||
|
||||
@@ -1566,6 +1566,9 @@ func New(options *Options) *API {
|
||||
r.Use(apiKeyMiddleware)
|
||||
r.Get("/", api.tailnetRPCConn)
|
||||
})
|
||||
r.Route("/init-script", func(r chi.Router) {
|
||||
r.Get("/{os}/{arch}", api.initScript)
|
||||
})
|
||||
})
|
||||
|
||||
if options.SwaggerEndpoint {
|
||||
|
||||
@@ -310,7 +310,8 @@ func assertSecurityDefined(t *testing.T, comment SwaggerComment) {
|
||||
comment.router == "/" ||
|
||||
comment.router == "/users/login" ||
|
||||
comment.router == "/users/otp/request" ||
|
||||
comment.router == "/users/otp/change-password" {
|
||||
comment.router == "/users/otp/change-password" ||
|
||||
comment.router == "/init-script/{os}/{arch}" {
|
||||
return // endpoints do not require authorization
|
||||
}
|
||||
assert.Containsf(t, authorizedSecurityTags, comment.security, "@Security must be either of these options: %v", authorizedSecurityTags)
|
||||
@@ -361,7 +362,8 @@ func assertProduce(t *testing.T, comment SwaggerComment) {
|
||||
(comment.router == "/licenses/{id}" && comment.method == "delete") ||
|
||||
(comment.router == "/debug/coordinator" && comment.method == "get") ||
|
||||
(comment.router == "/debug/tailnet" && comment.method == "get") ||
|
||||
(comment.router == "/workspaces/{workspace}/acl" && comment.method == "patch") {
|
||||
(comment.router == "/workspaces/{workspace}/acl" && comment.method == "patch") ||
|
||||
(comment.router == "/init-script/{os}/{arch}" && comment.method == "get") {
|
||||
return // Exception: HTTP 200 is returned without response entity
|
||||
}
|
||||
|
||||
|
||||
@@ -161,3 +161,9 @@ func (l *Set) Errors() []string {
|
||||
defer l.entitlementsMu.RUnlock()
|
||||
return slices.Clone(l.entitlements.Errors)
|
||||
}
|
||||
|
||||
func (l *Set) HasLicense() bool {
|
||||
l.entitlementsMu.RLock()
|
||||
defer l.entitlementsMu.RUnlock()
|
||||
return l.entitlements.HasLicense
|
||||
}
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
package coderd
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
|
||||
"github.com/coder/coder/v2/coderd/httpapi"
|
||||
"github.com/coder/coder/v2/codersdk"
|
||||
"github.com/coder/coder/v2/provisionersdk"
|
||||
)
|
||||
|
||||
// @Summary Get agent init script
|
||||
// @ID get-agent-init-script
|
||||
// @Produce text/plain
|
||||
// @Tags InitScript
|
||||
// @Param os path string true "Operating system"
|
||||
// @Param arch path string true "Architecture"
|
||||
// @Success 200 "Success"
|
||||
// @Router /init-script/{os}/{arch} [get]
|
||||
func (api *API) initScript(rw http.ResponseWriter, r *http.Request) {
|
||||
os := strings.ToLower(chi.URLParam(r, "os"))
|
||||
arch := strings.ToLower(chi.URLParam(r, "arch"))
|
||||
|
||||
script, exists := provisionersdk.AgentScriptEnv()[fmt.Sprintf("CODER_AGENT_SCRIPT_%s_%s", os, arch)]
|
||||
if !exists {
|
||||
httpapi.Write(r.Context(), rw, http.StatusBadRequest, codersdk.Response{
|
||||
Message: fmt.Sprintf("Unknown os/arch: %s/%s", os, arch),
|
||||
})
|
||||
return
|
||||
}
|
||||
script = strings.ReplaceAll(script, "${ACCESS_URL}", api.AccessURL.String()+"/")
|
||||
script = strings.ReplaceAll(script, "${AUTH_TYPE}", "token")
|
||||
|
||||
scriptBytes := []byte(script)
|
||||
hash := sha256.Sum256(scriptBytes)
|
||||
rw.Header().Set("Content-Digest", fmt.Sprintf("sha256:%x", base64.StdEncoding.EncodeToString(hash[:])))
|
||||
rw.Header().Set("Content-Type", "text/plain; charset=utf-8")
|
||||
rw.WriteHeader(http.StatusOK)
|
||||
_, _ = rw.Write(scriptBytes)
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
package coderd_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/coder/coder/v2/coderd/coderdtest"
|
||||
"github.com/coder/coder/v2/codersdk"
|
||||
)
|
||||
|
||||
func TestInitScript(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
t.Run("OK Windows amd64", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
client := coderdtest.New(t, nil)
|
||||
script, err := client.InitScript(context.Background(), "windows", "amd64")
|
||||
require.NoError(t, err)
|
||||
require.NotEmpty(t, script)
|
||||
require.Contains(t, script, "$env:CODER_AGENT_AUTH = \"token\"")
|
||||
require.Contains(t, script, "/bin/coder-windows-amd64.exe")
|
||||
})
|
||||
|
||||
t.Run("OK Windows arm64", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
client := coderdtest.New(t, nil)
|
||||
script, err := client.InitScript(context.Background(), "windows", "arm64")
|
||||
require.NoError(t, err)
|
||||
require.NotEmpty(t, script)
|
||||
require.Contains(t, script, "$env:CODER_AGENT_AUTH = \"token\"")
|
||||
require.Contains(t, script, "/bin/coder-windows-arm64.exe")
|
||||
})
|
||||
|
||||
t.Run("OK Linux amd64", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
client := coderdtest.New(t, nil)
|
||||
script, err := client.InitScript(context.Background(), "linux", "amd64")
|
||||
require.NoError(t, err)
|
||||
require.NotEmpty(t, script)
|
||||
require.Contains(t, script, "export CODER_AGENT_AUTH=\"token\"")
|
||||
require.Contains(t, script, "/bin/coder-linux-amd64")
|
||||
})
|
||||
|
||||
t.Run("OK Linux arm64", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
client := coderdtest.New(t, nil)
|
||||
script, err := client.InitScript(context.Background(), "linux", "arm64")
|
||||
require.NoError(t, err)
|
||||
require.NotEmpty(t, script)
|
||||
require.Contains(t, script, "export CODER_AGENT_AUTH=\"token\"")
|
||||
require.Contains(t, script, "/bin/coder-linux-arm64")
|
||||
})
|
||||
|
||||
t.Run("BadRequest", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
client := coderdtest.New(t, nil)
|
||||
_, err := client.InitScript(context.Background(), "darwin", "armv7")
|
||||
require.Error(t, err)
|
||||
var apiErr *codersdk.Error
|
||||
require.ErrorAs(t, err, &apiErr)
|
||||
require.Equal(t, http.StatusBadRequest, apiErr.StatusCode())
|
||||
require.Equal(t, "Unknown os/arch: darwin/armv7", apiErr.Message)
|
||||
})
|
||||
}
|
||||
@@ -1733,11 +1733,14 @@ func (s *server) completeTemplateImportJob(ctx context.Context, job database.Pro
|
||||
Bool: jobType.TemplateImport.HasAiTasks,
|
||||
Valid: true,
|
||||
},
|
||||
HasExternalAgent: sql.NullBool{},
|
||||
UpdatedAt: now,
|
||||
HasExternalAgent: sql.NullBool{
|
||||
Bool: jobType.TemplateImport.HasExternalAgents,
|
||||
Valid: true,
|
||||
},
|
||||
UpdatedAt: now,
|
||||
})
|
||||
if err != nil {
|
||||
return xerrors.Errorf("update template version external auth providers: %w", err)
|
||||
return xerrors.Errorf("update template version ai task and external agent: %w", err)
|
||||
}
|
||||
|
||||
// Process terraform values
|
||||
@@ -2027,6 +2030,14 @@ func (s *server) completeWorkspaceBuildJob(ctx context.Context, job database.Pro
|
||||
sidebarAppID = uuid.NullUUID{}
|
||||
}
|
||||
|
||||
hasExternalAgent := false
|
||||
for _, resource := range jobType.WorkspaceBuild.Resources {
|
||||
if resource.Type == "coder_external_agent" {
|
||||
hasExternalAgent = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Regardless of whether there is an AI task or not, update the field to indicate one way or the other since it
|
||||
// always defaults to nil. ONLY if has_ai_task=true MUST ai_task_sidebar_app_id be set.
|
||||
if err := db.UpdateWorkspaceBuildFlagsByID(ctx, database.UpdateWorkspaceBuildFlagsByIDParams{
|
||||
@@ -2035,11 +2046,14 @@ func (s *server) completeWorkspaceBuildJob(ctx context.Context, job database.Pro
|
||||
Bool: hasAITask,
|
||||
Valid: true,
|
||||
},
|
||||
HasExternalAgent: sql.NullBool{},
|
||||
SidebarAppID: sidebarAppID,
|
||||
UpdatedAt: now,
|
||||
HasExternalAgent: sql.NullBool{
|
||||
Bool: hasExternalAgent,
|
||||
Valid: true,
|
||||
},
|
||||
SidebarAppID: sidebarAppID,
|
||||
UpdatedAt: now,
|
||||
}); err != nil {
|
||||
return xerrors.Errorf("update workspace build ai tasks flag: %w", err)
|
||||
return xerrors.Errorf("update workspace build ai tasks and external agent flag: %w", err)
|
||||
}
|
||||
|
||||
// Insert timings inside the transaction now
|
||||
|
||||
@@ -223,6 +223,7 @@ func Workspaces(ctx context.Context, db database.Store, query string, page coder
|
||||
Valid: values.Has("outdated"),
|
||||
}
|
||||
filter.HasAITask = parser.NullableBoolean(values, sql.NullBool{}, "has-ai-task")
|
||||
filter.HasExternalAgent = parser.NullableBoolean(values, sql.NullBool{}, "has_external_agent")
|
||||
filter.OrganizationID = parseOrganization(ctx, db, parser, values, "organization")
|
||||
|
||||
type paramMatch struct {
|
||||
@@ -277,15 +278,16 @@ func Templates(ctx context.Context, db database.Store, actorID uuid.UUID, query
|
||||
|
||||
parser := httpapi.NewQueryParamParser()
|
||||
filter := database.GetTemplatesWithFilterParams{
|
||||
Deleted: parser.Boolean(values, false, "deleted"),
|
||||
OrganizationID: parseOrganization(ctx, db, parser, values, "organization"),
|
||||
ExactName: parser.String(values, "", "exact_name"),
|
||||
FuzzyName: parser.String(values, "", "name"),
|
||||
IDs: parser.UUIDs(values, []uuid.UUID{}, "ids"),
|
||||
Deprecated: parser.NullableBoolean(values, sql.NullBool{}, "deprecated"),
|
||||
HasAITask: parser.NullableBoolean(values, sql.NullBool{}, "has-ai-task"),
|
||||
AuthorID: parser.UUID(values, uuid.Nil, "author_id"),
|
||||
AuthorUsername: parser.String(values, "", "author"),
|
||||
Deleted: parser.Boolean(values, false, "deleted"),
|
||||
OrganizationID: parseOrganization(ctx, db, parser, values, "organization"),
|
||||
ExactName: parser.String(values, "", "exact_name"),
|
||||
FuzzyName: parser.String(values, "", "name"),
|
||||
IDs: parser.UUIDs(values, []uuid.UUID{}, "ids"),
|
||||
Deprecated: parser.NullableBoolean(values, sql.NullBool{}, "deprecated"),
|
||||
HasAITask: parser.NullableBoolean(values, sql.NullBool{}, "has-ai-task"),
|
||||
AuthorID: parser.UUID(values, uuid.Nil, "author_id"),
|
||||
AuthorUsername: parser.String(values, "", "author"),
|
||||
HasExternalAgent: parser.NullableBoolean(values, sql.NullBool{}, "has_external_agent"),
|
||||
}
|
||||
|
||||
if filter.AuthorUsername == codersdk.Me {
|
||||
|
||||
@@ -252,6 +252,36 @@ func TestSearchWorkspace(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "HasExternalAgentTrue",
|
||||
Query: "has_external_agent:true",
|
||||
Expected: database.GetWorkspacesParams{
|
||||
HasExternalAgent: sql.NullBool{
|
||||
Bool: true,
|
||||
Valid: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "HasExternalAgentFalse",
|
||||
Query: "has_external_agent:false",
|
||||
Expected: database.GetWorkspacesParams{
|
||||
HasExternalAgent: sql.NullBool{
|
||||
Bool: false,
|
||||
Valid: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "HasExternalAgentMissing",
|
||||
Query: "",
|
||||
Expected: database.GetWorkspacesParams{
|
||||
HasExternalAgent: sql.NullBool{
|
||||
Bool: false,
|
||||
Valid: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
// Failures
|
||||
{
|
||||
@@ -689,6 +719,36 @@ func TestSearchTemplates(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "HasExternalAgent",
|
||||
Query: "has_external_agent:true",
|
||||
Expected: database.GetTemplatesWithFilterParams{
|
||||
HasExternalAgent: sql.NullBool{
|
||||
Bool: true,
|
||||
Valid: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "HasExternalAgentFalse",
|
||||
Query: "has_external_agent:false",
|
||||
Expected: database.GetTemplatesWithFilterParams{
|
||||
HasExternalAgent: sql.NullBool{
|
||||
Bool: false,
|
||||
Valid: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "HasExternalAgentMissing",
|
||||
Query: "",
|
||||
Expected: database.GetTemplatesWithFilterParams{
|
||||
HasExternalAgent: sql.NullBool{
|
||||
Bool: false,
|
||||
Valid: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "MyTemplates",
|
||||
Query: "author:me",
|
||||
|
||||
@@ -2015,3 +2015,59 @@ func TestTemplateFilterHasAITask(t *testing.T) {
|
||||
require.Contains(t, templates, templateWithAITask)
|
||||
require.Contains(t, templates, templateWithoutAITask)
|
||||
}
|
||||
|
||||
func TestTemplateFilterHasExternalAgent(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
db, pubsub := dbtestutil.NewDB(t)
|
||||
client := coderdtest.New(t, &coderdtest.Options{
|
||||
Database: db,
|
||||
Pubsub: pubsub,
|
||||
IncludeProvisionerDaemon: true,
|
||||
})
|
||||
user := coderdtest.CreateFirstUser(t, client)
|
||||
|
||||
jobWithExternalAgent := dbgen.ProvisionerJob(t, db, pubsub, database.ProvisionerJob{
|
||||
OrganizationID: user.OrganizationID,
|
||||
InitiatorID: user.UserID,
|
||||
Tags: database.StringMap{},
|
||||
Type: database.ProvisionerJobTypeTemplateVersionImport,
|
||||
})
|
||||
jobWithoutExternalAgent := dbgen.ProvisionerJob(t, db, pubsub, database.ProvisionerJob{
|
||||
OrganizationID: user.OrganizationID,
|
||||
InitiatorID: user.UserID,
|
||||
Tags: database.StringMap{},
|
||||
Type: database.ProvisionerJobTypeTemplateVersionImport,
|
||||
})
|
||||
versionWithExternalAgent := dbgen.TemplateVersion(t, db, database.TemplateVersion{
|
||||
OrganizationID: user.OrganizationID,
|
||||
CreatedBy: user.UserID,
|
||||
HasExternalAgent: sql.NullBool{Bool: true, Valid: true},
|
||||
JobID: jobWithExternalAgent.ID,
|
||||
})
|
||||
versionWithoutExternalAgent := dbgen.TemplateVersion(t, db, database.TemplateVersion{
|
||||
OrganizationID: user.OrganizationID,
|
||||
CreatedBy: user.UserID,
|
||||
HasExternalAgent: sql.NullBool{Bool: false, Valid: true},
|
||||
JobID: jobWithoutExternalAgent.ID,
|
||||
})
|
||||
templateWithExternalAgent := coderdtest.CreateTemplate(t, client, user.OrganizationID, versionWithExternalAgent.ID)
|
||||
templateWithoutExternalAgent := coderdtest.CreateTemplate(t, client, user.OrganizationID, versionWithoutExternalAgent.ID)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
||||
defer cancel()
|
||||
|
||||
templates, err := client.Templates(ctx, codersdk.TemplateFilter{
|
||||
SearchQuery: "has_external_agent:true",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, templates, 1)
|
||||
require.Equal(t, templateWithExternalAgent.ID, templates[0].ID)
|
||||
|
||||
templates, err = client.Templates(ctx, codersdk.TemplateFilter{
|
||||
SearchQuery: "has_external_agent:false",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, templates, 1)
|
||||
require.Equal(t, templateWithoutExternalAgent.ID, templates[0].ID)
|
||||
}
|
||||
|
||||
@@ -1963,6 +1963,7 @@ func convertTemplateVersion(version database.TemplateVersion, job codersdk.Provi
|
||||
Archived: version.Archived,
|
||||
Warnings: warnings,
|
||||
MatchedProvisioners: matchedProvisioners,
|
||||
HasExternalAgent: version.HasExternalAgent.Bool,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2221,3 +2221,36 @@ func TestTemplateArchiveVersions(t *testing.T) {
|
||||
require.NoError(t, err, "fetch all versions")
|
||||
require.Len(t, remaining, totalVersions-len(expArchived)-len(allFailed)+1, "remaining versions")
|
||||
}
|
||||
|
||||
func TestTemplateVersionHasExternalAgent(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
|
||||
user := coderdtest.CreateFirstUser(t, client)
|
||||
|
||||
ctx := testutil.Context(t, testutil.WaitMedium)
|
||||
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
|
||||
Parse: echo.ParseComplete,
|
||||
ProvisionPlan: []*proto.Response{
|
||||
{
|
||||
Type: &proto.Response_Plan{
|
||||
Plan: &proto.PlanComplete{
|
||||
Resources: []*proto.Resource{
|
||||
{
|
||||
Name: "example",
|
||||
Type: "coder_external_agent",
|
||||
},
|
||||
},
|
||||
HasExternalAgents: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
ProvisionApply: echo.ApplyComplete,
|
||||
})
|
||||
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
|
||||
|
||||
version, err := client.TemplateVersion(ctx, version.ID)
|
||||
require.NoError(t, err)
|
||||
require.True(t, version.HasExternalAgent)
|
||||
}
|
||||
|
||||
@@ -1157,6 +1157,11 @@ func (api *API) convertWorkspaceBuild(
|
||||
aiTasksSidebarAppID = &build.AITaskSidebarAppID.UUID
|
||||
}
|
||||
|
||||
var hasExternalAgent *bool
|
||||
if build.HasExternalAgent.Valid {
|
||||
hasExternalAgent = &build.HasExternalAgent.Bool
|
||||
}
|
||||
|
||||
apiJob := convertProvisionerJob(job)
|
||||
transition := codersdk.WorkspaceTransition(build.Transition)
|
||||
return codersdk.WorkspaceBuild{
|
||||
@@ -1185,6 +1190,7 @@ func (api *API) convertWorkspaceBuild(
|
||||
TemplateVersionPresetID: presetID,
|
||||
HasAITask: hasAITask,
|
||||
AITaskSidebarAppID: aiTasksSidebarAppID,
|
||||
HasExternalAgent: hasExternalAgent,
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -138,7 +138,7 @@ func (api *API) workspace(rw http.ResponseWriter, r *http.Request) {
|
||||
// @Security CoderSessionToken
|
||||
// @Produce json
|
||||
// @Tags Workspaces
|
||||
// @Param q query string false "Search query in the format `key:value`. Available keys are: owner, template, name, status, has-agent, dormant, last_used_after, last_used_before, has-ai-task."
|
||||
// @Param q query string false "Search query in the format `key:value`. Available keys are: owner, template, name, status, has-agent, dormant, last_used_after, last_used_before, has-ai-task, has_external_agent."
|
||||
// @Param limit query int false "Page limit"
|
||||
// @Param offset query int false "Page offset"
|
||||
// @Success 200 {object} codersdk.WorkspacesResponse
|
||||
|
||||
Reference in New Issue
Block a user