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

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

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

* GET /api/v2/init-script - Gets the agent initialization script
  * By default, it returns a script for Linux (amd64), but with query parameters (os and arch) you can get the init script for different platforms
* GET /api/v2/workspaces/{workspace}/external-agent/{agent}/credentials - Gets credentials for an external agent **(enterprise)**
* Updated queries to filter workspaces/templates by the has_external_agent field
This commit is contained in:
Kacper Sawicki
2025-08-19 10:41:33 +02:00
committed by GitHub
parent f085c37af3
commit 9edceef0bf
53 changed files with 1616 additions and 98 deletions
+122
View File
@@ -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)
})
}