diff --git a/.github/workflows/coder.yaml b/.github/workflows/coder.yaml index 8b29a3861f871..22c7583290147 100644 --- a/.github/workflows/coder.yaml +++ b/.github/workflows/coder.yaml @@ -165,12 +165,13 @@ jobs: -covermode=atomic -coverprofile="gotests.coverage" -timeout=3m -count=1 -race -parallel=2 - - uses: actions/setup-node@v2 + - name: Setup Node for DataDog CLI + uses: actions/setup-node@v2 if: always() && github.actor != 'dependabot[bot]' with: node-version: "14" - - name: Cache DataDog CI + - name: Cache DataDog CLI if: always() && github.actor != 'dependabot[bot]' uses: actions/cache@v2 with: diff --git a/.golangci.yml b/.golangci.yml index abad144557b76..c85be43163feb 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -100,6 +100,10 @@ linters-settings: # - whyNoLint # - wrapperFunc # - yodaStyleExpr + settings: + ruleguard: + failOn: all + rules: rules.go goimports: local-prefixes: coder.com,cdr.dev,go.coder.com,github.com/cdr,github.com/coder diff --git a/coderd/coderd.go b/coderd/coderd.go index cd60b044e9d33..abce77ce2a10e 100644 --- a/coderd/coderd.go +++ b/coderd/coderd.go @@ -86,7 +86,6 @@ func New(options *Options) http.Handler { r.Get("/", api.workspaces) r.Route("/{user}", func(r chi.Router) { r.Use(httpmw.ExtractUserParam(options.Database)) - r.Get("/", api.workspaces) r.Post("/", api.postWorkspaceByUser) r.Route("/{workspace}", func(r chi.Router) { r.Use(httpmw.ExtractWorkspaceParam(options.Database)) diff --git a/coderd/coderd_test.go b/coderd/coderd_test.go new file mode 100644 index 0000000000000..ef360326b1d8e --- /dev/null +++ b/coderd/coderd_test.go @@ -0,0 +1,11 @@ +package coderd_test + +import ( + "testing" + + "go.uber.org/goleak" +) + +func TestMain(m *testing.M) { + goleak.VerifyTestMain(m) +} diff --git a/coderd/coderdtest/coderdtest.go b/coderd/coderdtest/coderdtest.go index 86a50fabd0d28..430ed0c66a283 100644 --- a/coderd/coderdtest/coderdtest.go +++ b/coderd/coderdtest/coderdtest.go @@ -7,16 +7,18 @@ import ( "net/http/httptest" "net/url" "os" + "strings" "testing" "time" + "github.com/google/uuid" + "github.com/moby/moby/pkg/namesgenerator" "github.com/stretchr/testify/require" "cdr.dev/slog" "cdr.dev/slog/sloggers/slogtest" "github.com/coder/coder/coderd" "github.com/coder/coder/codersdk" - "github.com/coder/coder/cryptorand" "github.com/coder/coder/database" "github.com/coder/coder/database/databasefake" "github.com/coder/coder/database/postgres" @@ -26,47 +28,49 @@ import ( "github.com/coder/coder/provisionersdk/proto" ) -// Server represents a test instance of coderd. -// The database is intentionally omitted from -// this struct to promote data being exposed via -// the API. -type Server struct { - Client *codersdk.Client - URL *url.URL -} - -// RandomInitialUser generates a random initial user and authenticates -// it with the client on the Server struct. -func (s *Server) RandomInitialUser(t *testing.T) coderd.CreateInitialUserRequest { - username, err := cryptorand.String(12) - require.NoError(t, err) - password, err := cryptorand.String(12) - require.NoError(t, err) - organization, err := cryptorand.String(12) - require.NoError(t, err) +// New constructs a new coderd test instance. This returned Server +// should contain no side-effects. +func New(t *testing.T) *codersdk.Client { + // This can be hotswapped for a live database instance. + db := databasefake.New() + pubsub := database.NewPubsubInMemory() + if os.Getenv("DB") != "" { + connectionURL, close, err := postgres.Open() + require.NoError(t, err) + t.Cleanup(close) + sqlDB, err := sql.Open("postgres", connectionURL) + require.NoError(t, err) + t.Cleanup(func() { + _ = sqlDB.Close() + }) + err = database.Migrate(sqlDB) + require.NoError(t, err) + db = database.New(sqlDB) - req := coderd.CreateInitialUserRequest{ - Email: "testuser@coder.com", - Username: username, - Password: password, - Organization: organization, + pubsub, err = database.NewPubsub(context.Background(), sqlDB, connectionURL) + require.NoError(t, err) + t.Cleanup(func() { + _ = pubsub.Close() + }) } - _, 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, + handler := coderd.New(&coderd.Options{ + Logger: slogtest.Make(t, nil).Leveled(slog.LevelDebug), + Database: db, + Pubsub: pubsub, }) + srv := httptest.NewServer(handler) + serverURL, err := url.Parse(srv.URL) require.NoError(t, err) - err = s.Client.SetSessionToken(login.SessionToken) - require.NoError(t, err) - return req + t.Cleanup(srv.Close) + + return codersdk.New(serverURL) } -// AddProvisionerd launches a new provisionerd instance with the -// test provisioner registered. -func (s *Server) AddProvisionerd(t *testing.T) io.Closer { +// 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() { @@ -81,7 +85,7 @@ func (s *Server) AddProvisionerd(t *testing.T) io.Closer { require.NoError(t, err) }() - closer := provisionerd.New(s.Client.ProvisionerDaemonClient, &provisionerd.Options{ + 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, @@ -96,44 +100,87 @@ func (s *Server) AddProvisionerd(t *testing.T) io.Closer { return closer } -// New constructs a new coderd test instance. This returned Server -// should contain no side-effects. -func New(t *testing.T) Server { - // This can be hotswapped for a live database instance. - db := databasefake.New() - pubsub := database.NewPubsubInMemory() - if os.Getenv("DB") != "" { - connectionURL, close, err := postgres.Open() - require.NoError(t, err) - t.Cleanup(close) - sqlDB, err := sql.Open("postgres", connectionURL) - require.NoError(t, err) - t.Cleanup(func() { - _ = sqlDB.Close() - }) - err = database.Migrate(sqlDB) - require.NoError(t, err) - db = database.New(sqlDB) +// 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) - pubsub, err = database.NewPubsub(context.Background(), sqlDB, connectionURL) + 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) - t.Cleanup(func() { - _ = pubsub.Close() - }) - } + return projectVersion.Import.Status.Completed() + }, 3*time.Second, 25*time.Millisecond) + return projectVersion +} - handler := coderd.New(&coderd.Options{ - Logger: slogtest.Make(t, nil).Leveled(slog.LevelDebug), - Database: db, - Pubsub: pubsub, +// 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(), }) - srv := httptest.NewServer(handler) - serverURL, err := url.Parse(srv.URL) require.NoError(t, err) - t.Cleanup(srv.Close) + return workspace +} - return Server{ - Client: codersdk.New(serverURL), - URL: serverURL, - } +// 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), "_", "-") } diff --git a/coderd/coderdtest/coderdtest_test.go b/coderd/coderdtest/coderdtest_test.go index b7312f96864fc..ca0e1d9a8a04f 100644 --- a/coderd/coderdtest/coderdtest_test.go +++ b/coderd/coderdtest/coderdtest_test.go @@ -1,11 +1,16 @@ package coderdtest_test import ( + "context" "testing" "go.uber.org/goleak" + "github.com/stretchr/testify/require" + + "github.com/coder/coder/coderd" "github.com/coder/coder/coderd/coderdtest" + "github.com/coder/coder/database" ) func TestMain(m *testing.M) { @@ -14,7 +19,18 @@ func TestMain(m *testing.M) { func TestNew(t *testing.T) { t.Parallel() - server := coderdtest.New(t) - _ = server.RandomInitialUser(t) - _ = server.AddProvisionerd(t) + client := coderdtest.New(t) + user := coderdtest.CreateInitialUser(t, client) + closer := coderdtest.NewProvisionerDaemon(t, client) + project := coderdtest.CreateProject(t, client, user.Organization) + version := coderdtest.CreateProjectVersion(t, client, user.Organization, project.Name, nil) + coderdtest.AwaitProjectVersionImported(t, client, user.Organization, project.Name, version.Name) + workspace := coderdtest.CreateWorkspace(t, client, "me", project.ID) + history, err := client.CreateWorkspaceHistory(context.Background(), "me", workspace.Name, coderd.CreateWorkspaceHistoryRequest{ + ProjectVersionID: version.ID, + Transition: database.WorkspaceTransitionCreate, + }) + require.NoError(t, err) + coderdtest.AwaitWorkspaceHistoryProvisioned(t, client, "me", workspace.Name, history.Name) + closer.Close() } diff --git a/coderd/projects.go b/coderd/projects.go index 991d20f0e79cf..e9ff4edeaaba5 100644 --- a/coderd/projects.go +++ b/coderd/projects.go @@ -49,6 +49,9 @@ func (api *api) projects(rw http.ResponseWriter, r *http.Request) { }) return } + if projects == nil { + projects = []database.Project{} + } render.Status(r, http.StatusOK) render.JSON(rw, r, projects) } @@ -66,6 +69,9 @@ func (api *api) projectsByOrganization(rw http.ResponseWriter, r *http.Request) }) return } + if projects == nil { + projects = []database.Project{} + } render.Status(r, http.StatusOK) render.JSON(rw, r, projects) } @@ -124,32 +130,6 @@ func (*api) projectByOrganization(rw http.ResponseWriter, r *http.Request) { render.JSON(rw, r, project) } -// Returns all workspaces for a specific project. -func (api *api) workspacesByProject(rw http.ResponseWriter, r *http.Request) { - apiKey := httpmw.APIKey(r) - project := httpmw.ProjectParam(r) - workspaces, err := api.Database.GetWorkspacesByProjectAndUserID(r.Context(), database.GetWorkspacesByProjectAndUserIDParams{ - OwnerID: apiKey.UserID, - ProjectID: project.ID, - }) - if errors.Is(err, sql.ErrNoRows) { - err = nil - } - if err != nil { - httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ - Message: fmt.Sprintf("get workspaces: %s", err), - }) - return - } - - apiWorkspaces := make([]Workspace, 0, len(workspaces)) - for _, workspace := range workspaces { - apiWorkspaces = append(apiWorkspaces, convertWorkspace(workspace)) - } - render.Status(r, http.StatusOK) - render.JSON(rw, r, apiWorkspaces) -} - // Creates parameters for a project. // This should validate the calling user has permissions! func (api *api) postParametersByProject(rw http.ResponseWriter, r *http.Request) { diff --git a/coderd/projects_test.go b/coderd/projects_test.go index b5bbc03766522..5788f66647543 100644 --- a/coderd/projects_test.go +++ b/coderd/projects_test.go @@ -2,124 +2,109 @@ package coderd_test import ( "context" + "net/http" "testing" - "time" "github.com/stretchr/testify/require" "github.com/coder/coder/coderd" "github.com/coder/coder/coderd/coderdtest" + "github.com/coder/coder/codersdk" "github.com/coder/coder/database" - "github.com/coder/coder/provisioner/echo" - "github.com/coder/coder/provisionersdk/proto" ) func TestProjects(t *testing.T) { t.Parallel() - t.Run("Create", func(t *testing.T) { + t.Run("ListEmpty", 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, - }) + client := coderdtest.New(t) + _ = coderdtest.CreateInitialUser(t, client) + projects, err := client.Projects(context.Background(), "") require.NoError(t, err) + require.NotNil(t, projects) + require.Len(t, projects, 0) }) - t.Run("AlreadyExists", func(t *testing.T) { + t.Run("List", func(t *testing.T) { t.Parallel() - server := coderdtest.New(t) - user := server.RandomInitialUser(t) - _, err := server.Client.CreateProject(context.Background(), user.Organization, coderd.CreateProjectRequest{ - Name: "someproject", - Provisioner: database.ProvisionerTypeEcho, - }) + client := coderdtest.New(t) + user := coderdtest.CreateInitialUser(t, client) + _ = coderdtest.CreateProject(t, client, user.Organization) + projects, err := client.Projects(context.Background(), "") require.NoError(t, err) - _, err = server.Client.CreateProject(context.Background(), user.Organization, coderd.CreateProjectRequest{ - Name: "someproject", - Provisioner: database.ProvisionerTypeEcho, - }) - require.Error(t, err) + require.Len(t, projects, 1) }) +} +func TestProjectsByOrganization(t *testing.T) { + t.Parallel() t.Run("ListEmpty", func(t *testing.T) { t.Parallel() - server := coderdtest.New(t) - _ = server.RandomInitialUser(t) - projects, err := server.Client.Projects(context.Background(), "") + client := coderdtest.New(t) + user := coderdtest.CreateInitialUser(t, client) + projects, err := client.Projects(context.Background(), user.Organization) require.NoError(t, err) + require.NotNil(t, projects) require.Len(t, projects, 0) }) t.Run("List", func(t *testing.T) { t.Parallel() - server := coderdtest.New(t) - user := server.RandomInitialUser(t) - _, err := server.Client.CreateProject(context.Background(), user.Organization, coderd.CreateProjectRequest{ - Name: "someproject", - Provisioner: database.ProvisionerTypeEcho, - }) - require.NoError(t, err) - // Ensure global query works. - projects, err := server.Client.Projects(context.Background(), "") - require.NoError(t, err) - require.Len(t, projects, 1) - - // Ensure specified query works. - projects, err = server.Client.Projects(context.Background(), user.Organization) + client := coderdtest.New(t) + user := coderdtest.CreateInitialUser(t, client) + _ = coderdtest.CreateProject(t, client, user.Organization) + projects, err := client.Projects(context.Background(), "") require.NoError(t, err) require.Len(t, projects, 1) }) +} - t.Run("ListEmpty", func(t *testing.T) { +func TestPostProjectsByOrganization(t *testing.T) { + t.Parallel() + t.Run("Create", func(t *testing.T) { t.Parallel() - server := coderdtest.New(t) - user := server.RandomInitialUser(t) - - projects, err := server.Client.Projects(context.Background(), user.Organization) - require.NoError(t, err) - require.Len(t, projects, 0) + client := coderdtest.New(t) + user := coderdtest.CreateInitialUser(t, client) + _ = coderdtest.CreateProject(t, client, user.Organization) }) - t.Run("Single", func(t *testing.T) { + t.Run("AlreadyExists", func(t *testing.T) { t.Parallel() - server := coderdtest.New(t) - user := server.RandomInitialUser(t) - project, err := server.Client.CreateProject(context.Background(), user.Organization, coderd.CreateProjectRequest{ - Name: "someproject", + client := coderdtest.New(t) + user := coderdtest.CreateInitialUser(t, client) + project := coderdtest.CreateProject(t, client, user.Organization) + _, err := client.CreateProject(context.Background(), user.Organization, coderd.CreateProjectRequest{ + Name: project.Name, Provisioner: database.ProvisionerTypeEcho, }) - require.NoError(t, err) - _, err = server.Client.Project(context.Background(), user.Organization, project.Name) - require.NoError(t, err) + var apiErr *codersdk.Error + require.ErrorAs(t, err, &apiErr) + require.Equal(t, http.StatusConflict, apiErr.StatusCode()) }) +} - t.Run("Parameters", func(t *testing.T) { +func TestProjectByOrganization(t *testing.T) { + t.Parallel() + t.Run("Get", 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.ProjectParameters(context.Background(), user.Organization, project.Name) + client := coderdtest.New(t) + user := coderdtest.CreateInitialUser(t, client) + project := coderdtest.CreateProject(t, client, user.Organization) + _, err := client.Project(context.Background(), user.Organization, project.Name) require.NoError(t, err) }) +} - t.Run("CreateParameter", func(t *testing.T) { +func TestPostParametersByProject(t *testing.T) { + t.Parallel() + t.Run("Create", func(t *testing.T) { t.Parallel() - server := coderdtest.New(t) - user := server.RandomInitialUser(t) - project, err := server.Client.CreateProject(context.Background(), user.Organization, coderd.CreateProjectRequest{ - Name: "someproject", - Provisioner: database.ProvisionerTypeEcho, - }) - require.NoError(t, err) - _, err = server.Client.CreateProjectParameter(context.Background(), user.Organization, project.Name, coderd.CreateParameterValueRequest{ - Name: "hi", + client := coderdtest.New(t) + user := coderdtest.CreateInitialUser(t, client) + project := coderdtest.CreateProject(t, client, user.Organization) + _, err := client.CreateProjectParameter(context.Background(), user.Organization, project.Name, coderd.CreateParameterValueRequest{ + Name: "somename", SourceValue: "tomato", SourceScheme: database.ParameterSourceSchemeData, DestinationScheme: database.ParameterDestinationSchemeEnvironmentVariable, @@ -127,40 +112,36 @@ func TestProjects(t *testing.T) { }) require.NoError(t, err) }) +} - t.Run("Import", func(t *testing.T) { +func TestParametersByProject(t *testing.T) { + t.Parallel() + t.Run("ListEmpty", func(t *testing.T) { t.Parallel() - server := coderdtest.New(t) - user := server.RandomInitialUser(t) - _ = server.AddProvisionerd(t) - project, err := server.Client.CreateProject(context.Background(), user.Organization, coderd.CreateProjectRequest{ - Name: "someproject", - Provisioner: database.ProvisionerTypeEcho, - }) - require.NoError(t, err) - data, err := echo.Tar([]*proto.Parse_Response{{ - Type: &proto.Parse_Response_Complete{ - Complete: &proto.Parse_Complete{ - ParameterSchemas: []*proto.ParameterSchema{{ - Name: "example", - }}, - }, - }, - }}, nil) + client := coderdtest.New(t) + user := coderdtest.CreateInitialUser(t, client) + project := coderdtest.CreateProject(t, client, user.Organization) + params, err := client.ProjectParameters(context.Background(), user.Organization, project.Name) require.NoError(t, err) - version, err := server.Client.CreateProjectVersion(context.Background(), user.Organization, project.Name, coderd.CreateProjectVersionRequest{ - StorageMethod: database.ProjectStorageMethodInlineArchive, - StorageSource: data, + 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.Eventually(t, func() bool { - projectVersion, err := server.Client.ProjectVersion(context.Background(), user.Organization, project.Name, version.Name) - require.NoError(t, err) - return projectVersion.Import.Status.Completed() - }, 15*time.Second, 10*time.Millisecond) - params, err := server.Client.ProjectVersionParameters(context.Background(), user.Organization, project.Name, version.Name) + params, err := client.ProjectParameters(context.Background(), user.Organization, project.Name) require.NoError(t, err) + require.NotNil(t, params) require.Len(t, params, 1) - require.Equal(t, "example", params[0].Name) }) } diff --git a/coderd/projectversion.go b/coderd/projectversion.go index ac1906f2be069..1f045e1c09101 100644 --- a/coderd/projectversion.go +++ b/coderd/projectversion.go @@ -110,19 +110,11 @@ func (api *api) postProjectVersionByOrganization(rw http.ResponseWriter, r *http return } - switch createProjectVersion.StorageMethod { - case database.ProjectStorageMethodInlineArchive: - tarReader := tar.NewReader(bytes.NewReader(createProjectVersion.StorageSource)) - _, err := tarReader.Next() - if err != nil { - httpapi.Write(rw, http.StatusBadRequest, httpapi.Response{ - Message: "the archive must be a tar", - }) - return - } - default: + tarReader := tar.NewReader(bytes.NewReader(createProjectVersion.StorageSource)) + _, err := tarReader.Next() + if err != nil { httpapi.Write(rw, http.StatusBadRequest, httpapi.Response{ - Message: fmt.Sprintf("unsupported storage method %s", createProjectVersion.StorageMethod), + Message: "the archive must be a tar", }) return } @@ -132,7 +124,7 @@ func (api *api) postProjectVersionByOrganization(rw http.ResponseWriter, r *http var provisionerJob database.ProvisionerJob var projectVersion database.ProjectVersion - err := api.Database.InTx(func(db database.Store) error { + err = api.Database.InTx(func(db database.Store) error { projectVersionID := uuid.New() input, err := json.Marshal(projectImportJob{ ProjectVersionID: projectVersionID, diff --git a/coderd/projectversion_test.go b/coderd/projectversion_test.go index 69d8d011b42d6..50fe0e4fd0227 100644 --- a/coderd/projectversion_test.go +++ b/coderd/projectversion_test.go @@ -1,103 +1,129 @@ package coderd_test import ( - "archive/tar" - "bytes" "context" + "net/http" "testing" "github.com/stretchr/testify/require" "github.com/coder/coder/coderd" "github.com/coder/coder/coderd/coderdtest" + "github.com/coder/coder/codersdk" "github.com/coder/coder/database" "github.com/coder/coder/provisioner/echo" "github.com/coder/coder/provisionersdk/proto" ) -func TestProjectVersion(t *testing.T) { +func TestProjectVersionsByOrganization(t *testing.T) { t.Parallel() - - t.Run("NoHistory", func(t *testing.T) { + t.Run("ListEmpty", func(t *testing.T) { t.Parallel() - server := coderdtest.New(t) - user := server.RandomInitialUser(t) - project, err := server.Client.CreateProject(context.Background(), user.Organization, coderd.CreateProjectRequest{ - Name: "someproject", - Provisioner: database.ProvisionerTypeEcho, - }) - require.NoError(t, err) - versions, err := server.Client.ProjectVersions(context.Background(), user.Organization, project.Name) + client := coderdtest.New(t) + user := coderdtest.CreateInitialUser(t, client) + project := coderdtest.CreateProject(t, client, user.Organization) + versions, err := client.ProjectVersions(context.Background(), user.Organization, project.Name) require.NoError(t, err) + require.NotNil(t, versions) require.Len(t, versions, 0) }) - t.Run("CreateVersion", func(t *testing.T) { + t.Run("List", func(t *testing.T) { t.Parallel() - server := coderdtest.New(t) - user := server.RandomInitialUser(t) - project, err := server.Client.CreateProject(context.Background(), user.Organization, coderd.CreateProjectRequest{ - Name: "someproject", - Provisioner: database.ProvisionerTypeEcho, - }) - require.NoError(t, err) - data, err := echo.Tar([]*proto.Parse_Response{{ - Type: &proto.Parse_Response_Complete{ - Complete: &proto.Parse_Complete{}, - }, - }}, nil) - require.NoError(t, err) - version, err := server.Client.CreateProjectVersion(context.Background(), user.Organization, project.Name, coderd.CreateProjectVersionRequest{ - StorageMethod: database.ProjectStorageMethodInlineArchive, - StorageSource: data, - }) - require.NoError(t, err) - versions, err := server.Client.ProjectVersions(context.Background(), user.Organization, project.Name) + client := coderdtest.New(t) + user := coderdtest.CreateInitialUser(t, client) + project := coderdtest.CreateProject(t, client, user.Organization) + _ = coderdtest.CreateProjectVersion(t, client, user.Organization, project.Name, nil) + versions, err := client.ProjectVersions(context.Background(), user.Organization, project.Name) require.NoError(t, err) require.Len(t, versions, 1) + }) +} - _, err = server.Client.ProjectVersion(context.Background(), user.Organization, project.Name, version.Name) - require.NoError(t, err) +func TestProjectVersionByOrganizationAndName(t *testing.T) { + t.Parallel() + t.Run("Get", func(t *testing.T) { + t.Parallel() + client := coderdtest.New(t) + user := coderdtest.CreateInitialUser(t, client) + project := coderdtest.CreateProject(t, client, user.Organization) + version := coderdtest.CreateProjectVersion(t, client, user.Organization, project.Name, nil) + require.Equal(t, version.Import.Status, coderd.ProvisionerJobStatusPending) }) +} - t.Run("CreateHistoryArchiveTooBig", func(t *testing.T) { +func TestPostProjectVersionByOrganization(t *testing.T) { + t.Parallel() + t.Run("Create", func(t *testing.T) { t.Parallel() - server := coderdtest.New(t) - user := server.RandomInitialUser(t) - project, err := server.Client.CreateProject(context.Background(), user.Organization, coderd.CreateProjectRequest{ - Name: "someproject", - Provisioner: database.ProvisionerTypeEcho, - }) - require.NoError(t, err) - 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(), + 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("InvalidStorage", func(t *testing.T) { + t.Parallel() + client := coderdtest.New(t) + user := coderdtest.CreateInitialUser(t, client) + project := coderdtest.CreateProject(t, client, user.Organization) + _, err := client.CreateProjectVersion(context.Background(), user.Organization, project.Name, coderd.CreateProjectVersionRequest{ + StorageMethod: database.ProjectStorageMethod("invalid"), + StorageSource: []byte{}, }) require.Error(t, err) }) +} - t.Run("CreateHistoryInvalidArchive", func(t *testing.T) { +func TestProjectVersionParametersByOrganizationAndName(t *testing.T) { + t.Parallel() + t.Run("NotImported", 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, + 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{{}}, }) - require.NoError(t, err) - _, err = server.Client.CreateProjectVersion(context.Background(), user.Organization, project.Name, coderd.CreateProjectVersionRequest{ - StorageMethod: database.ProjectStorageMethodInlineArchive, - StorageSource: []byte{}, + 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", + }}, + }, + }, + }}, }) - require.Error(t, err) + 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) }) } diff --git a/coderd/provisionerdaemons.go b/coderd/provisionerdaemons.go index 69886a6848a40..f6d616383ab1a 100644 --- a/coderd/provisionerdaemons.go +++ b/coderd/provisionerdaemons.go @@ -6,6 +6,7 @@ import ( "encoding/json" "errors" "fmt" + "io" "net/http" "reflect" "time" @@ -35,7 +36,6 @@ func (api *api) provisionerDaemons(rw http.ResponseWriter, r *http.Request) { daemons, err := api.Database.GetProvisionerDaemons(r.Context()) if errors.Is(err, sql.ErrNoRows) { err = nil - daemons = []database.ProvisionerDaemon{} } if err != nil { httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ @@ -43,7 +43,9 @@ func (api *api) provisionerDaemons(rw http.ResponseWriter, r *http.Request) { }) return } - + if daemons == nil { + daemons = []database.ProvisionerDaemon{} + } render.Status(r, http.StatusOK) render.JSON(rw, r, daemons) } @@ -51,7 +53,7 @@ func (api *api) provisionerDaemons(rw http.ResponseWriter, r *http.Request) { // Serves the provisioner daemon protobuf API over a WebSocket. func (api *api) provisionerDaemonsServe(rw http.ResponseWriter, r *http.Request) { conn, err := websocket.Accept(rw, r, &websocket.AcceptOptions{ - // Need to disable compression to avoid a data-race + // Need to disable compression to avoid a data-race. CompressionMode: websocket.CompressionDisabled, }) if err != nil { @@ -75,7 +77,9 @@ func (api *api) provisionerDaemonsServe(rw http.ResponseWriter, r *http.Request) // Multiplexes the incoming connection using yamux. // This allows multiple function calls to occur over // the same connection. - session, err := yamux.Server(websocket.NetConn(r.Context(), conn, websocket.MessageBinary), nil) + config := yamux.DefaultConfig() + config.LogOutput = io.Discard + session, err := yamux.Server(websocket.NetConn(r.Context(), conn, websocket.MessageBinary), config) if err != nil { _ = conn.Close(websocket.StatusInternalError, fmt.Sprintf("multiplex server: %s", err)) return @@ -221,25 +225,11 @@ func (server *provisionerdServer) AcquireJob(ctx context.Context, _ *proto.Empty protoParameters = append(protoParameters, parameter.Proto) } - provisionerState := []byte{} - // If workspace history exists before this entry, use that state. - // We can't use the before state everytime, because if a job fails - // for some random reason, the workspace shouldn't be reset. - // - // Maybe we should make state global on a workspace? - if workspaceHistory.BeforeID.Valid { - beforeHistory, err := server.Database.GetWorkspaceHistoryByID(ctx, workspaceHistory.BeforeID.UUID) - if err != nil { - return nil, failJob(fmt.Sprintf("get workspace history: %s", err)) - } - provisionerState = beforeHistory.ProvisionerState - } - protoJob.Type = &proto.AcquiredJob_WorkspaceProvision_{ WorkspaceProvision: &proto.AcquiredJob_WorkspaceProvision{ WorkspaceHistoryId: workspaceHistory.ID.String(), WorkspaceName: workspace.Name, - State: provisionerState, + State: workspaceHistory.ProvisionerState, ParameterValues: protoParameters, }, } @@ -286,10 +276,10 @@ func (server *provisionerdServer) UpdateJob(stream proto.DRPCProvisionerDaemon_U return xerrors.Errorf("get job: %w", err) } if !job.WorkerID.Valid { - return errors.New("job isn't running yet") + return xerrors.New("job isn't running yet") } if job.WorkerID.UUID.String() != server.ID.String() { - return errors.New("you don't own this job") + return xerrors.New("you don't own this job") } err = server.Database.UpdateProvisionerJobByID(stream.Context(), database.UpdateProvisionerJobByIDParams{ diff --git a/coderd/provisionerdaemons_test.go b/coderd/provisionerdaemons_test.go index 5cba701d5a34e..f37923aa00a30 100644 --- a/coderd/provisionerdaemons_test.go +++ b/coderd/provisionerdaemons_test.go @@ -11,16 +11,18 @@ import ( ) func TestProvisionerDaemons(t *testing.T) { + // Tests for properly processing specific job + // types should be placed in their respective + // resource location. + // + // eg. project import is a project-related job t.Parallel() - t.Run("Register", func(t *testing.T) { - t.Parallel() - server := coderdtest.New(t) - _ = server.AddProvisionerd(t) - require.Eventually(t, func() bool { - daemons, err := server.Client.ProvisionerDaemons(context.Background()) - require.NoError(t, err) - return len(daemons) > 0 - }, time.Second, 10*time.Millisecond) - }) + client := coderdtest.New(t) + _ = coderdtest.NewProvisionerDaemon(t, client) + require.Eventually(t, func() bool { + daemons, err := client.ProvisionerDaemons(context.Background()) + require.NoError(t, err) + return len(daemons) > 0 + }, time.Second, 25*time.Millisecond) } diff --git a/coderd/provisioners.go b/coderd/provisioners.go index 959e69b565801..f2cd46eb4b763 100644 --- a/coderd/provisioners.go +++ b/coderd/provisioners.go @@ -70,7 +70,7 @@ func convertProvisionerJob(provisionerJob database.ProvisionerJob) ProvisionerJo job.Status = ProvisionerJobStatusRunning } - if job.Error != "" { + if !provisionerJob.CancelledAt.Valid && job.Error != "" { job.Status = ProvisionerJobStatusFailed } diff --git a/coderd/users.go b/coderd/users.go index 8aafc1f1bd9e0..0644a78d01aff 100644 --- a/coderd/users.go +++ b/coderd/users.go @@ -195,6 +195,10 @@ func (api *api) organizationsByUser(rw http.ResponseWriter, r *http.Request) { user := httpmw.UserParam(r) organizations, err := api.Database.GetOrganizationsByUserID(r.Context(), user.ID) + if errors.Is(err, sql.ErrNoRows) { + err = nil + organizations = []database.Organization{} + } if err != nil { httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ Message: fmt.Sprintf("get organizations: %s", err.Error()), diff --git a/coderd/users_test.go b/coderd/users_test.go index 11b533b0f7bd8..b3f36b3dd0914 100644 --- a/coderd/users_test.go +++ b/coderd/users_test.go @@ -9,107 +9,143 @@ import ( "github.com/coder/coder/coderd" "github.com/coder/coder/coderd/coderdtest" + "github.com/coder/coder/codersdk" "github.com/coder/coder/httpmw" ) -func TestUsers(t *testing.T) { +func TestPostUser(t *testing.T) { t.Parallel() - - t.Run("Authenticated", func(t *testing.T) { + t.Run("BadRequest", func(t *testing.T) { t.Parallel() - server := coderdtest.New(t) - _ = server.RandomInitialUser(t) - _, err := server.Client.User(context.Background(), "") - require.NoError(t, err) + client := coderdtest.New(t) + _, err := client.CreateInitialUser(context.Background(), coderd.CreateInitialUserRequest{}) + require.Error(t, err) }) - t.Run("CreateMultipleInitial", func(t *testing.T) { + t.Run("AlreadyExists", 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", + client := coderdtest.New(t) + _ = coderdtest.CreateInitialUser(t, client) + _, err := client.CreateInitialUser(context.Background(), coderd.CreateInitialUserRequest{ + Email: "some@email.com", + Username: "exampleuser", Password: "password", + Organization: "someorg", }) - require.Error(t, err) + var apiErr *codersdk.Error + require.ErrorAs(t, err, &apiErr) + require.Equal(t, http.StatusConflict, apiErr.StatusCode()) }) - t.Run("Login", func(t *testing.T) { + t.Run("Create", 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: user.Password, - }) - require.NoError(t, err) + client := coderdtest.New(t) + _ = coderdtest.CreateInitialUser(t, client) }) +} - t.Run("LoginInvalidUser", func(t *testing.T) { +func TestPostUsers(t *testing.T) { + t.Parallel() + t.Run("BadRequest", func(t *testing.T) { t.Parallel() - server := coderdtest.New(t) - _, err := server.Client.LoginWithPassword(context.Background(), coderd.LoginWithPasswordRequest{ - Email: "hello@io.io", - Password: "wowie", - }) + client := coderdtest.New(t) + _, err := client.CreateInitialUser(context.Background(), coderd.CreateInitialUserRequest{}) require.Error(t, err) }) - t.Run("LoginBadPassword", func(t *testing.T) { + t.Run("Conflicting", 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", + 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", }) - require.Error(t, err) + var apiErr *codersdk.Error + require.ErrorAs(t, err, &apiErr) + require.Equal(t, http.StatusConflict, apiErr.StatusCode()) }) - t.Run("ListOrganizations", func(t *testing.T) { + t.Run("Create", func(t *testing.T) { t.Parallel() - server := coderdtest.New(t) - _ = server.RandomInitialUser(t) - orgs, err := server.Client.UserOrganizations(context.Background(), "") + 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) - require.Len(t, orgs, 1) }) +} - t.Run("CreateUser", func(t *testing.T) { +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() - server := coderdtest.New(t) - _ = server.RandomInitialUser(t) - _, err := server.Client.CreateUser(context.Background(), coderd.CreateUserRequest{ - Email: "wow@ok.io", - Username: "tomato", - Password: "bananas", + client := coderdtest.New(t) + _, err := client.LoginWithPassword(context.Background(), coderd.LoginWithPasswordRequest{ + Email: "my@email.org", + Password: "password", }) - require.NoError(t, err) + 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("CreateUserConflict", func(t *testing.T) { + t.Run("Success", 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", + client := coderdtest.New(t) + user := coderdtest.CreateInitialUser(t, client) + _, err := client.LoginWithPassword(context.Background(), coderd.LoginWithPasswordRequest{ + Email: user.Email, + Password: user.Password, }) - require.Error(t, err) + require.NoError(t, err) }) } -func TestLogout(t *testing.T) { +func TestPostLogout(t *testing.T) { t.Parallel() - t.Run("LogoutShouldClearCookie", func(t *testing.T) { + t.Run("ClearCookie", func(t *testing.T) { t.Parallel() - server := coderdtest.New(t) - fullURL, err := server.URL.Parse("/api/v2/logout") + client := coderdtest.New(t) + fullURL, err := client.URL.Parse("/api/v2/logout") require.NoError(t, err, "Server URL should parse successfully") req, err := http.NewRequestWithContext(context.Background(), http.MethodPost, fullURL.String(), nil) diff --git a/coderd/workspacehistory.go b/coderd/workspacehistory.go index 8ebdee7df1702..dae2a4d157436 100644 --- a/coderd/workspacehistory.go +++ b/coderd/workspacehistory.go @@ -74,12 +74,12 @@ func (api *api) postWorkspaceHistoryByUser(rw http.ResponseWriter, r *http.Reque projectVersionJobStatus := convertProvisionerJob(projectVersionJob).Status switch projectVersionJobStatus { case ProvisionerJobStatusPending, ProvisionerJobStatusRunning: - httpapi.Write(rw, http.StatusPreconditionFailed, httpapi.Response{ + httpapi.Write(rw, http.StatusNotAcceptable, httpapi.Response{ Message: fmt.Sprintf("The provided project version is %s. Wait for it to complete importing!", projectVersionJobStatus), }) return case ProvisionerJobStatusFailed: - httpapi.Write(rw, http.StatusBadRequest, httpapi.Response{ + httpapi.Write(rw, http.StatusPreconditionFailed, httpapi.Response{ Message: fmt.Sprintf("The provided project version %q has failed to import. You cannot create workspaces using it!", projectVersion.Name), }) return @@ -87,6 +87,7 @@ func (api *api) postWorkspaceHistoryByUser(rw http.ResponseWriter, r *http.Reque httpapi.Write(rw, http.StatusPreconditionFailed, httpapi.Response{ Message: "The provided project version was canceled during import. You cannot create workspaces using it!", }) + return } project, err := api.Database.GetProjectByID(r.Context(), projectVersion.ProjectID) @@ -102,7 +103,7 @@ func (api *api) postWorkspaceHistoryByUser(rw http.ResponseWriter, r *http.Reque priorHistory, err := api.Database.GetWorkspaceHistoryByWorkspaceIDWithoutAfter(r.Context(), workspace.ID) if err == nil { priorJob, err := api.Database.GetProvisionerJobByID(r.Context(), priorHistory.ProvisionJobID) - if err == nil && convertProvisionerJob(priorJob).Status.Completed() { + if err == nil && !convertProvisionerJob(priorJob).Status.Completed() { httpapi.Write(rw, http.StatusConflict, httpapi.Response{ Message: "a workspace build is already active", }) @@ -113,8 +114,7 @@ func (api *api) postWorkspaceHistoryByUser(rw http.ResponseWriter, r *http.Reque UUID: priorHistory.ID, Valid: true, } - } - if !errors.Is(err, sql.ErrNoRows) { + } else if !errors.Is(err, sql.ErrNoRows) { httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ Message: fmt.Sprintf("get prior workspace history: %s", err), }) @@ -168,8 +168,9 @@ func (api *api) postWorkspaceHistoryByUser(rw http.ResponseWriter, r *http.Reque if priorHistoryID.Valid { // Update the prior history entries "after" column. err = db.UpdateWorkspaceHistoryByID(r.Context(), database.UpdateWorkspaceHistoryByIDParams{ - ID: priorHistory.ID, - UpdatedAt: database.Now(), + ID: priorHistory.ID, + ProvisionerState: priorHistory.ProvisionerState, + UpdatedAt: database.Now(), AfterID: uuid.NullUUID{ UUID: workspaceHistory.ID, Valid: true, @@ -197,9 +198,10 @@ func (api *api) postWorkspaceHistoryByUser(rw http.ResponseWriter, r *http.Reque func (api *api) workspaceHistoryByUser(rw http.ResponseWriter, r *http.Request) { workspace := httpmw.WorkspaceParam(r) - histories, err := api.Database.GetWorkspaceHistoryByWorkspaceID(r.Context(), workspace.ID) + history, err := api.Database.GetWorkspaceHistoryByWorkspaceID(r.Context(), workspace.ID) if errors.Is(err, sql.ErrNoRows) { err = nil + history = []database.WorkspaceHistory{} } if err != nil { httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ @@ -208,8 +210,8 @@ func (api *api) workspaceHistoryByUser(rw http.ResponseWriter, r *http.Request) return } - apiHistory := make([]WorkspaceHistory, 0, len(histories)) - for _, history := range histories { + apiHistory := make([]WorkspaceHistory, 0, len(history)) + for _, history := range history { job, err := api.Database.GetProvisionerJobByID(r.Context(), history.ProvisionJobID) if err != nil { httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ diff --git a/coderd/workspacehistory_test.go b/coderd/workspacehistory_test.go index 484269892ad3f..b7ef8855264fb 100644 --- a/coderd/workspacehistory_test.go +++ b/coderd/workspacehistory_test.go @@ -2,8 +2,8 @@ package coderd_test import ( "context" + "net/http" "testing" - "time" "github.com/google/uuid" "github.com/stretchr/testify/require" @@ -13,141 +13,150 @@ import ( "github.com/coder/coder/codersdk" "github.com/coder/coder/database" "github.com/coder/coder/provisioner/echo" + "github.com/coder/coder/provisionersdk/proto" ) -func TestWorkspaceHistory(t *testing.T) { +func TestPostWorkspaceHistoryByUser(t *testing.T) { t.Parallel() - - setupProjectAndWorkspace := func(t *testing.T, client *codersdk.Client, user coderd.CreateInitialUserRequest) (coderd.Project, coderd.Workspace) { - project, err := client.CreateProject(context.Background(), user.Organization, coderd.CreateProjectRequest{ - Name: "banana", - Provisioner: database.ProvisionerTypeEcho, - }) - require.NoError(t, err) - workspace, err := client.CreateWorkspace(context.Background(), "", coderd.CreateWorkspaceRequest{ - Name: "example", - ProjectID: project.ID, - }) - require.NoError(t, err) - return project, workspace - } - - setupProjectVersion := func(t *testing.T, client *codersdk.Client, user coderd.CreateInitialUserRequest, project coderd.Project, data []byte) coderd.ProjectVersion { - projectVersion, err := client.CreateProjectVersion(context.Background(), user.Organization, project.Name, coderd.CreateProjectVersionRequest{ - StorageMethod: database.ProjectStorageMethodInlineArchive, - StorageSource: data, + t.Run("NoProjectVersion", func(t *testing.T) { + t.Parallel() + client := coderdtest.New(t) + user := coderdtest.CreateInitialUser(t, client) + project := coderdtest.CreateProject(t, client, user.Organization) + workspace := coderdtest.CreateWorkspace(t, client, "me", project.ID) + _, err := client.CreateWorkspaceHistory(context.Background(), "", workspace.Name, coderd.CreateWorkspaceHistoryRequest{ + ProjectVersionID: uuid.New(), + Transition: database.WorkspaceTransitionCreate, }) - 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 - } + require.Error(t, err) + var apiErr *codersdk.Error + require.ErrorAs(t, err, &apiErr) + require.Equal(t, http.StatusBadRequest, apiErr.StatusCode()) + }) - t.Run("AllHistory", func(t *testing.T) { + t.Run("ProjectVersionFailedImport", func(t *testing.T) { t.Parallel() - server := coderdtest.New(t) - user := server.RandomInitialUser(t) - _ = server.AddProvisionerd(t) - project, workspace := setupProjectAndWorkspace(t, server.Client, user) - history, err := server.Client.ListWorkspaceHistory(context.Background(), "", workspace.Name) - require.NoError(t, err) - require.Len(t, history, 0) - data, err := echo.Tar(echo.ParseComplete, echo.ProvisionComplete) - require.NoError(t, err) - projectVersion := setupProjectVersion(t, server.Client, user, project, data) - _, err = server.Client.CreateWorkspaceHistory(context.Background(), "", workspace.Name, coderd.CreateWorkspaceHistoryRequest{ - ProjectVersionID: projectVersion.ID, + 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.NoError(t, err) - history, err = server.Client.ListWorkspaceHistory(context.Background(), "", workspace.Name) - require.NoError(t, err) - require.Len(t, history, 1) + require.Error(t, err) + var apiErr *codersdk.Error + require.ErrorAs(t, err, &apiErr) + require.Equal(t, http.StatusPreconditionFailed, apiErr.StatusCode()) }) - t.Run("LatestHistory", func(t *testing.T) { + t.Run("AlreadyActive", 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, + 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 = server.Client.WorkspaceHistory(context.Background(), "", workspace.Name, "") - 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("CreateHistory", func(t *testing.T) { + t.Run("UpdatePriorAfterField", 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) + 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) - projectVersion := setupProjectVersion(t, server.Client, user, project, data) - _, err = server.Client.CreateWorkspaceHistory(context.Background(), "", workspace.Name, coderd.CreateWorkspaceHistoryRequest{ - ProjectVersionID: projectVersion.ID, + 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()) - 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) + firstHistory, err = client.WorkspaceHistory(context.Background(), "", workspace.Name, firstHistory.Name) + require.NoError(t, err) + require.Equal(t, secondHistory.ID.String(), firstHistory.AfterID.String()) }) +} - t.Run("CreateHistoryAlreadyInProgress", func(t *testing.T) { +func TestWorkspaceHistoryByUser(t *testing.T) { + t.Parallel() + t.Run("ListEmpty", func(t *testing.T) { t.Parallel() - server := coderdtest.New(t) - user := server.RandomInitialUser(t) - _ = server.AddProvisionerd(t) - project, workspace := setupProjectAndWorkspace(t, server.Client, user) - data, err := echo.Tar(echo.ParseComplete, echo.ProvisionComplete) + 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) - projectVersion := setupProjectVersion(t, server.Client, user, project, data) + require.NotNil(t, history) + require.Len(t, history, 0) + }) - _, err = server.Client.CreateWorkspaceHistory(context.Background(), "", workspace.Name, coderd.CreateWorkspaceHistoryRequest{ - ProjectVersionID: projectVersion.ID, + 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) - - _, err = server.Client.CreateWorkspaceHistory(context.Background(), "", workspace.Name, coderd.CreateWorkspaceHistoryRequest{ - ProjectVersionID: projectVersion.ID, - Transition: database.WorkspaceTransitionCreate, - }) - require.Error(t, err) + history, err := client.ListWorkspaceHistory(context.Background(), "me", workspace.Name) + require.NoError(t, err) + require.NotNil(t, history) + require.Len(t, history, 1) }) +} - 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(), - Transition: database.WorkspaceTransitionCreate, - }) - require.Error(t, err) +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) } diff --git a/coderd/workspacehistorylogs.go b/coderd/workspacehistorylogs.go index ecf8e1fda9d2b..029167c0a7572 100644 --- a/coderd/workspacehistorylogs.go +++ b/coderd/workspacehistorylogs.go @@ -87,7 +87,6 @@ func (api *api) workspaceHistoryLogsByName(rw http.ResponseWriter, r *http.Reque }) if errors.Is(err, sql.ErrNoRows) { err = nil - logs = []database.WorkspaceHistoryLog{} } if err != nil { httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ @@ -95,6 +94,9 @@ func (api *api) workspaceHistoryLogsByName(rw http.ResponseWriter, r *http.Reque }) return } + if logs == nil { + logs = []database.WorkspaceHistoryLog{} + } render.Status(r, http.StatusOK) render.JSON(rw, r, logs) return @@ -113,12 +115,8 @@ func (api *api) workspaceHistoryLogsByName(rw http.ResponseWriter, r *http.Reque select { case bufferedLogs <- log: default: - // This is a case that shouldn't happen, but totally could. - // There's no way to stream data from the database, so we'll - // need to maintain some level of internal buffer. - // - // If this overflows users could miss logs when streaming. - // We warn to make sure we know when it happens! + // If this overflows users could miss logs streaming. This can happen + // if a database request takes a long amount of time, and we get a lot of logs. api.Logger.Warn(r.Context(), "workspace history log overflowing channel") } } diff --git a/coderd/workspacehistorylogs_test.go b/coderd/workspacehistorylogs_test.go index f507001beb4d5..1e95a0f506cc1 100644 --- a/coderd/workspacehistorylogs_test.go +++ b/coderd/workspacehistorylogs_test.go @@ -9,90 +9,132 @@ import ( "github.com/coder/coder/coderd" "github.com/coder/coder/coderd/coderdtest" - "github.com/coder/coder/codersdk" "github.com/coder/coder/database" "github.com/coder/coder/provisioner/echo" "github.com/coder/coder/provisionersdk/proto" ) -func TestWorkspaceHistoryLogs(t *testing.T) { +func TestWorkspaceHistoryLogsByName(t *testing.T) { t.Parallel() - - setupProjectAndWorkspace := func(t *testing.T, client *codersdk.Client, user coderd.CreateInitialUserRequest) (coderd.Project, coderd.Workspace) { - project, err := client.CreateProject(context.Background(), user.Organization, coderd.CreateProjectRequest{ - Name: "banana", - Provisioner: database.ProvisionerTypeEcho, + 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: 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{}, + }, + }}, }) - require.NoError(t, err) - workspace, err := client.CreateWorkspace(context.Background(), "", coderd.CreateWorkspaceRequest{ - Name: "example", - ProjectID: project.ID, + 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) - 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, - }) + // Successfully return empty logs before the job starts! + logs, err := client.WorkspaceHistoryLogs(context.Background(), "", workspace.Name, history.Name) 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 - } + require.NotNil(t, logs) + require.Len(t, logs, 0) - 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) + coderdtest.AwaitWorkspaceHistoryProvisioned(t, client, "", workspace.Name, history.Name) - workspaceHistory, err := server.Client.CreateWorkspaceHistory(context.Background(), "", workspace.Name, coderd.CreateWorkspaceHistoryRequest{ - ProjectVersionID: projectVersion.ID, - Transition: database.WorkspaceTransitionCreate, + // 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) }) - require.NoError(t, err) - - now := database.Now() - logChan, err := server.Client.FollowWorkspaceHistoryLogsAfter(context.Background(), "", workspace.Name, workspaceHistory.Name, now) - require.NoError(t, err) - - for { - log, more := <-logChan - if !more { - break - } - t.Logf("Output: %s", log.Output) - } - t.Run("ReturnAll", func(t *testing.T) { + t.Run("StreamAfterComplete", 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) + before := time.Now().UTC() + history, err := client.CreateWorkspaceHistory(context.Background(), "", workspace.Name, coderd.CreateWorkspaceHistoryRequest{ + ProjectVersionID: version.ID, + Transition: database.WorkspaceTransitionCreate, + }) + require.NoError(t, err) + coderdtest.AwaitWorkspaceHistoryProvisioned(t, client, "", workspace.Name, history.Name) - _, err := server.Client.WorkspaceHistoryLogs(context.Background(), "", workspace.Name, workspaceHistory.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("Between", func(t *testing.T) { + 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) - _, err := server.Client.WorkspaceHistoryLogsBetween(context.Background(), "", workspace.Name, workspaceHistory.Name, time.Time{}, database.Now()) + 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) }) } diff --git a/coderd/workspaces.go b/coderd/workspaces.go index 01ef9870cecd4..4ea1ba2706202 100644 --- a/coderd/workspaces.go +++ b/coderd/workspaces.go @@ -137,7 +137,7 @@ func (api *api) postWorkspaceByUser(rw http.ResponseWriter, r *http.Request) { render.JSON(rw, r, convertWorkspace(workspace)) } -// Returns a single singleWorkspace. +// Returns a single workspace. func (*api) workspaceByUser(rw http.ResponseWriter, r *http.Request) { workspace := httpmw.WorkspaceParam(r) @@ -145,6 +145,32 @@ func (*api) workspaceByUser(rw http.ResponseWriter, r *http.Request) { render.JSON(rw, r, convertWorkspace(workspace)) } +// Returns all workspaces for a specific project. +func (api *api) workspacesByProject(rw http.ResponseWriter, r *http.Request) { + apiKey := httpmw.APIKey(r) + project := httpmw.ProjectParam(r) + workspaces, err := api.Database.GetWorkspacesByProjectAndUserID(r.Context(), database.GetWorkspacesByProjectAndUserIDParams{ + OwnerID: apiKey.UserID, + ProjectID: project.ID, + }) + if errors.Is(err, sql.ErrNoRows) { + err = nil + } + if err != nil { + httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ + Message: fmt.Sprintf("get workspaces: %s", err), + }) + return + } + + apiWorkspaces := make([]Workspace, 0, len(workspaces)) + for _, workspace := range workspaces { + apiWorkspaces = append(apiWorkspaces, convertWorkspace(workspace)) + } + render.Status(r, http.StatusOK) + render.JSON(rw, r, apiWorkspaces) +} + // Converts the internal workspace representation to a public external-facing model. func convertWorkspace(workspace database.Workspace) Workspace { return Workspace(workspace) diff --git a/coderd/workspaces_test.go b/coderd/workspaces_test.go index 2ee817899ca64..bc170a92ab4b8 100644 --- a/coderd/workspaces_test.go +++ b/coderd/workspaces_test.go @@ -2,6 +2,7 @@ package coderd_test import ( "context" + "net/http" "testing" "github.com/google/uuid" @@ -10,143 +11,135 @@ import ( "github.com/coder/coder/coderd" "github.com/coder/coder/coderd/coderdtest" "github.com/coder/coder/codersdk" - "github.com/coder/coder/database" ) func TestWorkspaces(t *testing.T) { t.Parallel() - t.Run("ListNone", func(t *testing.T) { t.Parallel() - server := coderdtest.New(t) - _ = server.RandomInitialUser(t) - workspaces, err := server.Client.WorkspacesByUser(context.Background(), "") + client := coderdtest.New(t) + _ = coderdtest.CreateInitialUser(t, client) + workspaces, err := client.Workspaces(context.Background(), "") require.NoError(t, err) + require.NotNil(t, workspaces) require.Len(t, workspaces, 0) }) - setupProjectAndWorkspace := func(t *testing.T, client *codersdk.Client, user coderd.CreateInitialUserRequest) (coderd.Project, coderd.Workspace) { - project, err := client.CreateProject(context.Background(), user.Organization, coderd.CreateProjectRequest{ - Name: "banana", - Provisioner: database.ProvisionerTypeEcho, - }) - require.NoError(t, err) - workspace, err := client.CreateWorkspace(context.Background(), "", coderd.CreateWorkspaceRequest{ - Name: "example", - ProjectID: project.ID, - }) - require.NoError(t, err) - return project, workspace - } - t.Run("List", func(t *testing.T) { t.Parallel() - server := coderdtest.New(t) - user := server.RandomInitialUser(t) - _, _ = setupProjectAndWorkspace(t, server.Client, user) - workspaces, err := server.Client.WorkspacesByUser(context.Background(), "") + client := coderdtest.New(t) + user := coderdtest.CreateInitialUser(t, client) + project := coderdtest.CreateProject(t, client, user.Organization) + _ = coderdtest.CreateWorkspace(t, client, "", project.ID) + workspaces, err := client.Workspaces(context.Background(), "") require.NoError(t, err) require.Len(t, workspaces, 1) }) +} - t.Run("ListNoneForProject", func(t *testing.T) { +func TestPostWorkspaceByUser(t *testing.T) { + t.Parallel() + t.Run("InvalidProject", func(t *testing.T) { t.Parallel() - server := coderdtest.New(t) - user := server.RandomInitialUser(t) - project, err := server.Client.CreateProject(context.Background(), user.Organization, coderd.CreateProjectRequest{ - Name: "banana", - Provisioner: database.ProvisionerTypeEcho, + client := coderdtest.New(t) + _ = coderdtest.CreateInitialUser(t, client) + _, err := client.CreateWorkspace(context.Background(), "", coderd.CreateWorkspaceRequest{ + ProjectID: uuid.New(), + Name: "workspace", }) - require.NoError(t, err) - workspaces, err := server.Client.WorkspacesByProject(context.Background(), user.Organization, project.Name) - require.NoError(t, err) - require.Len(t, workspaces, 0) + require.Error(t, err) + var apiErr *codersdk.Error + require.ErrorAs(t, err, &apiErr) + require.Equal(t, http.StatusBadRequest, apiErr.StatusCode()) }) - t.Run("ListForProject", func(t *testing.T) { + t.Run("NoProjectAccess", func(t *testing.T) { t.Parallel() - server := coderdtest.New(t) - user := server.RandomInitialUser(t) - project, _ := setupProjectAndWorkspace(t, server.Client, user) - workspaces, err := server.Client.WorkspacesByProject(context.Background(), user.Organization, project.Name) - require.NoError(t, err) - require.Len(t, workspaces, 1) - }) + client := coderdtest.New(t) + user := coderdtest.CreateInitialUser(t, client) + project := coderdtest.CreateProject(t, client, user.Organization) - t.Run("CreateInvalidInput", func(t *testing.T) { - t.Parallel() - server := coderdtest.New(t) - user := server.RandomInitialUser(t) - project, err := server.Client.CreateProject(context.Background(), user.Organization, coderd.CreateProjectRequest{ - Name: "banana", - Provisioner: database.ProvisionerTypeEcho, + anotherUser := coderd.CreateUserRequest{ + Email: "another@user.org", + Username: "someuser", + Password: "somepass", + } + _, err := client.CreateUser(context.Background(), anotherUser) + require.NoError(t, err) + token, err := client.LoginWithPassword(context.Background(), coderd.LoginWithPasswordRequest{ + Email: anotherUser.Email, + Password: anotherUser.Password, }) require.NoError(t, err) - _, err = server.Client.CreateWorkspace(context.Background(), "", coderd.CreateWorkspaceRequest{ + err = client.SetSessionToken(token.SessionToken) + require.NoError(t, err) + + _, err = client.CreateWorkspace(context.Background(), "", coderd.CreateWorkspaceRequest{ ProjectID: project.ID, - Name: "$$$", + Name: "workspace", }) require.Error(t, err) + var apiErr *codersdk.Error + require.ErrorAs(t, err, &apiErr) + require.Equal(t, http.StatusUnauthorized, apiErr.StatusCode()) }) - t.Run("CreateInvalidProject", func(t *testing.T) { + t.Run("AlreadyExists", func(t *testing.T) { t.Parallel() - server := coderdtest.New(t) - _ = server.RandomInitialUser(t) - _, err := server.Client.CreateWorkspace(context.Background(), "", coderd.CreateWorkspaceRequest{ - ProjectID: uuid.New(), - Name: "moo", + client := coderdtest.New(t) + user := coderdtest.CreateInitialUser(t, client) + project := coderdtest.CreateProject(t, client, user.Organization) + workspace := coderdtest.CreateWorkspace(t, client, "", project.ID) + _, err := client.CreateWorkspace(context.Background(), "", coderd.CreateWorkspaceRequest{ + ProjectID: project.ID, + Name: workspace.Name, }) require.Error(t, err) + var apiErr *codersdk.Error + require.ErrorAs(t, err, &apiErr) + require.Equal(t, http.StatusConflict, apiErr.StatusCode()) }) - t.Run("CreateNotInProjectOrganization", func(t *testing.T) { + t.Run("Create", func(t *testing.T) { t.Parallel() - server := coderdtest.New(t) - initial := server.RandomInitialUser(t) - project, err := server.Client.CreateProject(context.Background(), initial.Organization, coderd.CreateProjectRequest{ - Name: "banana", - Provisioner: database.ProvisionerTypeEcho, - }) - require.NoError(t, err) - _, err = server.Client.CreateUser(context.Background(), coderd.CreateUserRequest{ - Email: "hello@ok.io", - Username: "example", - Password: "password", - }) - require.NoError(t, err) - token, err := server.Client.LoginWithPassword(context.Background(), coderd.LoginWithPasswordRequest{ - Email: "hello@ok.io", - Password: "password", - }) - require.NoError(t, err) - err = server.Client.SetSessionToken(token.SessionToken) - require.NoError(t, err) - _, err = server.Client.CreateWorkspace(context.Background(), "", coderd.CreateWorkspaceRequest{ - ProjectID: project.ID, - Name: "moo", - }) - require.Error(t, err) + client := coderdtest.New(t) + user := coderdtest.CreateInitialUser(t, client) + project := coderdtest.CreateProject(t, client, user.Organization) + _ = coderdtest.CreateWorkspace(t, client, "", project.ID) }) +} - t.Run("CreateAlreadyExists", func(t *testing.T) { +func TestWorkspaceByUser(t *testing.T) { + t.Parallel() + client := coderdtest.New(t) + user := coderdtest.CreateInitialUser(t, client) + project := coderdtest.CreateProject(t, client, user.Organization) + workspace := coderdtest.CreateWorkspace(t, client, "", project.ID) + _, err := client.Workspace(context.Background(), "", workspace.Name) + require.NoError(t, err) +} + +func TestWorkspacesByProject(t *testing.T) { + t.Parallel() + t.Run("ListEmpty", func(t *testing.T) { t.Parallel() - server := coderdtest.New(t) - user := server.RandomInitialUser(t) - project, workspace := setupProjectAndWorkspace(t, server.Client, user) - _, err := server.Client.CreateWorkspace(context.Background(), "", coderd.CreateWorkspaceRequest{ - Name: workspace.Name, - ProjectID: project.ID, - }) - require.Error(t, err) + client := coderdtest.New(t) + user := coderdtest.CreateInitialUser(t, client) + project := coderdtest.CreateProject(t, client, user.Organization) + workspaces, err := client.WorkspacesByProject(context.Background(), user.Organization, project.Name) + require.NoError(t, err) + require.NotNil(t, workspaces) }) - t.Run("Single", func(t *testing.T) { + t.Run("List", func(t *testing.T) { t.Parallel() - server := coderdtest.New(t) - user := server.RandomInitialUser(t) - _, workspace := setupProjectAndWorkspace(t, server.Client, user) - _, err := server.Client.Workspace(context.Background(), "", workspace.Name) + client := coderdtest.New(t) + user := coderdtest.CreateInitialUser(t, client) + project := coderdtest.CreateProject(t, client, user.Organization) + _ = coderdtest.CreateWorkspace(t, client, "", project.ID) + workspaces, err := client.WorkspacesByProject(context.Background(), user.Organization, project.Name) require.NoError(t, err) + require.NotNil(t, workspaces) + require.Len(t, workspaces, 1) }) } diff --git a/codersdk/client.go b/codersdk/client.go index b4931a91e8b91..4bd5a111cb949 100644 --- a/codersdk/client.go +++ b/codersdk/client.go @@ -10,6 +10,7 @@ import ( "net/http" "net/http/cookiejar" "net/url" + "strings" "golang.org/x/xerrors" @@ -20,14 +21,15 @@ import ( // New creates a Coder client for the provided URL. func New(serverURL *url.URL) *Client { return &Client{ - url: serverURL, + URL: serverURL, httpClient: &http.Client{}, } } // Client is an HTTP caller for methods to the Coder API. type Client struct { - url *url.URL + URL *url.URL + httpClient *http.Client } @@ -40,7 +42,7 @@ func (c *Client) SetSessionToken(token string) error { return err } } - c.httpClient.Jar.SetCookies(c.url, []*http.Cookie{{ + c.httpClient.Jar.SetCookies(c.URL, []*http.Cookie{{ Name: httpmw.AuthCookie, Value: token, }}) @@ -50,7 +52,7 @@ func (c *Client) SetSessionToken(token string) error { // request performs an HTTP request with the body provided. // The caller is responsible for closing the response body. func (c *Client) request(ctx context.Context, method, path string, body interface{}) (*http.Response, error) { - serverURL, err := c.url.Parse(path) + serverURL, err := c.URL.Parse(path) if err != nil { return nil, xerrors.Errorf("parse url: %w", err) } @@ -112,5 +114,10 @@ func (e *Error) StatusCode() int { } func (e *Error) Error() string { - return fmt.Sprintf("status code %d: %s", e.statusCode, e.Message) + var builder strings.Builder + _, _ = fmt.Fprintf(&builder, "status code %d: %s", e.statusCode, e.Message) + for _, err := range e.Errors { + _, _ = fmt.Fprintf(&builder, "\n\t%s: %s", err.Field, err.Code) + } + return builder.String() } diff --git a/codersdk/projects_test.go b/codersdk/projects_test.go index 957a759b3e062..bf072657c97bd 100644 --- a/codersdk/projects_test.go +++ b/codersdk/projects_test.go @@ -1,8 +1,6 @@ package codersdk_test import ( - "archive/tar" - "bytes" "context" "testing" @@ -15,160 +13,183 @@ import ( func TestProjects(t *testing.T) { t.Parallel() - - t.Run("UnauthenticatedList", func(t *testing.T) { + t.Run("Error", func(t *testing.T) { t.Parallel() - server := coderdtest.New(t) - _, err := server.Client.Projects(context.Background(), "") + client := coderdtest.New(t) + _, err := client.Projects(context.Background(), "") require.Error(t, err) }) t.Run("List", func(t *testing.T) { t.Parallel() - server := coderdtest.New(t) - user := server.RandomInitialUser(t) - _, err := server.Client.Projects(context.Background(), "") - require.NoError(t, err) - _, err = server.Client.Projects(context.Background(), user.Organization) + client := coderdtest.New(t) + _ = coderdtest.CreateInitialUser(t, client) + _, err := client.Projects(context.Background(), "") require.NoError(t, err) }) +} - t.Run("UnauthenticatedCreate", func(t *testing.T) { +func TestProject(t *testing.T) { + t.Parallel() + t.Run("Error", func(t *testing.T) { t.Parallel() - server := coderdtest.New(t) - _, err := server.Client.CreateProject(context.Background(), "", coderd.CreateProjectRequest{}) + client := coderdtest.New(t) + _, err := client.Project(context.Background(), "", "") require.Error(t, err) }) - t.Run("Create", func(t *testing.T) { + t.Run("Get", func(t *testing.T) { t.Parallel() - server := coderdtest.New(t) - user := server.RandomInitialUser(t) - _, err := server.Client.CreateProject(context.Background(), user.Organization, coderd.CreateProjectRequest{ - Name: "bananas", + 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.NoError(t, err) + require.Error(t, err) }) - t.Run("UnauthenticatedSingle", func(t *testing.T) { + t.Run("Create", func(t *testing.T) { + t.Parallel() + client := coderdtest.New(t) + user := coderdtest.CreateInitialUser(t, client) + _ = coderdtest.CreateProject(t, client, user.Organization) + }) +} + +func TestProjectVersions(t *testing.T) { + t.Parallel() + t.Run("Error", func(t *testing.T) { t.Parallel() - server := coderdtest.New(t) - _, err := server.Client.Project(context.Background(), "wow", "example") + client := coderdtest.New(t) + _, err := client.ProjectVersions(context.Background(), "some", "project") require.Error(t, err) }) - t.Run("Single", func(t *testing.T) { + t.Run("List", func(t *testing.T) { t.Parallel() - server := coderdtest.New(t) - user := server.RandomInitialUser(t) - _, err := server.Client.CreateProject(context.Background(), user.Organization, coderd.CreateProjectRequest{ - Name: "bananas", - Provisioner: database.ProvisionerTypeEcho, - }) - require.NoError(t, err) - _, err = server.Client.Project(context.Background(), user.Organization, "bananas") + client := coderdtest.New(t) + user := coderdtest.CreateInitialUser(t, client) + project := coderdtest.CreateProject(t, client, user.Organization) + + _, err := client.ProjectVersions(context.Background(), user.Organization, project.Name) require.NoError(t, err) }) +} - t.Run("UnauthenticatedHistory", func(t *testing.T) { +func TestProjectVersion(t *testing.T) { + t.Parallel() + t.Run("Error", func(t *testing.T) { t.Parallel() - server := coderdtest.New(t) - _, err := server.Client.ProjectVersions(context.Background(), "org", "project") + client := coderdtest.New(t) + _, err := client.ProjectVersion(context.Background(), "some", "project", "version") require.Error(t, err) }) - t.Run("History", func(t *testing.T) { + t.Run("Get", func(t *testing.T) { t.Parallel() - server := coderdtest.New(t) - user := server.RandomInitialUser(t) - project, err := server.Client.CreateProject(context.Background(), user.Organization, coderd.CreateProjectRequest{ - Name: "bananas", - Provisioner: database.ProvisionerTypeEcho, - }) - require.NoError(t, err) - _, err = server.Client.ProjectVersions(context.Background(), user.Organization, project.Name) + client := coderdtest.New(t) + user := coderdtest.CreateInitialUser(t, client) + project := coderdtest.CreateProject(t, client, user.Organization) + version := coderdtest.CreateProjectVersion(t, client, user.Organization, project.Name, nil) + _, err := client.ProjectVersion(context.Background(), user.Organization, project.Name, version.Name) require.NoError(t, err) }) +} - t.Run("CreateHistoryUnauthenticated", func(t *testing.T) { +func TestCreateProjectVersion(t *testing.T) { + t.Parallel() + t.Run("Error", func(t *testing.T) { t.Parallel() - server := coderdtest.New(t) - _, err := server.Client.CreateProjectVersion(context.Background(), "org", "project", coderd.CreateProjectVersionRequest{ - StorageMethod: database.ProjectStorageMethodInlineArchive, - StorageSource: []byte{}, - }) + client := coderdtest.New(t) + _, err := client.CreateProjectVersion(context.Background(), "some", "project", coderd.CreateProjectVersionRequest{}) require.Error(t, err) }) - t.Run("CreateHistory", func(t *testing.T) { + t.Run("Create", func(t *testing.T) { t.Parallel() - server := coderdtest.New(t) - user := server.RandomInitialUser(t) - project, err := server.Client.CreateProject(context.Background(), user.Organization, coderd.CreateProjectRequest{ - Name: "bananas", - Provisioner: database.ProvisionerTypeEcho, - }) - require.NoError(t, err) - var buffer bytes.Buffer - writer := tar.NewWriter(&buffer) - err = writer.WriteHeader(&tar.Header{ - Name: "file", - Size: 1 << 10, - }) - require.NoError(t, err) - _, err = writer.Write(make([]byte, 1<<10)) - require.NoError(t, err) - version, err := server.Client.CreateProjectVersion(context.Background(), user.Organization, project.Name, coderd.CreateProjectVersionRequest{ - StorageMethod: database.ProjectStorageMethodInlineArchive, - StorageSource: buffer.Bytes(), - }) - require.NoError(t, err) + 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) + }) +} - _, err = server.Client.ProjectVersion(context.Background(), user.Organization, project.Name, version.Name) - require.NoError(t, err) +func TestProjectVersionParameters(t *testing.T) { + t.Parallel() + t.Run("Error", func(t *testing.T) { + t.Parallel() + client := coderdtest.New(t) + _, err := client.ProjectVersionParameters(context.Background(), "some", "project", "version") + require.Error(t, err) }) - t.Run("Parameters", func(t *testing.T) { + t.Run("List", func(t *testing.T) { t.Parallel() - server := coderdtest.New(t) - user := server.RandomInitialUser(t) - project, err := server.Client.CreateProject(context.Background(), user.Organization, coderd.CreateProjectRequest{ - Name: "someproject", - Provisioner: database.ProvisionerTypeEcho, - }) + client := coderdtest.New(t) + user := coderdtest.CreateInitialUser(t, client) + coderdtest.NewProvisionerDaemon(t, client) + project := coderdtest.CreateProject(t, client, user.Organization) + version := coderdtest.CreateProjectVersion(t, client, user.Organization, project.Name, nil) + coderdtest.AwaitProjectVersionImported(t, client, user.Organization, project.Name, version.Name) + _, err := client.ProjectVersionParameters(context.Background(), user.Organization, project.Name, version.Name) require.NoError(t, err) - params, err := server.Client.ProjectParameters(context.Background(), user.Organization, project.Name) - require.NoError(t, err) - require.NotNil(t, params) - require.Len(t, params, 0) }) +} - t.Run("CreateParameter", func(t *testing.T) { +func TestProjectParameters(t *testing.T) { + t.Parallel() + t.Run("Error", func(t *testing.T) { t.Parallel() - server := coderdtest.New(t) - user := server.RandomInitialUser(t) - project, err := server.Client.CreateProject(context.Background(), user.Organization, coderd.CreateProjectRequest{ - Name: "someproject", - Provisioner: database.ProvisionerTypeEcho, - }) - require.NoError(t, err) - param, err := server.Client.CreateProjectParameter(context.Background(), user.Organization, project.Name, coderd.CreateParameterValueRequest{ - Name: "hi", - SourceValue: "tomato", - SourceScheme: database.ParameterSourceSchemeData, - DestinationScheme: database.ParameterDestinationSchemeEnvironmentVariable, - DestinationValue: "moo", - }) + client := coderdtest.New(t) + _, err := client.ProjectParameters(context.Background(), "some", "project") + require.Error(t, err) + }) + + t.Run("List", func(t *testing.T) { + t.Parallel() + client := coderdtest.New(t) + user := coderdtest.CreateInitialUser(t, client) + coderdtest.NewProvisionerDaemon(t, client) + project := coderdtest.CreateProject(t, client, user.Organization) + _, err := client.ProjectParameters(context.Background(), user.Organization, project.Name) require.NoError(t, err) - require.Equal(t, "hi", param.Name) }) +} - t.Run("HistoryParametersError", func(t *testing.T) { +func TestCreateProjectParameter(t *testing.T) { + t.Parallel() + t.Run("Error", func(t *testing.T) { t.Parallel() - server := coderdtest.New(t) - user := server.RandomInitialUser(t) - _, err := server.Client.ProjectVersionParameters(context.Background(), user.Organization, "nothing", "nope") + client := coderdtest.New(t) + _, err := client.CreateProjectParameter(context.Background(), "some", "project", coderd.CreateParameterValueRequest{}) require.Error(t, err) }) + + t.Run("Create", func(t *testing.T) { + t.Parallel() + client := coderdtest.New(t) + user := coderdtest.CreateInitialUser(t, client) + coderdtest.NewProvisionerDaemon(t, client) + project := coderdtest.CreateProject(t, client, user.Organization) + _, err := client.CreateProjectParameter(context.Background(), user.Organization, project.Name, coderd.CreateParameterValueRequest{ + Name: "example", + SourceValue: "source-value", + SourceScheme: database.ParameterSourceSchemeData, + DestinationScheme: database.ParameterDestinationSchemeEnvironmentVariable, + DestinationValue: "destination-value", + }) + require.NoError(t, err) + }) } diff --git a/codersdk/provisioners.go b/codersdk/provisioners.go index cfc908a7d39b3..5f96271a4218d 100644 --- a/codersdk/provisioners.go +++ b/codersdk/provisioners.go @@ -3,6 +3,7 @@ package codersdk import ( "context" "encoding/json" + "io" "net/http" "github.com/hashicorp/yamux" @@ -29,12 +30,14 @@ func (c *Client) ProvisionerDaemons(ctx context.Context) ([]coderd.ProvisionerDa // ProvisionerDaemonClient returns the gRPC service for a provisioner daemon implementation. func (c *Client) ProvisionerDaemonClient(ctx context.Context) (proto.DRPCProvisionerDaemonClient, error) { - serverURL, err := c.url.Parse("/api/v2/provisioners/daemons/serve") + serverURL, err := c.URL.Parse("/api/v2/provisioners/daemons/serve") if err != nil { return nil, xerrors.Errorf("parse url: %w", err) } conn, res, err := websocket.Dial(ctx, serverURL.String(), &websocket.DialOptions{ HTTPClient: c.httpClient, + // Need to disable compression to avoid a data-race. + CompressionMode: websocket.CompressionDisabled, }) if err != nil { if res == nil { @@ -42,7 +45,9 @@ func (c *Client) ProvisionerDaemonClient(ctx context.Context) (proto.DRPCProvisi } return nil, readBodyAsError(res) } - session, err := yamux.Client(websocket.NetConn(context.Background(), conn, websocket.MessageBinary), nil) + config := yamux.DefaultConfig() + config.LogOutput = io.Discard + session, err := yamux.Client(websocket.NetConn(ctx, conn, websocket.MessageBinary), config) if err != nil { return nil, xerrors.Errorf("multiplex client: %w", err) } diff --git a/codersdk/provisioners_test.go b/codersdk/provisioners_test.go new file mode 100644 index 0000000000000..9bb4528ebec1e --- /dev/null +++ b/codersdk/provisioners_test.go @@ -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) + }) +} diff --git a/codersdk/users_test.go b/codersdk/users_test.go index 26f1e7d3fd646..3425c9204f3ca 100644 --- a/codersdk/users_test.go +++ b/codersdk/users_test.go @@ -10,61 +10,104 @@ import ( "github.com/coder/coder/coderd/coderdtest" ) -func TestUsers(t *testing.T) { +func TestCreateInitialUser(t *testing.T) { t.Parallel() - t.Run("CreateInitial", func(t *testing.T) { + t.Run("Error", func(t *testing.T) { t.Parallel() - server := coderdtest.New(t) - _, err := server.Client.CreateInitialUser(context.Background(), coderd.CreateInitialUserRequest{ - Email: "wowie@coder.com", - Organization: "somethin", - Username: "tester", - Password: "moo", + client := coderdtest.New(t) + _, err := client.CreateInitialUser(context.Background(), coderd.CreateInitialUserRequest{}) + require.Error(t, err) + }) + + t.Run("Create", func(t *testing.T) { + t.Parallel() + client := coderdtest.New(t) + _ = coderdtest.CreateInitialUser(t, client) + }) +} + +func TestCreateUser(t *testing.T) { + t.Parallel() + t.Run("Error", func(t *testing.T) { + t.Parallel() + client := coderdtest.New(t) + _, err := client.CreateUser(context.Background(), coderd.CreateUserRequest{}) + require.Error(t, err) + }) + + t.Run("Create", func(t *testing.T) { + t.Parallel() + client := coderdtest.New(t) + _ = coderdtest.CreateInitialUser(t, client) + _, err := client.CreateUser(context.Background(), coderd.CreateUserRequest{ + Email: "example@coder.com", + Username: "something", + Password: "password", }) require.NoError(t, err) }) +} - t.Run("NoUser", func(t *testing.T) { +func TestLoginWithPassword(t *testing.T) { + t.Parallel() + t.Run("Error", func(t *testing.T) { t.Parallel() - server := coderdtest.New(t) - _, err := server.Client.User(context.Background(), "") + client := coderdtest.New(t) + _, err := client.LoginWithPassword(context.Background(), coderd.LoginWithPasswordRequest{}) require.Error(t, err) }) - t.Run("User", func(t *testing.T) { + t.Run("Success", func(t *testing.T) { t.Parallel() - server := coderdtest.New(t) - _ = server.RandomInitialUser(t) - _, err := server.Client.User(context.Background(), "") + 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) +} - t.Run("UserOrganizations", func(t *testing.T) { +func TestUser(t *testing.T) { + t.Parallel() + t.Run("Error", 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) + client := coderdtest.New(t) + _, err := client.User(context.Background(), "") + require.Error(t, err) }) - t.Run("LogoutIsSuccessful", func(t *testing.T) { + t.Run("Get", func(t *testing.T) { t.Parallel() - server := coderdtest.New(t) - _ = server.RandomInitialUser(t) - err := server.Client.Logout(context.Background()) + client := coderdtest.New(t) + _ = coderdtest.CreateInitialUser(t, client) + _, err := client.User(context.Background(), "") require.NoError(t, err) }) +} - t.Run("CreateMultiple", func(t *testing.T) { +func TestUserOrganizations(t *testing.T) { + t.Parallel() + t.Run("Error", func(t *testing.T) { t.Parallel() - server := coderdtest.New(t) - _ = server.RandomInitialUser(t) - _, err := server.Client.CreateUser(context.Background(), coderd.CreateUserRequest{ - Email: "wow@ok.io", - Username: "example", - Password: "tomato", - }) + client := coderdtest.New(t) + _, 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) }) } diff --git a/codersdk/workspaces.go b/codersdk/workspaces.go index d6ddf14f7b082..82c2ce2853c0f 100644 --- a/codersdk/workspaces.go +++ b/codersdk/workspaces.go @@ -15,7 +15,7 @@ import ( // Workspaces returns all workspaces the authenticated session has access to. // If owner is specified, all workspaces for an organization will be returned. // If owner is empty, all workspaces the caller has access to will be returned. -func (c *Client) WorkspacesByUser(ctx context.Context, user string) ([]coderd.Workspace, error) { +func (c *Client) Workspaces(ctx context.Context, user string) ([]coderd.Workspace, error) { route := "/api/v2/workspaces" if user != "" { route += fmt.Sprintf("/%s", user) diff --git a/codersdk/workspaces_test.go b/codersdk/workspaces_test.go index 21e0d5bcaa1a6..5ca82e4fbf4f4 100644 --- a/codersdk/workspaces_test.go +++ b/codersdk/workspaces_test.go @@ -3,167 +3,233 @@ package codersdk_test import ( "context" "testing" + "time" - "github.com/google/uuid" "github.com/stretchr/testify/require" "github.com/coder/coder/coderd" "github.com/coder/coder/coderd/coderdtest" "github.com/coder/coder/database" + "github.com/coder/coder/provisioner/echo" + "github.com/coder/coder/provisionersdk/proto" ) func TestWorkspaces(t *testing.T) { t.Parallel() - t.Run("ListError", func(t *testing.T) { + t.Run("Error", func(t *testing.T) { t.Parallel() - server := coderdtest.New(t) - _, err := server.Client.WorkspacesByUser(context.Background(), "") + client := coderdtest.New(t) + _, err := client.Workspaces(context.Background(), "") require.Error(t, err) }) - t.Run("ListNoOwner", func(t *testing.T) { + t.Run("List", func(t *testing.T) { t.Parallel() - server := coderdtest.New(t) - _, err := server.Client.WorkspacesByUser(context.Background(), "") + client := coderdtest.New(t) + _ = coderdtest.CreateInitialUser(t, client) + _, err := client.Workspaces(context.Background(), "") + require.NoError(t, err) + }) +} + +func TestWorkspacesByProject(t *testing.T) { + t.Parallel() + t.Run("Error", func(t *testing.T) { + t.Parallel() + client := coderdtest.New(t) + _, err := client.WorkspacesByProject(context.Background(), "", "") require.Error(t, err) }) - t.Run("ListByUser", func(t *testing.T) { + t.Run("List", func(t *testing.T) { t.Parallel() - server := coderdtest.New(t) - user := server.RandomInitialUser(t) - project, err := server.Client.CreateProject(context.Background(), user.Organization, coderd.CreateProjectRequest{ - Name: "tomato", - Provisioner: database.ProvisionerTypeEcho, - }) - require.NoError(t, err) - _, err = server.Client.CreateWorkspace(context.Background(), "", coderd.CreateWorkspaceRequest{ - Name: "wow", - ProjectID: project.ID, - }) - require.NoError(t, err) - _, err = server.Client.WorkspacesByUser(context.Background(), "me") + client := coderdtest.New(t) + user := coderdtest.CreateInitialUser(t, client) + project := coderdtest.CreateProject(t, client, user.Organization) + _, err := client.WorkspacesByProject(context.Background(), user.Organization, project.Name) require.NoError(t, err) }) +} - t.Run("ListByProject", func(t *testing.T) { +func TestWorkspace(t *testing.T) { + t.Parallel() + t.Run("Error", func(t *testing.T) { t.Parallel() - server := coderdtest.New(t) - user := server.RandomInitialUser(t) - project, err := server.Client.CreateProject(context.Background(), user.Organization, coderd.CreateProjectRequest{ - Name: "tomato", - Provisioner: database.ProvisionerTypeEcho, - }) - require.NoError(t, err) - _, err = server.Client.CreateWorkspace(context.Background(), "", coderd.CreateWorkspaceRequest{ - Name: "wow", - ProjectID: project.ID, - }) - require.NoError(t, err) - _, err = server.Client.WorkspacesByProject(context.Background(), user.Organization, project.Name) + client := coderdtest.New(t) + _, err := client.Workspace(context.Background(), "", "") + require.Error(t, err) + }) + + t.Run("Get", func(t *testing.T) { + t.Parallel() + client := coderdtest.New(t) + user := coderdtest.CreateInitialUser(t, client) + project := coderdtest.CreateProject(t, client, user.Organization) + workspace := coderdtest.CreateWorkspace(t, client, "", project.ID) + _, err := client.Workspace(context.Background(), "", workspace.Name) require.NoError(t, err) }) +} - t.Run("ListByProjectError", func(t *testing.T) { +func TestListWorkspaceHistory(t *testing.T) { + t.Parallel() + t.Run("Error", func(t *testing.T) { t.Parallel() - server := coderdtest.New(t) - _, err := server.Client.WorkspacesByProject(context.Background(), "", "") + client := coderdtest.New(t) + _, err := client.ListWorkspaceHistory(context.Background(), "", "") require.Error(t, err) }) - t.Run("CreateError", func(t *testing.T) { + t.Run("List", func(t *testing.T) { t.Parallel() - server := coderdtest.New(t) - _, err := server.Client.CreateWorkspace(context.Background(), "no", coderd.CreateWorkspaceRequest{}) + client := coderdtest.New(t) + user := coderdtest.CreateInitialUser(t, client) + project := coderdtest.CreateProject(t, client, user.Organization) + workspace := coderdtest.CreateWorkspace(t, client, "", project.ID) + _, err := client.ListWorkspaceHistory(context.Background(), "", workspace.Name) + require.NoError(t, err) + }) +} + +func TestWorkspaceHistory(t *testing.T) { + t.Parallel() + t.Run("Error", func(t *testing.T) { + t.Parallel() + client := coderdtest.New(t) + _, err := client.WorkspaceHistory(context.Background(), "", "", "") require.Error(t, err) }) - t.Run("Single", func(t *testing.T) { + t.Run("Get", func(t *testing.T) { t.Parallel() - server := coderdtest.New(t) - user := server.RandomInitialUser(t) - project, err := server.Client.CreateProject(context.Background(), user.Organization, coderd.CreateProjectRequest{ - Name: "tomato", - Provisioner: database.ProvisionerTypeEcho, - }) - require.NoError(t, err) - workspace, err := server.Client.CreateWorkspace(context.Background(), "", coderd.CreateWorkspaceRequest{ - Name: "wow", - ProjectID: project.ID, + client := coderdtest.New(t) + user := coderdtest.CreateInitialUser(t, client) + _ = coderdtest.NewProvisionerDaemon(t, client) + project := coderdtest.CreateProject(t, client, user.Organization) + version := coderdtest.CreateProjectVersion(t, client, user.Organization, project.Name, nil) + coderdtest.AwaitProjectVersionImported(t, client, user.Organization, project.Name, version.Name) + workspace := coderdtest.CreateWorkspace(t, client, "", project.ID) + _, err := client.CreateWorkspaceHistory(context.Background(), "", workspace.Name, coderd.CreateWorkspaceHistoryRequest{ + ProjectVersionID: version.ID, + Transition: database.WorkspaceTransitionCreate, }) require.NoError(t, err) - _, err = server.Client.Workspace(context.Background(), "", workspace.Name) - require.NoError(t, err) }) +} - t.Run("SingleError", func(t *testing.T) { +func TestCreateWorkspace(t *testing.T) { + t.Parallel() + t.Run("Error", func(t *testing.T) { t.Parallel() - server := coderdtest.New(t) - _, err := server.Client.Workspace(context.Background(), "", "blob") + client := coderdtest.New(t) + _, err := client.CreateWorkspace(context.Background(), "", coderd.CreateWorkspaceRequest{}) require.Error(t, err) }) - t.Run("History", func(t *testing.T) { + t.Run("Get", func(t *testing.T) { t.Parallel() - server := coderdtest.New(t) - user := server.RandomInitialUser(t) - project, err := server.Client.CreateProject(context.Background(), user.Organization, coderd.CreateProjectRequest{ - Name: "tomato", - Provisioner: database.ProvisionerTypeEcho, - }) - require.NoError(t, err) - workspace, err := server.Client.CreateWorkspace(context.Background(), "", coderd.CreateWorkspaceRequest{ - Name: "wow", - ProjectID: project.ID, + client := coderdtest.New(t) + user := coderdtest.CreateInitialUser(t, client) + project := coderdtest.CreateProject(t, client, user.Organization) + _ = coderdtest.CreateWorkspace(t, client, "", project.ID) + }) +} + +func TestCreateWorkspaceHistory(t *testing.T) { + t.Parallel() + t.Run("Error", func(t *testing.T) { + t.Parallel() + client := coderdtest.New(t) + _, err := client.CreateWorkspaceHistory(context.Background(), "", "", coderd.CreateWorkspaceHistoryRequest{}) + require.Error(t, err) + }) + + t.Run("Create", func(t *testing.T) { + t.Parallel() + client := coderdtest.New(t) + user := coderdtest.CreateInitialUser(t, client) + _ = coderdtest.NewProvisionerDaemon(t, client) + project := coderdtest.CreateProject(t, client, user.Organization) + version := coderdtest.CreateProjectVersion(t, client, user.Organization, project.Name, nil) + coderdtest.AwaitProjectVersionImported(t, client, user.Organization, project.Name, version.Name) + workspace := coderdtest.CreateWorkspace(t, client, "", project.ID) + _, err := client.CreateWorkspaceHistory(context.Background(), "", workspace.Name, coderd.CreateWorkspaceHistoryRequest{ + ProjectVersionID: version.ID, + Transition: database.WorkspaceTransitionCreate, }) require.NoError(t, err) - _, err = server.Client.ListWorkspaceHistory(context.Background(), "", workspace.Name) - require.NoError(t, err) }) +} - t.Run("HistoryError", func(t *testing.T) { +func TestWorkspaceHistoryLogs(t *testing.T) { + t.Parallel() + t.Run("Error", func(t *testing.T) { t.Parallel() - server := coderdtest.New(t) - _, err := server.Client.ListWorkspaceHistory(context.Background(), "", "blob") + client := coderdtest.New(t) + _, err := client.WorkspaceHistoryLogs(context.Background(), "", "", "") require.Error(t, err) }) - t.Run("LatestHistory", func(t *testing.T) { + t.Run("List", func(t *testing.T) { t.Parallel() - server := coderdtest.New(t) - user := server.RandomInitialUser(t) - project, err := server.Client.CreateProject(context.Background(), user.Organization, coderd.CreateProjectRequest{ - Name: "tomato", - Provisioner: database.ProvisionerTypeEcho, + 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) - workspace, err := server.Client.CreateWorkspace(context.Background(), "", coderd.CreateWorkspaceRequest{ - Name: "wow", - ProjectID: project.ID, - }) + _, err = client.WorkspaceHistoryLogs(context.Background(), "", workspace.Name, history.Name) require.NoError(t, err) - _, err = server.Client.WorkspaceHistory(context.Background(), "", workspace.Name, "") + }) +} + +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("CreateHistory", func(t *testing.T) { + t.Run("Stream", func(t *testing.T) { t.Parallel() - server := coderdtest.New(t) - user := server.RandomInitialUser(t) - project, err := server.Client.CreateProject(context.Background(), user.Organization, coderd.CreateProjectRequest{ - Name: "tomato", - Provisioner: database.ProvisionerTypeEcho, - }) - require.NoError(t, err) - workspace, err := server.Client.CreateWorkspace(context.Background(), "", coderd.CreateWorkspaceRequest{ - Name: "wow", - ProjectID: project.ID, + 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{}, + }, + }}, }) - require.NoError(t, err) - _, err = server.Client.CreateWorkspaceHistory(context.Background(), "", workspace.Name, coderd.CreateWorkspaceHistoryRequest{ - ProjectVersionID: uuid.New(), + 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.Error(t, err) + 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) }) } diff --git a/database/dump.sql b/database/dump.sql index 9da449f498079..62ca942f03e67 100644 --- a/database/dump.sql +++ b/database/dump.sql @@ -137,6 +137,26 @@ CREATE TABLE project ( active_version_id uuid ); +CREATE TABLE project_parameter ( + id uuid NOT NULL, + created_at timestamp with time zone NOT NULL, + project_version_id uuid NOT NULL, + name character varying(64) NOT NULL, + description character varying(8192) DEFAULT ''::character varying NOT NULL, + default_source_scheme parameter_source_scheme, + default_source_value text, + allow_override_source boolean NOT NULL, + default_destination_scheme parameter_destination_scheme, + default_destination_value text, + allow_override_destination boolean NOT NULL, + default_refresh text NOT NULL, + redisplay_value boolean NOT NULL, + validation_error character varying(256) NOT NULL, + validation_condition character varying(512) NOT NULL, + validation_type_system parameter_type_system NOT NULL, + validation_value_type character varying(64) NOT NULL +); + CREATE TABLE project_version ( id uuid NOT NULL, project_id uuid NOT NULL, @@ -158,26 +178,6 @@ CREATE TABLE project_version_log ( output character varying(1024) NOT NULL ); -CREATE TABLE project_parameter ( - id uuid NOT NULL, - created_at timestamp with time zone NOT NULL, - project_version_id uuid NOT NULL, - name character varying(64) NOT NULL, - description character varying(8192) DEFAULT ''::character varying NOT NULL, - default_source_scheme parameter_source_scheme, - default_source_value text, - allow_override_source boolean NOT NULL, - default_destination_scheme parameter_destination_scheme, - default_destination_value text, - allow_override_destination boolean NOT NULL, - default_refresh text NOT NULL, - redisplay_value boolean NOT NULL, - validation_error character varying(256) NOT NULL, - validation_condition character varying(512) NOT NULL, - validation_type_system parameter_type_system NOT NULL, - validation_value_type character varying(64) NOT NULL -); - CREATE TABLE provisioner_daemon ( id uuid NOT NULL, created_at timestamp with time zone NOT NULL, @@ -282,15 +282,6 @@ ALTER TABLE ONLY parameter_value ALTER TABLE ONLY parameter_value ADD CONSTRAINT parameter_value_name_scope_scope_id_key UNIQUE (name, scope, scope_id); -ALTER TABLE ONLY project_version - ADD CONSTRAINT project_version_id_key UNIQUE (id); - -ALTER TABLE ONLY project_version_log - ADD CONSTRAINT project_version_log_id_key UNIQUE (id); - -ALTER TABLE ONLY project_version - ADD CONSTRAINT project_version_project_id_name_key UNIQUE (project_id, name); - ALTER TABLE ONLY project ADD CONSTRAINT project_id_key UNIQUE (id); @@ -303,6 +294,15 @@ ALTER TABLE ONLY project_parameter ALTER TABLE ONLY project_parameter ADD CONSTRAINT project_parameter_project_version_id_name_key UNIQUE (project_version_id, name); +ALTER TABLE ONLY project_version + ADD CONSTRAINT project_version_id_key UNIQUE (id); + +ALTER TABLE ONLY project_version_log + ADD CONSTRAINT project_version_log_id_key UNIQUE (id); + +ALTER TABLE ONLY project_version + ADD CONSTRAINT project_version_project_id_name_key UNIQUE (project_id, name); + ALTER TABLE ONLY provisioner_daemon ADD CONSTRAINT provisioner_daemon_id_key UNIQUE (id); @@ -339,15 +339,15 @@ ALTER TABLE ONLY workspace_resource ALTER TABLE ONLY workspace_resource ADD CONSTRAINT workspace_resource_workspace_history_id_name_key UNIQUE (workspace_history_id, name); +ALTER TABLE ONLY project_parameter + ADD CONSTRAINT project_parameter_project_version_id_fkey FOREIGN KEY (project_version_id) REFERENCES project_version(id) ON DELETE CASCADE; + ALTER TABLE ONLY project_version_log ADD CONSTRAINT project_version_log_project_version_id_fkey FOREIGN KEY (project_version_id) REFERENCES project_version(id) ON DELETE CASCADE; ALTER TABLE ONLY project_version ADD CONSTRAINT project_version_project_id_fkey FOREIGN KEY (project_id) REFERENCES project(id); -ALTER TABLE ONLY project_parameter - ADD CONSTRAINT project_parameter_project_version_id_fkey FOREIGN KEY (project_version_id) REFERENCES project_version(id) ON DELETE CASCADE; - ALTER TABLE ONLY provisioner_job ADD CONSTRAINT provisioner_job_project_id_fkey FOREIGN KEY (project_id) REFERENCES project(id) ON DELETE CASCADE; diff --git a/go.mod b/go.mod index 7b2557fa99db8..328e0bf127178 100644 --- a/go.mod +++ b/go.mod @@ -28,6 +28,7 @@ require ( github.com/pion/logging v0.2.2 github.com/pion/transport v0.13.0 github.com/pion/webrtc/v3 v3.1.21 + github.com/quasilyte/go-ruleguard/dsl v0.3.16 github.com/spf13/cobra v1.3.0 github.com/stretchr/testify v1.7.0 github.com/unrolled/secure v1.0.9 diff --git a/go.sum b/go.sum index a05e8a7a74e34..c2b41be6dcec0 100644 --- a/go.sum +++ b/go.sum @@ -1087,6 +1087,8 @@ github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4O github.com/prometheus/procfs v0.2.0/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= +github.com/quasilyte/go-ruleguard/dsl v0.3.16 h1:yJtIpd4oyNS+/c/gKqxNwoGO9+lPOsy1A4BzKjJRcrI= +github.com/quasilyte/go-ruleguard/dsl v0.3.16/go.mod h1:KeCP03KrjuSO0H1kTuZQCWlQPulDV6YMIXmpQss17rU= github.com/remyoudompheng/bigfft v0.0.0-20190728182440-6a916e37a237/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= diff --git a/peer/conn_test.go b/peer/conn_test.go index c55bb56b06614..519e5f3b743db 100644 --- a/peer/conn_test.go +++ b/peer/conn_test.go @@ -2,7 +2,6 @@ package peer_test import ( "context" - "errors" "io" "net" "net/http" @@ -17,6 +16,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.uber.org/goleak" + "golang.org/x/xerrors" "cdr.dev/slog" "cdr.dev/slog/sloggers/slogtest" @@ -231,7 +231,7 @@ func TestConn(t *testing.T) { t.Parallel() conn, err := peer.Client([]webrtc.ICEServer{}, nil) require.NoError(t, err) - expectedErr := errors.New("wow") + expectedErr := xerrors.New("wow") _ = conn.CloseWithError(expectedErr) _, err = conn.Dial(context.Background(), "", nil) require.ErrorIs(t, err, expectedErr) diff --git a/provisioner/echo/serve.go b/provisioner/echo/serve.go index 12172b6c5864e..a0bb57dad1e82 100644 --- a/provisioner/echo/serve.go +++ b/provisioner/echo/serve.go @@ -48,6 +48,10 @@ func (*echo) Parse(request *proto.Parse_Request, stream proto.DRPCProvisioner_Pa path := filepath.Join(request.Directory, fmt.Sprintf("%d.parse.protobuf", index)) _, err := os.Stat(path) if err != nil { + if index == 0 { + // Error if nothing is around to enable failed states. + return xerrors.New("no state") + } break } data, err := os.ReadFile(path) @@ -64,7 +68,8 @@ func (*echo) Parse(request *proto.Parse_Request, stream proto.DRPCProvisioner_Pa return err } } - return nil + <-stream.Context().Done() + return stream.Context().Err() } // Provision reads requests from the provided directory to stream responses. @@ -73,6 +78,10 @@ func (*echo) Provision(request *proto.Provision_Request, stream proto.DRPCProvis path := filepath.Join(request.Directory, fmt.Sprintf("%d.provision.protobuf", index)) _, err := os.Stat(path) if err != nil { + if index == 0 { + // Error if nothing is around to enable failed states. + return xerrors.New("no state") + } break } data, err := os.ReadFile(path) @@ -89,14 +98,24 @@ func (*echo) Provision(request *proto.Provision_Request, stream proto.DRPCProvis return err } } - return nil + <-stream.Context().Done() + return stream.Context().Err() +} + +type Responses struct { + Parse []*proto.Parse_Response + Provision []*proto.Provision_Response } // Tar returns a tar archive of responses to provisioner operations. -func Tar(parseResponses []*proto.Parse_Response, provisionResponses []*proto.Provision_Response) ([]byte, error) { +func Tar(responses *Responses) ([]byte, error) { + if responses == nil { + responses = &Responses{ParseComplete, ProvisionComplete} + } + var buffer bytes.Buffer writer := tar.NewWriter(&buffer) - for index, response := range parseResponses { + for index, response := range responses.Parse { data, err := protobuf.Marshal(response) if err != nil { return nil, err @@ -113,7 +132,7 @@ func Tar(parseResponses []*proto.Parse_Response, provisionResponses []*proto.Pro return nil, err } } - for index, response := range provisionResponses { + for index, response := range responses.Provision { data, err := protobuf.Marshal(response) if err != nil { return nil, err diff --git a/provisioner/echo/serve_test.go b/provisioner/echo/serve_test.go index ce8a1e078bad1..adf8cced81a90 100644 --- a/provisioner/echo/serve_test.go +++ b/provisioner/echo/serve_test.go @@ -53,7 +53,9 @@ func TestEcho(t *testing.T) { }, }, }} - data, err := echo.Tar(responses, nil) + data, err := echo.Tar(&echo.Responses{ + Parse: responses, + }) require.NoError(t, err) client, err := api.Parse(ctx, &proto.Parse_Request{ Directory: unpackTar(t, data), @@ -86,7 +88,9 @@ func TestEcho(t *testing.T) { }, }, }} - data, err := echo.Tar(nil, responses) + data, err := echo.Tar(&echo.Responses{ + Provision: responses, + }) require.NoError(t, err) client, err := api.Provision(ctx, &proto.Provision_Request{ Directory: unpackTar(t, data), diff --git a/provisionerd/provisionerd.go b/provisionerd/provisionerd.go index 2eff81620b87c..78f8c40f4c374 100644 --- a/provisionerd/provisionerd.go +++ b/provisionerd/provisionerd.go @@ -15,6 +15,7 @@ import ( "time" "github.com/hashicorp/yamux" + "go.uber.org/atomic" "cdr.dev/slog" "github.com/coder/coder/provisionerd/proto" @@ -54,7 +55,8 @@ func New(clientDialer Dialer, opts *Options) io.Closer { closeCancel: ctxCancel, closed: make(chan struct{}), - jobRunning: make(chan struct{}), + jobRunning: make(chan struct{}), + jobCancelled: *atomic.NewBool(true), } // Start off with a closed channel so // isRunningJob() returns properly. @@ -77,10 +79,11 @@ type provisionerDaemon struct { closeError error // Locked when acquiring or canceling a job. - jobMutex sync.Mutex - jobID string - jobRunning chan struct{} - jobCancel context.CancelFunc + jobMutex sync.Mutex + jobID string + jobRunning chan struct{} + jobCancelled atomic.Bool + jobCancel context.CancelFunc } // Connect establishes a connection to coderd. @@ -193,6 +196,7 @@ func (p *provisionerDaemon) acquireJob(ctx context.Context) { } ctx, p.jobCancel = context.WithCancel(ctx) p.jobRunning = make(chan struct{}) + p.jobCancelled.Store(false) p.jobID = job.JobId p.opts.Logger.Info(context.Background(), "acquired job", @@ -220,7 +224,7 @@ func (p *provisionerDaemon) runJob(ctx context.Context, job *proto.AcquiredJob) JobId: job.JobId, }) if err != nil { - go p.cancelActiveJobf("send periodic update: %s", err) + p.cancelActiveJobf("send periodic update: %s", err) return } } @@ -247,13 +251,13 @@ func (p *provisionerDaemon) runJob(ctx context.Context, job *proto.AcquiredJob) // It's safe to cast this ProvisionerType. This data is coming directly from coderd. provisioner, hasProvisioner := p.opts.Provisioners[job.Provisioner] if !hasProvisioner { - go p.cancelActiveJobf("provisioner %q not registered", job.Provisioner) + p.cancelActiveJobf("provisioner %q not registered", job.Provisioner) return } err := os.MkdirAll(p.opts.WorkDirectory, 0700) if err != nil { - go p.cancelActiveJobf("create work directory %q: %s", p.opts.WorkDirectory, err) + p.cancelActiveJobf("create work directory %q: %s", p.opts.WorkDirectory, err) return } @@ -265,13 +269,13 @@ func (p *provisionerDaemon) runJob(ctx context.Context, job *proto.AcquiredJob) break } if err != nil { - go p.cancelActiveJobf("read project source archive: %s", err) + p.cancelActiveJobf("read project source archive: %s", err) return } // #nosec path := filepath.Join(p.opts.WorkDirectory, header.Name) if !strings.HasPrefix(path, filepath.Clean(p.opts.WorkDirectory)) { - go p.cancelActiveJobf("tar attempts to target relative upper directory") + p.cancelActiveJobf("tar attempts to target relative upper directory") return } mode := header.FileInfo().Mode() @@ -282,14 +286,14 @@ func (p *provisionerDaemon) runJob(ctx context.Context, job *proto.AcquiredJob) case tar.TypeDir: err = os.MkdirAll(path, mode) if err != nil { - go p.cancelActiveJobf("mkdir %q: %s", path, err) + p.cancelActiveJobf("mkdir %q: %s", path, err) return } p.opts.Logger.Debug(context.Background(), "extracted directory", slog.F("path", path)) case tar.TypeReg: file, err := os.OpenFile(path, os.O_CREATE|os.O_RDWR, mode) if err != nil { - go p.cancelActiveJobf("create file %q (mode %s): %s", path, mode, err) + p.cancelActiveJobf("create file %q (mode %s): %s", path, mode, err) return } // Max file size of 10MB. @@ -299,12 +303,12 @@ func (p *provisionerDaemon) runJob(ctx context.Context, job *proto.AcquiredJob) } if err != nil { _ = file.Close() - go p.cancelActiveJobf("copy file %q: %s", path, err) + p.cancelActiveJobf("copy file %q: %s", path, err) return } err = file.Close() if err != nil { - go p.cancelActiveJobf("close file %q: %s", path, err) + p.cancelActiveJobf("close file %q: %s", path, err) return } p.opts.Logger.Debug(context.Background(), "extracted file", @@ -331,7 +335,7 @@ func (p *provisionerDaemon) runJob(ctx context.Context, job *proto.AcquiredJob) p.runWorkspaceProvision(ctx, provisioner, job) default: - go p.cancelActiveJobf("unknown job type %q; ensure your provisioner daemon is up-to-date", reflect.TypeOf(job.Type).String()) + p.cancelActiveJobf("unknown job type %q; ensure your provisioner daemon is up-to-date", reflect.TypeOf(job.Type).String()) return } @@ -347,14 +351,14 @@ func (p *provisionerDaemon) runProjectImport(ctx context.Context, provisioner sd Directory: p.opts.WorkDirectory, }) if err != nil { - go p.cancelActiveJobf("parse source: %s", err) + p.cancelActiveJobf("parse source: %s", err) return } defer stream.Close() for { msg, err := stream.Recv() if err != nil { - go p.cancelActiveJobf("recv parse source: %s", err) + p.cancelActiveJobf("recv parse source: %s", err) return } switch msgType := msg.Type.(type) { @@ -375,7 +379,7 @@ func (p *provisionerDaemon) runProjectImport(ctx context.Context, provisioner sd }}, }) if err != nil { - go p.cancelActiveJobf("update job: %s", err) + p.cancelActiveJobf("update job: %s", err) return } case *sdkproto.Parse_Response_Complete: @@ -391,13 +395,13 @@ func (p *provisionerDaemon) runProjectImport(ctx context.Context, provisioner sd }, }) if err != nil { - go p.cancelActiveJobf("complete job: %s", err) + p.cancelActiveJobf("complete job: %s", err) return } // Return so we stop looping! return default: - go p.cancelActiveJobf("invalid message type %q received from provisioner", + p.cancelActiveJobf("invalid message type %q received from provisioner", reflect.TypeOf(msg.Type).String()) return } @@ -411,7 +415,7 @@ func (p *provisionerDaemon) runWorkspaceProvision(ctx context.Context, provision State: job.GetWorkspaceProvision().State, }) if err != nil { - go p.cancelActiveJobf("provision: %s", err) + p.cancelActiveJobf("provision: %s", err) return } defer stream.Close() @@ -419,7 +423,7 @@ func (p *provisionerDaemon) runWorkspaceProvision(ctx context.Context, provision for { msg, err := stream.Recv() if err != nil { - go p.cancelActiveJobf("recv workspace provision: %s", err) + p.cancelActiveJobf("recv workspace provision: %s", err) return } switch msgType := msg.Type.(type) { @@ -440,7 +444,7 @@ func (p *provisionerDaemon) runWorkspaceProvision(ctx context.Context, provision }}, }) if err != nil { - go p.cancelActiveJobf("send job update: %s", err) + p.cancelActiveJobf("send job update: %s", err) return } case *sdkproto.Provision_Response_Complete: @@ -462,13 +466,13 @@ func (p *provisionerDaemon) runWorkspaceProvision(ctx context.Context, provision }, }) if err != nil { - go p.cancelActiveJobf("complete job: %s", err) + p.cancelActiveJobf("complete job: %s", err) return } // Return so we stop looping! return default: - go p.cancelActiveJobf("invalid message type %q received from provisioner", + p.cancelActiveJobf("invalid message type %q received from provisioner", reflect.TypeOf(msg.Type).String()) return } @@ -481,12 +485,16 @@ func (p *provisionerDaemon) cancelActiveJobf(format string, args ...interface{}) errMsg := fmt.Sprintf(format, args...) if !p.isRunningJob() { if p.isClosed() { - // We don't want to log if we're already closed! return } - p.opts.Logger.Warn(context.Background(), "skipping job cancel; none running", slog.F("error_message", errMsg)) + p.opts.Logger.Info(context.Background(), "skipping job cancel; none running", slog.F("error_message", errMsg)) return } + if p.jobCancelled.Load() { + p.opts.Logger.Warn(context.Background(), "job has already been canceled", slog.F("error_messsage", errMsg)) + return + } + p.jobCancelled.Store(true) p.jobCancel() p.opts.Logger.Info(context.Background(), "canceling running job", slog.F("error_message", errMsg), @@ -500,7 +508,6 @@ func (p *provisionerDaemon) cancelActiveJobf(format string, args ...interface{}) p.opts.Logger.Warn(context.Background(), "failed to notify of cancel; job is no longer running", slog.Error(err)) return } - <-p.jobRunning p.opts.Logger.Debug(context.Background(), "canceled running job") } @@ -534,6 +541,7 @@ func (p *provisionerDaemon) closeWithError(err error) error { errMsg = err.Error() } p.cancelActiveJobf(errMsg) + <-p.jobRunning p.closeCancel() p.opts.Logger.Debug(context.Background(), "closing server with error", slog.Error(err)) diff --git a/provisionerd/provisionerd_test.go b/provisionerd/provisionerd_test.go index 376bfd1eaadb1..5a32b6fb2030e 100644 --- a/provisionerd/provisionerd_test.go +++ b/provisionerd/provisionerd_test.go @@ -4,7 +4,6 @@ import ( "archive/tar" "bytes" "context" - "errors" "io" "os" "path/filepath" @@ -15,6 +14,7 @@ import ( "github.com/stretchr/testify/require" "go.uber.org/atomic" "go.uber.org/goleak" + "golang.org/x/xerrors" "storj.io/drpc/drpcmux" "storj.io/drpc/drpcserver" @@ -52,7 +52,7 @@ func TestProvisionerd(t *testing.T) { completeChan := make(chan struct{}) closer := createProvisionerd(t, func(ctx context.Context) (proto.DRPCProvisionerDaemonClient, error) { defer close(completeChan) - return nil, errors.New("an error") + return nil, xerrors.New("an error") }, provisionerd.Provisioners{}) <-completeChan require.NoError(t, closer.Close()) diff --git a/provisionersdk/serve_test.go b/provisionersdk/serve_test.go index cf2dd7517df82..601fdc7ea11df 100644 --- a/provisionersdk/serve_test.go +++ b/provisionersdk/serve_test.go @@ -39,6 +39,7 @@ func TestProvisionerSDK(t *testing.T) { _, err = stream.Recv() require.Equal(t, drpcerr.Unimplemented, int(drpcerr.Code(err))) }) + t.Run("ServeClosedPipe", func(t *testing.T) { t.Parallel() client, server := provisionersdk.TransportPipe() diff --git a/rules.go b/rules.go new file mode 100644 index 0000000000000..7a95c89e016ed --- /dev/null +++ b/rules.go @@ -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) +}