feat: unexpose coderdtest.NewWithAPI (#2613)

* feat: unexpose coderdtest.NewWithAPI
This commit is contained in:
Jon Ayers 2022-06-27 13:50:52 -05:00 committed by GitHub
parent 7dfec821f5
commit 2353687610
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 409 additions and 241 deletions

View File

@ -2,7 +2,6 @@ package cli_test
import (
"context"
"database/sql"
"fmt"
"os"
"testing"
@ -13,7 +12,6 @@ import (
"github.com/coder/coder/cli/clitest"
"github.com/coder/coder/coderd/coderdtest"
"github.com/coder/coder/coderd/database"
"github.com/coder/coder/codersdk"
"github.com/coder/coder/provisioner/echo"
"github.com/coder/coder/provisionersdk/proto"
@ -255,44 +253,42 @@ func TestCreate(t *testing.T) {
t.Run("FailedDryRun", func(t *testing.T) {
t.Parallel()
client, api := coderdtest.NewWithAPI(t, &coderdtest.Options{IncludeProvisionerD: true})
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerD: true})
user := coderdtest.CreateFirstUser(t, client)
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete,
Parse: []*proto.Parse_Response{{
Type: &proto.Parse_Response_Complete{
Complete: &proto.Parse_Complete{
ParameterSchemas: echo.ParameterSuccess,
},
},
}},
ProvisionDryRun: []*proto.Provision_Response{
{
Type: &proto.Provision_Response_Complete{
Complete: &proto.Provision_Complete{
Error: "test error",
},
Complete: &proto.Provision_Complete{},
},
},
},
})
tempDir := t.TempDir()
parameterFile, _ := os.CreateTemp(tempDir, "testParameterFile*.yaml")
_, _ = parameterFile.WriteString(fmt.Sprintf("%s: %q", echo.ParameterExecKey, echo.ParameterError("fail")))
// The template import job should end up failed, but we need it to be
// succeeded so the dry-run can begin.
version = coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
require.Equal(t, codersdk.ProvisionerJobFailed, version.Job.Status, "job is not failed")
err := api.Database.UpdateProvisionerJobWithCompleteByID(context.Background(), database.UpdateProvisionerJobWithCompleteByIDParams{
ID: version.Job.ID,
CompletedAt: sql.NullTime{
Time: time.Now(),
Valid: true,
},
UpdatedAt: time.Now(),
Error: sql.NullString{},
})
require.NoError(t, err, "update provisioner job")
require.Equal(t, codersdk.ProvisionerJobSucceeded, version.Job.Status, "job is not failed")
_ = coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
cmd, root := clitest.New(t, "create", "test")
cmd, root := clitest.New(t, "create", "test", "--parameter-file", parameterFile.Name())
clitest.SetupConfig(t, client, root)
pty := ptytest.New(t)
cmd.SetIn(pty.Input())
cmd.SetOut(pty.Output())
err = cmd.Execute()
err := cmd.Execute()
require.Error(t, err)
require.ErrorContains(t, err, "dry-run workspace")
})

1
cmd/coder/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
coder

View File

