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:
+2
-1
@@ -70,7 +70,8 @@
|
||||
"most_recently_seen": null
|
||||
},
|
||||
"template_version_preset_id": null,
|
||||
"has_ai_task": false
|
||||
"has_ai_task": false,
|
||||
"has_external_agent": false
|
||||
},
|
||||
"latest_app_status": null,
|
||||
"outdated": false,
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
"last_seen_at": "====[timestamp]=====",
|
||||
"name": "test-daemon",
|
||||
"version": "v0.0.0-devel",
|
||||
"api_version": "1.8",
|
||||
"api_version": "1.9",
|
||||
"provisioners": [
|
||||
"echo"
|
||||
],
|
||||
|
||||
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
|
||||
|
||||
@@ -88,7 +88,8 @@ const (
|
||||
// ManagedAgentLimit is a usage period feature, so the value in the license
|
||||
// contains both a soft and hard limit. Refer to
|
||||
// enterprise/coderd/license/license.go for the license format.
|
||||
FeatureManagedAgentLimit FeatureName = "managed_agent_limit"
|
||||
FeatureManagedAgentLimit FeatureName = "managed_agent_limit"
|
||||
FeatureWorkspaceExternalAgent FeatureName = "workspace_external_agent"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -115,6 +116,7 @@ var (
|
||||
FeatureMultipleOrganizations,
|
||||
FeatureWorkspacePrebuilds,
|
||||
FeatureManagedAgentLimit,
|
||||
FeatureWorkspaceExternalAgent,
|
||||
}
|
||||
|
||||
// FeatureNamesMap is a map of all feature names for quick lookups.
|
||||
@@ -155,6 +157,7 @@ func (n FeatureName) AlwaysEnable() bool {
|
||||
FeatureCustomRoles: true,
|
||||
FeatureMultipleOrganizations: true,
|
||||
FeatureWorkspacePrebuilds: true,
|
||||
FeatureWorkspaceExternalAgent: true,
|
||||
}[n]
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
package codersdk
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func (c *Client) InitScript(ctx context.Context, os, arch string) (string, error) {
|
||||
url := fmt.Sprintf("/api/v2/init-script/%s/%s", os, arch)
|
||||
res, err := c.Request(ctx, http.MethodGet, url, nil)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
|
||||
if res.StatusCode != http.StatusOK {
|
||||
return "", ReadBodyAsError(res)
|
||||
}
|
||||
|
||||
script, err := io.ReadAll(res.Body)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return string(script), nil
|
||||
}
|
||||
@@ -33,6 +33,8 @@ type TemplateVersion struct {
|
||||
|
||||
Warnings []TemplateVersionWarning `json:"warnings,omitempty" enums:"DEPRECATED_PARAMETERS"`
|
||||
MatchedProvisioners *MatchedProvisioners `json:"matched_provisioners,omitempty"`
|
||||
|
||||
HasExternalAgent bool `json:"has_external_agent"`
|
||||
}
|
||||
|
||||
type TemplateVersionExternalAuth struct {
|
||||
|
||||
@@ -90,6 +90,7 @@ type WorkspaceBuild struct {
|
||||
TemplateVersionPresetID *uuid.UUID `json:"template_version_preset_id" format:"uuid"`
|
||||
HasAITask *bool `json:"has_ai_task,omitempty"`
|
||||
AITaskSidebarAppID *uuid.UUID `json:"ai_task_sidebar_app_id,omitempty" format:"uuid"`
|
||||
HasExternalAgent *bool `json:"has_external_agent,omitempty"`
|
||||
}
|
||||
|
||||
// WorkspaceResource describes resources used to create a workspace, for instance:
|
||||
|
||||
@@ -689,3 +689,23 @@ func (c *Client) UpdateWorkspaceACL(ctx context.Context, workspaceID uuid.UUID,
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ExternalAgentCredentials contains the credentials needed for an external agent to connect to Coder.
|
||||
type ExternalAgentCredentials struct {
|
||||
Command string `json:"command"`
|
||||
AgentToken string `json:"agent_token"`
|
||||
}
|
||||
|
||||
func (c *Client) WorkspaceExternalAgentCredentials(ctx context.Context, workspaceID uuid.UUID, agentName string) (ExternalAgentCredentials, error) {
|
||||
path := fmt.Sprintf("/api/v2/workspaces/%s/external-agent/%s/credentials", workspaceID.String(), agentName)
|
||||
res, err := c.Request(ctx, http.MethodGet, path, nil)
|
||||
if err != nil {
|
||||
return ExternalAgentCredentials{}, err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
if res.StatusCode != http.StatusOK {
|
||||
return ExternalAgentCredentials{}, ReadBodyAsError(res)
|
||||
}
|
||||
var credentials ExternalAgentCredentials
|
||||
return credentials, json.NewDecoder(res.Body).Decode(&credentials)
|
||||
}
|
||||
|
||||
+2
-14
@@ -47,18 +47,6 @@
|
||||
"path": "./about/contributing/documentation.md",
|
||||
"icon_path": "./images/icons/document.svg"
|
||||
},
|
||||
{
|
||||
"title": "Modules",
|
||||
"description": "Learn how to contribute modules to Coder",
|
||||
"path": "./about/contributing/modules.md",
|
||||
"icon_path": "./images/icons/gear.svg"
|
||||
},
|
||||
{
|
||||
"title": "Templates",
|
||||
"description": "Learn how to contribute templates to Coder",
|
||||
"path": "./about/contributing/templates.md",
|
||||
"icon_path": "./images/icons/picture.svg"
|
||||
},
|
||||
{
|
||||
"title": "Backend",
|
||||
"description": "Our guide for backend development",
|
||||
@@ -714,8 +702,8 @@
|
||||
"path": "./admin/integrations/platformx.md"
|
||||
},
|
||||
{
|
||||
"title": "DX",
|
||||
"description": "Tag Coder Users with DX",
|
||||
"title": "DX Data Cloud",
|
||||
"description": "Tag Coder Users with DX Data Cloud",
|
||||
"path": "./admin/integrations/dx-data-cloud.md"
|
||||
},
|
||||
{
|
||||
|
||||
Generated
+6
@@ -33,6 +33,7 @@ curl -X GET http://coder-server:8080/api/v2/users/{user}/workspace/{workspacenam
|
||||
"daily_cost": 0,
|
||||
"deadline": "2019-08-24T14:15:22Z",
|
||||
"has_ai_task": true,
|
||||
"has_external_agent": true,
|
||||
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
|
||||
"initiator_id": "06588898-9a84-4b35-ba8f-f9cbd64946f3",
|
||||
"initiator_name": "string",
|
||||
@@ -271,6 +272,7 @@ curl -X GET http://coder-server:8080/api/v2/workspacebuilds/{workspacebuild} \
|
||||
"daily_cost": 0,
|
||||
"deadline": "2019-08-24T14:15:22Z",
|
||||
"has_ai_task": true,
|
||||
"has_external_agent": true,
|
||||
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
|
||||
"initiator_id": "06588898-9a84-4b35-ba8f-f9cbd64946f3",
|
||||
"initiator_name": "string",
|
||||
@@ -998,6 +1000,7 @@ curl -X GET http://coder-server:8080/api/v2/workspacebuilds/{workspacebuild}/sta
|
||||
"daily_cost": 0,
|
||||
"deadline": "2019-08-24T14:15:22Z",
|
||||
"has_ai_task": true,
|
||||
"has_external_agent": true,
|
||||
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
|
||||
"initiator_id": "06588898-9a84-4b35-ba8f-f9cbd64946f3",
|
||||
"initiator_name": "string",
|
||||
@@ -1309,6 +1312,7 @@ curl -X GET http://coder-server:8080/api/v2/workspaces/{workspace}/builds \
|
||||
"daily_cost": 0,
|
||||
"deadline": "2019-08-24T14:15:22Z",
|
||||
"has_ai_task": true,
|
||||
"has_external_agent": true,
|
||||
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
|
||||
"initiator_id": "06588898-9a84-4b35-ba8f-f9cbd64946f3",
|
||||
"initiator_name": "string",
|
||||
@@ -1528,6 +1532,7 @@ Status Code **200**
|
||||
| `» daily_cost` | integer | false | | |
|
||||
| `» deadline` | string(date-time) | false | | |
|
||||
| `» has_ai_task` | boolean | false | | |
|
||||
| `» has_external_agent` | boolean | false | | |
|
||||
| `» id` | string(uuid) | false | | |
|
||||
| `» initiator_id` | string(uuid) | false | | |
|
||||
| `» initiator_name` | string | false | | |
|
||||
@@ -1802,6 +1807,7 @@ curl -X POST http://coder-server:8080/api/v2/workspaces/{workspace}/builds \
|
||||
"daily_cost": 0,
|
||||
"deadline": "2019-08-24T14:15:22Z",
|
||||
"has_ai_task": true,
|
||||
"has_external_agent": true,
|
||||
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
|
||||
"initiator_id": "06588898-9a84-4b35-ba8f-f9cbd64946f3",
|
||||
"initiator_name": "string",
|
||||
|
||||
Generated
+39
@@ -4254,3 +4254,42 @@ curl -X PATCH http://coder-server:8080/api/v2/workspaceproxies/{workspaceproxy}
|
||||
| 200 | [OK](https://tools.ietf.org/html/rfc7231#section-6.3.1) | OK | [codersdk.WorkspaceProxy](schemas.md#codersdkworkspaceproxy) |
|
||||
|
||||
To perform this operation, you must be authenticated. [Learn more](authentication.md).
|
||||
|
||||
## Get workspace external agent credentials
|
||||
|
||||
### Code samples
|
||||
|
||||
```shell
|
||||
# Example request using curl
|
||||
curl -X GET http://coder-server:8080/api/v2/workspaces/{workspace}/external-agent/{agent}/credentials \
|
||||
-H 'Accept: application/json' \
|
||||
-H 'Coder-Session-Token: API_KEY'
|
||||
```
|
||||
|
||||
`GET /workspaces/{workspace}/external-agent/{agent}/credentials`
|
||||
|
||||
### Parameters
|
||||
|
||||
| Name | In | Type | Required | Description |
|
||||
|-------------|------|--------------|----------|--------------|
|
||||
| `workspace` | path | string(uuid) | true | Workspace ID |
|
||||
| `agent` | path | string | true | Agent name |
|
||||
|
||||
### Example responses
|
||||
|
||||
> 200 Response
|
||||
|
||||
```json
|
||||
{
|
||||
"agent_token": "string",
|
||||
"command": "string"
|
||||
}
|
||||
```
|
||||
|
||||
### Responses
|
||||
|
||||
| Status | Meaning | Description | Schema |
|
||||
|--------|---------------------------------------------------------|-------------|----------------------------------------------------------------------------------|
|
||||
| 200 | [OK](https://tools.ietf.org/html/rfc7231#section-6.3.1) | OK | [codersdk.ExternalAgentCredentials](schemas.md#codersdkexternalagentcredentials) |
|
||||
|
||||
To perform this operation, you must be authenticated. [Learn more](authentication.md).
|
||||
|
||||
Generated
+26
@@ -0,0 +1,26 @@
|
||||
# InitScript
|
||||
|
||||
## Get agent init script
|
||||
|
||||
### Code samples
|
||||
|
||||
```shell
|
||||
# Example request using curl
|
||||
curl -X GET http://coder-server:8080/api/v2/init-script/{os}/{arch}
|
||||
|
||||
```
|
||||
|
||||
`GET /init-script/{os}/{arch}`
|
||||
|
||||
### Parameters
|
||||
|
||||
| Name | In | Type | Required | Description |
|
||||
|--------|------|--------|----------|------------------|
|
||||
| `os` | path | string | true | Operating system |
|
||||
| `arch` | path | string | true | Architecture |
|
||||
|
||||
### Responses
|
||||
|
||||
| Status | Meaning | Description | Schema |
|
||||
|--------|---------------------------------------------------------|-------------|--------|
|
||||
| 200 | [OK](https://tools.ietf.org/html/rfc7231#section-6.3.1) | Success | |
|
||||
Generated
+22
@@ -3322,6 +3322,22 @@ CreateWorkspaceRequest provides options for creating a new workspace. Only one o
|
||||
| `mcp-server-http` |
|
||||
| `workspace-sharing` |
|
||||
|
||||
## codersdk.ExternalAgentCredentials
|
||||
|
||||
```json
|
||||
{
|
||||
"agent_token": "string",
|
||||
"command": "string"
|
||||
}
|
||||
```
|
||||
|
||||
### Properties
|
||||
|
||||
| Name | Type | Required | Restrictions | Description |
|
||||
|---------------|--------|----------|--------------|-------------|
|
||||
| `agent_token` | string | false | | |
|
||||
| `command` | string | false | | |
|
||||
|
||||
## codersdk.ExternalAuth
|
||||
|
||||
```json
|
||||
@@ -7614,6 +7630,7 @@ Restarts will only happen on weekdays in this list on weeks which line up with W
|
||||
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
|
||||
"username": "string"
|
||||
},
|
||||
"has_external_agent": true,
|
||||
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
|
||||
"job": {
|
||||
"available_workers": [
|
||||
@@ -7678,6 +7695,7 @@ Restarts will only happen on weekdays in this list on weeks which line up with W
|
||||
| `archived` | boolean | false | | |
|
||||
| `created_at` | string | false | | |
|
||||
| `created_by` | [codersdk.MinimalUser](#codersdkminimaluser) | false | | |
|
||||
| `has_external_agent` | boolean | false | | |
|
||||
| `id` | string | false | | |
|
||||
| `job` | [codersdk.ProvisionerJob](#codersdkprovisionerjob) | false | | |
|
||||
| `matched_provisioners` | [codersdk.MatchedProvisioners](#codersdkmatchedprovisioners) | false | | |
|
||||
@@ -8813,6 +8831,7 @@ If the schedule is empty, the user will be updated to use the default schedule.|
|
||||
"daily_cost": 0,
|
||||
"deadline": "2019-08-24T14:15:22Z",
|
||||
"has_ai_task": true,
|
||||
"has_external_agent": true,
|
||||
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
|
||||
"initiator_id": "06588898-9a84-4b35-ba8f-f9cbd64946f3",
|
||||
"initiator_name": "string",
|
||||
@@ -9923,6 +9942,7 @@ If the schedule is empty, the user will be updated to use the default schedule.|
|
||||
"daily_cost": 0,
|
||||
"deadline": "2019-08-24T14:15:22Z",
|
||||
"has_ai_task": true,
|
||||
"has_external_agent": true,
|
||||
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
|
||||
"initiator_id": "06588898-9a84-4b35-ba8f-f9cbd64946f3",
|
||||
"initiator_name": "string",
|
||||
@@ -10132,6 +10152,7 @@ If the schedule is empty, the user will be updated to use the default schedule.|
|
||||
| `daily_cost` | integer | false | | |
|
||||
| `deadline` | string | false | | |
|
||||
| `has_ai_task` | boolean | false | | |
|
||||
| `has_external_agent` | boolean | false | | |
|
||||
| `id` | string | false | | |
|
||||
| `initiator_id` | string | false | | |
|
||||
| `initiator_name` | string | false | | |
|
||||
@@ -10671,6 +10692,7 @@ If the schedule is empty, the user will be updated to use the default schedule.|
|
||||
"daily_cost": 0,
|
||||
"deadline": "2019-08-24T14:15:22Z",
|
||||
"has_ai_task": true,
|
||||
"has_external_agent": true,
|
||||
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
|
||||
"initiator_id": "06588898-9a84-4b35-ba8f-f9cbd64946f3",
|
||||
"initiator_name": "string",
|
||||
|
||||
Generated
+9
@@ -462,6 +462,7 @@ curl -X GET http://coder-server:8080/api/v2/organizations/{organization}/templat
|
||||
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
|
||||
"username": "string"
|
||||
},
|
||||
"has_external_agent": true,
|
||||
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
|
||||
"job": {
|
||||
"available_workers": [
|
||||
@@ -561,6 +562,7 @@ curl -X GET http://coder-server:8080/api/v2/organizations/{organization}/templat
|
||||
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
|
||||
"username": "string"
|
||||
},
|
||||
"has_external_agent": true,
|
||||
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
|
||||
"job": {
|
||||
"available_workers": [
|
||||
@@ -684,6 +686,7 @@ curl -X POST http://coder-server:8080/api/v2/organizations/{organization}/templa
|
||||
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
|
||||
"username": "string"
|
||||
},
|
||||
"has_external_agent": true,
|
||||
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
|
||||
"job": {
|
||||
"available_workers": [
|
||||
@@ -1250,6 +1253,7 @@ curl -X GET http://coder-server:8080/api/v2/templates/{template}/versions \
|
||||
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
|
||||
"username": "string"
|
||||
},
|
||||
"has_external_agent": true,
|
||||
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
|
||||
"job": {
|
||||
"available_workers": [
|
||||
@@ -1327,6 +1331,7 @@ Status Code **200**
|
||||
| `»» avatar_url` | string(uri) | false | | |
|
||||
| `»» id` | string(uuid) | true | | |
|
||||
| `»» username` | string | true | | |
|
||||
| `» has_external_agent` | boolean | false | | |
|
||||
| `» id` | string(uuid) | false | | |
|
||||
| `» job` | [codersdk.ProvisionerJob](schemas.md#codersdkprovisionerjob) | false | | |
|
||||
| `»» available_workers` | array | false | | |
|
||||
@@ -1531,6 +1536,7 @@ curl -X GET http://coder-server:8080/api/v2/templates/{template}/versions/{templ
|
||||
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
|
||||
"username": "string"
|
||||
},
|
||||
"has_external_agent": true,
|
||||
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
|
||||
"job": {
|
||||
"available_workers": [
|
||||
@@ -1608,6 +1614,7 @@ Status Code **200**
|
||||
| `»» avatar_url` | string(uri) | false | | |
|
||||
| `»» id` | string(uuid) | true | | |
|
||||
| `»» username` | string | true | | |
|
||||
| `» has_external_agent` | boolean | false | | |
|
||||
| `» id` | string(uuid) | false | | |
|
||||
| `» job` | [codersdk.ProvisionerJob](schemas.md#codersdkprovisionerjob) | false | | |
|
||||
| `»» available_workers` | array | false | | |
|
||||
@@ -1702,6 +1709,7 @@ curl -X GET http://coder-server:8080/api/v2/templateversions/{templateversion} \
|
||||
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
|
||||
"username": "string"
|
||||
},
|
||||
"has_external_agent": true,
|
||||
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
|
||||
"job": {
|
||||
"available_workers": [
|
||||
@@ -1810,6 +1818,7 @@ curl -X PATCH http://coder-server:8080/api/v2/templateversions/{templateversion}
|
||||
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
|
||||
"username": "string"
|
||||
},
|
||||
"has_external_agent": true,
|
||||
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
|
||||
"job": {
|
||||
"available_workers": [
|
||||
|
||||
Generated
+11
-5
@@ -88,6 +88,7 @@ of the template will be used.
|
||||
"daily_cost": 0,
|
||||
"deadline": "2019-08-24T14:15:22Z",
|
||||
"has_ai_task": true,
|
||||
"has_external_agent": true,
|
||||
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
|
||||
"initiator_id": "06588898-9a84-4b35-ba8f-f9cbd64946f3",
|
||||
"initiator_name": "string",
|
||||
@@ -376,6 +377,7 @@ curl -X GET http://coder-server:8080/api/v2/users/{user}/workspace/{workspacenam
|
||||
"daily_cost": 0,
|
||||
"deadline": "2019-08-24T14:15:22Z",
|
||||
"has_ai_task": true,
|
||||
"has_external_agent": true,
|
||||
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
|
||||
"initiator_id": "06588898-9a84-4b35-ba8f-f9cbd64946f3",
|
||||
"initiator_name": "string",
|
||||
@@ -689,6 +691,7 @@ of the template will be used.
|
||||
"daily_cost": 0,
|
||||
"deadline": "2019-08-24T14:15:22Z",
|
||||
"has_ai_task": true,
|
||||
"has_external_agent": true,
|
||||
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
|
||||
"initiator_id": "06588898-9a84-4b35-ba8f-f9cbd64946f3",
|
||||
"initiator_name": "string",
|
||||
@@ -930,11 +933,11 @@ curl -X GET http://coder-server:8080/api/v2/workspaces \
|
||||
|
||||
### Parameters
|
||||
|
||||
| Name | In | Type | Required | Description |
|
||||
|----------|-------|---------|----------|----------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| `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. |
|
||||
| `limit` | query | integer | false | Page limit |
|
||||
| `offset` | query | integer | false | Page offset |
|
||||
| Name | In | Type | Required | Description |
|
||||
|----------|-------|---------|----------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| `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. |
|
||||
| `limit` | query | integer | false | Page limit |
|
||||
| `offset` | query | integer | false | Page offset |
|
||||
|
||||
### Example responses
|
||||
|
||||
@@ -980,6 +983,7 @@ curl -X GET http://coder-server:8080/api/v2/workspaces \
|
||||
"daily_cost": 0,
|
||||
"deadline": "2019-08-24T14:15:22Z",
|
||||
"has_ai_task": true,
|
||||
"has_external_agent": true,
|
||||
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
|
||||
"initiator_id": "06588898-9a84-4b35-ba8f-f9cbd64946f3",
|
||||
"initiator_name": "string",
|
||||
@@ -1252,6 +1256,7 @@ curl -X GET http://coder-server:8080/api/v2/workspaces/{workspace} \
|
||||
"daily_cost": 0,
|
||||
"deadline": "2019-08-24T14:15:22Z",
|
||||
"has_ai_task": true,
|
||||
"has_external_agent": true,
|
||||
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
|
||||
"initiator_id": "06588898-9a84-4b35-ba8f-f9cbd64946f3",
|
||||
"initiator_name": "string",
|
||||
@@ -1699,6 +1704,7 @@ curl -X PUT http://coder-server:8080/api/v2/workspaces/{workspace}/dormant \
|
||||
"daily_cost": 0,
|
||||
"deadline": "2019-08-24T14:15:22Z",
|
||||
"has_ai_task": true,
|
||||
"has_external_agent": true,
|
||||
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
|
||||
"initiator_id": "06588898-9a84-4b35-ba8f-f9cbd64946f3",
|
||||
"initiator_name": "string",
|
||||
|
||||
+49
-37
@@ -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{
|
||||
|
||||
@@ -3,6 +3,7 @@ package license
|
||||
import (
|
||||
"context"
|
||||
"crypto/ed25519"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"math"
|
||||
"time"
|
||||
@@ -94,10 +95,34 @@ func Entitlements(
|
||||
return codersdk.Entitlements{}, xerrors.Errorf("query active user count: %w", err)
|
||||
}
|
||||
|
||||
// nolint:gocritic // Getting external workspaces is a system function.
|
||||
externalWorkspaces, err := db.GetWorkspaces(dbauthz.AsSystemRestricted(ctx), database.GetWorkspacesParams{
|
||||
HasExternalAgent: sql.NullBool{
|
||||
Bool: true,
|
||||
Valid: true,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return codersdk.Entitlements{}, xerrors.Errorf("query external workspaces: %w", err)
|
||||
}
|
||||
|
||||
// nolint:gocritic // Getting external templates is a system function.
|
||||
externalTemplates, err := db.GetTemplatesWithFilter(dbauthz.AsSystemRestricted(ctx), database.GetTemplatesWithFilterParams{
|
||||
HasExternalAgent: sql.NullBool{
|
||||
Bool: true,
|
||||
Valid: true,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return codersdk.Entitlements{}, xerrors.Errorf("query external templates: %w", err)
|
||||
}
|
||||
|
||||
entitlements, err := LicensesEntitlements(ctx, now, licenses, enablements, keys, FeatureArguments{
|
||||
ActiveUserCount: activeUserCount,
|
||||
ReplicaCount: replicaCount,
|
||||
ExternalAuthCount: externalAuthCount,
|
||||
ActiveUserCount: activeUserCount,
|
||||
ReplicaCount: replicaCount,
|
||||
ExternalAuthCount: externalAuthCount,
|
||||
ExternalWorkspaceCount: int64(len(externalWorkspaces)),
|
||||
ExternalTemplateCount: int64(len(externalTemplates)),
|
||||
ManagedAgentCountFn: func(ctx context.Context, startTime time.Time, endTime time.Time) (int64, error) {
|
||||
// nolint:gocritic // Requires permission to read all workspaces to read managed agent count.
|
||||
return db.GetManagedAgentCount(dbauthz.AsSystemRestricted(ctx), database.GetManagedAgentCountParams{
|
||||
@@ -114,9 +139,11 @@ func Entitlements(
|
||||
}
|
||||
|
||||
type FeatureArguments struct {
|
||||
ActiveUserCount int64
|
||||
ReplicaCount int
|
||||
ExternalAuthCount int
|
||||
ActiveUserCount int64
|
||||
ReplicaCount int
|
||||
ExternalAuthCount int
|
||||
ExternalWorkspaceCount int64
|
||||
ExternalTemplateCount int64
|
||||
// Unfortunately, managed agent count is not a simple count of the current
|
||||
// state of the world, but a count between two points in time determined by
|
||||
// the licenses.
|
||||
@@ -418,6 +445,30 @@ func LicensesEntitlements(
|
||||
}
|
||||
}
|
||||
|
||||
if featureArguments.ExternalWorkspaceCount > 0 {
|
||||
feature := entitlements.Features[codersdk.FeatureWorkspaceExternalAgent]
|
||||
switch feature.Entitlement {
|
||||
case codersdk.EntitlementNotEntitled:
|
||||
entitlements.Errors = append(entitlements.Errors,
|
||||
"You have external workspaces but your license is not entitled to this feature.")
|
||||
case codersdk.EntitlementGracePeriod:
|
||||
entitlements.Warnings = append(entitlements.Warnings,
|
||||
"You have external workspaces but your license is expired.")
|
||||
}
|
||||
}
|
||||
|
||||
if featureArguments.ExternalTemplateCount > 0 {
|
||||
feature := entitlements.Features[codersdk.FeatureWorkspaceExternalAgent]
|
||||
switch feature.Entitlement {
|
||||
case codersdk.EntitlementNotEntitled:
|
||||
entitlements.Errors = append(entitlements.Errors,
|
||||
"You have templates which use external agents but your license is not entitled to this feature.")
|
||||
case codersdk.EntitlementGracePeriod:
|
||||
entitlements.Warnings = append(entitlements.Warnings,
|
||||
"You have templates which use external agents but your license is expired.")
|
||||
}
|
||||
}
|
||||
|
||||
// Managed agent warnings are applied based on usage period. We only
|
||||
// generate a warning if the license actually has managed agents.
|
||||
// Note that agents are free when unlicensed.
|
||||
|
||||
@@ -723,6 +723,12 @@ func TestEntitlements(t *testing.T) {
|
||||
return true
|
||||
})).
|
||||
Return(int64(175), nil)
|
||||
mDB.EXPECT().
|
||||
GetWorkspaces(gomock.Any(), gomock.Any()).
|
||||
Return([]database.GetWorkspacesRow{}, nil)
|
||||
mDB.EXPECT().
|
||||
GetTemplatesWithFilter(gomock.Any(), gomock.Any()).
|
||||
Return([]database.Template{}, nil)
|
||||
|
||||
entitlements, err := license.Entitlements(context.Background(), mDB, 1, 0, coderdenttest.Keys, all)
|
||||
require.NoError(t, err)
|
||||
@@ -766,6 +772,7 @@ func TestLicenseEntitlements(t *testing.T) {
|
||||
codersdk.FeatureUserRoleManagement: true,
|
||||
codersdk.FeatureAccessControl: true,
|
||||
codersdk.FeatureControlSharedPorts: true,
|
||||
codersdk.FeatureWorkspaceExternalAgent: true,
|
||||
}
|
||||
|
||||
legacyLicense := func() *coderdenttest.LicenseOptions {
|
||||
@@ -1109,6 +1116,32 @@ func TestLicenseEntitlements(t *testing.T) {
|
||||
assert.Equal(t, int64(200), *feature.Actual)
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "ExternalWorkspace",
|
||||
Licenses: []*coderdenttest.LicenseOptions{
|
||||
enterpriseLicense().UserLimit(100),
|
||||
},
|
||||
Arguments: license.FeatureArguments{
|
||||
ExternalWorkspaceCount: 1,
|
||||
},
|
||||
AssertEntitlements: func(t *testing.T, entitlements codersdk.Entitlements) {
|
||||
assert.Equal(t, codersdk.EntitlementEntitled, entitlements.Features[codersdk.FeatureWorkspaceExternalAgent].Entitlement)
|
||||
assert.True(t, entitlements.Features[codersdk.FeatureWorkspaceExternalAgent].Enabled)
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "ExternalTemplate",
|
||||
Licenses: []*coderdenttest.LicenseOptions{
|
||||
enterpriseLicense().UserLimit(100),
|
||||
},
|
||||
Arguments: license.FeatureArguments{
|
||||
ExternalTemplateCount: 1,
|
||||
},
|
||||
AssertEntitlements: func(t *testing.T, entitlements codersdk.Entitlements) {
|
||||
assert.Equal(t, codersdk.EntitlementEntitled, entitlements.Features[codersdk.FeatureWorkspaceExternalAgent].Entitlement)
|
||||
assert.True(t, entitlements.Features[codersdk.FeatureWorkspaceExternalAgent].Enabled)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
|
||||
@@ -2,9 +2,14 @@ package coderd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
|
||||
"github.com/coder/coder/v2/coderd/database"
|
||||
"github.com/coder/coder/v2/coderd/httpapi"
|
||||
"github.com/coder/coder/v2/coderd/httpmw"
|
||||
"github.com/coder/coder/v2/codersdk"
|
||||
)
|
||||
|
||||
@@ -17,3 +22,77 @@ func (api *API) shouldBlockNonBrowserConnections(rw http.ResponseWriter) bool {
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// @Summary Get workspace external agent credentials
|
||||
// @ID get-workspace-external-agent-credentials
|
||||
// @Security CoderSessionToken
|
||||
// @Produce json
|
||||
// @Tags Enterprise
|
||||
// @Param workspace path string true "Workspace ID" format(uuid)
|
||||
// @Param agent path string true "Agent name"
|
||||
// @Success 200 {object} codersdk.ExternalAgentCredentials
|
||||
// @Router /workspaces/{workspace}/external-agent/{agent}/credentials [get]
|
||||
func (api *API) workspaceExternalAgentCredentials(rw http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
workspace := httpmw.WorkspaceParam(r)
|
||||
agentName := chi.URLParam(r, "agent")
|
||||
|
||||
build, err := api.Database.GetLatestWorkspaceBuildByWorkspaceID(ctx, workspace.ID)
|
||||
if err != nil {
|
||||
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
|
||||
Message: "Failed to get latest workspace build.",
|
||||
Detail: err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
if !build.HasExternalAgent.Bool {
|
||||
httpapi.Write(ctx, rw, http.StatusNotFound, codersdk.Response{
|
||||
Message: "Workspace does not have an external agent.",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
agents, err := api.Database.GetWorkspaceAgentsByWorkspaceAndBuildNumber(ctx, database.GetWorkspaceAgentsByWorkspaceAndBuildNumberParams{
|
||||
WorkspaceID: workspace.ID,
|
||||
BuildNumber: build.BuildNumber,
|
||||
})
|
||||
if err != nil {
|
||||
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
|
||||
Message: "Failed to get workspace agents.",
|
||||
Detail: err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
var agent *database.WorkspaceAgent
|
||||
for i := range agents {
|
||||
if agents[i].Name == agentName {
|
||||
agent = &agents[i]
|
||||
break
|
||||
}
|
||||
}
|
||||
if agent == nil {
|
||||
httpapi.Write(ctx, rw, http.StatusNotFound, codersdk.Response{
|
||||
Message: fmt.Sprintf("External agent '%s' not found in workspace.", agentName),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if agent.AuthInstanceID.Valid {
|
||||
httpapi.Write(ctx, rw, http.StatusNotFound, codersdk.Response{
|
||||
Message: "External agent is authenticated with an instance ID.",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
initScriptURL := fmt.Sprintf("%s/api/v2/init-script/%s/%s", api.AccessURL.String(), agent.OperatingSystem, agent.Architecture)
|
||||
command := fmt.Sprintf("CODER_AGENT_TOKEN=%q curl -fsSL %q | sh", agent.AuthToken.String(), initScriptURL)
|
||||
if agent.OperatingSystem == "windows" {
|
||||
command = fmt.Sprintf("$env:CODER_AGENT_TOKEN=%q; iwr -useb %q | iex", agent.AuthToken.String(), initScriptURL)
|
||||
}
|
||||
|
||||
httpapi.Write(ctx, rw, http.StatusOK, codersdk.ExternalAgentCredentials{
|
||||
AgentToken: agent.AuthToken.String(),
|
||||
Command: command,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package coderd_test
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
@@ -12,6 +13,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/coder/coder/v2/coderd/database"
|
||||
"github.com/coder/coder/v2/coderd/database/dbfake"
|
||||
"github.com/coder/coder/v2/coderd/database/dbgen"
|
||||
"github.com/coder/coder/v2/coderd/database/dbtestutil"
|
||||
"github.com/coder/coder/v2/provisionersdk"
|
||||
@@ -344,3 +346,123 @@ func setupWorkspaceAgent(t *testing.T, client *codersdk.Client, user codersdk.Cr
|
||||
|
||||
return setupResp{workspace, sdkAgent, agnt}
|
||||
}
|
||||
|
||||
func TestWorkspaceExternalAgentCredentials(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
client, db, user := coderdenttest.NewWithDatabase(t, &coderdenttest.Options{
|
||||
LicenseOptions: &coderdenttest.LicenseOptions{
|
||||
Features: license.Features{
|
||||
codersdk.FeatureWorkspaceExternalAgent: 1,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
t.Run("Success - linux", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx := testutil.Context(t, testutil.WaitShort)
|
||||
|
||||
r := dbfake.WorkspaceBuild(t, db, database.WorkspaceTable{
|
||||
OrganizationID: user.OrganizationID,
|
||||
OwnerID: user.UserID,
|
||||
}).Seed(database.WorkspaceBuild{
|
||||
HasExternalAgent: sql.NullBool{
|
||||
Bool: true,
|
||||
Valid: true,
|
||||
},
|
||||
}).Resource(&proto.Resource{
|
||||
Name: "test-agent",
|
||||
Type: "coder_external_agent",
|
||||
}).WithAgent(func(a []*proto.Agent) []*proto.Agent {
|
||||
a[0].Name = "test-agent"
|
||||
a[0].OperatingSystem = "linux"
|
||||
a[0].Architecture = "amd64"
|
||||
return a
|
||||
}).Do()
|
||||
|
||||
credentials, err := client.WorkspaceExternalAgentCredentials(
|
||||
ctx, r.Workspace.ID, "test-agent")
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, r.AgentToken, credentials.AgentToken)
|
||||
expectedCommand := fmt.Sprintf("CODER_AGENT_TOKEN=%q curl -fsSL \"%s/api/v2/init-script/linux/amd64\" | sh", r.AgentToken, client.URL)
|
||||
require.Equal(t, expectedCommand, credentials.Command)
|
||||
})
|
||||
|
||||
t.Run("Success - windows", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx := testutil.Context(t, testutil.WaitShort)
|
||||
|
||||
r := dbfake.WorkspaceBuild(t, db, database.WorkspaceTable{
|
||||
OrganizationID: user.OrganizationID,
|
||||
OwnerID: user.UserID,
|
||||
}).Resource(&proto.Resource{
|
||||
Name: "test-agent",
|
||||
Type: "coder_external_agent",
|
||||
}).Seed(database.WorkspaceBuild{
|
||||
HasExternalAgent: sql.NullBool{
|
||||
Bool: true,
|
||||
Valid: true,
|
||||
},
|
||||
}).WithAgent(func(a []*proto.Agent) []*proto.Agent {
|
||||
a[0].Name = "test-agent"
|
||||
a[0].OperatingSystem = "windows"
|
||||
a[0].Architecture = "amd64"
|
||||
return a
|
||||
}).Do()
|
||||
|
||||
credentials, err := client.WorkspaceExternalAgentCredentials(
|
||||
ctx, r.Workspace.ID, "test-agent")
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, r.AgentToken, credentials.AgentToken)
|
||||
expectedCommand := fmt.Sprintf("$env:CODER_AGENT_TOKEN=%q; iwr -useb \"%s/api/v2/init-script/windows/amd64\" | iex", r.AgentToken, client.URL)
|
||||
require.Equal(t, expectedCommand, credentials.Command)
|
||||
})
|
||||
|
||||
t.Run("WithInstanceID - should return 404", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx := testutil.Context(t, testutil.WaitShort)
|
||||
|
||||
r := dbfake.WorkspaceBuild(t, db, database.WorkspaceTable{
|
||||
OrganizationID: user.OrganizationID,
|
||||
OwnerID: user.UserID,
|
||||
}).Seed(database.WorkspaceBuild{
|
||||
HasExternalAgent: sql.NullBool{
|
||||
Bool: true,
|
||||
Valid: true,
|
||||
},
|
||||
}).Resource(&proto.Resource{
|
||||
Name: "test-agent",
|
||||
Type: "coder_external_agent",
|
||||
}).WithAgent(func(a []*proto.Agent) []*proto.Agent {
|
||||
a[0].Name = "test-agent"
|
||||
a[0].Auth = &proto.Agent_InstanceId{
|
||||
InstanceId: uuid.New().String(),
|
||||
}
|
||||
return a
|
||||
}).Do()
|
||||
|
||||
_, err := client.WorkspaceExternalAgentCredentials(ctx, r.Workspace.ID, "test-agent")
|
||||
require.Error(t, err)
|
||||
var apiErr *codersdk.Error
|
||||
require.ErrorAs(t, err, &apiErr)
|
||||
require.Equal(t, "External agent is authenticated with an instance ID.", apiErr.Message)
|
||||
})
|
||||
|
||||
t.Run("No external agent - should return 404", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx := testutil.Context(t, testutil.WaitShort)
|
||||
|
||||
r := dbfake.WorkspaceBuild(t, db, database.WorkspaceTable{
|
||||
OrganizationID: user.OrganizationID,
|
||||
OwnerID: user.UserID,
|
||||
}).Do()
|
||||
|
||||
_, err := client.WorkspaceExternalAgentCredentials(ctx, r.Workspace.ID, "test-agent")
|
||||
require.Error(t, err)
|
||||
var apiErr *codersdk.Error
|
||||
require.ErrorAs(t, err, &apiErr)
|
||||
require.Equal(t, "Workspace does not have an external agent.", apiErr.Message)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -363,6 +363,7 @@ func (e *executor) plan(ctx, killCtx context.Context, env, vars []string, logr l
|
||||
ModuleFiles: moduleFiles,
|
||||
HasAiTasks: state.HasAITasks,
|
||||
AiTasks: state.AITasks,
|
||||
HasExternalAgents: state.HasExternalAgents,
|
||||
}
|
||||
|
||||
return msg, nil
|
||||
|
||||
@@ -1135,6 +1135,31 @@ func TestProvision(t *testing.T) {
|
||||
HasAiTasks: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "external-agent",
|
||||
Files: map[string]string{
|
||||
"main.tf": `terraform {
|
||||
required_providers {
|
||||
coder = {
|
||||
source = "coder/coder"
|
||||
version = ">= 2.7.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
resource "coder_external_agent" "example" {
|
||||
agent_id = "123"
|
||||
}
|
||||
`,
|
||||
},
|
||||
Response: &proto.PlanComplete{
|
||||
Resources: []*proto.Resource{{
|
||||
Name: "example",
|
||||
Type: "coder_external_agent",
|
||||
}},
|
||||
HasExternalAgents: true,
|
||||
},
|
||||
SkipCacheProviders: true,
|
||||
},
|
||||
}
|
||||
|
||||
// Remove unused cache dirs before running tests.
|
||||
@@ -1237,6 +1262,7 @@ func TestProvision(t *testing.T) {
|
||||
require.Equal(t, string(modulesWant), string(modulesGot))
|
||||
|
||||
require.Equal(t, planComplete.HasAiTasks, testCase.Response.HasAiTasks)
|
||||
require.Equal(t, planComplete.HasExternalAgents, testCase.Response.HasExternalAgents)
|
||||
}
|
||||
|
||||
if testCase.Apply {
|
||||
|
||||
@@ -165,6 +165,7 @@ type State struct {
|
||||
ExternalAuthProviders []*proto.ExternalAuthProviderResource
|
||||
AITasks []*proto.AITask
|
||||
HasAITasks bool
|
||||
HasExternalAgents bool
|
||||
}
|
||||
|
||||
var ErrInvalidTerraformAddr = xerrors.New("invalid terraform address")
|
||||
@@ -188,6 +189,20 @@ func hasAITaskResources(graph *gographviz.Graph) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func hasExternalAgentResources(graph *gographviz.Graph) bool {
|
||||
for _, node := range graph.Nodes.Lookup {
|
||||
if label, exists := node.Attrs["label"]; exists {
|
||||
labelValue := strings.Trim(label, `"`)
|
||||
// The first condition is for the case where the resource is in the root module.
|
||||
// The second condition is for the case where the resource is in a child module.
|
||||
if strings.HasPrefix(labelValue, "coder_external_agent.") || strings.Contains(labelValue, ".coder_external_agent.") {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// ConvertState consumes Terraform state and a GraphViz representation
|
||||
// produced by `terraform graph` to produce resources consumable by Coder.
|
||||
// nolint:gocognit // This function makes more sense being large for now, until refactored.
|
||||
@@ -1065,6 +1080,7 @@ func ConvertState(ctx context.Context, modules []*tfjson.StateModule, rawGraph s
|
||||
ExternalAuthProviders: externalAuthProviders,
|
||||
HasAITasks: hasAITasks,
|
||||
AITasks: aiTasks,
|
||||
HasExternalAgents: hasExternalAgentResources(graph),
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -1252,7 +1268,8 @@ func findResourcesInGraph(graph *gographviz.Graph, tfResourcesByLabel map[string
|
||||
continue
|
||||
}
|
||||
// Don't associate Coder resources with other Coder resources!
|
||||
if strings.HasPrefix(resource.Type, "coder_") {
|
||||
// Except for coder_external_agent, which is a special case.
|
||||
if strings.HasPrefix(resource.Type, "coder_") && resource.Type != "coder_external_agent" {
|
||||
continue
|
||||
}
|
||||
graphResources = append(graphResources, &graphResource{
|
||||
|
||||
@@ -1573,6 +1573,35 @@ func TestAITasks(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestExternalAgents(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx, logger := ctxAndLogger(t)
|
||||
|
||||
t.Run("External agents can be defined", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// nolint:dogsled
|
||||
_, filename, _, _ := runtime.Caller(0)
|
||||
|
||||
dir := filepath.Join(filepath.Dir(filename), "testdata", "resources", "external-agents")
|
||||
tfPlanRaw, err := os.ReadFile(filepath.Join(dir, "external-agents.tfplan.json"))
|
||||
require.NoError(t, err)
|
||||
var tfPlan tfjson.Plan
|
||||
err = json.Unmarshal(tfPlanRaw, &tfPlan)
|
||||
require.NoError(t, err)
|
||||
tfPlanGraph, err := os.ReadFile(filepath.Join(dir, "external-agents.tfplan.dot"))
|
||||
require.NoError(t, err)
|
||||
|
||||
state, err := terraform.ConvertState(ctx, []*tfjson.StateModule{tfPlan.PlannedValues.RootModule, tfPlan.PriorState.Values.RootModule}, string(tfPlanGraph), logger)
|
||||
require.NotNil(t, state)
|
||||
require.NoError(t, err)
|
||||
require.True(t, state.HasExternalAgents)
|
||||
require.Len(t, state.Resources, 1)
|
||||
require.Len(t, state.Resources[0].Agents, 1)
|
||||
require.Equal(t, "dev1", state.Resources[0].Agents[0].Name)
|
||||
})
|
||||
}
|
||||
|
||||
// sortResource ensures resources appear in a consistent ordering
|
||||
// to prevent tests from flaking.
|
||||
func sortResources(resources []*proto.Resource) {
|
||||
|
||||
Generated
Vendored
+22
@@ -0,0 +1,22 @@
|
||||
digraph {
|
||||
compound = "true"
|
||||
newrank = "true"
|
||||
subgraph "root" {
|
||||
"[root] coder_agent.dev1 (expand)" [label = "coder_agent.dev1", shape = "box"]
|
||||
"[root] coder_external_agent.dev1 (expand)" [label = "coder_external_agent.dev1", shape = "box"]
|
||||
"[root] data.coder_provisioner.me (expand)" [label = "data.coder_provisioner.me", shape = "box"]
|
||||
"[root] data.coder_workspace.me (expand)" [label = "data.coder_workspace.me", shape = "box"]
|
||||
"[root] data.coder_workspace_owner.me (expand)" [label = "data.coder_workspace_owner.me", shape = "box"]
|
||||
"[root] provider[\"registry.terraform.io/coder/coder\"]" [label = "provider[\"registry.terraform.io/coder/coder\"]", shape = "diamond"]
|
||||
"[root] coder_agent.dev1 (expand)" -> "[root] provider[\"registry.terraform.io/coder/coder\"]"
|
||||
"[root] coder_external_agent.dev1 (expand)" -> "[root] coder_agent.dev1 (expand)"
|
||||
"[root] data.coder_provisioner.me (expand)" -> "[root] provider[\"registry.terraform.io/coder/coder\"]"
|
||||
"[root] data.coder_workspace.me (expand)" -> "[root] provider[\"registry.terraform.io/coder/coder\"]"
|
||||
"[root] data.coder_workspace_owner.me (expand)" -> "[root] provider[\"registry.terraform.io/coder/coder\"]"
|
||||
"[root] provider[\"registry.terraform.io/coder/coder\"] (close)" -> "[root] coder_external_agent.dev1 (expand)"
|
||||
"[root] provider[\"registry.terraform.io/coder/coder\"] (close)" -> "[root] data.coder_provisioner.me (expand)"
|
||||
"[root] provider[\"registry.terraform.io/coder/coder\"] (close)" -> "[root] data.coder_workspace.me (expand)"
|
||||
"[root] provider[\"registry.terraform.io/coder/coder\"] (close)" -> "[root] data.coder_workspace_owner.me (expand)"
|
||||
"[root] root" -> "[root] provider[\"registry.terraform.io/coder/coder\"] (close)"
|
||||
}
|
||||
}
|
||||
Generated
Vendored
+277
@@ -0,0 +1,277 @@
|
||||
{
|
||||
"format_version": "1.2",
|
||||
"terraform_version": "1.12.2",
|
||||
"planned_values": {
|
||||
"root_module": {
|
||||
"resources": [
|
||||
{
|
||||
"address": "coder_agent.dev1",
|
||||
"mode": "managed",
|
||||
"type": "coder_agent",
|
||||
"name": "dev1",
|
||||
"provider_name": "registry.terraform.io/coder/coder",
|
||||
"schema_version": 1,
|
||||
"values": {
|
||||
"api_key_scope": "all",
|
||||
"arch": "amd64",
|
||||
"auth": "token",
|
||||
"connection_timeout": 120,
|
||||
"dir": null,
|
||||
"env": null,
|
||||
"metadata": [],
|
||||
"motd_file": null,
|
||||
"order": null,
|
||||
"os": "linux",
|
||||
"resources_monitoring": [],
|
||||
"shutdown_script": null,
|
||||
"startup_script": null,
|
||||
"startup_script_behavior": "non-blocking",
|
||||
"troubleshooting_url": null
|
||||
},
|
||||
"sensitive_values": {
|
||||
"display_apps": [],
|
||||
"metadata": [],
|
||||
"resources_monitoring": [],
|
||||
"token": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"address": "coder_external_agent.dev1",
|
||||
"mode": "managed",
|
||||
"type": "coder_external_agent",
|
||||
"name": "dev1",
|
||||
"provider_name": "registry.terraform.io/coder/coder",
|
||||
"schema_version": 1,
|
||||
"sensitive_values": {
|
||||
"agent_id": true
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"resource_changes": [
|
||||
{
|
||||
"address": "coder_agent.dev1",
|
||||
"mode": "managed",
|
||||
"type": "coder_agent",
|
||||
"name": "dev1",
|
||||
"provider_name": "registry.terraform.io/coder/coder",
|
||||
"change": {
|
||||
"actions": [
|
||||
"create"
|
||||
],
|
||||
"before": null,
|
||||
"after": {
|
||||
"api_key_scope": "all",
|
||||
"arch": "amd64",
|
||||
"auth": "token",
|
||||
"connection_timeout": 120,
|
||||
"dir": null,
|
||||
"env": null,
|
||||
"metadata": [],
|
||||
"motd_file": null,
|
||||
"order": null,
|
||||
"os": "linux",
|
||||
"resources_monitoring": [],
|
||||
"shutdown_script": null,
|
||||
"startup_script": null,
|
||||
"startup_script_behavior": "non-blocking",
|
||||
"troubleshooting_url": null
|
||||
},
|
||||
"after_unknown": {
|
||||
"display_apps": true,
|
||||
"id": true,
|
||||
"init_script": true,
|
||||
"metadata": [],
|
||||
"resources_monitoring": [],
|
||||
"token": true
|
||||
},
|
||||
"before_sensitive": false,
|
||||
"after_sensitive": {
|
||||
"display_apps": [],
|
||||
"metadata": [],
|
||||
"resources_monitoring": [],
|
||||
"token": true
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"address": "coder_external_agent.dev1",
|
||||
"mode": "managed",
|
||||
"type": "coder_external_agent",
|
||||
"name": "dev1",
|
||||
"provider_name": "registry.terraform.io/coder/coder",
|
||||
"change": {
|
||||
"actions": [
|
||||
"create"
|
||||
],
|
||||
"before": null,
|
||||
"after": {},
|
||||
"after_unknown": {
|
||||
"agent_id": true,
|
||||
"id": true
|
||||
},
|
||||
"before_sensitive": false,
|
||||
"after_sensitive": {
|
||||
"agent_id": true
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"prior_state": {
|
||||
"format_version": "1.0",
|
||||
"terraform_version": "1.12.2",
|
||||
"values": {
|
||||
"root_module": {
|
||||
"resources": [
|
||||
{
|
||||
"address": "data.coder_provisioner.me",
|
||||
"mode": "data",
|
||||
"type": "coder_provisioner",
|
||||
"name": "me",
|
||||
"provider_name": "registry.terraform.io/coder/coder",
|
||||
"schema_version": 1,
|
||||
"values": {
|
||||
"arch": "amd64",
|
||||
"id": "d607be41-7697-475f-8257-2f6e24adbede",
|
||||
"os": "linux"
|
||||
},
|
||||
"sensitive_values": {}
|
||||
},
|
||||
{
|
||||
"address": "data.coder_workspace.me",
|
||||
"mode": "data",
|
||||
"type": "coder_workspace",
|
||||
"name": "me",
|
||||
"provider_name": "registry.terraform.io/coder/coder",
|
||||
"schema_version": 1,
|
||||
"values": {
|
||||
"access_port": 443,
|
||||
"access_url": "https://dev.coder.com/",
|
||||
"id": "0b7fc772-5e27-4096-b8a3-9e6a8b914ebe",
|
||||
"is_prebuild": false,
|
||||
"is_prebuild_claim": false,
|
||||
"name": "kacper",
|
||||
"prebuild_count": 0,
|
||||
"start_count": 1,
|
||||
"template_id": "",
|
||||
"template_name": "",
|
||||
"template_version": "",
|
||||
"transition": "start"
|
||||
},
|
||||
"sensitive_values": {}
|
||||
},
|
||||
{
|
||||
"address": "data.coder_workspace_owner.me",
|
||||
"mode": "data",
|
||||
"type": "coder_workspace_owner",
|
||||
"name": "me",
|
||||
"provider_name": "registry.terraform.io/coder/coder",
|
||||
"schema_version": 0,
|
||||
"values": {
|
||||
"email": "default@example.com",
|
||||
"full_name": "kacpersaw",
|
||||
"groups": [],
|
||||
"id": "1ebd1795-7cf2-47c5-8024-5d56e68f1681",
|
||||
"login_type": null,
|
||||
"name": "default",
|
||||
"oidc_access_token": "",
|
||||
"rbac_roles": [],
|
||||
"session_token": "",
|
||||
"ssh_private_key": "",
|
||||
"ssh_public_key": ""
|
||||
},
|
||||
"sensitive_values": {
|
||||
"groups": [],
|
||||
"oidc_access_token": true,
|
||||
"rbac_roles": [],
|
||||
"session_token": true,
|
||||
"ssh_private_key": true
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"configuration": {
|
||||
"provider_config": {
|
||||
"coder": {
|
||||
"name": "coder",
|
||||
"full_name": "registry.terraform.io/coder/coder",
|
||||
"version_constraint": ">= 2.0.0"
|
||||
}
|
||||
},
|
||||
"root_module": {
|
||||
"resources": [
|
||||
{
|
||||
"address": "coder_agent.dev1",
|
||||
"mode": "managed",
|
||||
"type": "coder_agent",
|
||||
"name": "dev1",
|
||||
"provider_config_key": "coder",
|
||||
"expressions": {
|
||||
"arch": {
|
||||
"constant_value": "amd64"
|
||||
},
|
||||
"os": {
|
||||
"constant_value": "linux"
|
||||
}
|
||||
},
|
||||
"schema_version": 1
|
||||
},
|
||||
{
|
||||
"address": "coder_external_agent.dev1",
|
||||
"mode": "managed",
|
||||
"type": "coder_external_agent",
|
||||
"name": "dev1",
|
||||
"provider_config_key": "coder",
|
||||
"expressions": {
|
||||
"agent_id": {
|
||||
"references": [
|
||||
"coder_agent.dev1.token",
|
||||
"coder_agent.dev1"
|
||||
]
|
||||
}
|
||||
},
|
||||
"schema_version": 1
|
||||
},
|
||||
{
|
||||
"address": "data.coder_provisioner.me",
|
||||
"mode": "data",
|
||||
"type": "coder_provisioner",
|
||||
"name": "me",
|
||||
"provider_config_key": "coder",
|
||||
"schema_version": 1
|
||||
},
|
||||
{
|
||||
"address": "data.coder_workspace.me",
|
||||
"mode": "data",
|
||||
"type": "coder_workspace",
|
||||
"name": "me",
|
||||
"provider_config_key": "coder",
|
||||
"schema_version": 1
|
||||
},
|
||||
{
|
||||
"address": "data.coder_workspace_owner.me",
|
||||
"mode": "data",
|
||||
"type": "coder_workspace_owner",
|
||||
"name": "me",
|
||||
"provider_config_key": "coder",
|
||||
"schema_version": 0
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"relevant_attributes": [
|
||||
{
|
||||
"resource": "coder_agent.dev1",
|
||||
"attribute": [
|
||||
"token"
|
||||
]
|
||||
}
|
||||
],
|
||||
"timestamp": "2025-07-31T11:08:54Z",
|
||||
"applyable": true,
|
||||
"complete": true,
|
||||
"errored": false
|
||||
}
|
||||
Generated
Vendored
+22
@@ -0,0 +1,22 @@
|
||||
digraph {
|
||||
compound = "true"
|
||||
newrank = "true"
|
||||
subgraph "root" {
|
||||
"[root] coder_agent.dev1 (expand)" [label = "coder_agent.dev1", shape = "box"]
|
||||
"[root] coder_external_agent.dev1 (expand)" [label = "coder_external_agent.dev1", shape = "box"]
|
||||
"[root] data.coder_provisioner.me (expand)" [label = "data.coder_provisioner.me", shape = "box"]
|
||||
"[root] data.coder_workspace.me (expand)" [label = "data.coder_workspace.me", shape = "box"]
|
||||
"[root] data.coder_workspace_owner.me (expand)" [label = "data.coder_workspace_owner.me", shape = "box"]
|
||||
"[root] provider[\"registry.terraform.io/coder/coder\"]" [label = "provider[\"registry.terraform.io/coder/coder\"]", shape = "diamond"]
|
||||
"[root] coder_agent.dev1 (expand)" -> "[root] provider[\"registry.terraform.io/coder/coder\"]"
|
||||
"[root] coder_external_agent.dev1 (expand)" -> "[root] coder_agent.dev1 (expand)"
|
||||
"[root] data.coder_provisioner.me (expand)" -> "[root] provider[\"registry.terraform.io/coder/coder\"]"
|
||||
"[root] data.coder_workspace.me (expand)" -> "[root] provider[\"registry.terraform.io/coder/coder\"]"
|
||||
"[root] data.coder_workspace_owner.me (expand)" -> "[root] provider[\"registry.terraform.io/coder/coder\"]"
|
||||
"[root] provider[\"registry.terraform.io/coder/coder\"] (close)" -> "[root] coder_external_agent.dev1 (expand)"
|
||||
"[root] provider[\"registry.terraform.io/coder/coder\"] (close)" -> "[root] data.coder_provisioner.me (expand)"
|
||||
"[root] provider[\"registry.terraform.io/coder/coder\"] (close)" -> "[root] data.coder_workspace.me (expand)"
|
||||
"[root] provider[\"registry.terraform.io/coder/coder\"] (close)" -> "[root] data.coder_workspace_owner.me (expand)"
|
||||
"[root] root" -> "[root] provider[\"registry.terraform.io/coder/coder\"] (close)"
|
||||
}
|
||||
}
|
||||
Generated
Vendored
+138
@@ -0,0 +1,138 @@
|
||||
{
|
||||
"format_version": "1.0",
|
||||
"terraform_version": "1.12.2",
|
||||
"values": {
|
||||
"root_module": {
|
||||
"resources": [
|
||||
{
|
||||
"address": "data.coder_provisioner.me",
|
||||
"mode": "data",
|
||||
"type": "coder_provisioner",
|
||||
"name": "me",
|
||||
"provider_name": "registry.terraform.io/coder/coder",
|
||||
"schema_version": 1,
|
||||
"values": {
|
||||
"arch": "amd64",
|
||||
"id": "0ce4713c-28d6-4999-9381-52b8a603b672",
|
||||
"os": "linux"
|
||||
},
|
||||
"sensitive_values": {}
|
||||
},
|
||||
{
|
||||
"address": "data.coder_workspace.me",
|
||||
"mode": "data",
|
||||
"type": "coder_workspace",
|
||||
"name": "me",
|
||||
"provider_name": "registry.terraform.io/coder/coder",
|
||||
"schema_version": 1,
|
||||
"values": {
|
||||
"access_port": 443,
|
||||
"access_url": "https://dev.coder.com/",
|
||||
"id": "dfa1dbe8-ad31-410b-b201-a4ed4d884938",
|
||||
"is_prebuild": false,
|
||||
"is_prebuild_claim": false,
|
||||
"name": "kacper",
|
||||
"prebuild_count": 0,
|
||||
"start_count": 1,
|
||||
"template_id": "",
|
||||
"template_name": "",
|
||||
"template_version": "",
|
||||
"transition": "start"
|
||||
},
|
||||
"sensitive_values": {}
|
||||
},
|
||||
{
|
||||
"address": "data.coder_workspace_owner.me",
|
||||
"mode": "data",
|
||||
"type": "coder_workspace_owner",
|
||||
"name": "me",
|
||||
"provider_name": "registry.terraform.io/coder/coder",
|
||||
"schema_version": 0,
|
||||
"values": {
|
||||
"email": "default@example.com",
|
||||
"full_name": "kacpersaw",
|
||||
"groups": [],
|
||||
"id": "f5e82b90-ea22-4288-8286-9cf7af651143",
|
||||
"login_type": null,
|
||||
"name": "default",
|
||||
"oidc_access_token": "",
|
||||
"rbac_roles": [],
|
||||
"session_token": "",
|
||||
"ssh_private_key": "",
|
||||
"ssh_public_key": ""
|
||||
},
|
||||
"sensitive_values": {
|
||||
"groups": [],
|
||||
"oidc_access_token": true,
|
||||
"rbac_roles": [],
|
||||
"session_token": true,
|
||||
"ssh_private_key": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"address": "coder_agent.dev1",
|
||||
"mode": "managed",
|
||||
"type": "coder_agent",
|
||||
"name": "dev1",
|
||||
"provider_name": "registry.terraform.io/coder/coder",
|
||||
"schema_version": 1,
|
||||
"values": {
|
||||
"api_key_scope": "all",
|
||||
"arch": "amd64",
|
||||
"auth": "token",
|
||||
"connection_timeout": 120,
|
||||
"dir": null,
|
||||
"display_apps": [
|
||||
{
|
||||
"port_forwarding_helper": true,
|
||||
"ssh_helper": true,
|
||||
"vscode": true,
|
||||
"vscode_insiders": false,
|
||||
"web_terminal": true
|
||||
}
|
||||
],
|
||||
"env": null,
|
||||
"id": "15a35370-3b2e-4ee7-8b28-81cef0152d8b",
|
||||
"init_script": "",
|
||||
"metadata": [],
|
||||
"motd_file": null,
|
||||
"order": null,
|
||||
"os": "linux",
|
||||
"resources_monitoring": [],
|
||||
"shutdown_script": null,
|
||||
"startup_script": null,
|
||||
"startup_script_behavior": "non-blocking",
|
||||
"token": "d054c66b-cc5c-41ae-aa0c-2098a1075272",
|
||||
"troubleshooting_url": null
|
||||
},
|
||||
"sensitive_values": {
|
||||
"display_apps": [
|
||||
{}
|
||||
],
|
||||
"metadata": [],
|
||||
"resources_monitoring": [],
|
||||
"token": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"address": "coder_external_agent.dev1",
|
||||
"mode": "managed",
|
||||
"type": "coder_external_agent",
|
||||
"name": "dev1",
|
||||
"provider_name": "registry.terraform.io/coder/coder",
|
||||
"schema_version": 1,
|
||||
"values": {
|
||||
"agent_id": "d054c66b-cc5c-41ae-aa0c-2098a1075272",
|
||||
"id": "4d87dd70-879c-4347-b0c1-b8f3587d1021"
|
||||
},
|
||||
"sensitive_values": {
|
||||
"agent_id": true
|
||||
},
|
||||
"depends_on": [
|
||||
"coder_agent.dev1"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
terraform {
|
||||
required_providers {
|
||||
coder = {
|
||||
source = "coder/coder"
|
||||
version = ">=2.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
data "coder_provisioner" "me" {}
|
||||
data "coder_workspace" "me" {}
|
||||
data "coder_workspace_owner" "me" {}
|
||||
|
||||
resource "coder_agent" "dev1" {
|
||||
os = "linux"
|
||||
arch = "amd64"
|
||||
}
|
||||
|
||||
resource "coder_external_agent" "dev1" {
|
||||
agent_id = coder_agent.dev1.token
|
||||
}
|
||||
+1
-1
@@ -1 +1 @@
|
||||
1.11.4
|
||||
1.12.2
|
||||
|
||||
Generated
+14
-3
@@ -1403,6 +1403,7 @@ type CompletedJob_TemplateImport struct {
|
||||
ModuleFiles []byte `protobuf:"bytes,10,opt,name=module_files,json=moduleFiles,proto3" json:"module_files,omitempty"`
|
||||
ModuleFilesHash []byte `protobuf:"bytes,11,opt,name=module_files_hash,json=moduleFilesHash,proto3" json:"module_files_hash,omitempty"`
|
||||
HasAiTasks bool `protobuf:"varint,12,opt,name=has_ai_tasks,json=hasAiTasks,proto3" json:"has_ai_tasks,omitempty"`
|
||||
HasExternalAgents bool `protobuf:"varint,13,opt,name=has_external_agents,json=hasExternalAgents,proto3" json:"has_external_agents,omitempty"`
|
||||
}
|
||||
|
||||
func (x *CompletedJob_TemplateImport) Reset() {
|
||||
@@ -1521,6 +1522,13 @@ func (x *CompletedJob_TemplateImport) GetHasAiTasks() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (x *CompletedJob_TemplateImport) GetHasExternalAgents() bool {
|
||||
if x != nil {
|
||||
return x.HasExternalAgents
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
type CompletedJob_TemplateDryRun struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
@@ -1710,7 +1718,7 @@ var file_provisionerd_proto_provisionerd_proto_rawDesc = []byte{
|
||||
0x6d, 0x69, 0x6e, 0x67, 0x52, 0x07, 0x74, 0x69, 0x6d, 0x69, 0x6e, 0x67, 0x73, 0x1a, 0x10, 0x0a,
|
||||
0x0e, 0x54, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x49, 0x6d, 0x70, 0x6f, 0x72, 0x74, 0x1a,
|
||||
0x10, 0x0a, 0x0e, 0x54, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x44, 0x72, 0x79, 0x52, 0x75,
|
||||
0x6e, 0x42, 0x06, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x22, 0x8b, 0x0b, 0x0a, 0x0c, 0x43, 0x6f,
|
||||
0x6e, 0x42, 0x06, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x22, 0xbb, 0x0b, 0x0a, 0x0c, 0x43, 0x6f,
|
||||
0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x4a, 0x6f, 0x62, 0x12, 0x15, 0x0a, 0x06, 0x6a, 0x6f,
|
||||
0x62, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x6a, 0x6f, 0x62, 0x49,
|
||||
0x64, 0x12, 0x54, 0x0a, 0x0f, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, 0x62,
|
||||
@@ -1749,7 +1757,7 @@ var file_provisionerd_proto_provisionerd_proto_rawDesc = []byte{
|
||||
0x63, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x12, 0x2e, 0x0a, 0x08, 0x61, 0x69, 0x5f, 0x74, 0x61,
|
||||
0x73, 0x6b, 0x73, 0x18, 0x06, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x70, 0x72, 0x6f, 0x76,
|
||||
0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x41, 0x49, 0x54, 0x61, 0x73, 0x6b, 0x52, 0x07,
|
||||
0x61, 0x69, 0x54, 0x61, 0x73, 0x6b, 0x73, 0x1a, 0x9f, 0x05, 0x0a, 0x0e, 0x54, 0x65, 0x6d, 0x70,
|
||||
0x61, 0x69, 0x54, 0x61, 0x73, 0x6b, 0x73, 0x1a, 0xcf, 0x05, 0x0a, 0x0e, 0x54, 0x65, 0x6d, 0x70,
|
||||
0x6c, 0x61, 0x74, 0x65, 0x49, 0x6d, 0x70, 0x6f, 0x72, 0x74, 0x12, 0x3e, 0x0a, 0x0f, 0x73, 0x74,
|
||||
0x61, 0x72, 0x74, 0x5f, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x18, 0x01, 0x20,
|
||||
0x03, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65,
|
||||
@@ -1791,7 +1799,10 @@ var file_provisionerd_proto_provisionerd_proto_rawDesc = []byte{
|
||||
0x18, 0x0b, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0f, 0x6d, 0x6f, 0x64, 0x75, 0x6c, 0x65, 0x46, 0x69,
|
||||
0x6c, 0x65, 0x73, 0x48, 0x61, 0x73, 0x68, 0x12, 0x20, 0x0a, 0x0c, 0x68, 0x61, 0x73, 0x5f, 0x61,
|
||||
0x69, 0x5f, 0x74, 0x61, 0x73, 0x6b, 0x73, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0a, 0x68,
|
||||
0x61, 0x73, 0x41, 0x69, 0x54, 0x61, 0x73, 0x6b, 0x73, 0x1a, 0x74, 0x0a, 0x0e, 0x54, 0x65, 0x6d,
|
||||
0x61, 0x73, 0x41, 0x69, 0x54, 0x61, 0x73, 0x6b, 0x73, 0x12, 0x2e, 0x0a, 0x13, 0x68, 0x61, 0x73,
|
||||
0x5f, 0x65, 0x78, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x5f, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x73,
|
||||
0x18, 0x0d, 0x20, 0x01, 0x28, 0x08, 0x52, 0x11, 0x68, 0x61, 0x73, 0x45, 0x78, 0x74, 0x65, 0x72,
|
||||
0x6e, 0x61, 0x6c, 0x41, 0x67, 0x65, 0x6e, 0x74, 0x73, 0x1a, 0x74, 0x0a, 0x0e, 0x54, 0x65, 0x6d,
|
||||
0x70, 0x6c, 0x61, 0x74, 0x65, 0x44, 0x72, 0x79, 0x52, 0x75, 0x6e, 0x12, 0x33, 0x0a, 0x09, 0x72,
|
||||
0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x15,
|
||||
0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x52, 0x65, 0x73,
|
||||
|
||||
@@ -95,6 +95,7 @@ message CompletedJob {
|
||||
bytes module_files = 10;
|
||||
bytes module_files_hash = 11;
|
||||
bool has_ai_tasks = 12;
|
||||
bool has_external_agents = 13;
|
||||
}
|
||||
message TemplateDryRun {
|
||||
repeated provisioner.Resource resources = 1;
|
||||
|
||||
@@ -47,9 +47,12 @@ import "github.com/coder/coder/v2/apiversion"
|
||||
//
|
||||
// API v1.8:
|
||||
// - Add new fields `description` and `icon` to `Preset`.
|
||||
//
|
||||
// API v1.9:
|
||||
// - Added new field named 'has_external_agent' in 'CompleteJob.TemplateImport'
|
||||
const (
|
||||
CurrentMajor = 1
|
||||
CurrentMinor = 8
|
||||
CurrentMinor = 9
|
||||
)
|
||||
|
||||
// CurrentVersion is the current provisionerd API version.
|
||||
|
||||
@@ -600,8 +600,9 @@ func (r *Runner) runTemplateImport(ctx context.Context) (*proto.CompletedJob, *p
|
||||
// ModuleFiles are not on the stopProvision. So grab from the startProvision.
|
||||
ModuleFiles: startProvision.ModuleFiles,
|
||||
// ModuleFileHash will be populated if the file is uploaded async
|
||||
ModuleFilesHash: []byte{},
|
||||
HasAiTasks: startProvision.HasAITasks,
|
||||
ModuleFilesHash: []byte{},
|
||||
HasAiTasks: startProvision.HasAITasks,
|
||||
HasExternalAgents: startProvision.HasExternalAgents,
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
@@ -666,6 +667,7 @@ type templateImportProvision struct {
|
||||
Plan json.RawMessage
|
||||
ModuleFiles []byte
|
||||
HasAITasks bool
|
||||
HasExternalAgents bool
|
||||
}
|
||||
|
||||
// Performs a dry-run provision when importing a template.
|
||||
@@ -807,6 +809,7 @@ func (r *Runner) runTemplateImportProvisionWithRichParameters(
|
||||
Plan: c.Plan,
|
||||
ModuleFiles: moduleFilesData,
|
||||
HasAITasks: c.HasAiTasks,
|
||||
HasExternalAgents: c.HasExternalAgents,
|
||||
}, nil
|
||||
default:
|
||||
return nil, xerrors.Errorf("invalid message type %q received from provisioner",
|
||||
|
||||
Generated
+15
-4
@@ -3401,8 +3401,9 @@ type PlanComplete struct {
|
||||
// still need to know that such resources are defined.
|
||||
//
|
||||
// See `hasAITaskResources` in provisioner/terraform/resources.go for more details.
|
||||
HasAiTasks bool `protobuf:"varint,13,opt,name=has_ai_tasks,json=hasAiTasks,proto3" json:"has_ai_tasks,omitempty"`
|
||||
AiTasks []*AITask `protobuf:"bytes,14,rep,name=ai_tasks,json=aiTasks,proto3" json:"ai_tasks,omitempty"`
|
||||
HasAiTasks bool `protobuf:"varint,13,opt,name=has_ai_tasks,json=hasAiTasks,proto3" json:"has_ai_tasks,omitempty"`
|
||||
AiTasks []*AITask `protobuf:"bytes,14,rep,name=ai_tasks,json=aiTasks,proto3" json:"ai_tasks,omitempty"`
|
||||
HasExternalAgents bool `protobuf:"varint,15,opt,name=has_external_agents,json=hasExternalAgents,proto3" json:"has_external_agents,omitempty"`
|
||||
}
|
||||
|
||||
func (x *PlanComplete) Reset() {
|
||||
@@ -3528,6 +3529,13 @@ func (x *PlanComplete) GetAiTasks() []*AITask {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *PlanComplete) GetHasExternalAgents() bool {
|
||||
if x != nil {
|
||||
return x.HasExternalAgents
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// ApplyRequest asks the provisioner to apply the changes. Apply MUST be preceded by a successful plan request/response
|
||||
// in the same Session. The plan data is not transmitted over the wire and is cached by the provisioner in the Session.
|
||||
type ApplyRequest struct {
|
||||
@@ -4855,7 +4863,7 @@ var file_provisionersdk_proto_provisioner_proto_rawDesc = []byte{
|
||||
0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x12, 0x2a, 0x0a, 0x11,
|
||||
0x6f, 0x6d, 0x69, 0x74, 0x5f, 0x6d, 0x6f, 0x64, 0x75, 0x6c, 0x65, 0x5f, 0x66, 0x69, 0x6c, 0x65,
|
||||
0x73, 0x18, 0x06, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0f, 0x6f, 0x6d, 0x69, 0x74, 0x4d, 0x6f, 0x64,
|
||||
0x75, 0x6c, 0x65, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x22, 0x91, 0x05, 0x0a, 0x0c, 0x50, 0x6c, 0x61,
|
||||
0x75, 0x6c, 0x65, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x22, 0xc1, 0x05, 0x0a, 0x0c, 0x50, 0x6c, 0x61,
|
||||
0x6e, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x65, 0x72, 0x72,
|
||||
0x6f, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x12,
|
||||
0x33, 0x0a, 0x09, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x18, 0x02, 0x20, 0x03,
|
||||
@@ -4896,7 +4904,10 @@ var file_provisionersdk_proto_provisioner_proto_rawDesc = []byte{
|
||||
0x52, 0x0a, 0x68, 0x61, 0x73, 0x41, 0x69, 0x54, 0x61, 0x73, 0x6b, 0x73, 0x12, 0x2e, 0x0a, 0x08,
|
||||
0x61, 0x69, 0x5f, 0x74, 0x61, 0x73, 0x6b, 0x73, 0x18, 0x0e, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x13,
|
||||
0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x41, 0x49, 0x54,
|
||||
0x61, 0x73, 0x6b, 0x52, 0x07, 0x61, 0x69, 0x54, 0x61, 0x73, 0x6b, 0x73, 0x22, 0x41, 0x0a, 0x0c,
|
||||
0x61, 0x73, 0x6b, 0x52, 0x07, 0x61, 0x69, 0x54, 0x61, 0x73, 0x6b, 0x73, 0x12, 0x2e, 0x0a, 0x13,
|
||||
0x68, 0x61, 0x73, 0x5f, 0x65, 0x78, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x5f, 0x61, 0x67, 0x65,
|
||||
0x6e, 0x74, 0x73, 0x18, 0x0f, 0x20, 0x01, 0x28, 0x08, 0x52, 0x11, 0x68, 0x61, 0x73, 0x45, 0x78,
|
||||
0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x41, 0x67, 0x65, 0x6e, 0x74, 0x73, 0x22, 0x41, 0x0a, 0x0c,
|
||||
0x41, 0x70, 0x70, 0x6c, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x31, 0x0a, 0x08,
|
||||
0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15,
|
||||
0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x4d, 0x65, 0x74,
|
||||
|
||||
@@ -419,6 +419,7 @@ message PlanComplete {
|
||||
// See `hasAITaskResources` in provisioner/terraform/resources.go for more details.
|
||||
bool has_ai_tasks = 13;
|
||||
repeated provisioner.AITask ai_tasks = 14;
|
||||
bool has_external_agents = 15;
|
||||
}
|
||||
|
||||
// ApplyRequest asks the provisioner to apply the changes. Apply MUST be preceded by a successful plan request/response
|
||||
|
||||
Generated
+4
@@ -462,6 +462,7 @@ export interface PlanComplete {
|
||||
*/
|
||||
hasAiTasks: boolean;
|
||||
aiTasks: AITask[];
|
||||
hasExternalAgents: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1395,6 +1396,9 @@ export const PlanComplete = {
|
||||
for (const v of message.aiTasks) {
|
||||
AITask.encode(v!, writer.uint32(114).fork()).ldelim();
|
||||
}
|
||||
if (message.hasExternalAgents === true) {
|
||||
writer.uint32(120).bool(message.hasExternalAgents);
|
||||
}
|
||||
return writer;
|
||||
},
|
||||
};
|
||||
|
||||
Generated
+10
@@ -939,6 +939,12 @@ export const Experiments: Experiment[] = [
|
||||
"workspace-usage",
|
||||
];
|
||||
|
||||
// From codersdk/workspaces.go
|
||||
export interface ExternalAgentCredentials {
|
||||
readonly command: string;
|
||||
readonly agent_token: string;
|
||||
}
|
||||
|
||||
// From codersdk/externalauth.go
|
||||
export interface ExternalAuth {
|
||||
readonly authenticated: boolean;
|
||||
@@ -1052,6 +1058,7 @@ export type FeatureName =
|
||||
| "user_limit"
|
||||
| "user_role_management"
|
||||
| "workspace_batch_actions"
|
||||
| "workspace_external_agent"
|
||||
| "workspace_prebuilds"
|
||||
| "workspace_proxy";
|
||||
|
||||
@@ -1075,6 +1082,7 @@ export const FeatureNames: FeatureName[] = [
|
||||
"user_limit",
|
||||
"user_role_management",
|
||||
"workspace_batch_actions",
|
||||
"workspace_external_agent",
|
||||
"workspace_prebuilds",
|
||||
"workspace_proxy",
|
||||
];
|
||||
@@ -3000,6 +3008,7 @@ export interface TemplateVersion {
|
||||
readonly archived: boolean;
|
||||
readonly warnings?: readonly TemplateVersionWarning[];
|
||||
readonly matched_provisioners?: MatchedProvisioners;
|
||||
readonly has_external_agent: boolean;
|
||||
}
|
||||
|
||||
// From codersdk/templateversions.go
|
||||
@@ -3876,6 +3885,7 @@ export interface WorkspaceBuild {
|
||||
readonly template_version_preset_id: string | null;
|
||||
readonly has_ai_task?: boolean;
|
||||
readonly ai_task_sidebar_app_id?: string;
|
||||
readonly has_external_agent?: boolean;
|
||||
}
|
||||
|
||||
// From codersdk/workspacebuilds.go
|
||||
|
||||
@@ -732,6 +732,7 @@ You can add instructions here
|
||||
[Some link info](https://coder.com)`,
|
||||
created_by: MockUserOwner,
|
||||
archived: false,
|
||||
has_external_agent: false,
|
||||
};
|
||||
|
||||
export const MockTemplateVersion2: TypesGen.TemplateVersion = {
|
||||
@@ -751,6 +752,7 @@ You can add instructions here
|
||||
[Some link info](https://coder.com)`,
|
||||
created_by: MockUserOwner,
|
||||
archived: false,
|
||||
has_external_agent: false,
|
||||
};
|
||||
|
||||
export const MockTemplateVersionWithMarkdownMessage: TypesGen.TemplateVersion =
|
||||
|
||||
Reference in New Issue
Block a user