Skip to content

feat: add dbfake for workspace builds and resources #10426

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 10 commits into from
Nov 2, 2023
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
156 changes: 55 additions & 101 deletions cli/agent_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,9 @@ import (
"github.com/coder/coder/v2/agent"
"github.com/coder/coder/v2/cli/clitest"
"github.com/coder/coder/v2/coderd/coderdtest"
"github.com/coder/coder/v2/coderd/database"
"github.com/coder/coder/v2/coderd/database/dbfake"
"github.com/coder/coder/v2/codersdk"
"github.com/coder/coder/v2/provisioner/echo"
"github.com/coder/coder/v2/provisionersdk/proto"
"github.com/coder/coder/v2/pty/ptytest"
)
Expand All @@ -28,20 +29,12 @@ func TestWorkspaceAgent(t *testing.T) {
t.Run("LogDirectory", func(t *testing.T) {
t.Parallel()

authToken := uuid.NewString()
client := coderdtest.New(t, &coderdtest.Options{
IncludeProvisionerDaemon: true,
})
client, db := coderdtest.NewWithDatabase(t, nil)
user := coderdtest.CreateFirstUser(t, client)
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete,
ProvisionApply: echo.ProvisionApplyWithAgent(authToken),
ws, authToken := dbfake.WorkspaceWithAgent(t, db, database.Workspace{
OrganizationID: user.OrganizationID,
OwnerID: user.UserID,
})
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID)
coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, workspace.LatestBuild.ID)

logDir := t.TempDir()
inv, _ := clitest.New(t,
"agent",
Expand All @@ -57,7 +50,7 @@ func TestWorkspaceAgent(t *testing.T) {
ctx := inv.Context()
pty.ExpectMatchContext(ctx, "agent is starting now")

coderdtest.AwaitWorkspaceAgents(t, client, workspace.ID)
coderdtest.AwaitWorkspaceAgents(t, client, ws.ID)

info, err := os.Stat(filepath.Join(logDir, "coder-agent.log"))
require.NoError(t, err)
Expand All @@ -68,33 +61,23 @@ func TestWorkspaceAgent(t *testing.T) {
t.Parallel()
instanceID := "instanceidentifier"
certificates, metadataClient := coderdtest.NewAzureInstanceIdentity(t, instanceID)
client := coderdtest.New(t, &coderdtest.Options{
AzureCertificates: certificates,
IncludeProvisionerDaemon: true,
client, db := coderdtest.NewWithDatabase(t, &coderdtest.Options{
AzureCertificates: certificates,
})
user := coderdtest.CreateFirstUser(t, client)
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete,
ProvisionApply: []*proto.Response{{
Type: &proto.Response_Apply{
Apply: &proto.ApplyComplete{
Resources: []*proto.Resource{{
Name: "somename",
Type: "someinstance",
Agents: []*proto.Agent{{
Auth: &proto.Agent_InstanceId{
InstanceId: instanceID,
},
}},
}},
},
ws := dbfake.Workspace(t, db, database.Workspace{
OrganizationID: user.OrganizationID,
OwnerID: user.UserID,
})
dbfake.WorkspaceBuild(t, db, ws, database.WorkspaceBuild{}, &proto.Resource{
Name: "somename",
Type: "someinstance",
Agents: []*proto.Agent{{
Auth: &proto.Agent_InstanceId{
InstanceId: instanceID,
},
}},
})
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID)
coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, workspace.LatestBuild.ID)

inv, _ := clitest.New(t, "agent", "--auth", "azure-instance-identity", "--agent-url", client.URL.String())
inv = inv.WithContext(
Expand All @@ -103,8 +86,8 @@ func TestWorkspaceAgent(t *testing.T) {
)
ctx := inv.Context()
clitest.Start(t, inv)
coderdtest.AwaitWorkspaceAgents(t, client, workspace.ID)
workspace, err := client.Workspace(ctx, workspace.ID)
coderdtest.AwaitWorkspaceAgents(t, client, ws.ID)
workspace, err := client.Workspace(ctx, ws.ID)
require.NoError(t, err)
resources := workspace.LatestBuild.Resources
if assert.NotEmpty(t, workspace.LatestBuild.Resources) && assert.NotEmpty(t, resources[0].Agents) {
Expand All @@ -120,33 +103,23 @@ func TestWorkspaceAgent(t *testing.T) {
t.Parallel()
instanceID := "instanceidentifier"
certificates, metadataClient := coderdtest.NewAWSInstanceIdentity(t, instanceID)
client := coderdtest.New(t, &coderdtest.Options{
AWSCertificates: certificates,
IncludeProvisionerDaemon: true,
client, db := coderdtest.NewWithDatabase(t, &coderdtest.Options{
AWSCertificates: certificates,
})
user := coderdtest.CreateFirstUser(t, client)
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete,
ProvisionApply: []*proto.Response{{
Type: &proto.Response_Apply{
Apply: &proto.ApplyComplete{
Resources: []*proto.Resource{{
Name: "somename",
Type: "someinstance",
Agents: []*proto.Agent{{
Auth: &proto.Agent_InstanceId{
InstanceId: instanceID,
},
}},
}},
},
ws := dbfake.Workspace(t, db, database.Workspace{
OrganizationID: user.OrganizationID,
OwnerID: user.UserID,
})
dbfake.WorkspaceBuild(t, db, ws, database.WorkspaceBuild{}, &proto.Resource{
Name: "somename",
Type: "someinstance",
Agents: []*proto.Agent{{
Auth: &proto.Agent_InstanceId{
InstanceId: instanceID,
},
}},
})
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID)
coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, workspace.LatestBuild.ID)

inv, _ := clitest.New(t, "agent", "--auth", "aws-instance-identity", "--agent-url", client.URL.String())
inv = inv.WithContext(
Expand All @@ -155,8 +128,8 @@ func TestWorkspaceAgent(t *testing.T) {
)
clitest.Start(t, inv)
ctx := inv.Context()
coderdtest.AwaitWorkspaceAgents(t, client, workspace.ID)
workspace, err := client.Workspace(ctx, workspace.ID)
coderdtest.AwaitWorkspaceAgents(t, client, ws.ID)
workspace, err := client.Workspace(ctx, ws.ID)
require.NoError(t, err)
resources := workspace.LatestBuild.Resources
if assert.NotEmpty(t, resources) && assert.NotEmpty(t, resources[0].Agents) {
Expand All @@ -172,35 +145,24 @@ func TestWorkspaceAgent(t *testing.T) {
t.Parallel()
instanceID := "instanceidentifier"
validator, metadataClient := coderdtest.NewGoogleInstanceIdentity(t, instanceID, false)
client := coderdtest.New(t, &coderdtest.Options{
GoogleTokenValidator: validator,
IncludeProvisionerDaemon: true,
client, db := coderdtest.NewWithDatabase(t, &coderdtest.Options{
GoogleTokenValidator: validator,
})
owner := coderdtest.CreateFirstUser(t, client)
member, _ := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID)
version := coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete,
ProvisionApply: []*proto.Response{{
Type: &proto.Response_Apply{
Apply: &proto.ApplyComplete{
Resources: []*proto.Resource{{
Name: "somename",
Type: "someinstance",
Agents: []*proto.Agent{{
Auth: &proto.Agent_InstanceId{
InstanceId: instanceID,
},
}},
}},
},
member, memberUser := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID)
ws := dbfake.Workspace(t, db, database.Workspace{
OrganizationID: owner.OrganizationID,
OwnerID: memberUser.ID,
})
dbfake.WorkspaceBuild(t, db, ws, database.WorkspaceBuild{}, &proto.Resource{
Name: "somename",
Type: "someinstance",
Agents: []*proto.Agent{{
Auth: &proto.Agent_InstanceId{
InstanceId: instanceID,
},
}},
})
template := coderdtest.CreateTemplate(t, client, owner.OrganizationID, version.ID)
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
workspace := coderdtest.CreateWorkspace(t, member, owner.OrganizationID, template.ID)
coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, workspace.LatestBuild.ID)

inv, cfg := clitest.New(t, "agent", "--auth", "google-instance-identity", "--agent-url", client.URL.String())
ptytest.New(t).Attach(inv)
clitest.SetupConfig(t, member, cfg)
Expand All @@ -212,9 +174,8 @@ func TestWorkspaceAgent(t *testing.T) {
)

ctx := inv.Context()

coderdtest.AwaitWorkspaceAgents(t, client, workspace.ID)
workspace, err := client.Workspace(ctx, workspace.ID)
coderdtest.AwaitWorkspaceAgents(t, client, ws.ID)
workspace, err := client.Workspace(ctx, ws.ID)
require.NoError(t, err)
resources := workspace.LatestBuild.Resources
if assert.NotEmpty(t, resources) && assert.NotEmpty(t, resources[0].Agents) {
Expand Down Expand Up @@ -244,19 +205,12 @@ func TestWorkspaceAgent(t *testing.T) {
t.Run("PostStartup", func(t *testing.T) {
t.Parallel()

authToken := uuid.NewString()
client := coderdtest.New(t, &coderdtest.Options{
IncludeProvisionerDaemon: true,
})
client, db := coderdtest.NewWithDatabase(t, nil)
user := coderdtest.CreateFirstUser(t, client)
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete,
ProvisionApply: echo.ProvisionApplyWithAgent(authToken),
ws, authToken := dbfake.WorkspaceWithAgent(t, db, database.Workspace{
OrganizationID: user.OrganizationID,
OwnerID: user.UserID,
})
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID)
coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, workspace.LatestBuild.ID)

logDir := t.TempDir()
inv, _ := clitest.New(t,
Expand All @@ -274,7 +228,7 @@ func TestWorkspaceAgent(t *testing.T) {
clitest.Start(t, inv)
pty.ExpectMatchContext(inv.Context(), "agent is starting now")

resources := coderdtest.AwaitWorkspaceAgents(t, client, workspace.ID)
resources := coderdtest.AwaitWorkspaceAgents(t, client, ws.ID)
require.Len(t, resources, 1)
require.Len(t, resources[0].Agents, 1)
require.Len(t, resources[0].Agents[0].Subsystems, 2)
Expand Down
7 changes: 7 additions & 0 deletions coderd/coderdtest/coderdtest.go
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,13 @@ func New(t testing.TB, options *Options) *codersdk.Client {
return client
}

// NewWithDatabase constructs a codersdk client connected to an in-memory API instance.
// The database is returned to provide direct data manipulation for tests.
func NewWithDatabase(t testing.TB, options *Options) (*codersdk.Client, database.Store) {
client, _, api := NewWithAPI(t, options)
return client, api.Database
}

// NewWithProvisionerCloser returns a client as well as a handle to close
// the provisioner. This is a temporary function while work is done to
// standardize how provisioners are registered with coderd. The option
Expand Down
124 changes: 124 additions & 0 deletions coderd/database/dbfake/dbfake.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
package dbfake

import (
"context"
"database/sql"
"encoding/json"
"testing"

"github.com/google/uuid"
"github.com/stretchr/testify/require"

"github.com/coder/coder/v2/coderd/database"
"github.com/coder/coder/v2/coderd/database/dbauthz"
"github.com/coder/coder/v2/coderd/database/dbgen"
"github.com/coder/coder/v2/coderd/database/dbtime"
"github.com/coder/coder/v2/coderd/provisionerdserver"
"github.com/coder/coder/v2/coderd/telemetry"
"github.com/coder/coder/v2/provisionersdk/proto"
sdkproto "github.com/coder/coder/v2/provisionersdk/proto"
)

// Workspace inserts a workspace into the database.
func Workspace(t testing.TB, db database.Store, seed database.Workspace) database.Workspace {
t.Helper()

// This intentionally fulfills the minimum requirements of the schema.
// Tests can provide a custom template ID if necessary.
if seed.TemplateID == uuid.Nil {
template := dbgen.Template(t, db, database.Template{
OrganizationID: seed.OrganizationID,
CreatedBy: seed.OwnerID,
})
seed.TemplateID = template.ID
seed.OwnerID = template.CreatedBy
seed.OrganizationID = template.OrganizationID
}
return dbgen.Workspace(t, db, seed)
}

// WorkspaceWithAgent is a helper that generates a workspace with a single resource
// that has an agent attached to it. The agent token is returned.
func WorkspaceWithAgent(t testing.TB, db database.Store, seed database.Workspace) (database.Workspace, string) {
t.Helper()
authToken := uuid.NewString()
ws := Workspace(t, db, seed)
WorkspaceBuild(t, db, ws, database.WorkspaceBuild{}, &proto.Resource{
Name: "example",
Type: "aws_instance",
Agents: []*proto.Agent{{
Id: uuid.NewString(),
Auth: &proto.Agent_Token{
Token: authToken,
},
}},
})
return ws, authToken
}

// WorkspaceBuild inserts a build and a successful job into the database.
func WorkspaceBuild(t testing.TB, db database.Store, ws database.Workspace, seed database.WorkspaceBuild, resources ...*sdkproto.Resource) database.WorkspaceBuild {
t.Helper()
jobID := uuid.New()
seed.JobID = jobID
seed.WorkspaceID = ws.ID
// This intentionally fulfills the minimum requirements of the schema.
// Tests can provide a custom version ID if necessary.
if seed.TemplateVersionID == uuid.Nil {
jobID := uuid.New()
templateVersion := dbgen.TemplateVersion(t, db, database.TemplateVersion{
JobID: jobID,
OrganizationID: ws.OrganizationID,
CreatedBy: ws.OwnerID,
TemplateID: uuid.NullUUID{
UUID: ws.TemplateID,
Valid: true,
},
})
payload, _ := json.Marshal(provisionerdserver.TemplateVersionImportJob{
TemplateVersionID: templateVersion.ID,
})
dbgen.ProvisionerJob(t, db, nil, database.ProvisionerJob{
ID: jobID,
OrganizationID: ws.OrganizationID,
Input: payload,
Type: database.ProvisionerJobTypeTemplateVersionImport,
CompletedAt: sql.NullTime{
Time: dbtime.Now(),
Valid: true,
},
})
seed.TemplateVersionID = templateVersion.ID
}
build := dbgen.WorkspaceBuild(t, db, seed)

payload, err := json.Marshal(provisionerdserver.WorkspaceProvisionJob{
WorkspaceBuildID: build.ID,
})
require.NoError(t, err)
job := dbgen.ProvisionerJob(t, db, nil, database.ProvisionerJob{
ID: jobID,
Input: payload,
OrganizationID: ws.OrganizationID,
CompletedAt: sql.NullTime{
Time: dbtime.Now(),
Valid: true,
},
})
ProvisionerJobResources(t, db, job.ID, seed.Transition, resources...)
return build
}

// ProvisionerJobResources inserts a series of resources into a provisioner job.
func ProvisionerJobResources(t testing.TB, db database.Store, job uuid.UUID, transition database.WorkspaceTransition, resources ...*sdkproto.Resource) {
t.Helper()
if transition == "" {
// Default to start!
transition = database.WorkspaceTransitionStart
}
for _, resource := range resources {
//nolint:gocritic // This is only used by tests.
err := provisionerdserver.InsertWorkspaceResource(dbauthz.AsSystemRestricted(context.Background()), db, job, transition, resource, &telemetry.Snapshot{})
require.NoError(t, err)
}
}
Loading