chore: Add test helpers to improve coverage (#166)

* chore: Rename ProjectHistory to ProjectVersion

Version more accurately represents version storage. This
forks from the WorkspaceHistory name, but I think it's
easier to understand Workspace history.

* Rename files

* Standardize tests a bit more

* Remove Server struct from coderdtest

* Improve test coverage for workspace history

* Fix linting errors

* Fix coderd test leak

* Fix coderd test leak

* Improve workspace history logs

* Standardize test structure for codersdk

* Fix linting errors

* Fix WebSocket compression

* Update coderd/workspaces.go

Co-authored-by: Bryan <bryan@coder.com>

* Add test for listing project parameters

* Cache npm dependencies with setup node

* Remove windows npm cache key

Co-authored-by: Bryan <bryan@coder.com>
This commit is contained in:
Kyle Carberry 2022-02-05 18:24:51 -06:00 committed by GitHub
parent f19770b2c6
commit 1796dc6c2f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
38 changed files with 1575 additions and 1166 deletions

View File

@ -165,12 +165,13 @@ jobs:
-covermode=atomic -coverprofile="gotests.coverage" -timeout=3m
-count=1 -race -parallel=2
- uses: actions/setup-node@v2
- name: Setup Node for DataDog CLI
uses: actions/setup-node@v2
if: always() && github.actor != 'dependabot[bot]'
with:
node-version: "14"
- name: Cache DataDog CI
- name: Cache DataDog CLI
if: always() && github.actor != 'dependabot[bot]'
uses: actions/cache@v2
with:

View File

@ -100,6 +100,10 @@ linters-settings:
# - whyNoLint
# - wrapperFunc
# - yodaStyleExpr
settings:
ruleguard:
failOn: all
rules: rules.go
goimports:
local-prefixes: coder.com,cdr.dev,go.coder.com,github.com/cdr,github.com/coder

View File

@ -86,7 +86,6 @@ func New(options *Options) http.Handler {
r.Get("/", api.workspaces)
r.Route("/{user}", func(r chi.Router) {
r.Use(httpmw.ExtractUserParam(options.Database))
r.Get("/", api.workspaces)
r.Post("/", api.postWorkspaceByUser)
r.Route("/{workspace}", func(r chi.Router) {
r.Use(httpmw.ExtractWorkspaceParam(options.Database))

11
coderd/coderd_test.go Normal file
View File

@ -0,0 +1,11 @@
package coderd_test
import (
"testing"
"go.uber.org/goleak"
)
func TestMain(m *testing.M) {
goleak.VerifyTestMain(m)
}

View File

@ -7,16 +7,18 @@ import (
"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/cryptorand"
"github.com/coder/coder/database"
"github.com/coder/coder/database/databasefake"
"github.com/coder/coder/database/postgres"
@ -26,79 +28,9 @@ import (
"github.com/coder/coder/provisionersdk/proto"
)
// Server represents a test instance of coderd.
// The database is intentionally omitted from
// this struct to promote data being exposed via
// the API.
type Server struct {
Client *codersdk.Client
URL *url.URL
}
// RandomInitialUser generates a random initial user and authenticates
// it with the client on the Server struct.
func (s *Server) RandomInitialUser(t *testing.T) coderd.CreateInitialUserRequest {
username, err := cryptorand.String(12)
require.NoError(t, err)
password, err := cryptorand.String(12)
require.NoError(t, err)
organization, err := cryptorand.String(12)
require.NoError(t, err)
req := coderd.CreateInitialUserRequest{
Email: "testuser@coder.com",
Username: username,
Password: password,
Organization: organization,
}
_, err = s.Client.CreateInitialUser(context.Background(), req)
require.NoError(t, err)
login, err := s.Client.LoginWithPassword(context.Background(), coderd.LoginWithPasswordRequest{
Email: "testuser@coder.com",
Password: password,
})
require.NoError(t, err)
err = s.Client.SetSessionToken(login.SessionToken)
require.NoError(t, err)
return req
}
// AddProvisionerd launches a new provisionerd instance with the
// test provisioner registered.
func (s *Server) AddProvisionerd(t *testing.T) 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(s.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
}
// New constructs a new coderd test instance. This returned Server
// should contain no side-effects.
func New(t *testing.T) Server {
func New(t *testing.T) *codersdk.Client {
// This can be hotswapped for a live database instance.
db := databasefake.New()
pubsub := database.NewPubsubInMemory()
@ -132,8 +64,123 @@ func New(t *testing.T) Server {
require.NoError(t, err)
t.Cleanup(srv.Close)
return Server{
Client: codersdk.New(serverURL),
URL: serverURL,
}
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)
err = client.SetSessionToken(login.SessionToken)
require.NoError(t, err)
return req
}
// 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) coderd.Project {
project, err := client.CreateProject(context.Background(), organization, coderd.CreateProjectRequest{
Name: randomUsername(),
Provisioner: database.ProvisionerTypeEcho,
})
require.NoError(t, err)
return project
}
// CreateProjectVersion creates a project version for the "echo" provisioner
// for compatibility with testing.
func CreateProjectVersion(t *testing.T, client *codersdk.Client, organization, project string, responses *echo.Responses) coderd.ProjectVersion {
data, err := echo.Tar(responses)
require.NoError(t, err)
version, err := client.CreateProjectVersion(context.Background(), organization, project, coderd.CreateProjectVersionRequest{
StorageMethod: database.ProjectStorageMethodInlineArchive,
StorageSource: data,
})
require.NoError(t, err)
return version
}
// AwaitProjectVersionImported awaits for the project import job to reach completed status.
func AwaitProjectVersionImported(t *testing.T, client *codersdk.Client, organization, project, version string) coderd.ProjectVersion {
var projectVersion coderd.ProjectVersion
require.Eventually(t, func() bool {
var err error
projectVersion, err = client.ProjectVersion(context.Background(), organization, project, version)
require.NoError(t, err)
return projectVersion.Import.Status.Completed()
}, 3*time.Second, 25*time.Millisecond)
return projectVersion
}
// 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
}
// AwaitWorkspaceHistoryProvisioned awaits for the workspace provision job to reach completed status.
func AwaitWorkspaceHistoryProvisioned(t *testing.T, client *codersdk.Client, user, workspace, history string) coderd.WorkspaceHistory {
var workspaceHistory coderd.WorkspaceHistory
require.Eventually(t, func() bool {
var err error
workspaceHistory, err = client.WorkspaceHistory(context.Background(), user, workspace, history)
require.NoError(t, err)
return workspaceHistory.Provision.Status.Completed()
}, 3*time.Second, 25*time.Millisecond)
return workspaceHistory
}
func randomUsername() string {
return strings.ReplaceAll(namesgenerator.GetRandomName(0), "_", "-")
}

View File

@ -1,11 +1,16 @@
package coderdtest_test
import (
"context"
"testing"
"go.uber.org/goleak"
"github.com/stretchr/testify/require"
"github.com/coder/coder/coderd"
"github.com/coder/coder/coderd/coderdtest"
"github.com/coder/coder/database"
)
func TestMain(m *testing.M) {
@ -14,7 +19,18 @@ func TestMain(m *testing.M) {
func TestNew(t *testing.T) {
t.Parallel()
server := coderdtest.New(t)
_ = server.RandomInitialUser(t)
_ = server.AddProvisionerd(t)
client := coderdtest.New(t)
user := coderdtest.CreateInitialUser(t, client)
closer := coderdtest.NewProvisionerDaemon(t, client)
project := coderdtest.CreateProject(t, client, user.Organization)
version := coderdtest.CreateProjectVersion(t, client, user.Organization, project.Name, nil)
coderdtest.AwaitProjectVersionImported(t, client, user.Organization, project.Name, version.Name)
workspace := coderdtest.CreateWorkspace(t, client, "me", project.ID)
history, err := client.CreateWorkspaceHistory(context.Background(), "me", workspace.Name, coderd.CreateWorkspaceHistoryRequest{
ProjectVersionID: version.ID,
Transition: database.WorkspaceTransitionCreate,
})
require.NoError(t, err)
coderdtest.AwaitWorkspaceHistoryProvisioned(t, client, "me", workspace.Name, history.Name)
closer.Close()
}

View File

@ -49,6 +49,9 @@ func (api *api) projects(rw http.ResponseWriter, r *http.Request) {
})
return
}
if projects == nil {
projects = []database.Project{}
}
render.Status(r, http.StatusOK)
render.JSON(rw, r, projects)
}
@ -66,6 +69,9 @@ func (api *api) projectsByOrganization(rw http.ResponseWriter, r *http.Request)
})
return
}
if projects == nil {
projects = []database.Project{}
}
render.Status(r, http.StatusOK)
render.JSON(rw, r, projects)
}
@ -124,32 +130,6 @@ func (*api) projectByOrganization(rw http.ResponseWriter, r *http.Request) {
render.JSON(rw, r, project)
}
// Returns all workspaces for a specific project.
func (api *api) workspacesByProject(rw http.ResponseWriter, r *http.Request) {
apiKey := httpmw.APIKey(r)
project := httpmw.ProjectParam(r)
workspaces, err := api.Database.GetWorkspacesByProjectAndUserID(r.Context(), database.GetWorkspacesByProjectAndUserIDParams{
OwnerID: apiKey.UserID,
ProjectID: project.ID,
})
if errors.Is(err, sql.ErrNoRows) {
err = nil
}
if err != nil {
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
Message: fmt.Sprintf("get workspaces: %s", err),
})
return
}
apiWorkspaces := make([]Workspace, 0, len(workspaces))
for _, workspace := range workspaces {
apiWorkspaces = append(apiWorkspaces, convertWorkspace(workspace))
}
render.Status(r, http.StatusOK)
render.JSON(rw, r, apiWorkspaces)
}
// Creates parameters for a project.
// This should validate the calling user has permissions!
func (api *api) postParametersByProject(rw http.ResponseWriter, r *http.Request) {

View File

@ -2,124 +2,109 @@ package coderd_test
import (
"context"
"net/http"
"testing"
"time"
"github.com/stretchr/testify/require"
"github.com/coder/coder/coderd"
"github.com/coder/coder/coderd/coderdtest"
"github.com/coder/coder/codersdk"
"github.com/coder/coder/database"
"github.com/coder/coder/provisioner/echo"
"github.com/coder/coder/provisionersdk/proto"
)
func TestProjects(t *testing.T) {
t.Parallel()
t.Run("Create", func(t *testing.T) {
t.Parallel()
server := coderdtest.New(t)
user := server.RandomInitialUser(t)
_, err := server.Client.CreateProject(context.Background(), user.Organization, coderd.CreateProjectRequest{
Name: "someproject",
Provisioner: database.ProvisionerTypeEcho,
})
require.NoError(t, err)
})
t.Run("AlreadyExists", func(t *testing.T) {
t.Parallel()
server := coderdtest.New(t)
user := server.RandomInitialUser(t)
_, err := server.Client.CreateProject(context.Background(), user.Organization, coderd.CreateProjectRequest{
Name: "someproject",
Provisioner: database.ProvisionerTypeEcho,
})
require.NoError(t, err)
_, err = server.Client.CreateProject(context.Background(), user.Organization, coderd.CreateProjectRequest{
Name: "someproject",
Provisioner: database.ProvisionerTypeEcho,
})
require.Error(t, err)
})
t.Run("ListEmpty", func(t *testing.T) {
t.Parallel()
server := coderdtest.New(t)
_ = server.RandomInitialUser(t)
projects, err := server.Client.Projects(context.Background(), "")
client := coderdtest.New(t)
_ = coderdtest.CreateInitialUser(t, client)
projects, err := client.Projects(context.Background(), "")
require.NoError(t, err)
require.NotNil(t, projects)
require.Len(t, projects, 0)
})
t.Run("List", func(t *testing.T) {
t.Parallel()
server := coderdtest.New(t)
user := server.RandomInitialUser(t)
_, err := server.Client.CreateProject(context.Background(), user.Organization, coderd.CreateProjectRequest{
Name: "someproject",
Provisioner: database.ProvisionerTypeEcho,
})
require.NoError(t, err)
// Ensure global query works.
projects, err := server.Client.Projects(context.Background(), "")
require.NoError(t, err)
require.Len(t, projects, 1)
// Ensure specified query works.
projects, err = server.Client.Projects(context.Background(), user.Organization)
client := coderdtest.New(t)
user := coderdtest.CreateInitialUser(t, client)
_ = coderdtest.CreateProject(t, client, user.Organization)
projects, err := client.Projects(context.Background(), "")
require.NoError(t, err)
require.Len(t, projects, 1)
})
}
func TestProjectsByOrganization(t *testing.T) {
t.Parallel()
t.Run("ListEmpty", func(t *testing.T) {
t.Parallel()
server := coderdtest.New(t)
user := server.RandomInitialUser(t)
projects, err := server.Client.Projects(context.Background(), user.Organization)
client := coderdtest.New(t)
user := coderdtest.CreateInitialUser(t, client)
projects, err := client.Projects(context.Background(), user.Organization)
require.NoError(t, err)
require.NotNil(t, projects)
require.Len(t, projects, 0)
})
t.Run("Single", func(t *testing.T) {
t.Run("List", func(t *testing.T) {
t.Parallel()
server := coderdtest.New(t)
user := server.RandomInitialUser(t)
project, err := server.Client.CreateProject(context.Background(), user.Organization, coderd.CreateProjectRequest{
Name: "someproject",
Provisioner: database.ProvisionerTypeEcho,
})
require.NoError(t, err)
_, err = server.Client.Project(context.Background(), user.Organization, project.Name)
client := coderdtest.New(t)
user := coderdtest.CreateInitialUser(t, client)
_ = coderdtest.CreateProject(t, client, user.Organization)
projects, err := client.Projects(context.Background(), "")
require.NoError(t, err)
require.Len(t, projects, 1)
})
}
func TestPostProjectsByOrganization(t *testing.T) {
t.Parallel()
t.Run("Create", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t)
user := coderdtest.CreateInitialUser(t, client)
_ = coderdtest.CreateProject(t, client, user.Organization)
})
t.Run("Parameters", func(t *testing.T) {
t.Run("AlreadyExists", func(t *testing.T) {
t.Parallel()
server := coderdtest.New(t)
user := server.RandomInitialUser(t)
project, err := server.Client.CreateProject(context.Background(), user.Organization, coderd.CreateProjectRequest{
Name: "someproject",
client := coderdtest.New(t)
user := coderdtest.CreateInitialUser(t, client)
project := coderdtest.CreateProject(t, client, user.Organization)
_, err := client.CreateProject(context.Background(), user.Organization, coderd.CreateProjectRequest{
Name: project.Name,
Provisioner: database.ProvisionerTypeEcho,
})
require.NoError(t, err)
_, err = server.Client.ProjectParameters(context.Background(), user.Organization, project.Name)
var apiErr *codersdk.Error
require.ErrorAs(t, err, &apiErr)
require.Equal(t, http.StatusConflict, apiErr.StatusCode())
})
}
func TestProjectByOrganization(t *testing.T) {
t.Parallel()
t.Run("Get", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t)
user := coderdtest.CreateInitialUser(t, client)
project := coderdtest.CreateProject(t, client, user.Organization)
_, err := client.Project(context.Background(), user.Organization, project.Name)
require.NoError(t, err)
})
}
t.Run("CreateParameter", func(t *testing.T) {
func TestPostParametersByProject(t *testing.T) {
t.Parallel()
t.Run("Create", func(t *testing.T) {
t.Parallel()
server := coderdtest.New(t)
user := server.RandomInitialUser(t)
project, err := server.Client.CreateProject(context.Background(), user.Organization, coderd.CreateProjectRequest{
Name: "someproject",
Provisioner: database.ProvisionerTypeEcho,
})
require.NoError(t, err)
_, err = server.Client.CreateProjectParameter(context.Background(), user.Organization, project.Name, coderd.CreateParameterValueRequest{
Name: "hi",
client := coderdtest.New(t)
user := coderdtest.CreateInitialUser(t, client)
project := coderdtest.CreateProject(t, client, user.Organization)
_, err := client.CreateProjectParameter(context.Background(), user.Organization, project.Name, coderd.CreateParameterValueRequest{
Name: "somename",
SourceValue: "tomato",
SourceScheme: database.ParameterSourceSchemeData,
DestinationScheme: database.ParameterDestinationSchemeEnvironmentVariable,
@ -127,40 +112,36 @@ func TestProjects(t *testing.T) {
})
require.NoError(t, err)
})
}
t.Run("Import", func(t *testing.T) {
func TestParametersByProject(t *testing.T) {
t.Parallel()
t.Run("ListEmpty", func(t *testing.T) {
t.Parallel()
server := coderdtest.New(t)
user := server.RandomInitialUser(t)
_ = server.AddProvisionerd(t)
project, err := server.Client.CreateProject(context.Background(), user.Organization, coderd.CreateProjectRequest{
Name: "someproject",
Provisioner: database.ProvisionerTypeEcho,
client := coderdtest.New(t)
user := coderdtest.CreateInitialUser(t, client)
project := coderdtest.CreateProject(t, client, user.Organization)
params, err := client.ProjectParameters(context.Background(), user.Organization, project.Name)
require.NoError(t, err)
require.NotNil(t, params)
})
t.Run("List", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t)
user := coderdtest.CreateInitialUser(t, client)
project := coderdtest.CreateProject(t, client, user.Organization)
_, err := client.CreateProjectParameter(context.Background(), user.Organization, project.Name, coderd.CreateParameterValueRequest{
Name: "example",
SourceValue: "source-value",
SourceScheme: database.ParameterSourceSchemeData,
DestinationScheme: database.ParameterDestinationSchemeEnvironmentVariable,
DestinationValue: "destination-value",
})
require.NoError(t, err)
data, err := echo.Tar([]*proto.Parse_Response{{
Type: &proto.Parse_Response_Complete{
Complete: &proto.Parse_Complete{
ParameterSchemas: []*proto.ParameterSchema{{
Name: "example",
}},
},
},
}}, nil)
require.NoError(t, err)
version, err := server.Client.CreateProjectVersion(context.Background(), user.Organization, project.Name, coderd.CreateProjectVersionRequest{
StorageMethod: database.ProjectStorageMethodInlineArchive,
StorageSource: data,
})
require.NoError(t, err)
require.Eventually(t, func() bool {
projectVersion, err := server.Client.ProjectVersion(context.Background(), user.Organization, project.Name, version.Name)
require.NoError(t, err)
return projectVersion.Import.Status.Completed()
}, 15*time.Second, 10*time.Millisecond)
params, err := server.Client.ProjectVersionParameters(context.Background(), user.Organization, project.Name, version.Name)
params, err := client.ProjectParameters(context.Background(), user.Organization, project.Name)
require.NoError(t, err)
require.NotNil(t, params)
require.Len(t, params, 1)
require.Equal(t, "example", params[0].Name)
})
}

View File

@ -110,19 +110,11 @@ func (api *api) postProjectVersionByOrganization(rw http.ResponseWriter, r *http
return
}
switch createProjectVersion.StorageMethod {
case database.ProjectStorageMethodInlineArchive:
tarReader := tar.NewReader(bytes.NewReader(createProjectVersion.StorageSource))
_, err := tarReader.Next()
if err != nil {
httpapi.Write(rw, http.StatusBadRequest, httpapi.Response{
Message: "the archive must be a tar",
})
return
}
default:
tarReader := tar.NewReader(bytes.NewReader(createProjectVersion.StorageSource))
_, err := tarReader.Next()
if err != nil {
httpapi.Write(rw, http.StatusBadRequest, httpapi.Response{
Message: fmt.Sprintf("unsupported storage method %s", createProjectVersion.StorageMethod),
Message: "the archive must be a tar",
})
return
}
@ -132,7 +124,7 @@ func (api *api) postProjectVersionByOrganization(rw http.ResponseWriter, r *http
var provisionerJob database.ProvisionerJob
var projectVersion database.ProjectVersion
err := api.Database.InTx(func(db database.Store) error {
err = api.Database.InTx(func(db database.Store) error {
projectVersionID := uuid.New()
input, err := json.Marshal(projectImportJob{
ProjectVersionID: projectVersionID,

View File

@ -1,103 +1,129 @@
package coderd_test
import (
"archive/tar"
"bytes"
"context"
"net/http"
"testing"
"github.com/stretchr/testify/require"
"github.com/coder/coder/coderd"
"github.com/coder/coder/coderd/coderdtest"
"github.com/coder/coder/codersdk"
"github.com/coder/coder/database"
"github.com/coder/coder/provisioner/echo"
"github.com/coder/coder/provisionersdk/proto"
)
func TestProjectVersion(t *testing.T) {
func TestProjectVersionsByOrganization(t *testing.T) {
t.Parallel()
t.Run("NoHistory", func(t *testing.T) {
t.Run("ListEmpty", func(t *testing.T) {
t.Parallel()
server := coderdtest.New(t)
user := server.RandomInitialUser(t)
project, err := server.Client.CreateProject(context.Background(), user.Organization, coderd.CreateProjectRequest{
Name: "someproject",
Provisioner: database.ProvisionerTypeEcho,
})
require.NoError(t, err)
versions, err := server.Client.ProjectVersions(context.Background(), user.Organization, project.Name)
client := coderdtest.New(t)
user := coderdtest.CreateInitialUser(t, client)
project := coderdtest.CreateProject(t, client, user.Organization)
versions, err := client.ProjectVersions(context.Background(), user.Organization, project.Name)
require.NoError(t, err)
require.NotNil(t, versions)
require.Len(t, versions, 0)
})
t.Run("CreateVersion", func(t *testing.T) {
t.Run("List", func(t *testing.T) {
t.Parallel()
server := coderdtest.New(t)
user := server.RandomInitialUser(t)
project, err := server.Client.CreateProject(context.Background(), user.Organization, coderd.CreateProjectRequest{
Name: "someproject",
Provisioner: database.ProvisionerTypeEcho,
})
require.NoError(t, err)
data, err := echo.Tar([]*proto.Parse_Response{{
Type: &proto.Parse_Response_Complete{
Complete: &proto.Parse_Complete{},
},
}}, nil)
require.NoError(t, err)
version, err := server.Client.CreateProjectVersion(context.Background(), user.Organization, project.Name, coderd.CreateProjectVersionRequest{
StorageMethod: database.ProjectStorageMethodInlineArchive,
StorageSource: data,
})
require.NoError(t, err)
versions, err := server.Client.ProjectVersions(context.Background(), user.Organization, project.Name)
client := coderdtest.New(t)
user := coderdtest.CreateInitialUser(t, client)
project := coderdtest.CreateProject(t, client, user.Organization)
_ = coderdtest.CreateProjectVersion(t, client, user.Organization, project.Name, nil)
versions, err := client.ProjectVersions(context.Background(), user.Organization, project.Name)
require.NoError(t, err)
require.Len(t, versions, 1)
})
}
_, err = server.Client.ProjectVersion(context.Background(), user.Organization, project.Name, version.Name)
require.NoError(t, err)
func TestProjectVersionByOrganizationAndName(t *testing.T) {
t.Parallel()
t.Run("Get", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t)
user := coderdtest.CreateInitialUser(t, client)
project := coderdtest.CreateProject(t, client, user.Organization)
version := coderdtest.CreateProjectVersion(t, client, user.Organization, project.Name, nil)
require.Equal(t, version.Import.Status, coderd.ProvisionerJobStatusPending)
})
}
func TestPostProjectVersionByOrganization(t *testing.T) {
t.Parallel()
t.Run("Create", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t)
user := coderdtest.CreateInitialUser(t, client)
project := coderdtest.CreateProject(t, client, user.Organization)
_ = coderdtest.CreateProjectVersion(t, client, user.Organization, project.Name, nil)
})
t.Run("CreateHistoryArchiveTooBig", func(t *testing.T) {
t.Run("InvalidStorage", func(t *testing.T) {
t.Parallel()
server := coderdtest.New(t)
user := server.RandomInitialUser(t)
project, err := server.Client.CreateProject(context.Background(), user.Organization, coderd.CreateProjectRequest{
Name: "someproject",
Provisioner: database.ProvisionerTypeEcho,
})
require.NoError(t, err)
var buffer bytes.Buffer
writer := tar.NewWriter(&buffer)
err = writer.WriteHeader(&tar.Header{
Name: "file",
Size: 1 << 21,
})
require.NoError(t, err)
_, err = writer.Write(make([]byte, 1<<21))
require.NoError(t, err)
_, err = server.Client.CreateProjectVersion(context.Background(), user.Organization, project.Name, coderd.CreateProjectVersionRequest{
StorageMethod: database.ProjectStorageMethodInlineArchive,
StorageSource: buffer.Bytes(),
})
require.Error(t, err)
})
t.Run("CreateHistoryInvalidArchive", func(t *testing.T) {
t.Parallel()
server := coderdtest.New(t)
user := server.RandomInitialUser(t)
project, err := server.Client.CreateProject(context.Background(), user.Organization, coderd.CreateProjectRequest{
Name: "someproject",
Provisioner: database.ProvisionerTypeEcho,
})
require.NoError(t, err)
_, err = server.Client.CreateProjectVersion(context.Background(), user.Organization, project.Name, coderd.CreateProjectVersionRequest{
StorageMethod: database.ProjectStorageMethodInlineArchive,
client := coderdtest.New(t)
user := coderdtest.CreateInitialUser(t, client)
project := coderdtest.CreateProject(t, client, user.Organization)
_, err := client.CreateProjectVersion(context.Background(), user.Organization, project.Name, coderd.CreateProjectVersionRequest{
StorageMethod: database.ProjectStorageMethod("invalid"),
StorageSource: []byte{},
})
require.Error(t, err)
})
}
func TestProjectVersionParametersByOrganizationAndName(t *testing.T) {
t.Parallel()
t.Run("NotImported", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t)
user := coderdtest.CreateInitialUser(t, client)
project := coderdtest.CreateProject(t, client, user.Organization)
version := coderdtest.CreateProjectVersion(t, client, user.Organization, project.Name, nil)
_, err := client.ProjectVersionParameters(context.Background(), user.Organization, project.Name, version.Name)
var apiErr *codersdk.Error
require.ErrorAs(t, err, &apiErr)
require.Equal(t, http.StatusPreconditionRequired, apiErr.StatusCode())
})
t.Run("FailedImport", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t)
user := coderdtest.CreateInitialUser(t, client)
_ = coderdtest.NewProvisionerDaemon(t, client)
project := coderdtest.CreateProject(t, client, user.Organization)
version := coderdtest.CreateProjectVersion(t, client, user.Organization, project.Name, &echo.Responses{
Provision: []*proto.Provision_Response{{}},
})
coderdtest.AwaitProjectVersionImported(t, client, user.Organization, project.Name, version.Name)
_, err := client.ProjectVersionParameters(context.Background(), user.Organization, project.Name, version.Name)
var apiErr *codersdk.Error
require.ErrorAs(t, err, &apiErr)
require.Equal(t, http.StatusPreconditionFailed, apiErr.StatusCode())
})
t.Run("List", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t)
user := coderdtest.CreateInitialUser(t, client)
_ = coderdtest.NewProvisionerDaemon(t, client)
project := coderdtest.CreateProject(t, client, user.Organization)
version := coderdtest.CreateProjectVersion(t, client, user.Organization, project.Name, &echo.Responses{
Parse: []*proto.Parse_Response{{
Type: &proto.Parse_Response_Complete{
Complete: &proto.Parse_Complete{
ParameterSchemas: []*proto.ParameterSchema{{
Name: "example",
}},
},
},
}},
})
coderdtest.AwaitProjectVersionImported(t, client, user.Organization, project.Name, version.Name)
params, err := client.ProjectVersionParameters(context.Background(), user.Organization, project.Name, version.Name)
require.NoError(t, err)
require.Len(t, params, 1)
})
}

View File

@ -6,6 +6,7 @@ import (
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"reflect"
"time"
@ -35,7 +36,6 @@ func (api *api) provisionerDaemons(rw http.ResponseWriter, r *http.Request) {
daemons, err := api.Database.GetProvisionerDaemons(r.Context())
if errors.Is(err, sql.ErrNoRows) {
err = nil
daemons = []database.ProvisionerDaemon{}
}
if err != nil {
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
@ -43,7 +43,9 @@ func (api *api) provisionerDaemons(rw http.ResponseWriter, r *http.Request) {
})
return
}
if daemons == nil {
daemons = []database.ProvisionerDaemon{}
}
render.Status(r, http.StatusOK)
render.JSON(rw, r, daemons)
}
@ -51,7 +53,7 @@ func (api *api) provisionerDaemons(rw http.ResponseWriter, r *http.Request) {
// Serves the provisioner daemon protobuf API over a WebSocket.
func (api *api) provisionerDaemonsServe(rw http.ResponseWriter, r *http.Request) {
conn, err := websocket.Accept(rw, r, &websocket.AcceptOptions{
// Need to disable compression to avoid a data-race
// Need to disable compression to avoid a data-race.
CompressionMode: websocket.CompressionDisabled,
})
if err != nil {
@ -75,7 +77,9 @@ func (api *api) provisionerDaemonsServe(rw http.ResponseWriter, r *http.Request)
// Multiplexes the incoming connection using yamux.
// This allows multiple function calls to occur over
// the same connection.
session, err := yamux.Server(websocket.NetConn(r.Context(), conn, websocket.MessageBinary), nil)
config := yamux.DefaultConfig()
config.LogOutput = io.Discard
session, err := yamux.Server(websocket.NetConn(r.Context(), conn, websocket.MessageBinary), config)
if err != nil {
_ = conn.Close(websocket.StatusInternalError, fmt.Sprintf("multiplex server: %s", err))
return
@ -221,25 +225,11 @@ func (server *provisionerdServer) AcquireJob(ctx context.Context, _ *proto.Empty
protoParameters = append(protoParameters, parameter.Proto)
}
provisionerState := []byte{}
// If workspace history exists before this entry, use that state.
// We can't use the before state everytime, because if a job fails
// for some random reason, the workspace shouldn't be reset.
//
// Maybe we should make state global on a workspace?
if workspaceHistory.BeforeID.Valid {
beforeHistory, err := server.Database.GetWorkspaceHistoryByID(ctx, workspaceHistory.BeforeID.UUID)
if err != nil {
return nil, failJob(fmt.Sprintf("get workspace history: %s", err))
}
provisionerState = beforeHistory.ProvisionerState
}
protoJob.Type = &proto.AcquiredJob_WorkspaceProvision_{
WorkspaceProvision: &proto.AcquiredJob_WorkspaceProvision{
WorkspaceHistoryId: workspaceHistory.ID.String(),
WorkspaceName: workspace.Name,
State: provisionerState,
State: workspaceHistory.ProvisionerState,
ParameterValues: protoParameters,
},
}
@ -286,10 +276,10 @@ func (server *provisionerdServer) UpdateJob(stream proto.DRPCProvisionerDaemon_U
return xerrors.Errorf("get job: %w", err)
}
if !job.WorkerID.Valid {
return errors.New("job isn't running yet")
return xerrors.New("job isn't running yet")
}
if job.WorkerID.UUID.String() != server.ID.String() {
return errors.New("you don't own this job")
return xerrors.New("you don't own this job")
}
err = server.Database.UpdateProvisionerJobByID(stream.Context(), database.UpdateProvisionerJobByIDParams{

View File

@ -11,16 +11,18 @@ import (
)
func TestProvisionerDaemons(t *testing.T) {
// Tests for properly processing specific job
// types should be placed in their respective
// resource location.
//
// eg. project import is a project-related job
t.Parallel()
t.Run("Register", func(t *testing.T) {
t.Parallel()
server := coderdtest.New(t)
_ = server.AddProvisionerd(t)
require.Eventually(t, func() bool {
daemons, err := server.Client.ProvisionerDaemons(context.Background())
require.NoError(t, err)
return len(daemons) > 0
}, time.Second, 10*time.Millisecond)
})
client := coderdtest.New(t)
_ = coderdtest.NewProvisionerDaemon(t, client)
require.Eventually(t, func() bool {
daemons, err := client.ProvisionerDaemons(context.Background())
require.NoError(t, err)
return len(daemons) > 0
}, time.Second, 25*time.Millisecond)
}

View File

@ -70,7 +70,7 @@ func convertProvisionerJob(provisionerJob database.ProvisionerJob) ProvisionerJo
job.Status = ProvisionerJobStatusRunning
}
if job.Error != "" {
if !provisionerJob.CancelledAt.Valid && job.Error != "" {
job.Status = ProvisionerJobStatusFailed
}

View File

@ -195,6 +195,10 @@ func (api *api) organizationsByUser(rw http.ResponseWriter, r *http.Request) {
user := httpmw.UserParam(r)
organizations, err := api.Database.GetOrganizationsByUserID(r.Context(), user.ID)
if errors.Is(err, sql.ErrNoRows) {
err = nil
organizations = []database.Organization{}
}
if err != nil {
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
Message: fmt.Sprintf("get organizations: %s", err.Error()),

View File

@ -9,107 +9,143 @@ import (
"github.com/coder/coder/coderd"
"github.com/coder/coder/coderd/coderdtest"
"github.com/coder/coder/codersdk"
"github.com/coder/coder/httpmw"
)
func TestUsers(t *testing.T) {
func TestPostUser(t *testing.T) {
t.Parallel()
t.Run("Authenticated", func(t *testing.T) {
t.Run("BadRequest", func(t *testing.T) {
t.Parallel()
server := coderdtest.New(t)
_ = server.RandomInitialUser(t)
_, err := server.Client.User(context.Background(), "")
require.NoError(t, err)
})
t.Run("CreateMultipleInitial", func(t *testing.T) {
t.Parallel()
server := coderdtest.New(t)
_ = server.RandomInitialUser(t)
_, err := server.Client.CreateInitialUser(context.Background(), coderd.CreateInitialUserRequest{
Email: "dummy@coder.com",
Organization: "bananas",
Username: "fake",
Password: "password",
})
client := coderdtest.New(t)
_, err := client.CreateInitialUser(context.Background(), coderd.CreateInitialUserRequest{})
require.Error(t, err)
})
t.Run("Login", func(t *testing.T) {
t.Run("AlreadyExists", func(t *testing.T) {
t.Parallel()
server := coderdtest.New(t)
user := server.RandomInitialUser(t)
_, err := server.Client.LoginWithPassword(context.Background(), coderd.LoginWithPasswordRequest{
client := coderdtest.New(t)
_ = coderdtest.CreateInitialUser(t, client)
_, err := client.CreateInitialUser(context.Background(), coderd.CreateInitialUserRequest{
Email: "some@email.com",
Username: "exampleuser",
Password: "password",
Organization: "someorg",
})
var apiErr *codersdk.Error
require.ErrorAs(t, err, &apiErr)
require.Equal(t, http.StatusConflict, apiErr.StatusCode())
})
t.Run("Create", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t)
_ = coderdtest.CreateInitialUser(t, client)
})
}
func TestPostUsers(t *testing.T) {
t.Parallel()
t.Run("BadRequest", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t)
_, err := client.CreateInitialUser(context.Background(), coderd.CreateInitialUserRequest{})
require.Error(t, err)
})
t.Run("Conflicting", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t)
user := coderdtest.CreateInitialUser(t, client)
_, err := client.CreateInitialUser(context.Background(), coderd.CreateInitialUserRequest{
Email: user.Email,
Username: user.Username,
Password: "password",
Organization: "someorg",
})
var apiErr *codersdk.Error
require.ErrorAs(t, err, &apiErr)
require.Equal(t, http.StatusConflict, apiErr.StatusCode())
})
t.Run("Create", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t)
_ = coderdtest.CreateInitialUser(t, client)
_, err := client.CreateUser(context.Background(), coderd.CreateUserRequest{
Email: "another@user.org",
Username: "someone-else",
Password: "testing",
})
require.NoError(t, err)
})
}
func TestUserByName(t *testing.T) {
t.Parallel()
client := coderdtest.New(t)
_ = coderdtest.CreateInitialUser(t, client)
_, err := client.User(context.Background(), "")
require.NoError(t, err)
}
func TestOrganizationsByUser(t *testing.T) {
t.Parallel()
client := coderdtest.New(t)
_ = coderdtest.CreateInitialUser(t, client)
orgs, err := client.UserOrganizations(context.Background(), "")
require.NoError(t, err)
require.NotNil(t, orgs)
require.Len(t, orgs, 1)
}
func TestPostLogin(t *testing.T) {
t.Parallel()
t.Run("InvalidUser", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t)
_, err := client.LoginWithPassword(context.Background(), coderd.LoginWithPasswordRequest{
Email: "my@email.org",
Password: "password",
})
var apiErr *codersdk.Error
require.ErrorAs(t, err, &apiErr)
require.Equal(t, http.StatusUnauthorized, apiErr.StatusCode())
})
t.Run("BadPassword", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t)
user := coderdtest.CreateInitialUser(t, client)
_, err := client.LoginWithPassword(context.Background(), coderd.LoginWithPasswordRequest{
Email: user.Email,
Password: "badpass",
})
var apiErr *codersdk.Error
require.ErrorAs(t, err, &apiErr)
require.Equal(t, http.StatusUnauthorized, apiErr.StatusCode())
})
t.Run("Success", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t)
user := coderdtest.CreateInitialUser(t, client)
_, err := client.LoginWithPassword(context.Background(), coderd.LoginWithPasswordRequest{
Email: user.Email,
Password: user.Password,
})
require.NoError(t, err)
})
t.Run("LoginInvalidUser", func(t *testing.T) {
t.Parallel()
server := coderdtest.New(t)
_, err := server.Client.LoginWithPassword(context.Background(), coderd.LoginWithPasswordRequest{
Email: "hello@io.io",
Password: "wowie",
})
require.Error(t, err)
})
t.Run("LoginBadPassword", func(t *testing.T) {
t.Parallel()
server := coderdtest.New(t)
user := server.RandomInitialUser(t)
_, err := server.Client.LoginWithPassword(context.Background(), coderd.LoginWithPasswordRequest{
Email: user.Email,
Password: "bananas",
})
require.Error(t, err)
})
t.Run("ListOrganizations", func(t *testing.T) {
t.Parallel()
server := coderdtest.New(t)
_ = server.RandomInitialUser(t)
orgs, err := server.Client.UserOrganizations(context.Background(), "")
require.NoError(t, err)
require.Len(t, orgs, 1)
})
t.Run("CreateUser", func(t *testing.T) {
t.Parallel()
server := coderdtest.New(t)
_ = server.RandomInitialUser(t)
_, err := server.Client.CreateUser(context.Background(), coderd.CreateUserRequest{
Email: "wow@ok.io",
Username: "tomato",
Password: "bananas",
})
require.NoError(t, err)
})
t.Run("CreateUserConflict", func(t *testing.T) {
t.Parallel()
server := coderdtest.New(t)
user := server.RandomInitialUser(t)
_, err := server.Client.CreateUser(context.Background(), coderd.CreateUserRequest{
Email: "wow@ok.io",
Username: user.Username,
Password: "bananas",
})
require.Error(t, err)
})
}
func TestLogout(t *testing.T) {
func TestPostLogout(t *testing.T) {
t.Parallel()
t.Run("LogoutShouldClearCookie", func(t *testing.T) {
t.Run("ClearCookie", func(t *testing.T) {
t.Parallel()
server := coderdtest.New(t)
fullURL, err := server.URL.Parse("/api/v2/logout")
client := coderdtest.New(t)
fullURL, err := client.URL.Parse("/api/v2/logout")
require.NoError(t, err, "Server URL should parse successfully")
req, err := http.NewRequestWithContext(context.Background(), http.MethodPost, fullURL.String(), nil)

View File

@ -74,12 +74,12 @@ func (api *api) postWorkspaceHistoryByUser(rw http.ResponseWriter, r *http.Reque
projectVersionJobStatus := convertProvisionerJob(projectVersionJob).Status
switch projectVersionJobStatus {
case ProvisionerJobStatusPending, ProvisionerJobStatusRunning:
httpapi.Write(rw, http.StatusPreconditionFailed, httpapi.Response{
httpapi.Write(rw, http.StatusNotAcceptable, httpapi.Response{
Message: fmt.Sprintf("The provided project version is %s. Wait for it to complete importing!", projectVersionJobStatus),
})
return
case ProvisionerJobStatusFailed:
httpapi.Write(rw, http.StatusBadRequest, httpapi.Response{
httpapi.Write(rw, http.StatusPreconditionFailed, httpapi.Response{
Message: fmt.Sprintf("The provided project version %q has failed to import. You cannot create workspaces using it!", projectVersion.Name),
})
return
@ -87,6 +87,7 @@ func (api *api) postWorkspaceHistoryByUser(rw http.ResponseWriter, r *http.Reque
httpapi.Write(rw, http.StatusPreconditionFailed, httpapi.Response{
Message: "The provided project version was canceled during import. You cannot create workspaces using it!",
})
return
}
project, err := api.Database.GetProjectByID(r.Context(), projectVersion.ProjectID)
@ -102,7 +103,7 @@ func (api *api) postWorkspaceHistoryByUser(rw http.ResponseWriter, r *http.Reque
priorHistory, err := api.Database.GetWorkspaceHistoryByWorkspaceIDWithoutAfter(r.Context(), workspace.ID)
if err == nil {
priorJob, err := api.Database.GetProvisionerJobByID(r.Context(), priorHistory.ProvisionJobID)
if err == nil && convertProvisionerJob(priorJob).Status.Completed() {
if err == nil && !convertProvisionerJob(priorJob).Status.Completed() {
httpapi.Write(rw, http.StatusConflict, httpapi.Response{
Message: "a workspace build is already active",
})
@ -113,8 +114,7 @@ func (api *api) postWorkspaceHistoryByUser(rw http.ResponseWriter, r *http.Reque
UUID: priorHistory.ID,
Valid: true,
}
}
if !errors.Is(err, sql.ErrNoRows) {
} else if !errors.Is(err, sql.ErrNoRows) {
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
Message: fmt.Sprintf("get prior workspace history: %s", err),
})
@ -168,8 +168,9 @@ func (api *api) postWorkspaceHistoryByUser(rw http.ResponseWriter, r *http.Reque
if priorHistoryID.Valid {
// Update the prior history entries "after" column.
err = db.UpdateWorkspaceHistoryByID(r.Context(), database.UpdateWorkspaceHistoryByIDParams{
ID: priorHistory.ID,
UpdatedAt: database.Now(),
ID: priorHistory.ID,
ProvisionerState: priorHistory.ProvisionerState,
UpdatedAt: database.Now(),
AfterID: uuid.NullUUID{
UUID: workspaceHistory.ID,
Valid: true,
@ -197,9 +198,10 @@ func (api *api) postWorkspaceHistoryByUser(rw http.ResponseWriter, r *http.Reque
func (api *api) workspaceHistoryByUser(rw http.ResponseWriter, r *http.Request) {
workspace := httpmw.WorkspaceParam(r)
histories, err := api.Database.GetWorkspaceHistoryByWorkspaceID(r.Context(), workspace.ID)
history, err := api.Database.GetWorkspaceHistoryByWorkspaceID(r.Context(), workspace.ID)
if errors.Is(err, sql.ErrNoRows) {
err = nil
history = []database.WorkspaceHistory{}
}
if err != nil {
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
@ -208,8 +210,8 @@ func (api *api) workspaceHistoryByUser(rw http.ResponseWriter, r *http.Request)
return
}
apiHistory := make([]WorkspaceHistory, 0, len(histories))
for _, history := range histories {
apiHistory := make([]WorkspaceHistory, 0, len(history))
for _, history := range history {
job, err := api.Database.GetProvisionerJobByID(r.Context(), history.ProvisionJobID)
if err != nil {
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{

View File

@ -2,8 +2,8 @@ package coderd_test
import (
"context"
"net/http"
"testing"
"time"
"github.com/google/uuid"
"github.com/stretchr/testify/require"
@ -13,141 +13,150 @@ import (
"github.com/coder/coder/codersdk"
"github.com/coder/coder/database"
"github.com/coder/coder/provisioner/echo"
"github.com/coder/coder/provisionersdk/proto"
)
func TestWorkspaceHistory(t *testing.T) {
func TestPostWorkspaceHistoryByUser(t *testing.T) {
t.Parallel()
setupProjectAndWorkspace := func(t *testing.T, client *codersdk.Client, user coderd.CreateInitialUserRequest) (coderd.Project, coderd.Workspace) {
project, err := client.CreateProject(context.Background(), user.Organization, coderd.CreateProjectRequest{
Name: "banana",
Provisioner: database.ProvisionerTypeEcho,
})
require.NoError(t, err)
workspace, err := client.CreateWorkspace(context.Background(), "", coderd.CreateWorkspaceRequest{
Name: "example",
ProjectID: project.ID,
})
require.NoError(t, err)
return project, workspace
}
setupProjectVersion := func(t *testing.T, client *codersdk.Client, user coderd.CreateInitialUserRequest, project coderd.Project, data []byte) coderd.ProjectVersion {
projectVersion, err := client.CreateProjectVersion(context.Background(), user.Organization, project.Name, coderd.CreateProjectVersionRequest{
StorageMethod: database.ProjectStorageMethodInlineArchive,
StorageSource: data,
})
require.NoError(t, err)
require.Eventually(t, func() bool {
version, err := client.ProjectVersion(context.Background(), user.Organization, project.Name, projectVersion.Name)
require.NoError(t, err)
t.Logf("Import status: %s\n", version.Import.Status)
return version.Import.Status.Completed()
}, 15*time.Second, 50*time.Millisecond)
return projectVersion
}
t.Run("AllHistory", func(t *testing.T) {
t.Run("NoProjectVersion", func(t *testing.T) {
t.Parallel()
server := coderdtest.New(t)
user := server.RandomInitialUser(t)
_ = server.AddProvisionerd(t)
project, workspace := setupProjectAndWorkspace(t, server.Client, user)
history, err := server.Client.ListWorkspaceHistory(context.Background(), "", workspace.Name)
require.NoError(t, err)
require.Len(t, history, 0)
data, err := echo.Tar(echo.ParseComplete, echo.ProvisionComplete)
require.NoError(t, err)
projectVersion := setupProjectVersion(t, server.Client, user, project, data)
_, err = server.Client.CreateWorkspaceHistory(context.Background(), "", workspace.Name, coderd.CreateWorkspaceHistoryRequest{
ProjectVersionID: projectVersion.ID,
Transition: database.WorkspaceTransitionCreate,
})
require.NoError(t, err)
history, err = server.Client.ListWorkspaceHistory(context.Background(), "", workspace.Name)
require.NoError(t, err)
require.Len(t, history, 1)
})
t.Run("LatestHistory", func(t *testing.T) {
t.Parallel()
server := coderdtest.New(t)
user := server.RandomInitialUser(t)
_ = server.AddProvisionerd(t)
project, workspace := setupProjectAndWorkspace(t, server.Client, user)
_, err := server.Client.WorkspaceHistory(context.Background(), "", workspace.Name, "")
require.Error(t, err)
data, err := echo.Tar(echo.ParseComplete, echo.ProvisionComplete)
require.NoError(t, err)
projectVersion := setupProjectVersion(t, server.Client, user, project, data)
_, err = server.Client.CreateWorkspaceHistory(context.Background(), "", workspace.Name, coderd.CreateWorkspaceHistoryRequest{
ProjectVersionID: projectVersion.ID,
Transition: database.WorkspaceTransitionCreate,
})
require.NoError(t, err)
_, err = server.Client.WorkspaceHistory(context.Background(), "", workspace.Name, "")
require.NoError(t, err)
})
t.Run("CreateHistory", func(t *testing.T) {
t.Parallel()
server := coderdtest.New(t)
user := server.RandomInitialUser(t)
_ = server.AddProvisionerd(t)
project, workspace := setupProjectAndWorkspace(t, server.Client, user)
data, err := echo.Tar(echo.ParseComplete, echo.ProvisionComplete)
require.NoError(t, err)
projectVersion := setupProjectVersion(t, server.Client, user, project, data)
_, err = server.Client.CreateWorkspaceHistory(context.Background(), "", workspace.Name, coderd.CreateWorkspaceHistoryRequest{
ProjectVersionID: projectVersion.ID,
Transition: database.WorkspaceTransitionCreate,
})
require.NoError(t, err)
var workspaceHistory coderd.WorkspaceHistory
require.Eventually(t, func() bool {
workspaceHistory, err = server.Client.WorkspaceHistory(context.Background(), "", workspace.Name, "")
require.NoError(t, err)
return workspaceHistory.Provision.Status.Completed()
}, 15*time.Second, 50*time.Millisecond)
require.Equal(t, "", workspaceHistory.Provision.Error)
require.Equal(t, coderd.ProvisionerJobStatusSucceeded, workspaceHistory.Provision.Status)
})
t.Run("CreateHistoryAlreadyInProgress", func(t *testing.T) {
t.Parallel()
server := coderdtest.New(t)
user := server.RandomInitialUser(t)
_ = server.AddProvisionerd(t)
project, workspace := setupProjectAndWorkspace(t, server.Client, user)
data, err := echo.Tar(echo.ParseComplete, echo.ProvisionComplete)
require.NoError(t, err)
projectVersion := setupProjectVersion(t, server.Client, user, project, data)
_, err = server.Client.CreateWorkspaceHistory(context.Background(), "", workspace.Name, coderd.CreateWorkspaceHistoryRequest{
ProjectVersionID: projectVersion.ID,
Transition: database.WorkspaceTransitionCreate,
})
require.NoError(t, err)
_, err = server.Client.CreateWorkspaceHistory(context.Background(), "", workspace.Name, coderd.CreateWorkspaceHistoryRequest{
ProjectVersionID: projectVersion.ID,
Transition: database.WorkspaceTransitionCreate,
})
require.Error(t, err)
})
t.Run("CreateHistoryInvalidProjectVersion", func(t *testing.T) {
t.Parallel()
server := coderdtest.New(t)
user := server.RandomInitialUser(t)
_ = server.AddProvisionerd(t)
_, workspace := setupProjectAndWorkspace(t, server.Client, user)
_, err := server.Client.CreateWorkspaceHistory(context.Background(), "", workspace.Name, coderd.CreateWorkspaceHistoryRequest{
client := coderdtest.New(t)
user := coderdtest.CreateInitialUser(t, client)
project := coderdtest.CreateProject(t, client, user.Organization)
workspace := coderdtest.CreateWorkspace(t, client, "me", project.ID)
_, err := client.CreateWorkspaceHistory(context.Background(), "", workspace.Name, coderd.CreateWorkspaceHistoryRequest{
ProjectVersionID: uuid.New(),
Transition: database.WorkspaceTransitionCreate,
})
require.Error(t, err)
var apiErr *codersdk.Error
require.ErrorAs(t, err, &apiErr)
require.Equal(t, http.StatusBadRequest, apiErr.StatusCode())
})
t.Run("ProjectVersionFailedImport", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t)
user := coderdtest.CreateInitialUser(t, client)
coderdtest.NewProvisionerDaemon(t, client)
project := coderdtest.CreateProject(t, client, user.Organization)
version := coderdtest.CreateProjectVersion(t, client, user.Organization, project.Name, &echo.Responses{
Provision: []*proto.Provision_Response{{}},
})
coderdtest.AwaitProjectVersionImported(t, client, user.Organization, project.Name, version.Name)
workspace := coderdtest.CreateWorkspace(t, client, "me", project.ID)
_, err := client.CreateWorkspaceHistory(context.Background(), "", workspace.Name, coderd.CreateWorkspaceHistoryRequest{
ProjectVersionID: version.ID,
Transition: database.WorkspaceTransitionCreate,
})
require.Error(t, err)
var apiErr *codersdk.Error
require.ErrorAs(t, err, &apiErr)
require.Equal(t, http.StatusPreconditionFailed, apiErr.StatusCode())
})
t.Run("AlreadyActive", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t)
user := coderdtest.CreateInitialUser(t, client)
coderdtest.NewProvisionerDaemon(t, client)
project := coderdtest.CreateProject(t, client, user.Organization)
version := coderdtest.CreateProjectVersion(t, client, user.Organization, project.Name, nil)
coderdtest.AwaitProjectVersionImported(t, client, user.Organization, project.Name, version.Name)
workspace := coderdtest.CreateWorkspace(t, client, "me", project.ID)
_, err := client.CreateWorkspaceHistory(context.Background(), "", workspace.Name, coderd.CreateWorkspaceHistoryRequest{
ProjectVersionID: version.ID,
Transition: database.WorkspaceTransitionCreate,
})
require.NoError(t, err)
_, err = client.CreateWorkspaceHistory(context.Background(), "", workspace.Name, coderd.CreateWorkspaceHistoryRequest{
ProjectVersionID: version.ID,
Transition: database.WorkspaceTransitionCreate,
})
require.Error(t, err)
var apiErr *codersdk.Error
require.ErrorAs(t, err, &apiErr)
require.Equal(t, http.StatusConflict, apiErr.StatusCode())
})
t.Run("UpdatePriorAfterField", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t)
user := coderdtest.CreateInitialUser(t, client)
coderdtest.NewProvisionerDaemon(t, client)
project := coderdtest.CreateProject(t, client, user.Organization)
version := coderdtest.CreateProjectVersion(t, client, user.Organization, project.Name, nil)
coderdtest.AwaitProjectVersionImported(t, client, user.Organization, project.Name, version.Name)
workspace := coderdtest.CreateWorkspace(t, client, "me", project.ID)
firstHistory, err := client.CreateWorkspaceHistory(context.Background(), "", workspace.Name, coderd.CreateWorkspaceHistoryRequest{
ProjectVersionID: version.ID,
Transition: database.WorkspaceTransitionCreate,
})
require.NoError(t, err)
coderdtest.AwaitWorkspaceHistoryProvisioned(t, client, "me", workspace.Name, firstHistory.Name)
secondHistory, err := client.CreateWorkspaceHistory(context.Background(), "", workspace.Name, coderd.CreateWorkspaceHistoryRequest{
ProjectVersionID: version.ID,
Transition: database.WorkspaceTransitionCreate,
})
require.NoError(t, err)
require.Equal(t, firstHistory.ID.String(), secondHistory.BeforeID.String())
firstHistory, err = client.WorkspaceHistory(context.Background(), "", workspace.Name, firstHistory.Name)
require.NoError(t, err)
require.Equal(t, secondHistory.ID.String(), firstHistory.AfterID.String())
})
}
func TestWorkspaceHistoryByUser(t *testing.T) {
t.Parallel()
t.Run("ListEmpty", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t)
user := coderdtest.CreateInitialUser(t, client)
coderdtest.NewProvisionerDaemon(t, client)
project := coderdtest.CreateProject(t, client, user.Organization)
workspace := coderdtest.CreateWorkspace(t, client, "me", project.ID)
history, err := client.ListWorkspaceHistory(context.Background(), "me", workspace.Name)
require.NoError(t, err)
require.NotNil(t, history)
require.Len(t, history, 0)
})
t.Run("List", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t)
user := coderdtest.CreateInitialUser(t, client)
coderdtest.NewProvisionerDaemon(t, client)
project := coderdtest.CreateProject(t, client, user.Organization)
version := coderdtest.CreateProjectVersion(t, client, user.Organization, project.Name, nil)
coderdtest.AwaitProjectVersionImported(t, client, user.Organization, project.Name, version.Name)
workspace := coderdtest.CreateWorkspace(t, client, "me", project.ID)
_, err := client.CreateWorkspaceHistory(context.Background(), "", workspace.Name, coderd.CreateWorkspaceHistoryRequest{
ProjectVersionID: version.ID,
Transition: database.WorkspaceTransitionCreate,
})
require.NoError(t, err)
history, err := client.ListWorkspaceHistory(context.Background(), "me", workspace.Name)
require.NoError(t, err)
require.NotNil(t, history)
require.Len(t, history, 1)
})
}
func TestWorkspaceHistoryByName(t *testing.T) {
t.Parallel()
client := coderdtest.New(t)
user := coderdtest.CreateInitialUser(t, client)
coderdtest.NewProvisionerDaemon(t, client)
project := coderdtest.CreateProject(t, client, user.Organization)
version := coderdtest.CreateProjectVersion(t, client, user.Organization, project.Name, nil)
coderdtest.AwaitProjectVersionImported(t, client, user.Organization, project.Name, version.Name)
workspace := coderdtest.CreateWorkspace(t, client, "me", project.ID)
history, err := client.CreateWorkspaceHistory(context.Background(), "", workspace.Name, coderd.CreateWorkspaceHistoryRequest{
ProjectVersionID: version.ID,
Transition: database.WorkspaceTransitionCreate,
})
require.NoError(t, err)
_, err = client.WorkspaceHistory(context.Background(), "me", workspace.Name, history.Name)
require.NoError(t, err)
}

View File

@ -87,7 +87,6 @@ func (api *api) workspaceHistoryLogsByName(rw http.ResponseWriter, r *http.Reque
})
if errors.Is(err, sql.ErrNoRows) {
err = nil
logs = []database.WorkspaceHistoryLog{}
}
if err != nil {
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
@ -95,6 +94,9 @@ func (api *api) workspaceHistoryLogsByName(rw http.ResponseWriter, r *http.Reque
})
return
}
if logs == nil {
logs = []database.WorkspaceHistoryLog{}
}
render.Status(r, http.StatusOK)
render.JSON(rw, r, logs)
return
@ -113,12 +115,8 @@ func (api *api) workspaceHistoryLogsByName(rw http.ResponseWriter, r *http.Reque
select {
case bufferedLogs <- log:
default:
// This is a case that shouldn't happen, but totally could.
// There's no way to stream data from the database, so we'll
// need to maintain some level of internal buffer.
//
// If this overflows users could miss logs when streaming.
// We warn to make sure we know when it happens!
// If this overflows users could miss logs streaming. This can happen
// if a database request takes a long amount of time, and we get a lot of logs.
api.Logger.Warn(r.Context(), "workspace history log overflowing channel")
}
}

View File

@ -9,90 +9,132 @@ import (
"github.com/coder/coder/coderd"
"github.com/coder/coder/coderd/coderdtest"
"github.com/coder/coder/codersdk"
"github.com/coder/coder/database"
"github.com/coder/coder/provisioner/echo"
"github.com/coder/coder/provisionersdk/proto"
)
func TestWorkspaceHistoryLogs(t *testing.T) {
func TestWorkspaceHistoryLogsByName(t *testing.T) {
t.Parallel()
setupProjectAndWorkspace := func(t *testing.T, client *codersdk.Client, user coderd.CreateInitialUserRequest) (coderd.Project, coderd.Workspace) {
project, err := client.CreateProject(context.Background(), user.Organization, coderd.CreateProjectRequest{
Name: "banana",
Provisioner: database.ProvisionerTypeEcho,
})
require.NoError(t, err)
workspace, err := client.CreateWorkspace(context.Background(), "", coderd.CreateWorkspaceRequest{
Name: "example",
ProjectID: project.ID,
})
require.NoError(t, err)
return project, workspace
}
setupProjectVersion := func(t *testing.T, client *codersdk.Client, user coderd.CreateInitialUserRequest, project coderd.Project, data []byte) coderd.ProjectVersion {
projectVersion, err := client.CreateProjectVersion(context.Background(), user.Organization, project.Name, coderd.CreateProjectVersionRequest{
StorageMethod: database.ProjectStorageMethodInlineArchive,
StorageSource: data,
})
require.NoError(t, err)
require.Eventually(t, func() bool {
hist, err := client.ProjectVersion(context.Background(), user.Organization, project.Name, projectVersion.Name)
require.NoError(t, err)
return hist.Import.Status.Completed()
}, 15*time.Second, 50*time.Millisecond)
return projectVersion
}
server := coderdtest.New(t)
user := server.RandomInitialUser(t)
_ = server.AddProvisionerd(t)
project, workspace := setupProjectAndWorkspace(t, server.Client, user)
data, err := echo.Tar(echo.ParseComplete, []*proto.Provision_Response{{
Type: &proto.Provision_Response_Log{
Log: &proto.Log{
Output: "test",
},
},
}, {
Type: &proto.Provision_Response_Complete{
Complete: &proto.Provision_Complete{},
},
}})
require.NoError(t, err)
projectVersion := setupProjectVersion(t, server.Client, user, project, data)
workspaceHistory, err := server.Client.CreateWorkspaceHistory(context.Background(), "", workspace.Name, coderd.CreateWorkspaceHistoryRequest{
ProjectVersionID: projectVersion.ID,
Transition: database.WorkspaceTransitionCreate,
})
require.NoError(t, err)
now := database.Now()
logChan, err := server.Client.FollowWorkspaceHistoryLogsAfter(context.Background(), "", workspace.Name, workspaceHistory.Name, now)
require.NoError(t, err)
for {
log, more := <-logChan
if !more {
break
}
t.Logf("Output: %s", log.Output)
}
t.Run("ReturnAll", func(t *testing.T) {
t.Run("List", func(t *testing.T) {
t.Parallel()
_, err := server.Client.WorkspaceHistoryLogs(context.Background(), "", workspace.Name, workspaceHistory.Name)
client := coderdtest.New(t)
user := coderdtest.CreateInitialUser(t, client)
coderdtest.NewProvisionerDaemon(t, client)
project := coderdtest.CreateProject(t, client, user.Organization)
version := coderdtest.CreateProjectVersion(t, client, user.Organization, project.Name, &echo.Responses{
Parse: echo.ParseComplete,
Provision: []*proto.Provision_Response{{
Type: &proto.Provision_Response_Log{
Log: &proto.Log{
Level: proto.LogLevel_INFO,
Output: "log-output",
},
},
}, {
Type: &proto.Provision_Response_Complete{
Complete: &proto.Provision_Complete{},
},
}},
})
coderdtest.AwaitProjectVersionImported(t, client, user.Organization, project.Name, version.Name)
workspace := coderdtest.CreateWorkspace(t, client, "me", project.ID)
history, err := client.CreateWorkspaceHistory(context.Background(), "", workspace.Name, coderd.CreateWorkspaceHistoryRequest{
ProjectVersionID: version.ID,
Transition: database.WorkspaceTransitionCreate,
})
require.NoError(t, err)
// Successfully return empty logs before the job starts!
logs, err := client.WorkspaceHistoryLogs(context.Background(), "", workspace.Name, history.Name)
require.NoError(t, err)
require.NotNil(t, logs)
require.Len(t, logs, 0)
coderdtest.AwaitWorkspaceHistoryProvisioned(t, client, "", workspace.Name, history.Name)
// Return the log after completion!
logs, err = client.WorkspaceHistoryLogs(context.Background(), "", workspace.Name, history.Name)
require.NoError(t, err)
require.NotNil(t, logs)
require.Len(t, logs, 1)
})
t.Run("Between", func(t *testing.T) {
t.Run("StreamAfterComplete", func(t *testing.T) {
t.Parallel()
_, err := server.Client.WorkspaceHistoryLogsBetween(context.Background(), "", workspace.Name, workspaceHistory.Name, time.Time{}, database.Now())
client := coderdtest.New(t)
user := coderdtest.CreateInitialUser(t, client)
coderdtest.NewProvisionerDaemon(t, client)
project := coderdtest.CreateProject(t, client, user.Organization)
version := coderdtest.CreateProjectVersion(t, client, user.Organization, project.Name, &echo.Responses{
Parse: echo.ParseComplete,
Provision: []*proto.Provision_Response{{
Type: &proto.Provision_Response_Log{
Log: &proto.Log{
Level: proto.LogLevel_INFO,
Output: "log-output",
},
},
}, {
Type: &proto.Provision_Response_Complete{
Complete: &proto.Provision_Complete{},
},
}},
})
coderdtest.AwaitProjectVersionImported(t, client, user.Organization, project.Name, version.Name)
workspace := coderdtest.CreateWorkspace(t, client, "me", project.ID)
before := time.Now().UTC()
history, err := client.CreateWorkspaceHistory(context.Background(), "", workspace.Name, coderd.CreateWorkspaceHistoryRequest{
ProjectVersionID: version.ID,
Transition: database.WorkspaceTransitionCreate,
})
require.NoError(t, err)
coderdtest.AwaitWorkspaceHistoryProvisioned(t, client, "", workspace.Name, history.Name)
logs, err := client.FollowWorkspaceHistoryLogsAfter(context.Background(), "", workspace.Name, history.Name, before)
require.NoError(t, err)
log := <-logs
require.Equal(t, "log-output", log.Output)
// Make sure the channel automatically closes!
_, ok := <-logs
require.False(t, ok)
})
t.Run("StreamWhileRunning", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t)
user := coderdtest.CreateInitialUser(t, client)
coderdtest.NewProvisionerDaemon(t, client)
project := coderdtest.CreateProject(t, client, user.Organization)
version := coderdtest.CreateProjectVersion(t, client, user.Organization, project.Name, &echo.Responses{
Parse: echo.ParseComplete,
Provision: []*proto.Provision_Response{{
Type: &proto.Provision_Response_Log{
Log: &proto.Log{
Level: proto.LogLevel_INFO,
Output: "log-output",
},
},
}, {
Type: &proto.Provision_Response_Complete{
Complete: &proto.Provision_Complete{},
},
}},
})
coderdtest.AwaitProjectVersionImported(t, client, user.Organization, project.Name, version.Name)
workspace := coderdtest.CreateWorkspace(t, client, "me", project.ID)
history, err := client.CreateWorkspaceHistory(context.Background(), "", workspace.Name, coderd.CreateWorkspaceHistoryRequest{
ProjectVersionID: version.ID,
Transition: database.WorkspaceTransitionCreate,
})
require.NoError(t, err)
logs, err := client.FollowWorkspaceHistoryLogsAfter(context.Background(), "", workspace.Name, history.Name, time.Time{})
require.NoError(t, err)
log := <-logs
require.Equal(t, "log-output", log.Output)
// Make sure the channel automatically closes!
_, ok := <-logs
require.False(t, ok)
})
}

View File

@ -137,7 +137,7 @@ func (api *api) postWorkspaceByUser(rw http.ResponseWriter, r *http.Request) {
render.JSON(rw, r, convertWorkspace(workspace))
}
// Returns a single singleWorkspace.
// Returns a single workspace.
func (*api) workspaceByUser(rw http.ResponseWriter, r *http.Request) {
workspace := httpmw.WorkspaceParam(r)
@ -145,6 +145,32 @@ func (*api) workspaceByUser(rw http.ResponseWriter, r *http.Request) {
render.JSON(rw, r, convertWorkspace(workspace))
}
// Returns all workspaces for a specific project.
func (api *api) workspacesByProject(rw http.ResponseWriter, r *http.Request) {
apiKey := httpmw.APIKey(r)
project := httpmw.ProjectParam(r)
workspaces, err := api.Database.GetWorkspacesByProjectAndUserID(r.Context(), database.GetWorkspacesByProjectAndUserIDParams{
OwnerID: apiKey.UserID,
ProjectID: project.ID,
})
if errors.Is(err, sql.ErrNoRows) {
err = nil
}
if err != nil {
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
Message: fmt.Sprintf("get workspaces: %s", err),
})
return
}
apiWorkspaces := make([]Workspace, 0, len(workspaces))
for _, workspace := range workspaces {
apiWorkspaces = append(apiWorkspaces, convertWorkspace(workspace))
}
render.Status(r, http.StatusOK)
render.JSON(rw, r, apiWorkspaces)
}
// Converts the internal workspace representation to a public external-facing model.
func convertWorkspace(workspace database.Workspace) Workspace {
return Workspace(workspace)

View File

@ -2,6 +2,7 @@ package coderd_test
import (
"context"
"net/http"
"testing"
"github.com/google/uuid"
@ -10,143 +11,135 @@ import (
"github.com/coder/coder/coderd"
"github.com/coder/coder/coderd/coderdtest"
"github.com/coder/coder/codersdk"
"github.com/coder/coder/database"
)
func TestWorkspaces(t *testing.T) {
t.Parallel()
t.Run("ListNone", func(t *testing.T) {
t.Parallel()
server := coderdtest.New(t)
_ = server.RandomInitialUser(t)
workspaces, err := server.Client.WorkspacesByUser(context.Background(), "")
client := coderdtest.New(t)
_ = coderdtest.CreateInitialUser(t, client)
workspaces, err := client.Workspaces(context.Background(), "")
require.NoError(t, err)
require.NotNil(t, workspaces)
require.Len(t, workspaces, 0)
})
setupProjectAndWorkspace := func(t *testing.T, client *codersdk.Client, user coderd.CreateInitialUserRequest) (coderd.Project, coderd.Workspace) {
project, err := client.CreateProject(context.Background(), user.Organization, coderd.CreateProjectRequest{
Name: "banana",
Provisioner: database.ProvisionerTypeEcho,
})
require.NoError(t, err)
workspace, err := client.CreateWorkspace(context.Background(), "", coderd.CreateWorkspaceRequest{
Name: "example",
ProjectID: project.ID,
})
require.NoError(t, err)
return project, workspace
}
t.Run("List", func(t *testing.T) {
t.Parallel()
server := coderdtest.New(t)
user := server.RandomInitialUser(t)
_, _ = setupProjectAndWorkspace(t, server.Client, user)
workspaces, err := server.Client.WorkspacesByUser(context.Background(), "")
client := coderdtest.New(t)
user := coderdtest.CreateInitialUser(t, client)
project := coderdtest.CreateProject(t, client, user.Organization)
_ = coderdtest.CreateWorkspace(t, client, "", project.ID)
workspaces, err := client.Workspaces(context.Background(), "")
require.NoError(t, err)
require.Len(t, workspaces, 1)
})
t.Run("ListNoneForProject", func(t *testing.T) {
t.Parallel()
server := coderdtest.New(t)
user := server.RandomInitialUser(t)
project, err := server.Client.CreateProject(context.Background(), user.Organization, coderd.CreateProjectRequest{
Name: "banana",
Provisioner: database.ProvisionerTypeEcho,
})
require.NoError(t, err)
workspaces, err := server.Client.WorkspacesByProject(context.Background(), user.Organization, project.Name)
require.NoError(t, err)
require.Len(t, workspaces, 0)
})
t.Run("ListForProject", func(t *testing.T) {
t.Parallel()
server := coderdtest.New(t)
user := server.RandomInitialUser(t)
project, _ := setupProjectAndWorkspace(t, server.Client, user)
workspaces, err := server.Client.WorkspacesByProject(context.Background(), user.Organization, project.Name)
require.NoError(t, err)
require.Len(t, workspaces, 1)
})
t.Run("CreateInvalidInput", func(t *testing.T) {
t.Parallel()
server := coderdtest.New(t)
user := server.RandomInitialUser(t)
project, err := server.Client.CreateProject(context.Background(), user.Organization, coderd.CreateProjectRequest{
Name: "banana",
Provisioner: database.ProvisionerTypeEcho,
})
require.NoError(t, err)
_, err = server.Client.CreateWorkspace(context.Background(), "", coderd.CreateWorkspaceRequest{
ProjectID: project.ID,
Name: "$$$",
})
require.Error(t, err)
})
t.Run("CreateInvalidProject", func(t *testing.T) {
t.Parallel()
server := coderdtest.New(t)
_ = server.RandomInitialUser(t)
_, err := server.Client.CreateWorkspace(context.Background(), "", coderd.CreateWorkspaceRequest{
ProjectID: uuid.New(),
Name: "moo",
})
require.Error(t, err)
})
t.Run("CreateNotInProjectOrganization", func(t *testing.T) {
t.Parallel()
server := coderdtest.New(t)
initial := server.RandomInitialUser(t)
project, err := server.Client.CreateProject(context.Background(), initial.Organization, coderd.CreateProjectRequest{
Name: "banana",
Provisioner: database.ProvisionerTypeEcho,
})
require.NoError(t, err)
_, err = server.Client.CreateUser(context.Background(), coderd.CreateUserRequest{
Email: "hello@ok.io",
Username: "example",
Password: "password",
})
require.NoError(t, err)
token, err := server.Client.LoginWithPassword(context.Background(), coderd.LoginWithPasswordRequest{
Email: "hello@ok.io",
Password: "password",
})
require.NoError(t, err)
err = server.Client.SetSessionToken(token.SessionToken)
require.NoError(t, err)
_, err = server.Client.CreateWorkspace(context.Background(), "", coderd.CreateWorkspaceRequest{
ProjectID: project.ID,
Name: "moo",
})
require.Error(t, err)
})
t.Run("CreateAlreadyExists", func(t *testing.T) {
t.Parallel()
server := coderdtest.New(t)
user := server.RandomInitialUser(t)
project, workspace := setupProjectAndWorkspace(t, server.Client, user)
_, err := server.Client.CreateWorkspace(context.Background(), "", coderd.CreateWorkspaceRequest{
Name: workspace.Name,
ProjectID: project.ID,
})
require.Error(t, err)
})
t.Run("Single", func(t *testing.T) {
t.Parallel()
server := coderdtest.New(t)
user := server.RandomInitialUser(t)
_, workspace := setupProjectAndWorkspace(t, server.Client, user)
_, err := server.Client.Workspace(context.Background(), "", workspace.Name)
require.NoError(t, err)
})
}
func TestPostWorkspaceByUser(t *testing.T) {
t.Parallel()
t.Run("InvalidProject", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t)
_ = coderdtest.CreateInitialUser(t, client)
_, err := client.CreateWorkspace(context.Background(), "", coderd.CreateWorkspaceRequest{
ProjectID: 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("NoProjectAccess", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t)
user := coderdtest.CreateInitialUser(t, client)
project := coderdtest.CreateProject(t, client, user.Organization)
anotherUser := coderd.CreateUserRequest{
Email: "another@user.org",
Username: "someuser",
Password: "somepass",
}
_, err := client.CreateUser(context.Background(), anotherUser)
require.NoError(t, err)
token, err := client.LoginWithPassword(context.Background(), coderd.LoginWithPasswordRequest{
Email: anotherUser.Email,
Password: anotherUser.Password,
})
require.NoError(t, err)
err = client.SetSessionToken(token.SessionToken)
require.NoError(t, err)
_, err = client.CreateWorkspace(context.Background(), "", coderd.CreateWorkspaceRequest{
ProjectID: project.ID,
Name: "workspace",
})
require.Error(t, err)
var apiErr *codersdk.Error
require.ErrorAs(t, err, &apiErr)
require.Equal(t, http.StatusUnauthorized, apiErr.StatusCode())
})
t.Run("AlreadyExists", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t)
user := coderdtest.CreateInitialUser(t, client)
project := coderdtest.CreateProject(t, client, user.Organization)
workspace := coderdtest.CreateWorkspace(t, client, "", project.ID)
_, err := client.CreateWorkspace(context.Background(), "", coderd.CreateWorkspaceRequest{
ProjectID: project.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("Create", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t)
user := coderdtest.CreateInitialUser(t, client)
project := coderdtest.CreateProject(t, client, user.Organization)
_ = coderdtest.CreateWorkspace(t, client, "", project.ID)
})
}
func TestWorkspaceByUser(t *testing.T) {
t.Parallel()
client := coderdtest.New(t)
user := coderdtest.CreateInitialUser(t, client)
project := coderdtest.CreateProject(t, client, user.Organization)
workspace := coderdtest.CreateWorkspace(t, client, "", project.ID)
_, err := client.Workspace(context.Background(), "", workspace.Name)
require.NoError(t, err)
}
func TestWorkspacesByProject(t *testing.T) {
t.Parallel()
t.Run("ListEmpty", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t)
user := coderdtest.CreateInitialUser(t, client)
project := coderdtest.CreateProject(t, client, user.Organization)
workspaces, err := client.WorkspacesByProject(context.Background(), user.Organization, project.Name)
require.NoError(t, err)
require.NotNil(t, workspaces)
})
t.Run("List", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t)
user := coderdtest.CreateInitialUser(t, client)
project := coderdtest.CreateProject(t, client, user.Organization)
_ = coderdtest.CreateWorkspace(t, client, "", project.ID)
workspaces, err := client.WorkspacesByProject(context.Background(), user.Organization, project.Name)
require.NoError(t, err)
require.NotNil(t, workspaces)
require.Len(t, workspaces, 1)
})
}

View File

@ -10,6 +10,7 @@ import (
"net/http"
"net/http/cookiejar"
"net/url"
"strings"
"golang.org/x/xerrors"
@ -20,14 +21,15 @@ import (
// New creates a Coder client for the provided URL.
func New(serverURL *url.URL) *Client {
return &Client{
url: serverURL,
URL: serverURL,
httpClient: &http.Client{},
}
}
// Client is an HTTP caller for methods to the Coder API.
type Client struct {
url *url.URL
URL *url.URL
httpClient *http.Client
}
@ -40,7 +42,7 @@ func (c *Client) SetSessionToken(token string) error {
return err
}
}
c.httpClient.Jar.SetCookies(c.url, []*http.Cookie{{
c.httpClient.Jar.SetCookies(c.URL, []*http.Cookie{{
Name: httpmw.AuthCookie,
Value: token,
}})
@ -50,7 +52,7 @@ func (c *Client) SetSessionToken(token string) error {
// request performs an HTTP request with the body provided.
// The caller is responsible for closing the response body.
func (c *Client) request(ctx context.Context, method, path string, body interface{}) (*http.Response, error) {
serverURL, err := c.url.Parse(path)
serverURL, err := c.URL.Parse(path)
if err != nil {
return nil, xerrors.Errorf("parse url: %w", err)
}
@ -112,5 +114,10 @@ func (e *Error) StatusCode() int {
}
func (e *Error) Error() string {
return fmt.Sprintf("status code %d: %s", e.statusCode, e.Message)
var builder strings.Builder
_, _ = fmt.Fprintf(&builder, "status code %d: %s", e.statusCode, e.Message)
for _, err := range e.Errors {
_, _ = fmt.Fprintf(&builder, "\n\t%s: %s", err.Field, err.Code)
}
return builder.String()
}

View File

@ -1,8 +1,6 @@
package codersdk_test
import (
"archive/tar"
"bytes"
"context"
"testing"
@ -15,160 +13,183 @@ import (
func TestProjects(t *testing.T) {
t.Parallel()
t.Run("UnauthenticatedList", func(t *testing.T) {
t.Run("Error", func(t *testing.T) {
t.Parallel()
server := coderdtest.New(t)
_, err := server.Client.Projects(context.Background(), "")
client := coderdtest.New(t)
_, err := client.Projects(context.Background(), "")
require.Error(t, err)
})
t.Run("List", func(t *testing.T) {
t.Parallel()
server := coderdtest.New(t)
user := server.RandomInitialUser(t)
_, err := server.Client.Projects(context.Background(), "")
require.NoError(t, err)
_, err = server.Client.Projects(context.Background(), user.Organization)
client := coderdtest.New(t)
_ = coderdtest.CreateInitialUser(t, client)
_, err := client.Projects(context.Background(), "")
require.NoError(t, err)
})
}
t.Run("UnauthenticatedCreate", func(t *testing.T) {
func TestProject(t *testing.T) {
t.Parallel()
t.Run("Error", func(t *testing.T) {
t.Parallel()
server := coderdtest.New(t)
_, err := server.Client.CreateProject(context.Background(), "", coderd.CreateProjectRequest{})
client := coderdtest.New(t)
_, err := client.Project(context.Background(), "", "")
require.Error(t, err)
})
t.Run("Get", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t)
user := coderdtest.CreateInitialUser(t, client)
project := coderdtest.CreateProject(t, client, user.Organization)
_, err := client.Project(context.Background(), user.Organization, project.Name)
require.NoError(t, err)
})
}
func TestCreateProject(t *testing.T) {
t.Parallel()
t.Run("Error", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t)
_, err := client.CreateProject(context.Background(), "org", coderd.CreateProjectRequest{
Name: "something",
Provisioner: database.ProvisionerTypeEcho,
})
require.Error(t, err)
})
t.Run("Create", func(t *testing.T) {
t.Parallel()
server := coderdtest.New(t)
user := server.RandomInitialUser(t)
_, err := server.Client.CreateProject(context.Background(), user.Organization, coderd.CreateProjectRequest{
Name: "bananas",
Provisioner: database.ProvisionerTypeEcho,
})
require.NoError(t, err)
})
t.Run("UnauthenticatedSingle", func(t *testing.T) {
t.Parallel()
server := coderdtest.New(t)
_, err := server.Client.Project(context.Background(), "wow", "example")
require.Error(t, err)
})
t.Run("Single", func(t *testing.T) {
t.Parallel()
server := coderdtest.New(t)
user := server.RandomInitialUser(t)
_, err := server.Client.CreateProject(context.Background(), user.Organization, coderd.CreateProjectRequest{
Name: "bananas",
Provisioner: database.ProvisionerTypeEcho,
})
require.NoError(t, err)
_, err = server.Client.Project(context.Background(), user.Organization, "bananas")
require.NoError(t, err)
})
t.Run("UnauthenticatedHistory", func(t *testing.T) {
t.Parallel()
server := coderdtest.New(t)
_, err := server.Client.ProjectVersions(context.Background(), "org", "project")
require.Error(t, err)
})
t.Run("History", func(t *testing.T) {
t.Parallel()
server := coderdtest.New(t)
user := server.RandomInitialUser(t)
project, err := server.Client.CreateProject(context.Background(), user.Organization, coderd.CreateProjectRequest{
Name: "bananas",
Provisioner: database.ProvisionerTypeEcho,
})
require.NoError(t, err)
_, err = server.Client.ProjectVersions(context.Background(), user.Organization, project.Name)
require.NoError(t, err)
})
t.Run("CreateHistoryUnauthenticated", func(t *testing.T) {
t.Parallel()
server := coderdtest.New(t)
_, err := server.Client.CreateProjectVersion(context.Background(), "org", "project", coderd.CreateProjectVersionRequest{
StorageMethod: database.ProjectStorageMethodInlineArchive,
StorageSource: []byte{},
})
require.Error(t, err)
})
t.Run("CreateHistory", func(t *testing.T) {
t.Parallel()
server := coderdtest.New(t)
user := server.RandomInitialUser(t)
project, err := server.Client.CreateProject(context.Background(), user.Organization, coderd.CreateProjectRequest{
Name: "bananas",
Provisioner: database.ProvisionerTypeEcho,
})
require.NoError(t, err)
var buffer bytes.Buffer
writer := tar.NewWriter(&buffer)
err = writer.WriteHeader(&tar.Header{
Name: "file",
Size: 1 << 10,
})
require.NoError(t, err)
_, err = writer.Write(make([]byte, 1<<10))
require.NoError(t, err)
version, err := server.Client.CreateProjectVersion(context.Background(), user.Organization, project.Name, coderd.CreateProjectVersionRequest{
StorageMethod: database.ProjectStorageMethodInlineArchive,
StorageSource: buffer.Bytes(),
})
require.NoError(t, err)
_, err = server.Client.ProjectVersion(context.Background(), user.Organization, project.Name, version.Name)
require.NoError(t, err)
})
t.Run("Parameters", func(t *testing.T) {
t.Parallel()
server := coderdtest.New(t)
user := server.RandomInitialUser(t)
project, err := server.Client.CreateProject(context.Background(), user.Organization, coderd.CreateProjectRequest{
Name: "someproject",
Provisioner: database.ProvisionerTypeEcho,
})
require.NoError(t, err)
params, err := server.Client.ProjectParameters(context.Background(), user.Organization, project.Name)
require.NoError(t, err)
require.NotNil(t, params)
require.Len(t, params, 0)
})
t.Run("CreateParameter", func(t *testing.T) {
t.Parallel()
server := coderdtest.New(t)
user := server.RandomInitialUser(t)
project, err := server.Client.CreateProject(context.Background(), user.Organization, coderd.CreateProjectRequest{
Name: "someproject",
Provisioner: database.ProvisionerTypeEcho,
})
require.NoError(t, err)
param, err := server.Client.CreateProjectParameter(context.Background(), user.Organization, project.Name, coderd.CreateParameterValueRequest{
Name: "hi",
SourceValue: "tomato",
SourceScheme: database.ParameterSourceSchemeData,
DestinationScheme: database.ParameterDestinationSchemeEnvironmentVariable,
DestinationValue: "moo",
})
require.NoError(t, err)
require.Equal(t, "hi", param.Name)
})
t.Run("HistoryParametersError", func(t *testing.T) {
t.Parallel()
server := coderdtest.New(t)
user := server.RandomInitialUser(t)
_, err := server.Client.ProjectVersionParameters(context.Background(), user.Organization, "nothing", "nope")
require.Error(t, err)
client := coderdtest.New(t)
user := coderdtest.CreateInitialUser(t, client)
_ = coderdtest.CreateProject(t, client, user.Organization)
})
}
func TestProjectVersions(t *testing.T) {
t.Parallel()
t.Run("Error", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t)
_, err := client.ProjectVersions(context.Background(), "some", "project")
require.Error(t, err)
})
t.Run("List", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t)
user := coderdtest.CreateInitialUser(t, client)
project := coderdtest.CreateProject(t, client, user.Organization)
_, err := client.ProjectVersions(context.Background(), user.Organization, project.Name)
require.NoError(t, err)
})
}
func TestProjectVersion(t *testing.T) {
t.Parallel()
t.Run("Error", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t)
_, err := client.ProjectVersion(context.Background(), "some", "project", "version")
require.Error(t, err)
})
t.Run("Get", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t)
user := coderdtest.CreateInitialUser(t, client)
project := coderdtest.CreateProject(t, client, user.Organization)
version := coderdtest.CreateProjectVersion(t, client, user.Organization, project.Name, nil)
_, err := client.ProjectVersion(context.Background(), user.Organization, project.Name, version.Name)
require.NoError(t, err)
})
}
func TestCreateProjectVersion(t *testing.T) {
t.Parallel()
t.Run("Error", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t)
_, err := client.CreateProjectVersion(context.Background(), "some", "project", coderd.CreateProjectVersionRequest{})
require.Error(t, err)
})
t.Run("Create", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t)
user := coderdtest.CreateInitialUser(t, client)
project := coderdtest.CreateProject(t, client, user.Organization)
_ = coderdtest.CreateProjectVersion(t, client, user.Organization, project.Name, nil)
})
}
func TestProjectVersionParameters(t *testing.T) {
t.Parallel()
t.Run("Error", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t)
_, err := client.ProjectVersionParameters(context.Background(), "some", "project", "version")
require.Error(t, err)
})
t.Run("List", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t)
user := coderdtest.CreateInitialUser(t, client)
coderdtest.NewProvisionerDaemon(t, client)
project := coderdtest.CreateProject(t, client, user.Organization)
version := coderdtest.CreateProjectVersion(t, client, user.Organization, project.Name, nil)
coderdtest.AwaitProjectVersionImported(t, client, user.Organization, project.Name, version.Name)
_, err := client.ProjectVersionParameters(context.Background(), user.Organization, project.Name, version.Name)
require.NoError(t, err)
})
}
func TestProjectParameters(t *testing.T) {
t.Parallel()
t.Run("Error", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t)
_, err := client.ProjectParameters(context.Background(), "some", "project")
require.Error(t, err)
})
t.Run("List", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t)
user := coderdtest.CreateInitialUser(t, client)
coderdtest.NewProvisionerDaemon(t, client)
project := coderdtest.CreateProject(t, client, user.Organization)
_, err := client.ProjectParameters(context.Background(), user.Organization, project.Name)
require.NoError(t, err)
})
}
func TestCreateProjectParameter(t *testing.T) {
t.Parallel()
t.Run("Error", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t)
_, err := client.CreateProjectParameter(context.Background(), "some", "project", coderd.CreateParameterValueRequest{})
require.Error(t, err)
})
t.Run("Create", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t)
user := coderdtest.CreateInitialUser(t, client)
coderdtest.NewProvisionerDaemon(t, client)
project := coderdtest.CreateProject(t, client, user.Organization)
_, err := client.CreateProjectParameter(context.Background(), user.Organization, project.Name, coderd.CreateParameterValueRequest{
Name: "example",
SourceValue: "source-value",
SourceScheme: database.ParameterSourceSchemeData,
DestinationScheme: database.ParameterDestinationSchemeEnvironmentVariable,
DestinationValue: "destination-value",
})
require.NoError(t, err)
})
}

View File

@ -3,6 +3,7 @@ package codersdk
import (
"context"
"encoding/json"
"io"
"net/http"
"github.com/hashicorp/yamux"
@ -29,12 +30,14 @@ func (c *Client) ProvisionerDaemons(ctx context.Context) ([]coderd.ProvisionerDa
// ProvisionerDaemonClient returns the gRPC service for a provisioner daemon implementation.
func (c *Client) ProvisionerDaemonClient(ctx context.Context) (proto.DRPCProvisionerDaemonClient, error) {
serverURL, err := c.url.Parse("/api/v2/provisioners/daemons/serve")
serverURL, err := c.URL.Parse("/api/v2/provisioners/daemons/serve")
if err != nil {
return nil, xerrors.Errorf("parse url: %w", err)
}
conn, res, err := websocket.Dial(ctx, serverURL.String(), &websocket.DialOptions{
HTTPClient: c.httpClient,
// Need to disable compression to avoid a data-race.
CompressionMode: websocket.CompressionDisabled,
})
if err != nil {
if res == nil {
@ -42,7 +45,9 @@ func (c *Client) ProvisionerDaemonClient(ctx context.Context) (proto.DRPCProvisi
}
return nil, readBodyAsError(res)
}
session, err := yamux.Client(websocket.NetConn(context.Background(), conn, websocket.MessageBinary), nil)
config := yamux.DefaultConfig()
config.LogOutput = io.Discard
session, err := yamux.Client(websocket.NetConn(ctx, conn, websocket.MessageBinary), config)
if err != nil {
return nil, xerrors.Errorf("multiplex client: %w", err)
}

View File

@ -0,0 +1,46 @@
package codersdk_test
import (
"context"
"testing"
"github.com/stretchr/testify/require"
"github.com/coder/coder/coderd/coderdtest"
"github.com/coder/coder/provisionerd/proto"
)
func TestProvisionerDaemons(t *testing.T) {
t.Parallel()
t.Run("Get", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t)
_, err := client.ProvisionerDaemons(context.Background())
require.NoError(t, err)
})
}
func TestProvisionerDaemonClient(t *testing.T) {
t.Parallel()
t.Run("Error", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t)
ctx, cancelFunc := context.WithCancel(context.Background())
daemon, err := client.ProvisionerDaemonClient(ctx)
require.NoError(t, err)
cancelFunc()
_, err = daemon.AcquireJob(context.Background(), &proto.Empty{})
require.Error(t, err)
})
t.Run("Connect", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t)
ctx, cancelFunc := context.WithCancel(context.Background())
defer cancelFunc()
daemon, err := client.ProvisionerDaemonClient(ctx)
require.NoError(t, err)
_, err = daemon.AcquireJob(ctx, &proto.Empty{})
require.NoError(t, err)
})
}

View File

@ -10,61 +10,104 @@ import (
"github.com/coder/coder/coderd/coderdtest"
)
func TestUsers(t *testing.T) {
func TestCreateInitialUser(t *testing.T) {
t.Parallel()
t.Run("CreateInitial", func(t *testing.T) {
t.Run("Error", func(t *testing.T) {
t.Parallel()
server := coderdtest.New(t)
_, err := server.Client.CreateInitialUser(context.Background(), coderd.CreateInitialUserRequest{
Email: "wowie@coder.com",
Organization: "somethin",
Username: "tester",
Password: "moo",
})
require.NoError(t, err)
})
t.Run("NoUser", func(t *testing.T) {
t.Parallel()
server := coderdtest.New(t)
_, err := server.Client.User(context.Background(), "")
client := coderdtest.New(t)
_, err := client.CreateInitialUser(context.Background(), coderd.CreateInitialUserRequest{})
require.Error(t, err)
})
t.Run("User", func(t *testing.T) {
t.Run("Create", func(t *testing.T) {
t.Parallel()
server := coderdtest.New(t)
_ = server.RandomInitialUser(t)
_, err := server.Client.User(context.Background(), "")
require.NoError(t, err)
client := coderdtest.New(t)
_ = coderdtest.CreateInitialUser(t, client)
})
}
func TestCreateUser(t *testing.T) {
t.Parallel()
t.Run("Error", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t)
_, err := client.CreateUser(context.Background(), coderd.CreateUserRequest{})
require.Error(t, err)
})
t.Run("UserOrganizations", func(t *testing.T) {
t.Run("Create", func(t *testing.T) {
t.Parallel()
server := coderdtest.New(t)
_ = server.RandomInitialUser(t)
orgs, err := server.Client.UserOrganizations(context.Background(), "")
require.NoError(t, err)
require.Len(t, orgs, 1)
})
t.Run("LogoutIsSuccessful", func(t *testing.T) {
t.Parallel()
server := coderdtest.New(t)
_ = server.RandomInitialUser(t)
err := server.Client.Logout(context.Background())
require.NoError(t, err)
})
t.Run("CreateMultiple", func(t *testing.T) {
t.Parallel()
server := coderdtest.New(t)
_ = server.RandomInitialUser(t)
_, err := server.Client.CreateUser(context.Background(), coderd.CreateUserRequest{
Email: "wow@ok.io",
Username: "example",
Password: "tomato",
client := coderdtest.New(t)
_ = coderdtest.CreateInitialUser(t, client)
_, err := client.CreateUser(context.Background(), coderd.CreateUserRequest{
Email: "example@coder.com",
Username: "something",
Password: "password",
})
require.NoError(t, err)
})
}
func TestLoginWithPassword(t *testing.T) {
t.Parallel()
t.Run("Error", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t)
_, err := client.LoginWithPassword(context.Background(), coderd.LoginWithPasswordRequest{})
require.Error(t, err)
})
t.Run("Success", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t)
user := coderdtest.CreateInitialUser(t, client)
_, err := client.LoginWithPassword(context.Background(), coderd.LoginWithPasswordRequest{
Email: user.Email,
Password: user.Password,
})
require.NoError(t, err)
})
}
func TestLogout(t *testing.T) {
t.Parallel()
client := coderdtest.New(t)
err := client.Logout(context.Background())
require.NoError(t, err)
}
func TestUser(t *testing.T) {
t.Parallel()
t.Run("Error", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t)
_, err := client.User(context.Background(), "")
require.Error(t, err)
})
t.Run("Get", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t)
_ = coderdtest.CreateInitialUser(t, client)
_, err := client.User(context.Background(), "")
require.NoError(t, err)
})
}
func TestUserOrganizations(t *testing.T) {
t.Parallel()
t.Run("Error", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t)
_, err := client.UserOrganizations(context.Background(), "")
require.Error(t, err)
})
t.Run("List", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t)
_ = coderdtest.CreateInitialUser(t, client)
_, err := client.UserOrganizations(context.Background(), "")
require.NoError(t, err)
})
}

View File

@ -15,7 +15,7 @@ import (
// Workspaces returns all workspaces the authenticated session has access to.
// If owner is specified, all workspaces for an organization will be returned.
// If owner is empty, all workspaces the caller has access to will be returned.
func (c *Client) WorkspacesByUser(ctx context.Context, user string) ([]coderd.Workspace, error) {
func (c *Client) Workspaces(ctx context.Context, user string) ([]coderd.Workspace, error) {
route := "/api/v2/workspaces"
if user != "" {
route += fmt.Sprintf("/%s", user)

View File

@ -3,167 +3,233 @@ package codersdk_test
import (
"context"
"testing"
"time"
"github.com/google/uuid"
"github.com/stretchr/testify/require"
"github.com/coder/coder/coderd"
"github.com/coder/coder/coderd/coderdtest"
"github.com/coder/coder/database"
"github.com/coder/coder/provisioner/echo"
"github.com/coder/coder/provisionersdk/proto"
)
func TestWorkspaces(t *testing.T) {
t.Parallel()
t.Run("ListError", func(t *testing.T) {
t.Run("Error", func(t *testing.T) {
t.Parallel()
server := coderdtest.New(t)
_, err := server.Client.WorkspacesByUser(context.Background(), "")
client := coderdtest.New(t)
_, err := client.Workspaces(context.Background(), "")
require.Error(t, err)
})
t.Run("ListNoOwner", func(t *testing.T) {
t.Run("List", func(t *testing.T) {
t.Parallel()
server := coderdtest.New(t)
_, err := server.Client.WorkspacesByUser(context.Background(), "")
require.Error(t, err)
})
t.Run("ListByUser", func(t *testing.T) {
t.Parallel()
server := coderdtest.New(t)
user := server.RandomInitialUser(t)
project, err := server.Client.CreateProject(context.Background(), user.Organization, coderd.CreateProjectRequest{
Name: "tomato",
Provisioner: database.ProvisionerTypeEcho,
})
client := coderdtest.New(t)
_ = coderdtest.CreateInitialUser(t, client)
_, err := client.Workspaces(context.Background(), "")
require.NoError(t, err)
_, err = server.Client.CreateWorkspace(context.Background(), "", coderd.CreateWorkspaceRequest{
Name: "wow",
ProjectID: project.ID,
})
require.NoError(t, err)
_, err = server.Client.WorkspacesByUser(context.Background(), "me")
require.NoError(t, err)
})
t.Run("ListByProject", func(t *testing.T) {
t.Parallel()
server := coderdtest.New(t)
user := server.RandomInitialUser(t)
project, err := server.Client.CreateProject(context.Background(), user.Organization, coderd.CreateProjectRequest{
Name: "tomato",
Provisioner: database.ProvisionerTypeEcho,
})
require.NoError(t, err)
_, err = server.Client.CreateWorkspace(context.Background(), "", coderd.CreateWorkspaceRequest{
Name: "wow",
ProjectID: project.ID,
})
require.NoError(t, err)
_, err = server.Client.WorkspacesByProject(context.Background(), user.Organization, project.Name)
require.NoError(t, err)
})
t.Run("ListByProjectError", func(t *testing.T) {
t.Parallel()
server := coderdtest.New(t)
_, err := server.Client.WorkspacesByProject(context.Background(), "", "")
require.Error(t, err)
})
t.Run("CreateError", func(t *testing.T) {
t.Parallel()
server := coderdtest.New(t)
_, err := server.Client.CreateWorkspace(context.Background(), "no", coderd.CreateWorkspaceRequest{})
require.Error(t, err)
})
t.Run("Single", func(t *testing.T) {
t.Parallel()
server := coderdtest.New(t)
user := server.RandomInitialUser(t)
project, err := server.Client.CreateProject(context.Background(), user.Organization, coderd.CreateProjectRequest{
Name: "tomato",
Provisioner: database.ProvisionerTypeEcho,
})
require.NoError(t, err)
workspace, err := server.Client.CreateWorkspace(context.Background(), "", coderd.CreateWorkspaceRequest{
Name: "wow",
ProjectID: project.ID,
})
require.NoError(t, err)
_, err = server.Client.Workspace(context.Background(), "", workspace.Name)
require.NoError(t, err)
})
t.Run("SingleError", func(t *testing.T) {
t.Parallel()
server := coderdtest.New(t)
_, err := server.Client.Workspace(context.Background(), "", "blob")
require.Error(t, err)
})
t.Run("History", func(t *testing.T) {
t.Parallel()
server := coderdtest.New(t)
user := server.RandomInitialUser(t)
project, err := server.Client.CreateProject(context.Background(), user.Organization, coderd.CreateProjectRequest{
Name: "tomato",
Provisioner: database.ProvisionerTypeEcho,
})
require.NoError(t, err)
workspace, err := server.Client.CreateWorkspace(context.Background(), "", coderd.CreateWorkspaceRequest{
Name: "wow",
ProjectID: project.ID,
})
require.NoError(t, err)
_, err = server.Client.ListWorkspaceHistory(context.Background(), "", workspace.Name)
require.NoError(t, err)
})
t.Run("HistoryError", func(t *testing.T) {
t.Parallel()
server := coderdtest.New(t)
_, err := server.Client.ListWorkspaceHistory(context.Background(), "", "blob")
require.Error(t, err)
})
t.Run("LatestHistory", func(t *testing.T) {
t.Parallel()
server := coderdtest.New(t)
user := server.RandomInitialUser(t)
project, err := server.Client.CreateProject(context.Background(), user.Organization, coderd.CreateProjectRequest{
Name: "tomato",
Provisioner: database.ProvisionerTypeEcho,
})
require.NoError(t, err)
workspace, err := server.Client.CreateWorkspace(context.Background(), "", coderd.CreateWorkspaceRequest{
Name: "wow",
ProjectID: project.ID,
})
require.NoError(t, err)
_, err = server.Client.WorkspaceHistory(context.Background(), "", workspace.Name, "")
require.Error(t, err)
})
t.Run("CreateHistory", func(t *testing.T) {
t.Parallel()
server := coderdtest.New(t)
user := server.RandomInitialUser(t)
project, err := server.Client.CreateProject(context.Background(), user.Organization, coderd.CreateProjectRequest{
Name: "tomato",
Provisioner: database.ProvisionerTypeEcho,
})
require.NoError(t, err)
workspace, err := server.Client.CreateWorkspace(context.Background(), "", coderd.CreateWorkspaceRequest{
Name: "wow",
ProjectID: project.ID,
})
require.NoError(t, err)
_, err = server.Client.CreateWorkspaceHistory(context.Background(), "", workspace.Name, coderd.CreateWorkspaceHistoryRequest{
ProjectVersionID: uuid.New(),
Transition: database.WorkspaceTransitionCreate,
})
require.Error(t, err)
})
}
func TestWorkspacesByProject(t *testing.T) {
t.Parallel()
t.Run("Error", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t)
_, err := client.WorkspacesByProject(context.Background(), "", "")
require.Error(t, err)
})
t.Run("List", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t)
user := coderdtest.CreateInitialUser(t, client)
project := coderdtest.CreateProject(t, client, user.Organization)
_, err := client.WorkspacesByProject(context.Background(), user.Organization, project.Name)
require.NoError(t, err)
})
}
func TestWorkspace(t *testing.T) {
t.Parallel()
t.Run("Error", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t)
_, err := client.Workspace(context.Background(), "", "")
require.Error(t, err)
})
t.Run("Get", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t)
user := coderdtest.CreateInitialUser(t, client)
project := coderdtest.CreateProject(t, client, user.Organization)
workspace := coderdtest.CreateWorkspace(t, client, "", project.ID)
_, err := client.Workspace(context.Background(), "", workspace.Name)
require.NoError(t, err)
})
}
func TestListWorkspaceHistory(t *testing.T) {
t.Parallel()
t.Run("Error", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t)
_, err := client.ListWorkspaceHistory(context.Background(), "", "")
require.Error(t, err)
})
t.Run("List", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t)
user := coderdtest.CreateInitialUser(t, client)
project := coderdtest.CreateProject(t, client, user.Organization)
workspace := coderdtest.CreateWorkspace(t, client, "", project.ID)
_, err := client.ListWorkspaceHistory(context.Background(), "", workspace.Name)
require.NoError(t, err)
})
}
func TestWorkspaceHistory(t *testing.T) {
t.Parallel()
t.Run("Error", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t)
_, err := client.WorkspaceHistory(context.Background(), "", "", "")
require.Error(t, err)
})
t.Run("Get", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t)
user := coderdtest.CreateInitialUser(t, client)
_ = coderdtest.NewProvisionerDaemon(t, client)
project := coderdtest.CreateProject(t, client, user.Organization)
version := coderdtest.CreateProjectVersion(t, client, user.Organization, project.Name, nil)
coderdtest.AwaitProjectVersionImported(t, client, user.Organization, project.Name, version.Name)
workspace := coderdtest.CreateWorkspace(t, client, "", project.ID)
_, err := client.CreateWorkspaceHistory(context.Background(), "", workspace.Name, coderd.CreateWorkspaceHistoryRequest{
ProjectVersionID: version.ID,
Transition: database.WorkspaceTransitionCreate,
})
require.NoError(t, err)
})
}
func TestCreateWorkspace(t *testing.T) {
t.Parallel()
t.Run("Error", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t)
_, err := client.CreateWorkspace(context.Background(), "", coderd.CreateWorkspaceRequest{})
require.Error(t, err)
})
t.Run("Get", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t)
user := coderdtest.CreateInitialUser(t, client)
project := coderdtest.CreateProject(t, client, user.Organization)
_ = coderdtest.CreateWorkspace(t, client, "", project.ID)
})
}
func TestCreateWorkspaceHistory(t *testing.T) {
t.Parallel()
t.Run("Error", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t)
_, err := client.CreateWorkspaceHistory(context.Background(), "", "", coderd.CreateWorkspaceHistoryRequest{})
require.Error(t, err)
})
t.Run("Create", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t)
user := coderdtest.CreateInitialUser(t, client)
_ = coderdtest.NewProvisionerDaemon(t, client)
project := coderdtest.CreateProject(t, client, user.Organization)
version := coderdtest.CreateProjectVersion(t, client, user.Organization, project.Name, nil)
coderdtest.AwaitProjectVersionImported(t, client, user.Organization, project.Name, version.Name)
workspace := coderdtest.CreateWorkspace(t, client, "", project.ID)
_, err := client.CreateWorkspaceHistory(context.Background(), "", workspace.Name, coderd.CreateWorkspaceHistoryRequest{
ProjectVersionID: version.ID,
Transition: database.WorkspaceTransitionCreate,
})
require.NoError(t, err)
})
}
func TestWorkspaceHistoryLogs(t *testing.T) {
t.Parallel()
t.Run("Error", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t)
_, err := client.WorkspaceHistoryLogs(context.Background(), "", "", "")
require.Error(t, err)
})
t.Run("List", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t)
user := coderdtest.CreateInitialUser(t, client)
_ = coderdtest.NewProvisionerDaemon(t, client)
project := coderdtest.CreateProject(t, client, user.Organization)
version := coderdtest.CreateProjectVersion(t, client, user.Organization, project.Name, nil)
coderdtest.AwaitProjectVersionImported(t, client, user.Organization, project.Name, version.Name)
workspace := coderdtest.CreateWorkspace(t, client, "", project.ID)
history, err := client.CreateWorkspaceHistory(context.Background(), "", workspace.Name, coderd.CreateWorkspaceHistoryRequest{
ProjectVersionID: version.ID,
Transition: database.WorkspaceTransitionCreate,
})
require.NoError(t, err)
_, err = client.WorkspaceHistoryLogs(context.Background(), "", workspace.Name, history.Name)
require.NoError(t, err)
})
}
func TestFollowWorkspaceHistoryLogsAfter(t *testing.T) {
t.Parallel()
t.Run("Error", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t)
_, err := client.FollowWorkspaceHistoryLogsAfter(context.Background(), "", "", "", time.Time{})
require.Error(t, err)
})
t.Run("Stream", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t)
user := coderdtest.CreateInitialUser(t, client)
_ = coderdtest.NewProvisionerDaemon(t, client)
project := coderdtest.CreateProject(t, client, user.Organization)
version := coderdtest.CreateProjectVersion(t, client, user.Organization, project.Name, &echo.Responses{
Parse: echo.ParseComplete,
Provision: []*proto.Provision_Response{{
Type: &proto.Provision_Response_Log{
Log: &proto.Log{
Output: "hello",
},
},
}, {
Type: &proto.Provision_Response_Complete{
Complete: &proto.Provision_Complete{},
},
}},
})
coderdtest.AwaitProjectVersionImported(t, client, user.Organization, project.Name, version.Name)
workspace := coderdtest.CreateWorkspace(t, client, "", project.ID)
history, err := client.CreateWorkspaceHistory(context.Background(), "", workspace.Name, coderd.CreateWorkspaceHistoryRequest{
ProjectVersionID: version.ID,
Transition: database.WorkspaceTransitionCreate,
})
require.NoError(t, err)
logs, err := client.FollowWorkspaceHistoryLogsAfter(context.Background(), "", workspace.Name, history.Name, time.Time{})
require.NoError(t, err)
_, ok := <-logs
require.True(t, ok)
_, ok = <-logs
require.False(t, ok)
})
}