@ -287,7 +287,11 @@ func New(options *Options) *API {
r.Post("/authorization", api.checkPermissions)
r.Post("/keys", api.postAPIKey)
r.Route("/keys", func(r chi.Router) {
r.Post("/", api.postAPIKey)
r.Get("/{keyid}", api.apiKey)
})
r.Route("/organizations", func(r chi.Router) {
r.Get("/", api.organizationsByUser)
r.Get("/{organizationname}", api.organizationByUserAndName)

View File

@ -2,8 +2,14 @@ package coderd_test
import (
"context"
"crypto/x509"
"database/sql"
"io"
"net"
"net/http"
"net/http/httptest"
"net/url"
"os"
"strconv"
"strings"
"testing"
@ -14,10 +20,23 @@ import (
"github.com/stretchr/testify/require"
"go.uber.org/goleak"
"golang.org/x/xerrors"
"google.golang.org/api/idtoken"
"google.golang.org/api/option"
"cdr.dev/slog"
"cdr.dev/slog/sloggers/slogtest"
"github.com/coder/coder/buildinfo"
"github.com/coder/coder/coderd"
"github.com/coder/coder/coderd/autobuild/executor"
"github.com/coder/coder/coderd/coderdtest"
"github.com/coder/coder/coderd/database"
"github.com/coder/coder/coderd/database/databasefake"
"github.com/coder/coder/coderd/database/postgres"
"github.com/coder/coder/coderd/gitsshkey"
"github.com/coder/coder/coderd/rbac"
"github.com/coder/coder/coderd/telemetry"
"github.com/coder/coder/coderd/turnconn"
"github.com/coder/coder/codersdk"
"github.com/coder/coder/provisioner/echo"
"github.com/coder/coder/provisionersdk/proto"
@ -39,13 +58,96 @@ func TestBuildInfo(t *testing.T) {
// TestAuthorizeAllEndpoints will check `authorize` is called on every endpoint registered.
func TestAuthorizeAllEndpoints(t *testing.T) {
t.Parallel()
ctx := context.Background()
var (
ctx = context.Background()
authorizer = &fakeAuthorizer{}
)
authorizer := &fakeAuthorizer{}
client, api := coderdtest.NewWithAPI(t, &coderdtest.Options{
Authorizer: authorizer,
IncludeProvisionerD: true,
})
// This function was taken from coderdtest.newWithAPI. It is intentionally
// copied to avoid exposing the API to other tests in coderd. Tests should
// not need a reference to coderd.API...this test is an exception.
newClient := func(authorizer rbac.Authorizer) (*codersdk.Client, *coderd.API) {
// This can be hotswapped for a live database instance.
db := databasefake.New()
pubsub := database.NewPubsubInMemory()
if os.Getenv("DB") != "" {
connectionURL, closePg, err := postgres.Open()
require.NoError(t, err)
t.Cleanup(closePg)
sqlDB, err := sql.Open("postgres", connectionURL)
require.NoError(t, err)
t.Cleanup(func() {
_ = sqlDB.Close()
})
err = database.MigrateUp(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()
})
}
tickerCh := make(chan time.Time)
t.Cleanup(func() { close(tickerCh) })
ctx, cancelFunc := context.WithCancel(context.Background())
lifecycleExecutor := executor.New(
ctx,
db,
slogtest.Make(t, nil).Named("autobuild.executor").Leveled(slog.LevelDebug),
tickerCh,
).WithStatsChannel(nil)
lifecycleExecutor.Run()
srv := httptest.NewUnstartedServer(nil)
srv.Config.BaseContext = func(_ net.Listener) context.Context {
return ctx
}
srv.Start()
serverURL, err := url.Parse(srv.URL)
require.NoError(t, err)
turnServer, err := turnconn.New(nil)
require.NoError(t, err)
validator, err := idtoken.NewValidator(ctx, option.WithoutAuthentication())
require.NoError(t, err)
// We set the handler after server creation for the access URL.
coderAPI := coderd.New(&coderd.Options{
AgentConnectionUpdateFrequency: 150 * time.Millisecond,
AccessURL: serverURL,
Logger: slogtest.Make(t, nil).Leveled(slog.LevelDebug),
Database: db,
Pubsub: pubsub,
AWSCertificates: nil,
AzureCertificates: x509.VerifyOptions{},
GithubOAuth2Config: nil,
GoogleTokenValidator: validator,
SSHKeygenAlgorithm: gitsshkey.AlgorithmEd25519,
TURNServer: turnServer,
APIRateLimit: 0,
Authorizer: authorizer,
Telemetry: telemetry.NewNoop(),
})
srv.Config.Handler = coderAPI.Handler
_ = coderdtest.NewProvisionerDaemon(t, coderAPI)
t.Cleanup(func() {
cancelFunc()
_ = turnServer.Close()
srv.Close()
_ = coderAPI.Close()
})
return codersdk.New(serverURL), coderAPI
}
client, api := newClient(authorizer)
admin := coderdtest.CreateFirstUser(t, client)
// The provisioner will call to coderd and register itself. This is async,
// so we wait for it to occur.

View File

@ -74,12 +74,31 @@ type Options struct {
// New constructs a codersdk client connected to an in-memory API instance.
func New(t *testing.T, options *Options) *codersdk.Client {
client, _ := NewWithAPI(t, options)
client, _ := newWithCloser(t, options)
return client
}
// NewWithAPI constructs a codersdk client connected to the returned in-memory API instance.
func NewWithAPI(t *testing.T, options *Options) (*codersdk.Client, *coderd.API) {
// 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
// to include a provisioner is set to true for convenience.
func NewWithProvisionerCloser(t *testing.T, options *Options) (*codersdk.Client, io.Closer) {
if options == nil {
options = &Options{}
}
options.IncludeProvisionerD = true
client, closer := newWithCloser(t, options)
return client, closer
}
// newWithCloser constructs a codersdk client connected to an in-memory API instance.
// The returned closer closes a provisioner if it was provided
// The API is intentionally not returned here because coderd tests should not
// require a handle to the API. Do not expose the API or wrath shall descend
// upon thee. Even the io.Closer that is exposed here shouldn't be exposed
// and is a temporary measure while the API to register provisioners is ironed
// out.
func newWithCloser(t *testing.T, options *Options) (*codersdk.Client, io.Closer) {
if options == nil {
options = &Options{}
}
@ -169,17 +188,21 @@ func NewWithAPI(t *testing.T, options *Options) (*codersdk.Client, *coderd.API)
Telemetry: telemetry.NewNoop(),
})
srv.Config.Handler = coderAPI.Handler
var provisionerCloser io.Closer = nopcloser{}
if options.IncludeProvisionerD {
_ = NewProvisionerDaemon(t, coderAPI)
provisionerCloser = NewProvisionerDaemon(t, coderAPI)
}
t.Cleanup(func() {
cancelFunc()
_ = turnServer.Close()
srv.Close()
_ = coderAPI.Close()
_ = provisionerCloser.Close()
})
return codersdk.New(serverURL), coderAPI
return codersdk.New(serverURL), provisionerCloser
}
// NewProvisionerDaemon launches a provisionerd instance configured to work
@ -648,3 +671,7 @@ type roundTripper func(req *http.Request) (*http.Response, error)
func (r roundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
return r(req)
}
type nopcloser struct{}
func (nopcloser) Close() error { return nil }

View File

@ -14,9 +14,10 @@ func TestMain(m *testing.M) {
func TestNew(t *testing.T) {
t.Parallel()
client, coderAPI := coderdtest.NewWithAPI(t, nil)
client := coderdtest.New(t, &coderdtest.Options{
IncludeProvisionerD: true,
})
user := coderdtest.CreateFirstUser(t, client)
closer := coderdtest.NewProvisionerDaemon(t, coderAPI)
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
@ -25,5 +26,4 @@ func TestNew(t *testing.T) {
coderdtest.AwaitWorkspaceAgents(t, client, workspace.LatestBuild.ID)
_, _ = coderdtest.NewGoogleInstanceIdentity(t, "example", false)
_, _ = coderdtest.NewAWSInstanceIdentity(t, "an-instance")
closer.Close()
}

View File

@ -79,9 +79,10 @@ func TestGitSSHKey(t *testing.T) {
func TestAgentGitSSHKey(t *testing.T) {
t.Parallel()
client, coderAPI := coderdtest.NewWithAPI(t, nil)
client := coderdtest.New(t, &coderdtest.Options{
IncludeProvisionerD: true,
})
user := coderdtest.CreateFirstUser(t, client)
daemonCloser := coderdtest.NewProvisionerDaemon(t, coderAPI)
authToken := uuid.NewString()
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete,
@ -107,7 +108,6 @@ func TestAgentGitSSHKey(t *testing.T) {
coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, project.ID)
coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID)
daemonCloser.Close()
agentClient := codersdk.New(client.URL)
agentClient.SessionToken = authToken

View File

@ -2,7 +2,6 @@ package coderd_test
import (
"context"
"database/sql"
"net/http"
"testing"
"time"
@ -12,7 +11,6 @@ import (
"github.com/stretchr/testify/require"
"github.com/coder/coder/coderd/coderdtest"
"github.com/coder/coder/coderd/database"
"github.com/coder/coder/codersdk"
"github.com/coder/coder/provisioner/echo"
"github.com/coder/coder/provisionersdk/proto"
@ -555,18 +553,30 @@ func TestTemplateVersionDryRun(t *testing.T) {
t.Run("OK", func(t *testing.T) {
t.Parallel()
client, api := coderdtest.NewWithAPI(t, &coderdtest.Options{IncludeProvisionerD: true})
client, closer := coderdtest.NewWithProvisionerCloser(t, nil)
defer closer.Close()
user := coderdtest.CreateFirstUser(t, client)
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete,
Provision: []*proto.Provision_Response{{
Type: &proto.Provision_Response_Log{
Log: &proto.Log{},
Provision: []*proto.Provision_Response{
{
Type: &proto.Provision_Response_Log{
Log: &proto.Log{},
}},
{
Type: &proto.Provision_Response_Complete{
Complete: &proto.Provision_Complete{},
},
},
}},
},
})
forceCompleteTemplateVersionJob(t, api.Database, client, version)
version = coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
require.Equal(t, codersdk.ProvisionerJobSucceeded, version.Job.Status)
closer.Close()
// Create the dry-run
job, err := client.CreateTemplateVersionDryRun(context.Background(), version.ID, codersdk.CreateTemplateVersionDryRunRequest{
ParameterValues: []codersdk.CreateParameterRequest{},
@ -578,7 +588,7 @@ func TestTemplateVersionDryRun(t *testing.T) {
assert.NoError(t, err)
t.Logf("Status: %s", job.Status)
return job.Status == codersdk.ProvisionerJobRunning
return job.Status == codersdk.ProvisionerJobPending
}, 5*time.Second, 25*time.Millisecond)
err = client.CancelTemplateVersionDryRun(context.Background(), version.ID, job.ID)
@ -589,7 +599,7 @@ func TestTemplateVersionDryRun(t *testing.T) {
assert.NoError(t, err)
t.Logf("Status: %s", job.Status)
return job.Status == codersdk.ProvisionerJobCanceled
return job.Status == codersdk.ProvisionerJobCanceling
}, 5*time.Second, 25*time.Millisecond)
})
@ -622,17 +632,29 @@ func TestTemplateVersionDryRun(t *testing.T) {
t.Run("AlreadyCanceled", func(t *testing.T) {
t.Parallel()
client, api := coderdtest.NewWithAPI(t, &coderdtest.Options{IncludeProvisionerD: true})
client, closer := coderdtest.NewWithProvisionerCloser(t, nil)
defer closer.Close()
user := coderdtest.CreateFirstUser(t, client)
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete,
Provision: []*proto.Provision_Response{{
Type: &proto.Provision_Response_Log{
Log: &proto.Log{},
Provision: []*proto.Provision_Response{
{
Type: &proto.Provision_Response_Log{
Log: &proto.Log{},
}},
{
Type: &proto.Provision_Response_Complete{
Complete: &proto.Provision_Complete{},
},
},
}},
},
})
forceCompleteTemplateVersionJob(t, api.Database, client, version)
version = coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
require.Equal(t, codersdk.ProvisionerJobSucceeded, version.Job.Status)
closer.Close()
// Create the dry-run
job, err := client.CreateTemplateVersionDryRun(context.Background(), version.ID, codersdk.CreateTemplateVersionDryRunRequest{
@ -753,23 +775,3 @@ func TestPaginatedTemplateVersions(t *testing.T) {
})
}
}
func forceCompleteTemplateVersionJob(t *testing.T, db database.Store, client *codersdk.Client, version codersdk.TemplateVersion) {
t.Helper()
// HACK: we need the template version job to be finished so the dry-run job
// can be created. We do this by canceling the job and then marking it as
// successful.
err := client.CancelTemplateVersion(context.Background(), version.ID)
require.NoError(t, err)
err = db.UpdateProvisionerJobWithCompleteByID(context.Background(), database.UpdateProvisionerJobWithCompleteByIDParams{
ID: version.Job.ID,
UpdatedAt: time.Now(),
CompletedAt: sql.NullTime{
Time: time.Now(),
Valid: true,
},
Error: sql.NullString{},
})
require.NoError(t, err)
}

View File

@ -719,6 +719,34 @@ func (api *API) postAPIKey(rw http.ResponseWriter, r *http.Request) {
httpapi.Write(rw, http.StatusCreated, codersdk.GenerateAPIKeyResponse{Key: sessionToken})
}
func (api *API) apiKey(rw http.ResponseWriter, r *http.Request) {
var (
ctx = r.Context()
user = httpmw.UserParam(r)
)
if !api.Authorize(r, rbac.ActionRead, rbac.ResourceAPIKey.WithOwner(user.ID.String())) {
httpapi.ResourceNotFound(rw)
return
}
keyID := chi.URLParam(r, "keyid")
key, err := api.Database.GetAPIKeyByID(ctx, keyID)
if errors.Is(err, sql.ErrNoRows) {
httpapi.ResourceNotFound(rw)
return
}
if err != nil {
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
Message: "Internal error fetching API key.",
Detail: err.Error(),
})
return
}
httpapi.Write(rw, http.StatusOK, convertAPIKey(key))
}
// Clear the user's session cookie.
func (api *API) postLogout(rw http.ResponseWriter, r *http.Request) {
// Get a blank token cookie.
@ -1008,3 +1036,16 @@ func parseUserStatus(v string) ([]database.UserStatus, error) {
}
return statuses, nil
}
func convertAPIKey(k database.APIKey) codersdk.APIKey {
return codersdk.APIKey{
ID: k.ID,
UserID: k.UserID,
LastUsed: k.LastUsed,
ExpiresAt: k.ExpiresAt,
CreatedAt: k.CreatedAt,
UpdatedAt: k.UpdatedAt,
LoginType: codersdk.LoginType(k.LoginType),
LifetimeSeconds: k.LifetimeSeconds,
}
}

View File

@ -2,7 +2,6 @@ package coderd_test
import (
"context"
"database/sql"
"fmt"
"net/http"
"sort"
@ -14,8 +13,6 @@ import (
"github.com/stretchr/testify/require"
"github.com/coder/coder/coderd/coderdtest"
"github.com/coder/coder/coderd/database"
"github.com/coder/coder/coderd/database/databasefake"
"github.com/coder/coder/coderd/httpmw"
"github.com/coder/coder/coderd/rbac"
"github.com/coder/coder/codersdk"
@ -138,94 +135,24 @@ func TestPostLogin(t *testing.T) {
var (
ctx = context.Background()
)
client, api := coderdtest.NewWithAPI(t, nil)
client := coderdtest.New(t, nil)
admin := coderdtest.CreateFirstUser(t, client)
split := strings.Split(client.SessionToken, "-")
loginKey, err := api.Database.GetAPIKeyByID(ctx, split[0])
key, err := client.GetAPIKey(ctx, admin.UserID.String(), split[0])
require.NoError(t, err, "fetch login key")
require.Equal(t, int64(86400), loginKey.LifetimeSeconds, "default should be 86400")
require.Equal(t, int64(86400), key.LifetimeSeconds, "default should be 86400")
// Generated tokens have a longer life
token, err := client.CreateAPIKey(ctx, admin.UserID.String())
require.NoError(t, err, "make new api key")
split = strings.Split(token.Key, "-")
apiKey, err := api.Database.GetAPIKeyByID(ctx, split[0])
apiKey, err := client.GetAPIKey(ctx, admin.UserID.String(), split[0])
require.NoError(t, err, "fetch api key")
require.True(t, apiKey.ExpiresAt.After(time.Now().Add(time.Hour*24*6)), "api key lasts more than 6 days")
require.True(t, apiKey.ExpiresAt.After(loginKey.ExpiresAt.Add(time.Hour)), "api key should be longer expires")
require.Greater(t, apiKey.LifetimeSeconds, loginKey.LifetimeSeconds, "api key should have longer lifetime")
})
t.Run("APIKeyExtend", func(t *testing.T) {
t.Parallel()
var (
ctx = context.Background()
)
client, api := coderdtest.NewWithAPI(t, nil)
admin := coderdtest.CreateFirstUser(t, client)
token, err := client.CreateAPIKey(ctx, admin.UserID.String())
require.NoError(t, err, "make new api key")
client.SessionToken = token.Key
split := strings.Split(token.Key, "-")
apiKey, err := api.Database.GetAPIKeyByID(ctx, split[0])
require.NoError(t, err, "fetch api key")
err = api.Database.UpdateAPIKeyByID(ctx, database.UpdateAPIKeyByIDParams{
ID: apiKey.ID,
LastUsed: apiKey.LastUsed,
IPAddress: apiKey.IPAddress,
// This should cause a refresh
ExpiresAt: apiKey.ExpiresAt.Add(time.Hour * -2),
OAuthAccessToken: apiKey.OAuthAccessToken,
OAuthRefreshToken: apiKey.OAuthRefreshToken,
OAuthExpiry: apiKey.OAuthExpiry,
})
require.NoError(t, err, "update api key")
_, err = client.User(ctx, codersdk.Me)
require.NoError(t, err, "fetch user")
apiKey, err = api.Database.GetAPIKeyByID(ctx, split[0])
require.NoError(t, err, "fetch refreshed api key")
// 1 minute tolerance
require.True(t, apiKey.ExpiresAt.After(time.Now().Add(time.Hour*24*7).Add(time.Minute*-1)), "api key lasts 7 days")
})
t.Run("LoginKeyExtend", func(t *testing.T) {
t.Parallel()
var (
ctx = context.Background()
)
client, api := coderdtest.NewWithAPI(t, nil)
_ = coderdtest.CreateFirstUser(t, client)
split := strings.Split(client.SessionToken, "-")
apiKey, err := api.Database.GetAPIKeyByID(ctx, split[0])
require.NoError(t, err, "fetch login key")
err = api.Database.UpdateAPIKeyByID(ctx, database.UpdateAPIKeyByIDParams{
ID: apiKey.ID,
LastUsed: apiKey.LastUsed,
IPAddress: apiKey.IPAddress,
// This should cause a refresh
ExpiresAt: apiKey.ExpiresAt.Add(time.Hour * -2),
OAuthAccessToken: apiKey.OAuthAccessToken,
OAuthRefreshToken: apiKey.OAuthRefreshToken,
OAuthExpiry: apiKey.OAuthExpiry,
})
require.NoError(t, err, "update login key")
_, err = client.User(ctx, codersdk.Me)
require.NoError(t, err, "fetch user")
apiKey, err = api.Database.GetAPIKeyByID(ctx, split[0])
require.NoError(t, err, "fetch refreshed login key")
// 1 minute tolerance
require.True(t, apiKey.ExpiresAt.After(time.Now().Add(time.Hour*24).Add(time.Minute*-1)), "login key lasts 24 hrs")
require.True(t, apiKey.ExpiresAt.After(key.ExpiresAt.Add(time.Hour)), "api key should be longer expires")
require.Greater(t, apiKey.LifetimeSeconds, key.LifetimeSeconds, "api key should have longer lifetime")
})
}
@ -237,11 +164,11 @@ func TestPostLogout(t *testing.T) {
t.Parallel()
ctx := context.Background()
client, api := coderdtest.NewWithAPI(t, nil)
_ = coderdtest.CreateFirstUser(t, client)
client := coderdtest.New(t, nil)
admin := coderdtest.CreateFirstUser(t, client)
keyID := strings.Split(client.SessionToken, "-")[0]
apiKey, err := api.Database.GetAPIKeyByID(ctx, keyID)
apiKey, err := client.GetAPIKey(ctx, admin.UserID.String(), keyID)
require.NoError(t, err)
require.Equal(t, keyID, apiKey.ID, "API key should exist in the database")
@ -259,44 +186,10 @@ func TestPostLogout(t *testing.T) {
require.Equal(t, httpmw.SessionTokenKey, cookies[0].Name, "Cookie should be the auth cookie")
require.Equal(t, -1, cookies[0].MaxAge, "Cookie should be set to delete")
apiKey, err = api.Database.GetAPIKeyByID(ctx, keyID)
require.ErrorIs(t, err, sql.ErrNoRows, "API key should not exist in the database")
})
t.Run("LogoutWithoutKey", func(t *testing.T) {
t.Parallel()
ctx := context.Background()
client, api := coderdtest.NewWithAPI(t, nil)
_ = coderdtest.CreateFirstUser(t, client)
keyID := strings.Split(client.SessionToken, "-")[0]
apiKey, err := api.Database.GetAPIKeyByID(ctx, keyID)
require.NoError(t, err)
require.Equal(t, keyID, apiKey.ID, "API key should exist in the database")
// Setting a fake database without the API Key to be used by the API.
// The middleware that extracts the API key is already set to read
// from the original database.
dbWithoutKey := databasefake.New()
api.Database = dbWithoutKey
fullURL, err := client.URL.Parse("/api/v2/users/logout")
require.NoError(t, err, "Server URL should parse successfully")
res, err := client.Request(ctx, http.MethodPost, fullURL.String(), nil)
require.NoError(t, err, "/logout request should succeed")
res.Body.Close()
require.Equal(t, http.StatusInternalServerError, res.StatusCode)
cookies := res.Cookies()
require.Len(t, cookies, 1, "Exactly one cookie should be returned")
require.Equal(t, httpmw.SessionTokenKey, cookies[0].Name, "Cookie should be the auth cookie")
require.Equal(t, -1, cookies[0].MaxAge, "Cookie should be set to delete")
apiKey, err = api.Database.GetAPIKeyByID(ctx, keyID)
require.ErrorIs(t, err, sql.ErrNoRows, "API key should not exist in the database")
_, err = client.GetAPIKey(ctx, admin.UserID.String(), keyID)
var sdkErr = &codersdk.Error{}
require.ErrorAs(t, err, &sdkErr)
require.Equal(t, http.StatusUnauthorized, sdkErr.StatusCode(), "Expecting 401")
})
}

View File

@ -27,9 +27,10 @@ func TestWorkspaceAgent(t *testing.T) {
t.Parallel()
t.Run("Connect", func(t *testing.T) {
t.Parallel()
client, coderAPI := coderdtest.NewWithAPI(t, nil)
client := coderdtest.New(t, &coderdtest.Options{
IncludeProvisionerD: true,
})
user := coderdtest.CreateFirstUser(t, client)
daemonCloser := coderdtest.NewProvisionerDaemon(t, coderAPI)
authToken := uuid.NewString()
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete,
@ -56,7 +57,6 @@ func TestWorkspaceAgent(t *testing.T) {
coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID)
coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID)
daemonCloser.Close()
resources, err := client.WorkspaceResourcesByBuild(context.Background(), workspace.LatestBuild.ID)
require.NoError(t, err)
@ -72,9 +72,10 @@ func TestWorkspaceAgentListen(t *testing.T) {
t.Run("Connect", func(t *testing.T) {
t.Parallel()
client, coderAPI := coderdtest.NewWithAPI(t, nil)
client := coderdtest.New(t, &coderdtest.Options{
IncludeProvisionerD: true,
})
user := coderdtest.CreateFirstUser(t, client)
daemonCloser := coderdtest.NewProvisionerDaemon(t, coderAPI)
authToken := uuid.NewString()
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete,
@ -100,7 +101,6 @@ func TestWorkspaceAgentListen(t *testing.T) {
coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID)
coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID)
daemonCloser.Close()
agentClient := codersdk.New(client.URL)
agentClient.SessionToken = authToken
@ -124,11 +124,11 @@ func TestWorkspaceAgentListen(t *testing.T) {
t.Parallel()
ctx := context.Background()
client, coderAPI := coderdtest.NewWithAPI(t, nil)
user := coderdtest.CreateFirstUser(t, client)
daemonCloser := coderdtest.NewProvisionerDaemon(t, coderAPI)
defer daemonCloser.Close()
client := coderdtest.New(t, &coderdtest.Options{
IncludeProvisionerD: true,
})
user := coderdtest.CreateFirstUser(t, client)
authToken := uuid.NewString()
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete,
@ -196,9 +196,11 @@ func TestWorkspaceAgentListen(t *testing.T) {
func TestWorkspaceAgentTURN(t *testing.T) {
t.Parallel()
client, coderAPI := coderdtest.NewWithAPI(t, nil)
client := coderdtest.New(t, &coderdtest.Options{
IncludeProvisionerD: true,
})
user := coderdtest.CreateFirstUser(t, client)
daemonCloser := coderdtest.NewProvisionerDaemon(t, coderAPI)
authToken := uuid.NewString()
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete,
@ -224,7 +226,6 @@ func TestWorkspaceAgentTURN(t *testing.T) {
coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID)
coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID)
daemonCloser.Close()
agentClient := codersdk.New(client.URL)
agentClient.SessionToken = authToken
@ -257,9 +258,10 @@ func TestWorkspaceAgentPTY(t *testing.T) {
// it seems like it could be either.
t.Skip("ConPTY appears to be inconsistent on Windows.")
}
client, coderAPI := coderdtest.NewWithAPI(t, nil)
client := coderdtest.New(t, &coderdtest.Options{
IncludeProvisionerD: true,
})
user := coderdtest.CreateFirstUser(t, client)
daemonCloser := coderdtest.NewProvisionerDaemon(t, coderAPI)
authToken := uuid.NewString()
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete,
@ -285,7 +287,6 @@ func TestWorkspaceAgentPTY(t *testing.T) {
coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID)
coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID)
daemonCloser.Close()
agentClient := codersdk.New(client.URL)
agentClient.SessionToken = authToken

View File

@ -36,9 +36,10 @@ func TestWorkspaceAppsProxyPath(t *testing.T) {
go server.Serve(ln)
tcpAddr, _ := ln.Addr().(*net.TCPAddr)
client, coderAPI := coderdtest.NewWithAPI(t, nil)
client := coderdtest.New(t, &coderdtest.Options{
IncludeProvisionerD: true,
})
user := coderdtest.CreateFirstUser(t, client)
coderdtest.NewProvisionerDaemon(t, coderAPI)
authToken := uuid.NewString()
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete,

View File

@ -237,12 +237,12 @@ func TestWorkspaceBuildResources(t *testing.T) {
t.Parallel()
t.Run("ListRunning", func(t *testing.T) {
t.Parallel()
client, coderAPI := coderdtest.NewWithAPI(t, nil)
client := coderdtest.New(t, &coderdtest.Options{
IncludeProvisionerD: true,
})
user := coderdtest.CreateFirstUser(t, client)
closeDaemon := coderdtest.NewProvisionerDaemon(t, coderAPI)
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
closeDaemon.Close()
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID)
_, err := client.WorkspaceResourcesByBuild(context.Background(), workspace.LatestBuild.ID)

View File

@ -46,9 +46,10 @@ func TestWorkspaceResource(t *testing.T) {
t.Run("Apps", func(t *testing.T) {
t.Parallel()
client, coderd := coderdtest.NewWithAPI(t, nil)
client := coderdtest.New(t, &coderdtest.Options{
IncludeProvisionerD: true,
})
user := coderdtest.CreateFirstUser(t, client)
coderdtest.NewProvisionerDaemon(t, coderd)
app := &proto.App{
Name: "code-server",
Command: "some-command",

View File

@ -656,14 +656,15 @@ func TestPostWorkspaceBuild(t *testing.T) {
t.Run("AlreadyActive", func(t *testing.T) {
t.Parallel()
client, coderAPI := coderdtest.NewWithAPI(t, nil)
client, closer := coderdtest.NewWithProvisionerCloser(t, nil)
defer closer.Close()
user := coderdtest.CreateFirstUser(t, client)
closeDaemon := coderdtest.NewProvisionerDaemon(t, coderAPI)
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
closer.Close()
// Close here so workspace build doesn't process!
closeDaemon.Close()
workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID)
_, err := client.CreateWorkspaceBuild(context.Background(), workspace.ID, codersdk.CreateWorkspaceBuildRequest{
TemplateVersionID: template.ActiveVersionID,
@ -694,15 +695,15 @@ func TestPostWorkspaceBuild(t *testing.T) {
t.Run("WithState", func(t *testing.T) {
t.Parallel()
client, coderAPI := coderdtest.NewWithAPI(t, nil)
client := coderdtest.New(t, &coderdtest.Options{
IncludeProvisionerD: true,
})
user := coderdtest.CreateFirstUser(t, client)
closeDaemon := coderdtest.NewProvisionerDaemon(t, coderAPI)
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID)
coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID)
_ = closeDaemon.Close()
wantState := []byte("something")
build, err := client.CreateWorkspaceBuild(context.Background(), workspace.ID, codersdk.CreateWorkspaceBuildRequest{
TemplateVersionID: template.ActiveVersionID,

View File

@ -22,6 +22,13 @@ const (
UserStatusSuspended UserStatus = "suspended"
)
type LoginType string
const (
LoginTypePassword LoginType = "password"
LoginTypeGithub LoginType = "github"
)
type UsersRequest struct {
Search string `json:"search,omitempty" typescript:"-"`
// Filter users by status.
@ -44,6 +51,17 @@ type User struct {
Roles []Role `json:"roles"`
}
type APIKey struct {
ID string `json:"id" validate:"required"`
UserID uuid.UUID `json:"user_id" validate:"required"`
LastUsed time.Time `json:"last_used" validate:"required"`
ExpiresAt time.Time `json:"expires_at" validate:"required"`
CreatedAt time.Time `json:"created_at" validate:"required"`
UpdatedAt time.Time `json:"updated_at" validate:"required"`
LoginType LoginType `json:"login_type" validate:"required"`
LifetimeSeconds int64 `json:"lifetime_seconds" validate:"required"`
}
type CreateFirstUserRequest struct {
Email string `json:"email" validate:"required,email"`
Username string `json:"username" validate:"required,username"`
@ -314,6 +332,19 @@ func (c *Client) CreateAPIKey(ctx context.Context, user string) (*GenerateAPIKey
return apiKey, json.NewDecoder(res.Body).Decode(apiKey)
}
func (c *Client) GetAPIKey(ctx context.Context, user string, id string) (*APIKey, error) {
res, err := c.Request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/users/%s/keys/%s", user, id), nil)
if err != nil {
return nil, err
}
defer res.Body.Close()
if res.StatusCode > http.StatusCreated {
return nil, readBodyAsError(res)
}
apiKey := &APIKey{}
return apiKey, json.NewDecoder(res.Body).Decode(apiKey)
}
// LoginWithPassword creates a session token authenticating with an email and password.
// Call `SetSessionToken()` to apply the newly acquired token to the client.
func (c *Client) LoginWithPassword(ctx context.Context, req LoginWithPasswordRequest) (LoginWithPasswordResponse, error) {

View File

@ -6,6 +6,7 @@ import (
"context"
"fmt"
"path/filepath"
"strings"
"golang.org/x/xerrors"
protobuf "google.golang.org/protobuf/proto"
@ -16,6 +17,25 @@ import (
"github.com/coder/coder/provisionersdk/proto"
)
const (
ParameterExecKey = "echo.exec"
errorKey = "error"
successKey = "success"
)
func ParameterError(s string) string {
return formatExecValue(errorKey, s)
}
func ParameterSucceed() string {
return formatExecValue(successKey, "")
}
func formatExecValue(key, value string) string {
return fmt.Sprintf("%s=%s", key, value)
}
var (
// ParseComplete is a helper to indicate an empty parse completion.
ParseComplete = []*proto.Parse_Response{{
@ -29,6 +49,21 @@ var (
Complete: &proto.Provision_Complete{},
},
}}
ParameterSuccess = []*proto.ParameterSchema{
{
AllowOverrideSource: true,
Name: ParameterExecKey,
Description: "description 1",
DefaultSource: &proto.ParameterSource{
Scheme: proto.ParameterSource_DATA,
Value: formatExecValue(successKey, ""),
},
DefaultDestination: &proto.ParameterDestination{
Scheme: proto.ParameterDestination_PROVISIONER_VARIABLE,
},
},
}
)
// Serve starts the echo provisioner.
@ -86,6 +121,23 @@ func (e *echo) Provision(stream proto.DRPCProvisioner_ProvisionStream) error {
// A cancel could occur here!
return nil
}
for _, param := range request.ParameterValues {
if param.Name == ParameterExecKey {
toks := strings.Split(param.Value, "=")
if len(toks) < 2 {
break
}
switch toks[0] {
case errorKey:
return xerrors.Errorf("returning error: %v", toks[1])
default:
// Do nothing
}
}
}
for index := 0; ; index++ {
extension := ".protobuf"
if request.DryRun {

View File

@ -1,5 +1,17 @@
// Code generated by 'make coder/scripts/apitypings/main.go'. DO NOT EDIT.
// From codersdk/users.go:54:6
export interface APIKey {
readonly id: string
readonly user_id: string
readonly last_used: string
readonly expires_at: string
readonly created_at: string
readonly updated_at: string
readonly login_type: LoginType
readonly lifetime_seconds: number
}
// From codersdk/workspaceagents.go:36:6
export interface AWSInstanceIdentityToken {
readonly signature: string
@ -12,7 +24,7 @@ export interface AgentGitSSHKey {
readonly private_key: string
}
// From codersdk/users.go:156:6
// From codersdk/users.go:174:6
export interface AuthMethods {
readonly password: boolean
readonly github: boolean
@ -37,7 +49,7 @@ export interface ComputedParameter extends Parameter {
readonly default_source_value: boolean
}
// From codersdk/users.go:47:6
// From codersdk/users.go:65:6
export interface CreateFirstUserRequest {
readonly email: string
readonly username: string
@ -45,13 +57,13 @@ export interface CreateFirstUserRequest {
readonly organization: string
}
// From codersdk/users.go:55:6
// From codersdk/users.go:73:6
export interface CreateFirstUserResponse {
readonly user_id: string
readonly organization_id: string
}
// From codersdk/users.go:151:6
// From codersdk/users.go:169:6
export interface CreateOrganizationRequest {
readonly name: string
}
@ -90,7 +102,7 @@ export interface CreateTemplateVersionRequest {
readonly parameter_values?: CreateParameterRequest[]
}
// From codersdk/users.go:60:6
// From codersdk/users.go:78:6
export interface CreateUserRequest {
readonly email: string
readonly username: string
@ -116,7 +128,7 @@ export interface CreateWorkspaceRequest {
readonly parameter_values?: CreateParameterRequest[]
}
// From codersdk/users.go:147:6
// From codersdk/users.go:165:6
export interface GenerateAPIKeyResponse {
readonly key: string
}
@ -134,13 +146,13 @@ export interface GoogleInstanceIdentityToken {
readonly json_web_token: string
}
// From codersdk/users.go:136:6
// From codersdk/users.go:154:6
export interface LoginWithPasswordRequest {
readonly email: string
readonly password: string
}
// From codersdk/users.go:142:6
// From codersdk/users.go:160:6
export interface LoginWithPasswordResponse {
readonly session_token: string
}
@ -283,7 +295,7 @@ export interface UpdateActiveTemplateVersion {
readonly id: string
}
// From codersdk/users.go:76:6
// From codersdk/users.go:94:6
export interface UpdateRoles {
readonly roles: string[]
}
@ -295,13 +307,13 @@ export interface UpdateTemplateMeta {
readonly min_autostart_interval_ms?: number
}
// From codersdk/users.go:71:6
// From codersdk/users.go:89:6
export interface UpdateUserPasswordRequest {
readonly old_password: string
readonly password: string
}
// From codersdk/users.go:67:6
// From codersdk/users.go:85:6
export interface UpdateUserProfileRequest {
readonly username: string
}
@ -321,7 +333,7 @@ export interface UploadResponse {
readonly hash: string
}
// From codersdk/users.go:37:6
// From codersdk/users.go:44:6
export interface User {
readonly id: string
readonly email: string
@ -332,13 +344,13 @@ export interface User {
readonly roles: Role[]
}
// From codersdk/users.go:101:6
// From codersdk/users.go:119:6
export interface UserAuthorization {
readonly object: UserAuthorizationObject
readonly action: string
}
// From codersdk/users.go:117:6
// From codersdk/users.go:135:6
export interface UserAuthorizationObject {
readonly resource_type: string
readonly owner_id?: string
@ -346,21 +358,21 @@ export interface UserAuthorizationObject {
readonly resource_id?: string
}
// From codersdk/users.go:90:6
// From codersdk/users.go:108:6
export interface UserAuthorizationRequest {
readonly checks: Record<string, UserAuthorization>
}
// From codersdk/users.go:85:6
// From codersdk/users.go:103:6
export type UserAuthorizationResponse = Record<string, boolean>
// From codersdk/users.go:80:6
// From codersdk/users.go:98:6
export interface UserRoles {
readonly roles: string[]
readonly organization_roles: Record<string, string[]>
}
// From codersdk/users.go:25:6
// From codersdk/users.go:32:6
export interface UsersRequest extends Pagination {
readonly q?: string
}
@ -500,6 +512,9 @@ export type LogLevel = "debug" | "error" | "info" | "trace" | "warn"
// From codersdk/provisionerdaemons.go:21:6
export type LogSource = "provisioner" | "provisioner_daemon"
// From codersdk/users.go:25:6
export type LoginType = "github" | "password"
// From codersdk/parameters.go:29:6
export type ParameterDestinationScheme = "environment_variable" | "none" | "provisioner_variable"