mirror of
https://github.com/coder/coder.git
synced 2026-06-03 04:58:23 +00:00
584c61acb5
## Problem Workspaces showed as "Healthy" immediately after creation while the agent was still downloading, starting, or connecting. If the agent never connected, the workspace stayed "Healthy" for the entire connection timeout (~120s), then abruptly flipped to "Unhealthy". ## Root cause In `db2sdk.WorkspaceAgent`, the health switch had no case for `WorkspaceAgentConnecting`. Agents in `connecting` status with a non-`off` lifecycle (e.g. `created` after a fresh build) fell through to the `default` case and were marked `Healthy = true`. ## Fix Add an explicit case for `WorkspaceAgentConnecting` that sets `Healthy = false` with reason `"agent has not yet connected"`. The case is placed after the existing `!connected + off` case (which correctly catches stopped agents as "not running") and before the `timeout`/`disconnected` cases. ``` Status + Lifecycle → Health reason ────────────────────────────────────────────────────── any !connected + off → "agent is not running" connecting + created/starting → "agent has not yet connected" ← NEW timeout + any → "agent is taking too long to connect" disconnected + any → "agent has lost connection" connected + start_error → "agent startup script exited with an error" connected + shutting_down → "agent is shutting down" connected + ready/starting → healthy ``` The frontend already handles this case — `getAgentHealthIssue()` returns "Workspace agent is still connecting" with `severity: "info"` for unhealthy workspaces with connecting agents. ## Test changes - **Healthy test**: now actually connects the agent via `agenttest.New` before asserting health (previously passed due to the bug). - **New Connecting test**: verifies a never-connected agent is correctly marked unhealthy. - **Mixed health test**: connects a1 and waits for the mixed state (`a1.Healthy && !workspace.Healthy`) to avoid a race where both agents are initially connecting. - **Sub-agent excluded test**: connects the parent agent and waits for it to be healthy before creating the sub-agent. - **TestWorkspaceAgent/Connect**: flipped assertion to `Health.Healthy == false` for a `dbfake` agent that never connects. <details> <summary>Review notes</summary> ### Known follow-up The `healthy:false` workspace search filter maps to `[disconnected, timeout]` and does not include `connecting`. This is a pre-existing gap that is now more consequential — a workspace unhealthy solely due to a connecting agent won't appear in `healthy:false` results. Worth a follow-up issue. ### Deep review findings addressed | Finding | Severity | Status | |---------|----------|--------| | Mixed health test race (all 3 reviewers) | P2 | Fixed — tightened `Eventually` condition | | `TestWorkspaceAgent/Connect` assertion break | P1 | Fixed — flipped assertion | | CLI renders red for connecting agents | Obs | Acknowledged — design trade-off, accurate but visually strong for transient state | | Switch case ordering overlap | Obs | Documented with inline comment | </details> > 🤖 This PR was created with the help of Coder Agents, and needs a human review. 🧑💻
6237 lines
230 KiB
Go
6237 lines
230 KiB
Go
package coderd_test
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"database/sql"
|
|
"encoding/json"
|
|
"fmt"
|
|
"math"
|
|
"net/http"
|
|
"slices"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
"github.com/prometheus/client_golang/prometheus"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"cdr.dev/slog/v3"
|
|
"github.com/coder/coder/v2/agent/agenttest"
|
|
"github.com/coder/coder/v2/coderd"
|
|
"github.com/coder/coder/v2/coderd/audit"
|
|
"github.com/coder/coder/v2/coderd/autobuild"
|
|
"github.com/coder/coder/v2/coderd/coderdtest"
|
|
"github.com/coder/coder/v2/coderd/coderdtest/promhelp"
|
|
"github.com/coder/coder/v2/coderd/database"
|
|
"github.com/coder/coder/v2/coderd/database/dbauthz"
|
|
"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/coderd/database/dbtime"
|
|
"github.com/coder/coder/v2/coderd/notifications"
|
|
"github.com/coder/coder/v2/coderd/notifications/notificationstest"
|
|
"github.com/coder/coder/v2/coderd/provisionerdserver"
|
|
"github.com/coder/coder/v2/coderd/rbac"
|
|
"github.com/coder/coder/v2/coderd/rbac/policy"
|
|
"github.com/coder/coder/v2/coderd/render"
|
|
"github.com/coder/coder/v2/coderd/schedule"
|
|
"github.com/coder/coder/v2/coderd/schedule/cron"
|
|
"github.com/coder/coder/v2/coderd/util/ptr"
|
|
"github.com/coder/coder/v2/coderd/util/slice"
|
|
"github.com/coder/coder/v2/coderd/wsbuilder"
|
|
"github.com/coder/coder/v2/codersdk"
|
|
"github.com/coder/coder/v2/cryptorand"
|
|
"github.com/coder/coder/v2/provisioner/echo"
|
|
"github.com/coder/coder/v2/provisionersdk/proto"
|
|
"github.com/coder/coder/v2/testutil"
|
|
"github.com/coder/terraform-provider-coder/v2/provider"
|
|
)
|
|
|
|
func TestWorkspace(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
t.Run("OK", func(t *testing.T) {
|
|
t.Parallel()
|
|
client, _, api := coderdtest.NewWithAPI(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
|
|
user := coderdtest.CreateFirstUser(t, client)
|
|
authz := coderdtest.AssertRBAC(t, api, client)
|
|
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
|
|
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
|
|
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
|
|
workspace := coderdtest.CreateWorkspace(t, client, template.ID)
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
|
defer cancel()
|
|
|
|
authz.Reset() // Reset all previous checks done in setup.
|
|
ws, err := client.Workspace(ctx, workspace.ID)
|
|
authz.AssertChecked(t, policy.ActionRead, ws)
|
|
require.NoError(t, err)
|
|
require.Equal(t, user.UserID, ws.LatestBuild.InitiatorID)
|
|
require.Equal(t, codersdk.BuildReasonInitiator, ws.LatestBuild.Reason)
|
|
|
|
org, err := client.Organization(ctx, ws.OrganizationID)
|
|
require.NoError(t, err)
|
|
require.Equal(t, ws.OrganizationName, org.Name)
|
|
})
|
|
|
|
t.Run("Deleted", func(t *testing.T) {
|
|
t.Parallel()
|
|
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
|
|
user := coderdtest.CreateFirstUser(t, client)
|
|
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
|
|
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
|
|
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
|
|
workspace := coderdtest.CreateWorkspace(t, client, template.ID)
|
|
coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, workspace.LatestBuild.ID)
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
|
defer cancel()
|
|
|
|
// Getting with deleted=true should still work.
|
|
_, err := client.DeletedWorkspace(ctx, workspace.ID)
|
|
require.NoError(t, err)
|
|
|
|
// Delete the workspace
|
|
build, err := client.CreateWorkspaceBuild(ctx, workspace.ID, codersdk.CreateWorkspaceBuildRequest{
|
|
Transition: codersdk.WorkspaceTransitionDelete,
|
|
})
|
|
require.NoError(t, err, "delete the workspace")
|
|
coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, build.ID)
|
|
|
|
// Getting with deleted=true should work.
|
|
workspaceNew, err := client.DeletedWorkspace(ctx, workspace.ID)
|
|
require.NoError(t, err)
|
|
require.Equal(t, workspace.ID, workspaceNew.ID)
|
|
|
|
// Getting with deleted=false should not work.
|
|
_, err = client.Workspace(ctx, workspace.ID)
|
|
require.Error(t, err)
|
|
require.ErrorContains(t, err, "410") // gone
|
|
})
|
|
|
|
t.Run("Rename", func(t *testing.T) {
|
|
t.Parallel()
|
|
client := coderdtest.New(t, &coderdtest.Options{
|
|
IncludeProvisionerDaemon: true,
|
|
AllowWorkspaceRenames: true,
|
|
})
|
|
user := coderdtest.CreateFirstUser(t, client)
|
|
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
|
|
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
|
|
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
|
|
ws1 := coderdtest.CreateWorkspace(t, client, template.ID)
|
|
ws2 := coderdtest.CreateWorkspace(t, client, template.ID)
|
|
coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, ws1.LatestBuild.ID)
|
|
coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, ws2.LatestBuild.ID)
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitMedium)
|
|
defer cancel()
|
|
|
|
want := ws1.Name + "-test"
|
|
if len(want) > 32 {
|
|
want = want[:32-5] + "-test"
|
|
}
|
|
// Sometimes truncated names result in `--test` which is not an allowed name.
|
|
want = strings.ReplaceAll(want, "--", "-")
|
|
err := client.UpdateWorkspace(ctx, ws1.ID, codersdk.UpdateWorkspaceRequest{
|
|
Name: want,
|
|
})
|
|
require.NoError(t, err, "workspace rename failed")
|
|
|
|
ws, err := client.Workspace(ctx, ws1.ID)
|
|
require.NoError(t, err)
|
|
require.Equal(t, want, ws.Name, "workspace name not updated")
|
|
|
|
err = client.UpdateWorkspace(ctx, ws1.ID, codersdk.UpdateWorkspaceRequest{
|
|
Name: ws2.Name,
|
|
})
|
|
require.Error(t, err, "workspace rename should have failed")
|
|
})
|
|
|
|
t.Run("RenameDisabled", func(t *testing.T) {
|
|
t.Parallel()
|
|
client := coderdtest.New(t, &coderdtest.Options{
|
|
IncludeProvisionerDaemon: true,
|
|
AllowWorkspaceRenames: false,
|
|
})
|
|
user := coderdtest.CreateFirstUser(t, client)
|
|
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
|
|
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
|
|
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
|
|
ws1 := coderdtest.CreateWorkspace(t, client, template.ID)
|
|
coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, ws1.LatestBuild.ID)
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitMedium)
|
|
defer cancel()
|
|
|
|
want := "new-name"
|
|
err := client.UpdateWorkspace(ctx, ws1.ID, codersdk.UpdateWorkspaceRequest{
|
|
Name: want,
|
|
})
|
|
require.ErrorContains(t, err, "Workspace renames are not allowed")
|
|
})
|
|
|
|
t.Run("TemplateProperties", func(t *testing.T) {
|
|
t.Parallel()
|
|
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
|
|
user := coderdtest.CreateFirstUser(t, client)
|
|
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
|
|
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
|
|
|
|
const templateIcon = "/img/icon.svg"
|
|
const templateDisplayName = "This is template"
|
|
templateAllowUserCancelWorkspaceJobs := false
|
|
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID, func(ctr *codersdk.CreateTemplateRequest) {
|
|
ctr.Icon = templateIcon
|
|
ctr.DisplayName = templateDisplayName
|
|
ctr.AllowUserCancelWorkspaceJobs = &templateAllowUserCancelWorkspaceJobs
|
|
})
|
|
require.NotEmpty(t, template.Name)
|
|
require.NotEmpty(t, template.DisplayName)
|
|
require.NotEmpty(t, template.Icon)
|
|
require.False(t, template.AllowUserCancelWorkspaceJobs)
|
|
workspace := coderdtest.CreateWorkspace(t, client, template.ID)
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
|
defer cancel()
|
|
|
|
ws, err := client.Workspace(ctx, workspace.ID)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, user.UserID, ws.LatestBuild.InitiatorID)
|
|
assert.Equal(t, codersdk.BuildReasonInitiator, ws.LatestBuild.Reason)
|
|
assert.Equal(t, template.Name, ws.TemplateName)
|
|
assert.Equal(t, templateIcon, ws.TemplateIcon)
|
|
assert.Equal(t, templateDisplayName, ws.TemplateDisplayName)
|
|
assert.Equal(t, templateAllowUserCancelWorkspaceJobs, ws.TemplateAllowUserCancelWorkspaceJobs)
|
|
})
|
|
|
|
t.Run("Health", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
t.Run("Healthy", func(t *testing.T) {
|
|
t.Parallel()
|
|
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
|
|
user := coderdtest.CreateFirstUser(t, client)
|
|
authToken := uuid.NewString()
|
|
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
|
|
Parse: echo.ParseComplete,
|
|
ProvisionGraph: echo.ProvisionGraphWithAgent(authToken),
|
|
})
|
|
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
|
|
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
|
|
workspace := coderdtest.CreateWorkspace(t, client, template.ID)
|
|
coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, workspace.LatestBuild.ID)
|
|
|
|
_ = agenttest.New(t, client.URL, authToken)
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
|
defer cancel()
|
|
|
|
var err error
|
|
testutil.Eventually(ctx, t, func(ctx context.Context) bool {
|
|
workspace, err = client.Workspace(ctx, workspace.ID)
|
|
return assert.NoError(t, err) && workspace.Health.Healthy
|
|
}, testutil.IntervalMedium)
|
|
|
|
agent := workspace.LatestBuild.Resources[0].Agents[0]
|
|
|
|
assert.True(t, workspace.Health.Healthy)
|
|
assert.Equal(t, []uuid.UUID{}, workspace.Health.FailingAgents)
|
|
assert.True(t, agent.Health.Healthy)
|
|
assert.Empty(t, agent.Health.Reason)
|
|
})
|
|
|
|
t.Run("Connecting", func(t *testing.T) {
|
|
t.Parallel()
|
|
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
|
|
user := coderdtest.CreateFirstUser(t, client)
|
|
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
|
|
Parse: echo.ParseComplete,
|
|
ProvisionGraph: []*proto.Response{{
|
|
Type: &proto.Response_Graph{
|
|
Graph: &proto.GraphComplete{
|
|
Resources: []*proto.Resource{{
|
|
Name: "some",
|
|
Type: "example",
|
|
Agents: []*proto.Agent{{
|
|
Id: uuid.NewString(),
|
|
Name: "dev",
|
|
Auth: &proto.Agent_Token{},
|
|
}},
|
|
}},
|
|
},
|
|
},
|
|
}},
|
|
})
|
|
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
|
|
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
|
|
workspace := coderdtest.CreateWorkspace(t, client, template.ID)
|
|
coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, workspace.LatestBuild.ID)
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
|
defer cancel()
|
|
|
|
workspace, err := client.Workspace(ctx, workspace.ID)
|
|
require.NoError(t, err)
|
|
|
|
agent := workspace.LatestBuild.Resources[0].Agents[0]
|
|
|
|
assert.False(t, workspace.Health.Healthy)
|
|
assert.Equal(t, []uuid.UUID{agent.ID}, workspace.Health.FailingAgents)
|
|
assert.False(t, agent.Health.Healthy)
|
|
assert.Equal(t, "agent has not yet connected", agent.Health.Reason)
|
|
})
|
|
|
|
t.Run("Unhealthy", func(t *testing.T) {
|
|
t.Parallel()
|
|
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
|
|
user := coderdtest.CreateFirstUser(t, client)
|
|
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
|
|
Parse: echo.ParseComplete,
|
|
ProvisionGraph: []*proto.Response{{
|
|
Type: &proto.Response_Graph{
|
|
Graph: &proto.GraphComplete{
|
|
Resources: []*proto.Resource{{
|
|
Name: "some",
|
|
Type: "example",
|
|
Agents: []*proto.Agent{{
|
|
Id: uuid.NewString(),
|
|
Name: "dev",
|
|
Auth: &proto.Agent_Token{},
|
|
ConnectionTimeoutSeconds: 1,
|
|
}},
|
|
}},
|
|
},
|
|
},
|
|
}},
|
|
})
|
|
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
|
|
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
|
|
workspace := coderdtest.CreateWorkspace(t, client, template.ID)
|
|
coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, workspace.LatestBuild.ID)
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
|
defer cancel()
|
|
|
|
var err error
|
|
testutil.Eventually(ctx, t, func(ctx context.Context) bool {
|
|
workspace, err = client.Workspace(ctx, workspace.ID)
|
|
return assert.NoError(t, err) && !workspace.Health.Healthy
|
|
}, testutil.IntervalMedium)
|
|
|
|
agent := workspace.LatestBuild.Resources[0].Agents[0]
|
|
|
|
assert.False(t, workspace.Health.Healthy)
|
|
assert.Equal(t, []uuid.UUID{agent.ID}, workspace.Health.FailingAgents)
|
|
assert.False(t, agent.Health.Healthy)
|
|
assert.NotEmpty(t, agent.Health.Reason)
|
|
})
|
|
|
|
t.Run("Mixed health", func(t *testing.T) {
|
|
t.Parallel()
|
|
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
|
|
user := coderdtest.CreateFirstUser(t, client)
|
|
a1AuthToken := uuid.NewString()
|
|
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
|
|
Parse: echo.ParseComplete,
|
|
ProvisionGraph: []*proto.Response{{
|
|
Type: &proto.Response_Graph{
|
|
Graph: &proto.GraphComplete{
|
|
Resources: []*proto.Resource{{
|
|
Name: "some",
|
|
Type: "example",
|
|
Agents: []*proto.Agent{{
|
|
Id: uuid.NewString(),
|
|
Name: "a1",
|
|
Auth: &proto.Agent_Token{
|
|
Token: a1AuthToken,
|
|
},
|
|
}, {
|
|
Id: uuid.NewString(),
|
|
Name: "a2",
|
|
Auth: &proto.Agent_Token{},
|
|
ConnectionTimeoutSeconds: 1,
|
|
}},
|
|
}},
|
|
},
|
|
},
|
|
}},
|
|
})
|
|
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
|
|
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
|
|
workspace := coderdtest.CreateWorkspace(t, client, template.ID)
|
|
coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, workspace.LatestBuild.ID)
|
|
|
|
_ = agenttest.New(t, client.URL, a1AuthToken)
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
|
defer cancel()
|
|
|
|
var err error
|
|
testutil.Eventually(ctx, t, func(ctx context.Context) bool {
|
|
workspace, err = client.Workspace(ctx, workspace.ID)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
// Wait for the mixed state: a1 connected (healthy)
|
|
// and workspace unhealthy (because a2 timed out).
|
|
agent1 := workspace.LatestBuild.Resources[0].Agents[0]
|
|
return agent1.Health.Healthy && !workspace.Health.Healthy
|
|
}, testutil.IntervalMedium)
|
|
|
|
assert.False(t, workspace.Health.Healthy)
|
|
assert.Len(t, workspace.Health.FailingAgents, 1)
|
|
|
|
agent1 := workspace.LatestBuild.Resources[0].Agents[0]
|
|
agent2 := workspace.LatestBuild.Resources[0].Agents[1]
|
|
|
|
assert.Equal(t, []uuid.UUID{agent2.ID}, workspace.Health.FailingAgents)
|
|
assert.True(t, agent1.Health.Healthy)
|
|
assert.Empty(t, agent1.Health.Reason)
|
|
assert.False(t, agent2.Health.Healthy)
|
|
assert.NotEmpty(t, agent2.Health.Reason)
|
|
})
|
|
|
|
t.Run("Sub-agent excluded", func(t *testing.T) {
|
|
t.Parallel()
|
|
// This test verifies that sub-agents (e.g., devcontainer agents)
|
|
// are excluded from the workspace health calculation. When a
|
|
// devcontainer is rebuilding, the sub-agent may be temporarily
|
|
// disconnected, but this should not make the workspace unhealthy.
|
|
client, db := coderdtest.NewWithDatabase(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
|
|
user := coderdtest.CreateFirstUser(t, client)
|
|
authToken := uuid.NewString()
|
|
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
|
|
Parse: echo.ParseComplete,
|
|
ProvisionGraph: []*proto.Response{{
|
|
Type: &proto.Response_Graph{
|
|
Graph: &proto.GraphComplete{
|
|
Resources: []*proto.Resource{{
|
|
Name: "some",
|
|
Type: "example",
|
|
Agents: []*proto.Agent{{
|
|
Id: uuid.NewString(),
|
|
Name: "parent",
|
|
Auth: &proto.Agent_Token{
|
|
Token: authToken,
|
|
},
|
|
}},
|
|
}},
|
|
},
|
|
},
|
|
}},
|
|
})
|
|
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
|
|
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
|
|
workspace := coderdtest.CreateWorkspace(t, client, template.ID)
|
|
coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, workspace.LatestBuild.ID)
|
|
|
|
_ = agenttest.New(t, client.URL, authToken)
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
|
defer cancel()
|
|
|
|
// Wait for the parent agent to connect and be healthy.
|
|
var parentAgent codersdk.WorkspaceAgent
|
|
testutil.Eventually(ctx, t, func(ctx context.Context) bool {
|
|
var err error
|
|
workspace, err = client.Workspace(ctx, workspace.ID)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
parentAgent = workspace.LatestBuild.Resources[0].Agents[0]
|
|
return parentAgent.Health.Healthy
|
|
}, testutil.IntervalMedium)
|
|
require.True(t, parentAgent.Health.Healthy, "parent agent should be healthy")
|
|
|
|
// Create a sub-agent with a short connection timeout so it becomes
|
|
// unhealthy quickly (simulating a devcontainer rebuild scenario).
|
|
subAgent := dbgen.WorkspaceAgent(t, db, database.WorkspaceAgent{
|
|
ParentID: uuid.NullUUID{Valid: true, UUID: parentAgent.ID},
|
|
ResourceID: parentAgent.ResourceID,
|
|
Name: "subagent",
|
|
ConnectionTimeoutSeconds: 1,
|
|
})
|
|
|
|
// Wait for the sub-agent to become unhealthy due to timeout.
|
|
var subAgentUnhealthy bool
|
|
require.Eventually(t, func() bool {
|
|
var err error
|
|
workspace, err = client.Workspace(ctx, workspace.ID)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
for _, res := range workspace.LatestBuild.Resources {
|
|
for _, agent := range res.Agents {
|
|
if agent.ID == subAgent.ID && !agent.Health.Healthy {
|
|
subAgentUnhealthy = true
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
return false
|
|
}, testutil.WaitShort, testutil.IntervalFast, "sub-agent should become unhealthy")
|
|
|
|
require.True(t, subAgentUnhealthy, "sub-agent should be unhealthy")
|
|
|
|
// Verify that the workspace is still healthy because sub-agents
|
|
// are excluded from the health calculation.
|
|
assert.True(t, workspace.Health.Healthy, "workspace should be healthy despite unhealthy sub-agent")
|
|
assert.Empty(t, workspace.Health.FailingAgents, "failing agents should not include sub-agent")
|
|
})
|
|
})
|
|
|
|
t.Run("Archived", func(t *testing.T) {
|
|
t.Parallel()
|
|
ownerClient := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
|
|
owner := coderdtest.CreateFirstUser(t, ownerClient)
|
|
|
|
client, _ := coderdtest.CreateAnotherUser(t, ownerClient, owner.OrganizationID, rbac.RoleTemplateAdmin())
|
|
|
|
active := coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID, nil)
|
|
coderdtest.AwaitTemplateVersionJobCompleted(t, client, active.ID)
|
|
template := coderdtest.CreateTemplate(t, client, owner.OrganizationID, active.ID)
|
|
// We need another version because the active template version cannot be
|
|
// archived.
|
|
version := coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID, nil, func(request *codersdk.CreateTemplateVersionRequest) {
|
|
request.TemplateID = template.ID
|
|
})
|
|
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
|
|
|
|
ctx := testutil.Context(t, testutil.WaitMedium)
|
|
|
|
err := client.SetArchiveTemplateVersion(ctx, version.ID, true)
|
|
require.NoError(t, err, "archive version")
|
|
|
|
_, err = client.CreateWorkspace(ctx, owner.OrganizationID, codersdk.Me, codersdk.CreateWorkspaceRequest{
|
|
TemplateVersionID: version.ID,
|
|
Name: "testworkspace",
|
|
})
|
|
require.Error(t, err, "create workspace with archived version")
|
|
require.ErrorContains(t, err, "Archived template versions cannot")
|
|
})
|
|
|
|
t.Run("WorkspaceBan", func(t *testing.T) {
|
|
t.Parallel()
|
|
owner, _, _ := coderdtest.NewWithAPI(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
|
|
first := coderdtest.CreateFirstUser(t, owner)
|
|
|
|
version := coderdtest.CreateTemplateVersion(t, owner, first.OrganizationID, nil)
|
|
coderdtest.AwaitTemplateVersionJobCompleted(t, owner, version.ID)
|
|
template := coderdtest.CreateTemplate(t, owner, first.OrganizationID, version.ID)
|
|
|
|
goodClient, _ := coderdtest.CreateAnotherUser(t, owner, first.OrganizationID)
|
|
|
|
// When a user with workspace-creation-ban
|
|
client, user := coderdtest.CreateAnotherUser(t, owner, first.OrganizationID, rbac.ScopedRoleOrgWorkspaceCreationBan(first.OrganizationID))
|
|
|
|
// Ensure a similar user can create a workspace
|
|
coderdtest.CreateWorkspace(t, goodClient, template.ID)
|
|
|
|
ctx := testutil.Context(t, testutil.WaitLong)
|
|
// Then: Cannot create a workspace
|
|
_, err := client.CreateUserWorkspace(ctx, codersdk.Me, codersdk.CreateWorkspaceRequest{
|
|
TemplateID: template.ID,
|
|
TemplateVersionID: uuid.UUID{},
|
|
Name: "random",
|
|
})
|
|
require.Error(t, err)
|
|
var apiError *codersdk.Error
|
|
require.ErrorAs(t, err, &apiError)
|
|
require.Equal(t, http.StatusForbidden, apiError.StatusCode())
|
|
|
|
// When: workspace-ban use has a workspace
|
|
wrk, err := owner.CreateUserWorkspace(ctx, user.ID.String(), codersdk.CreateWorkspaceRequest{
|
|
TemplateID: template.ID,
|
|
TemplateVersionID: uuid.UUID{},
|
|
Name: "random",
|
|
})
|
|
require.NoError(t, err)
|
|
coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, wrk.LatestBuild.ID)
|
|
|
|
// Then: They cannot delete said workspace
|
|
_, err = client.CreateWorkspaceBuild(ctx, wrk.ID, codersdk.CreateWorkspaceBuildRequest{
|
|
Transition: codersdk.WorkspaceTransitionDelete,
|
|
ProvisionerState: []byte{},
|
|
})
|
|
require.Error(t, err)
|
|
require.ErrorAs(t, err, &apiError)
|
|
require.Equal(t, http.StatusForbidden, apiError.StatusCode())
|
|
})
|
|
|
|
t.Run("TemplateVersionPreset", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
// Test Utility variables
|
|
templateVersionParameters := []*proto.RichParameter{
|
|
{Name: "param1", Type: "string", Required: false, DefaultValue: "default1"},
|
|
{Name: "param2", Type: "string", Required: false, DefaultValue: "default2"},
|
|
{Name: "param3", Type: "string", Required: false, DefaultValue: "default3"},
|
|
}
|
|
presetParameters := []*proto.PresetParameter{
|
|
{Name: "param1", Value: "value1"},
|
|
{Name: "param2", Value: "value2"},
|
|
{Name: "param3", Value: "value3"},
|
|
}
|
|
emptyPreset := &proto.Preset{
|
|
Name: "Empty Preset",
|
|
}
|
|
presetWithParameters := &proto.Preset{
|
|
Name: "Preset With Parameters",
|
|
Parameters: presetParameters,
|
|
}
|
|
|
|
testCases := []struct {
|
|
name string
|
|
presets []*proto.Preset
|
|
templateVersionParameters []*proto.RichParameter
|
|
selectedPresetIndex *int
|
|
}{
|
|
{
|
|
name: "No Presets - No Template Parameters",
|
|
presets: []*proto.Preset{},
|
|
},
|
|
{
|
|
name: "No Presets - With Template Parameters",
|
|
presets: []*proto.Preset{},
|
|
templateVersionParameters: templateVersionParameters,
|
|
},
|
|
{
|
|
name: "Single Preset - No Preset Parameters But With Template Parameters",
|
|
presets: []*proto.Preset{emptyPreset},
|
|
templateVersionParameters: templateVersionParameters,
|
|
selectedPresetIndex: ptr.Ref(0),
|
|
},
|
|
{
|
|
name: "Single Preset - No Preset Parameters And No Template Parameters",
|
|
presets: []*proto.Preset{emptyPreset},
|
|
selectedPresetIndex: ptr.Ref(0),
|
|
},
|
|
{
|
|
name: "Single Preset - With Preset Parameters But No Template Parameters",
|
|
presets: []*proto.Preset{presetWithParameters},
|
|
selectedPresetIndex: ptr.Ref(0),
|
|
},
|
|
{
|
|
name: "Single Preset - With Matching Parameters",
|
|
presets: []*proto.Preset{presetWithParameters},
|
|
templateVersionParameters: templateVersionParameters,
|
|
selectedPresetIndex: ptr.Ref(0),
|
|
},
|
|
{
|
|
name: "Single Preset - With Partial Matching Parameters",
|
|
presets: []*proto.Preset{{
|
|
Name: "test",
|
|
Parameters: presetParameters,
|
|
}},
|
|
templateVersionParameters: templateVersionParameters[:2],
|
|
selectedPresetIndex: ptr.Ref(0),
|
|
},
|
|
{
|
|
name: "Multiple Presets - No Parameters",
|
|
presets: []*proto.Preset{
|
|
{Name: "preset1"},
|
|
{Name: "preset2"},
|
|
{Name: "preset3"},
|
|
},
|
|
selectedPresetIndex: ptr.Ref(0),
|
|
},
|
|
{
|
|
name: "Multiple Presets - First Has Parameters",
|
|
presets: []*proto.Preset{
|
|
{
|
|
Name: "preset1",
|
|
Parameters: presetParameters,
|
|
},
|
|
{Name: "preset2"},
|
|
{Name: "preset3"},
|
|
},
|
|
selectedPresetIndex: ptr.Ref(0),
|
|
},
|
|
{
|
|
name: "Multiple Presets - First Has Matching Parameters",
|
|
presets: []*proto.Preset{
|
|
presetWithParameters,
|
|
{Name: "preset2"},
|
|
{Name: "preset3"},
|
|
},
|
|
templateVersionParameters: templateVersionParameters,
|
|
selectedPresetIndex: ptr.Ref(0),
|
|
},
|
|
{
|
|
name: "Multiple Presets - Middle Has Parameters",
|
|
presets: []*proto.Preset{
|
|
{Name: "preset1"},
|
|
presetWithParameters,
|
|
{Name: "preset3"},
|
|
},
|
|
selectedPresetIndex: ptr.Ref(1),
|
|
},
|
|
{
|
|
name: "Multiple Presets - Middle Has Matching Parameters",
|
|
presets: []*proto.Preset{
|
|
{Name: "preset1"},
|
|
presetWithParameters,
|
|
{Name: "preset3"},
|
|
},
|
|
templateVersionParameters: templateVersionParameters,
|
|
selectedPresetIndex: ptr.Ref(1),
|
|
},
|
|
{
|
|
name: "Multiple Presets - Last Has Parameters",
|
|
presets: []*proto.Preset{
|
|
{Name: "preset1"},
|
|
{Name: "preset2"},
|
|
presetWithParameters,
|
|
},
|
|
selectedPresetIndex: ptr.Ref(2),
|
|
},
|
|
{
|
|
name: "Multiple Presets - Last Has Matching Parameters",
|
|
presets: []*proto.Preset{
|
|
{Name: "preset1"},
|
|
{Name: "preset2"},
|
|
presetWithParameters,
|
|
},
|
|
templateVersionParameters: templateVersionParameters,
|
|
selectedPresetIndex: ptr.Ref(2),
|
|
},
|
|
{
|
|
name: "Multiple Presets - All Have Parameters",
|
|
presets: []*proto.Preset{
|
|
{
|
|
Name: "preset1",
|
|
Parameters: presetParameters[:1],
|
|
},
|
|
{
|
|
Name: "preset2",
|
|
Parameters: presetParameters[1:2],
|
|
},
|
|
{
|
|
Name: "preset3",
|
|
Parameters: presetParameters[2:3],
|
|
},
|
|
},
|
|
selectedPresetIndex: ptr.Ref(1),
|
|
},
|
|
{
|
|
name: "Multiple Presets - All Have Partially Matching Parameters",
|
|
presets: []*proto.Preset{
|
|
{
|
|
Name: "preset1",
|
|
Parameters: presetParameters[:1],
|
|
},
|
|
{
|
|
Name: "preset2",
|
|
Parameters: presetParameters[1:2],
|
|
},
|
|
{
|
|
Name: "preset3",
|
|
Parameters: presetParameters[2:3],
|
|
},
|
|
},
|
|
templateVersionParameters: templateVersionParameters,
|
|
selectedPresetIndex: ptr.Ref(1),
|
|
},
|
|
{
|
|
name: "Multiple presets - With Overlapping Matching Parameters",
|
|
presets: []*proto.Preset{
|
|
{
|
|
Name: "preset1",
|
|
Parameters: []*proto.PresetParameter{
|
|
{Name: "param1", Value: "expectedValue1"},
|
|
{Name: "param2", Value: "expectedValue2"},
|
|
},
|
|
},
|
|
{
|
|
Name: "preset2",
|
|
Parameters: []*proto.PresetParameter{
|
|
{Name: "param1", Value: "incorrectValue1"},
|
|
{Name: "param2", Value: "incorrectValue2"},
|
|
},
|
|
},
|
|
},
|
|
templateVersionParameters: templateVersionParameters,
|
|
selectedPresetIndex: ptr.Ref(0),
|
|
},
|
|
{
|
|
name: "Multiple Presets - With Parameters But Not Used",
|
|
presets: []*proto.Preset{
|
|
{
|
|
Name: "preset1",
|
|
Parameters: presetParameters[:1],
|
|
},
|
|
{
|
|
Name: "preset2",
|
|
Parameters: presetParameters[1:2],
|
|
},
|
|
},
|
|
templateVersionParameters: templateVersionParameters,
|
|
},
|
|
{
|
|
name: "Multiple Presets - With Matching Parameters But Not Used",
|
|
presets: []*proto.Preset{
|
|
{
|
|
Name: "preset1",
|
|
Parameters: presetParameters[:1],
|
|
},
|
|
{
|
|
Name: "preset2",
|
|
Parameters: presetParameters[1:2],
|
|
},
|
|
},
|
|
templateVersionParameters: templateVersionParameters[0:2],
|
|
},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
client, _, api := coderdtest.NewWithAPI(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
|
|
user := coderdtest.CreateFirstUser(t, client)
|
|
authz := coderdtest.AssertRBAC(t, api, client)
|
|
|
|
// Create a plan response with the specified presets and parameters
|
|
graphResponse := &proto.Response{
|
|
Type: &proto.Response_Graph{
|
|
Graph: &proto.GraphComplete{
|
|
Presets: tc.presets,
|
|
Parameters: tc.templateVersionParameters,
|
|
},
|
|
},
|
|
}
|
|
|
|
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
|
|
Parse: echo.ParseComplete,
|
|
ProvisionGraph: []*proto.Response{graphResponse},
|
|
ProvisionApply: echo.ApplyComplete,
|
|
})
|
|
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
|
|
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
|
|
|
|
ctx := testutil.Context(t, testutil.WaitLong)
|
|
|
|
// Check createdPresets
|
|
createdPresets, err := client.TemplateVersionPresets(ctx, version.ID)
|
|
require.NoError(t, err)
|
|
require.Equal(t, len(tc.presets), len(createdPresets))
|
|
|
|
for _, createdPreset := range createdPresets {
|
|
presetIndex := slices.IndexFunc(tc.presets, func(expectedPreset *proto.Preset) bool {
|
|
return expectedPreset.Name == createdPreset.Name
|
|
})
|
|
require.NotEqual(t, -1, presetIndex, "Preset %s should be present", createdPreset.Name)
|
|
|
|
// Verify that the preset has the expected parameters
|
|
for _, expectedPresetParam := range tc.presets[presetIndex].Parameters {
|
|
paramFoundAtIndex := slices.IndexFunc(createdPreset.Parameters, func(createdPresetParam codersdk.PresetParameter) bool {
|
|
return expectedPresetParam.Name == createdPresetParam.Name && expectedPresetParam.Value == createdPresetParam.Value
|
|
})
|
|
require.NotEqual(t, -1, paramFoundAtIndex, "Parameter %s should be present in preset", expectedPresetParam.Name)
|
|
}
|
|
}
|
|
|
|
// Create workspace with or without preset
|
|
var workspace codersdk.Workspace
|
|
if tc.selectedPresetIndex != nil {
|
|
// Use the selected preset
|
|
workspace = coderdtest.CreateWorkspace(t, client, template.ID, func(request *codersdk.CreateWorkspaceRequest) {
|
|
request.TemplateVersionPresetID = createdPresets[*tc.selectedPresetIndex].ID
|
|
})
|
|
} else {
|
|
workspace = coderdtest.CreateWorkspace(t, client, template.ID)
|
|
}
|
|
|
|
// Verify workspace details
|
|
authz.Reset() // Reset all previous checks done in setup.
|
|
ws, err := client.Workspace(ctx, workspace.ID)
|
|
authz.AssertChecked(t, policy.ActionRead, ws)
|
|
require.NoError(t, err)
|
|
require.Equal(t, user.UserID, ws.LatestBuild.InitiatorID)
|
|
require.Equal(t, codersdk.BuildReasonInitiator, ws.LatestBuild.Reason)
|
|
|
|
// Check that the preset ID is set if expected
|
|
require.Equal(t, tc.selectedPresetIndex == nil, ws.LatestBuild.TemplateVersionPresetID == nil)
|
|
|
|
if tc.selectedPresetIndex == nil {
|
|
// No preset selected, so no further checks are needed
|
|
// Pre-preset tests cover this case sufficiently.
|
|
return
|
|
}
|
|
|
|
// If we get here, we expect a preset to be selected.
|
|
// So we need to assert that selecting the preset had all the correct consequences.
|
|
require.Equal(t, createdPresets[*tc.selectedPresetIndex].ID, *ws.LatestBuild.TemplateVersionPresetID)
|
|
|
|
selectedPresetParameters := tc.presets[*tc.selectedPresetIndex].Parameters
|
|
|
|
// Get parameters that were applied to the latest workspace build
|
|
builds, err := client.WorkspaceBuilds(ctx, codersdk.WorkspaceBuildsRequest{
|
|
WorkspaceID: ws.ID,
|
|
})
|
|
require.NoError(t, err)
|
|
require.Equal(t, 1, len(builds))
|
|
gotWorkspaceBuildParameters, err := client.WorkspaceBuildParameters(ctx, builds[0].ID)
|
|
require.NoError(t, err)
|
|
|
|
// Count how many parameters were set by the preset
|
|
parametersSetByPreset := slice.CountMatchingPairs(
|
|
gotWorkspaceBuildParameters,
|
|
selectedPresetParameters,
|
|
func(gotParameter codersdk.WorkspaceBuildParameter, presetParameter *proto.PresetParameter) bool {
|
|
namesMatch := gotParameter.Name == presetParameter.Name
|
|
valuesMatch := gotParameter.Value == presetParameter.Value
|
|
return namesMatch && valuesMatch
|
|
},
|
|
)
|
|
|
|
// Count how many parameters should have been set by the preset
|
|
expectedParamCount := slice.CountMatchingPairs(
|
|
selectedPresetParameters,
|
|
tc.templateVersionParameters,
|
|
func(presetParam *proto.PresetParameter, templateParam *proto.RichParameter) bool {
|
|
return presetParam.Name == templateParam.Name
|
|
},
|
|
)
|
|
|
|
// Verify that only the expected number of parameters were set by the preset
|
|
require.Equal(t, expectedParamCount, parametersSetByPreset,
|
|
"Expected %d parameters to be set, but found %d", expectedParamCount, parametersSetByPreset)
|
|
})
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestResolveAutostart(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
ownerClient, db := coderdtest.NewWithDatabase(t, nil)
|
|
owner := coderdtest.CreateFirstUser(t, ownerClient)
|
|
|
|
param := database.TemplateVersionParameter{
|
|
Name: "param",
|
|
DefaultValue: "",
|
|
Required: true,
|
|
}
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
|
defer cancel()
|
|
|
|
client, member := coderdtest.CreateAnotherUser(t, ownerClient, owner.OrganizationID)
|
|
resp := dbfake.WorkspaceBuild(t, db, database.WorkspaceTable{
|
|
OwnerID: member.ID,
|
|
OrganizationID: owner.OrganizationID,
|
|
AutomaticUpdates: database.AutomaticUpdatesAlways,
|
|
}).Seed(database.WorkspaceBuild{
|
|
InitiatorID: member.ID,
|
|
}).Do()
|
|
|
|
workspace := resp.Workspace
|
|
version1 := resp.TemplateVersion
|
|
|
|
version2 := dbfake.TemplateVersion(t, db).
|
|
Seed(database.TemplateVersion{
|
|
CreatedBy: owner.UserID,
|
|
OrganizationID: owner.OrganizationID,
|
|
TemplateID: version1.TemplateID,
|
|
}).
|
|
Params(param).Do()
|
|
|
|
// Autostart shouldn't be possible if parameters do not match.
|
|
resolveResp, err := client.ResolveAutostart(ctx, workspace.ID.String())
|
|
require.NoError(t, err)
|
|
require.True(t, resolveResp.ParameterMismatch)
|
|
|
|
_ = dbfake.WorkspaceBuild(t, db, workspace).
|
|
Seed(database.WorkspaceBuild{
|
|
BuildNumber: 2,
|
|
TemplateVersionID: version2.TemplateVersion.ID,
|
|
}).
|
|
Params(database.WorkspaceBuildParameter{
|
|
Name: "param",
|
|
Value: "hello",
|
|
}).Do()
|
|
|
|
// We should be able to autostart since parameters are updated.
|
|
resolveResp, err = client.ResolveAutostart(ctx, workspace.ID.String())
|
|
require.NoError(t, err)
|
|
require.False(t, resolveResp.ParameterMismatch)
|
|
|
|
// Create another version that has the same parameters as version2.
|
|
// We should be able to update without issue.
|
|
_ = dbfake.TemplateVersion(t, db).Seed(database.TemplateVersion{
|
|
CreatedBy: owner.UserID,
|
|
OrganizationID: owner.OrganizationID,
|
|
TemplateID: version1.TemplateID,
|
|
}).Params(param).Do()
|
|
|
|
// Even though we're out of date we should still be able to autostart
|
|
// since parameters resolve.
|
|
resolveResp, err = client.ResolveAutostart(ctx, workspace.ID.String())
|
|
require.NoError(t, err)
|
|
require.False(t, resolveResp.ParameterMismatch)
|
|
}
|
|
|
|
func TestWorkspacesSortOrder(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
client, db := coderdtest.NewWithDatabase(t, nil)
|
|
firstUser := coderdtest.CreateFirstUser(t, client)
|
|
secondUserClient, secondUser := coderdtest.CreateAnotherUserMutators(t, client, firstUser.OrganizationID, []rbac.RoleIdentifier{rbac.RoleOwner()}, func(r *codersdk.CreateUserRequestWithOrgs) {
|
|
r.Username = "zzz"
|
|
})
|
|
|
|
// c-workspace should be running
|
|
wsbC := dbfake.WorkspaceBuild(t, db, database.WorkspaceTable{Name: "c-workspace", OwnerID: firstUser.UserID, OrganizationID: firstUser.OrganizationID}).Do()
|
|
|
|
// b-workspace should be stopped
|
|
wsbB := dbfake.WorkspaceBuild(t, db, database.WorkspaceTable{Name: "b-workspace", OwnerID: firstUser.UserID, OrganizationID: firstUser.OrganizationID}).Seed(database.WorkspaceBuild{Transition: database.WorkspaceTransitionStop}).Do()
|
|
|
|
// a-workspace should be running
|
|
wsbA := dbfake.WorkspaceBuild(t, db, database.WorkspaceTable{Name: "a-workspace", OwnerID: firstUser.UserID, OrganizationID: firstUser.OrganizationID}).Do()
|
|
|
|
// d-workspace should be stopped
|
|
wsbD := dbfake.WorkspaceBuild(t, db, database.WorkspaceTable{Name: "d-workspace", OwnerID: secondUser.ID, OrganizationID: firstUser.OrganizationID}).Seed(database.WorkspaceBuild{Transition: database.WorkspaceTransitionStop}).Do()
|
|
|
|
// e-workspace should also be stopped
|
|
wsbE := dbfake.WorkspaceBuild(t, db, database.WorkspaceTable{Name: "e-workspace", OwnerID: secondUser.ID, OrganizationID: firstUser.OrganizationID}).Seed(database.WorkspaceBuild{Transition: database.WorkspaceTransitionStop}).Do()
|
|
|
|
// f-workspace is also stopped, but is marked as favorite
|
|
wsbF := dbfake.WorkspaceBuild(t, db, database.WorkspaceTable{Name: "f-workspace", OwnerID: firstUser.UserID, OrganizationID: firstUser.OrganizationID}).Seed(database.WorkspaceBuild{Transition: database.WorkspaceTransitionStop}).Do()
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
|
defer cancel()
|
|
require.NoError(t, client.FavoriteWorkspace(ctx, wsbF.Workspace.ID)) // need to do this via API call for now
|
|
|
|
workspacesResponse, err := client.Workspaces(ctx, codersdk.WorkspaceFilter{})
|
|
require.NoError(t, err, "(first) fetch workspaces")
|
|
workspaces := workspacesResponse.Workspaces
|
|
|
|
expectedNames := []string{
|
|
wsbF.Workspace.Name, // favorite
|
|
wsbA.Workspace.Name, // running
|
|
wsbC.Workspace.Name, // running
|
|
wsbB.Workspace.Name, // stopped, testuser < zzz
|
|
wsbD.Workspace.Name, // stopped, zzz > testuser
|
|
wsbE.Workspace.Name, // stopped, zzz > testuser
|
|
}
|
|
|
|
actualNames := make([]string, 0, len(expectedNames))
|
|
for _, w := range workspaces {
|
|
actualNames = append(actualNames, w.Name)
|
|
}
|
|
|
|
// the correct sorting order is:
|
|
// 1. Favorite workspaces (we have one, workspace-f)
|
|
// 2. Running workspaces
|
|
// 3. Sort by usernames
|
|
// 4. Sort by workspace names
|
|
assert.Equal(t, expectedNames, actualNames)
|
|
|
|
// Once again but this time as a different user. This time we do not expect to see another
|
|
// user's favorites first.
|
|
workspacesResponse, err = secondUserClient.Workspaces(ctx, codersdk.WorkspaceFilter{})
|
|
require.NoError(t, err, "(second) fetch workspaces")
|
|
workspaces = workspacesResponse.Workspaces
|
|
|
|
expectedNames = []string{
|
|
wsbA.Workspace.Name, // running
|
|
wsbC.Workspace.Name, // running
|
|
wsbB.Workspace.Name, // stopped, testuser < zzz
|
|
wsbF.Workspace.Name, // stopped, testuser < zzz
|
|
wsbD.Workspace.Name, // stopped, zzz > testuser
|
|
wsbE.Workspace.Name, // stopped, zzz > testuser
|
|
}
|
|
|
|
actualNames = make([]string, 0, len(expectedNames))
|
|
for _, w := range workspaces {
|
|
actualNames = append(actualNames, w.Name)
|
|
}
|
|
|
|
// the correct sorting order is:
|
|
// 1. Favorite workspaces (we have none this time)
|
|
// 2. Running workspaces
|
|
// 3. Sort by usernames
|
|
// 4. Sort by workspace names
|
|
assert.Equal(t, expectedNames, actualNames)
|
|
}
|
|
|
|
func TestPostWorkspacesByOrganization(t *testing.T) {
|
|
t.Parallel()
|
|
t.Run("InvalidTemplate", func(t *testing.T) {
|
|
t.Parallel()
|
|
client := coderdtest.New(t, nil)
|
|
user := coderdtest.CreateFirstUser(t, client)
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
|
defer cancel()
|
|
|
|
_, err := client.CreateWorkspace(ctx, user.OrganizationID, codersdk.Me, codersdk.CreateWorkspaceRequest{
|
|
TemplateID: uuid.New(),
|
|
Name: "workspace",
|
|
})
|
|
require.Error(t, err)
|
|
var apiErr *codersdk.Error
|
|
require.ErrorAs(t, err, &apiErr)
|
|
require.Equal(t, http.StatusBadRequest, apiErr.StatusCode())
|
|
})
|
|
|
|
t.Run("AlreadyExists", func(t *testing.T) {
|
|
t.Parallel()
|
|
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
|
|
user := coderdtest.CreateFirstUser(t, client)
|
|
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
|
|
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
|
|
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
|
|
workspace := coderdtest.CreateWorkspace(t, client, template.ID)
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
|
defer cancel()
|
|
|
|
_, err := client.CreateWorkspace(ctx, user.OrganizationID, codersdk.Me, codersdk.CreateWorkspaceRequest{
|
|
TemplateID: template.ID,
|
|
Name: workspace.Name,
|
|
})
|
|
require.Error(t, err)
|
|
var apiErr *codersdk.Error
|
|
require.ErrorAs(t, err, &apiErr)
|
|
require.Equal(t, http.StatusConflict, apiErr.StatusCode())
|
|
})
|
|
|
|
t.Run("CreateSendsNotification", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
enqueuer := notificationstest.FakeEnqueuer{}
|
|
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true, NotificationsEnqueuer: &enqueuer})
|
|
user := coderdtest.CreateFirstUser(t, client)
|
|
templateAdminClient, templateAdmin := coderdtest.CreateAnotherUser(t, client, user.OrganizationID, rbac.RoleTemplateAdmin())
|
|
memberClient, memberUser := coderdtest.CreateAnotherUser(t, client, user.OrganizationID)
|
|
|
|
version := coderdtest.CreateTemplateVersion(t, templateAdminClient, user.OrganizationID, nil)
|
|
template := coderdtest.CreateTemplate(t, templateAdminClient, user.OrganizationID, version.ID)
|
|
coderdtest.AwaitTemplateVersionJobCompleted(t, templateAdminClient, version.ID)
|
|
|
|
workspace := coderdtest.CreateWorkspace(t, memberClient, template.ID)
|
|
coderdtest.AwaitWorkspaceBuildJobCompleted(t, memberClient, workspace.LatestBuild.ID)
|
|
|
|
sent := enqueuer.Sent(notificationstest.WithTemplateID(notifications.TemplateWorkspaceCreated))
|
|
require.Len(t, sent, 2)
|
|
|
|
receivers := make([]uuid.UUID, len(sent))
|
|
for idx, notif := range sent {
|
|
receivers[idx] = notif.UserID
|
|
}
|
|
|
|
// Check the notification was sent to the first user and template admin
|
|
require.Contains(t, receivers, templateAdmin.ID)
|
|
require.Contains(t, receivers, user.UserID)
|
|
require.NotContains(t, receivers, memberUser.ID)
|
|
|
|
require.Contains(t, sent[0].Targets, template.ID)
|
|
require.Contains(t, sent[0].Targets, workspace.ID)
|
|
require.Contains(t, sent[0].Targets, workspace.OrganizationID)
|
|
require.Contains(t, sent[0].Targets, workspace.OwnerID)
|
|
|
|
require.Contains(t, sent[1].Targets, template.ID)
|
|
require.Contains(t, sent[1].Targets, workspace.ID)
|
|
require.Contains(t, sent[1].Targets, workspace.OrganizationID)
|
|
require.Contains(t, sent[1].Targets, workspace.OwnerID)
|
|
})
|
|
|
|
t.Run("CreateSendsNotificationToCorrectUser", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
enqueuer := notificationstest.FakeEnqueuer{}
|
|
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true, NotificationsEnqueuer: &enqueuer})
|
|
user := coderdtest.CreateFirstUser(t, client)
|
|
templateAdminClient, _ := coderdtest.CreateAnotherUser(t, client, user.OrganizationID, rbac.RoleTemplateAdmin(), rbac.RoleOwner())
|
|
_, memberUser := coderdtest.CreateAnotherUser(t, client, user.OrganizationID)
|
|
|
|
version := coderdtest.CreateTemplateVersion(t, templateAdminClient, user.OrganizationID, nil)
|
|
template := coderdtest.CreateTemplate(t, templateAdminClient, user.OrganizationID, version.ID)
|
|
coderdtest.AwaitTemplateVersionJobCompleted(t, templateAdminClient, version.ID)
|
|
|
|
ctx := testutil.Context(t, testutil.WaitShort)
|
|
workspace, err := templateAdminClient.CreateUserWorkspace(ctx, memberUser.Username, codersdk.CreateWorkspaceRequest{
|
|
TemplateID: template.ID,
|
|
Name: coderdtest.RandomUsername(t),
|
|
})
|
|
require.NoError(t, err)
|
|
coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, workspace.LatestBuild.ID)
|
|
|
|
sent := enqueuer.Sent(notificationstest.WithTemplateID(notifications.TemplateWorkspaceCreated))
|
|
require.Len(t, sent, 1)
|
|
require.Equal(t, user.UserID, sent[0].UserID)
|
|
require.Contains(t, sent[0].Targets, template.ID)
|
|
require.Contains(t, sent[0].Targets, workspace.ID)
|
|
require.Contains(t, sent[0].Targets, workspace.OrganizationID)
|
|
require.Contains(t, sent[0].Targets, workspace.OwnerID)
|
|
|
|
owner, ok := sent[0].Data["owner"].(map[string]any)
|
|
require.True(t, ok, "notification data should have owner")
|
|
require.Equal(t, memberUser.ID, owner["id"])
|
|
require.Equal(t, memberUser.Name, owner["name"])
|
|
require.Equal(t, memberUser.Email, owner["email"])
|
|
})
|
|
|
|
t.Run("CreateWithAuditLogs", func(t *testing.T) {
|
|
t.Parallel()
|
|
auditor := audit.NewMock()
|
|
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true, Auditor: auditor})
|
|
user := coderdtest.CreateFirstUser(t, client)
|
|
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
|
|
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
|
|
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
|
|
workspace := coderdtest.CreateWorkspace(t, client, template.ID)
|
|
coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, workspace.LatestBuild.ID)
|
|
assert.True(t, auditor.Contains(t, database.AuditLog{
|
|
ResourceType: database.ResourceTypeWorkspace,
|
|
Action: database.AuditActionCreate,
|
|
ResourceTarget: workspace.Name,
|
|
}))
|
|
})
|
|
|
|
t.Run("CreateFromVersionWithAuditLogs", func(t *testing.T) {
|
|
t.Parallel()
|
|
auditor := audit.NewMock()
|
|
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true, Auditor: auditor})
|
|
user := coderdtest.CreateFirstUser(t, client)
|
|
versionDefault := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
|
|
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, versionDefault.ID)
|
|
versionTest := coderdtest.UpdateTemplateVersion(t, client, user.OrganizationID, nil, template.ID)
|
|
coderdtest.AwaitTemplateVersionJobCompleted(t, client, versionDefault.ID)
|
|
coderdtest.AwaitTemplateVersionJobCompleted(t, client, versionTest.ID)
|
|
defaultWorkspace := coderdtest.CreateWorkspace(t, client, uuid.Nil,
|
|
func(c *codersdk.CreateWorkspaceRequest) { c.TemplateVersionID = versionDefault.ID },
|
|
)
|
|
testWorkspace := coderdtest.CreateWorkspace(t, client, uuid.Nil,
|
|
func(c *codersdk.CreateWorkspaceRequest) { c.TemplateVersionID = versionTest.ID },
|
|
)
|
|
defaultWorkspaceBuild := coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, defaultWorkspace.LatestBuild.ID)
|
|
testWorkspaceBuild := coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, testWorkspace.LatestBuild.ID)
|
|
|
|
require.Equal(t, testWorkspaceBuild.TemplateVersionID, versionTest.ID)
|
|
require.Equal(t, defaultWorkspaceBuild.TemplateVersionID, versionDefault.ID)
|
|
assert.True(t, auditor.Contains(t, database.AuditLog{
|
|
ResourceType: database.ResourceTypeWorkspace,
|
|
Action: database.AuditActionCreate,
|
|
ResourceTarget: defaultWorkspace.Name,
|
|
}))
|
|
})
|
|
|
|
t.Run("InvalidCombinationOfTemplateAndTemplateVersion", func(t *testing.T) {
|
|
t.Parallel()
|
|
auditor := audit.NewMock()
|
|
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true, Auditor: auditor})
|
|
user := coderdtest.CreateFirstUser(t, client)
|
|
versionTest := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
|
|
versionDefault := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
|
|
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, versionDefault.ID)
|
|
coderdtest.AwaitTemplateVersionJobCompleted(t, client, versionTest.ID)
|
|
coderdtest.AwaitTemplateVersionJobCompleted(t, client, versionDefault.ID)
|
|
|
|
name, se := cryptorand.String(8)
|
|
require.NoError(t, se)
|
|
req := codersdk.CreateWorkspaceRequest{
|
|
// Deny setting both of these ID fields, even if they might correlate.
|
|
// Allowing both to be set would just create extra work for everyone involved.
|
|
TemplateID: template.ID,
|
|
TemplateVersionID: versionTest.ID,
|
|
Name: name,
|
|
AutostartSchedule: ptr.Ref("CRON_TZ=US/Central 30 9 * * 1-5"),
|
|
TTLMillis: ptr.Ref((8 * time.Hour).Milliseconds()),
|
|
}
|
|
_, err := client.CreateWorkspace(context.Background(), user.OrganizationID, codersdk.Me, req)
|
|
|
|
require.Error(t, err)
|
|
})
|
|
|
|
t.Run("CreateWithDeletedTemplate", func(t *testing.T) {
|
|
t.Parallel()
|
|
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
|
|
user := coderdtest.CreateFirstUser(t, client)
|
|
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
|
|
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
|
|
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
|
|
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
|
defer cancel()
|
|
err := client.DeleteTemplate(ctx, template.ID)
|
|
require.NoError(t, err)
|
|
_, err = client.CreateWorkspace(ctx, user.OrganizationID, codersdk.Me, codersdk.CreateWorkspaceRequest{
|
|
TemplateID: template.ID,
|
|
Name: "testing",
|
|
})
|
|
require.Error(t, err)
|
|
var apiErr *codersdk.Error
|
|
require.ErrorAs(t, err, &apiErr)
|
|
require.Equal(t, http.StatusNotFound, apiErr.StatusCode())
|
|
})
|
|
|
|
t.Run("TemplateNoTTL", func(t *testing.T) {
|
|
t.Parallel()
|
|
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
|
|
user := coderdtest.CreateFirstUser(t, client)
|
|
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
|
|
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID, func(ctr *codersdk.CreateTemplateRequest) {
|
|
ctr.DefaultTTLMillis = ptr.Ref(int64(0))
|
|
})
|
|
// Given: the template has no default TTL set
|
|
require.Zero(t, template.DefaultTTLMillis)
|
|
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
|
|
|
|
// When: we create a workspace with autostop not enabled
|
|
workspace := coderdtest.CreateWorkspace(t, client, template.ID, func(cwr *codersdk.CreateWorkspaceRequest) {
|
|
cwr.TTLMillis = ptr.Ref(int64(0))
|
|
})
|
|
coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, workspace.LatestBuild.ID)
|
|
|
|
// Then: No TTL should be set by the template
|
|
require.Nil(t, workspace.TTLMillis)
|
|
})
|
|
|
|
t.Run("TemplateCustomTTL", func(t *testing.T) {
|
|
t.Parallel()
|
|
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
|
|
user := coderdtest.CreateFirstUser(t, client)
|
|
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
|
|
templateTTL := 24 * time.Hour.Milliseconds()
|
|
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID, func(ctr *codersdk.CreateTemplateRequest) {
|
|
ctr.DefaultTTLMillis = ptr.Ref(templateTTL)
|
|
})
|
|
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
|
|
workspace := coderdtest.CreateWorkspace(t, client, template.ID, func(cwr *codersdk.CreateWorkspaceRequest) {
|
|
cwr.TTLMillis = nil // ensure that no default TTL is set
|
|
})
|
|
coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, workspace.LatestBuild.ID)
|
|
|
|
// TTL should be set by the template
|
|
require.Equal(t, templateTTL, template.DefaultTTLMillis)
|
|
require.Equal(t, templateTTL, *workspace.TTLMillis)
|
|
})
|
|
|
|
t.Run("InvalidTTL", func(t *testing.T) {
|
|
t.Parallel()
|
|
t.Run("BelowMin", func(t *testing.T) {
|
|
t.Parallel()
|
|
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
|
|
user := coderdtest.CreateFirstUser(t, client)
|
|
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
|
|
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
|
|
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
|
defer cancel()
|
|
|
|
req := codersdk.CreateWorkspaceRequest{
|
|
TemplateID: template.ID,
|
|
Name: "testing",
|
|
TTLMillis: ptr.Ref((59 * time.Second).Milliseconds()),
|
|
}
|
|
_, err := client.CreateWorkspace(ctx, template.OrganizationID, codersdk.Me, req)
|
|
require.Error(t, err)
|
|
var apiErr *codersdk.Error
|
|
require.ErrorAs(t, err, &apiErr)
|
|
require.Equal(t, http.StatusBadRequest, apiErr.StatusCode())
|
|
require.Len(t, apiErr.Validations, 1)
|
|
require.Equal(t, "ttl_ms", apiErr.Validations[0].Field)
|
|
require.Equal(t, "time until shutdown must be at least one minute", apiErr.Validations[0].Detail)
|
|
})
|
|
})
|
|
|
|
t.Run("TemplateDefaultTTL", func(t *testing.T) {
|
|
t.Parallel()
|
|
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
|
|
user := coderdtest.CreateFirstUser(t, client)
|
|
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
|
|
exp := 24 * time.Hour.Milliseconds()
|
|
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID, func(ctr *codersdk.CreateTemplateRequest) {
|
|
ctr.DefaultTTLMillis = &exp
|
|
})
|
|
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
|
defer cancel()
|
|
|
|
// no TTL provided should use template default
|
|
req := codersdk.CreateWorkspaceRequest{
|
|
TemplateID: template.ID,
|
|
Name: "testing",
|
|
}
|
|
ws, err := client.CreateWorkspace(ctx, template.OrganizationID, codersdk.Me, req)
|
|
require.NoError(t, err)
|
|
require.EqualValues(t, exp, *ws.TTLMillis)
|
|
|
|
// TTL provided should override template default
|
|
req.Name = "testing2"
|
|
exp = 1 * time.Hour.Milliseconds()
|
|
req.TTLMillis = &exp
|
|
ws, err = client.CreateWorkspace(ctx, template.OrganizationID, codersdk.Me, req)
|
|
require.NoError(t, err)
|
|
require.EqualValues(t, exp, *ws.TTLMillis)
|
|
})
|
|
|
|
t.Run("NoProvisionersAvailable", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
// Given: a coderd instance with a provisioner daemon
|
|
store, ps, db := dbtestutil.NewDBWithSQLDB(t)
|
|
client, closeDaemon := coderdtest.NewWithProvisionerCloser(t, &coderdtest.Options{
|
|
Database: store,
|
|
Pubsub: ps,
|
|
IncludeProvisionerDaemon: true,
|
|
})
|
|
defer closeDaemon.Close()
|
|
|
|
// Given: a user, template, and workspace
|
|
user := coderdtest.CreateFirstUser(t, client)
|
|
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
|
|
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
|
|
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
|
|
|
|
// Given: all the provisioner daemons disappear
|
|
ctx := testutil.Context(t, testutil.WaitLong)
|
|
_, err := db.ExecContext(ctx, `DELETE FROM provisioner_daemons;`)
|
|
require.NoError(t, err)
|
|
|
|
// When: a new workspace is created
|
|
ws, err := client.CreateUserWorkspace(ctx, codersdk.Me, codersdk.CreateWorkspaceRequest{
|
|
TemplateID: template.ID,
|
|
Name: "testing",
|
|
})
|
|
// Then: the request succeeds
|
|
require.NoError(t, err)
|
|
// Then: the workspace build is pending
|
|
require.Equal(t, codersdk.ProvisionerJobPending, ws.LatestBuild.Job.Status)
|
|
// Then: the workspace build has no matched provisioners
|
|
if assert.NotNil(t, ws.LatestBuild.MatchedProvisioners) {
|
|
assert.Zero(t, ws.LatestBuild.MatchedProvisioners.Count)
|
|
assert.Zero(t, ws.LatestBuild.MatchedProvisioners.Available)
|
|
assert.Zero(t, ws.LatestBuild.MatchedProvisioners.MostRecentlySeen.Time)
|
|
assert.False(t, ws.LatestBuild.MatchedProvisioners.MostRecentlySeen.Valid)
|
|
}
|
|
})
|
|
|
|
t.Run("AllProvisionersStale", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
// Given: a coderd instance with a provisioner daemon
|
|
store, ps, db := dbtestutil.NewDBWithSQLDB(t)
|
|
client, _, api := coderdtest.NewWithAPI(t, &coderdtest.Options{
|
|
Database: store,
|
|
Pubsub: ps,
|
|
IncludeProvisionerDaemon: false,
|
|
})
|
|
|
|
// Create a new provisioner with a heartbeater that does nothing.
|
|
provisioner := coderdtest.NewTaggedProvisionerDaemon(t, api, "test-provisioner", nil, coderd.MemoryProvisionerWithHeartbeatOverride(func(ctx context.Context) error {
|
|
// The default heartbeat updates the `last_seen_at` column in the database.
|
|
// By overriding it to do nothing, we can simulate a provisioner that is not sending heartbeats, and is therefore stale.
|
|
return nil
|
|
}))
|
|
defer provisioner.Close()
|
|
|
|
// Given: a user, template, and workspace
|
|
user := coderdtest.CreateFirstUser(t, client)
|
|
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
|
|
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
|
|
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
|
|
|
|
// Given: all the provisioner daemons have not been seen for a while
|
|
ctx := testutil.Context(t, testutil.WaitLong)
|
|
newLastSeenAt := dbtime.Now().Add(-time.Hour)
|
|
_, err := db.ExecContext(ctx, `UPDATE provisioner_daemons SET last_seen_at = $1;`, newLastSeenAt)
|
|
require.NoError(t, err)
|
|
|
|
// When: a new workspace is created
|
|
ws, err := client.CreateUserWorkspace(ctx, codersdk.Me, codersdk.CreateWorkspaceRequest{
|
|
TemplateID: template.ID,
|
|
Name: "testing",
|
|
})
|
|
// Then: the request succeeds
|
|
require.NoError(t, err)
|
|
// Then: the workspace build is pending
|
|
require.Equal(t, codersdk.ProvisionerJobPending, ws.LatestBuild.Job.Status)
|
|
// Then: we can see that there are some provisioners that are stale
|
|
if assert.NotNil(t, ws.LatestBuild.MatchedProvisioners) {
|
|
assert.Equal(t, 1, ws.LatestBuild.MatchedProvisioners.Count)
|
|
assert.Zero(t, ws.LatestBuild.MatchedProvisioners.Available)
|
|
assert.Equal(t, newLastSeenAt.UTC(), ws.LatestBuild.MatchedProvisioners.MostRecentlySeen.Time.UTC())
|
|
assert.True(t, ws.LatestBuild.MatchedProvisioners.MostRecentlySeen.Valid)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestWorkspaceByOwnerAndName(t *testing.T) {
|
|
t.Parallel()
|
|
t.Run("NotFound", func(t *testing.T) {
|
|
t.Parallel()
|
|
client := coderdtest.New(t, nil)
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
|
defer cancel()
|
|
|
|
_, err := client.WorkspaceByOwnerAndName(ctx, codersdk.Me, "something", codersdk.WorkspaceOptions{})
|
|
var apiErr *codersdk.Error
|
|
require.ErrorAs(t, err, &apiErr)
|
|
require.Equal(t, http.StatusUnauthorized, apiErr.StatusCode())
|
|
})
|
|
t.Run("Get", func(t *testing.T) {
|
|
t.Parallel()
|
|
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
|
|
user := coderdtest.CreateFirstUser(t, client)
|
|
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
|
|
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
|
|
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
|
|
workspace := coderdtest.CreateWorkspace(t, client, template.ID)
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
|
defer cancel()
|
|
|
|
_, err := client.WorkspaceByOwnerAndName(ctx, codersdk.Me, workspace.Name, codersdk.WorkspaceOptions{})
|
|
require.NoError(t, err)
|
|
})
|
|
t.Run("Deleted", func(t *testing.T) {
|
|
t.Parallel()
|
|
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
|
|
user := coderdtest.CreateFirstUser(t, client)
|
|
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
|
|
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
|
|
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
|
|
workspace := coderdtest.CreateWorkspace(t, client, template.ID)
|
|
coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, workspace.LatestBuild.ID)
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
|
defer cancel()
|
|
|
|
// Given:
|
|
// We delete the workspace
|
|
build, err := client.CreateWorkspaceBuild(ctx, workspace.ID, codersdk.CreateWorkspaceBuildRequest{
|
|
Transition: codersdk.WorkspaceTransitionDelete,
|
|
})
|
|
require.NoError(t, err, "delete the workspace")
|
|
coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, build.ID)
|
|
|
|
// Then:
|
|
// When we call without includes_deleted, we don't expect to get the workspace back
|
|
_, err = client.WorkspaceByOwnerAndName(ctx, workspace.OwnerName, workspace.Name, codersdk.WorkspaceOptions{})
|
|
require.ErrorContains(t, err, "404")
|
|
|
|
// Then:
|
|
// When we call with includes_deleted, we should get the workspace back
|
|
workspaceNew, err := client.WorkspaceByOwnerAndName(ctx, workspace.OwnerName, workspace.Name, codersdk.WorkspaceOptions{IncludeDeleted: true})
|
|
require.NoError(t, err)
|
|
require.Equal(t, workspace.ID, workspaceNew.ID)
|
|
|
|
// Given:
|
|
// We recreate the workspace with the same name
|
|
workspace, err = client.CreateWorkspace(ctx, user.OrganizationID, codersdk.Me, codersdk.CreateWorkspaceRequest{
|
|
TemplateID: workspace.TemplateID,
|
|
Name: workspace.Name,
|
|
AutostartSchedule: workspace.AutostartSchedule,
|
|
TTLMillis: workspace.TTLMillis,
|
|
AutomaticUpdates: workspace.AutomaticUpdates,
|
|
})
|
|
require.NoError(t, err)
|
|
coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, workspace.LatestBuild.ID)
|
|
|
|
// Then:
|
|
// We can fetch the most recent workspace
|
|
workspaceNew, err = client.WorkspaceByOwnerAndName(ctx, workspace.OwnerName, workspace.Name, codersdk.WorkspaceOptions{})
|
|
require.NoError(t, err)
|
|
require.Equal(t, workspace.ID, workspaceNew.ID)
|
|
|
|
// Given:
|
|
// We delete the workspace again
|
|
build, err = client.CreateWorkspaceBuild(ctx, workspace.ID, codersdk.CreateWorkspaceBuildRequest{
|
|
Transition: codersdk.WorkspaceTransitionDelete,
|
|
})
|
|
require.NoError(t, err, "delete the workspace")
|
|
coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, build.ID)
|
|
|
|
// Then:
|
|
// When we fetch the deleted workspace, we get the most recently deleted one
|
|
workspaceNew, err = client.WorkspaceByOwnerAndName(ctx, workspace.OwnerName, workspace.Name, codersdk.WorkspaceOptions{IncludeDeleted: true})
|
|
require.NoError(t, err)
|
|
require.Equal(t, workspace.ID, workspaceNew.ID)
|
|
})
|
|
}
|
|
|
|
// TestWorkspaceFilterAllStatus tests workspace status is correctly set given a set of conditions.
|
|
func TestWorkspaceFilterAllStatus(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
// For this test, we do not care about permissions.
|
|
ctx := dbauthz.AsSystemRestricted(context.Background())
|
|
db, pubsub := dbtestutil.NewDB(t)
|
|
client := coderdtest.New(t, &coderdtest.Options{
|
|
Database: db,
|
|
Pubsub: pubsub,
|
|
})
|
|
|
|
owner := coderdtest.CreateFirstUser(t, client)
|
|
|
|
file := dbgen.File(t, db, database.File{
|
|
CreatedBy: owner.UserID,
|
|
})
|
|
versionJob := dbgen.ProvisionerJob(t, db, pubsub, database.ProvisionerJob{
|
|
OrganizationID: owner.OrganizationID,
|
|
InitiatorID: owner.UserID,
|
|
WorkerID: uuid.NullUUID{},
|
|
FileID: file.ID,
|
|
Tags: database.StringMap{
|
|
"custom": "true",
|
|
},
|
|
})
|
|
version := dbgen.TemplateVersion(t, db, database.TemplateVersion{
|
|
OrganizationID: owner.OrganizationID,
|
|
JobID: versionJob.ID,
|
|
CreatedBy: owner.UserID,
|
|
})
|
|
template := dbgen.Template(t, db, database.Template{
|
|
OrganizationID: owner.OrganizationID,
|
|
ActiveVersionID: version.ID,
|
|
CreatedBy: owner.UserID,
|
|
})
|
|
|
|
makeWorkspace := func(workspace database.WorkspaceTable, job database.ProvisionerJob, transition database.WorkspaceTransition) (database.WorkspaceTable, database.WorkspaceBuild, database.ProvisionerJob) {
|
|
db := db
|
|
|
|
workspace.OwnerID = owner.UserID
|
|
workspace.OrganizationID = owner.OrganizationID
|
|
workspace.TemplateID = template.ID
|
|
workspace = dbgen.Workspace(t, db, workspace)
|
|
|
|
jobID := uuid.New()
|
|
job.ID = jobID
|
|
job.Type = database.ProvisionerJobTypeWorkspaceBuild
|
|
job.OrganizationID = owner.OrganizationID
|
|
// Need to prevent acquire from getting this job.
|
|
job.Tags = database.StringMap{
|
|
jobID.String(): "true",
|
|
}
|
|
job = dbgen.ProvisionerJob(t, db, pubsub, job)
|
|
|
|
build := dbgen.WorkspaceBuild(t, db, database.WorkspaceBuild{
|
|
WorkspaceID: workspace.ID,
|
|
TemplateVersionID: version.ID,
|
|
BuildNumber: 1,
|
|
Transition: transition,
|
|
InitiatorID: owner.UserID,
|
|
JobID: job.ID,
|
|
})
|
|
|
|
var err error
|
|
job, err = db.GetProvisionerJobByID(ctx, job.ID)
|
|
require.NoError(t, err)
|
|
|
|
return workspace, build, job
|
|
}
|
|
|
|
// pending
|
|
makeWorkspace(database.WorkspaceTable{
|
|
Name: string(database.WorkspaceStatusPending),
|
|
}, database.ProvisionerJob{
|
|
StartedAt: sql.NullTime{Valid: false},
|
|
}, database.WorkspaceTransitionStart)
|
|
|
|
// starting
|
|
makeWorkspace(database.WorkspaceTable{
|
|
Name: string(database.WorkspaceStatusStarting),
|
|
}, database.ProvisionerJob{
|
|
StartedAt: sql.NullTime{Time: time.Now().Add(time.Second * -2), Valid: true},
|
|
}, database.WorkspaceTransitionStart)
|
|
|
|
// running
|
|
makeWorkspace(database.WorkspaceTable{
|
|
Name: string(database.WorkspaceStatusRunning),
|
|
}, database.ProvisionerJob{
|
|
CompletedAt: sql.NullTime{Time: time.Now(), Valid: true},
|
|
StartedAt: sql.NullTime{Time: time.Now().Add(time.Second * -2), Valid: true},
|
|
}, database.WorkspaceTransitionStart)
|
|
|
|
// stopping
|
|
makeWorkspace(database.WorkspaceTable{
|
|
Name: string(database.WorkspaceStatusStopping),
|
|
}, database.ProvisionerJob{
|
|
StartedAt: sql.NullTime{Time: time.Now().Add(time.Second * -2), Valid: true},
|
|
}, database.WorkspaceTransitionStop)
|
|
|
|
// stopped
|
|
makeWorkspace(database.WorkspaceTable{
|
|
Name: string(database.WorkspaceStatusStopped),
|
|
}, database.ProvisionerJob{
|
|
StartedAt: sql.NullTime{Time: time.Now().Add(time.Second * -2), Valid: true},
|
|
CompletedAt: sql.NullTime{Time: time.Now(), Valid: true},
|
|
}, database.WorkspaceTransitionStop)
|
|
|
|
// failed -- delete
|
|
makeWorkspace(database.WorkspaceTable{
|
|
Name: string(database.WorkspaceStatusFailed) + "-deleted",
|
|
}, database.ProvisionerJob{
|
|
StartedAt: sql.NullTime{Time: time.Now().Add(time.Second * -2), Valid: true},
|
|
CompletedAt: sql.NullTime{Time: time.Now(), Valid: true},
|
|
Error: sql.NullString{String: "Some error", Valid: true},
|
|
}, database.WorkspaceTransitionDelete)
|
|
|
|
// failed -- stop
|
|
makeWorkspace(database.WorkspaceTable{
|
|
Name: string(database.WorkspaceStatusFailed) + "-stopped",
|
|
}, database.ProvisionerJob{
|
|
StartedAt: sql.NullTime{Time: time.Now().Add(time.Second * -2), Valid: true},
|
|
CompletedAt: sql.NullTime{Time: time.Now(), Valid: true},
|
|
Error: sql.NullString{String: "Some error", Valid: true},
|
|
}, database.WorkspaceTransitionStop)
|
|
|
|
// canceling
|
|
makeWorkspace(database.WorkspaceTable{
|
|
Name: string(database.WorkspaceStatusCanceling),
|
|
}, database.ProvisionerJob{
|
|
StartedAt: sql.NullTime{Time: time.Now().Add(time.Second * -2), Valid: true},
|
|
CanceledAt: sql.NullTime{Time: time.Now(), Valid: true},
|
|
}, database.WorkspaceTransitionStart)
|
|
|
|
// canceled
|
|
makeWorkspace(database.WorkspaceTable{
|
|
Name: string(database.WorkspaceStatusCanceled),
|
|
}, database.ProvisionerJob{
|
|
StartedAt: sql.NullTime{Time: time.Now().Add(time.Second * -2), Valid: true},
|
|
CanceledAt: sql.NullTime{Time: time.Now(), Valid: true},
|
|
CompletedAt: sql.NullTime{Time: time.Now(), Valid: true},
|
|
}, database.WorkspaceTransitionStart)
|
|
|
|
// deleting
|
|
makeWorkspace(database.WorkspaceTable{
|
|
Name: string(database.WorkspaceStatusDeleting),
|
|
}, database.ProvisionerJob{
|
|
StartedAt: sql.NullTime{Time: time.Now().Add(time.Second * -2), Valid: true},
|
|
}, database.WorkspaceTransitionDelete)
|
|
|
|
// deleted
|
|
makeWorkspace(database.WorkspaceTable{
|
|
Name: string(database.WorkspaceStatusDeleted),
|
|
}, database.ProvisionerJob{
|
|
StartedAt: sql.NullTime{Time: time.Now().Add(time.Second * -2), Valid: true},
|
|
CompletedAt: sql.NullTime{Time: time.Now(), Valid: true},
|
|
}, database.WorkspaceTransitionDelete)
|
|
|
|
apiCtx, cancel := context.WithTimeout(ctx, testutil.WaitShort)
|
|
defer cancel()
|
|
workspaces, err := client.Workspaces(apiCtx, codersdk.WorkspaceFilter{})
|
|
require.NoError(t, err)
|
|
|
|
// Make sure all workspaces have the correct status
|
|
var statuses []codersdk.WorkspaceStatus
|
|
for _, apiWorkspace := range workspaces.Workspaces {
|
|
expStatus := strings.Split(apiWorkspace.Name, "-")
|
|
if !assert.Equal(t, expStatus[0], string(apiWorkspace.LatestBuild.Status), "workspace has incorrect status") {
|
|
d, _ := json.Marshal(apiWorkspace)
|
|
var buf bytes.Buffer
|
|
_ = json.Indent(&buf, d, "", "\t")
|
|
t.Logf("Incorrect workspace: %s", buf.String())
|
|
}
|
|
statuses = append(statuses, apiWorkspace.LatestBuild.Status)
|
|
}
|
|
|
|
// Now test the filter
|
|
for _, status := range statuses {
|
|
ctx, cancel := context.WithTimeout(ctx, testutil.WaitShort)
|
|
|
|
workspaces, err := client.Workspaces(ctx, codersdk.WorkspaceFilter{
|
|
Status: string(status),
|
|
})
|
|
require.NoErrorf(t, err, "fetch with status: %s", status)
|
|
for _, workspace := range workspaces.Workspaces {
|
|
assert.Equal(t, status, workspace.LatestBuild.Status, "expect matching status to filter")
|
|
}
|
|
cancel()
|
|
}
|
|
}
|
|
|
|
// TestWorkspaceFilter creates a set of workspaces, users, and organizations
|
|
// to run various filters against for testing.
|
|
func TestWorkspaceFilter(t *testing.T) {
|
|
t.Parallel()
|
|
// Manual tests still occur below, so this is safe to disable.
|
|
t.Skip("This test is slow and flaky. See: https://github.com/coder/coder/issues/2854")
|
|
// nolint:unused
|
|
type coderUser struct {
|
|
*codersdk.Client
|
|
User codersdk.User
|
|
Org codersdk.Organization
|
|
}
|
|
|
|
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
|
|
first := coderdtest.CreateFirstUser(t, client)
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
|
t.Cleanup(cancel)
|
|
|
|
users := make([]coderUser, 0)
|
|
for i := 0; i < 10; i++ {
|
|
userClient, user := coderdtest.CreateAnotherUser(t, client, first.OrganizationID, rbac.RoleOwner())
|
|
|
|
if i%3 == 0 {
|
|
var err error
|
|
user, err = client.UpdateUserProfile(ctx, user.ID.String(), codersdk.UpdateUserProfileRequest{
|
|
Username: strings.ToUpper(user.Username),
|
|
})
|
|
require.NoError(t, err, "uppercase username")
|
|
}
|
|
|
|
org, err := userClient.CreateOrganization(ctx, codersdk.CreateOrganizationRequest{
|
|
Name: user.Username + "-org",
|
|
})
|
|
require.NoError(t, err, "create org")
|
|
|
|
users = append(users, coderUser{
|
|
Client: userClient,
|
|
User: user,
|
|
Org: org,
|
|
})
|
|
}
|
|
|
|
type madeWorkspace struct {
|
|
Owner codersdk.User
|
|
Workspace codersdk.Workspace
|
|
Template codersdk.Template
|
|
}
|
|
|
|
availTemplates := make([]codersdk.Template, 0)
|
|
allWorkspaces := make([]madeWorkspace, 0)
|
|
upperTemplates := make([]string, 0)
|
|
|
|
// Create some random workspaces
|
|
var count int
|
|
for i, user := range users {
|
|
version := coderdtest.CreateTemplateVersion(t, client, user.Org.ID, nil)
|
|
|
|
// Create a template & workspace in the user's org
|
|
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
|
|
|
|
var template codersdk.Template
|
|
if i%3 == 0 {
|
|
template = coderdtest.CreateTemplate(t, client, user.Org.ID, version.ID, func(request *codersdk.CreateTemplateRequest) {
|
|
request.Name = strings.ToUpper(request.Name)
|
|
})
|
|
upperTemplates = append(upperTemplates, template.Name)
|
|
} else {
|
|
template = coderdtest.CreateTemplate(t, client, user.Org.ID, version.ID)
|
|
}
|
|
|
|
availTemplates = append(availTemplates, template)
|
|
workspace := coderdtest.CreateWorkspace(t, user.Client, template.ID, func(request *codersdk.CreateWorkspaceRequest) {
|
|
if count%3 == 0 {
|
|
request.Name = strings.ToUpper(request.Name)
|
|
}
|
|
})
|
|
allWorkspaces = append(allWorkspaces, madeWorkspace{
|
|
Workspace: workspace,
|
|
Template: template,
|
|
Owner: user.User,
|
|
})
|
|
|
|
// Make a workspace with a random template
|
|
idx, _ := cryptorand.Intn(len(availTemplates))
|
|
randTemplate := availTemplates[idx]
|
|
randWorkspace := coderdtest.CreateWorkspace(t, user.Client, randTemplate.ID)
|
|
allWorkspaces = append(allWorkspaces, madeWorkspace{
|
|
Workspace: randWorkspace,
|
|
Template: randTemplate,
|
|
Owner: user.User,
|
|
})
|
|
}
|
|
|
|
// Make sure all workspaces are done. Do it after all are made
|
|
for i, w := range allWorkspaces {
|
|
latest := coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, w.Workspace.LatestBuild.ID)
|
|
allWorkspaces[i].Workspace.LatestBuild = latest
|
|
}
|
|
|
|
// --- Setup done ---
|
|
testCases := []struct {
|
|
Name string
|
|
Filter codersdk.WorkspaceFilter
|
|
// If FilterF is true, we include it in the expected results
|
|
FilterF func(f codersdk.WorkspaceFilter, workspace madeWorkspace) bool
|
|
}{
|
|
{
|
|
Name: "All",
|
|
Filter: codersdk.WorkspaceFilter{},
|
|
FilterF: func(_ codersdk.WorkspaceFilter, _ madeWorkspace) bool {
|
|
return true
|
|
},
|
|
},
|
|
{
|
|
Name: "Owner",
|
|
Filter: codersdk.WorkspaceFilter{
|
|
Owner: strings.ToUpper(users[2].User.Username),
|
|
},
|
|
FilterF: func(f codersdk.WorkspaceFilter, workspace madeWorkspace) bool {
|
|
return strings.EqualFold(workspace.Owner.Username, f.Owner)
|
|
},
|
|
},
|
|
{
|
|
Name: "TemplateName",
|
|
Filter: codersdk.WorkspaceFilter{
|
|
Template: strings.ToUpper(allWorkspaces[5].Template.Name),
|
|
},
|
|
FilterF: func(f codersdk.WorkspaceFilter, workspace madeWorkspace) bool {
|
|
return strings.EqualFold(workspace.Template.Name, f.Template)
|
|
},
|
|
},
|
|
{
|
|
Name: "UpperTemplateName",
|
|
Filter: codersdk.WorkspaceFilter{
|
|
Template: upperTemplates[0],
|
|
},
|
|
FilterF: func(f codersdk.WorkspaceFilter, workspace madeWorkspace) bool {
|
|
return strings.EqualFold(workspace.Template.Name, f.Template)
|
|
},
|
|
},
|
|
{
|
|
Name: "Name",
|
|
Filter: codersdk.WorkspaceFilter{
|
|
// Use a common letter... one has to have this letter in it
|
|
Name: "a",
|
|
},
|
|
FilterF: func(f codersdk.WorkspaceFilter, workspace madeWorkspace) bool {
|
|
return strings.ContainsAny(workspace.Workspace.Name, "Aa")
|
|
},
|
|
},
|
|
{
|
|
Name: "Q-Owner/Name",
|
|
Filter: codersdk.WorkspaceFilter{
|
|
FilterQuery: allWorkspaces[5].Owner.Username + "/" + strings.ToUpper(allWorkspaces[5].Workspace.Name),
|
|
},
|
|
FilterF: func(f codersdk.WorkspaceFilter, workspace madeWorkspace) bool {
|
|
if strings.EqualFold(workspace.Owner.Username, allWorkspaces[5].Owner.Username) &&
|
|
strings.Contains(strings.ToLower(workspace.Workspace.Name), strings.ToLower(allWorkspaces[5].Workspace.Name)) {
|
|
return true
|
|
}
|
|
|
|
return false
|
|
},
|
|
},
|
|
{
|
|
Name: "Many filters",
|
|
Filter: codersdk.WorkspaceFilter{
|
|
Owner: allWorkspaces[3].Owner.Username,
|
|
Template: allWorkspaces[3].Template.Name,
|
|
Name: allWorkspaces[3].Workspace.Name,
|
|
},
|
|
FilterF: func(f codersdk.WorkspaceFilter, workspace madeWorkspace) bool {
|
|
if strings.EqualFold(workspace.Owner.Username, f.Owner) &&
|
|
strings.Contains(strings.ToLower(workspace.Workspace.Name), strings.ToLower(f.Name)) &&
|
|
strings.EqualFold(workspace.Template.Name, f.Template) {
|
|
return true
|
|
}
|
|
return false
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, c := range testCases {
|
|
t.Run(c.Name, func(t *testing.T) {
|
|
t.Parallel()
|
|
ctx := testutil.Context(t, testutil.WaitShort)
|
|
workspaces, err := client.Workspaces(ctx, c.Filter)
|
|
require.NoError(t, err, "fetch workspaces")
|
|
|
|
exp := make([]codersdk.Workspace, 0)
|
|
for _, made := range allWorkspaces {
|
|
if c.FilterF(c.Filter, made) {
|
|
exp = append(exp, made.Workspace)
|
|
}
|
|
}
|
|
require.ElementsMatch(t, exp, workspaces, "expected workspaces returned")
|
|
})
|
|
}
|
|
|
|
t.Run("Shared", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
dv := coderdtest.DeploymentValues(t)
|
|
|
|
var (
|
|
client, db = coderdtest.NewWithDatabase(t, &coderdtest.Options{
|
|
DeploymentValues: dv,
|
|
})
|
|
orgOwner = coderdtest.CreateFirstUser(t, client)
|
|
_, workspaceOwner = coderdtest.CreateAnotherUser(t, client, orgOwner.OrganizationID, rbac.ScopedRoleOrgAuditor(orgOwner.OrganizationID))
|
|
sharedWorkspace = dbfake.WorkspaceBuild(t, db, database.WorkspaceTable{
|
|
OwnerID: workspaceOwner.ID,
|
|
OrganizationID: orgOwner.OrganizationID,
|
|
}).Do().Workspace
|
|
_ = dbfake.WorkspaceBuild(t, db, database.WorkspaceTable{
|
|
OwnerID: workspaceOwner.ID,
|
|
OrganizationID: orgOwner.OrganizationID,
|
|
}).Do().Workspace
|
|
_, toShareWithUser = coderdtest.CreateAnotherUser(t, client, orgOwner.OrganizationID)
|
|
ctx = testutil.Context(t, testutil.WaitMedium)
|
|
)
|
|
|
|
client.UpdateWorkspaceACL(ctx, sharedWorkspace.ID, codersdk.UpdateWorkspaceACL{
|
|
UserRoles: map[string]codersdk.WorkspaceRole{
|
|
toShareWithUser.ID.String(): codersdk.WorkspaceRoleUse,
|
|
},
|
|
})
|
|
|
|
workspaces, err := client.Workspaces(ctx, codersdk.WorkspaceFilter{
|
|
Shared: ptr.Ref(true),
|
|
})
|
|
require.NoError(t, err, "fetch workspaces")
|
|
require.Equal(t, 1, workspaces.Count, "expected only one workspace")
|
|
require.Equal(t, workspaces.Workspaces[0].ID, sharedWorkspace.ID)
|
|
})
|
|
|
|
t.Run("NotShared", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
dv := coderdtest.DeploymentValues(t)
|
|
|
|
var (
|
|
client, db = coderdtest.NewWithDatabase(t, &coderdtest.Options{
|
|
DeploymentValues: dv,
|
|
})
|
|
orgOwner = coderdtest.CreateFirstUser(t, client)
|
|
_, workspaceOwner = coderdtest.CreateAnotherUser(t, client, orgOwner.OrganizationID, rbac.ScopedRoleOrgAuditor(orgOwner.OrganizationID))
|
|
sharedWorkspace = dbfake.WorkspaceBuild(t, db, database.WorkspaceTable{
|
|
OwnerID: workspaceOwner.ID,
|
|
OrganizationID: orgOwner.OrganizationID,
|
|
}).Do().Workspace
|
|
notSharedWorkspace = dbfake.WorkspaceBuild(t, db, database.WorkspaceTable{
|
|
OwnerID: workspaceOwner.ID,
|
|
OrganizationID: orgOwner.OrganizationID,
|
|
}).Do().Workspace
|
|
_, toShareWithUser = coderdtest.CreateAnotherUser(t, client, orgOwner.OrganizationID)
|
|
ctx = testutil.Context(t, testutil.WaitMedium)
|
|
)
|
|
|
|
client.UpdateWorkspaceACL(ctx, sharedWorkspace.ID, codersdk.UpdateWorkspaceACL{
|
|
UserRoles: map[string]codersdk.WorkspaceRole{
|
|
toShareWithUser.ID.String(): codersdk.WorkspaceRoleUse,
|
|
},
|
|
})
|
|
|
|
workspaces, err := client.Workspaces(ctx, codersdk.WorkspaceFilter{
|
|
Shared: ptr.Ref(false),
|
|
})
|
|
require.NoError(t, err, "fetch workspaces")
|
|
require.Equal(t, 1, workspaces.Count, "expected only one workspace")
|
|
require.Equal(t, workspaces.Workspaces[0].ID, notSharedWorkspace.ID)
|
|
})
|
|
|
|
t.Run("SharedWithUserByID", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
dv := coderdtest.DeploymentValues(t)
|
|
|
|
var (
|
|
client, db = coderdtest.NewWithDatabase(t, &coderdtest.Options{
|
|
DeploymentValues: dv,
|
|
})
|
|
orgOwner = coderdtest.CreateFirstUser(t, client)
|
|
_, workspaceOwner = coderdtest.CreateAnotherUser(t, client, orgOwner.OrganizationID, rbac.ScopedRoleOrgAuditor(orgOwner.OrganizationID))
|
|
sharedWorkspace = dbfake.WorkspaceBuild(t, db, database.WorkspaceTable{
|
|
OwnerID: workspaceOwner.ID,
|
|
OrganizationID: orgOwner.OrganizationID,
|
|
}).Do().Workspace
|
|
_ = dbfake.WorkspaceBuild(t, db, database.WorkspaceTable{
|
|
OwnerID: workspaceOwner.ID,
|
|
OrganizationID: orgOwner.OrganizationID,
|
|
}).Do().Workspace
|
|
_, toShareWithUser = coderdtest.CreateAnotherUser(t, client, orgOwner.OrganizationID)
|
|
ctx = testutil.Context(t, testutil.WaitMedium)
|
|
)
|
|
|
|
client.UpdateWorkspaceACL(ctx, sharedWorkspace.ID, codersdk.UpdateWorkspaceACL{
|
|
UserRoles: map[string]codersdk.WorkspaceRole{
|
|
toShareWithUser.ID.String(): codersdk.WorkspaceRoleUse,
|
|
},
|
|
})
|
|
|
|
workspaces, err := client.Workspaces(ctx, codersdk.WorkspaceFilter{
|
|
SharedWithUser: toShareWithUser.ID.String(),
|
|
})
|
|
require.NoError(t, err, "fetch workspaces")
|
|
require.Equal(t, 1, workspaces.Count, "expected only one workspace")
|
|
require.Equal(t, workspaces.Workspaces[0].ID, sharedWorkspace.ID)
|
|
})
|
|
|
|
t.Run("SharedWithUserByUsername", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
dv := coderdtest.DeploymentValues(t)
|
|
|
|
var (
|
|
client, db = coderdtest.NewWithDatabase(t, &coderdtest.Options{
|
|
DeploymentValues: dv,
|
|
})
|
|
orgOwner = coderdtest.CreateFirstUser(t, client)
|
|
_, workspaceOwner = coderdtest.CreateAnotherUser(t, client, orgOwner.OrganizationID, rbac.ScopedRoleOrgAuditor(orgOwner.OrganizationID))
|
|
sharedWorkspace = dbfake.WorkspaceBuild(t, db, database.WorkspaceTable{
|
|
OwnerID: workspaceOwner.ID,
|
|
OrganizationID: orgOwner.OrganizationID,
|
|
}).Do().Workspace
|
|
_ = dbfake.WorkspaceBuild(t, db, database.WorkspaceTable{
|
|
OwnerID: workspaceOwner.ID,
|
|
OrganizationID: orgOwner.OrganizationID,
|
|
}).Do().Workspace
|
|
_, toShareWithUser = coderdtest.CreateAnotherUser(t, client, orgOwner.OrganizationID)
|
|
ctx = testutil.Context(t, testutil.WaitMedium)
|
|
)
|
|
|
|
client.UpdateWorkspaceACL(ctx, sharedWorkspace.ID, codersdk.UpdateWorkspaceACL{
|
|
UserRoles: map[string]codersdk.WorkspaceRole{
|
|
toShareWithUser.ID.String(): codersdk.WorkspaceRoleUse,
|
|
},
|
|
})
|
|
|
|
workspaces, err := client.Workspaces(ctx, codersdk.WorkspaceFilter{
|
|
SharedWithUser: toShareWithUser.Username,
|
|
})
|
|
require.NoError(t, err, "fetch workspaces")
|
|
require.Equal(t, 1, workspaces.Count, "expected only one workspace")
|
|
require.Equal(t, workspaces.Workspaces[0].ID, sharedWorkspace.ID)
|
|
})
|
|
}
|
|
|
|
// TestWorkspaceFilterManual runs some specific setups with basic checks.
|
|
func TestWorkspaceFilterManual(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
expectIDs := func(t *testing.T, exp []codersdk.Workspace, got []codersdk.Workspace) {
|
|
t.Helper()
|
|
expIDs := make([]uuid.UUID, 0, len(exp))
|
|
for _, e := range exp {
|
|
expIDs = append(expIDs, e.ID)
|
|
}
|
|
|
|
gotIDs := make([]uuid.UUID, 0, len(got))
|
|
for _, g := range got {
|
|
gotIDs = append(gotIDs, g.ID)
|
|
}
|
|
require.ElementsMatchf(t, expIDs, gotIDs, "expected IDs")
|
|
}
|
|
|
|
t.Run("Name", func(t *testing.T) {
|
|
t.Parallel()
|
|
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
|
|
user := coderdtest.CreateFirstUser(t, client)
|
|
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
|
|
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
|
|
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
|
|
workspace := coderdtest.CreateWorkspace(t, client, template.ID)
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
|
defer cancel()
|
|
|
|
// full match
|
|
res, err := client.Workspaces(ctx, codersdk.WorkspaceFilter{
|
|
Name: workspace.Name,
|
|
})
|
|
require.NoError(t, err)
|
|
require.Len(t, res.Workspaces, 1, workspace.Name)
|
|
require.Equal(t, workspace.ID, res.Workspaces[0].ID)
|
|
|
|
// partial match
|
|
res, err = client.Workspaces(ctx, codersdk.WorkspaceFilter{
|
|
Name: workspace.Name[1 : len(workspace.Name)-2],
|
|
})
|
|
require.NoError(t, err)
|
|
require.Len(t, res.Workspaces, 1)
|
|
require.Equal(t, workspace.ID, res.Workspaces[0].ID)
|
|
|
|
// no match
|
|
res, err = client.Workspaces(ctx, codersdk.WorkspaceFilter{
|
|
Name: "$$$$",
|
|
})
|
|
require.NoError(t, err)
|
|
require.Len(t, res.Workspaces, 0)
|
|
})
|
|
t.Run("Owner", func(t *testing.T) {
|
|
t.Parallel()
|
|
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
|
|
user := coderdtest.CreateFirstUser(t, client)
|
|
otherUser, _ := coderdtest.CreateAnotherUser(t, client, user.OrganizationID, rbac.RoleOwner())
|
|
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
|
|
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
|
|
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
|
|
|
|
// Add a non-matching workspace
|
|
coderdtest.CreateWorkspace(t, otherUser, template.ID)
|
|
|
|
workspaces := []codersdk.Workspace{
|
|
coderdtest.CreateWorkspace(t, client, template.ID),
|
|
coderdtest.CreateWorkspace(t, client, template.ID),
|
|
}
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
|
defer cancel()
|
|
|
|
sdkUser, err := client.User(ctx, codersdk.Me)
|
|
require.NoError(t, err)
|
|
|
|
// match owner name
|
|
res, err := client.Workspaces(ctx, codersdk.WorkspaceFilter{
|
|
FilterQuery: fmt.Sprintf("owner:%s", sdkUser.Username),
|
|
})
|
|
require.NoError(t, err)
|
|
require.Len(t, res.Workspaces, len(workspaces))
|
|
for _, found := range res.Workspaces {
|
|
require.Equal(t, found.OwnerName, sdkUser.Username)
|
|
}
|
|
})
|
|
t.Run("IDs", func(t *testing.T) {
|
|
t.Parallel()
|
|
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
|
|
user := coderdtest.CreateFirstUser(t, client)
|
|
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
|
|
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
|
|
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
|
|
alpha := coderdtest.CreateWorkspace(t, client, template.ID)
|
|
bravo := coderdtest.CreateWorkspace(t, client, template.ID)
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
|
defer cancel()
|
|
|
|
// full match
|
|
res, err := client.Workspaces(ctx, codersdk.WorkspaceFilter{
|
|
FilterQuery: fmt.Sprintf("id:%s,%s", alpha.ID, bravo.ID),
|
|
})
|
|
require.NoError(t, err)
|
|
require.Len(t, res.Workspaces, 2)
|
|
require.True(t, slices.ContainsFunc(res.Workspaces, func(workspace codersdk.Workspace) bool {
|
|
return workspace.ID == alpha.ID
|
|
}), "alpha workspace")
|
|
require.True(t, slices.ContainsFunc(res.Workspaces, func(workspace codersdk.Workspace) bool {
|
|
return workspace.ID == alpha.ID
|
|
}), "bravo workspace")
|
|
|
|
// no match
|
|
res, err = client.Workspaces(ctx, codersdk.WorkspaceFilter{
|
|
FilterQuery: fmt.Sprintf("id:%s", uuid.NewString()),
|
|
})
|
|
require.NoError(t, err)
|
|
require.Len(t, res.Workspaces, 0)
|
|
})
|
|
t.Run("Template", func(t *testing.T) {
|
|
t.Parallel()
|
|
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
|
|
user := coderdtest.CreateFirstUser(t, client)
|
|
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
|
|
version2 := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
|
|
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
|
|
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version2.ID)
|
|
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
|
|
template2 := coderdtest.CreateTemplate(t, client, user.OrganizationID, version2.ID)
|
|
workspace := coderdtest.CreateWorkspace(t, client, template.ID)
|
|
_ = coderdtest.CreateWorkspace(t, client, template2.ID)
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
|
defer cancel()
|
|
|
|
// empty
|
|
res, err := client.Workspaces(ctx, codersdk.WorkspaceFilter{})
|
|
require.NoError(t, err)
|
|
require.Len(t, res.Workspaces, 2)
|
|
|
|
// single template
|
|
res, err = client.Workspaces(ctx, codersdk.WorkspaceFilter{
|
|
Template: template.Name,
|
|
})
|
|
require.NoError(t, err)
|
|
require.Len(t, res.Workspaces, 1)
|
|
require.Equal(t, workspace.ID, res.Workspaces[0].ID)
|
|
})
|
|
t.Run("Status", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
|
|
user := coderdtest.CreateFirstUser(t, client)
|
|
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
|
|
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
|
|
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
|
|
workspace1 := coderdtest.CreateWorkspace(t, client, template.ID)
|
|
workspace2 := coderdtest.CreateWorkspace(t, client, template.ID)
|
|
|
|
// wait for workspaces to be "running"
|
|
_ = coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, workspace1.LatestBuild.ID)
|
|
_ = coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, workspace2.LatestBuild.ID)
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
|
defer cancel()
|
|
|
|
// filter finds both running workspaces
|
|
ws1, err := client.Workspaces(ctx, codersdk.WorkspaceFilter{})
|
|
require.NoError(t, err)
|
|
require.Len(t, ws1.Workspaces, 2)
|
|
|
|
// stop workspace1
|
|
build1 := coderdtest.CreateWorkspaceBuild(t, client, workspace1, database.WorkspaceTransitionStop)
|
|
_ = coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, build1.ID)
|
|
|
|
// filter finds one running workspace
|
|
ws2, err := client.Workspaces(ctx, codersdk.WorkspaceFilter{
|
|
Status: "running",
|
|
})
|
|
require.NoError(t, err)
|
|
require.Len(t, ws2.Workspaces, 1)
|
|
require.Equal(t, workspace2.ID, ws2.Workspaces[0].ID)
|
|
|
|
// stop workspace2
|
|
build2 := coderdtest.CreateWorkspaceBuild(t, client, workspace2, database.WorkspaceTransitionStop)
|
|
_ = coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, build2.ID)
|
|
|
|
// filter finds no running workspaces
|
|
ws3, err := client.Workspaces(ctx, codersdk.WorkspaceFilter{
|
|
Status: "running",
|
|
})
|
|
require.NoError(t, err)
|
|
require.Len(t, ws3.Workspaces, 0)
|
|
})
|
|
t.Run("FilterQuery", func(t *testing.T) {
|
|
t.Parallel()
|
|
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
|
|
user := coderdtest.CreateFirstUser(t, client)
|
|
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
|
|
version2 := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
|
|
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
|
|
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version2.ID)
|
|
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
|
|
template2 := coderdtest.CreateTemplate(t, client, user.OrganizationID, version2.ID)
|
|
workspace := coderdtest.CreateWorkspace(t, client, template.ID)
|
|
_ = coderdtest.CreateWorkspace(t, client, template2.ID)
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
|
defer cancel()
|
|
|
|
org, err := client.Organization(ctx, user.OrganizationID)
|
|
require.NoError(t, err)
|
|
|
|
// single workspace
|
|
res, err := client.Workspaces(ctx, codersdk.WorkspaceFilter{
|
|
FilterQuery: fmt.Sprintf("template:%s %s/%s", template.Name, workspace.OwnerName, workspace.Name),
|
|
})
|
|
require.NoError(t, err)
|
|
require.Len(t, res.Workspaces, 1)
|
|
require.Equal(t, workspace.ID, res.Workspaces[0].ID)
|
|
require.Equal(t, workspace.OrganizationName, org.Name)
|
|
})
|
|
t.Run("FilterQueryHasAgentConnecting", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
client := coderdtest.New(t, &coderdtest.Options{
|
|
IncludeProvisionerDaemon: true,
|
|
})
|
|
user := coderdtest.CreateFirstUser(t, client)
|
|
authToken := uuid.NewString()
|
|
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
|
|
Parse: echo.ParseComplete,
|
|
ProvisionPlan: echo.PlanComplete,
|
|
ProvisionGraph: echo.ProvisionGraphWithAgent(authToken),
|
|
})
|
|
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
|
|
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
|
|
workspace := coderdtest.CreateWorkspace(t, client, template.ID)
|
|
coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, workspace.LatestBuild.ID)
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
|
defer cancel()
|
|
|
|
res, err := client.Workspaces(ctx, codersdk.WorkspaceFilter{
|
|
FilterQuery: fmt.Sprintf("has-agent:%s", "connecting"),
|
|
})
|
|
require.NoError(t, err)
|
|
require.Len(t, res.Workspaces, 1)
|
|
require.Equal(t, workspace.ID, res.Workspaces[0].ID)
|
|
})
|
|
t.Run("FilterQueryHasAgentConnected", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
client := coderdtest.New(t, &coderdtest.Options{
|
|
IncludeProvisionerDaemon: true,
|
|
})
|
|
user := coderdtest.CreateFirstUser(t, client)
|
|
authToken := uuid.NewString()
|
|
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
|
|
Parse: echo.ParseComplete,
|
|
ProvisionPlan: echo.PlanComplete,
|
|
ProvisionGraph: echo.ProvisionGraphWithAgent(authToken),
|
|
})
|
|
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
|
|
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
|
|
workspace := coderdtest.CreateWorkspace(t, client, template.ID)
|
|
coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, workspace.LatestBuild.ID)
|
|
|
|
_ = agenttest.New(t, client.URL, authToken)
|
|
_ = coderdtest.AwaitWorkspaceAgents(t, client, workspace.ID)
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
|
defer cancel()
|
|
|
|
res, err := client.Workspaces(ctx, codersdk.WorkspaceFilter{
|
|
FilterQuery: fmt.Sprintf("has-agent:%s", "connected"),
|
|
})
|
|
require.NoError(t, err)
|
|
require.Len(t, res.Workspaces, 1)
|
|
require.Equal(t, workspace.ID, res.Workspaces[0].ID)
|
|
})
|
|
t.Run("FilterQueryHasAgentTimeout", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
client := coderdtest.New(t, &coderdtest.Options{
|
|
IncludeProvisionerDaemon: true,
|
|
})
|
|
user := coderdtest.CreateFirstUser(t, client)
|
|
authToken := uuid.NewString()
|
|
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
|
|
Parse: echo.ParseComplete,
|
|
ProvisionPlan: echo.PlanComplete,
|
|
ProvisionGraph: []*proto.Response{{
|
|
Type: &proto.Response_Graph{
|
|
Graph: &proto.GraphComplete{
|
|
Resources: []*proto.Resource{{
|
|
Name: "example",
|
|
Type: "aws_instance",
|
|
Agents: []*proto.Agent{{
|
|
Id: uuid.NewString(),
|
|
Name: "dev",
|
|
Auth: &proto.Agent_Token{
|
|
Token: authToken,
|
|
},
|
|
ConnectionTimeoutSeconds: 1,
|
|
}},
|
|
}},
|
|
},
|
|
},
|
|
}},
|
|
})
|
|
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
|
|
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
|
|
workspace := coderdtest.CreateWorkspace(t, client, template.ID)
|
|
coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, workspace.LatestBuild.ID)
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitMedium)
|
|
defer cancel()
|
|
|
|
testutil.Eventually(ctx, t, func(ctx context.Context) (done bool) {
|
|
workspaces, err := client.Workspaces(ctx, codersdk.WorkspaceFilter{
|
|
FilterQuery: fmt.Sprintf("has-agent:%s", "timeout"),
|
|
})
|
|
require.NoError(t, err)
|
|
return workspaces.Count == 1
|
|
}, testutil.IntervalMedium, "agent status timeout")
|
|
})
|
|
t.Run("Dormant", func(t *testing.T) {
|
|
// this test has a licensed counterpart in enterprise/coderd/workspaces_test.go: FilterQueryHasDeletingByAndLicensed
|
|
t.Parallel()
|
|
client, db := coderdtest.NewWithDatabase(t, nil)
|
|
user := coderdtest.CreateFirstUser(t, client)
|
|
template := dbfake.TemplateVersion(t, db).Seed(database.TemplateVersion{
|
|
OrganizationID: user.OrganizationID,
|
|
CreatedBy: user.UserID,
|
|
}).Do().Template
|
|
|
|
// update template with inactivity ttl
|
|
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
|
defer cancel()
|
|
|
|
dormantWorkspace := dbfake.WorkspaceBuild(t, db, database.WorkspaceTable{
|
|
TemplateID: template.ID,
|
|
OwnerID: user.UserID,
|
|
OrganizationID: user.OrganizationID,
|
|
}).Do().Workspace
|
|
|
|
// Create another workspace to validate that we do not return active workspaces.
|
|
_ = dbfake.WorkspaceBuild(t, db, database.WorkspaceTable{
|
|
TemplateID: template.ID,
|
|
OwnerID: user.UserID,
|
|
OrganizationID: user.OrganizationID,
|
|
}).Do()
|
|
|
|
err := client.UpdateWorkspaceDormancy(ctx, dormantWorkspace.ID, codersdk.UpdateWorkspaceDormancy{
|
|
Dormant: true,
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
// Test that no filter returns both workspaces.
|
|
res, err := client.Workspaces(ctx, codersdk.WorkspaceFilter{})
|
|
require.NoError(t, err)
|
|
require.Len(t, res.Workspaces, 2)
|
|
|
|
// Test that filtering for dormant only returns our dormant workspace.
|
|
res, err = client.Workspaces(ctx, codersdk.WorkspaceFilter{
|
|
FilterQuery: "dormant:true",
|
|
})
|
|
require.NoError(t, err)
|
|
require.Len(t, res.Workspaces, 1)
|
|
require.Equal(t, dormantWorkspace.ID, res.Workspaces[0].ID)
|
|
require.NotNil(t, res.Workspaces[0].DormantAt)
|
|
})
|
|
t.Run("LastUsed", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
client, _, api := coderdtest.NewWithAPI(t, &coderdtest.Options{
|
|
IncludeProvisionerDaemon: true,
|
|
})
|
|
user := coderdtest.CreateFirstUser(t, client)
|
|
authToken := uuid.NewString()
|
|
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
|
|
Parse: echo.ParseComplete,
|
|
ProvisionPlan: echo.PlanComplete,
|
|
ProvisionGraph: echo.ProvisionGraphWithAgent(authToken),
|
|
})
|
|
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
|
|
_ = coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
|
|
|
|
// update template with inactivity ttl
|
|
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
|
defer cancel()
|
|
|
|
now := dbtime.Now()
|
|
before := coderdtest.CreateWorkspace(t, client, template.ID)
|
|
_ = coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, before.LatestBuild.ID)
|
|
|
|
after := coderdtest.CreateWorkspace(t, client, template.ID)
|
|
_ = coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, after.LatestBuild.ID)
|
|
|
|
err := api.Database.UpdateWorkspaceLastUsedAt(dbauthz.AsSystemRestricted(ctx), database.UpdateWorkspaceLastUsedAtParams{
|
|
ID: before.ID,
|
|
LastUsedAt: now.UTC().Add(time.Hour * -1),
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
err = api.Database.UpdateWorkspaceLastUsedAt(dbauthz.AsSystemRestricted(ctx), database.UpdateWorkspaceLastUsedAtParams{
|
|
ID: after.ID,
|
|
LastUsedAt: now.UTC().Add(time.Hour * 1),
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
beforeRes, err := client.Workspaces(ctx, codersdk.WorkspaceFilter{
|
|
FilterQuery: fmt.Sprintf("last_used_before:%q", now.Format(time.RFC3339)),
|
|
})
|
|
require.NoError(t, err)
|
|
require.Len(t, beforeRes.Workspaces, 1)
|
|
require.Equal(t, before.ID, beforeRes.Workspaces[0].ID)
|
|
|
|
afterRes, err := client.Workspaces(ctx, codersdk.WorkspaceFilter{
|
|
FilterQuery: fmt.Sprintf("last_used_after:%q", now.Format(time.RFC3339)),
|
|
})
|
|
require.NoError(t, err)
|
|
require.Len(t, afterRes.Workspaces, 1)
|
|
require.Equal(t, after.ID, afterRes.Workspaces[0].ID)
|
|
})
|
|
t.Run("Updated", func(t *testing.T) {
|
|
t.Parallel()
|
|
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
|
|
user := coderdtest.CreateFirstUser(t, client)
|
|
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
|
|
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
|
|
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
|
|
workspace := coderdtest.CreateWorkspace(t, client, template.ID)
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
|
defer cancel()
|
|
|
|
// Workspace is up-to-date
|
|
res, err := client.Workspaces(ctx, codersdk.WorkspaceFilter{
|
|
FilterQuery: "outdated:false",
|
|
})
|
|
require.NoError(t, err)
|
|
require.Len(t, res.Workspaces, 1)
|
|
require.Equal(t, workspace.ID, res.Workspaces[0].ID)
|
|
|
|
res, err = client.Workspaces(ctx, codersdk.WorkspaceFilter{
|
|
FilterQuery: "outdated:true",
|
|
})
|
|
require.NoError(t, err)
|
|
require.Len(t, res.Workspaces, 0)
|
|
|
|
// Now make it out of date
|
|
newTv := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil, func(request *codersdk.CreateTemplateVersionRequest) {
|
|
request.TemplateID = template.ID
|
|
})
|
|
coderdtest.AwaitTemplateVersionJobCompleted(t, client, newTv.ID)
|
|
err = client.UpdateActiveTemplateVersion(ctx, template.ID, codersdk.UpdateActiveTemplateVersion{
|
|
ID: newTv.ID,
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
// Check the query again
|
|
res, err = client.Workspaces(ctx, codersdk.WorkspaceFilter{
|
|
FilterQuery: "outdated:false",
|
|
})
|
|
require.NoError(t, err)
|
|
require.Len(t, res.Workspaces, 0)
|
|
|
|
res, err = client.Workspaces(ctx, codersdk.WorkspaceFilter{
|
|
FilterQuery: "outdated:true",
|
|
})
|
|
require.NoError(t, err)
|
|
require.Len(t, res.Workspaces, 1)
|
|
require.Equal(t, workspace.ID, res.Workspaces[0].ID)
|
|
})
|
|
|
|
t.Run("HealthyFilter", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
t.Run("Healthy", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
// healthy:true should return workspaces with connected agents
|
|
// and exclude workspaces with disconnected agents
|
|
client, db := coderdtest.NewWithDatabase(t, nil)
|
|
user := coderdtest.CreateFirstUser(t, client)
|
|
|
|
// Create a workspace with a connected agent
|
|
connectedBuild := dbfake.WorkspaceBuild(t, db, database.WorkspaceTable{
|
|
OrganizationID: user.OrganizationID,
|
|
OwnerID: user.UserID,
|
|
Name: "connected-workspace",
|
|
}).WithAgent().Do()
|
|
|
|
// Mark the agent as connected
|
|
now := time.Now()
|
|
require.Len(t, connectedBuild.Agents, 1)
|
|
//nolint:gocritic // This is a test, we need system context to update agent connection
|
|
ctx := dbauthz.AsSystemRestricted(context.Background())
|
|
err := db.UpdateWorkspaceAgentConnectionByID(ctx, database.UpdateWorkspaceAgentConnectionByIDParams{
|
|
ID: connectedBuild.Agents[0].ID,
|
|
FirstConnectedAt: sql.NullTime{Time: now, Valid: true},
|
|
LastConnectedAt: sql.NullTime{Time: now, Valid: true},
|
|
DisconnectedAt: sql.NullTime{},
|
|
UpdatedAt: now,
|
|
LastConnectedReplicaID: uuid.NullUUID{},
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
// Create a workspace with a disconnected agent
|
|
disconnectedBuild := dbfake.WorkspaceBuild(t, db, database.WorkspaceTable{
|
|
OrganizationID: user.OrganizationID,
|
|
OwnerID: user.UserID,
|
|
Name: "disconnected-workspace",
|
|
}).WithAgent().Do()
|
|
|
|
// Mark the agent as disconnected
|
|
require.Len(t, disconnectedBuild.Agents, 1)
|
|
disconnectedTime := now.Add(-time.Hour)
|
|
err = db.UpdateWorkspaceAgentConnectionByID(ctx, database.UpdateWorkspaceAgentConnectionByIDParams{
|
|
ID: disconnectedBuild.Agents[0].ID,
|
|
FirstConnectedAt: sql.NullTime{Time: disconnectedTime, Valid: true},
|
|
LastConnectedAt: sql.NullTime{Time: disconnectedTime, Valid: true},
|
|
DisconnectedAt: sql.NullTime{Time: now, Valid: true},
|
|
UpdatedAt: now,
|
|
LastConnectedReplicaID: uuid.NullUUID{},
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
|
defer cancel()
|
|
|
|
// healthy:true should only return the connected workspace
|
|
res, err := client.Workspaces(ctx, codersdk.WorkspaceFilter{
|
|
FilterQuery: "healthy:true",
|
|
})
|
|
require.NoError(t, err)
|
|
require.Len(t, res.Workspaces, 1)
|
|
require.Equal(t, connectedBuild.Workspace.ID, res.Workspaces[0].ID)
|
|
})
|
|
|
|
t.Run("Unhealthy", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
// healthy:false should return workspaces with disconnected or timed out agents
|
|
// and exclude workspaces with connected agents
|
|
store, ps, sqlDB := dbtestutil.NewDBWithSQLDB(t)
|
|
client := coderdtest.New(t, &coderdtest.Options{
|
|
Database: store,
|
|
Pubsub: ps,
|
|
})
|
|
user := coderdtest.CreateFirstUser(t, client)
|
|
now := time.Now()
|
|
|
|
//nolint:gocritic // This is a test, we need system context to update agent connection
|
|
ctx := dbauthz.AsSystemRestricted(context.Background())
|
|
|
|
// Create a workspace with a connected agent (should be excluded)
|
|
connectedBuild := dbfake.WorkspaceBuild(t, store, database.WorkspaceTable{
|
|
OrganizationID: user.OrganizationID,
|
|
OwnerID: user.UserID,
|
|
Name: "connected-workspace",
|
|
}).WithAgent().Do()
|
|
require.Len(t, connectedBuild.Agents, 1)
|
|
err := store.UpdateWorkspaceAgentConnectionByID(ctx, database.UpdateWorkspaceAgentConnectionByIDParams{
|
|
ID: connectedBuild.Agents[0].ID,
|
|
FirstConnectedAt: sql.NullTime{Time: now, Valid: true},
|
|
LastConnectedAt: sql.NullTime{Time: now, Valid: true},
|
|
DisconnectedAt: sql.NullTime{},
|
|
UpdatedAt: now,
|
|
LastConnectedReplicaID: uuid.NullUUID{},
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
// Create a workspace with a disconnected agent
|
|
disconnectedBuild := dbfake.WorkspaceBuild(t, store, database.WorkspaceTable{
|
|
OrganizationID: user.OrganizationID,
|
|
OwnerID: user.UserID,
|
|
Name: "disconnected-workspace",
|
|
}).WithAgent().Do()
|
|
require.Len(t, disconnectedBuild.Agents, 1)
|
|
disconnectedTime := now.Add(-time.Hour)
|
|
err = store.UpdateWorkspaceAgentConnectionByID(ctx, database.UpdateWorkspaceAgentConnectionByIDParams{
|
|
ID: disconnectedBuild.Agents[0].ID,
|
|
FirstConnectedAt: sql.NullTime{Time: disconnectedTime, Valid: true},
|
|
LastConnectedAt: sql.NullTime{Time: disconnectedTime, Valid: true},
|
|
DisconnectedAt: sql.NullTime{Time: now, Valid: true},
|
|
UpdatedAt: now,
|
|
LastConnectedReplicaID: uuid.NullUUID{},
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
// Create a workspace with a timed out agent (never connected, timeout exceeded)
|
|
timedOutBuild := dbfake.WorkspaceBuild(t, store, database.WorkspaceTable{
|
|
OrganizationID: user.OrganizationID,
|
|
OwnerID: user.UserID,
|
|
Name: "timeout-workspace",
|
|
}).WithAgent(func(agents []*proto.Agent) []*proto.Agent {
|
|
agents[0].ConnectionTimeoutSeconds = 1
|
|
return agents
|
|
}).Do()
|
|
require.Len(t, timedOutBuild.Agents, 1)
|
|
// Set created_at to the past so the timeout is exceeded
|
|
_, err = sqlDB.ExecContext(ctx, "UPDATE workspace_agents SET created_at = $1 WHERE id = $2",
|
|
now.Add(-time.Hour), timedOutBuild.Agents[0].ID)
|
|
require.NoError(t, err)
|
|
|
|
testCtx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
|
defer cancel()
|
|
|
|
// healthy:false should return both disconnected and timed out workspaces
|
|
res, err := client.Workspaces(testCtx, codersdk.WorkspaceFilter{
|
|
FilterQuery: "healthy:false",
|
|
})
|
|
require.NoError(t, err)
|
|
require.Len(t, res.Workspaces, 2)
|
|
workspaceIDs := []uuid.UUID{res.Workspaces[0].ID, res.Workspaces[1].ID}
|
|
require.Contains(t, workspaceIDs, disconnectedBuild.Workspace.ID)
|
|
require.Contains(t, workspaceIDs, timedOutBuild.Workspace.ID)
|
|
})
|
|
})
|
|
t.Run("Params", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
const (
|
|
paramOneName = "one"
|
|
paramTwoName = "two"
|
|
paramThreeName = "three"
|
|
paramOptional = "optional"
|
|
)
|
|
|
|
makeParameters := func(extra ...*proto.RichParameter) *echo.Responses {
|
|
return &echo.Responses{
|
|
Parse: echo.ParseComplete,
|
|
ProvisionGraph: []*proto.Response{
|
|
{
|
|
Type: &proto.Response_Graph{
|
|
Graph: &proto.GraphComplete{
|
|
Parameters: append([]*proto.RichParameter{
|
|
{Name: paramOneName, Description: "", Mutable: true, Type: "string"},
|
|
{Name: paramTwoName, DisplayName: "", Description: "", Mutable: true, Type: "string"},
|
|
{Name: paramThreeName, Description: "", Mutable: true, Type: "string"},
|
|
}, extra...),
|
|
},
|
|
},
|
|
},
|
|
},
|
|
ProvisionApply: echo.ApplyComplete,
|
|
}
|
|
}
|
|
|
|
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
|
|
user := coderdtest.CreateFirstUser(t, client)
|
|
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, makeParameters(&proto.RichParameter{Name: paramOptional, Description: "", Mutable: true, Type: "string"}))
|
|
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
|
|
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
|
|
noOptionalVersion := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, makeParameters(), func(request *codersdk.CreateTemplateVersionRequest) {
|
|
request.TemplateID = template.ID
|
|
})
|
|
coderdtest.AwaitTemplateVersionJobCompleted(t, client, noOptionalVersion.ID)
|
|
|
|
// foo :: one=foo, two=bar, one=baz, optional=optional
|
|
foo := coderdtest.CreateWorkspace(t, client, uuid.Nil, func(request *codersdk.CreateWorkspaceRequest) {
|
|
request.TemplateVersionID = version.ID
|
|
request.RichParameterValues = []codersdk.WorkspaceBuildParameter{
|
|
{
|
|
Name: paramOneName,
|
|
Value: "foo",
|
|
},
|
|
{
|
|
Name: paramTwoName,
|
|
Value: "bar",
|
|
},
|
|
{
|
|
Name: paramThreeName,
|
|
Value: "baz",
|
|
},
|
|
{
|
|
Name: paramOptional,
|
|
Value: "optional",
|
|
},
|
|
}
|
|
})
|
|
|
|
// bar :: one=foo, two=bar, three=baz, optional=optional
|
|
bar := coderdtest.CreateWorkspace(t, client, uuid.Nil, func(request *codersdk.CreateWorkspaceRequest) {
|
|
request.TemplateVersionID = version.ID
|
|
request.RichParameterValues = []codersdk.WorkspaceBuildParameter{
|
|
{
|
|
Name: paramOneName,
|
|
Value: "bar",
|
|
},
|
|
{
|
|
Name: paramTwoName,
|
|
Value: "bar",
|
|
},
|
|
{
|
|
Name: paramThreeName,
|
|
Value: "baz",
|
|
},
|
|
{
|
|
Name: paramOptional,
|
|
Value: "optional",
|
|
},
|
|
}
|
|
})
|
|
|
|
// baz :: one=baz, two=baz, three=baz
|
|
baz := coderdtest.CreateWorkspace(t, client, uuid.Nil, func(request *codersdk.CreateWorkspaceRequest) {
|
|
request.TemplateVersionID = noOptionalVersion.ID
|
|
request.RichParameterValues = []codersdk.WorkspaceBuildParameter{
|
|
{
|
|
Name: paramOneName,
|
|
Value: "unique",
|
|
},
|
|
{
|
|
Name: paramTwoName,
|
|
Value: "baz",
|
|
},
|
|
{
|
|
Name: paramThreeName,
|
|
Value: "baz",
|
|
},
|
|
}
|
|
})
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
|
defer cancel()
|
|
|
|
//nolint:tparallel,paralleltest
|
|
t.Run("has_param", func(t *testing.T) {
|
|
// Checks the existence of a param value
|
|
// all match
|
|
all, err := client.Workspaces(ctx, codersdk.WorkspaceFilter{
|
|
FilterQuery: fmt.Sprintf("param:%s", paramOneName),
|
|
})
|
|
require.NoError(t, err)
|
|
expectIDs(t, []codersdk.Workspace{foo, bar, baz}, all.Workspaces)
|
|
|
|
// Some match
|
|
optional, err := client.Workspaces(ctx, codersdk.WorkspaceFilter{
|
|
FilterQuery: fmt.Sprintf("param:%s", paramOptional),
|
|
})
|
|
require.NoError(t, err)
|
|
expectIDs(t, []codersdk.Workspace{foo, bar}, optional.Workspaces)
|
|
|
|
// None match
|
|
none, err := client.Workspaces(ctx, codersdk.WorkspaceFilter{
|
|
FilterQuery: "param:not-a-param",
|
|
})
|
|
require.NoError(t, err)
|
|
require.Len(t, none.Workspaces, 0)
|
|
})
|
|
|
|
//nolint:tparallel,paralleltest
|
|
t.Run("exact_param", func(t *testing.T) {
|
|
// All match
|
|
all, err := client.Workspaces(ctx, codersdk.WorkspaceFilter{
|
|
FilterQuery: fmt.Sprintf("param:%s=%s", paramThreeName, "baz"),
|
|
})
|
|
require.NoError(t, err)
|
|
expectIDs(t, []codersdk.Workspace{foo, bar, baz}, all.Workspaces)
|
|
|
|
// Two match
|
|
two, err := client.Workspaces(ctx, codersdk.WorkspaceFilter{
|
|
FilterQuery: fmt.Sprintf("param:%s=%s", paramTwoName, "bar"),
|
|
})
|
|
require.NoError(t, err)
|
|
expectIDs(t, []codersdk.Workspace{foo, bar}, two.Workspaces)
|
|
|
|
// Only 1 matches
|
|
one, err := client.Workspaces(ctx, codersdk.WorkspaceFilter{
|
|
FilterQuery: fmt.Sprintf("param:%s=%s", paramOneName, "foo"),
|
|
})
|
|
require.NoError(t, err)
|
|
expectIDs(t, []codersdk.Workspace{foo}, one.Workspaces)
|
|
})
|
|
|
|
//nolint:tparallel,paralleltest
|
|
t.Run("exact_param_and_has", func(t *testing.T) {
|
|
all, err := client.Workspaces(ctx, codersdk.WorkspaceFilter{
|
|
FilterQuery: fmt.Sprintf("param:not=athing param:%s=%s param:%s=%s", paramOptional, "optional", paramOneName, "unique"),
|
|
})
|
|
require.NoError(t, err)
|
|
expectIDs(t, []codersdk.Workspace{foo, bar, baz}, all.Workspaces)
|
|
})
|
|
})
|
|
}
|
|
|
|
func TestOffsetLimit(t *testing.T) {
|
|
t.Parallel()
|
|
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
|
defer cancel()
|
|
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
|
|
user := coderdtest.CreateFirstUser(t, client)
|
|
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
|
|
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
|
|
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
|
|
_ = coderdtest.CreateWorkspace(t, client, template.ID)
|
|
_ = coderdtest.CreateWorkspace(t, client, template.ID)
|
|
_ = coderdtest.CreateWorkspace(t, client, template.ID)
|
|
|
|
// Case 1: empty finds all workspaces
|
|
ws, err := client.Workspaces(ctx, codersdk.WorkspaceFilter{})
|
|
require.NoError(t, err)
|
|
require.Len(t, ws.Workspaces, 3)
|
|
|
|
// Case 2: offset 1 finds 2 workspaces
|
|
ws, err = client.Workspaces(ctx, codersdk.WorkspaceFilter{
|
|
Offset: 1,
|
|
})
|
|
require.NoError(t, err)
|
|
require.Len(t, ws.Workspaces, 2)
|
|
|
|
// Case 3: offset 1 limit 1 finds 1 workspace
|
|
ws, err = client.Workspaces(ctx, codersdk.WorkspaceFilter{
|
|
Offset: 1,
|
|
Limit: 1,
|
|
})
|
|
require.NoError(t, err)
|
|
require.Len(t, ws.Workspaces, 1)
|
|
|
|
// Case 4: offset 3 finds no workspaces
|
|
ws, err = client.Workspaces(ctx, codersdk.WorkspaceFilter{
|
|
Offset: 3,
|
|
})
|
|
require.NoError(t, err)
|
|
require.Len(t, ws.Workspaces, 0)
|
|
require.Equal(t, ws.Count, 3) // can't find workspaces, but count is non-zero
|
|
|
|
// Case 5: offset out of range
|
|
ws, err = client.Workspaces(ctx, codersdk.WorkspaceFilter{
|
|
Offset: math.MaxInt32 + 1, // Potential risk: pq: OFFSET must not be negative
|
|
})
|
|
require.Error(t, err)
|
|
}
|
|
|
|
func TestWorkspaceUpdateAutostart(t *testing.T) {
|
|
t.Parallel()
|
|
dublinLoc := mustLocation(t, "Europe/Dublin")
|
|
|
|
testCases := []struct {
|
|
name string
|
|
schedule *string
|
|
expectedError string
|
|
at time.Time
|
|
expectedNext time.Time
|
|
expectedInterval time.Duration
|
|
}{
|
|
{
|
|
name: "disable autostart",
|
|
schedule: ptr.Ref(""),
|
|
expectedError: "",
|
|
},
|
|
{
|
|
name: "friday to monday",
|
|
schedule: ptr.Ref("CRON_TZ=Europe/Dublin 30 9 * * 1-5"),
|
|
expectedError: "",
|
|
at: time.Date(2022, 5, 6, 9, 31, 0, 0, dublinLoc),
|
|
expectedNext: time.Date(2022, 5, 9, 9, 30, 0, 0, dublinLoc),
|
|
expectedInterval: 71*time.Hour + 59*time.Minute,
|
|
},
|
|
{
|
|
name: "monday to tuesday",
|
|
schedule: ptr.Ref("CRON_TZ=Europe/Dublin 30 9 * * 1-5"),
|
|
expectedError: "",
|
|
at: time.Date(2022, 5, 9, 9, 31, 0, 0, dublinLoc),
|
|
expectedNext: time.Date(2022, 5, 10, 9, 30, 0, 0, dublinLoc),
|
|
expectedInterval: 23*time.Hour + 59*time.Minute,
|
|
},
|
|
{
|
|
// DST in Ireland began on Mar 27 in 2022 at 0100. Forward 1 hour.
|
|
name: "DST start",
|
|
schedule: ptr.Ref("CRON_TZ=Europe/Dublin 30 9 * * *"),
|
|
expectedError: "",
|
|
at: time.Date(2022, 3, 26, 9, 31, 0, 0, dublinLoc),
|
|
expectedNext: time.Date(2022, 3, 27, 9, 30, 0, 0, dublinLoc),
|
|
expectedInterval: 22*time.Hour + 59*time.Minute,
|
|
},
|
|
{
|
|
// DST in Ireland ends on Oct 30 in 2022 at 0200. Back 1 hour.
|
|
name: "DST end",
|
|
schedule: ptr.Ref("CRON_TZ=Europe/Dublin 30 9 * * *"),
|
|
expectedError: "",
|
|
at: time.Date(2022, 10, 29, 9, 31, 0, 0, dublinLoc),
|
|
expectedNext: time.Date(2022, 10, 30, 9, 30, 0, 0, dublinLoc),
|
|
expectedInterval: 24*time.Hour + 59*time.Minute,
|
|
},
|
|
{
|
|
name: "invalid location",
|
|
schedule: ptr.Ref("CRON_TZ=Imaginary/Place 30 9 * * 1-5"),
|
|
expectedError: "parse schedule: provided bad location Imaginary/Place: unknown time zone Imaginary/Place",
|
|
},
|
|
{
|
|
name: "invalid schedule",
|
|
schedule: ptr.Ref("asdf asdf asdf "),
|
|
expectedError: `validate weekly schedule: expected schedule to consist of 5 fields with an optional CRON_TZ=<timezone> prefix`,
|
|
},
|
|
{
|
|
name: "only 3 values",
|
|
schedule: ptr.Ref("CRON_TZ=Europe/Dublin 30 9 *"),
|
|
expectedError: `validate weekly schedule: expected schedule to consist of 5 fields with an optional CRON_TZ=<timezone> prefix`,
|
|
},
|
|
}
|
|
|
|
for _, testCase := range testCases {
|
|
t.Run(testCase.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
var (
|
|
auditor = audit.NewMock()
|
|
client = coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true, Auditor: auditor})
|
|
user = coderdtest.CreateFirstUser(t, client)
|
|
version = coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
|
|
_ = coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
|
|
project = coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
|
|
workspace = coderdtest.CreateWorkspace(t, client, project.ID, func(cwr *codersdk.CreateWorkspaceRequest) {
|
|
cwr.AutostartSchedule = nil
|
|
cwr.TTLMillis = nil
|
|
})
|
|
)
|
|
|
|
// await job to ensure audit logs for workspace_build start are created
|
|
_ = coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, workspace.LatestBuild.ID)
|
|
|
|
// ensure test invariant: new workspaces have no autostart schedule.
|
|
require.Empty(t, workspace.AutostartSchedule, "expected newly-minted workspace to have no autostart schedule")
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
|
defer cancel()
|
|
|
|
err := client.UpdateWorkspaceAutostart(ctx, workspace.ID, codersdk.UpdateWorkspaceAutostartRequest{
|
|
Schedule: testCase.schedule,
|
|
})
|
|
|
|
if testCase.expectedError != "" {
|
|
require.ErrorContains(t, err, testCase.expectedError, "Invalid autostart schedule")
|
|
return
|
|
}
|
|
|
|
require.NoError(t, err, "expected no error setting workspace autostart schedule")
|
|
|
|
updated, err := client.Workspace(ctx, workspace.ID)
|
|
require.NoError(t, err, "fetch updated workspace")
|
|
|
|
if testCase.schedule == nil || *testCase.schedule == "" {
|
|
require.Nil(t, updated.AutostartSchedule)
|
|
return
|
|
}
|
|
|
|
require.EqualValues(t, *testCase.schedule, *updated.AutostartSchedule, "expected autostart schedule to equal requested")
|
|
|
|
sched, err := cron.Weekly(*updated.AutostartSchedule)
|
|
require.NoError(t, err, "parse returned schedule")
|
|
|
|
next := sched.Next(testCase.at)
|
|
require.Equal(t, testCase.expectedNext, next, "unexpected next scheduled autostart time")
|
|
interval := next.Sub(testCase.at)
|
|
require.Equal(t, testCase.expectedInterval, interval, "unexpected interval")
|
|
|
|
require.Eventually(t, func() bool {
|
|
if len(auditor.AuditLogs()) < 7 {
|
|
return false
|
|
}
|
|
return auditor.AuditLogs()[6].Action == database.AuditActionWrite ||
|
|
auditor.AuditLogs()[5].Action == database.AuditActionWrite
|
|
}, testutil.WaitShort, testutil.IntervalFast)
|
|
})
|
|
}
|
|
|
|
t.Run("CustomAutostartDisabledByTemplate", func(t *testing.T) {
|
|
t.Parallel()
|
|
var (
|
|
tss = schedule.MockTemplateScheduleStore{
|
|
GetFn: func(_ context.Context, _ database.Store, _ uuid.UUID) (schedule.TemplateScheduleOptions, error) {
|
|
return schedule.TemplateScheduleOptions{
|
|
UserAutostartEnabled: false,
|
|
UserAutostopEnabled: false,
|
|
DefaultTTL: 0,
|
|
AutostopRequirement: schedule.TemplateAutostopRequirement{},
|
|
}, nil
|
|
},
|
|
SetFn: func(_ context.Context, _ database.Store, tpl database.Template, _ schedule.TemplateScheduleOptions) (database.Template, error) {
|
|
return tpl, nil
|
|
},
|
|
}
|
|
|
|
client = coderdtest.New(t, &coderdtest.Options{
|
|
IncludeProvisionerDaemon: true,
|
|
TemplateScheduleStore: tss,
|
|
})
|
|
user = coderdtest.CreateFirstUser(t, client)
|
|
version = coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
|
|
_ = coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
|
|
project = coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
|
|
workspace = coderdtest.CreateWorkspace(t, client, project.ID, func(cwr *codersdk.CreateWorkspaceRequest) {
|
|
cwr.AutostartSchedule = nil
|
|
cwr.TTLMillis = nil
|
|
})
|
|
)
|
|
|
|
// await job to ensure audit logs for workspace_build start are created
|
|
_ = coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, workspace.LatestBuild.ID)
|
|
|
|
// ensure test invariant: new workspaces have no autostart schedule.
|
|
require.Empty(t, workspace.AutostartSchedule, "expected newly-minted workspace to have no autostart schedule")
|
|
|
|
ctx := testutil.Context(t, testutil.WaitLong)
|
|
|
|
err := client.UpdateWorkspaceAutostart(ctx, workspace.ID, codersdk.UpdateWorkspaceAutostartRequest{
|
|
Schedule: ptr.Ref("CRON_TZ=Europe/Dublin 30 9 * * 1-5"),
|
|
})
|
|
require.ErrorContains(t, err, "Autostart is not allowed for workspaces using this template")
|
|
})
|
|
|
|
t.Run("NotFound", func(t *testing.T) {
|
|
t.Parallel()
|
|
var (
|
|
client = coderdtest.New(t, nil)
|
|
_ = coderdtest.CreateFirstUser(t, client)
|
|
wsid = uuid.New()
|
|
req = codersdk.UpdateWorkspaceAutostartRequest{
|
|
Schedule: ptr.Ref("9 30 1-5"),
|
|
}
|
|
)
|
|
|
|
ctx := testutil.Context(t, testutil.WaitLong)
|
|
|
|
err := client.UpdateWorkspaceAutostart(ctx, wsid, req)
|
|
require.IsType(t, err, &codersdk.Error{}, "expected codersdk.Error")
|
|
coderSDKErr, _ := err.(*codersdk.Error) //nolint:errorlint
|
|
require.Equal(t, coderSDKErr.StatusCode(), 404, "expected status code 404")
|
|
require.Contains(t, coderSDKErr.Message, "Resource not found", "unexpected response code")
|
|
})
|
|
}
|
|
|
|
func TestWorkspaceUpdateTTL(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
testCases := []struct {
|
|
name string
|
|
ttlMillis *int64
|
|
expectedError string
|
|
modifyTemplate func(*codersdk.CreateTemplateRequest)
|
|
}{
|
|
{
|
|
name: "disable ttl",
|
|
ttlMillis: nil,
|
|
expectedError: "",
|
|
modifyTemplate: func(ctr *codersdk.CreateTemplateRequest) {
|
|
ctr.DefaultTTLMillis = ptr.Ref((8 * time.Hour).Milliseconds())
|
|
},
|
|
},
|
|
{
|
|
name: "update ttl",
|
|
ttlMillis: ptr.Ref(12 * time.Hour.Milliseconds()),
|
|
expectedError: "",
|
|
modifyTemplate: func(ctr *codersdk.CreateTemplateRequest) {
|
|
ctr.DefaultTTLMillis = ptr.Ref((8 * time.Hour).Milliseconds())
|
|
},
|
|
},
|
|
{
|
|
name: "below minimum ttl",
|
|
ttlMillis: ptr.Ref((30 * time.Second).Milliseconds()),
|
|
expectedError: "time until shutdown must be at least one minute",
|
|
},
|
|
{
|
|
name: "minimum ttl",
|
|
ttlMillis: ptr.Ref(time.Minute.Milliseconds()),
|
|
expectedError: "",
|
|
},
|
|
{
|
|
name: "maximum ttl",
|
|
ttlMillis: ptr.Ref((24 * 30 * time.Hour).Milliseconds()),
|
|
expectedError: "",
|
|
},
|
|
{
|
|
name: "above maximum ttl",
|
|
ttlMillis: ptr.Ref((24*30*time.Hour + time.Minute).Milliseconds()),
|
|
expectedError: "time until shutdown must be less than 30 days",
|
|
},
|
|
}
|
|
|
|
for _, testCase := range testCases {
|
|
t.Run(testCase.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
mutators := make([]func(*codersdk.CreateTemplateRequest), 0)
|
|
if testCase.modifyTemplate != nil {
|
|
mutators = append(mutators, testCase.modifyTemplate)
|
|
}
|
|
var (
|
|
auditor = audit.NewMock()
|
|
client = coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true, Auditor: auditor})
|
|
user = coderdtest.CreateFirstUser(t, client)
|
|
version = coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
|
|
_ = coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
|
|
project = coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID, mutators...)
|
|
workspace = coderdtest.CreateWorkspace(t, client, project.ID, func(cwr *codersdk.CreateWorkspaceRequest) {
|
|
cwr.AutostartSchedule = nil
|
|
cwr.TTLMillis = nil
|
|
})
|
|
_ = coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, workspace.LatestBuild.ID)
|
|
)
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
|
defer cancel()
|
|
|
|
err := client.UpdateWorkspaceTTL(ctx, workspace.ID, codersdk.UpdateWorkspaceTTLRequest{
|
|
TTLMillis: testCase.ttlMillis,
|
|
})
|
|
|
|
if testCase.expectedError != "" {
|
|
require.ErrorContains(t, err, testCase.expectedError, "unexpected error when setting workspace autostop schedule")
|
|
return
|
|
}
|
|
|
|
require.NoError(t, err, "expected no error setting workspace autostop schedule")
|
|
|
|
updated, err := client.Workspace(ctx, workspace.ID)
|
|
require.NoError(t, err, "fetch updated workspace")
|
|
|
|
require.Equal(t, testCase.ttlMillis, updated.TTLMillis, "expected autostop ttl to equal requested")
|
|
|
|
require.Eventually(t, func() bool {
|
|
if len(auditor.AuditLogs()) != 7 {
|
|
return false
|
|
}
|
|
return auditor.AuditLogs()[6].Action == database.AuditActionWrite ||
|
|
auditor.AuditLogs()[5].Action == database.AuditActionWrite
|
|
}, testutil.WaitMedium, testutil.IntervalFast, "expected audit log to be written")
|
|
})
|
|
}
|
|
|
|
t.Run("ModifyAutostopWithRunningWorkspace", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
testCases := []struct {
|
|
name string
|
|
fromTTL *int64
|
|
toTTL *int64
|
|
afterUpdate func(t *testing.T, before, after codersdk.NullTime)
|
|
}{
|
|
{
|
|
name: "RemoveAutostopRemovesDeadline",
|
|
fromTTL: ptr.Ref((8 * time.Hour).Milliseconds()),
|
|
toTTL: nil,
|
|
afterUpdate: func(t *testing.T, before, after codersdk.NullTime) {
|
|
require.NotZero(t, before)
|
|
require.Zero(t, after)
|
|
},
|
|
},
|
|
{
|
|
name: "AddAutostopDoesNotAddDeadline",
|
|
fromTTL: nil,
|
|
toTTL: ptr.Ref((8 * time.Hour).Milliseconds()),
|
|
afterUpdate: func(t *testing.T, before, after codersdk.NullTime) {
|
|
require.Zero(t, before)
|
|
require.Zero(t, after)
|
|
},
|
|
},
|
|
{
|
|
name: "IncreaseAutostopDoesNotModifyDeadline",
|
|
fromTTL: ptr.Ref((4 * time.Hour).Milliseconds()),
|
|
toTTL: ptr.Ref((8 * time.Hour).Milliseconds()),
|
|
afterUpdate: func(t *testing.T, before, after codersdk.NullTime) {
|
|
require.NotZero(t, before)
|
|
require.NotZero(t, after)
|
|
require.Equal(t, before, after)
|
|
},
|
|
},
|
|
{
|
|
name: "DecreaseAutostopDoesNotModifyDeadline",
|
|
fromTTL: ptr.Ref((8 * time.Hour).Milliseconds()),
|
|
toTTL: ptr.Ref((4 * time.Hour).Milliseconds()),
|
|
afterUpdate: func(t *testing.T, before, after codersdk.NullTime) {
|
|
require.NotZero(t, before)
|
|
require.NotZero(t, after)
|
|
require.Equal(t, before, after)
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, testCase := range testCases {
|
|
t.Run(testCase.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
var (
|
|
client = coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
|
|
user = coderdtest.CreateFirstUser(t, client)
|
|
version = coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
|
|
_ = coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
|
|
template = coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
|
|
workspace = coderdtest.CreateWorkspace(t, client, template.ID, func(cwr *codersdk.CreateWorkspaceRequest) {
|
|
cwr.TTLMillis = testCase.fromTTL
|
|
})
|
|
build = coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, workspace.LatestBuild.ID)
|
|
)
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
|
defer cancel()
|
|
|
|
// Re-fetch the workspace build. This is required because
|
|
// `AwaitWorkspaceBuildJobCompleted` can return stale data.
|
|
build, err := client.WorkspaceBuild(ctx, build.ID)
|
|
require.NoError(t, err)
|
|
|
|
deadlineBefore := build.Deadline
|
|
|
|
err = client.UpdateWorkspaceTTL(ctx, workspace.ID, codersdk.UpdateWorkspaceTTLRequest{
|
|
TTLMillis: testCase.toTTL,
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
build, err = client.WorkspaceBuild(ctx, build.ID)
|
|
require.NoError(t, err)
|
|
|
|
deadlineAfter := build.Deadline
|
|
|
|
testCase.afterUpdate(t, deadlineBefore, deadlineAfter)
|
|
})
|
|
}
|
|
})
|
|
|
|
t.Run("RemoveAutostopWithRunningWorkspaceWithMaxDeadline", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
var (
|
|
ctx = testutil.Context(t, testutil.WaitLong)
|
|
client, db = coderdtest.NewWithDatabase(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
|
|
user = coderdtest.CreateFirstUser(t, client)
|
|
version = coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
|
|
_ = coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
|
|
template = coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
|
|
deadline = 8 * time.Hour
|
|
maxDeadline = 10 * time.Hour
|
|
workspace = coderdtest.CreateWorkspace(t, client, template.ID, func(cwr *codersdk.CreateWorkspaceRequest) {
|
|
cwr.TTLMillis = ptr.Ref(deadline.Milliseconds())
|
|
})
|
|
build = coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, workspace.LatestBuild.ID)
|
|
)
|
|
|
|
// This is a hack, but the max_deadline isn't precisely configurable
|
|
// without a lot of unnecessary hassle.
|
|
dbBuild, err := db.GetWorkspaceBuildByID(dbauthz.AsSystemRestricted(ctx), build.ID)
|
|
require.NoError(t, err)
|
|
dbJob, err := db.GetProvisionerJobByID(dbauthz.AsSystemRestricted(ctx), dbBuild.JobID)
|
|
require.NoError(t, err)
|
|
require.True(t, dbJob.CompletedAt.Valid)
|
|
initialDeadline := dbJob.CompletedAt.Time.Add(deadline)
|
|
expectedMaxDeadline := dbJob.CompletedAt.Time.Add(maxDeadline)
|
|
err = db.UpdateWorkspaceBuildDeadlineByID(dbauthz.AsSystemRestricted(ctx), database.UpdateWorkspaceBuildDeadlineByIDParams{
|
|
ID: build.ID,
|
|
Deadline: initialDeadline,
|
|
MaxDeadline: expectedMaxDeadline,
|
|
UpdatedAt: dbtime.Now(),
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
// Remove autostop.
|
|
err = client.UpdateWorkspaceTTL(ctx, workspace.ID, codersdk.UpdateWorkspaceTTLRequest{
|
|
TTLMillis: nil,
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
// Expect that the deadline is set to the max_deadline.
|
|
build, err = client.WorkspaceBuild(ctx, build.ID)
|
|
require.NoError(t, err)
|
|
require.True(t, build.Deadline.Valid)
|
|
require.WithinDuration(t, build.Deadline.Time, expectedMaxDeadline, time.Second)
|
|
require.True(t, build.MaxDeadline.Valid)
|
|
require.WithinDuration(t, build.MaxDeadline.Time, expectedMaxDeadline, time.Second)
|
|
})
|
|
|
|
t.Run("CustomAutostopDisabledByTemplate", func(t *testing.T) {
|
|
t.Parallel()
|
|
var (
|
|
tss = schedule.MockTemplateScheduleStore{
|
|
GetFn: func(_ context.Context, _ database.Store, _ uuid.UUID) (schedule.TemplateScheduleOptions, error) {
|
|
return schedule.TemplateScheduleOptions{
|
|
UserAutostartEnabled: false,
|
|
UserAutostopEnabled: false,
|
|
DefaultTTL: 0,
|
|
AutostopRequirement: schedule.TemplateAutostopRequirement{},
|
|
}, nil
|
|
},
|
|
SetFn: func(_ context.Context, _ database.Store, tpl database.Template, _ schedule.TemplateScheduleOptions) (database.Template, error) {
|
|
return tpl, nil
|
|
},
|
|
}
|
|
|
|
client = coderdtest.New(t, &coderdtest.Options{
|
|
IncludeProvisionerDaemon: true,
|
|
TemplateScheduleStore: tss,
|
|
})
|
|
user = coderdtest.CreateFirstUser(t, client)
|
|
version = coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
|
|
_ = coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
|
|
project = coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
|
|
workspace = coderdtest.CreateWorkspace(t, client, project.ID, func(cwr *codersdk.CreateWorkspaceRequest) {
|
|
cwr.AutostartSchedule = nil
|
|
cwr.TTLMillis = nil
|
|
})
|
|
)
|
|
|
|
// await job to ensure audit logs for workspace_build start are created
|
|
_ = coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, workspace.LatestBuild.ID)
|
|
|
|
// ensure test invariant: new workspaces have no autostart schedule.
|
|
require.Empty(t, workspace.AutostartSchedule, "expected newly-minted workspace to have no autostart schedule")
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
|
defer cancel()
|
|
|
|
err := client.UpdateWorkspaceTTL(ctx, workspace.ID, codersdk.UpdateWorkspaceTTLRequest{
|
|
TTLMillis: ptr.Ref(time.Hour.Milliseconds()),
|
|
})
|
|
require.ErrorContains(t, err, "Custom autostop TTL is not allowed for workspaces using this template")
|
|
})
|
|
|
|
t.Run("NotFound", func(t *testing.T) {
|
|
t.Parallel()
|
|
var (
|
|
client = coderdtest.New(t, nil)
|
|
_ = coderdtest.CreateFirstUser(t, client)
|
|
wsid = uuid.New()
|
|
req = codersdk.UpdateWorkspaceTTLRequest{
|
|
TTLMillis: ptr.Ref(time.Hour.Milliseconds()),
|
|
}
|
|
)
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
|
defer cancel()
|
|
|
|
err := client.UpdateWorkspaceTTL(ctx, wsid, req)
|
|
require.IsType(t, err, &codersdk.Error{}, "expected codersdk.Error")
|
|
coderSDKErr, _ := err.(*codersdk.Error) //nolint:errorlint
|
|
require.Equal(t, coderSDKErr.StatusCode(), 404, "expected status code 404")
|
|
require.Contains(t, coderSDKErr.Message, "Resource not found", "unexpected response code")
|
|
})
|
|
}
|
|
|
|
func TestWorkspaceExtend(t *testing.T) {
|
|
t.Parallel()
|
|
var (
|
|
ttl = 8 * time.Hour
|
|
newDeadline = time.Now().Add(ttl + time.Hour).UTC()
|
|
client = coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
|
|
user = coderdtest.CreateFirstUser(t, client)
|
|
version = coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
|
|
_ = coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
|
|
template = coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
|
|
workspace = coderdtest.CreateWorkspace(t, client, template.ID, func(cwr *codersdk.CreateWorkspaceRequest) {
|
|
cwr.TTLMillis = ptr.Ref(ttl.Milliseconds())
|
|
})
|
|
_ = coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, workspace.LatestBuild.ID)
|
|
)
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
|
defer cancel()
|
|
|
|
workspace, err := client.Workspace(ctx, workspace.ID)
|
|
require.NoError(t, err, "fetch provisioned workspace")
|
|
oldDeadline := workspace.LatestBuild.Deadline.Time
|
|
|
|
// Updating the deadline should succeed
|
|
req := codersdk.PutExtendWorkspaceRequest{
|
|
Deadline: newDeadline,
|
|
}
|
|
err = client.PutExtendWorkspace(ctx, workspace.ID, req)
|
|
require.NoError(t, err, "failed to extend workspace")
|
|
|
|
// Ensure deadline set correctly
|
|
updated, err := client.Workspace(ctx, workspace.ID)
|
|
require.NoError(t, err, "failed to fetch updated workspace")
|
|
require.WithinDuration(t, newDeadline, updated.LatestBuild.Deadline.Time, time.Minute)
|
|
|
|
// Zero time should fail
|
|
err = client.PutExtendWorkspace(ctx, workspace.ID, codersdk.PutExtendWorkspaceRequest{
|
|
Deadline: time.Time{},
|
|
})
|
|
require.ErrorContains(t, err, "deadline: Validation failed for tag \"required\" with value: \"0001-01-01 00:00:00 +0000 UTC\"", "setting an empty deadline on a workspace should fail")
|
|
|
|
// Updating with a deadline less than 30 minutes in the future should fail
|
|
deadlineTooSoon := time.Now().Add(15 * time.Minute) // XXX: time.Now
|
|
err = client.PutExtendWorkspace(ctx, workspace.ID, codersdk.PutExtendWorkspaceRequest{
|
|
Deadline: deadlineTooSoon,
|
|
})
|
|
require.ErrorContains(t, err, "unexpected status code 400: Cannot extend workspace: new deadline must be at least 30 minutes in the future", "setting a deadline less than 30 minutes in the future should fail")
|
|
|
|
// Updating with a deadline 30 minutes in the future should succeed
|
|
deadlineJustSoonEnough := time.Now().Add(30 * time.Minute)
|
|
err = client.PutExtendWorkspace(ctx, workspace.ID, codersdk.PutExtendWorkspaceRequest{
|
|
Deadline: deadlineJustSoonEnough,
|
|
})
|
|
require.NoError(t, err, "setting a deadline at least 30 minutes in the future should succeed")
|
|
|
|
// Updating with a deadline an hour before the previous deadline should succeed
|
|
err = client.PutExtendWorkspace(ctx, workspace.ID, codersdk.PutExtendWorkspaceRequest{
|
|
Deadline: oldDeadline.Add(-time.Hour),
|
|
})
|
|
require.NoError(t, err, "setting an earlier deadline should not fail")
|
|
|
|
// Ensure deadline still set correctly
|
|
updated, err = client.Workspace(ctx, workspace.ID)
|
|
require.NoError(t, err, "failed to fetch updated workspace")
|
|
require.WithinDuration(t, oldDeadline.Add(-time.Hour), updated.LatestBuild.Deadline.Time, time.Minute)
|
|
}
|
|
|
|
func TestWorkspaceUpdateAutomaticUpdates_OK(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
var (
|
|
auditor = audit.NewMock()
|
|
adminClient = coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true, Auditor: auditor})
|
|
admin = coderdtest.CreateFirstUser(t, adminClient)
|
|
client, user = coderdtest.CreateAnotherUser(t, adminClient, admin.OrganizationID)
|
|
version = coderdtest.CreateTemplateVersion(t, adminClient, admin.OrganizationID, nil)
|
|
_ = coderdtest.AwaitTemplateVersionJobCompleted(t, adminClient, version.ID)
|
|
project = coderdtest.CreateTemplate(t, adminClient, admin.OrganizationID, version.ID)
|
|
workspace = coderdtest.CreateWorkspace(t, client, project.ID, func(cwr *codersdk.CreateWorkspaceRequest) {
|
|
cwr.AutostartSchedule = nil
|
|
cwr.TTLMillis = nil
|
|
cwr.AutomaticUpdates = codersdk.AutomaticUpdatesNever
|
|
})
|
|
)
|
|
|
|
// await job to ensure audit logs for workspace_build start are created
|
|
_ = coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, workspace.LatestBuild.ID)
|
|
|
|
// ensure test invariant: new workspaces have automatic updates set to never
|
|
require.Equal(t, codersdk.AutomaticUpdatesNever, workspace.AutomaticUpdates, "expected newly-minted workspace to automatic updates set to never")
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
|
defer cancel()
|
|
|
|
err := client.UpdateWorkspaceAutomaticUpdates(ctx, workspace.ID, codersdk.UpdateWorkspaceAutomaticUpdatesRequest{
|
|
AutomaticUpdates: codersdk.AutomaticUpdatesAlways,
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
updated, err := client.Workspace(ctx, workspace.ID)
|
|
require.NoError(t, err)
|
|
require.Equal(t, codersdk.AutomaticUpdatesAlways, updated.AutomaticUpdates)
|
|
|
|
require.Eventually(t, func() bool {
|
|
var found bool
|
|
for _, l := range auditor.AuditLogs() {
|
|
if l.Action == database.AuditActionWrite &&
|
|
l.UserID == user.ID &&
|
|
l.ResourceID == workspace.ID {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
return found
|
|
}, testutil.WaitShort, testutil.IntervalFast, "did not find expected audit log")
|
|
}
|
|
|
|
func TestUpdateWorkspaceAutomaticUpdates_NotFound(t *testing.T) {
|
|
t.Parallel()
|
|
var (
|
|
client = coderdtest.New(t, nil)
|
|
_ = coderdtest.CreateFirstUser(t, client)
|
|
wsid = uuid.New()
|
|
req = codersdk.UpdateWorkspaceAutomaticUpdatesRequest{
|
|
AutomaticUpdates: codersdk.AutomaticUpdatesNever,
|
|
}
|
|
)
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
|
defer cancel()
|
|
|
|
err := client.UpdateWorkspaceAutomaticUpdates(ctx, wsid, req)
|
|
require.IsType(t, err, &codersdk.Error{}, "expected codersdk.Error")
|
|
coderSDKErr, _ := err.(*codersdk.Error) //nolint:errorlint
|
|
require.Equal(t, coderSDKErr.StatusCode(), 404, "expected status code 404")
|
|
require.Contains(t, coderSDKErr.Message, "Resource not found", "unexpected response code")
|
|
}
|
|
|
|
func TestWorkspaceWatcher(t *testing.T) {
|
|
t.Parallel()
|
|
client, closeFunc := coderdtest.NewWithProvisionerCloser(t, &coderdtest.Options{
|
|
IncludeProvisionerDaemon: true,
|
|
AllowWorkspaceRenames: true,
|
|
})
|
|
defer closeFunc.Close()
|
|
user := coderdtest.CreateFirstUser(t, client)
|
|
authToken := uuid.NewString()
|
|
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
|
|
Parse: echo.ParseComplete,
|
|
ProvisionPlan: echo.PlanComplete,
|
|
ProvisionGraph: []*proto.Response{{
|
|
Type: &proto.Response_Graph{
|
|
Graph: &proto.GraphComplete{
|
|
Resources: []*proto.Resource{{
|
|
Name: "example",
|
|
Type: "aws_instance",
|
|
Agents: []*proto.Agent{{
|
|
Id: uuid.NewString(),
|
|
Name: "dev",
|
|
Auth: &proto.Agent_Token{
|
|
Token: authToken,
|
|
},
|
|
ConnectionTimeoutSeconds: 1,
|
|
}},
|
|
}},
|
|
},
|
|
},
|
|
}},
|
|
})
|
|
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
|
|
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
|
|
workspace := coderdtest.CreateWorkspace(t, client, template.ID)
|
|
coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, workspace.LatestBuild.ID)
|
|
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
|
defer cancel()
|
|
|
|
wc, err := client.WatchWorkspace(ctx, workspace.ID)
|
|
require.NoError(t, err)
|
|
|
|
// Wait events are easier to debug with timestamped logs.
|
|
logger := testutil.Logger(t).Named(t.Name())
|
|
wait := func(event string, ready func(w codersdk.Workspace) bool) {
|
|
for {
|
|
select {
|
|
case <-ctx.Done():
|
|
require.FailNow(t, "timed out waiting for event", event)
|
|
case w, ok := <-wc:
|
|
require.True(t, ok, "watch channel closed: %s", event)
|
|
if ready == nil || ready(w) {
|
|
logger.Info(ctx, "done waiting for event",
|
|
slog.F("event", event),
|
|
slog.F("workspace", w))
|
|
return
|
|
}
|
|
logger.Info(ctx, "skipped update for event",
|
|
slog.F("event", event),
|
|
slog.F("workspace", w))
|
|
}
|
|
}
|
|
}
|
|
|
|
coderdtest.CreateWorkspaceBuild(t, client, workspace, database.WorkspaceTransitionStart)
|
|
wait("workspace build being created", nil)
|
|
wait("workspace build being acquired", nil)
|
|
wait("workspace build completing", nil)
|
|
|
|
// Unfortunately, this will add ~1s to the test due to the granularity
|
|
// of agent timeout seconds. However, if we don't do this we won't know
|
|
// which trigger we received when waiting for connection.
|
|
//
|
|
// Note that the first timeout is from `coderdtest.CreateWorkspace` and
|
|
// the latter is from `coderdtest.CreateWorkspaceBuild`.
|
|
wait("agent timeout after create", nil)
|
|
wait("agent timeout after start", nil)
|
|
|
|
agt := agenttest.New(t, client.URL, authToken)
|
|
_ = coderdtest.AwaitWorkspaceAgents(t, client, workspace.ID)
|
|
|
|
wait("agent connected/ready", func(w codersdk.Workspace) bool {
|
|
return w.LatestBuild.Resources[0].Agents[0].Status == codersdk.WorkspaceAgentConnected &&
|
|
w.LatestBuild.Resources[0].Agents[0].LifecycleState == codersdk.WorkspaceAgentLifecycleReady
|
|
})
|
|
agt.Close()
|
|
wait("agent disconnected", func(w codersdk.Workspace) bool {
|
|
return w.LatestBuild.Resources[0].Agents[0].Status == codersdk.WorkspaceAgentDisconnected
|
|
})
|
|
|
|
err = client.UpdateWorkspace(ctx, workspace.ID, codersdk.UpdateWorkspaceRequest{
|
|
Name: "another",
|
|
})
|
|
require.NoError(t, err)
|
|
wait("update workspace name", nil)
|
|
|
|
// Add a new version that will fail.
|
|
badVersion := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
|
|
Parse: echo.ParseComplete,
|
|
ProvisionPlan: echo.PlanComplete,
|
|
ProvisionInit: echo.InitComplete,
|
|
ProvisionGraph: echo.GraphComplete,
|
|
ProvisionApply: []*proto.Response{{
|
|
Type: &proto.Response_Apply{
|
|
Apply: &proto.ApplyComplete{
|
|
Error: "test error",
|
|
},
|
|
},
|
|
}},
|
|
}, func(req *codersdk.CreateTemplateVersionRequest) {
|
|
req.TemplateID = template.ID
|
|
})
|
|
coderdtest.AwaitTemplateVersionJobCompleted(t, client, badVersion.ID)
|
|
err = client.UpdateActiveTemplateVersion(ctx, template.ID, codersdk.UpdateActiveTemplateVersion{
|
|
ID: badVersion.ID,
|
|
})
|
|
require.NoError(t, err)
|
|
wait("update active template version", nil)
|
|
|
|
// Build with the new template; should end up with a failure state.
|
|
_ = coderdtest.CreateWorkspaceBuild(t, client, workspace, database.WorkspaceTransitionStart, func(req *codersdk.CreateWorkspaceBuildRequest) {
|
|
req.TemplateVersionID = badVersion.ID
|
|
})
|
|
// We want to verify pending state here, but it's possible that we reach
|
|
// failed state fast enough that we never see pending.
|
|
sawFailed := false
|
|
wait("workspace build pending or failed", func(w codersdk.Workspace) bool {
|
|
switch w.LatestBuild.Status {
|
|
case codersdk.WorkspaceStatusPending:
|
|
return true
|
|
case codersdk.WorkspaceStatusFailed:
|
|
sawFailed = true
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
})
|
|
if !sawFailed {
|
|
wait("workspace build failed", func(w codersdk.Workspace) bool {
|
|
return w.LatestBuild.Status == codersdk.WorkspaceStatusFailed
|
|
})
|
|
}
|
|
|
|
closeFunc.Close()
|
|
build := coderdtest.CreateWorkspaceBuild(t, client, workspace, database.WorkspaceTransitionStart)
|
|
wait("first is for the workspace build itself", nil)
|
|
err = client.CancelWorkspaceBuild(ctx, build.ID, codersdk.CancelWorkspaceBuildParams{})
|
|
require.NoError(t, err)
|
|
wait("second is for the build cancel", nil)
|
|
}
|
|
|
|
func TestWatchAllWorkspaceBuilds(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
// Enable the workspace build updates experiment.
|
|
client, closer := coderdtest.NewWithProvisionerCloser(t, &coderdtest.Options{
|
|
IncludeProvisionerDaemon: true,
|
|
DeploymentValues: coderdtest.DeploymentValues(t, func(dv *codersdk.DeploymentValues) {
|
|
dv.Experiments = []string{string(codersdk.ExperimentWorkspaceBuildUpdates)}
|
|
}),
|
|
})
|
|
defer closer.Close()
|
|
user := coderdtest.CreateFirstUser(t, client)
|
|
|
|
// Create a simple template version.
|
|
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
|
|
Parse: echo.ParseComplete,
|
|
ProvisionPlan: echo.PlanComplete,
|
|
ProvisionGraph: []*proto.Response{{
|
|
Type: &proto.Response_Graph{
|
|
Graph: &proto.GraphComplete{
|
|
Resources: []*proto.Resource{{
|
|
Name: "example",
|
|
Type: "aws_instance",
|
|
}},
|
|
},
|
|
},
|
|
}},
|
|
})
|
|
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
|
|
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
|
defer cancel()
|
|
|
|
// Subscribe to all workspace build updates via SSE BEFORE creating workspaces
|
|
// so we can use it to wait for the initial builds.
|
|
decoder, err := client.WatchAllWorkspaceBuilds(ctx)
|
|
require.NoError(t, err)
|
|
defer decoder.Close()
|
|
|
|
updates := decoder.Chan()
|
|
logger := testutil.Logger(t).Named(t.Name())
|
|
|
|
// Helper to wait for a specific update.
|
|
waitForUpdate := func(event string, workspaceID uuid.UUID, expectedTransition, expectedStatus string) codersdk.WorkspaceBuildUpdate {
|
|
t.Helper()
|
|
for {
|
|
select {
|
|
case <-ctx.Done():
|
|
require.FailNow(t, "timed out waiting for event", event)
|
|
return codersdk.WorkspaceBuildUpdate{}
|
|
case update, ok := <-updates:
|
|
if !ok {
|
|
require.FailNow(t, "updates channel closed", event)
|
|
return codersdk.WorkspaceBuildUpdate{}
|
|
}
|
|
logger.Info(ctx, "received workspace build update",
|
|
slog.F("event", event),
|
|
slog.F("workspace_id", update.WorkspaceID),
|
|
slog.F("build_id", update.BuildID),
|
|
slog.F("transition", update.Transition),
|
|
slog.F("job_status", update.JobStatus),
|
|
slog.F("build_number", update.BuildNumber))
|
|
if update.WorkspaceID == workspaceID && update.Transition == expectedTransition && update.JobStatus == expectedStatus {
|
|
return update
|
|
}
|
|
// Keep waiting if this isn't the update we're looking for.
|
|
logger.Info(ctx, "skipping update, not matching expected",
|
|
slog.F("expected_workspace_id", workspaceID),
|
|
slog.F("expected_transition", expectedTransition),
|
|
slog.F("expected_status", expectedStatus))
|
|
}
|
|
}
|
|
}
|
|
|
|
// Create two workspaces and wait for their initial builds via the SSE channel.
|
|
workspace1 := coderdtest.CreateWorkspace(t, client, template.ID)
|
|
update := waitForUpdate("workspace1 initial build", workspace1.ID, "start", "succeeded")
|
|
require.Equal(t, workspace1.ID, update.WorkspaceID)
|
|
require.Equal(t, int32(1), update.BuildNumber)
|
|
|
|
workspace2 := coderdtest.CreateWorkspace(t, client, template.ID)
|
|
update = waitForUpdate("workspace2 initial build", workspace2.ID, "start", "succeeded")
|
|
require.Equal(t, workspace2.ID, update.WorkspaceID)
|
|
require.Equal(t, int32(1), update.BuildNumber)
|
|
|
|
// Stop workspace 1.
|
|
_ = coderdtest.CreateWorkspaceBuild(t, client, workspace1, database.WorkspaceTransitionStop)
|
|
update = waitForUpdate("workspace1 stop", workspace1.ID, "stop", "succeeded")
|
|
require.Equal(t, workspace1.ID, update.WorkspaceID)
|
|
|
|
// Stop workspace 2.
|
|
_ = coderdtest.CreateWorkspaceBuild(t, client, workspace2, database.WorkspaceTransitionStop)
|
|
update = waitForUpdate("workspace2 stop", workspace2.ID, "stop", "succeeded")
|
|
require.Equal(t, workspace2.ID, update.WorkspaceID)
|
|
|
|
// Start workspace 1 again.
|
|
_ = coderdtest.CreateWorkspaceBuild(t, client, workspace1, database.WorkspaceTransitionStart)
|
|
update = waitForUpdate("workspace1 start", workspace1.ID, "start", "succeeded")
|
|
require.Equal(t, workspace1.ID, update.WorkspaceID)
|
|
|
|
// Start workspace 2 again.
|
|
_ = coderdtest.CreateWorkspaceBuild(t, client, workspace2, database.WorkspaceTransitionStart)
|
|
update = waitForUpdate("workspace2 start", workspace2.ID, "start", "succeeded")
|
|
require.Equal(t, workspace2.ID, update.WorkspaceID)
|
|
}
|
|
|
|
func mustLocation(t *testing.T, location string) *time.Location {
|
|
t.Helper()
|
|
loc, err := time.LoadLocation(location)
|
|
if err != nil {
|
|
t.Errorf("failed to load location %s: %s", location, err.Error())
|
|
}
|
|
|
|
return loc
|
|
}
|
|
|
|
func TestWorkspaceResource(t *testing.T) {
|
|
t.Parallel()
|
|
t.Run("Get", func(t *testing.T) {
|
|
t.Parallel()
|
|
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
|
|
user := coderdtest.CreateFirstUser(t, client)
|
|
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
|
|
Parse: echo.ParseComplete,
|
|
ProvisionGraph: []*proto.Response{{
|
|
Type: &proto.Response_Graph{
|
|
Graph: &proto.GraphComplete{
|
|
Resources: []*proto.Resource{{
|
|
Name: "beta",
|
|
Type: "example",
|
|
Icon: "/icon/server.svg",
|
|
Agents: []*proto.Agent{{
|
|
Id: "something",
|
|
Name: "b",
|
|
Auth: &proto.Agent_Token{},
|
|
}, {
|
|
Id: "another",
|
|
Name: "a",
|
|
Auth: &proto.Agent_Token{},
|
|
}},
|
|
}, {
|
|
Name: "alpha",
|
|
Type: "example",
|
|
}},
|
|
},
|
|
},
|
|
}},
|
|
})
|
|
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
|
|
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
|
|
workspace := coderdtest.CreateWorkspace(t, client, template.ID)
|
|
coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, workspace.LatestBuild.ID)
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
|
defer cancel()
|
|
|
|
workspace, err := client.Workspace(ctx, workspace.ID)
|
|
require.NoError(t, err)
|
|
require.Len(t, workspace.LatestBuild.Resources[0].Agents, 2)
|
|
// Ensure Icon is present
|
|
require.Equal(t, "/icon/server.svg", workspace.LatestBuild.Resources[0].Icon)
|
|
})
|
|
|
|
t.Run("Apps", func(t *testing.T) {
|
|
t.Parallel()
|
|
client := coderdtest.New(t, &coderdtest.Options{
|
|
IncludeProvisionerDaemon: true,
|
|
})
|
|
user := coderdtest.CreateFirstUser(t, client)
|
|
apps := []*proto.App{
|
|
{
|
|
Slug: "code-server",
|
|
DisplayName: "code-server",
|
|
Command: "some-command",
|
|
Url: "http://localhost:3000",
|
|
Icon: "/code.svg",
|
|
},
|
|
{
|
|
Slug: "code-server-2",
|
|
DisplayName: "code-server-2",
|
|
Command: "some-command",
|
|
Url: "http://localhost:3000",
|
|
Icon: "/code.svg",
|
|
Healthcheck: &proto.Healthcheck{
|
|
Url: "http://localhost:3000",
|
|
Interval: 5,
|
|
Threshold: 6,
|
|
},
|
|
},
|
|
}
|
|
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
|
|
Parse: echo.ParseComplete,
|
|
ProvisionGraph: []*proto.Response{{
|
|
Type: &proto.Response_Graph{
|
|
Graph: &proto.GraphComplete{
|
|
Resources: []*proto.Resource{{
|
|
Name: "some",
|
|
Type: "example",
|
|
Agents: []*proto.Agent{{
|
|
Id: "something",
|
|
Name: "dev",
|
|
Auth: &proto.Agent_Token{},
|
|
Apps: apps,
|
|
}},
|
|
}},
|
|
},
|
|
},
|
|
}},
|
|
})
|
|
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
|
|
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
|
|
workspace := coderdtest.CreateWorkspace(t, client, template.ID)
|
|
coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, workspace.LatestBuild.ID)
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
|
defer cancel()
|
|
|
|
workspace, err := client.Workspace(ctx, workspace.ID)
|
|
require.NoError(t, err)
|
|
require.Len(t, workspace.LatestBuild.Resources[0].Agents, 1)
|
|
agent := workspace.LatestBuild.Resources[0].Agents[0]
|
|
require.Len(t, agent.Apps, 2)
|
|
got := agent.Apps[0]
|
|
app := apps[0]
|
|
require.EqualValues(t, app.Command, got.Command)
|
|
require.EqualValues(t, app.Icon, got.Icon)
|
|
require.EqualValues(t, app.DisplayName, got.DisplayName)
|
|
require.EqualValues(t, codersdk.WorkspaceAppHealthDisabled, got.Health)
|
|
require.EqualValues(t, "", got.Healthcheck.URL)
|
|
require.EqualValues(t, 0, got.Healthcheck.Interval)
|
|
require.EqualValues(t, 0, got.Healthcheck.Threshold)
|
|
got = agent.Apps[1]
|
|
app = apps[1]
|
|
require.EqualValues(t, app.Command, got.Command)
|
|
require.EqualValues(t, app.Icon, got.Icon)
|
|
require.EqualValues(t, app.DisplayName, got.DisplayName)
|
|
require.EqualValues(t, codersdk.WorkspaceAppHealthInitializing, got.Health)
|
|
require.EqualValues(t, app.Healthcheck.Url, got.Healthcheck.URL)
|
|
require.EqualValues(t, app.Healthcheck.Interval, got.Healthcheck.Interval)
|
|
require.EqualValues(t, app.Healthcheck.Threshold, got.Healthcheck.Threshold)
|
|
})
|
|
|
|
t.Run("Apps_DisplayOrder", func(t *testing.T) {
|
|
t.Parallel()
|
|
client := coderdtest.New(t, &coderdtest.Options{
|
|
IncludeProvisionerDaemon: true,
|
|
})
|
|
user := coderdtest.CreateFirstUser(t, client)
|
|
apps := []*proto.App{
|
|
{
|
|
Slug: "aaa",
|
|
DisplayName: "aaa",
|
|
},
|
|
{
|
|
Slug: "aaa-code-server",
|
|
Order: 4,
|
|
},
|
|
{
|
|
Slug: "bbb-code-server",
|
|
Order: 3,
|
|
},
|
|
{
|
|
Slug: "bbb",
|
|
},
|
|
}
|
|
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
|
|
Parse: echo.ParseComplete,
|
|
ProvisionGraph: []*proto.Response{{
|
|
Type: &proto.Response_Graph{
|
|
Graph: &proto.GraphComplete{
|
|
Resources: []*proto.Resource{{
|
|
Name: "some",
|
|
Type: "example",
|
|
Agents: []*proto.Agent{{
|
|
Id: "something",
|
|
Name: "dev",
|
|
Auth: &proto.Agent_Token{},
|
|
Apps: apps,
|
|
}},
|
|
}},
|
|
},
|
|
},
|
|
}},
|
|
})
|
|
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
|
|
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
|
|
workspace := coderdtest.CreateWorkspace(t, client, template.ID)
|
|
coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, workspace.LatestBuild.ID)
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
|
defer cancel()
|
|
|
|
workspace, err := client.Workspace(ctx, workspace.ID)
|
|
require.NoError(t, err)
|
|
require.Len(t, workspace.LatestBuild.Resources[0].Agents, 1)
|
|
agent := workspace.LatestBuild.Resources[0].Agents[0]
|
|
require.Len(t, agent.Apps, 4)
|
|
require.Equal(t, "bbb", agent.Apps[0].Slug) // empty-display-name < "aaa"
|
|
require.Equal(t, "aaa", agent.Apps[1].Slug) // no order < any order
|
|
require.Equal(t, "bbb-code-server", agent.Apps[2].Slug) // order = 3 < order = 4
|
|
require.Equal(t, "aaa-code-server", agent.Apps[3].Slug)
|
|
})
|
|
|
|
t.Run("Metadata", func(t *testing.T) {
|
|
t.Parallel()
|
|
client := coderdtest.New(t, &coderdtest.Options{
|
|
IncludeProvisionerDaemon: true,
|
|
})
|
|
user := coderdtest.CreateFirstUser(t, client)
|
|
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
|
|
Parse: echo.ParseComplete,
|
|
ProvisionGraph: []*proto.Response{{
|
|
Type: &proto.Response_Graph{
|
|
Graph: &proto.GraphComplete{
|
|
Resources: []*proto.Resource{{
|
|
Name: "some",
|
|
Type: "example",
|
|
Agents: []*proto.Agent{{
|
|
Id: "something",
|
|
Name: "dev",
|
|
Auth: &proto.Agent_Token{},
|
|
}},
|
|
Metadata: []*proto.Resource_Metadata{{
|
|
Key: "foo",
|
|
Value: "bar",
|
|
}, {
|
|
Key: "null",
|
|
IsNull: true,
|
|
}, {
|
|
Key: "empty",
|
|
}, {
|
|
Key: "secret",
|
|
Value: "squirrel",
|
|
Sensitive: true,
|
|
}},
|
|
}},
|
|
},
|
|
},
|
|
}},
|
|
})
|
|
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
|
|
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
|
|
workspace := coderdtest.CreateWorkspace(t, client, template.ID)
|
|
coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, workspace.LatestBuild.ID)
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
|
defer cancel()
|
|
|
|
workspace, err := client.Workspace(ctx, workspace.ID)
|
|
require.NoError(t, err)
|
|
metadata := workspace.LatestBuild.Resources[0].Metadata
|
|
require.Equal(t, []codersdk.WorkspaceResourceMetadata{{
|
|
Key: "foo",
|
|
Value: "bar",
|
|
}, {
|
|
Key: "empty",
|
|
}, {
|
|
Key: "secret",
|
|
Value: "squirrel",
|
|
Sensitive: true,
|
|
}}, metadata)
|
|
})
|
|
}
|
|
|
|
func TestWorkspaceWithRichParameters(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
const (
|
|
firstParameterName = "first_parameter"
|
|
firstParameterType = "string"
|
|
firstParameterDescription = "This is _first_ *parameter*"
|
|
firstParameterValue = "1"
|
|
|
|
secondParameterName = "second_parameter"
|
|
secondParameterDisplayName = "Second Parameter"
|
|
secondParameterType = "number"
|
|
secondParameterDescription = "_This_ is second *parameter*"
|
|
secondParameterValue = "2"
|
|
secondParameterValidationMonotonic = codersdk.MonotonicOrderIncreasing
|
|
|
|
thirdParameterName = "third_parameter"
|
|
thirdParameterType = "list(string)"
|
|
thirdParameterFormType = proto.ParameterFormType_MULTISELECT
|
|
thirdParameterDefault = `["red"]`
|
|
thirdParameterOption = "red"
|
|
)
|
|
|
|
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
|
|
user := coderdtest.CreateFirstUser(t, client)
|
|
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
|
|
Parse: echo.ParseComplete,
|
|
ProvisionGraph: []*proto.Response{
|
|
{
|
|
Type: &proto.Response_Graph{
|
|
Graph: &proto.GraphComplete{
|
|
Parameters: []*proto.RichParameter{
|
|
{
|
|
Name: firstParameterName,
|
|
Type: firstParameterType,
|
|
Description: firstParameterDescription,
|
|
FormType: proto.ParameterFormType_INPUT,
|
|
},
|
|
{
|
|
Name: secondParameterName,
|
|
DisplayName: secondParameterDisplayName,
|
|
Type: secondParameterType,
|
|
Description: secondParameterDescription,
|
|
ValidationMin: ptr.Ref(int32(1)),
|
|
ValidationMax: ptr.Ref(int32(3)),
|
|
ValidationMonotonic: string(secondParameterValidationMonotonic),
|
|
FormType: proto.ParameterFormType_INPUT,
|
|
},
|
|
{
|
|
Name: thirdParameterName,
|
|
Type: thirdParameterType,
|
|
DefaultValue: thirdParameterDefault,
|
|
Options: []*proto.RichParameterOption{
|
|
{
|
|
Name: thirdParameterOption,
|
|
Value: thirdParameterOption,
|
|
},
|
|
},
|
|
FormType: thirdParameterFormType,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
ProvisionApply: []*proto.Response{{
|
|
Type: &proto.Response_Apply{
|
|
Apply: &proto.ApplyComplete{},
|
|
},
|
|
}},
|
|
})
|
|
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
|
defer cancel()
|
|
|
|
firstParameterDescriptionPlaintext, err := render.PlaintextFromMarkdown(firstParameterDescription)
|
|
require.NoError(t, err)
|
|
secondParameterDescriptionPlaintext, err := render.PlaintextFromMarkdown(secondParameterDescription)
|
|
require.NoError(t, err)
|
|
|
|
templateRichParameters, err := client.TemplateVersionRichParameters(ctx, version.ID)
|
|
require.NoError(t, err)
|
|
require.Len(t, templateRichParameters, 3)
|
|
require.Equal(t, firstParameterName, templateRichParameters[0].Name)
|
|
require.Equal(t, firstParameterType, templateRichParameters[0].Type)
|
|
require.Equal(t, firstParameterDescription, templateRichParameters[0].Description)
|
|
require.Equal(t, firstParameterDescriptionPlaintext, templateRichParameters[0].DescriptionPlaintext)
|
|
require.Equal(t, codersdk.ValidationMonotonicOrder(""), templateRichParameters[0].ValidationMonotonic) // no validation for string
|
|
|
|
require.Equal(t, secondParameterName, templateRichParameters[1].Name)
|
|
require.Equal(t, secondParameterDisplayName, templateRichParameters[1].DisplayName)
|
|
require.Equal(t, secondParameterType, templateRichParameters[1].Type)
|
|
require.Equal(t, secondParameterDescription, templateRichParameters[1].Description)
|
|
require.Equal(t, secondParameterDescriptionPlaintext, templateRichParameters[1].DescriptionPlaintext)
|
|
require.Equal(t, secondParameterValidationMonotonic, templateRichParameters[1].ValidationMonotonic)
|
|
|
|
third := templateRichParameters[2]
|
|
require.Equal(t, thirdParameterName, third.Name)
|
|
require.Equal(t, thirdParameterType, third.Type)
|
|
require.Equal(t, string(database.ParameterFormTypeMultiSelect), third.FormType)
|
|
require.Equal(t, thirdParameterDefault, third.DefaultValue)
|
|
require.Equal(t, thirdParameterOption, third.Options[0].Name)
|
|
require.Equal(t, thirdParameterOption, third.Options[0].Value)
|
|
|
|
expectedBuildParameters := []codersdk.WorkspaceBuildParameter{
|
|
{Name: firstParameterName, Value: firstParameterValue},
|
|
{Name: secondParameterName, Value: secondParameterValue},
|
|
{Name: thirdParameterName, Value: thirdParameterDefault},
|
|
}
|
|
|
|
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
|
|
workspace := coderdtest.CreateWorkspace(t, client, template.ID, func(cwr *codersdk.CreateWorkspaceRequest) {
|
|
cwr.RichParameterValues = expectedBuildParameters
|
|
})
|
|
|
|
workspaceBuild := coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, workspace.LatestBuild.ID)
|
|
require.Equal(t, codersdk.WorkspaceStatusRunning, workspaceBuild.Status)
|
|
|
|
workspaceBuildParameters, err := client.WorkspaceBuildParameters(ctx, workspaceBuild.ID)
|
|
require.NoError(t, err)
|
|
require.ElementsMatch(t, expectedBuildParameters, workspaceBuildParameters)
|
|
}
|
|
|
|
func TestWorkspaceWithMultiSelectFailure(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
|
|
user := coderdtest.CreateFirstUser(t, client)
|
|
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
|
|
Parse: echo.ParseComplete,
|
|
ProvisionGraph: []*proto.Response{
|
|
{
|
|
Type: &proto.Response_Graph{
|
|
Graph: &proto.GraphComplete{
|
|
Parameters: []*proto.RichParameter{
|
|
{
|
|
Name: "param",
|
|
Type: provider.OptionTypeListString,
|
|
DefaultValue: `["red"]`,
|
|
Options: []*proto.RichParameterOption{
|
|
{
|
|
Name: "red",
|
|
Value: "red",
|
|
},
|
|
},
|
|
FormType: proto.ParameterFormType_MULTISELECT,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
ProvisionApply: []*proto.Response{{
|
|
Type: &proto.Response_Apply{
|
|
Apply: &proto.ApplyComplete{},
|
|
},
|
|
}},
|
|
})
|
|
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
|
defer cancel()
|
|
|
|
templateRichParameters, err := client.TemplateVersionRichParameters(ctx, version.ID)
|
|
require.NoError(t, err)
|
|
require.Len(t, templateRichParameters, 1)
|
|
|
|
expectedBuildParameters := []codersdk.WorkspaceBuildParameter{
|
|
// purple is not in the response set
|
|
{Name: "param", Value: `["red", "purple"]`},
|
|
}
|
|
|
|
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
|
|
req := codersdk.CreateWorkspaceRequest{
|
|
TemplateID: template.ID,
|
|
Name: coderdtest.RandomUsername(t),
|
|
AutostartSchedule: ptr.Ref("CRON_TZ=US/Central 30 9 * * 1-5"),
|
|
TTLMillis: ptr.Ref((8 * time.Hour).Milliseconds()),
|
|
AutomaticUpdates: codersdk.AutomaticUpdatesNever,
|
|
RichParameterValues: expectedBuildParameters,
|
|
}
|
|
|
|
_, err = client.CreateUserWorkspace(context.Background(), codersdk.Me, req)
|
|
require.Error(t, err)
|
|
var apiError *codersdk.Error
|
|
require.ErrorAs(t, err, &apiError)
|
|
require.Equal(t, http.StatusBadRequest, apiError.StatusCode())
|
|
}
|
|
|
|
func TestWorkspaceWithOptionalRichParameters(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
const (
|
|
firstParameterName = "first_parameter"
|
|
firstParameterType = "string"
|
|
firstParameterDescription = "This is _first_ *parameter*"
|
|
firstParameterDefaultValue = "1"
|
|
|
|
secondParameterName = "second_parameter"
|
|
secondParameterType = "number"
|
|
secondParameterDescription = "_This_ is second *parameter*"
|
|
secondParameterRequired = true
|
|
secondParameterValue = "333"
|
|
)
|
|
|
|
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
|
|
user := coderdtest.CreateFirstUser(t, client)
|
|
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
|
|
Parse: echo.ParseComplete,
|
|
ProvisionGraph: []*proto.Response{
|
|
{
|
|
Type: &proto.Response_Graph{
|
|
Graph: &proto.GraphComplete{
|
|
Parameters: []*proto.RichParameter{
|
|
{
|
|
Name: firstParameterName,
|
|
Type: firstParameterType,
|
|
Description: firstParameterDescription,
|
|
DefaultValue: firstParameterDefaultValue,
|
|
},
|
|
{
|
|
Name: secondParameterName,
|
|
Type: secondParameterType,
|
|
Description: secondParameterDescription,
|
|
Required: secondParameterRequired,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
ProvisionApply: []*proto.Response{{
|
|
Type: &proto.Response_Apply{
|
|
Apply: &proto.ApplyComplete{},
|
|
},
|
|
}},
|
|
})
|
|
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
|
defer cancel()
|
|
|
|
templateRichParameters, err := client.TemplateVersionRichParameters(ctx, version.ID)
|
|
require.NoError(t, err)
|
|
require.Len(t, templateRichParameters, 2)
|
|
require.Equal(t, firstParameterName, templateRichParameters[0].Name)
|
|
require.Equal(t, firstParameterType, templateRichParameters[0].Type)
|
|
require.Equal(t, firstParameterDescription, templateRichParameters[0].Description)
|
|
require.Equal(t, firstParameterDefaultValue, templateRichParameters[0].DefaultValue)
|
|
require.Equal(t, secondParameterName, templateRichParameters[1].Name)
|
|
require.Equal(t, secondParameterType, templateRichParameters[1].Type)
|
|
require.Equal(t, secondParameterDescription, templateRichParameters[1].Description)
|
|
require.Equal(t, secondParameterRequired, templateRichParameters[1].Required)
|
|
|
|
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
|
|
workspace := coderdtest.CreateWorkspace(t, client, template.ID, func(cwr *codersdk.CreateWorkspaceRequest) {
|
|
cwr.RichParameterValues = []codersdk.WorkspaceBuildParameter{
|
|
// First parameter is optional, so coder will pick the default value.
|
|
{Name: secondParameterName, Value: secondParameterValue},
|
|
}
|
|
})
|
|
|
|
workspaceBuild := coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, workspace.LatestBuild.ID)
|
|
require.Equal(t, codersdk.WorkspaceStatusRunning, workspaceBuild.Status)
|
|
|
|
workspaceBuildParameters, err := client.WorkspaceBuildParameters(ctx, workspaceBuild.ID)
|
|
require.NoError(t, err)
|
|
|
|
expectedBuildParameters := []codersdk.WorkspaceBuildParameter{
|
|
// Coderd inserts the default for the missing parameter
|
|
{Name: firstParameterName, Value: firstParameterDefaultValue},
|
|
{Name: secondParameterName, Value: secondParameterValue},
|
|
}
|
|
require.ElementsMatch(t, expectedBuildParameters, workspaceBuildParameters)
|
|
}
|
|
|
|
func TestWorkspaceWithEphemeralRichParameters(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
const (
|
|
firstParameterName = "first_parameter"
|
|
firstParameterType = "string"
|
|
firstParameterDescription = "This is first parameter"
|
|
firstParameterMutable = true
|
|
firstParameterDefaultValue = "1"
|
|
firstParameterValue = "i_am_first_parameter"
|
|
|
|
ephemeralParameterName = "second_parameter"
|
|
ephemeralParameterType = "string"
|
|
ephemeralParameterDescription = "This is second parameter"
|
|
ephemeralParameterDefaultValue = ""
|
|
ephemeralParameterMutable = true
|
|
ephemeralParameterValue = "i_am_ephemeral"
|
|
)
|
|
|
|
// Create template version with ephemeral parameter
|
|
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
|
|
user := coderdtest.CreateFirstUser(t, client)
|
|
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
|
|
Parse: echo.ParseComplete,
|
|
ProvisionGraph: []*proto.Response{
|
|
{
|
|
Type: &proto.Response_Graph{
|
|
Graph: &proto.GraphComplete{
|
|
Parameters: []*proto.RichParameter{
|
|
{
|
|
Name: firstParameterName,
|
|
Type: firstParameterType,
|
|
Description: firstParameterDescription,
|
|
DefaultValue: firstParameterDefaultValue,
|
|
Mutable: firstParameterMutable,
|
|
},
|
|
{
|
|
Name: ephemeralParameterName,
|
|
Type: ephemeralParameterType,
|
|
Description: ephemeralParameterDescription,
|
|
DefaultValue: ephemeralParameterDefaultValue,
|
|
Mutable: ephemeralParameterMutable,
|
|
Ephemeral: true,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
ProvisionApply: []*proto.Response{{
|
|
Type: &proto.Response_Apply{
|
|
Apply: &proto.ApplyComplete{},
|
|
},
|
|
}},
|
|
})
|
|
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
|
|
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
|
|
|
|
// Create workspace with default values
|
|
workspace := coderdtest.CreateWorkspace(t, client, template.ID)
|
|
workspaceBuild := coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, workspace.LatestBuild.ID)
|
|
require.Equal(t, codersdk.WorkspaceStatusRunning, workspaceBuild.Status)
|
|
|
|
// Verify workspace build parameters (default values)
|
|
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
|
defer cancel()
|
|
|
|
workspaceBuildParameters, err := client.WorkspaceBuildParameters(ctx, workspaceBuild.ID)
|
|
require.NoError(t, err)
|
|
|
|
expectedBuildParameters := []codersdk.WorkspaceBuildParameter{
|
|
{Name: firstParameterName, Value: firstParameterDefaultValue},
|
|
{Name: ephemeralParameterName, Value: ephemeralParameterDefaultValue},
|
|
}
|
|
require.ElementsMatch(t, expectedBuildParameters, workspaceBuildParameters)
|
|
|
|
// Trigger workspace build job with ephemeral parameter
|
|
workspaceBuild, err = client.CreateWorkspaceBuild(ctx, workspaceBuild.WorkspaceID, codersdk.CreateWorkspaceBuildRequest{
|
|
Transition: codersdk.WorkspaceTransitionStart,
|
|
RichParameterValues: []codersdk.WorkspaceBuildParameter{
|
|
{
|
|
Name: ephemeralParameterName,
|
|
Value: ephemeralParameterValue,
|
|
},
|
|
},
|
|
})
|
|
require.NoError(t, err)
|
|
workspaceBuild = coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, workspaceBuild.ID)
|
|
require.Equal(t, codersdk.WorkspaceStatusRunning, workspaceBuild.Status)
|
|
|
|
// Verify workspace build parameters (including ephemeral)
|
|
workspaceBuildParameters, err = client.WorkspaceBuildParameters(ctx, workspaceBuild.ID)
|
|
require.NoError(t, err)
|
|
|
|
expectedBuildParameters = []codersdk.WorkspaceBuildParameter{
|
|
{Name: firstParameterName, Value: firstParameterDefaultValue},
|
|
{Name: ephemeralParameterName, Value: ephemeralParameterValue},
|
|
}
|
|
require.ElementsMatch(t, expectedBuildParameters, workspaceBuildParameters)
|
|
|
|
// Trigger workspace build one more time without the ephemeral parameter
|
|
workspaceBuild, err = client.CreateWorkspaceBuild(ctx, workspaceBuild.WorkspaceID, codersdk.CreateWorkspaceBuildRequest{
|
|
Transition: codersdk.WorkspaceTransitionStart,
|
|
RichParameterValues: []codersdk.WorkspaceBuildParameter{
|
|
{
|
|
Name: firstParameterName,
|
|
Value: firstParameterValue,
|
|
},
|
|
},
|
|
})
|
|
require.NoError(t, err)
|
|
workspaceBuild = coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, workspaceBuild.ID)
|
|
require.Equal(t, codersdk.WorkspaceStatusRunning, workspaceBuild.Status)
|
|
|
|
// Verify workspace build parameters (ephemeral should be back to default)
|
|
workspaceBuildParameters, err = client.WorkspaceBuildParameters(ctx, workspaceBuild.ID)
|
|
require.NoError(t, err)
|
|
|
|
expectedBuildParameters = []codersdk.WorkspaceBuildParameter{
|
|
{Name: firstParameterName, Value: firstParameterValue},
|
|
{Name: ephemeralParameterName, Value: ephemeralParameterDefaultValue},
|
|
}
|
|
require.ElementsMatch(t, expectedBuildParameters, workspaceBuildParameters)
|
|
}
|
|
|
|
func TestWorkspaceDormant(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
t.Run("OK", func(t *testing.T) {
|
|
t.Parallel()
|
|
var (
|
|
auditRecorder = audit.NewMock()
|
|
client = coderdtest.New(t, &coderdtest.Options{
|
|
IncludeProvisionerDaemon: true,
|
|
Auditor: auditRecorder,
|
|
})
|
|
user = coderdtest.CreateFirstUser(t, client)
|
|
version = coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
|
|
_ = coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
|
|
timeTilDormantAutoDelete = time.Minute
|
|
)
|
|
|
|
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID, func(ctr *codersdk.CreateTemplateRequest) {
|
|
ctr.TimeTilDormantAutoDeleteMillis = ptr.Ref[int64](timeTilDormantAutoDelete.Milliseconds())
|
|
})
|
|
workspace := coderdtest.CreateWorkspace(t, client, template.ID)
|
|
_ = coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, workspace.LatestBuild.ID)
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
|
defer cancel()
|
|
|
|
lastUsedAt := workspace.LastUsedAt
|
|
auditRecorder.ResetLogs()
|
|
err := client.UpdateWorkspaceDormancy(ctx, workspace.ID, codersdk.UpdateWorkspaceDormancy{
|
|
Dormant: true,
|
|
})
|
|
require.NoError(t, err)
|
|
require.True(t, auditRecorder.Contains(t, database.AuditLog{
|
|
Action: database.AuditActionWrite,
|
|
ResourceType: database.ResourceTypeWorkspace,
|
|
ResourceTarget: workspace.Name,
|
|
}))
|
|
|
|
workspace = coderdtest.MustWorkspace(t, client, workspace.ID)
|
|
require.NoError(t, err, "fetch provisioned workspace")
|
|
// The template doesn't have a time_til_dormant_autodelete set so this should be nil.
|
|
require.Nil(t, workspace.DeletingAt)
|
|
require.NotNil(t, workspace.DormantAt)
|
|
require.WithinRange(t, *workspace.DormantAt, dbtime.Now().Add(-time.Second*10), dbtime.Now())
|
|
require.Equal(t, lastUsedAt, workspace.LastUsedAt)
|
|
|
|
workspace = coderdtest.MustWorkspace(t, client, workspace.ID)
|
|
lastUsedAt = workspace.LastUsedAt
|
|
err = client.UpdateWorkspaceDormancy(ctx, workspace.ID, codersdk.UpdateWorkspaceDormancy{
|
|
Dormant: false,
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
workspace, err = client.Workspace(ctx, workspace.ID)
|
|
require.NoError(t, err, "fetch provisioned workspace")
|
|
require.Nil(t, workspace.DormantAt)
|
|
// The template doesn't have a time_til_dormant_autodelete set so this should be nil.
|
|
require.Nil(t, workspace.DeletingAt)
|
|
// The last_used_at should get updated when we activate the workspace.
|
|
require.True(t, workspace.LastUsedAt.After(lastUsedAt))
|
|
})
|
|
|
|
// #20925: this test originally validated that you could **not** start a dormant workspace.
|
|
// The client was required to explicitly update the dormancy status before starting.
|
|
// This led to a 'whack-a-mole' situation where various code paths that create a workspace build
|
|
// would need to special case dormant workspaces.
|
|
// Now, a dormant workspace will automatically 'wake up' on start.
|
|
t.Run("StartWakesUpDormantWorkspace", func(t *testing.T) {
|
|
t.Parallel()
|
|
var (
|
|
auditor = audit.NewMock()
|
|
client = coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true, Auditor: auditor})
|
|
user = coderdtest.CreateFirstUser(t, client)
|
|
version = coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
|
|
_ = coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
|
|
template = coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
|
|
workspace = coderdtest.CreateWorkspace(t, client, template.ID)
|
|
_ = coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, workspace.LatestBuild.ID)
|
|
)
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
|
defer cancel()
|
|
|
|
err := client.UpdateWorkspaceDormancy(ctx, workspace.ID, codersdk.UpdateWorkspaceDormancy{
|
|
Dormant: true,
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
// Should be able to stop a workspace while it is dormant.
|
|
coderdtest.MustTransitionWorkspace(t, client, workspace.ID, codersdk.WorkspaceTransitionStart, codersdk.WorkspaceTransitionStop)
|
|
|
|
// Reset the auditor
|
|
auditor.ResetLogs()
|
|
// Assert test invariant: workspace is dormant.
|
|
workspace, err = client.Workspace(ctx, workspace.ID)
|
|
require.NoError(t, err, "fetch dormant workspace")
|
|
if assert.NotNil(t, workspace.DormantAt, "workspace must be dormant") {
|
|
require.WithinDuration(t, *workspace.DormantAt, dbtime.Now(), 10*time.Second)
|
|
}
|
|
// Starting a dormant workspace should 'wake' it.
|
|
wb, err := client.CreateWorkspaceBuild(ctx, workspace.ID, codersdk.CreateWorkspaceBuildRequest{
|
|
TemplateVersionID: template.ActiveVersionID,
|
|
Transition: codersdk.WorkspaceTransition(database.WorkspaceTransitionStart),
|
|
})
|
|
require.NoError(t, err)
|
|
coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, wb.ID)
|
|
|
|
// After starting, the workspace should no longer be dormant.
|
|
updatedWs, err := client.Workspace(ctx, workspace.ID)
|
|
require.NoError(t, err, "fetch updated workspace")
|
|
require.Nil(t, updatedWs.DormantAt)
|
|
|
|
// There should be an audit log for both the dormancy update and the start.
|
|
require.Len(t, auditor.AuditLogs(), 2)
|
|
require.True(t, auditor.Contains(t, database.AuditLog{
|
|
Action: database.AuditActionWrite,
|
|
ResourceType: database.ResourceTypeWorkspace,
|
|
}))
|
|
require.True(t, auditor.Contains(t, database.AuditLog{
|
|
Action: database.AuditActionStart,
|
|
ResourceType: database.ResourceTypeWorkspaceBuild,
|
|
}))
|
|
})
|
|
}
|
|
|
|
func TestWorkspaceFavoriteUnfavorite(t *testing.T) {
|
|
t.Parallel()
|
|
// Given:
|
|
var (
|
|
auditRecorder = audit.NewMock()
|
|
client, db = coderdtest.NewWithDatabase(t, &coderdtest.Options{
|
|
Auditor: auditRecorder,
|
|
})
|
|
owner = coderdtest.CreateFirstUser(t, client)
|
|
memberClient, member = coderdtest.CreateAnotherUser(t, client, owner.OrganizationID)
|
|
// This will be our 'favorite' workspace
|
|
wsb1 = dbfake.WorkspaceBuild(t, db, database.WorkspaceTable{OwnerID: member.ID, OrganizationID: owner.OrganizationID}).Do()
|
|
wsb2 = dbfake.WorkspaceBuild(t, db, database.WorkspaceTable{OwnerID: owner.UserID, OrganizationID: owner.OrganizationID}).Do()
|
|
)
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
|
defer cancel()
|
|
|
|
// Initially, workspace should not be favored for member.
|
|
ws, err := memberClient.Workspace(ctx, wsb1.Workspace.ID)
|
|
require.NoError(t, err)
|
|
require.False(t, ws.Favorite)
|
|
|
|
// When user favorites workspace
|
|
err = memberClient.FavoriteWorkspace(ctx, wsb1.Workspace.ID)
|
|
require.NoError(t, err)
|
|
|
|
// Then it should be favored for them.
|
|
ws, err = memberClient.Workspace(ctx, wsb1.Workspace.ID)
|
|
require.NoError(t, err)
|
|
require.True(t, ws.Favorite)
|
|
|
|
// And it should be audited.
|
|
require.True(t, auditRecorder.Contains(t, database.AuditLog{
|
|
Action: database.AuditActionWrite,
|
|
ResourceType: database.ResourceTypeWorkspace,
|
|
ResourceTarget: wsb1.Workspace.Name,
|
|
UserID: member.ID,
|
|
}))
|
|
auditRecorder.ResetLogs()
|
|
|
|
// This should not show for the owner.
|
|
ws, err = client.Workspace(ctx, wsb1.Workspace.ID)
|
|
require.NoError(t, err)
|
|
require.False(t, ws.Favorite)
|
|
|
|
// When member unfavorites workspace
|
|
err = memberClient.UnfavoriteWorkspace(ctx, wsb1.Workspace.ID)
|
|
require.NoError(t, err)
|
|
|
|
// Then it should no longer be favored
|
|
ws, err = memberClient.Workspace(ctx, wsb1.Workspace.ID)
|
|
require.NoError(t, err)
|
|
require.False(t, ws.Favorite, "no longer favorite")
|
|
|
|
// And it should show in the audit logs.
|
|
require.True(t, auditRecorder.Contains(t, database.AuditLog{
|
|
Action: database.AuditActionWrite,
|
|
ResourceType: database.ResourceTypeWorkspace,
|
|
ResourceTarget: wsb1.Workspace.Name,
|
|
UserID: member.ID,
|
|
}))
|
|
|
|
// Users without write access to the workspace should not be able to perform the above.
|
|
err = memberClient.FavoriteWorkspace(ctx, wsb2.Workspace.ID)
|
|
var sdkErr *codersdk.Error
|
|
require.ErrorAs(t, err, &sdkErr)
|
|
require.Equal(t, http.StatusNotFound, sdkErr.StatusCode())
|
|
err = memberClient.UnfavoriteWorkspace(ctx, wsb2.Workspace.ID)
|
|
require.ErrorAs(t, err, &sdkErr)
|
|
require.Equal(t, http.StatusNotFound, sdkErr.StatusCode())
|
|
|
|
// You should not be able to favorite any workspace you do not own, even if you are the owner.
|
|
err = client.FavoriteWorkspace(ctx, wsb1.Workspace.ID)
|
|
require.ErrorAs(t, err, &sdkErr)
|
|
require.Equal(t, http.StatusForbidden, sdkErr.StatusCode())
|
|
|
|
err = client.UnfavoriteWorkspace(ctx, wsb1.Workspace.ID)
|
|
require.ErrorAs(t, err, &sdkErr)
|
|
require.Equal(t, http.StatusForbidden, sdkErr.StatusCode())
|
|
}
|
|
|
|
func TestWorkspaceUsageTracking(t *testing.T) {
|
|
t.Parallel()
|
|
t.Run("NoExperiment", func(t *testing.T) {
|
|
t.Parallel()
|
|
client, db := coderdtest.NewWithDatabase(t, nil)
|
|
user := coderdtest.CreateFirstUser(t, client)
|
|
tmpDir := t.TempDir()
|
|
r := dbfake.WorkspaceBuild(t, db, database.WorkspaceTable{
|
|
OrganizationID: user.OrganizationID,
|
|
OwnerID: user.UserID,
|
|
}).WithAgent(func(agents []*proto.Agent) []*proto.Agent {
|
|
agents[0].Directory = tmpDir
|
|
return agents
|
|
}).Do()
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitMedium)
|
|
defer cancel()
|
|
|
|
// continue legacy behavior
|
|
err := client.PostWorkspaceUsage(ctx, r.Workspace.ID)
|
|
require.NoError(t, err)
|
|
err = client.PostWorkspaceUsageWithBody(ctx, r.Workspace.ID, codersdk.PostWorkspaceUsageRequest{})
|
|
require.NoError(t, err)
|
|
})
|
|
t.Run("Experiment", func(t *testing.T) {
|
|
t.Parallel()
|
|
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitMedium)
|
|
defer cancel()
|
|
dv := coderdtest.DeploymentValues(t)
|
|
dv.Experiments = []string{string(codersdk.ExperimentWorkspaceUsage)}
|
|
client, db := coderdtest.NewWithDatabase(t, &coderdtest.Options{
|
|
DeploymentValues: dv,
|
|
})
|
|
user := coderdtest.CreateFirstUser(t, client)
|
|
tmpDir := t.TempDir()
|
|
org := dbgen.Organization(t, db, database.Organization{})
|
|
_ = dbgen.OrganizationMember(t, db, database.OrganizationMember{
|
|
UserID: user.UserID,
|
|
OrganizationID: org.ID,
|
|
})
|
|
templateVersion := dbgen.TemplateVersion(t, db, database.TemplateVersion{
|
|
OrganizationID: org.ID,
|
|
CreatedBy: user.UserID,
|
|
})
|
|
template := dbgen.Template(t, db, database.Template{
|
|
OrganizationID: org.ID,
|
|
ActiveVersionID: templateVersion.ID,
|
|
CreatedBy: user.UserID,
|
|
DefaultTTL: int64(8 * time.Hour),
|
|
})
|
|
_, err := client.UpdateTemplateMeta(ctx, template.ID, codersdk.UpdateTemplateMeta{
|
|
ActivityBumpMillis: 8 * time.Hour.Milliseconds(),
|
|
})
|
|
require.NoError(t, err)
|
|
r := dbfake.WorkspaceBuild(t, db, database.WorkspaceTable{
|
|
OrganizationID: user.OrganizationID,
|
|
OwnerID: user.UserID,
|
|
TemplateID: template.ID,
|
|
Ttl: sql.NullInt64{Valid: true, Int64: int64(8 * time.Hour)},
|
|
}).WithAgent(func(agents []*proto.Agent) []*proto.Agent {
|
|
agents[0].Directory = tmpDir
|
|
return agents
|
|
}).Do()
|
|
|
|
// continue legacy behavior
|
|
err = client.PostWorkspaceUsage(ctx, r.Workspace.ID)
|
|
require.NoError(t, err)
|
|
err = client.PostWorkspaceUsageWithBody(ctx, r.Workspace.ID, codersdk.PostWorkspaceUsageRequest{})
|
|
require.NoError(t, err)
|
|
|
|
workspace, err := client.Workspace(ctx, r.Workspace.ID)
|
|
require.NoError(t, err)
|
|
|
|
// only agent id fails
|
|
err = client.PostWorkspaceUsageWithBody(ctx, r.Workspace.ID, codersdk.PostWorkspaceUsageRequest{
|
|
AgentID: workspace.LatestBuild.Resources[0].Agents[0].ID,
|
|
})
|
|
require.ErrorContains(t, err, "agent_id")
|
|
// only app name fails
|
|
err = client.PostWorkspaceUsageWithBody(ctx, r.Workspace.ID, codersdk.PostWorkspaceUsageRequest{
|
|
AppName: "ssh",
|
|
})
|
|
require.ErrorContains(t, err, "app_name")
|
|
// unknown app name fails
|
|
err = client.PostWorkspaceUsageWithBody(ctx, r.Workspace.ID, codersdk.PostWorkspaceUsageRequest{
|
|
AgentID: workspace.LatestBuild.Resources[0].Agents[0].ID,
|
|
AppName: "unknown",
|
|
})
|
|
require.ErrorContains(t, err, "app_name")
|
|
|
|
// vscode works
|
|
err = client.PostWorkspaceUsageWithBody(ctx, r.Workspace.ID, codersdk.PostWorkspaceUsageRequest{
|
|
AgentID: workspace.LatestBuild.Resources[0].Agents[0].ID,
|
|
AppName: "vscode",
|
|
})
|
|
require.NoError(t, err)
|
|
// jetbrains works
|
|
err = client.PostWorkspaceUsageWithBody(ctx, r.Workspace.ID, codersdk.PostWorkspaceUsageRequest{
|
|
AgentID: workspace.LatestBuild.Resources[0].Agents[0].ID,
|
|
AppName: "jetbrains",
|
|
})
|
|
require.NoError(t, err)
|
|
// reconnecting-pty works
|
|
err = client.PostWorkspaceUsageWithBody(ctx, r.Workspace.ID, codersdk.PostWorkspaceUsageRequest{
|
|
AgentID: workspace.LatestBuild.Resources[0].Agents[0].ID,
|
|
AppName: "reconnecting-pty",
|
|
})
|
|
require.NoError(t, err)
|
|
// ssh works
|
|
err = client.PostWorkspaceUsageWithBody(ctx, r.Workspace.ID, codersdk.PostWorkspaceUsageRequest{
|
|
AgentID: workspace.LatestBuild.Resources[0].Agents[0].ID,
|
|
AppName: "ssh",
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
// ensure deadline has been bumped
|
|
newWorkspace, err := client.Workspace(ctx, r.Workspace.ID)
|
|
require.NoError(t, err)
|
|
require.True(t, workspace.LatestBuild.Deadline.Valid)
|
|
require.True(t, newWorkspace.LatestBuild.Deadline.Valid)
|
|
require.Greater(t, newWorkspace.LatestBuild.Deadline.Time, workspace.LatestBuild.Deadline.Time)
|
|
})
|
|
}
|
|
|
|
func TestWorkspaceNotifications(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
t.Run("Dormant", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
t.Run("InitiatorNotOwner", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
// Given
|
|
var (
|
|
notifyEnq = ¬ificationstest.FakeEnqueuer{}
|
|
client = coderdtest.New(t, &coderdtest.Options{
|
|
IncludeProvisionerDaemon: true,
|
|
NotificationsEnqueuer: notifyEnq,
|
|
})
|
|
user = coderdtest.CreateFirstUser(t, client)
|
|
memberClient, _ = coderdtest.CreateAnotherUser(t, client, user.OrganizationID, rbac.RoleOwner())
|
|
version = coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
|
|
_ = coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
|
|
template = coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
|
|
workspace = coderdtest.CreateWorkspace(t, client, template.ID)
|
|
_ = coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, workspace.LatestBuild.ID)
|
|
)
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
|
t.Cleanup(cancel)
|
|
|
|
// When
|
|
err := memberClient.UpdateWorkspaceDormancy(ctx, workspace.ID, codersdk.UpdateWorkspaceDormancy{
|
|
Dormant: true,
|
|
})
|
|
|
|
// Then
|
|
require.NoError(t, err, "mark workspace as dormant")
|
|
sent := notifyEnq.Sent(notificationstest.WithTemplateID(notifications.TemplateWorkspaceDormant))
|
|
require.Len(t, sent, 1)
|
|
require.Equal(t, sent[0].TemplateID, notifications.TemplateWorkspaceDormant)
|
|
require.Equal(t, sent[0].UserID, workspace.OwnerID)
|
|
require.Contains(t, sent[0].Targets, template.ID)
|
|
require.Contains(t, sent[0].Targets, workspace.ID)
|
|
require.Contains(t, sent[0].Targets, workspace.OrganizationID)
|
|
require.Contains(t, sent[0].Targets, workspace.OwnerID)
|
|
})
|
|
|
|
t.Run("InitiatorIsOwner", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
// Given
|
|
var (
|
|
notifyEnq = ¬ificationstest.FakeEnqueuer{}
|
|
client = coderdtest.New(t, &coderdtest.Options{
|
|
IncludeProvisionerDaemon: true,
|
|
NotificationsEnqueuer: notifyEnq,
|
|
})
|
|
user = coderdtest.CreateFirstUser(t, client)
|
|
version = coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
|
|
_ = coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
|
|
template = coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
|
|
workspace = coderdtest.CreateWorkspace(t, client, template.ID)
|
|
_ = coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, workspace.LatestBuild.ID)
|
|
)
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
|
t.Cleanup(cancel)
|
|
|
|
// When
|
|
err := client.UpdateWorkspaceDormancy(ctx, workspace.ID, codersdk.UpdateWorkspaceDormancy{
|
|
Dormant: true,
|
|
})
|
|
|
|
// Then
|
|
require.NoError(t, err, "mark workspace as dormant")
|
|
require.Len(t, notifyEnq.Sent(notificationstest.WithTemplateID(notifications.TemplateWorkspaceDormant)), 0)
|
|
})
|
|
|
|
t.Run("ActivateDormantWorkspace", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
// Given
|
|
var (
|
|
notifyEnq = ¬ificationstest.FakeEnqueuer{}
|
|
client = coderdtest.New(t, &coderdtest.Options{
|
|
IncludeProvisionerDaemon: true,
|
|
NotificationsEnqueuer: notifyEnq,
|
|
})
|
|
user = coderdtest.CreateFirstUser(t, client)
|
|
version = coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
|
|
_ = coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
|
|
template = coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
|
|
workspace = coderdtest.CreateWorkspace(t, client, template.ID)
|
|
_ = coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, workspace.LatestBuild.ID)
|
|
)
|
|
|
|
// When
|
|
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
|
t.Cleanup(cancel)
|
|
|
|
// Make workspace dormant before activate it
|
|
err := client.UpdateWorkspaceDormancy(ctx, workspace.ID, codersdk.UpdateWorkspaceDormancy{
|
|
Dormant: true,
|
|
})
|
|
require.NoError(t, err, "mark workspace as dormant")
|
|
// Clear notifications before activating the workspace
|
|
notifyEnq.Clear()
|
|
|
|
// Then
|
|
err = client.UpdateWorkspaceDormancy(ctx, workspace.ID, codersdk.UpdateWorkspaceDormancy{
|
|
Dormant: false,
|
|
})
|
|
require.NoError(t, err, "mark workspace as active")
|
|
require.Len(t, notifyEnq.Sent(), 0)
|
|
})
|
|
})
|
|
}
|
|
|
|
func TestWorkspaceTimings(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
db, pubsub := dbtestutil.NewDB(t)
|
|
client := coderdtest.New(t, &coderdtest.Options{
|
|
Database: db,
|
|
Pubsub: pubsub,
|
|
})
|
|
coderdtest.CreateFirstUser(t, client)
|
|
|
|
t.Run("LatestBuild", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
// Given: a workspace with many builds, provisioner, and agent script timings
|
|
db, pubsub := dbtestutil.NewDB(t)
|
|
client := coderdtest.New(t, &coderdtest.Options{
|
|
Database: db,
|
|
Pubsub: pubsub,
|
|
})
|
|
owner := coderdtest.CreateFirstUser(t, client)
|
|
file := dbgen.File(t, db, database.File{
|
|
CreatedBy: owner.UserID,
|
|
})
|
|
versionJob := dbgen.ProvisionerJob(t, db, pubsub, database.ProvisionerJob{
|
|
OrganizationID: owner.OrganizationID,
|
|
InitiatorID: owner.UserID,
|
|
FileID: file.ID,
|
|
Tags: database.StringMap{
|
|
"custom": "true",
|
|
},
|
|
})
|
|
version := dbgen.TemplateVersion(t, db, database.TemplateVersion{
|
|
OrganizationID: owner.OrganizationID,
|
|
JobID: versionJob.ID,
|
|
CreatedBy: owner.UserID,
|
|
})
|
|
template := dbgen.Template(t, db, database.Template{
|
|
OrganizationID: owner.OrganizationID,
|
|
ActiveVersionID: version.ID,
|
|
CreatedBy: owner.UserID,
|
|
})
|
|
ws := dbgen.Workspace(t, db, database.WorkspaceTable{
|
|
OwnerID: owner.UserID,
|
|
OrganizationID: owner.OrganizationID,
|
|
TemplateID: template.ID,
|
|
})
|
|
|
|
// Create multiple builds
|
|
var buildNumber int32
|
|
makeBuild := func() database.WorkspaceBuild {
|
|
buildNumber++
|
|
jobID := uuid.New()
|
|
job := dbgen.ProvisionerJob(t, db, pubsub, database.ProvisionerJob{
|
|
ID: jobID,
|
|
OrganizationID: owner.OrganizationID,
|
|
Tags: database.StringMap{jobID.String(): "true"},
|
|
})
|
|
return dbgen.WorkspaceBuild(t, db, database.WorkspaceBuild{
|
|
WorkspaceID: ws.ID,
|
|
TemplateVersionID: version.ID,
|
|
InitiatorID: owner.UserID,
|
|
JobID: job.ID,
|
|
BuildNumber: buildNumber,
|
|
})
|
|
}
|
|
makeBuild()
|
|
makeBuild()
|
|
latestBuild := makeBuild()
|
|
|
|
// Add provisioner timings
|
|
dbgen.ProvisionerJobTimings(t, db, latestBuild, 5)
|
|
|
|
// Add agent script timings
|
|
resource := dbgen.WorkspaceResource(t, db, database.WorkspaceResource{
|
|
JobID: latestBuild.JobID,
|
|
})
|
|
agent := dbgen.WorkspaceAgent(t, db, database.WorkspaceAgent{
|
|
ResourceID: resource.ID,
|
|
})
|
|
scripts := dbgen.WorkspaceAgentScripts(t, db, 3, database.WorkspaceAgentScript{
|
|
WorkspaceAgentID: agent.ID,
|
|
})
|
|
dbgen.WorkspaceAgentScriptTimings(t, db, scripts)
|
|
|
|
// When: fetching the timings
|
|
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
|
t.Cleanup(cancel)
|
|
res, err := client.WorkspaceTimings(ctx, ws.ID)
|
|
|
|
// Then: expect the timings to be returned
|
|
require.NoError(t, err)
|
|
require.Len(t, res.ProvisionerTimings, 5)
|
|
require.Len(t, res.AgentScriptTimings, 3)
|
|
})
|
|
|
|
t.Run("NonExistentWorkspace", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
// When: fetching an inexistent workspace
|
|
workspaceID := uuid.New()
|
|
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
|
t.Cleanup(cancel)
|
|
_, err := client.WorkspaceTimings(ctx, workspaceID)
|
|
|
|
// Then: expect a not found error
|
|
require.Error(t, err)
|
|
require.Contains(t, err.Error(), "not found")
|
|
})
|
|
}
|
|
|
|
// TestOIDCRemoved emulates a user logging in with OIDC, then that OIDC
|
|
// auth method being removed.
|
|
func TestOIDCRemoved(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
owner, db := coderdtest.NewWithDatabase(t, &coderdtest.Options{
|
|
IncludeProvisionerDaemon: true,
|
|
})
|
|
first := coderdtest.CreateFirstUser(t, owner)
|
|
|
|
user, userData := coderdtest.CreateAnotherUser(t, owner, first.OrganizationID, rbac.ScopedRoleOrgAdmin(first.OrganizationID))
|
|
|
|
ctx := testutil.Context(t, testutil.WaitMedium)
|
|
_, err := db.UpdateUserLoginType(dbauthz.AsSystemRestricted(ctx), database.UpdateUserLoginTypeParams{
|
|
NewLoginType: database.LoginTypeOIDC,
|
|
UserID: userData.ID,
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
_, err = db.InsertUserLink(dbauthz.AsSystemRestricted(ctx), database.InsertUserLinkParams{
|
|
UserID: userData.ID,
|
|
LoginType: database.LoginTypeOIDC,
|
|
LinkedID: "random",
|
|
OAuthAccessToken: "foobar",
|
|
OAuthAccessTokenKeyID: sql.NullString{},
|
|
OAuthRefreshToken: "refresh",
|
|
OAuthRefreshTokenKeyID: sql.NullString{},
|
|
OAuthExpiry: time.Now().Add(time.Hour * -1),
|
|
Claims: database.UserLinkClaims{},
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
version := coderdtest.CreateTemplateVersion(t, owner, first.OrganizationID, nil)
|
|
_ = coderdtest.AwaitTemplateVersionJobCompleted(t, owner, version.ID)
|
|
template := coderdtest.CreateTemplate(t, owner, first.OrganizationID, version.ID)
|
|
|
|
wrk := coderdtest.CreateWorkspace(t, user, template.ID)
|
|
coderdtest.AwaitWorkspaceBuildJobCompleted(t, owner, wrk.LatestBuild.ID)
|
|
|
|
deleteBuild, err := owner.CreateWorkspaceBuild(ctx, wrk.ID, codersdk.CreateWorkspaceBuildRequest{
|
|
Transition: codersdk.WorkspaceTransitionDelete,
|
|
})
|
|
require.NoError(t, err, "delete the workspace")
|
|
coderdtest.AwaitWorkspaceBuildJobCompleted(t, owner, deleteBuild.ID)
|
|
}
|
|
|
|
func TestWorkspaceFilterHasAITask(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)
|
|
|
|
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
|
|
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
|
|
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
|
|
|
|
ctx := testutil.Context(t, testutil.WaitLong)
|
|
|
|
// Helper function to create workspace with optional task.
|
|
createWorkspace := func(jobCompleted, createTask bool, prompt string) uuid.UUID {
|
|
// TODO(mafredri): The bellow comment is based on deprecated logic and
|
|
// kept only present to test that the old observable behavior works as
|
|
// intended.
|
|
//
|
|
// When a provisioner job uses these tags, no provisioner will match it.
|
|
// We do this so jobs will always be stuck in "pending", allowing us to
|
|
// exercise the intermediary state when has_ai_task is nil and we
|
|
// compensate by looking at pending provisioning jobs.
|
|
// See GetWorkspaces clauses.
|
|
unpickableTags := database.StringMap{"custom": "true"}
|
|
|
|
ws := dbgen.Workspace(t, db, database.WorkspaceTable{
|
|
OwnerID: user.UserID,
|
|
OrganizationID: user.OrganizationID,
|
|
TemplateID: template.ID,
|
|
})
|
|
|
|
jobConfig := database.ProvisionerJob{
|
|
OrganizationID: user.OrganizationID,
|
|
InitiatorID: user.UserID,
|
|
Tags: unpickableTags,
|
|
}
|
|
if jobCompleted {
|
|
jobConfig.CompletedAt = sql.NullTime{Time: time.Now(), Valid: true}
|
|
}
|
|
job := dbgen.ProvisionerJob(t, db, pubsub, jobConfig)
|
|
res := dbgen.WorkspaceResource(t, db, database.WorkspaceResource{JobID: job.ID})
|
|
agnt := dbgen.WorkspaceAgent(t, db, database.WorkspaceAgent{ResourceID: res.ID})
|
|
taskApp := dbgen.WorkspaceApp(t, db, database.WorkspaceApp{AgentID: agnt.ID})
|
|
build := dbgen.WorkspaceBuild(t, db, database.WorkspaceBuild{
|
|
WorkspaceID: ws.ID,
|
|
TemplateVersionID: version.ID,
|
|
InitiatorID: user.UserID,
|
|
JobID: job.ID,
|
|
BuildNumber: 1,
|
|
})
|
|
|
|
if createTask {
|
|
task := dbgen.Task(t, db, database.TaskTable{
|
|
WorkspaceID: uuid.NullUUID{UUID: ws.ID, Valid: true},
|
|
OrganizationID: user.OrganizationID,
|
|
OwnerID: user.UserID,
|
|
TemplateVersionID: version.ID,
|
|
Prompt: prompt,
|
|
})
|
|
dbgen.TaskWorkspaceApp(t, db, database.TaskWorkspaceApp{
|
|
TaskID: task.ID,
|
|
WorkspaceBuildNumber: build.BuildNumber,
|
|
WorkspaceAgentID: uuid.NullUUID{UUID: agnt.ID, Valid: true},
|
|
WorkspaceAppID: uuid.NullUUID{UUID: taskApp.ID, Valid: true},
|
|
})
|
|
}
|
|
|
|
return ws.ID
|
|
}
|
|
|
|
// Create workspaces with tasks.
|
|
wsWithTask1 := createWorkspace(true, true, "Build me a web app")
|
|
wsWithTask2 := createWorkspace(false, true, "Another task")
|
|
|
|
// Create workspaces without tasks
|
|
wsWithoutTask1 := createWorkspace(true, false, "")
|
|
wsWithoutTask2 := createWorkspace(false, false, "")
|
|
|
|
// Test filtering for workspaces with AI tasks
|
|
// Should include: wsWithTask1 and wsWithTask2
|
|
res, err := client.Workspaces(ctx, codersdk.WorkspaceFilter{
|
|
FilterQuery: "has-ai-task:true",
|
|
})
|
|
require.NoError(t, err)
|
|
require.Len(t, res.Workspaces, 2)
|
|
workspaceIDs := []uuid.UUID{res.Workspaces[0].ID, res.Workspaces[1].ID}
|
|
require.Contains(t, workspaceIDs, wsWithTask1)
|
|
require.Contains(t, workspaceIDs, wsWithTask2)
|
|
|
|
// Test filtering for workspaces without AI tasks
|
|
// Should include: wsWithoutTask1, wsWithoutTask2, wsWithoutTask3
|
|
res, err = client.Workspaces(ctx, codersdk.WorkspaceFilter{
|
|
FilterQuery: "has-ai-task:false",
|
|
})
|
|
require.NoError(t, err)
|
|
require.Len(t, res.Workspaces, 2)
|
|
workspaceIDs = []uuid.UUID{res.Workspaces[0].ID, res.Workspaces[1].ID}
|
|
require.Contains(t, workspaceIDs, wsWithoutTask1)
|
|
require.Contains(t, workspaceIDs, wsWithoutTask2)
|
|
|
|
// Test no filter returns all
|
|
res, err = client.Workspaces(ctx, codersdk.WorkspaceFilter{})
|
|
require.NoError(t, err)
|
|
require.Len(t, res.Workspaces, 4)
|
|
}
|
|
|
|
func TestWorkspaceListTasks(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
ctx := testutil.Context(t, testutil.WaitShort)
|
|
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
|
|
user := coderdtest.CreateFirstUser(t, client)
|
|
|
|
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
|
|
Parse: echo.ParseComplete,
|
|
ProvisionApply: echo.ApplyComplete,
|
|
ProvisionGraph: []*proto.Response{
|
|
{Type: &proto.Response_Graph{Graph: &proto.GraphComplete{
|
|
HasAiTasks: true,
|
|
}}},
|
|
},
|
|
})
|
|
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
|
|
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
|
|
|
|
// Given: a regular user workspace
|
|
workspaceWithoutTask, err := client.CreateUserWorkspace(ctx, codersdk.Me, codersdk.CreateWorkspaceRequest{
|
|
TemplateID: template.ID,
|
|
Name: "user-workspace",
|
|
})
|
|
require.NoError(t, err)
|
|
coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, workspaceWithoutTask.LatestBuild.ID)
|
|
|
|
// Given: a workspace associated with a task
|
|
task, err := client.CreateTask(ctx, codersdk.Me, codersdk.CreateTaskRequest{
|
|
TemplateVersionID: template.ActiveVersionID,
|
|
Input: "Some task prompt",
|
|
})
|
|
require.NoError(t, err)
|
|
assert.True(t, task.WorkspaceID.Valid)
|
|
workspaceWithTask, err := client.Workspace(ctx, task.WorkspaceID.UUID)
|
|
require.NoError(t, err)
|
|
coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, workspaceWithTask.LatestBuild.ID)
|
|
assert.NotEmpty(t, task.Name)
|
|
assert.Equal(t, template.ID, task.TemplateID)
|
|
|
|
// When: listing the workspaces
|
|
workspaces, err := client.Workspaces(ctx, codersdk.WorkspaceFilter{})
|
|
require.NoError(t, err)
|
|
|
|
assert.Equal(t, workspaces.Count, 2)
|
|
|
|
// Then: verify TaskID is only set for task workspaces
|
|
for _, workspace := range workspaces.Workspaces {
|
|
if workspace.ID == workspaceWithoutTask.ID {
|
|
assert.False(t, workspace.TaskID.Valid)
|
|
} else if workspace.ID == workspaceWithTask.ID {
|
|
assert.True(t, workspace.TaskID.Valid)
|
|
assert.Equal(t, task.ID, workspace.TaskID.UUID)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestWorkspaceAppUpsertRestart(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
client := coderdtest.New(t, &coderdtest.Options{
|
|
IncludeProvisionerDaemon: true,
|
|
})
|
|
user := coderdtest.CreateFirstUser(t, client)
|
|
|
|
// Define an app to be created with the workspace
|
|
apps := []*proto.App{
|
|
{
|
|
Id: uuid.NewString(),
|
|
Slug: "test-app",
|
|
DisplayName: "Test App",
|
|
Command: "test-command",
|
|
Url: "http://localhost:8080",
|
|
Icon: "/test.svg",
|
|
},
|
|
}
|
|
|
|
// Create template version with workspace app
|
|
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
|
|
Parse: echo.ParseComplete,
|
|
ProvisionGraph: []*proto.Response{{
|
|
Type: &proto.Response_Graph{
|
|
Graph: &proto.GraphComplete{
|
|
Resources: []*proto.Resource{{
|
|
Name: "test-resource",
|
|
Type: "example",
|
|
Agents: []*proto.Agent{{
|
|
Id: uuid.NewString(),
|
|
Name: "dev",
|
|
Auth: &proto.Agent_Token{},
|
|
Apps: apps,
|
|
}},
|
|
}},
|
|
},
|
|
},
|
|
}},
|
|
})
|
|
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
|
|
|
|
// Create template and workspace
|
|
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
|
|
workspace := coderdtest.CreateWorkspace(t, client, template.ID)
|
|
coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, workspace.LatestBuild.ID)
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
|
defer cancel()
|
|
|
|
// Verify initial workspace has the app
|
|
workspace, err := client.Workspace(ctx, workspace.ID)
|
|
require.NoError(t, err)
|
|
require.Len(t, workspace.LatestBuild.Resources[0].Agents, 1)
|
|
agent := workspace.LatestBuild.Resources[0].Agents[0]
|
|
require.Len(t, agent.Apps, 1)
|
|
require.Equal(t, "test-app", agent.Apps[0].Slug)
|
|
require.Equal(t, "Test App", agent.Apps[0].DisplayName)
|
|
|
|
// Stop the workspace
|
|
stopBuild := coderdtest.CreateWorkspaceBuild(t, client, workspace, database.WorkspaceTransitionStop)
|
|
coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, stopBuild.ID)
|
|
|
|
// Restart the workspace (this will trigger upsert for the app)
|
|
startBuild := coderdtest.CreateWorkspaceBuild(t, client, workspace, database.WorkspaceTransitionStart)
|
|
coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, startBuild.ID)
|
|
|
|
// Verify the workspace restarted successfully
|
|
workspace, err = client.Workspace(ctx, workspace.ID)
|
|
require.NoError(t, err)
|
|
require.Equal(t, codersdk.WorkspaceStatusRunning, workspace.LatestBuild.Status)
|
|
|
|
// Verify the app is still present after restart (upsert worked)
|
|
require.Len(t, workspace.LatestBuild.Resources[0].Agents, 1)
|
|
agent = workspace.LatestBuild.Resources[0].Agents[0]
|
|
require.Len(t, agent.Apps, 1)
|
|
require.Equal(t, "test-app", agent.Apps[0].Slug)
|
|
require.Equal(t, "Test App", agent.Apps[0].DisplayName)
|
|
|
|
// Verify the provisioner job completed successfully (no error)
|
|
require.Equal(t, codersdk.ProvisionerJobSucceeded, workspace.LatestBuild.Job.Status)
|
|
require.Empty(t, workspace.LatestBuild.Job.Error)
|
|
}
|
|
|
|
func TestMultipleAITasksDisallowed(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)
|
|
|
|
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
|
|
Parse: echo.ParseComplete,
|
|
ProvisionGraph: []*proto.Response{{
|
|
Type: &proto.Response_Graph{
|
|
Graph: &proto.GraphComplete{
|
|
HasAiTasks: true,
|
|
AiTasks: []*proto.AITask{
|
|
{
|
|
Id: uuid.NewString(),
|
|
SidebarApp: &proto.AITaskSidebarApp{
|
|
Id: uuid.NewString(),
|
|
},
|
|
},
|
|
{
|
|
Id: uuid.NewString(),
|
|
SidebarApp: &proto.AITaskSidebarApp{
|
|
Id: uuid.NewString(),
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}},
|
|
})
|
|
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
|
|
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
|
|
|
|
ws := coderdtest.CreateWorkspace(t, client, template.ID)
|
|
coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, ws.LatestBuild.ID)
|
|
|
|
ctx := dbauthz.AsSystemRestricted(t.Context())
|
|
pj, err := db.GetProvisionerJobByID(ctx, ws.LatestBuild.Job.ID)
|
|
require.NoError(t, err)
|
|
require.Contains(t, pj.Error.String, "only one 'coder_ai_task' resource can be provisioned per template")
|
|
}
|
|
|
|
func TestUpdateWorkspaceACL(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
t.Run("OK", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
dv := coderdtest.DeploymentValues(t)
|
|
|
|
adminClient := coderdtest.New(t, &coderdtest.Options{
|
|
IncludeProvisionerDaemon: true,
|
|
DeploymentValues: dv,
|
|
})
|
|
adminUser := coderdtest.CreateFirstUser(t, adminClient)
|
|
orgID := adminUser.OrganizationID
|
|
client, _ := coderdtest.CreateAnotherUser(t, adminClient, orgID)
|
|
_, friend := coderdtest.CreateAnotherUser(t, adminClient, orgID)
|
|
|
|
tv := coderdtest.CreateTemplateVersion(t, adminClient, orgID, nil)
|
|
coderdtest.AwaitTemplateVersionJobCompleted(t, adminClient, tv.ID)
|
|
template := coderdtest.CreateTemplate(t, adminClient, orgID, tv.ID)
|
|
|
|
ws := coderdtest.CreateWorkspace(t, client, template.ID)
|
|
coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, ws.LatestBuild.ID)
|
|
|
|
ctx := testutil.Context(t, testutil.WaitMedium)
|
|
err := client.UpdateWorkspaceACL(ctx, ws.ID, codersdk.UpdateWorkspaceACL{
|
|
UserRoles: map[string]codersdk.WorkspaceRole{
|
|
friend.ID.String(): codersdk.WorkspaceRoleAdmin,
|
|
},
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
workspaceACL, err := client.WorkspaceACL(ctx, ws.ID)
|
|
require.NoError(t, err)
|
|
require.Len(t, workspaceACL.Users, 1)
|
|
require.Equal(t, workspaceACL.Users[0].ID, friend.ID)
|
|
require.Equal(t, workspaceACL.Users[0].Role, codersdk.WorkspaceRoleAdmin)
|
|
})
|
|
|
|
t.Run("UnknownUserID", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
dv := coderdtest.DeploymentValues(t)
|
|
|
|
adminClient := coderdtest.New(t, &coderdtest.Options{
|
|
IncludeProvisionerDaemon: true,
|
|
DeploymentValues: dv,
|
|
})
|
|
adminUser := coderdtest.CreateFirstUser(t, adminClient)
|
|
orgID := adminUser.OrganizationID
|
|
client, _ := coderdtest.CreateAnotherUser(t, adminClient, orgID)
|
|
|
|
tv := coderdtest.CreateTemplateVersion(t, adminClient, orgID, nil)
|
|
coderdtest.AwaitTemplateVersionJobCompleted(t, adminClient, tv.ID)
|
|
template := coderdtest.CreateTemplate(t, adminClient, orgID, tv.ID)
|
|
|
|
ws := coderdtest.CreateWorkspace(t, client, template.ID)
|
|
coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, ws.LatestBuild.ID)
|
|
|
|
ctx := testutil.Context(t, testutil.WaitMedium)
|
|
err := client.UpdateWorkspaceACL(ctx, ws.ID, codersdk.UpdateWorkspaceACL{
|
|
UserRoles: map[string]codersdk.WorkspaceRole{
|
|
uuid.NewString(): codersdk.WorkspaceRoleAdmin,
|
|
},
|
|
})
|
|
require.Error(t, err)
|
|
cerr, ok := codersdk.AsError(err)
|
|
require.True(t, ok)
|
|
require.Len(t, cerr.Validations, 1)
|
|
require.Equal(t, cerr.Validations[0].Field, "user_roles")
|
|
})
|
|
|
|
t.Run("DeletedUser", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
dv := coderdtest.DeploymentValues(t)
|
|
|
|
adminClient := coderdtest.New(t, &coderdtest.Options{
|
|
IncludeProvisionerDaemon: true,
|
|
DeploymentValues: dv,
|
|
})
|
|
adminUser := coderdtest.CreateFirstUser(t, adminClient)
|
|
orgID := adminUser.OrganizationID
|
|
client, _ := coderdtest.CreateAnotherUser(t, adminClient, orgID)
|
|
_, mike := coderdtest.CreateAnotherUser(t, adminClient, orgID)
|
|
|
|
tv := coderdtest.CreateTemplateVersion(t, adminClient, orgID, nil)
|
|
coderdtest.AwaitTemplateVersionJobCompleted(t, adminClient, tv.ID)
|
|
template := coderdtest.CreateTemplate(t, adminClient, orgID, tv.ID)
|
|
|
|
ws := coderdtest.CreateWorkspace(t, client, template.ID)
|
|
coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, ws.LatestBuild.ID)
|
|
|
|
ctx := testutil.Context(t, testutil.WaitMedium)
|
|
err := adminClient.DeleteUser(ctx, mike.ID)
|
|
require.NoError(t, err)
|
|
err = client.UpdateWorkspaceACL(ctx, ws.ID, codersdk.UpdateWorkspaceACL{
|
|
UserRoles: map[string]codersdk.WorkspaceRole{
|
|
mike.ID.String(): codersdk.WorkspaceRoleAdmin,
|
|
},
|
|
})
|
|
require.Error(t, err)
|
|
cerr, ok := codersdk.AsError(err)
|
|
require.True(t, ok)
|
|
require.Len(t, cerr.Validations, 1)
|
|
require.Equal(t, cerr.Validations[0].Field, "user_roles")
|
|
})
|
|
|
|
//nolint:tparallel,paralleltest // Modifies package global rbac.workspaceACLDisabled.
|
|
t.Run("CannotChangeOwnRole", func(t *testing.T) {
|
|
// Save and restore the global to avoid affecting other tests.
|
|
prevWorkspaceACLDisabled := rbac.WorkspaceACLDisabled()
|
|
rbac.SetWorkspaceACLDisabled(false)
|
|
t.Cleanup(func() { rbac.SetWorkspaceACLDisabled(prevWorkspaceACLDisabled) })
|
|
|
|
dv := coderdtest.DeploymentValues(t)
|
|
|
|
adminClient := coderdtest.New(t, &coderdtest.Options{
|
|
IncludeProvisionerDaemon: true,
|
|
DeploymentValues: dv,
|
|
})
|
|
adminUser := coderdtest.CreateFirstUser(t, adminClient)
|
|
orgID := adminUser.OrganizationID
|
|
workspaceOwnerClient, workspaceOwner := coderdtest.CreateAnotherUser(t, adminClient, orgID)
|
|
sharedAdminClient, sharedAdminUser := coderdtest.CreateAnotherUser(t, adminClient, orgID)
|
|
|
|
tv := coderdtest.CreateTemplateVersion(t, adminClient, orgID, nil)
|
|
coderdtest.AwaitTemplateVersionJobCompleted(t, adminClient, tv.ID)
|
|
template := coderdtest.CreateTemplate(t, adminClient, orgID, tv.ID)
|
|
|
|
ws := coderdtest.CreateWorkspace(t, workspaceOwnerClient, template.ID)
|
|
coderdtest.AwaitWorkspaceBuildJobCompleted(t, workspaceOwnerClient, ws.LatestBuild.ID)
|
|
|
|
ctx := testutil.Context(t, testutil.WaitMedium)
|
|
|
|
// Share the workspace with another user as admin.
|
|
err := workspaceOwnerClient.UpdateWorkspaceACL(ctx, ws.ID, codersdk.UpdateWorkspaceACL{
|
|
UserRoles: map[string]codersdk.WorkspaceRole{
|
|
sharedAdminUser.ID.String(): codersdk.WorkspaceRoleAdmin,
|
|
},
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
// The shared admin user should not be able to change their own role.
|
|
err = sharedAdminClient.UpdateWorkspaceACL(ctx, ws.ID, codersdk.UpdateWorkspaceACL{
|
|
UserRoles: map[string]codersdk.WorkspaceRole{
|
|
sharedAdminUser.ID.String(): codersdk.WorkspaceRoleUse,
|
|
},
|
|
})
|
|
require.Error(t, err)
|
|
cerr, ok := codersdk.AsError(err)
|
|
require.True(t, ok)
|
|
require.Equal(t, http.StatusBadRequest, cerr.StatusCode())
|
|
require.Contains(t, cerr.Message, "You cannot change your own workspace sharing role")
|
|
|
|
// The workspace owner should also not be able to change their own role.
|
|
err = workspaceOwnerClient.UpdateWorkspaceACL(ctx, ws.ID, codersdk.UpdateWorkspaceACL{
|
|
UserRoles: map[string]codersdk.WorkspaceRole{
|
|
workspaceOwner.ID.String(): codersdk.WorkspaceRoleUse,
|
|
},
|
|
})
|
|
require.Error(t, err)
|
|
cerr, ok = codersdk.AsError(err)
|
|
require.True(t, ok)
|
|
require.Equal(t, http.StatusBadRequest, cerr.StatusCode())
|
|
require.Contains(t, cerr.Message, "You cannot change your own workspace sharing role")
|
|
|
|
// But the workspace owner should still be able to change the shared admin's role.
|
|
err = workspaceOwnerClient.UpdateWorkspaceACL(ctx, ws.ID, codersdk.UpdateWorkspaceACL{
|
|
UserRoles: map[string]codersdk.WorkspaceRole{
|
|
sharedAdminUser.ID.String(): codersdk.WorkspaceRoleUse,
|
|
},
|
|
})
|
|
require.NoError(t, err)
|
|
})
|
|
}
|
|
|
|
func TestDeleteWorkspaceACL(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
t.Run("WorkspaceOwnerCanDelete", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
var (
|
|
client, db = coderdtest.NewWithDatabase(t, nil)
|
|
admin = coderdtest.CreateFirstUser(t, client)
|
|
workspaceOwnerClient, workspaceOwner = coderdtest.CreateAnotherUser(t, client, admin.OrganizationID)
|
|
_, toShareWithUser = coderdtest.CreateAnotherUser(t, client, admin.OrganizationID)
|
|
workspace = dbfake.WorkspaceBuild(t, db, database.WorkspaceTable{
|
|
OwnerID: workspaceOwner.ID,
|
|
OrganizationID: admin.OrganizationID,
|
|
}).Do().Workspace
|
|
)
|
|
|
|
ctx := testutil.Context(t, testutil.WaitMedium)
|
|
|
|
err := workspaceOwnerClient.UpdateWorkspaceACL(ctx, workspace.ID, codersdk.UpdateWorkspaceACL{
|
|
UserRoles: map[string]codersdk.WorkspaceRole{
|
|
toShareWithUser.ID.String(): codersdk.WorkspaceRoleUse,
|
|
},
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
err = workspaceOwnerClient.DeleteWorkspaceACL(ctx, workspace.ID)
|
|
require.NoError(t, err)
|
|
|
|
acl, err := workspaceOwnerClient.WorkspaceACL(ctx, workspace.ID)
|
|
require.NoError(t, err)
|
|
require.Empty(t, acl.Users)
|
|
})
|
|
|
|
t.Run("SharedUsersCannot", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
var (
|
|
client, db = coderdtest.NewWithDatabase(t, nil)
|
|
admin = coderdtest.CreateFirstUser(t, client)
|
|
workspaceOwnerClient, workspaceOwner = coderdtest.CreateAnotherUser(t, client, admin.OrganizationID)
|
|
sharedUseClient, toShareWithUser = coderdtest.CreateAnotherUser(t, client, admin.OrganizationID)
|
|
workspace = dbfake.WorkspaceBuild(t, db, database.WorkspaceTable{
|
|
OwnerID: workspaceOwner.ID,
|
|
OrganizationID: admin.OrganizationID,
|
|
}).Do().Workspace
|
|
)
|
|
|
|
ctx := testutil.Context(t, testutil.WaitMedium)
|
|
|
|
err := workspaceOwnerClient.UpdateWorkspaceACL(ctx, workspace.ID, codersdk.UpdateWorkspaceACL{
|
|
UserRoles: map[string]codersdk.WorkspaceRole{
|
|
toShareWithUser.ID.String(): codersdk.WorkspaceRoleUse,
|
|
},
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
err = sharedUseClient.DeleteWorkspaceACL(ctx, workspace.ID)
|
|
assert.Error(t, err)
|
|
|
|
acl, err := workspaceOwnerClient.WorkspaceACL(ctx, workspace.ID)
|
|
require.NoError(t, err)
|
|
require.Equal(t, acl.Users[0].ID, toShareWithUser.ID)
|
|
})
|
|
}
|
|
|
|
// `use`-role shares are granted `workspace:read` via the workspace RBAC ACL
|
|
// list, so they should be able to read the ACL.
|
|
//
|
|
//nolint:tparallel,paralleltest // Test modifies a package global (rbac.workspaceACLDisabled).
|
|
func TestWorkspaceReadCanListACL(t *testing.T) {
|
|
// Be defensive by saving/restoring the modified package global.
|
|
prevWorkspaceACLDisabled := rbac.WorkspaceACLDisabled()
|
|
rbac.SetWorkspaceACLDisabled(false)
|
|
t.Cleanup(func() { rbac.SetWorkspaceACLDisabled(prevWorkspaceACLDisabled) })
|
|
|
|
var (
|
|
client, db = coderdtest.NewWithDatabase(t, nil)
|
|
admin = coderdtest.CreateFirstUser(t, client)
|
|
workspaceOwnerClient, workspaceOwner = coderdtest.CreateAnotherUser(t, client, admin.OrganizationID)
|
|
sharedUserClientA, sharedUserA = coderdtest.CreateAnotherUser(t, client, admin.OrganizationID)
|
|
_, sharedUserB = coderdtest.CreateAnotherUser(t, client, admin.OrganizationID)
|
|
sharedGroup = dbgen.Group(t, db, database.Group{OrganizationID: admin.OrganizationID})
|
|
workspace = dbfake.WorkspaceBuild(t, db, database.WorkspaceTable{
|
|
OwnerID: workspaceOwner.ID,
|
|
OrganizationID: admin.OrganizationID,
|
|
}).Do().Workspace
|
|
)
|
|
|
|
ctx := testutil.Context(t, testutil.WaitMedium)
|
|
|
|
err := workspaceOwnerClient.UpdateWorkspaceACL(ctx, workspace.ID, codersdk.UpdateWorkspaceACL{
|
|
UserRoles: map[string]codersdk.WorkspaceRole{
|
|
sharedUserA.ID.String(): codersdk.WorkspaceRoleUse,
|
|
sharedUserB.ID.String(): codersdk.WorkspaceRoleAdmin,
|
|
},
|
|
GroupRoles: map[string]codersdk.WorkspaceRole{
|
|
sharedGroup.ID.String(): codersdk.WorkspaceRoleUse,
|
|
},
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
acl, err := sharedUserClientA.WorkspaceACL(ctx, workspace.ID)
|
|
require.NoError(t, err)
|
|
require.Len(t, acl.Users, 2)
|
|
require.Len(t, acl.Groups, 1)
|
|
|
|
gotRoles := make(map[uuid.UUID]codersdk.WorkspaceRole, len(acl.Users))
|
|
for _, u := range acl.Users {
|
|
gotRoles[u.ID] = u.Role
|
|
}
|
|
require.Equal(t, codersdk.WorkspaceRoleUse, gotRoles[sharedUserA.ID])
|
|
require.Equal(t, codersdk.WorkspaceRoleAdmin, gotRoles[sharedUserB.ID])
|
|
|
|
gotGroupRoles := make(map[uuid.UUID]codersdk.WorkspaceRole, len(acl.Groups))
|
|
for _, g := range acl.Groups {
|
|
gotGroupRoles[g.ID] = g.Role
|
|
}
|
|
require.Equal(t, codersdk.WorkspaceRoleUse, gotGroupRoles[sharedGroup.ID])
|
|
}
|
|
|
|
// nolint:tparallel,paralleltest // Subtests modify a package global (rbac.workspaceACLDisabled).
|
|
func TestWorkspaceSharingDisabled(t *testing.T) {
|
|
t.Run("CanAccessWhenEnabled", func(t *testing.T) {
|
|
var (
|
|
client, db = coderdtest.NewWithDatabase(t, &coderdtest.Options{
|
|
DeploymentValues: coderdtest.DeploymentValues(t, func(dv *codersdk.DeploymentValues) {
|
|
// DisableWorkspaceSharing is false (default)
|
|
}),
|
|
})
|
|
admin = coderdtest.CreateFirstUser(t, client)
|
|
_, wsOwner = coderdtest.CreateAnotherUser(t, client, admin.OrganizationID)
|
|
userClient, user = coderdtest.CreateAnotherUser(t, client, admin.OrganizationID)
|
|
)
|
|
|
|
ctx := testutil.Context(t, testutil.WaitMedium)
|
|
|
|
// Create workspace with ACL granting access to user
|
|
ws := dbfake.WorkspaceBuild(t, db, database.WorkspaceTable{
|
|
OwnerID: wsOwner.ID,
|
|
OrganizationID: admin.OrganizationID,
|
|
UserACL: database.WorkspaceACL{
|
|
user.ID.String(): database.WorkspaceACLEntry{
|
|
Permissions: []policy.Action{
|
|
policy.ActionRead, policy.ActionSSH, policy.ActionApplicationConnect,
|
|
},
|
|
},
|
|
},
|
|
}).Do().Workspace
|
|
|
|
// User SHOULD be able to access workspace when sharing is enabled
|
|
fetchedWs, err := userClient.Workspace(ctx, ws.ID)
|
|
require.NoError(t, err)
|
|
require.Equal(t, ws.ID, fetchedWs.ID)
|
|
})
|
|
|
|
t.Run("NoAccessWhenDisabled", func(t *testing.T) {
|
|
t.Cleanup(func() {
|
|
rbac.ReloadBuiltinRoles(nil)
|
|
})
|
|
|
|
var (
|
|
client, db = coderdtest.NewWithDatabase(t, &coderdtest.Options{
|
|
DeploymentValues: coderdtest.DeploymentValues(t, func(dv *codersdk.DeploymentValues) {
|
|
dv.DisableWorkspaceSharing = true
|
|
}),
|
|
})
|
|
admin = coderdtest.CreateFirstUser(t, client)
|
|
_, wsOwner = coderdtest.CreateAnotherUser(t, client, admin.OrganizationID)
|
|
userClient, user = coderdtest.CreateAnotherUser(t, client, admin.OrganizationID)
|
|
)
|
|
|
|
ctx := testutil.Context(t, testutil.WaitMedium)
|
|
|
|
// Create workspace with ACL granting access to user directly in DB
|
|
ws := dbfake.WorkspaceBuild(t, db, database.WorkspaceTable{
|
|
OwnerID: wsOwner.ID,
|
|
OrganizationID: admin.OrganizationID,
|
|
UserACL: database.WorkspaceACL{
|
|
user.ID.String(): database.WorkspaceACLEntry{
|
|
Permissions: []policy.Action{
|
|
policy.ActionRead, policy.ActionSSH, policy.ActionApplicationConnect,
|
|
},
|
|
},
|
|
},
|
|
}).Do().Workspace
|
|
|
|
// User should NOT be able to access workspace when sharing is disabled
|
|
_, err := userClient.Workspace(ctx, ws.ID)
|
|
require.Error(t, err)
|
|
var sdkErr *codersdk.Error
|
|
require.ErrorAs(t, err, &sdkErr)
|
|
require.Equal(t, http.StatusNotFound, sdkErr.StatusCode())
|
|
})
|
|
}
|
|
|
|
func TestWorkspaceAvailableUsers(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
t.Run("OrgAdminCanListUsers", func(t *testing.T) {
|
|
t.Parallel()
|
|
client := coderdtest.New(t, nil)
|
|
owner := coderdtest.CreateFirstUser(t, client)
|
|
|
|
ctx := testutil.Context(t, testutil.WaitMedium)
|
|
|
|
// Create an org admin and additional users
|
|
orgAdminClient, _ := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID, rbac.ScopedRoleOrgAdmin(owner.OrganizationID))
|
|
_, user1 := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID)
|
|
_, user2 := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID)
|
|
|
|
// Org admin should be able to list available users
|
|
users, err := orgAdminClient.WorkspaceAvailableUsers(ctx, owner.OrganizationID, "me")
|
|
require.NoError(t, err)
|
|
require.GreaterOrEqual(t, len(users), 4) // owner + orgAdmin + 2 users
|
|
|
|
// Verify the users we created are in the list
|
|
usernames := make([]string, 0, len(users))
|
|
for _, u := range users {
|
|
usernames = append(usernames, u.Username)
|
|
}
|
|
require.Contains(t, usernames, user1.Username)
|
|
require.Contains(t, usernames, user2.Username)
|
|
})
|
|
|
|
t.Run("MemberCannotListUsers", func(t *testing.T) {
|
|
t.Parallel()
|
|
client := coderdtest.New(t, nil)
|
|
owner := coderdtest.CreateFirstUser(t, client)
|
|
|
|
ctx := testutil.Context(t, testutil.WaitMedium)
|
|
|
|
// Create a regular member
|
|
memberClient, _ := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID)
|
|
|
|
// Regular member should not be able to list available users
|
|
_, err := memberClient.WorkspaceAvailableUsers(ctx, owner.OrganizationID, "me")
|
|
require.Error(t, err)
|
|
var apiErr *codersdk.Error
|
|
require.ErrorAs(t, err, &apiErr)
|
|
require.Equal(t, http.StatusForbidden, apiErr.StatusCode())
|
|
})
|
|
}
|
|
|
|
func TestWorkspaceCreateWithImplicitPreset(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
// Helper function to create template with presets
|
|
createTemplateWithPresets := func(t *testing.T, client *codersdk.Client, user codersdk.CreateFirstUserResponse, presets []*proto.Preset) (codersdk.Template, codersdk.TemplateVersion) {
|
|
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
|
|
Parse: echo.ParseComplete,
|
|
ProvisionGraph: []*proto.Response{
|
|
{
|
|
Type: &proto.Response_Graph{
|
|
Graph: &proto.GraphComplete{
|
|
Presets: presets,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
})
|
|
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
|
|
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
|
|
return template, version
|
|
}
|
|
|
|
// Helper function to create workspace and verify preset usage
|
|
createWorkspaceAndVerifyPreset := func(t *testing.T, client *codersdk.Client, template codersdk.Template, expectedPresetID *uuid.UUID, params []codersdk.WorkspaceBuildParameter) codersdk.Workspace {
|
|
wsName := testutil.GetRandomNameHyphenated(t)
|
|
var ws codersdk.Workspace
|
|
if len(params) > 0 {
|
|
ws = coderdtest.CreateWorkspace(t, client, template.ID, func(cwr *codersdk.CreateWorkspaceRequest) {
|
|
cwr.Name = wsName
|
|
cwr.RichParameterValues = params
|
|
})
|
|
} else {
|
|
ws = coderdtest.CreateWorkspace(t, client, template.ID, func(cwr *codersdk.CreateWorkspaceRequest) {
|
|
cwr.Name = wsName
|
|
})
|
|
}
|
|
require.Equal(t, wsName, ws.Name)
|
|
|
|
coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, ws.LatestBuild.ID)
|
|
|
|
// Verify the preset was used if expected
|
|
if expectedPresetID != nil {
|
|
require.NotNil(t, ws.LatestBuild.TemplateVersionPresetID)
|
|
require.Equal(t, *expectedPresetID, *ws.LatestBuild.TemplateVersionPresetID)
|
|
} else {
|
|
require.Nil(t, ws.LatestBuild.TemplateVersionPresetID)
|
|
}
|
|
|
|
return ws
|
|
}
|
|
|
|
t.Run("NoPresets", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
|
|
user := coderdtest.CreateFirstUser(t, client)
|
|
|
|
// Create template with no presets
|
|
template, _ := createTemplateWithPresets(t, client, user, []*proto.Preset{})
|
|
|
|
// Test workspace creation with no parameters
|
|
createWorkspaceAndVerifyPreset(t, client, template, nil, nil)
|
|
|
|
// Test workspace creation with parameters (should still work, no preset matching)
|
|
createWorkspaceAndVerifyPreset(t, client, template, nil, []codersdk.WorkspaceBuildParameter{
|
|
{Name: "param1", Value: "value1"},
|
|
})
|
|
})
|
|
|
|
t.Run("SinglePresetNoParameters", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
|
|
user := coderdtest.CreateFirstUser(t, client)
|
|
|
|
// Create template with single preset that has no parameters
|
|
preset := &proto.Preset{
|
|
Name: "empty-preset",
|
|
Description: "A preset with no parameters",
|
|
Parameters: []*proto.PresetParameter{},
|
|
}
|
|
template, version := createTemplateWithPresets(t, client, user, []*proto.Preset{preset})
|
|
|
|
// Get the preset ID from the database
|
|
ctx := context.Background()
|
|
presets, err := client.TemplateVersionPresets(ctx, version.ID)
|
|
require.NoError(t, err)
|
|
require.Len(t, presets, 1)
|
|
presetID := presets[0].ID
|
|
|
|
// Test workspace creation with no parameters - should match the preset
|
|
createWorkspaceAndVerifyPreset(t, client, template, &presetID, nil)
|
|
|
|
// Test workspace creation with parameters - should not match the preset
|
|
createWorkspaceAndVerifyPreset(t, client, template, &presetID, []codersdk.WorkspaceBuildParameter{
|
|
{Name: "param1", Value: "value1"},
|
|
})
|
|
})
|
|
|
|
t.Run("SinglePresetWithParameters", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
|
|
user := coderdtest.CreateFirstUser(t, client)
|
|
|
|
// Create template with single preset that has parameters
|
|
preset := &proto.Preset{
|
|
Name: "param-preset",
|
|
Description: "A preset with parameters",
|
|
Parameters: []*proto.PresetParameter{
|
|
{Name: "param1", Value: "value1"},
|
|
{Name: "param2", Value: "value2"},
|
|
},
|
|
}
|
|
template, version := createTemplateWithPresets(t, client, user, []*proto.Preset{preset})
|
|
|
|
// Get the preset ID from the database
|
|
ctx := context.Background()
|
|
presets, err := client.TemplateVersionPresets(ctx, version.ID)
|
|
require.NoError(t, err)
|
|
require.Len(t, presets, 1)
|
|
presetID := presets[0].ID
|
|
|
|
// Test workspace creation with no parameters - should not match the preset
|
|
createWorkspaceAndVerifyPreset(t, client, template, nil, nil)
|
|
|
|
// Test workspace creation with exact matching parameters - should match the preset
|
|
createWorkspaceAndVerifyPreset(t, client, template, &presetID, []codersdk.WorkspaceBuildParameter{
|
|
{Name: "param1", Value: "value1"},
|
|
{Name: "param2", Value: "value2"},
|
|
})
|
|
|
|
// Test workspace creation with partial matching parameters - should not match the preset
|
|
createWorkspaceAndVerifyPreset(t, client, template, nil, []codersdk.WorkspaceBuildParameter{
|
|
{Name: "param1", Value: "value1"},
|
|
})
|
|
|
|
// Test workspace creation with different parameter values - should not match the preset
|
|
createWorkspaceAndVerifyPreset(t, client, template, nil, []codersdk.WorkspaceBuildParameter{
|
|
{Name: "param1", Value: "value1"},
|
|
{Name: "param2", Value: "different"},
|
|
})
|
|
|
|
// Test workspace creation with extra parameters - should match the preset
|
|
createWorkspaceAndVerifyPreset(t, client, template, &presetID, []codersdk.WorkspaceBuildParameter{
|
|
{Name: "param1", Value: "value1"},
|
|
{Name: "param2", Value: "value2"},
|
|
{Name: "param3", Value: "value3"},
|
|
})
|
|
})
|
|
|
|
t.Run("MultiplePresets", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
|
|
user := coderdtest.CreateFirstUser(t, client)
|
|
|
|
// Create template with multiple presets
|
|
preset1 := &proto.Preset{
|
|
Name: "empty-preset",
|
|
Description: "A preset with no parameters",
|
|
Parameters: []*proto.PresetParameter{},
|
|
}
|
|
preset2 := &proto.Preset{
|
|
Name: "single-param-preset",
|
|
Description: "A preset with one parameter",
|
|
Parameters: []*proto.PresetParameter{
|
|
{Name: "param1", Value: "value1"},
|
|
},
|
|
}
|
|
preset3 := &proto.Preset{
|
|
Name: "multi-param-preset",
|
|
Description: "A preset with multiple parameters",
|
|
Parameters: []*proto.PresetParameter{
|
|
{Name: "param1", Value: "value1"},
|
|
{Name: "param2", Value: "value2"},
|
|
},
|
|
}
|
|
template, version := createTemplateWithPresets(t, client, user, []*proto.Preset{preset1, preset2, preset3})
|
|
|
|
// Get the preset IDs from the database
|
|
ctx := context.Background()
|
|
presets, err := client.TemplateVersionPresets(ctx, version.ID)
|
|
require.NoError(t, err)
|
|
require.Len(t, presets, 3)
|
|
|
|
// Sort presets by name to get consistent ordering
|
|
var emptyPresetID, singleParamPresetID, multiParamPresetID uuid.UUID
|
|
for _, p := range presets {
|
|
switch p.Name {
|
|
case "empty-preset":
|
|
emptyPresetID = p.ID
|
|
case "single-param-preset":
|
|
singleParamPresetID = p.ID
|
|
case "multi-param-preset":
|
|
multiParamPresetID = p.ID
|
|
}
|
|
}
|
|
|
|
// Test workspace creation with no parameters - should match empty preset
|
|
createWorkspaceAndVerifyPreset(t, client, template, &emptyPresetID, nil)
|
|
|
|
// Test workspace creation with single parameter - should match single param preset
|
|
createWorkspaceAndVerifyPreset(t, client, template, &singleParamPresetID, []codersdk.WorkspaceBuildParameter{
|
|
{Name: "param1", Value: "value1"},
|
|
})
|
|
|
|
// Test workspace creation with multiple parameters - should match multi param preset
|
|
createWorkspaceAndVerifyPreset(t, client, template, &multiParamPresetID, []codersdk.WorkspaceBuildParameter{
|
|
{Name: "param1", Value: "value1"},
|
|
{Name: "param2", Value: "value2"},
|
|
})
|
|
|
|
// Test workspace creation with non-matching parameters - should not match any preset
|
|
createWorkspaceAndVerifyPreset(t, client, template, &emptyPresetID, []codersdk.WorkspaceBuildParameter{
|
|
{Name: "param1", Value: "different"},
|
|
})
|
|
})
|
|
|
|
t.Run("PresetSpecifiedExplicitly", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
|
|
user := coderdtest.CreateFirstUser(t, client)
|
|
|
|
// Create template with multiple presets
|
|
preset1 := &proto.Preset{
|
|
Name: "preset1",
|
|
Description: "First preset",
|
|
Parameters: []*proto.PresetParameter{
|
|
{Name: "param1", Value: "value1"},
|
|
},
|
|
}
|
|
preset2 := &proto.Preset{
|
|
Name: "preset2",
|
|
Description: "Second preset",
|
|
Parameters: []*proto.PresetParameter{
|
|
{Name: "param1", Value: "value2"},
|
|
},
|
|
}
|
|
template, version := createTemplateWithPresets(t, client, user, []*proto.Preset{preset1, preset2})
|
|
|
|
// Get the preset IDs from the database
|
|
ctx := context.Background()
|
|
presets, err := client.TemplateVersionPresets(ctx, version.ID)
|
|
require.NoError(t, err)
|
|
require.Len(t, presets, 2)
|
|
|
|
var preset1ID, preset2ID uuid.UUID
|
|
for _, p := range presets {
|
|
switch p.Name {
|
|
case "preset1":
|
|
preset1ID = p.ID
|
|
case "preset2":
|
|
preset2ID = p.ID
|
|
}
|
|
}
|
|
|
|
// Test workspace creation with preset1 specified explicitly - should use preset1 regardless of parameters
|
|
ws := coderdtest.CreateWorkspace(t, client, template.ID, func(cwr *codersdk.CreateWorkspaceRequest) {
|
|
cwr.TemplateVersionPresetID = preset1ID
|
|
cwr.RichParameterValues = []codersdk.WorkspaceBuildParameter{
|
|
{Name: "param1", Value: "value2"}, // This would normally match preset2
|
|
}
|
|
})
|
|
coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, ws.LatestBuild.ID)
|
|
require.NotNil(t, ws.LatestBuild.TemplateVersionPresetID)
|
|
require.Equal(t, preset1ID, *ws.LatestBuild.TemplateVersionPresetID)
|
|
|
|
// Test workspace creation with preset2 specified explicitly - should use preset2 regardless of parameters
|
|
ws2 := coderdtest.CreateWorkspace(t, client, template.ID, func(cwr *codersdk.CreateWorkspaceRequest) {
|
|
cwr.TemplateVersionPresetID = preset2ID
|
|
cwr.RichParameterValues = []codersdk.WorkspaceBuildParameter{
|
|
{Name: "param1", Value: "value1"}, // This would normally match preset1
|
|
}
|
|
})
|
|
coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, ws2.LatestBuild.ID)
|
|
require.NotNil(t, ws2.LatestBuild.TemplateVersionPresetID)
|
|
require.Equal(t, preset2ID, *ws2.LatestBuild.TemplateVersionPresetID)
|
|
})
|
|
}
|
|
|
|
func TestProvisionerJobQueueWaitMetric(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
logger := testutil.Logger(t)
|
|
reg := prometheus.NewRegistry()
|
|
metrics := provisionerdserver.NewMetrics(logger)
|
|
err := metrics.Register(reg)
|
|
require.NoError(t, err)
|
|
|
|
client := coderdtest.New(t, &coderdtest.Options{
|
|
IncludeProvisionerDaemon: true,
|
|
ProvisionerdServerMetrics: metrics,
|
|
})
|
|
user := coderdtest.CreateFirstUser(t, client)
|
|
|
|
// Create a template version - this triggers a template_version_import job.
|
|
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
|
|
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
|
|
|
|
// Check that the queue wait metric was recorded for the template_version_import job.
|
|
importMetric := promhelp.MetricValue(t, reg, "coderd_provisioner_job_queue_wait_seconds", prometheus.Labels{
|
|
"provisioner_type": string(database.ProvisionerTypeEcho),
|
|
"job_type": string(database.ProvisionerJobTypeTemplateVersionImport),
|
|
"transition": "",
|
|
"build_reason": "",
|
|
})
|
|
require.NotNil(t, importMetric, "import job metric should be recorded")
|
|
importHistogram := importMetric.GetHistogram()
|
|
require.NotNil(t, importHistogram)
|
|
require.Equal(t, uint64(1), importHistogram.GetSampleCount(), "import job should have 1 sample")
|
|
require.Greater(t, importHistogram.GetSampleSum(), 0.0, "import job queue wait should be non-zero")
|
|
|
|
// Create a template and workspace - this triggers a workspace_build job.
|
|
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
|
|
workspace := coderdtest.CreateWorkspace(t, client, template.ID)
|
|
coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, workspace.LatestBuild.ID)
|
|
|
|
// Check that the queue wait metric was recorded for the workspace_build job.
|
|
buildMetric := promhelp.MetricValue(t, reg, "coderd_provisioner_job_queue_wait_seconds", prometheus.Labels{
|
|
"provisioner_type": string(database.ProvisionerTypeEcho),
|
|
"job_type": string(database.ProvisionerJobTypeWorkspaceBuild),
|
|
"transition": string(database.WorkspaceTransitionStart),
|
|
"build_reason": string(database.BuildReasonInitiator),
|
|
})
|
|
require.NotNil(t, buildMetric, "workspace build job metric should be recorded")
|
|
buildHistogram := buildMetric.GetHistogram()
|
|
require.NotNil(t, buildHistogram)
|
|
require.Equal(t, uint64(1), buildHistogram.GetSampleCount(), "workspace build job should have 1 sample")
|
|
require.Greater(t, buildHistogram.GetSampleSum(), 0.0, "workspace build job queue wait should be non-zero")
|
|
}
|
|
|
|
func TestWorkspaceBuildsEnqueuedMetric(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
var (
|
|
logger = testutil.Logger(t)
|
|
reg = prometheus.NewRegistry()
|
|
metrics = provisionerdserver.NewMetrics(logger)
|
|
|
|
sched = mustSchedule(t, "CRON_TZ=UTC 0 * * * *")
|
|
tickCh = make(chan time.Time)
|
|
statsCh = make(chan autobuild.Stats)
|
|
)
|
|
|
|
err := metrics.Register(reg)
|
|
require.NoError(t, err)
|
|
|
|
wsBuilderMetrics, err := wsbuilder.NewMetrics(reg)
|
|
require.NoError(t, err)
|
|
|
|
client, db := coderdtest.NewWithDatabase(t, &coderdtest.Options{
|
|
IncludeProvisionerDaemon: true,
|
|
ProvisionerdServerMetrics: metrics,
|
|
WorkspaceBuilderMetrics: wsBuilderMetrics,
|
|
AutobuildTicker: tickCh,
|
|
AutobuildStats: statsCh,
|
|
})
|
|
user := coderdtest.CreateFirstUser(t, client)
|
|
|
|
// Create a template and workspace with autostart schedule.
|
|
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
|
|
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
|
|
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
|
|
workspace := coderdtest.CreateWorkspace(t, client, template.ID, func(cwr *codersdk.CreateWorkspaceRequest) {
|
|
cwr.AutostartSchedule = ptr.Ref(sched.String())
|
|
})
|
|
coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, workspace.LatestBuild.ID)
|
|
|
|
// Stop the workspace to prepare for autostart.
|
|
workspace = coderdtest.MustTransitionWorkspace(t, client, workspace.ID, codersdk.WorkspaceTransitionStart, codersdk.WorkspaceTransitionStop)
|
|
|
|
// Trigger an autostart build via the autobuild ticker. This verifies that
|
|
// autostart builds are recorded with build_reason="autostart".
|
|
p, err := coderdtest.GetProvisionerForTags(db, time.Now(), workspace.OrganizationID, map[string]string{})
|
|
require.NoError(t, err)
|
|
|
|
go func() {
|
|
tickTime := sched.Next(workspace.LatestBuild.CreatedAt)
|
|
coderdtest.UpdateProvisionerLastSeenAt(t, db, p.ID, tickTime)
|
|
tickCh <- tickTime
|
|
close(tickCh)
|
|
}()
|
|
|
|
// Wait for the autostart to complete.
|
|
stats := <-statsCh
|
|
require.Len(t, stats.Errors, 0)
|
|
require.Len(t, stats.Transitions, 1)
|
|
require.Contains(t, stats.Transitions, workspace.ID)
|
|
require.Equal(t, database.WorkspaceTransitionStart, stats.Transitions[workspace.ID])
|
|
|
|
// Verify the workspace was autostarted.
|
|
workspace = coderdtest.MustWorkspace(t, client, workspace.ID)
|
|
coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, workspace.LatestBuild.ID)
|
|
require.Equal(t, codersdk.BuildReasonAutostart, workspace.LatestBuild.Reason)
|
|
|
|
// Now check the autostart metric was recorded.
|
|
autostartCount := promhelp.CounterValue(t, reg, "coderd_workspace_builds_enqueued_total", prometheus.Labels{
|
|
"provisioner_type": string(database.ProvisionerTypeEcho),
|
|
"build_reason": string(database.BuildReasonAutostart),
|
|
"transition": string(database.WorkspaceTransitionStart),
|
|
"status": wsbuilder.BuildStatusSuccess,
|
|
})
|
|
require.Equal(t, 1, autostartCount, "autostart should record 1 enqueue with build_reason=autostart")
|
|
}
|
|
|
|
func mustSchedule(t *testing.T, s string) *cron.Schedule {
|
|
t.Helper()
|
|
sched, err := cron.Weekly(s)
|
|
require.NoError(t, err)
|
|
return sched
|
|
}
|