64
database/dump.sql generated
View File

@ -137,6 +137,26 @@ CREATE TABLE project (
active_version_id uuid
);
CREATE TABLE project_parameter (
id uuid NOT NULL,
created_at timestamp with time zone NOT NULL,
project_version_id uuid NOT NULL,
name character varying(64) NOT NULL,
description character varying(8192) DEFAULT ''::character varying NOT NULL,
default_source_scheme parameter_source_scheme,
default_source_value text,
allow_override_source boolean NOT NULL,
default_destination_scheme parameter_destination_scheme,
default_destination_value text,
allow_override_destination boolean NOT NULL,
default_refresh text NOT NULL,
redisplay_value boolean NOT NULL,
validation_error character varying(256) NOT NULL,
validation_condition character varying(512) NOT NULL,
validation_type_system parameter_type_system NOT NULL,
validation_value_type character varying(64) NOT NULL
);
CREATE TABLE project_version (
id uuid NOT NULL,
project_id uuid NOT NULL,
@ -158,26 +178,6 @@ CREATE TABLE project_version_log (
output character varying(1024) NOT NULL
);
CREATE TABLE project_parameter (
id uuid NOT NULL,
created_at timestamp with time zone NOT NULL,
project_version_id uuid NOT NULL,
name character varying(64) NOT NULL,
description character varying(8192) DEFAULT ''::character varying NOT NULL,
default_source_scheme parameter_source_scheme,
default_source_value text,
allow_override_source boolean NOT NULL,
default_destination_scheme parameter_destination_scheme,
default_destination_value text,
allow_override_destination boolean NOT NULL,
default_refresh text NOT NULL,
redisplay_value boolean NOT NULL,
validation_error character varying(256) NOT NULL,
validation_condition character varying(512) NOT NULL,
validation_type_system parameter_type_system NOT NULL,
validation_value_type character varying(64) NOT NULL
);
CREATE TABLE provisioner_daemon (
id uuid NOT NULL,
created_at timestamp with time zone NOT NULL,
@ -282,15 +282,6 @@ ALTER TABLE ONLY parameter_value
ALTER TABLE ONLY parameter_value
ADD CONSTRAINT parameter_value_name_scope_scope_id_key UNIQUE (name, scope, scope_id);
ALTER TABLE ONLY project_version
ADD CONSTRAINT project_version_id_key UNIQUE (id);
ALTER TABLE ONLY project_version_log
ADD CONSTRAINT project_version_log_id_key UNIQUE (id);
ALTER TABLE ONLY project_version
ADD CONSTRAINT project_version_project_id_name_key UNIQUE (project_id, name);
ALTER TABLE ONLY project
ADD CONSTRAINT project_id_key UNIQUE (id);
@ -303,6 +294,15 @@ ALTER TABLE ONLY project_parameter
ALTER TABLE ONLY project_parameter
ADD CONSTRAINT project_parameter_project_version_id_name_key UNIQUE (project_version_id, name);
ALTER TABLE ONLY project_version
ADD CONSTRAINT project_version_id_key UNIQUE (id);
ALTER TABLE ONLY project_version_log
ADD CONSTRAINT project_version_log_id_key UNIQUE (id);
ALTER TABLE ONLY project_version
ADD CONSTRAINT project_version_project_id_name_key UNIQUE (project_id, name);
ALTER TABLE ONLY provisioner_daemon
ADD CONSTRAINT provisioner_daemon_id_key UNIQUE (id);
@ -339,15 +339,15 @@ ALTER TABLE ONLY workspace_resource
ALTER TABLE ONLY workspace_resource
ADD CONSTRAINT workspace_resource_workspace_history_id_name_key UNIQUE (workspace_history_id, name);
ALTER TABLE ONLY project_parameter
ADD CONSTRAINT project_parameter_project_version_id_fkey FOREIGN KEY (project_version_id) REFERENCES project_version(id) ON DELETE CASCADE;
ALTER TABLE ONLY project_version_log
ADD CONSTRAINT project_version_log_project_version_id_fkey FOREIGN KEY (project_version_id) REFERENCES project_version(id) ON DELETE CASCADE;
ALTER TABLE ONLY project_version
ADD CONSTRAINT project_version_project_id_fkey FOREIGN KEY (project_id) REFERENCES project(id);
ALTER TABLE ONLY project_parameter
ADD CONSTRAINT project_parameter_project_version_id_fkey FOREIGN KEY (project_version_id) REFERENCES project_version(id) ON DELETE CASCADE;
ALTER TABLE ONLY provisioner_job
ADD CONSTRAINT provisioner_job_project_id_fkey FOREIGN KEY (project_id) REFERENCES project(id) ON DELETE CASCADE;

1
go.mod
View File

@ -28,6 +28,7 @@ require (
github.com/pion/logging v0.2.2
github.com/pion/transport v0.13.0
github.com/pion/webrtc/v3 v3.1.21
github.com/quasilyte/go-ruleguard/dsl v0.3.16
github.com/spf13/cobra v1.3.0
github.com/stretchr/testify v1.7.0
github.com/unrolled/secure v1.0.9

2
go.sum
View File

@ -1087,6 +1087,8 @@ github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4O
github.com/prometheus/procfs v0.2.0/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=
github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
github.com/quasilyte/go-ruleguard/dsl v0.3.16 h1:yJtIpd4oyNS+/c/gKqxNwoGO9+lPOsy1A4BzKjJRcrI=
github.com/quasilyte/go-ruleguard/dsl v0.3.16/go.mod h1:KeCP03KrjuSO0H1kTuZQCWlQPulDV6YMIXmpQss17rU=
github.com/remyoudompheng/bigfft v0.0.0-20190728182440-6a916e37a237/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=

View File

@ -2,7 +2,6 @@ package peer_test
import (
"context"
"errors"
"io"
"net"
"net/http"
@ -17,6 +16,7 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.uber.org/goleak"
"golang.org/x/xerrors"
"cdr.dev/slog"
"cdr.dev/slog/sloggers/slogtest"
@ -231,7 +231,7 @@ func TestConn(t *testing.T) {
t.Parallel()
conn, err := peer.Client([]webrtc.ICEServer{}, nil)
require.NoError(t, err)
expectedErr := errors.New("wow")
expectedErr := xerrors.New("wow")
_ = conn.CloseWithError(expectedErr)
_, err = conn.Dial(context.Background(), "", nil)
require.ErrorIs(t, err, expectedErr)

View File

@ -48,6 +48,10 @@ func (*echo) Parse(request *proto.Parse_Request, stream proto.DRPCProvisioner_Pa
path := filepath.Join(request.Directory, fmt.Sprintf("%d.parse.protobuf", index))
_, err := os.Stat(path)
if err != nil {
if index == 0 {
// Error if nothing is around to enable failed states.
return xerrors.New("no state")
}
break
}
data, err := os.ReadFile(path)
@ -64,7 +68,8 @@ func (*echo) Parse(request *proto.Parse_Request, stream proto.DRPCProvisioner_Pa
return err
}
}
return nil
<-stream.Context().Done()
return stream.Context().Err()
}
// Provision reads requests from the provided directory to stream responses.
@ -73,6 +78,10 @@ func (*echo) Provision(request *proto.Provision_Request, stream proto.DRPCProvis
path := filepath.Join(request.Directory, fmt.Sprintf("%d.provision.protobuf", index))
_, err := os.Stat(path)
if err != nil {
if index == 0 {
// Error if nothing is around to enable failed states.
return xerrors.New("no state")
}
break
}
data, err := os.ReadFile(path)
@ -89,14 +98,24 @@ func (*echo) Provision(request *proto.Provision_Request, stream proto.DRPCProvis
return err
}
}
return nil
<-stream.Context().Done()
return stream.Context().Err()
}
type Responses struct {
Parse []*proto.Parse_Response
Provision []*proto.Provision_Response
}
// Tar returns a tar archive of responses to provisioner operations.
func Tar(parseResponses []*proto.Parse_Response, provisionResponses []*proto.Provision_Response) ([]byte, error) {
func Tar(responses *Responses) ([]byte, error) {
if responses == nil {
responses = &Responses{ParseComplete, ProvisionComplete}
}
var buffer bytes.Buffer
writer := tar.NewWriter(&buffer)
for index, response := range parseResponses {
for index, response := range responses.Parse {
data, err := protobuf.Marshal(response)
if err != nil {
return nil, err
@ -113,7 +132,7 @@ func Tar(parseResponses []*proto.Parse_Response, provisionResponses []*proto.Pro
return nil, err
}
}
for index, response := range provisionResponses {
for index, response := range responses.Provision {
data, err := protobuf.Marshal(response)
if err != nil {
return nil, err

View File

@ -53,7 +53,9 @@ func TestEcho(t *testing.T) {
},
},
}}
data, err := echo.Tar(responses, nil)
data, err := echo.Tar(&echo.Responses{
Parse: responses,
})
require.NoError(t, err)
client, err := api.Parse(ctx, &proto.Parse_Request{
Directory: unpackTar(t, data),
@ -86,7 +88,9 @@ func TestEcho(t *testing.T) {
},
},
}}
data, err := echo.Tar(nil, responses)
data, err := echo.Tar(&echo.Responses{
Provision: responses,
})
require.NoError(t, err)
client, err := api.Provision(ctx, &proto.Provision_Request{
Directory: unpackTar(t, data),

View File

@ -15,6 +15,7 @@ import (
"time"
"github.com/hashicorp/yamux"
"go.uber.org/atomic"
"cdr.dev/slog"
"github.com/coder/coder/provisionerd/proto"
@ -54,7 +55,8 @@ func New(clientDialer Dialer, opts *Options) io.Closer {
closeCancel: ctxCancel,
closed: make(chan struct{}),
jobRunning: make(chan struct{}),
jobRunning: make(chan struct{}),
jobCancelled: *atomic.NewBool(true),
}
// Start off with a closed channel so
// isRunningJob() returns properly.
@ -77,10 +79,11 @@ type provisionerDaemon struct {
closeError error
// Locked when acquiring or canceling a job.
jobMutex sync.Mutex
jobID string
jobRunning chan struct{}
jobCancel context.CancelFunc
jobMutex sync.Mutex
jobID string
jobRunning chan struct{}
jobCancelled atomic.Bool
jobCancel context.CancelFunc
}
// Connect establishes a connection to coderd.
@ -193,6 +196,7 @@ func (p *provisionerDaemon) acquireJob(ctx context.Context) {
}
ctx, p.jobCancel = context.WithCancel(ctx)
p.jobRunning = make(chan struct{})
p.jobCancelled.Store(false)
p.jobID = job.JobId
p.opts.Logger.Info(context.Background(), "acquired job",
@ -220,7 +224,7 @@ func (p *provisionerDaemon) runJob(ctx context.Context, job *proto.AcquiredJob)
JobId: job.JobId,
})
if err != nil {
go p.cancelActiveJobf("send periodic update: %s", err)
p.cancelActiveJobf("send periodic update: %s", err)
return
}
}
@ -247,13 +251,13 @@ func (p *provisionerDaemon) runJob(ctx context.Context, job *proto.AcquiredJob)
// It's safe to cast this ProvisionerType. This data is coming directly from coderd.
provisioner, hasProvisioner := p.opts.Provisioners[job.Provisioner]
if !hasProvisioner {
go p.cancelActiveJobf("provisioner %q not registered", job.Provisioner)
p.cancelActiveJobf("provisioner %q not registered", job.Provisioner)
return
}
err := os.MkdirAll(p.opts.WorkDirectory, 0700)
if err != nil {
go p.cancelActiveJobf("create work directory %q: %s", p.opts.WorkDirectory, err)
p.cancelActiveJobf("create work directory %q: %s", p.opts.WorkDirectory, err)
return
}
@ -265,13 +269,13 @@ func (p *provisionerDaemon) runJob(ctx context.Context, job *proto.AcquiredJob)
break
}
if err != nil {
go p.cancelActiveJobf("read project source archive: %s", err)
p.cancelActiveJobf("read project source archive: %s", err)
return
}
// #nosec
path := filepath.Join(p.opts.WorkDirectory, header.Name)
if !strings.HasPrefix(path, filepath.Clean(p.opts.WorkDirectory)) {
go p.cancelActiveJobf("tar attempts to target relative upper directory")
p.cancelActiveJobf("tar attempts to target relative upper directory")
return
}
mode := header.FileInfo().Mode()
@ -282,14 +286,14 @@ func (p *provisionerDaemon) runJob(ctx context.Context, job *proto.AcquiredJob)
case tar.TypeDir:
err = os.MkdirAll(path, mode)
if err != nil {
go p.cancelActiveJobf("mkdir %q: %s", path, err)
p.cancelActiveJobf("mkdir %q: %s", path, err)
return
}
p.opts.Logger.Debug(context.Background(), "extracted directory", slog.F("path", path))
case tar.TypeReg:
file, err := os.OpenFile(path, os.O_CREATE|os.O_RDWR, mode)
if err != nil {
go p.cancelActiveJobf("create file %q (mode %s): %s", path, mode, err)
p.cancelActiveJobf("create file %q (mode %s): %s", path, mode, err)
return
}
// Max file size of 10MB.
@ -299,12 +303,12 @@ func (p *provisionerDaemon) runJob(ctx context.Context, job *proto.AcquiredJob)
}
if err != nil {
_ = file.Close()
go p.cancelActiveJobf("copy file %q: %s", path, err)
p.cancelActiveJobf("copy file %q: %s", path, err)
return
}
err = file.Close()
if err != nil {
go p.cancelActiveJobf("close file %q: %s", path, err)
p.cancelActiveJobf("close file %q: %s", path, err)
return
}
p.opts.Logger.Debug(context.Background(), "extracted file",
@ -331,7 +335,7 @@ func (p *provisionerDaemon) runJob(ctx context.Context, job *proto.AcquiredJob)
p.runWorkspaceProvision(ctx, provisioner, job)
default:
go p.cancelActiveJobf("unknown job type %q; ensure your provisioner daemon is up-to-date", reflect.TypeOf(job.Type).String())
p.cancelActiveJobf("unknown job type %q; ensure your provisioner daemon is up-to-date", reflect.TypeOf(job.Type).String())
return
}
@ -347,14 +351,14 @@ func (p *provisionerDaemon) runProjectImport(ctx context.Context, provisioner sd
Directory: p.opts.WorkDirectory,
})
if err != nil {
go p.cancelActiveJobf("parse source: %s", err)
p.cancelActiveJobf("parse source: %s", err)
return
}
defer stream.Close()
for {
msg, err := stream.Recv()
if err != nil {
go p.cancelActiveJobf("recv parse source: %s", err)
p.cancelActiveJobf("recv parse source: %s", err)
return
}
switch msgType := msg.Type.(type) {
@ -375,7 +379,7 @@ func (p *provisionerDaemon) runProjectImport(ctx context.Context, provisioner sd
}},
})
if err != nil {
go p.cancelActiveJobf("update job: %s", err)
p.cancelActiveJobf("update job: %s", err)
return
}
case *sdkproto.Parse_Response_Complete:
@ -391,13 +395,13 @@ func (p *provisionerDaemon) runProjectImport(ctx context.Context, provisioner sd
},
})
if err != nil {
go p.cancelActiveJobf("complete job: %s", err)
p.cancelActiveJobf("complete job: %s", err)
return
}
// Return so we stop looping!
return
default:
go p.cancelActiveJobf("invalid message type %q received from provisioner",
p.cancelActiveJobf("invalid message type %q received from provisioner",
reflect.TypeOf(msg.Type).String())
return
}
@ -411,7 +415,7 @@ func (p *provisionerDaemon) runWorkspaceProvision(ctx context.Context, provision
State: job.GetWorkspaceProvision().State,
})
if err != nil {
go p.cancelActiveJobf("provision: %s", err)
p.cancelActiveJobf("provision: %s", err)
return
}
defer stream.Close()
@ -419,7 +423,7 @@ func (p *provisionerDaemon) runWorkspaceProvision(ctx context.Context, provision
for {
msg, err := stream.Recv()
if err != nil {
go p.cancelActiveJobf("recv workspace provision: %s", err)
p.cancelActiveJobf("recv workspace provision: %s", err)
return
}
switch msgType := msg.Type.(type) {
@ -440,7 +444,7 @@ func (p *provisionerDaemon) runWorkspaceProvision(ctx context.Context, provision
}},
})
if err != nil {
go p.cancelActiveJobf("send job update: %s", err)
p.cancelActiveJobf("send job update: %s", err)
return
}
case *sdkproto.Provision_Response_Complete:
@ -462,13 +466,13 @@ func (p *provisionerDaemon) runWorkspaceProvision(ctx context.Context, provision
},
})
if err != nil {
go p.cancelActiveJobf("complete job: %s", err)
p.cancelActiveJobf("complete job: %s", err)
return
}
// Return so we stop looping!
return
default:
go p.cancelActiveJobf("invalid message type %q received from provisioner",
p.cancelActiveJobf("invalid message type %q received from provisioner",
reflect.TypeOf(msg.Type).String())
return
}
@ -481,12 +485,16 @@ func (p *provisionerDaemon) cancelActiveJobf(format string, args ...interface{})
errMsg := fmt.Sprintf(format, args...)
if !p.isRunningJob() {
if p.isClosed() {
// We don't want to log if we're already closed!
return
}
p.opts.Logger.Warn(context.Background(), "skipping job cancel; none running", slog.F("error_message", errMsg))
p.opts.Logger.Info(context.Background(), "skipping job cancel; none running", slog.F("error_message", errMsg))
return
}
if p.jobCancelled.Load() {
p.opts.Logger.Warn(context.Background(), "job has already been canceled", slog.F("error_messsage", errMsg))
return
}
p.jobCancelled.Store(true)
p.jobCancel()
p.opts.Logger.Info(context.Background(), "canceling running job",
slog.F("error_message", errMsg),
@ -500,7 +508,6 @@ func (p *provisionerDaemon) cancelActiveJobf(format string, args ...interface{})
p.opts.Logger.Warn(context.Background(), "failed to notify of cancel; job is no longer running", slog.Error(err))
return
}
<-p.jobRunning
p.opts.Logger.Debug(context.Background(), "canceled running job")
}
@ -534,6 +541,7 @@ func (p *provisionerDaemon) closeWithError(err error) error {
errMsg = err.Error()
}
p.cancelActiveJobf(errMsg)
<-p.jobRunning
p.closeCancel()
p.opts.Logger.Debug(context.Background(), "closing server with error", slog.Error(err))

