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
38 changed files with 1575 additions and 1166 deletions
+3 -2
View File
@@ -165,12 +165,13 @@ jobs:
-covermode=atomic -coverprofile="gotests.coverage" -timeout=3m -covermode=atomic -coverprofile="gotests.coverage" -timeout=3m
-count=1 -race -parallel=2 -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]' if: always() && github.actor != 'dependabot[bot]'
with: with:
node-version: "14" node-version: "14"
- name: Cache DataDog CI - name: Cache DataDog CLI
if: always() && github.actor != 'dependabot[bot]' if: always() && github.actor != 'dependabot[bot]'
uses: actions/cache@v2 uses: actions/cache@v2
with: with:
+4
View File
@@ -100,6 +100,10 @@ linters-settings:
# - whyNoLint # - whyNoLint
# - wrapperFunc # - wrapperFunc
# - yodaStyleExpr # - yodaStyleExpr
settings:
ruleguard:
failOn: all
rules: rules.go
goimports: goimports:
local-prefixes: coder.com,cdr.dev,go.coder.com,github.com/cdr,github.com/coder local-prefixes: coder.com,cdr.dev,go.coder.com,github.com/cdr,github.com/coder
-1
View File
@@ -86,7 +86,6 @@ func New(options *Options) http.Handler {
r.Get("/", api.workspaces) r.Get("/", api.workspaces)
r.Route("/{user}", func(r chi.Router) { r.Route("/{user}", func(r chi.Router) {
r.Use(httpmw.ExtractUserParam(options.Database)) r.Use(httpmw.ExtractUserParam(options.Database))
r.Get("/", api.workspaces)
r.Post("/", api.postWorkspaceByUser) r.Post("/", api.postWorkspaceByUser)
r.Route("/{workspace}", func(r chi.Router) { r.Route("/{workspace}", func(r chi.Router) {
r.Use(httpmw.ExtractWorkspaceParam(options.Database)) r.Use(httpmw.ExtractWorkspaceParam(options.Database))
+11
View File
@@ -0,0 +1,11 @@
package coderd_test
import (
"testing"
"go.uber.org/goleak"
)
func TestMain(m *testing.M) {
goleak.VerifyTestMain(m)
}
+123 -76
View File
@@ -7,16 +7,18 @@ import (
"net/http/httptest" "net/http/httptest"
"net/url" "net/url"
"os" "os"
"strings"
"testing" "testing"
"time" "time"
"github.com/google/uuid"
"github.com/moby/moby/pkg/namesgenerator"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"cdr.dev/slog" "cdr.dev/slog"
"cdr.dev/slog/sloggers/slogtest" "cdr.dev/slog/sloggers/slogtest"
"github.com/coder/coder/coderd" "github.com/coder/coder/coderd"
"github.com/coder/coder/codersdk" "github.com/coder/coder/codersdk"
"github.com/coder/coder/cryptorand"
"github.com/coder/coder/database" "github.com/coder/coder/database"
"github.com/coder/coder/database/databasefake" "github.com/coder/coder/database/databasefake"
"github.com/coder/coder/database/postgres" "github.com/coder/coder/database/postgres"
@@ -26,79 +28,9 @@ import (
"github.com/coder/coder/provisionersdk/proto" "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 // New constructs a new coderd test instance. This returned Server
// should contain no side-effects. // 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. // This can be hotswapped for a live database instance.
db := databasefake.New() db := databasefake.New()
pubsub := database.NewPubsubInMemory() pubsub := database.NewPubsubInMemory()
@@ -132,8 +64,123 @@ func New(t *testing.T) Server {
require.NoError(t, err) require.NoError(t, err)
t.Cleanup(srv.Close) t.Cleanup(srv.Close)
return Server{ return codersdk.New(serverURL)
Client: codersdk.New(serverURL), }
URL: 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), "_", "-")
} }
+19 -3
View File
@@ -1,11 +1,16 @@
package coderdtest_test package coderdtest_test
import ( import (
"context"
"testing" "testing"
"go.uber.org/goleak" "go.uber.org/goleak"
"github.com/stretchr/testify/require"
"github.com/coder/coder/coderd"
"github.com/coder/coder/coderd/coderdtest" "github.com/coder/coder/coderd/coderdtest"
"github.com/coder/coder/database"
) )
func TestMain(m *testing.M) { func TestMain(m *testing.M) {
@@ -14,7 +19,18 @@ func TestMain(m *testing.M) {
func TestNew(t *testing.T) { func TestNew(t *testing.T) {
t.Parallel() t.Parallel()
server := coderdtest.New(t) client := coderdtest.New(t)
_ = server.RandomInitialUser(t) user := coderdtest.CreateInitialUser(t, client)
_ = server.AddProvisionerd(t) 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()
} }
+6 -26
View File
@@ -49,6 +49,9 @@ func (api *api) projects(rw http.ResponseWriter, r *http.Request) {
}) })
return return
} }
if projects == nil {
projects = []database.Project{}
}
render.Status(r, http.StatusOK) render.Status(r, http.StatusOK)
render.JSON(rw, r, projects) render.JSON(rw, r, projects)
} }
@@ -66,6 +69,9 @@ func (api *api) projectsByOrganization(rw http.ResponseWriter, r *http.Request)
}) })
return return
} }
if projects == nil {
projects = []database.Project{}
}
render.Status(r, http.StatusOK) render.Status(r, http.StatusOK)
render.JSON(rw, r, projects) render.JSON(rw, r, projects)
} }
@@ -124,32 +130,6 @@ func (*api) projectByOrganization(rw http.ResponseWriter, r *http.Request) {
render.JSON(rw, r, project) 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. // Creates parameters for a project.
// This should validate the calling user has permissions! // This should validate the calling user has permissions!
func (api *api) postParametersByProject(rw http.ResponseWriter, r *http.Request) { func (api *api) postParametersByProject(rw http.ResponseWriter, r *http.Request) {
+87 -106
View File
@@ -2,124 +2,109 @@ package coderd_test
import ( import (
"context" "context"
"net/http"
"testing" "testing"
"time"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"github.com/coder/coder/coderd" "github.com/coder/coder/coderd"
"github.com/coder/coder/coderd/coderdtest" "github.com/coder/coder/coderd/coderdtest"
"github.com/coder/coder/codersdk"
"github.com/coder/coder/database" "github.com/coder/coder/database"
"github.com/coder/coder/provisioner/echo"
"github.com/coder/coder/provisionersdk/proto"
) )
func TestProjects(t *testing.T) { func TestProjects(t *testing.T) {
t.Parallel() 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.Run("ListEmpty", func(t *testing.T) {
t.Parallel() t.Parallel()
server := coderdtest.New(t) client := coderdtest.New(t)
_ = server.RandomInitialUser(t) _ = coderdtest.CreateInitialUser(t, client)
projects, err := server.Client.Projects(context.Background(), "") projects, err := client.Projects(context.Background(), "")
require.NoError(t, err) require.NoError(t, err)
require.NotNil(t, projects)
require.Len(t, projects, 0) require.Len(t, projects, 0)
}) })
t.Run("List", func(t *testing.T) { t.Run("List", func(t *testing.T) {
t.Parallel() t.Parallel()
server := coderdtest.New(t) client := coderdtest.New(t)
user := server.RandomInitialUser(t) user := coderdtest.CreateInitialUser(t, client)
_, err := server.Client.CreateProject(context.Background(), user.Organization, coderd.CreateProjectRequest{ _ = coderdtest.CreateProject(t, client, user.Organization)
Name: "someproject", projects, err := client.Projects(context.Background(), "")
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)
require.NoError(t, err) require.NoError(t, err)
require.Len(t, projects, 1) require.Len(t, projects, 1)
}) })
}
func TestProjectsByOrganization(t *testing.T) {
t.Parallel()
t.Run("ListEmpty", func(t *testing.T) { t.Run("ListEmpty", func(t *testing.T) {
t.Parallel() t.Parallel()
server := coderdtest.New(t) client := coderdtest.New(t)
user := server.RandomInitialUser(t) user := coderdtest.CreateInitialUser(t, client)
projects, err := client.Projects(context.Background(), user.Organization)
projects, err := server.Client.Projects(context.Background(), user.Organization)
require.NoError(t, err) require.NoError(t, err)
require.NotNil(t, projects)
require.Len(t, projects, 0) require.Len(t, projects, 0)
}) })
t.Run("Single", func(t *testing.T) { t.Run("List", func(t *testing.T) {
t.Parallel() t.Parallel()
server := coderdtest.New(t) client := coderdtest.New(t)
user := server.RandomInitialUser(t) user := coderdtest.CreateInitialUser(t, client)
project, err := server.Client.CreateProject(context.Background(), user.Organization, coderd.CreateProjectRequest{ _ = coderdtest.CreateProject(t, client, user.Organization)
Name: "someproject", projects, err := client.Projects(context.Background(), "")
Provisioner: database.ProvisionerTypeEcho,
})
require.NoError(t, err)
_, err = server.Client.Project(context.Background(), user.Organization, project.Name)
require.NoError(t, err) 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() t.Parallel()
server := coderdtest.New(t) client := coderdtest.New(t)
user := server.RandomInitialUser(t) user := coderdtest.CreateInitialUser(t, client)
project, err := server.Client.CreateProject(context.Background(), user.Organization, coderd.CreateProjectRequest{ project := coderdtest.CreateProject(t, client, user.Organization)
Name: "someproject", _, err := client.CreateProject(context.Background(), user.Organization, coderd.CreateProjectRequest{
Name: project.Name,
Provisioner: database.ProvisionerTypeEcho, Provisioner: database.ProvisionerTypeEcho,
}) })
require.NoError(t, err) var apiErr *codersdk.Error
_, err = server.Client.ProjectParameters(context.Background(), user.Organization, project.Name) 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) 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() t.Parallel()
server := coderdtest.New(t) client := coderdtest.New(t)
user := server.RandomInitialUser(t) user := coderdtest.CreateInitialUser(t, client)
project, err := server.Client.CreateProject(context.Background(), user.Organization, coderd.CreateProjectRequest{ project := coderdtest.CreateProject(t, client, user.Organization)
Name: "someproject", _, err := client.CreateProjectParameter(context.Background(), user.Organization, project.Name, coderd.CreateParameterValueRequest{
Provisioner: database.ProvisionerTypeEcho, Name: "somename",
})
require.NoError(t, err)
_, err = server.Client.CreateProjectParameter(context.Background(), user.Organization, project.Name, coderd.CreateParameterValueRequest{
Name: "hi",
SourceValue: "tomato", SourceValue: "tomato",
SourceScheme: database.ParameterSourceSchemeData, SourceScheme: database.ParameterSourceSchemeData,
DestinationScheme: database.ParameterDestinationSchemeEnvironmentVariable, DestinationScheme: database.ParameterDestinationSchemeEnvironmentVariable,
@@ -127,40 +112,36 @@ func TestProjects(t *testing.T) {
}) })
require.NoError(t, err) 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() t.Parallel()
server := coderdtest.New(t) client := coderdtest.New(t)
user := server.RandomInitialUser(t) user := coderdtest.CreateInitialUser(t, client)
_ = server.AddProvisionerd(t) project := coderdtest.CreateProject(t, client, user.Organization)
project, err := server.Client.CreateProject(context.Background(), user.Organization, coderd.CreateProjectRequest{ params, err := client.ProjectParameters(context.Background(), user.Organization, project.Name)
Name: "someproject", require.NoError(t, err)
Provisioner: database.ProvisionerTypeEcho, 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) require.NoError(t, err)
data, err := echo.Tar([]*proto.Parse_Response{{ params, err := client.ProjectParameters(context.Background(), user.Organization, project.Name)
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)
require.NoError(t, err) require.NoError(t, err)
require.NotNil(t, params)
require.Len(t, params, 1) require.Len(t, params, 1)
require.Equal(t, "example", params[0].Name)
}) })
} }
+5 -13
View File
@@ -110,19 +110,11 @@ func (api *api) postProjectVersionByOrganization(rw http.ResponseWriter, r *http
return return
} }
switch createProjectVersion.StorageMethod { tarReader := tar.NewReader(bytes.NewReader(createProjectVersion.StorageSource))
case database.ProjectStorageMethodInlineArchive: _, err := tarReader.Next()
tarReader := tar.NewReader(bytes.NewReader(createProjectVersion.StorageSource)) if err != nil {
_, err := tarReader.Next()
if err != nil {
httpapi.Write(rw, http.StatusBadRequest, httpapi.Response{
Message: "the archive must be a tar",
})
return
}
default:
httpapi.Write(rw, http.StatusBadRequest, httpapi.Response{ httpapi.Write(rw, http.StatusBadRequest, httpapi.Response{
Message: fmt.Sprintf("unsupported storage method %s", createProjectVersion.StorageMethod), Message: "the archive must be a tar",
}) })
return return
} }
@@ -132,7 +124,7 @@ func (api *api) postProjectVersionByOrganization(rw http.ResponseWriter, r *http
var provisionerJob database.ProvisionerJob var provisionerJob database.ProvisionerJob
var projectVersion database.ProjectVersion 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() projectVersionID := uuid.New()
input, err := json.Marshal(projectImportJob{ input, err := json.Marshal(projectImportJob{
ProjectVersionID: projectVersionID, ProjectVersionID: projectVersionID,
+96 -70
View File
@@ -1,103 +1,129 @@
package coderd_test package coderd_test
import ( import (
"archive/tar"
"bytes"
"context" "context"
"net/http"
"testing" "testing"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"github.com/coder/coder/coderd" "github.com/coder/coder/coderd"
"github.com/coder/coder/coderd/coderdtest" "github.com/coder/coder/coderd/coderdtest"
"github.com/coder/coder/codersdk"
"github.com/coder/coder/database" "github.com/coder/coder/database"
"github.com/coder/coder/provisioner/echo" "github.com/coder/coder/provisioner/echo"
"github.com/coder/coder/provisionersdk/proto" "github.com/coder/coder/provisionersdk/proto"
) )
func TestProjectVersion(t *testing.T) { func TestProjectVersionsByOrganization(t *testing.T) {
t.Parallel() t.Parallel()
t.Run("ListEmpty", func(t *testing.T) {
t.Run("NoHistory", func(t *testing.T) {
t.Parallel() t.Parallel()
server := coderdtest.New(t) client := coderdtest.New(t)
user := server.RandomInitialUser(t) user := coderdtest.CreateInitialUser(t, client)
project, err := server.Client.CreateProject(context.Background(), user.Organization, coderd.CreateProjectRequest{ project := coderdtest.CreateProject(t, client, user.Organization)
Name: "someproject", versions, err := client.ProjectVersions(context.Background(), user.Organization, project.Name)
Provisioner: database.ProvisionerTypeEcho,
})
require.NoError(t, err)
versions, err := server.Client.ProjectVersions(context.Background(), user.Organization, project.Name)
require.NoError(t, err) require.NoError(t, err)
require.NotNil(t, versions)
require.Len(t, versions, 0) require.Len(t, versions, 0)
}) })
t.Run("CreateVersion", func(t *testing.T) { t.Run("List", func(t *testing.T) {
t.Parallel() t.Parallel()
server := coderdtest.New(t) client := coderdtest.New(t)
user := server.RandomInitialUser(t) user := coderdtest.CreateInitialUser(t, client)
project, err := server.Client.CreateProject(context.Background(), user.Organization, coderd.CreateProjectRequest{ project := coderdtest.CreateProject(t, client, user.Organization)
Name: "someproject", _ = coderdtest.CreateProjectVersion(t, client, user.Organization, project.Name, nil)
Provisioner: database.ProvisionerTypeEcho, versions, err := client.ProjectVersions(context.Background(), user.Organization, project.Name)
})
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)
require.NoError(t, err) require.NoError(t, err)
require.Len(t, versions, 1) require.Len(t, versions, 1)
})
}
_, err = server.Client.ProjectVersion(context.Background(), user.Organization, project.Name, version.Name) func TestProjectVersionByOrganizationAndName(t *testing.T) {
require.NoError(t, err) 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() t.Parallel()
server := coderdtest.New(t) client := coderdtest.New(t)
user := server.RandomInitialUser(t) user := coderdtest.CreateInitialUser(t, client)
project, err := server.Client.CreateProject(context.Background(), user.Organization, coderd.CreateProjectRequest{ project := coderdtest.CreateProject(t, client, user.Organization)
Name: "someproject", _, err := client.CreateProjectVersion(context.Background(), user.Organization, project.Name, coderd.CreateProjectVersionRequest{
Provisioner: database.ProvisionerTypeEcho, StorageMethod: database.ProjectStorageMethod("invalid"),
})
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,
StorageSource: []byte{}, StorageSource: []byte{},
}) })
require.Error(t, err) 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)
})
}
+11 -21
View File
@@ -6,6 +6,7 @@ import (
"encoding/json" "encoding/json"
"errors" "errors"
"fmt" "fmt"
"io"
"net/http" "net/http"
"reflect" "reflect"
"time" "time"
@@ -35,7 +36,6 @@ func (api *api) provisionerDaemons(rw http.ResponseWriter, r *http.Request) {
daemons, err := api.Database.GetProvisionerDaemons(r.Context()) daemons, err := api.Database.GetProvisionerDaemons(r.Context())
if errors.Is(err, sql.ErrNoRows) { if errors.Is(err, sql.ErrNoRows) {
err = nil err = nil
daemons = []database.ProvisionerDaemon{}
} }
if err != nil { if err != nil {
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
@@ -43,7 +43,9 @@ func (api *api) provisionerDaemons(rw http.ResponseWriter, r *http.Request) {
}) })
return return
} }
if daemons == nil {
daemons = []database.ProvisionerDaemon{}
}
render.Status(r, http.StatusOK) render.Status(r, http.StatusOK)
render.JSON(rw, r, daemons) 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. // Serves the provisioner daemon protobuf API over a WebSocket.
func (api *api) provisionerDaemonsServe(rw http.ResponseWriter, r *http.Request) { func (api *api) provisionerDaemonsServe(rw http.ResponseWriter, r *http.Request) {
conn, err := websocket.Accept(rw, r, &websocket.AcceptOptions{ 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, CompressionMode: websocket.CompressionDisabled,
}) })
if err != nil { if err != nil {
@@ -75,7 +77,9 @@ func (api *api) provisionerDaemonsServe(rw http.ResponseWriter, r *http.Request)
// Multiplexes the incoming connection using yamux. // Multiplexes the incoming connection using yamux.
// This allows multiple function calls to occur over // This allows multiple function calls to occur over
// the same connection. // 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 { if err != nil {
_ = conn.Close(websocket.StatusInternalError, fmt.Sprintf("multiplex server: %s", err)) _ = conn.Close(websocket.StatusInternalError, fmt.Sprintf("multiplex server: %s", err))
return return
@@ -221,25 +225,11 @@ func (server *provisionerdServer) AcquireJob(ctx context.Context, _ *proto.Empty
protoParameters = append(protoParameters, parameter.Proto) 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_{ protoJob.Type = &proto.AcquiredJob_WorkspaceProvision_{
WorkspaceProvision: &proto.AcquiredJob_WorkspaceProvision{ WorkspaceProvision: &proto.AcquiredJob_WorkspaceProvision{
WorkspaceHistoryId: workspaceHistory.ID.String(), WorkspaceHistoryId: workspaceHistory.ID.String(),
WorkspaceName: workspace.Name, WorkspaceName: workspace.Name,
State: provisionerState, State: workspaceHistory.ProvisionerState,
ParameterValues: protoParameters, ParameterValues: protoParameters,
}, },
} }
@@ -286,10 +276,10 @@ func (server *provisionerdServer) UpdateJob(stream proto.DRPCProvisionerDaemon_U
return xerrors.Errorf("get job: %w", err) return xerrors.Errorf("get job: %w", err)
} }
if !job.WorkerID.Valid { 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() { 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{ err = server.Database.UpdateProvisionerJobByID(stream.Context(), database.UpdateProvisionerJobByIDParams{
+12 -10
View File
@@ -11,16 +11,18 @@ import (
) )
func TestProvisionerDaemons(t *testing.T) { 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.Parallel()
t.Run("Register", func(t *testing.T) { client := coderdtest.New(t)
t.Parallel() _ = coderdtest.NewProvisionerDaemon(t, client)
server := coderdtest.New(t) require.Eventually(t, func() bool {
_ = server.AddProvisionerd(t) daemons, err := client.ProvisionerDaemons(context.Background())
require.Eventually(t, func() bool { require.NoError(t, err)
daemons, err := server.Client.ProvisionerDaemons(context.Background()) return len(daemons) > 0
require.NoError(t, err) }, time.Second, 25*time.Millisecond)
return len(daemons) > 0
}, time.Second, 10*time.Millisecond)
})
} }
+1 -1
View File
@@ -70,7 +70,7 @@ func convertProvisionerJob(provisionerJob database.ProvisionerJob) ProvisionerJo
job.Status = ProvisionerJobStatusRunning job.Status = ProvisionerJobStatusRunning
} }
if job.Error != "" { if !provisionerJob.CancelledAt.Valid && job.Error != "" {
job.Status = ProvisionerJobStatusFailed job.Status = ProvisionerJobStatusFailed
} }
+4
View File
@@ -195,6 +195,10 @@ func (api *api) organizationsByUser(rw http.ResponseWriter, r *http.Request) {
user := httpmw.UserParam(r) user := httpmw.UserParam(r)
organizations, err := api.Database.GetOrganizationsByUserID(r.Context(), user.ID) organizations, err := api.Database.GetOrganizationsByUserID(r.Context(), user.ID)
if errors.Is(err, sql.ErrNoRows) {
err = nil
organizations = []database.Organization{}
}
if err != nil { if err != nil {
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
Message: fmt.Sprintf("get organizations: %s", err.Error()), Message: fmt.Sprintf("get organizations: %s", err.Error()),
+117 -81
View File
@@ -9,107 +9,143 @@ import (
"github.com/coder/coder/coderd" "github.com/coder/coder/coderd"
"github.com/coder/coder/coderd/coderdtest" "github.com/coder/coder/coderd/coderdtest"
"github.com/coder/coder/codersdk"
"github.com/coder/coder/httpmw" "github.com/coder/coder/httpmw"
) )
func TestUsers(t *testing.T) { func TestPostUser(t *testing.T) {
t.Parallel() t.Parallel()
t.Run("BadRequest", func(t *testing.T) {
t.Run("Authenticated", func(t *testing.T) {
t.Parallel() t.Parallel()
server := coderdtest.New(t) client := coderdtest.New(t)
_ = server.RandomInitialUser(t) _, err := client.CreateInitialUser(context.Background(), coderd.CreateInitialUserRequest{})
_, 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",
})
require.Error(t, err) require.Error(t, err)
}) })
t.Run("Login", func(t *testing.T) { t.Run("AlreadyExists", func(t *testing.T) {
t.Parallel() t.Parallel()
server := coderdtest.New(t) client := coderdtest.New(t)
user := server.RandomInitialUser(t) _ = coderdtest.CreateInitialUser(t, client)
_, err := server.Client.LoginWithPassword(context.Background(), coderd.LoginWithPasswordRequest{ _, 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, Email: user.Email,
Password: user.Password, Password: user.Password,
}) })
require.NoError(t, err) 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.Parallel()
t.Run("LogoutShouldClearCookie", func(t *testing.T) { t.Run("ClearCookie", func(t *testing.T) {
t.Parallel() t.Parallel()
server := coderdtest.New(t) client := coderdtest.New(t)
fullURL, err := server.URL.Parse("/api/v2/logout") fullURL, err := client.URL.Parse("/api/v2/logout")
require.NoError(t, err, "Server URL should parse successfully") require.NoError(t, err, "Server URL should parse successfully")
req, err := http.NewRequestWithContext(context.Background(), http.MethodPost, fullURL.String(), nil) req, err := http.NewRequestWithContext(context.Background(), http.MethodPost, fullURL.String(), nil)
+12 -10
View File
@@ -74,12 +74,12 @@ func (api *api) postWorkspaceHistoryByUser(rw http.ResponseWriter, r *http.Reque
projectVersionJobStatus := convertProvisionerJob(projectVersionJob).Status projectVersionJobStatus := convertProvisionerJob(projectVersionJob).Status
switch projectVersionJobStatus { switch projectVersionJobStatus {
case ProvisionerJobStatusPending, ProvisionerJobStatusRunning: 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), Message: fmt.Sprintf("The provided project version is %s. Wait for it to complete importing!", projectVersionJobStatus),
}) })
return return
case ProvisionerJobStatusFailed: 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), Message: fmt.Sprintf("The provided project version %q has failed to import. You cannot create workspaces using it!", projectVersion.Name),
}) })
return return
@@ -87,6 +87,7 @@ func (api *api) postWorkspaceHistoryByUser(rw http.ResponseWriter, r *http.Reque
httpapi.Write(rw, http.StatusPreconditionFailed, httpapi.Response{ httpapi.Write(rw, http.StatusPreconditionFailed, httpapi.Response{
Message: "The provided project version was canceled during import. You cannot create workspaces using it!", 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) 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) priorHistory, err := api.Database.GetWorkspaceHistoryByWorkspaceIDWithoutAfter(r.Context(), workspace.ID)
if err == nil { if err == nil {
priorJob, err := api.Database.GetProvisionerJobByID(r.Context(), priorHistory.ProvisionJobID) 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{ httpapi.Write(rw, http.StatusConflict, httpapi.Response{
Message: "a workspace build is already active", Message: "a workspace build is already active",
}) })
@@ -113,8 +114,7 @@ func (api *api) postWorkspaceHistoryByUser(rw http.ResponseWriter, r *http.Reque
UUID: priorHistory.ID, UUID: priorHistory.ID,
Valid: true, Valid: true,
} }
} } else if !errors.Is(err, sql.ErrNoRows) {
if !errors.Is(err, sql.ErrNoRows) {
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
Message: fmt.Sprintf("get prior workspace history: %s", err), 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 { if priorHistoryID.Valid {
// Update the prior history entries "after" column. // Update the prior history entries "after" column.
err = db.UpdateWorkspaceHistoryByID(r.Context(), database.UpdateWorkspaceHistoryByIDParams{ err = db.UpdateWorkspaceHistoryByID(r.Context(), database.UpdateWorkspaceHistoryByIDParams{
ID: priorHistory.ID, ID: priorHistory.ID,
UpdatedAt: database.Now(), ProvisionerState: priorHistory.ProvisionerState,
UpdatedAt: database.Now(),
AfterID: uuid.NullUUID{ AfterID: uuid.NullUUID{
UUID: workspaceHistory.ID, UUID: workspaceHistory.ID,
Valid: true, 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) { func (api *api) workspaceHistoryByUser(rw http.ResponseWriter, r *http.Request) {
workspace := httpmw.WorkspaceParam(r) 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) { if errors.Is(err, sql.ErrNoRows) {
err = nil err = nil
history = []database.WorkspaceHistory{}
} }
if err != nil { if err != nil {
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
@@ -208,8 +210,8 @@ func (api *api) workspaceHistoryByUser(rw http.ResponseWriter, r *http.Request)
return return
} }
apiHistory := make([]WorkspaceHistory, 0, len(histories)) apiHistory := make([]WorkspaceHistory, 0, len(history))
for _, history := range histories { for _, history := range history {
job, err := api.Database.GetProvisionerJobByID(r.Context(), history.ProvisionJobID) job, err := api.Database.GetProvisionerJobByID(r.Context(), history.ProvisionJobID)
if err != nil { if err != nil {
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
+138 -129
View File
@@ -2,8 +2,8 @@ package coderd_test
import ( import (
"context" "context"
"net/http"
"testing" "testing"
"time"
"github.com/google/uuid" "github.com/google/uuid"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
@@ -13,141 +13,150 @@ import (
"github.com/coder/coder/codersdk" "github.com/coder/coder/codersdk"
"github.com/coder/coder/database" "github.com/coder/coder/database"
"github.com/coder/coder/provisioner/echo" "github.com/coder/coder/provisioner/echo"
"github.com/coder/coder/provisionersdk/proto"
) )
func TestWorkspaceHistory(t *testing.T) { func TestPostWorkspaceHistoryByUser(t *testing.T) {
t.Parallel() t.Parallel()
t.Run("NoProjectVersion", func(t *testing.T) {
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.Parallel() t.Parallel()
server := coderdtest.New(t) client := coderdtest.New(t)
user := server.RandomInitialUser(t) user := coderdtest.CreateInitialUser(t, client)
_ = server.AddProvisionerd(t) project := coderdtest.CreateProject(t, client, user.Organization)
project, workspace := setupProjectAndWorkspace(t, server.Client, user) workspace := coderdtest.CreateWorkspace(t, client, "me", project.ID)
history, err := server.Client.ListWorkspaceHistory(context.Background(), "", workspace.Name) _, err := client.CreateWorkspaceHistory(context.Background(), "", workspace.Name, coderd.CreateWorkspaceHistoryRequest{
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{
ProjectVersionID: uuid.New(), ProjectVersionID: uuid.New(),
Transition: database.WorkspaceTransitionCreate, Transition: database.WorkspaceTransitionCreate,
}) })
require.Error(t, err) 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)
}
+5 -7
View File
@@ -87,7 +87,6 @@ func (api *api) workspaceHistoryLogsByName(rw http.ResponseWriter, r *http.Reque
}) })
if errors.Is(err, sql.ErrNoRows) { if errors.Is(err, sql.ErrNoRows) {
err = nil err = nil
logs = []database.WorkspaceHistoryLog{}
} }
if err != nil { if err != nil {
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
@@ -95,6 +94,9 @@ func (api *api) workspaceHistoryLogsByName(rw http.ResponseWriter, r *http.Reque
}) })
return return
} }
if logs == nil {
logs = []database.WorkspaceHistoryLog{}
}
render.Status(r, http.StatusOK) render.Status(r, http.StatusOK)
render.JSON(rw, r, logs) render.JSON(rw, r, logs)
return return
@@ -113,12 +115,8 @@ func (api *api) workspaceHistoryLogsByName(rw http.ResponseWriter, r *http.Reque
select { select {
case bufferedLogs <- log: case bufferedLogs <- log:
default: default:
// This is a case that shouldn't happen, but totally could. // If this overflows users could miss logs streaming. This can happen
// There's no way to stream data from the database, so we'll // if a database request takes a long amount of time, and we get a lot of logs.
// 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!
api.Logger.Warn(r.Context(), "workspace history log overflowing channel") api.Logger.Warn(r.Context(), "workspace history log overflowing channel")
} }
} }
+115 -73
View File
@@ -9,90 +9,132 @@ import (
"github.com/coder/coder/coderd" "github.com/coder/coder/coderd"
"github.com/coder/coder/coderd/coderdtest" "github.com/coder/coder/coderd/coderdtest"
"github.com/coder/coder/codersdk"
"github.com/coder/coder/database" "github.com/coder/coder/database"
"github.com/coder/coder/provisioner/echo" "github.com/coder/coder/provisioner/echo"
"github.com/coder/coder/provisionersdk/proto" "github.com/coder/coder/provisionersdk/proto"
) )
func TestWorkspaceHistoryLogs(t *testing.T) { func TestWorkspaceHistoryLogsByName(t *testing.T) {
t.Parallel() t.Parallel()
t.Run("List", func(t *testing.T) {
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.Parallel() t.Parallel()
client := coderdtest.New(t)
_, err := server.Client.WorkspaceHistoryLogs(context.Background(), "", workspace.Name, workspaceHistory.Name) 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) 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() t.Parallel()
client := coderdtest.New(t)
_, err := server.Client.WorkspaceHistoryLogsBetween(context.Background(), "", workspace.Name, workspaceHistory.Name, time.Time{}, database.Now()) 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) 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)
}) })
} }
+27 -1
View File
@@ -137,7 +137,7 @@ func (api *api) postWorkspaceByUser(rw http.ResponseWriter, r *http.Request) {
render.JSON(rw, r, convertWorkspace(workspace)) render.JSON(rw, r, convertWorkspace(workspace))
} }
// Returns a single singleWorkspace. // Returns a single workspace.
func (*api) workspaceByUser(rw http.ResponseWriter, r *http.Request) { func (*api) workspaceByUser(rw http.ResponseWriter, r *http.Request) {
workspace := httpmw.WorkspaceParam(r) workspace := httpmw.WorkspaceParam(r)
@@ -145,6 +145,32 @@ func (*api) workspaceByUser(rw http.ResponseWriter, r *http.Request) {
render.JSON(rw, r, convertWorkspace(workspace)) 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. // Converts the internal workspace representation to a public external-facing model.
func convertWorkspace(workspace database.Workspace) Workspace { func convertWorkspace(workspace database.Workspace) Workspace {
return Workspace(workspace) return Workspace(workspace)
+117 -124
View File
@@ -2,6 +2,7 @@ package coderd_test
import ( import (
"context" "context"
"net/http"
"testing" "testing"
"github.com/google/uuid" "github.com/google/uuid"
@@ -10,143 +11,135 @@ import (
"github.com/coder/coder/coderd" "github.com/coder/coder/coderd"
"github.com/coder/coder/coderd/coderdtest" "github.com/coder/coder/coderd/coderdtest"
"github.com/coder/coder/codersdk" "github.com/coder/coder/codersdk"
"github.com/coder/coder/database"
) )
func TestWorkspaces(t *testing.T) { func TestWorkspaces(t *testing.T) {
t.Parallel() t.Parallel()
t.Run("ListNone", func(t *testing.T) { t.Run("ListNone", func(t *testing.T) {
t.Parallel() t.Parallel()
server := coderdtest.New(t) client := coderdtest.New(t)
_ = server.RandomInitialUser(t) _ = coderdtest.CreateInitialUser(t, client)
workspaces, err := server.Client.WorkspacesByUser(context.Background(), "") workspaces, err := client.Workspaces(context.Background(), "")
require.NoError(t, err) require.NoError(t, err)
require.NotNil(t, workspaces)
require.Len(t, workspaces, 0) 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.Run("List", func(t *testing.T) {
t.Parallel() t.Parallel()
server := coderdtest.New(t) client := coderdtest.New(t)
user := server.RandomInitialUser(t) user := coderdtest.CreateInitialUser(t, client)
_, _ = setupProjectAndWorkspace(t, server.Client, user) project := coderdtest.CreateProject(t, client, user.Organization)
workspaces, err := server.Client.WorkspacesByUser(context.Background(), "") _ = coderdtest.CreateWorkspace(t, client, "", project.ID)
workspaces, err := client.Workspaces(context.Background(), "")
require.NoError(t, err) require.NoError(t, err)
require.Len(t, workspaces, 1) require.Len(t, workspaces, 1)
}) })
}
t.Run("ListNoneForProject", func(t *testing.T) {
t.Parallel() func TestPostWorkspaceByUser(t *testing.T) {
server := coderdtest.New(t) t.Parallel()
user := server.RandomInitialUser(t) t.Run("InvalidProject", func(t *testing.T) {
project, err := server.Client.CreateProject(context.Background(), user.Organization, coderd.CreateProjectRequest{ t.Parallel()
Name: "banana", client := coderdtest.New(t)
Provisioner: database.ProvisionerTypeEcho, _ = coderdtest.CreateInitialUser(t, client)
}) _, err := client.CreateWorkspace(context.Background(), "", coderd.CreateWorkspaceRequest{
require.NoError(t, err) ProjectID: uuid.New(),
workspaces, err := server.Client.WorkspacesByProject(context.Background(), user.Organization, project.Name) Name: "workspace",
require.NoError(t, err) })
require.Len(t, workspaces, 0) require.Error(t, err)
}) var apiErr *codersdk.Error
require.ErrorAs(t, err, &apiErr)
t.Run("ListForProject", func(t *testing.T) { require.Equal(t, http.StatusBadRequest, apiErr.StatusCode())
t.Parallel() })
server := coderdtest.New(t)
user := server.RandomInitialUser(t) t.Run("NoProjectAccess", func(t *testing.T) {
project, _ := setupProjectAndWorkspace(t, server.Client, user) t.Parallel()
workspaces, err := server.Client.WorkspacesByProject(context.Background(), user.Organization, project.Name) client := coderdtest.New(t)
require.NoError(t, err) user := coderdtest.CreateInitialUser(t, client)
require.Len(t, workspaces, 1) project := coderdtest.CreateProject(t, client, user.Organization)
})
anotherUser := coderd.CreateUserRequest{
t.Run("CreateInvalidInput", func(t *testing.T) { Email: "another@user.org",
t.Parallel() Username: "someuser",
server := coderdtest.New(t) Password: "somepass",
user := server.RandomInitialUser(t) }
project, err := server.Client.CreateProject(context.Background(), user.Organization, coderd.CreateProjectRequest{ _, err := client.CreateUser(context.Background(), anotherUser)
Name: "banana", require.NoError(t, err)
Provisioner: database.ProvisionerTypeEcho, token, err := client.LoginWithPassword(context.Background(), coderd.LoginWithPasswordRequest{
}) Email: anotherUser.Email,
require.NoError(t, err) Password: anotherUser.Password,
_, err = server.Client.CreateWorkspace(context.Background(), "", coderd.CreateWorkspaceRequest{ })
ProjectID: project.ID, require.NoError(t, err)
Name: "$$$", err = client.SetSessionToken(token.SessionToken)
}) require.NoError(t, err)
require.Error(t, err)
}) _, err = client.CreateWorkspace(context.Background(), "", coderd.CreateWorkspaceRequest{
ProjectID: project.ID,
t.Run("CreateInvalidProject", func(t *testing.T) { Name: "workspace",
t.Parallel() })
server := coderdtest.New(t) require.Error(t, err)
_ = server.RandomInitialUser(t) var apiErr *codersdk.Error
_, err := server.Client.CreateWorkspace(context.Background(), "", coderd.CreateWorkspaceRequest{ require.ErrorAs(t, err, &apiErr)
ProjectID: uuid.New(), require.Equal(t, http.StatusUnauthorized, apiErr.StatusCode())
Name: "moo", })
})
require.Error(t, err) t.Run("AlreadyExists", func(t *testing.T) {
}) t.Parallel()
client := coderdtest.New(t)
t.Run("CreateNotInProjectOrganization", func(t *testing.T) { user := coderdtest.CreateInitialUser(t, client)
t.Parallel() project := coderdtest.CreateProject(t, client, user.Organization)
server := coderdtest.New(t) workspace := coderdtest.CreateWorkspace(t, client, "", project.ID)
initial := server.RandomInitialUser(t) _, err := client.CreateWorkspace(context.Background(), "", coderd.CreateWorkspaceRequest{
project, err := server.Client.CreateProject(context.Background(), initial.Organization, coderd.CreateProjectRequest{ ProjectID: project.ID,
Name: "banana", Name: workspace.Name,
Provisioner: database.ProvisionerTypeEcho, })
}) require.Error(t, err)
require.NoError(t, err) var apiErr *codersdk.Error
_, err = server.Client.CreateUser(context.Background(), coderd.CreateUserRequest{ require.ErrorAs(t, err, &apiErr)
Email: "hello@ok.io", require.Equal(t, http.StatusConflict, apiErr.StatusCode())
Username: "example", })
Password: "password",
}) t.Run("Create", func(t *testing.T) {
require.NoError(t, err) t.Parallel()
token, err := server.Client.LoginWithPassword(context.Background(), coderd.LoginWithPasswordRequest{ client := coderdtest.New(t)
Email: "hello@ok.io", user := coderdtest.CreateInitialUser(t, client)
Password: "password", project := coderdtest.CreateProject(t, client, user.Organization)
}) _ = coderdtest.CreateWorkspace(t, client, "", project.ID)
require.NoError(t, err) })
err = server.Client.SetSessionToken(token.SessionToken) }
require.NoError(t, err)
_, err = server.Client.CreateWorkspace(context.Background(), "", coderd.CreateWorkspaceRequest{ func TestWorkspaceByUser(t *testing.T) {
ProjectID: project.ID, t.Parallel()
Name: "moo", client := coderdtest.New(t)
}) user := coderdtest.CreateInitialUser(t, client)
require.Error(t, err) project := coderdtest.CreateProject(t, client, user.Organization)
}) workspace := coderdtest.CreateWorkspace(t, client, "", project.ID)
_, err := client.Workspace(context.Background(), "", workspace.Name)
t.Run("CreateAlreadyExists", func(t *testing.T) { require.NoError(t, err)
t.Parallel() }
server := coderdtest.New(t)
user := server.RandomInitialUser(t) func TestWorkspacesByProject(t *testing.T) {
project, workspace := setupProjectAndWorkspace(t, server.Client, user) t.Parallel()
_, err := server.Client.CreateWorkspace(context.Background(), "", coderd.CreateWorkspaceRequest{ t.Run("ListEmpty", func(t *testing.T) {
Name: workspace.Name, t.Parallel()
ProjectID: project.ID, client := coderdtest.New(t)
}) user := coderdtest.CreateInitialUser(t, client)
require.Error(t, err) project := coderdtest.CreateProject(t, client, user.Organization)
}) workspaces, err := client.WorkspacesByProject(context.Background(), user.Organization, project.Name)
require.NoError(t, err)
t.Run("Single", func(t *testing.T) { require.NotNil(t, workspaces)
t.Parallel() })
server := coderdtest.New(t)
user := server.RandomInitialUser(t) t.Run("List", func(t *testing.T) {
_, workspace := setupProjectAndWorkspace(t, server.Client, user) t.Parallel()
_, err := server.Client.Workspace(context.Background(), "", workspace.Name) client := coderdtest.New(t)
require.NoError(t, err) 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)
})
} }
+12 -5
View File
@@ -10,6 +10,7 @@ import (
"net/http" "net/http"
"net/http/cookiejar" "net/http/cookiejar"
"net/url" "net/url"
"strings"
"golang.org/x/xerrors" "golang.org/x/xerrors"
@@ -20,14 +21,15 @@ import (
// New creates a Coder client for the provided URL. // New creates a Coder client for the provided URL.
func New(serverURL *url.URL) *Client { func New(serverURL *url.URL) *Client {
return &Client{ return &Client{
url: serverURL, URL: serverURL,
httpClient: &http.Client{}, httpClient: &http.Client{},
} }
} }
// Client is an HTTP caller for methods to the Coder API. // Client is an HTTP caller for methods to the Coder API.
type Client struct { type Client struct {
url *url.URL URL *url.URL
httpClient *http.Client httpClient *http.Client
} }
@@ -40,7 +42,7 @@ func (c *Client) SetSessionToken(token string) error {
return err return err
} }
} }
c.httpClient.Jar.SetCookies(c.url, []*http.Cookie{{ c.httpClient.Jar.SetCookies(c.URL, []*http.Cookie{{
Name: httpmw.AuthCookie, Name: httpmw.AuthCookie,
Value: token, Value: token,
}}) }})
@@ -50,7 +52,7 @@ func (c *Client) SetSessionToken(token string) error {
// request performs an HTTP request with the body provided. // request performs an HTTP request with the body provided.
// The caller is responsible for closing the response body. // The caller is responsible for closing the response body.
func (c *Client) request(ctx context.Context, method, path string, body interface{}) (*http.Response, error) { 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 { if err != nil {
return nil, xerrors.Errorf("parse url: %w", err) return nil, xerrors.Errorf("parse url: %w", err)
} }
@@ -112,5 +114,10 @@ func (e *Error) StatusCode() int {
} }
func (e *Error) Error() string { 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()
} }
+163 -142
View File
@@ -1,8 +1,6 @@
package codersdk_test package codersdk_test
import ( import (
"archive/tar"
"bytes"
"context" "context"
"testing" "testing"
@@ -15,160 +13,183 @@ import (
func TestProjects(t *testing.T) { func TestProjects(t *testing.T) {
t.Parallel() t.Parallel()
t.Run("Error", func(t *testing.T) {
t.Run("UnauthenticatedList", func(t *testing.T) {
t.Parallel() t.Parallel()
server := coderdtest.New(t) client := coderdtest.New(t)
_, err := server.Client.Projects(context.Background(), "") _, err := client.Projects(context.Background(), "")
require.Error(t, err) require.Error(t, err)
}) })
t.Run("List", func(t *testing.T) { t.Run("List", func(t *testing.T) {
t.Parallel() t.Parallel()
server := coderdtest.New(t) client := coderdtest.New(t)
user := server.RandomInitialUser(t) _ = coderdtest.CreateInitialUser(t, client)
_, err := server.Client.Projects(context.Background(), "") _, err := client.Projects(context.Background(), "")
require.NoError(t, err)
_, err = server.Client.Projects(context.Background(), user.Organization)
require.NoError(t, err) 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() t.Parallel()
server := coderdtest.New(t) client := coderdtest.New(t)
_, err := server.Client.CreateProject(context.Background(), "", coderd.CreateProjectRequest{}) _, 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) require.Error(t, err)
}) })
t.Run("Create", func(t *testing.T) { t.Run("Create", func(t *testing.T) {
t.Parallel() t.Parallel()
server := coderdtest.New(t) client := coderdtest.New(t)
user := server.RandomInitialUser(t) user := coderdtest.CreateInitialUser(t, client)
_, err := server.Client.CreateProject(context.Background(), user.Organization, coderd.CreateProjectRequest{ _ = coderdtest.CreateProject(t, client, user.Organization)
Name: "bananas", })
Provisioner: database.ProvisionerTypeEcho, }
})
require.NoError(t, err) func TestProjectVersions(t *testing.T) {
}) t.Parallel()
t.Run("Error", func(t *testing.T) {
t.Run("UnauthenticatedSingle", func(t *testing.T) { t.Parallel()
t.Parallel() client := coderdtest.New(t)
server := coderdtest.New(t) _, err := client.ProjectVersions(context.Background(), "some", "project")
_, err := server.Client.Project(context.Background(), "wow", "example") require.Error(t, err)
require.Error(t, err) })
})
t.Run("List", func(t *testing.T) {
t.Run("Single", func(t *testing.T) { t.Parallel()
t.Parallel() client := coderdtest.New(t)
server := coderdtest.New(t) user := coderdtest.CreateInitialUser(t, client)
user := server.RandomInitialUser(t) project := coderdtest.CreateProject(t, client, user.Organization)
_, err := server.Client.CreateProject(context.Background(), user.Organization, coderd.CreateProjectRequest{
Name: "bananas", _, err := client.ProjectVersions(context.Background(), user.Organization, project.Name)
Provisioner: database.ProvisionerTypeEcho, require.NoError(t, err)
}) })
require.NoError(t, err) }
_, err = server.Client.Project(context.Background(), user.Organization, "bananas")
require.NoError(t, err) func TestProjectVersion(t *testing.T) {
}) t.Parallel()
t.Run("Error", func(t *testing.T) {
t.Run("UnauthenticatedHistory", func(t *testing.T) { t.Parallel()
t.Parallel() client := coderdtest.New(t)
server := coderdtest.New(t) _, err := client.ProjectVersion(context.Background(), "some", "project", "version")
_, err := server.Client.ProjectVersions(context.Background(), "org", "project") require.Error(t, err)
require.Error(t, err) })
})
t.Run("Get", func(t *testing.T) {
t.Run("History", func(t *testing.T) { t.Parallel()
t.Parallel() client := coderdtest.New(t)
server := coderdtest.New(t) user := coderdtest.CreateInitialUser(t, client)
user := server.RandomInitialUser(t) project := coderdtest.CreateProject(t, client, user.Organization)
project, err := server.Client.CreateProject(context.Background(), user.Organization, coderd.CreateProjectRequest{ version := coderdtest.CreateProjectVersion(t, client, user.Organization, project.Name, nil)
Name: "bananas", _, err := client.ProjectVersion(context.Background(), user.Organization, project.Name, version.Name)
Provisioner: database.ProvisionerTypeEcho, require.NoError(t, err)
}) })
require.NoError(t, err) }
_, err = server.Client.ProjectVersions(context.Background(), user.Organization, project.Name)
require.NoError(t, err) func TestCreateProjectVersion(t *testing.T) {
}) t.Parallel()
t.Run("Error", func(t *testing.T) {
t.Run("CreateHistoryUnauthenticated", func(t *testing.T) { t.Parallel()
t.Parallel() client := coderdtest.New(t)
server := coderdtest.New(t) _, err := client.CreateProjectVersion(context.Background(), "some", "project", coderd.CreateProjectVersionRequest{})
_, err := server.Client.CreateProjectVersion(context.Background(), "org", "project", coderd.CreateProjectVersionRequest{ require.Error(t, err)
StorageMethod: database.ProjectStorageMethodInlineArchive, })
StorageSource: []byte{},
}) t.Run("Create", func(t *testing.T) {
require.Error(t, err) t.Parallel()
}) client := coderdtest.New(t)
user := coderdtest.CreateInitialUser(t, client)
t.Run("CreateHistory", func(t *testing.T) { project := coderdtest.CreateProject(t, client, user.Organization)
t.Parallel() _ = coderdtest.CreateProjectVersion(t, client, user.Organization, project.Name, nil)
server := coderdtest.New(t) })
user := server.RandomInitialUser(t) }
project, err := server.Client.CreateProject(context.Background(), user.Organization, coderd.CreateProjectRequest{
Name: "bananas", func TestProjectVersionParameters(t *testing.T) {
Provisioner: database.ProvisionerTypeEcho, t.Parallel()
}) t.Run("Error", func(t *testing.T) {
require.NoError(t, err) t.Parallel()
var buffer bytes.Buffer client := coderdtest.New(t)
writer := tar.NewWriter(&buffer) _, err := client.ProjectVersionParameters(context.Background(), "some", "project", "version")
err = writer.WriteHeader(&tar.Header{ require.Error(t, err)
Name: "file", })
Size: 1 << 10,
}) t.Run("List", func(t *testing.T) {
require.NoError(t, err) t.Parallel()
_, err = writer.Write(make([]byte, 1<<10)) client := coderdtest.New(t)
require.NoError(t, err) user := coderdtest.CreateInitialUser(t, client)
version, err := server.Client.CreateProjectVersion(context.Background(), user.Organization, project.Name, coderd.CreateProjectVersionRequest{ coderdtest.NewProvisionerDaemon(t, client)
StorageMethod: database.ProjectStorageMethodInlineArchive, project := coderdtest.CreateProject(t, client, user.Organization)
StorageSource: buffer.Bytes(), version := coderdtest.CreateProjectVersion(t, client, user.Organization, project.Name, nil)
}) coderdtest.AwaitProjectVersionImported(t, client, user.Organization, project.Name, version.Name)
require.NoError(t, err) _, err := client.ProjectVersionParameters(context.Background(), user.Organization, project.Name, version.Name)
require.NoError(t, err)
_, err = server.Client.ProjectVersion(context.Background(), user.Organization, project.Name, version.Name) })
require.NoError(t, err) }
})
func TestProjectParameters(t *testing.T) {
t.Run("Parameters", func(t *testing.T) { t.Parallel()
t.Parallel() t.Run("Error", func(t *testing.T) {
server := coderdtest.New(t) t.Parallel()
user := server.RandomInitialUser(t) client := coderdtest.New(t)
project, err := server.Client.CreateProject(context.Background(), user.Organization, coderd.CreateProjectRequest{ _, err := client.ProjectParameters(context.Background(), "some", "project")
Name: "someproject", require.Error(t, err)
Provisioner: database.ProvisionerTypeEcho, })
})
require.NoError(t, err) t.Run("List", func(t *testing.T) {
params, err := server.Client.ProjectParameters(context.Background(), user.Organization, project.Name) t.Parallel()
require.NoError(t, err) client := coderdtest.New(t)
require.NotNil(t, params) user := coderdtest.CreateInitialUser(t, client)
require.Len(t, params, 0) coderdtest.NewProvisionerDaemon(t, client)
}) project := coderdtest.CreateProject(t, client, user.Organization)
_, err := client.ProjectParameters(context.Background(), user.Organization, project.Name)
t.Run("CreateParameter", func(t *testing.T) { require.NoError(t, err)
t.Parallel() })
server := coderdtest.New(t) }
user := server.RandomInitialUser(t)
project, err := server.Client.CreateProject(context.Background(), user.Organization, coderd.CreateProjectRequest{ func TestCreateProjectParameter(t *testing.T) {
Name: "someproject", t.Parallel()
Provisioner: database.ProvisionerTypeEcho, t.Run("Error", func(t *testing.T) {
}) t.Parallel()
require.NoError(t, err) client := coderdtest.New(t)
param, err := server.Client.CreateProjectParameter(context.Background(), user.Organization, project.Name, coderd.CreateParameterValueRequest{ _, err := client.CreateProjectParameter(context.Background(), "some", "project", coderd.CreateParameterValueRequest{})
Name: "hi", require.Error(t, err)
SourceValue: "tomato", })
SourceScheme: database.ParameterSourceSchemeData,
DestinationScheme: database.ParameterDestinationSchemeEnvironmentVariable, t.Run("Create", func(t *testing.T) {
DestinationValue: "moo", t.Parallel()
}) client := coderdtest.New(t)
require.NoError(t, err) user := coderdtest.CreateInitialUser(t, client)
require.Equal(t, "hi", param.Name) coderdtest.NewProvisionerDaemon(t, client)
}) project := coderdtest.CreateProject(t, client, user.Organization)
_, err := client.CreateProjectParameter(context.Background(), user.Organization, project.Name, coderd.CreateParameterValueRequest{
t.Run("HistoryParametersError", func(t *testing.T) { Name: "example",
t.Parallel() SourceValue: "source-value",
server := coderdtest.New(t) SourceScheme: database.ParameterSourceSchemeData,
user := server.RandomInitialUser(t) DestinationScheme: database.ParameterDestinationSchemeEnvironmentVariable,
_, err := server.Client.ProjectVersionParameters(context.Background(), user.Organization, "nothing", "nope") DestinationValue: "destination-value",
require.Error(t, err) })
require.NoError(t, err)
}) })
} }
+7 -2
View File
@@ -3,6 +3,7 @@ package codersdk
import ( import (
"context" "context"
"encoding/json" "encoding/json"
"io"
"net/http" "net/http"
"github.com/hashicorp/yamux" "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. // ProvisionerDaemonClient returns the gRPC service for a provisioner daemon implementation.
func (c *Client) ProvisionerDaemonClient(ctx context.Context) (proto.DRPCProvisionerDaemonClient, error) { 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 { if err != nil {
return nil, xerrors.Errorf("parse url: %w", err) return nil, xerrors.Errorf("parse url: %w", err)
} }
conn, res, err := websocket.Dial(ctx, serverURL.String(), &websocket.DialOptions{ conn, res, err := websocket.Dial(ctx, serverURL.String(), &websocket.DialOptions{
HTTPClient: c.httpClient, HTTPClient: c.httpClient,
// Need to disable compression to avoid a data-race.
CompressionMode: websocket.CompressionDisabled,
}) })
if err != nil { if err != nil {
if res == nil { if res == nil {
@@ -42,7 +45,9 @@ func (c *Client) ProvisionerDaemonClient(ctx context.Context) (proto.DRPCProvisi
} }
return nil, readBodyAsError(res) 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 { if err != nil {
return nil, xerrors.Errorf("multiplex client: %w", err) return nil, xerrors.Errorf("multiplex client: %w", err)
} }
+46
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)
})
}
+88 -45
View File
@@ -10,61 +10,104 @@ import (
"github.com/coder/coder/coderd/coderdtest" "github.com/coder/coder/coderd/coderdtest"
) )
func TestUsers(t *testing.T) { func TestCreateInitialUser(t *testing.T) {
t.Parallel() t.Parallel()
t.Run("CreateInitial", func(t *testing.T) { t.Run("Error", func(t *testing.T) {
t.Parallel() t.Parallel()
server := coderdtest.New(t) client := coderdtest.New(t)
_, err := server.Client.CreateInitialUser(context.Background(), coderd.CreateInitialUserRequest{ _, err := 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(), "")
require.Error(t, err) require.Error(t, err)
}) })
t.Run("User", func(t *testing.T) { t.Run("Create", func(t *testing.T) {
t.Parallel() t.Parallel()
server := coderdtest.New(t) client := coderdtest.New(t)
_ = server.RandomInitialUser(t) _ = coderdtest.CreateInitialUser(t, client)
_, err := server.Client.User(context.Background(), "") })
require.NoError(t, err) }
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() t.Parallel()
server := coderdtest.New(t) client := coderdtest.New(t)
_ = server.RandomInitialUser(t) _ = coderdtest.CreateInitialUser(t, client)
orgs, err := server.Client.UserOrganizations(context.Background(), "") _, err := client.CreateUser(context.Background(), coderd.CreateUserRequest{
require.NoError(t, err) Email: "example@coder.com",
require.Len(t, orgs, 1) Username: "something",
}) Password: "password",
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",
}) })
require.NoError(t, err) 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)
})
}
+1 -1
View File
@@ -15,7 +15,7 @@ import (
// Workspaces returns all workspaces the authenticated session has access to. // 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 specified, all workspaces for an organization will be returned.
// If owner is empty, all workspaces the caller has access to 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" route := "/api/v2/workspaces"
if user != "" { if user != "" {
route += fmt.Sprintf("/%s", user) route += fmt.Sprintf("/%s", user)
+212 -146
View File
@@ -3,167 +3,233 @@ package codersdk_test
import ( import (
"context" "context"
"testing" "testing"
"time"
"github.com/google/uuid"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"github.com/coder/coder/coderd" "github.com/coder/coder/coderd"
"github.com/coder/coder/coderd/coderdtest" "github.com/coder/coder/coderd/coderdtest"
"github.com/coder/coder/database" "github.com/coder/coder/database"
"github.com/coder/coder/provisioner/echo"
"github.com/coder/coder/provisionersdk/proto"
) )
func TestWorkspaces(t *testing.T) { func TestWorkspaces(t *testing.T) {
t.Parallel() t.Parallel()
t.Run("ListError", func(t *testing.T) { t.Run("Error", func(t *testing.T) {
t.Parallel() t.Parallel()
server := coderdtest.New(t) client := coderdtest.New(t)
_, err := server.Client.WorkspacesByUser(context.Background(), "") _, err := client.Workspaces(context.Background(), "")
require.Error(t, err) require.Error(t, err)
}) })
t.Run("ListNoOwner", func(t *testing.T) { t.Run("List", func(t *testing.T) {
t.Parallel() t.Parallel()
server := coderdtest.New(t) client := coderdtest.New(t)
_, err := server.Client.WorkspacesByUser(context.Background(), "") _ = coderdtest.CreateInitialUser(t, client)
require.Error(t, err) _, err := client.Workspaces(context.Background(), "")
})
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,
})
require.NoError(t, err) require.NoError(t, err)
_, err = server.Client.CreateWorkspace(context.Background(), "", coderd.CreateWorkspaceRequest{ })
Name: "wow", }
ProjectID: project.ID,
}) func TestWorkspacesByProject(t *testing.T) {
require.NoError(t, err) t.Parallel()
_, err = server.Client.WorkspacesByUser(context.Background(), "me") t.Run("Error", func(t *testing.T) {
require.NoError(t, err) t.Parallel()
}) client := coderdtest.New(t)
_, err := client.WorkspacesByProject(context.Background(), "", "")
t.Run("ListByProject", func(t *testing.T) { require.Error(t, err)
t.Parallel() })
server := coderdtest.New(t)
user := server.RandomInitialUser(t) t.Run("List", func(t *testing.T) {
project, err := server.Client.CreateProject(context.Background(), user.Organization, coderd.CreateProjectRequest{ t.Parallel()
Name: "tomato", client := coderdtest.New(t)
Provisioner: database.ProvisionerTypeEcho, user := coderdtest.CreateInitialUser(t, client)
}) project := coderdtest.CreateProject(t, client, user.Organization)
require.NoError(t, err) _, err := client.WorkspacesByProject(context.Background(), user.Organization, project.Name)
_, err = server.Client.CreateWorkspace(context.Background(), "", coderd.CreateWorkspaceRequest{ require.NoError(t, err)
Name: "wow", })
ProjectID: project.ID, }
})
require.NoError(t, err) func TestWorkspace(t *testing.T) {
_, err = server.Client.WorkspacesByProject(context.Background(), user.Organization, project.Name) t.Parallel()
require.NoError(t, err) t.Run("Error", func(t *testing.T) {
}) t.Parallel()
client := coderdtest.New(t)
t.Run("ListByProjectError", func(t *testing.T) { _, err := client.Workspace(context.Background(), "", "")
t.Parallel() require.Error(t, err)
server := coderdtest.New(t) })
_, err := server.Client.WorkspacesByProject(context.Background(), "", "")
require.Error(t, err) t.Run("Get", func(t *testing.T) {
}) t.Parallel()
client := coderdtest.New(t)
t.Run("CreateError", func(t *testing.T) { user := coderdtest.CreateInitialUser(t, client)
t.Parallel() project := coderdtest.CreateProject(t, client, user.Organization)
server := coderdtest.New(t) workspace := coderdtest.CreateWorkspace(t, client, "", project.ID)
_, err := server.Client.CreateWorkspace(context.Background(), "no", coderd.CreateWorkspaceRequest{}) _, err := client.Workspace(context.Background(), "", workspace.Name)
require.Error(t, err) require.NoError(t, err)
}) })
}
t.Run("Single", func(t *testing.T) {
t.Parallel() func TestListWorkspaceHistory(t *testing.T) {
server := coderdtest.New(t) t.Parallel()
user := server.RandomInitialUser(t) t.Run("Error", func(t *testing.T) {
project, err := server.Client.CreateProject(context.Background(), user.Organization, coderd.CreateProjectRequest{ t.Parallel()
Name: "tomato", client := coderdtest.New(t)
Provisioner: database.ProvisionerTypeEcho, _, err := client.ListWorkspaceHistory(context.Background(), "", "")
}) require.Error(t, err)
require.NoError(t, err) })
workspace, err := server.Client.CreateWorkspace(context.Background(), "", coderd.CreateWorkspaceRequest{
Name: "wow", t.Run("List", func(t *testing.T) {
ProjectID: project.ID, t.Parallel()
}) client := coderdtest.New(t)
require.NoError(t, err) user := coderdtest.CreateInitialUser(t, client)
_, err = server.Client.Workspace(context.Background(), "", workspace.Name) project := coderdtest.CreateProject(t, client, user.Organization)
require.NoError(t, err) workspace := coderdtest.CreateWorkspace(t, client, "", project.ID)
}) _, err := client.ListWorkspaceHistory(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") func TestWorkspaceHistory(t *testing.T) {
require.Error(t, err) t.Parallel()
}) t.Run("Error", func(t *testing.T) {
t.Parallel()
t.Run("History", func(t *testing.T) { client := coderdtest.New(t)
t.Parallel() _, err := client.WorkspaceHistory(context.Background(), "", "", "")
server := coderdtest.New(t) require.Error(t, err)
user := server.RandomInitialUser(t) })
project, err := server.Client.CreateProject(context.Background(), user.Organization, coderd.CreateProjectRequest{
Name: "tomato", t.Run("Get", func(t *testing.T) {
Provisioner: database.ProvisionerTypeEcho, t.Parallel()
}) client := coderdtest.New(t)
require.NoError(t, err) user := coderdtest.CreateInitialUser(t, client)
workspace, err := server.Client.CreateWorkspace(context.Background(), "", coderd.CreateWorkspaceRequest{ _ = coderdtest.NewProvisionerDaemon(t, client)
Name: "wow", project := coderdtest.CreateProject(t, client, user.Organization)
ProjectID: project.ID, version := coderdtest.CreateProjectVersion(t, client, user.Organization, project.Name, nil)
}) coderdtest.AwaitProjectVersionImported(t, client, user.Organization, project.Name, version.Name)
require.NoError(t, err) workspace := coderdtest.CreateWorkspace(t, client, "", project.ID)
_, err = server.Client.ListWorkspaceHistory(context.Background(), "", workspace.Name) _, err := client.CreateWorkspaceHistory(context.Background(), "", workspace.Name, coderd.CreateWorkspaceHistoryRequest{
require.NoError(t, err) ProjectVersionID: version.ID,
}) Transition: database.WorkspaceTransitionCreate,
})
t.Run("HistoryError", func(t *testing.T) { require.NoError(t, err)
t.Parallel() })
server := coderdtest.New(t) }
_, err := server.Client.ListWorkspaceHistory(context.Background(), "", "blob")
require.Error(t, err) func TestCreateWorkspace(t *testing.T) {
}) t.Parallel()
t.Run("Error", func(t *testing.T) {
t.Run("LatestHistory", func(t *testing.T) { t.Parallel()
t.Parallel() client := coderdtest.New(t)
server := coderdtest.New(t) _, err := client.CreateWorkspace(context.Background(), "", coderd.CreateWorkspaceRequest{})
user := server.RandomInitialUser(t) require.Error(t, err)
project, err := server.Client.CreateProject(context.Background(), user.Organization, coderd.CreateProjectRequest{ })
Name: "tomato",
Provisioner: database.ProvisionerTypeEcho, t.Run("Get", func(t *testing.T) {
}) t.Parallel()
require.NoError(t, err) client := coderdtest.New(t)
workspace, err := server.Client.CreateWorkspace(context.Background(), "", coderd.CreateWorkspaceRequest{ user := coderdtest.CreateInitialUser(t, client)
Name: "wow", project := coderdtest.CreateProject(t, client, user.Organization)
ProjectID: project.ID, _ = coderdtest.CreateWorkspace(t, client, "", project.ID)
}) })
require.NoError(t, err) }
_, err = server.Client.WorkspaceHistory(context.Background(), "", workspace.Name, "")
require.Error(t, err) func TestCreateWorkspaceHistory(t *testing.T) {
}) t.Parallel()
t.Run("Error", func(t *testing.T) {
t.Run("CreateHistory", func(t *testing.T) { t.Parallel()
t.Parallel() client := coderdtest.New(t)
server := coderdtest.New(t) _, err := client.CreateWorkspaceHistory(context.Background(), "", "", coderd.CreateWorkspaceHistoryRequest{})
user := server.RandomInitialUser(t) require.Error(t, err)
project, err := server.Client.CreateProject(context.Background(), user.Organization, coderd.CreateProjectRequest{ })
Name: "tomato",
Provisioner: database.ProvisionerTypeEcho, t.Run("Create", func(t *testing.T) {
}) t.Parallel()
require.NoError(t, err) client := coderdtest.New(t)
workspace, err := server.Client.CreateWorkspace(context.Background(), "", coderd.CreateWorkspaceRequest{ user := coderdtest.CreateInitialUser(t, client)
Name: "wow", _ = coderdtest.NewProvisionerDaemon(t, client)
ProjectID: project.ID, project := coderdtest.CreateProject(t, client, user.Organization)
}) version := coderdtest.CreateProjectVersion(t, client, user.Organization, project.Name, nil)
require.NoError(t, err) coderdtest.AwaitProjectVersionImported(t, client, user.Organization, project.Name, version.Name)
_, err = server.Client.CreateWorkspaceHistory(context.Background(), "", workspace.Name, coderd.CreateWorkspaceHistoryRequest{ workspace := coderdtest.CreateWorkspace(t, client, "", project.ID)
ProjectVersionID: uuid.New(), _, err := client.CreateWorkspaceHistory(context.Background(), "", workspace.Name, coderd.CreateWorkspaceHistoryRequest{
Transition: database.WorkspaceTransitionCreate, ProjectVersionID: version.ID,
}) Transition: database.WorkspaceTransitionCreate,
require.Error(t, err) })
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)
}) })
} }
+32 -32
View File
@@ -137,6 +137,26 @@ CREATE TABLE project (
active_version_id uuid 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 ( CREATE TABLE project_version (
id uuid NOT NULL, id uuid NOT NULL,
project_id uuid NOT NULL, project_id uuid NOT NULL,
@@ -158,26 +178,6 @@ CREATE TABLE project_version_log (
output character varying(1024) NOT NULL 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 ( CREATE TABLE provisioner_daemon (
id uuid NOT NULL, id uuid NOT NULL,
created_at timestamp with time zone NOT NULL, created_at timestamp with time zone NOT NULL,
@@ -282,15 +282,6 @@ ALTER TABLE ONLY parameter_value
ALTER TABLE ONLY parameter_value ALTER TABLE ONLY parameter_value
ADD CONSTRAINT parameter_value_name_scope_scope_id_key UNIQUE (name, scope, scope_id); 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 ALTER TABLE ONLY project
ADD CONSTRAINT project_id_key UNIQUE (id); ADD CONSTRAINT project_id_key UNIQUE (id);
@@ -303,6 +294,15 @@ ALTER TABLE ONLY project_parameter
ALTER TABLE ONLY project_parameter ALTER TABLE ONLY project_parameter
ADD CONSTRAINT project_parameter_project_version_id_name_key UNIQUE (project_version_id, name); 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 ALTER TABLE ONLY provisioner_daemon
ADD CONSTRAINT provisioner_daemon_id_key UNIQUE (id); ADD CONSTRAINT provisioner_daemon_id_key UNIQUE (id);
@@ -339,15 +339,15 @@ ALTER TABLE ONLY workspace_resource
ALTER TABLE ONLY workspace_resource ALTER TABLE ONLY workspace_resource
ADD CONSTRAINT workspace_resource_workspace_history_id_name_key UNIQUE (workspace_history_id, name); 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 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; 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 ALTER TABLE ONLY project_version
ADD CONSTRAINT project_version_project_id_fkey FOREIGN KEY (project_id) REFERENCES project(id); 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 ALTER TABLE ONLY provisioner_job
ADD CONSTRAINT provisioner_job_project_id_fkey FOREIGN KEY (project_id) REFERENCES project(id) ON DELETE CASCADE; ADD CONSTRAINT provisioner_job_project_id_fkey FOREIGN KEY (project_id) REFERENCES project(id) ON DELETE CASCADE;
+1
View File
@@ -28,6 +28,7 @@ require (
github.com/pion/logging v0.2.2 github.com/pion/logging v0.2.2
github.com/pion/transport v0.13.0 github.com/pion/transport v0.13.0
github.com/pion/webrtc/v3 v3.1.21 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/spf13/cobra v1.3.0
github.com/stretchr/testify v1.7.0 github.com/stretchr/testify v1.7.0
github.com/unrolled/secure v1.0.9 github.com/unrolled/secure v1.0.9
+2
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.2.0/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=
github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= 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/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-20190728182440-6a916e37a237/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/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= github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
+2 -2
View File
@@ -2,7 +2,6 @@ package peer_test
import ( import (
"context" "context"
"errors"
"io" "io"
"net" "net"
"net/http" "net/http"
@@ -17,6 +16,7 @@ import (
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"go.uber.org/goleak" "go.uber.org/goleak"
"golang.org/x/xerrors"
"cdr.dev/slog" "cdr.dev/slog"
"cdr.dev/slog/sloggers/slogtest" "cdr.dev/slog/sloggers/slogtest"
@@ -231,7 +231,7 @@ func TestConn(t *testing.T) {
t.Parallel() t.Parallel()
conn, err := peer.Client([]webrtc.ICEServer{}, nil) conn, err := peer.Client([]webrtc.ICEServer{}, nil)
require.NoError(t, err) require.NoError(t, err)
expectedErr := errors.New("wow") expectedErr := xerrors.New("wow")
_ = conn.CloseWithError(expectedErr) _ = conn.CloseWithError(expectedErr)
_, err = conn.Dial(context.Background(), "", nil) _, err = conn.Dial(context.Background(), "", nil)
require.ErrorIs(t, err, expectedErr) require.ErrorIs(t, err, expectedErr)
+24 -5
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)) path := filepath.Join(request.Directory, fmt.Sprintf("%d.parse.protobuf", index))
_, err := os.Stat(path) _, err := os.Stat(path)
if err != nil { if err != nil {
if index == 0 {
// Error if nothing is around to enable failed states.
return xerrors.New("no state")
}
break break
} }
data, err := os.ReadFile(path) data, err := os.ReadFile(path)
@@ -64,7 +68,8 @@ func (*echo) Parse(request *proto.Parse_Request, stream proto.DRPCProvisioner_Pa
return err return err
} }
} }
return nil <-stream.Context().Done()
return stream.Context().Err()
} }
// Provision reads requests from the provided directory to stream responses. // 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)) path := filepath.Join(request.Directory, fmt.Sprintf("%d.provision.protobuf", index))
_, err := os.Stat(path) _, err := os.Stat(path)
if err != nil { if err != nil {
if index == 0 {
// Error if nothing is around to enable failed states.
return xerrors.New("no state")
}
break break
} }
data, err := os.ReadFile(path) data, err := os.ReadFile(path)
@@ -89,14 +98,24 @@ func (*echo) Provision(request *proto.Provision_Request, stream proto.DRPCProvis
return err 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. // 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 var buffer bytes.Buffer
writer := tar.NewWriter(&buffer) writer := tar.NewWriter(&buffer)
for index, response := range parseResponses { for index, response := range responses.Parse {
data, err := protobuf.Marshal(response) data, err := protobuf.Marshal(response)
if err != nil { if err != nil {
return nil, err return nil, err
@@ -113,7 +132,7 @@ func Tar(parseResponses []*proto.Parse_Response, provisionResponses []*proto.Pro
return nil, err return nil, err
} }
} }
for index, response := range provisionResponses { for index, response := range responses.Provision {
data, err := protobuf.Marshal(response) data, err := protobuf.Marshal(response)
if err != nil { if err != nil {
return nil, err return nil, err
+6 -2
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) require.NoError(t, err)
client, err := api.Parse(ctx, &proto.Parse_Request{ client, err := api.Parse(ctx, &proto.Parse_Request{
Directory: unpackTar(t, data), 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) require.NoError(t, err)
client, err := api.Provision(ctx, &proto.Provision_Request{ client, err := api.Provision(ctx, &proto.Provision_Request{
Directory: unpackTar(t, data), Directory: unpackTar(t, data),
+36 -28
View File
@@ -15,6 +15,7 @@ import (
"time" "time"
"github.com/hashicorp/yamux" "github.com/hashicorp/yamux"
"go.uber.org/atomic"
"cdr.dev/slog" "cdr.dev/slog"
"github.com/coder/coder/provisionerd/proto" "github.com/coder/coder/provisionerd/proto"
@@ -54,7 +55,8 @@ func New(clientDialer Dialer, opts *Options) io.Closer {
closeCancel: ctxCancel, closeCancel: ctxCancel,
closed: make(chan struct{}), closed: make(chan struct{}),
jobRunning: make(chan struct{}), jobRunning: make(chan struct{}),
jobCancelled: *atomic.NewBool(true),
} }
// Start off with a closed channel so // Start off with a closed channel so
// isRunningJob() returns properly. // isRunningJob() returns properly.
@@ -77,10 +79,11 @@ type provisionerDaemon struct {
closeError error closeError error
// Locked when acquiring or canceling a job. // Locked when acquiring or canceling a job.
jobMutex sync.Mutex jobMutex sync.Mutex
jobID string jobID string
jobRunning chan struct{} jobRunning chan struct{}
jobCancel context.CancelFunc jobCancelled atomic.Bool
jobCancel context.CancelFunc
} }
// Connect establishes a connection to coderd. // Connect establishes a connection to coderd.
@@ -193,6 +196,7 @@ func (p *provisionerDaemon) acquireJob(ctx context.Context) {
} }
ctx, p.jobCancel = context.WithCancel(ctx) ctx, p.jobCancel = context.WithCancel(ctx)
p.jobRunning = make(chan struct{}) p.jobRunning = make(chan struct{})
p.jobCancelled.Store(false)
p.jobID = job.JobId p.jobID = job.JobId
p.opts.Logger.Info(context.Background(), "acquired job", 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, JobId: job.JobId,
}) })
if err != nil { if err != nil {
go p.cancelActiveJobf("send periodic update: %s", err) p.cancelActiveJobf("send periodic update: %s", err)
return 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. // It's safe to cast this ProvisionerType. This data is coming directly from coderd.
provisioner, hasProvisioner := p.opts.Provisioners[job.Provisioner] provisioner, hasProvisioner := p.opts.Provisioners[job.Provisioner]
if !hasProvisioner { if !hasProvisioner {
go p.cancelActiveJobf("provisioner %q not registered", job.Provisioner) p.cancelActiveJobf("provisioner %q not registered", job.Provisioner)
return return
} }
err := os.MkdirAll(p.opts.WorkDirectory, 0700) err := os.MkdirAll(p.opts.WorkDirectory, 0700)
if err != nil { 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 return
} }
@@ -265,13 +269,13 @@ func (p *provisionerDaemon) runJob(ctx context.Context, job *proto.AcquiredJob)
break break
} }
if err != nil { if err != nil {
go p.cancelActiveJobf("read project source archive: %s", err) p.cancelActiveJobf("read project source archive: %s", err)
return return
} }
// #nosec // #nosec
path := filepath.Join(p.opts.WorkDirectory, header.Name) path := filepath.Join(p.opts.WorkDirectory, header.Name)
if !strings.HasPrefix(path, filepath.Clean(p.opts.WorkDirectory)) { 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 return
} }
mode := header.FileInfo().Mode() mode := header.FileInfo().Mode()
@@ -282,14 +286,14 @@ func (p *provisionerDaemon) runJob(ctx context.Context, job *proto.AcquiredJob)
case tar.TypeDir: case tar.TypeDir:
err = os.MkdirAll(path, mode) err = os.MkdirAll(path, mode)
if err != nil { if err != nil {
go p.cancelActiveJobf("mkdir %q: %s", path, err) p.cancelActiveJobf("mkdir %q: %s", path, err)
return return
} }
p.opts.Logger.Debug(context.Background(), "extracted directory", slog.F("path", path)) p.opts.Logger.Debug(context.Background(), "extracted directory", slog.F("path", path))
case tar.TypeReg: case tar.TypeReg:
file, err := os.OpenFile(path, os.O_CREATE|os.O_RDWR, mode) file, err := os.OpenFile(path, os.O_CREATE|os.O_RDWR, mode)
if err != nil { 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 return
} }
// Max file size of 10MB. // Max file size of 10MB.
@@ -299,12 +303,12 @@ func (p *provisionerDaemon) runJob(ctx context.Context, job *proto.AcquiredJob)
} }
if err != nil { if err != nil {
_ = file.Close() _ = file.Close()
go p.cancelActiveJobf("copy file %q: %s", path, err) p.cancelActiveJobf("copy file %q: %s", path, err)
return return
} }
err = file.Close() err = file.Close()
if err != nil { if err != nil {
go p.cancelActiveJobf("close file %q: %s", path, err) p.cancelActiveJobf("close file %q: %s", path, err)
return return
} }
p.opts.Logger.Debug(context.Background(), "extracted file", 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) p.runWorkspaceProvision(ctx, provisioner, job)
default: 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 return
} }
@@ -347,14 +351,14 @@ func (p *provisionerDaemon) runProjectImport(ctx context.Context, provisioner sd
Directory: p.opts.WorkDirectory, Directory: p.opts.WorkDirectory,
}) })
if err != nil { if err != nil {
go p.cancelActiveJobf("parse source: %s", err) p.cancelActiveJobf("parse source: %s", err)
return return
} }
defer stream.Close() defer stream.Close()
for { for {
msg, err := stream.Recv() msg, err := stream.Recv()
if err != nil { if err != nil {
go p.cancelActiveJobf("recv parse source: %s", err) p.cancelActiveJobf("recv parse source: %s", err)
return return
} }
switch msgType := msg.Type.(type) { switch msgType := msg.Type.(type) {
@@ -375,7 +379,7 @@ func (p *provisionerDaemon) runProjectImport(ctx context.Context, provisioner sd
}}, }},
}) })
if err != nil { if err != nil {
go p.cancelActiveJobf("update job: %s", err) p.cancelActiveJobf("update job: %s", err)
return return
} }
case *sdkproto.Parse_Response_Complete: case *sdkproto.Parse_Response_Complete:
@@ -391,13 +395,13 @@ func (p *provisionerDaemon) runProjectImport(ctx context.Context, provisioner sd
}, },
}) })
if err != nil { if err != nil {
go p.cancelActiveJobf("complete job: %s", err) p.cancelActiveJobf("complete job: %s", err)
return return
} }
// Return so we stop looping! // Return so we stop looping!
return return
default: 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()) reflect.TypeOf(msg.Type).String())
return return
} }
@@ -411,7 +415,7 @@ func (p *provisionerDaemon) runWorkspaceProvision(ctx context.Context, provision
State: job.GetWorkspaceProvision().State, State: job.GetWorkspaceProvision().State,
}) })
if err != nil { if err != nil {
go p.cancelActiveJobf("provision: %s", err) p.cancelActiveJobf("provision: %s", err)
return return
} }
defer stream.Close() defer stream.Close()
@@ -419,7 +423,7 @@ func (p *provisionerDaemon) runWorkspaceProvision(ctx context.Context, provision
for { for {
msg, err := stream.Recv() msg, err := stream.Recv()
if err != nil { if err != nil {
go p.cancelActiveJobf("recv workspace provision: %s", err) p.cancelActiveJobf("recv workspace provision: %s", err)
return return
} }
switch msgType := msg.Type.(type) { switch msgType := msg.Type.(type) {
@@ -440,7 +444,7 @@ func (p *provisionerDaemon) runWorkspaceProvision(ctx context.Context, provision
}}, }},
}) })
if err != nil { if err != nil {
go p.cancelActiveJobf("send job update: %s", err) p.cancelActiveJobf("send job update: %s", err)
return return
} }
case *sdkproto.Provision_Response_Complete: case *sdkproto.Provision_Response_Complete:
@@ -462,13 +466,13 @@ func (p *provisionerDaemon) runWorkspaceProvision(ctx context.Context, provision
}, },
}) })
if err != nil { if err != nil {
go p.cancelActiveJobf("complete job: %s", err) p.cancelActiveJobf("complete job: %s", err)
return return
} }
// Return so we stop looping! // Return so we stop looping!
return return
default: 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()) reflect.TypeOf(msg.Type).String())
return return
} }
@@ -481,12 +485,16 @@ func (p *provisionerDaemon) cancelActiveJobf(format string, args ...interface{})
errMsg := fmt.Sprintf(format, args...) errMsg := fmt.Sprintf(format, args...)
if !p.isRunningJob() { if !p.isRunningJob() {
if p.isClosed() { if p.isClosed() {
// We don't want to log if we're already closed!
return 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 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.jobCancel()
p.opts.Logger.Info(context.Background(), "canceling running job", p.opts.Logger.Info(context.Background(), "canceling running job",
slog.F("error_message", errMsg), 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)) p.opts.Logger.Warn(context.Background(), "failed to notify of cancel; job is no longer running", slog.Error(err))
return return
} }
<-p.jobRunning
p.opts.Logger.Debug(context.Background(), "canceled running job") p.opts.Logger.Debug(context.Background(), "canceled running job")
} }
@@ -534,6 +541,7 @@ func (p *provisionerDaemon) closeWithError(err error) error {
errMsg = err.Error() errMsg = err.Error()
} }
p.cancelActiveJobf(errMsg) p.cancelActiveJobf(errMsg)
<-p.jobRunning
p.closeCancel() p.closeCancel()
p.opts.Logger.Debug(context.Background(), "closing server with error", slog.Error(err)) p.opts.Logger.Debug(context.Background(), "closing server with error", slog.Error(err))
+2 -2
View File
@@ -4,7 +4,6 @@ import (
"archive/tar" "archive/tar"
"bytes" "bytes"
"context" "context"
"errors"
"io" "io"
"os" "os"
"path/filepath" "path/filepath"
@@ -15,6 +14,7 @@ import (
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"go.uber.org/atomic" "go.uber.org/atomic"
"go.uber.org/goleak" "go.uber.org/goleak"
"golang.org/x/xerrors"
"storj.io/drpc/drpcmux" "storj.io/drpc/drpcmux"
"storj.io/drpc/drpcserver" "storj.io/drpc/drpcserver"
@@ -52,7 +52,7 @@ func TestProvisionerd(t *testing.T) {
completeChan := make(chan struct{}) completeChan := make(chan struct{})
closer := createProvisionerd(t, func(ctx context.Context) (proto.DRPCProvisionerDaemonClient, error) { closer := createProvisionerd(t, func(ctx context.Context) (proto.DRPCProvisionerDaemonClient, error) {
defer close(completeChan) defer close(completeChan)
return nil, errors.New("an error") return nil, xerrors.New("an error")
}, provisionerd.Provisioners{}) }, provisionerd.Provisioners{})
<-completeChan <-completeChan
require.NoError(t, closer.Close()) require.NoError(t, closer.Close())
+1
View File
@@ -39,6 +39,7 @@ func TestProvisionerSDK(t *testing.T) {
_, err = stream.Recv() _, err = stream.Recv()
require.Equal(t, drpcerr.Unimplemented, int(drpcerr.Code(err))) require.Equal(t, drpcerr.Unimplemented, int(drpcerr.Code(err)))
}) })
t.Run("ServeClosedPipe", func(t *testing.T) { t.Run("ServeClosedPipe", func(t *testing.T) {
t.Parallel() t.Parallel()
client, server := provisionersdk.TransportPipe() client, server := provisionersdk.TransportPipe()
+27
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)
}