mirror of
https://github.com/coder/coder.git
synced 2025-06-28 04:33:02 +00:00
* feat: Add agent authentication based on instance ID Each cloud has it's own unique instance identity signatures, which can be used for zero-token authentication. This change adds support for tracking by "instance_id", and automatically authenticating with Google Cloud. * Add test for CLI * Fix workspace agent request name * Fix race with adding to wait group * Fix name of instance identity token
227 lines
7.1 KiB
Go
227 lines
7.1 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"
|
|
"go.opencensus.io/stats/view"
|
|
"google.golang.org/api/idtoken"
|
|
"google.golang.org/api/option"
|
|
|
|
"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"
|
|
)
|
|
|
|
type Options struct {
|
|
GoogleTokenValidator *idtoken.Validator
|
|
}
|
|
|
|
// New constructs an in-memory coderd instance and returns
|
|
// the connected client.
|
|
func New(t *testing.T, options *Options) *codersdk.Client {
|
|
// Stops the opencensus.io worker from leaking a goroutine.
|
|
// The worker isn't used anyways, and is an indirect dependency
|
|
// of the Google Cloud SDK.
|
|
t.Cleanup(func() {
|
|
view.Stop()
|
|
})
|
|
|
|
if options == nil {
|
|
options = &Options{}
|
|
}
|
|
if options.GoogleTokenValidator == nil {
|
|
ctx, cancelFunc := context.WithCancel(context.Background())
|
|
t.Cleanup(cancelFunc)
|
|
var err error
|
|
options.GoogleTokenValidator, err = idtoken.NewValidator(ctx, option.WithoutAuthentication())
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
// 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, closeWait := coderd.New(&coderd.Options{
|
|
Logger: slogtest.Make(t, nil).Leveled(slog.LevelDebug),
|
|
Database: db,
|
|
Pubsub: pubsub,
|
|
|
|
GoogleTokenValidator: options.GoogleTokenValidator,
|
|
})
|
|
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(func() {
|
|
srv.Close()
|
|
closeWait()
|
|
})
|
|
|
|
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), "_", "-")
|
|
}
|