View File

@ -4,7 +4,6 @@ import (
"archive/tar"
"bytes"
"context"
"errors"
"io"
"os"
"path/filepath"
@ -15,6 +14,7 @@ import (
"github.com/stretchr/testify/require"
"go.uber.org/atomic"
"go.uber.org/goleak"
"golang.org/x/xerrors"
"storj.io/drpc/drpcmux"
"storj.io/drpc/drpcserver"
@ -52,7 +52,7 @@ func TestProvisionerd(t *testing.T) {
completeChan := make(chan struct{})
closer := createProvisionerd(t, func(ctx context.Context) (proto.DRPCProvisionerDaemonClient, error) {
defer close(completeChan)
return nil, errors.New("an error")
return nil, xerrors.New("an error")
}, provisionerd.Provisioners{})
<-completeChan
require.NoError(t, closer.Close())

View File

@ -39,6 +39,7 @@ func TestProvisionerSDK(t *testing.T) {
_, err = stream.Recv()
require.Equal(t, drpcerr.Unimplemented, int(drpcerr.Code(err)))
})
t.Run("ServeClosedPipe", func(t *testing.T) {
t.Parallel()
client, server := provisionersdk.TransportPipe()

27
rules.go Normal file
View File

@ -0,0 +1,27 @@
package gorules
import (
"github.com/quasilyte/go-ruleguard/dsl"
)
// Use xerrors everywhere! It provides additional stacktrace info!
//nolint:unused,deadcode,varnamelen
func xerrors(m dsl.Matcher) {
m.Import("errors")
m.Import("fmt")
m.Import("golang.org/x/xerrors")
msg := "Use xerrors to provide additional stacktrace information!"
m.Match("fmt.Errorf($*args)").
Suggest("xerrors.New($args)").
Report(msg)
m.Match("fmt.Errorf($*args)").
Suggest("xerrors.Errorf($args)").
Report(msg)
m.Match("errors.New($msg)").
Where(m["msg"].Type.Is("string")).
Suggest("xerrors.New($msg)").
Report(msg)
}