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) +}
Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.
Alternative Proxies: