mirror of
https://github.com/coder/coder.git
synced 2025-07-03 16:13:58 +00:00
* feat: Add workspace agent for SSH This adds the initial agent that supports TTY and execution over SSH. It functions across MacOS, Windows, and Linux. This does not handle the coderd interaction yet, but does setup a simple path forward. * Fix pty tests on Windows * Fix log race * Lock around dial error to fix log output * Fix context return early * fix: Leaking yamux session after HTTP handler is closed Closes #317. We depended on the context canceling the yamux connection, but this isn't a sync operation. Explicitly calling close ensures the handler waits for yamux to complete before exit. * Lock around close return * Force failure with log * Fix failed handler * Upgrade dep * Fix defer inside loops * Fix context cancel for HTTP requests * Fix resize
197 lines
6.3 KiB
Go
197 lines
6.3 KiB
Go
package coderdtest
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"io"
|
|
"net"
|
|
"net/http/httptest"
|
|
"net/url"
|
|
"os"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
"github.com/moby/moby/pkg/namesgenerator"
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"cdr.dev/slog"
|
|
"cdr.dev/slog/sloggers/slogtest"
|
|
"github.com/coder/coder/coderd"
|
|
"github.com/coder/coder/codersdk"
|
|
"github.com/coder/coder/database"
|
|
"github.com/coder/coder/database/databasefake"
|
|
"github.com/coder/coder/database/postgres"
|
|
"github.com/coder/coder/provisioner/echo"
|
|
"github.com/coder/coder/provisionerd"
|
|
"github.com/coder/coder/provisionersdk"
|
|
"github.com/coder/coder/provisionersdk/proto"
|
|
)
|
|
|
|
// New constructs an in-memory coderd instance and returns
|
|
// the connected client.
|
|
func New(t *testing.T) *codersdk.Client {
|
|
// This can be hotswapped for a live database instance.
|
|
db := databasefake.New()
|
|
pubsub := database.NewPubsubInMemory()
|
|
if os.Getenv("DB") != "" {
|
|
connectionURL, close, err := postgres.Open()
|
|
require.NoError(t, err)
|
|
t.Cleanup(close)
|
|
sqlDB, err := sql.Open("postgres", connectionURL)
|
|
require.NoError(t, err)
|
|
t.Cleanup(func() {
|
|
_ = sqlDB.Close()
|
|
})
|
|
err = database.Migrate(sqlDB)
|
|
require.NoError(t, err)
|
|
db = database.New(sqlDB)
|
|
|
|
pubsub, err = database.NewPubsub(context.Background(), sqlDB, connectionURL)
|
|
require.NoError(t, err)
|
|
t.Cleanup(func() {
|
|
_ = pubsub.Close()
|
|
})
|
|
}
|
|
|
|
handler := coderd.New(&coderd.Options{
|
|
Logger: slogtest.Make(t, nil).Leveled(slog.LevelDebug),
|
|
Database: db,
|
|
Pubsub: pubsub,
|
|
})
|
|
srv := httptest.NewUnstartedServer(handler)
|
|
srv.Config.BaseContext = func(_ net.Listener) context.Context {
|
|
ctx, cancelFunc := context.WithCancel(context.Background())
|
|
t.Cleanup(cancelFunc)
|
|
return ctx
|
|
}
|
|
srv.Start()
|
|
serverURL, err := url.Parse(srv.URL)
|
|
require.NoError(t, err)
|
|
t.Cleanup(srv.Close)
|
|
|
|
return codersdk.New(serverURL)
|
|
}
|
|
|
|
// NewProvisionerDaemon launches a provisionerd instance configured to work
|
|
// well with coderd testing. It registers the "echo" provisioner for
|
|
// quick testing.
|
|
func NewProvisionerDaemon(t *testing.T, client *codersdk.Client) io.Closer {
|
|
echoClient, echoServer := provisionersdk.TransportPipe()
|
|
ctx, cancelFunc := context.WithCancel(context.Background())
|
|
t.Cleanup(func() {
|
|
_ = echoClient.Close()
|
|
_ = echoServer.Close()
|
|
cancelFunc()
|
|
})
|
|
go func() {
|
|
err := echo.Serve(ctx, &provisionersdk.ServeOptions{
|
|
Listener: echoServer,
|
|
})
|
|
require.NoError(t, err)
|
|
}()
|
|
|
|
closer := provisionerd.New(client.ProvisionerDaemonClient, &provisionerd.Options{
|
|
Logger: slogtest.Make(t, nil).Named("provisionerd").Leveled(slog.LevelDebug),
|
|
PollInterval: 50 * time.Millisecond,
|
|
UpdateInterval: 50 * time.Millisecond,
|
|
Provisioners: provisionerd.Provisioners{
|
|
string(database.ProvisionerTypeEcho): proto.NewDRPCProvisionerClient(provisionersdk.Conn(echoClient)),
|
|
},
|
|
WorkDirectory: t.TempDir(),
|
|
})
|
|
t.Cleanup(func() {
|
|
_ = closer.Close()
|
|
})
|
|
return closer
|
|
}
|
|
|
|
// CreateInitialUser creates a user with preset credentials and authenticates
|
|
// with the passed in codersdk client.
|
|
func CreateInitialUser(t *testing.T, client *codersdk.Client) coderd.CreateInitialUserRequest {
|
|
req := coderd.CreateInitialUserRequest{
|
|
Email: "testuser@coder.com",
|
|
Username: "testuser",
|
|
Password: "testpass",
|
|
Organization: "testorg",
|
|
}
|
|
_, err := client.CreateInitialUser(context.Background(), req)
|
|
require.NoError(t, err)
|
|
|
|
login, err := client.LoginWithPassword(context.Background(), coderd.LoginWithPasswordRequest{
|
|
Email: req.Email,
|
|
Password: req.Password,
|
|
})
|
|
require.NoError(t, err)
|
|
client.SessionToken = login.SessionToken
|
|
return req
|
|
}
|
|
|
|
// CreateProjectImportJob creates a project import provisioner job
|
|
// with the responses provided. It uses the "echo" provisioner for compatibility
|
|
// with testing.
|
|
func CreateProjectImportJob(t *testing.T, client *codersdk.Client, organization string, res *echo.Responses) coderd.ProvisionerJob {
|
|
data, err := echo.Tar(res)
|
|
require.NoError(t, err)
|
|
file, err := client.UploadFile(context.Background(), codersdk.ContentTypeTar, data)
|
|
require.NoError(t, err)
|
|
job, err := client.CreateProjectImportJob(context.Background(), organization, coderd.CreateProjectImportJobRequest{
|
|
StorageSource: file.Hash,
|
|
StorageMethod: database.ProvisionerStorageMethodFile,
|
|
Provisioner: database.ProvisionerTypeEcho,
|
|
})
|
|
require.NoError(t, err)
|
|
return job
|
|
}
|
|
|
|
// CreateProject creates a project with the "echo" provisioner for
|
|
// compatibility with testing. The name assigned is randomly generated.
|
|
func CreateProject(t *testing.T, client *codersdk.Client, organization string, job uuid.UUID) coderd.Project {
|
|
project, err := client.CreateProject(context.Background(), organization, coderd.CreateProjectRequest{
|
|
Name: randomUsername(),
|
|
VersionImportJobID: job,
|
|
})
|
|
require.NoError(t, err)
|
|
return project
|
|
}
|
|
|
|
// AwaitProjectImportJob awaits for an import job to reach completed status.
|
|
func AwaitProjectImportJob(t *testing.T, client *codersdk.Client, organization string, job uuid.UUID) coderd.ProvisionerJob {
|
|
var provisionerJob coderd.ProvisionerJob
|
|
require.Eventually(t, func() bool {
|
|
var err error
|
|
provisionerJob, err = client.ProjectImportJob(context.Background(), organization, job)
|
|
require.NoError(t, err)
|
|
return provisionerJob.Status.Completed()
|
|
}, 5*time.Second, 25*time.Millisecond)
|
|
return provisionerJob
|
|
}
|
|
|
|
// AwaitWorkspaceProvisionJob awaits for a workspace provision job to reach completed status.
|
|
func AwaitWorkspaceProvisionJob(t *testing.T, client *codersdk.Client, organization string, job uuid.UUID) coderd.ProvisionerJob {
|
|
var provisionerJob coderd.ProvisionerJob
|
|
require.Eventually(t, func() bool {
|
|
var err error
|
|
provisionerJob, err = client.WorkspaceProvisionJob(context.Background(), organization, job)
|
|
require.NoError(t, err)
|
|
return provisionerJob.Status.Completed()
|
|
}, 5*time.Second, 25*time.Millisecond)
|
|
return provisionerJob
|
|
}
|
|
|
|
// CreateWorkspace creates a workspace for the user and project provided.
|
|
// A random name is generated for it.
|
|
func CreateWorkspace(t *testing.T, client *codersdk.Client, user string, projectID uuid.UUID) coderd.Workspace {
|
|
workspace, err := client.CreateWorkspace(context.Background(), user, coderd.CreateWorkspaceRequest{
|
|
ProjectID: projectID,
|
|
Name: randomUsername(),
|
|
})
|
|
require.NoError(t, err)
|
|
return workspace
|
|
}
|
|
|
|
func randomUsername() string {
|
|
return strings.ReplaceAll(namesgenerator.GetRandomName(0), "_", "-")
|
|
}
|