diff --git a/.github/workflows/coder.yaml b/.github/workflows/coder.yaml index fad4245c3c838..72cf026d09212 100644 --- a/.github/workflows/coder.yaml +++ b/.github/workflows/coder.yaml @@ -157,6 +157,7 @@ jobs: GOMAXPROCS: ${{ runner.os == 'Windows' && 1 || 2 }} run: gotestsum --junitfile="gotests.xml" --packages="./..." -- -covermode=atomic -coverprofile="gotests.coverage" + -coverpkg=./...,github.com/coder/coder/codersdk -timeout=3m -count=$GOCOUNT -race -short -failfast - name: Upload DataDog Trace @@ -172,6 +173,7 @@ jobs: if: runner.os == 'Linux' run: DB=true gotestsum --junitfile="gotests.xml" --packages="./..." -- -covermode=atomic -coverprofile="gotests.coverage" -timeout=3m + -coverpkg=./...,github.com/coder/coder/codersdk -count=1 -race -parallel=2 -failfast - name: Upload DataDog Trace diff --git a/.vscode/settings.json b/.vscode/settings.json index 32e7a2ddf4e34..9b49cb22b9247 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -6,6 +6,9 @@ "go.lintFlags": ["--fast"], "go.lintOnSave": "package", "go.coverOnSave": true, + // The codersdk is used by coderd another other packages extensively. + // To reduce redundancy in tests, it's covered by other packages. + "go.testFlags": ["-coverpkg=./.,github.com/coder/coder/codersdk"], "go.coverageDecorator": { "type": "gutter", "coveredHighlightColor": "rgba(64,128,128,0.5)", diff --git a/agent/agent.go b/agent/agent.go index 285efe3dc9836..a121b2150947f 100644 --- a/agent/agent.go +++ b/agent/agent.go @@ -59,9 +59,9 @@ type Options struct { Logger slog.Logger } -type Dialer func(ctx context.Context) (*peerbroker.Listener, error) +type Dialer func(ctx context.Context, options *peer.ConnOptions) (*peerbroker.Listener, error) -func New(dialer Dialer, options *Options) io.Closer { +func New(dialer Dialer, options *peer.ConnOptions) io.Closer { ctx, cancelFunc := context.WithCancel(context.Background()) server := &server{ clientDialer: dialer, @@ -75,11 +75,12 @@ func New(dialer Dialer, options *Options) io.Closer { type server struct { clientDialer Dialer - options *Options + options *peer.ConnOptions - closeCancel context.CancelFunc - closeMutex sync.Mutex - closed chan struct{} + connCloseWait sync.WaitGroup + closeCancel context.CancelFunc + closeMutex sync.Mutex + closed chan struct{} sshServer *ssh.Server } @@ -249,7 +250,7 @@ func (s *server) run(ctx context.Context) { // An exponential back-off occurs when the connection is failing to dial. // This is to prevent server spam in case of a coderd outage. for retrier := retry.New(50*time.Millisecond, 10*time.Second); retrier.Wait(ctx); { - peerListener, err = s.clientDialer(ctx) + peerListener, err = s.clientDialer(ctx, s.options) if err != nil { if errors.Is(err, context.Canceled) { return @@ -279,11 +280,18 @@ func (s *server) run(ctx context.Context) { s.run(ctx) return } + s.closeMutex.Lock() + s.connCloseWait.Add(1) + s.closeMutex.Unlock() go s.handlePeerConn(ctx, conn) } } func (s *server) handlePeerConn(ctx context.Context, conn *peer.Conn) { + go func() { + <-conn.Closed() + s.connCloseWait.Done() + }() for { channel, err := conn.Accept(ctx) if err != nil { @@ -325,5 +333,6 @@ func (s *server) Close() error { close(s.closed) s.closeCancel() _ = s.sshServer.Close() + s.connCloseWait.Wait() return nil } diff --git a/agent/agent_test.go b/agent/agent_test.go index 75f737a94b22a..cc25969b1e69f 100644 --- a/agent/agent_test.go +++ b/agent/agent_test.go @@ -94,11 +94,9 @@ func TestAgent(t *testing.T) { func setup(t *testing.T) proto.DRPCPeerBrokerClient { client, server := provisionersdk.TransportPipe() - closer := agent.New(func(ctx context.Context) (*peerbroker.Listener, error) { - return peerbroker.Listen(server, nil, &peer.ConnOptions{ - Logger: slogtest.Make(t, nil), - }) - }, &agent.Options{ + closer := agent.New(func(ctx context.Context, opts *peer.ConnOptions) (*peerbroker.Listener, error) { + return peerbroker.Listen(server, nil, opts) + }, &peer.ConnOptions{ Logger: slogtest.Make(t, nil).Leveled(slog.LevelDebug), }) t.Cleanup(func() { diff --git a/cli/login.go b/cli/login.go index 74094aace7865..c924958e7cafa 100644 --- a/cli/login.go +++ b/cli/login.go @@ -59,7 +59,7 @@ func login() *cobra.Command { } client := codersdk.New(serverURL) - hasInitialUser, err := client.HasInitialUser(cmd.Context()) + hasInitialUser, err := client.HasFirstUser(cmd.Context()) if err != nil { return xerrors.Errorf("has initial user: %w", err) } @@ -119,7 +119,7 @@ func login() *cobra.Command { return xerrors.Errorf("specify password prompt: %w", err) } - _, err = client.CreateInitialUser(cmd.Context(), coderd.CreateInitialUserRequest{ + _, err = client.CreateFirstUser(cmd.Context(), coderd.CreateFirstUserRequest{ Email: email, Username: username, Password: password, diff --git a/cli/login_test.go b/cli/login_test.go index ee3dcdce9c090..e6ed0ce4401ef 100644 --- a/cli/login_test.go +++ b/cli/login_test.go @@ -1,13 +1,11 @@ package cli_test import ( - "context" "testing" "github.com/stretchr/testify/require" "github.com/coder/coder/cli/clitest" - "github.com/coder/coder/coderd" "github.com/coder/coder/coderd/coderdtest" "github.com/coder/coder/pty/ptytest" ) @@ -56,18 +54,7 @@ func TestLogin(t *testing.T) { t.Run("ExistingUserValidTokenTTY", func(t *testing.T) { t.Parallel() client := coderdtest.New(t, nil) - _, err := client.CreateInitialUser(context.Background(), coderd.CreateInitialUserRequest{ - Username: "test-user", - Email: "test-user@coder.com", - Organization: "acme-corp", - Password: "password", - }) - require.NoError(t, err) - token, err := client.LoginWithPassword(context.Background(), coderd.LoginWithPasswordRequest{ - Email: "test-user@coder.com", - Password: "password", - }) - require.NoError(t, err) + coderdtest.CreateFirstUser(t, client) root, _ := clitest.New(t, "login", client.URL.String(), "--force-tty", "--no-open") pty := ptytest.New(t) @@ -79,20 +66,14 @@ func TestLogin(t *testing.T) { }() pty.ExpectMatch("Paste your token here:") - pty.WriteLine(token.SessionToken) + pty.WriteLine(client.SessionToken) pty.ExpectMatch("Welcome to Coder") }) t.Run("ExistingUserInvalidTokenTTY", func(t *testing.T) { t.Parallel() client := coderdtest.New(t, nil) - _, err := client.CreateInitialUser(context.Background(), coderd.CreateInitialUserRequest{ - Username: "test-user", - Email: "test-user@coder.com", - Organization: "acme-corp", - Password: "password", - }) - require.NoError(t, err) + coderdtest.CreateFirstUser(t, client) root, _ := clitest.New(t, "login", client.URL.String(), "--force-tty", "--no-open") pty := ptytest.New(t) diff --git a/cli/projectcreate.go b/cli/projectcreate.go index a1e5027c26715..b4d7ee46e3443 100644 --- a/cli/projectcreate.go +++ b/cli/projectcreate.go @@ -56,7 +56,7 @@ func projectCreate() *cobra.Command { Default: filepath.Base(directory), Label: "What's your project's name?", Validate: func(s string) error { - project, _ := client.Project(cmd.Context(), organization.Name, s) + project, _ := client.ProjectByName(cmd.Context(), organization.ID, s) if project.ID.String() != uuid.Nil.String() { return xerrors.New("A project already exists with that name!") } @@ -71,9 +71,9 @@ func projectCreate() *cobra.Command { if err != nil { return err } - project, err := client.CreateProject(cmd.Context(), organization.Name, coderd.CreateProjectRequest{ - Name: name, - VersionImportJobID: job.ID, + project, err := client.CreateProject(cmd.Context(), organization.ID, coderd.CreateProjectRequest{ + Name: name, + VersionID: job.ID, }) if err != nil { return err @@ -118,7 +118,7 @@ func projectCreate() *cobra.Command { return cmd } -func validateProjectVersionSource(cmd *cobra.Command, client *codersdk.Client, organization coderd.Organization, provisioner database.ProvisionerType, directory string, parameters ...coderd.CreateParameterValueRequest) (*coderd.ProvisionerJob, error) { +func validateProjectVersionSource(cmd *cobra.Command, client *codersdk.Client, organization coderd.Organization, provisioner database.ProvisionerType, directory string, parameters ...coderd.CreateParameterRequest) (*coderd.ProjectVersion, error) { spin := spinner.New(spinner.CharSets[5], 100*time.Millisecond) spin.Writer = cmd.OutOrStdout() spin.Suffix = " Uploading current directory..." @@ -133,13 +133,13 @@ func validateProjectVersionSource(cmd *cobra.Command, client *codersdk.Client, o if err != nil { return nil, err } - resp, err := client.UploadFile(cmd.Context(), codersdk.ContentTypeTar, tarData) + resp, err := client.Upload(cmd.Context(), codersdk.ContentTypeTar, tarData) if err != nil { return nil, err } before := time.Now() - job, err := client.CreateProjectImportJob(cmd.Context(), organization.Name, coderd.CreateProjectImportJobRequest{ + version, err := client.CreateProjectVersion(cmd.Context(), organization.ID, coderd.CreateProjectVersionRequest{ StorageMethod: database.ProvisionerStorageMethodFile, StorageSource: resp.Hash, Provisioner: provisioner, @@ -149,7 +149,7 @@ func validateProjectVersionSource(cmd *cobra.Command, client *codersdk.Client, o return nil, err } spin.Suffix = " Waiting for the import to complete..." - logs, err := client.ProjectImportJobLogsAfter(cmd.Context(), organization.Name, job.ID, before) + logs, err := client.ProjectVersionLogsAfter(cmd.Context(), version.ID, before) if err != nil { return nil, err } @@ -162,22 +162,22 @@ func validateProjectVersionSource(cmd *cobra.Command, client *codersdk.Client, o logBuffer = append(logBuffer, log) } - job, err = client.ProjectImportJob(cmd.Context(), organization.Name, job.ID) + version, err = client.ProjectVersion(cmd.Context(), version.ID) if err != nil { return nil, err } - parameterSchemas, err := client.ProjectImportJobSchemas(cmd.Context(), organization.Name, job.ID) + parameterSchemas, err := client.ProjectVersionSchema(cmd.Context(), version.ID) if err != nil { return nil, err } - parameterValues, err := client.ProjectImportJobParameters(cmd.Context(), organization.Name, job.ID) + parameterValues, err := client.ProjectVersionParameters(cmd.Context(), version.ID) if err != nil { return nil, err } spin.Stop() - if provisionerd.IsMissingParameterError(job.Error) { - valuesBySchemaID := map[string]coderd.ComputedParameterValue{} + if provisionerd.IsMissingParameterError(version.Job.Error) { + valuesBySchemaID := map[string]coderd.ProjectVersionParameter{} for _, parameterValue := range parameterValues { valuesBySchemaID[parameterValue.SchemaID.String()] = parameterValue } @@ -192,7 +192,7 @@ func validateProjectVersionSource(cmd *cobra.Command, client *codersdk.Client, o if err != nil { return nil, err } - parameters = append(parameters, coderd.CreateParameterValueRequest{ + parameters = append(parameters, coderd.CreateParameterRequest{ Name: parameterSchema.Name, SourceValue: value, SourceScheme: database.ParameterSourceSchemeData, @@ -202,21 +202,20 @@ func validateProjectVersionSource(cmd *cobra.Command, client *codersdk.Client, o return validateProjectVersionSource(cmd, client, organization, provisioner, directory, parameters...) } - if job.Status != coderd.ProvisionerJobStatusSucceeded { + if version.Job.Status != coderd.ProvisionerJobSucceeded { for _, log := range logBuffer { _, _ = fmt.Fprintf(cmd.OutOrStdout(), "%s %s\n", color.HiGreenString("[tf]"), log.Output) } - - return nil, xerrors.New(job.Error) + return nil, xerrors.New(version.Job.Error) } _, _ = fmt.Fprintf(cmd.OutOrStdout(), "%s Successfully imported project source!\n", color.HiGreenString("✓")) - resources, err := client.ProjectImportJobResources(cmd.Context(), organization.Name, job.ID) + resources, err := client.ProjectVersionResources(cmd.Context(), version.ID) if err != nil { return nil, err } - return &job, displayProjectImportInfo(cmd, parameterSchemas, parameterValues, resources) + return &version, displayProjectImportInfo(cmd, parameterSchemas, parameterValues, resources) } func tarDirectory(directory string) ([]byte, error) { diff --git a/cli/projectcreate_test.go b/cli/projectcreate_test.go index b973de4688295..ad9c010b107bf 100644 --- a/cli/projectcreate_test.go +++ b/cli/projectcreate_test.go @@ -18,7 +18,7 @@ func TestProjectCreate(t *testing.T) { t.Run("NoParameters", func(t *testing.T) { t.Parallel() client := coderdtest.New(t, nil) - coderdtest.CreateInitialUser(t, client) + coderdtest.CreateFirstUser(t, client) source := clitest.CreateProjectVersionSource(t, &echo.Responses{ Parse: echo.ParseComplete, Provision: echo.ProvisionComplete, @@ -54,7 +54,7 @@ func TestProjectCreate(t *testing.T) { t.Run("Parameter", func(t *testing.T) { t.Parallel() client := coderdtest.New(t, nil) - coderdtest.CreateInitialUser(t, client) + coderdtest.CreateFirstUser(t, client) source := clitest.CreateProjectVersionSource(t, &echo.Responses{ Parse: []*proto.Parse_Response{{ Type: &proto.Parse_Response_Complete{ diff --git a/cli/projectlist.go b/cli/projectlist.go index 9bb60568d3aa8..741cea2433ba2 100644 --- a/cli/projectlist.go +++ b/cli/projectlist.go @@ -23,7 +23,7 @@ func projectList() *cobra.Command { if err != nil { return err } - projects, err := client.Projects(cmd.Context(), organization.Name) + projects, err := client.ProjectsByOrganization(cmd.Context(), organization.ID) if err != nil { return err } diff --git a/cli/projectlist_test.go b/cli/projectlist_test.go index a1340989946b2..db2392bf2797d 100644 --- a/cli/projectlist_test.go +++ b/cli/projectlist_test.go @@ -15,7 +15,7 @@ func TestProjectList(t *testing.T) { t.Run("None", func(t *testing.T) { t.Parallel() client := coderdtest.New(t, nil) - coderdtest.CreateInitialUser(t, client) + coderdtest.CreateFirstUser(t, client) cmd, root := clitest.New(t, "projects", "list") clitest.SetupConfig(t, client, root) pty := ptytest.New(t) @@ -33,12 +33,12 @@ func TestProjectList(t *testing.T) { t.Run("List", func(t *testing.T) { t.Parallel() client := coderdtest.New(t, nil) - user := coderdtest.CreateInitialUser(t, client) + user := coderdtest.CreateFirstUser(t, client) daemon := coderdtest.NewProvisionerDaemon(t, client) - job := coderdtest.CreateProjectImportJob(t, client, user.Organization, nil) - coderdtest.AwaitProjectImportJob(t, client, user.Organization, job.ID) + version := coderdtest.CreateProjectVersion(t, client, user.OrganizationID, nil) + coderdtest.AwaitProjectVersionJob(t, client, version.ID) _ = daemon.Close() - project := coderdtest.CreateProject(t, client, user.Organization, job.ID) + project := coderdtest.CreateProject(t, client, user.OrganizationID, version.ID) cmd, root := clitest.New(t, "projects", "list") clitest.SetupConfig(t, client, root) pty := ptytest.New(t) diff --git a/cli/projects.go b/cli/projects.go index 0f04dc05cd259..74c4424a537e6 100644 --- a/cli/projects.go +++ b/cli/projects.go @@ -40,8 +40,8 @@ func projects() *cobra.Command { return cmd } -func displayProjectImportInfo(cmd *cobra.Command, parameterSchemas []coderd.ParameterSchema, parameterValues []coderd.ComputedParameterValue, resources []coderd.ProvisionerJobResource) error { - schemaByID := map[string]coderd.ParameterSchema{} +func displayProjectImportInfo(cmd *cobra.Command, parameterSchemas []coderd.ProjectVersionParameterSchema, parameterValues []coderd.ProjectVersionParameter, resources []coderd.WorkspaceResource) error { + schemaByID := map[string]coderd.ProjectVersionParameterSchema{} for _, schema := range parameterSchemas { schemaByID[schema.ID.String()] = schema } diff --git a/cli/root.go b/cli/root.go index 054d9d84f942b..58f6e05bde49e 100644 --- a/cli/root.go +++ b/cli/root.go @@ -108,7 +108,7 @@ func createClient(cmd *cobra.Command) (*codersdk.Client, error) { // currentOrganization returns the currently active organization for the authenticated user. func currentOrganization(cmd *cobra.Command, client *codersdk.Client) (coderd.Organization, error) { - orgs, err := client.UserOrganizations(cmd.Context(), "me") + orgs, err := client.OrganizationsByUser(cmd.Context(), "me") if err != nil { return coderd.Organization{}, nil } diff --git a/cli/workspaceagent.go b/cli/workspaceagent.go new file mode 100644 index 0000000000000..e8dc4e2da2487 --- /dev/null +++ b/cli/workspaceagent.go @@ -0,0 +1,57 @@ +package cli + +import ( + "net/url" + "os" + + "github.com/powersj/whatsthis/pkg/cloud" + "github.com/spf13/cobra" + "golang.org/x/xerrors" + + "github.com/coder/coder/agent" + "github.com/coder/coder/codersdk" +) + +func workspaceAgent() *cobra.Command { + return &cobra.Command{ + Use: "agent", + // This command isn't useful for users, and seems + // more likely to confuse. + Hidden: true, + RunE: func(cmd *cobra.Command, args []string) error { + coderURLRaw, exists := os.LookupEnv("CODER_URL") + if !exists { + return xerrors.New("CODER_URL must be set") + } + coderURL, err := url.Parse(coderURLRaw) + if err != nil { + return xerrors.Errorf("parse %q: %w", coderURLRaw, err) + } + client := codersdk.New(coderURL) + sessionToken, exists := os.LookupEnv("CODER_TOKEN") + if !exists { + probe, err := cloud.New() + if err != nil { + return xerrors.Errorf("probe cloud: %w", err) + } + if !probe.Detected { + return xerrors.Errorf("no valid authentication method found; set \"CODER_TOKEN\"") + } + switch { + case probe.GCP(): + response, err := client.AuthWorkspaceGoogleInstanceIdentity(cmd.Context(), "", nil) + if err != nil { + return xerrors.Errorf("authenticate workspace with gcp: %w", err) + } + sessionToken = response.SessionToken + default: + return xerrors.Errorf("%q authentication not supported; set \"CODER_TOKEN\" instead", probe.Name) + } + } + client.SessionToken = sessionToken + closer := agent.New(client.ListenWorkspaceAgent, nil) + <-cmd.Context().Done() + return closer.Close() + }, + } +} diff --git a/cli/workspacecreate.go b/cli/workspacecreate.go index f4eae0ab5d846..156324240fad8 100644 --- a/cli/workspacecreate.go +++ b/cli/workspacecreate.go @@ -39,7 +39,7 @@ func workspaceCreate() *cobra.Command { if s == "" { return xerrors.Errorf("You must provide a name!") } - workspace, _ := client.Workspace(cmd.Context(), "", s) + workspace, _ := client.WorkspaceByName(cmd.Context(), "", s) if workspace.ID.String() != uuid.Nil.String() { return xerrors.New("A workspace already exists with that name!") } @@ -56,23 +56,23 @@ func workspaceCreate() *cobra.Command { _, _ = fmt.Fprintf(cmd.OutOrStdout(), "%s Previewing project create...\n", caret) - project, err := client.Project(cmd.Context(), organization.Name, args[0]) + project, err := client.ProjectByName(cmd.Context(), organization.ID, args[0]) if err != nil { return err } - projectVersion, err := client.ProjectVersion(cmd.Context(), organization.Name, project.Name, project.ActiveVersionID.String()) + projectVersion, err := client.ProjectVersion(cmd.Context(), project.ActiveVersionID) if err != nil { return err } - parameterSchemas, err := client.ProjectImportJobSchemas(cmd.Context(), organization.Name, projectVersion.ImportJobID) + parameterSchemas, err := client.ProjectVersionSchema(cmd.Context(), projectVersion.ID) if err != nil { return err } - parameterValues, err := client.ProjectImportJobParameters(cmd.Context(), organization.Name, projectVersion.ImportJobID) + parameterValues, err := client.ProjectVersionParameters(cmd.Context(), projectVersion.ID) if err != nil { return err } - resources, err := client.ProjectImportJobResources(cmd.Context(), organization.Name, projectVersion.ImportJobID) + resources, err := client.ProjectVersionResources(cmd.Context(), projectVersion.ID) if err != nil { return err } @@ -100,7 +100,7 @@ func workspaceCreate() *cobra.Command { if err != nil { return err } - history, err := client.CreateWorkspaceHistory(cmd.Context(), "", workspace.Name, coderd.CreateWorkspaceHistoryRequest{ + version, err := client.CreateWorkspaceBuild(cmd.Context(), workspace.ID, coderd.CreateWorkspaceBuildRequest{ ProjectVersionID: projectVersion.ID, Transition: database.WorkspaceTransitionStart, }) @@ -108,7 +108,7 @@ func workspaceCreate() *cobra.Command { return err } - logs, err := client.WorkspaceProvisionJobLogsAfter(cmd.Context(), organization.Name, history.ProvisionJobID, time.Time{}) + logs, err := client.WorkspaceBuildLogsAfter(cmd.Context(), version.ID, time.Time{}) if err != nil { return err } diff --git a/cli/workspacecreate_test.go b/cli/workspacecreate_test.go index 812f59db38d7f..6709da6aa057e 100644 --- a/cli/workspacecreate_test.go +++ b/cli/workspacecreate_test.go @@ -17,9 +17,9 @@ func TestWorkspaceCreate(t *testing.T) { t.Run("Create", func(t *testing.T) { t.Parallel() client := coderdtest.New(t, nil) - user := coderdtest.CreateInitialUser(t, client) + user := coderdtest.CreateFirstUser(t, client) _ = coderdtest.NewProvisionerDaemon(t, client) - job := coderdtest.CreateProjectImportJob(t, client, user.Organization, &echo.Responses{ + version := coderdtest.CreateProjectVersion(t, client, user.OrganizationID, &echo.Responses{ Parse: echo.ParseComplete, Provision: []*proto.Provision_Response{{ Type: &proto.Provision_Response_Complete{ @@ -32,8 +32,8 @@ func TestWorkspaceCreate(t *testing.T) { }, }}, }) - coderdtest.AwaitProjectImportJob(t, client, user.Organization, job.ID) - project := coderdtest.CreateProject(t, client, user.Organization, job.ID) + coderdtest.AwaitProjectVersionJob(t, client, version.ID) + project := coderdtest.CreateProject(t, client, user.OrganizationID, version.ID) cmd, root := clitest.New(t, "workspaces", "create", project.Name) clitest.SetupConfig(t, client, root) diff --git a/cli/workspaces.go b/cli/workspaces.go index d405f00cea88b..b470fc7df1c60 100644 --- a/cli/workspaces.go +++ b/cli/workspaces.go @@ -6,6 +6,7 @@ func workspaces() *cobra.Command { cmd := &cobra.Command{ Use: "workspaces", } + cmd.AddCommand(workspaceAgent()) cmd.AddCommand(workspaceCreate()) return cmd diff --git a/codecov.yml b/codecov.yml index 7666963e4f0cf..0a45a9a582c18 100644 --- a/codecov.yml +++ b/codecov.yml @@ -26,9 +26,13 @@ ignore: # This is generated code. - database/models.go - database/query.sql.go - # All coderd tests fail if this doesn't work. - database/databasefake + # These are generated or don't require tests. + - cmd + - database/dump + - database/postgres - peerbroker/proto - provisionerd/proto - provisionersdk/proto - scripts/datadog-cireport + - rules.go diff --git a/coderd/cmd/root.go b/coderd/cmd/root.go index 162390898aa77..230b1327a1242 100644 --- a/coderd/cmd/root.go +++ b/coderd/cmd/root.go @@ -98,7 +98,7 @@ func newProvisionerDaemon(ctx context.Context, client *codersdk.Client, logger s if err != nil { return nil, err } - return provisionerd.New(client.ProvisionerDaemonClient, &provisionerd.Options{ + return provisionerd.New(client.ListenProvisionerDaemon, &provisionerd.Options{ Logger: logger, PollInterval: 50 * time.Millisecond, UpdateInterval: 50 * time.Millisecond, diff --git a/coderd/coderd.go b/coderd/coderd.go index 69a8432aa53bf..6a21929411eb2 100644 --- a/coderd/coderd.go +++ b/coderd/coderd.go @@ -41,119 +41,131 @@ func New(options *Options) (http.Handler, func()) { Message: "👋", }) }) - r.Post("/login", api.postLogin) - r.Post("/logout", api.postLogout) - - // Used for setup. - r.Get("/user", api.user) - r.Post("/user", api.postUser) - r.Route("/users", func(r chi.Router) { + r.Route("/files", func(r chi.Router) { + r.Use(httpmw.ExtractAPIKey(options.Database, nil)) + r.Get("/{hash}", api.fileByHash) + r.Post("/", api.postFile) + }) + r.Route("/organizations/{organization}", func(r chi.Router) { r.Use( httpmw.ExtractAPIKey(options.Database, nil), + httpmw.ExtractOrganizationParam(options.Database), ) - r.Post("/", api.postUsers) - - r.Route("/{user}", func(r chi.Router) { - r.Use(httpmw.ExtractUserParam(options.Database)) - r.Get("/", api.userByName) - r.Get("/organizations", api.organizationsByUser) - r.Post("/keys", api.postKeyForUser) + r.Get("/", api.organization) + r.Get("/provisionerdaemons", api.provisionerDaemonsByOrganization) + r.Post("/projectversions", api.postProjectVersionsByOrganization) + r.Route("/projects", func(r chi.Router) { + r.Post("/", api.postProjectsByOrganization) + r.Get("/", api.projectsByOrganization) + r.Get("/{projectname}", api.projectByOrganizationAndName) + }) + }) + r.Route("/parameters/{scope}/{id}", func(r chi.Router) { + r.Use(httpmw.ExtractAPIKey(options.Database, nil)) + r.Post("/", api.postParameter) + r.Get("/", api.parameters) + r.Route("/{name}", func(r chi.Router) { + r.Delete("/", api.deleteParameter) }) }) - r.Route("/projects", func(r chi.Router) { + r.Route("/projects/{project}", func(r chi.Router) { r.Use( httpmw.ExtractAPIKey(options.Database, nil), + httpmw.ExtractProjectParam(options.Database), + httpmw.ExtractOrganizationParam(options.Database), ) - r.Get("/", api.projects) - r.Route("/{organization}", func(r chi.Router) { - r.Use(httpmw.ExtractOrganizationParam(options.Database)) - r.Get("/", api.projectsByOrganization) - r.Post("/", api.postProjectsByOrganization) - r.Route("/{project}", func(r chi.Router) { - r.Use(httpmw.ExtractProjectParam(options.Database)) - r.Get("/", api.projectByOrganization) - r.Get("/workspaces", api.workspacesByProject) - r.Route("/parameters", func(r chi.Router) { - r.Get("/", api.parametersByProject) - r.Post("/", api.postParametersByProject) - }) - r.Route("/versions", func(r chi.Router) { - r.Get("/", api.projectVersionsByOrganization) - r.Post("/", api.postProjectVersionByOrganization) - r.Route("/{projectversion}", func(r chi.Router) { - r.Use(httpmw.ExtractProjectVersionParam(api.Database)) - r.Get("/", api.projectVersionByOrganizationAndName) - }) - }) - }) + r.Get("/", api.project) + r.Route("/versions", func(r chi.Router) { + r.Get("/", api.projectVersionsByProject) + r.Patch("/versions", nil) + r.Get("/{projectversionname}", api.projectVersionByName) }) }) + r.Route("/projectversions/{projectversion}", func(r chi.Router) { + r.Use( + httpmw.ExtractAPIKey(options.Database, nil), + httpmw.ExtractProjectVersionParam(options.Database), + httpmw.ExtractOrganizationParam(options.Database), + ) - // Listing operations specific to resources should go under - // their respective routes. eg. /orgs//workspaces - r.Route("/workspaces", func(r chi.Router) { - r.Use(httpmw.ExtractAPIKey(options.Database, nil)) - r.Get("/", api.workspaces) - r.Route("/{user}", func(r chi.Router) { - r.Use(httpmw.ExtractUserParam(options.Database)) - r.Post("/", api.postWorkspaceByUser) - r.Route("/{workspace}", func(r chi.Router) { - r.Use(httpmw.ExtractWorkspaceParam(options.Database)) - r.Get("/", api.workspaceByUser) - r.Route("/version", func(r chi.Router) { - r.Post("/", api.postWorkspaceHistoryByUser) - r.Get("/", api.workspaceHistoryByUser) - r.Route("/{workspacehistory}", func(r chi.Router) { - r.Use(httpmw.ExtractWorkspaceHistoryParam(options.Database)) - r.Get("/", api.workspaceHistoryByName) - }) + r.Get("/", api.projectVersion) + r.Get("/schema", api.projectVersionSchema) + r.Get("/parameters", api.projectVersionParameters) + r.Get("/resources", api.projectVersionResources) + r.Get("/logs", api.projectVersionLogs) + }) + r.Route("/provisionerdaemons", func(r chi.Router) { + r.Route("/me", func(r chi.Router) { + r.Get("/listen", api.provisionerDaemonsListen) + }) + }) + r.Route("/users", func(r chi.Router) { + r.Get("/first", api.firstUser) + r.Post("/first", api.postFirstUser) + r.Post("/login", api.postLogin) + r.Post("/logout", api.postLogout) + r.Group(func(r chi.Router) { + r.Use(httpmw.ExtractAPIKey(options.Database, nil)) + r.Post("/", api.postUsers) + r.Route("/{user}", func(r chi.Router) { + r.Use(httpmw.ExtractUserParam(options.Database)) + r.Get("/", api.userByName) + r.Get("/organizations", api.organizationsByUser) + r.Post("/organizations", api.postOrganizationsByUser) + r.Post("/keys", api.postAPIKey) + r.Route("/organizations", func(r chi.Router) { + r.Post("/", api.postOrganizationsByUser) + r.Get("/", api.organizationsByUser) + r.Get("/{organizationname}", api.organizationByUserAndName) + }) + r.Route("/workspaces", func(r chi.Router) { + r.Post("/", api.postWorkspacesByUser) + r.Get("/", api.workspacesByUser) + r.Get("/{workspacename}", api.workspaceByUserAndName) }) }) }) }) - - r.Route("/workspaceagent", func(r chi.Router) { - r.Route("/authenticate", func(r chi.Router) { - r.Post("/google-instance-identity", api.postAuthenticateWorkspaceAgentUsingGoogleInstanceIdentity) + r.Route("/workspaceresources", func(r chi.Router) { + r.Route("/auth", func(r chi.Router) { + r.Post("/google-instance-identity", api.postWorkspaceAuthGoogleInstanceIdentity) + }) + r.Route("/agent", func(r chi.Router) { + r.Use(httpmw.ExtractWorkspaceAgent(options.Database)) + r.Get("/", api.workspaceAgentListen) + }) + r.Route("/{workspaceresource}", func(r chi.Router) { + r.Use( + httpmw.ExtractAPIKey(options.Database, nil), + httpmw.ExtractWorkspaceResourceParam(options.Database), + httpmw.ExtractWorkspaceParam(options.Database), + ) + r.Get("/", api.workspaceResource) + r.Get("/dial", api.workspaceResourceDial) }) }) - - r.Route("/upload", func(r chi.Router) { - r.Use(httpmw.ExtractAPIKey(options.Database, nil)) - r.Post("/", api.postUpload) - }) - - r.Route("/projectimport/{organization}", func(r chi.Router) { + r.Route("/workspaces/{workspace}", func(r chi.Router) { r.Use( httpmw.ExtractAPIKey(options.Database, nil), - httpmw.ExtractOrganizationParam(options.Database), + httpmw.ExtractWorkspaceParam(options.Database), ) - r.Post("/", api.postProjectImportByOrganization) - r.Route("/{provisionerjob}", func(r chi.Router) { - r.Use(httpmw.ExtractProvisionerJobParam(options.Database)) - r.Get("/", api.provisionerJobByID) - r.Get("/schemas", api.projectImportJobSchemasByID) - r.Get("/parameters", api.projectImportJobParametersByID) - r.Get("/resources", api.projectImportJobResourcesByID) - r.Get("/logs", api.provisionerJobLogsByID) + r.Get("/", api.workspace) + r.Route("/builds", func(r chi.Router) { + r.Get("/", api.workspaceBuilds) + r.Post("/", api.postWorkspaceBuilds) + r.Get("/latest", api.workspaceBuildLatest) + r.Get("/{workspacebuildname}", api.workspaceBuildByName) }) }) - - r.Route("/workspaceprovision/{organization}", func(r chi.Router) { + r.Route("/workspacebuilds/{workspacebuild}", func(r chi.Router) { r.Use( httpmw.ExtractAPIKey(options.Database, nil), - httpmw.ExtractOrganizationParam(options.Database), + httpmw.ExtractWorkspaceBuildParam(options.Database), + httpmw.ExtractWorkspaceParam(options.Database), ) - r.Route("/{provisionerjob}", func(r chi.Router) { - r.Use(httpmw.ExtractProvisionerJobParam(options.Database)) - r.Get("/", api.provisionerJobByID) - r.Get("/logs", api.provisionerJobLogsByID) - }) - }) - - r.Route("/provisioners/daemons", func(r chi.Router) { - r.Get("/", api.provisionerDaemons) - r.Get("/serve", api.provisionerDaemonsServe) + r.Get("/", api.workspaceBuild) + r.Get("/logs", api.workspaceBuildLogs) + r.Get("/resources", api.workspaceBuildResources) }) }) r.NotFound(site.Handler(options.Logger).ServeHTTP) diff --git a/coderd/coderdtest/coderdtest.go b/coderd/coderdtest/coderdtest.go index 58dd9ceb53292..63c7c875c9cff 100644 --- a/coderd/coderdtest/coderdtest.go +++ b/coderd/coderdtest/coderdtest.go @@ -15,7 +15,6 @@ import ( "github.com/google/uuid" "github.com/moby/moby/pkg/namesgenerator" "github.com/stretchr/testify/require" - "go.opencensus.io/stats/view" "google.golang.org/api/idtoken" "google.golang.org/api/option" @@ -39,13 +38,6 @@ type Options struct { // New constructs an in-memory coderd instance and returns // the connected client. func New(t *testing.T, options *Options) *codersdk.Client { - // Stops the opencensus.io worker from leaking a goroutine. - // The worker isn't used anyways, and is an indirect dependency - // of the Google Cloud SDK. - t.Cleanup(func() { - view.Stop() - }) - if options == nil { options = &Options{} } @@ -125,7 +117,7 @@ func NewProvisionerDaemon(t *testing.T, client *codersdk.Client) io.Closer { require.NoError(t, err) }() - closer := provisionerd.New(client.ProvisionerDaemonClient, &provisionerd.Options{ + closer := provisionerd.New(client.ListenProvisionerDaemon, &provisionerd.Options{ Logger: slogtest.Make(t, nil).Named("provisionerd").Leveled(slog.LevelDebug), PollInterval: 50 * time.Millisecond, UpdateInterval: 50 * time.Millisecond, @@ -140,16 +132,16 @@ func NewProvisionerDaemon(t *testing.T, client *codersdk.Client) io.Closer { return closer } -// CreateInitialUser creates a user with preset credentials and authenticates +// CreateFirstUser 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{ +func CreateFirstUser(t *testing.T, client *codersdk.Client) coderd.CreateFirstUserResponse { + req := coderd.CreateFirstUserRequest{ Email: "testuser@coder.com", Username: "testuser", Password: "testpass", Organization: "testorg", } - _, err := client.CreateInitialUser(context.Background(), req) + resp, err := client.CreateFirstUser(context.Background(), req) require.NoError(t, err) login, err := client.LoginWithPassword(context.Background(), coderd.LoginWithPasswordRequest{ @@ -158,59 +150,101 @@ func CreateInitialUser(t *testing.T, client *codersdk.Client) coderd.CreateIniti }) require.NoError(t, err) client.SessionToken = login.SessionToken - return req + return resp } -// CreateProjectImportJob creates a project import provisioner job +// CreateAnotherUser creates and authenticates a new user. +func CreateAnotherUser(t *testing.T, client *codersdk.Client, organization string) *codersdk.Client { + req := coderd.CreateUserRequest{ + Email: namesgenerator.GetRandomName(1) + "@coder.com", + Username: randomUsername(), + Password: "testpass", + OrganizationID: organization, + } + _, err := client.CreateUser(context.Background(), req) + require.NoError(t, err) + + login, err := client.LoginWithPassword(context.Background(), coderd.LoginWithPasswordRequest{ + Email: req.Email, + Password: req.Password, + }) + require.NoError(t, err) + + other := codersdk.New(client.URL) + other.SessionToken = login.SessionToken + return other +} + +// CreateProjectVersion creates a project import provisioner job // with the responses provided. It uses the "echo" provisioner for compatibility // with testing. -func CreateProjectImportJob(t *testing.T, client *codersdk.Client, organization string, res *echo.Responses) coderd.ProvisionerJob { +func CreateProjectVersion(t *testing.T, client *codersdk.Client, organization string, res *echo.Responses) coderd.ProjectVersion { data, err := echo.Tar(res) require.NoError(t, err) - file, err := client.UploadFile(context.Background(), codersdk.ContentTypeTar, data) + file, err := client.Upload(context.Background(), codersdk.ContentTypeTar, data) require.NoError(t, err) - job, err := client.CreateProjectImportJob(context.Background(), organization, coderd.CreateProjectImportJobRequest{ + projectVersion, err := client.CreateProjectVersion(context.Background(), organization, coderd.CreateProjectVersionRequest{ StorageSource: file.Hash, StorageMethod: database.ProvisionerStorageMethodFile, Provisioner: database.ProvisionerTypeEcho, }) require.NoError(t, err) - return job + return projectVersion } // 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, job uuid.UUID) coderd.Project { +func CreateProject(t *testing.T, client *codersdk.Client, organization string, version uuid.UUID) coderd.Project { project, err := client.CreateProject(context.Background(), organization, coderd.CreateProjectRequest{ - Name: randomUsername(), - VersionImportJobID: job, + Name: randomUsername(), + VersionID: version, }) require.NoError(t, err) return project } // AwaitProjectImportJob awaits for an import job to reach completed status. -func AwaitProjectImportJob(t *testing.T, client *codersdk.Client, organization string, job uuid.UUID) coderd.ProvisionerJob { - var provisionerJob coderd.ProvisionerJob +func AwaitProjectVersionJob(t *testing.T, client *codersdk.Client, version uuid.UUID) coderd.ProjectVersion { + var projectVersion coderd.ProjectVersion + require.Eventually(t, func() bool { + var err error + projectVersion, err = client.ProjectVersion(context.Background(), version) + require.NoError(t, err) + return projectVersion.Job.CompletedAt != nil + }, 5*time.Second, 25*time.Millisecond) + return projectVersion +} + +// AwaitWorkspaceBuildJob waits for a workspace provision job to reach completed status. +func AwaitWorkspaceBuildJob(t *testing.T, client *codersdk.Client, build uuid.UUID) coderd.WorkspaceBuild { + var workspaceBuild coderd.WorkspaceBuild require.Eventually(t, func() bool { var err error - provisionerJob, err = client.ProjectImportJob(context.Background(), organization, job) + workspaceBuild, err = client.WorkspaceBuild(context.Background(), build) require.NoError(t, err) - return provisionerJob.Status.Completed() + return workspaceBuild.Job.CompletedAt != nil }, 5*time.Second, 25*time.Millisecond) - return provisionerJob + return workspaceBuild } -// AwaitWorkspaceProvisionJob awaits for a workspace provision job to reach completed status. -func AwaitWorkspaceProvisionJob(t *testing.T, client *codersdk.Client, organization string, job uuid.UUID) coderd.ProvisionerJob { - var provisionerJob coderd.ProvisionerJob +// AwaitWorkspaceAgents waits for all resources with agents to be connected. +func AwaitWorkspaceAgents(t *testing.T, client *codersdk.Client, build uuid.UUID) []coderd.WorkspaceResource { + var resources []coderd.WorkspaceResource require.Eventually(t, func() bool { var err error - provisionerJob, err = client.WorkspaceProvisionJob(context.Background(), organization, job) + resources, err = client.WorkspaceResourcesByBuild(context.Background(), build) require.NoError(t, err) - return provisionerJob.Status.Completed() + for _, resource := range resources { + if resource.Agent == nil { + continue + } + if resource.Agent.UpdatedAt.IsZero() { + return false + } + } + return true }, 5*time.Second, 25*time.Millisecond) - return provisionerJob + return resources } // CreateWorkspace creates a workspace for the user and project provided. diff --git a/coderd/coderdtest/coderdtest_test.go b/coderd/coderdtest/coderdtest_test.go index cab9fe5a9decc..6f4cb6127e25b 100644 --- a/coderd/coderdtest/coderdtest_test.go +++ b/coderd/coderdtest/coderdtest_test.go @@ -20,17 +20,18 @@ func TestMain(m *testing.M) { func TestNew(t *testing.T) { t.Parallel() client := coderdtest.New(t, nil) - user := coderdtest.CreateInitialUser(t, client) + user := coderdtest.CreateFirstUser(t, client) closer := coderdtest.NewProvisionerDaemon(t, client) - job := coderdtest.CreateProjectImportJob(t, client, user.Organization, nil) - coderdtest.AwaitProjectImportJob(t, client, user.Organization, job.ID) - project := coderdtest.CreateProject(t, client, user.Organization, job.ID) + version := coderdtest.CreateProjectVersion(t, client, user.OrganizationID, nil) + coderdtest.AwaitProjectVersionJob(t, client, version.ID) + project := coderdtest.CreateProject(t, client, user.OrganizationID, version.ID) workspace := coderdtest.CreateWorkspace(t, client, "me", project.ID) - history, err := client.CreateWorkspaceHistory(context.Background(), "me", workspace.Name, coderd.CreateWorkspaceHistoryRequest{ + build, err := client.CreateWorkspaceBuild(context.Background(), workspace.ID, coderd.CreateWorkspaceBuildRequest{ ProjectVersionID: project.ActiveVersionID, Transition: database.WorkspaceTransitionStart, }) require.NoError(t, err) - coderdtest.AwaitWorkspaceProvisionJob(t, client, user.Organization, history.ProvisionJobID) + coderdtest.AwaitWorkspaceBuildJob(t, client, build.ID) + coderdtest.AwaitWorkspaceAgents(t, client, build.ID) closer.Close() } diff --git a/coderd/files.go b/coderd/files.go index e25ccd8d749b0..2d753b44132d0 100644 --- a/coderd/files.go +++ b/coderd/files.go @@ -2,11 +2,14 @@ package coderd import ( "crypto/sha256" + "database/sql" "encoding/hex" + "errors" "fmt" "io" "net/http" + "github.com/go-chi/chi/v5" "github.com/go-chi/render" "github.com/coder/coder/database" @@ -14,11 +17,12 @@ import ( "github.com/coder/coder/httpmw" ) -type UploadFileResponse struct { +// UploadResponse contains the hash to reference the uploaded file. +type UploadResponse struct { Hash string `json:"hash"` } -func (api *api) postUpload(rw http.ResponseWriter, r *http.Request) { +func (api *api) postFile(rw http.ResponseWriter, r *http.Request) { apiKey := httpmw.APIKey(r) contentType := r.Header.Get("Content-Type") @@ -45,7 +49,7 @@ func (api *api) postUpload(rw http.ResponseWriter, r *http.Request) { if err == nil { // The file already exists! render.Status(r, http.StatusOK) - render.JSON(rw, r, UploadFileResponse{ + render.JSON(rw, r, UploadResponse{ Hash: file.Hash, }) return @@ -64,7 +68,33 @@ func (api *api) postUpload(rw http.ResponseWriter, r *http.Request) { return } render.Status(r, http.StatusCreated) - render.JSON(rw, r, UploadFileResponse{ + render.JSON(rw, r, UploadResponse{ Hash: file.Hash, }) } + +func (api *api) fileByHash(rw http.ResponseWriter, r *http.Request) { + hash := chi.URLParam(r, "hash") + if hash == "" { + httpapi.Write(rw, http.StatusBadRequest, httpapi.Response{ + Message: "hash must be provided", + }) + return + } + file, err := api.Database.GetFileByHash(r.Context(), hash) + if errors.Is(err, sql.ErrNoRows) { + httpapi.Write(rw, http.StatusNotFound, httpapi.Response{ + Message: "no file exists with that hash", + }) + return + } + if err != nil { + httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ + Message: fmt.Sprintf("get file: %s", err), + }) + return + } + rw.Header().Set("Content-Type", file.Mimetype) + rw.WriteHeader(http.StatusOK) + _, _ = rw.Write(file.Data) +} diff --git a/coderd/files_test.go b/coderd/files_test.go index ad00a6c5b656e..016774a030c88 100644 --- a/coderd/files_test.go +++ b/coderd/files_test.go @@ -2,6 +2,7 @@ package coderd_test import ( "context" + "net/http" "testing" "github.com/stretchr/testify/require" @@ -10,32 +11,57 @@ import ( "github.com/coder/coder/codersdk" ) -func TestPostUpload(t *testing.T) { +func TestPostFiles(t *testing.T) { t.Parallel() t.Run("BadContentType", func(t *testing.T) { t.Parallel() client := coderdtest.New(t, nil) - _ = coderdtest.CreateInitialUser(t, client) - _, err := client.UploadFile(context.Background(), "bad", []byte{'a'}) + _ = coderdtest.CreateFirstUser(t, client) + _, err := client.Upload(context.Background(), "bad", []byte{'a'}) require.Error(t, err) }) t.Run("Insert", func(t *testing.T) { t.Parallel() client := coderdtest.New(t, nil) - _ = coderdtest.CreateInitialUser(t, client) - _, err := client.UploadFile(context.Background(), codersdk.ContentTypeTar, make([]byte, 1024)) + _ = coderdtest.CreateFirstUser(t, client) + _, err := client.Upload(context.Background(), codersdk.ContentTypeTar, make([]byte, 1024)) require.NoError(t, err) }) t.Run("InsertAlreadyExists", func(t *testing.T) { t.Parallel() client := coderdtest.New(t, nil) - _ = coderdtest.CreateInitialUser(t, client) + _ = coderdtest.CreateFirstUser(t, client) data := make([]byte, 1024) - _, err := client.UploadFile(context.Background(), codersdk.ContentTypeTar, data) + _, err := client.Upload(context.Background(), codersdk.ContentTypeTar, data) require.NoError(t, err) - _, err = client.UploadFile(context.Background(), codersdk.ContentTypeTar, data) + _, err = client.Upload(context.Background(), codersdk.ContentTypeTar, data) require.NoError(t, err) }) } + +func TestDownload(t *testing.T) { + t.Parallel() + t.Run("NotFound", func(t *testing.T) { + t.Parallel() + client := coderdtest.New(t, nil) + _ = coderdtest.CreateFirstUser(t, client) + _, _, err := client.Download(context.Background(), "something") + var apiErr *codersdk.Error + require.ErrorAs(t, err, &apiErr) + require.Equal(t, http.StatusNotFound, apiErr.StatusCode()) + }) + + t.Run("Insert", func(t *testing.T) { + t.Parallel() + client := coderdtest.New(t, nil) + _ = coderdtest.CreateFirstUser(t, client) + resp, err := client.Upload(context.Background(), codersdk.ContentTypeTar, make([]byte, 1024)) + require.NoError(t, err) + data, contentType, err := client.Download(context.Background(), resp.Hash) + require.NoError(t, err) + require.Len(t, data, 1024) + require.Equal(t, codersdk.ContentTypeTar, contentType) + }) +} diff --git a/coderd/organizations.go b/coderd/organizations.go index 0f438274598b3..c8f3e4fc975b9 100644 --- a/coderd/organizations.go +++ b/coderd/organizations.go @@ -1,9 +1,21 @@ package coderd import ( + "database/sql" + "errors" + "fmt" + "net/http" "time" + "github.com/go-chi/chi/v5" + "github.com/go-chi/render" + "github.com/google/uuid" + "github.com/moby/moby/pkg/namesgenerator" + "golang.org/x/xerrors" + "github.com/coder/coder/database" + "github.com/coder/coder/httpapi" + "github.com/coder/coder/httpmw" ) // Organization is the JSON representation of a Coder organization. @@ -14,6 +26,317 @@ type Organization struct { UpdatedAt time.Time `json:"updated_at" validate:"required"` } +// CreateProjectVersionRequest enables callers to create a new Project Version. +type CreateProjectVersionRequest struct { + // ProjectID optionally associates a version with a project. + ProjectID *uuid.UUID `json:"project_id"` + + StorageMethod database.ProvisionerStorageMethod `json:"storage_method" validate:"oneof=file,required"` + StorageSource string `json:"storage_source" validate:"required"` + Provisioner database.ProvisionerType `json:"provisioner" validate:"oneof=terraform echo,required"` + // ParameterValues allows for additional parameters to be provided + // during the dry-run provision stage. + ParameterValues []CreateParameterRequest `json:"parameter_values"` +} + +// CreateProjectRequest provides options when creating a project. +type CreateProjectRequest struct { + Name string `json:"name" validate:"username,required"` + + // VersionID is an in-progress or completed job to use as + // an initial version of the project. + // + // This is required on creation to enable a user-flow of validating a + // project works. There is no reason the data-model cannot support + // empty projects, but it doesn't make sense for users. + VersionID uuid.UUID `json:"project_version_id" validate:"required"` +} + +func (*api) organization(rw http.ResponseWriter, r *http.Request) { + organization := httpmw.OrganizationParam(r) + render.Status(r, http.StatusOK) + render.JSON(rw, r, convertOrganization(organization)) +} + +func (api *api) provisionerDaemonsByOrganization(rw http.ResponseWriter, r *http.Request) { + daemons, err := api.Database.GetProvisionerDaemons(r.Context()) + if errors.Is(err, sql.ErrNoRows) { + err = nil + } + if err != nil { + httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ + Message: fmt.Sprintf("get provisioner daemons: %s", err), + }) + return + } + if daemons == nil { + daemons = []database.ProvisionerDaemon{} + } + render.Status(r, http.StatusOK) + render.JSON(rw, r, daemons) +} + +// Creates a new version of a project. An import job is queued to parse the storage method provided. +func (api *api) postProjectVersionsByOrganization(rw http.ResponseWriter, r *http.Request) { + apiKey := httpmw.APIKey(r) + organization := httpmw.OrganizationParam(r) + var req CreateProjectVersionRequest + if !httpapi.Read(rw, r, &req) { + return + } + if req.ProjectID != nil { + _, err := api.Database.GetProjectByID(r.Context(), *req.ProjectID) + if errors.Is(err, sql.ErrNoRows) { + httpapi.Write(rw, http.StatusNotFound, httpapi.Response{ + Message: "project does not exist", + }) + return + } + if err != nil { + httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ + Message: fmt.Sprintf("get project: %s", err), + }) + return + } + } + + file, err := api.Database.GetFileByHash(r.Context(), req.StorageSource) + if errors.Is(err, sql.ErrNoRows) { + httpapi.Write(rw, http.StatusNotFound, httpapi.Response{ + Message: "file not found", + }) + return + } + if err != nil { + httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ + Message: fmt.Sprintf("get file: %s", err), + }) + return + } + + var projectVersion database.ProjectVersion + var provisionerJob database.ProvisionerJob + err = api.Database.InTx(func(db database.Store) error { + jobID := uuid.New() + for _, parameterValue := range req.ParameterValues { + _, err = db.InsertParameterValue(r.Context(), database.InsertParameterValueParams{ + ID: uuid.New(), + Name: parameterValue.Name, + CreatedAt: database.Now(), + UpdatedAt: database.Now(), + Scope: database.ParameterScopeImportJob, + ScopeID: jobID.String(), + SourceScheme: parameterValue.SourceScheme, + SourceValue: parameterValue.SourceValue, + DestinationScheme: parameterValue.DestinationScheme, + }) + if err != nil { + return xerrors.Errorf("insert parameter value: %w", err) + } + } + + provisionerJob, err = api.Database.InsertProvisionerJob(r.Context(), database.InsertProvisionerJobParams{ + ID: jobID, + CreatedAt: database.Now(), + UpdatedAt: database.Now(), + OrganizationID: organization.ID, + InitiatorID: apiKey.UserID, + Provisioner: req.Provisioner, + StorageMethod: database.ProvisionerStorageMethodFile, + StorageSource: file.Hash, + Type: database.ProvisionerJobTypeProjectVersionImport, + Input: []byte{'{', '}'}, + }) + if err != nil { + return xerrors.Errorf("insert provisioner job: %w", err) + } + + var projectID uuid.NullUUID + if req.ProjectID != nil { + projectID = uuid.NullUUID{ + UUID: *req.ProjectID, + Valid: true, + } + } + + projectVersion, err = api.Database.InsertProjectVersion(r.Context(), database.InsertProjectVersionParams{ + ID: uuid.New(), + ProjectID: projectID, + OrganizationID: organization.ID, + CreatedAt: database.Now(), + UpdatedAt: database.Now(), + Name: namesgenerator.GetRandomName(1), + Description: "", + JobID: provisionerJob.ID, + }) + if err != nil { + return xerrors.Errorf("insert project version: %w", err) + } + return nil + }) + if err != nil { + httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ + Message: err.Error(), + }) + return + } + + render.Status(r, http.StatusCreated) + render.JSON(rw, r, convertProjectVersion(projectVersion, convertProvisionerJob(provisionerJob))) +} + +// Create a new project in an organization. +func (api *api) postProjectsByOrganization(rw http.ResponseWriter, r *http.Request) { + var createProject CreateProjectRequest + if !httpapi.Read(rw, r, &createProject) { + return + } + organization := httpmw.OrganizationParam(r) + _, err := api.Database.GetProjectByOrganizationAndName(r.Context(), database.GetProjectByOrganizationAndNameParams{ + OrganizationID: organization.ID, + Name: createProject.Name, + }) + if err == nil { + httpapi.Write(rw, http.StatusConflict, httpapi.Response{ + Message: fmt.Sprintf("project %q already exists", createProject.Name), + Errors: []httpapi.Error{{ + Field: "name", + Code: "exists", + }}, + }) + return + } + if !errors.Is(err, sql.ErrNoRows) { + httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ + Message: fmt.Sprintf("get project by name: %s", err), + }) + return + } + projectVersion, err := api.Database.GetProjectVersionByID(r.Context(), createProject.VersionID) + if errors.Is(err, sql.ErrNoRows) { + httpapi.Write(rw, http.StatusNotFound, httpapi.Response{ + Message: "project version does not exist", + }) + } + if err != nil { + httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ + Message: fmt.Sprintf("get project version by id: %s", err), + }) + return + } + importJob, err := api.Database.GetProvisionerJobByID(r.Context(), projectVersion.JobID) + if err != nil { + httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ + Message: fmt.Sprintf("get import job by id: %s", err), + }) + return + } + + var project Project + err = api.Database.InTx(func(db database.Store) error { + dbProject, err := db.InsertProject(r.Context(), database.InsertProjectParams{ + ID: uuid.New(), + CreatedAt: database.Now(), + UpdatedAt: database.Now(), + OrganizationID: organization.ID, + Name: createProject.Name, + Provisioner: importJob.Provisioner, + ActiveVersionID: projectVersion.ID, + }) + if err != nil { + return xerrors.Errorf("insert project: %s", err) + } + err = db.UpdateProjectVersionByID(r.Context(), database.UpdateProjectVersionByIDParams{ + ID: projectVersion.ID, + ProjectID: uuid.NullUUID{ + UUID: dbProject.ID, + Valid: true, + }, + }) + if err != nil { + return xerrors.Errorf("insert project version: %s", err) + } + project = convertProject(dbProject, 0) + return nil + }) + if err != nil { + httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ + Message: err.Error(), + }) + return + } + + render.Status(r, http.StatusCreated) + render.JSON(rw, r, project) +} + +func (api *api) projectsByOrganization(rw http.ResponseWriter, r *http.Request) { + organization := httpmw.OrganizationParam(r) + projects, err := api.Database.GetProjectsByOrganization(r.Context(), organization.ID) + if errors.Is(err, sql.ErrNoRows) { + err = nil + } + if err != nil { + httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ + Message: fmt.Sprintf("get projects: %s", err.Error()), + }) + return + } + projectIDs := make([]uuid.UUID, 0, len(projects)) + for _, project := range projects { + projectIDs = append(projectIDs, project.ID) + } + workspaceCounts, err := api.Database.GetWorkspaceOwnerCountsByProjectIDs(r.Context(), projectIDs) + if errors.Is(err, sql.ErrNoRows) { + err = nil + } + if err != nil { + httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ + Message: fmt.Sprintf("get workspace counts: %s", err.Error()), + }) + return + } + render.Status(r, http.StatusOK) + render.JSON(rw, r, convertProjects(projects, workspaceCounts)) +} + +func (api *api) projectByOrganizationAndName(rw http.ResponseWriter, r *http.Request) { + organization := httpmw.OrganizationParam(r) + projectName := chi.URLParam(r, "projectname") + project, err := api.Database.GetProjectByOrganizationAndName(r.Context(), database.GetProjectByOrganizationAndNameParams{ + OrganizationID: organization.ID, + Name: projectName, + }) + if errors.Is(err, sql.ErrNoRows) { + httpapi.Write(rw, http.StatusNotFound, httpapi.Response{ + Message: fmt.Sprintf("no project found by name %q in the %q organization", projectName, organization.Name), + }) + return + } + if err != nil { + httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ + Message: fmt.Sprintf("get project by organization and name: %s", err), + }) + return + } + workspaceCounts, err := api.Database.GetWorkspaceOwnerCountsByProjectIDs(r.Context(), []uuid.UUID{project.ID}) + if errors.Is(err, sql.ErrNoRows) { + err = nil + } + if err != nil { + httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ + Message: fmt.Sprintf("get workspace counts: %s", err.Error()), + }) + return + } + count := uint32(0) + if len(workspaceCounts) > 0 { + count = uint32(workspaceCounts[0].Count) + } + render.Status(r, http.StatusOK) + render.JSON(rw, r, convertProject(project, count)) +} + // convertOrganization consumes the database representation and outputs an API friendly representation. func convertOrganization(organization database.Organization) Organization { return Organization{ diff --git a/coderd/organizations_test.go b/coderd/organizations_test.go new file mode 100644 index 0000000000000..b37ac9419d14c --- /dev/null +++ b/coderd/organizations_test.go @@ -0,0 +1,179 @@ +package coderd_test + +import ( + "context" + "net/http" + "testing" + + "github.com/google/uuid" + "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" +) + +func TestProvisionerDaemonsByOrganization(t *testing.T) { + t.Parallel() + t.Run("NoAuth", func(t *testing.T) { + t.Parallel() + client := coderdtest.New(t, nil) + _, err := client.ProvisionerDaemonsByOrganization(context.Background(), "someorg") + require.Error(t, err) + }) + + t.Run("Get", func(t *testing.T) { + t.Parallel() + client := coderdtest.New(t, nil) + user := coderdtest.CreateFirstUser(t, client) + _, err := client.ProvisionerDaemonsByOrganization(context.Background(), user.OrganizationID) + require.NoError(t, err) + }) +} + +func TestPostProjectVersionsByOrganization(t *testing.T) { + t.Parallel() + t.Run("InvalidProject", func(t *testing.T) { + t.Parallel() + client := coderdtest.New(t, nil) + user := coderdtest.CreateFirstUser(t, client) + projectID := uuid.New() + _, err := client.CreateProjectVersion(context.Background(), user.OrganizationID, coderd.CreateProjectVersionRequest{ + ProjectID: &projectID, + StorageMethod: database.ProvisionerStorageMethodFile, + StorageSource: "hash", + Provisioner: database.ProvisionerTypeEcho, + }) + var apiErr *codersdk.Error + require.ErrorAs(t, err, &apiErr) + require.Equal(t, http.StatusNotFound, apiErr.StatusCode()) + }) + + t.Run("FileNotFound", func(t *testing.T) { + t.Parallel() + client := coderdtest.New(t, nil) + user := coderdtest.CreateFirstUser(t, client) + _, err := client.CreateProjectVersion(context.Background(), user.OrganizationID, coderd.CreateProjectVersionRequest{ + StorageMethod: database.ProvisionerStorageMethodFile, + StorageSource: "hash", + Provisioner: database.ProvisionerTypeEcho, + }) + var apiErr *codersdk.Error + require.ErrorAs(t, err, &apiErr) + require.Equal(t, http.StatusNotFound, apiErr.StatusCode()) + }) + + t.Run("WithParameters", func(t *testing.T) { + t.Parallel() + client := coderdtest.New(t, nil) + user := coderdtest.CreateFirstUser(t, client) + data, err := echo.Tar(&echo.Responses{ + Parse: echo.ParseComplete, + Provision: echo.ProvisionComplete, + ProvisionDryRun: echo.ProvisionComplete, + }) + require.NoError(t, err) + file, err := client.Upload(context.Background(), codersdk.ContentTypeTar, data) + require.NoError(t, err) + _, err = client.CreateProjectVersion(context.Background(), user.OrganizationID, coderd.CreateProjectVersionRequest{ + StorageMethod: database.ProvisionerStorageMethodFile, + StorageSource: file.Hash, + Provisioner: database.ProvisionerTypeEcho, + ParameterValues: []coderd.CreateParameterRequest{{ + Name: "example", + SourceValue: "value", + SourceScheme: database.ParameterSourceSchemeData, + DestinationScheme: database.ParameterDestinationSchemeProvisionerVariable, + }}, + }) + require.NoError(t, err) + }) +} + +func TestPostProjectsByOrganization(t *testing.T) { + t.Parallel() + t.Run("Create", func(t *testing.T) { + t.Parallel() + client := coderdtest.New(t, nil) + user := coderdtest.CreateFirstUser(t, client) + version := coderdtest.CreateProjectVersion(t, client, user.OrganizationID, nil) + _ = coderdtest.CreateProject(t, client, user.OrganizationID, version.ID) + }) + + t.Run("AlreadyExists", func(t *testing.T) { + t.Parallel() + client := coderdtest.New(t, nil) + user := coderdtest.CreateFirstUser(t, client) + version := coderdtest.CreateProjectVersion(t, client, user.OrganizationID, nil) + project := coderdtest.CreateProject(t, client, user.OrganizationID, version.ID) + _, err := client.CreateProject(context.Background(), user.OrganizationID, coderd.CreateProjectRequest{ + Name: project.Name, + VersionID: version.ID, + }) + var apiErr *codersdk.Error + require.ErrorAs(t, err, &apiErr) + require.Equal(t, http.StatusConflict, apiErr.StatusCode()) + }) + + t.Run("NoVersion", func(t *testing.T) { + t.Parallel() + client := coderdtest.New(t, nil) + user := coderdtest.CreateFirstUser(t, client) + _, err := client.CreateProject(context.Background(), user.OrganizationID, coderd.CreateProjectRequest{ + Name: "test", + VersionID: uuid.New(), + }) + var apiErr *codersdk.Error + require.ErrorAs(t, err, &apiErr) + require.Equal(t, http.StatusNotFound, apiErr.StatusCode()) + }) +} + +func TestProjectsByOrganization(t *testing.T) { + t.Parallel() + t.Run("ListEmpty", func(t *testing.T) { + t.Parallel() + client := coderdtest.New(t, nil) + user := coderdtest.CreateFirstUser(t, client) + projects, err := client.ProjectsByOrganization(context.Background(), user.OrganizationID) + require.NoError(t, err) + require.NotNil(t, projects) + require.Len(t, projects, 0) + }) + + t.Run("List", func(t *testing.T) { + t.Parallel() + client := coderdtest.New(t, nil) + user := coderdtest.CreateFirstUser(t, client) + version := coderdtest.CreateProjectVersion(t, client, user.OrganizationID, nil) + coderdtest.CreateProject(t, client, user.OrganizationID, version.ID) + projects, err := client.ProjectsByOrganization(context.Background(), user.OrganizationID) + require.NoError(t, err) + require.Len(t, projects, 1) + }) +} + +func TestProjectByOrganizationAndName(t *testing.T) { + t.Parallel() + t.Run("NotFound", func(t *testing.T) { + t.Parallel() + client := coderdtest.New(t, nil) + user := coderdtest.CreateFirstUser(t, client) + _, err := client.ProjectByName(context.Background(), user.OrganizationID, "something") + var apiErr *codersdk.Error + require.ErrorAs(t, err, &apiErr) + require.Equal(t, http.StatusNotFound, apiErr.StatusCode()) + }) + + t.Run("Found", func(t *testing.T) { + t.Parallel() + client := coderdtest.New(t, nil) + user := coderdtest.CreateFirstUser(t, client) + version := coderdtest.CreateProjectVersion(t, client, user.OrganizationID, nil) + project := coderdtest.CreateProject(t, client, user.OrganizationID, version.ID) + _, err := client.ProjectByName(context.Background(), user.OrganizationID, project.Name) + require.NoError(t, err) + }) +} diff --git a/coderd/parameters.go b/coderd/parameters.go new file mode 100644 index 0000000000000..5b0d8e7efde9b --- /dev/null +++ b/coderd/parameters.go @@ -0,0 +1,189 @@ +package coderd + +import ( + "database/sql" + "errors" + "fmt" + "net/http" + "time" + + "github.com/go-chi/chi/v5" + "github.com/go-chi/render" + "github.com/google/uuid" + + "github.com/coder/coder/database" + "github.com/coder/coder/httpapi" +) + +type ParameterScope string + +const ( + ParameterOrganization ParameterScope = "organization" + ParameterProject ParameterScope = "project" + ParameterUser ParameterScope = "user" + ParameterWorkspace ParameterScope = "workspace" +) + +// Parameter represents a set value for the scope. +type Parameter struct { + ID uuid.UUID `db:"id" json:"id"` + CreatedAt time.Time `db:"created_at" json:"created_at"` + UpdatedAt time.Time `db:"updated_at" json:"updated_at"` + Scope ParameterScope `db:"scope" json:"scope"` + ScopeID string `db:"scope_id" json:"scope_id"` + Name string `db:"name" json:"name"` + SourceScheme database.ParameterSourceScheme `db:"source_scheme" json:"source_scheme"` + DestinationScheme database.ParameterDestinationScheme `db:"destination_scheme" json:"destination_scheme"` +} + +// CreateParameterRequest is used to create a new parameter value for a scope. +type CreateParameterRequest struct { + Name string `json:"name" validate:"required"` + SourceValue string `json:"source_value" validate:"required"` + SourceScheme database.ParameterSourceScheme `json:"source_scheme" validate:"oneof=data,required"` + DestinationScheme database.ParameterDestinationScheme `json:"destination_scheme" validate:"oneof=environment_variable provisioner_variable,required"` +} + +func (api *api) postParameter(rw http.ResponseWriter, r *http.Request) { + var createRequest CreateParameterRequest + if !httpapi.Read(rw, r, &createRequest) { + return + } + scope, scopeID, valid := readScopeAndID(rw, r) + if !valid { + return + } + _, err := api.Database.GetParameterValueByScopeAndName(r.Context(), database.GetParameterValueByScopeAndNameParams{ + Scope: scope, + ScopeID: scopeID, + Name: createRequest.Name, + }) + if err == nil { + httpapi.Write(rw, http.StatusConflict, httpapi.Response{ + Message: fmt.Sprintf("a parameter already exists in scope %q with name %q", scope, createRequest.Name), + }) + return + } + if !errors.Is(err, sql.ErrNoRows) { + httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ + Message: fmt.Sprintf("get parameter value: %s", err), + }) + return + } + parameterValue, err := api.Database.InsertParameterValue(r.Context(), database.InsertParameterValueParams{ + ID: uuid.New(), + Name: createRequest.Name, + CreatedAt: database.Now(), + UpdatedAt: database.Now(), + Scope: scope, + ScopeID: scopeID, + SourceScheme: createRequest.SourceScheme, + SourceValue: createRequest.SourceValue, + DestinationScheme: createRequest.DestinationScheme, + }) + if err != nil { + httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ + Message: fmt.Sprintf("insert parameter value: %s", err), + }) + return + } + + render.Status(r, http.StatusCreated) + render.JSON(rw, r, convertParameterValue(parameterValue)) +} + +func (api *api) parameters(rw http.ResponseWriter, r *http.Request) { + scope, scopeID, valid := readScopeAndID(rw, r) + if !valid { + return + } + parameterValues, err := api.Database.GetParameterValuesByScope(r.Context(), database.GetParameterValuesByScopeParams{ + Scope: scope, + ScopeID: scopeID, + }) + if errors.Is(err, sql.ErrNoRows) { + err = nil + } + if err != nil { + httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ + Message: fmt.Sprintf("get parameter values by scope: %s", err), + }) + return + } + apiParameterValues := make([]Parameter, 0, len(parameterValues)) + for _, parameterValue := range parameterValues { + apiParameterValues = append(apiParameterValues, convertParameterValue(parameterValue)) + } + + render.Status(r, http.StatusOK) + render.JSON(rw, r, apiParameterValues) +} + +func (api *api) deleteParameter(rw http.ResponseWriter, r *http.Request) { + scope, scopeID, valid := readScopeAndID(rw, r) + if !valid { + return + } + name := chi.URLParam(r, "name") + parameterValue, err := api.Database.GetParameterValueByScopeAndName(r.Context(), database.GetParameterValueByScopeAndNameParams{ + Scope: scope, + ScopeID: scopeID, + Name: name, + }) + if errors.Is(err, sql.ErrNoRows) { + httpapi.Write(rw, http.StatusNotFound, httpapi.Response{ + Message: fmt.Sprintf("parameter doesn't exist in the provided scope with name %q", name), + }) + return + } + if err != nil { + httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ + Message: fmt.Sprintf("get parameter value: %s", err), + }) + return + } + err = api.Database.DeleteParameterValueByID(r.Context(), parameterValue.ID) + if err != nil { + httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ + Message: fmt.Sprintf("delete parameter: %s", err), + }) + return + } + httpapi.Write(rw, http.StatusOK, httpapi.Response{ + Message: "parameter deleted", + }) +} + +func convertParameterValue(parameterValue database.ParameterValue) Parameter { + return Parameter{ + ID: parameterValue.ID, + CreatedAt: parameterValue.CreatedAt, + UpdatedAt: parameterValue.UpdatedAt, + Scope: ParameterScope(parameterValue.Scope), + ScopeID: parameterValue.ScopeID, + Name: parameterValue.Name, + SourceScheme: parameterValue.SourceScheme, + DestinationScheme: parameterValue.DestinationScheme, + } +} + +func readScopeAndID(rw http.ResponseWriter, r *http.Request) (database.ParameterScope, string, bool) { + var scope database.ParameterScope + switch chi.URLParam(r, "scope") { + case string(ParameterOrganization): + scope = database.ParameterScopeOrganization + case string(ParameterProject): + scope = database.ParameterScopeProject + case string(ParameterUser): + scope = database.ParameterScopeUser + case string(ParameterWorkspace): + scope = database.ParameterScopeWorkspace + default: + httpapi.Write(rw, http.StatusBadRequest, httpapi.Response{ + Message: fmt.Sprintf("invalid scope %q", scope), + }) + return scope, "", false + } + + return scope, chi.URLParam(r, "id"), true +} diff --git a/coderd/parameters_test.go b/coderd/parameters_test.go new file mode 100644 index 0000000000000..06fb5757739bc --- /dev/null +++ b/coderd/parameters_test.go @@ -0,0 +1,121 @@ +package coderd_test + +import ( + "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" +) + +func TestPostParameter(t *testing.T) { + t.Parallel() + t.Run("BadScope", func(t *testing.T) { + t.Parallel() + client := coderdtest.New(t, nil) + user := coderdtest.CreateFirstUser(t, client) + _, err := client.CreateParameter(context.Background(), coderd.ParameterScope("something"), user.OrganizationID, coderd.CreateParameterRequest{ + Name: "example", + SourceValue: "tomato", + SourceScheme: database.ParameterSourceSchemeData, + DestinationScheme: database.ParameterDestinationSchemeProvisionerVariable, + }) + var apiErr *codersdk.Error + require.ErrorAs(t, err, &apiErr) + require.Equal(t, http.StatusBadRequest, apiErr.StatusCode()) + }) + + t.Run("Create", func(t *testing.T) { + t.Parallel() + client := coderdtest.New(t, nil) + user := coderdtest.CreateFirstUser(t, client) + _, err := client.CreateParameter(context.Background(), coderd.ParameterOrganization, user.OrganizationID, coderd.CreateParameterRequest{ + Name: "example", + SourceValue: "tomato", + SourceScheme: database.ParameterSourceSchemeData, + DestinationScheme: database.ParameterDestinationSchemeProvisionerVariable, + }) + require.NoError(t, err) + }) + + t.Run("AlreadyExists", func(t *testing.T) { + t.Parallel() + client := coderdtest.New(t, nil) + user := coderdtest.CreateFirstUser(t, client) + _, err := client.CreateParameter(context.Background(), coderd.ParameterOrganization, user.OrganizationID, coderd.CreateParameterRequest{ + Name: "example", + SourceValue: "tomato", + SourceScheme: database.ParameterSourceSchemeData, + DestinationScheme: database.ParameterDestinationSchemeProvisionerVariable, + }) + require.NoError(t, err) + + _, err = client.CreateParameter(context.Background(), coderd.ParameterOrganization, user.OrganizationID, coderd.CreateParameterRequest{ + Name: "example", + SourceValue: "tomato", + SourceScheme: database.ParameterSourceSchemeData, + DestinationScheme: database.ParameterDestinationSchemeProvisionerVariable, + }) + var apiErr *codersdk.Error + require.ErrorAs(t, err, &apiErr) + require.Equal(t, http.StatusConflict, apiErr.StatusCode()) + }) +} + +func TestParameters(t *testing.T) { + t.Parallel() + t.Run("ListEmpty", func(t *testing.T) { + t.Parallel() + client := coderdtest.New(t, nil) + user := coderdtest.CreateFirstUser(t, client) + _, err := client.Parameters(context.Background(), coderd.ParameterOrganization, user.OrganizationID) + require.NoError(t, err) + }) + t.Run("List", func(t *testing.T) { + t.Parallel() + client := coderdtest.New(t, nil) + user := coderdtest.CreateFirstUser(t, client) + _, err := client.CreateParameter(context.Background(), coderd.ParameterOrganization, user.OrganizationID, coderd.CreateParameterRequest{ + Name: "example", + SourceValue: "tomato", + SourceScheme: database.ParameterSourceSchemeData, + DestinationScheme: database.ParameterDestinationSchemeProvisionerVariable, + }) + require.NoError(t, err) + params, err := client.Parameters(context.Background(), coderd.ParameterOrganization, user.OrganizationID) + require.NoError(t, err) + require.Len(t, params, 1) + }) +} + +func TestDeleteParameter(t *testing.T) { + t.Parallel() + t.Run("NotExist", func(t *testing.T) { + t.Parallel() + client := coderdtest.New(t, nil) + user := coderdtest.CreateFirstUser(t, client) + err := client.DeleteParameter(context.Background(), coderd.ParameterOrganization, user.OrganizationID, "something") + var apiErr *codersdk.Error + require.ErrorAs(t, err, &apiErr) + require.Equal(t, http.StatusNotFound, apiErr.StatusCode()) + }) + t.Run("Delete", func(t *testing.T) { + t.Parallel() + client := coderdtest.New(t, nil) + user := coderdtest.CreateFirstUser(t, client) + param, err := client.CreateParameter(context.Background(), coderd.ParameterOrganization, user.OrganizationID, coderd.CreateParameterRequest{ + Name: "example", + SourceValue: "tomato", + SourceScheme: database.ParameterSourceSchemeData, + DestinationScheme: database.ParameterDestinationSchemeProvisionerVariable, + }) + require.NoError(t, err) + err = client.DeleteParameter(context.Background(), coderd.ParameterOrganization, user.OrganizationID, param.Name) + require.NoError(t, err) + }) +} diff --git a/coderd/projectimport.go b/coderd/projectimport.go deleted file mode 100644 index 5ece718c99a90..0000000000000 --- a/coderd/projectimport.go +++ /dev/null @@ -1,182 +0,0 @@ -package coderd - -import ( - "database/sql" - "errors" - "fmt" - "net/http" - - "github.com/go-chi/render" - "github.com/google/uuid" - - "github.com/coder/coder/coderd/parameter" - "github.com/coder/coder/database" - "github.com/coder/coder/httpapi" - "github.com/coder/coder/httpmw" -) - -// ParameterSchema represents a parameter parsed from project version source. -type ParameterSchema database.ParameterSchema - -// ComputedParameterValue represents a computed parameter value. -type ComputedParameterValue parameter.ComputedValue - -// CreateProjectImportJobRequest provides options to create a project import job. -type CreateProjectImportJobRequest struct { - StorageMethod database.ProvisionerStorageMethod `json:"storage_method" validate:"oneof=file,required"` - StorageSource string `json:"storage_source" validate:"required"` - Provisioner database.ProvisionerType `json:"provisioner" validate:"oneof=terraform echo,required"` - // ParameterValues allows for additional parameters to be provided - // during the dry-run provision stage. - ParameterValues []CreateParameterValueRequest `json:"parameter_values"` -} - -// Create a new project import job! -func (api *api) postProjectImportByOrganization(rw http.ResponseWriter, r *http.Request) { - apiKey := httpmw.APIKey(r) - organization := httpmw.OrganizationParam(r) - var req CreateProjectImportJobRequest - if !httpapi.Read(rw, r, &req) { - return - } - file, err := api.Database.GetFileByHash(r.Context(), req.StorageSource) - if errors.Is(err, sql.ErrNoRows) { - httpapi.Write(rw, http.StatusBadRequest, httpapi.Response{ - Message: "file not found", - }) - return - } - if err != nil { - httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ - Message: fmt.Sprintf("get file: %s", err), - }) - return - } - - jobID := uuid.New() - for _, parameterValue := range req.ParameterValues { - _, err = api.Database.InsertParameterValue(r.Context(), database.InsertParameterValueParams{ - ID: uuid.New(), - Name: parameterValue.Name, - CreatedAt: database.Now(), - UpdatedAt: database.Now(), - Scope: database.ParameterScopeImportJob, - ScopeID: jobID.String(), - SourceScheme: parameterValue.SourceScheme, - SourceValue: parameterValue.SourceValue, - DestinationScheme: parameterValue.DestinationScheme, - }) - if err != nil { - httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ - Message: fmt.Sprintf("insert parameter value: %s", err), - }) - return - } - } - - job, err := api.Database.InsertProvisionerJob(r.Context(), database.InsertProvisionerJobParams{ - ID: jobID, - CreatedAt: database.Now(), - UpdatedAt: database.Now(), - OrganizationID: organization.ID, - InitiatorID: apiKey.UserID, - Provisioner: req.Provisioner, - StorageMethod: database.ProvisionerStorageMethodFile, - StorageSource: file.Hash, - Type: database.ProvisionerJobTypeProjectVersionImport, - Input: []byte{'{', '}'}, - }) - if err != nil { - httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ - Message: fmt.Sprintf("insert provisioner job: %s", err), - }) - return - } - - render.Status(r, http.StatusCreated) - render.JSON(rw, r, convertProvisionerJob(job)) -} - -// Returns imported parameter schemas from a completed job! -func (api *api) projectImportJobSchemasByID(rw http.ResponseWriter, r *http.Request) { - job := httpmw.ProvisionerJobParam(r) - if !convertProvisionerJob(job).Status.Completed() { - httpapi.Write(rw, http.StatusPreconditionFailed, httpapi.Response{ - Message: "Job hasn't completed!", - }) - return - } - - schemas, err := api.Database.GetParameterSchemasByJobID(r.Context(), job.ID) - if errors.Is(err, sql.ErrNoRows) { - err = nil - } - if err != nil { - httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ - Message: fmt.Sprintf("list parameter schemas: %s", err), - }) - return - } - if schemas == nil { - schemas = []database.ParameterSchema{} - } - render.Status(r, http.StatusOK) - render.JSON(rw, r, schemas) -} - -// Returns computed parameters for an import job by ID. -func (api *api) projectImportJobParametersByID(rw http.ResponseWriter, r *http.Request) { - apiKey := httpmw.APIKey(r) - job := httpmw.ProvisionerJobParam(r) - if !convertProvisionerJob(job).Status.Completed() { - httpapi.Write(rw, http.StatusPreconditionFailed, httpapi.Response{ - Message: "Job hasn't completed!", - }) - return - } - values, err := parameter.Compute(r.Context(), api.Database, parameter.ComputeScope{ - ProjectImportJobID: job.ID, - OrganizationID: job.OrganizationID, - UserID: apiKey.UserID, - }, ¶meter.ComputeOptions{ - // We *never* want to send the client secret parameter values. - HideRedisplayValues: true, - }) - if err != nil { - httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ - Message: fmt.Sprintf("compute values: %s", err), - }) - return - } - if values == nil { - values = []parameter.ComputedValue{} - } - render.Status(r, http.StatusOK) - render.JSON(rw, r, values) -} - -// Returns resources for an import job by ID. -func (api *api) projectImportJobResourcesByID(rw http.ResponseWriter, r *http.Request) { - job := httpmw.ProvisionerJobParam(r) - if !convertProvisionerJob(job).Status.Completed() { - httpapi.Write(rw, http.StatusPreconditionFailed, httpapi.Response{ - Message: "Job hasn't completed!", - }) - return - } - resources, err := api.Database.GetProvisionerJobResourcesByJobID(r.Context(), job.ID) - if errors.Is(err, sql.ErrNoRows) { - err = nil - } - if err != nil { - httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ - Message: fmt.Sprintf("get project import job resources: %s", err), - }) - return - } - if resources == nil { - resources = []database.ProvisionerJobResource{} - } - render.Status(r, http.StatusOK) - render.JSON(rw, r, resources) -} diff --git a/coderd/projectimport_test.go b/coderd/projectimport_test.go deleted file mode 100644 index e253f1ccdc929..0000000000000 --- a/coderd/projectimport_test.go +++ /dev/null @@ -1,163 +0,0 @@ -package coderd_test - -import ( - "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 TestPostProjectImportByOrganization(t *testing.T) { - t.Parallel() - t.Run("FileNotFound", func(t *testing.T) { - t.Parallel() - client := coderdtest.New(t, nil) - user := coderdtest.CreateInitialUser(t, client) - _, err := client.CreateProjectImportJob(context.Background(), user.Organization, coderd.CreateProjectImportJobRequest{ - StorageMethod: database.ProvisionerStorageMethodFile, - StorageSource: "bananas", - Provisioner: database.ProvisionerTypeEcho, - }) - require.Error(t, err) - }) - t.Run("Create", func(t *testing.T) { - t.Parallel() - client := coderdtest.New(t, nil) - user := coderdtest.CreateInitialUser(t, client) - _ = coderdtest.CreateProjectImportJob(t, client, user.Organization, nil) - }) -} - -func TestProjectImportJobSchemasByID(t *testing.T) { - t.Parallel() - t.Run("ListRunning", func(t *testing.T) { - t.Parallel() - client := coderdtest.New(t, nil) - user := coderdtest.CreateInitialUser(t, client) - job := coderdtest.CreateProjectImportJob(t, client, user.Organization, nil) - _, err := client.ProjectImportJobSchemas(context.Background(), user.Organization, job.ID) - 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, nil) - user := coderdtest.CreateInitialUser(t, client) - coderdtest.NewProvisionerDaemon(t, client) - job := coderdtest.CreateProjectImportJob(t, client, user.Organization, &echo.Responses{ - Parse: []*proto.Parse_Response{{ - Type: &proto.Parse_Response_Complete{ - Complete: &proto.Parse_Complete{ - ParameterSchemas: []*proto.ParameterSchema{{ - Name: "example", - DefaultDestination: &proto.ParameterDestination{ - Scheme: proto.ParameterDestination_PROVISIONER_VARIABLE, - }, - }}, - }, - }, - }}, - Provision: echo.ProvisionComplete, - }) - coderdtest.AwaitProjectImportJob(t, client, user.Organization, job.ID) - schemas, err := client.ProjectImportJobSchemas(context.Background(), user.Organization, job.ID) - require.NoError(t, err) - require.NotNil(t, schemas) - require.Len(t, schemas, 1) - }) -} - -func TestProjectImportJobParametersByID(t *testing.T) { - t.Parallel() - t.Run("ListRunning", func(t *testing.T) { - t.Parallel() - client := coderdtest.New(t, nil) - user := coderdtest.CreateInitialUser(t, client) - job := coderdtest.CreateProjectImportJob(t, client, user.Organization, nil) - _, err := client.ProjectImportJobSchemas(context.Background(), user.Organization, job.ID) - 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, nil) - user := coderdtest.CreateInitialUser(t, client) - coderdtest.NewProvisionerDaemon(t, client) - job := coderdtest.CreateProjectImportJob(t, client, user.Organization, &echo.Responses{ - Parse: []*proto.Parse_Response{{ - Type: &proto.Parse_Response_Complete{ - Complete: &proto.Parse_Complete{ - ParameterSchemas: []*proto.ParameterSchema{{ - Name: "example", - RedisplayValue: true, - DefaultSource: &proto.ParameterSource{ - Scheme: proto.ParameterSource_DATA, - Value: "hello", - }, - DefaultDestination: &proto.ParameterDestination{ - Scheme: proto.ParameterDestination_PROVISIONER_VARIABLE, - }, - }}, - }, - }, - }}, - Provision: echo.ProvisionComplete, - }) - coderdtest.AwaitProjectImportJob(t, client, user.Organization, job.ID) - params, err := client.ProjectImportJobParameters(context.Background(), user.Organization, job.ID) - require.NoError(t, err) - require.NotNil(t, params) - require.Len(t, params, 1) - require.Equal(t, "hello", params[0].SourceValue) - }) -} - -func TestProjectImportJobResourcesByID(t *testing.T) { - t.Parallel() - t.Run("ListRunning", func(t *testing.T) { - t.Parallel() - client := coderdtest.New(t, nil) - user := coderdtest.CreateInitialUser(t, client) - job := coderdtest.CreateProjectImportJob(t, client, user.Organization, nil) - _, err := client.ProjectImportJobResources(context.Background(), user.Organization, job.ID) - 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, nil) - user := coderdtest.CreateInitialUser(t, client) - coderdtest.NewProvisionerDaemon(t, client) - job := coderdtest.CreateProjectImportJob(t, client, user.Organization, &echo.Responses{ - Parse: echo.ParseComplete, - Provision: []*proto.Provision_Response{{ - Type: &proto.Provision_Response_Complete{ - Complete: &proto.Provision_Complete{ - Resources: []*proto.Resource{{ - Name: "some", - Type: "example", - }}, - }, - }, - }}, - }) - coderdtest.AwaitProjectImportJob(t, client, user.Organization, job.ID) - resources, err := client.ProjectImportJobResources(context.Background(), user.Organization, job.ID) - require.NoError(t, err) - require.NotNil(t, resources) - require.Len(t, resources, 2) - require.Equal(t, "some", resources[0].Name) - require.Equal(t, "example", resources[0].Type) - }) -} diff --git a/coderd/projects.go b/coderd/projects.go index b14018800b4e3..9369ed1fec203 100644 --- a/coderd/projects.go +++ b/coderd/projects.go @@ -7,27 +7,15 @@ import ( "net/http" "time" + "github.com/go-chi/chi/v5" "github.com/go-chi/render" "github.com/google/uuid" - "github.com/moby/moby/pkg/namesgenerator" - "golang.org/x/xerrors" "github.com/coder/coder/database" "github.com/coder/coder/httpapi" "github.com/coder/coder/httpmw" ) -// ParameterValue represents a set value for the scope. -type ParameterValue database.ParameterValue - -// CreateParameterValueRequest is used to create a new parameter value for a scope. -type CreateParameterValueRequest struct { - Name string `json:"name" validate:"required"` - SourceValue string `json:"source_value" validate:"required"` - SourceScheme database.ParameterSourceScheme `json:"source_scheme" validate:"oneof=data,required"` - DestinationScheme database.ParameterDestinationScheme `json:"destination_scheme" validate:"oneof=environment_variable provisioner_variable,required"` -} - // Project is the JSON representation of a Coder project. // This type matches the database object for now, but is // abstracted for ease of change later on. @@ -42,48 +30,10 @@ type Project struct { WorkspaceOwnerCount uint32 `json:"workspace_owner_count"` } -// CreateProjectRequest enables callers to create a new Project. -type CreateProjectRequest struct { - Name string `json:"name" validate:"username,required"` - - // VersionImportJobID is an in-progress or completed job to use as - // an initial version of the project. - // - // This is required on creation to enable a user-flow of validating - // the project works. There is no reason the data-model cannot support - // empty projects, but it doesn't make sense for users. - VersionImportJobID uuid.UUID `json:"import_job_id" validate:"required"` -} - -// Lists all projects the authenticated user has access to. -func (api *api) projects(rw http.ResponseWriter, r *http.Request) { - apiKey := httpmw.APIKey(r) - organizations, err := api.Database.GetOrganizationsByUserID(r.Context(), apiKey.UserID) - if err != nil { - httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ - Message: fmt.Sprintf("get organizations: %s", err.Error()), - }) - return - } - organizationIDs := make([]string, 0, len(organizations)) - for _, organization := range organizations { - organizationIDs = append(organizationIDs, organization.ID) - } - projects, err := api.Database.GetProjectsByOrganizationIDs(r.Context(), organizationIDs) - if errors.Is(err, sql.ErrNoRows) { - err = nil - } - if err != nil { - httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ - Message: fmt.Sprintf("get projects: %s", err.Error()), - }) - return - } - projectIDs := make([]uuid.UUID, 0, len(projects)) - for _, project := range projects { - projectIDs = append(projectIDs, project.ID) - } - workspaceCounts, err := api.Database.GetWorkspaceOwnerCountsByProjectIDs(r.Context(), projectIDs) +// Returns a single project. +func (api *api) project(rw http.ResponseWriter, r *http.Request) { + project := httpmw.ProjectParam(r) + workspaceCounts, err := api.Database.GetWorkspaceOwnerCountsByProjectIDs(r.Context(), []uuid.UUID{project.ID}) if errors.Is(err, sql.ErrNoRows) { err = nil } @@ -93,184 +43,91 @@ func (api *api) projects(rw http.ResponseWriter, r *http.Request) { }) return } + count := uint32(0) + if len(workspaceCounts) > 0 { + count = uint32(workspaceCounts[0].Count) + } + render.Status(r, http.StatusOK) - render.JSON(rw, r, convertProjects(projects, workspaceCounts)) + render.JSON(rw, r, convertProject(project, count)) } -// Lists all projects in an organization. -func (api *api) projectsByOrganization(rw http.ResponseWriter, r *http.Request) { - organization := httpmw.OrganizationParam(r) - projects, err := api.Database.GetProjectsByOrganizationIDs(r.Context(), []string{organization.ID}) +func (api *api) projectVersionsByProject(rw http.ResponseWriter, r *http.Request) { + project := httpmw.ProjectParam(r) + + versions, err := api.Database.GetProjectVersionsByProjectID(r.Context(), project.ID) if errors.Is(err, sql.ErrNoRows) { err = nil } if err != nil { httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ - Message: fmt.Sprintf("get projects: %s", err.Error()), + Message: fmt.Sprintf("get project version: %s", err), }) return } - projectIDs := make([]uuid.UUID, 0, len(projects)) - for _, project := range projects { - projectIDs = append(projectIDs, project.ID) - } - workspaceCounts, err := api.Database.GetWorkspaceOwnerCountsByProjectIDs(r.Context(), projectIDs) - if errors.Is(err, sql.ErrNoRows) { - err = nil + jobIDs := make([]uuid.UUID, 0, len(versions)) + for _, version := range versions { + jobIDs = append(jobIDs, version.JobID) } + jobs, err := api.Database.GetProvisionerJobsByIDs(r.Context(), jobIDs) if err != nil { httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ - Message: fmt.Sprintf("get workspace counts: %s", err.Error()), + Message: fmt.Sprintf("get jobs: %s", err), }) return } - render.Status(r, http.StatusOK) - render.JSON(rw, r, convertProjects(projects, workspaceCounts)) -} - -// Create a new project in an organization. -func (api *api) postProjectsByOrganization(rw http.ResponseWriter, r *http.Request) { - var createProject CreateProjectRequest - if !httpapi.Read(rw, r, &createProject) { - return - } - organization := httpmw.OrganizationParam(r) - _, err := api.Database.GetProjectByOrganizationAndName(r.Context(), database.GetProjectByOrganizationAndNameParams{ - OrganizationID: organization.ID, - Name: createProject.Name, - }) - if err == nil { - httpapi.Write(rw, http.StatusConflict, httpapi.Response{ - Message: fmt.Sprintf("project %q already exists", createProject.Name), - Errors: []httpapi.Error{{ - Field: "name", - Code: "exists", - }}, - }) - return - } - if !errors.Is(err, sql.ErrNoRows) { - httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ - Message: fmt.Sprintf("get project by name: %s", err), - }) - return - } - importJob, err := api.Database.GetProvisionerJobByID(r.Context(), createProject.VersionImportJobID) - if errors.Is(err, sql.ErrNoRows) { - httpapi.Write(rw, http.StatusNotFound, httpapi.Response{ - Message: "import job does not exist", - }) - } - if err != nil { - httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ - Message: fmt.Sprintf("get import job by id: %s", err), - }) - return + jobByID := map[string]database.ProvisionerJob{} + for _, job := range jobs { + jobByID[job.ID.String()] = job } - var project Project - err = api.Database.InTx(func(db database.Store) error { - projectVersionID := uuid.New() - dbProject, err := db.InsertProject(r.Context(), database.InsertProjectParams{ - ID: uuid.New(), - CreatedAt: database.Now(), - UpdatedAt: database.Now(), - OrganizationID: organization.ID, - Name: createProject.Name, - Provisioner: importJob.Provisioner, - ActiveVersionID: projectVersionID, - }) - if err != nil { - return xerrors.Errorf("insert project: %s", err) - } - _, err = db.InsertProjectVersion(r.Context(), database.InsertProjectVersionParams{ - ID: projectVersionID, - ProjectID: dbProject.ID, - CreatedAt: database.Now(), - UpdatedAt: database.Now(), - Name: namesgenerator.GetRandomName(1), - ImportJobID: importJob.ID, - }) - if err != nil { - return xerrors.Errorf("insert project version: %s", err) + apiVersion := make([]ProjectVersion, 0) + for _, version := range versions { + job, exists := jobByID[version.JobID.String()] + if !exists { + httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ + Message: fmt.Sprintf("job %q doesn't exist for version %q", version.JobID, version.ID), + }) + return } - project = convertProject(dbProject, 0) - return nil - }) - if err != nil { - httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ - Message: err.Error(), - }) - return + apiVersion = append(apiVersion, convertProjectVersion(version, convertProvisionerJob(job))) } - - render.Status(r, http.StatusCreated) - render.JSON(rw, r, project) -} - -// Returns a single project. -func (*api) projectByOrganization(rw http.ResponseWriter, r *http.Request) { - project := httpmw.ProjectParam(r) - render.Status(r, http.StatusOK) - render.JSON(rw, r, project) + render.JSON(rw, r, apiVersion) } -// Creates parameters for a project. -// This should validate the calling user has permissions! -func (api *api) postParametersByProject(rw http.ResponseWriter, r *http.Request) { +func (api *api) projectVersionByName(rw http.ResponseWriter, r *http.Request) { project := httpmw.ProjectParam(r) - var createRequest CreateParameterValueRequest - if !httpapi.Read(rw, r, &createRequest) { + projectVersionName := chi.URLParam(r, "projectversionname") + projectVersion, err := api.Database.GetProjectVersionByProjectIDAndName(r.Context(), database.GetProjectVersionByProjectIDAndNameParams{ + ProjectID: uuid.NullUUID{ + UUID: project.ID, + Valid: true, + }, + Name: projectVersionName, + }) + if errors.Is(err, sql.ErrNoRows) { + httpapi.Write(rw, http.StatusNotFound, httpapi.Response{ + Message: fmt.Sprintf("no project version found by name %q", projectVersionName), + }) return } - parameterValue, err := api.Database.InsertParameterValue(r.Context(), database.InsertParameterValueParams{ - ID: uuid.New(), - Name: createRequest.Name, - CreatedAt: database.Now(), - UpdatedAt: database.Now(), - Scope: database.ParameterScopeProject, - ScopeID: project.ID.String(), - SourceScheme: createRequest.SourceScheme, - SourceValue: createRequest.SourceValue, - DestinationScheme: createRequest.DestinationScheme, - }) if err != nil { httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ - Message: fmt.Sprintf("insert parameter value: %s", err), + Message: fmt.Sprintf("get project version by name: %s", err), }) return } - - render.Status(r, http.StatusCreated) - render.JSON(rw, r, parameterValue) -} - -// Lists parameters for a project. -func (api *api) parametersByProject(rw http.ResponseWriter, r *http.Request) { - project := httpmw.ProjectParam(r) - parameterValues, err := api.Database.GetParameterValuesByScope(r.Context(), database.GetParameterValuesByScopeParams{ - Scope: database.ParameterScopeProject, - ScopeID: project.ID.String(), - }) - if errors.Is(err, sql.ErrNoRows) { - err = nil - parameterValues = []database.ParameterValue{} - } + job, err := api.Database.GetProvisionerJobByID(r.Context(), projectVersion.JobID) if err != nil { httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ - Message: fmt.Sprintf("get parameter values: %s", err), + Message: fmt.Sprintf("get provisioner job: %s", err), }) return } - apiParameterValues := make([]ParameterValue, 0, len(parameterValues)) - for _, parameterValue := range parameterValues { - apiParameterValues = append(apiParameterValues, convertParameterValue(parameterValue)) - } - render.Status(r, http.StatusOK) - render.JSON(rw, r, apiParameterValues) + render.JSON(rw, r, convertProjectVersion(projectVersion, convertProvisionerJob(job))) } func convertProjects(projects []database.Project, workspaceCounts []database.GetWorkspaceOwnerCountsByProjectIDsRow) []Project { @@ -304,8 +161,3 @@ func convertProject(project database.Project, workspaceOwnerCount uint32) Projec WorkspaceOwnerCount: workspaceOwnerCount, } } - -func convertParameterValue(parameterValue database.ParameterValue) ParameterValue { - parameterValue.SourceValue = "" - return ParameterValue(parameterValue) -} diff --git a/coderd/projects_test.go b/coderd/projects_test.go index db1daafea4979..7d82dfbf4b1f4 100644 --- a/coderd/projects_test.go +++ b/coderd/projects_test.go @@ -7,163 +7,59 @@ import ( "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" ) -func TestProjects(t *testing.T) { +func TestProject(t *testing.T) { t.Parallel() - t.Run("ListEmpty", func(t *testing.T) { - t.Parallel() - client := coderdtest.New(t, nil) - _ = coderdtest.CreateInitialUser(t, client) - projects, err := client.Projects(context.Background(), "") - require.NoError(t, err) - require.NotNil(t, projects) - require.Len(t, projects, 0) - }) - - t.Run("List", func(t *testing.T) { - t.Parallel() - client := coderdtest.New(t, nil) - user := coderdtest.CreateInitialUser(t, client) - job := coderdtest.CreateProjectImportJob(t, client, user.Organization, nil) - _ = coderdtest.CreateProject(t, client, user.Organization, job.ID) - projects, err := client.Projects(context.Background(), "") - require.NoError(t, err) - require.Len(t, projects, 1) - }) - - t.Run("ListWorkspaceOwnerCount", func(t *testing.T) { + t.Run("Get", func(t *testing.T) { t.Parallel() client := coderdtest.New(t, nil) - user := coderdtest.CreateInitialUser(t, client) - coderdtest.NewProvisionerDaemon(t, client) - job := coderdtest.CreateProjectImportJob(t, client, user.Organization, nil) - coderdtest.AwaitProjectImportJob(t, client, user.Organization, job.ID) - project := coderdtest.CreateProject(t, client, user.Organization, job.ID) - _ = coderdtest.CreateWorkspace(t, client, "", project.ID) - _ = coderdtest.CreateWorkspace(t, client, "", project.ID) - projects, err := client.Projects(context.Background(), "") + user := coderdtest.CreateFirstUser(t, client) + version := coderdtest.CreateProjectVersion(t, client, user.OrganizationID, nil) + project := coderdtest.CreateProject(t, client, user.OrganizationID, version.ID) + _, err := client.Project(context.Background(), project.ID) require.NoError(t, err) - require.Len(t, projects, 1) - require.Equal(t, projects[0].WorkspaceOwnerCount, uint32(1)) }) } -func TestProjectsByOrganization(t *testing.T) { +func TestProjectVersionsByProject(t *testing.T) { t.Parallel() - t.Run("ListEmpty", func(t *testing.T) { - t.Parallel() - client := coderdtest.New(t, nil) - 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.Run("Get", func(t *testing.T) { t.Parallel() client := coderdtest.New(t, nil) - user := coderdtest.CreateInitialUser(t, client) - job := coderdtest.CreateProjectImportJob(t, client, user.Organization, nil) - _ = coderdtest.CreateProject(t, client, user.Organization, job.ID) - projects, err := client.Projects(context.Background(), "") + user := coderdtest.CreateFirstUser(t, client) + version := coderdtest.CreateProjectVersion(t, client, user.OrganizationID, nil) + project := coderdtest.CreateProject(t, client, user.OrganizationID, version.ID) + versions, err := client.ProjectVersionsByProject(context.Background(), project.ID) require.NoError(t, err) - require.Len(t, projects, 1) + require.Len(t, versions, 1) }) } -func TestPostProjectsByOrganization(t *testing.T) { +func TestProjectVersionByName(t *testing.T) { t.Parallel() - t.Run("Create", func(t *testing.T) { + t.Run("NotFound", func(t *testing.T) { t.Parallel() client := coderdtest.New(t, nil) - user := coderdtest.CreateInitialUser(t, client) - job := coderdtest.CreateProjectImportJob(t, client, user.Organization, nil) - _ = coderdtest.CreateProject(t, client, user.Organization, job.ID) - }) - - t.Run("AlreadyExists", func(t *testing.T) { - t.Parallel() - client := coderdtest.New(t, nil) - user := coderdtest.CreateInitialUser(t, client) - job := coderdtest.CreateProjectImportJob(t, client, user.Organization, nil) - project := coderdtest.CreateProject(t, client, user.Organization, job.ID) - _, err := client.CreateProject(context.Background(), user.Organization, coderd.CreateProjectRequest{ - Name: project.Name, - VersionImportJobID: job.ID, - }) + user := coderdtest.CreateFirstUser(t, client) + version := coderdtest.CreateProjectVersion(t, client, user.OrganizationID, nil) + project := coderdtest.CreateProject(t, client, user.OrganizationID, version.ID) + _, err := client.ProjectVersionByName(context.Background(), project.ID, "nothing") var apiErr *codersdk.Error require.ErrorAs(t, err, &apiErr) - require.Equal(t, http.StatusConflict, apiErr.StatusCode()) - }) -} - -func TestProjectByOrganization(t *testing.T) { - t.Parallel() - t.Run("Get", func(t *testing.T) { - t.Parallel() - client := coderdtest.New(t, nil) - user := coderdtest.CreateInitialUser(t, client) - job := coderdtest.CreateProjectImportJob(t, client, user.Organization, nil) - project := coderdtest.CreateProject(t, client, user.Organization, job.ID) - _, err := client.Project(context.Background(), user.Organization, project.Name) - require.NoError(t, err) - }) -} - -func TestPostParametersByProject(t *testing.T) { - t.Parallel() - t.Run("Create", func(t *testing.T) { - t.Parallel() - client := coderdtest.New(t, nil) - user := coderdtest.CreateInitialUser(t, client) - job := coderdtest.CreateProjectImportJob(t, client, user.Organization, nil) - project := coderdtest.CreateProject(t, client, user.Organization, job.ID) - _, err := client.CreateProjectParameter(context.Background(), user.Organization, project.Name, coderd.CreateParameterValueRequest{ - Name: "somename", - SourceValue: "tomato", - SourceScheme: database.ParameterSourceSchemeData, - DestinationScheme: database.ParameterDestinationSchemeEnvironmentVariable, - }) - require.NoError(t, err) + require.Equal(t, http.StatusNotFound, apiErr.StatusCode()) }) -} -func TestParametersByProject(t *testing.T) { - t.Parallel() - t.Run("ListEmpty", func(t *testing.T) { + t.Run("Found", func(t *testing.T) { t.Parallel() client := coderdtest.New(t, nil) - user := coderdtest.CreateInitialUser(t, client) - job := coderdtest.CreateProjectImportJob(t, client, user.Organization, nil) - project := coderdtest.CreateProject(t, client, user.Organization, job.ID) - params, err := client.ProjectParameters(context.Background(), user.Organization, project.Name) - require.NoError(t, err) - require.NotNil(t, params) - }) - - t.Run("List", func(t *testing.T) { - t.Parallel() - client := coderdtest.New(t, nil) - user := coderdtest.CreateInitialUser(t, client) - job := coderdtest.CreateProjectImportJob(t, client, user.Organization, nil) - project := coderdtest.CreateProject(t, client, user.Organization, job.ID) - _, err := client.CreateProjectParameter(context.Background(), user.Organization, project.Name, coderd.CreateParameterValueRequest{ - Name: "example", - SourceValue: "source-value", - SourceScheme: database.ParameterSourceSchemeData, - DestinationScheme: database.ParameterDestinationSchemeEnvironmentVariable, - }) - require.NoError(t, err) - params, err := client.ProjectParameters(context.Background(), user.Organization, project.Name) + user := coderdtest.CreateFirstUser(t, client) + version := coderdtest.CreateProjectVersion(t, client, user.OrganizationID, nil) + project := coderdtest.CreateProject(t, client, user.OrganizationID, version.ID) + _, err := client.ProjectVersionByName(context.Background(), project.ID, version.Name) require.NoError(t, err) - require.NotNil(t, params) - require.Len(t, params, 1) }) } diff --git a/coderd/projectversion.go b/coderd/projectversion.go deleted file mode 100644 index c1b3ecdd29ace..0000000000000 --- a/coderd/projectversion.go +++ /dev/null @@ -1,113 +0,0 @@ -package coderd - -import ( - "database/sql" - "errors" - "fmt" - "net/http" - "time" - - "github.com/go-chi/render" - "github.com/google/uuid" - "github.com/moby/moby/pkg/namesgenerator" - - "github.com/coder/coder/database" - "github.com/coder/coder/httpapi" - "github.com/coder/coder/httpmw" -) - -// ProjectVersion represents a single version of a project. -type ProjectVersion struct { - ID uuid.UUID `json:"id"` - ProjectID uuid.UUID `json:"project_id"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` - Name string `json:"name"` - ImportJobID uuid.UUID `json:"import_job_id"` -} - -// CreateProjectVersionRequest enables callers to create a new Project Version. -type CreateProjectVersionRequest struct { - ImportJobID uuid.UUID `json:"import_job_id" validate:"required"` -} - -// Lists versions for a single project. -func (api *api) projectVersionsByOrganization(rw http.ResponseWriter, r *http.Request) { - project := httpmw.ProjectParam(r) - - version, err := api.Database.GetProjectVersionsByProjectID(r.Context(), project.ID) - if errors.Is(err, sql.ErrNoRows) { - err = nil - } - if err != nil { - httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ - Message: fmt.Sprintf("get project version: %s", err), - }) - return - } - apiVersion := make([]ProjectVersion, 0) - for _, version := range version { - apiVersion = append(apiVersion, convertProjectVersion(version)) - } - render.Status(r, http.StatusOK) - render.JSON(rw, r, apiVersion) -} - -// Return a single project version by organization and name. -func (*api) projectVersionByOrganizationAndName(rw http.ResponseWriter, r *http.Request) { - projectVersion := httpmw.ProjectVersionParam(r) - render.Status(r, http.StatusOK) - render.JSON(rw, r, convertProjectVersion(projectVersion)) -} - -// Creates a new version of the project. An import job is queued to parse -// the storage method provided. Once completed, the import job will specify -// the version as latest. -func (api *api) postProjectVersionByOrganization(rw http.ResponseWriter, r *http.Request) { - var createProjectVersion CreateProjectVersionRequest - if !httpapi.Read(rw, r, &createProjectVersion) { - return - } - job, err := api.Database.GetProvisionerJobByID(r.Context(), createProjectVersion.ImportJobID) - if errors.Is(err, sql.ErrNoRows) { - httpapi.Write(rw, http.StatusNotFound, httpapi.Response{ - Message: "job not found", - }) - return - } - if err != nil { - httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ - Message: fmt.Sprintf("get provisioner job: %s", err), - }) - return - } - project := httpmw.ProjectParam(r) - projectVersion, err := api.Database.InsertProjectVersion(r.Context(), database.InsertProjectVersionParams{ - ID: uuid.New(), - ProjectID: project.ID, - CreatedAt: database.Now(), - UpdatedAt: database.Now(), - Name: namesgenerator.GetRandomName(1), - ImportJobID: job.ID, - }) - if err != nil { - httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ - Message: fmt.Sprintf("insert project version: %s", err), - }) - return - } - - render.Status(r, http.StatusCreated) - render.JSON(rw, r, convertProjectVersion(projectVersion)) -} - -func convertProjectVersion(version database.ProjectVersion) ProjectVersion { - return ProjectVersion{ - ID: version.ID, - ProjectID: version.ProjectID, - CreatedAt: version.CreatedAt, - UpdatedAt: version.UpdatedAt, - Name: version.Name, - ImportJobID: version.ImportJobID, - } -} diff --git a/coderd/projectversion_test.go b/coderd/projectversion_test.go deleted file mode 100644 index e9937d4ba4f2c..0000000000000 --- a/coderd/projectversion_test.go +++ /dev/null @@ -1,54 +0,0 @@ -package coderd_test - -import ( - "context" - "testing" - - "github.com/stretchr/testify/require" - - "github.com/coder/coder/coderd" - "github.com/coder/coder/coderd/coderdtest" -) - -func TestProjectVersionsByOrganization(t *testing.T) { - t.Parallel() - t.Run("List", func(t *testing.T) { - t.Parallel() - client := coderdtest.New(t, nil) - user := coderdtest.CreateInitialUser(t, client) - job := coderdtest.CreateProjectImportJob(t, client, user.Organization, nil) - project := coderdtest.CreateProject(t, client, user.Organization, job.ID) - versions, err := client.ProjectVersions(context.Background(), user.Organization, project.Name) - require.NoError(t, err) - require.NotNil(t, versions) - require.Len(t, versions, 1) - }) -} - -func TestProjectVersionByOrganizationAndName(t *testing.T) { - t.Parallel() - t.Run("Get", func(t *testing.T) { - t.Parallel() - client := coderdtest.New(t, nil) - user := coderdtest.CreateInitialUser(t, client) - job := coderdtest.CreateProjectImportJob(t, client, user.Organization, nil) - project := coderdtest.CreateProject(t, client, user.Organization, job.ID) - _, err := client.ProjectVersion(context.Background(), user.Organization, project.Name, project.ActiveVersionID.String()) - require.NoError(t, err) - }) -} - -func TestPostProjectVersionByOrganization(t *testing.T) { - t.Parallel() - t.Run("Create", func(t *testing.T) { - t.Parallel() - client := coderdtest.New(t, nil) - user := coderdtest.CreateInitialUser(t, client) - job := coderdtest.CreateProjectImportJob(t, client, user.Organization, nil) - project := coderdtest.CreateProject(t, client, user.Organization, job.ID) - _, err := client.CreateProjectVersion(context.Background(), user.Organization, project.Name, coderd.CreateProjectVersionRequest{ - ImportJobID: job.ID, - }) - require.NoError(t, err) - }) -} diff --git a/coderd/projectversions.go b/coderd/projectversions.go new file mode 100644 index 0000000000000..a32c8db7e6d16 --- /dev/null +++ b/coderd/projectversions.go @@ -0,0 +1,150 @@ +package coderd + +import ( + "database/sql" + "errors" + "fmt" + "net/http" + "time" + + "github.com/go-chi/render" + "github.com/google/uuid" + + "github.com/coder/coder/coderd/parameter" + "github.com/coder/coder/database" + "github.com/coder/coder/httpapi" + "github.com/coder/coder/httpmw" +) + +// ProjectVersion represents a single version of a project. +type ProjectVersion struct { + ID uuid.UUID `json:"id"` + ProjectID *uuid.UUID `json:"project_id,omitempty"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + Name string `json:"name"` + Job ProvisionerJob `json:"job"` +} + +// ProjectVersionParameterSchema represents a parameter parsed from project version source. +type ProjectVersionParameterSchema database.ParameterSchema + +// ProjectVersionParameter represents a computed parameter value. +type ProjectVersionParameter parameter.ComputedValue + +func (api *api) projectVersion(rw http.ResponseWriter, r *http.Request) { + projectVersion := httpmw.ProjectVersionParam(r) + job, err := api.Database.GetProvisionerJobByID(r.Context(), projectVersion.JobID) + if err != nil { + httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ + Message: fmt.Sprintf("get provisioner job: %s", err), + }) + return + } + render.Status(r, http.StatusOK) + render.JSON(rw, r, convertProjectVersion(projectVersion, convertProvisionerJob(job))) +} + +func (api *api) projectVersionSchema(rw http.ResponseWriter, r *http.Request) { + projectVersion := httpmw.ProjectVersionParam(r) + job, err := api.Database.GetProvisionerJobByID(r.Context(), projectVersion.JobID) + if err != nil { + httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ + Message: fmt.Sprintf("get provisioner job: %s", err), + }) + return + } + if !job.CompletedAt.Valid { + httpapi.Write(rw, http.StatusPreconditionFailed, httpapi.Response{ + Message: "Project version job hasn't completed!", + }) + return + } + schemas, err := api.Database.GetParameterSchemasByJobID(r.Context(), job.ID) + if errors.Is(err, sql.ErrNoRows) { + err = nil + } + if err != nil { + httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ + Message: fmt.Sprintf("list parameter schemas: %s", err), + }) + return + } + if schemas == nil { + schemas = []database.ParameterSchema{} + } + render.Status(r, http.StatusOK) + render.JSON(rw, r, schemas) +} + +func (api *api) projectVersionParameters(rw http.ResponseWriter, r *http.Request) { + apiKey := httpmw.APIKey(r) + projectVersion := httpmw.ProjectVersionParam(r) + job, err := api.Database.GetProvisionerJobByID(r.Context(), projectVersion.JobID) + if err != nil { + httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ + Message: fmt.Sprintf("get provisioner job: %s", err), + }) + return + } + if !job.CompletedAt.Valid { + httpapi.Write(rw, http.StatusPreconditionFailed, httpapi.Response{ + Message: "Job hasn't completed!", + }) + return + } + values, err := parameter.Compute(r.Context(), api.Database, parameter.ComputeScope{ + ProjectImportJobID: job.ID, + OrganizationID: job.OrganizationID, + UserID: apiKey.UserID, + }, ¶meter.ComputeOptions{ + // We *never* want to send the client secret parameter values. + HideRedisplayValues: true, + }) + if err != nil { + httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ + Message: fmt.Sprintf("compute values: %s", err), + }) + return + } + if values == nil { + values = []parameter.ComputedValue{} + } + render.Status(r, http.StatusOK) + render.JSON(rw, r, values) +} + +func (api *api) projectVersionResources(rw http.ResponseWriter, r *http.Request) { + projectVersion := httpmw.ProjectVersionParam(r) + job, err := api.Database.GetProvisionerJobByID(r.Context(), projectVersion.JobID) + if err != nil { + httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ + Message: fmt.Sprintf("get provisioner job: %s", err), + }) + return + } + api.provisionerJobResources(rw, r, job) +} + +func (api *api) projectVersionLogs(rw http.ResponseWriter, r *http.Request) { + projectVersion := httpmw.ProjectVersionParam(r) + job, err := api.Database.GetProvisionerJobByID(r.Context(), projectVersion.JobID) + if err != nil { + httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ + Message: fmt.Sprintf("get provisioner job: %s", err), + }) + return + } + api.provisionerJobLogs(rw, r, job) +} + +func convertProjectVersion(version database.ProjectVersion, job ProvisionerJob) ProjectVersion { + return ProjectVersion{ + ID: version.ID, + ProjectID: &version.ProjectID.UUID, + CreatedAt: version.CreatedAt, + UpdatedAt: version.UpdatedAt, + Name: version.Name, + Job: job, + } +} diff --git a/coderd/projectversions_test.go b/coderd/projectversions_test.go new file mode 100644 index 0000000000000..b6efc2ad687a6 --- /dev/null +++ b/coderd/projectversions_test.go @@ -0,0 +1,205 @@ +package coderd_test + +import ( + "context" + "net/http" + "testing" + "time" + + "github.com/google/uuid" + "github.com/stretchr/testify/require" + + "github.com/coder/coder/coderd/coderdtest" + "github.com/coder/coder/codersdk" + "github.com/coder/coder/provisioner/echo" + "github.com/coder/coder/provisionersdk/proto" +) + +func TestProjectVersion(t *testing.T) { + t.Parallel() + t.Run("Get", func(t *testing.T) { + t.Parallel() + client := coderdtest.New(t, nil) + user := coderdtest.CreateFirstUser(t, client) + version := coderdtest.CreateProjectVersion(t, client, user.OrganizationID, nil) + _, err := client.ProjectVersion(context.Background(), version.ID) + require.NoError(t, err) + }) +} + +func TestProjectVersionSchema(t *testing.T) { + t.Parallel() + t.Run("ListRunning", func(t *testing.T) { + t.Parallel() + client := coderdtest.New(t, nil) + user := coderdtest.CreateFirstUser(t, client) + version := coderdtest.CreateProjectVersion(t, client, user.OrganizationID, nil) + _, err := client.ProjectVersionSchema(context.Background(), version.ID) + 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, nil) + user := coderdtest.CreateFirstUser(t, client) + coderdtest.NewProvisionerDaemon(t, client) + version := coderdtest.CreateProjectVersion(t, client, user.OrganizationID, &echo.Responses{ + Parse: []*proto.Parse_Response{{ + Type: &proto.Parse_Response_Complete{ + Complete: &proto.Parse_Complete{ + ParameterSchemas: []*proto.ParameterSchema{{ + Name: "example", + DefaultDestination: &proto.ParameterDestination{ + Scheme: proto.ParameterDestination_PROVISIONER_VARIABLE, + }, + }}, + }, + }, + }}, + Provision: echo.ProvisionComplete, + }) + coderdtest.AwaitProjectVersionJob(t, client, version.ID) + schemas, err := client.ProjectVersionSchema(context.Background(), version.ID) + require.NoError(t, err) + require.NotNil(t, schemas) + require.Len(t, schemas, 1) + }) +} + +func TestProjectVersionParameters(t *testing.T) { + t.Parallel() + t.Run("ListRunning", func(t *testing.T) { + t.Parallel() + client := coderdtest.New(t, nil) + user := coderdtest.CreateFirstUser(t, client) + version := coderdtest.CreateProjectVersion(t, client, user.OrganizationID, nil) + _, err := client.ProjectVersionParameters(context.Background(), version.ID) + 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, nil) + user := coderdtest.CreateFirstUser(t, client) + coderdtest.NewProvisionerDaemon(t, client) + version := coderdtest.CreateProjectVersion(t, client, user.OrganizationID, &echo.Responses{ + Parse: []*proto.Parse_Response{{ + Type: &proto.Parse_Response_Complete{ + Complete: &proto.Parse_Complete{ + ParameterSchemas: []*proto.ParameterSchema{{ + Name: "example", + RedisplayValue: true, + DefaultSource: &proto.ParameterSource{ + Scheme: proto.ParameterSource_DATA, + Value: "hello", + }, + DefaultDestination: &proto.ParameterDestination{ + Scheme: proto.ParameterDestination_PROVISIONER_VARIABLE, + }, + }}, + }, + }, + }}, + Provision: echo.ProvisionComplete, + }) + coderdtest.AwaitProjectVersionJob(t, client, version.ID) + params, err := client.ProjectVersionParameters(context.Background(), version.ID) + require.NoError(t, err) + require.NotNil(t, params) + require.Len(t, params, 1) + require.Equal(t, "hello", params[0].SourceValue) + }) +} + +func TestProjectVersionResources(t *testing.T) { + t.Parallel() + t.Run("ListRunning", func(t *testing.T) { + t.Parallel() + client := coderdtest.New(t, nil) + user := coderdtest.CreateFirstUser(t, client) + version := coderdtest.CreateProjectVersion(t, client, user.OrganizationID, nil) + _, err := client.ProjectVersionResources(context.Background(), version.ID) + 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, nil) + user := coderdtest.CreateFirstUser(t, client) + coderdtest.NewProvisionerDaemon(t, client) + version := coderdtest.CreateProjectVersion(t, client, user.OrganizationID, &echo.Responses{ + Parse: echo.ParseComplete, + Provision: []*proto.Provision_Response{{ + Type: &proto.Provision_Response_Complete{ + Complete: &proto.Provision_Complete{ + Resources: []*proto.Resource{{ + Name: "some", + Type: "example", + Agent: &proto.Agent{ + Id: "something", + Auth: &proto.Agent_Token{}, + }, + }, { + Name: "another", + Type: "example", + }}, + }, + }, + }}, + }) + coderdtest.AwaitProjectVersionJob(t, client, version.ID) + resources, err := client.ProjectVersionResources(context.Background(), version.ID) + require.NoError(t, err) + require.NotNil(t, resources) + require.Len(t, resources, 4) + require.Equal(t, "some", resources[0].Name) + require.Equal(t, "example", resources[0].Type) + require.NotNil(t, resources[0].Agent) + }) +} + +func TestProjectVersionLogs(t *testing.T) { + t.Parallel() + client := coderdtest.New(t, nil) + user := coderdtest.CreateFirstUser(t, client) + coderdtest.NewProvisionerDaemon(t, client) + before := time.Now() + version := coderdtest.CreateProjectVersion(t, client, user.OrganizationID, &echo.Responses{ + Parse: echo.ParseComplete, + Provision: []*proto.Provision_Response{{ + Type: &proto.Provision_Response_Log{ + Log: &proto.Log{ + Level: proto.LogLevel_INFO, + Output: "example", + }, + }, + }, { + Type: &proto.Provision_Response_Complete{ + Complete: &proto.Provision_Complete{ + Resources: []*proto.Resource{{ + Name: "some", + Type: "example", + Agent: &proto.Agent{ + Id: "something", + Auth: &proto.Agent_Token{ + Token: uuid.NewString(), + }, + }, + }, { + Name: "another", + Type: "example", + }}, + }, + }, + }}, + }) + ctx, cancelFunc := context.WithCancel(context.Background()) + t.Cleanup(cancelFunc) + logs, err := client.ProjectVersionLogsAfter(ctx, version.ID, before) + require.NoError(t, err) + log := <-logs + require.Equal(t, "example", log.Output) +} diff --git a/coderd/provisionerdaemons.go b/coderd/provisionerdaemons.go index d30b876b99547..dfd9b13ed52d6 100644 --- a/coderd/provisionerdaemons.go +++ b/coderd/provisionerdaemons.go @@ -12,7 +12,6 @@ import ( "reflect" "time" - "github.com/go-chi/render" "github.com/google/uuid" "github.com/hashicorp/yamux" "github.com/moby/moby/pkg/namesgenerator" @@ -33,27 +32,8 @@ import ( type ProvisionerDaemon database.ProvisionerDaemon -// Lists all registered provisioner daemons. -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 - } - if err != nil { - httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ - Message: fmt.Sprintf("get provisioner daemons: %s", err), - }) - return - } - if daemons == nil { - daemons = []database.ProvisionerDaemon{} - } - render.Status(r, http.StatusOK) - render.JSON(rw, r, daemons) -} - // Serves the provisioner daemon protobuf API over a WebSocket. -func (api *api) provisionerDaemonsServe(rw http.ResponseWriter, r *http.Request) { +func (api *api) provisionerDaemonsListen(rw http.ResponseWriter, r *http.Request) { api.websocketWaitGroup.Add(1) defer api.websocketWaitGroup.Done() @@ -113,8 +93,8 @@ func (api *api) provisionerDaemonsServe(rw http.ResponseWriter, r *http.Request) // The input for a "workspace_provision" job. type workspaceProvisionJob struct { - WorkspaceHistoryID uuid.UUID `json:"workspace_history_id"` - DryRun bool `json:"dry_run"` + WorkspaceBuildID uuid.UUID `json:"workspace_build_id"` + DryRun bool `json:"dry_run"` } // Implementation of the provisioner daemon protobuf server. @@ -182,32 +162,32 @@ func (server *provisionerdServer) AcquireJob(ctx context.Context, _ *proto.Empty UserName: user.Username, } switch job.Type { - case database.ProvisionerJobTypeWorkspaceProvision: + case database.ProvisionerJobTypeWorkspaceBuild: var input workspaceProvisionJob err = json.Unmarshal(job.Input, &input) if err != nil { return nil, failJob(fmt.Sprintf("unmarshal job input %q: %s", job.Input, err)) } - workspaceHistory, err := server.Database.GetWorkspaceHistoryByID(ctx, input.WorkspaceHistoryID) + workspaceBuild, err := server.Database.GetWorkspaceBuildByID(ctx, input.WorkspaceBuildID) if err != nil { - return nil, failJob(fmt.Sprintf("get workspace history: %s", err)) + return nil, failJob(fmt.Sprintf("get workspace build: %s", err)) } - workspace, err := server.Database.GetWorkspaceByID(ctx, workspaceHistory.WorkspaceID) + workspace, err := server.Database.GetWorkspaceByID(ctx, workspaceBuild.WorkspaceID) if err != nil { return nil, failJob(fmt.Sprintf("get workspace: %s", err)) } - projectVersion, err := server.Database.GetProjectVersionByID(ctx, workspaceHistory.ProjectVersionID) + projectVersion, err := server.Database.GetProjectVersionByID(ctx, workspaceBuild.ProjectVersionID) if err != nil { return nil, failJob(fmt.Sprintf("get project version: %s", err)) } - project, err := server.Database.GetProjectByID(ctx, projectVersion.ProjectID) + project, err := server.Database.GetProjectByID(ctx, projectVersion.ProjectID.UUID) if err != nil { return nil, failJob(fmt.Sprintf("get project: %s", err)) } // Compute parameters for the workspace to consume. parameters, err := parameter.Compute(ctx, server.Database, parameter.ComputeScope{ - ProjectImportJobID: projectVersion.ImportJobID, + ProjectImportJobID: projectVersion.JobID, OrganizationID: job.OrganizationID, ProjectID: uuid.NullUUID{ UUID: project.ID, @@ -231,17 +211,17 @@ func (server *provisionerdServer) AcquireJob(ctx context.Context, _ *proto.Empty } protoParameters = append(protoParameters, converted) } - transition, err := convertWorkspaceTransition(workspaceHistory.Transition) + transition, err := convertWorkspaceTransition(workspaceBuild.Transition) if err != nil { return nil, failJob(fmt.Sprint("convert workspace transition: %w", err)) } - protoJob.Type = &proto.AcquiredJob_WorkspaceProvision_{ - WorkspaceProvision: &proto.AcquiredJob_WorkspaceProvision{ - WorkspaceHistoryId: workspaceHistory.ID.String(), - WorkspaceName: workspace.Name, - State: workspaceHistory.ProvisionerState, - ParameterValues: protoParameters, + protoJob.Type = &proto.AcquiredJob_WorkspaceBuild_{ + WorkspaceBuild: &proto.AcquiredJob_WorkspaceBuild{ + WorkspaceBuildId: workspaceBuild.ID.String(), + WorkspaceName: workspace.Name, + State: workspaceBuild.ProvisionerState, + ParameterValues: protoParameters, Metadata: &sdkproto.Provision_Metadata{ CoderUrl: server.AccessURL.String(), WorkspaceTransition: transition, @@ -432,8 +412,8 @@ func (server *provisionerdServer) FailJob(ctx context.Context, failJob *proto.Fa return nil, xerrors.Errorf("update provisioner job: %w", err) } switch jobType := failJob.Type.(type) { - case *proto.FailedJob_WorkspaceProvision_: - if jobType.WorkspaceProvision.State == nil { + case *proto.FailedJob_WorkspaceBuild_: + if jobType.WorkspaceBuild.State == nil { break } var input workspaceProvisionJob @@ -441,13 +421,13 @@ func (server *provisionerdServer) FailJob(ctx context.Context, failJob *proto.Fa if err != nil { return nil, xerrors.Errorf("unmarshal workspace provision input: %w", err) } - err = server.Database.UpdateWorkspaceHistoryByID(ctx, database.UpdateWorkspaceHistoryByIDParams{ + err = server.Database.UpdateWorkspaceBuildByID(ctx, database.UpdateWorkspaceBuildByIDParams{ ID: jobID, UpdatedAt: database.Now(), - ProvisionerState: jobType.WorkspaceProvision.State, + ProvisionerState: jobType.WorkspaceBuild.State, }) if err != nil { - return nil, xerrors.Errorf("update workspace history state: %w", err) + return nil, xerrors.Errorf("update workspace build state: %w", err) } case *proto.FailedJob_ProjectImport_: } @@ -481,7 +461,7 @@ func (server *provisionerdServer) CompleteJob(ctx context.Context, completed *pr slog.F("resource_type", resource.Type), slog.F("transition", transition)) - err = insertProvisionerJobResource(ctx, server.Database, jobID, transition, resource) + err = insertWorkspaceResource(ctx, server.Database, jobID, transition, resource) if err != nil { return nil, xerrors.Errorf("insert resource: %w", err) } @@ -503,16 +483,16 @@ func (server *provisionerdServer) CompleteJob(ctx context.Context, completed *pr if err != nil { return nil, xerrors.Errorf("complete job: %w", err) } - case *proto.CompletedJob_WorkspaceProvision_: + case *proto.CompletedJob_WorkspaceBuild_: var input workspaceProvisionJob err = json.Unmarshal(job.Input, &input) if err != nil { return nil, xerrors.Errorf("unmarshal job data: %w", err) } - workspaceHistory, err := server.Database.GetWorkspaceHistoryByID(ctx, input.WorkspaceHistoryID) + workspaceBuild, err := server.Database.GetWorkspaceBuildByID(ctx, input.WorkspaceBuildID) if err != nil { - return nil, xerrors.Errorf("get workspace history: %w", err) + return nil, xerrors.Errorf("get workspace build: %w", err) } err = server.Database.InTx(func(db database.Store) error { @@ -527,17 +507,17 @@ func (server *provisionerdServer) CompleteJob(ctx context.Context, completed *pr if err != nil { return xerrors.Errorf("update provisioner job: %w", err) } - err = db.UpdateWorkspaceHistoryByID(ctx, database.UpdateWorkspaceHistoryByIDParams{ - ID: workspaceHistory.ID, + err = db.UpdateWorkspaceBuildByID(ctx, database.UpdateWorkspaceBuildByIDParams{ + ID: workspaceBuild.ID, UpdatedAt: database.Now(), - ProvisionerState: jobType.WorkspaceProvision.State, + ProvisionerState: jobType.WorkspaceBuild.State, }) if err != nil { - return xerrors.Errorf("update workspace history: %w", err) + return xerrors.Errorf("update workspace build: %w", err) } // This could be a bulk insert to improve performance. - for _, protoResource := range jobType.WorkspaceProvision.Resources { - err = insertProvisionerJobResource(ctx, db, job.ID, workspaceHistory.Transition, protoResource) + for _, protoResource := range jobType.WorkspaceBuild.Resources { + err = insertWorkspaceResource(ctx, db, job.ID, workspaceBuild.Transition, protoResource) if err != nil { return xerrors.Errorf("insert provisioner job: %w", err) } @@ -555,8 +535,8 @@ func (server *provisionerdServer) CompleteJob(ctx context.Context, completed *pr return &proto.Empty{}, nil } -func insertProvisionerJobResource(ctx context.Context, db database.Store, jobID uuid.UUID, transition database.WorkspaceTransition, protoResource *sdkproto.Resource) error { - resource, err := db.InsertProvisionerJobResource(ctx, database.InsertProvisionerJobResourceParams{ +func insertWorkspaceResource(ctx context.Context, db database.Store, jobID uuid.UUID, transition database.WorkspaceTransition, protoResource *sdkproto.Resource) error { + resource, err := db.InsertWorkspaceResource(ctx, database.InsertWorkspaceResourceParams{ ID: uuid.New(), CreatedAt: database.Now(), JobID: jobID, @@ -590,12 +570,19 @@ func insertProvisionerJobResource(ctx context.Context, db database.Store, jobID Valid: true, } } + authToken := uuid.New() + if protoResource.Agent.GetToken() != "" { + authToken, err = uuid.Parse(protoResource.Agent.GetToken()) + if err != nil { + return xerrors.Errorf("invalid auth token format; must be uuid: %w", err) + } + } - _, err := db.InsertProvisionerJobAgent(ctx, database.InsertProvisionerJobAgentParams{ + _, err := db.InsertWorkspaceAgent(ctx, database.InsertWorkspaceAgentParams{ ID: resource.AgentID.UUID, CreatedAt: database.Now(), ResourceID: resource.ID, - AuthToken: uuid.New(), + AuthToken: authToken, AuthInstanceID: instanceID, EnvironmentVariables: env, StartupScript: sql.NullString{ diff --git a/coderd/provisionerdaemons_test.go b/coderd/provisionerdaemons_test.go deleted file mode 100644 index 22584d03b5853..0000000000000 --- a/coderd/provisionerdaemons_test.go +++ /dev/null @@ -1,25 +0,0 @@ -package coderd_test - -import ( - "context" - "testing" - "time" - - "github.com/stretchr/testify/require" - - "github.com/coder/coder/coderd/coderdtest" -) - -func TestProvisionerDaemons(t *testing.T) { - // Tests for properly processing specific job types should be placed - // in their respective files. - t.Parallel() - - client := coderdtest.New(t, nil) - _ = coderdtest.NewProvisionerDaemon(t, client) - require.Eventually(t, func() bool { - daemons, err := client.ProvisionerDaemons(context.Background()) - require.NoError(t, err) - return len(daemons) > 0 - }, 3*time.Second, 50*time.Millisecond) -} diff --git a/coderd/provisionerjobs.go b/coderd/provisionerjobs.go index 8c343256f15ee..62f7b6f64ed0e 100644 --- a/coderd/provisionerjobs.go +++ b/coderd/provisionerjobs.go @@ -17,38 +17,29 @@ import ( "github.com/coder/coder/database" "github.com/coder/coder/httpapi" - "github.com/coder/coder/httpmw" ) +// ProvisionerJobStaus represents the at-time state of a job. type ProvisionerJobStatus string -// Completed returns whether the job is still processing. -func (p ProvisionerJobStatus) Completed() bool { - return p == ProvisionerJobStatusSucceeded || p == ProvisionerJobStatusFailed || p == ProvisionerJobStatusCancelled -} - const ( - ProvisionerJobStatusPending ProvisionerJobStatus = "pending" - ProvisionerJobStatusRunning ProvisionerJobStatus = "running" - ProvisionerJobStatusSucceeded ProvisionerJobStatus = "succeeded" - ProvisionerJobStatusCancelled ProvisionerJobStatus = "canceled" - ProvisionerJobStatusFailed ProvisionerJobStatus = "failed" + ProvisionerJobPending ProvisionerJobStatus = "pending" + ProvisionerJobRunning ProvisionerJobStatus = "running" + ProvisionerJobSucceeded ProvisionerJobStatus = "succeeded" + ProvisionerJobCancelled ProvisionerJobStatus = "canceled" + ProvisionerJobFailed ProvisionerJobStatus = "failed" ) type ProvisionerJob struct { - ID uuid.UUID `json:"id"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` - StartedAt *time.Time `json:"started_at,omitempty"` - CancelledAt *time.Time `json:"canceled_at,omitempty"` - CompletedAt *time.Time `json:"completed_at,omitempty"` - Status ProvisionerJobStatus `json:"status"` - Error string `json:"error,omitempty"` - Provisioner database.ProvisionerType `json:"provisioner"` - WorkerID *uuid.UUID `json:"worker_id,omitempty"` + ID uuid.UUID `json:"id"` + CreatedAt time.Time `json:"created_at"` + StartedAt *time.Time `json:"started_at,omitempty"` + CompletedAt *time.Time `json:"completed_at,omitempty"` + Error string `json:"error,omitempty"` + Status ProvisionerJobStatus `json:"status"` + WorkerID *uuid.UUID `json:"worker_id,omitempty"` } -// ProvisionerJobLog represents a single log from a provisioner job. type ProvisionerJobLog struct { ID uuid.UUID `json:"id"` CreatedAt time.Time `json:"created_at"` @@ -57,31 +48,6 @@ type ProvisionerJobLog struct { Output string `json:"output"` } -type ProvisionerJobResource struct { - ID uuid.UUID `json:"id"` - CreatedAt time.Time `json:"created_at"` - JobID uuid.UUID `json:"job_id"` - Transition database.WorkspaceTransition `json:"workspace_transition"` - Type string `json:"type"` - Name string `json:"name"` -} - -type ProvisionerJobAgent struct { - ID uuid.UUID `json:"id"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` - ResourceID uuid.UUID `json:"resource_id"` - InstanceID string `json:"instance_id,omitempty"` - EnvironmentVariables map[string]string `json:"environment_variables"` - StartupScript string `json:"startup_script,omitempty"` -} - -func (*api) provisionerJobByID(rw http.ResponseWriter, r *http.Request) { - job := httpmw.ProvisionerJobParam(r) - render.Status(r, http.StatusOK) - render.JSON(rw, r, convertProvisionerJob(job)) -} - // Returns provisioner logs based on query parameters. // The intended usage for a client to stream all logs (with JS API): // const timestamp = new Date().getTime(); @@ -89,7 +55,7 @@ func (*api) provisionerJobByID(rw http.ResponseWriter, r *http.Request) { // 2. GET /logs?after=&follow // The combination of these responses should provide all current logs // to the consumer, and future logs are streamed in the follow request. -func (api *api) provisionerJobLogsByID(rw http.ResponseWriter, r *http.Request) { +func (api *api) provisionerJobLogs(rw http.ResponseWriter, r *http.Request, job database.ProvisionerJob) { follow := r.URL.Query().Has("follow") afterRaw := r.URL.Query().Get("after") beforeRaw := r.URL.Query().Get("before") @@ -131,7 +97,6 @@ func (api *api) provisionerJobLogsByID(rw http.ResponseWriter, r *http.Request) before = database.Now() } - job := httpmw.ProvisionerJobParam(r) if !follow { logs, err := api.Database.GetProvisionerLogsByIDBetween(r.Context(), database.GetProvisionerLogsByIDBetweenParams{ JobID: job.ID, @@ -231,13 +196,56 @@ func (api *api) provisionerJobLogsByID(rw http.ResponseWriter, r *http.Request) api.Logger.Warn(r.Context(), "streaming job logs; checking if completed", slog.Error(err), slog.F("job_id", job.ID.String())) continue } - if convertProvisionerJob(job).Status.Completed() { + if job.CompletedAt.Valid { return } } } } +func (api *api) provisionerJobResources(rw http.ResponseWriter, r *http.Request, job database.ProvisionerJob) { + if !job.CompletedAt.Valid { + httpapi.Write(rw, http.StatusPreconditionFailed, httpapi.Response{ + Message: "Job hasn't completed!", + }) + return + } + resources, err := api.Database.GetWorkspaceResourcesByJobID(r.Context(), job.ID) + if errors.Is(err, sql.ErrNoRows) { + err = nil + } + if err != nil { + httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ + Message: fmt.Sprintf("get provisioner job resources: %s", err), + }) + return + } + apiResources := make([]WorkspaceResource, 0) + for _, resource := range resources { + if !resource.AgentID.Valid { + apiResources = append(apiResources, convertWorkspaceResource(resource, nil)) + continue + } + agent, err := api.Database.GetWorkspaceAgentByResourceID(r.Context(), resource.ID) + if err != nil { + httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ + Message: fmt.Sprintf("get provisioner job agent: %s", err), + }) + return + } + apiAgent, err := convertWorkspaceAgent(agent) + if err != nil { + httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ + Message: fmt.Sprintf("convert provisioner job agent: %s", err), + }) + return + } + apiResources = append(apiResources, convertWorkspaceResource(resource, &apiAgent)) + } + render.Status(r, http.StatusOK) + render.JSON(rw, r, apiResources) +} + func convertProvisionerJobLog(provisionerJobLog database.ProvisionerJobLog) ProvisionerJobLog { return ProvisionerJobLog{ ID: provisionerJobLog.ID, @@ -250,19 +258,14 @@ func convertProvisionerJobLog(provisionerJobLog database.ProvisionerJobLog) Prov func convertProvisionerJob(provisionerJob database.ProvisionerJob) ProvisionerJob { job := ProvisionerJob{ - ID: provisionerJob.ID, - CreatedAt: provisionerJob.CreatedAt, - UpdatedAt: provisionerJob.UpdatedAt, - Error: provisionerJob.Error.String, - Provisioner: provisionerJob.Provisioner, + ID: provisionerJob.ID, + CreatedAt: provisionerJob.CreatedAt, + Error: provisionerJob.Error.String, } // Applying values optional to the struct. if provisionerJob.StartedAt.Valid { job.StartedAt = &provisionerJob.StartedAt.Time } - if provisionerJob.CancelledAt.Valid { - job.CancelledAt = &provisionerJob.CancelledAt.Time - } if provisionerJob.CompletedAt.Valid { job.CompletedAt = &provisionerJob.CompletedAt.Time } @@ -272,20 +275,20 @@ func convertProvisionerJob(provisionerJob database.ProvisionerJob) ProvisionerJo switch { case provisionerJob.CancelledAt.Valid: - job.Status = ProvisionerJobStatusCancelled + job.Status = ProvisionerJobCancelled case !provisionerJob.StartedAt.Valid: - job.Status = ProvisionerJobStatusPending + job.Status = ProvisionerJobPending case provisionerJob.CompletedAt.Valid: if job.Error == "" { - job.Status = ProvisionerJobStatusSucceeded + job.Status = ProvisionerJobSucceeded } else { - job.Status = ProvisionerJobStatusFailed + job.Status = ProvisionerJobFailed } case database.Now().Sub(provisionerJob.UpdatedAt) > 30*time.Second: - job.Status = ProvisionerJobStatusFailed + job.Status = ProvisionerJobFailed job.Error = "Worker failed to update job in time." default: - job.Status = ProvisionerJobStatusRunning + job.Status = ProvisionerJobRunning } return job diff --git a/coderd/provisionerjobs_test.go b/coderd/provisionerjobs_test.go index b33169a58fd1a..828062bcf7014 100644 --- a/coderd/provisionerjobs_test.go +++ b/coderd/provisionerjobs_test.go @@ -2,7 +2,6 @@ package coderd_test import ( "context" - "net/http" "testing" "time" @@ -10,203 +9,19 @@ 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 TestPostProvisionerImportJobByOrganization(t *testing.T) { +func TestProvisionerJobLogs(t *testing.T) { t.Parallel() - t.Run("Create", func(t *testing.T) { - t.Parallel() - client := coderdtest.New(t, nil) - user := coderdtest.CreateInitialUser(t, client) - _ = coderdtest.NewProvisionerDaemon(t, client) - before := time.Now() - job := coderdtest.CreateProjectImportJob(t, client, user.Organization, &echo.Responses{ - Parse: []*proto.Parse_Response{{ - Type: &proto.Parse_Response_Complete{ - Complete: &proto.Parse_Complete{ - ParameterSchemas: []*proto.ParameterSchema{}, - }, - }, - }}, - Provision: []*proto.Provision_Response{{ - Type: &proto.Provision_Response_Complete{ - Complete: &proto.Provision_Complete{ - Resources: []*proto.Resource{{ - Name: "dev", - Type: "ec2_instance", - }}, - }, - }, - }}, - }) - logs, err := client.ProjectImportJobLogsAfter(context.Background(), user.Organization, job.ID, before) - require.NoError(t, err) - for { - log, ok := <-logs - if !ok { - break - } - t.Log(log.Output) - } - }) - - t.Run("CreateWithParameters", func(t *testing.T) { - t.Parallel() - client := coderdtest.New(t, nil) - user := coderdtest.CreateInitialUser(t, client) - _ = coderdtest.NewProvisionerDaemon(t, client) - data, err := echo.Tar(&echo.Responses{ - Parse: []*proto.Parse_Response{{ - Type: &proto.Parse_Response_Complete{ - Complete: &proto.Parse_Complete{ - ParameterSchemas: []*proto.ParameterSchema{{ - Name: "test", - RedisplayValue: true, - }}, - }, - }, - }}, - Provision: echo.ProvisionComplete, - }) - require.NoError(t, err) - file, err := client.UploadFile(context.Background(), codersdk.ContentTypeTar, data) - require.NoError(t, err) - job, err := client.CreateProjectImportJob(context.Background(), user.Organization, coderd.CreateProjectImportJobRequest{ - StorageSource: file.Hash, - StorageMethod: database.ProvisionerStorageMethodFile, - Provisioner: database.ProvisionerTypeEcho, - ParameterValues: []coderd.CreateParameterValueRequest{{ - Name: "test", - SourceValue: "somevalue", - SourceScheme: database.ParameterSourceSchemeData, - DestinationScheme: database.ParameterDestinationSchemeProvisionerVariable, - }}, - }) - require.NoError(t, err) - job = coderdtest.AwaitProjectImportJob(t, client, user.Organization, job.ID) - values, err := client.ProjectImportJobParameters(context.Background(), user.Organization, job.ID) - require.NoError(t, err) - require.Equal(t, "somevalue", values[0].SourceValue) - }) -} - -func TestProvisionerJobParametersByID(t *testing.T) { - t.Parallel() - t.Run("NotImported", func(t *testing.T) { - t.Parallel() - client := coderdtest.New(t, nil) - user := coderdtest.CreateInitialUser(t, client) - job := coderdtest.CreateProjectImportJob(t, client, user.Organization, nil) - _, err := client.ProjectImportJobParameters(context.Background(), user.Organization, job.ID) - 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, nil) - user := coderdtest.CreateInitialUser(t, client) - _ = coderdtest.NewProvisionerDaemon(t, client) - job := coderdtest.CreateProjectImportJob(t, client, user.Organization, &echo.Responses{ - Parse: []*proto.Parse_Response{{ - Type: &proto.Parse_Response_Complete{ - Complete: &proto.Parse_Complete{ - ParameterSchemas: []*proto.ParameterSchema{{ - Name: "example", - DefaultSource: &proto.ParameterSource{ - Scheme: proto.ParameterSource_DATA, - Value: "hello", - }, - DefaultDestination: &proto.ParameterDestination{ - Scheme: proto.ParameterDestination_PROVISIONER_VARIABLE, - }, - }}, - }, - }, - }}, - Provision: echo.ProvisionComplete, - }) - job = coderdtest.AwaitProjectImportJob(t, client, user.Organization, job.ID) - params, err := client.ProjectImportJobParameters(context.Background(), user.Organization, job.ID) - require.NoError(t, err) - require.Len(t, params, 1) - }) - - t.Run("ListNoRedisplay", func(t *testing.T) { - t.Parallel() - client := coderdtest.New(t, nil) - user := coderdtest.CreateInitialUser(t, client) - _ = coderdtest.NewProvisionerDaemon(t, client) - job := coderdtest.CreateProjectImportJob(t, client, user.Organization, &echo.Responses{ - Parse: []*proto.Parse_Response{{ - Type: &proto.Parse_Response_Complete{ - Complete: &proto.Parse_Complete{ - ParameterSchemas: []*proto.ParameterSchema{{ - Name: "example", - DefaultSource: &proto.ParameterSource{ - Scheme: proto.ParameterSource_DATA, - Value: "tomato", - }, - DefaultDestination: &proto.ParameterDestination{ - Scheme: proto.ParameterDestination_PROVISIONER_VARIABLE, - }, - RedisplayValue: false, - }}, - }, - }, - }}, - Provision: echo.ProvisionComplete, - }) - coderdtest.AwaitProjectImportJob(t, client, user.Organization, job.ID) - params, err := client.ProjectImportJobParameters(context.Background(), user.Organization, job.ID) - require.NoError(t, err) - require.Len(t, params, 1) - require.NotNil(t, params[0]) - require.Equal(t, params[0].SourceValue, "") - }) -} - -func TestProvisionerJobResourcesByID(t *testing.T) { - t.Parallel() - t.Run("List", func(t *testing.T) { - t.Parallel() - client := coderdtest.New(t, nil) - user := coderdtest.CreateInitialUser(t, client) - _ = coderdtest.NewProvisionerDaemon(t, client) - job := coderdtest.CreateProjectImportJob(t, client, user.Organization, &echo.Responses{ - Parse: echo.ParseComplete, - Provision: []*proto.Provision_Response{{ - Type: &proto.Provision_Response_Complete{ - Complete: &proto.Provision_Complete{ - Resources: []*proto.Resource{{ - Name: "hello", - Type: "ec2_instance", - }}, - }, - }, - }}, - }) - coderdtest.AwaitProjectImportJob(t, client, user.Organization, job.ID) - resources, err := client.ProjectImportJobResources(context.Background(), user.Organization, job.ID) - require.NoError(t, err) - // One for start, and one for stop! - require.Len(t, resources, 2) - }) -} - -func TestProvisionerJobLogsByName(t *testing.T) { - t.Parallel() - t.Run("List", func(t *testing.T) { + t.Run("StreamAfterComplete", func(t *testing.T) { t.Parallel() client := coderdtest.New(t, nil) - user := coderdtest.CreateInitialUser(t, client) + user := coderdtest.CreateFirstUser(t, client) coderdtest.NewProvisionerDaemon(t, client) - job := coderdtest.CreateProjectImportJob(t, client, user.Organization, &echo.Responses{ + version := coderdtest.CreateProjectVersion(t, client, user.OrganizationID, &echo.Responses{ Parse: echo.ParseComplete, Provision: []*proto.Provision_Response{{ Type: &proto.Provision_Response_Log{ @@ -221,28 +36,35 @@ func TestProvisionerJobLogsByName(t *testing.T) { }, }}, }) - project := coderdtest.CreateProject(t, client, user.Organization, job.ID) - coderdtest.AwaitProjectImportJob(t, client, user.Organization, job.ID) + project := coderdtest.CreateProject(t, client, user.OrganizationID, version.ID) + coderdtest.AwaitProjectVersionJob(t, client, version.ID) workspace := coderdtest.CreateWorkspace(t, client, "me", project.ID) - history, err := client.CreateWorkspaceHistory(context.Background(), "", workspace.Name, coderd.CreateWorkspaceHistoryRequest{ + before := time.Now().UTC() + build, err := client.CreateWorkspaceBuild(context.Background(), workspace.ID, coderd.CreateWorkspaceBuildRequest{ ProjectVersionID: project.ActiveVersionID, Transition: database.WorkspaceTransitionStart, }) require.NoError(t, err) - coderdtest.AwaitProjectImportJob(t, client, user.Organization, history.ProvisionJobID) - // Return the log after completion! - logs, err := client.WorkspaceProvisionJobLogsBefore(context.Background(), user.Organization, history.ProvisionJobID, time.Time{}) + coderdtest.AwaitWorkspaceBuildJob(t, client, build.ID) + + ctx, cancelFunc := context.WithCancel(context.Background()) + t.Cleanup(cancelFunc) + logs, err := client.WorkspaceBuildLogsAfter(ctx, build.ID, before) require.NoError(t, err) - require.NotNil(t, logs) - require.Len(t, logs, 1) + log, ok := <-logs + require.True(t, ok) + require.Equal(t, "log-output", log.Output) + // Make sure the channel automatically closes! + _, ok = <-logs + require.False(t, ok) }) - t.Run("StreamAfterComplete", func(t *testing.T) { + t.Run("StreamWhileRunning", func(t *testing.T) { t.Parallel() client := coderdtest.New(t, nil) - user := coderdtest.CreateInitialUser(t, client) + user := coderdtest.CreateFirstUser(t, client) coderdtest.NewProvisionerDaemon(t, client) - job := coderdtest.CreateProjectImportJob(t, client, user.Organization, &echo.Responses{ + version := coderdtest.CreateProjectVersion(t, client, user.OrganizationID, &echo.Responses{ Parse: echo.ParseComplete, Provision: []*proto.Provision_Response{{ Type: &proto.Provision_Response_Log{ @@ -257,33 +79,31 @@ func TestProvisionerJobLogsByName(t *testing.T) { }, }}, }) - project := coderdtest.CreateProject(t, client, user.Organization, job.ID) - coderdtest.AwaitProjectImportJob(t, client, user.Organization, job.ID) + project := coderdtest.CreateProject(t, client, user.OrganizationID, version.ID) + coderdtest.AwaitProjectVersionJob(t, client, version.ID) workspace := coderdtest.CreateWorkspace(t, client, "me", project.ID) - before := time.Now().UTC() - history, err := client.CreateWorkspaceHistory(context.Background(), "", workspace.Name, coderd.CreateWorkspaceHistoryRequest{ + before := database.Now() + build, err := client.CreateWorkspaceBuild(context.Background(), workspace.ID, coderd.CreateWorkspaceBuildRequest{ ProjectVersionID: project.ActiveVersionID, Transition: database.WorkspaceTransitionStart, }) require.NoError(t, err) - coderdtest.AwaitProjectImportJob(t, client, user.Organization, history.ProvisionJobID) - - logs, err := client.WorkspaceProvisionJobLogsAfter(context.Background(), user.Organization, history.ProvisionJobID, before) + ctx, cancelFunc := context.WithCancel(context.Background()) + t.Cleanup(cancelFunc) + logs, err := client.WorkspaceBuildLogsAfter(ctx, build.ID, before) require.NoError(t, err) - log, ok := <-logs - require.True(t, ok) + log := <-logs require.Equal(t, "log-output", log.Output) - // Make sure the channel automatically closes! - _, ok = <-logs + _, ok := <-logs require.False(t, ok) }) - t.Run("StreamWhileRunning", func(t *testing.T) { + t.Run("List", func(t *testing.T) { t.Parallel() client := coderdtest.New(t, nil) - user := coderdtest.CreateInitialUser(t, client) + user := coderdtest.CreateFirstUser(t, client) coderdtest.NewProvisionerDaemon(t, client) - job := coderdtest.CreateProjectImportJob(t, client, user.Organization, &echo.Responses{ + version := coderdtest.CreateProjectVersion(t, client, user.OrganizationID, &echo.Responses{ Parse: echo.ParseComplete, Provision: []*proto.Provision_Response{{ Type: &proto.Provision_Response_Log{ @@ -298,21 +118,17 @@ func TestProvisionerJobLogsByName(t *testing.T) { }, }}, }) - project := coderdtest.CreateProject(t, client, user.Organization, job.ID) - coderdtest.AwaitProjectImportJob(t, client, user.Organization, job.ID) + project := coderdtest.CreateProject(t, client, user.OrganizationID, version.ID) + coderdtest.AwaitProjectVersionJob(t, client, version.ID) workspace := coderdtest.CreateWorkspace(t, client, "me", project.ID) - before := database.Now() - history, err := client.CreateWorkspaceHistory(context.Background(), "", workspace.Name, coderd.CreateWorkspaceHistoryRequest{ + build, err := client.CreateWorkspaceBuild(context.Background(), workspace.ID, coderd.CreateWorkspaceBuildRequest{ ProjectVersionID: project.ActiveVersionID, Transition: database.WorkspaceTransitionStart, }) require.NoError(t, err) - logs, err := client.WorkspaceProvisionJobLogsAfter(context.Background(), user.Organization, history.ProvisionJobID, before) + coderdtest.AwaitWorkspaceBuildJob(t, client, build.ID) + logs, err := client.WorkspaceBuildLogsBefore(context.Background(), build.ID, time.Now()) 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) + require.Len(t, logs, 1) }) } diff --git a/coderd/users.go b/coderd/users.go index a660f31fdfa94..3398b2447f684 100644 --- a/coderd/users.go +++ b/coderd/users.go @@ -8,6 +8,7 @@ import ( "net/http" "time" + "github.com/go-chi/chi/v5" "github.com/go-chi/render" "github.com/google/uuid" "golang.org/x/xerrors" @@ -27,21 +28,24 @@ type User struct { Username string `json:"username" validate:"required"` } -// CreateInitialUserRequest provides options to create the initial -// user for a Coder deployment. The organization provided will be -// created as well. -type CreateInitialUserRequest struct { +type CreateFirstUserRequest struct { Email string `json:"email" validate:"required,email"` Username string `json:"username" validate:"required,username"` Password string `json:"password" validate:"required"` Organization string `json:"organization" validate:"required,username"` } -// CreateUserRequest provides options for creating a new user. +// CreateFirstUserResponse contains IDs for newly created user info. +type CreateFirstUserResponse struct { + UserID string `json:"user_id"` + OrganizationID string `json:"organization_id"` +} + type CreateUserRequest struct { - Email string `json:"email" validate:"required,email"` - Username string `json:"username" validate:"required,username"` - Password string `json:"password" validate:"required"` + Email string `json:"email" validate:"required,email"` + Username string `json:"username" validate:"required,username"` + Password string `json:"password" validate:"required"` + OrganizationID string `json:"organization_id" validate:"required"` } // LoginWithPasswordRequest enables callers to authenticate with email and password. @@ -60,8 +64,18 @@ type GenerateAPIKeyResponse struct { Key string `json:"key"` } +type CreateOrganizationRequest struct { + Name string `json:"name" validate:"required,username"` +} + +// CreateWorkspaceRequest provides options for creating a new workspace. +type CreateWorkspaceRequest struct { + ProjectID uuid.UUID `json:"project_id" validate:"required"` + Name string `json:"name" validate:"username,required"` +} + // Returns whether the initial user has been created or not. -func (api *api) user(rw http.ResponseWriter, r *http.Request) { +func (api *api) firstUser(rw http.ResponseWriter, r *http.Request) { userCount, err := api.Database.GetUserCount(r.Context()) if err != nil { httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ @@ -81,8 +95,8 @@ func (api *api) user(rw http.ResponseWriter, r *http.Request) { } // Creates the initial user for a Coder deployment. -func (api *api) postUser(rw http.ResponseWriter, r *http.Request) { - var createUser CreateInitialUserRequest +func (api *api) postFirstUser(rw http.ResponseWriter, r *http.Request) { + var createUser CreateFirstUserRequest if !httpapi.Read(rw, r, &createUser) { return } @@ -111,6 +125,7 @@ func (api *api) postUser(rw http.ResponseWriter, r *http.Request) { // Create the user, organization, and membership to the user. var user database.User + var organization database.Organization err = api.Database.InTx(func(s database.Store) error { user, err = api.Database.InsertUser(r.Context(), database.InsertUserParams{ ID: uuid.NewString(), @@ -124,7 +139,7 @@ func (api *api) postUser(rw http.ResponseWriter, r *http.Request) { if err != nil { return xerrors.Errorf("create user: %w", err) } - organization, err := api.Database.InsertOrganization(r.Context(), database.InsertOrganizationParams{ + organization, err = api.Database.InsertOrganization(r.Context(), database.InsertOrganizationParams{ ID: uuid.NewString(), Name: createUser.Organization, CreatedAt: database.Now(), @@ -153,11 +168,16 @@ func (api *api) postUser(rw http.ResponseWriter, r *http.Request) { } render.Status(r, http.StatusCreated) - render.JSON(rw, r, convertUser(user)) + render.JSON(rw, r, CreateFirstUserResponse{ + UserID: user.ID, + OrganizationID: organization.ID, + }) } // Creates a new user. func (api *api) postUsers(rw http.ResponseWriter, r *http.Request) { + apiKey := httpmw.APIKey(r) + var createUser CreateUserRequest if !httpapi.Read(rw, r, &createUser) { return @@ -179,6 +199,37 @@ func (api *api) postUsers(rw http.ResponseWriter, r *http.Request) { return } + organization, err := api.Database.GetOrganizationByID(r.Context(), createUser.OrganizationID) + if errors.Is(err, sql.ErrNoRows) { + httpapi.Write(rw, http.StatusNotFound, httpapi.Response{ + Message: "organization does not exist with the provided id", + }) + return + } + if err != nil { + httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ + Message: fmt.Sprintf("get organization: %s", err), + }) + return + } + // Check if the caller has permissions to the organization requested. + _, err = api.Database.GetOrganizationMemberByUserID(r.Context(), database.GetOrganizationMemberByUserIDParams{ + OrganizationID: organization.ID, + UserID: apiKey.UserID, + }) + if errors.Is(err, sql.ErrNoRows) { + httpapi.Write(rw, http.StatusUnauthorized, httpapi.Response{ + Message: "you are not authorized to add members to that organization", + }) + return + } + if err != nil { + httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ + Message: fmt.Sprintf("get organization member: %s", err), + }) + return + } + hashedPassword, err := userpassword.Hash(createUser.Password) if err != nil { httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ @@ -187,18 +238,35 @@ func (api *api) postUsers(rw http.ResponseWriter, r *http.Request) { return } - user, err := api.Database.InsertUser(r.Context(), database.InsertUserParams{ - ID: uuid.NewString(), - Email: createUser.Email, - HashedPassword: []byte(hashedPassword), - Username: createUser.Username, - LoginType: database.LoginTypeBuiltIn, - CreatedAt: database.Now(), - UpdatedAt: database.Now(), + var user database.User + err = api.Database.InTx(func(db database.Store) error { + user, err = db.InsertUser(r.Context(), database.InsertUserParams{ + ID: uuid.NewString(), + Email: createUser.Email, + HashedPassword: []byte(hashedPassword), + Username: createUser.Username, + LoginType: database.LoginTypeBuiltIn, + CreatedAt: database.Now(), + UpdatedAt: database.Now(), + }) + if err != nil { + return xerrors.Errorf("create user: %w", err) + } + _, err = db.InsertOrganizationMember(r.Context(), database.InsertOrganizationMemberParams{ + OrganizationID: organization.ID, + UserID: user.ID, + CreatedAt: database.Now(), + UpdatedAt: database.Now(), + Roles: []string{}, + }) + if err != nil { + return xerrors.Errorf("create organization member: %w", err) + } + return nil }) if err != nil { httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ - Message: fmt.Sprintf("create user: %s", err.Error()), + Message: err.Error(), }) return } @@ -240,6 +308,97 @@ func (api *api) organizationsByUser(rw http.ResponseWriter, r *http.Request) { render.JSON(rw, r, publicOrganizations) } +func (api *api) organizationByUserAndName(rw http.ResponseWriter, r *http.Request) { + user := httpmw.UserParam(r) + organizationName := chi.URLParam(r, "organizationname") + organization, err := api.Database.GetOrganizationByName(r.Context(), organizationName) + if errors.Is(err, sql.ErrNoRows) { + httpapi.Write(rw, http.StatusNotFound, httpapi.Response{ + Message: fmt.Sprintf("no organization found by name %q", organizationName), + }) + return + } + if err != nil { + httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ + Message: fmt.Sprintf("get organization by name: %s", err), + }) + return + } + _, err = api.Database.GetOrganizationMemberByUserID(r.Context(), database.GetOrganizationMemberByUserIDParams{ + OrganizationID: organization.ID, + UserID: user.ID, + }) + if errors.Is(err, sql.ErrNoRows) { + httpapi.Write(rw, http.StatusUnauthorized, httpapi.Response{ + Message: "you are not a member of that organization", + }) + return + } + if err != nil { + httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ + Message: fmt.Sprintf("get organization member: %s", err), + }) + return + } + + render.Status(r, http.StatusOK) + render.JSON(rw, r, convertOrganization(organization)) +} + +func (api *api) postOrganizationsByUser(rw http.ResponseWriter, r *http.Request) { + user := httpmw.UserParam(r) + var req CreateOrganizationRequest + if !httpapi.Read(rw, r, &req) { + return + } + _, err := api.Database.GetOrganizationByName(r.Context(), req.Name) + if err == nil { + httpapi.Write(rw, http.StatusConflict, httpapi.Response{ + Message: "organization already exists with that name", + }) + return + } + if !errors.Is(err, sql.ErrNoRows) { + httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ + Message: fmt.Sprintf("get organization: %s", err.Error()), + }) + return + } + + var organization database.Organization + err = api.Database.InTx(func(db database.Store) error { + organization, err = api.Database.InsertOrganization(r.Context(), database.InsertOrganizationParams{ + ID: uuid.NewString(), + Name: req.Name, + CreatedAt: database.Now(), + UpdatedAt: database.Now(), + }) + if err != nil { + return xerrors.Errorf("create organization: %w", err) + } + _, err = api.Database.InsertOrganizationMember(r.Context(), database.InsertOrganizationMemberParams{ + OrganizationID: organization.ID, + UserID: user.ID, + CreatedAt: database.Now(), + UpdatedAt: database.Now(), + Roles: []string{"organization-admin"}, + }) + if err != nil { + return xerrors.Errorf("create organization member: %w", err) + } + return nil + }) + if err != nil { + httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ + Message: err.Error(), + }) + return + } + + render.Status(r, http.StatusCreated) + render.JSON(rw, r, convertOrganization(organization)) +} + // Authenticates the user with an email and password. func (api *api) postLogin(rw http.ResponseWriter, r *http.Request) { var loginWithPassword LoginWithPasswordRequest @@ -318,7 +477,7 @@ func (api *api) postLogin(rw http.ResponseWriter, r *http.Request) { } // Creates a new session key, used for logging in via the CLI -func (api *api) postKeyForUser(rw http.ResponseWriter, r *http.Request) { +func (api *api) postAPIKey(rw http.ResponseWriter, r *http.Request) { user := httpmw.UserParam(r) apiKey := httpmw.APIKey(r) @@ -375,6 +534,141 @@ func (*api) postLogout(rw http.ResponseWriter, r *http.Request) { render.Status(r, http.StatusOK) } +// Create a new workspace for the currently authenticated user. +func (api *api) postWorkspacesByUser(rw http.ResponseWriter, r *http.Request) { + var createWorkspace CreateWorkspaceRequest + if !httpapi.Read(rw, r, &createWorkspace) { + return + } + apiKey := httpmw.APIKey(r) + project, err := api.Database.GetProjectByID(r.Context(), createWorkspace.ProjectID) + if errors.Is(err, sql.ErrNoRows) { + httpapi.Write(rw, http.StatusBadRequest, httpapi.Response{ + Message: fmt.Sprintf("project %q doesn't exist", createWorkspace.ProjectID.String()), + Errors: []httpapi.Error{{ + Field: "project_id", + Code: "not_found", + }}, + }) + return + } + if err != nil { + httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ + Message: fmt.Sprintf("get project: %s", err), + }) + return + } + _, err = api.Database.GetOrganizationMemberByUserID(r.Context(), database.GetOrganizationMemberByUserIDParams{ + OrganizationID: project.OrganizationID, + UserID: apiKey.UserID, + }) + if errors.Is(err, sql.ErrNoRows) { + httpapi.Write(rw, http.StatusUnauthorized, httpapi.Response{ + Message: "you aren't allowed to access projects in that organization", + }) + return + } + if err != nil { + httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ + Message: fmt.Sprintf("get organization member: %s", err), + }) + return + } + + workspace, err := api.Database.GetWorkspaceByUserIDAndName(r.Context(), database.GetWorkspaceByUserIDAndNameParams{ + OwnerID: apiKey.UserID, + Name: createWorkspace.Name, + }) + if err == nil { + // If the workspace already exists, don't allow creation. + project, err := api.Database.GetProjectByID(r.Context(), workspace.ProjectID) + if err != nil { + httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ + Message: fmt.Sprintf("find project for conflicting workspace name %q: %s", createWorkspace.Name, err), + }) + return + } + // The project is fetched for clarity to the user on where the conflicting name may be. + httpapi.Write(rw, http.StatusConflict, httpapi.Response{ + Message: fmt.Sprintf("workspace %q already exists in the %q project", createWorkspace.Name, project.Name), + Errors: []httpapi.Error{{ + Field: "name", + Code: "exists", + }}, + }) + return + } + if !errors.Is(err, sql.ErrNoRows) { + httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ + Message: fmt.Sprintf("get workspace by name: %s", err.Error()), + }) + return + } + + // Workspaces are created without any versions. + workspace, err = api.Database.InsertWorkspace(r.Context(), database.InsertWorkspaceParams{ + ID: uuid.New(), + CreatedAt: database.Now(), + UpdatedAt: database.Now(), + OwnerID: apiKey.UserID, + ProjectID: project.ID, + Name: createWorkspace.Name, + }) + if err != nil { + httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ + Message: fmt.Sprintf("insert workspace: %s", err), + }) + return + } + + render.Status(r, http.StatusCreated) + render.JSON(rw, r, convertWorkspace(workspace)) +} + +func (api *api) workspacesByUser(rw http.ResponseWriter, r *http.Request) { + user := httpmw.UserParam(r) + workspaces, err := api.Database.GetWorkspacesByUserID(r.Context(), user.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) +} + +func (api *api) workspaceByUserAndName(rw http.ResponseWriter, r *http.Request) { + user := httpmw.UserParam(r) + workspaceName := chi.URLParam(r, "workspacename") + workspace, err := api.Database.GetWorkspaceByUserIDAndName(r.Context(), database.GetWorkspaceByUserIDAndNameParams{ + OwnerID: user.ID, + Name: workspaceName, + }) + if errors.Is(err, sql.ErrNoRows) { + httpapi.Write(rw, http.StatusNotFound, httpapi.Response{ + Message: fmt.Sprintf("no workspace found by name %q", workspaceName), + }) + return + } + if err != nil { + httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ + Message: fmt.Sprintf("get workspace by name: %s", err), + }) + return + } + + render.Status(r, http.StatusOK) + render.JSON(rw, r, convertWorkspace(workspace)) +} + // Generates a new ID and secret for an API key. func generateAPIKeyIDSecret() (id string, secret string, err error) { // Length of an API Key ID. diff --git a/coderd/users_test.go b/coderd/users_test.go index 5316b781a04b3..f28302bbff428 100644 --- a/coderd/users_test.go +++ b/coderd/users_test.go @@ -5,6 +5,7 @@ import ( "net/http" "testing" + "github.com/google/uuid" "github.com/stretchr/testify/require" "github.com/coder/coder/coderd" @@ -13,89 +14,188 @@ import ( "github.com/coder/coder/httpmw" ) -func TestUser(t *testing.T) { +func TestFirstUser(t *testing.T) { t.Parallel() - t.Run("NotFound", func(t *testing.T) { + t.Run("BadRequest", func(t *testing.T) { t.Parallel() client := coderdtest.New(t, nil) - has, err := client.HasInitialUser(context.Background()) - require.NoError(t, err) - require.False(t, has) + _, err := client.CreateFirstUser(context.Background(), coderd.CreateFirstUserRequest{}) + require.Error(t, err) }) - t.Run("Found", func(t *testing.T) { + t.Run("AlreadyExists", func(t *testing.T) { t.Parallel() client := coderdtest.New(t, nil) - _ = coderdtest.CreateInitialUser(t, client) - has, err := client.HasInitialUser(context.Background()) - require.NoError(t, err) - require.True(t, has) + _ = coderdtest.CreateFirstUser(t, client) + _, err := client.CreateFirstUser(context.Background(), coderd.CreateFirstUserRequest{ + Email: "some@email.com", + Username: "exampleuser", + Password: "password", + Organization: "someorg", + }) + var apiErr *codersdk.Error + require.ErrorAs(t, err, &apiErr) + require.Equal(t, http.StatusConflict, apiErr.StatusCode()) + }) + + t.Run("Create", func(t *testing.T) { + t.Parallel() + client := coderdtest.New(t, nil) + _ = coderdtest.CreateFirstUser(t, client) }) } -func TestPostUser(t *testing.T) { +func TestPostLogin(t *testing.T) { t.Parallel() - t.Run("BadRequest", func(t *testing.T) { + t.Run("InvalidUser", func(t *testing.T) { t.Parallel() client := coderdtest.New(t, nil) - _, err := client.CreateInitialUser(context.Background(), coderd.CreateInitialUserRequest{}) - require.Error(t, err) + _, err := client.LoginWithPassword(context.Background(), coderd.LoginWithPasswordRequest{ + Email: "my@email.org", + Password: "password", + }) + var apiErr *codersdk.Error + require.ErrorAs(t, err, &apiErr) + require.Equal(t, http.StatusUnauthorized, apiErr.StatusCode()) }) - t.Run("AlreadyExists", func(t *testing.T) { + t.Run("BadPassword", func(t *testing.T) { t.Parallel() client := coderdtest.New(t, nil) - _ = coderdtest.CreateInitialUser(t, client) - _, err := client.CreateInitialUser(context.Background(), coderd.CreateInitialUserRequest{ - Email: "some@email.com", - Username: "exampleuser", - Password: "password", - Organization: "someorg", + req := coderd.CreateFirstUserRequest{ + Email: "testuser@coder.com", + Username: "testuser", + Password: "testpass", + Organization: "testorg", + } + _, err := client.CreateFirstUser(context.Background(), req) + require.NoError(t, err) + _, err = client.LoginWithPassword(context.Background(), coderd.LoginWithPasswordRequest{ + Email: req.Email, + Password: "badpass", }) var apiErr *codersdk.Error require.ErrorAs(t, err, &apiErr) - require.Equal(t, http.StatusConflict, apiErr.StatusCode()) + require.Equal(t, http.StatusUnauthorized, apiErr.StatusCode()) }) - t.Run("Create", func(t *testing.T) { + t.Run("Success", func(t *testing.T) { t.Parallel() client := coderdtest.New(t, nil) - _ = coderdtest.CreateInitialUser(t, client) + req := coderd.CreateFirstUserRequest{ + Email: "testuser@coder.com", + Username: "testuser", + Password: "testpass", + Organization: "testorg", + } + _, err := client.CreateFirstUser(context.Background(), req) + require.NoError(t, err) + _, err = client.LoginWithPassword(context.Background(), coderd.LoginWithPasswordRequest{ + Email: req.Email, + Password: req.Password, + }) + require.NoError(t, err) + }) +} + +func TestPostLogout(t *testing.T) { + t.Parallel() + + t.Run("ClearCookie", func(t *testing.T) { + t.Parallel() + + client := coderdtest.New(t, nil) + fullURL, err := client.URL.Parse("/api/v2/users/logout") + require.NoError(t, err, "Server URL should parse successfully") + + req, err := http.NewRequestWithContext(context.Background(), http.MethodPost, fullURL.String(), nil) + require.NoError(t, err, "/logout request construction should succeed") + + httpClient := &http.Client{} + + response, err := httpClient.Do(req) + require.NoError(t, err, "/logout request should succeed") + response.Body.Close() + + cookies := response.Cookies() + require.Len(t, cookies, 1, "Exactly one cookie should be returned") + + require.Equal(t, cookies[0].Name, httpmw.AuthCookie, "Cookie should be the auth cookie") + require.Equal(t, cookies[0].MaxAge, -1, "Cookie should be set to delete") }) } func TestPostUsers(t *testing.T) { t.Parallel() - t.Run("BadRequest", func(t *testing.T) { + t.Run("NoAuth", func(t *testing.T) { t.Parallel() client := coderdtest.New(t, nil) - _, err := client.CreateInitialUser(context.Background(), coderd.CreateInitialUserRequest{}) + _, err := client.CreateUser(context.Background(), coderd.CreateUserRequest{}) require.Error(t, err) }) t.Run("Conflicting", func(t *testing.T) { t.Parallel() client := coderdtest.New(t, nil) - user := coderdtest.CreateInitialUser(t, client) - _, err := client.CreateInitialUser(context.Background(), coderd.CreateInitialUserRequest{ - Email: user.Email, - Username: user.Username, - Password: "password", - Organization: "someorg", + coderdtest.CreateFirstUser(t, client) + me, err := client.User(context.Background(), "") + require.NoError(t, err) + _, err = client.CreateUser(context.Background(), coderd.CreateUserRequest{ + Email: me.Email, + Username: me.Username, + Password: "password", + OrganizationID: "someorg", }) var apiErr *codersdk.Error require.ErrorAs(t, err, &apiErr) require.Equal(t, http.StatusConflict, apiErr.StatusCode()) }) + t.Run("OrganizationNotFound", func(t *testing.T) { + t.Parallel() + client := coderdtest.New(t, nil) + coderdtest.CreateFirstUser(t, client) + _, err := client.CreateUser(context.Background(), coderd.CreateUserRequest{ + OrganizationID: "not-exists", + Email: "another@user.org", + Username: "someone-else", + Password: "testing", + }) + var apiErr *codersdk.Error + require.ErrorAs(t, err, &apiErr) + require.Equal(t, http.StatusNotFound, apiErr.StatusCode()) + }) + + t.Run("OrganizationNoAccess", func(t *testing.T) { + t.Parallel() + client := coderdtest.New(t, nil) + first := coderdtest.CreateFirstUser(t, client) + other := coderdtest.CreateAnotherUser(t, client, first.OrganizationID) + org, err := other.CreateOrganization(context.Background(), "", coderd.CreateOrganizationRequest{ + Name: "another", + }) + require.NoError(t, err) + + _, err = client.CreateUser(context.Background(), coderd.CreateUserRequest{ + Email: "some@domain.com", + Username: "anotheruser", + Password: "testing", + OrganizationID: org.ID, + }) + var apiErr *codersdk.Error + require.ErrorAs(t, err, &apiErr) + require.Equal(t, http.StatusUnauthorized, apiErr.StatusCode()) + }) + t.Run("Create", func(t *testing.T) { t.Parallel() client := coderdtest.New(t, nil) - _ = coderdtest.CreateInitialUser(t, client) + user := coderdtest.CreateFirstUser(t, client) _, err := client.CreateUser(context.Background(), coderd.CreateUserRequest{ - Email: "another@user.org", - Username: "someone-else", - Password: "testing", + OrganizationID: user.OrganizationID, + Email: "another@user.org", + Username: "someone-else", + Password: "testing", }) require.NoError(t, err) }) @@ -104,7 +204,7 @@ func TestPostUsers(t *testing.T) { func TestUserByName(t *testing.T) { t.Parallel() client := coderdtest.New(t, nil) - _ = coderdtest.CreateInitialUser(t, client) + _ = coderdtest.CreateFirstUser(t, client) _, err := client.User(context.Background(), "") require.NoError(t, err) } @@ -112,62 +212,87 @@ func TestUserByName(t *testing.T) { func TestOrganizationsByUser(t *testing.T) { t.Parallel() client := coderdtest.New(t, nil) - _ = coderdtest.CreateInitialUser(t, client) - orgs, err := client.UserOrganizations(context.Background(), "") + _ = coderdtest.CreateFirstUser(t, client) + orgs, err := client.OrganizationsByUser(context.Background(), "") require.NoError(t, err) require.NotNil(t, orgs) require.Len(t, orgs, 1) } -func TestPostKey(t *testing.T) { +func TestOrganizationByUserAndName(t *testing.T) { t.Parallel() - t.Run("InvalidUser", func(t *testing.T) { + t.Run("NoExist", func(t *testing.T) { t.Parallel() client := coderdtest.New(t, nil) - _ = coderdtest.CreateInitialUser(t, client) + coderdtest.CreateFirstUser(t, client) + _, err := client.OrganizationByName(context.Background(), "", "nothing") + var apiErr *codersdk.Error + require.ErrorAs(t, err, &apiErr) + require.Equal(t, http.StatusNotFound, apiErr.StatusCode()) + }) - // Clear session token - client.SessionToken = "" - // ...and request an API key - _, err := client.CreateAPIKey(context.Background()) + t.Run("NoMember", func(t *testing.T) { + t.Parallel() + client := coderdtest.New(t, nil) + first := coderdtest.CreateFirstUser(t, client) + other := coderdtest.CreateAnotherUser(t, client, first.OrganizationID) + org, err := other.CreateOrganization(context.Background(), "", coderd.CreateOrganizationRequest{ + Name: "another", + }) + require.NoError(t, err) + _, err = client.OrganizationByName(context.Background(), "", org.Name) var apiErr *codersdk.Error require.ErrorAs(t, err, &apiErr) require.Equal(t, http.StatusUnauthorized, apiErr.StatusCode()) }) - t.Run("Success", func(t *testing.T) { + t.Run("Valid", func(t *testing.T) { t.Parallel() client := coderdtest.New(t, nil) - _ = coderdtest.CreateInitialUser(t, client) - apiKey, err := client.CreateAPIKey(context.Background()) - require.NotNil(t, apiKey) - require.GreaterOrEqual(t, len(apiKey.Key), 2) + user := coderdtest.CreateFirstUser(t, client) + org, err := client.Organization(context.Background(), user.OrganizationID) + require.NoError(t, err) + _, err = client.OrganizationByName(context.Background(), "", org.Name) require.NoError(t, err) }) } -func TestPostLogin(t *testing.T) { +func TestPostOrganizationsByUser(t *testing.T) { t.Parallel() - t.Run("InvalidUser", func(t *testing.T) { + t.Run("Conflict", func(t *testing.T) { t.Parallel() client := coderdtest.New(t, nil) - _, err := client.LoginWithPassword(context.Background(), coderd.LoginWithPasswordRequest{ - Email: "my@email.org", - Password: "password", + user := coderdtest.CreateFirstUser(t, client) + org, err := client.Organization(context.Background(), user.OrganizationID) + require.NoError(t, err) + _, err = client.CreateOrganization(context.Background(), "", coderd.CreateOrganizationRequest{ + Name: org.Name, }) var apiErr *codersdk.Error require.ErrorAs(t, err, &apiErr) - require.Equal(t, http.StatusUnauthorized, apiErr.StatusCode()) + require.Equal(t, http.StatusConflict, apiErr.StatusCode()) }) - t.Run("BadPassword", func(t *testing.T) { + t.Run("Create", func(t *testing.T) { t.Parallel() client := coderdtest.New(t, nil) - user := coderdtest.CreateInitialUser(t, client) - _, err := client.LoginWithPassword(context.Background(), coderd.LoginWithPasswordRequest{ - Email: user.Email, - Password: "badpass", + _ = coderdtest.CreateFirstUser(t, client) + _, err := client.CreateOrganization(context.Background(), "", coderd.CreateOrganizationRequest{ + Name: "new", }) + require.NoError(t, err) + }) +} + +func TestPostAPIKey(t *testing.T) { + t.Parallel() + t.Run("InvalidUser", func(t *testing.T) { + t.Parallel() + client := coderdtest.New(t, nil) + _ = coderdtest.CreateFirstUser(t, client) + + client.SessionToken = "" + _, err := client.CreateAPIKey(context.Background(), "") var apiErr *codersdk.Error require.ErrorAs(t, err, &apiErr) require.Equal(t, http.StatusUnauthorized, apiErr.StatusCode()) @@ -176,38 +301,121 @@ func TestPostLogin(t *testing.T) { t.Run("Success", func(t *testing.T) { t.Parallel() client := coderdtest.New(t, nil) - user := coderdtest.CreateInitialUser(t, client) - _, err := client.LoginWithPassword(context.Background(), coderd.LoginWithPasswordRequest{ - Email: user.Email, - Password: user.Password, - }) + _ = coderdtest.CreateFirstUser(t, client) + apiKey, err := client.CreateAPIKey(context.Background(), "") + require.NotNil(t, apiKey) + require.GreaterOrEqual(t, len(apiKey.Key), 2) require.NoError(t, err) }) } -func TestPostLogout(t *testing.T) { +func TestPostWorkspacesByUser(t *testing.T) { t.Parallel() - - t.Run("ClearCookie", func(t *testing.T) { + t.Run("InvalidProject", func(t *testing.T) { t.Parallel() + client := coderdtest.New(t, nil) + _ = coderdtest.CreateFirstUser(t, client) + _, err := client.CreateWorkspace(context.Background(), "", coderd.CreateWorkspaceRequest{ + ProjectID: uuid.New(), + Name: "workspace", + }) + require.Error(t, err) + var apiErr *codersdk.Error + require.ErrorAs(t, err, &apiErr) + require.Equal(t, http.StatusBadRequest, apiErr.StatusCode()) + }) + t.Run("NoProjectAccess", func(t *testing.T) { + t.Parallel() client := coderdtest.New(t, nil) - fullURL, err := client.URL.Parse("/api/v2/logout") - require.NoError(t, err, "Server URL should parse successfully") + first := coderdtest.CreateFirstUser(t, client) - req, err := http.NewRequestWithContext(context.Background(), http.MethodPost, fullURL.String(), nil) - require.NoError(t, err, "/logout request construction should succeed") + other := coderdtest.CreateAnotherUser(t, client, first.OrganizationID) + org, err := other.CreateOrganization(context.Background(), "", coderd.CreateOrganizationRequest{ + Name: "another", + }) + require.NoError(t, err) + version := coderdtest.CreateProjectVersion(t, other, org.ID, nil) + project := coderdtest.CreateProject(t, other, org.ID, version.ID) - httpClient := &http.Client{} + _, err = client.CreateWorkspace(context.Background(), "", coderd.CreateWorkspaceRequest{ + ProjectID: project.ID, + Name: "workspace", + }) + require.Error(t, err) + var apiErr *codersdk.Error + require.ErrorAs(t, err, &apiErr) + require.Equal(t, http.StatusUnauthorized, apiErr.StatusCode()) + }) - response, err := httpClient.Do(req) - require.NoError(t, err, "/logout request should succeed") - response.Body.Close() + t.Run("AlreadyExists", func(t *testing.T) { + t.Parallel() + client := coderdtest.New(t, nil) + user := coderdtest.CreateFirstUser(t, client) + job := coderdtest.CreateProjectVersion(t, client, user.OrganizationID, nil) + project := coderdtest.CreateProject(t, client, user.OrganizationID, job.ID) + 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()) + }) - cookies := response.Cookies() - require.Len(t, cookies, 1, "Exactly one cookie should be returned") + t.Run("Create", func(t *testing.T) { + t.Parallel() + client := coderdtest.New(t, nil) + user := coderdtest.CreateFirstUser(t, client) + job := coderdtest.CreateProjectVersion(t, client, user.OrganizationID, nil) + project := coderdtest.CreateProject(t, client, user.OrganizationID, job.ID) + _ = coderdtest.CreateWorkspace(t, client, "", project.ID) + }) +} - require.Equal(t, cookies[0].Name, httpmw.AuthCookie, "Cookie should be the auth cookie") - require.Equal(t, cookies[0].MaxAge, -1, "Cookie should be set to delete") +func TestWorkspacesByUser(t *testing.T) { + t.Parallel() + t.Run("ListEmpty", func(t *testing.T) { + t.Parallel() + client := coderdtest.New(t, nil) + coderdtest.CreateFirstUser(t, client) + _, err := client.WorkspacesByUser(context.Background(), "") + require.NoError(t, err) + }) + t.Run("List", func(t *testing.T) { + t.Parallel() + client := coderdtest.New(t, nil) + user := coderdtest.CreateFirstUser(t, client) + job := coderdtest.CreateProjectVersion(t, client, user.OrganizationID, nil) + project := coderdtest.CreateProject(t, client, user.OrganizationID, job.ID) + _ = coderdtest.CreateWorkspace(t, client, "", project.ID) + workspaces, err := client.WorkspacesByUser(context.Background(), "") + require.NoError(t, err) + require.Len(t, workspaces, 1) + }) +} + +func TestWorkspaceByUserAndName(t *testing.T) { + t.Parallel() + t.Run("NotFound", func(t *testing.T) { + t.Parallel() + client := coderdtest.New(t, nil) + coderdtest.CreateFirstUser(t, client) + _, err := client.WorkspaceByName(context.Background(), "", "something") + var apiErr *codersdk.Error + require.ErrorAs(t, err, &apiErr) + require.Equal(t, http.StatusNotFound, apiErr.StatusCode()) + }) + t.Run("Get", func(t *testing.T) { + t.Parallel() + client := coderdtest.New(t, nil) + user := coderdtest.CreateFirstUser(t, client) + job := coderdtest.CreateProjectVersion(t, client, user.OrganizationID, nil) + project := coderdtest.CreateProject(t, client, user.OrganizationID, job.ID) + workspace := coderdtest.CreateWorkspace(t, client, "", project.ID) + _, err := client.WorkspaceByName(context.Background(), "", workspace.Name) + require.NoError(t, err) }) } diff --git a/coderd/workspacebuilds.go b/coderd/workspacebuilds.go new file mode 100644 index 0000000000000..6db8dbf21ea43 --- /dev/null +++ b/coderd/workspacebuilds.go @@ -0,0 +1,96 @@ +package coderd + +import ( + "fmt" + "net/http" + "time" + + "github.com/go-chi/render" + "github.com/google/uuid" + + "github.com/coder/coder/database" + "github.com/coder/coder/httpapi" + "github.com/coder/coder/httpmw" +) + +// WorkspaceBuild is an at-point representation of a workspace state. +// Iterate on before/after to determine a chronological history. +type WorkspaceBuild struct { + ID uuid.UUID `json:"id"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + WorkspaceID uuid.UUID `json:"workspace_id"` + ProjectVersionID uuid.UUID `json:"project_version_id"` + BeforeID uuid.UUID `json:"before_id"` + AfterID uuid.UUID `json:"after_id"` + Name string `json:"name"` + Transition database.WorkspaceTransition `json:"transition"` + Initiator string `json:"initiator"` + Job ProvisionerJob `json:"job"` +} + +func (api *api) workspaceBuild(rw http.ResponseWriter, r *http.Request) { + workspaceBuild := httpmw.WorkspaceBuildParam(r) + job, err := api.Database.GetProvisionerJobByID(r.Context(), workspaceBuild.JobID) + if err != nil { + httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ + Message: fmt.Sprintf("get provisioner job: %s", err), + }) + return + } + render.Status(r, http.StatusOK) + render.JSON(rw, r, convertWorkspaceBuild(workspaceBuild, convertProvisionerJob(job))) +} + +func (api *api) workspaceBuildResources(rw http.ResponseWriter, r *http.Request) { + workspaceBuild := httpmw.WorkspaceBuildParam(r) + job, err := api.Database.GetProvisionerJobByID(r.Context(), workspaceBuild.JobID) + if err != nil { + httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ + Message: fmt.Sprintf("get provisioner job: %s", err), + }) + return + } + api.provisionerJobResources(rw, r, job) +} + +func (api *api) workspaceBuildLogs(rw http.ResponseWriter, r *http.Request) { + workspaceBuild := httpmw.WorkspaceBuildParam(r) + job, err := api.Database.GetProvisionerJobByID(r.Context(), workspaceBuild.JobID) + if err != nil { + httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ + Message: fmt.Sprintf("get provisioner job: %s", err), + }) + return + } + api.provisionerJobLogs(rw, r, job) +} + +func convertWorkspaceBuild(workspaceBuild database.WorkspaceBuild, job ProvisionerJob) WorkspaceBuild { + //nolint:unconvert + return WorkspaceBuild(WorkspaceBuild{ + ID: workspaceBuild.ID, + CreatedAt: workspaceBuild.CreatedAt, + UpdatedAt: workspaceBuild.UpdatedAt, + WorkspaceID: workspaceBuild.WorkspaceID, + ProjectVersionID: workspaceBuild.ProjectVersionID, + BeforeID: workspaceBuild.BeforeID.UUID, + AfterID: workspaceBuild.AfterID.UUID, + Name: workspaceBuild.Name, + Transition: workspaceBuild.Transition, + Initiator: workspaceBuild.Initiator, + Job: job, + }) +} + +func convertWorkspaceResource(resource database.WorkspaceResource, agent *WorkspaceAgent) WorkspaceResource { + return WorkspaceResource{ + ID: resource.ID, + CreatedAt: resource.CreatedAt, + JobID: resource.JobID, + Transition: resource.Transition, + Type: resource.Type, + Name: resource.Name, + Agent: agent, + } +} diff --git a/coderd/workspacebuilds_test.go b/coderd/workspacebuilds_test.go new file mode 100644 index 0000000000000..3dc62333670fb --- /dev/null +++ b/coderd/workspacebuilds_test.go @@ -0,0 +1,150 @@ +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 TestWorkspaceBuild(t *testing.T) { + t.Parallel() + client := coderdtest.New(t, nil) + user := coderdtest.CreateFirstUser(t, client) + coderdtest.NewProvisionerDaemon(t, client) + version := coderdtest.CreateProjectVersion(t, client, user.OrganizationID, nil) + project := coderdtest.CreateProject(t, client, user.OrganizationID, version.ID) + coderdtest.AwaitProjectVersionJob(t, client, version.ID) + workspace := coderdtest.CreateWorkspace(t, client, "me", project.ID) + build, err := client.CreateWorkspaceBuild(context.Background(), workspace.ID, coderd.CreateWorkspaceBuildRequest{ + ProjectVersionID: project.ActiveVersionID, + Transition: database.WorkspaceTransitionStart, + }) + require.NoError(t, err) + _, err = client.WorkspaceBuild(context.Background(), build.ID) + require.NoError(t, err) +} + +func TestWorkspaceBuildResources(t *testing.T) { + t.Parallel() + t.Run("ListRunning", func(t *testing.T) { + t.Parallel() + client := coderdtest.New(t, nil) + user := coderdtest.CreateFirstUser(t, client) + closeDaemon := coderdtest.NewProvisionerDaemon(t, client) + version := coderdtest.CreateProjectVersion(t, client, user.OrganizationID, nil) + coderdtest.AwaitProjectVersionJob(t, client, version.ID) + closeDaemon.Close() + project := coderdtest.CreateProject(t, client, user.OrganizationID, version.ID) + workspace := coderdtest.CreateWorkspace(t, client, "", project.ID) + build, err := client.CreateWorkspaceBuild(context.Background(), workspace.ID, coderd.CreateWorkspaceBuildRequest{ + ProjectVersionID: project.ActiveVersionID, + Transition: database.WorkspaceTransitionStart, + }) + require.NoError(t, err) + _, err = client.WorkspaceResourcesByBuild(context.Background(), build.ID) + 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, nil) + user := coderdtest.CreateFirstUser(t, client) + coderdtest.NewProvisionerDaemon(t, client) + version := coderdtest.CreateProjectVersion(t, client, user.OrganizationID, &echo.Responses{ + Parse: echo.ParseComplete, + Provision: []*proto.Provision_Response{{ + Type: &proto.Provision_Response_Complete{ + Complete: &proto.Provision_Complete{ + Resources: []*proto.Resource{{ + Name: "some", + Type: "example", + Agent: &proto.Agent{ + Id: "something", + Auth: &proto.Agent_Token{}, + }, + }, { + Name: "another", + Type: "example", + }}, + }, + }, + }}, + }) + coderdtest.AwaitProjectVersionJob(t, client, version.ID) + project := coderdtest.CreateProject(t, client, user.OrganizationID, version.ID) + workspace := coderdtest.CreateWorkspace(t, client, "", project.ID) + build, err := client.CreateWorkspaceBuild(context.Background(), workspace.ID, coderd.CreateWorkspaceBuildRequest{ + ProjectVersionID: project.ActiveVersionID, + Transition: database.WorkspaceTransitionStart, + }) + require.NoError(t, err) + coderdtest.AwaitWorkspaceBuildJob(t, client, build.ID) + resources, err := client.WorkspaceResourcesByBuild(context.Background(), build.ID) + require.NoError(t, err) + require.NotNil(t, resources) + require.Len(t, resources, 2) + require.Equal(t, "some", resources[0].Name) + require.Equal(t, "example", resources[0].Type) + require.NotNil(t, resources[0].Agent) + }) +} + +func TestWorkspaceBuildLogs(t *testing.T) { + t.Parallel() + client := coderdtest.New(t, nil) + user := coderdtest.CreateFirstUser(t, client) + coderdtest.NewProvisionerDaemon(t, client) + before := time.Now() + version := coderdtest.CreateProjectVersion(t, client, user.OrganizationID, &echo.Responses{ + Parse: echo.ParseComplete, + Provision: []*proto.Provision_Response{{ + Type: &proto.Provision_Response_Log{ + Log: &proto.Log{ + Level: proto.LogLevel_INFO, + Output: "example", + }, + }, + }, { + Type: &proto.Provision_Response_Complete{ + Complete: &proto.Provision_Complete{ + Resources: []*proto.Resource{{ + Name: "some", + Type: "example", + Agent: &proto.Agent{ + Id: "something", + Auth: &proto.Agent_Token{}, + }, + }, { + Name: "another", + Type: "example", + }}, + }, + }, + }}, + }) + coderdtest.AwaitProjectVersionJob(t, client, version.ID) + project := coderdtest.CreateProject(t, client, user.OrganizationID, version.ID) + workspace := coderdtest.CreateWorkspace(t, client, "", project.ID) + build, err := client.CreateWorkspaceBuild(context.Background(), workspace.ID, coderd.CreateWorkspaceBuildRequest{ + ProjectVersionID: project.ActiveVersionID, + Transition: database.WorkspaceTransitionStart, + }) + require.NoError(t, err) + ctx, cancelFunc := context.WithCancel(context.Background()) + t.Cleanup(cancelFunc) + logs, err := client.WorkspaceBuildLogsAfter(ctx, build.ID, before) + require.NoError(t, err) + log := <-logs + require.Equal(t, "example", log.Output) +} diff --git a/coderd/workspacehistory.go b/coderd/workspacehistory.go deleted file mode 100644 index b94a0e99ff46d..0000000000000 --- a/coderd/workspacehistory.go +++ /dev/null @@ -1,244 +0,0 @@ -package coderd - -import ( - "database/sql" - "encoding/json" - "errors" - "fmt" - "net/http" - "time" - - "github.com/go-chi/render" - "github.com/google/uuid" - "github.com/moby/moby/pkg/namesgenerator" - "golang.org/x/xerrors" - - "github.com/coder/coder/database" - "github.com/coder/coder/httpapi" - "github.com/coder/coder/httpmw" -) - -// WorkspaceHistory is an at-point representation of a workspace state. -// Iterate on before/after to determine a chronological history. -type WorkspaceHistory struct { - ID uuid.UUID `json:"id"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` - WorkspaceID uuid.UUID `json:"workspace_id"` - ProjectVersionID uuid.UUID `json:"project_version_id"` - BeforeID uuid.UUID `json:"before_id"` - AfterID uuid.UUID `json:"after_id"` - Name string `json:"name"` - Transition database.WorkspaceTransition `json:"transition"` - Initiator string `json:"initiator"` - ProvisionJobID uuid.UUID `json:"provision_job_id"` -} - -// CreateWorkspaceHistoryRequest provides options to update the latest workspace history. -type CreateWorkspaceHistoryRequest struct { - ProjectVersionID uuid.UUID `json:"project_version_id" validate:"required"` - Transition database.WorkspaceTransition `json:"transition" validate:"oneof=create start stop delete,required"` -} - -func (api *api) postWorkspaceHistoryByUser(rw http.ResponseWriter, r *http.Request) { - var createBuild CreateWorkspaceHistoryRequest - if !httpapi.Read(rw, r, &createBuild) { - return - } - user := httpmw.UserParam(r) - workspace := httpmw.WorkspaceParam(r) - projectVersion, err := api.Database.GetProjectVersionByID(r.Context(), createBuild.ProjectVersionID) - if errors.Is(err, sql.ErrNoRows) { - httpapi.Write(rw, http.StatusBadRequest, httpapi.Response{ - Message: "project version not found", - Errors: []httpapi.Error{{ - Field: "project_version_id", - Code: "exists", - }}, - }) - return - } - if err != nil { - httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ - Message: fmt.Sprintf("get project version: %s", err), - }) - return - } - projectVersionJob, err := api.Database.GetProvisionerJobByID(r.Context(), projectVersion.ImportJobID) - if err != nil { - httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ - Message: fmt.Sprintf("get provisioner job: %s", err), - }) - return - } - projectVersionJobStatus := convertProvisionerJob(projectVersionJob).Status - switch projectVersionJobStatus { - case ProvisionerJobStatusPending, ProvisionerJobStatusRunning: - 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.StatusPreconditionFailed, httpapi.Response{ - Message: fmt.Sprintf("The provided project version %q has failed to import. You cannot create workspaces using it!", projectVersion.Name), - }) - return - case ProvisionerJobStatusCancelled: - 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) - if err != nil { - httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ - Message: fmt.Sprintf("get project: %s", err), - }) - return - } - - // Store prior history ID if it exists to update it after we create new! - priorHistoryID := uuid.NullUUID{} - 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() { - httpapi.Write(rw, http.StatusConflict, httpapi.Response{ - Message: "a workspace build is already active", - }) - return - } - - priorHistoryID = uuid.NullUUID{ - UUID: priorHistory.ID, - Valid: true, - } - } else if !errors.Is(err, sql.ErrNoRows) { - httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ - Message: fmt.Sprintf("get prior workspace history: %s", err), - }) - return - } - - var workspaceHistory database.WorkspaceHistory - // This must happen in a transaction to ensure history can be inserted, and - // the prior history can update it's "after" column to point at the new. - err = api.Database.InTx(func(db database.Store) error { - provisionerJobID := uuid.New() - workspaceHistory, err = db.InsertWorkspaceHistory(r.Context(), database.InsertWorkspaceHistoryParams{ - ID: uuid.New(), - CreatedAt: database.Now(), - UpdatedAt: database.Now(), - WorkspaceID: workspace.ID, - ProjectVersionID: projectVersion.ID, - BeforeID: priorHistoryID, - Name: namesgenerator.GetRandomName(1), - Initiator: user.ID, - Transition: createBuild.Transition, - ProvisionJobID: provisionerJobID, - }) - if err != nil { - return xerrors.Errorf("insert workspace history: %w", err) - } - - input, err := json.Marshal(workspaceProvisionJob{ - WorkspaceHistoryID: workspaceHistory.ID, - }) - if err != nil { - return xerrors.Errorf("marshal provision job: %w", err) - } - - _, err = db.InsertProvisionerJob(r.Context(), database.InsertProvisionerJobParams{ - ID: provisionerJobID, - CreatedAt: database.Now(), - UpdatedAt: database.Now(), - InitiatorID: user.ID, - OrganizationID: project.OrganizationID, - Provisioner: project.Provisioner, - Type: database.ProvisionerJobTypeWorkspaceProvision, - StorageMethod: projectVersionJob.StorageMethod, - StorageSource: projectVersionJob.StorageSource, - Input: input, - }) - if err != nil { - return xerrors.Errorf("insert provisioner job: %w", err) - } - - if priorHistoryID.Valid { - // Update the prior history entries "after" column. - err = db.UpdateWorkspaceHistoryByID(r.Context(), database.UpdateWorkspaceHistoryByIDParams{ - ID: priorHistory.ID, - ProvisionerState: priorHistory.ProvisionerState, - UpdatedAt: database.Now(), - AfterID: uuid.NullUUID{ - UUID: workspaceHistory.ID, - Valid: true, - }, - }) - if err != nil { - return xerrors.Errorf("update prior workspace history: %w", err) - } - } - - return nil - }) - if err != nil { - httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ - Message: err.Error(), - }) - return - } - - render.Status(r, http.StatusCreated) - render.JSON(rw, r, convertWorkspaceHistory(workspaceHistory)) -} - -// Returns all workspace history. This is not sorted. Use before/after to chronologically sort. -func (api *api) workspaceHistoryByUser(rw http.ResponseWriter, r *http.Request) { - workspace := httpmw.WorkspaceParam(r) - - 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{ - Message: fmt.Sprintf("get workspace history: %s", err), - }) - return - } - - apiHistory := make([]WorkspaceHistory, 0, len(history)) - for _, history := range history { - apiHistory = append(apiHistory, convertWorkspaceHistory(history)) - } - - render.Status(r, http.StatusOK) - render.JSON(rw, r, apiHistory) -} - -func (*api) workspaceHistoryByName(rw http.ResponseWriter, r *http.Request) { - workspaceHistory := httpmw.WorkspaceHistoryParam(r) - render.Status(r, http.StatusOK) - render.JSON(rw, r, convertWorkspaceHistory(workspaceHistory)) -} - -// Converts the internal history representation to a public external-facing model. -func convertWorkspaceHistory(workspaceHistory database.WorkspaceHistory) WorkspaceHistory { - //nolint:unconvert - return WorkspaceHistory(WorkspaceHistory{ - ID: workspaceHistory.ID, - CreatedAt: workspaceHistory.CreatedAt, - UpdatedAt: workspaceHistory.UpdatedAt, - WorkspaceID: workspaceHistory.WorkspaceID, - ProjectVersionID: workspaceHistory.ProjectVersionID, - BeforeID: workspaceHistory.BeforeID.UUID, - AfterID: workspaceHistory.AfterID.UUID, - Name: workspaceHistory.Name, - Transition: workspaceHistory.Transition, - Initiator: workspaceHistory.Initiator, - ProvisionJobID: workspaceHistory.ProvisionJobID, - }) -} diff --git a/coderd/workspacehistory_test.go b/coderd/workspacehistory_test.go deleted file mode 100644 index f00ebdaff5679..0000000000000 --- a/coderd/workspacehistory_test.go +++ /dev/null @@ -1,166 +0,0 @@ -package coderd_test - -import ( - "context" - "net/http" - "testing" - - "github.com/google/uuid" - "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 TestPostWorkspaceHistoryByUser(t *testing.T) { - t.Parallel() - t.Run("NoProjectVersion", func(t *testing.T) { - t.Parallel() - client := coderdtest.New(t, nil) - user := coderdtest.CreateInitialUser(t, client) - job := coderdtest.CreateProjectImportJob(t, client, user.Organization, nil) - project := coderdtest.CreateProject(t, client, user.Organization, job.ID) - workspace := coderdtest.CreateWorkspace(t, client, "me", project.ID) - _, err := client.CreateWorkspaceHistory(context.Background(), "", workspace.Name, coderd.CreateWorkspaceHistoryRequest{ - ProjectVersionID: uuid.New(), - Transition: database.WorkspaceTransitionStart, - }) - require.Error(t, err) - var apiErr *codersdk.Error - require.ErrorAs(t, err, &apiErr) - require.Equal(t, http.StatusBadRequest, apiErr.StatusCode()) - }) - - t.Run("ProjectVersionFailedImport", func(t *testing.T) { - t.Parallel() - client := coderdtest.New(t, nil) - user := coderdtest.CreateInitialUser(t, client) - coderdtest.NewProvisionerDaemon(t, client) - job := coderdtest.CreateProjectImportJob(t, client, user.Organization, &echo.Responses{ - Provision: []*proto.Provision_Response{{}}, - }) - project := coderdtest.CreateProject(t, client, user.Organization, job.ID) - coderdtest.AwaitProjectImportJob(t, client, user.Organization, job.ID) - workspace := coderdtest.CreateWorkspace(t, client, "me", project.ID) - _, err := client.CreateWorkspaceHistory(context.Background(), "", workspace.Name, coderd.CreateWorkspaceHistoryRequest{ - ProjectVersionID: project.ActiveVersionID, - Transition: database.WorkspaceTransitionStart, - }) - require.Error(t, err) - var apiErr *codersdk.Error - require.ErrorAs(t, err, &apiErr) - require.Equal(t, http.StatusPreconditionFailed, apiErr.StatusCode()) - }) - - t.Run("AlreadyActive", func(t *testing.T) { - t.Parallel() - client := coderdtest.New(t, nil) - user := coderdtest.CreateInitialUser(t, client) - closeDaemon := coderdtest.NewProvisionerDaemon(t, client) - job := coderdtest.CreateProjectImportJob(t, client, user.Organization, nil) - project := coderdtest.CreateProject(t, client, user.Organization, job.ID) - coderdtest.AwaitProjectImportJob(t, client, user.Organization, job.ID) - // Close here so workspace history doesn't process! - closeDaemon.Close() - workspace := coderdtest.CreateWorkspace(t, client, "me", project.ID) - _, err := client.CreateWorkspaceHistory(context.Background(), "", workspace.Name, coderd.CreateWorkspaceHistoryRequest{ - ProjectVersionID: project.ActiveVersionID, - Transition: database.WorkspaceTransitionStart, - }) - require.NoError(t, err) - _, err = client.CreateWorkspaceHistory(context.Background(), "", workspace.Name, coderd.CreateWorkspaceHistoryRequest{ - ProjectVersionID: project.ActiveVersionID, - Transition: database.WorkspaceTransitionStart, - }) - require.Error(t, err) - var apiErr *codersdk.Error - require.ErrorAs(t, err, &apiErr) - require.Equal(t, http.StatusConflict, apiErr.StatusCode()) - }) - - t.Run("UpdatePriorAfterField", func(t *testing.T) { - t.Parallel() - client := coderdtest.New(t, nil) - user := coderdtest.CreateInitialUser(t, client) - coderdtest.NewProvisionerDaemon(t, client) - job := coderdtest.CreateProjectImportJob(t, client, user.Organization, nil) - project := coderdtest.CreateProject(t, client, user.Organization, job.ID) - coderdtest.AwaitProjectImportJob(t, client, user.Organization, job.ID) - workspace := coderdtest.CreateWorkspace(t, client, "me", project.ID) - firstHistory, err := client.CreateWorkspaceHistory(context.Background(), "", workspace.Name, coderd.CreateWorkspaceHistoryRequest{ - ProjectVersionID: project.ActiveVersionID, - Transition: database.WorkspaceTransitionStart, - }) - require.NoError(t, err) - coderdtest.AwaitWorkspaceProvisionJob(t, client, user.Organization, firstHistory.ProvisionJobID) - secondHistory, err := client.CreateWorkspaceHistory(context.Background(), "", workspace.Name, coderd.CreateWorkspaceHistoryRequest{ - ProjectVersionID: project.ActiveVersionID, - Transition: database.WorkspaceTransitionStart, - }) - require.NoError(t, err) - require.Equal(t, firstHistory.ID.String(), secondHistory.BeforeID.String()) - - firstHistory, err = client.WorkspaceHistory(context.Background(), "", workspace.Name, firstHistory.Name) - require.NoError(t, err) - require.Equal(t, secondHistory.ID.String(), firstHistory.AfterID.String()) - }) -} - -func TestWorkspaceHistoryByUser(t *testing.T) { - t.Parallel() - t.Run("ListEmpty", func(t *testing.T) { - t.Parallel() - client := coderdtest.New(t, nil) - user := coderdtest.CreateInitialUser(t, client) - coderdtest.NewProvisionerDaemon(t, client) - job := coderdtest.CreateProjectImportJob(t, client, user.Organization, nil) - project := coderdtest.CreateProject(t, client, user.Organization, job.ID) - workspace := coderdtest.CreateWorkspace(t, client, "me", project.ID) - history, err := client.ListWorkspaceHistory(context.Background(), "me", workspace.Name) - require.NoError(t, err) - require.NotNil(t, history) - require.Len(t, history, 0) - }) - - t.Run("List", func(t *testing.T) { - t.Parallel() - client := coderdtest.New(t, nil) - user := coderdtest.CreateInitialUser(t, client) - coderdtest.NewProvisionerDaemon(t, client) - job := coderdtest.CreateProjectImportJob(t, client, user.Organization, nil) - project := coderdtest.CreateProject(t, client, user.Organization, job.ID) - coderdtest.AwaitProjectImportJob(t, client, user.Organization, job.ID) - workspace := coderdtest.CreateWorkspace(t, client, "me", project.ID) - _, err := client.CreateWorkspaceHistory(context.Background(), "", workspace.Name, coderd.CreateWorkspaceHistoryRequest{ - ProjectVersionID: project.ActiveVersionID, - Transition: database.WorkspaceTransitionStart, - }) - require.NoError(t, err) - history, err := client.ListWorkspaceHistory(context.Background(), "me", workspace.Name) - require.NoError(t, err) - require.NotNil(t, history) - require.Len(t, history, 1) - }) -} - -func TestWorkspaceHistoryByName(t *testing.T) { - t.Parallel() - client := coderdtest.New(t, nil) - user := coderdtest.CreateInitialUser(t, client) - coderdtest.NewProvisionerDaemon(t, client) - job := coderdtest.CreateProjectImportJob(t, client, user.Organization, nil) - coderdtest.AwaitProjectImportJob(t, client, user.Organization, job.ID) - project := coderdtest.CreateProject(t, client, user.Organization, job.ID) - workspace := coderdtest.CreateWorkspace(t, client, "me", project.ID) - history, err := client.CreateWorkspaceHistory(context.Background(), "", workspace.Name, coderd.CreateWorkspaceHistoryRequest{ - ProjectVersionID: project.ActiveVersionID, - Transition: database.WorkspaceTransitionStart, - }) - require.NoError(t, err) - _, err = client.WorkspaceHistory(context.Background(), "me", workspace.Name, history.Name) - require.NoError(t, err) -} diff --git a/coderd/workspaceagent.go b/coderd/workspaceresourceauth.go similarity index 82% rename from coderd/workspaceagent.go rename to coderd/workspaceresourceauth.go index 2e45046fa3ff0..1f906cbbb14a4 100644 --- a/coderd/workspaceagent.go +++ b/coderd/workspaceresourceauth.go @@ -28,7 +28,7 @@ type WorkspaceAgentAuthenticateResponse struct { // Google Compute Engine supports instance identity verification: // https://cloud.google.com/compute/docs/instances/verifying-instance-identity // Using this, we can exchange a signed instance payload for an agent token. -func (api *api) postAuthenticateWorkspaceAgentUsingGoogleInstanceIdentity(rw http.ResponseWriter, r *http.Request) { +func (api *api) postWorkspaceAuthGoogleInstanceIdentity(rw http.ResponseWriter, r *http.Request) { var req GoogleInstanceIdentityToken if !httpapi.Read(rw, r, &req) { return @@ -56,7 +56,7 @@ func (api *api) postAuthenticateWorkspaceAgentUsingGoogleInstanceIdentity(rw htt }) return } - agent, err := api.Database.GetProvisionerJobAgentByInstanceID(r.Context(), claims.Google.ComputeEngine.InstanceID) + agent, err := api.Database.GetWorkspaceAgentByInstanceID(r.Context(), claims.Google.ComputeEngine.InstanceID) if errors.Is(err, sql.ErrNoRows) { httpapi.Write(rw, http.StatusNotFound, httpapi.Response{ Message: fmt.Sprintf("instance with id %q not found", claims.Google.ComputeEngine.InstanceID), @@ -69,7 +69,7 @@ func (api *api) postAuthenticateWorkspaceAgentUsingGoogleInstanceIdentity(rw htt }) return } - resource, err := api.Database.GetProvisionerJobResourceByID(r.Context(), agent.ResourceID) + resource, err := api.Database.GetWorkspaceResourceByID(r.Context(), agent.ResourceID) if err != nil { httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ Message: fmt.Sprintf("get provisioner job resource: %s", err), @@ -83,7 +83,7 @@ func (api *api) postAuthenticateWorkspaceAgentUsingGoogleInstanceIdentity(rw htt }) return } - if job.Type != database.ProvisionerJobTypeWorkspaceProvision { + if job.Type != database.ProvisionerJobTypeWorkspaceBuild { httpapi.Write(rw, http.StatusBadRequest, httpapi.Response{ Message: fmt.Sprintf("%q jobs cannot be authenticated", job.Type), }) @@ -97,20 +97,20 @@ func (api *api) postAuthenticateWorkspaceAgentUsingGoogleInstanceIdentity(rw htt }) return } - resourceHistory, err := api.Database.GetWorkspaceHistoryByID(r.Context(), jobData.WorkspaceHistoryID) + resourceHistory, err := api.Database.GetWorkspaceBuildByID(r.Context(), jobData.WorkspaceBuildID) if err != nil { httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ - Message: fmt.Sprintf("get workspace history: %s", err), + Message: fmt.Sprintf("get workspace build: %s", err), }) return } // This token should only be exchanged if the instance ID is valid // for the latest history. If an instance ID is recycled by a cloud, // we'd hate to leak access to a user's workspace. - latestHistory, err := api.Database.GetWorkspaceHistoryByWorkspaceIDWithoutAfter(r.Context(), resourceHistory.WorkspaceID) + latestHistory, err := api.Database.GetWorkspaceBuildByWorkspaceIDWithoutAfter(r.Context(), resourceHistory.WorkspaceID) if err != nil { httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ - Message: fmt.Sprintf("get latest workspace history: %s", err), + Message: fmt.Sprintf("get latest workspace build: %s", err), }) return } diff --git a/coderd/workspaceagent_test.go b/coderd/workspaceresourceauth_test.go similarity index 85% rename from coderd/workspaceagent_test.go rename to coderd/workspaceresourceauth_test.go index c48dfc75af1d1..5c1263f77696f 100644 --- a/coderd/workspaceagent_test.go +++ b/coderd/workspaceresourceauth_test.go @@ -28,7 +28,7 @@ import ( "github.com/coder/coder/provisionersdk/proto" ) -func TestPostWorkspaceAgentAuthenticateGoogleInstanceIdentity(t *testing.T) { +func TestPostWorkspaceAuthGoogleInstanceIdentity(t *testing.T) { t.Parallel() t.Run("Expired", func(t *testing.T) { t.Parallel() @@ -38,7 +38,7 @@ func TestPostWorkspaceAgentAuthenticateGoogleInstanceIdentity(t *testing.T) { client := coderdtest.New(t, &coderdtest.Options{ GoogleTokenValidator: validator, }) - _, err := client.AuthenticateWorkspaceAgentUsingGoogleCloudIdentity(context.Background(), "", createMetadataClient(signedKey)) + _, err := client.AuthWorkspaceGoogleInstanceIdentity(context.Background(), "", createMetadataClient(signedKey)) var apiErr *codersdk.Error require.ErrorAs(t, err, &apiErr) require.Equal(t, http.StatusUnauthorized, apiErr.StatusCode()) @@ -52,7 +52,7 @@ func TestPostWorkspaceAgentAuthenticateGoogleInstanceIdentity(t *testing.T) { client := coderdtest.New(t, &coderdtest.Options{ GoogleTokenValidator: validator, }) - _, err := client.AuthenticateWorkspaceAgentUsingGoogleCloudIdentity(context.Background(), "", createMetadataClient(signedKey)) + _, err := client.AuthWorkspaceGoogleInstanceIdentity(context.Background(), "", createMetadataClient(signedKey)) var apiErr *codersdk.Error require.ErrorAs(t, err, &apiErr) require.Equal(t, http.StatusNotFound, apiErr.StatusCode()) @@ -66,9 +66,9 @@ func TestPostWorkspaceAgentAuthenticateGoogleInstanceIdentity(t *testing.T) { client := coderdtest.New(t, &coderdtest.Options{ GoogleTokenValidator: validator, }) - user := coderdtest.CreateInitialUser(t, client) + user := coderdtest.CreateFirstUser(t, client) coderdtest.NewProvisionerDaemon(t, client) - job := coderdtest.CreateProjectImportJob(t, client, user.Organization, &echo.Responses{ + version := coderdtest.CreateProjectVersion(t, client, user.OrganizationID, &echo.Responses{ Parse: echo.ParseComplete, Provision: []*proto.Provision_Response{{ Type: &proto.Provision_Response_Complete{ @@ -88,17 +88,17 @@ func TestPostWorkspaceAgentAuthenticateGoogleInstanceIdentity(t *testing.T) { }, }}, }) - project := coderdtest.CreateProject(t, client, user.Organization, job.ID) - coderdtest.AwaitProjectImportJob(t, client, user.Organization, job.ID) + project := coderdtest.CreateProject(t, client, user.OrganizationID, version.ID) + coderdtest.AwaitProjectVersionJob(t, client, version.ID) workspace := coderdtest.CreateWorkspace(t, client, "me", project.ID) - firstHistory, err := client.CreateWorkspaceHistory(context.Background(), "", workspace.Name, coderd.CreateWorkspaceHistoryRequest{ + build, err := client.CreateWorkspaceBuild(context.Background(), workspace.ID, coderd.CreateWorkspaceBuildRequest{ ProjectVersionID: project.ActiveVersionID, Transition: database.WorkspaceTransitionStart, }) require.NoError(t, err) - coderdtest.AwaitWorkspaceProvisionJob(t, client, user.Organization, firstHistory.ProvisionJobID) + coderdtest.AwaitWorkspaceBuildJob(t, client, build.ID) - _, err = client.AuthenticateWorkspaceAgentUsingGoogleCloudIdentity(context.Background(), "", createMetadataClient(signedKey)) + _, err = client.AuthWorkspaceGoogleInstanceIdentity(context.Background(), "", createMetadataClient(signedKey)) require.NoError(t, err) }) } diff --git a/coderd/workspaceresources.go b/coderd/workspaceresources.go new file mode 100644 index 0000000000000..f753a5f477ddb --- /dev/null +++ b/coderd/workspaceresources.go @@ -0,0 +1,236 @@ +package coderd + +import ( + "database/sql" + "encoding/json" + "fmt" + "io" + "net/http" + "time" + + "github.com/go-chi/render" + "github.com/google/uuid" + "github.com/hashicorp/yamux" + "golang.org/x/xerrors" + "nhooyr.io/websocket" + + "github.com/coder/coder/database" + "github.com/coder/coder/httpapi" + "github.com/coder/coder/httpmw" + "github.com/coder/coder/peerbroker" + "github.com/coder/coder/peerbroker/proto" + "github.com/coder/coder/provisionersdk" +) + +type WorkspaceResource struct { + ID uuid.UUID `json:"id"` + CreatedAt time.Time `json:"created_at"` + JobID uuid.UUID `json:"job_id"` + Transition database.WorkspaceTransition `json:"workspace_transition"` + Type string `json:"type"` + Name string `json:"name"` + Agent *WorkspaceAgent `json:"agent,omitempty"` +} + +type WorkspaceAgent struct { + ID uuid.UUID `json:"id"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + ResourceID uuid.UUID `json:"resource_id"` + InstanceID string `json:"instance_id,omitempty"` + EnvironmentVariables map[string]string `json:"environment_variables"` + StartupScript string `json:"startup_script,omitempty"` +} + +type WorkspaceAgentResourceMetadata struct { + MemoryTotal uint64 `json:"memory_total"` + DiskTotal uint64 `json:"disk_total"` + CPUCores uint64 `json:"cpu_cores"` + CPUModel string `json:"cpu_model"` + CPUMhz float64 `json:"cpu_mhz"` +} + +type WorkspaceAgentInstanceMetadata struct { + JailOrchestrator string `json:"jail_orchestrator"` + OperatingSystem string `json:"operating_system"` + Platform string `json:"platform"` + PlatformFamily string `json:"platform_family"` + KernelVersion string `json:"kernel_version"` + KernelArchitecture string `json:"kernel_architecture"` + Cloud string `json:"cloud"` + Jail string `json:"jail"` + VNC bool `json:"vnc"` +} + +func (api *api) workspaceResource(rw http.ResponseWriter, r *http.Request) { + workspaceBuild := httpmw.WorkspaceBuildParam(r) + workspaceResource := httpmw.WorkspaceResourceParam(r) + job, err := api.Database.GetProvisionerJobByID(r.Context(), workspaceBuild.JobID) + if err != nil { + httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ + Message: fmt.Sprintf("get provisioner job: %s", err), + }) + return + } + if !job.CompletedAt.Valid { + httpapi.Write(rw, http.StatusPreconditionFailed, httpapi.Response{ + Message: "Job hasn't completed!", + }) + return + } + var apiAgent *WorkspaceAgent + if workspaceResource.AgentID.Valid { + agent, err := api.Database.GetWorkspaceAgentByResourceID(r.Context(), workspaceResource.ID) + if err != nil { + httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ + Message: fmt.Sprintf("get provisioner job agent: %s", err), + }) + return + } + convertedAgent, err := convertWorkspaceAgent(agent) + if err != nil { + httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ + Message: fmt.Sprintf("convert provisioner job agent: %s", err), + }) + return + } + apiAgent = &convertedAgent + } + + render.Status(r, http.StatusOK) + render.JSON(rw, r, convertWorkspaceResource(workspaceResource, apiAgent)) +} + +func (api *api) workspaceResourceDial(rw http.ResponseWriter, r *http.Request) { + api.websocketWaitGroup.Add(1) + defer api.websocketWaitGroup.Done() + + resource := httpmw.WorkspaceResourceParam(r) + if !resource.AgentID.Valid { + httpapi.Write(rw, http.StatusBadRequest, httpapi.Response{ + Message: "resource doesn't have an agent", + }) + return + } + agent, err := api.Database.GetWorkspaceAgentByResourceID(r.Context(), resource.ID) + if err != nil { + httpapi.Write(rw, http.StatusBadRequest, httpapi.Response{ + Message: fmt.Sprintf("get provisioner job agent: %s", err), + }) + return + } + conn, err := websocket.Accept(rw, r, &websocket.AcceptOptions{ + CompressionMode: websocket.CompressionDisabled, + }) + if err != nil { + httpapi.Write(rw, http.StatusBadRequest, httpapi.Response{ + Message: fmt.Sprintf("accept websocket: %s", err), + }) + return + } + defer func() { + _ = conn.Close(websocket.StatusNormalClosure, "") + }() + 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.StatusAbnormalClosure, err.Error()) + return + } + err = peerbroker.ProxyListen(r.Context(), session, peerbroker.ProxyOptions{ + ChannelID: agent.ID.String(), + Logger: api.Logger.Named("peerbroker-proxy-dial"), + Pubsub: api.Pubsub, + }) + if err != nil { + _ = conn.Close(websocket.StatusInternalError, fmt.Sprintf("serve: %s", err)) + return + } +} + +func (api *api) workspaceAgentListen(rw http.ResponseWriter, r *http.Request) { + api.websocketWaitGroup.Add(1) + defer api.websocketWaitGroup.Done() + + agent := httpmw.WorkspaceAgent(r) + conn, err := websocket.Accept(rw, r, &websocket.AcceptOptions{ + CompressionMode: websocket.CompressionDisabled, + }) + if err != nil { + httpapi.Write(rw, http.StatusBadRequest, httpapi.Response{ + Message: fmt.Sprintf("accept websocket: %s", err), + }) + return + } + defer func() { + _ = conn.Close(websocket.StatusNormalClosure, "") + }() + 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.StatusAbnormalClosure, err.Error()) + return + } + closer, err := peerbroker.ProxyDial(proto.NewDRPCPeerBrokerClient(provisionersdk.Conn(session)), peerbroker.ProxyOptions{ + ChannelID: agent.ID.String(), + Pubsub: api.Pubsub, + Logger: api.Logger.Named("peerbroker-proxy-listen"), + }) + if err != nil { + _ = conn.Close(websocket.StatusAbnormalClosure, err.Error()) + return + } + defer closer.Close() + err = api.Database.UpdateWorkspaceAgentByID(r.Context(), database.UpdateWorkspaceAgentByIDParams{ + ID: agent.ID, + UpdatedAt: sql.NullTime{ + Time: database.Now(), + Valid: true, + }, + }) + if err != nil { + _ = conn.Close(websocket.StatusAbnormalClosure, err.Error()) + return + } + ticker := time.NewTicker(5 * time.Second) + defer ticker.Stop() + for { + select { + case <-session.CloseChan(): + return + case <-ticker.C: + err = api.Database.UpdateWorkspaceAgentByID(r.Context(), database.UpdateWorkspaceAgentByIDParams{ + ID: agent.ID, + UpdatedAt: sql.NullTime{ + Time: database.Now(), + Valid: true, + }, + }) + if err != nil { + _ = conn.Close(websocket.StatusAbnormalClosure, err.Error()) + return + } + } + } +} + +func convertWorkspaceAgent(agent database.WorkspaceAgent) (WorkspaceAgent, error) { + var envs map[string]string + if agent.EnvironmentVariables.Valid { + err := json.Unmarshal(agent.EnvironmentVariables.RawMessage, &envs) + if err != nil { + return WorkspaceAgent{}, xerrors.Errorf("unmarshal: %w", err) + } + } + return WorkspaceAgent{ + ID: agent.ID, + CreatedAt: agent.CreatedAt, + UpdatedAt: agent.UpdatedAt.Time, + ResourceID: agent.ResourceID, + InstanceID: agent.AuthInstanceID.String, + StartupScript: agent.StartupScript.String, + EnvironmentVariables: envs, + }, nil +} diff --git a/coderd/workspaceresources_test.go b/coderd/workspaceresources_test.go new file mode 100644 index 0000000000000..2b7476a2ef8ae --- /dev/null +++ b/coderd/workspaceresources_test.go @@ -0,0 +1,126 @@ +package coderd_test + +import ( + "context" + "testing" + + "github.com/google/uuid" + "github.com/stretchr/testify/require" + + "cdr.dev/slog" + "cdr.dev/slog/sloggers/slogtest" + + "github.com/coder/coder/agent" + "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/peer" + "github.com/coder/coder/peerbroker" + "github.com/coder/coder/provisioner/echo" + "github.com/coder/coder/provisionersdk/proto" +) + +func TestWorkspaceResource(t *testing.T) { + t.Parallel() + t.Run("Get", func(t *testing.T) { + t.Parallel() + client := coderdtest.New(t, nil) + user := coderdtest.CreateFirstUser(t, client) + coderdtest.NewProvisionerDaemon(t, client) + version := coderdtest.CreateProjectVersion(t, client, user.OrganizationID, &echo.Responses{ + Parse: echo.ParseComplete, + Provision: []*proto.Provision_Response{{ + Type: &proto.Provision_Response_Complete{ + Complete: &proto.Provision_Complete{ + Resources: []*proto.Resource{{ + Name: "some", + Type: "example", + Agent: &proto.Agent{ + Id: "something", + Auth: &proto.Agent_Token{}, + }, + }}, + }, + }, + }}, + }) + coderdtest.AwaitProjectVersionJob(t, client, version.ID) + project := coderdtest.CreateProject(t, client, user.OrganizationID, version.ID) + workspace := coderdtest.CreateWorkspace(t, client, "", project.ID) + build, err := client.CreateWorkspaceBuild(context.Background(), workspace.ID, coderd.CreateWorkspaceBuildRequest{ + ProjectVersionID: project.ActiveVersionID, + Transition: database.WorkspaceTransitionStart, + }) + require.NoError(t, err) + coderdtest.AwaitWorkspaceBuildJob(t, client, build.ID) + resources, err := client.WorkspaceResourcesByBuild(context.Background(), build.ID) + require.NoError(t, err) + _, err = client.WorkspaceResource(context.Background(), resources[0].ID) + require.NoError(t, err) + }) +} + +func TestWorkspaceAgentListen(t *testing.T) { + t.Parallel() + client := coderdtest.New(t, nil) + user := coderdtest.CreateFirstUser(t, client) + daemonCloser := coderdtest.NewProvisionerDaemon(t, client) + authToken := uuid.NewString() + version := coderdtest.CreateProjectVersion(t, client, user.OrganizationID, &echo.Responses{ + Parse: echo.ParseComplete, + ProvisionDryRun: echo.ProvisionComplete, + Provision: []*proto.Provision_Response{{ + Type: &proto.Provision_Response_Complete{ + Complete: &proto.Provision_Complete{ + Resources: []*proto.Resource{{ + Name: "example", + Type: "aws_instance", + Agent: &proto.Agent{ + Id: uuid.NewString(), + Auth: &proto.Agent_Token{ + Token: authToken, + }, + }, + }}, + }, + }, + }}, + }) + project := coderdtest.CreateProject(t, client, user.OrganizationID, version.ID) + coderdtest.AwaitProjectVersionJob(t, client, version.ID) + workspace := coderdtest.CreateWorkspace(t, client, "me", project.ID) + build, err := client.CreateWorkspaceBuild(context.Background(), workspace.ID, coderd.CreateWorkspaceBuildRequest{ + ProjectVersionID: project.ActiveVersionID, + Transition: database.WorkspaceTransitionStart, + }) + require.NoError(t, err) + coderdtest.AwaitWorkspaceBuildJob(t, client, build.ID) + daemonCloser.Close() + + agentClient := codersdk.New(client.URL) + agentClient.SessionToken = authToken + agentCloser := agent.New(agentClient.ListenWorkspaceAgent, &peer.ConnOptions{ + Logger: slogtest.Make(t, nil), + }) + t.Cleanup(func() { + _ = agentCloser.Close() + }) + resources := coderdtest.AwaitWorkspaceAgents(t, client, build.ID) + workspaceClient, err := client.DialWorkspaceAgent(context.Background(), resources[0].ID) + require.NoError(t, err) + t.Cleanup(func() { + _ = workspaceClient.DRPCConn().Close() + }) + stream, err := workspaceClient.NegotiateConnection(context.Background()) + require.NoError(t, err) + conn, err := peerbroker.Dial(stream, nil, &peer.ConnOptions{ + Logger: slogtest.Make(t, nil).Named("client").Leveled(slog.LevelDebug), + }) + require.NoError(t, err) + t.Cleanup(func() { + _ = conn.Close() + }) + _, err = conn.Ping() + require.NoError(t, err) +} diff --git a/coderd/workspaces.go b/coderd/workspaces.go index 4ea1ba2706202..ad89e67fc99d4 100644 --- a/coderd/workspaces.go +++ b/coderd/workspaces.go @@ -2,12 +2,16 @@ package coderd import ( "database/sql" + "encoding/json" "errors" "fmt" "net/http" + "github.com/go-chi/chi/v5" "github.com/go-chi/render" "github.com/google/uuid" + "github.com/moby/moby/pkg/namesgenerator" + "golang.org/x/xerrors" "github.com/coder/coder/database" "github.com/coder/coder/httpapi" @@ -18,160 +22,268 @@ import ( // project versions, and can be updated. type Workspace database.Workspace -// CreateWorkspaceRequest provides options for creating a new workspace. -type CreateWorkspaceRequest struct { - ProjectID uuid.UUID `json:"project_id" validate:"required"` - Name string `json:"name" validate:"username,required"` +// CreateWorkspaceBuildRequest provides options to update the latest workspace build. +type CreateWorkspaceBuildRequest struct { + ProjectVersionID uuid.UUID `json:"project_version_id" validate:"required"` + Transition database.WorkspaceTransition `json:"transition" validate:"oneof=create start stop delete,required"` } -// Returns all workspaces across all projects and organizations. -func (api *api) workspaces(rw http.ResponseWriter, r *http.Request) { - apiKey := httpmw.APIKey(r) - workspaces, err := api.Database.GetWorkspacesByUserID(r.Context(), apiKey.UserID) - if errors.Is(err, sql.ErrNoRows) { - err = nil +func (*api) workspace(rw http.ResponseWriter, r *http.Request) { + workspace := httpmw.WorkspaceParam(r) + render.Status(r, http.StatusOK) + render.JSON(rw, r, convertWorkspace(workspace)) +} + +func (api *api) workspaceBuilds(rw http.ResponseWriter, r *http.Request) { + workspace := httpmw.WorkspaceParam(r) + + builds, err := api.Database.GetWorkspaceBuildByWorkspaceID(r.Context(), workspace.ID) + if err != nil { + return } + jobIDs := make([]uuid.UUID, 0, len(builds)) + for _, version := range builds { + jobIDs = append(jobIDs, version.JobID) + } + jobs, err := api.Database.GetProvisionerJobsByIDs(r.Context(), jobIDs) if err != nil { httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ - Message: fmt.Sprintf("get workspaces: %s", err), + Message: fmt.Sprintf("get jobs: %s", err), }) return } + jobByID := map[string]database.ProvisionerJob{} + for _, job := range jobs { + jobByID[job.ID.String()] = job + } - apiWorkspaces := make([]Workspace, 0, len(workspaces)) - for _, workspace := range workspaces { - apiWorkspaces = append(apiWorkspaces, convertWorkspace(workspace)) + apiBuilds := make([]WorkspaceBuild, 0) + for _, build := range builds { + job, exists := jobByID[build.JobID.String()] + if !exists { + httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ + Message: fmt.Sprintf("job %q doesn't exist for build %q", build.JobID, build.ID), + }) + return + } + apiBuilds = append(apiBuilds, convertWorkspaceBuild(build, convertProvisionerJob(job))) } + render.Status(r, http.StatusOK) - render.JSON(rw, r, apiWorkspaces) + render.JSON(rw, r, apiBuilds) } -// Create a new workspace for the currently authenticated user. -func (api *api) postWorkspaceByUser(rw http.ResponseWriter, r *http.Request) { - var createWorkspace CreateWorkspaceRequest - if !httpapi.Read(rw, r, &createWorkspace) { +func (api *api) postWorkspaceBuilds(rw http.ResponseWriter, r *http.Request) { + apiKey := httpmw.APIKey(r) + workspace := httpmw.WorkspaceParam(r) + var createBuild CreateWorkspaceBuildRequest + if !httpapi.Read(rw, r, &createBuild) { return } - apiKey := httpmw.APIKey(r) - project, err := api.Database.GetProjectByID(r.Context(), createWorkspace.ProjectID) + projectVersion, err := api.Database.GetProjectVersionByID(r.Context(), createBuild.ProjectVersionID) if errors.Is(err, sql.ErrNoRows) { httpapi.Write(rw, http.StatusBadRequest, httpapi.Response{ - Message: fmt.Sprintf("project %q doesn't exist", createWorkspace.ProjectID.String()), + Message: "project version not found", Errors: []httpapi.Error{{ - Field: "project_id", - Code: "not_found", + Field: "project_version_id", + Code: "exists", }}, }) return } if err != nil { httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ - Message: fmt.Sprintf("get project: %s", err), + Message: fmt.Sprintf("get project version: %s", err), }) return } - _, err = api.Database.GetOrganizationMemberByUserID(r.Context(), database.GetOrganizationMemberByUserIDParams{ - OrganizationID: project.OrganizationID, - UserID: apiKey.UserID, - }) - if errors.Is(err, sql.ErrNoRows) { - httpapi.Write(rw, http.StatusUnauthorized, httpapi.Response{ - Message: "you aren't allowed to access projects in that organization", + projectVersionJob, err := api.Database.GetProvisionerJobByID(r.Context(), projectVersion.JobID) + if err != nil { + httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ + Message: fmt.Sprintf("get provisioner job: %s", err), }) return } + projectVersionJobStatus := convertProvisionerJob(projectVersionJob).Status + switch projectVersionJobStatus { + case ProvisionerJobPending, ProvisionerJobRunning: + 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 ProvisionerJobFailed: + 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 + case ProvisionerJobCancelled: + 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.UUID) if err != nil { httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ - Message: fmt.Sprintf("get organization member: %s", err), + Message: fmt.Sprintf("get project: %s", err), }) return } - workspace, err := api.Database.GetWorkspaceByUserIDAndName(r.Context(), database.GetWorkspaceByUserIDAndNameParams{ - OwnerID: apiKey.UserID, - Name: createWorkspace.Name, - }) + // Store prior history ID if it exists to update it after we create new! + priorHistoryID := uuid.NullUUID{} + priorHistory, err := api.Database.GetWorkspaceBuildByWorkspaceIDWithoutAfter(r.Context(), workspace.ID) if err == nil { - // If the workspace already exists, don't allow creation. - project, err := api.Database.GetProjectByID(r.Context(), workspace.ProjectID) - if err != nil { - httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ - Message: fmt.Sprintf("find project for conflicting workspace name %q: %s", createWorkspace.Name, err), + priorJob, err := api.Database.GetProvisionerJobByID(r.Context(), priorHistory.JobID) + if err == nil && !priorJob.CompletedAt.Valid { + httpapi.Write(rw, http.StatusConflict, httpapi.Response{ + Message: "a workspace build is already active", }) return } - // The project is fetched for clarity to the user on where the conflicting name may be. - httpapi.Write(rw, http.StatusConflict, httpapi.Response{ - Message: fmt.Sprintf("workspace %q already exists in the %q project", createWorkspace.Name, project.Name), - Errors: []httpapi.Error{{ - Field: "name", - Code: "exists", - }}, - }) - return - } - if !errors.Is(err, sql.ErrNoRows) { + + priorHistoryID = uuid.NullUUID{ + UUID: priorHistory.ID, + Valid: true, + } + } else if !errors.Is(err, sql.ErrNoRows) { httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ - Message: fmt.Sprintf("get workspace by name: %s", err.Error()), + Message: fmt.Sprintf("get prior workspace build: %s", err), }) return } - // Workspaces are created without any versions. - workspace, err = api.Database.InsertWorkspace(r.Context(), database.InsertWorkspaceParams{ - ID: uuid.New(), - CreatedAt: database.Now(), - UpdatedAt: database.Now(), - OwnerID: apiKey.UserID, - ProjectID: project.ID, - Name: createWorkspace.Name, + var workspaceBuild database.WorkspaceBuild + var provisionerJob database.ProvisionerJob + // This must happen in a transaction to ensure history can be inserted, and + // the prior history can update it's "after" column to point at the new. + err = api.Database.InTx(func(db database.Store) error { + workspaceBuildID := uuid.New() + input, err := json.Marshal(workspaceProvisionJob{ + WorkspaceBuildID: workspaceBuildID, + }) + if err != nil { + return xerrors.Errorf("marshal provision job: %w", err) + } + provisionerJob, err = db.InsertProvisionerJob(r.Context(), database.InsertProvisionerJobParams{ + ID: uuid.New(), + CreatedAt: database.Now(), + UpdatedAt: database.Now(), + InitiatorID: apiKey.UserID, + OrganizationID: project.OrganizationID, + Provisioner: project.Provisioner, + Type: database.ProvisionerJobTypeWorkspaceBuild, + StorageMethod: projectVersionJob.StorageMethod, + StorageSource: projectVersionJob.StorageSource, + Input: input, + }) + if err != nil { + return xerrors.Errorf("insert provisioner job: %w", err) + } + workspaceBuild, err = db.InsertWorkspaceBuild(r.Context(), database.InsertWorkspaceBuildParams{ + ID: workspaceBuildID, + CreatedAt: database.Now(), + UpdatedAt: database.Now(), + WorkspaceID: workspace.ID, + ProjectVersionID: projectVersion.ID, + BeforeID: priorHistoryID, + Name: namesgenerator.GetRandomName(1), + Initiator: apiKey.UserID, + Transition: createBuild.Transition, + JobID: provisionerJob.ID, + }) + if err != nil { + return xerrors.Errorf("insert workspace build: %w", err) + } + + if priorHistoryID.Valid { + // Update the prior history entries "after" column. + err = db.UpdateWorkspaceBuildByID(r.Context(), database.UpdateWorkspaceBuildByIDParams{ + ID: priorHistory.ID, + ProvisionerState: priorHistory.ProvisionerState, + UpdatedAt: database.Now(), + AfterID: uuid.NullUUID{ + UUID: workspaceBuild.ID, + Valid: true, + }, + }) + if err != nil { + return xerrors.Errorf("update prior workspace build: %w", err) + } + } + + return nil }) if err != nil { httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ - Message: fmt.Sprintf("insert workspace: %s", err), + Message: err.Error(), }) return } render.Status(r, http.StatusCreated) - render.JSON(rw, r, convertWorkspace(workspace)) + render.JSON(rw, r, convertWorkspaceBuild(workspaceBuild, convertProvisionerJob(provisionerJob))) } -// Returns a single workspace. -func (*api) workspaceByUser(rw http.ResponseWriter, r *http.Request) { +func (api *api) workspaceBuildLatest(rw http.ResponseWriter, r *http.Request) { workspace := httpmw.WorkspaceParam(r) + workspaceBuild, err := api.Database.GetWorkspaceBuildByWorkspaceIDWithoutAfter(r.Context(), workspace.ID) + if errors.Is(err, sql.ErrNoRows) { + httpapi.Write(rw, http.StatusNotFound, httpapi.Response{ + Message: "no workspace build found", + }) + return + } + if err != nil { + httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ + Message: fmt.Sprintf("get workspace build by name: %s", err), + }) + return + } + job, err := api.Database.GetProvisionerJobByID(r.Context(), workspaceBuild.JobID) + if err != nil { + httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ + Message: fmt.Sprintf("get provisioner job: %s", err), + }) + return + } render.Status(r, http.StatusOK) - render.JSON(rw, r, convertWorkspace(workspace)) + render.JSON(rw, r, convertWorkspaceBuild(workspaceBuild, convertProvisionerJob(job))) } -// 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, +func (api *api) workspaceBuildByName(rw http.ResponseWriter, r *http.Request) { + workspace := httpmw.WorkspaceParam(r) + workspaceBuildName := chi.URLParam(r, "workspacebuildname") + workspaceBuild, err := api.Database.GetWorkspaceBuildByWorkspaceIDAndName(r.Context(), database.GetWorkspaceBuildByWorkspaceIDAndNameParams{ + WorkspaceID: workspace.ID, + Name: workspaceBuildName, }) if errors.Is(err, sql.ErrNoRows) { - err = nil + httpapi.Write(rw, http.StatusNotFound, httpapi.Response{ + Message: fmt.Sprintf("no workspace build found by name %q", workspaceBuildName), + }) + return } if err != nil { httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ - Message: fmt.Sprintf("get workspaces: %s", err), + Message: fmt.Sprintf("get workspace build by name: %s", err), }) return } - - apiWorkspaces := make([]Workspace, 0, len(workspaces)) - for _, workspace := range workspaces { - apiWorkspaces = append(apiWorkspaces, convertWorkspace(workspace)) + job, err := api.Database.GetProvisionerJobByID(r.Context(), workspaceBuild.JobID) + if err != nil { + httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ + Message: fmt.Sprintf("get provisioner job: %s", err), + }) + return } + render.Status(r, http.StatusOK) - render.JSON(rw, r, apiWorkspaces) + render.JSON(rw, r, convertWorkspaceBuild(workspaceBuild, convertProvisionerJob(job))) } -// 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 5591a2947221f..0e43c53562f03 100644 --- a/coderd/workspaces_test.go +++ b/coderd/workspaces_test.go @@ -11,42 +11,34 @@ 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 TestWorkspaces(t *testing.T) { +func TestWorkspace(t *testing.T) { t.Parallel() - t.Run("ListNone", func(t *testing.T) { - t.Parallel() - client := coderdtest.New(t, nil) - _ = coderdtest.CreateInitialUser(t, client) - workspaces, err := client.Workspaces(context.Background(), "") - require.NoError(t, err) - require.NotNil(t, workspaces) - require.Len(t, workspaces, 0) - }) - - t.Run("List", func(t *testing.T) { - t.Parallel() - client := coderdtest.New(t, nil) - user := coderdtest.CreateInitialUser(t, client) - job := coderdtest.CreateProjectImportJob(t, client, user.Organization, nil) - project := coderdtest.CreateProject(t, client, user.Organization, job.ID) - _ = coderdtest.CreateWorkspace(t, client, "", project.ID) - workspaces, err := client.Workspaces(context.Background(), "") - require.NoError(t, err) - require.Len(t, workspaces, 1) - }) + client := coderdtest.New(t, nil) + user := coderdtest.CreateFirstUser(t, client) + job := coderdtest.CreateProjectVersion(t, client, user.OrganizationID, nil) + project := coderdtest.CreateProject(t, client, user.OrganizationID, job.ID) + workspace := coderdtest.CreateWorkspace(t, client, "", project.ID) + _, err := client.Workspace(context.Background(), workspace.ID) + require.NoError(t, err) } -func TestPostWorkspaceByUser(t *testing.T) { +func TestPostWorkspaceBuild(t *testing.T) { t.Parallel() - t.Run("InvalidProject", func(t *testing.T) { + t.Run("NoProjectVersion", func(t *testing.T) { t.Parallel() client := coderdtest.New(t, nil) - _ = coderdtest.CreateInitialUser(t, client) - _, err := client.CreateWorkspace(context.Background(), "", coderd.CreateWorkspaceRequest{ - ProjectID: uuid.New(), - Name: "workspace", + user := coderdtest.CreateFirstUser(t, client) + job := coderdtest.CreateProjectVersion(t, client, user.OrganizationID, nil) + project := coderdtest.CreateProject(t, client, user.OrganizationID, job.ID) + workspace := coderdtest.CreateWorkspace(t, client, "me", project.ID) + _, err := client.CreateWorkspaceBuild(context.Background(), workspace.ID, coderd.CreateWorkspaceBuildRequest{ + ProjectVersionID: uuid.New(), + Transition: database.WorkspaceTransitionStart, }) require.Error(t, err) var apiErr *codersdk.Error @@ -54,48 +46,46 @@ func TestPostWorkspaceByUser(t *testing.T) { require.Equal(t, http.StatusBadRequest, apiErr.StatusCode()) }) - t.Run("NoProjectAccess", func(t *testing.T) { + t.Run("ProjectVersionFailedImport", func(t *testing.T) { t.Parallel() client := coderdtest.New(t, nil) - user := coderdtest.CreateInitialUser(t, client) - job := coderdtest.CreateProjectImportJob(t, client, user.Organization, nil) - project := coderdtest.CreateProject(t, client, user.Organization, job.ID) - - 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, + user := coderdtest.CreateFirstUser(t, client) + coderdtest.NewProvisionerDaemon(t, client) + version := coderdtest.CreateProjectVersion(t, client, user.OrganizationID, &echo.Responses{ + Provision: []*proto.Provision_Response{{}}, }) - require.NoError(t, err) - client.SessionToken = token.SessionToken - require.NoError(t, err) - - _, err = client.CreateWorkspace(context.Background(), "", coderd.CreateWorkspaceRequest{ - ProjectID: project.ID, - Name: "workspace", + project := coderdtest.CreateProject(t, client, user.OrganizationID, version.ID) + coderdtest.AwaitProjectVersionJob(t, client, version.ID) + workspace := coderdtest.CreateWorkspace(t, client, "me", project.ID) + _, err := client.CreateWorkspaceBuild(context.Background(), workspace.ID, coderd.CreateWorkspaceBuildRequest{ + ProjectVersionID: project.ActiveVersionID, + Transition: database.WorkspaceTransitionStart, }) require.Error(t, err) var apiErr *codersdk.Error require.ErrorAs(t, err, &apiErr) - require.Equal(t, http.StatusUnauthorized, apiErr.StatusCode()) + require.Equal(t, http.StatusPreconditionFailed, apiErr.StatusCode()) }) - t.Run("AlreadyExists", func(t *testing.T) { + t.Run("AlreadyActive", func(t *testing.T) { t.Parallel() client := coderdtest.New(t, nil) - user := coderdtest.CreateInitialUser(t, client) - job := coderdtest.CreateProjectImportJob(t, client, user.Organization, nil) - project := coderdtest.CreateProject(t, client, user.Organization, job.ID) - workspace := coderdtest.CreateWorkspace(t, client, "", project.ID) - _, err := client.CreateWorkspace(context.Background(), "", coderd.CreateWorkspaceRequest{ - ProjectID: project.ID, - Name: workspace.Name, + user := coderdtest.CreateFirstUser(t, client) + closeDaemon := coderdtest.NewProvisionerDaemon(t, client) + version := coderdtest.CreateProjectVersion(t, client, user.OrganizationID, nil) + project := coderdtest.CreateProject(t, client, user.OrganizationID, version.ID) + coderdtest.AwaitProjectVersionJob(t, client, version.ID) + // Close here so workspace build doesn't process! + closeDaemon.Close() + workspace := coderdtest.CreateWorkspace(t, client, "me", project.ID) + _, err := client.CreateWorkspaceBuild(context.Background(), workspace.ID, coderd.CreateWorkspaceBuildRequest{ + ProjectVersionID: project.ActiveVersionID, + Transition: database.WorkspaceTransitionStart, + }) + require.NoError(t, err) + _, err = client.CreateWorkspaceBuild(context.Background(), workspace.ID, coderd.CreateWorkspaceBuildRequest{ + ProjectVersionID: project.ActiveVersionID, + Transition: database.WorkspaceTransitionStart, }) require.Error(t, err) var apiErr *codersdk.Error @@ -103,50 +93,102 @@ func TestPostWorkspaceByUser(t *testing.T) { require.Equal(t, http.StatusConflict, apiErr.StatusCode()) }) - t.Run("Create", func(t *testing.T) { + t.Run("UpdatePriorAfterField", func(t *testing.T) { t.Parallel() client := coderdtest.New(t, nil) - user := coderdtest.CreateInitialUser(t, client) - job := coderdtest.CreateProjectImportJob(t, client, user.Organization, nil) - project := coderdtest.CreateProject(t, client, user.Organization, job.ID) - _ = coderdtest.CreateWorkspace(t, client, "", project.ID) + user := coderdtest.CreateFirstUser(t, client) + coderdtest.NewProvisionerDaemon(t, client) + version := coderdtest.CreateProjectVersion(t, client, user.OrganizationID, nil) + project := coderdtest.CreateProject(t, client, user.OrganizationID, version.ID) + coderdtest.AwaitProjectVersionJob(t, client, version.ID) + workspace := coderdtest.CreateWorkspace(t, client, "me", project.ID) + firstBuild, err := client.CreateWorkspaceBuild(context.Background(), workspace.ID, coderd.CreateWorkspaceBuildRequest{ + ProjectVersionID: project.ActiveVersionID, + Transition: database.WorkspaceTransitionStart, + }) + require.NoError(t, err) + coderdtest.AwaitWorkspaceBuildJob(t, client, firstBuild.ID) + secondBuild, err := client.CreateWorkspaceBuild(context.Background(), workspace.ID, coderd.CreateWorkspaceBuildRequest{ + ProjectVersionID: project.ActiveVersionID, + Transition: database.WorkspaceTransitionStart, + }) + require.NoError(t, err) + require.Equal(t, firstBuild.ID.String(), secondBuild.BeforeID.String()) + + firstBuild, err = client.WorkspaceBuild(context.Background(), firstBuild.ID) + require.NoError(t, err) + require.Equal(t, secondBuild.ID.String(), firstBuild.AfterID.String()) }) } -func TestWorkspaceByUser(t *testing.T) { +func TestWorkspaceBuildLatest(t *testing.T) { t.Parallel() - client := coderdtest.New(t, nil) - user := coderdtest.CreateInitialUser(t, client) - job := coderdtest.CreateProjectImportJob(t, client, user.Organization, nil) - project := coderdtest.CreateProject(t, client, user.Organization, job.ID) - workspace := coderdtest.CreateWorkspace(t, client, "", project.ID) - _, err := client.Workspace(context.Background(), "", workspace.Name) - require.NoError(t, err) + t.Run("None", func(t *testing.T) { + t.Parallel() + client := coderdtest.New(t, nil) + user := coderdtest.CreateFirstUser(t, client) + coderdtest.NewProvisionerDaemon(t, client) + version := coderdtest.CreateProjectVersion(t, client, user.OrganizationID, nil) + project := coderdtest.CreateProject(t, client, user.OrganizationID, version.ID) + coderdtest.AwaitProjectVersionJob(t, client, version.ID) + workspace := coderdtest.CreateWorkspace(t, client, "me", project.ID) + _, err := client.WorkspaceBuildLatest(context.Background(), workspace.ID) + var apiErr *codersdk.Error + require.ErrorAs(t, err, &apiErr) + require.Equal(t, http.StatusNotFound, apiErr.StatusCode()) + }) + + t.Run("Found", func(t *testing.T) { + t.Parallel() + client := coderdtest.New(t, nil) + user := coderdtest.CreateFirstUser(t, client) + coderdtest.NewProvisionerDaemon(t, client) + version := coderdtest.CreateProjectVersion(t, client, user.OrganizationID, nil) + coderdtest.AwaitProjectVersionJob(t, client, version.ID) + project := coderdtest.CreateProject(t, client, user.OrganizationID, version.ID) + workspace := coderdtest.CreateWorkspace(t, client, "me", project.ID) + _, err := client.CreateWorkspaceBuild(context.Background(), workspace.ID, coderd.CreateWorkspaceBuildRequest{ + ProjectVersionID: project.ActiveVersionID, + Transition: database.WorkspaceTransitionStart, + }) + require.NoError(t, err) + _, err = client.WorkspaceBuildLatest(context.Background(), workspace.ID) + require.NoError(t, err) + }) } -func TestWorkspacesByProject(t *testing.T) { +func TestWorkspaceBuildByName(t *testing.T) { t.Parallel() - t.Run("ListEmpty", func(t *testing.T) { + t.Run("NotFound", func(t *testing.T) { t.Parallel() client := coderdtest.New(t, nil) - user := coderdtest.CreateInitialUser(t, client) - job := coderdtest.CreateProjectImportJob(t, client, user.Organization, nil) - project := coderdtest.CreateProject(t, client, user.Organization, job.ID) - workspaces, err := client.WorkspacesByProject(context.Background(), user.Organization, project.Name) - require.NoError(t, err) - require.NotNil(t, workspaces) + user := coderdtest.CreateFirstUser(t, client) + coderdtest.NewProvisionerDaemon(t, client) + version := coderdtest.CreateProjectVersion(t, client, user.OrganizationID, nil) + project := coderdtest.CreateProject(t, client, user.OrganizationID, version.ID) + coderdtest.AwaitProjectVersionJob(t, client, version.ID) + workspace := coderdtest.CreateWorkspace(t, client, "me", project.ID) + _, err := client.WorkspaceBuildByName(context.Background(), workspace.ID, "something") + var apiErr *codersdk.Error + require.ErrorAs(t, err, &apiErr) + require.Equal(t, http.StatusNotFound, apiErr.StatusCode()) }) - t.Run("List", func(t *testing.T) { + t.Run("Found", func(t *testing.T) { t.Parallel() client := coderdtest.New(t, nil) - user := coderdtest.CreateInitialUser(t, client) - job := coderdtest.CreateProjectImportJob(t, client, user.Organization, nil) - project := coderdtest.CreateProject(t, client, user.Organization, job.ID) - _ = coderdtest.CreateWorkspace(t, client, "", project.ID) - workspaces, err := client.WorkspacesByProject(context.Background(), user.Organization, project.Name) + user := coderdtest.CreateFirstUser(t, client) + coderdtest.NewProvisionerDaemon(t, client) + version := coderdtest.CreateProjectVersion(t, client, user.OrganizationID, nil) + coderdtest.AwaitProjectVersionJob(t, client, version.ID) + project := coderdtest.CreateProject(t, client, user.OrganizationID, version.ID) + workspace := coderdtest.CreateWorkspace(t, client, "me", project.ID) + build, err := client.CreateWorkspaceBuild(context.Background(), workspace.ID, coderd.CreateWorkspaceBuildRequest{ + ProjectVersionID: project.ActiveVersionID, + Transition: database.WorkspaceTransitionStart, + }) + require.NoError(t, err) + _, err = client.WorkspaceBuildByName(context.Background(), workspace.ID, build.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 976c31d06f3da..d47cd5fc341ea 100644 --- a/codersdk/client.go +++ b/codersdk/client.go @@ -81,6 +81,20 @@ func (c *Client) request(ctx context.Context, method, path string, body interfac // readBodyAsError reads the response as an httpapi.Message, and // wraps it in a codersdk.Error type for easy marshaling. func readBodyAsError(res *http.Response) error { + contentType := res.Header.Get("Content-Type") + if strings.HasPrefix(contentType, "text/plain") { + resp, err := io.ReadAll(res.Body) + if err != nil { + return xerrors.Errorf("read body: %w", err) + } + return &Error{ + statusCode: res.StatusCode, + Response: httpapi.Response{ + Message: string(resp), + }, + } + } + var m httpapi.Response err := json.NewDecoder(res.Body).Decode(&m) if err != nil { diff --git a/codersdk/files.go b/codersdk/files.go index c7eb642f76b68..565859f87a007 100644 --- a/codersdk/files.go +++ b/codersdk/files.go @@ -4,8 +4,8 @@ import ( "context" "encoding/json" "fmt" + "io" "net/http" - "net/url" "github.com/coder/coder/coderd" ) @@ -14,22 +14,36 @@ const ( ContentTypeTar = "application/x-tar" ) -func (c *Client) UploadFile(ctx context.Context, contentType string, content []byte) (coderd.UploadFileResponse, error) { - res, err := c.request(ctx, http.MethodPost, "/api/v2/upload", content, func(r *http.Request) { +// Upload uploads an arbitrary file with the content type provided. +// This is used to upload a source-code archive. +func (c *Client) Upload(ctx context.Context, contentType string, content []byte) (coderd.UploadResponse, error) { + res, err := c.request(ctx, http.MethodPost, "/api/v2/files", content, func(r *http.Request) { r.Header.Set("Content-Type", contentType) }) if err != nil { - return coderd.UploadFileResponse{}, err + return coderd.UploadResponse{}, err } defer res.Body.Close() if res.StatusCode != http.StatusCreated && res.StatusCode != http.StatusOK { - return coderd.UploadFileResponse{}, readBodyAsError(res) + return coderd.UploadResponse{}, readBodyAsError(res) } - var resp coderd.UploadFileResponse + var resp coderd.UploadResponse return resp, json.NewDecoder(res.Body).Decode(&resp) } -// DownloadURL returns -func (c *Client) DownloadURL(asset string) (*url.URL, error) { - return c.URL.Parse(fmt.Sprintf("/api/v2/downloads/%s", asset)) +// Download fetches a file by uploaded hash. +func (c *Client) Download(ctx context.Context, hash string) ([]byte, string, error) { + res, err := c.request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/files/%s", hash), nil) + if err != nil { + return nil, "", err + } + defer res.Body.Close() + if res.StatusCode != http.StatusOK { + return nil, "", readBodyAsError(res) + } + data, err := io.ReadAll(res.Body) + if err != nil { + return nil, "", err + } + return data, res.Header.Get("Content-Type"), nil } diff --git a/codersdk/files_test.go b/codersdk/files_test.go deleted file mode 100644 index 8a85cee963396..0000000000000 --- a/codersdk/files_test.go +++ /dev/null @@ -1,28 +0,0 @@ -package codersdk_test - -import ( - "context" - "testing" - - "github.com/stretchr/testify/require" - - "github.com/coder/coder/coderd/coderdtest" - "github.com/coder/coder/codersdk" -) - -func TestUpload(t *testing.T) { - t.Parallel() - t.Run("Error", func(t *testing.T) { - t.Parallel() - client := coderdtest.New(t, nil) - _, err := client.UploadFile(context.Background(), "wow", []byte{}) - require.Error(t, err) - }) - t.Run("Upload", func(t *testing.T) { - t.Parallel() - client := coderdtest.New(t, nil) - _ = coderdtest.CreateInitialUser(t, client) - _, err := client.UploadFile(context.Background(), codersdk.ContentTypeTar, []byte{'a'}) - require.NoError(t, err) - }) -} diff --git a/codersdk/organizations.go b/codersdk/organizations.go new file mode 100644 index 0000000000000..9a496bbb0dc1c --- /dev/null +++ b/codersdk/organizations.go @@ -0,0 +1,94 @@ +package codersdk + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + + "github.com/coder/coder/coderd" +) + +func (c *Client) Organization(ctx context.Context, id string) (coderd.Organization, error) { + res, err := c.request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/organizations/%s", id), nil) + if err != nil { + return coderd.Organization{}, err + } + defer res.Body.Close() + if res.StatusCode != http.StatusOK { + return coderd.Organization{}, readBodyAsError(res) + } + var organization coderd.Organization + return organization, json.NewDecoder(res.Body).Decode(&organization) +} + +// ProvisionerDaemonsByOrganization returns provisioner daemons available for an organization. +func (c *Client) ProvisionerDaemonsByOrganization(ctx context.Context, organization string) ([]coderd.ProvisionerDaemon, error) { + res, err := c.request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/organizations/%s/provisionerdaemons", organization), nil) + if err != nil { + return nil, err + } + defer res.Body.Close() + if res.StatusCode != http.StatusOK { + return nil, readBodyAsError(res) + } + var daemons []coderd.ProvisionerDaemon + return daemons, json.NewDecoder(res.Body).Decode(&daemons) +} + +// CreateProjectVersion processes source-code and optionally associates the version with a project. +// Executing without a project is useful for validating source-code. +func (c *Client) CreateProjectVersion(ctx context.Context, organization string, req coderd.CreateProjectVersionRequest) (coderd.ProjectVersion, error) { + res, err := c.request(ctx, http.MethodPost, fmt.Sprintf("/api/v2/organizations/%s/projectversions", organization), req) + if err != nil { + return coderd.ProjectVersion{}, err + } + defer res.Body.Close() + if res.StatusCode != http.StatusCreated { + return coderd.ProjectVersion{}, readBodyAsError(res) + } + var projectVersion coderd.ProjectVersion + return projectVersion, json.NewDecoder(res.Body).Decode(&projectVersion) +} + +// CreateProject creates a new project inside an organization. +func (c *Client) CreateProject(ctx context.Context, organization string, request coderd.CreateProjectRequest) (coderd.Project, error) { + res, err := c.request(ctx, http.MethodPost, fmt.Sprintf("/api/v2/organizations/%s/projects", organization), request) + if err != nil { + return coderd.Project{}, err + } + defer res.Body.Close() + if res.StatusCode != http.StatusCreated { + return coderd.Project{}, readBodyAsError(res) + } + var project coderd.Project + return project, json.NewDecoder(res.Body).Decode(&project) +} + +// ProjectsByOrganization lists all projects inside of an organization. +func (c *Client) ProjectsByOrganization(ctx context.Context, organization string) ([]coderd.Project, error) { + res, err := c.request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/organizations/%s/projects", organization), nil) + if err != nil { + return nil, err + } + defer res.Body.Close() + if res.StatusCode != http.StatusOK { + return nil, readBodyAsError(res) + } + var projects []coderd.Project + return projects, json.NewDecoder(res.Body).Decode(&projects) +} + +// ProjectByName finds a project inside the organization provided with a case-insensitive name. +func (c *Client) ProjectByName(ctx context.Context, organization, name string) (coderd.Project, error) { + res, err := c.request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/organizations/%s/projects/%s", organization, name), nil) + if err != nil { + return coderd.Project{}, err + } + defer res.Body.Close() + if res.StatusCode != http.StatusOK { + return coderd.Project{}, readBodyAsError(res) + } + var project coderd.Project + return project, json.NewDecoder(res.Body).Decode(&project) +} diff --git a/codersdk/parameters.go b/codersdk/parameters.go new file mode 100644 index 0000000000000..5d861b20606a3 --- /dev/null +++ b/codersdk/parameters.go @@ -0,0 +1,48 @@ +package codersdk + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + + "github.com/coder/coder/coderd" +) + +func (c *Client) CreateParameter(ctx context.Context, scope coderd.ParameterScope, id string, req coderd.CreateParameterRequest) (coderd.Parameter, error) { + res, err := c.request(ctx, http.MethodPost, fmt.Sprintf("/api/v2/parameters/%s/%s", scope, id), req) + if err != nil { + return coderd.Parameter{}, err + } + defer res.Body.Close() + if res.StatusCode != http.StatusCreated { + return coderd.Parameter{}, readBodyAsError(res) + } + var param coderd.Parameter + return param, json.NewDecoder(res.Body).Decode(¶m) +} + +func (c *Client) DeleteParameter(ctx context.Context, scope coderd.ParameterScope, id, name string) error { + res, err := c.request(ctx, http.MethodDelete, fmt.Sprintf("/api/v2/parameters/%s/%s/%s", scope, id, name), nil) + if err != nil { + return err + } + defer res.Body.Close() + if res.StatusCode != http.StatusOK { + return readBodyAsError(res) + } + return nil +} + +func (c *Client) Parameters(ctx context.Context, scope coderd.ParameterScope, id string) ([]coderd.Parameter, error) { + res, err := c.request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/parameters/%s/%s", scope, id), nil) + if err != nil { + return nil, err + } + defer res.Body.Close() + if res.StatusCode != http.StatusOK { + return nil, readBodyAsError(res) + } + var parameters []coderd.Parameter + return parameters, json.NewDecoder(res.Body).Decode(¶meters) +} diff --git a/codersdk/projectimport.go b/codersdk/projectimport.go deleted file mode 100644 index 7001d63153c09..0000000000000 --- a/codersdk/projectimport.go +++ /dev/null @@ -1,95 +0,0 @@ -package codersdk - -import ( - "context" - "encoding/json" - "fmt" - "net/http" - "time" - - "github.com/google/uuid" - - "github.com/coder/coder/coderd" -) - -// CreateProjectImportJob creates a new import job in the organization provided. -// ProjectImportJob is not associated with a project by default. Projects -// are created from import. -func (c *Client) CreateProjectImportJob(ctx context.Context, organization string, req coderd.CreateProjectImportJobRequest) (coderd.ProvisionerJob, error) { - res, err := c.request(ctx, http.MethodPost, fmt.Sprintf("/api/v2/projectimport/%s", organization), req) - if err != nil { - return coderd.ProvisionerJob{}, err - } - if res.StatusCode != http.StatusCreated { - defer res.Body.Close() - return coderd.ProvisionerJob{}, readBodyAsError(res) - } - var job coderd.ProvisionerJob - return job, json.NewDecoder(res.Body).Decode(&job) -} - -// ProjectImportJob returns an import job by ID. -func (c *Client) ProjectImportJob(ctx context.Context, organization string, job uuid.UUID) (coderd.ProvisionerJob, error) { - res, err := c.request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/projectimport/%s/%s", organization, job), nil) - if err != nil { - return coderd.ProvisionerJob{}, nil - } - defer res.Body.Close() - if res.StatusCode != http.StatusOK { - return coderd.ProvisionerJob{}, readBodyAsError(res) - } - var resp coderd.ProvisionerJob - return resp, json.NewDecoder(res.Body).Decode(&resp) -} - -// ProjectImportJobLogsBefore returns logs that occurred before a specific time. -func (c *Client) ProjectImportJobLogsBefore(ctx context.Context, organization string, job uuid.UUID, before time.Time) ([]coderd.ProvisionerJobLog, error) { - return c.provisionerJobLogsBefore(ctx, "projectimport", organization, job, before) -} - -// ProjectImportJobLogsAfter streams logs for a project import operation that occurred after a specific time. -func (c *Client) ProjectImportJobLogsAfter(ctx context.Context, organization string, job uuid.UUID, after time.Time) (<-chan coderd.ProvisionerJobLog, error) { - return c.provisionerJobLogsAfter(ctx, "projectimport", organization, job, after) -} - -// ProjectImportJobSchemas returns schemas for an import job by ID. -func (c *Client) ProjectImportJobSchemas(ctx context.Context, organization string, job uuid.UUID) ([]coderd.ParameterSchema, error) { - res, err := c.request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/projectimport/%s/%s/schemas", organization, job), nil) - if err != nil { - return nil, err - } - defer res.Body.Close() - if res.StatusCode != http.StatusOK { - return nil, readBodyAsError(res) - } - var params []coderd.ParameterSchema - return params, json.NewDecoder(res.Body).Decode(¶ms) -} - -// ProjectImportJobParameters returns computed parameters for a project import job. -func (c *Client) ProjectImportJobParameters(ctx context.Context, organization string, job uuid.UUID) ([]coderd.ComputedParameterValue, error) { - res, err := c.request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/projectimport/%s/%s/parameters", organization, job), nil) - if err != nil { - return nil, err - } - defer res.Body.Close() - if res.StatusCode != http.StatusOK { - return nil, readBodyAsError(res) - } - var params []coderd.ComputedParameterValue - return params, json.NewDecoder(res.Body).Decode(¶ms) -} - -// ProjectImportJobResources returns resources for a project import job. -func (c *Client) ProjectImportJobResources(ctx context.Context, organization string, job uuid.UUID) ([]coderd.ProvisionerJobResource, error) { - res, err := c.request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/projectimport/%s/%s/resources", organization, job), nil) - if err != nil { - return nil, err - } - defer res.Body.Close() - if res.StatusCode != http.StatusOK { - return nil, readBodyAsError(res) - } - var resources []coderd.ProvisionerJobResource - return resources, json.NewDecoder(res.Body).Decode(&resources) -} diff --git a/codersdk/projectimport_test.go b/codersdk/projectimport_test.go deleted file mode 100644 index 99039c29c68b0..0000000000000 --- a/codersdk/projectimport_test.go +++ /dev/null @@ -1,147 +0,0 @@ -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/provisioner/echo" - "github.com/coder/coder/provisionersdk/proto" -) - -func TestCreateProjectImportJob(t *testing.T) { - t.Parallel() - t.Run("Error", func(t *testing.T) { - t.Parallel() - client := coderdtest.New(t, nil) - _, err := client.CreateProjectImportJob(context.Background(), "", coderd.CreateProjectImportJobRequest{}) - require.Error(t, err) - }) - - t.Run("Create", func(t *testing.T) { - t.Parallel() - client := coderdtest.New(t, nil) - user := coderdtest.CreateInitialUser(t, client) - _ = coderdtest.CreateProjectImportJob(t, client, user.Organization, nil) - }) -} - -func TestProjectImportJob(t *testing.T) { - t.Parallel() - t.Run("Error", func(t *testing.T) { - t.Parallel() - client := coderdtest.New(t, nil) - _, err := client.ProjectImportJob(context.Background(), "", uuid.New()) - require.Error(t, err) - }) - - t.Run("Get", func(t *testing.T) { - t.Parallel() - client := coderdtest.New(t, nil) - user := coderdtest.CreateInitialUser(t, client) - job := coderdtest.CreateProjectImportJob(t, client, user.Organization, nil) - _, err := client.ProjectImportJob(context.Background(), user.Organization, job.ID) - require.NoError(t, err) - }) -} - -func TestProjectImportJobLogsBefore(t *testing.T) { - t.Parallel() - t.Run("Error", func(t *testing.T) { - t.Parallel() - client := coderdtest.New(t, nil) - _, err := client.ProjectImportJobLogsBefore(context.Background(), "", uuid.New(), time.Time{}) - require.Error(t, err) - }) - - t.Run("Get", func(t *testing.T) { - t.Parallel() - client := coderdtest.New(t, nil) - user := coderdtest.CreateInitialUser(t, client) - coderdtest.NewProvisionerDaemon(t, client) - before := time.Now() - job := coderdtest.CreateProjectImportJob(t, client, user.Organization, &echo.Responses{ - Parse: []*proto.Parse_Response{{ - Type: &proto.Parse_Response_Log{ - Log: &proto.Log{ - Output: "hello", - }, - }, - }}, - Provision: echo.ProvisionComplete, - }) - logs, err := client.ProjectImportJobLogsAfter(context.Background(), user.Organization, job.ID, before) - require.NoError(t, err) - <-logs - }) -} - -func TestProjectImportJobLogsAfter(t *testing.T) { - t.Parallel() - t.Run("Error", func(t *testing.T) { - t.Parallel() - client := coderdtest.New(t, nil) - _, err := client.ProjectImportJobLogsAfter(context.Background(), "", uuid.New(), time.Time{}) - require.Error(t, err) - }) - - t.Run("Get", func(t *testing.T) { - t.Parallel() - client := coderdtest.New(t, nil) - user := coderdtest.CreateInitialUser(t, client) - coderdtest.NewProvisionerDaemon(t, client) - job := coderdtest.CreateProjectImportJob(t, client, user.Organization, &echo.Responses{ - Parse: []*proto.Parse_Response{{ - Type: &proto.Parse_Response_Log{ - Log: &proto.Log{ - Output: "hello", - }, - }, - }, { - Type: &proto.Parse_Response_Complete{ - Complete: &proto.Parse_Complete{}, - }, - }}, - Provision: echo.ProvisionComplete, - }) - coderdtest.AwaitProjectImportJob(t, client, user.Organization, job.ID) - logs, err := client.ProjectImportJobLogsBefore(context.Background(), user.Organization, job.ID, time.Time{}) - require.NoError(t, err) - require.Len(t, logs, 1) - }) -} - -func TestProjectImportJobSchemas(t *testing.T) { - t.Parallel() - t.Run("Error", func(t *testing.T) { - t.Parallel() - client := coderdtest.New(t, nil) - _, err := client.ProjectImportJobSchemas(context.Background(), "", uuid.New()) - require.Error(t, err) - }) -} - -func TestProjectImportJobParameters(t *testing.T) { - t.Parallel() - t.Run("Error", func(t *testing.T) { - t.Parallel() - client := coderdtest.New(t, nil) - _, err := client.ProjectImportJobParameters(context.Background(), "", uuid.New()) - require.Error(t, err) - }) -} - -func TestProjectImportJobResources(t *testing.T) { - t.Parallel() - t.Run("Error", func(t *testing.T) { - t.Parallel() - client := coderdtest.New(t, nil) - _, err := client.ProjectImportJobResources(context.Background(), "", uuid.New()) - require.Error(t, err) - }) -} diff --git a/codersdk/projects.go b/codersdk/projects.go index 3e627ae571ed4..7d2116865e9f4 100644 --- a/codersdk/projects.go +++ b/codersdk/projects.go @@ -6,32 +6,14 @@ import ( "fmt" "net/http" + "github.com/google/uuid" + "github.com/coder/coder/coderd" ) -// Projects lists projects inside an organization. -// If organization is an empty string, all projects will be returned -// for the authenticated user. -func (c *Client) Projects(ctx context.Context, organization string) ([]coderd.Project, error) { - route := "/api/v2/projects" - if organization != "" { - route = fmt.Sprintf("/api/v2/projects/%s", organization) - } - res, err := c.request(ctx, http.MethodGet, route, nil) - if err != nil { - return nil, err - } - defer res.Body.Close() - if res.StatusCode != http.StatusOK { - return nil, readBodyAsError(res) - } - var projects []coderd.Project - return projects, json.NewDecoder(res.Body).Decode(&projects) -} - // Project returns a single project. -func (c *Client) Project(ctx context.Context, organization, project string) (coderd.Project, error) { - res, err := c.request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/projects/%s/%s", organization, project), nil) +func (c *Client) Project(ctx context.Context, project uuid.UUID) (coderd.Project, error) { + res, err := c.request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/projects/%s", project), nil) if err != nil { return coderd.Project{}, nil } @@ -43,23 +25,9 @@ func (c *Client) Project(ctx context.Context, organization, project string) (cod return resp, json.NewDecoder(res.Body).Decode(&resp) } -// CreateProject creates a new project inside an organization. -func (c *Client) CreateProject(ctx context.Context, organization string, request coderd.CreateProjectRequest) (coderd.Project, error) { - res, err := c.request(ctx, http.MethodPost, fmt.Sprintf("/api/v2/projects/%s", organization), request) - if err != nil { - return coderd.Project{}, err - } - defer res.Body.Close() - if res.StatusCode != http.StatusCreated { - return coderd.Project{}, readBodyAsError(res) - } - var project coderd.Project - return project, json.NewDecoder(res.Body).Decode(&project) -} - -// ProjectVersions lists versions of a project. -func (c *Client) ProjectVersions(ctx context.Context, organization, project string) ([]coderd.ProjectVersion, error) { - res, err := c.request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/projects/%s/%s/versions", organization, project), nil) +// ProjectVersionsByProject lists versions associated with a project. +func (c *Client) ProjectVersionsByProject(ctx context.Context, project uuid.UUID) ([]coderd.ProjectVersion, error) { + res, err := c.request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/projects/%s/versions", project), nil) if err != nil { return nil, err } @@ -71,9 +39,10 @@ func (c *Client) ProjectVersions(ctx context.Context, organization, project stri return projectVersion, json.NewDecoder(res.Body).Decode(&projectVersion) } -// ProjectVersion returns project version by name. -func (c *Client) ProjectVersion(ctx context.Context, organization, project, version string) (coderd.ProjectVersion, error) { - res, err := c.request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/projects/%s/%s/versions/%s", organization, project, version), nil) +// ProjectVersionByName returns a project version by it's friendly name. +// This is used for path-based routing. Like: /projects/example/versions/helloworld +func (c *Client) ProjectVersionByName(ctx context.Context, project uuid.UUID, name string) (coderd.ProjectVersion, error) { + res, err := c.request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/projects/%s/versions/%s", project, name), nil) if err != nil { return coderd.ProjectVersion{}, err } @@ -84,45 +53,3 @@ func (c *Client) ProjectVersion(ctx context.Context, organization, project, vers var projectVersion coderd.ProjectVersion return projectVersion, json.NewDecoder(res.Body).Decode(&projectVersion) } - -// CreateProjectVersion inserts a new version for the project. -func (c *Client) CreateProjectVersion(ctx context.Context, organization, project string, request coderd.CreateProjectVersionRequest) (coderd.ProjectVersion, error) { - res, err := c.request(ctx, http.MethodPost, fmt.Sprintf("/api/v2/projects/%s/%s/versions", organization, project), request) - if err != nil { - return coderd.ProjectVersion{}, err - } - defer res.Body.Close() - if res.StatusCode != http.StatusCreated { - return coderd.ProjectVersion{}, readBodyAsError(res) - } - var projectVersion coderd.ProjectVersion - return projectVersion, json.NewDecoder(res.Body).Decode(&projectVersion) -} - -// ProjectParameters returns parameters scoped to a project. -func (c *Client) ProjectParameters(ctx context.Context, organization, project string) ([]coderd.ParameterValue, error) { - res, err := c.request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/projects/%s/%s/parameters", organization, project), nil) - if err != nil { - return nil, err - } - defer res.Body.Close() - if res.StatusCode != http.StatusOK { - return nil, readBodyAsError(res) - } - var params []coderd.ParameterValue - return params, json.NewDecoder(res.Body).Decode(¶ms) -} - -// CreateProjectParameter creates a new parameter value scoped to a project. -func (c *Client) CreateProjectParameter(ctx context.Context, organization, project string, req coderd.CreateParameterValueRequest) (coderd.ParameterValue, error) { - res, err := c.request(ctx, http.MethodPost, fmt.Sprintf("/api/v2/projects/%s/%s/parameters", organization, project), req) - if err != nil { - return coderd.ParameterValue{}, err - } - defer res.Body.Close() - if res.StatusCode != http.StatusCreated { - return coderd.ParameterValue{}, readBodyAsError(res) - } - var param coderd.ParameterValue - return param, json.NewDecoder(res.Body).Decode(¶m) -} diff --git a/codersdk/projects_test.go b/codersdk/projects_test.go deleted file mode 100644 index 694dcb60136f0..0000000000000 --- a/codersdk/projects_test.go +++ /dev/null @@ -1,179 +0,0 @@ -package codersdk_test - -import ( - "context" - "testing" - - "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" -) - -func TestProjects(t *testing.T) { - t.Parallel() - t.Run("Error", func(t *testing.T) { - t.Parallel() - client := coderdtest.New(t, nil) - _, err := client.Projects(context.Background(), "") - require.Error(t, err) - }) - - t.Run("List", func(t *testing.T) { - t.Parallel() - client := coderdtest.New(t, nil) - _ = coderdtest.CreateInitialUser(t, client) - _, err := client.Projects(context.Background(), "") - require.NoError(t, err) - }) -} - -func TestProject(t *testing.T) { - t.Parallel() - t.Run("Error", func(t *testing.T) { - t.Parallel() - client := coderdtest.New(t, nil) - _, err := client.Project(context.Background(), "", "") - require.Error(t, err) - }) - - t.Run("Get", func(t *testing.T) { - t.Parallel() - client := coderdtest.New(t, nil) - user := coderdtest.CreateInitialUser(t, client) - job := coderdtest.CreateProjectImportJob(t, client, user.Organization, nil) - project := coderdtest.CreateProject(t, client, user.Organization, job.ID) - _, 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, nil) - _, err := client.CreateProject(context.Background(), "org", coderd.CreateProjectRequest{ - Name: "something", - VersionImportJobID: uuid.New(), - }) - require.Error(t, err) - }) - - t.Run("Create", func(t *testing.T) { - t.Parallel() - client := coderdtest.New(t, nil) - user := coderdtest.CreateInitialUser(t, client) - job := coderdtest.CreateProjectImportJob(t, client, user.Organization, nil) - _ = coderdtest.CreateProject(t, client, user.Organization, job.ID) - }) -} - -func TestProjectVersions(t *testing.T) { - t.Parallel() - t.Run("Error", func(t *testing.T) { - t.Parallel() - client := coderdtest.New(t, nil) - _, err := client.ProjectVersions(context.Background(), "some", "project") - require.Error(t, err) - }) - - t.Run("List", func(t *testing.T) { - t.Parallel() - client := coderdtest.New(t, nil) - user := coderdtest.CreateInitialUser(t, client) - job := coderdtest.CreateProjectImportJob(t, client, user.Organization, nil) - project := coderdtest.CreateProject(t, client, user.Organization, job.ID) - _, err := client.ProjectVersions(context.Background(), user.Organization, project.Name) - require.NoError(t, err) - }) -} - -func TestProjectVersion(t *testing.T) { - t.Parallel() - t.Run("Error", func(t *testing.T) { - t.Parallel() - client := coderdtest.New(t, nil) - _, err := client.ProjectVersion(context.Background(), "some", "project", "version") - require.Error(t, err) - }) - - t.Run("Get", func(t *testing.T) { - t.Parallel() - client := coderdtest.New(t, nil) - user := coderdtest.CreateInitialUser(t, client) - job := coderdtest.CreateProjectImportJob(t, client, user.Organization, nil) - project := coderdtest.CreateProject(t, client, user.Organization, job.ID) - _, err := client.ProjectVersion(context.Background(), user.Organization, project.Name, project.ActiveVersionID.String()) - require.NoError(t, err) - }) -} - -func TestCreateProjectVersion(t *testing.T) { - t.Parallel() - t.Run("Error", func(t *testing.T) { - t.Parallel() - client := coderdtest.New(t, nil) - _, err := client.CreateProjectVersion(context.Background(), "some", "project", coderd.CreateProjectVersionRequest{}) - require.Error(t, err) - }) - - t.Run("Create", func(t *testing.T) { - t.Parallel() - client := coderdtest.New(t, nil) - user := coderdtest.CreateInitialUser(t, client) - job := coderdtest.CreateProjectImportJob(t, client, user.Organization, nil) - project := coderdtest.CreateProject(t, client, user.Organization, job.ID) - _, err := client.CreateProjectVersion(context.Background(), user.Organization, project.Name, coderd.CreateProjectVersionRequest{ - ImportJobID: job.ID, - }) - require.NoError(t, err) - }) -} - -func TestProjectParameters(t *testing.T) { - t.Parallel() - t.Run("Error", func(t *testing.T) { - t.Parallel() - client := coderdtest.New(t, nil) - _, err := client.ProjectParameters(context.Background(), "some", "project") - require.Error(t, err) - }) - - t.Run("List", func(t *testing.T) { - t.Parallel() - client := coderdtest.New(t, nil) - user := coderdtest.CreateInitialUser(t, client) - job := coderdtest.CreateProjectImportJob(t, client, user.Organization, nil) - project := coderdtest.CreateProject(t, client, user.Organization, job.ID) - _, err := client.ProjectParameters(context.Background(), user.Organization, project.Name) - require.NoError(t, err) - }) -} - -func TestCreateProjectParameter(t *testing.T) { - t.Parallel() - t.Run("Error", func(t *testing.T) { - t.Parallel() - client := coderdtest.New(t, nil) - _, 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, nil) - user := coderdtest.CreateInitialUser(t, client) - job := coderdtest.CreateProjectImportJob(t, client, user.Organization, nil) - project := coderdtest.CreateProject(t, client, user.Organization, job.ID) - _, err := client.CreateProjectParameter(context.Background(), user.Organization, project.Name, coderd.CreateParameterValueRequest{ - Name: "example", - SourceValue: "source-value", - SourceScheme: database.ParameterSourceSchemeData, - DestinationScheme: database.ParameterDestinationSchemeEnvironmentVariable, - }) - require.NoError(t, err) - }) -} diff --git a/codersdk/projectversions.go b/codersdk/projectversions.go new file mode 100644 index 0000000000000..40530ff68eb08 --- /dev/null +++ b/codersdk/projectversions.go @@ -0,0 +1,79 @@ +package codersdk + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + "time" + + "github.com/google/uuid" + + "github.com/coder/coder/coderd" +) + +// ProjectVersion returns a project version by ID. +func (c *Client) ProjectVersion(ctx context.Context, id uuid.UUID) (coderd.ProjectVersion, error) { + res, err := c.request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/projectversions/%s", id), nil) + if err != nil { + return coderd.ProjectVersion{}, err + } + defer res.Body.Close() + if res.StatusCode != http.StatusOK { + return coderd.ProjectVersion{}, readBodyAsError(res) + } + var version coderd.ProjectVersion + return version, json.NewDecoder(res.Body).Decode(&version) +} + +// ProjectVersionSchema returns schemas for a project version by ID. +func (c *Client) ProjectVersionSchema(ctx context.Context, version uuid.UUID) ([]coderd.ProjectVersionParameterSchema, error) { + res, err := c.request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/projectversions/%s/schema", version), nil) + if err != nil { + return nil, err + } + defer res.Body.Close() + if res.StatusCode != http.StatusOK { + return nil, readBodyAsError(res) + } + var params []coderd.ProjectVersionParameterSchema + return params, json.NewDecoder(res.Body).Decode(¶ms) +} + +// ProjectVersionParameters returns computed parameters for a project version. +func (c *Client) ProjectVersionParameters(ctx context.Context, version uuid.UUID) ([]coderd.ProjectVersionParameter, error) { + res, err := c.request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/projectversions/%s/parameters", version), nil) + if err != nil { + return nil, err + } + defer res.Body.Close() + if res.StatusCode != http.StatusOK { + return nil, readBodyAsError(res) + } + var params []coderd.ProjectVersionParameter + return params, json.NewDecoder(res.Body).Decode(¶ms) +} + +// ProjectVersionResources returns resources a project version declares. +func (c *Client) ProjectVersionResources(ctx context.Context, version uuid.UUID) ([]coderd.WorkspaceResource, error) { + res, err := c.request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/projectversions/%s/resources", version), nil) + if err != nil { + return nil, err + } + defer res.Body.Close() + if res.StatusCode != http.StatusOK { + return nil, readBodyAsError(res) + } + var resources []coderd.WorkspaceResource + return resources, json.NewDecoder(res.Body).Decode(&resources) +} + +// ProjectVersionLogsBefore returns logs that occurred before a specific time. +func (c *Client) ProjectVersionLogsBefore(ctx context.Context, version uuid.UUID, before time.Time) ([]coderd.ProvisionerJobLog, error) { + return c.provisionerJobLogsBefore(ctx, fmt.Sprintf("/api/v2/projectversions/%s/logs", version), before) +} + +// ProjectVersionLogsAfter streams logs for a project version that occurred after a specific time. +func (c *Client) ProjectVersionLogsAfter(ctx context.Context, version uuid.UUID, after time.Time) (<-chan coderd.ProvisionerJobLog, error) { + return c.provisionerJobLogsAfter(ctx, fmt.Sprintf("/api/v2/projectversions/%s/logs", version), after) +} diff --git a/codersdk/provisioners.go b/codersdk/provisionerdaemons.go similarity index 63% rename from codersdk/provisioners.go rename to codersdk/provisionerdaemons.go index afef953beabb9..446399d3eece3 100644 --- a/codersdk/provisioners.go +++ b/codersdk/provisionerdaemons.go @@ -10,7 +10,6 @@ import ( "strconv" "time" - "github.com/google/uuid" "github.com/hashicorp/yamux" "golang.org/x/xerrors" "nhooyr.io/websocket" @@ -20,23 +19,9 @@ import ( "github.com/coder/coder/provisionersdk" ) -// ProvisionerDaemons returns registered provisionerd instances. -func (c *Client) ProvisionerDaemons(ctx context.Context) ([]coderd.ProvisionerDaemon, error) { - res, err := c.request(ctx, http.MethodGet, "/api/v2/provisioners/daemons", nil) - if err != nil { - return nil, err - } - defer res.Body.Close() - if res.StatusCode != http.StatusOK { - return nil, readBodyAsError(res) - } - var daemons []coderd.ProvisionerDaemon - return daemons, json.NewDecoder(res.Body).Decode(&daemons) -} - -// 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") +// ListenProvisionerDaemon returns the gRPC service for a provisioner daemon implementation. +func (c *Client) ListenProvisionerDaemon(ctx context.Context) (proto.DRPCProvisionerDaemonClient, error) { + serverURL, err := c.URL.Parse("/api/v2/provisionerdaemons/me/listen") if err != nil { return nil, xerrors.Errorf("parse url: %w", err) } @@ -63,12 +48,12 @@ func (c *Client) ProvisionerDaemonClient(ctx context.Context) (proto.DRPCProvisi // provisionerJobLogsBefore provides log output that occurred before a time. // This is abstracted from a specific job type to provide consistency between // APIs. Logs is the only shared route between jobs. -func (c *Client) provisionerJobLogsBefore(ctx context.Context, jobType, organization string, job uuid.UUID, before time.Time) ([]coderd.ProvisionerJobLog, error) { +func (c *Client) provisionerJobLogsBefore(ctx context.Context, path string, before time.Time) ([]coderd.ProvisionerJobLog, error) { values := url.Values{} if !before.IsZero() { values["before"] = []string{strconv.FormatInt(before.UTC().UnixMilli(), 10)} } - res, err := c.request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/%s/%s/%s/logs?%s", jobType, organization, job, values.Encode()), nil) + res, err := c.request(ctx, http.MethodGet, fmt.Sprintf("%s?%s", path, values.Encode()), nil) if err != nil { return nil, err } @@ -82,12 +67,12 @@ func (c *Client) provisionerJobLogsBefore(ctx context.Context, jobType, organiza } // provisionerJobLogsAfter streams logs that occurred after a specific time. -func (c *Client) provisionerJobLogsAfter(ctx context.Context, jobType, organization string, job uuid.UUID, after time.Time) (<-chan coderd.ProvisionerJobLog, error) { +func (c *Client) provisionerJobLogsAfter(ctx context.Context, path string, after time.Time) (<-chan coderd.ProvisionerJobLog, error) { afterQuery := "" if !after.IsZero() { afterQuery = fmt.Sprintf("&after=%d", after.UTC().UnixMilli()) } - res, err := c.request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/%s/%s/%s/logs?follow%s", jobType, organization, job, afterQuery), nil) + res, err := c.request(ctx, http.MethodGet, fmt.Sprintf("%s?follow%s", path, afterQuery), nil) if err != nil { return nil, err } diff --git a/codersdk/provisioners_test.go b/codersdk/provisioners_test.go deleted file mode 100644 index 9fbea9469303e..0000000000000 --- a/codersdk/provisioners_test.go +++ /dev/null @@ -1,46 +0,0 @@ -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, nil) - _, 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, nil) - 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, nil) - 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.go b/codersdk/users.go index 08b152f9cf8d0..1973471dfbcdc 100644 --- a/codersdk/users.go +++ b/codersdk/users.go @@ -9,10 +9,9 @@ import ( "github.com/coder/coder/coderd" ) -// HasInitialUser returns whether the initial user has already been -// created or not. -func (c *Client) HasInitialUser(ctx context.Context) (bool, error) { - res, err := c.request(ctx, http.MethodGet, "/api/v2/user", nil) +// HasFirstUser returns whether the first user has been created. +func (c *Client) HasFirstUser(ctx context.Context) (bool, error) { + res, err := c.request(ctx, http.MethodGet, "/api/v2/users/first", nil) if err != nil { return false, err } @@ -26,20 +25,19 @@ func (c *Client) HasInitialUser(ctx context.Context) (bool, error) { return true, nil } -// CreateInitialUser attempts to create the first user on a Coder deployment. -// This initial user has superadmin privileges. If >0 users exist, this request -// will fail. -func (c *Client) CreateInitialUser(ctx context.Context, req coderd.CreateInitialUserRequest) (coderd.User, error) { - res, err := c.request(ctx, http.MethodPost, "/api/v2/user", req) +// CreateFirstUser attempts to create the first user on a Coder deployment. +// This initial user has superadmin privileges. If >0 users exist, this request will fail. +func (c *Client) CreateFirstUser(ctx context.Context, req coderd.CreateFirstUserRequest) (coderd.CreateFirstUserResponse, error) { + res, err := c.request(ctx, http.MethodPost, "/api/v2/users/first", req) if err != nil { - return coderd.User{}, err + return coderd.CreateFirstUserResponse{}, err } defer res.Body.Close() if res.StatusCode != http.StatusCreated { - return coderd.User{}, readBodyAsError(res) + return coderd.CreateFirstUserResponse{}, readBodyAsError(res) } - var user coderd.User - return user, json.NewDecoder(res.Body).Decode(&user) + var resp coderd.CreateFirstUserResponse + return resp, json.NewDecoder(res.Body).Decode(&resp) } // CreateUser creates a new user. @@ -56,9 +54,12 @@ func (c *Client) CreateUser(ctx context.Context, req coderd.CreateUserRequest) ( return user, json.NewDecoder(res.Body).Decode(&user) } -// CreateAPIKey calls the /api-key API -func (c *Client) CreateAPIKey(ctx context.Context) (*coderd.GenerateAPIKeyResponse, error) { - res, err := c.request(ctx, http.MethodPost, "/api/v2/users/me/keys", nil) +// CreateAPIKey generates an API key for the user ID provided. +func (c *Client) CreateAPIKey(ctx context.Context, id string) (*coderd.GenerateAPIKeyResponse, error) { + if id == "" { + id = "me" + } + res, err := c.request(ctx, http.MethodPost, fmt.Sprintf("/api/v2/users/%s/keys", id), nil) if err != nil { return nil, err } @@ -73,7 +74,7 @@ func (c *Client) CreateAPIKey(ctx context.Context) (*coderd.GenerateAPIKeyRespon // LoginWithPassword creates a session token authenticating with an email and password. // Call `SetSessionToken()` to apply the newly acquired token to the client. func (c *Client) LoginWithPassword(ctx context.Context, req coderd.LoginWithPasswordRequest) (coderd.LoginWithPasswordResponse, error) { - res, err := c.request(ctx, http.MethodPost, "/api/v2/login", req) + res, err := c.request(ctx, http.MethodPost, "/api/v2/users/login", req) if err != nil { return coderd.LoginWithPasswordResponse{}, err } @@ -94,7 +95,7 @@ func (c *Client) LoginWithPassword(ctx context.Context, req coderd.LoginWithPass func (c *Client) Logout(ctx context.Context) error { // Since `LoginWithPassword` doesn't actually set a SessionToken // (it requires a call to SetSessionToken), this is essentially a no-op - res, err := c.request(ctx, http.MethodPost, "/api/v2/logout", nil) + res, err := c.request(ctx, http.MethodPost, "/api/v2/users/logout", nil) if err != nil { return err } @@ -120,8 +121,8 @@ func (c *Client) User(ctx context.Context, id string) (coderd.User, error) { return user, json.NewDecoder(res.Body).Decode(&user) } -// UserOrganizations fetches organizations a user is part of. -func (c *Client) UserOrganizations(ctx context.Context, id string) ([]coderd.Organization, error) { +// OrganizationsByUser returns all organizations the user is a member of. +func (c *Client) OrganizationsByUser(ctx context.Context, id string) ([]coderd.Organization, error) { if id == "" { id = "me" } @@ -130,9 +131,92 @@ func (c *Client) UserOrganizations(ctx context.Context, id string) ([]coderd.Org return nil, err } defer res.Body.Close() - if res.StatusCode != http.StatusOK { + if res.StatusCode > http.StatusOK { return nil, readBodyAsError(res) } var orgs []coderd.Organization return orgs, json.NewDecoder(res.Body).Decode(&orgs) } + +func (c *Client) OrganizationByName(ctx context.Context, user, name string) (coderd.Organization, error) { + if user == "" { + user = "me" + } + res, err := c.request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/users/%s/organizations/%s", user, name), nil) + if err != nil { + return coderd.Organization{}, err + } + defer res.Body.Close() + if res.StatusCode != http.StatusOK { + return coderd.Organization{}, readBodyAsError(res) + } + var org coderd.Organization + return org, json.NewDecoder(res.Body).Decode(&org) +} + +// CreateOrganization creates an organization and adds the provided user as an admin. +func (c *Client) CreateOrganization(ctx context.Context, user string, req coderd.CreateOrganizationRequest) (coderd.Organization, error) { + if user == "" { + user = "me" + } + res, err := c.request(ctx, http.MethodPost, fmt.Sprintf("/api/v2/users/%s/organizations", user), req) + if err != nil { + return coderd.Organization{}, err + } + defer res.Body.Close() + if res.StatusCode != http.StatusCreated { + return coderd.Organization{}, readBodyAsError(res) + } + var org coderd.Organization + return org, json.NewDecoder(res.Body).Decode(&org) +} + +// CreateWorkspace creates a new workspace for the project specified. +func (c *Client) CreateWorkspace(ctx context.Context, user string, request coderd.CreateWorkspaceRequest) (coderd.Workspace, error) { + if user == "" { + user = "me" + } + res, err := c.request(ctx, http.MethodPost, fmt.Sprintf("/api/v2/users/%s/workspaces", user), request) + if err != nil { + return coderd.Workspace{}, err + } + defer res.Body.Close() + if res.StatusCode != http.StatusCreated { + return coderd.Workspace{}, readBodyAsError(res) + } + var workspace coderd.Workspace + return workspace, json.NewDecoder(res.Body).Decode(&workspace) +} + +// WorkspacesByUser returns all workspaces the specified user has access to. +func (c *Client) WorkspacesByUser(ctx context.Context, user string) ([]coderd.Workspace, error) { + if user == "" { + user = "me" + } + res, err := c.request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/users/%s/workspaces", user), nil) + if err != nil { + return nil, err + } + defer res.Body.Close() + if res.StatusCode != http.StatusOK { + return nil, readBodyAsError(res) + } + var workspaces []coderd.Workspace + return workspaces, json.NewDecoder(res.Body).Decode(&workspaces) +} + +func (c *Client) WorkspaceByName(ctx context.Context, user, name string) (coderd.Workspace, error) { + if user == "" { + user = "me" + } + res, err := c.request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/users/%s/workspaces/%s", user, name), nil) + if err != nil { + return coderd.Workspace{}, err + } + defer res.Body.Close() + if res.StatusCode != http.StatusOK { + return coderd.Workspace{}, readBodyAsError(res) + } + var workspace coderd.Workspace + return workspace, json.NewDecoder(res.Body).Decode(&workspace) +} diff --git a/codersdk/users_test.go b/codersdk/users_test.go deleted file mode 100644 index 74f4eca6ebda5..0000000000000 --- a/codersdk/users_test.go +++ /dev/null @@ -1,133 +0,0 @@ -package codersdk_test - -import ( - "context" - "testing" - - "github.com/stretchr/testify/require" - - "github.com/coder/coder/coderd" - "github.com/coder/coder/coderd/coderdtest" -) - -func TestHasInitialUser(t *testing.T) { - t.Parallel() - t.Run("NotFound", func(t *testing.T) { - t.Parallel() - client := coderdtest.New(t, nil) - has, err := client.HasInitialUser(context.Background()) - require.NoError(t, err) - require.False(t, has) - }) - - t.Run("Found", func(t *testing.T) { - t.Parallel() - client := coderdtest.New(t, nil) - _ = coderdtest.CreateInitialUser(t, client) - has, err := client.HasInitialUser(context.Background()) - require.NoError(t, err) - require.True(t, has) - }) -} - -func TestCreateInitialUser(t *testing.T) { - t.Parallel() - t.Run("Error", func(t *testing.T) { - t.Parallel() - client := coderdtest.New(t, nil) - _, err := client.CreateInitialUser(context.Background(), coderd.CreateInitialUserRequest{}) - require.Error(t, err) - }) - - t.Run("Create", func(t *testing.T) { - t.Parallel() - client := coderdtest.New(t, nil) - _ = coderdtest.CreateInitialUser(t, client) - }) -} - -func TestCreateUser(t *testing.T) { - t.Parallel() - t.Run("Error", func(t *testing.T) { - t.Parallel() - client := coderdtest.New(t, nil) - _, err := client.CreateUser(context.Background(), coderd.CreateUserRequest{}) - require.Error(t, err) - }) - - t.Run("Create", func(t *testing.T) { - t.Parallel() - client := coderdtest.New(t, nil) - _ = coderdtest.CreateInitialUser(t, client) - _, err := client.CreateUser(context.Background(), coderd.CreateUserRequest{ - Email: "example@coder.com", - Username: "something", - Password: "password", - }) - require.NoError(t, err) - }) -} - -func TestLoginWithPassword(t *testing.T) { - t.Parallel() - t.Run("Error", func(t *testing.T) { - t.Parallel() - client := coderdtest.New(t, nil) - _, err := client.LoginWithPassword(context.Background(), coderd.LoginWithPasswordRequest{}) - require.Error(t, err) - }) - - t.Run("Success", func(t *testing.T) { - t.Parallel() - client := coderdtest.New(t, nil) - 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, nil) - err := client.Logout(context.Background()) - require.NoError(t, err) -} - -func TestUser(t *testing.T) { - t.Parallel() - t.Run("Error", func(t *testing.T) { - t.Parallel() - client := coderdtest.New(t, nil) - _, err := client.User(context.Background(), "") - require.Error(t, err) - }) - - t.Run("Get", func(t *testing.T) { - t.Parallel() - client := coderdtest.New(t, nil) - _ = coderdtest.CreateInitialUser(t, client) - _, err := client.User(context.Background(), "") - require.NoError(t, err) - }) -} - -func TestUserOrganizations(t *testing.T) { - t.Parallel() - t.Run("Error", func(t *testing.T) { - t.Parallel() - client := coderdtest.New(t, nil) - _, err := client.UserOrganizations(context.Background(), "") - require.Error(t, err) - }) - - t.Run("List", func(t *testing.T) { - t.Parallel() - client := coderdtest.New(t, nil) - _ = coderdtest.CreateInitialUser(t, client) - _, err := client.UserOrganizations(context.Background(), "") - require.NoError(t, err) - }) -} diff --git a/codersdk/workspaceagent_test.go b/codersdk/workspaceagent_test.go deleted file mode 100644 index 6c09f54e828ee..0000000000000 --- a/codersdk/workspaceagent_test.go +++ /dev/null @@ -1,37 +0,0 @@ -package codersdk_test - -import ( - "bytes" - "context" - "io" - "net/http" - "testing" - - "cloud.google.com/go/compute/metadata" - "github.com/stretchr/testify/require" - - "github.com/coder/coder/coderd/coderdtest" -) - -func TestAuthenticateWorkspaceAgentUsingGoogleCloudIdentity(t *testing.T) { - t.Parallel() - t.Run("Error", func(t *testing.T) { - t.Parallel() - client := coderdtest.New(t, nil) - _, err := client.AuthenticateWorkspaceAgentUsingGoogleCloudIdentity(context.Background(), "", metadata.NewClient(&http.Client{ - Transport: roundTripper(func(req *http.Request) (*http.Response, error) { - return &http.Response{ - StatusCode: http.StatusOK, - Body: io.NopCloser(bytes.NewReader([]byte("sometoken"))), - }, nil - }), - })) - require.Error(t, err) - }) -} - -type roundTripper func(req *http.Request) (*http.Response, error) - -func (r roundTripper) RoundTrip(req *http.Request) (*http.Response, error) { - return r(req) -} diff --git a/codersdk/workspacebuilds.go b/codersdk/workspacebuilds.go new file mode 100644 index 0000000000000..f62eea4a0e5df --- /dev/null +++ b/codersdk/workspacebuilds.go @@ -0,0 +1,52 @@ +package codersdk + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + "time" + + "github.com/google/uuid" + + "github.com/coder/coder/coderd" +) + +// WorkspaceBuild returns a single workspace build for a workspace. +// If history is "", the latest version is returned. +func (c *Client) WorkspaceBuild(ctx context.Context, id uuid.UUID) (coderd.WorkspaceBuild, error) { + res, err := c.request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/workspacebuilds/%s", id), nil) + if err != nil { + return coderd.WorkspaceBuild{}, err + } + defer res.Body.Close() + if res.StatusCode != http.StatusOK { + return coderd.WorkspaceBuild{}, readBodyAsError(res) + } + var workspaceBuild coderd.WorkspaceBuild + return workspaceBuild, json.NewDecoder(res.Body).Decode(&workspaceBuild) +} + +// WorkspaceResourcesByBuild returns resources for a workspace build. +func (c *Client) WorkspaceResourcesByBuild(ctx context.Context, build uuid.UUID) ([]coderd.WorkspaceResource, error) { + res, err := c.request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/workspacebuilds/%s/resources", build), nil) + if err != nil { + return nil, err + } + defer res.Body.Close() + if res.StatusCode != http.StatusOK { + return nil, readBodyAsError(res) + } + var resources []coderd.WorkspaceResource + return resources, json.NewDecoder(res.Body).Decode(&resources) +} + +// WorkspaceBuildLogsBefore returns logs that occurred before a specific time. +func (c *Client) WorkspaceBuildLogsBefore(ctx context.Context, version uuid.UUID, before time.Time) ([]coderd.ProvisionerJobLog, error) { + return c.provisionerJobLogsBefore(ctx, fmt.Sprintf("/api/v2/workspacebuilds/%s/logs", version), before) +} + +// WorkspaceBuildLogsAfter streams logs for a workspace build that occurred after a specific time. +func (c *Client) WorkspaceBuildLogsAfter(ctx context.Context, version uuid.UUID, after time.Time) (<-chan coderd.ProvisionerJobLog, error) { + return c.provisionerJobLogsAfter(ctx, fmt.Sprintf("/api/v2/workspacebuilds/%s/logs", version), after) +} diff --git a/codersdk/workspaceagent.go b/codersdk/workspaceresourceauth.go similarity index 73% rename from codersdk/workspaceagent.go rename to codersdk/workspaceresourceauth.go index 7bfcab9202bfb..c2522444eeced 100644 --- a/codersdk/workspaceagent.go +++ b/codersdk/workspaceresourceauth.go @@ -12,11 +12,11 @@ import ( "github.com/coder/coder/coderd" ) -// AuthenticateWorkspaceAgentUsingGoogleCloudIdentity uses the Google Compute Engine Metadata API to +// AuthWorkspaceGoogleInstanceIdentity uses the Google Compute Engine Metadata API to // fetch a signed JWT, and exchange it for a session token for a workspace agent. // // The requesting instance must be registered as a resource in the latest history for a workspace. -func (c *Client) AuthenticateWorkspaceAgentUsingGoogleCloudIdentity(ctx context.Context, serviceAccount string, gcpClient *metadata.Client) (coderd.WorkspaceAgentAuthenticateResponse, error) { +func (c *Client) AuthWorkspaceGoogleInstanceIdentity(ctx context.Context, serviceAccount string, gcpClient *metadata.Client) (coderd.WorkspaceAgentAuthenticateResponse, error) { if serviceAccount == "" { // This is the default name specified by Google. serviceAccount = "default" @@ -29,7 +29,7 @@ func (c *Client) AuthenticateWorkspaceAgentUsingGoogleCloudIdentity(ctx context. if err != nil { return coderd.WorkspaceAgentAuthenticateResponse{}, xerrors.Errorf("get metadata identity: %w", err) } - res, err := c.request(ctx, http.MethodPost, "/api/v2/workspaceagent/authenticate/google-instance-identity", coderd.GoogleInstanceIdentityToken{ + res, err := c.request(ctx, http.MethodPost, "/api/v2/workspaceresources/auth/google-instance-identity", coderd.GoogleInstanceIdentityToken{ JSONWebToken: jwt, }) if err != nil { diff --git a/codersdk/workspaceresources.go b/codersdk/workspaceresources.go new file mode 100644 index 0000000000000..39dcb0920aee7 --- /dev/null +++ b/codersdk/workspaceresources.go @@ -0,0 +1,110 @@ +package codersdk + +import ( + "context" + "encoding/json" + "fmt" + "io" + "net/http" + "net/http/cookiejar" + + "github.com/google/uuid" + "github.com/hashicorp/yamux" + "golang.org/x/xerrors" + "nhooyr.io/websocket" + + "github.com/coder/coder/coderd" + "github.com/coder/coder/httpmw" + "github.com/coder/coder/peer" + "github.com/coder/coder/peerbroker" + "github.com/coder/coder/peerbroker/proto" + "github.com/coder/coder/provisionersdk" +) + +func (c *Client) WorkspaceResource(ctx context.Context, id uuid.UUID) (coderd.WorkspaceResource, error) { + res, err := c.request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/workspaceresources/%s", id), nil) + if err != nil { + return coderd.WorkspaceResource{}, err + } + defer res.Body.Close() + if res.StatusCode != http.StatusOK { + return coderd.WorkspaceResource{}, readBodyAsError(res) + } + var resource coderd.WorkspaceResource + return resource, json.NewDecoder(res.Body).Decode(&resource) +} + +// DialWorkspaceAgent creates a connection to the specified resource. +func (c *Client) DialWorkspaceAgent(ctx context.Context, resource uuid.UUID) (proto.DRPCPeerBrokerClient, error) { + serverURL, err := c.URL.Parse(fmt.Sprintf("/api/v2/workspaceresources/%s/dial", resource.String())) + if err != nil { + return nil, xerrors.Errorf("parse url: %w", err) + } + jar, err := cookiejar.New(nil) + if err != nil { + return nil, xerrors.Errorf("create cookie jar: %w", err) + } + jar.SetCookies(serverURL, []*http.Cookie{{ + Name: httpmw.AuthCookie, + Value: c.SessionToken, + }}) + httpClient := &http.Client{ + Jar: jar, + } + conn, res, err := websocket.Dial(ctx, serverURL.String(), &websocket.DialOptions{ + HTTPClient: httpClient, + // Need to disable compression to avoid a data-race. + CompressionMode: websocket.CompressionDisabled, + }) + if err != nil { + if res == nil { + return nil, err + } + return nil, readBodyAsError(res) + } + 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) + } + return proto.NewDRPCPeerBrokerClient(provisionersdk.Conn(session)), nil +} + +// ListenWorkspaceAgent connects as a workspace agent. +// It obtains the agent ID based off the session token. +func (c *Client) ListenWorkspaceAgent(ctx context.Context, opts *peer.ConnOptions) (*peerbroker.Listener, error) { + serverURL, err := c.URL.Parse("/api/v2/workspaceresources/agent") + if err != nil { + return nil, xerrors.Errorf("parse url: %w", err) + } + jar, err := cookiejar.New(nil) + if err != nil { + return nil, xerrors.Errorf("create cookie jar: %w", err) + } + jar.SetCookies(serverURL, []*http.Cookie{{ + Name: httpmw.AuthCookie, + Value: c.SessionToken, + }}) + httpClient := &http.Client{ + Jar: jar, + } + conn, res, err := websocket.Dial(ctx, serverURL.String(), &websocket.DialOptions{ + HTTPClient: httpClient, + // Need to disable compression to avoid a data-race. + CompressionMode: websocket.CompressionDisabled, + }) + if err != nil { + if res == nil { + return nil, err + } + return nil, readBodyAsError(res) + } + 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) + } + return peerbroker.Listen(session, nil, opts) +} diff --git a/codersdk/workspaces.go b/codersdk/workspaces.go index 28f926c518049..9aacf60d39db2 100644 --- a/codersdk/workspaces.go +++ b/codersdk/workspaces.go @@ -5,53 +5,15 @@ import ( "encoding/json" "fmt" "net/http" - "time" "github.com/google/uuid" "github.com/coder/coder/coderd" ) -// 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) Workspaces(ctx context.Context, user string) ([]coderd.Workspace, error) { - route := "/api/v2/workspaces" - if user != "" { - route += fmt.Sprintf("/%s", user) - } - res, err := c.request(ctx, http.MethodGet, route, nil) - if err != nil { - return nil, err - } - defer res.Body.Close() - if res.StatusCode != http.StatusOK { - return nil, readBodyAsError(res) - } - var workspaces []coderd.Workspace - return workspaces, json.NewDecoder(res.Body).Decode(&workspaces) -} - -// WorkspacesByProject lists all workspaces for a specific project. -func (c *Client) WorkspacesByProject(ctx context.Context, organization, project string) ([]coderd.Workspace, error) { - res, err := c.request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/projects/%s/%s/workspaces", organization, project), nil) - if err != nil { - return nil, err - } - defer res.Body.Close() - if res.StatusCode != http.StatusOK { - return nil, readBodyAsError(res) - } - var workspaces []coderd.Workspace - return workspaces, json.NewDecoder(res.Body).Decode(&workspaces) -} - -// Workspace returns a single workspace by owner and name. -func (c *Client) Workspace(ctx context.Context, owner, name string) (coderd.Workspace, error) { - if owner == "" { - owner = "me" - } - res, err := c.request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/workspaces/%s/%s", owner, name), nil) +// Workspace returns a single workspace. +func (c *Client) Workspace(ctx context.Context, id uuid.UUID) (coderd.Workspace, error) { + res, err := c.request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/workspaces/%s", id), nil) if err != nil { return coderd.Workspace{}, err } @@ -63,12 +25,8 @@ func (c *Client) Workspace(ctx context.Context, owner, name string) (coderd.Work return workspace, json.NewDecoder(res.Body).Decode(&workspace) } -// ListWorkspaceHistory returns historical data for workspace builds. -func (c *Client) ListWorkspaceHistory(ctx context.Context, owner, workspace string) ([]coderd.WorkspaceHistory, error) { - if owner == "" { - owner = "me" - } - res, err := c.request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/workspaces/%s/%s/version", owner, workspace), nil) +func (c *Client) WorkspaceBuilds(ctx context.Context, workspace uuid.UUID) ([]coderd.WorkspaceBuild, error) { + res, err := c.request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/workspaces/%s/builds", workspace), nil) if err != nil { return nil, err } @@ -76,84 +34,46 @@ func (c *Client) ListWorkspaceHistory(ctx context.Context, owner, workspace stri if res.StatusCode != http.StatusOK { return nil, readBodyAsError(res) } - var workspaceHistory []coderd.WorkspaceHistory - return workspaceHistory, json.NewDecoder(res.Body).Decode(&workspaceHistory) + var workspaceBuild []coderd.WorkspaceBuild + return workspaceBuild, json.NewDecoder(res.Body).Decode(&workspaceBuild) } -// WorkspaceHistory returns a single workspace history for a workspace. -// If history is "", the latest version is returned. -func (c *Client) WorkspaceHistory(ctx context.Context, owner, workspace, history string) (coderd.WorkspaceHistory, error) { - if owner == "" { - owner = "me" - } - if history == "" { - history = "latest" - } - res, err := c.request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/workspaces/%s/%s/version/%s", owner, workspace, history), nil) +// CreateWorkspaceBuild queues a new build to occur for a workspace. +func (c *Client) CreateWorkspaceBuild(ctx context.Context, workspace uuid.UUID, request coderd.CreateWorkspaceBuildRequest) (coderd.WorkspaceBuild, error) { + res, err := c.request(ctx, http.MethodPost, fmt.Sprintf("/api/v2/workspaces/%s/builds", workspace), request) if err != nil { - return coderd.WorkspaceHistory{}, err - } - defer res.Body.Close() - if res.StatusCode != http.StatusOK { - return coderd.WorkspaceHistory{}, readBodyAsError(res) - } - var workspaceHistory coderd.WorkspaceHistory - return workspaceHistory, json.NewDecoder(res.Body).Decode(&workspaceHistory) -} - -// CreateWorkspace creates a new workspace for the project specified. -func (c *Client) CreateWorkspace(ctx context.Context, user string, request coderd.CreateWorkspaceRequest) (coderd.Workspace, error) { - if user == "" { - user = "me" - } - res, err := c.request(ctx, http.MethodPost, fmt.Sprintf("/api/v2/workspaces/%s", user), request) - if err != nil { - return coderd.Workspace{}, err + return coderd.WorkspaceBuild{}, err } defer res.Body.Close() if res.StatusCode != http.StatusCreated { - return coderd.Workspace{}, readBodyAsError(res) + return coderd.WorkspaceBuild{}, readBodyAsError(res) } - var workspace coderd.Workspace - return workspace, json.NewDecoder(res.Body).Decode(&workspace) + var workspaceBuild coderd.WorkspaceBuild + return workspaceBuild, json.NewDecoder(res.Body).Decode(&workspaceBuild) } -// CreateWorkspaceHistory queues a new build to occur for a workspace. -func (c *Client) CreateWorkspaceHistory(ctx context.Context, owner, workspace string, request coderd.CreateWorkspaceHistoryRequest) (coderd.WorkspaceHistory, error) { - if owner == "" { - owner = "me" - } - res, err := c.request(ctx, http.MethodPost, fmt.Sprintf("/api/v2/workspaces/%s/%s/version", owner, workspace), request) +func (c *Client) WorkspaceBuildByName(ctx context.Context, workspace uuid.UUID, name string) (coderd.WorkspaceBuild, error) { + res, err := c.request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/workspaces/%s/builds/%s", workspace, name), nil) if err != nil { - return coderd.WorkspaceHistory{}, err + return coderd.WorkspaceBuild{}, err } defer res.Body.Close() - if res.StatusCode != http.StatusCreated { - return coderd.WorkspaceHistory{}, readBodyAsError(res) + if res.StatusCode != http.StatusOK { + return coderd.WorkspaceBuild{}, readBodyAsError(res) } - var workspaceHistory coderd.WorkspaceHistory - return workspaceHistory, json.NewDecoder(res.Body).Decode(&workspaceHistory) + var workspaceBuild coderd.WorkspaceBuild + return workspaceBuild, json.NewDecoder(res.Body).Decode(&workspaceBuild) } -func (c *Client) WorkspaceProvisionJob(ctx context.Context, organization string, job uuid.UUID) (coderd.ProvisionerJob, error) { - res, err := c.request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/workspaceprovision/%s/%s", organization, job), nil) +func (c *Client) WorkspaceBuildLatest(ctx context.Context, workspace uuid.UUID) (coderd.WorkspaceBuild, error) { + res, err := c.request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/workspaces/%s/builds/latest", workspace), nil) if err != nil { - return coderd.ProvisionerJob{}, nil + return coderd.WorkspaceBuild{}, err } defer res.Body.Close() if res.StatusCode != http.StatusOK { - return coderd.ProvisionerJob{}, readBodyAsError(res) + return coderd.WorkspaceBuild{}, readBodyAsError(res) } - var resp coderd.ProvisionerJob - return resp, json.NewDecoder(res.Body).Decode(&resp) -} - -// WorkspaceProvisionJobLogsBefore returns logs that occurred before a specific time. -func (c *Client) WorkspaceProvisionJobLogsBefore(ctx context.Context, organization string, job uuid.UUID, before time.Time) ([]coderd.ProvisionerJobLog, error) { - return c.provisionerJobLogsBefore(ctx, "workspaceprovision", organization, job, before) -} - -// WorkspaceProvisionJobLogsAfter streams logs for a workspace provision operation that occurred after a specific time. -func (c *Client) WorkspaceProvisionJobLogsAfter(ctx context.Context, organization string, job uuid.UUID, after time.Time) (<-chan coderd.ProvisionerJobLog, error) { - return c.provisionerJobLogsAfter(ctx, "workspaceprovision", organization, job, after) + var workspaceBuild coderd.WorkspaceBuild + return workspaceBuild, json.NewDecoder(res.Body).Decode(&workspaceBuild) } diff --git a/codersdk/workspaces_test.go b/codersdk/workspaces_test.go deleted file mode 100644 index 74d7a21e10bf2..0000000000000 --- a/codersdk/workspaces_test.go +++ /dev/null @@ -1,163 +0,0 @@ -package codersdk_test - -import ( - "context" - "testing" - - "github.com/stretchr/testify/require" - - "github.com/coder/coder/coderd" - "github.com/coder/coder/coderd/coderdtest" - "github.com/coder/coder/database" -) - -func TestWorkspaces(t *testing.T) { - t.Parallel() - t.Run("Error", func(t *testing.T) { - t.Parallel() - client := coderdtest.New(t, nil) - _, err := client.Workspaces(context.Background(), "") - require.Error(t, err) - }) - - t.Run("List", func(t *testing.T) { - t.Parallel() - client := coderdtest.New(t, nil) - _ = 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, nil) - _, err := client.WorkspacesByProject(context.Background(), "", "") - require.Error(t, err) - }) - - t.Run("List", func(t *testing.T) { - t.Parallel() - client := coderdtest.New(t, nil) - user := coderdtest.CreateInitialUser(t, client) - job := coderdtest.CreateProjectImportJob(t, client, user.Organization, nil) - project := coderdtest.CreateProject(t, client, user.Organization, job.ID) - _, err := client.WorkspacesByProject(context.Background(), user.Organization, project.Name) - require.NoError(t, err) - }) -} - -func TestWorkspace(t *testing.T) { - t.Parallel() - t.Run("Error", func(t *testing.T) { - t.Parallel() - client := coderdtest.New(t, nil) - _, err := client.Workspace(context.Background(), "", "") - require.Error(t, err) - }) - - t.Run("Get", func(t *testing.T) { - t.Parallel() - client := coderdtest.New(t, nil) - user := coderdtest.CreateInitialUser(t, client) - job := coderdtest.CreateProjectImportJob(t, client, user.Organization, nil) - project := coderdtest.CreateProject(t, client, user.Organization, job.ID) - workspace := coderdtest.CreateWorkspace(t, client, "", project.ID) - _, err := client.Workspace(context.Background(), "", workspace.Name) - require.NoError(t, err) - }) -} - -func TestListWorkspaceHistory(t *testing.T) { - t.Parallel() - t.Run("Error", func(t *testing.T) { - t.Parallel() - client := coderdtest.New(t, nil) - _, err := client.ListWorkspaceHistory(context.Background(), "", "") - require.Error(t, err) - }) - - t.Run("List", func(t *testing.T) { - t.Parallel() - client := coderdtest.New(t, nil) - user := coderdtest.CreateInitialUser(t, client) - job := coderdtest.CreateProjectImportJob(t, client, user.Organization, nil) - project := coderdtest.CreateProject(t, client, user.Organization, job.ID) - 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, nil) - _, err := client.WorkspaceHistory(context.Background(), "", "", "") - require.Error(t, err) - }) - - t.Run("Get", func(t *testing.T) { - t.Parallel() - client := coderdtest.New(t, nil) - user := coderdtest.CreateInitialUser(t, client) - _ = coderdtest.NewProvisionerDaemon(t, client) - job := coderdtest.CreateProjectImportJob(t, client, user.Organization, nil) - project := coderdtest.CreateProject(t, client, user.Organization, job.ID) - coderdtest.AwaitProjectImportJob(t, client, user.Organization, job.ID) - workspace := coderdtest.CreateWorkspace(t, client, "", project.ID) - _, err := client.CreateWorkspaceHistory(context.Background(), "", workspace.Name, coderd.CreateWorkspaceHistoryRequest{ - ProjectVersionID: project.ActiveVersionID, - Transition: database.WorkspaceTransitionStart, - }) - require.NoError(t, err) - }) -} - -func TestCreateWorkspace(t *testing.T) { - t.Parallel() - t.Run("Error", func(t *testing.T) { - t.Parallel() - client := coderdtest.New(t, nil) - _, err := client.CreateWorkspace(context.Background(), "", coderd.CreateWorkspaceRequest{}) - require.Error(t, err) - }) - - t.Run("Get", func(t *testing.T) { - t.Parallel() - client := coderdtest.New(t, nil) - user := coderdtest.CreateInitialUser(t, client) - job := coderdtest.CreateProjectImportJob(t, client, user.Organization, nil) - project := coderdtest.CreateProject(t, client, user.Organization, job.ID) - _ = 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, nil) - _, err := client.CreateWorkspaceHistory(context.Background(), "", "", coderd.CreateWorkspaceHistoryRequest{}) - require.Error(t, err) - }) - - t.Run("Create", func(t *testing.T) { - t.Parallel() - client := coderdtest.New(t, nil) - user := coderdtest.CreateInitialUser(t, client) - _ = coderdtest.NewProvisionerDaemon(t, client) - job := coderdtest.CreateProjectImportJob(t, client, user.Organization, nil) - project := coderdtest.CreateProject(t, client, user.Organization, job.ID) - coderdtest.AwaitProjectImportJob(t, client, user.Organization, job.ID) - workspace := coderdtest.CreateWorkspace(t, client, "", project.ID) - _, err := client.CreateWorkspaceHistory(context.Background(), "", workspace.Name, coderd.CreateWorkspaceHistoryRequest{ - ProjectVersionID: project.ActiveVersionID, - Transition: database.WorkspaceTransitionStart, - }) - require.NoError(t, err) - }) -} diff --git a/database/databasefake/databasefake.go b/database/databasefake/databasefake.go index 1e09a2a74f4f9..24fd970a3a033 100644 --- a/database/databasefake/databasefake.go +++ b/database/databasefake/databasefake.go @@ -28,9 +28,9 @@ func New() database.Store { provisionerJobs: make([]database.ProvisionerJob, 0), provisionerJobLog: make([]database.ProvisionerJobLog, 0), workspace: make([]database.Workspace, 0), - provisionerJobResource: make([]database.ProvisionerJobResource, 0), - workspaceHistory: make([]database.WorkspaceHistory, 0), - provisionerJobAgent: make([]database.ProvisionerJobAgent, 0), + provisionerJobResource: make([]database.WorkspaceResource, 0), + workspaceBuild: make([]database.WorkspaceBuild, 0), + provisionerJobAgent: make([]database.WorkspaceAgent, 0), } } @@ -52,11 +52,11 @@ type fakeQuerier struct { projectVersion []database.ProjectVersion provisionerDaemons []database.ProvisionerDaemon provisionerJobs []database.ProvisionerJob - provisionerJobAgent []database.ProvisionerJobAgent - provisionerJobResource []database.ProvisionerJobResource + provisionerJobAgent []database.WorkspaceAgent + provisionerJobResource []database.WorkspaceResource provisionerJobLog []database.ProvisionerJobLog workspace []database.Workspace - workspaceHistory []database.WorkspaceHistory + workspaceBuild []database.WorkspaceBuild } // InTx doesn't rollback data properly for in-memory yet. @@ -92,6 +92,21 @@ func (q *fakeQuerier) AcquireProvisionerJob(_ context.Context, arg database.Acqu return database.ProvisionerJob{}, sql.ErrNoRows } +func (q *fakeQuerier) DeleteParameterValueByID(_ context.Context, id uuid.UUID) error { + q.mutex.Lock() + defer q.mutex.Unlock() + + for index, parameterValue := range q.parameterValue { + if parameterValue.ID.String() != id.String() { + continue + } + q.parameterValue[index] = q.parameterValue[len(q.parameterValue)-1] + q.parameterValue = q.parameterValue[:len(q.parameterValue)-1] + return nil + } + return sql.ErrNoRows +} + func (q *fakeQuerier) GetAPIKeyByID(_ context.Context, id string) (database.APIKey, error) { q.mutex.Lock() defer q.mutex.Unlock() @@ -210,83 +225,75 @@ func (q *fakeQuerier) GetWorkspaceOwnerCountsByProjectIDs(_ context.Context, pro return res, nil } -func (q *fakeQuerier) GetWorkspaceHistoryByID(_ context.Context, id uuid.UUID) (database.WorkspaceHistory, error) { +func (q *fakeQuerier) GetWorkspaceBuildByID(_ context.Context, id uuid.UUID) (database.WorkspaceBuild, error) { q.mutex.Lock() defer q.mutex.Unlock() - for _, history := range q.workspaceHistory { + for _, history := range q.workspaceBuild { if history.ID.String() == id.String() { return history, nil } } - return database.WorkspaceHistory{}, sql.ErrNoRows + return database.WorkspaceBuild{}, sql.ErrNoRows } -func (q *fakeQuerier) GetWorkspaceHistoryByWorkspaceIDWithoutAfter(_ context.Context, workspaceID uuid.UUID) (database.WorkspaceHistory, error) { +func (q *fakeQuerier) GetWorkspaceBuildByJobID(_ context.Context, jobID uuid.UUID) (database.WorkspaceBuild, error) { q.mutex.Lock() defer q.mutex.Unlock() - for _, workspaceHistory := range q.workspaceHistory { - if workspaceHistory.WorkspaceID.String() != workspaceID.String() { - continue - } - if !workspaceHistory.AfterID.Valid { - return workspaceHistory, nil + for _, build := range q.workspaceBuild { + if build.JobID.String() == jobID.String() { + return build, nil } } - return database.WorkspaceHistory{}, sql.ErrNoRows + return database.WorkspaceBuild{}, sql.ErrNoRows } -func (q *fakeQuerier) GetWorkspaceHistoryByWorkspaceID(_ context.Context, workspaceID uuid.UUID) ([]database.WorkspaceHistory, error) { +func (q *fakeQuerier) GetWorkspaceBuildByWorkspaceIDWithoutAfter(_ context.Context, workspaceID uuid.UUID) (database.WorkspaceBuild, error) { q.mutex.Lock() defer q.mutex.Unlock() - history := make([]database.WorkspaceHistory, 0) - for _, workspaceHistory := range q.workspaceHistory { - if workspaceHistory.WorkspaceID.String() == workspaceID.String() { - history = append(history, workspaceHistory) + for _, workspaceBuild := range q.workspaceBuild { + if workspaceBuild.WorkspaceID.String() != workspaceID.String() { + continue + } + if !workspaceBuild.AfterID.Valid { + return workspaceBuild, nil } } - if len(history) == 0 { - return nil, sql.ErrNoRows - } - return history, nil + return database.WorkspaceBuild{}, sql.ErrNoRows } -func (q *fakeQuerier) GetWorkspaceHistoryByWorkspaceIDAndName(_ context.Context, arg database.GetWorkspaceHistoryByWorkspaceIDAndNameParams) (database.WorkspaceHistory, error) { +func (q *fakeQuerier) GetWorkspaceBuildByWorkspaceID(_ context.Context, workspaceID uuid.UUID) ([]database.WorkspaceBuild, error) { q.mutex.Lock() defer q.mutex.Unlock() - for _, workspaceHistory := range q.workspaceHistory { - if workspaceHistory.WorkspaceID.String() != arg.WorkspaceID.String() { - continue - } - if !strings.EqualFold(workspaceHistory.Name, arg.Name) { - continue + history := make([]database.WorkspaceBuild, 0) + for _, workspaceBuild := range q.workspaceBuild { + if workspaceBuild.WorkspaceID.String() == workspaceID.String() { + history = append(history, workspaceBuild) } - return workspaceHistory, nil } - return database.WorkspaceHistory{}, sql.ErrNoRows + if len(history) == 0 { + return nil, sql.ErrNoRows + } + return history, nil } -func (q *fakeQuerier) GetWorkspacesByProjectAndUserID(_ context.Context, arg database.GetWorkspacesByProjectAndUserIDParams) ([]database.Workspace, error) { +func (q *fakeQuerier) GetWorkspaceBuildByWorkspaceIDAndName(_ context.Context, arg database.GetWorkspaceBuildByWorkspaceIDAndNameParams) (database.WorkspaceBuild, error) { q.mutex.Lock() defer q.mutex.Unlock() - workspaces := make([]database.Workspace, 0) - for _, workspace := range q.workspace { - if workspace.OwnerID != arg.OwnerID { + for _, workspaceBuild := range q.workspaceBuild { + if workspaceBuild.WorkspaceID.String() != arg.WorkspaceID.String() { continue } - if workspace.ProjectID.String() != arg.ProjectID.String() { + if !strings.EqualFold(workspaceBuild.Name, arg.Name) { continue } - workspaces = append(workspaces, workspace) + return workspaceBuild, nil } - if len(workspaces) == 0 { - return nil, sql.ErrNoRows - } - return workspaces, nil + return database.WorkspaceBuild{}, sql.ErrNoRows } func (q *fakeQuerier) GetWorkspacesByUserID(_ context.Context, ownerID string) ([]database.Workspace, error) { @@ -406,7 +413,7 @@ func (q *fakeQuerier) GetProjectVersionsByProjectID(_ context.Context, projectID version := make([]database.ProjectVersion, 0) for _, projectVersion := range q.projectVersion { - if projectVersion.ProjectID.String() != projectID.String() { + if projectVersion.ProjectID.UUID.String() != projectID.String() { continue } version = append(version, projectVersion) @@ -422,7 +429,7 @@ func (q *fakeQuerier) GetProjectVersionByProjectIDAndName(_ context.Context, arg defer q.mutex.Unlock() for _, projectVersion := range q.projectVersion { - if projectVersion.ProjectID.String() != arg.ProjectID.String() { + if projectVersion.ProjectID.UUID.String() != arg.ProjectID.UUID.String() { continue } if !strings.EqualFold(projectVersion.Name, arg.Name) { @@ -463,17 +470,34 @@ func (q *fakeQuerier) GetParameterSchemasByJobID(_ context.Context, jobID uuid.U return parameters, nil } -func (q *fakeQuerier) GetProjectsByOrganizationIDs(_ context.Context, ids []string) ([]database.Project, error) { +func (q *fakeQuerier) GetParameterValueByScopeAndName(_ context.Context, arg database.GetParameterValueByScopeAndNameParams) (database.ParameterValue, error) { + q.mutex.Lock() + defer q.mutex.Unlock() + + for _, parameterValue := range q.parameterValue { + if parameterValue.Scope != arg.Scope { + continue + } + if parameterValue.ScopeID != arg.ScopeID { + continue + } + if parameterValue.Name != arg.Name { + continue + } + return parameterValue, nil + } + return database.ParameterValue{}, sql.ErrNoRows +} + +func (q *fakeQuerier) GetProjectsByOrganization(_ context.Context, organizationID string) ([]database.Project, error) { q.mutex.Lock() defer q.mutex.Unlock() projects := make([]database.Project, 0) for _, project := range q.project { - for _, id := range ids { - if project.OrganizationID == id { - projects = append(projects, project) - break - } + if project.OrganizationID == organizationID { + projects = append(projects, project) + break } } if len(projects) == 0 { @@ -508,7 +532,19 @@ func (q *fakeQuerier) GetProvisionerDaemons(_ context.Context) ([]database.Provi return q.provisionerDaemons, nil } -func (q *fakeQuerier) GetProvisionerJobAgentByInstanceID(_ context.Context, instanceID string) (database.ProvisionerJobAgent, error) { +func (q *fakeQuerier) GetWorkspaceAgentByAuthToken(_ context.Context, authToken uuid.UUID) (database.WorkspaceAgent, error) { + q.mutex.Lock() + defer q.mutex.Unlock() + + for _, agent := range q.provisionerJobAgent { + if agent.AuthToken.String() == authToken.String() { + return agent, nil + } + } + return database.WorkspaceAgent{}, sql.ErrNoRows +} + +func (q *fakeQuerier) GetWorkspaceAgentByInstanceID(_ context.Context, instanceID string) (database.WorkspaceAgent, error) { q.mutex.Lock() defer q.mutex.Unlock() @@ -519,25 +555,19 @@ func (q *fakeQuerier) GetProvisionerJobAgentByInstanceID(_ context.Context, inst return agent, nil } } - return database.ProvisionerJobAgent{}, sql.ErrNoRows + return database.WorkspaceAgent{}, sql.ErrNoRows } -func (q *fakeQuerier) GetProvisionerJobAgentsByResourceIDs(_ context.Context, ids []uuid.UUID) ([]database.ProvisionerJobAgent, error) { +func (q *fakeQuerier) GetWorkspaceAgentByResourceID(_ context.Context, resourceID uuid.UUID) (database.WorkspaceAgent, error) { q.mutex.Lock() defer q.mutex.Unlock() - agents := make([]database.ProvisionerJobAgent, 0) for _, agent := range q.provisionerJobAgent { - for _, id := range ids { - if agent.ResourceID.String() == id.String() { - agents = append(agents, agent) - } + if agent.ResourceID.String() == resourceID.String() { + return agent, nil } } - if len(agents) == 0 { - return nil, sql.ErrNoRows - } - return agents, nil + return database.WorkspaceAgent{}, sql.ErrNoRows } func (q *fakeQuerier) GetProvisionerDaemonByID(_ context.Context, id uuid.UUID) (database.ProvisionerDaemon, error) { @@ -566,7 +596,7 @@ func (q *fakeQuerier) GetProvisionerJobByID(_ context.Context, id uuid.UUID) (da return database.ProvisionerJob{}, sql.ErrNoRows } -func (q *fakeQuerier) GetProvisionerJobResourceByID(_ context.Context, id uuid.UUID) (database.ProvisionerJobResource, error) { +func (q *fakeQuerier) GetWorkspaceResourceByID(_ context.Context, id uuid.UUID) (database.WorkspaceResource, error) { q.mutex.Lock() defer q.mutex.Unlock() @@ -575,14 +605,14 @@ func (q *fakeQuerier) GetProvisionerJobResourceByID(_ context.Context, id uuid.U return resource, nil } } - return database.ProvisionerJobResource{}, sql.ErrNoRows + return database.WorkspaceResource{}, sql.ErrNoRows } -func (q *fakeQuerier) GetProvisionerJobResourcesByJobID(_ context.Context, jobID uuid.UUID) ([]database.ProvisionerJobResource, error) { +func (q *fakeQuerier) GetWorkspaceResourcesByJobID(_ context.Context, jobID uuid.UUID) ([]database.WorkspaceResource, error) { q.mutex.Lock() defer q.mutex.Unlock() - resources := make([]database.ProvisionerJobResource, 0) + resources := make([]database.WorkspaceResource, 0) for _, resource := range q.provisionerJobResource { if resource.JobID.String() != jobID.String() { continue @@ -595,6 +625,26 @@ func (q *fakeQuerier) GetProvisionerJobResourcesByJobID(_ context.Context, jobID return resources, nil } +func (q *fakeQuerier) GetProvisionerJobsByIDs(_ context.Context, ids []uuid.UUID) ([]database.ProvisionerJob, error) { + q.mutex.Lock() + defer q.mutex.Unlock() + + jobs := make([]database.ProvisionerJob, 0) + for _, job := range q.provisionerJobs { + for _, id := range ids { + if id.String() == job.ID.String() { + jobs = append(jobs, job) + break + } + } + } + if len(jobs) == 0 { + return nil, sql.ErrNoRows + } + + return jobs, nil +} + func (q *fakeQuerier) GetProvisionerLogsByIDBetween(_ context.Context, arg database.GetProvisionerLogsByIDBetweenParams) ([]database.ProvisionerJobLog, error) { q.mutex.Lock() defer q.mutex.Unlock() @@ -734,13 +784,14 @@ func (q *fakeQuerier) InsertProjectVersion(_ context.Context, arg database.Inser //nolint:gosimple version := database.ProjectVersion{ - ID: arg.ID, - ProjectID: arg.ProjectID, - CreatedAt: arg.CreatedAt, - UpdatedAt: arg.UpdatedAt, - Name: arg.Name, - Description: arg.Description, - ImportJobID: arg.ImportJobID, + ID: arg.ID, + ProjectID: arg.ProjectID, + OrganizationID: arg.OrganizationID, + CreatedAt: arg.CreatedAt, + UpdatedAt: arg.UpdatedAt, + Name: arg.Name, + Description: arg.Description, + JobID: arg.JobID, } q.projectVersion = append(q.projectVersion, version) return version, nil @@ -797,10 +848,11 @@ func (q *fakeQuerier) InsertProvisionerDaemon(_ context.Context, arg database.In defer q.mutex.Unlock() daemon := database.ProvisionerDaemon{ - ID: arg.ID, - CreatedAt: arg.CreatedAt, - Name: arg.Name, - Provisioners: arg.Provisioners, + ID: arg.ID, + CreatedAt: arg.CreatedAt, + OrganizationID: arg.OrganizationID, + Name: arg.Name, + Provisioners: arg.Provisioners, } q.provisionerDaemons = append(q.provisionerDaemons, daemon) return daemon, nil @@ -826,12 +878,12 @@ func (q *fakeQuerier) InsertProvisionerJob(_ context.Context, arg database.Inser return job, nil } -func (q *fakeQuerier) InsertProvisionerJobAgent(_ context.Context, arg database.InsertProvisionerJobAgentParams) (database.ProvisionerJobAgent, error) { +func (q *fakeQuerier) InsertWorkspaceAgent(_ context.Context, arg database.InsertWorkspaceAgentParams) (database.WorkspaceAgent, error) { q.mutex.Lock() defer q.mutex.Unlock() //nolint:gosimple - agent := database.ProvisionerJobAgent{ + agent := database.WorkspaceAgent{ ID: arg.ID, CreatedAt: arg.CreatedAt, UpdatedAt: arg.UpdatedAt, @@ -847,12 +899,12 @@ func (q *fakeQuerier) InsertProvisionerJobAgent(_ context.Context, arg database. return agent, nil } -func (q *fakeQuerier) InsertProvisionerJobResource(_ context.Context, arg database.InsertProvisionerJobResourceParams) (database.ProvisionerJobResource, error) { +func (q *fakeQuerier) InsertWorkspaceResource(_ context.Context, arg database.InsertWorkspaceResourceParams) (database.WorkspaceResource, error) { q.mutex.Lock() defer q.mutex.Unlock() //nolint:gosimple - resource := database.ProvisionerJobResource{ + resource := database.WorkspaceResource{ ID: arg.ID, CreatedAt: arg.CreatedAt, JobID: arg.JobID, @@ -900,11 +952,11 @@ func (q *fakeQuerier) InsertWorkspace(_ context.Context, arg database.InsertWork return workspace, nil } -func (q *fakeQuerier) InsertWorkspaceHistory(_ context.Context, arg database.InsertWorkspaceHistoryParams) (database.WorkspaceHistory, error) { +func (q *fakeQuerier) InsertWorkspaceBuild(_ context.Context, arg database.InsertWorkspaceBuildParams) (database.WorkspaceBuild, error) { q.mutex.Lock() defer q.mutex.Unlock() - workspaceHistory := database.WorkspaceHistory{ + workspaceBuild := database.WorkspaceBuild{ ID: arg.ID, CreatedAt: arg.CreatedAt, UpdatedAt: arg.UpdatedAt, @@ -914,11 +966,11 @@ func (q *fakeQuerier) InsertWorkspaceHistory(_ context.Context, arg database.Ins BeforeID: arg.BeforeID, Transition: arg.Transition, Initiator: arg.Initiator, - ProvisionJobID: arg.ProvisionJobID, + JobID: arg.JobID, ProvisionerState: arg.ProvisionerState, } - q.workspaceHistory = append(q.workspaceHistory, workspaceHistory) - return workspaceHistory, nil + q.workspaceBuild = append(q.workspaceBuild, workspaceBuild) + return workspaceBuild, nil } func (q *fakeQuerier) UpdateAPIKeyByID(_ context.Context, arg database.UpdateAPIKeyByIDParams) error { @@ -940,6 +992,22 @@ func (q *fakeQuerier) UpdateAPIKeyByID(_ context.Context, arg database.UpdateAPI return sql.ErrNoRows } +func (q *fakeQuerier) UpdateProjectVersionByID(_ context.Context, arg database.UpdateProjectVersionByIDParams) error { + q.mutex.Lock() + defer q.mutex.Unlock() + + for index, projectVersion := range q.projectVersion { + if projectVersion.ID.String() != arg.ID.String() { + continue + } + projectVersion.ProjectID = arg.ProjectID + projectVersion.UpdatedAt = arg.UpdatedAt + q.projectVersion[index] = projectVersion + return nil + } + return sql.ErrNoRows +} + func (q *fakeQuerier) UpdateProvisionerDaemonByID(_ context.Context, arg database.UpdateProvisionerDaemonByIDParams) error { q.mutex.Lock() defer q.mutex.Unlock() @@ -956,6 +1024,21 @@ func (q *fakeQuerier) UpdateProvisionerDaemonByID(_ context.Context, arg databas return sql.ErrNoRows } +func (q *fakeQuerier) UpdateWorkspaceAgentByID(_ context.Context, arg database.UpdateWorkspaceAgentByIDParams) error { + q.mutex.Lock() + defer q.mutex.Unlock() + + for index, agent := range q.provisionerJobAgent { + if agent.ID.String() != arg.ID.String() { + continue + } + agent.UpdatedAt = arg.UpdatedAt + q.provisionerJobAgent[index] = agent + return nil + } + return sql.ErrNoRows +} + func (q *fakeQuerier) UpdateProvisionerJobByID(_ context.Context, arg database.UpdateProvisionerJobByIDParams) error { q.mutex.Lock() defer q.mutex.Unlock() @@ -989,18 +1072,18 @@ func (q *fakeQuerier) UpdateProvisionerJobWithCompleteByID(_ context.Context, ar return sql.ErrNoRows } -func (q *fakeQuerier) UpdateWorkspaceHistoryByID(_ context.Context, arg database.UpdateWorkspaceHistoryByIDParams) error { +func (q *fakeQuerier) UpdateWorkspaceBuildByID(_ context.Context, arg database.UpdateWorkspaceBuildByIDParams) error { q.mutex.Lock() defer q.mutex.Unlock() - for index, workspaceHistory := range q.workspaceHistory { - if workspaceHistory.ID.String() != arg.ID.String() { + for index, workspaceBuild := range q.workspaceBuild { + if workspaceBuild.ID.String() != arg.ID.String() { continue } - workspaceHistory.UpdatedAt = arg.UpdatedAt - workspaceHistory.AfterID = arg.AfterID - workspaceHistory.ProvisionerState = arg.ProvisionerState - q.workspaceHistory[index] = workspaceHistory + workspaceBuild.UpdatedAt = arg.UpdatedAt + workspaceBuild.AfterID = arg.AfterID + workspaceBuild.ProvisionerState = arg.ProvisionerState + q.workspaceBuild[index] = workspaceBuild return nil } return sql.ErrNoRows diff --git a/database/dump.sql b/database/dump.sql index 23095aad1819f..479b878b30a6a 100644 --- a/database/dump.sql +++ b/database/dump.sql @@ -45,7 +45,7 @@ CREATE TYPE parameter_type_system AS ENUM ( CREATE TYPE provisioner_job_type AS ENUM ( 'project_version_import', - 'workspace_provision' + 'workspace_build' ); CREATE TYPE provisioner_storage_method AS ENUM ( @@ -143,11 +143,11 @@ CREATE TABLE parameter_schema ( CREATE TABLE parameter_value ( id uuid NOT NULL, - name character varying(64) NOT NULL, created_at timestamp with time zone NOT NULL, updated_at timestamp with time zone NOT NULL, scope parameter_scope NOT NULL, scope_id text NOT NULL, + name character varying(64) NOT NULL, source_scheme parameter_source_scheme NOT NULL, source_value text NOT NULL, destination_scheme parameter_destination_scheme NOT NULL @@ -165,18 +165,20 @@ CREATE TABLE project ( CREATE TABLE project_version ( id uuid NOT NULL, - project_id uuid NOT NULL, + project_id uuid, + organization_id text NOT NULL, created_at timestamp with time zone NOT NULL, updated_at timestamp with time zone NOT NULL, name character varying(64) NOT NULL, description character varying(1048576) NOT NULL, - import_job_id uuid NOT NULL + job_id uuid NOT NULL ); CREATE TABLE provisioner_daemon ( id uuid NOT NULL, created_at timestamp with time zone NOT NULL, updated_at timestamp with time zone, + organization_id text, name character varying(64) NOT NULL, provisioners provisioner_type[] NOT NULL ); @@ -199,19 +201,6 @@ CREATE TABLE provisioner_job ( worker_id uuid ); -CREATE TABLE provisioner_job_agent ( - id uuid NOT NULL, - created_at timestamp with time zone NOT NULL, - updated_at timestamp with time zone, - resource_id uuid NOT NULL, - auth_token uuid NOT NULL, - auth_instance_id character varying(64), - environment_variables jsonb, - startup_script character varying(65534), - instance_metadata jsonb, - resource_metadata jsonb -); - CREATE TABLE provisioner_job_log ( id uuid NOT NULL, job_id uuid NOT NULL, @@ -221,16 +210,6 @@ CREATE TABLE provisioner_job_log ( output character varying(1024) NOT NULL ); -CREATE TABLE provisioner_job_resource ( - id uuid NOT NULL, - created_at timestamp with time zone NOT NULL, - job_id uuid NOT NULL, - transition workspace_transition NOT NULL, - type character varying(256) NOT NULL, - name character varying(64) NOT NULL, - agent_id uuid -); - CREATE TABLE users ( id text NOT NULL, email text NOT NULL, @@ -262,7 +241,20 @@ CREATE TABLE workspace ( name character varying(64) NOT NULL ); -CREATE TABLE workspace_history ( +CREATE TABLE workspace_agent ( + id uuid NOT NULL, + created_at timestamp with time zone NOT NULL, + updated_at timestamp with time zone, + resource_id uuid NOT NULL, + auth_token uuid NOT NULL, + auth_instance_id character varying(64), + environment_variables jsonb, + startup_script character varying(65534), + instance_metadata jsonb, + resource_metadata jsonb +); + +CREATE TABLE workspace_build ( id uuid NOT NULL, created_at timestamp with time zone NOT NULL, updated_at timestamp with time zone NOT NULL, @@ -274,7 +266,17 @@ CREATE TABLE workspace_history ( transition workspace_transition NOT NULL, initiator character varying(255) NOT NULL, provisioner_state bytea, - provision_job_id uuid NOT NULL + job_id uuid NOT NULL +); + +CREATE TABLE workspace_resource ( + id uuid NOT NULL, + created_at timestamp with time zone NOT NULL, + job_id uuid NOT NULL, + transition workspace_transition NOT NULL, + type character varying(256) NOT NULL, + name character varying(64) NOT NULL, + agent_id uuid ); ALTER TABLE ONLY file @@ -290,7 +292,7 @@ ALTER TABLE ONLY parameter_value ADD CONSTRAINT parameter_value_id_key UNIQUE (id); ALTER TABLE ONLY parameter_value - ADD CONSTRAINT parameter_value_name_scope_scope_id_key UNIQUE (name, scope, scope_id); + ADD CONSTRAINT parameter_value_scope_id_name_key UNIQUE (scope_id, name); ALTER TABLE ONLY project ADD CONSTRAINT project_id_key UNIQUE (id); @@ -310,26 +312,26 @@ ALTER TABLE ONLY provisioner_daemon ALTER TABLE ONLY provisioner_daemon ADD CONSTRAINT provisioner_daemon_name_key UNIQUE (name); -ALTER TABLE ONLY provisioner_job_agent - ADD CONSTRAINT provisioner_job_agent_auth_token_key UNIQUE (auth_token); - -ALTER TABLE ONLY provisioner_job_agent - ADD CONSTRAINT provisioner_job_agent_id_key UNIQUE (id); - ALTER TABLE ONLY provisioner_job ADD CONSTRAINT provisioner_job_id_key UNIQUE (id); ALTER TABLE ONLY provisioner_job_log ADD CONSTRAINT provisioner_job_log_id_key UNIQUE (id); -ALTER TABLE ONLY provisioner_job_resource - ADD CONSTRAINT provisioner_job_resource_id_key UNIQUE (id); +ALTER TABLE ONLY workspace_agent + ADD CONSTRAINT workspace_agent_auth_token_key UNIQUE (auth_token); -ALTER TABLE ONLY workspace_history - ADD CONSTRAINT workspace_history_id_key UNIQUE (id); +ALTER TABLE ONLY workspace_agent + ADD CONSTRAINT workspace_agent_id_key UNIQUE (id); -ALTER TABLE ONLY workspace_history - ADD CONSTRAINT workspace_history_workspace_id_name_key UNIQUE (workspace_id, name); +ALTER TABLE ONLY workspace_build + ADD CONSTRAINT workspace_build_id_key UNIQUE (id); + +ALTER TABLE ONLY workspace_build + ADD CONSTRAINT workspace_build_job_id_key UNIQUE (job_id); + +ALTER TABLE ONLY workspace_build + ADD CONSTRAINT workspace_build_workspace_id_name_key UNIQUE (workspace_id, name); ALTER TABLE ONLY workspace ADD CONSTRAINT workspace_id_key UNIQUE (id); @@ -337,27 +339,33 @@ ALTER TABLE ONLY workspace ALTER TABLE ONLY workspace ADD CONSTRAINT workspace_owner_id_name_key UNIQUE (owner_id, name); +ALTER TABLE ONLY workspace_resource + ADD CONSTRAINT workspace_resource_id_key UNIQUE (id); + ALTER TABLE ONLY parameter_schema ADD CONSTRAINT parameter_schema_job_id_fkey FOREIGN KEY (job_id) REFERENCES provisioner_job(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 provisioner_job_agent - ADD CONSTRAINT provisioner_job_agent_resource_id_fkey FOREIGN KEY (resource_id) REFERENCES provisioner_job_resource(id) ON DELETE CASCADE; - ALTER TABLE ONLY provisioner_job_log ADD CONSTRAINT provisioner_job_log_job_id_fkey FOREIGN KEY (job_id) REFERENCES provisioner_job(id) ON DELETE CASCADE; -ALTER TABLE ONLY provisioner_job_resource - ADD CONSTRAINT provisioner_job_resource_job_id_fkey FOREIGN KEY (job_id) REFERENCES provisioner_job(id) ON DELETE CASCADE; +ALTER TABLE ONLY workspace_agent + ADD CONSTRAINT workspace_agent_resource_id_fkey FOREIGN KEY (resource_id) REFERENCES workspace_resource(id) ON DELETE CASCADE; + +ALTER TABLE ONLY workspace_build + ADD CONSTRAINT workspace_build_job_id_fkey FOREIGN KEY (job_id) REFERENCES provisioner_job(id) ON DELETE CASCADE; -ALTER TABLE ONLY workspace_history - ADD CONSTRAINT workspace_history_project_version_id_fkey FOREIGN KEY (project_version_id) REFERENCES project_version(id) ON DELETE CASCADE; +ALTER TABLE ONLY workspace_build + ADD CONSTRAINT workspace_build_project_version_id_fkey FOREIGN KEY (project_version_id) REFERENCES project_version(id) ON DELETE CASCADE; -ALTER TABLE ONLY workspace_history - ADD CONSTRAINT workspace_history_workspace_id_fkey FOREIGN KEY (workspace_id) REFERENCES workspace(id) ON DELETE CASCADE; +ALTER TABLE ONLY workspace_build + ADD CONSTRAINT workspace_build_workspace_id_fkey FOREIGN KEY (workspace_id) REFERENCES workspace(id) ON DELETE CASCADE; ALTER TABLE ONLY workspace ADD CONSTRAINT workspace_project_id_fkey FOREIGN KEY (project_id) REFERENCES project(id); +ALTER TABLE ONLY workspace_resource + ADD CONSTRAINT workspace_resource_job_id_fkey FOREIGN KEY (job_id) REFERENCES provisioner_job(id) ON DELETE CASCADE; + diff --git a/database/migrations/000002_projects.up.sql b/database/migrations/000002_projects.up.sql index 64426143d94d9..49222cd5d8104 100644 --- a/database/migrations/000002_projects.up.sql +++ b/database/migrations/000002_projects.up.sql @@ -33,7 +33,8 @@ CREATE TABLE project ( CREATE TABLE project_version ( id uuid NOT NULL UNIQUE, -- This should be indexed. - project_id uuid NOT NULL REFERENCES project (id), + project_id uuid REFERENCES project (id), + organization_id text NOT NULL, created_at timestamptz NOT NULL, updated_at timestamptz NOT NULL, -- Name is generated for ease of differentiation. @@ -42,9 +43,8 @@ CREATE TABLE project_version ( -- Extracted from a README.md on import. -- Maximum of 1MB. description varchar(1048576) NOT NULL, - -- The import job for a Project Version. This is used - -- to detect if an import was successful. - import_job_id uuid NOT NULL, + -- The job ID for building the project version. + job_id uuid NOT NULL, -- Disallow projects to have the same build name -- multiple times. UNIQUE(project_id, name) diff --git a/database/migrations/000003_workspaces.up.sql b/database/migrations/000003_workspaces.up.sql index 97e3083d31a5f..b06b10362c4e0 100644 --- a/database/migrations/000003_workspaces.up.sql +++ b/database/migrations/000003_workspaces.up.sql @@ -14,21 +14,3 @@ CREATE TYPE workspace_transition AS ENUM ( 'delete' ); --- Workspace transition represents a change in workspace state. -CREATE TABLE workspace_history ( - id uuid NOT NULL UNIQUE, - created_at timestamptz NOT NULL, - updated_at timestamptz NOT NULL, - workspace_id uuid NOT NULL REFERENCES workspace (id) ON DELETE CASCADE, - project_version_id uuid NOT NULL REFERENCES project_version (id) ON DELETE CASCADE, - name varchar(64) NOT NULL, - before_id uuid, - after_id uuid, - transition workspace_transition NOT NULL, - initiator varchar(255) NOT NULL, - -- State stored by the provisioner - provisioner_state bytea, - -- Job ID of the action - provision_job_id uuid NOT NULL, - UNIQUE(workspace_id, name) -); diff --git a/database/migrations/000004_jobs.up.sql b/database/migrations/000004_jobs.up.sql index f8e1ca8db9fd5..8e4ebda49119e 100644 --- a/database/migrations/000004_jobs.up.sql +++ b/database/migrations/000004_jobs.up.sql @@ -2,6 +2,7 @@ CREATE TABLE IF NOT EXISTS provisioner_daemon ( id uuid NOT NULL UNIQUE, created_at timestamptz NOT NULL, updated_at timestamptz, + organization_id text, -- Name is generated for ease of differentiation. -- eg. WowBananas16 name varchar(64) NOT NULL UNIQUE, @@ -10,7 +11,7 @@ CREATE TABLE IF NOT EXISTS provisioner_daemon ( CREATE TYPE provisioner_job_type AS ENUM ( 'project_version_import', - 'workspace_provision' + 'workspace_build' ); CREATE TYPE provisioner_storage_method AS ENUM ('file'); @@ -55,8 +56,7 @@ CREATE TABLE IF NOT EXISTS provisioner_job_log ( output varchar(1024) NOT NULL ); --- Resources from multiple workspace states are stored here post project-import job. -CREATE TABLE provisioner_job_resource ( +CREATE TABLE workspace_resource ( id uuid NOT NULL UNIQUE, created_at timestamptz NOT NULL, job_id uuid NOT NULL REFERENCES provisioner_job(id) ON DELETE CASCADE, @@ -66,12 +66,11 @@ CREATE TABLE provisioner_job_resource ( agent_id uuid ); --- Agents that associate with a specific resource. -CREATE TABLE provisioner_job_agent ( +CREATE TABLE workspace_agent ( id uuid NOT NULL UNIQUE, created_at timestamptz NOT NULL, updated_at timestamptz, - resource_id uuid NOT NULL REFERENCES provisioner_job_resource (id) ON DELETE CASCADE, + resource_id uuid NOT NULL REFERENCES workspace_resource (id) ON DELETE CASCADE, auth_token uuid NOT NULL UNIQUE, auth_instance_id varchar(64), environment_variables jsonb, @@ -133,14 +132,32 @@ CREATE TABLE parameter_schema ( -- Parameters are provided to jobs for provisioning and to workspaces. CREATE TABLE parameter_value ( id uuid NOT NULL UNIQUE, - name varchar(64) NOT NULL, created_at timestamptz NOT NULL, updated_at timestamptz NOT NULL, scope parameter_scope NOT NULL, scope_id text NOT NULL, + name varchar(64) NOT NULL, source_scheme parameter_source_scheme NOT NULL, source_value text NOT NULL, destination_scheme parameter_destination_scheme NOT NULL, -- Prevents duplicates for parameters in the same scope. - UNIQUE(name, scope, scope_id) + UNIQUE(scope_id, name) ); + +CREATE TABLE workspace_build ( + id uuid NOT NULL UNIQUE, + created_at timestamptz NOT NULL, + updated_at timestamptz NOT NULL, + workspace_id uuid NOT NULL REFERENCES workspace (id) ON DELETE CASCADE, + project_version_id uuid NOT NULL REFERENCES project_version (id) ON DELETE CASCADE, + name varchar(64) NOT NULL, + before_id uuid, + after_id uuid, + transition workspace_transition NOT NULL, + initiator varchar(255) NOT NULL, + -- State stored by the provisioner + provisioner_state bytea, + -- Job ID of the action + job_id uuid NOT NULL UNIQUE REFERENCES provisioner_job(id) ON DELETE CASCADE, + UNIQUE(workspace_id, name) +); \ No newline at end of file diff --git a/database/models.go b/database/models.go index bf4ffbbe30524..7a40b3649091b 100644 --- a/database/models.go +++ b/database/models.go @@ -157,7 +157,7 @@ type ProvisionerJobType string const ( ProvisionerJobTypeProjectVersionImport ProvisionerJobType = "project_version_import" - ProvisionerJobTypeWorkspaceProvision ProvisionerJobType = "workspace_provision" + ProvisionerJobTypeWorkspaceBuild ProvisionerJobType = "workspace_build" ) func (e *ProvisionerJobType) Scan(src interface{}) error { @@ -323,11 +323,11 @@ type ParameterSchema struct { type ParameterValue struct { ID uuid.UUID `db:"id" json:"id"` - Name string `db:"name" json:"name"` CreatedAt time.Time `db:"created_at" json:"created_at"` UpdatedAt time.Time `db:"updated_at" json:"updated_at"` Scope ParameterScope `db:"scope" json:"scope"` ScopeID string `db:"scope_id" json:"scope_id"` + Name string `db:"name" json:"name"` SourceScheme ParameterSourceScheme `db:"source_scheme" json:"source_scheme"` SourceValue string `db:"source_value" json:"source_value"` DestinationScheme ParameterDestinationScheme `db:"destination_scheme" json:"destination_scheme"` @@ -344,21 +344,23 @@ type Project struct { } type ProjectVersion struct { - ID uuid.UUID `db:"id" json:"id"` - ProjectID uuid.UUID `db:"project_id" json:"project_id"` - CreatedAt time.Time `db:"created_at" json:"created_at"` - UpdatedAt time.Time `db:"updated_at" json:"updated_at"` - Name string `db:"name" json:"name"` - Description string `db:"description" json:"description"` - ImportJobID uuid.UUID `db:"import_job_id" json:"import_job_id"` + ID uuid.UUID `db:"id" json:"id"` + ProjectID uuid.NullUUID `db:"project_id" json:"project_id"` + OrganizationID string `db:"organization_id" json:"organization_id"` + CreatedAt time.Time `db:"created_at" json:"created_at"` + UpdatedAt time.Time `db:"updated_at" json:"updated_at"` + Name string `db:"name" json:"name"` + Description string `db:"description" json:"description"` + JobID uuid.UUID `db:"job_id" json:"job_id"` } type ProvisionerDaemon struct { - ID uuid.UUID `db:"id" json:"id"` - CreatedAt time.Time `db:"created_at" json:"created_at"` - UpdatedAt sql.NullTime `db:"updated_at" json:"updated_at"` - Name string `db:"name" json:"name"` - Provisioners []ProvisionerType `db:"provisioners" json:"provisioners"` + ID uuid.UUID `db:"id" json:"id"` + CreatedAt time.Time `db:"created_at" json:"created_at"` + UpdatedAt sql.NullTime `db:"updated_at" json:"updated_at"` + OrganizationID sql.NullString `db:"organization_id" json:"organization_id"` + Name string `db:"name" json:"name"` + Provisioners []ProvisionerType `db:"provisioners" json:"provisioners"` } type ProvisionerJob struct { @@ -379,19 +381,6 @@ type ProvisionerJob struct { WorkerID uuid.NullUUID `db:"worker_id" json:"worker_id"` } -type ProvisionerJobAgent struct { - ID uuid.UUID `db:"id" json:"id"` - CreatedAt time.Time `db:"created_at" json:"created_at"` - UpdatedAt sql.NullTime `db:"updated_at" json:"updated_at"` - ResourceID uuid.UUID `db:"resource_id" json:"resource_id"` - AuthToken uuid.UUID `db:"auth_token" json:"auth_token"` - AuthInstanceID sql.NullString `db:"auth_instance_id" json:"auth_instance_id"` - EnvironmentVariables pqtype.NullRawMessage `db:"environment_variables" json:"environment_variables"` - StartupScript sql.NullString `db:"startup_script" json:"startup_script"` - InstanceMetadata pqtype.NullRawMessage `db:"instance_metadata" json:"instance_metadata"` - ResourceMetadata pqtype.NullRawMessage `db:"resource_metadata" json:"resource_metadata"` -} - type ProvisionerJobLog struct { ID uuid.UUID `db:"id" json:"id"` JobID uuid.UUID `db:"job_id" json:"job_id"` @@ -401,16 +390,6 @@ type ProvisionerJobLog struct { Output string `db:"output" json:"output"` } -type ProvisionerJobResource struct { - ID uuid.UUID `db:"id" json:"id"` - CreatedAt time.Time `db:"created_at" json:"created_at"` - JobID uuid.UUID `db:"job_id" json:"job_id"` - Transition WorkspaceTransition `db:"transition" json:"transition"` - Type string `db:"type" json:"type"` - Name string `db:"name" json:"name"` - AgentID uuid.NullUUID `db:"agent_id" json:"agent_id"` -} - type User struct { ID string `db:"id" json:"id"` Email string `db:"email" json:"email"` @@ -442,7 +421,20 @@ type Workspace struct { Name string `db:"name" json:"name"` } -type WorkspaceHistory struct { +type WorkspaceAgent struct { + ID uuid.UUID `db:"id" json:"id"` + CreatedAt time.Time `db:"created_at" json:"created_at"` + UpdatedAt sql.NullTime `db:"updated_at" json:"updated_at"` + ResourceID uuid.UUID `db:"resource_id" json:"resource_id"` + AuthToken uuid.UUID `db:"auth_token" json:"auth_token"` + AuthInstanceID sql.NullString `db:"auth_instance_id" json:"auth_instance_id"` + EnvironmentVariables pqtype.NullRawMessage `db:"environment_variables" json:"environment_variables"` + StartupScript sql.NullString `db:"startup_script" json:"startup_script"` + InstanceMetadata pqtype.NullRawMessage `db:"instance_metadata" json:"instance_metadata"` + ResourceMetadata pqtype.NullRawMessage `db:"resource_metadata" json:"resource_metadata"` +} + +type WorkspaceBuild struct { ID uuid.UUID `db:"id" json:"id"` CreatedAt time.Time `db:"created_at" json:"created_at"` UpdatedAt time.Time `db:"updated_at" json:"updated_at"` @@ -454,5 +446,15 @@ type WorkspaceHistory struct { Transition WorkspaceTransition `db:"transition" json:"transition"` Initiator string `db:"initiator" json:"initiator"` ProvisionerState []byte `db:"provisioner_state" json:"provisioner_state"` - ProvisionJobID uuid.UUID `db:"provision_job_id" json:"provision_job_id"` + JobID uuid.UUID `db:"job_id" json:"job_id"` +} + +type WorkspaceResource struct { + ID uuid.UUID `db:"id" json:"id"` + CreatedAt time.Time `db:"created_at" json:"created_at"` + JobID uuid.UUID `db:"job_id" json:"job_id"` + Transition WorkspaceTransition `db:"transition" json:"transition"` + Type string `db:"type" json:"type"` + Name string `db:"name" json:"name"` + AgentID uuid.NullUUID `db:"agent_id" json:"agent_id"` } diff --git a/database/querier.go b/database/querier.go index faa24ac3a4970..efe59d802276e 100644 --- a/database/querier.go +++ b/database/querier.go @@ -10,6 +10,7 @@ import ( type querier interface { AcquireProvisionerJob(ctx context.Context, arg AcquireProvisionerJobParams) (ProvisionerJob, error) + DeleteParameterValueByID(ctx context.Context, id uuid.UUID) error GetAPIKeyByID(ctx context.Context, id string) (APIKey, error) GetFileByHash(ctx context.Context, hash string) (File, error) GetOrganizationByID(ctx context.Context, id string) (Organization, error) @@ -17,32 +18,35 @@ type querier interface { GetOrganizationMemberByUserID(ctx context.Context, arg GetOrganizationMemberByUserIDParams) (OrganizationMember, error) GetOrganizationsByUserID(ctx context.Context, userID string) ([]Organization, error) GetParameterSchemasByJobID(ctx context.Context, jobID uuid.UUID) ([]ParameterSchema, error) + GetParameterValueByScopeAndName(ctx context.Context, arg GetParameterValueByScopeAndNameParams) (ParameterValue, error) GetParameterValuesByScope(ctx context.Context, arg GetParameterValuesByScopeParams) ([]ParameterValue, error) GetProjectByID(ctx context.Context, id uuid.UUID) (Project, error) GetProjectByOrganizationAndName(ctx context.Context, arg GetProjectByOrganizationAndNameParams) (Project, error) GetProjectVersionByID(ctx context.Context, id uuid.UUID) (ProjectVersion, error) GetProjectVersionByProjectIDAndName(ctx context.Context, arg GetProjectVersionByProjectIDAndNameParams) (ProjectVersion, error) - GetProjectVersionsByProjectID(ctx context.Context, projectID uuid.UUID) ([]ProjectVersion, error) - GetProjectsByOrganizationIDs(ctx context.Context, ids []string) ([]Project, error) + GetProjectVersionsByProjectID(ctx context.Context, dollar_1 uuid.UUID) ([]ProjectVersion, error) + GetProjectsByOrganization(ctx context.Context, organizationID string) ([]Project, error) GetProvisionerDaemonByID(ctx context.Context, id uuid.UUID) (ProvisionerDaemon, error) GetProvisionerDaemons(ctx context.Context) ([]ProvisionerDaemon, error) - GetProvisionerJobAgentByInstanceID(ctx context.Context, authInstanceID string) (ProvisionerJobAgent, error) - GetProvisionerJobAgentsByResourceIDs(ctx context.Context, ids []uuid.UUID) ([]ProvisionerJobAgent, error) GetProvisionerJobByID(ctx context.Context, id uuid.UUID) (ProvisionerJob, error) - GetProvisionerJobResourceByID(ctx context.Context, id uuid.UUID) (ProvisionerJobResource, error) - GetProvisionerJobResourcesByJobID(ctx context.Context, jobID uuid.UUID) ([]ProvisionerJobResource, error) + GetProvisionerJobsByIDs(ctx context.Context, ids []uuid.UUID) ([]ProvisionerJob, error) GetProvisionerLogsByIDBetween(ctx context.Context, arg GetProvisionerLogsByIDBetweenParams) ([]ProvisionerJobLog, error) GetUserByEmailOrUsername(ctx context.Context, arg GetUserByEmailOrUsernameParams) (User, error) GetUserByID(ctx context.Context, id string) (User, error) GetUserCount(ctx context.Context) (int64, error) + GetWorkspaceAgentByAuthToken(ctx context.Context, authToken uuid.UUID) (WorkspaceAgent, error) + GetWorkspaceAgentByInstanceID(ctx context.Context, authInstanceID string) (WorkspaceAgent, error) + GetWorkspaceAgentByResourceID(ctx context.Context, resourceID uuid.UUID) (WorkspaceAgent, error) + GetWorkspaceBuildByID(ctx context.Context, id uuid.UUID) (WorkspaceBuild, error) + GetWorkspaceBuildByJobID(ctx context.Context, jobID uuid.UUID) (WorkspaceBuild, error) + GetWorkspaceBuildByWorkspaceID(ctx context.Context, workspaceID uuid.UUID) ([]WorkspaceBuild, error) + GetWorkspaceBuildByWorkspaceIDAndName(ctx context.Context, arg GetWorkspaceBuildByWorkspaceIDAndNameParams) (WorkspaceBuild, error) + GetWorkspaceBuildByWorkspaceIDWithoutAfter(ctx context.Context, workspaceID uuid.UUID) (WorkspaceBuild, error) GetWorkspaceByID(ctx context.Context, id uuid.UUID) (Workspace, error) GetWorkspaceByUserIDAndName(ctx context.Context, arg GetWorkspaceByUserIDAndNameParams) (Workspace, error) - GetWorkspaceHistoryByID(ctx context.Context, id uuid.UUID) (WorkspaceHistory, error) - GetWorkspaceHistoryByWorkspaceID(ctx context.Context, workspaceID uuid.UUID) ([]WorkspaceHistory, error) - GetWorkspaceHistoryByWorkspaceIDAndName(ctx context.Context, arg GetWorkspaceHistoryByWorkspaceIDAndNameParams) (WorkspaceHistory, error) - GetWorkspaceHistoryByWorkspaceIDWithoutAfter(ctx context.Context, workspaceID uuid.UUID) (WorkspaceHistory, error) GetWorkspaceOwnerCountsByProjectIDs(ctx context.Context, ids []uuid.UUID) ([]GetWorkspaceOwnerCountsByProjectIDsRow, error) - GetWorkspacesByProjectAndUserID(ctx context.Context, arg GetWorkspacesByProjectAndUserIDParams) ([]Workspace, error) + GetWorkspaceResourceByID(ctx context.Context, id uuid.UUID) (WorkspaceResource, error) + GetWorkspaceResourcesByJobID(ctx context.Context, jobID uuid.UUID) ([]WorkspaceResource, error) GetWorkspacesByUserID(ctx context.Context, ownerID string) ([]Workspace, error) InsertAPIKey(ctx context.Context, arg InsertAPIKeyParams) (APIKey, error) InsertFile(ctx context.Context, arg InsertFileParams) (File, error) @@ -54,17 +58,19 @@ type querier interface { InsertProjectVersion(ctx context.Context, arg InsertProjectVersionParams) (ProjectVersion, error) InsertProvisionerDaemon(ctx context.Context, arg InsertProvisionerDaemonParams) (ProvisionerDaemon, error) InsertProvisionerJob(ctx context.Context, arg InsertProvisionerJobParams) (ProvisionerJob, error) - InsertProvisionerJobAgent(ctx context.Context, arg InsertProvisionerJobAgentParams) (ProvisionerJobAgent, error) InsertProvisionerJobLogs(ctx context.Context, arg InsertProvisionerJobLogsParams) ([]ProvisionerJobLog, error) - InsertProvisionerJobResource(ctx context.Context, arg InsertProvisionerJobResourceParams) (ProvisionerJobResource, error) InsertUser(ctx context.Context, arg InsertUserParams) (User, error) InsertWorkspace(ctx context.Context, arg InsertWorkspaceParams) (Workspace, error) - InsertWorkspaceHistory(ctx context.Context, arg InsertWorkspaceHistoryParams) (WorkspaceHistory, error) + InsertWorkspaceAgent(ctx context.Context, arg InsertWorkspaceAgentParams) (WorkspaceAgent, error) + InsertWorkspaceBuild(ctx context.Context, arg InsertWorkspaceBuildParams) (WorkspaceBuild, error) + InsertWorkspaceResource(ctx context.Context, arg InsertWorkspaceResourceParams) (WorkspaceResource, error) UpdateAPIKeyByID(ctx context.Context, arg UpdateAPIKeyByIDParams) error + UpdateProjectVersionByID(ctx context.Context, arg UpdateProjectVersionByIDParams) error UpdateProvisionerDaemonByID(ctx context.Context, arg UpdateProvisionerDaemonByIDParams) error UpdateProvisionerJobByID(ctx context.Context, arg UpdateProvisionerJobByIDParams) error UpdateProvisionerJobWithCompleteByID(ctx context.Context, arg UpdateProvisionerJobWithCompleteByIDParams) error - UpdateWorkspaceHistoryByID(ctx context.Context, arg UpdateWorkspaceHistoryByIDParams) error + UpdateWorkspaceAgentByID(ctx context.Context, arg UpdateWorkspaceAgentByIDParams) error + UpdateWorkspaceBuildByID(ctx context.Context, arg UpdateWorkspaceBuildByIDParams) error } var _ querier = (*sqlQuerier)(nil) diff --git a/database/query.sql b/database/query.sql index eabea0c43d709..18aa7f1c9b625 100644 --- a/database/query.sql +++ b/database/query.sql @@ -36,6 +36,12 @@ WHERE 1 ) RETURNING *; +-- name: DeleteParameterValueByID :exec +DELETE FROM + parameter_value +WHERE + id = $1; + -- name: GetAPIKeyByID :one SELECT * @@ -136,6 +142,18 @@ WHERE scope = $1 AND scope_id = $2; +-- name: GetParameterValueByScopeAndName :one +SELECT + * +FROM + parameter_value +WHERE + scope = $1 + AND scope_id = $2 + AND name = $3 +LIMIT + 1; + -- name: GetProjectByID :one SELECT * @@ -157,13 +175,13 @@ WHERE LIMIT 1; --- name: GetProjectsByOrganizationIDs :many +-- name: GetProjectsByOrganization :many SELECT * FROM project WHERE - organization_id = ANY(@ids :: text [ ]); + organization_id = $1; -- name: GetParameterSchemasByJobID :many SELECT @@ -179,7 +197,7 @@ SELECT FROM project_version WHERE - project_id = $1; + project_id = $1 :: uuid; -- name: GetProjectVersionByProjectIDAndName :one SELECT @@ -226,11 +244,19 @@ SELECT FROM provisioner_daemon; --- name: GetProvisionerJobAgentByInstanceID :one +-- name: GetWorkspaceAgentByAuthToken :one SELECT * FROM - provisioner_job_agent + workspace_agent +WHERE + auth_token = $1; + +-- name: GetWorkspaceAgentByInstanceID :one +SELECT + * +FROM + workspace_agent WHERE auth_instance_id = @auth_instance_id :: text ORDER BY @@ -244,6 +270,14 @@ FROM WHERE id = $1; +-- name: GetProvisionerJobsByIDs :many +SELECT + * +FROM + provisioner_job +WHERE + id = ANY(@ids :: uuid [ ]); + -- name: GetWorkspaceByID :one SELECT * @@ -271,15 +305,6 @@ WHERE owner_id = @owner_id AND LOWER(name) = LOWER(@name); --- name: GetWorkspacesByProjectAndUserID :many -SELECT - * -FROM - workspace -WHERE - owner_id = $1 - AND project_id = $2; - -- name: GetWorkspaceOwnerCountsByProjectIDs :many SELECT project_id, @@ -292,67 +317,77 @@ GROUP BY project_id, owner_id; --- name: GetWorkspaceHistoryByID :one +-- name: GetWorkspaceBuildByID :one SELECT * FROM - workspace_history + workspace_build WHERE id = $1 LIMIT 1; --- name: GetWorkspaceHistoryByWorkspaceIDAndName :one +-- name: GetWorkspaceBuildByJobID :one SELECT * FROM - workspace_history + workspace_build +WHERE + job_id = $1 +LIMIT + 1; + +-- name: GetWorkspaceBuildByWorkspaceIDAndName :one +SELECT + * +FROM + workspace_build WHERE workspace_id = $1 AND name = $2; --- name: GetWorkspaceHistoryByWorkspaceID :many +-- name: GetWorkspaceBuildByWorkspaceID :many SELECT * FROM - workspace_history + workspace_build WHERE workspace_id = $1; --- name: GetWorkspaceHistoryByWorkspaceIDWithoutAfter :one +-- name: GetWorkspaceBuildByWorkspaceIDWithoutAfter :one SELECT * FROM - workspace_history + workspace_build WHERE workspace_id = $1 AND after_id IS NULL LIMIT 1; --- name: GetProvisionerJobResourceByID :one +-- name: GetWorkspaceResourceByID :one SELECT * FROM - provisioner_job_resource + workspace_resource WHERE id = $1; --- name: GetProvisionerJobResourcesByJobID :many +-- name: GetWorkspaceResourcesByJobID :many SELECT * FROM - provisioner_job_resource + workspace_resource WHERE job_id = $1; --- name: GetProvisionerJobAgentsByResourceIDs :many +-- name: GetWorkspaceAgentByResourceID :one SELECT * FROM - provisioner_job_agent + workspace_agent WHERE - resource_id = ANY(@ids :: uuid [ ]); + resource_id = $1; -- name: InsertAPIKey :one INSERT INTO @@ -457,9 +492,9 @@ INSERT INTO VALUES ($1, $2, $3, $4, $5, $6, $7) RETURNING *; --- name: InsertProvisionerJobResource :one +-- name: InsertWorkspaceResource :one INSERT INTO - provisioner_job_resource ( + workspace_resource ( id, created_at, job_id, @@ -476,14 +511,15 @@ INSERT INTO project_version ( id, project_id, + organization_id, created_at, updated_at, name, description, - import_job_id + job_id ) VALUES - ($1, $2, $3, $4, $5, $6, $7) RETURNING *; + ($1, $2, $3, $4, $5, $6, $7, $8) RETURNING *; -- name: InsertParameterSchema :one INSERT INTO @@ -527,9 +563,9 @@ VALUES -- name: InsertProvisionerDaemon :one INSERT INTO - provisioner_daemon (id, created_at, name, provisioners) + provisioner_daemon (id, created_at, organization_id, name, provisioners) VALUES - ($1, $2, $3, $4) RETURNING *; + ($1, $2, $3, $4, $5) RETURNING *; -- name: InsertProvisionerJob :one INSERT INTO @@ -577,9 +613,9 @@ INSERT INTO VALUES ($1, $2, $3, $4, $5, $6) RETURNING *; --- name: InsertProvisionerJobAgent :one +-- name: InsertWorkspaceAgent :one INSERT INTO - provisioner_job_agent ( + workspace_agent ( id, created_at, updated_at, @@ -594,9 +630,9 @@ INSERT INTO VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10) RETURNING *; --- name: InsertWorkspaceHistory :one +-- name: InsertWorkspaceBuild :one INSERT INTO - workspace_history ( + workspace_build ( id, created_at, updated_at, @@ -606,7 +642,7 @@ INSERT INTO name, transition, initiator, - provision_job_id, + job_id, provisioner_state ) VALUES @@ -624,6 +660,15 @@ SET WHERE id = $1; +-- name: UpdateProjectVersionByID :exec +UPDATE + project_version +SET + project_id = $2, + updated_at = $3 +WHERE + id = $1; + -- name: UpdateProvisionerDaemonByID :exec UPDATE provisioner_daemon @@ -652,9 +697,17 @@ SET WHERE id = $1; --- name: UpdateWorkspaceHistoryByID :exec +-- name: UpdateWorkspaceAgentByID :exec +UPDATE + workspace_agent +SET + updated_at = $2 +WHERE + id = $1; + +-- name: UpdateWorkspaceBuildByID :exec UPDATE - workspace_history + workspace_build SET updated_at = $2, after_id = $3, diff --git a/database/query.sql.go b/database/query.sql.go index c74ecbcbf94e5..57dbc7e4af671 100644 --- a/database/query.sql.go +++ b/database/query.sql.go @@ -76,6 +76,18 @@ func (q *sqlQuerier) AcquireProvisionerJob(ctx context.Context, arg AcquireProvi return i, err } +const deleteParameterValueByID = `-- name: DeleteParameterValueByID :exec +DELETE FROM + parameter_value +WHERE + id = $1 +` + +func (q *sqlQuerier) DeleteParameterValueByID(ctx context.Context, id uuid.UUID) error { + _, err := q.db.ExecContext(ctx, deleteParameterValueByID, id) + return err +} + const getAPIKeyByID = `-- name: GetAPIKeyByID :one SELECT id, hashed_secret, user_id, application, name, last_used, expires_at, created_at, updated_at, login_type, oidc_access_token, oidc_refresh_token, oidc_id_token, oidc_expiry, devurl_token @@ -319,9 +331,45 @@ func (q *sqlQuerier) GetParameterSchemasByJobID(ctx context.Context, jobID uuid. return items, nil } +const getParameterValueByScopeAndName = `-- name: GetParameterValueByScopeAndName :one +SELECT + id, created_at, updated_at, scope, scope_id, name, source_scheme, source_value, destination_scheme +FROM + parameter_value +WHERE + scope = $1 + AND scope_id = $2 + AND name = $3 +LIMIT + 1 +` + +type GetParameterValueByScopeAndNameParams struct { + Scope ParameterScope `db:"scope" json:"scope"` + ScopeID string `db:"scope_id" json:"scope_id"` + Name string `db:"name" json:"name"` +} + +func (q *sqlQuerier) GetParameterValueByScopeAndName(ctx context.Context, arg GetParameterValueByScopeAndNameParams) (ParameterValue, error) { + row := q.db.QueryRowContext(ctx, getParameterValueByScopeAndName, arg.Scope, arg.ScopeID, arg.Name) + var i ParameterValue + err := row.Scan( + &i.ID, + &i.CreatedAt, + &i.UpdatedAt, + &i.Scope, + &i.ScopeID, + &i.Name, + &i.SourceScheme, + &i.SourceValue, + &i.DestinationScheme, + ) + return i, err +} + const getParameterValuesByScope = `-- name: GetParameterValuesByScope :many SELECT - id, name, created_at, updated_at, scope, scope_id, source_scheme, source_value, destination_scheme + id, created_at, updated_at, scope, scope_id, name, source_scheme, source_value, destination_scheme FROM parameter_value WHERE @@ -345,11 +393,11 @@ func (q *sqlQuerier) GetParameterValuesByScope(ctx context.Context, arg GetParam var i ParameterValue if err := rows.Scan( &i.ID, - &i.Name, &i.CreatedAt, &i.UpdatedAt, &i.Scope, &i.ScopeID, + &i.Name, &i.SourceScheme, &i.SourceValue, &i.DestinationScheme, @@ -427,7 +475,7 @@ func (q *sqlQuerier) GetProjectByOrganizationAndName(ctx context.Context, arg Ge const getProjectVersionByID = `-- name: GetProjectVersionByID :one SELECT - id, project_id, created_at, updated_at, name, description, import_job_id + id, project_id, organization_id, created_at, updated_at, name, description, job_id FROM project_version WHERE @@ -440,18 +488,19 @@ func (q *sqlQuerier) GetProjectVersionByID(ctx context.Context, id uuid.UUID) (P err := row.Scan( &i.ID, &i.ProjectID, + &i.OrganizationID, &i.CreatedAt, &i.UpdatedAt, &i.Name, &i.Description, - &i.ImportJobID, + &i.JobID, ) return i, err } const getProjectVersionByProjectIDAndName = `-- name: GetProjectVersionByProjectIDAndName :one SELECT - id, project_id, created_at, updated_at, name, description, import_job_id + id, project_id, organization_id, created_at, updated_at, name, description, job_id FROM project_version WHERE @@ -460,8 +509,8 @@ WHERE ` type GetProjectVersionByProjectIDAndNameParams struct { - ProjectID uuid.UUID `db:"project_id" json:"project_id"` - Name string `db:"name" json:"name"` + ProjectID uuid.NullUUID `db:"project_id" json:"project_id"` + Name string `db:"name" json:"name"` } func (q *sqlQuerier) GetProjectVersionByProjectIDAndName(ctx context.Context, arg GetProjectVersionByProjectIDAndNameParams) (ProjectVersion, error) { @@ -470,26 +519,27 @@ func (q *sqlQuerier) GetProjectVersionByProjectIDAndName(ctx context.Context, ar err := row.Scan( &i.ID, &i.ProjectID, + &i.OrganizationID, &i.CreatedAt, &i.UpdatedAt, &i.Name, &i.Description, - &i.ImportJobID, + &i.JobID, ) return i, err } const getProjectVersionsByProjectID = `-- name: GetProjectVersionsByProjectID :many SELECT - id, project_id, created_at, updated_at, name, description, import_job_id + id, project_id, organization_id, created_at, updated_at, name, description, job_id FROM project_version WHERE - project_id = $1 + project_id = $1 :: uuid ` -func (q *sqlQuerier) GetProjectVersionsByProjectID(ctx context.Context, projectID uuid.UUID) ([]ProjectVersion, error) { - rows, err := q.db.QueryContext(ctx, getProjectVersionsByProjectID, projectID) +func (q *sqlQuerier) GetProjectVersionsByProjectID(ctx context.Context, dollar_1 uuid.UUID) ([]ProjectVersion, error) { + rows, err := q.db.QueryContext(ctx, getProjectVersionsByProjectID, dollar_1) if err != nil { return nil, err } @@ -500,11 +550,12 @@ func (q *sqlQuerier) GetProjectVersionsByProjectID(ctx context.Context, projectI if err := rows.Scan( &i.ID, &i.ProjectID, + &i.OrganizationID, &i.CreatedAt, &i.UpdatedAt, &i.Name, &i.Description, - &i.ImportJobID, + &i.JobID, ); err != nil { return nil, err } @@ -519,17 +570,17 @@ func (q *sqlQuerier) GetProjectVersionsByProjectID(ctx context.Context, projectI return items, nil } -const getProjectsByOrganizationIDs = `-- name: GetProjectsByOrganizationIDs :many +const getProjectsByOrganization = `-- name: GetProjectsByOrganization :many SELECT id, created_at, updated_at, organization_id, name, provisioner, active_version_id FROM project WHERE - organization_id = ANY($1 :: text [ ]) + organization_id = $1 ` -func (q *sqlQuerier) GetProjectsByOrganizationIDs(ctx context.Context, ids []string) ([]Project, error) { - rows, err := q.db.QueryContext(ctx, getProjectsByOrganizationIDs, pq.Array(ids)) +func (q *sqlQuerier) GetProjectsByOrganization(ctx context.Context, organizationID string) ([]Project, error) { + rows, err := q.db.QueryContext(ctx, getProjectsByOrganization, organizationID) if err != nil { return nil, err } @@ -561,7 +612,7 @@ func (q *sqlQuerier) GetProjectsByOrganizationIDs(ctx context.Context, ids []str const getProvisionerDaemonByID = `-- name: GetProvisionerDaemonByID :one SELECT - id, created_at, updated_at, name, provisioners + id, created_at, updated_at, organization_id, name, provisioners FROM provisioner_daemon WHERE @@ -575,6 +626,7 @@ func (q *sqlQuerier) GetProvisionerDaemonByID(ctx context.Context, id uuid.UUID) &i.ID, &i.CreatedAt, &i.UpdatedAt, + &i.OrganizationID, &i.Name, pq.Array(&i.Provisioners), ) @@ -583,7 +635,7 @@ func (q *sqlQuerier) GetProvisionerDaemonByID(ctx context.Context, id uuid.UUID) const getProvisionerDaemons = `-- name: GetProvisionerDaemons :many SELECT - id, created_at, updated_at, name, provisioners + id, created_at, updated_at, organization_id, name, provisioners FROM provisioner_daemon ` @@ -601,6 +653,7 @@ func (q *sqlQuerier) GetProvisionerDaemons(ctx context.Context) ([]ProvisionerDa &i.ID, &i.CreatedAt, &i.UpdatedAt, + &i.OrganizationID, &i.Name, pq.Array(&i.Provisioners), ); err != nil { @@ -617,78 +670,6 @@ func (q *sqlQuerier) GetProvisionerDaemons(ctx context.Context) ([]ProvisionerDa return items, nil } -const getProvisionerJobAgentByInstanceID = `-- name: GetProvisionerJobAgentByInstanceID :one -SELECT - id, created_at, updated_at, resource_id, auth_token, auth_instance_id, environment_variables, startup_script, instance_metadata, resource_metadata -FROM - provisioner_job_agent -WHERE - auth_instance_id = $1 :: text -ORDER BY - created_at DESC -` - -func (q *sqlQuerier) GetProvisionerJobAgentByInstanceID(ctx context.Context, authInstanceID string) (ProvisionerJobAgent, error) { - row := q.db.QueryRowContext(ctx, getProvisionerJobAgentByInstanceID, authInstanceID) - var i ProvisionerJobAgent - err := row.Scan( - &i.ID, - &i.CreatedAt, - &i.UpdatedAt, - &i.ResourceID, - &i.AuthToken, - &i.AuthInstanceID, - &i.EnvironmentVariables, - &i.StartupScript, - &i.InstanceMetadata, - &i.ResourceMetadata, - ) - return i, err -} - -const getProvisionerJobAgentsByResourceIDs = `-- name: GetProvisionerJobAgentsByResourceIDs :many -SELECT - id, created_at, updated_at, resource_id, auth_token, auth_instance_id, environment_variables, startup_script, instance_metadata, resource_metadata -FROM - provisioner_job_agent -WHERE - resource_id = ANY($1 :: uuid [ ]) -` - -func (q *sqlQuerier) GetProvisionerJobAgentsByResourceIDs(ctx context.Context, ids []uuid.UUID) ([]ProvisionerJobAgent, error) { - rows, err := q.db.QueryContext(ctx, getProvisionerJobAgentsByResourceIDs, pq.Array(ids)) - if err != nil { - return nil, err - } - defer rows.Close() - var items []ProvisionerJobAgent - for rows.Next() { - var i ProvisionerJobAgent - if err := rows.Scan( - &i.ID, - &i.CreatedAt, - &i.UpdatedAt, - &i.ResourceID, - &i.AuthToken, - &i.AuthInstanceID, - &i.EnvironmentVariables, - &i.StartupScript, - &i.InstanceMetadata, - &i.ResourceMetadata, - ); err != nil { - return nil, err - } - items = append(items, i) - } - if err := rows.Close(); err != nil { - return nil, err - } - if err := rows.Err(); err != nil { - return nil, err - } - return items, nil -} - const getProvisionerJobByID = `-- name: GetProvisionerJobByID :one SELECT id, created_at, updated_at, started_at, cancelled_at, completed_at, error, organization_id, initiator_id, provisioner, storage_method, storage_source, type, input, worker_id @@ -721,56 +702,40 @@ func (q *sqlQuerier) GetProvisionerJobByID(ctx context.Context, id uuid.UUID) (P return i, err } -const getProvisionerJobResourceByID = `-- name: GetProvisionerJobResourceByID :one -SELECT - id, created_at, job_id, transition, type, name, agent_id -FROM - provisioner_job_resource -WHERE - id = $1 -` - -func (q *sqlQuerier) GetProvisionerJobResourceByID(ctx context.Context, id uuid.UUID) (ProvisionerJobResource, error) { - row := q.db.QueryRowContext(ctx, getProvisionerJobResourceByID, id) - var i ProvisionerJobResource - err := row.Scan( - &i.ID, - &i.CreatedAt, - &i.JobID, - &i.Transition, - &i.Type, - &i.Name, - &i.AgentID, - ) - return i, err -} - -const getProvisionerJobResourcesByJobID = `-- name: GetProvisionerJobResourcesByJobID :many +const getProvisionerJobsByIDs = `-- name: GetProvisionerJobsByIDs :many SELECT - id, created_at, job_id, transition, type, name, agent_id + id, created_at, updated_at, started_at, cancelled_at, completed_at, error, organization_id, initiator_id, provisioner, storage_method, storage_source, type, input, worker_id FROM - provisioner_job_resource + provisioner_job WHERE - job_id = $1 + id = ANY($1 :: uuid [ ]) ` -func (q *sqlQuerier) GetProvisionerJobResourcesByJobID(ctx context.Context, jobID uuid.UUID) ([]ProvisionerJobResource, error) { - rows, err := q.db.QueryContext(ctx, getProvisionerJobResourcesByJobID, jobID) +func (q *sqlQuerier) GetProvisionerJobsByIDs(ctx context.Context, ids []uuid.UUID) ([]ProvisionerJob, error) { + rows, err := q.db.QueryContext(ctx, getProvisionerJobsByIDs, pq.Array(ids)) if err != nil { return nil, err } defer rows.Close() - var items []ProvisionerJobResource + var items []ProvisionerJob for rows.Next() { - var i ProvisionerJobResource + var i ProvisionerJob if err := rows.Scan( &i.ID, &i.CreatedAt, - &i.JobID, - &i.Transition, + &i.UpdatedAt, + &i.StartedAt, + &i.CancelledAt, + &i.CompletedAt, + &i.Error, + &i.OrganizationID, + &i.InitiatorID, + &i.Provisioner, + &i.StorageMethod, + &i.StorageSource, &i.Type, - &i.Name, - &i.AgentID, + &i.Input, + &i.WorkerID, ); err != nil { return nil, err } @@ -932,74 +897,134 @@ func (q *sqlQuerier) GetUserCount(ctx context.Context) (int64, error) { return count, err } -const getWorkspaceByID = `-- name: GetWorkspaceByID :one +const getWorkspaceAgentByAuthToken = `-- name: GetWorkspaceAgentByAuthToken :one SELECT - id, created_at, updated_at, owner_id, project_id, name + id, created_at, updated_at, resource_id, auth_token, auth_instance_id, environment_variables, startup_script, instance_metadata, resource_metadata FROM - workspace + workspace_agent WHERE - id = $1 -LIMIT - 1 + auth_token = $1 ` -func (q *sqlQuerier) GetWorkspaceByID(ctx context.Context, id uuid.UUID) (Workspace, error) { - row := q.db.QueryRowContext(ctx, getWorkspaceByID, id) - var i Workspace +func (q *sqlQuerier) GetWorkspaceAgentByAuthToken(ctx context.Context, authToken uuid.UUID) (WorkspaceAgent, error) { + row := q.db.QueryRowContext(ctx, getWorkspaceAgentByAuthToken, authToken) + var i WorkspaceAgent err := row.Scan( &i.ID, &i.CreatedAt, &i.UpdatedAt, - &i.OwnerID, - &i.ProjectID, - &i.Name, + &i.ResourceID, + &i.AuthToken, + &i.AuthInstanceID, + &i.EnvironmentVariables, + &i.StartupScript, + &i.InstanceMetadata, + &i.ResourceMetadata, ) return i, err } -const getWorkspaceByUserIDAndName = `-- name: GetWorkspaceByUserIDAndName :one +const getWorkspaceAgentByInstanceID = `-- name: GetWorkspaceAgentByInstanceID :one SELECT - id, created_at, updated_at, owner_id, project_id, name + id, created_at, updated_at, resource_id, auth_token, auth_instance_id, environment_variables, startup_script, instance_metadata, resource_metadata FROM - workspace + workspace_agent WHERE - owner_id = $1 - AND LOWER(name) = LOWER($2) + auth_instance_id = $1 :: text +ORDER BY + created_at DESC ` -type GetWorkspaceByUserIDAndNameParams struct { - OwnerID string `db:"owner_id" json:"owner_id"` - Name string `db:"name" json:"name"` +func (q *sqlQuerier) GetWorkspaceAgentByInstanceID(ctx context.Context, authInstanceID string) (WorkspaceAgent, error) { + row := q.db.QueryRowContext(ctx, getWorkspaceAgentByInstanceID, authInstanceID) + var i WorkspaceAgent + err := row.Scan( + &i.ID, + &i.CreatedAt, + &i.UpdatedAt, + &i.ResourceID, + &i.AuthToken, + &i.AuthInstanceID, + &i.EnvironmentVariables, + &i.StartupScript, + &i.InstanceMetadata, + &i.ResourceMetadata, + ) + return i, err } -func (q *sqlQuerier) GetWorkspaceByUserIDAndName(ctx context.Context, arg GetWorkspaceByUserIDAndNameParams) (Workspace, error) { - row := q.db.QueryRowContext(ctx, getWorkspaceByUserIDAndName, arg.OwnerID, arg.Name) - var i Workspace +const getWorkspaceAgentByResourceID = `-- name: GetWorkspaceAgentByResourceID :one +SELECT + id, created_at, updated_at, resource_id, auth_token, auth_instance_id, environment_variables, startup_script, instance_metadata, resource_metadata +FROM + workspace_agent +WHERE + resource_id = $1 +` + +func (q *sqlQuerier) GetWorkspaceAgentByResourceID(ctx context.Context, resourceID uuid.UUID) (WorkspaceAgent, error) { + row := q.db.QueryRowContext(ctx, getWorkspaceAgentByResourceID, resourceID) + var i WorkspaceAgent err := row.Scan( &i.ID, &i.CreatedAt, &i.UpdatedAt, - &i.OwnerID, - &i.ProjectID, - &i.Name, + &i.ResourceID, + &i.AuthToken, + &i.AuthInstanceID, + &i.EnvironmentVariables, + &i.StartupScript, + &i.InstanceMetadata, + &i.ResourceMetadata, ) return i, err } -const getWorkspaceHistoryByID = `-- name: GetWorkspaceHistoryByID :one +const getWorkspaceBuildByID = `-- name: GetWorkspaceBuildByID :one SELECT - id, created_at, updated_at, workspace_id, project_version_id, name, before_id, after_id, transition, initiator, provisioner_state, provision_job_id + id, created_at, updated_at, workspace_id, project_version_id, name, before_id, after_id, transition, initiator, provisioner_state, job_id FROM - workspace_history + workspace_build WHERE id = $1 LIMIT 1 ` -func (q *sqlQuerier) GetWorkspaceHistoryByID(ctx context.Context, id uuid.UUID) (WorkspaceHistory, error) { - row := q.db.QueryRowContext(ctx, getWorkspaceHistoryByID, id) - var i WorkspaceHistory +func (q *sqlQuerier) GetWorkspaceBuildByID(ctx context.Context, id uuid.UUID) (WorkspaceBuild, error) { + row := q.db.QueryRowContext(ctx, getWorkspaceBuildByID, id) + var i WorkspaceBuild + err := row.Scan( + &i.ID, + &i.CreatedAt, + &i.UpdatedAt, + &i.WorkspaceID, + &i.ProjectVersionID, + &i.Name, + &i.BeforeID, + &i.AfterID, + &i.Transition, + &i.Initiator, + &i.ProvisionerState, + &i.JobID, + ) + return i, err +} + +const getWorkspaceBuildByJobID = `-- name: GetWorkspaceBuildByJobID :one +SELECT + id, created_at, updated_at, workspace_id, project_version_id, name, before_id, after_id, transition, initiator, provisioner_state, job_id +FROM + workspace_build +WHERE + job_id = $1 +LIMIT + 1 +` + +func (q *sqlQuerier) GetWorkspaceBuildByJobID(ctx context.Context, jobID uuid.UUID) (WorkspaceBuild, error) { + row := q.db.QueryRowContext(ctx, getWorkspaceBuildByJobID, jobID) + var i WorkspaceBuild err := row.Scan( &i.ID, &i.CreatedAt, @@ -1012,29 +1037,29 @@ func (q *sqlQuerier) GetWorkspaceHistoryByID(ctx context.Context, id uuid.UUID) &i.Transition, &i.Initiator, &i.ProvisionerState, - &i.ProvisionJobID, + &i.JobID, ) return i, err } -const getWorkspaceHistoryByWorkspaceID = `-- name: GetWorkspaceHistoryByWorkspaceID :many +const getWorkspaceBuildByWorkspaceID = `-- name: GetWorkspaceBuildByWorkspaceID :many SELECT - id, created_at, updated_at, workspace_id, project_version_id, name, before_id, after_id, transition, initiator, provisioner_state, provision_job_id + id, created_at, updated_at, workspace_id, project_version_id, name, before_id, after_id, transition, initiator, provisioner_state, job_id FROM - workspace_history + workspace_build WHERE workspace_id = $1 ` -func (q *sqlQuerier) GetWorkspaceHistoryByWorkspaceID(ctx context.Context, workspaceID uuid.UUID) ([]WorkspaceHistory, error) { - rows, err := q.db.QueryContext(ctx, getWorkspaceHistoryByWorkspaceID, workspaceID) +func (q *sqlQuerier) GetWorkspaceBuildByWorkspaceID(ctx context.Context, workspaceID uuid.UUID) ([]WorkspaceBuild, error) { + rows, err := q.db.QueryContext(ctx, getWorkspaceBuildByWorkspaceID, workspaceID) if err != nil { return nil, err } defer rows.Close() - var items []WorkspaceHistory + var items []WorkspaceBuild for rows.Next() { - var i WorkspaceHistory + var i WorkspaceBuild if err := rows.Scan( &i.ID, &i.CreatedAt, @@ -1047,7 +1072,7 @@ func (q *sqlQuerier) GetWorkspaceHistoryByWorkspaceID(ctx context.Context, works &i.Transition, &i.Initiator, &i.ProvisionerState, - &i.ProvisionJobID, + &i.JobID, ); err != nil { return nil, err } @@ -1062,24 +1087,24 @@ func (q *sqlQuerier) GetWorkspaceHistoryByWorkspaceID(ctx context.Context, works return items, nil } -const getWorkspaceHistoryByWorkspaceIDAndName = `-- name: GetWorkspaceHistoryByWorkspaceIDAndName :one +const getWorkspaceBuildByWorkspaceIDAndName = `-- name: GetWorkspaceBuildByWorkspaceIDAndName :one SELECT - id, created_at, updated_at, workspace_id, project_version_id, name, before_id, after_id, transition, initiator, provisioner_state, provision_job_id + id, created_at, updated_at, workspace_id, project_version_id, name, before_id, after_id, transition, initiator, provisioner_state, job_id FROM - workspace_history + workspace_build WHERE workspace_id = $1 AND name = $2 ` -type GetWorkspaceHistoryByWorkspaceIDAndNameParams struct { +type GetWorkspaceBuildByWorkspaceIDAndNameParams struct { WorkspaceID uuid.UUID `db:"workspace_id" json:"workspace_id"` Name string `db:"name" json:"name"` } -func (q *sqlQuerier) GetWorkspaceHistoryByWorkspaceIDAndName(ctx context.Context, arg GetWorkspaceHistoryByWorkspaceIDAndNameParams) (WorkspaceHistory, error) { - row := q.db.QueryRowContext(ctx, getWorkspaceHistoryByWorkspaceIDAndName, arg.WorkspaceID, arg.Name) - var i WorkspaceHistory +func (q *sqlQuerier) GetWorkspaceBuildByWorkspaceIDAndName(ctx context.Context, arg GetWorkspaceBuildByWorkspaceIDAndNameParams) (WorkspaceBuild, error) { + row := q.db.QueryRowContext(ctx, getWorkspaceBuildByWorkspaceIDAndName, arg.WorkspaceID, arg.Name) + var i WorkspaceBuild err := row.Scan( &i.ID, &i.CreatedAt, @@ -1092,16 +1117,16 @@ func (q *sqlQuerier) GetWorkspaceHistoryByWorkspaceIDAndName(ctx context.Context &i.Transition, &i.Initiator, &i.ProvisionerState, - &i.ProvisionJobID, + &i.JobID, ) return i, err } -const getWorkspaceHistoryByWorkspaceIDWithoutAfter = `-- name: GetWorkspaceHistoryByWorkspaceIDWithoutAfter :one +const getWorkspaceBuildByWorkspaceIDWithoutAfter = `-- name: GetWorkspaceBuildByWorkspaceIDWithoutAfter :one SELECT - id, created_at, updated_at, workspace_id, project_version_id, name, before_id, after_id, transition, initiator, provisioner_state, provision_job_id + id, created_at, updated_at, workspace_id, project_version_id, name, before_id, after_id, transition, initiator, provisioner_state, job_id FROM - workspace_history + workspace_build WHERE workspace_id = $1 AND after_id IS NULL @@ -1109,9 +1134,9 @@ LIMIT 1 ` -func (q *sqlQuerier) GetWorkspaceHistoryByWorkspaceIDWithoutAfter(ctx context.Context, workspaceID uuid.UUID) (WorkspaceHistory, error) { - row := q.db.QueryRowContext(ctx, getWorkspaceHistoryByWorkspaceIDWithoutAfter, workspaceID) - var i WorkspaceHistory +func (q *sqlQuerier) GetWorkspaceBuildByWorkspaceIDWithoutAfter(ctx context.Context, workspaceID uuid.UUID) (WorkspaceBuild, error) { + row := q.db.QueryRowContext(ctx, getWorkspaceBuildByWorkspaceIDWithoutAfter, workspaceID) + var i WorkspaceBuild err := row.Scan( &i.ID, &i.CreatedAt, @@ -1124,7 +1149,61 @@ func (q *sqlQuerier) GetWorkspaceHistoryByWorkspaceIDWithoutAfter(ctx context.Co &i.Transition, &i.Initiator, &i.ProvisionerState, - &i.ProvisionJobID, + &i.JobID, + ) + return i, err +} + +const getWorkspaceByID = `-- name: GetWorkspaceByID :one +SELECT + id, created_at, updated_at, owner_id, project_id, name +FROM + workspace +WHERE + id = $1 +LIMIT + 1 +` + +func (q *sqlQuerier) GetWorkspaceByID(ctx context.Context, id uuid.UUID) (Workspace, error) { + row := q.db.QueryRowContext(ctx, getWorkspaceByID, id) + var i Workspace + err := row.Scan( + &i.ID, + &i.CreatedAt, + &i.UpdatedAt, + &i.OwnerID, + &i.ProjectID, + &i.Name, + ) + return i, err +} + +const getWorkspaceByUserIDAndName = `-- name: GetWorkspaceByUserIDAndName :one +SELECT + id, created_at, updated_at, owner_id, project_id, name +FROM + workspace +WHERE + owner_id = $1 + AND LOWER(name) = LOWER($2) +` + +type GetWorkspaceByUserIDAndNameParams struct { + OwnerID string `db:"owner_id" json:"owner_id"` + Name string `db:"name" json:"name"` +} + +func (q *sqlQuerier) GetWorkspaceByUserIDAndName(ctx context.Context, arg GetWorkspaceByUserIDAndNameParams) (Workspace, error) { + row := q.db.QueryRowContext(ctx, getWorkspaceByUserIDAndName, arg.OwnerID, arg.Name) + var i Workspace + err := row.Scan( + &i.ID, + &i.CreatedAt, + &i.UpdatedAt, + &i.OwnerID, + &i.ProjectID, + &i.Name, ) return i, err } @@ -1170,37 +1249,56 @@ func (q *sqlQuerier) GetWorkspaceOwnerCountsByProjectIDs(ctx context.Context, id return items, nil } -const getWorkspacesByProjectAndUserID = `-- name: GetWorkspacesByProjectAndUserID :many +const getWorkspaceResourceByID = `-- name: GetWorkspaceResourceByID :one +SELECT + id, created_at, job_id, transition, type, name, agent_id +FROM + workspace_resource +WHERE + id = $1 +` + +func (q *sqlQuerier) GetWorkspaceResourceByID(ctx context.Context, id uuid.UUID) (WorkspaceResource, error) { + row := q.db.QueryRowContext(ctx, getWorkspaceResourceByID, id) + var i WorkspaceResource + err := row.Scan( + &i.ID, + &i.CreatedAt, + &i.JobID, + &i.Transition, + &i.Type, + &i.Name, + &i.AgentID, + ) + return i, err +} + +const getWorkspaceResourcesByJobID = `-- name: GetWorkspaceResourcesByJobID :many SELECT - id, created_at, updated_at, owner_id, project_id, name + id, created_at, job_id, transition, type, name, agent_id FROM - workspace + workspace_resource WHERE - owner_id = $1 - AND project_id = $2 + job_id = $1 ` -type GetWorkspacesByProjectAndUserIDParams struct { - OwnerID string `db:"owner_id" json:"owner_id"` - ProjectID uuid.UUID `db:"project_id" json:"project_id"` -} - -func (q *sqlQuerier) GetWorkspacesByProjectAndUserID(ctx context.Context, arg GetWorkspacesByProjectAndUserIDParams) ([]Workspace, error) { - rows, err := q.db.QueryContext(ctx, getWorkspacesByProjectAndUserID, arg.OwnerID, arg.ProjectID) +func (q *sqlQuerier) GetWorkspaceResourcesByJobID(ctx context.Context, jobID uuid.UUID) ([]WorkspaceResource, error) { + rows, err := q.db.QueryContext(ctx, getWorkspaceResourcesByJobID, jobID) if err != nil { return nil, err } defer rows.Close() - var items []Workspace + var items []WorkspaceResource for rows.Next() { - var i Workspace + var i WorkspaceResource if err := rows.Scan( &i.ID, &i.CreatedAt, - &i.UpdatedAt, - &i.OwnerID, - &i.ProjectID, + &i.JobID, + &i.Transition, + &i.Type, &i.Name, + &i.AgentID, ); err != nil { return nil, err } @@ -1578,7 +1676,7 @@ INSERT INTO destination_scheme ) VALUES - ($1, $2, $3, $4, $5, $6, $7, $8, $9) RETURNING id, name, created_at, updated_at, scope, scope_id, source_scheme, source_value, destination_scheme + ($1, $2, $3, $4, $5, $6, $7, $8, $9) RETURNING id, created_at, updated_at, scope, scope_id, name, source_scheme, source_value, destination_scheme ` type InsertParameterValueParams struct { @@ -1608,11 +1706,11 @@ func (q *sqlQuerier) InsertParameterValue(ctx context.Context, arg InsertParamet var i ParameterValue err := row.Scan( &i.ID, - &i.Name, &i.CreatedAt, &i.UpdatedAt, &i.Scope, &i.ScopeID, + &i.Name, &i.SourceScheme, &i.SourceValue, &i.DestinationScheme, @@ -1673,67 +1771,73 @@ INSERT INTO project_version ( id, project_id, + organization_id, created_at, updated_at, name, description, - import_job_id + job_id ) VALUES - ($1, $2, $3, $4, $5, $6, $7) RETURNING id, project_id, created_at, updated_at, name, description, import_job_id + ($1, $2, $3, $4, $5, $6, $7, $8) RETURNING id, project_id, organization_id, created_at, updated_at, name, description, job_id ` type InsertProjectVersionParams struct { - ID uuid.UUID `db:"id" json:"id"` - ProjectID uuid.UUID `db:"project_id" json:"project_id"` - CreatedAt time.Time `db:"created_at" json:"created_at"` - UpdatedAt time.Time `db:"updated_at" json:"updated_at"` - Name string `db:"name" json:"name"` - Description string `db:"description" json:"description"` - ImportJobID uuid.UUID `db:"import_job_id" json:"import_job_id"` + ID uuid.UUID `db:"id" json:"id"` + ProjectID uuid.NullUUID `db:"project_id" json:"project_id"` + OrganizationID string `db:"organization_id" json:"organization_id"` + CreatedAt time.Time `db:"created_at" json:"created_at"` + UpdatedAt time.Time `db:"updated_at" json:"updated_at"` + Name string `db:"name" json:"name"` + Description string `db:"description" json:"description"` + JobID uuid.UUID `db:"job_id" json:"job_id"` } func (q *sqlQuerier) InsertProjectVersion(ctx context.Context, arg InsertProjectVersionParams) (ProjectVersion, error) { row := q.db.QueryRowContext(ctx, insertProjectVersion, arg.ID, arg.ProjectID, + arg.OrganizationID, arg.CreatedAt, arg.UpdatedAt, arg.Name, arg.Description, - arg.ImportJobID, + arg.JobID, ) var i ProjectVersion err := row.Scan( &i.ID, &i.ProjectID, + &i.OrganizationID, &i.CreatedAt, &i.UpdatedAt, &i.Name, &i.Description, - &i.ImportJobID, + &i.JobID, ) return i, err } const insertProvisionerDaemon = `-- name: InsertProvisionerDaemon :one INSERT INTO - provisioner_daemon (id, created_at, name, provisioners) + provisioner_daemon (id, created_at, organization_id, name, provisioners) VALUES - ($1, $2, $3, $4) RETURNING id, created_at, updated_at, name, provisioners + ($1, $2, $3, $4, $5) RETURNING id, created_at, updated_at, organization_id, name, provisioners ` type InsertProvisionerDaemonParams struct { - ID uuid.UUID `db:"id" json:"id"` - CreatedAt time.Time `db:"created_at" json:"created_at"` - Name string `db:"name" json:"name"` - Provisioners []ProvisionerType `db:"provisioners" json:"provisioners"` + ID uuid.UUID `db:"id" json:"id"` + CreatedAt time.Time `db:"created_at" json:"created_at"` + OrganizationID sql.NullString `db:"organization_id" json:"organization_id"` + Name string `db:"name" json:"name"` + Provisioners []ProvisionerType `db:"provisioners" json:"provisioners"` } func (q *sqlQuerier) InsertProvisionerDaemon(ctx context.Context, arg InsertProvisionerDaemonParams) (ProvisionerDaemon, error) { row := q.db.QueryRowContext(ctx, insertProvisionerDaemon, arg.ID, arg.CreatedAt, + arg.OrganizationID, arg.Name, pq.Array(arg.Provisioners), ) @@ -1742,6 +1846,7 @@ func (q *sqlQuerier) InsertProvisionerDaemon(ctx context.Context, arg InsertProv &i.ID, &i.CreatedAt, &i.UpdatedAt, + &i.OrganizationID, &i.Name, pq.Array(&i.Provisioners), ) @@ -1813,66 +1918,6 @@ func (q *sqlQuerier) InsertProvisionerJob(ctx context.Context, arg InsertProvisi return i, err } -const insertProvisionerJobAgent = `-- name: InsertProvisionerJobAgent :one -INSERT INTO - provisioner_job_agent ( - id, - created_at, - updated_at, - resource_id, - auth_token, - auth_instance_id, - environment_variables, - startup_script, - instance_metadata, - resource_metadata - ) -VALUES - ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10) RETURNING id, created_at, updated_at, resource_id, auth_token, auth_instance_id, environment_variables, startup_script, instance_metadata, resource_metadata -` - -type InsertProvisionerJobAgentParams struct { - ID uuid.UUID `db:"id" json:"id"` - CreatedAt time.Time `db:"created_at" json:"created_at"` - UpdatedAt sql.NullTime `db:"updated_at" json:"updated_at"` - ResourceID uuid.UUID `db:"resource_id" json:"resource_id"` - AuthToken uuid.UUID `db:"auth_token" json:"auth_token"` - AuthInstanceID sql.NullString `db:"auth_instance_id" json:"auth_instance_id"` - EnvironmentVariables pqtype.NullRawMessage `db:"environment_variables" json:"environment_variables"` - StartupScript sql.NullString `db:"startup_script" json:"startup_script"` - InstanceMetadata pqtype.NullRawMessage `db:"instance_metadata" json:"instance_metadata"` - ResourceMetadata pqtype.NullRawMessage `db:"resource_metadata" json:"resource_metadata"` -} - -func (q *sqlQuerier) InsertProvisionerJobAgent(ctx context.Context, arg InsertProvisionerJobAgentParams) (ProvisionerJobAgent, error) { - row := q.db.QueryRowContext(ctx, insertProvisionerJobAgent, - arg.ID, - arg.CreatedAt, - arg.UpdatedAt, - arg.ResourceID, - arg.AuthToken, - arg.AuthInstanceID, - arg.EnvironmentVariables, - arg.StartupScript, - arg.InstanceMetadata, - arg.ResourceMetadata, - ) - var i ProvisionerJobAgent - err := row.Scan( - &i.ID, - &i.CreatedAt, - &i.UpdatedAt, - &i.ResourceID, - &i.AuthToken, - &i.AuthInstanceID, - &i.EnvironmentVariables, - &i.StartupScript, - &i.InstanceMetadata, - &i.ResourceMetadata, - ) - return i, err -} - const insertProvisionerJobLogs = `-- name: InsertProvisionerJobLogs :many INSERT INTO provisioner_job_log @@ -1931,54 +1976,6 @@ func (q *sqlQuerier) InsertProvisionerJobLogs(ctx context.Context, arg InsertPro return items, nil } -const insertProvisionerJobResource = `-- name: InsertProvisionerJobResource :one -INSERT INTO - provisioner_job_resource ( - id, - created_at, - job_id, - transition, - type, - name, - agent_id - ) -VALUES - ($1, $2, $3, $4, $5, $6, $7) RETURNING id, created_at, job_id, transition, type, name, agent_id -` - -type InsertProvisionerJobResourceParams struct { - ID uuid.UUID `db:"id" json:"id"` - CreatedAt time.Time `db:"created_at" json:"created_at"` - JobID uuid.UUID `db:"job_id" json:"job_id"` - Transition WorkspaceTransition `db:"transition" json:"transition"` - Type string `db:"type" json:"type"` - Name string `db:"name" json:"name"` - AgentID uuid.NullUUID `db:"agent_id" json:"agent_id"` -} - -func (q *sqlQuerier) InsertProvisionerJobResource(ctx context.Context, arg InsertProvisionerJobResourceParams) (ProvisionerJobResource, error) { - row := q.db.QueryRowContext(ctx, insertProvisionerJobResource, - arg.ID, - arg.CreatedAt, - arg.JobID, - arg.Transition, - arg.Type, - arg.Name, - arg.AgentID, - ) - var i ProvisionerJobResource - err := row.Scan( - &i.ID, - &i.CreatedAt, - &i.JobID, - &i.Transition, - &i.Type, - &i.Name, - &i.AgentID, - ) - return i, err -} - const insertUser = `-- name: InsertUser :one INSERT INTO users ( @@ -2087,9 +2084,69 @@ func (q *sqlQuerier) InsertWorkspace(ctx context.Context, arg InsertWorkspacePar return i, err } -const insertWorkspaceHistory = `-- name: InsertWorkspaceHistory :one +const insertWorkspaceAgent = `-- name: InsertWorkspaceAgent :one +INSERT INTO + workspace_agent ( + id, + created_at, + updated_at, + resource_id, + auth_token, + auth_instance_id, + environment_variables, + startup_script, + instance_metadata, + resource_metadata + ) +VALUES + ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10) RETURNING id, created_at, updated_at, resource_id, auth_token, auth_instance_id, environment_variables, startup_script, instance_metadata, resource_metadata +` + +type InsertWorkspaceAgentParams struct { + ID uuid.UUID `db:"id" json:"id"` + CreatedAt time.Time `db:"created_at" json:"created_at"` + UpdatedAt sql.NullTime `db:"updated_at" json:"updated_at"` + ResourceID uuid.UUID `db:"resource_id" json:"resource_id"` + AuthToken uuid.UUID `db:"auth_token" json:"auth_token"` + AuthInstanceID sql.NullString `db:"auth_instance_id" json:"auth_instance_id"` + EnvironmentVariables pqtype.NullRawMessage `db:"environment_variables" json:"environment_variables"` + StartupScript sql.NullString `db:"startup_script" json:"startup_script"` + InstanceMetadata pqtype.NullRawMessage `db:"instance_metadata" json:"instance_metadata"` + ResourceMetadata pqtype.NullRawMessage `db:"resource_metadata" json:"resource_metadata"` +} + +func (q *sqlQuerier) InsertWorkspaceAgent(ctx context.Context, arg InsertWorkspaceAgentParams) (WorkspaceAgent, error) { + row := q.db.QueryRowContext(ctx, insertWorkspaceAgent, + arg.ID, + arg.CreatedAt, + arg.UpdatedAt, + arg.ResourceID, + arg.AuthToken, + arg.AuthInstanceID, + arg.EnvironmentVariables, + arg.StartupScript, + arg.InstanceMetadata, + arg.ResourceMetadata, + ) + var i WorkspaceAgent + err := row.Scan( + &i.ID, + &i.CreatedAt, + &i.UpdatedAt, + &i.ResourceID, + &i.AuthToken, + &i.AuthInstanceID, + &i.EnvironmentVariables, + &i.StartupScript, + &i.InstanceMetadata, + &i.ResourceMetadata, + ) + return i, err +} + +const insertWorkspaceBuild = `-- name: InsertWorkspaceBuild :one INSERT INTO - workspace_history ( + workspace_build ( id, created_at, updated_at, @@ -2099,14 +2156,14 @@ INSERT INTO name, transition, initiator, - provision_job_id, + job_id, provisioner_state ) VALUES - ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11) RETURNING id, created_at, updated_at, workspace_id, project_version_id, name, before_id, after_id, transition, initiator, provisioner_state, provision_job_id + ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11) RETURNING id, created_at, updated_at, workspace_id, project_version_id, name, before_id, after_id, transition, initiator, provisioner_state, job_id ` -type InsertWorkspaceHistoryParams struct { +type InsertWorkspaceBuildParams struct { ID uuid.UUID `db:"id" json:"id"` CreatedAt time.Time `db:"created_at" json:"created_at"` UpdatedAt time.Time `db:"updated_at" json:"updated_at"` @@ -2116,12 +2173,12 @@ type InsertWorkspaceHistoryParams struct { Name string `db:"name" json:"name"` Transition WorkspaceTransition `db:"transition" json:"transition"` Initiator string `db:"initiator" json:"initiator"` - ProvisionJobID uuid.UUID `db:"provision_job_id" json:"provision_job_id"` + JobID uuid.UUID `db:"job_id" json:"job_id"` ProvisionerState []byte `db:"provisioner_state" json:"provisioner_state"` } -func (q *sqlQuerier) InsertWorkspaceHistory(ctx context.Context, arg InsertWorkspaceHistoryParams) (WorkspaceHistory, error) { - row := q.db.QueryRowContext(ctx, insertWorkspaceHistory, +func (q *sqlQuerier) InsertWorkspaceBuild(ctx context.Context, arg InsertWorkspaceBuildParams) (WorkspaceBuild, error) { + row := q.db.QueryRowContext(ctx, insertWorkspaceBuild, arg.ID, arg.CreatedAt, arg.UpdatedAt, @@ -2131,10 +2188,10 @@ func (q *sqlQuerier) InsertWorkspaceHistory(ctx context.Context, arg InsertWorks arg.Name, arg.Transition, arg.Initiator, - arg.ProvisionJobID, + arg.JobID, arg.ProvisionerState, ) - var i WorkspaceHistory + var i WorkspaceBuild err := row.Scan( &i.ID, &i.CreatedAt, @@ -2147,7 +2204,55 @@ func (q *sqlQuerier) InsertWorkspaceHistory(ctx context.Context, arg InsertWorks &i.Transition, &i.Initiator, &i.ProvisionerState, - &i.ProvisionJobID, + &i.JobID, + ) + return i, err +} + +const insertWorkspaceResource = `-- name: InsertWorkspaceResource :one +INSERT INTO + workspace_resource ( + id, + created_at, + job_id, + transition, + type, + name, + agent_id + ) +VALUES + ($1, $2, $3, $4, $5, $6, $7) RETURNING id, created_at, job_id, transition, type, name, agent_id +` + +type InsertWorkspaceResourceParams struct { + ID uuid.UUID `db:"id" json:"id"` + CreatedAt time.Time `db:"created_at" json:"created_at"` + JobID uuid.UUID `db:"job_id" json:"job_id"` + Transition WorkspaceTransition `db:"transition" json:"transition"` + Type string `db:"type" json:"type"` + Name string `db:"name" json:"name"` + AgentID uuid.NullUUID `db:"agent_id" json:"agent_id"` +} + +func (q *sqlQuerier) InsertWorkspaceResource(ctx context.Context, arg InsertWorkspaceResourceParams) (WorkspaceResource, error) { + row := q.db.QueryRowContext(ctx, insertWorkspaceResource, + arg.ID, + arg.CreatedAt, + arg.JobID, + arg.Transition, + arg.Type, + arg.Name, + arg.AgentID, + ) + var i WorkspaceResource + err := row.Scan( + &i.ID, + &i.CreatedAt, + &i.JobID, + &i.Transition, + &i.Type, + &i.Name, + &i.AgentID, ) return i, err } @@ -2186,6 +2291,27 @@ func (q *sqlQuerier) UpdateAPIKeyByID(ctx context.Context, arg UpdateAPIKeyByIDP return err } +const updateProjectVersionByID = `-- name: UpdateProjectVersionByID :exec +UPDATE + project_version +SET + project_id = $2, + updated_at = $3 +WHERE + id = $1 +` + +type UpdateProjectVersionByIDParams struct { + ID uuid.UUID `db:"id" json:"id"` + ProjectID uuid.NullUUID `db:"project_id" json:"project_id"` + UpdatedAt time.Time `db:"updated_at" json:"updated_at"` +} + +func (q *sqlQuerier) UpdateProjectVersionByID(ctx context.Context, arg UpdateProjectVersionByIDParams) error { + _, err := q.db.ExecContext(ctx, updateProjectVersionByID, arg.ID, arg.ProjectID, arg.UpdatedAt) + return err +} + const updateProvisionerDaemonByID = `-- name: UpdateProvisionerDaemonByID :exec UPDATE provisioner_daemon @@ -2257,9 +2383,28 @@ func (q *sqlQuerier) UpdateProvisionerJobWithCompleteByID(ctx context.Context, a return err } -const updateWorkspaceHistoryByID = `-- name: UpdateWorkspaceHistoryByID :exec +const updateWorkspaceAgentByID = `-- name: UpdateWorkspaceAgentByID :exec +UPDATE + workspace_agent +SET + updated_at = $2 +WHERE + id = $1 +` + +type UpdateWorkspaceAgentByIDParams struct { + ID uuid.UUID `db:"id" json:"id"` + UpdatedAt sql.NullTime `db:"updated_at" json:"updated_at"` +} + +func (q *sqlQuerier) UpdateWorkspaceAgentByID(ctx context.Context, arg UpdateWorkspaceAgentByIDParams) error { + _, err := q.db.ExecContext(ctx, updateWorkspaceAgentByID, arg.ID, arg.UpdatedAt) + return err +} + +const updateWorkspaceBuildByID = `-- name: UpdateWorkspaceBuildByID :exec UPDATE - workspace_history + workspace_build SET updated_at = $2, after_id = $3, @@ -2268,15 +2413,15 @@ WHERE id = $1 ` -type UpdateWorkspaceHistoryByIDParams struct { +type UpdateWorkspaceBuildByIDParams struct { ID uuid.UUID `db:"id" json:"id"` UpdatedAt time.Time `db:"updated_at" json:"updated_at"` AfterID uuid.NullUUID `db:"after_id" json:"after_id"` ProvisionerState []byte `db:"provisioner_state" json:"provisioner_state"` } -func (q *sqlQuerier) UpdateWorkspaceHistoryByID(ctx context.Context, arg UpdateWorkspaceHistoryByIDParams) error { - _, err := q.db.ExecContext(ctx, updateWorkspaceHistoryByID, +func (q *sqlQuerier) UpdateWorkspaceBuildByID(ctx context.Context, arg UpdateWorkspaceBuildByIDParams) error { + _, err := q.db.ExecContext(ctx, updateWorkspaceBuildByID, arg.ID, arg.UpdatedAt, arg.AfterID, diff --git a/develop.sh b/develop.sh index f7df8100b0e81..ecc3c0d80509b 100755 --- a/develop.sh +++ b/develop.sh @@ -18,7 +18,7 @@ function create_initial_user() { curl -X POST \ -d "{\"email\": \"$EMAIL\", \"username\": \"$USERNAME\", \"organization\": \"$ORGANIZATION\", \"password\": \"$PASSWORD\"}" \ -H 'Content-Type:application/json' \ - http://localhost:3000/api/v2/user + http://localhost:3000/api/v2/users/first } # Run yarn install, to make sure node_modules are ready to go diff --git a/go.mod b/go.mod index d41f89e52ea94..d41fe46a35028 100644 --- a/go.mod +++ b/go.mod @@ -14,11 +14,8 @@ replace github.com/hashicorp/terraform-config-inspect => github.com/kylecarbs/te // Required until https://github.com/chzyer/readline/pull/198 is merged. replace github.com/chzyer/readline => github.com/kylecarbs/readline v0.0.0-20220211054233-0d62993714c8 -// Required until https://github.com/census-instrumentation/opencensus-go/pull/1272 is merged. -replace go.opencensus.io => github.com/kylecarbs/opencensus-go v0.23.1-0.20220220184033-4441763886a2 - -// Required until https://github.com/pion/ice/pull/425 is merged. -replace github.com/pion/ice/v2 => github.com/kylecarbs/ice/v2 v2.1.8-0.20220221162453-b262a62902c3 +// opencensus-go leaks a goroutine by default. +replace go.opencensus.io => github.com/kylecarbs/opencensus-go v0.23.1-0.20220307014935-4d0325a68f8b require ( cdr.dev/slog v1.4.1 @@ -53,6 +50,7 @@ require ( github.com/pion/transport v0.13.0 github.com/pion/webrtc/v3 v3.1.24 github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 + github.com/powersj/whatsthis v1.3.0 github.com/quasilyte/go-ruleguard/dsl v0.3.17 github.com/spf13/cobra v1.3.0 github.com/stretchr/testify v1.7.0 diff --git a/go.sum b/go.sum index 68dd21a43d74e..586f91508d8e2 100644 --- a/go.sum +++ b/go.sum @@ -47,6 +47,7 @@ cloud.google.com/go/compute v1.5.0 h1:b1zWmYuuHz7gO9kDcM/EpHGr06UgsYNRpNJzI2kFiL cloud.google.com/go/compute v1.5.0/go.mod h1:9SMHyhJlzhlkJqrPAc839t2BZFTSk6Jdj6mkzQJeu0M= cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= +cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk= cloud.google.com/go/firestore v1.6.1/go.mod h1:asNXNOzBdyVQmEU+ggO8UPodTkEVFW5Qx+rwHnAz+EY= cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= @@ -196,6 +197,7 @@ github.com/bitly/go-hostpool v0.0.0-20171023180738-a3a6125de932/go.mod h1:NOuUCS github.com/bitly/go-simplejson v0.5.0/go.mod h1:cXHtHw4XUPsvGaxgjIAn8PhEWG9NfngEKAMDJEczWVA= github.com/bits-and-blooms/bitset v1.2.0/go.mod h1:gIdJ4wp64HaoK2YrL1Q5/N7Y16edYb8uY+O0FJTyyDA= github.com/bkaradzic/go-lz4 v1.0.0/go.mod h1:0YdlkowM3VswSROI7qDxhRvJ3sLhlFrRRwjwegp5jy4= +github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84= github.com/blang/semver v3.1.0+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4= @@ -340,6 +342,7 @@ github.com/containers/ocicrypt v1.1.0/go.mod h1:b8AOe0YR67uU8OqfVNcznfFpAzu3rdgU github.com/containers/ocicrypt v1.1.1/go.mod h1:Dm55fwWm1YZAjYRaJ94z2mfZikIyIN4B0oB3dj3jFxY= github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= +github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= github.com/coreos/go-iptables v0.4.5/go.mod h1:/mVI274lEDI2ns62jHCDnCyBF9Iwsmekav8Dbxlm1MU= github.com/coreos/go-iptables v0.5.0/go.mod h1:/mVI274lEDI2ns62jHCDnCyBF9Iwsmekav8Dbxlm1MU= github.com/coreos/go-oidc v2.1.0+incompatible/go.mod h1:CgnwVTmzoESiwO9qyAFEMiHoZ1nMCKZlZ9V6mm3/LKc= @@ -674,7 +677,9 @@ github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t github.com/grpc-ecosystem/grpc-gateway v1.9.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed/go.mod h1:tMWxXQ9wFIaZeTI9F+hmhFiGpFmhOHzyShyFUhRm0H4= +github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q= github.com/hashicorp/consul/api v1.11.0/go.mod h1:XjsvQN+RJGWI2TWy1/kqaE16HrR2J/FWgkYjdZQsX9M= +github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= github.com/hashicorp/consul/sdk v0.8.0/go.mod h1:GBvyrGALthsZObzUGsfgHZQDXjg4lOjagTIwIR1vPms= github.com/hashicorp/errwrap v0.0.0-20141028054710-7554cd9344ce/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= @@ -706,6 +711,7 @@ github.com/hashicorp/go-plugin v1.3.0/go.mod h1:F9eH4LrE/ZsRdbwhfjs9k9HoDUwAHnYt github.com/hashicorp/go-plugin v1.4.1 h1:6UltRQlLN9iZO513VveELp5xyaFxVD2+1OVylE+2E+w= github.com/hashicorp/go-plugin v1.4.1/go.mod h1:5fGEH17QVwTTcR0zV7yhDPLLmFX9YSZ38b18Udy6vYQ= github.com/hashicorp/go-retryablehttp v0.5.3/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs= +github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU= github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8= github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= @@ -717,6 +723,7 @@ github.com/hashicorp/go-version v1.2.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09 github.com/hashicorp/go-version v1.3.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= github.com/hashicorp/go-version v1.4.0 h1:aAQzgqIrRKRa7w75CKpbBxYsmUoPjzVm1W59ca1L0J4= github.com/hashicorp/go-version v1.4.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= +github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= @@ -731,10 +738,13 @@ github.com/hashicorp/hcl/v2 v2.11.1 h1:yTyWcXcm9XB0TEkyU/JCRU6rYy4K+mgLtzn2wlrJb github.com/hashicorp/hcl/v2 v2.11.1/go.mod h1:FwWsfWEjyV/CMj8s/gqAuiviY72rJ1/oayI9WftqcKg= github.com/hashicorp/logutils v1.0.0 h1:dLEQVugN8vlakKOUE3ihGLTZJRB4j+M2cdTm/ORI65Y= github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= +github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ= github.com/hashicorp/mdns v1.0.1/go.mod h1:4gW7WsVCke5TE7EPeYliwHlRUyBtfCwuFwuMg2DmyNY= github.com/hashicorp/mdns v1.0.4/go.mod h1:mtBihi+LeNXGtG8L9dX59gAEa12BDtBQSp4v/YAJqrc= +github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I= github.com/hashicorp/memberlist v0.2.2/go.mod h1:MS2lj3INKhZjWNqd3N0m3J+Jxf3DAOnAH9VT3Sh9MUE= github.com/hashicorp/memberlist v0.3.0/go.mod h1:MS2lj3INKhZjWNqd3N0m3J+Jxf3DAOnAH9VT3Sh9MUE= +github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc= github.com/hashicorp/serf v0.9.5/go.mod h1:UWDWwZeL5cuWDJdl0C6wrvrUwEqtQ4ZKBKKENpqIUyk= github.com/hashicorp/serf v0.9.6/go.mod h1:TXZNMjZQijwlDvp+r0b63xZ45H7JmCmgg4gpTwn9UV4= github.com/hashicorp/terraform-json v0.13.0 h1:Li9L+lKD1FO5RVFRM1mMMIBDoUHslOniyEi5CM+FWGY= @@ -883,10 +893,8 @@ github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/ktrysmt/go-bitbucket v0.6.4/go.mod h1:9u0v3hsd2rqCHRIpbir1oP7F58uo5dq19sBYvuMoyQ4= -github.com/kylecarbs/ice/v2 v2.1.8-0.20220221162453-b262a62902c3 h1:/SkVJxNTLozVOnU5OAQhnwt5Nb7h15c1BHad6t4h5MM= -github.com/kylecarbs/ice/v2 v2.1.8-0.20220221162453-b262a62902c3/go.mod h1:Op8jlPtjeiycsXh93Cs4jK82C9j/kh7vef6ztIOvtIQ= -github.com/kylecarbs/opencensus-go v0.23.1-0.20220220184033-4441763886a2 h1:tvKg/uBu9IqevKJECOWdHWnFxuyQZo7dBeilpx+FugY= -github.com/kylecarbs/opencensus-go v0.23.1-0.20220220184033-4441763886a2/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E= +github.com/kylecarbs/opencensus-go v0.23.1-0.20220307014935-4d0325a68f8b h1:1Y1X6aR78kMEQE1iCjQodB3lA7VO4jB88Wf8ZrzXSsA= +github.com/kylecarbs/opencensus-go v0.23.1-0.20220307014935-4d0325a68f8b/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E= github.com/kylecarbs/promptui v0.8.1-0.20201231190244-d8f2159af2b2 h1:MUREBTh4kybLY1KyuBfSx+QPfTB8XiUHs6ZxUhOPTnU= github.com/kylecarbs/promptui v0.8.1-0.20201231190244-d8f2159af2b2/go.mod h1:n4zTdgP0vr0S3w7/O/g98U+e0gwLScEXGwov2nIKuGQ= github.com/kylecarbs/readline v0.0.0-20220211054233-0d62993714c8 h1:Y7O3Z3YeNRtw14QrtHpevU4dSjCkov0J40MtQ7Nc0n8= @@ -915,6 +923,7 @@ github.com/lunixbochs/vtclean v1.0.0 h1:xu2sLAri4lGiovBDQKxl5mrXyESr3gUr5m5SM5+L github.com/lunixbochs/vtclean v1.0.0/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI= github.com/lyft/protoc-gen-star v0.5.3/go.mod h1:V0xaHgaf5oCCqmcxYcWiDfTiKsZsRc87/1qhoTACD8w= github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= +github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/magiconair/properties v1.8.5/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= @@ -957,9 +966,11 @@ github.com/miekg/dns v1.1.26/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKju github.com/miekg/dns v1.1.41/go.mod h1:p6aan82bvRIyn+zDIv9xYNUpwa73JcSh9BKwknJysuI= github.com/miekg/pkcs11 v1.0.3/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs= github.com/mistifyio/go-zfs v2.1.2-0.20190413222219-f784269be439+incompatible/go.mod h1:8AuVvqP/mXw1px98n46wfvcGfQ4ci2FwoAjKYxuo3Z4= +github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= github.com/mitchellh/cli v1.1.0/go.mod h1:xcISNoH86gajksDmfB23e/pu+B+GeFRMYmoHXxx3xhI= github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= +github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/go-testing-interface v0.0.0-20171004221916-a61a99592b77/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= @@ -970,6 +981,8 @@ github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7/go.mod h1:ZX github.com/mitchellh/go-wordwrap v1.0.0/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo= github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0= github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0= +github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg= +github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY= github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/mitchellh/mapstructure v0.0.0-20180220230111-00c29f56e238/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= @@ -1083,6 +1096,8 @@ github.com/pion/datachannel v1.5.2/go.mod h1:FTGQWaHrdCwIJ1rw6xBIfZVkslikjShim5y github.com/pion/dtls/v2 v2.1.2/go.mod h1:o6+WvyLDAlXF7YiPB/RlskRoeK+/JtuaZa5emwQcWus= github.com/pion/dtls/v2 v2.1.3 h1:3UF7udADqous+M2R5Uo2q/YaP4EzUoWKdfX2oscCUio= github.com/pion/dtls/v2 v2.1.3/go.mod h1:o6+WvyLDAlXF7YiPB/RlskRoeK+/JtuaZa5emwQcWus= +github.com/pion/ice/v2 v2.2.1 h1:R3MeuJZpU1ty3diPqpD5OxaxcZ15eprAc+EtUiSoFxg= +github.com/pion/ice/v2 v2.2.1/go.mod h1:Op8jlPtjeiycsXh93Cs4jK82C9j/kh7vef6ztIOvtIQ= github.com/pion/interceptor v0.1.7 h1:HThW0tIIKT9RRoDWGURe8rlZVOx0fJHxBHpA0ej0+bo= github.com/pion/interceptor v0.1.7/go.mod h1:Lh3JSl/cbJ2wP8I3ccrjh1K/deRGRn3UlSPuOTiHb6U= github.com/pion/logging v0.2.2 h1:M9+AIj/+pxNsDfAT64+MAVgJO0rsyLnoJKCqf//DoeY= @@ -1130,6 +1145,8 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= github.com/posener/complete v1.2.3/go.mod h1:WZIdtGGp+qx0sLrYKtIRAruyNpv6hFCicSgv7Sy7s/s= +github.com/powersj/whatsthis v1.3.0 h1:FhP+pZZr6rxBC2N/ydZOvzcFOx60Ujggy2ACYxa6Xac= +github.com/powersj/whatsthis v1.3.0/go.mod h1:8NwT2j1fdsmLLVBZ0uNPb1cvHwBHm6G3e0t/Kk7AsmI= github.com/pquerna/cachecontrol v0.0.0-20171018203845-0dec1b30a021/go.mod h1:prYjPmNq4d1NPVmpShWobRqXY3q7Vp+80DqgxxUrUIA= github.com/prometheus/client_golang v0.0.0-20180209125602-c332b6f63c06/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= @@ -1209,6 +1226,7 @@ github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= github.com/smartystreets/goconvey v0.0.0-20190330032615-68dc04aab96a/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= +github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= github.com/snowflakedb/gosnowflake v1.6.3/go.mod h1:6hLajn6yxuJ4xUHZegMekpq9rnQbGJ7TMwXjgTmA6lg= github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= @@ -1221,6 +1239,7 @@ github.com/spf13/cast v1.4.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkU github.com/spf13/cobra v0.0.2-0.20171109065643-2da4a54c5cee/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= github.com/spf13/cobra v1.0.0/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE= +github.com/spf13/cobra v1.1.1/go.mod h1:WnodtKOvamDL/PwE2M4iKs8aMDBZ5Q5klgD3qfVJQMI= github.com/spf13/cobra v1.3.0 h1:R7cSvGu+Vv+qX0gW5R/85dx2kmmJT5z5NM8ifdYjdn0= github.com/spf13/cobra v1.3.0/go.mod h1:BrRVncBjOJa/eUcVVm9CE+oC6as8k+VYr4NY7WCi9V4= github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= @@ -1233,6 +1252,7 @@ github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnIn github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE= +github.com/spf13/viper v1.7.0/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg= github.com/spf13/viper v1.10.0/go.mod h1:SoyBPwAtKDzypXNDFKN5kzH7ppppbGZtls1UpIy5AsM= github.com/stefanberger/go-pkcs11uri v0.0.0-20201008174630-78d3cae3a980/go.mod h1:AO3tvPzVZ/ayst6UlUKUv6rcPQInYe3IknH3jYhAKu8= github.com/stretchr/objx v0.0.0-20180129172003-8a3f7159479f/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= @@ -1438,6 +1458,7 @@ golang.org/x/net v0.0.0-20181011144130-49bb7cea24b1/go.mod h1:mL1N/T3taQHkDXs73r golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181108082009-03003ca0c849/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -1703,6 +1724,7 @@ golang.org/x/tools v0.0.0-20190927191325-030b2cf1153e/go.mod h1:b+2E5dAYhXwXZwtn golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= @@ -1952,6 +1974,7 @@ gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMy gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2/go.mod h1:Xk6kEKp8OKb+X14hQBKWaSkCsqBpgog8nAV2xsGOxlo= gopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec/go.mod h1:aPpfJ7XW+gOuirDoZ8gHhLh3kZ1B08FtV2bbmy7Jv3s= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= +gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/ini.v1 v1.66.2/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k= gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= diff --git a/httpmw/apikey_test.go b/httpmw/apikey_test.go index 4af9fb7173625..309c3917de604 100644 --- a/httpmw/apikey_test.go +++ b/httpmw/apikey_test.go @@ -27,6 +27,7 @@ func randomAPIKeyParts() (id string, secret string) { func TestAPIKey(t *testing.T) { t.Parallel() + successHandler := http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { // Only called if the API key passes through the handler. httpapi.Write(rw, http.StatusOK, httpapi.Response{ diff --git a/httpmw/httpmw.go b/httpmw/httpmw.go new file mode 100644 index 0000000000000..003b76da1e79e --- /dev/null +++ b/httpmw/httpmw.go @@ -0,0 +1,30 @@ +package httpmw + +import ( + "fmt" + "net/http" + + "github.com/go-chi/chi/v5" + "github.com/google/uuid" + + "github.com/coder/coder/httpapi" +) + +// parseUUID consumes a url parameter and parses it as a UUID. +func parseUUID(rw http.ResponseWriter, r *http.Request, param string) (uuid.UUID, bool) { + rawID := chi.URLParam(r, param) + if rawID == "" { + httpapi.Write(rw, http.StatusBadRequest, httpapi.Response{ + Message: fmt.Sprintf("%s must be provided", param), + }) + return uuid.UUID{}, false + } + parsed, err := uuid.Parse(rawID) + if err != nil { + httpapi.Write(rw, http.StatusBadRequest, httpapi.Response{ + Message: fmt.Sprintf("%s must be a uuid", param), + }) + return uuid.UUID{}, false + } + return parsed, true +} diff --git a/httpmw/organizationparam.go b/httpmw/organizationparam.go index a85903924c3bd..6937e060a144b 100644 --- a/httpmw/organizationparam.go +++ b/httpmw/organizationparam.go @@ -40,18 +40,17 @@ func OrganizationMemberParam(r *http.Request) database.OrganizationMember { func ExtractOrganizationParam(db database.Store) func(http.Handler) http.Handler { return func(next http.Handler) http.Handler { return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { - apiKey := APIKey(r) - organizationName := chi.URLParam(r, "organization") - if organizationName == "" { + organizationID := chi.URLParam(r, "organization") + if organizationID == "" { httpapi.Write(rw, http.StatusBadRequest, httpapi.Response{ - Message: "organization name must be provided", + Message: "organization must be provided", }) return } - organization, err := db.GetOrganizationByName(r.Context(), organizationName) + organization, err := db.GetOrganizationByID(r.Context(), organizationID) if errors.Is(err, sql.ErrNoRows) { httpapi.Write(rw, http.StatusNotFound, httpapi.Response{ - Message: fmt.Sprintf("organization %q does not exist", organizationName), + Message: fmt.Sprintf("organization %q does not exist", organizationID), }) return } @@ -61,6 +60,7 @@ func ExtractOrganizationParam(db database.Store) func(http.Handler) http.Handler }) return } + apiKey := APIKey(r) organizationMember, err := db.GetOrganizationMemberByUserID(r.Context(), database.GetOrganizationMemberByUserIDParams{ OrganizationID: organization.ID, UserID: apiKey.UserID, diff --git a/httpmw/organizationparam_test.go b/httpmw/organizationparam_test.go index 3fb387909c3a5..b957950f45075 100644 --- a/httpmw/organizationparam_test.go +++ b/httpmw/organizationparam_test.go @@ -113,7 +113,7 @@ func TestOrganizationParam(t *testing.T) { UpdatedAt: database.Now(), }) require.NoError(t, err) - chi.RouteContext(r.Context()).URLParams.Add("organization", organization.Name) + chi.RouteContext(r.Context()).URLParams.Add("organization", organization.ID) rtr.Use( httpmw.ExtractAPIKey(db, nil), httpmw.ExtractOrganizationParam(db), @@ -147,7 +147,7 @@ func TestOrganizationParam(t *testing.T) { UpdatedAt: database.Now(), }) require.NoError(t, err) - chi.RouteContext(r.Context()).URLParams.Add("organization", organization.Name) + chi.RouteContext(r.Context()).URLParams.Add("organization", organization.ID) rtr.Use( httpmw.ExtractAPIKey(db, nil), httpmw.ExtractOrganizationParam(db), diff --git a/httpmw/projectparam.go b/httpmw/projectparam.go index 099cc01d1514e..15d258b5df5ad 100644 --- a/httpmw/projectparam.go +++ b/httpmw/projectparam.go @@ -28,32 +28,25 @@ func ProjectParam(r *http.Request) database.Project { func ExtractProjectParam(db database.Store) func(http.Handler) http.Handler { return func(next http.Handler) http.Handler { return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { - organization := OrganizationParam(r) - projectName := chi.URLParam(r, "project") - if projectName == "" { - httpapi.Write(rw, http.StatusBadRequest, httpapi.Response{ - Message: "project name must be provided", - }) + projectID, parsed := parseUUID(rw, r, "project") + if !parsed { return } - project, err := db.GetProjectByOrganizationAndName(r.Context(), database.GetProjectByOrganizationAndNameParams{ - OrganizationID: organization.ID, - Name: projectName, - }) + project, err := db.GetProjectByID(r.Context(), projectID) if errors.Is(err, sql.ErrNoRows) { httpapi.Write(rw, http.StatusNotFound, httpapi.Response{ - Message: fmt.Sprintf("project %q does not exist", projectName), + Message: fmt.Sprintf("project %q does not exist", projectID), }) - return } if err != nil { httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ - Message: fmt.Sprintf("get project: %s", err.Error()), + Message: fmt.Sprintf("get project: %s", err), }) return } ctx := context.WithValue(r.Context(), projectParamContextKey{}, project) + chi.RouteContext(ctx).URLParams.Add("organization", project.OrganizationID) next.ServeHTTP(rw, r.WithContext(ctx)) }) } diff --git a/httpmw/projectparam_test.go b/httpmw/projectparam_test.go index 866fe3a9c3fc0..c8ea119a25125 100644 --- a/httpmw/projectparam_test.go +++ b/httpmw/projectparam_test.go @@ -74,7 +74,6 @@ func TestProjectParam(t *testing.T) { require.NoError(t, err) ctx := chi.NewRouteContext() - ctx.URLParams.Add("organization", organization.Name) r = r.WithContext(context.WithValue(r.Context(), chi.RouteCtxKey, ctx)) return r, organization } @@ -83,11 +82,7 @@ func TestProjectParam(t *testing.T) { t.Parallel() db := databasefake.New() rtr := chi.NewRouter() - rtr.Use( - httpmw.ExtractAPIKey(db, nil), - httpmw.ExtractOrganizationParam(db), - httpmw.ExtractProjectParam(db), - ) + rtr.Use(httpmw.ExtractProjectParam(db)) rtr.Get("/", nil) r, _ := setupAuthentication(db) rw := httptest.NewRecorder() @@ -102,15 +97,11 @@ func TestProjectParam(t *testing.T) { t.Parallel() db := databasefake.New() rtr := chi.NewRouter() - rtr.Use( - httpmw.ExtractAPIKey(db, nil), - httpmw.ExtractOrganizationParam(db), - httpmw.ExtractProjectParam(db), - ) + rtr.Use(httpmw.ExtractProjectParam(db)) rtr.Get("/", nil) r, _ := setupAuthentication(db) - chi.RouteContext(r.Context()).URLParams.Add("project", "nothin") + chi.RouteContext(r.Context()).URLParams.Add("project", uuid.NewString()) rw := httptest.NewRecorder() rtr.ServeHTTP(rw, r) @@ -119,14 +110,31 @@ func TestProjectParam(t *testing.T) { require.Equal(t, http.StatusNotFound, res.StatusCode) }) + t.Run("BadUUID", func(t *testing.T) { + t.Parallel() + db := databasefake.New() + rtr := chi.NewRouter() + rtr.Use(httpmw.ExtractProjectParam(db)) + rtr.Get("/", nil) + + r, _ := setupAuthentication(db) + chi.RouteContext(r.Context()).URLParams.Add("project", "not-a-uuid") + rw := httptest.NewRecorder() + rtr.ServeHTTP(rw, r) + + res := rw.Result() + defer res.Body.Close() + require.Equal(t, http.StatusBadRequest, res.StatusCode) + }) + t.Run("Project", func(t *testing.T) { t.Parallel() db := databasefake.New() rtr := chi.NewRouter() rtr.Use( httpmw.ExtractAPIKey(db, nil), - httpmw.ExtractOrganizationParam(db), httpmw.ExtractProjectParam(db), + httpmw.ExtractOrganizationParam(db), ) rtr.Get("/", func(rw http.ResponseWriter, r *http.Request) { _ = httpmw.ProjectParam(r) @@ -140,7 +148,7 @@ func TestProjectParam(t *testing.T) { Name: "moo", }) require.NoError(t, err) - chi.RouteContext(r.Context()).URLParams.Add("project", project.Name) + chi.RouteContext(r.Context()).URLParams.Add("project", project.ID.String()) rw := httptest.NewRecorder() rtr.ServeHTTP(rw, r) diff --git a/httpmw/projectversionparam.go b/httpmw/projectversionparam.go index ca8c47f76fb00..c506a05107dcb 100644 --- a/httpmw/projectversionparam.go +++ b/httpmw/projectversionparam.go @@ -8,7 +8,6 @@ import ( "net/http" "github.com/go-chi/chi/v5" - "github.com/google/uuid" "github.com/coder/coder/database" "github.com/coder/coder/httpapi" @@ -29,27 +28,14 @@ func ProjectVersionParam(r *http.Request) database.ProjectVersion { func ExtractProjectVersionParam(db database.Store) func(http.Handler) http.Handler { return func(next http.Handler) http.Handler { return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { - project := ProjectParam(r) - projectVersionName := chi.URLParam(r, "projectversion") - if projectVersionName == "" { - httpapi.Write(rw, http.StatusBadRequest, httpapi.Response{ - Message: "project version name must be provided", - }) + projectVersionID, parsed := parseUUID(rw, r, "projectversion") + if !parsed { return } - var projectVersion database.ProjectVersion - uuid, err := uuid.Parse(projectVersionName) - if err == nil { - projectVersion, err = db.GetProjectVersionByID(r.Context(), uuid) - } else { - projectVersion, err = db.GetProjectVersionByProjectIDAndName(r.Context(), database.GetProjectVersionByProjectIDAndNameParams{ - ProjectID: project.ID, - Name: projectVersionName, - }) - } + projectVersion, err := db.GetProjectVersionByID(r.Context(), projectVersionID) if errors.Is(err, sql.ErrNoRows) { httpapi.Write(rw, http.StatusNotFound, httpapi.Response{ - Message: fmt.Sprintf("project version %q does not exist", projectVersionName), + Message: fmt.Sprintf("project version %q does not exist", projectVersionID), }) return } @@ -61,6 +47,7 @@ func ExtractProjectVersionParam(db database.Store) func(http.Handler) http.Handl } ctx := context.WithValue(r.Context(), projectVersionParamContextKey{}, projectVersion) + chi.RouteContext(ctx).URLParams.Add("organization", projectVersion.OrganizationID) next.ServeHTTP(rw, r.WithContext(ctx)) }) } diff --git a/httpmw/projectversionparam_test.go b/httpmw/projectversionparam_test.go index b5054a58231b8..2e9e47e2e8798 100644 --- a/httpmw/projectversionparam_test.go +++ b/httpmw/projectversionparam_test.go @@ -90,12 +90,7 @@ func TestProjectVersionParam(t *testing.T) { t.Parallel() db := databasefake.New() rtr := chi.NewRouter() - rtr.Use( - httpmw.ExtractAPIKey(db, nil), - httpmw.ExtractOrganizationParam(db), - httpmw.ExtractProjectParam(db), - httpmw.ExtractProjectVersionParam(db), - ) + rtr.Use(httpmw.ExtractProjectVersionParam(db)) rtr.Get("/", nil) r, _ := setupAuthentication(db) rw := httptest.NewRecorder() @@ -110,16 +105,11 @@ func TestProjectVersionParam(t *testing.T) { t.Parallel() db := databasefake.New() rtr := chi.NewRouter() - rtr.Use( - httpmw.ExtractAPIKey(db, nil), - httpmw.ExtractOrganizationParam(db), - httpmw.ExtractProjectParam(db), - httpmw.ExtractProjectVersionParam(db), - ) + rtr.Use(httpmw.ExtractProjectVersionParam(db)) rtr.Get("/", nil) r, _ := setupAuthentication(db) - chi.RouteContext(r.Context()).URLParams.Add("projectversion", "nothin") + chi.RouteContext(r.Context()).URLParams.Add("projectversion", uuid.NewString()) rw := httptest.NewRecorder() rtr.ServeHTTP(rw, r) @@ -134,9 +124,8 @@ func TestProjectVersionParam(t *testing.T) { rtr := chi.NewRouter() rtr.Use( httpmw.ExtractAPIKey(db, nil), - httpmw.ExtractOrganizationParam(db), - httpmw.ExtractProjectParam(db), httpmw.ExtractProjectVersionParam(db), + httpmw.ExtractOrganizationParam(db), ) rtr.Get("/", func(rw http.ResponseWriter, r *http.Request) { _ = httpmw.ProjectVersionParam(r) @@ -145,12 +134,12 @@ func TestProjectVersionParam(t *testing.T) { r, project := setupAuthentication(db) projectVersion, err := db.InsertProjectVersion(context.Background(), database.InsertProjectVersionParams{ - ID: uuid.New(), - ProjectID: project.ID, - Name: "moo", + ID: uuid.New(), + OrganizationID: project.OrganizationID, + Name: "moo", }) require.NoError(t, err) - chi.RouteContext(r.Context()).URLParams.Add("projectversion", projectVersion.Name) + chi.RouteContext(r.Context()).URLParams.Add("projectversion", projectVersion.ID.String()) rw := httptest.NewRecorder() rtr.ServeHTTP(rw, r) diff --git a/httpmw/provisionerjobparam.go b/httpmw/provisionerjobparam.go deleted file mode 100644 index 1490dfbcde17c..0000000000000 --- a/httpmw/provisionerjobparam.go +++ /dev/null @@ -1,64 +0,0 @@ -package httpmw - -import ( - "context" - "database/sql" - "errors" - "fmt" - "net/http" - - "github.com/go-chi/chi/v5" - "github.com/google/uuid" - - "github.com/coder/coder/database" - "github.com/coder/coder/httpapi" -) - -type provisionerJobParamContextKey struct{} - -// ProvisionerJobParam returns the project from the ExtractProjectParam handler. -func ProvisionerJobParam(r *http.Request) database.ProvisionerJob { - provisionerJob, ok := r.Context().Value(provisionerJobParamContextKey{}).(database.ProvisionerJob) - if !ok { - panic("developer error: provisioner job param middleware not provided") - } - return provisionerJob -} - -// ExtractProvisionerJobParam grabs a provisioner job from the "provisionerjob" URL parameter. -func ExtractProvisionerJobParam(db database.Store) func(http.Handler) http.Handler { - return func(next http.Handler) http.Handler { - return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { - jobID := chi.URLParam(r, "provisionerjob") - if jobID == "" { - httpapi.Write(rw, http.StatusBadRequest, httpapi.Response{ - Message: "provisioner job must be provided", - }) - return - } - jobUUID, err := uuid.Parse(jobID) - if err != nil { - httpapi.Write(rw, http.StatusBadRequest, httpapi.Response{ - Message: "job id must be a uuid", - }) - return - } - job, err := db.GetProvisionerJobByID(r.Context(), jobUUID) - if errors.Is(err, sql.ErrNoRows) { - httpapi.Write(rw, http.StatusNotFound, httpapi.Response{ - Message: "job doesn't exist with that id", - }) - return - } - if err != nil { - httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ - Message: fmt.Sprintf("get provisioner job: %s", err), - }) - return - } - - ctx := context.WithValue(r.Context(), provisionerJobParamContextKey{}, job) - next.ServeHTTP(rw, r.WithContext(ctx)) - }) - } -} diff --git a/httpmw/provisionerjobparam_test.go b/httpmw/provisionerjobparam_test.go deleted file mode 100644 index 45aa268bf63eb..0000000000000 --- a/httpmw/provisionerjobparam_test.go +++ /dev/null @@ -1,109 +0,0 @@ -package httpmw_test - -import ( - "context" - "net/http" - "net/http/httptest" - "testing" - - "github.com/go-chi/chi/v5" - "github.com/google/uuid" - "github.com/stretchr/testify/require" - - "github.com/coder/coder/database" - "github.com/coder/coder/database/databasefake" - "github.com/coder/coder/httpmw" -) - -func TestProvisionerJobParam(t *testing.T) { - t.Parallel() - - setup := func(db database.Store) (*http.Request, database.ProvisionerJob) { - r := httptest.NewRequest("GET", "/", nil) - provisionerJob, err := db.InsertProvisionerJob(context.Background(), database.InsertProvisionerJobParams{ - ID: uuid.New(), - }) - require.NoError(t, err) - - ctx := chi.NewRouteContext() - r = r.WithContext(context.WithValue(r.Context(), chi.RouteCtxKey, ctx)) - return r, provisionerJob - } - - t.Run("None", func(t *testing.T) { - t.Parallel() - db := databasefake.New() - rtr := chi.NewRouter() - rtr.Use( - httpmw.ExtractProvisionerJobParam(db), - ) - rtr.Get("/", nil) - r, _ := setup(db) - rw := httptest.NewRecorder() - rtr.ServeHTTP(rw, r) - - res := rw.Result() - defer res.Body.Close() - require.Equal(t, http.StatusBadRequest, res.StatusCode) - }) - - t.Run("BadUUID", func(t *testing.T) { - t.Parallel() - db := databasefake.New() - rtr := chi.NewRouter() - rtr.Use( - httpmw.ExtractProvisionerJobParam(db), - ) - rtr.Get("/", nil) - - r, _ := setup(db) - chi.RouteContext(r.Context()).URLParams.Add("provisionerjob", "nothin") - rw := httptest.NewRecorder() - rtr.ServeHTTP(rw, r) - - res := rw.Result() - defer res.Body.Close() - require.Equal(t, http.StatusBadRequest, res.StatusCode) - }) - - t.Run("NotFound", func(t *testing.T) { - t.Parallel() - db := databasefake.New() - rtr := chi.NewRouter() - rtr.Use( - httpmw.ExtractProvisionerJobParam(db), - ) - rtr.Get("/", nil) - - r, _ := setup(db) - chi.RouteContext(r.Context()).URLParams.Add("provisionerjob", uuid.NewString()) - rw := httptest.NewRecorder() - rtr.ServeHTTP(rw, r) - - res := rw.Result() - defer res.Body.Close() - require.Equal(t, http.StatusNotFound, res.StatusCode) - }) - - t.Run("ProvisionerJob", func(t *testing.T) { - t.Parallel() - db := databasefake.New() - rtr := chi.NewRouter() - rtr.Use( - httpmw.ExtractProvisionerJobParam(db), - ) - rtr.Get("/", func(rw http.ResponseWriter, r *http.Request) { - _ = httpmw.ProvisionerJobParam(r) - rw.WriteHeader(http.StatusOK) - }) - - r, job := setup(db) - chi.RouteContext(r.Context()).URLParams.Add("provisionerjob", job.ID.String()) - rw := httptest.NewRecorder() - rtr.ServeHTTP(rw, r) - - res := rw.Result() - defer res.Body.Close() - require.Equal(t, http.StatusOK, res.StatusCode) - }) -} diff --git a/httpmw/workspaceagent.go b/httpmw/workspaceagent.go new file mode 100644 index 0000000000000..13fbf556b026e --- /dev/null +++ b/httpmw/workspaceagent.go @@ -0,0 +1,65 @@ +package httpmw + +import ( + "context" + "database/sql" + "errors" + "fmt" + "net/http" + + "github.com/google/uuid" + + "github.com/coder/coder/database" + "github.com/coder/coder/httpapi" +) + +type workspaceAgentContextKey struct{} + +// WorkspaceAgent returns the workspace agent from the ExtractAgent handler. +func WorkspaceAgent(r *http.Request) database.WorkspaceAgent { + user, ok := r.Context().Value(workspaceAgentContextKey{}).(database.WorkspaceAgent) + if !ok { + panic("developer error: agent middleware not provided") + } + return user +} + +// ExtractWorkspaceAgent requires authentication using a valid agent token. +func ExtractWorkspaceAgent(db database.Store) func(http.Handler) http.Handler { + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { + cookie, err := r.Cookie(AuthCookie) + if err != nil { + httpapi.Write(rw, http.StatusUnauthorized, httpapi.Response{ + Message: fmt.Sprintf("%q cookie must be provided", AuthCookie), + }) + return + } + token, err := uuid.Parse(cookie.Value) + if err != nil { + httpapi.Write(rw, http.StatusBadRequest, httpapi.Response{ + Message: fmt.Sprintf("parse token: %s", err), + }) + return + } + agent, err := db.GetWorkspaceAgentByAuthToken(r.Context(), token) + if errors.Is(err, sql.ErrNoRows) { + if errors.Is(err, sql.ErrNoRows) { + httpapi.Write(rw, http.StatusUnauthorized, httpapi.Response{ + Message: "agent token is invalid", + }) + return + } + } + if err != nil { + httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ + Message: fmt.Sprintf("get workspace agent: %s", err), + }) + return + } + + ctx := context.WithValue(r.Context(), workspaceAgentContextKey{}, agent) + next.ServeHTTP(rw, r.WithContext(ctx)) + }) + } +} diff --git a/httpmw/workspaceagent_test.go b/httpmw/workspaceagent_test.go new file mode 100644 index 0000000000000..fc14db017a1be --- /dev/null +++ b/httpmw/workspaceagent_test.go @@ -0,0 +1,73 @@ +package httpmw_test + +import ( + "context" + "net/http" + "net/http/httptest" + "testing" + + "github.com/go-chi/chi/v5" + "github.com/google/uuid" + "github.com/stretchr/testify/require" + + "github.com/coder/coder/database" + "github.com/coder/coder/database/databasefake" + "github.com/coder/coder/httpmw" +) + +func TestWorkspaceAgent(t *testing.T) { + t.Parallel() + + setup := func(db database.Store) (*http.Request, uuid.UUID) { + token := uuid.New() + r := httptest.NewRequest("GET", "/", nil) + r.AddCookie(&http.Cookie{ + Name: httpmw.AuthCookie, + Value: token.String(), + }) + return r, token + } + + t.Run("None", func(t *testing.T) { + t.Parallel() + db := databasefake.New() + rtr := chi.NewRouter() + rtr.Use( + httpmw.ExtractWorkspaceAgent(db), + ) + rtr.Get("/", nil) + r, _ := setup(db) + rw := httptest.NewRecorder() + rtr.ServeHTTP(rw, r) + + res := rw.Result() + defer res.Body.Close() + require.Equal(t, http.StatusUnauthorized, res.StatusCode) + }) + + t.Run("Found", func(t *testing.T) { + t.Parallel() + db := databasefake.New() + rtr := chi.NewRouter() + rtr.Use( + httpmw.ExtractWorkspaceAgent(db), + ) + rtr.Get("/", func(rw http.ResponseWriter, r *http.Request) { + _ = httpmw.WorkspaceAgent(r) + rw.WriteHeader(http.StatusOK) + }) + r, token := setup(db) + _, err := db.InsertWorkspaceAgent(context.Background(), database.InsertWorkspaceAgentParams{ + ID: uuid.New(), + AuthToken: token, + }) + require.NoError(t, err) + require.NoError(t, err) + rw := httptest.NewRecorder() + rtr.ServeHTTP(rw, r) + + res := rw.Result() + defer res.Body.Close() + require.Equal(t, http.StatusOK, res.StatusCode) + }) +} diff --git a/httpmw/workspacebuildparam.go b/httpmw/workspacebuildparam.go new file mode 100644 index 0000000000000..29e88d8f20654 --- /dev/null +++ b/httpmw/workspacebuildparam.go @@ -0,0 +1,56 @@ +package httpmw + +import ( + "context" + "database/sql" + "errors" + "fmt" + "net/http" + + "github.com/go-chi/chi/v5" + + "github.com/coder/coder/database" + "github.com/coder/coder/httpapi" +) + +type workspaceBuildParamContextKey struct{} + +// WorkspaceBuildParam returns the workspace build from the ExtractWorkspaceBuildParam handler. +func WorkspaceBuildParam(r *http.Request) database.WorkspaceBuild { + workspaceBuild, ok := r.Context().Value(workspaceBuildParamContextKey{}).(database.WorkspaceBuild) + if !ok { + panic("developer error: workspace build param middleware not provided") + } + return workspaceBuild +} + +// ExtractWorkspaceBuildParam grabs workspace build from the "workspacebuild" URL parameter. +func ExtractWorkspaceBuildParam(db database.Store) func(http.Handler) http.Handler { + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { + workspaceBuildID, parsed := parseUUID(rw, r, "workspacebuild") + if !parsed { + return + } + workspaceBuild, err := db.GetWorkspaceBuildByID(r.Context(), workspaceBuildID) + if errors.Is(err, sql.ErrNoRows) { + httpapi.Write(rw, http.StatusNotFound, httpapi.Response{ + Message: fmt.Sprintf("workspace build %q does not exist", workspaceBuildID), + }) + return + } + if err != nil { + httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ + Message: fmt.Sprintf("get workspace build: %s", err.Error()), + }) + return + } + + ctx := context.WithValue(r.Context(), workspaceBuildParamContextKey{}, workspaceBuild) + // This injects the "workspace" parameter, because it's expected the consumer + // will want to use the Workspace middleware to ensure the caller owns the workspace. + chi.RouteContext(ctx).URLParams.Add("workspace", workspaceBuild.WorkspaceID.String()) + next.ServeHTTP(rw, r.WithContext(ctx)) + }) + } +} diff --git a/httpmw/workspacehistoryparam_test.go b/httpmw/workspacebuildparam_test.go similarity index 64% rename from httpmw/workspacehistoryparam_test.go rename to httpmw/workspacebuildparam_test.go index 063f2fd7be3ca..7ef913770abc4 100644 --- a/httpmw/workspacehistoryparam_test.go +++ b/httpmw/workspacebuildparam_test.go @@ -19,7 +19,7 @@ import ( "github.com/coder/coder/httpmw" ) -func TestWorkspaceHistoryParam(t *testing.T) { +func TestWorkspaceBuildParam(t *testing.T) { t.Parallel() setupAuthentication := func(db database.Store) (*http.Request, database.Workspace) { @@ -74,12 +74,7 @@ func TestWorkspaceHistoryParam(t *testing.T) { t.Parallel() db := databasefake.New() rtr := chi.NewRouter() - rtr.Use( - httpmw.ExtractAPIKey(db, nil), - httpmw.ExtractUserParam(db), - httpmw.ExtractWorkspaceParam(db), - httpmw.ExtractWorkspaceHistoryParam(db), - ) + rtr.Use(httpmw.ExtractWorkspaceBuildParam(db)) rtr.Get("/", nil) r, _ := setupAuthentication(db) rw := httptest.NewRecorder() @@ -94,16 +89,11 @@ func TestWorkspaceHistoryParam(t *testing.T) { t.Parallel() db := databasefake.New() rtr := chi.NewRouter() - rtr.Use( - httpmw.ExtractAPIKey(db, nil), - httpmw.ExtractUserParam(db), - httpmw.ExtractWorkspaceParam(db), - httpmw.ExtractWorkspaceHistoryParam(db), - ) + rtr.Use(httpmw.ExtractWorkspaceBuildParam(db)) rtr.Get("/", nil) r, _ := setupAuthentication(db) - chi.RouteContext(r.Context()).URLParams.Add("workspacehistory", "nothin") + chi.RouteContext(r.Context()).URLParams.Add("workspacebuild", uuid.NewString()) rw := httptest.NewRecorder() rtr.ServeHTTP(rw, r) @@ -112,60 +102,28 @@ func TestWorkspaceHistoryParam(t *testing.T) { require.Equal(t, http.StatusNotFound, res.StatusCode) }) - t.Run("WorkspaceHistory", func(t *testing.T) { - t.Parallel() - db := databasefake.New() - rtr := chi.NewRouter() - rtr.Use( - httpmw.ExtractAPIKey(db, nil), - httpmw.ExtractUserParam(db), - httpmw.ExtractWorkspaceParam(db), - httpmw.ExtractWorkspaceHistoryParam(db), - ) - rtr.Get("/", func(rw http.ResponseWriter, r *http.Request) { - _ = httpmw.WorkspaceHistoryParam(r) - rw.WriteHeader(http.StatusOK) - }) - - r, workspace := setupAuthentication(db) - workspaceHistory, err := db.InsertWorkspaceHistory(context.Background(), database.InsertWorkspaceHistoryParams{ - ID: uuid.New(), - WorkspaceID: workspace.ID, - Name: "moo", - }) - require.NoError(t, err) - chi.RouteContext(r.Context()).URLParams.Add("workspacehistory", workspaceHistory.Name) - rw := httptest.NewRecorder() - rtr.ServeHTTP(rw, r) - - res := rw.Result() - defer res.Body.Close() - require.Equal(t, http.StatusOK, res.StatusCode) - }) - - t.Run("WorkspaceHistoryLatest", func(t *testing.T) { + t.Run("WorkspaceBuild", func(t *testing.T) { t.Parallel() db := databasefake.New() rtr := chi.NewRouter() rtr.Use( httpmw.ExtractAPIKey(db, nil), - httpmw.ExtractUserParam(db), + httpmw.ExtractWorkspaceBuildParam(db), httpmw.ExtractWorkspaceParam(db), - httpmw.ExtractWorkspaceHistoryParam(db), ) rtr.Get("/", func(rw http.ResponseWriter, r *http.Request) { - _ = httpmw.WorkspaceHistoryParam(r) + _ = httpmw.WorkspaceBuildParam(r) rw.WriteHeader(http.StatusOK) }) r, workspace := setupAuthentication(db) - _, err := db.InsertWorkspaceHistory(context.Background(), database.InsertWorkspaceHistoryParams{ + workspaceBuild, err := db.InsertWorkspaceBuild(context.Background(), database.InsertWorkspaceBuildParams{ ID: uuid.New(), WorkspaceID: workspace.ID, Name: "moo", }) require.NoError(t, err) - chi.RouteContext(r.Context()).URLParams.Add("workspacehistory", "latest") + chi.RouteContext(r.Context()).URLParams.Add("workspacebuild", workspaceBuild.ID.String()) rw := httptest.NewRecorder() rtr.ServeHTTP(rw, r) diff --git a/httpmw/workspacehistoryparam.go b/httpmw/workspacehistoryparam.go deleted file mode 100644 index ff426faf23c83..0000000000000 --- a/httpmw/workspacehistoryparam.go +++ /dev/null @@ -1,72 +0,0 @@ -package httpmw - -import ( - "context" - "database/sql" - "errors" - "fmt" - "net/http" - - "github.com/go-chi/chi/v5" - - "github.com/coder/coder/database" - "github.com/coder/coder/httpapi" -) - -type workspaceHistoryParamContextKey struct{} - -// WorkspaceHistoryParam returns the workspace history from the ExtractWorkspaceHistoryParam handler. -func WorkspaceHistoryParam(r *http.Request) database.WorkspaceHistory { - workspaceHistory, ok := r.Context().Value(workspaceHistoryParamContextKey{}).(database.WorkspaceHistory) - if !ok { - panic("developer error: workspace history param middleware not provided") - } - return workspaceHistory -} - -// ExtractWorkspaceHistoryParam grabs workspace history from the "workspacehistory" URL parameter. -func ExtractWorkspaceHistoryParam(db database.Store) func(http.Handler) http.Handler { - return func(next http.Handler) http.Handler { - return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { - workspace := WorkspaceParam(r) - workspaceHistoryName := chi.URLParam(r, "workspacehistory") - if workspaceHistoryName == "" { - httpapi.Write(rw, http.StatusBadRequest, httpapi.Response{ - Message: "workspace history name must be provided", - }) - return - } - var workspaceHistory database.WorkspaceHistory - var err error - if workspaceHistoryName == "latest" { - workspaceHistory, err = db.GetWorkspaceHistoryByWorkspaceIDWithoutAfter(r.Context(), workspace.ID) - if errors.Is(err, sql.ErrNoRows) { - httpapi.Write(rw, http.StatusNotFound, httpapi.Response{ - Message: "there is no workspace history", - }) - return - } - } else { - workspaceHistory, err = db.GetWorkspaceHistoryByWorkspaceIDAndName(r.Context(), database.GetWorkspaceHistoryByWorkspaceIDAndNameParams{ - WorkspaceID: workspace.ID, - Name: workspaceHistoryName, - }) - if errors.Is(err, sql.ErrNoRows) { - httpapi.Write(rw, http.StatusNotFound, httpapi.Response{ - Message: fmt.Sprintf("workspace history %q does not exist", workspaceHistoryName), - }) - return - } - } - if err != nil { - httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ - Message: fmt.Sprintf("get workspace history: %s", err.Error()), - }) - return - } - - ctx := context.WithValue(r.Context(), workspaceHistoryParamContextKey{}, workspaceHistory) - next.ServeHTTP(rw, r.WithContext(ctx)) - }) - } -} diff --git a/httpmw/workspaceparam.go b/httpmw/workspaceparam.go index dff494a24af23..4c1f852d97015 100644 --- a/httpmw/workspaceparam.go +++ b/httpmw/workspaceparam.go @@ -7,8 +7,6 @@ import ( "fmt" "net/http" - "github.com/go-chi/chi/v5" - "github.com/coder/coder/database" "github.com/coder/coder/httpapi" ) @@ -28,18 +26,11 @@ func WorkspaceParam(r *http.Request) database.Workspace { func ExtractWorkspaceParam(db database.Store) func(http.Handler) http.Handler { return func(next http.Handler) http.Handler { return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { - user := UserParam(r) - workspaceName := chi.URLParam(r, "workspace") - if workspaceName == "" { - httpapi.Write(rw, http.StatusBadRequest, httpapi.Response{ - Message: "workspace id must be provided", - }) + workspaceID, parsed := parseUUID(rw, r, "workspace") + if !parsed { return } - workspace, err := db.GetWorkspaceByUserIDAndName(r.Context(), database.GetWorkspaceByUserIDAndNameParams{ - OwnerID: user.ID, - Name: workspaceName, - }) + workspace, err := db.GetWorkspaceByID(r.Context(), workspaceID) if errors.Is(err, sql.ErrNoRows) { httpapi.Write(rw, http.StatusNotFound, httpapi.Response{ Message: fmt.Sprintf("workspace %q does not exist", workspace), @@ -53,6 +44,14 @@ func ExtractWorkspaceParam(db database.Store) func(http.Handler) http.Handler { return } + apiKey := APIKey(r) + if apiKey.UserID != workspace.OwnerID { + httpapi.Write(rw, http.StatusUnauthorized, httpapi.Response{ + Message: "getting non-personal workspaces isn't supported", + }) + return + } + ctx := context.WithValue(r.Context(), workspaceParamContextKey{}, workspace) next.ServeHTTP(rw, r.WithContext(ctx)) }) diff --git a/httpmw/workspaceparam_test.go b/httpmw/workspaceparam_test.go index 39dd15631965d..e68cc80920947 100644 --- a/httpmw/workspaceparam_test.go +++ b/httpmw/workspaceparam_test.go @@ -66,11 +66,7 @@ func TestWorkspaceParam(t *testing.T) { t.Parallel() db := databasefake.New() rtr := chi.NewRouter() - rtr.Use( - httpmw.ExtractAPIKey(db, nil), - httpmw.ExtractUserParam(db), - httpmw.ExtractWorkspaceParam(db), - ) + rtr.Use(httpmw.ExtractWorkspaceParam(db)) rtr.Get("/", nil) r, _ := setup(db) rw := httptest.NewRecorder() @@ -82,23 +78,44 @@ func TestWorkspaceParam(t *testing.T) { }) t.Run("NotFound", func(t *testing.T) { + t.Parallel() + db := databasefake.New() + rtr := chi.NewRouter() + rtr.Use(httpmw.ExtractWorkspaceParam(db)) + rtr.Get("/", nil) + r, _ := setup(db) + chi.RouteContext(r.Context()).URLParams.Add("workspace", uuid.NewString()) + rw := httptest.NewRecorder() + rtr.ServeHTTP(rw, r) + + res := rw.Result() + defer res.Body.Close() + require.Equal(t, http.StatusNotFound, res.StatusCode) + }) + + t.Run("NonPersonal", func(t *testing.T) { t.Parallel() db := databasefake.New() rtr := chi.NewRouter() rtr.Use( httpmw.ExtractAPIKey(db, nil), - httpmw.ExtractUserParam(db), httpmw.ExtractWorkspaceParam(db), ) rtr.Get("/", nil) r, _ := setup(db) - chi.RouteContext(r.Context()).URLParams.Add("workspace", "frog") + workspace, err := db.InsertWorkspace(context.Background(), database.InsertWorkspaceParams{ + ID: uuid.New(), + OwnerID: "not-me", + Name: "hello", + }) + require.NoError(t, err) + chi.RouteContext(r.Context()).URLParams.Add("workspace", workspace.ID.String()) rw := httptest.NewRecorder() rtr.ServeHTTP(rw, r) res := rw.Result() defer res.Body.Close() - require.Equal(t, http.StatusNotFound, res.StatusCode) + require.Equal(t, http.StatusUnauthorized, res.StatusCode) }) t.Run("Found", func(t *testing.T) { @@ -107,7 +124,6 @@ func TestWorkspaceParam(t *testing.T) { rtr := chi.NewRouter() rtr.Use( httpmw.ExtractAPIKey(db, nil), - httpmw.ExtractUserParam(db), httpmw.ExtractWorkspaceParam(db), ) rtr.Get("/", func(rw http.ResponseWriter, r *http.Request) { @@ -121,7 +137,7 @@ func TestWorkspaceParam(t *testing.T) { Name: "hello", }) require.NoError(t, err) - chi.RouteContext(r.Context()).URLParams.Add("workspace", workspace.Name) + chi.RouteContext(r.Context()).URLParams.Add("workspace", workspace.ID.String()) rw := httptest.NewRecorder() rtr.ServeHTTP(rw, r) diff --git a/httpmw/workspaceresourceparam.go b/httpmw/workspaceresourceparam.go new file mode 100644 index 0000000000000..a02d556866315 --- /dev/null +++ b/httpmw/workspaceresourceparam.go @@ -0,0 +1,76 @@ +package httpmw + +import ( + "context" + "database/sql" + "errors" + "fmt" + "net/http" + + "github.com/go-chi/chi/v5" + + "github.com/coder/coder/database" + "github.com/coder/coder/httpapi" +) + +type workspaceResourceParamContextKey struct{} + +// ProvisionerJobParam returns the project from the ExtractProjectParam handler. +func WorkspaceResourceParam(r *http.Request) database.WorkspaceResource { + resource, ok := r.Context().Value(workspaceResourceParamContextKey{}).(database.WorkspaceResource) + if !ok { + panic("developer error: workspace resource param middleware not provided") + } + return resource +} + +// ExtractWorkspaceResourceParam grabs a workspace resource from the "provisionerjob" URL parameter. +func ExtractWorkspaceResourceParam(db database.Store) func(http.Handler) http.Handler { + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { + resourceUUID, parsed := parseUUID(rw, r, "workspaceresource") + if !parsed { + return + } + resource, err := db.GetWorkspaceResourceByID(r.Context(), resourceUUID) + if errors.Is(err, sql.ErrNoRows) { + httpapi.Write(rw, http.StatusNotFound, httpapi.Response{ + Message: "resource doesn't exist with that id", + }) + return + } + if err != nil { + httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ + Message: fmt.Sprintf("get provisioner resource: %s", err), + }) + return + } + + job, err := db.GetProvisionerJobByID(r.Context(), resource.JobID) + if err != nil { + httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ + Message: fmt.Sprintf("get provisioner job: %s", err), + }) + return + } + if job.Type != database.ProvisionerJobTypeWorkspaceBuild { + httpapi.Write(rw, http.StatusBadRequest, httpapi.Response{ + Message: "Workspace resources can only be fetched for builds.", + }) + return + } + build, err := db.GetWorkspaceBuildByJobID(r.Context(), job.ID) + if err != nil { + httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ + Message: fmt.Sprintf("get workspace build: %s", err), + }) + return + } + + ctx := context.WithValue(r.Context(), workspaceResourceParamContextKey{}, resource) + ctx = context.WithValue(ctx, workspaceBuildParamContextKey{}, build) + chi.RouteContext(ctx).URLParams.Add("workspace", build.WorkspaceID.String()) + next.ServeHTTP(rw, r.WithContext(ctx)) + }) + } +} diff --git a/httpmw/workspaceresourceparam_test.go b/httpmw/workspaceresourceparam_test.go new file mode 100644 index 0000000000000..8ac404f84b9f3 --- /dev/null +++ b/httpmw/workspaceresourceparam_test.go @@ -0,0 +1,122 @@ +package httpmw_test + +import ( + "context" + "net/http" + "net/http/httptest" + "testing" + + "github.com/go-chi/chi/v5" + "github.com/google/uuid" + "github.com/stretchr/testify/require" + + "github.com/coder/coder/database" + "github.com/coder/coder/database/databasefake" + "github.com/coder/coder/httpmw" +) + +func TestWorkspaceResourceParam(t *testing.T) { + t.Parallel() + + setup := func(db database.Store, jobType database.ProvisionerJobType) (*http.Request, database.WorkspaceResource) { + r := httptest.NewRequest("GET", "/", nil) + job, err := db.InsertProvisionerJob(context.Background(), database.InsertProvisionerJobParams{ + ID: uuid.New(), + Type: jobType, + }) + require.NoError(t, err) + workspaceBuild, err := db.InsertWorkspaceBuild(context.Background(), database.InsertWorkspaceBuildParams{ + ID: uuid.New(), + JobID: job.ID, + }) + require.NoError(t, err) + resource, err := db.InsertWorkspaceResource(context.Background(), database.InsertWorkspaceResourceParams{ + ID: uuid.New(), + JobID: job.ID, + }) + require.NoError(t, err) + + ctx := chi.NewRouteContext() + ctx.URLParams.Add("workspacebuild", workspaceBuild.ID.String()) + r = r.WithContext(context.WithValue(r.Context(), chi.RouteCtxKey, ctx)) + return r, resource + } + + t.Run("None", func(t *testing.T) { + t.Parallel() + db := databasefake.New() + rtr := chi.NewRouter() + rtr.Use(httpmw.ExtractWorkspaceResourceParam(db)) + rtr.Get("/", nil) + r, _ := setup(db, database.ProvisionerJobTypeWorkspaceBuild) + rw := httptest.NewRecorder() + rtr.ServeHTTP(rw, r) + + res := rw.Result() + defer res.Body.Close() + require.Equal(t, http.StatusBadRequest, res.StatusCode) + }) + + t.Run("NotFound", func(t *testing.T) { + t.Parallel() + db := databasefake.New() + rtr := chi.NewRouter() + rtr.Use( + httpmw.ExtractWorkspaceResourceParam(db), + ) + rtr.Get("/", nil) + + r, _ := setup(db, database.ProvisionerJobTypeWorkspaceBuild) + chi.RouteContext(r.Context()).URLParams.Add("workspaceresource", uuid.NewString()) + rw := httptest.NewRecorder() + rtr.ServeHTTP(rw, r) + + res := rw.Result() + defer res.Body.Close() + require.Equal(t, http.StatusNotFound, res.StatusCode) + }) + + t.Run("FoundBadJobType", func(t *testing.T) { + t.Parallel() + db := databasefake.New() + rtr := chi.NewRouter() + rtr.Use( + httpmw.ExtractWorkspaceResourceParam(db), + ) + rtr.Get("/", func(rw http.ResponseWriter, r *http.Request) { + _ = httpmw.WorkspaceResourceParam(r) + rw.WriteHeader(http.StatusOK) + }) + + r, job := setup(db, database.ProvisionerJobTypeProjectVersionImport) + chi.RouteContext(r.Context()).URLParams.Add("workspaceresource", job.ID.String()) + rw := httptest.NewRecorder() + rtr.ServeHTTP(rw, r) + + res := rw.Result() + defer res.Body.Close() + require.Equal(t, http.StatusBadRequest, res.StatusCode) + }) + + t.Run("Found", func(t *testing.T) { + t.Parallel() + db := databasefake.New() + rtr := chi.NewRouter() + rtr.Use( + httpmw.ExtractWorkspaceResourceParam(db), + ) + rtr.Get("/", func(rw http.ResponseWriter, r *http.Request) { + _ = httpmw.WorkspaceResourceParam(r) + rw.WriteHeader(http.StatusOK) + }) + + r, job := setup(db, database.ProvisionerJobTypeWorkspaceBuild) + chi.RouteContext(r.Context()).URLParams.Add("workspaceresource", job.ID.String()) + rw := httptest.NewRecorder() + rtr.ServeHTTP(rw, r) + + res := rw.Result() + defer res.Body.Close() + require.Equal(t, http.StatusOK, res.StatusCode) + }) +} diff --git a/images/coder/run.sh b/images/coder/run.sh index 411e83b2b7dcf..eb04622165c1d 100755 --- a/images/coder/run.sh +++ b/images/coder/run.sh @@ -17,7 +17,7 @@ function create_initial_user() { curl -X POST \ -d '{"email": "'"$EMAIL"'", "username": "'"$USERNAME"'", "organization": "'"$ORGANIZATION"'", "password": "'"$PASSWORD"'"}' \ -H 'Content-Type:application/json' \ - "http://localhost:$PORT/api/v2/user" + "http://localhost:$PORT/api/v2/users/first" } # This is a way to run multiple processes in parallel, and have Ctrl-C work correctly diff --git a/peer/conn.go b/peer/conn.go index 6666398a69da7..e699f8889fede 100644 --- a/peer/conn.go +++ b/peer/conn.go @@ -584,7 +584,7 @@ func (c *Conn) CloseWithError(err error) error { // All logging, goroutines, and async functionality is cleaned up after this. c.dcClosedWaitGroup.Wait() - close(c.closed) c.opts.Logger.Debug(context.Background(), "closed") + close(c.closed) return err } diff --git a/peerbroker/listen.go b/peerbroker/listen.go index b9209a8cab7ff..7a134f9937a17 100644 --- a/peerbroker/listen.go +++ b/peerbroker/listen.go @@ -30,8 +30,9 @@ func Listen(connListener net.Listener, iceServersFunc ICEServersFunc, opts *peer } ctx, cancelFunc := context.WithCancel(context.Background()) listener := &Listener{ - connectionChannel: make(chan *peer.Conn), - iceServersFunc: iceServersFunc, + connectionChannel: make(chan *peer.Conn), + connectionListener: connListener, + iceServersFunc: iceServersFunc, closeFunc: cancelFunc, closed: make(chan struct{}), @@ -56,8 +57,9 @@ func Listen(connListener net.Listener, iceServersFunc ICEServersFunc, opts *peer } type Listener struct { - connectionChannel chan *peer.Conn - iceServersFunc ICEServersFunc + connectionChannel chan *peer.Conn + connectionListener net.Listener + iceServersFunc ICEServersFunc closeFunc context.CancelFunc closed chan struct{} @@ -89,6 +91,7 @@ func (l *Listener) closeWithError(err error) error { return l.closeError } + _ = l.connectionListener.Close() l.closeError = err l.closeFunc() close(l.closed) diff --git a/peerbroker/proxy.go b/peerbroker/proxy.go index 9a41277eeb35f..e732eb295c2c8 100644 --- a/peerbroker/proxy.go +++ b/peerbroker/proxy.go @@ -2,6 +2,7 @@ package peerbroker import ( "context" + "encoding/base64" "errors" "fmt" "io" @@ -118,7 +119,7 @@ func (p *proxyListen) NegotiateConnection(stream proto.DRPCPeerBroker_NegotiateC return xerrors.Errorf("maximum payload size %d exceeded", maxPayloadSizeBytes) } data = append([]byte(streamID), data...) - err = p.pubsub.Publish(proxyOutID(p.channelID), data) + err = p.pubsub.Publish(proxyOutID(p.channelID), marshal(data)) if err != nil { return xerrors.Errorf("publish: %w", err) } @@ -127,6 +128,11 @@ func (p *proxyListen) NegotiateConnection(stream proto.DRPCPeerBroker_NegotiateC } func (*proxyListen) onServerToClientMessage(streamID string, stream proto.DRPCPeerBroker_NegotiateConnectionStream, message []byte) error { + var err error + message, err = unmarshal(message) + if err != nil { + return xerrors.Errorf("decode: %w", err) + } if len(message) < streamIDLength { return xerrors.Errorf("got message length %d < %d", len(message), streamIDLength) } @@ -136,7 +142,7 @@ func (*proxyListen) onServerToClientMessage(streamID string, stream proto.DRPCPe return nil } var msg proto.Exchange - err := protobuf.Unmarshal(message[streamIDLength:], &msg) + err = protobuf.Unmarshal(message[streamIDLength:], &msg) if err != nil { return xerrors.Errorf("unmarshal message: %w", err) } @@ -173,10 +179,14 @@ func (p *proxyDial) listen() error { } func (p *proxyDial) onClientToServerMessage(ctx context.Context, message []byte) error { + var err error + message, err = unmarshal(message) + if err != nil { + return xerrors.Errorf("decode: %w", err) + } if len(message) < streamIDLength { return xerrors.Errorf("got message length %d < %d", len(message), streamIDLength) } - var err error streamID := string(message[0:streamIDLength]) p.streamMutex.Lock() stream, ok := p.streams[streamID] @@ -190,7 +200,7 @@ func (p *proxyDial) onClientToServerMessage(ctx context.Context, message []byte) go func() { defer stream.Close() - err = p.onServerToClientMessage(streamID, stream) + err := p.onServerToClientMessage(streamID, stream) if err != nil { p.logger.Debug(ctx, "failed to accept server message", slog.Error(err)) } @@ -236,7 +246,7 @@ func (p *proxyDial) onServerToClientMessage(streamID string, stream proto.DRPCPe return xerrors.Errorf("maximum payload size %d exceeded", maxPayloadSizeBytes) } data = append([]byte(streamID), data...) - err = p.pubsub.Publish(proxyInID(p.channelID), data) + err = p.pubsub.Publish(proxyInID(p.channelID), marshal(data)) if err != nil { return xerrors.Errorf("publish: %w", err) } @@ -251,6 +261,16 @@ func (p *proxyDial) Close() error { return nil } +// base64 needs to be used here to keep the pubsub messages in UTF-8 range. +// PostgreSQL cannot handle non UTF-8 messages over pubsub. +func marshal(data []byte) []byte { + return []byte(base64.StdEncoding.EncodeToString(data)) +} + +func unmarshal(data []byte) ([]byte, error) { + return base64.StdEncoding.DecodeString(string(data)) +} + func proxyOutID(channelID string) string { return fmt.Sprintf("%s-out", channelID) } diff --git a/provisioner/echo/serve.go b/provisioner/echo/serve.go index 0d2ac2e65d8cd..9d003ac8311cb 100644 --- a/provisioner/echo/serve.go +++ b/provisioner/echo/serve.go @@ -75,7 +75,11 @@ func (*echo) Parse(request *proto.Parse_Request, stream proto.DRPCProvisioner_Pa // Provision reads requests from the provided directory to stream responses. func (*echo) Provision(request *proto.Provision_Request, stream proto.DRPCProvisioner_ProvisionStream) error { for index := 0; ; index++ { - path := filepath.Join(request.Directory, fmt.Sprintf("%d.provision.protobuf", index)) + extension := ".protobuf" + if request.DryRun { + extension = ".dry.protobuf" + } + path := filepath.Join(request.Directory, fmt.Sprintf("%d.provision"+extension, index)) _, err := os.Stat(path) if err != nil { if index == 0 { @@ -107,14 +111,18 @@ func (*echo) Shutdown(_ context.Context, _ *proto.Empty) (*proto.Empty, error) { } type Responses struct { - Parse []*proto.Parse_Response - Provision []*proto.Provision_Response + Parse []*proto.Parse_Response + Provision []*proto.Provision_Response + ProvisionDryRun []*proto.Provision_Response } // Tar returns a tar archive of responses to provisioner operations. func Tar(responses *Responses) ([]byte, error) { if responses == nil { - responses = &Responses{ParseComplete, ProvisionComplete} + responses = &Responses{ParseComplete, ProvisionComplete, ProvisionComplete} + } + if responses.ProvisionDryRun == nil { + responses.ProvisionDryRun = responses.Provision } var buffer bytes.Buffer @@ -153,6 +161,23 @@ func Tar(responses *Responses) ([]byte, error) { return nil, err } } + for index, response := range responses.ProvisionDryRun { + data, err := protobuf.Marshal(response) + if err != nil { + return nil, err + } + err = writer.WriteHeader(&tar.Header{ + Name: fmt.Sprintf("%d.provision.dry.protobuf", index), + Size: int64(len(data)), + }) + if err != nil { + return nil, err + } + _, err = writer.Write(data) + if err != nil { + return nil, err + } + } err := writer.Flush() if err != nil { return nil, err diff --git a/provisionerd/proto/provisionerd.pb.go b/provisionerd/proto/provisionerd.pb.go index 8954b6a5ae16a..4a6f31fe1fa58 100644 --- a/provisionerd/proto/provisionerd.pb.go +++ b/provisionerd/proto/provisionerd.pb.go @@ -119,7 +119,7 @@ type AcquiredJob struct { UserName string `protobuf:"bytes,4,opt,name=user_name,json=userName,proto3" json:"user_name,omitempty"` ProjectSourceArchive []byte `protobuf:"bytes,5,opt,name=project_source_archive,json=projectSourceArchive,proto3" json:"project_source_archive,omitempty"` // Types that are assignable to Type: - // *AcquiredJob_WorkspaceProvision_ + // *AcquiredJob_WorkspaceBuild_ // *AcquiredJob_ProjectImport_ Type isAcquiredJob_Type `protobuf_oneof:"type"` } @@ -198,9 +198,9 @@ func (m *AcquiredJob) GetType() isAcquiredJob_Type { return nil } -func (x *AcquiredJob) GetWorkspaceProvision() *AcquiredJob_WorkspaceProvision { - if x, ok := x.GetType().(*AcquiredJob_WorkspaceProvision_); ok { - return x.WorkspaceProvision +func (x *AcquiredJob) GetWorkspaceBuild() *AcquiredJob_WorkspaceBuild { + if x, ok := x.GetType().(*AcquiredJob_WorkspaceBuild_); ok { + return x.WorkspaceBuild } return nil } @@ -216,15 +216,15 @@ type isAcquiredJob_Type interface { isAcquiredJob_Type() } -type AcquiredJob_WorkspaceProvision_ struct { - WorkspaceProvision *AcquiredJob_WorkspaceProvision `protobuf:"bytes,6,opt,name=workspace_provision,json=workspaceProvision,proto3,oneof"` +type AcquiredJob_WorkspaceBuild_ struct { + WorkspaceBuild *AcquiredJob_WorkspaceBuild `protobuf:"bytes,6,opt,name=workspace_build,json=workspaceBuild,proto3,oneof"` } type AcquiredJob_ProjectImport_ struct { ProjectImport *AcquiredJob_ProjectImport `protobuf:"bytes,7,opt,name=project_import,json=projectImport,proto3,oneof"` } -func (*AcquiredJob_WorkspaceProvision_) isAcquiredJob_Type() {} +func (*AcquiredJob_WorkspaceBuild_) isAcquiredJob_Type() {} func (*AcquiredJob_ProjectImport_) isAcquiredJob_Type() {} @@ -236,7 +236,7 @@ type FailedJob struct { JobId string `protobuf:"bytes,1,opt,name=job_id,json=jobId,proto3" json:"job_id,omitempty"` Error string `protobuf:"bytes,2,opt,name=error,proto3" json:"error,omitempty"` // Types that are assignable to Type: - // *FailedJob_WorkspaceProvision_ + // *FailedJob_WorkspaceBuild_ // *FailedJob_ProjectImport_ Type isFailedJob_Type `protobuf_oneof:"type"` } @@ -294,9 +294,9 @@ func (m *FailedJob) GetType() isFailedJob_Type { return nil } -func (x *FailedJob) GetWorkspaceProvision() *FailedJob_WorkspaceProvision { - if x, ok := x.GetType().(*FailedJob_WorkspaceProvision_); ok { - return x.WorkspaceProvision +func (x *FailedJob) GetWorkspaceBuild() *FailedJob_WorkspaceBuild { + if x, ok := x.GetType().(*FailedJob_WorkspaceBuild_); ok { + return x.WorkspaceBuild } return nil } @@ -312,15 +312,15 @@ type isFailedJob_Type interface { isFailedJob_Type() } -type FailedJob_WorkspaceProvision_ struct { - WorkspaceProvision *FailedJob_WorkspaceProvision `protobuf:"bytes,3,opt,name=workspace_provision,json=workspaceProvision,proto3,oneof"` +type FailedJob_WorkspaceBuild_ struct { + WorkspaceBuild *FailedJob_WorkspaceBuild `protobuf:"bytes,3,opt,name=workspace_build,json=workspaceBuild,proto3,oneof"` } type FailedJob_ProjectImport_ struct { ProjectImport *FailedJob_ProjectImport `protobuf:"bytes,4,opt,name=project_import,json=projectImport,proto3,oneof"` } -func (*FailedJob_WorkspaceProvision_) isFailedJob_Type() {} +func (*FailedJob_WorkspaceBuild_) isFailedJob_Type() {} func (*FailedJob_ProjectImport_) isFailedJob_Type() {} @@ -332,7 +332,7 @@ type CompletedJob struct { JobId string `protobuf:"bytes,1,opt,name=job_id,json=jobId,proto3" json:"job_id,omitempty"` // Types that are assignable to Type: - // *CompletedJob_WorkspaceProvision_ + // *CompletedJob_WorkspaceBuild_ // *CompletedJob_ProjectImport_ Type isCompletedJob_Type `protobuf_oneof:"type"` } @@ -383,9 +383,9 @@ func (m *CompletedJob) GetType() isCompletedJob_Type { return nil } -func (x *CompletedJob) GetWorkspaceProvision() *CompletedJob_WorkspaceProvision { - if x, ok := x.GetType().(*CompletedJob_WorkspaceProvision_); ok { - return x.WorkspaceProvision +func (x *CompletedJob) GetWorkspaceBuild() *CompletedJob_WorkspaceBuild { + if x, ok := x.GetType().(*CompletedJob_WorkspaceBuild_); ok { + return x.WorkspaceBuild } return nil } @@ -401,15 +401,15 @@ type isCompletedJob_Type interface { isCompletedJob_Type() } -type CompletedJob_WorkspaceProvision_ struct { - WorkspaceProvision *CompletedJob_WorkspaceProvision `protobuf:"bytes,2,opt,name=workspace_provision,json=workspaceProvision,proto3,oneof"` +type CompletedJob_WorkspaceBuild_ struct { + WorkspaceBuild *CompletedJob_WorkspaceBuild `protobuf:"bytes,2,opt,name=workspace_build,json=workspaceBuild,proto3,oneof"` } type CompletedJob_ProjectImport_ struct { ProjectImport *CompletedJob_ProjectImport `protobuf:"bytes,3,opt,name=project_import,json=projectImport,proto3,oneof"` } -func (*CompletedJob_WorkspaceProvision_) isCompletedJob_Type() {} +func (*CompletedJob_WorkspaceBuild_) isCompletedJob_Type() {} func (*CompletedJob_ProjectImport_) isCompletedJob_Type() {} @@ -598,20 +598,20 @@ func (x *UpdateJobResponse) GetParameterValues() []*proto.ParameterValue { return nil } -type AcquiredJob_WorkspaceProvision struct { +type AcquiredJob_WorkspaceBuild struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - WorkspaceHistoryId string `protobuf:"bytes,1,opt,name=workspace_history_id,json=workspaceHistoryId,proto3" json:"workspace_history_id,omitempty"` - WorkspaceName string `protobuf:"bytes,2,opt,name=workspace_name,json=workspaceName,proto3" json:"workspace_name,omitempty"` - ParameterValues []*proto.ParameterValue `protobuf:"bytes,3,rep,name=parameter_values,json=parameterValues,proto3" json:"parameter_values,omitempty"` - Metadata *proto.Provision_Metadata `protobuf:"bytes,4,opt,name=metadata,proto3" json:"metadata,omitempty"` - State []byte `protobuf:"bytes,5,opt,name=state,proto3" json:"state,omitempty"` + WorkspaceBuildId string `protobuf:"bytes,1,opt,name=workspace_build_id,json=workspaceBuildId,proto3" json:"workspace_build_id,omitempty"` + WorkspaceName string `protobuf:"bytes,2,opt,name=workspace_name,json=workspaceName,proto3" json:"workspace_name,omitempty"` + ParameterValues []*proto.ParameterValue `protobuf:"bytes,3,rep,name=parameter_values,json=parameterValues,proto3" json:"parameter_values,omitempty"` + Metadata *proto.Provision_Metadata `protobuf:"bytes,4,opt,name=metadata,proto3" json:"metadata,omitempty"` + State []byte `protobuf:"bytes,5,opt,name=state,proto3" json:"state,omitempty"` } -func (x *AcquiredJob_WorkspaceProvision) Reset() { - *x = AcquiredJob_WorkspaceProvision{} +func (x *AcquiredJob_WorkspaceBuild) Reset() { + *x = AcquiredJob_WorkspaceBuild{} if protoimpl.UnsafeEnabled { mi := &file_provisionerd_proto_provisionerd_proto_msgTypes[7] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) @@ -619,13 +619,13 @@ func (x *AcquiredJob_WorkspaceProvision) Reset() { } } -func (x *AcquiredJob_WorkspaceProvision) String() string { +func (x *AcquiredJob_WorkspaceBuild) String() string { return protoimpl.X.MessageStringOf(x) } -func (*AcquiredJob_WorkspaceProvision) ProtoMessage() {} +func (*AcquiredJob_WorkspaceBuild) ProtoMessage() {} -func (x *AcquiredJob_WorkspaceProvision) ProtoReflect() protoreflect.Message { +func (x *AcquiredJob_WorkspaceBuild) ProtoReflect() protoreflect.Message { mi := &file_provisionerd_proto_provisionerd_proto_msgTypes[7] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) @@ -637,40 +637,40 @@ func (x *AcquiredJob_WorkspaceProvision) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use AcquiredJob_WorkspaceProvision.ProtoReflect.Descriptor instead. -func (*AcquiredJob_WorkspaceProvision) Descriptor() ([]byte, []int) { +// Deprecated: Use AcquiredJob_WorkspaceBuild.ProtoReflect.Descriptor instead. +func (*AcquiredJob_WorkspaceBuild) Descriptor() ([]byte, []int) { return file_provisionerd_proto_provisionerd_proto_rawDescGZIP(), []int{1, 0} } -func (x *AcquiredJob_WorkspaceProvision) GetWorkspaceHistoryId() string { +func (x *AcquiredJob_WorkspaceBuild) GetWorkspaceBuildId() string { if x != nil { - return x.WorkspaceHistoryId + return x.WorkspaceBuildId } return "" } -func (x *AcquiredJob_WorkspaceProvision) GetWorkspaceName() string { +func (x *AcquiredJob_WorkspaceBuild) GetWorkspaceName() string { if x != nil { return x.WorkspaceName } return "" } -func (x *AcquiredJob_WorkspaceProvision) GetParameterValues() []*proto.ParameterValue { +func (x *AcquiredJob_WorkspaceBuild) GetParameterValues() []*proto.ParameterValue { if x != nil { return x.ParameterValues } return nil } -func (x *AcquiredJob_WorkspaceProvision) GetMetadata() *proto.Provision_Metadata { +func (x *AcquiredJob_WorkspaceBuild) GetMetadata() *proto.Provision_Metadata { if x != nil { return x.Metadata } return nil } -func (x *AcquiredJob_WorkspaceProvision) GetState() []byte { +func (x *AcquiredJob_WorkspaceBuild) GetState() []byte { if x != nil { return x.State } @@ -724,7 +724,7 @@ func (x *AcquiredJob_ProjectImport) GetMetadata() *proto.Provision_Metadata { return nil } -type FailedJob_WorkspaceProvision struct { +type FailedJob_WorkspaceBuild struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields @@ -732,8 +732,8 @@ type FailedJob_WorkspaceProvision struct { State []byte `protobuf:"bytes,1,opt,name=state,proto3" json:"state,omitempty"` } -func (x *FailedJob_WorkspaceProvision) Reset() { - *x = FailedJob_WorkspaceProvision{} +func (x *FailedJob_WorkspaceBuild) Reset() { + *x = FailedJob_WorkspaceBuild{} if protoimpl.UnsafeEnabled { mi := &file_provisionerd_proto_provisionerd_proto_msgTypes[9] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) @@ -741,13 +741,13 @@ func (x *FailedJob_WorkspaceProvision) Reset() { } } -func (x *FailedJob_WorkspaceProvision) String() string { +func (x *FailedJob_WorkspaceBuild) String() string { return protoimpl.X.MessageStringOf(x) } -func (*FailedJob_WorkspaceProvision) ProtoMessage() {} +func (*FailedJob_WorkspaceBuild) ProtoMessage() {} -func (x *FailedJob_WorkspaceProvision) ProtoReflect() protoreflect.Message { +func (x *FailedJob_WorkspaceBuild) ProtoReflect() protoreflect.Message { mi := &file_provisionerd_proto_provisionerd_proto_msgTypes[9] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) @@ -759,12 +759,12 @@ func (x *FailedJob_WorkspaceProvision) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use FailedJob_WorkspaceProvision.ProtoReflect.Descriptor instead. -func (*FailedJob_WorkspaceProvision) Descriptor() ([]byte, []int) { +// Deprecated: Use FailedJob_WorkspaceBuild.ProtoReflect.Descriptor instead. +func (*FailedJob_WorkspaceBuild) Descriptor() ([]byte, []int) { return file_provisionerd_proto_provisionerd_proto_rawDescGZIP(), []int{2, 0} } -func (x *FailedJob_WorkspaceProvision) GetState() []byte { +func (x *FailedJob_WorkspaceBuild) GetState() []byte { if x != nil { return x.State } @@ -809,7 +809,7 @@ func (*FailedJob_ProjectImport) Descriptor() ([]byte, []int) { return file_provisionerd_proto_provisionerd_proto_rawDescGZIP(), []int{2, 1} } -type CompletedJob_WorkspaceProvision struct { +type CompletedJob_WorkspaceBuild struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields @@ -818,8 +818,8 @@ type CompletedJob_WorkspaceProvision struct { Resources []*proto.Resource `protobuf:"bytes,2,rep,name=resources,proto3" json:"resources,omitempty"` } -func (x *CompletedJob_WorkspaceProvision) Reset() { - *x = CompletedJob_WorkspaceProvision{} +func (x *CompletedJob_WorkspaceBuild) Reset() { + *x = CompletedJob_WorkspaceBuild{} if protoimpl.UnsafeEnabled { mi := &file_provisionerd_proto_provisionerd_proto_msgTypes[11] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) @@ -827,13 +827,13 @@ func (x *CompletedJob_WorkspaceProvision) Reset() { } } -func (x *CompletedJob_WorkspaceProvision) String() string { +func (x *CompletedJob_WorkspaceBuild) String() string { return protoimpl.X.MessageStringOf(x) } -func (*CompletedJob_WorkspaceProvision) ProtoMessage() {} +func (*CompletedJob_WorkspaceBuild) ProtoMessage() {} -func (x *CompletedJob_WorkspaceProvision) ProtoReflect() protoreflect.Message { +func (x *CompletedJob_WorkspaceBuild) ProtoReflect() protoreflect.Message { mi := &file_provisionerd_proto_provisionerd_proto_msgTypes[11] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) @@ -845,19 +845,19 @@ func (x *CompletedJob_WorkspaceProvision) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use CompletedJob_WorkspaceProvision.ProtoReflect.Descriptor instead. -func (*CompletedJob_WorkspaceProvision) Descriptor() ([]byte, []int) { +// Deprecated: Use CompletedJob_WorkspaceBuild.ProtoReflect.Descriptor instead. +func (*CompletedJob_WorkspaceBuild) Descriptor() ([]byte, []int) { return file_provisionerd_proto_provisionerd_proto_rawDescGZIP(), []int{3, 0} } -func (x *CompletedJob_WorkspaceProvision) GetState() []byte { +func (x *CompletedJob_WorkspaceBuild) GetState() []byte { if x != nil { return x.State } return nil } -func (x *CompletedJob_WorkspaceProvision) GetResources() []*proto.Resource { +func (x *CompletedJob_WorkspaceBuild) GetResources() []*proto.Resource { if x != nil { return x.Resources } @@ -928,7 +928,7 @@ var file_provisionerd_proto_provisionerd_proto_rawDesc = []byte{ 0x6f, 0x6e, 0x65, 0x72, 0x64, 0x1a, 0x26, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x73, 0x64, 0x6b, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x07, 0x0a, - 0x05, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, 0xcc, 0x05, 0x0a, 0x0b, 0x41, 0x63, 0x71, 0x75, 0x69, + 0x05, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, 0xb8, 0x05, 0x0a, 0x0b, 0x41, 0x63, 0x71, 0x75, 0x69, 0x72, 0x65, 0x64, 0x4a, 0x6f, 0x62, 0x12, 0x15, 0x0a, 0x06, 0x6a, 0x6f, 0x62, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x6a, 0x6f, 0x62, 0x49, 0x64, 0x12, 0x1d, 0x0a, 0x0a, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x5f, 0x61, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, @@ -940,138 +940,135 @@ var file_provisionerd_proto_provisionerd_proto_rawDesc = []byte{ 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x5f, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x61, 0x72, 0x63, 0x68, 0x69, 0x76, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x14, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x41, 0x72, 0x63, 0x68, 0x69, 0x76, - 0x65, 0x12, 0x5f, 0x0a, 0x13, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, 0x70, - 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2c, - 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x64, 0x2e, 0x41, 0x63, - 0x71, 0x75, 0x69, 0x72, 0x65, 0x64, 0x4a, 0x6f, 0x62, 0x2e, 0x57, 0x6f, 0x72, 0x6b, 0x73, 0x70, - 0x61, 0x63, 0x65, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x48, 0x00, 0x52, 0x12, - 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, - 0x6f, 0x6e, 0x12, 0x50, 0x0a, 0x0e, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x5f, 0x69, 0x6d, - 0x70, 0x6f, 0x72, 0x74, 0x18, 0x07, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x27, 0x2e, 0x70, 0x72, 0x6f, + 0x65, 0x12, 0x53, 0x0a, 0x0f, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, 0x62, + 0x75, 0x69, 0x6c, 0x64, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x28, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x64, 0x2e, 0x41, 0x63, 0x71, 0x75, 0x69, 0x72, - 0x65, 0x64, 0x4a, 0x6f, 0x62, 0x2e, 0x50, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x49, 0x6d, 0x70, - 0x6f, 0x72, 0x74, 0x48, 0x00, 0x52, 0x0d, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x49, 0x6d, - 0x70, 0x6f, 0x72, 0x74, 0x1a, 0x88, 0x02, 0x0a, 0x12, 0x57, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, - 0x63, 0x65, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x30, 0x0a, 0x14, 0x77, - 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, 0x68, 0x69, 0x73, 0x74, 0x6f, 0x72, 0x79, - 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x12, 0x77, 0x6f, 0x72, 0x6b, 0x73, - 0x70, 0x61, 0x63, 0x65, 0x48, 0x69, 0x73, 0x74, 0x6f, 0x72, 0x79, 0x49, 0x64, 0x12, 0x25, 0x0a, - 0x0e, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, - 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, - 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x46, 0x0a, 0x10, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, - 0x72, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1b, - 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x50, 0x61, 0x72, - 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x0f, 0x70, 0x61, 0x72, - 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x12, 0x3b, 0x0a, 0x08, - 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1f, + 0x65, 0x64, 0x4a, 0x6f, 0x62, 0x2e, 0x57, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x42, + 0x75, 0x69, 0x6c, 0x64, 0x48, 0x00, 0x52, 0x0e, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, + 0x65, 0x42, 0x75, 0x69, 0x6c, 0x64, 0x12, 0x50, 0x0a, 0x0e, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, + 0x74, 0x5f, 0x69, 0x6d, 0x70, 0x6f, 0x72, 0x74, 0x18, 0x07, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x27, + 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x64, 0x2e, 0x41, 0x63, + 0x71, 0x75, 0x69, 0x72, 0x65, 0x64, 0x4a, 0x6f, 0x62, 0x2e, 0x50, 0x72, 0x6f, 0x6a, 0x65, 0x63, + 0x74, 0x49, 0x6d, 0x70, 0x6f, 0x72, 0x74, 0x48, 0x00, 0x52, 0x0d, 0x70, 0x72, 0x6f, 0x6a, 0x65, + 0x63, 0x74, 0x49, 0x6d, 0x70, 0x6f, 0x72, 0x74, 0x1a, 0x80, 0x02, 0x0a, 0x0e, 0x57, 0x6f, 0x72, + 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x42, 0x75, 0x69, 0x6c, 0x64, 0x12, 0x2c, 0x0a, 0x12, 0x77, + 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x5f, 0x69, + 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x10, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, + 0x63, 0x65, 0x42, 0x75, 0x69, 0x6c, 0x64, 0x49, 0x64, 0x12, 0x25, 0x0a, 0x0e, 0x77, 0x6f, 0x72, + 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x0d, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x4e, 0x61, 0x6d, 0x65, + 0x12, 0x46, 0x0a, 0x10, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x5f, 0x76, 0x61, + 0x6c, 0x75, 0x65, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x70, 0x72, 0x6f, + 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, + 0x65, 0x72, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x0f, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, + 0x65, 0x72, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x12, 0x3b, 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61, + 0x64, 0x61, 0x74, 0x61, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1f, 0x2e, 0x70, 0x72, 0x6f, + 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, + 0x6f, 0x6e, 0x2e, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x52, 0x08, 0x6d, 0x65, 0x74, + 0x61, 0x64, 0x61, 0x74, 0x61, 0x12, 0x14, 0x0a, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x18, 0x05, + 0x20, 0x01, 0x28, 0x0c, 0x52, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x1a, 0x4c, 0x0a, 0x0d, 0x50, + 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x49, 0x6d, 0x70, 0x6f, 0x72, 0x74, 0x12, 0x3b, 0x0a, 0x08, + 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1f, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x2e, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x52, - 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x12, 0x14, 0x0a, 0x05, 0x73, 0x74, 0x61, - 0x74, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x1a, - 0x4c, 0x0a, 0x0d, 0x50, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x49, 0x6d, 0x70, 0x6f, 0x72, 0x74, - 0x12, 0x3b, 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, 0x01, 0x20, 0x01, - 0x28, 0x0b, 0x32, 0x1f, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, - 0x2e, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x2e, 0x4d, 0x65, 0x74, 0x61, 0x64, - 0x61, 0x74, 0x61, 0x52, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x42, 0x06, 0x0a, - 0x04, 0x74, 0x79, 0x70, 0x65, 0x22, 0xac, 0x02, 0x0a, 0x09, 0x46, 0x61, 0x69, 0x6c, 0x65, 0x64, - 0x4a, 0x6f, 0x62, 0x12, 0x15, 0x0a, 0x06, 0x6a, 0x6f, 0x62, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x05, 0x6a, 0x6f, 0x62, 0x49, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x65, 0x72, - 0x72, 0x6f, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, - 0x12, 0x5d, 0x0a, 0x13, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, 0x70, 0x72, - 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2a, 0x2e, - 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x64, 0x2e, 0x46, 0x61, 0x69, - 0x6c, 0x65, 0x64, 0x4a, 0x6f, 0x62, 0x2e, 0x57, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, - 0x50, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x48, 0x00, 0x52, 0x12, 0x77, 0x6f, 0x72, - 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x12, + 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x42, 0x06, 0x0a, 0x04, 0x74, 0x79, 0x70, + 0x65, 0x22, 0x9c, 0x02, 0x0a, 0x09, 0x46, 0x61, 0x69, 0x6c, 0x65, 0x64, 0x4a, 0x6f, 0x62, 0x12, + 0x15, 0x0a, 0x06, 0x6a, 0x6f, 0x62, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x05, 0x6a, 0x6f, 0x62, 0x49, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, + 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x12, 0x51, 0x0a, 0x0f, + 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x18, + 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x26, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, + 0x6e, 0x65, 0x72, 0x64, 0x2e, 0x46, 0x61, 0x69, 0x6c, 0x65, 0x64, 0x4a, 0x6f, 0x62, 0x2e, 0x57, + 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x42, 0x75, 0x69, 0x6c, 0x64, 0x48, 0x00, 0x52, + 0x0e, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x42, 0x75, 0x69, 0x6c, 0x64, 0x12, 0x4e, 0x0a, 0x0e, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x5f, 0x69, 0x6d, 0x70, 0x6f, 0x72, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x25, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x64, 0x2e, 0x46, 0x61, 0x69, 0x6c, 0x65, 0x64, 0x4a, 0x6f, 0x62, 0x2e, 0x50, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x49, 0x6d, 0x70, 0x6f, 0x72, 0x74, 0x48, 0x00, 0x52, 0x0d, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x49, 0x6d, 0x70, 0x6f, 0x72, 0x74, 0x1a, - 0x2a, 0x0a, 0x12, 0x57, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x50, 0x72, 0x6f, 0x76, - 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x14, 0x0a, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x18, 0x01, - 0x20, 0x01, 0x28, 0x0c, 0x52, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x1a, 0x0f, 0x0a, 0x0d, 0x50, - 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x49, 0x6d, 0x70, 0x6f, 0x72, 0x74, 0x42, 0x06, 0x0a, 0x04, - 0x74, 0x79, 0x70, 0x65, 0x22, 0xd3, 0x03, 0x0a, 0x0c, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, - 0x65, 0x64, 0x4a, 0x6f, 0x62, 0x12, 0x15, 0x0a, 0x06, 0x6a, 0x6f, 0x62, 0x5f, 0x69, 0x64, 0x18, - 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x6a, 0x6f, 0x62, 0x49, 0x64, 0x12, 0x60, 0x0a, 0x13, - 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, - 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2d, 0x2e, 0x70, 0x72, 0x6f, 0x76, - 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x64, 0x2e, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, - 0x65, 0x64, 0x4a, 0x6f, 0x62, 0x2e, 0x57, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x50, - 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x48, 0x00, 0x52, 0x12, 0x77, 0x6f, 0x72, 0x6b, - 0x73, 0x70, 0x61, 0x63, 0x65, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x51, + 0x26, 0x0a, 0x0e, 0x57, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x42, 0x75, 0x69, 0x6c, + 0x64, 0x12, 0x14, 0x0a, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, + 0x52, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x1a, 0x0f, 0x0a, 0x0d, 0x50, 0x72, 0x6f, 0x6a, 0x65, + 0x63, 0x74, 0x49, 0x6d, 0x70, 0x6f, 0x72, 0x74, 0x42, 0x06, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, + 0x22, 0xc3, 0x03, 0x0a, 0x0c, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x4a, 0x6f, + 0x62, 0x12, 0x15, 0x0a, 0x06, 0x6a, 0x6f, 0x62, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x05, 0x6a, 0x6f, 0x62, 0x49, 0x64, 0x12, 0x54, 0x0a, 0x0f, 0x77, 0x6f, 0x72, 0x6b, + 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, + 0x0b, 0x32, 0x29, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x64, + 0x2e, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x4a, 0x6f, 0x62, 0x2e, 0x57, 0x6f, + 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x42, 0x75, 0x69, 0x6c, 0x64, 0x48, 0x00, 0x52, 0x0e, + 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x42, 0x75, 0x69, 0x6c, 0x64, 0x12, 0x51, 0x0a, 0x0e, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x5f, 0x69, 0x6d, 0x70, 0x6f, 0x72, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x28, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x64, 0x2e, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x4a, 0x6f, 0x62, 0x2e, 0x50, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x49, 0x6d, 0x70, 0x6f, 0x72, 0x74, 0x48, 0x00, 0x52, 0x0d, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x49, 0x6d, 0x70, 0x6f, 0x72, - 0x74, 0x1a, 0x5f, 0x0a, 0x12, 0x57, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x50, 0x72, - 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x14, 0x0a, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, - 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x12, 0x33, 0x0a, - 0x09, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, - 0x32, 0x15, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x52, - 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x09, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, - 0x65, 0x73, 0x1a, 0x8d, 0x01, 0x0a, 0x0d, 0x50, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x49, 0x6d, - 0x70, 0x6f, 0x72, 0x74, 0x12, 0x3e, 0x0a, 0x0f, 0x73, 0x74, 0x61, 0x72, 0x74, 0x5f, 0x72, 0x65, - 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x15, 0x2e, - 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x52, 0x65, 0x73, 0x6f, - 0x75, 0x72, 0x63, 0x65, 0x52, 0x0e, 0x73, 0x74, 0x61, 0x72, 0x74, 0x52, 0x65, 0x73, 0x6f, 0x75, - 0x72, 0x63, 0x65, 0x73, 0x12, 0x3c, 0x0a, 0x0e, 0x73, 0x74, 0x6f, 0x70, 0x5f, 0x72, 0x65, 0x73, + 0x74, 0x1a, 0x5b, 0x0a, 0x0e, 0x57, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x42, 0x75, + 0x69, 0x6c, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x0c, 0x52, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x12, 0x33, 0x0a, 0x09, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, - 0x72, 0x63, 0x65, 0x52, 0x0d, 0x73, 0x74, 0x6f, 0x70, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, - 0x65, 0x73, 0x42, 0x06, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x22, 0x9a, 0x01, 0x0a, 0x03, 0x4c, - 0x6f, 0x67, 0x12, 0x2f, 0x0a, 0x06, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x18, 0x01, 0x20, 0x01, - 0x28, 0x0e, 0x32, 0x17, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, - 0x64, 0x2e, 0x4c, 0x6f, 0x67, 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x06, 0x73, 0x6f, 0x75, - 0x72, 0x63, 0x65, 0x12, 0x2b, 0x0a, 0x05, 0x6c, 0x65, 0x76, 0x65, 0x6c, 0x18, 0x02, 0x20, 0x01, - 0x28, 0x0e, 0x32, 0x15, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, - 0x2e, 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x52, 0x05, 0x6c, 0x65, 0x76, 0x65, 0x6c, - 0x12, 0x1d, 0x0a, 0x0a, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x5f, 0x61, 0x74, 0x18, 0x03, - 0x20, 0x01, 0x28, 0x03, 0x52, 0x09, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x41, 0x74, 0x12, - 0x16, 0x0a, 0x06, 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x06, 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x22, 0x9b, 0x01, 0x0a, 0x10, 0x55, 0x70, 0x64, 0x61, - 0x74, 0x65, 0x4a, 0x6f, 0x62, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x15, 0x0a, 0x06, - 0x6a, 0x6f, 0x62, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x6a, 0x6f, - 0x62, 0x49, 0x64, 0x12, 0x25, 0x0a, 0x04, 0x6c, 0x6f, 0x67, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, - 0x0b, 0x32, 0x11, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x64, - 0x2e, 0x4c, 0x6f, 0x67, 0x52, 0x04, 0x6c, 0x6f, 0x67, 0x73, 0x12, 0x49, 0x0a, 0x11, 0x70, 0x61, - 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x5f, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x73, 0x18, - 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, - 0x6e, 0x65, 0x72, 0x2e, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x53, 0x63, 0x68, - 0x65, 0x6d, 0x61, 0x52, 0x10, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x53, 0x63, - 0x68, 0x65, 0x6d, 0x61, 0x73, 0x22, 0x5b, 0x0a, 0x11, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x4a, - 0x6f, 0x62, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x46, 0x0a, 0x10, 0x70, 0x61, - 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x18, 0x01, - 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, - 0x65, 0x72, 0x2e, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x56, 0x61, 0x6c, 0x75, - 0x65, 0x52, 0x0f, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x56, 0x61, 0x6c, 0x75, - 0x65, 0x73, 0x2a, 0x34, 0x0a, 0x09, 0x4c, 0x6f, 0x67, 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x12, - 0x16, 0x0a, 0x12, 0x50, 0x52, 0x4f, 0x56, 0x49, 0x53, 0x49, 0x4f, 0x4e, 0x45, 0x52, 0x5f, 0x44, - 0x41, 0x45, 0x4d, 0x4f, 0x4e, 0x10, 0x00, 0x12, 0x0f, 0x0a, 0x0b, 0x50, 0x52, 0x4f, 0x56, 0x49, - 0x53, 0x49, 0x4f, 0x4e, 0x45, 0x52, 0x10, 0x01, 0x32, 0x98, 0x02, 0x0a, 0x11, 0x50, 0x72, 0x6f, - 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x44, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x12, 0x3c, - 0x0a, 0x0a, 0x41, 0x63, 0x71, 0x75, 0x69, 0x72, 0x65, 0x4a, 0x6f, 0x62, 0x12, 0x13, 0x2e, 0x70, - 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x64, 0x2e, 0x45, 0x6d, 0x70, 0x74, - 0x79, 0x1a, 0x19, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x64, - 0x2e, 0x41, 0x63, 0x71, 0x75, 0x69, 0x72, 0x65, 0x64, 0x4a, 0x6f, 0x62, 0x12, 0x4c, 0x0a, 0x09, - 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x4a, 0x6f, 0x62, 0x12, 0x1e, 0x2e, 0x70, 0x72, 0x6f, 0x76, - 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x64, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x4a, - 0x6f, 0x62, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1f, 0x2e, 0x70, 0x72, 0x6f, 0x76, - 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x64, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x4a, - 0x6f, 0x62, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x37, 0x0a, 0x07, 0x46, 0x61, - 0x69, 0x6c, 0x4a, 0x6f, 0x62, 0x12, 0x17, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, - 0x6e, 0x65, 0x72, 0x64, 0x2e, 0x46, 0x61, 0x69, 0x6c, 0x65, 0x64, 0x4a, 0x6f, 0x62, 0x1a, 0x13, - 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x64, 0x2e, 0x45, 0x6d, - 0x70, 0x74, 0x79, 0x12, 0x3e, 0x0a, 0x0b, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x4a, - 0x6f, 0x62, 0x12, 0x1a, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, - 0x64, 0x2e, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x4a, 0x6f, 0x62, 0x1a, 0x13, - 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x64, 0x2e, 0x45, 0x6d, - 0x70, 0x74, 0x79, 0x42, 0x2b, 0x5a, 0x29, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, - 0x6d, 0x2f, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2f, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2f, 0x70, 0x72, - 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x64, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, - 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x72, 0x63, 0x65, 0x52, 0x09, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x1a, 0x8d, + 0x01, 0x0a, 0x0d, 0x50, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x49, 0x6d, 0x70, 0x6f, 0x72, 0x74, + 0x12, 0x3e, 0x0a, 0x0f, 0x73, 0x74, 0x61, 0x72, 0x74, 0x5f, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, + 0x63, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x70, 0x72, 0x6f, 0x76, + 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, + 0x52, 0x0e, 0x73, 0x74, 0x61, 0x72, 0x74, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, + 0x12, 0x3c, 0x0a, 0x0e, 0x73, 0x74, 0x6f, 0x70, 0x5f, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, + 0x65, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, + 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, + 0x0d, 0x73, 0x74, 0x6f, 0x70, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x42, 0x06, + 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x22, 0x9a, 0x01, 0x0a, 0x03, 0x4c, 0x6f, 0x67, 0x12, 0x2f, + 0x0a, 0x06, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x17, + 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x64, 0x2e, 0x4c, 0x6f, + 0x67, 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x06, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x12, + 0x2b, 0x0a, 0x05, 0x6c, 0x65, 0x76, 0x65, 0x6c, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x15, + 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x4c, 0x6f, 0x67, + 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x52, 0x05, 0x6c, 0x65, 0x76, 0x65, 0x6c, 0x12, 0x1d, 0x0a, 0x0a, + 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x5f, 0x61, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, + 0x52, 0x09, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x41, 0x74, 0x12, 0x16, 0x0a, 0x06, 0x6f, + 0x75, 0x74, 0x70, 0x75, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x6f, 0x75, 0x74, + 0x70, 0x75, 0x74, 0x22, 0x9b, 0x01, 0x0a, 0x10, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x4a, 0x6f, + 0x62, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x15, 0x0a, 0x06, 0x6a, 0x6f, 0x62, 0x5f, + 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x6a, 0x6f, 0x62, 0x49, 0x64, 0x12, + 0x25, 0x0a, 0x04, 0x6c, 0x6f, 0x67, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x11, 0x2e, + 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x64, 0x2e, 0x4c, 0x6f, 0x67, + 0x52, 0x04, 0x6c, 0x6f, 0x67, 0x73, 0x12, 0x49, 0x0a, 0x11, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x65, + 0x74, 0x65, 0x72, 0x5f, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, + 0x0b, 0x32, 0x1c, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, + 0x50, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x53, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x52, + 0x10, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x53, 0x63, 0x68, 0x65, 0x6d, 0x61, + 0x73, 0x22, 0x5b, 0x0a, 0x11, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x4a, 0x6f, 0x62, 0x52, 0x65, + 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x46, 0x0a, 0x10, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x65, + 0x74, 0x65, 0x72, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, + 0x32, 0x1b, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x50, + 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x0f, 0x70, + 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x2a, 0x34, + 0x0a, 0x09, 0x4c, 0x6f, 0x67, 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x12, 0x16, 0x0a, 0x12, 0x50, + 0x52, 0x4f, 0x56, 0x49, 0x53, 0x49, 0x4f, 0x4e, 0x45, 0x52, 0x5f, 0x44, 0x41, 0x45, 0x4d, 0x4f, + 0x4e, 0x10, 0x00, 0x12, 0x0f, 0x0a, 0x0b, 0x50, 0x52, 0x4f, 0x56, 0x49, 0x53, 0x49, 0x4f, 0x4e, + 0x45, 0x52, 0x10, 0x01, 0x32, 0x98, 0x02, 0x0a, 0x11, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, + 0x6f, 0x6e, 0x65, 0x72, 0x44, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x12, 0x3c, 0x0a, 0x0a, 0x41, 0x63, + 0x71, 0x75, 0x69, 0x72, 0x65, 0x4a, 0x6f, 0x62, 0x12, 0x13, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, + 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x64, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x19, 0x2e, + 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x64, 0x2e, 0x41, 0x63, 0x71, + 0x75, 0x69, 0x72, 0x65, 0x64, 0x4a, 0x6f, 0x62, 0x12, 0x4c, 0x0a, 0x09, 0x55, 0x70, 0x64, 0x61, + 0x74, 0x65, 0x4a, 0x6f, 0x62, 0x12, 0x1e, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, + 0x6e, 0x65, 0x72, 0x64, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x4a, 0x6f, 0x62, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1f, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, + 0x6e, 0x65, 0x72, 0x64, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x4a, 0x6f, 0x62, 0x52, 0x65, + 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x37, 0x0a, 0x07, 0x46, 0x61, 0x69, 0x6c, 0x4a, 0x6f, + 0x62, 0x12, 0x17, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x64, + 0x2e, 0x46, 0x61, 0x69, 0x6c, 0x65, 0x64, 0x4a, 0x6f, 0x62, 0x1a, 0x13, 0x2e, 0x70, 0x72, 0x6f, + 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x64, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, + 0x3e, 0x0a, 0x0b, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x4a, 0x6f, 0x62, 0x12, 0x1a, + 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x64, 0x2e, 0x43, 0x6f, + 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x4a, 0x6f, 0x62, 0x1a, 0x13, 0x2e, 0x70, 0x72, 0x6f, + 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x64, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x42, + 0x2b, 0x5a, 0x29, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x63, 0x6f, + 0x64, 0x65, 0x72, 0x2f, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2f, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, + 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x64, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, + 0x6f, 0x74, 0x6f, 0x33, } var ( @@ -1089,42 +1086,42 @@ func file_provisionerd_proto_provisionerd_proto_rawDescGZIP() []byte { var file_provisionerd_proto_provisionerd_proto_enumTypes = make([]protoimpl.EnumInfo, 1) var file_provisionerd_proto_provisionerd_proto_msgTypes = make([]protoimpl.MessageInfo, 13) var file_provisionerd_proto_provisionerd_proto_goTypes = []interface{}{ - (LogSource)(0), // 0: provisionerd.LogSource - (*Empty)(nil), // 1: provisionerd.Empty - (*AcquiredJob)(nil), // 2: provisionerd.AcquiredJob - (*FailedJob)(nil), // 3: provisionerd.FailedJob - (*CompletedJob)(nil), // 4: provisionerd.CompletedJob - (*Log)(nil), // 5: provisionerd.Log - (*UpdateJobRequest)(nil), // 6: provisionerd.UpdateJobRequest - (*UpdateJobResponse)(nil), // 7: provisionerd.UpdateJobResponse - (*AcquiredJob_WorkspaceProvision)(nil), // 8: provisionerd.AcquiredJob.WorkspaceProvision - (*AcquiredJob_ProjectImport)(nil), // 9: provisionerd.AcquiredJob.ProjectImport - (*FailedJob_WorkspaceProvision)(nil), // 10: provisionerd.FailedJob.WorkspaceProvision - (*FailedJob_ProjectImport)(nil), // 11: provisionerd.FailedJob.ProjectImport - (*CompletedJob_WorkspaceProvision)(nil), // 12: provisionerd.CompletedJob.WorkspaceProvision - (*CompletedJob_ProjectImport)(nil), // 13: provisionerd.CompletedJob.ProjectImport - (proto.LogLevel)(0), // 14: provisioner.LogLevel - (*proto.ParameterSchema)(nil), // 15: provisioner.ParameterSchema - (*proto.ParameterValue)(nil), // 16: provisioner.ParameterValue - (*proto.Provision_Metadata)(nil), // 17: provisioner.Provision.Metadata - (*proto.Resource)(nil), // 18: provisioner.Resource + (LogSource)(0), // 0: provisionerd.LogSource + (*Empty)(nil), // 1: provisionerd.Empty + (*AcquiredJob)(nil), // 2: provisionerd.AcquiredJob + (*FailedJob)(nil), // 3: provisionerd.FailedJob + (*CompletedJob)(nil), // 4: provisionerd.CompletedJob + (*Log)(nil), // 5: provisionerd.Log + (*UpdateJobRequest)(nil), // 6: provisionerd.UpdateJobRequest + (*UpdateJobResponse)(nil), // 7: provisionerd.UpdateJobResponse + (*AcquiredJob_WorkspaceBuild)(nil), // 8: provisionerd.AcquiredJob.WorkspaceBuild + (*AcquiredJob_ProjectImport)(nil), // 9: provisionerd.AcquiredJob.ProjectImport + (*FailedJob_WorkspaceBuild)(nil), // 10: provisionerd.FailedJob.WorkspaceBuild + (*FailedJob_ProjectImport)(nil), // 11: provisionerd.FailedJob.ProjectImport + (*CompletedJob_WorkspaceBuild)(nil), // 12: provisionerd.CompletedJob.WorkspaceBuild + (*CompletedJob_ProjectImport)(nil), // 13: provisionerd.CompletedJob.ProjectImport + (proto.LogLevel)(0), // 14: provisioner.LogLevel + (*proto.ParameterSchema)(nil), // 15: provisioner.ParameterSchema + (*proto.ParameterValue)(nil), // 16: provisioner.ParameterValue + (*proto.Provision_Metadata)(nil), // 17: provisioner.Provision.Metadata + (*proto.Resource)(nil), // 18: provisioner.Resource } var file_provisionerd_proto_provisionerd_proto_depIdxs = []int32{ - 8, // 0: provisionerd.AcquiredJob.workspace_provision:type_name -> provisionerd.AcquiredJob.WorkspaceProvision + 8, // 0: provisionerd.AcquiredJob.workspace_build:type_name -> provisionerd.AcquiredJob.WorkspaceBuild 9, // 1: provisionerd.AcquiredJob.project_import:type_name -> provisionerd.AcquiredJob.ProjectImport - 10, // 2: provisionerd.FailedJob.workspace_provision:type_name -> provisionerd.FailedJob.WorkspaceProvision + 10, // 2: provisionerd.FailedJob.workspace_build:type_name -> provisionerd.FailedJob.WorkspaceBuild 11, // 3: provisionerd.FailedJob.project_import:type_name -> provisionerd.FailedJob.ProjectImport - 12, // 4: provisionerd.CompletedJob.workspace_provision:type_name -> provisionerd.CompletedJob.WorkspaceProvision + 12, // 4: provisionerd.CompletedJob.workspace_build:type_name -> provisionerd.CompletedJob.WorkspaceBuild 13, // 5: provisionerd.CompletedJob.project_import:type_name -> provisionerd.CompletedJob.ProjectImport 0, // 6: provisionerd.Log.source:type_name -> provisionerd.LogSource 14, // 7: provisionerd.Log.level:type_name -> provisioner.LogLevel 5, // 8: provisionerd.UpdateJobRequest.logs:type_name -> provisionerd.Log 15, // 9: provisionerd.UpdateJobRequest.parameter_schemas:type_name -> provisioner.ParameterSchema 16, // 10: provisionerd.UpdateJobResponse.parameter_values:type_name -> provisioner.ParameterValue - 16, // 11: provisionerd.AcquiredJob.WorkspaceProvision.parameter_values:type_name -> provisioner.ParameterValue - 17, // 12: provisionerd.AcquiredJob.WorkspaceProvision.metadata:type_name -> provisioner.Provision.Metadata + 16, // 11: provisionerd.AcquiredJob.WorkspaceBuild.parameter_values:type_name -> provisioner.ParameterValue + 17, // 12: provisionerd.AcquiredJob.WorkspaceBuild.metadata:type_name -> provisioner.Provision.Metadata 17, // 13: provisionerd.AcquiredJob.ProjectImport.metadata:type_name -> provisioner.Provision.Metadata - 18, // 14: provisionerd.CompletedJob.WorkspaceProvision.resources:type_name -> provisioner.Resource + 18, // 14: provisionerd.CompletedJob.WorkspaceBuild.resources:type_name -> provisioner.Resource 18, // 15: provisionerd.CompletedJob.ProjectImport.start_resources:type_name -> provisioner.Resource 18, // 16: provisionerd.CompletedJob.ProjectImport.stop_resources:type_name -> provisioner.Resource 1, // 17: provisionerd.ProvisionerDaemon.AcquireJob:input_type -> provisionerd.Empty @@ -1233,7 +1230,7 @@ func file_provisionerd_proto_provisionerd_proto_init() { } } file_provisionerd_proto_provisionerd_proto_msgTypes[7].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*AcquiredJob_WorkspaceProvision); i { + switch v := v.(*AcquiredJob_WorkspaceBuild); i { case 0: return &v.state case 1: @@ -1257,7 +1254,7 @@ func file_provisionerd_proto_provisionerd_proto_init() { } } file_provisionerd_proto_provisionerd_proto_msgTypes[9].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*FailedJob_WorkspaceProvision); i { + switch v := v.(*FailedJob_WorkspaceBuild); i { case 0: return &v.state case 1: @@ -1281,7 +1278,7 @@ func file_provisionerd_proto_provisionerd_proto_init() { } } file_provisionerd_proto_provisionerd_proto_msgTypes[11].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*CompletedJob_WorkspaceProvision); i { + switch v := v.(*CompletedJob_WorkspaceBuild); i { case 0: return &v.state case 1: @@ -1306,15 +1303,15 @@ func file_provisionerd_proto_provisionerd_proto_init() { } } file_provisionerd_proto_provisionerd_proto_msgTypes[1].OneofWrappers = []interface{}{ - (*AcquiredJob_WorkspaceProvision_)(nil), + (*AcquiredJob_WorkspaceBuild_)(nil), (*AcquiredJob_ProjectImport_)(nil), } file_provisionerd_proto_provisionerd_proto_msgTypes[2].OneofWrappers = []interface{}{ - (*FailedJob_WorkspaceProvision_)(nil), + (*FailedJob_WorkspaceBuild_)(nil), (*FailedJob_ProjectImport_)(nil), } file_provisionerd_proto_provisionerd_proto_msgTypes[3].OneofWrappers = []interface{}{ - (*CompletedJob_WorkspaceProvision_)(nil), + (*CompletedJob_WorkspaceBuild_)(nil), (*CompletedJob_ProjectImport_)(nil), } type x struct{} diff --git a/provisionerd/proto/provisionerd.proto b/provisionerd/proto/provisionerd.proto index 55c546ef7676b..150878c7d5b13 100644 --- a/provisionerd/proto/provisionerd.proto +++ b/provisionerd/proto/provisionerd.proto @@ -11,8 +11,8 @@ message Empty {} // AcquiredJob is returned when a provisioner daemon has a job locked. message AcquiredJob { - message WorkspaceProvision { - string workspace_history_id = 1; + message WorkspaceBuild { + string workspace_build_id = 1; string workspace_name = 2; repeated provisioner.ParameterValue parameter_values = 3; provisioner.Provision.Metadata metadata = 4; @@ -27,13 +27,13 @@ message AcquiredJob { string user_name = 4; bytes project_source_archive = 5; oneof type { - WorkspaceProvision workspace_provision = 6; + WorkspaceBuild workspace_build = 6; ProjectImport project_import = 7; } } message FailedJob { - message WorkspaceProvision { + message WorkspaceBuild { bytes state = 1; } message ProjectImport{ @@ -41,14 +41,14 @@ message FailedJob { string job_id = 1; string error = 2; oneof type { - WorkspaceProvision workspace_provision = 3; + WorkspaceBuild workspace_build = 3; ProjectImport project_import = 4; } } // CompletedJob is sent when the provisioner daemon completes a job. message CompletedJob { - message WorkspaceProvision { + message WorkspaceBuild { bytes state = 1; repeated provisioner.Resource resources = 2; } @@ -58,7 +58,7 @@ message CompletedJob { } string job_id = 1; oneof type { - WorkspaceProvision workspace_provision = 2; + WorkspaceBuild workspace_build = 2; ProjectImport project_import = 3; } } diff --git a/provisionerd/provisionerd.go b/provisionerd/provisionerd.go index e7f2bd691a694..fcc409f799803 100644 --- a/provisionerd/provisionerd.go +++ b/provisionerd/provisionerd.go @@ -334,14 +334,14 @@ func (p *Server) runJob(ctx context.Context, job *proto.AcquiredJob) { p.opts.Logger.Debug(context.Background(), "acquired job is project import") p.runProjectImport(ctx, provisioner, job) - case *proto.AcquiredJob_WorkspaceProvision_: + case *proto.AcquiredJob_WorkspaceBuild_: p.opts.Logger.Debug(context.Background(), "acquired job is workspace provision", - slog.F("workspace_name", jobType.WorkspaceProvision.WorkspaceName), - slog.F("state_length", len(jobType.WorkspaceProvision.State)), - slog.F("parameters", jobType.WorkspaceProvision.ParameterValues), + slog.F("workspace_name", jobType.WorkspaceBuild.WorkspaceName), + slog.F("state_length", len(jobType.WorkspaceBuild.State)), + slog.F("parameters", jobType.WorkspaceBuild.ParameterValues), ) - p.runWorkspaceProvision(ctx, provisioner, job) + p.runWorkspaceBuild(ctx, provisioner, job) default: p.failActiveJobf("unknown job type %q; ensure your provisioner daemon is up-to-date", reflect.TypeOf(job.Type).String()) return @@ -513,12 +513,12 @@ func (p *Server) runProjectImportProvision(ctx context.Context, provisioner sdkp } } -func (p *Server) runWorkspaceProvision(ctx context.Context, provisioner sdkproto.DRPCProvisionerClient, job *proto.AcquiredJob) { +func (p *Server) runWorkspaceBuild(ctx context.Context, provisioner sdkproto.DRPCProvisionerClient, job *proto.AcquiredJob) { stream, err := provisioner.Provision(ctx, &sdkproto.Provision_Request{ Directory: p.opts.WorkDirectory, - ParameterValues: job.GetWorkspaceProvision().ParameterValues, - Metadata: job.GetWorkspaceProvision().Metadata, - State: job.GetWorkspaceProvision().State, + ParameterValues: job.GetWorkspaceBuild().ParameterValues, + Metadata: job.GetWorkspaceBuild().Metadata, + State: job.GetWorkspaceBuild().State, }) if err != nil { p.failActiveJobf("provision: %s", err) @@ -537,7 +537,7 @@ func (p *Server) runWorkspaceProvision(ctx context.Context, provisioner sdkproto p.opts.Logger.Debug(context.Background(), "workspace provision job logged", slog.F("level", msgType.Log.Level), slog.F("output", msgType.Log.Output), - slog.F("workspace_history_id", job.GetWorkspaceProvision().WorkspaceHistoryId), + slog.F("workspace_build_id", job.GetWorkspaceBuild().WorkspaceBuildId), ) _, err = p.client.UpdateJob(ctx, &proto.UpdateJobRequest{ @@ -561,8 +561,8 @@ func (p *Server) runWorkspaceProvision(ctx context.Context, provisioner sdkproto p.failActiveJob(&proto.FailedJob{ Error: msgType.Complete.Error, - Type: &proto.FailedJob_WorkspaceProvision_{ - WorkspaceProvision: &proto.FailedJob_WorkspaceProvision{ + Type: &proto.FailedJob_WorkspaceBuild_{ + WorkspaceBuild: &proto.FailedJob_WorkspaceBuild{ State: msgType.Complete.State, }, }, @@ -580,8 +580,8 @@ func (p *Server) runWorkspaceProvision(ctx context.Context, provisioner sdkproto // When we reconnect we can flush any of these cached values. _, err = p.client.CompleteJob(ctx, &proto.CompletedJob{ JobId: job.JobId, - Type: &proto.CompletedJob_WorkspaceProvision_{ - WorkspaceProvision: &proto.CompletedJob_WorkspaceProvision{ + Type: &proto.CompletedJob_WorkspaceBuild_{ + WorkspaceBuild: &proto.CompletedJob_WorkspaceBuild{ State: msgType.Complete.State, Resources: msgType.Complete.Resources, }, diff --git a/provisionerd/provisionerd_test.go b/provisionerd/provisionerd_test.go index 1eac1e77b49c4..f1af2f8607f84 100644 --- a/provisionerd/provisionerd_test.go +++ b/provisionerd/provisionerd_test.go @@ -294,7 +294,7 @@ func TestProvisionerd(t *testing.T) { require.NoError(t, closer.Close()) }) - t.Run("WorkspaceProvision", func(t *testing.T) { + t.Run("WorkspaceBuild", func(t *testing.T) { t.Parallel() var ( didComplete atomic.Bool @@ -316,8 +316,8 @@ func TestProvisionerd(t *testing.T) { ProjectSourceArchive: createTar(t, map[string]string{ "test.txt": "content", }), - Type: &proto.AcquiredJob_WorkspaceProvision_{ - WorkspaceProvision: &proto.AcquiredJob_WorkspaceProvision{ + Type: &proto.AcquiredJob_WorkspaceBuild_{ + WorkspaceBuild: &proto.AcquiredJob_WorkspaceBuild{ Metadata: &sdkproto.Provision_Metadata{}, }, }, @@ -363,7 +363,7 @@ func TestProvisionerd(t *testing.T) { require.NoError(t, closer.Close()) }) - t.Run("WorkspaceProvisionFailComplete", func(t *testing.T) { + t.Run("WorkspaceBuildFailComplete", func(t *testing.T) { t.Parallel() var ( didFail atomic.Bool @@ -384,8 +384,8 @@ func TestProvisionerd(t *testing.T) { ProjectSourceArchive: createTar(t, map[string]string{ "test.txt": "content", }), - Type: &proto.AcquiredJob_WorkspaceProvision_{ - WorkspaceProvision: &proto.AcquiredJob_WorkspaceProvision{ + Type: &proto.AcquiredJob_WorkspaceBuild_{ + WorkspaceBuild: &proto.AcquiredJob_WorkspaceBuild{ Metadata: &sdkproto.Provision_Metadata{}, }, }, @@ -430,8 +430,8 @@ func TestProvisionerd(t *testing.T) { ProjectSourceArchive: createTar(t, map[string]string{ "test.txt": "content", }), - Type: &proto.AcquiredJob_WorkspaceProvision_{ - WorkspaceProvision: &proto.AcquiredJob_WorkspaceProvision{ + Type: &proto.AcquiredJob_WorkspaceBuild_{ + WorkspaceBuild: &proto.AcquiredJob_WorkspaceBuild{ Metadata: &sdkproto.Provision_Metadata{}, }, }, diff --git a/provisionersdk/agent.go b/provisionersdk/agent.go index 7d97dd38ce70a..2f2a3a74dc68e 100644 --- a/provisionersdk/agent.go +++ b/provisionersdk/agent.go @@ -18,6 +18,7 @@ var ( $ProgressPreference = "SilentlyContinue" $ErrorActionPreference = "Stop" Invoke-WebRequest -Uri ${DOWNLOAD_URL} -OutFile $env:TEMP\coder.exe +$env:CODER_URL = "${ACCESS_URL}" Start-Process -FilePath $env:TEMP\coder.exe workspaces agent `, }, @@ -28,6 +29,7 @@ set -eu pipefail BINARY_LOCATION=$(mktemp -d)/coder curl -fsSL ${DOWNLOAD_URL} -o $BINARY_LOCATION chmod +x $BINARY_LOCATION +export CODER_URL="${ACCESS_URL}" exec $BINARY_LOCATION agent `, }, @@ -38,6 +40,7 @@ set -eu pipefail BINARY_LOCATION=$(mktemp -d)/coder curl -fsSL ${DOWNLOAD_URL} -o $BINARY_LOCATION chmod +x $BINARY_LOCATION +export CODER_URL="${ACCESS_URL}" exec $BINARY_LOCATION agent `, }, @@ -63,9 +66,16 @@ func AgentScript(coderURL *url.URL, operatingSystem, architecture string) (strin } return "", xerrors.Errorf("architecture %q not supported for %q. must be in: %v", architecture, operatingSystem, list) } - parsed, err := coderURL.Parse(fmt.Sprintf("/bin/coder-%s-%s", operatingSystem, architecture)) + downloadURL, err := coderURL.Parse(fmt.Sprintf("/bin/coder-%s-%s", operatingSystem, architecture)) if err != nil { - return "", xerrors.Errorf("parse url: %w", err) + return "", xerrors.Errorf("parse download url: %w", err) } - return strings.ReplaceAll(script, "${DOWNLOAD_URL}", parsed.String()), nil + accessURL, err := coderURL.Parse("/") + if err != nil { + return "", xerrors.Errorf("parse access url: %w", err) + } + return strings.NewReplacer( + "${DOWNLOAD_URL}", downloadURL.String(), + "${ACCESS_URL}", accessURL.String(), + ).Replace(script), nil } diff --git a/site/api.ts b/site/api.ts index 5339ccd15d851..febeee6c96dd1 100644 --- a/site/api.ts +++ b/site/api.ts @@ -108,7 +108,7 @@ export namespace Workspace { } export const login = async (email: string, password: string): Promise => { - const response = await fetch("/api/v2/login", { + const response = await fetch("/api/v2/users/login", { method: "POST", headers: { "Content-Type": "application/json", @@ -128,7 +128,7 @@ export const login = async (email: string, password: string): Promise => { - const response = await fetch("/api/v2/logout", { + const response = await fetch("/api/v2/users/logout", { method: "POST", }) diff --git a/site/e2e/globalSetup.ts b/site/e2e/globalSetup.ts index 192b06156bd6c..7a63071bc6415 100644 --- a/site/e2e/globalSetup.ts +++ b/site/e2e/globalSetup.ts @@ -11,7 +11,7 @@ const globalSetup = async (config: FullConfig): Promise => { }) // Create initial user - await context.post("/api/v2/user", { + await context.post("/api/v2/users/first", { data: { email, username, diff --git a/site/pages/projects/index.tsx b/site/pages/projects/index.tsx index 22bcee5e470c8..a5a69af37d9e6 100644 --- a/site/pages/projects/index.tsx +++ b/site/pages/projects/index.tsx @@ -18,8 +18,10 @@ import { CodeExample } from "../../components/CodeExample/CodeExample" const ProjectsPage: React.FC = () => { const styles = useStyles() const { me, signOut } = useUser(true) - const { data: projects, error } = useSWR("/api/v2/projects") const { data: orgs, error: orgsError } = useSWR("/api/v2/users/me/organizations") + const { data: projects, error } = useSWR( + orgs ? `/api/v2/organizations/${orgs[0].id}/projects` : null, + ) if (error) { return