From 051eddab13d6056cb9ceed77e9d884fb36df5298 Mon Sep 17 00:00:00 2001 From: Kyle Carberry Date: Thu, 3 Mar 2022 16:02:23 +0000 Subject: [PATCH 01/22] Add client for agent --- agent/agent.go | 8 +- agent/agent_test.go | 8 +- cli/workspaceagent.go | 56 ++++++++ coderd/cmd/root.go | 2 +- coderd/coderd.go | 13 +- coderd/coderdtest/coderdtest.go | 2 +- coderd/projectimport.go | 26 ---- coderd/provisionerdaemons.go | 9 +- coderd/provisionerjobs.go | 76 +++++++++++ coderd/workspaceagent.go | 150 +++++++++------------ coderd/workspaceagent_test.go | 174 ++++++------------------ coderd/workspaceagentauth.go | 147 +++++++++++++++++++++ coderd/workspaceagentauth_test.go | 182 ++++++++++++++++++++++++++ codersdk/client.go | 14 ++ codersdk/provisioners.go | 4 +- codersdk/provisioners_test.go | 4 +- codersdk/workspaceagent.go | 82 ++++++++++++ codersdk/workspaces.go | 13 ++ database/databasefake/databasefake.go | 13 ++ database/querier.go | 1 + database/query.sql | 8 ++ database/query.sql.go | 27 ++++ go.mod | 8 +- go.sum | 33 +++++ httpmw/workspaceagent.go | 65 +++++++++ httpmw/workspaceagent_test.go | 73 +++++++++++ httpmw/workspaceresourceparam.go | 64 +++++++++ httpmw/workspaceresourceparam_test.go | 109 +++++++++++++++ peerbroker/listen.go | 7 +- provisionersdk/agent.go | 16 ++- 30 files changed, 1124 insertions(+), 270 deletions(-) create mode 100644 cli/workspaceagent.go create mode 100644 coderd/workspaceagentauth.go create mode 100644 coderd/workspaceagentauth_test.go create mode 100644 httpmw/workspaceagent.go create mode 100644 httpmw/workspaceagent_test.go create mode 100644 httpmw/workspaceresourceparam.go create mode 100644 httpmw/workspaceresourceparam_test.go diff --git a/agent/agent.go b/agent/agent.go index 285efe3dc9836..d8dba42a47b0b 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,7 +75,7 @@ func New(dialer Dialer, options *Options) io.Closer { type server struct { clientDialer Dialer - options *Options + options *peer.ConnOptions closeCancel context.CancelFunc closeMutex sync.Mutex @@ -249,7 +249,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 diff --git a/agent/agent_test.go b/agent/agent_test.go index 662c054eae146..825600a47931e 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, &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, opts) + }, &peer.ConnOptions{ Logger: slogtest.Make(t, nil).Leveled(slog.LevelDebug), }) t.Cleanup(func() { diff --git a/cli/workspaceagent.go b/cli/workspaceagent.go new file mode 100644 index 0000000000000..75a462c0a100e --- /dev/null +++ b/cli/workspaceagent.go @@ -0,0 +1,56 @@ +package cli + +import ( + "net/url" + "os" + + "github.com/coder/coder/agent" + "github.com/coder/coder/codersdk" + "github.com/powersj/whatsthis/pkg/cloud" + "github.com/spf13/cobra" + "golang.org/x/xerrors" +) + +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.AuthenticateWorkspaceAgentUsingGoogleCloudIdentity(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.WorkspaceAgentServe, nil) + <-cmd.Context().Done() + return closer.Close() + }, + } +} diff --git a/coderd/cmd/root.go b/coderd/cmd/root.go index 162390898aa77..c5acf3bba3bd0 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.ProvisionerDaemonServe, &provisionerd.Options{ Logger: logger, PollInterval: 50 * time.Millisecond, UpdateInterval: 50 * time.Millisecond, diff --git a/coderd/coderd.go b/coderd/coderd.go index 69a8432aa53bf..90a34787404a1 100644 --- a/coderd/coderd.go +++ b/coderd/coderd.go @@ -116,6 +116,10 @@ func New(options *Options) (http.Handler, func()) { r.Route("/authenticate", func(r chi.Router) { r.Post("/google-instance-identity", api.postAuthenticateWorkspaceAgentUsingGoogleInstanceIdentity) }) + r.Group(func(r chi.Router) { + r.Use(httpmw.ExtractWorkspaceAgent(options.Database)) + r.Get("/serve", api.workspaceAgentServe) + }) }) r.Route("/upload", func(r chi.Router) { @@ -134,7 +138,7 @@ func New(options *Options) (http.Handler, func()) { r.Get("/", api.provisionerJobByID) r.Get("/schemas", api.projectImportJobSchemasByID) r.Get("/parameters", api.projectImportJobParametersByID) - r.Get("/resources", api.projectImportJobResourcesByID) + r.Get("/resources", api.provisionerJobResourcesByID) r.Get("/logs", api.provisionerJobLogsByID) }) }) @@ -148,6 +152,13 @@ func New(options *Options) (http.Handler, func()) { r.Use(httpmw.ExtractProvisionerJobParam(options.Database)) r.Get("/", api.provisionerJobByID) r.Get("/logs", api.provisionerJobLogsByID) + r.Route("/resources", func(r chi.Router) { + r.Get("/", api.provisionerJobResourcesByID) + r.Route("/{workspaceresource}", func(r chi.Router) { + r.Use(httpmw.ExtractWorkspaceResourceParam(options.Database)) + r.Get("/agent", api.workspaceAgentConnectByResource) + }) + }) }) }) diff --git a/coderd/coderdtest/coderdtest.go b/coderd/coderdtest/coderdtest.go index 58dd9ceb53292..905bd85b879e2 100644 --- a/coderd/coderdtest/coderdtest.go +++ b/coderd/coderdtest/coderdtest.go @@ -125,7 +125,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.ProvisionerDaemonServe, &provisionerd.Options{ Logger: slogtest.Make(t, nil).Named("provisionerd").Leveled(slog.LevelDebug), PollInterval: 50 * time.Millisecond, UpdateInterval: 50 * time.Millisecond, diff --git a/coderd/projectimport.go b/coderd/projectimport.go index 5ece718c99a90..9955e01ba3319 100644 --- a/coderd/projectimport.go +++ b/coderd/projectimport.go @@ -154,29 +154,3 @@ func (api *api) projectImportJobParametersByID(rw http.ResponseWriter, r *http.R 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/provisionerdaemons.go b/coderd/provisionerdaemons.go index d30b876b99547..b416ff1f66c93 100644 --- a/coderd/provisionerdaemons.go +++ b/coderd/provisionerdaemons.go @@ -590,12 +590,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{ 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/provisionerjobs.go b/coderd/provisionerjobs.go index 8c343256f15ee..e31f187d15365 100644 --- a/coderd/provisionerjobs.go +++ b/coderd/provisionerjobs.go @@ -12,6 +12,7 @@ import ( "github.com/go-chi/render" "github.com/google/uuid" + "golang.org/x/xerrors" "cdr.dev/slog" @@ -64,6 +65,7 @@ type ProvisionerJobResource struct { Transition database.WorkspaceTransition `json:"workspace_transition"` Type string `json:"type"` Name string `json:"name"` + Agent *ProvisionerJobAgent `json:"agent,omitempty"` } type ProvisionerJobAgent struct { @@ -238,6 +240,49 @@ func (api *api) provisionerJobLogsByID(rw http.ResponseWriter, r *http.Request) } } +func (api *api) provisionerJobResourcesByID(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 err != nil { + httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ + Message: fmt.Sprintf("get provisioner job resources: %s", err), + }) + return + } + apiResources := make([]ProvisionerJobResource, 0) + for _, resource := range resources { + if !resource.AgentID.Valid { + apiResources = append(apiResources, convertProvisionerJobResource(resource, nil)) + continue + } + // TODO: This should be combined. + agents, err := api.Database.GetProvisionerJobAgentsByResourceIDs(r.Context(), []uuid.UUID{resource.ID}) + if err != nil { + httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ + Message: fmt.Sprintf("get provisioner job agent: %s", err), + }) + return + } + agent := agents[0] + apiAgent, err := convertProvisionerJobAgent(agent) + if err != nil { + httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ + Message: fmt.Sprintf("convert provisioner job agent: %s", err), + }) + return + } + apiResources = append(apiResources, convertProvisionerJobResource(resource, &apiAgent)) + } + render.Status(r, http.StatusOK) + render.JSON(rw, r, apiResources) +} + func convertProvisionerJobLog(provisionerJobLog database.ProvisionerJobLog) ProvisionerJobLog { return ProvisionerJobLog{ ID: provisionerJobLog.ID, @@ -291,6 +336,37 @@ func convertProvisionerJob(provisionerJob database.ProvisionerJob) ProvisionerJo return job } +func convertProvisionerJobResource(resource database.ProvisionerJobResource, agent *ProvisionerJobAgent) ProvisionerJobResource { + return ProvisionerJobResource{ + ID: resource.ID, + CreatedAt: resource.CreatedAt, + JobID: resource.JobID, + Transition: resource.Transition, + Type: resource.Type, + Name: resource.Name, + Agent: agent, + } +} + +func convertProvisionerJobAgent(agent database.ProvisionerJobAgent) (ProvisionerJobAgent, error) { + var envs map[string]string + if agent.EnvironmentVariables.Valid { + err := json.Unmarshal(agent.EnvironmentVariables.RawMessage, &envs) + if err != nil { + return ProvisionerJobAgent{}, xerrors.Errorf("unmarshal: %w", err) + } + } + return ProvisionerJobAgent{ + ID: agent.ID, + CreatedAt: agent.CreatedAt, + UpdatedAt: agent.UpdatedAt.Time, + ResourceID: agent.ResourceID, + InstanceID: agent.AuthInstanceID.String, + StartupScript: agent.StartupScript.String, + EnvironmentVariables: envs, + }, nil +} + func provisionerJobLogsChannel(jobID uuid.UUID) string { return fmt.Sprintf("provisioner-log-logs:%s", jobID) } diff --git a/coderd/workspaceagent.go b/coderd/workspaceagent.go index 2e45046fa3ff0..4ebc77b100bd1 100644 --- a/coderd/workspaceagent.go +++ b/coderd/workspaceagent.go @@ -1,127 +1,103 @@ package coderd import ( - "database/sql" - "encoding/json" - "errors" "fmt" + "io" "net/http" - "github.com/go-chi/render" + "github.com/google/uuid" + "github.com/hashicorp/yamux" + "nhooyr.io/websocket" - "github.com/coder/coder/database" "github.com/coder/coder/httpapi" - - "github.com/mitchellh/mapstructure" + "github.com/coder/coder/httpmw" + "github.com/coder/coder/peerbroker" + "github.com/coder/coder/peerbroker/proto" + "github.com/coder/coder/provisionersdk" ) -type GoogleInstanceIdentityToken struct { - JSONWebToken string `json:"json_web_token" validate:"required"` -} +func (api *api) workspaceAgentConnectByResource(rw http.ResponseWriter, r *http.Request) { + api.websocketWaitGroup.Add(1) + defer api.websocketWaitGroup.Done() -// WorkspaceAgentAuthenticateResponse is returned when an instance ID -// has been exchanged for a session token. -type WorkspaceAgentAuthenticateResponse struct { - SessionToken string `json:"session_token"` -} - -// 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) { - var req GoogleInstanceIdentityToken - if !httpapi.Read(rw, r, &req) { - return - } - - // We leave the audience blank. It's not important we validate who made the token. - payload, err := api.GoogleTokenValidator.Validate(r.Context(), req.JSONWebToken, "") - if err != nil { - httpapi.Write(rw, http.StatusUnauthorized, httpapi.Response{ - Message: fmt.Sprintf("validate: %s", err), - }) - return - } - claims := struct { - Google struct { - ComputeEngine struct { - InstanceID string `mapstructure:"instance_id"` - } `mapstructure:"compute_engine"` - } `mapstructure:"google"` - }{} - err = mapstructure.Decode(payload.Claims, &claims) - if err != nil { + resource := httpmw.WorkspaceResourceParam(r) + if !resource.AgentID.Valid { httpapi.Write(rw, http.StatusBadRequest, httpapi.Response{ - Message: fmt.Sprintf("decode jwt claims: %s", err), - }) - return - } - agent, err := api.Database.GetProvisionerJobAgentByInstanceID(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), + Message: "resource doesn't have an agent", }) return } + agents, err := api.Database.GetProvisionerJobAgentsByResourceIDs(r.Context(), []uuid.UUID{resource.ID}) if err != nil { - httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ + httpapi.Write(rw, http.StatusBadRequest, httpapi.Response{ Message: fmt.Sprintf("get provisioner job agent: %s", err), }) return } - resource, err := api.Database.GetProvisionerJobResourceByID(r.Context(), agent.ResourceID) + agent := agents[0] + conn, err := websocket.Accept(rw, r, &websocket.AcceptOptions{ + CompressionMode: websocket.CompressionDisabled, + }) if err != nil { - httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ - Message: fmt.Sprintf("get provisioner job resource: %s", err), + httpapi.Write(rw, http.StatusBadRequest, httpapi.Response{ + Message: fmt.Sprintf("accept websocket: %s", err), }) return } - job, err := api.Database.GetProvisionerJobByID(r.Context(), resource.JobID) + 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 { - httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ - Message: fmt.Sprintf("get provisioner job: %s", err), - }) - return - } - if job.Type != database.ProvisionerJobTypeWorkspaceProvision { - httpapi.Write(rw, http.StatusBadRequest, httpapi.Response{ - Message: fmt.Sprintf("%q jobs cannot be authenticated", job.Type), - }) + _ = conn.Close(websocket.StatusAbnormalClosure, err.Error()) return } - var jobData workspaceProvisionJob - err = json.Unmarshal(job.Input, &jobData) + 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 { - httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ - Message: fmt.Sprintf("extract job data: %s", err), - }) + _ = conn.Close(websocket.StatusInternalError, fmt.Sprintf("serve: %s", err)) return } - resourceHistory, err := api.Database.GetWorkspaceHistoryByID(r.Context(), jobData.WorkspaceHistoryID) +} + +func (api *api) workspaceAgentServe(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.StatusInternalServerError, httpapi.Response{ - Message: fmt.Sprintf("get workspace history: %s", err), + httpapi.Write(rw, http.StatusBadRequest, httpapi.Response{ + Message: fmt.Sprintf("accept websocket: %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) + 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 { - httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ - Message: fmt.Sprintf("get latest workspace history: %s", err), - }) + _ = conn.Close(websocket.StatusAbnormalClosure, err.Error()) return } - if latestHistory.ID.String() != resourceHistory.ID.String() { - httpapi.Write(rw, http.StatusBadRequest, httpapi.Response{ - Message: fmt.Sprintf("resource found for id %q, but isn't registered on the latest history", claims.Google.ComputeEngine.InstanceID), - }) + 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 } - render.Status(r, http.StatusOK) - render.JSON(rw, r, WorkspaceAgentAuthenticateResponse{ - SessionToken: agent.AuthToken.String(), - }) + defer closer.Close() + <-session.CloseChan() } diff --git a/coderd/workspaceagent_test.go b/coderd/workspaceagent_test.go index c48dfc75af1d1..3737544f508ca 100644 --- a/coderd/workspaceagent_test.go +++ b/coderd/workspaceagent_test.go @@ -1,86 +1,45 @@ package coderd_test import ( - "bytes" "context" - "crypto/rand" - "crypto/rsa" - "encoding/base64" - "encoding/json" - "io/ioutil" - "math/big" - "net/http" "testing" "time" - "cloud.google.com/go/compute/metadata" - "github.com/golang-jwt/jwt" - "github.com/stretchr/testify/require" - "google.golang.org/api/idtoken" - "google.golang.org/api/option" - + "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/cryptorand" "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" + "github.com/google/uuid" + "github.com/stretchr/testify/require" ) -func TestPostWorkspaceAgentAuthenticateGoogleInstanceIdentity(t *testing.T) { +func TestWorkspaceAgentServe(t *testing.T) { t.Parallel() - t.Run("Expired", func(t *testing.T) { - t.Parallel() - instanceID := "instanceidentifier" - signedKey, keyID, privateKey := createSignedToken(t, instanceID, &jwt.MapClaims{}) - validator := createValidator(t, keyID, privateKey) - client := coderdtest.New(t, &coderdtest.Options{ - GoogleTokenValidator: validator, - }) - _, err := client.AuthenticateWorkspaceAgentUsingGoogleCloudIdentity(context.Background(), "", createMetadataClient(signedKey)) - var apiErr *codersdk.Error - require.ErrorAs(t, err, &apiErr) - require.Equal(t, http.StatusUnauthorized, apiErr.StatusCode()) - }) - - t.Run("InstanceNotFound", func(t *testing.T) { - t.Parallel() - instanceID := "instanceidentifier" - signedKey, keyID, privateKey := createSignedToken(t, instanceID, nil) - validator := createValidator(t, keyID, privateKey) - client := coderdtest.New(t, &coderdtest.Options{ - GoogleTokenValidator: validator, - }) - _, err := client.AuthenticateWorkspaceAgentUsingGoogleCloudIdentity(context.Background(), "", createMetadataClient(signedKey)) - var apiErr *codersdk.Error - require.ErrorAs(t, err, &apiErr) - require.Equal(t, http.StatusNotFound, apiErr.StatusCode()) - }) - t.Run("Success", func(t *testing.T) { t.Parallel() - instanceID := "instanceidentifier" - signedKey, keyID, privateKey := createSignedToken(t, instanceID, nil) - validator := createValidator(t, keyID, privateKey) - client := coderdtest.New(t, &coderdtest.Options{ - GoogleTokenValidator: validator, - }) + client := coderdtest.New(t, nil) user := coderdtest.CreateInitialUser(t, client) - coderdtest.NewProvisionerDaemon(t, client) + daemonCloser := coderdtest.NewProvisionerDaemon(t, client) + authToken := uuid.NewString() 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: "somename", - Type: "someinstance", + Name: "example", + Type: "aws_instance", Agent: &proto.Agent{ - Auth: &proto.Agent_GoogleInstanceIdentity{ - GoogleInstanceIdentity: &proto.GoogleInstanceIdentityAuth{ - InstanceId: instanceID, - }, + Id: uuid.NewString(), + Auth: &proto.Agent_Token{ + Token: authToken, }, }, }}, @@ -97,86 +56,33 @@ func TestPostWorkspaceAgentAuthenticateGoogleInstanceIdentity(t *testing.T) { }) require.NoError(t, err) coderdtest.AwaitWorkspaceProvisionJob(t, client, user.Organization, firstHistory.ProvisionJobID) - - _, err = client.AuthenticateWorkspaceAgentUsingGoogleCloudIdentity(context.Background(), "", createMetadataClient(signedKey)) + daemonCloser.Close() + resources, err := client.WorkspaceProvisionJobResources(context.Background(), user.Organization, firstHistory.ProvisionJobID) require.NoError(t, err) - }) -} + require.Len(t, resources, 1) -// Used to easily create an HTTP transport! -type roundTripper func(req *http.Request) (*http.Response, error) - -func (r roundTripper) RoundTrip(req *http.Request) (*http.Response, error) { - return r(req) -} - -// Create's a new Google metadata client to authenticate. -func createMetadataClient(signedKey string) *metadata.Client { - return metadata.NewClient(&http.Client{ - Transport: roundTripper(func(r *http.Request) (*http.Response, error) { - return &http.Response{ - StatusCode: http.StatusOK, - Body: ioutil.NopCloser(bytes.NewReader([]byte(signedKey))), - Header: make(http.Header), - }, nil - }), - }) -} + agentClient := codersdk.New(client.URL) + agentClient.SessionToken = authToken + agentCloser := agent.New(agentClient.WorkspaceAgentServe, &peer.ConnOptions{ + Logger: slogtest.Make(t, nil), + }) -// Create's a signed JWT with a randomly generated private key. -func createSignedToken(t *testing.T, instanceID string, claims *jwt.MapClaims) (signedKey string, keyID string, privateKey *rsa.PrivateKey) { - keyID, err := cryptorand.String(12) - require.NoError(t, err) - if claims == nil { - claims = &jwt.MapClaims{ - "exp": time.Now().AddDate(1, 0, 0).Unix(), - "google": map[string]interface{}{ - "compute_engine": map[string]string{ - "instance_id": instanceID, - }, - }, - } - } - token := jwt.NewWithClaims(jwt.SigningMethodRS256, claims) - token.Header["kid"] = keyID - privateKey, err = rsa.GenerateKey(rand.Reader, 2048) - require.NoError(t, err) - signedKey, err = token.SignedString(privateKey) - require.NoError(t, err) - return signedKey, keyID, privateKey -} + time.Sleep(time.Millisecond * 250) -// Create's a validator that verifies against the provided private key. -// In a production scenario, the validator calls against the Google OAuth API -// to obtain certificates. -func createValidator(t *testing.T, keyID string, privateKey *rsa.PrivateKey) *idtoken.Validator { - // Taken from: https://github.com/googleapis/google-api-go-client/blob/4bb729045d611fa77bdbeb971f6a1204ba23161d/idtoken/validate.go#L57-L75 - type jwk struct { - Kid string `json:"kid"` - N string `json:"n"` - E string `json:"e"` - } - type certResponse struct { - Keys []jwk `json:"keys"` - } + workspaceClient, err := client.WorkspaceAgentConnect(context.Background(), user.Organization, firstHistory.ProvisionJobID, resources[0].ID) + require.NoError(t, err) + 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) + _, err = conn.Ping() + require.NoError(t, err) - validator, err := idtoken.NewValidator(context.Background(), option.WithHTTPClient(&http.Client{ - Transport: roundTripper(func(r *http.Request) (*http.Response, error) { - data, err := json.Marshal(certResponse{ - Keys: []jwk{{ - Kid: keyID, - N: base64.RawURLEncoding.EncodeToString(privateKey.N.Bytes()), - E: base64.RawURLEncoding.EncodeToString(new(big.Int).SetInt64(int64(privateKey.E)).Bytes()), - }}, - }) - require.NoError(t, err) - return &http.Response{ - StatusCode: http.StatusOK, - Body: ioutil.NopCloser(bytes.NewReader(data)), - Header: make(http.Header), - }, nil - }), - })) - require.NoError(t, err) - return validator + workspaceClient.DRPCConn().Close() + conn.Close() + stream.Close() + agentCloser.Close() + }) } diff --git a/coderd/workspaceagentauth.go b/coderd/workspaceagentauth.go new file mode 100644 index 0000000000000..3d1913c54eedd --- /dev/null +++ b/coderd/workspaceagentauth.go @@ -0,0 +1,147 @@ +package coderd + +import ( + "database/sql" + "encoding/json" + "errors" + "fmt" + "net/http" + + "github.com/go-chi/render" + + "github.com/coder/coder/database" + "github.com/coder/coder/httpapi" + + "github.com/mitchellh/mapstructure" +) + +type GoogleInstanceIdentityToken struct { + JSONWebToken string `json:"json_web_token" validate:"required"` +} + +// WorkspaceAgentAuthenticateResponse is returned when an instance ID +// has been exchanged for a session token. +type WorkspaceAgentAuthenticateResponse struct { + SessionToken string `json:"session_token"` +} + +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"` +} + +// 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) { + var req GoogleInstanceIdentityToken + if !httpapi.Read(rw, r, &req) { + return + } + + // We leave the audience blank. It's not important we validate who made the token. + payload, err := api.GoogleTokenValidator.Validate(r.Context(), req.JSONWebToken, "") + if err != nil { + httpapi.Write(rw, http.StatusUnauthorized, httpapi.Response{ + Message: fmt.Sprintf("validate: %s", err), + }) + return + } + claims := struct { + Google struct { + ComputeEngine struct { + InstanceID string `mapstructure:"instance_id"` + } `mapstructure:"compute_engine"` + } `mapstructure:"google"` + }{} + err = mapstructure.Decode(payload.Claims, &claims) + if err != nil { + httpapi.Write(rw, http.StatusBadRequest, httpapi.Response{ + Message: fmt.Sprintf("decode jwt claims: %s", err), + }) + return + } + agent, err := api.Database.GetProvisionerJobAgentByInstanceID(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), + }) + return + } + if err != nil { + httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ + Message: fmt.Sprintf("get provisioner job agent: %s", err), + }) + return + } + resource, err := api.Database.GetProvisionerJobResourceByID(r.Context(), agent.ResourceID) + if err != nil { + httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ + Message: fmt.Sprintf("get provisioner job resource: %s", err), + }) + return + } + job, err := api.Database.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.ProvisionerJobTypeWorkspaceProvision { + httpapi.Write(rw, http.StatusBadRequest, httpapi.Response{ + Message: fmt.Sprintf("%q jobs cannot be authenticated", job.Type), + }) + return + } + var jobData workspaceProvisionJob + err = json.Unmarshal(job.Input, &jobData) + if err != nil { + httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ + Message: fmt.Sprintf("extract job data: %s", err), + }) + return + } + resourceHistory, err := api.Database.GetWorkspaceHistoryByID(r.Context(), jobData.WorkspaceHistoryID) + if err != nil { + httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ + Message: fmt.Sprintf("get workspace history: %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) + if err != nil { + httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ + Message: fmt.Sprintf("get latest workspace history: %s", err), + }) + return + } + if latestHistory.ID.String() != resourceHistory.ID.String() { + httpapi.Write(rw, http.StatusBadRequest, httpapi.Response{ + Message: fmt.Sprintf("resource found for id %q, but isn't registered on the latest history", claims.Google.ComputeEngine.InstanceID), + }) + return + } + render.Status(r, http.StatusOK) + render.JSON(rw, r, WorkspaceAgentAuthenticateResponse{ + SessionToken: agent.AuthToken.String(), + }) +} diff --git a/coderd/workspaceagentauth_test.go b/coderd/workspaceagentauth_test.go new file mode 100644 index 0000000000000..c48dfc75af1d1 --- /dev/null +++ b/coderd/workspaceagentauth_test.go @@ -0,0 +1,182 @@ +package coderd_test + +import ( + "bytes" + "context" + "crypto/rand" + "crypto/rsa" + "encoding/base64" + "encoding/json" + "io/ioutil" + "math/big" + "net/http" + "testing" + "time" + + "cloud.google.com/go/compute/metadata" + "github.com/golang-jwt/jwt" + "github.com/stretchr/testify/require" + "google.golang.org/api/idtoken" + "google.golang.org/api/option" + + "github.com/coder/coder/coderd" + "github.com/coder/coder/coderd/coderdtest" + "github.com/coder/coder/codersdk" + "github.com/coder/coder/cryptorand" + "github.com/coder/coder/database" + "github.com/coder/coder/provisioner/echo" + "github.com/coder/coder/provisionersdk/proto" +) + +func TestPostWorkspaceAgentAuthenticateGoogleInstanceIdentity(t *testing.T) { + t.Parallel() + t.Run("Expired", func(t *testing.T) { + t.Parallel() + instanceID := "instanceidentifier" + signedKey, keyID, privateKey := createSignedToken(t, instanceID, &jwt.MapClaims{}) + validator := createValidator(t, keyID, privateKey) + client := coderdtest.New(t, &coderdtest.Options{ + GoogleTokenValidator: validator, + }) + _, err := client.AuthenticateWorkspaceAgentUsingGoogleCloudIdentity(context.Background(), "", createMetadataClient(signedKey)) + var apiErr *codersdk.Error + require.ErrorAs(t, err, &apiErr) + require.Equal(t, http.StatusUnauthorized, apiErr.StatusCode()) + }) + + t.Run("InstanceNotFound", func(t *testing.T) { + t.Parallel() + instanceID := "instanceidentifier" + signedKey, keyID, privateKey := createSignedToken(t, instanceID, nil) + validator := createValidator(t, keyID, privateKey) + client := coderdtest.New(t, &coderdtest.Options{ + GoogleTokenValidator: validator, + }) + _, err := client.AuthenticateWorkspaceAgentUsingGoogleCloudIdentity(context.Background(), "", createMetadataClient(signedKey)) + var apiErr *codersdk.Error + require.ErrorAs(t, err, &apiErr) + require.Equal(t, http.StatusNotFound, apiErr.StatusCode()) + }) + + t.Run("Success", func(t *testing.T) { + t.Parallel() + instanceID := "instanceidentifier" + signedKey, keyID, privateKey := createSignedToken(t, instanceID, nil) + validator := createValidator(t, keyID, privateKey) + client := coderdtest.New(t, &coderdtest.Options{ + GoogleTokenValidator: validator, + }) + 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: "somename", + Type: "someinstance", + Agent: &proto.Agent{ + Auth: &proto.Agent_GoogleInstanceIdentity{ + GoogleInstanceIdentity: &proto.GoogleInstanceIdentityAuth{ + InstanceId: instanceID, + }, + }, + }, + }}, + }, + }, + }}, + }) + 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) + + _, err = client.AuthenticateWorkspaceAgentUsingGoogleCloudIdentity(context.Background(), "", createMetadataClient(signedKey)) + require.NoError(t, err) + }) +} + +// Used to easily create an HTTP transport! +type roundTripper func(req *http.Request) (*http.Response, error) + +func (r roundTripper) RoundTrip(req *http.Request) (*http.Response, error) { + return r(req) +} + +// Create's a new Google metadata client to authenticate. +func createMetadataClient(signedKey string) *metadata.Client { + return metadata.NewClient(&http.Client{ + Transport: roundTripper(func(r *http.Request) (*http.Response, error) { + return &http.Response{ + StatusCode: http.StatusOK, + Body: ioutil.NopCloser(bytes.NewReader([]byte(signedKey))), + Header: make(http.Header), + }, nil + }), + }) +} + +// Create's a signed JWT with a randomly generated private key. +func createSignedToken(t *testing.T, instanceID string, claims *jwt.MapClaims) (signedKey string, keyID string, privateKey *rsa.PrivateKey) { + keyID, err := cryptorand.String(12) + require.NoError(t, err) + if claims == nil { + claims = &jwt.MapClaims{ + "exp": time.Now().AddDate(1, 0, 0).Unix(), + "google": map[string]interface{}{ + "compute_engine": map[string]string{ + "instance_id": instanceID, + }, + }, + } + } + token := jwt.NewWithClaims(jwt.SigningMethodRS256, claims) + token.Header["kid"] = keyID + privateKey, err = rsa.GenerateKey(rand.Reader, 2048) + require.NoError(t, err) + signedKey, err = token.SignedString(privateKey) + require.NoError(t, err) + return signedKey, keyID, privateKey +} + +// Create's a validator that verifies against the provided private key. +// In a production scenario, the validator calls against the Google OAuth API +// to obtain certificates. +func createValidator(t *testing.T, keyID string, privateKey *rsa.PrivateKey) *idtoken.Validator { + // Taken from: https://github.com/googleapis/google-api-go-client/blob/4bb729045d611fa77bdbeb971f6a1204ba23161d/idtoken/validate.go#L57-L75 + type jwk struct { + Kid string `json:"kid"` + N string `json:"n"` + E string `json:"e"` + } + type certResponse struct { + Keys []jwk `json:"keys"` + } + + validator, err := idtoken.NewValidator(context.Background(), option.WithHTTPClient(&http.Client{ + Transport: roundTripper(func(r *http.Request) (*http.Response, error) { + data, err := json.Marshal(certResponse{ + Keys: []jwk{{ + Kid: keyID, + N: base64.RawURLEncoding.EncodeToString(privateKey.N.Bytes()), + E: base64.RawURLEncoding.EncodeToString(new(big.Int).SetInt64(int64(privateKey.E)).Bytes()), + }}, + }) + require.NoError(t, err) + return &http.Response{ + StatusCode: http.StatusOK, + Body: ioutil.NopCloser(bytes.NewReader(data)), + Header: make(http.Header), + }, nil + }), + })) + require.NoError(t, err) + return validator +} 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/provisioners.go b/codersdk/provisioners.go index afef953beabb9..2cee00a626438 100644 --- a/codersdk/provisioners.go +++ b/codersdk/provisioners.go @@ -34,8 +34,8 @@ func (c *Client) ProvisionerDaemons(ctx context.Context) ([]coderd.ProvisionerDa 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) { +// ProvisionerDaemonServe returns the gRPC service for a provisioner daemon implementation. +func (c *Client) ProvisionerDaemonServe(ctx context.Context) (proto.DRPCProvisionerDaemonClient, error) { serverURL, err := c.URL.Parse("/api/v2/provisioners/daemons/serve") if err != nil { return nil, xerrors.Errorf("parse url: %w", err) diff --git a/codersdk/provisioners_test.go b/codersdk/provisioners_test.go index 9fbea9469303e..222a74dbac021 100644 --- a/codersdk/provisioners_test.go +++ b/codersdk/provisioners_test.go @@ -26,7 +26,7 @@ func TestProvisionerDaemonClient(t *testing.T) { t.Parallel() client := coderdtest.New(t, nil) ctx, cancelFunc := context.WithCancel(context.Background()) - daemon, err := client.ProvisionerDaemonClient(ctx) + daemon, err := client.ProvisionerDaemonServe(ctx) require.NoError(t, err) cancelFunc() _, err = daemon.AcquireJob(context.Background(), &proto.Empty{}) @@ -38,7 +38,7 @@ func TestProvisionerDaemonClient(t *testing.T) { client := coderdtest.New(t, nil) ctx, cancelFunc := context.WithCancel(context.Background()) defer cancelFunc() - daemon, err := client.ProvisionerDaemonClient(ctx) + daemon, err := client.ProvisionerDaemonServe(ctx) require.NoError(t, err) _, err = daemon.AcquireJob(ctx, &proto.Empty{}) require.NoError(t, err) diff --git a/codersdk/workspaceagent.go b/codersdk/workspaceagent.go index 7bfcab9202bfb..57507e7c578c6 100644 --- a/codersdk/workspaceagent.go +++ b/codersdk/workspaceagent.go @@ -4,12 +4,22 @@ import ( "context" "encoding/json" "fmt" + "io" "net/http" + "net/http/cookiejar" "cloud.google.com/go/compute/metadata" "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" + "github.com/google/uuid" + "github.com/hashicorp/yamux" ) // AuthenticateWorkspaceAgentUsingGoogleCloudIdentity uses the Google Compute Engine Metadata API to @@ -42,3 +52,75 @@ func (c *Client) AuthenticateWorkspaceAgentUsingGoogleCloudIdentity(ctx context. var resp coderd.WorkspaceAgentAuthenticateResponse return resp, json.NewDecoder(res.Body).Decode(&resp) } + +func (c *Client) WorkspaceAgentConnect(ctx context.Context, organization string, job, resource uuid.UUID) (proto.DRPCPeerBrokerClient, error) { + serverURL, err := c.URL.Parse(fmt.Sprintf("/api/v2/workspaceprovision/%s/%s/resources/%s/agent", organization, job.String(), 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 +} + +func (c *Client) WorkspaceAgentServe(ctx context.Context, opts *peer.ConnOptions) (*peerbroker.Listener, error) { + serverURL, err := c.URL.Parse("/api/v2/workspaceagent/serve") + 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, opts) +} diff --git a/codersdk/workspaces.go b/codersdk/workspaces.go index 28f926c518049..4e7a97e4e614c 100644 --- a/codersdk/workspaces.go +++ b/codersdk/workspaces.go @@ -157,3 +157,16 @@ func (c *Client) WorkspaceProvisionJobLogsBefore(ctx context.Context, organizati 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) } + +func (c *Client) WorkspaceProvisionJobResources(ctx context.Context, organization string, job uuid.UUID) ([]coderd.ProvisionerJobResource, error) { + res, err := c.request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/workspaceprovision/%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/database/databasefake/databasefake.go b/database/databasefake/databasefake.go index 1e09a2a74f4f9..ad4d791d42e07 100644 --- a/database/databasefake/databasefake.go +++ b/database/databasefake/databasefake.go @@ -508,6 +508,19 @@ func (q *fakeQuerier) GetProvisionerDaemons(_ context.Context) ([]database.Provi return q.provisionerDaemons, nil } +func (q *fakeQuerier) GetProvisionerJobAgentByAuthToken(_ context.Context, authToken uuid.UUID) (database.ProvisionerJobAgent, error) { + q.mutex.Lock() + defer q.mutex.Unlock() + + for i := len(q.provisionerJobAgent) - 1; i >= 0; i-- { + agent := q.provisionerJobAgent[i] + if agent.AuthToken.String() == authToken.String() { + return agent, nil + } + } + return database.ProvisionerJobAgent{}, sql.ErrNoRows +} + func (q *fakeQuerier) GetProvisionerJobAgentByInstanceID(_ context.Context, instanceID string) (database.ProvisionerJobAgent, error) { q.mutex.Lock() defer q.mutex.Unlock() diff --git a/database/querier.go b/database/querier.go index faa24ac3a4970..e6d33a1a4002a 100644 --- a/database/querier.go +++ b/database/querier.go @@ -26,6 +26,7 @@ type querier interface { GetProjectsByOrganizationIDs(ctx context.Context, ids []string) ([]Project, error) GetProvisionerDaemonByID(ctx context.Context, id uuid.UUID) (ProvisionerDaemon, error) GetProvisionerDaemons(ctx context.Context) ([]ProvisionerDaemon, error) + GetProvisionerJobAgentByAuthToken(ctx context.Context, authToken uuid.UUID) (ProvisionerJobAgent, 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) diff --git a/database/query.sql b/database/query.sql index eabea0c43d709..3b2574acb15f1 100644 --- a/database/query.sql +++ b/database/query.sql @@ -226,6 +226,14 @@ SELECT FROM provisioner_daemon; +-- name: GetProvisionerJobAgentByAuthToken :one +SELECT + * +FROM + provisioner_job_agent +WHERE + auth_token = $1; + -- name: GetProvisionerJobAgentByInstanceID :one SELECT * diff --git a/database/query.sql.go b/database/query.sql.go index c74ecbcbf94e5..52475635e0785 100644 --- a/database/query.sql.go +++ b/database/query.sql.go @@ -617,6 +617,33 @@ func (q *sqlQuerier) GetProvisionerDaemons(ctx context.Context) ([]ProvisionerDa return items, nil } +const getProvisionerJobAgentByAuthToken = `-- name: GetProvisionerJobAgentByAuthToken :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_token = $1 +` + +func (q *sqlQuerier) GetProvisionerJobAgentByAuthToken(ctx context.Context, authToken uuid.UUID) (ProvisionerJobAgent, error) { + row := q.db.QueryRowContext(ctx, getProvisionerJobAgentByAuthToken, authToken) + 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 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 diff --git a/go.mod b/go.mod index 1180eba9f01cc..077e969ae9c69 100644 --- a/go.mod +++ b/go.mod @@ -37,6 +37,7 @@ require ( github.com/hashicorp/go-version v1.4.0 github.com/hashicorp/terraform-config-inspect v0.0.0-20211115214459-90acf1ca460f github.com/hashicorp/terraform-exec v0.15.0 + github.com/hashicorp/terraform-json v0.13.0 github.com/hashicorp/terraform-plugin-sdk/v2 v2.10.1 github.com/hashicorp/yamux v0.0.0-20211028200310-0bc27b27de87 github.com/justinas/nosurf v1.1.1 @@ -52,7 +53,9 @@ 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/shirou/gopsutil v3.21.11+incompatible github.com/spf13/cobra v1.3.0 github.com/stretchr/testify v1.7.0 github.com/tabbed/pqtype v0.1.1 @@ -90,6 +93,7 @@ require ( github.com/docker/docker v20.10.12+incompatible // indirect github.com/docker/go-connections v0.4.0 // indirect github.com/docker/go-units v0.4.0 // indirect + github.com/go-ole/go-ole v1.2.6 // indirect github.com/go-playground/locales v0.14.0 // indirect github.com/go-playground/universal-translator v0.18.0 // indirect github.com/gogo/protobuf v1.3.2 // indirect @@ -109,7 +113,6 @@ require ( github.com/hashicorp/hcl v1.0.0 // indirect github.com/hashicorp/hcl/v2 v2.11.1 // indirect github.com/hashicorp/logutils v1.0.0 // indirect - github.com/hashicorp/terraform-json v0.13.0 // indirect github.com/hashicorp/terraform-plugin-go v0.5.0 // indirect github.com/hashicorp/terraform-plugin-log v0.2.0 // indirect github.com/hashicorp/terraform-registry-address v0.0.0-20210412075316-9b2996cce896 // indirect @@ -147,10 +150,13 @@ require ( github.com/pmezard/go-difflib v1.0.0 // indirect github.com/sirupsen/logrus v1.8.1 // indirect github.com/spf13/pflag v1.0.5 // indirect + github.com/tklauser/go-sysconf v0.3.10 // indirect + github.com/tklauser/numcpus v0.4.0 // indirect github.com/vmihailenco/msgpack v4.0.4+incompatible // indirect github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect github.com/xeipuuv/gojsonschema v1.2.0 // indirect + github.com/yusufpapurcu/wmi v1.2.2 // indirect github.com/zclconf/go-cty v1.10.0 // indirect github.com/zeebo/errs v1.2.2 // indirect golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd // indirect diff --git a/go.sum b/go.sum index 68dd21a43d74e..d28ed5a7262da 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= @@ -482,6 +485,8 @@ github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9 github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas= github.com/go-logr/logr v0.2.0/go.mod h1:z6/tIYblkpsD+a4lm/fGIIU9mZ+XfAiaFtq7xTgseGU= +github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= +github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= github.com/go-openapi/jsonpointer v0.19.2/go.mod h1:3akKfEdA7DF1sugOqz1dVQHBcuDBPKZGEoHC/NkiQRg= github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= github.com/go-openapi/jsonreference v0.19.2/go.mod h1:jMjeRr2HHw6nAVajTXJ4eiUwohSTlpa0o73RUL1owJc= @@ -674,7 +679,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 +713,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 +725,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 +740,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= @@ -915,6 +927,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 +970,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 +985,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= @@ -1130,6 +1147,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= @@ -1193,6 +1212,8 @@ github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAm github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= github.com/sergi/go-diff v1.2.0 h1:XU+rvMAioB0UC3q1MFrIQy4Vo5/4VsRDQQXHsEya6xQ= github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= +github.com/shirou/gopsutil v3.21.11+incompatible h1:+1+c1VGhc88SSonWP6foOcLhvnKlUeu/erjjvaPEYiI= +github.com/shirou/gopsutil v3.21.11+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA= github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4= github.com/shopspring/decimal v0.0.0-20200227202807-02e2044944cc/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= @@ -1209,6 +1230,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 +1243,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 +1256,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= @@ -1255,6 +1279,10 @@ github.com/tabbed/pqtype v0.1.1 h1:PhEcb9JZ8jr7SUjJDFjRPxny0M8fkXZrxn/a9yQfoZg= github.com/tabbed/pqtype v0.1.1/go.mod h1:HLt2kLJPcUhODQkYn3mJkMHXVsuv3Z2n5NZEeKXL0Uk= github.com/tchap/go-patricia v2.2.6+incompatible/go.mod h1:bmLyhP68RS6kStMGxByiQ23RP/odRBOTVjwp2cDyi6I= github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= +github.com/tklauser/go-sysconf v0.3.10 h1:IJ1AZGZRWbY8T5Vfk04D9WOA5WSejdflXxP03OUqALw= +github.com/tklauser/go-sysconf v0.3.10/go.mod h1:C8XykCvCb+Gn0oNCWPIlcb0RuglQTYaQ2hGm7jmxEFk= +github.com/tklauser/numcpus v0.4.0 h1:E53Dm1HjH1/R2/aoCtXtPgzmElmn51aOkhCFSuZq//o= +github.com/tklauser/numcpus v0.4.0/go.mod h1:1+UI3pD8NW14VMwdgJNJ1ESk2UnwhAnz5hMwiKKqXCQ= github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM= @@ -1307,6 +1335,8 @@ github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +github.com/yusufpapurcu/wmi v1.2.2 h1:KBNDSne4vP5mbSWnJbO+51IMOXJB67QiYCSBrubbPRg= +github.com/yusufpapurcu/wmi v1.2.2/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= github.com/yvasiyarov/go-metrics v0.0.0-20140926110328-57bccd1ccd43/go.mod h1:aX5oPXxHm3bOH+xeAttToC8pqch2ScQN/JoXYupl6xs= github.com/yvasiyarov/gorelic v0.0.0-20141212073537-a9bba5b9ab50/go.mod h1:NUSPSUX/bi6SeDMUh6brw0nXpxHnc96TguQh0+r/ssA= github.com/yvasiyarov/newrelic_platform_go v0.0.0-20140908184405-b21fdbd4370f/go.mod h1:GlGEuHIJweS1mbCqG+7vt2nvWLzLLnRHbXz5JKd/Qbg= @@ -1438,6 +1468,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 +1734,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 +1984,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/workspaceagent.go b/httpmw/workspaceagent.go new file mode 100644 index 0000000000000..85a46ed0001cb --- /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.ProvisionerJobAgent { + user, ok := r.Context().Value(workspaceAgentContextKey{}).(database.ProvisionerJobAgent) + 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.GetProvisionerJobAgentByAuthToken(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..b36d74c9c23dc --- /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.InsertProvisionerJobAgent(context.Background(), database.InsertProvisionerJobAgentParams{ + 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/workspaceresourceparam.go b/httpmw/workspaceresourceparam.go new file mode 100644 index 0000000000000..2f19b153dc701 --- /dev/null +++ b/httpmw/workspaceresourceparam.go @@ -0,0 +1,64 @@ +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 workspaceResourceParamContextKey struct{} + +// ProvisionerJobParam returns the project from the ExtractProjectParam handler. +func WorkspaceResourceParam(r *http.Request) database.ProvisionerJobResource { + resource, ok := r.Context().Value(workspaceResourceParamContextKey{}).(database.ProvisionerJobResource) + 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) { + resourceID := chi.URLParam(r, "workspaceresource") + if resourceID == "" { + httpapi.Write(rw, http.StatusBadRequest, httpapi.Response{ + Message: "workspace resource must be provided", + }) + return + } + resourceUUID, err := uuid.Parse(resourceID) + if err != nil { + httpapi.Write(rw, http.StatusBadRequest, httpapi.Response{ + Message: "resource id must be a uuid", + }) + return + } + resource, err := db.GetProvisionerJobResourceByID(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 + } + + ctx := context.WithValue(r.Context(), workspaceResourceParamContextKey{}, resource) + next.ServeHTTP(rw, r.WithContext(ctx)) + }) + } +} diff --git a/httpmw/workspaceresourceparam_test.go b/httpmw/workspaceresourceparam_test.go new file mode 100644 index 0000000000000..454a061c665e8 --- /dev/null +++ b/httpmw/workspaceresourceparam_test.go @@ -0,0 +1,109 @@ +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) (*http.Request, database.ProvisionerJobResource) { + r := httptest.NewRequest("GET", "/", nil) + resource, err := db.InsertProvisionerJobResource(context.Background(), database.InsertProvisionerJobResourceParams{ + ID: uuid.New(), + }) + require.NoError(t, err) + + ctx := chi.NewRouteContext() + 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) + 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.ExtractWorkspaceResourceParam(db), + ) + rtr.Get("/", nil) + + r, _ := setup(db) + chi.RouteContext(r.Context()).URLParams.Add("workspaceresource", "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.ExtractWorkspaceResourceParam(db), + ) + rtr.Get("/", nil) + + r, _ := setup(db) + 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("ProvisionerJob", 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) + 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/peerbroker/listen.go b/peerbroker/listen.go index 8e92fe7a7c82d..00c9faf125ba8 100644 --- a/peerbroker/listen.go +++ b/peerbroker/listen.go @@ -22,7 +22,8 @@ import ( func Listen(connListener net.Listener, opts *peer.ConnOptions) (*Listener, error) { ctx, cancelFunc := context.WithCancel(context.Background()) listener := &Listener{ - connectionChannel: make(chan *peer.Conn), + connectionChannel: make(chan *peer.Conn), + connectionListener: connListener, closeFunc: cancelFunc, closed: make(chan struct{}), @@ -47,7 +48,8 @@ func Listen(connListener net.Listener, opts *peer.ConnOptions) (*Listener, error } type Listener struct { - connectionChannel chan *peer.Conn + connectionChannel chan *peer.Conn + connectionListener net.Listener closeFunc context.CancelFunc closed chan struct{} @@ -79,6 +81,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/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 } From 1aec36d1281e6dd4ebb06f3501119dcb285ad7df Mon Sep 17 00:00:00 2001 From: Kyle Carberry Date: Thu, 3 Mar 2022 16:42:28 +0000 Subject: [PATCH 02/22] Cleanup code --- coderd/coderd.go | 1 + coderd/provisionerjobs.go | 45 +++++++++++++++-- coderd/workspaceagent.go | 39 +++++++++++++-- coderd/workspaceagent_test.go | 20 +++++--- database/databasefake/databasefake.go | 32 +++++++----- database/querier.go | 3 +- database/query.sql | 12 ++++- database/query.sql.go | 71 ++++++++++++++------------- provisioner/echo/serve.go | 33 +++++++++++-- 9 files changed, 188 insertions(+), 68 deletions(-) diff --git a/coderd/coderd.go b/coderd/coderd.go index 90a34787404a1..b86de63834e00 100644 --- a/coderd/coderd.go +++ b/coderd/coderd.go @@ -156,6 +156,7 @@ func New(options *Options) (http.Handler, func()) { r.Get("/", api.provisionerJobResourcesByID) r.Route("/{workspaceresource}", func(r chi.Router) { r.Use(httpmw.ExtractWorkspaceResourceParam(options.Database)) + r.Get("/", api.provisionerJobResourceByID) r.Get("/agent", api.workspaceAgentConnectByResource) }) }) diff --git a/coderd/provisionerjobs.go b/coderd/provisionerjobs.go index e31f187d15365..3e2e665ee3fcc 100644 --- a/coderd/provisionerjobs.go +++ b/coderd/provisionerjobs.go @@ -261,15 +261,54 @@ func (api *api) provisionerJobResourcesByID(rw http.ResponseWriter, r *http.Requ apiResources = append(apiResources, convertProvisionerJobResource(resource, nil)) continue } - // TODO: This should be combined. - agents, err := api.Database.GetProvisionerJobAgentsByResourceIDs(r.Context(), []uuid.UUID{resource.ID}) + agent, err := api.Database.GetProvisionerJobAgentByResourceID(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 := convertProvisionerJobAgent(agent) + if err != nil { + httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ + Message: fmt.Sprintf("convert provisioner job agent: %s", err), + }) + return + } + apiResources = append(apiResources, convertProvisionerJobResource(resource, &apiAgent)) + } + render.Status(r, http.StatusOK) + render.JSON(rw, r, apiResources) +} + +func (api *api) provisionerJobResourceByID(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 err != nil { + httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ + Message: fmt.Sprintf("get provisioner job resources: %s", err), + }) + return + } + apiResources := make([]ProvisionerJobResource, 0) + for _, resource := range resources { + if !resource.AgentID.Valid { + apiResources = append(apiResources, convertProvisionerJobResource(resource, nil)) + continue + } + agent, err := api.Database.GetProvisionerJobAgentByResourceID(r.Context(), resource.ID) if err != nil { httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ Message: fmt.Sprintf("get provisioner job agent: %s", err), }) return } - agent := agents[0] apiAgent, err := convertProvisionerJobAgent(agent) if err != nil { httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ diff --git a/coderd/workspaceagent.go b/coderd/workspaceagent.go index 4ebc77b100bd1..f70c663416f5c 100644 --- a/coderd/workspaceagent.go +++ b/coderd/workspaceagent.go @@ -1,14 +1,16 @@ package coderd import ( + "database/sql" "fmt" "io" "net/http" + "time" - "github.com/google/uuid" "github.com/hashicorp/yamux" "nhooyr.io/websocket" + "github.com/coder/coder/database" "github.com/coder/coder/httpapi" "github.com/coder/coder/httpmw" "github.com/coder/coder/peerbroker" @@ -27,14 +29,13 @@ func (api *api) workspaceAgentConnectByResource(rw http.ResponseWriter, r *http. }) return } - agents, err := api.Database.GetProvisionerJobAgentsByResourceIDs(r.Context(), []uuid.UUID{resource.ID}) + agent, err := api.Database.GetProvisionerJobAgentByResourceID(r.Context(), resource.ID) if err != nil { httpapi.Write(rw, http.StatusBadRequest, httpapi.Response{ Message: fmt.Sprintf("get provisioner job agent: %s", err), }) return } - agent := agents[0] conn, err := websocket.Accept(rw, r, &websocket.AcceptOptions{ CompressionMode: websocket.CompressionDisabled, }) @@ -99,5 +100,35 @@ func (api *api) workspaceAgentServe(rw http.ResponseWriter, r *http.Request) { return } defer closer.Close() - <-session.CloseChan() + err = api.Database.UpdateProvisionerJobAgentByID(r.Context(), database.UpdateProvisionerJobAgentByIDParams{ + 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.UpdateProvisionerJobAgentByID(r.Context(), database.UpdateProvisionerJobAgentByIDParams{ + ID: agent.ID, + UpdatedAt: sql.NullTime{ + Time: database.Now(), + Valid: true, + }, + }) + if err != nil { + _ = conn.Close(websocket.StatusAbnormalClosure, err.Error()) + return + } + } + } } diff --git a/coderd/workspaceagent_test.go b/coderd/workspaceagent_test.go index 3737544f508ca..17d56b8c7a560 100644 --- a/coderd/workspaceagent_test.go +++ b/coderd/workspaceagent_test.go @@ -29,7 +29,8 @@ func TestWorkspaceAgentServe(t *testing.T) { daemonCloser := coderdtest.NewProvisionerDaemon(t, client) authToken := uuid.NewString() job := coderdtest.CreateProjectImportJob(t, client, user.Organization, &echo.Responses{ - Parse: echo.ParseComplete, + Parse: echo.ParseComplete, + ProvisionDryRun: echo.ProvisionComplete, Provision: []*proto.Provision_Response{{ Type: &proto.Provision_Response_Complete{ Complete: &proto.Provision_Complete{ @@ -50,16 +51,13 @@ func TestWorkspaceAgentServe(t *testing.T) { 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{ + history, 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) + coderdtest.AwaitWorkspaceProvisionJob(t, client, user.Organization, history.ProvisionJobID) daemonCloser.Close() - resources, err := client.WorkspaceProvisionJobResources(context.Background(), user.Organization, firstHistory.ProvisionJobID) - require.NoError(t, err) - require.Len(t, resources, 1) agentClient := codersdk.New(client.URL) agentClient.SessionToken = authToken @@ -67,9 +65,15 @@ func TestWorkspaceAgentServe(t *testing.T) { Logger: slogtest.Make(t, nil), }) - time.Sleep(time.Millisecond * 250) + var resources []coderd.ProvisionerJobResource + require.Eventually(t, func() bool { + resources, err = client.WorkspaceProvisionJobResources(context.Background(), user.Organization, history.ProvisionJobID) + require.NoError(t, err) + require.Len(t, resources, 1) + return !resources[0].Agent.UpdatedAt.IsZero() + }, 5*time.Second, 25*time.Millisecond) - workspaceClient, err := client.WorkspaceAgentConnect(context.Background(), user.Organization, firstHistory.ProvisionJobID, resources[0].ID) + workspaceClient, err := client.WorkspaceAgentConnect(context.Background(), user.Organization, history.ProvisionJobID, resources[0].ID) require.NoError(t, err) stream, err := workspaceClient.NegotiateConnection(context.Background()) require.NoError(t, err) diff --git a/database/databasefake/databasefake.go b/database/databasefake/databasefake.go index ad4d791d42e07..65cbbdd528d9e 100644 --- a/database/databasefake/databasefake.go +++ b/database/databasefake/databasefake.go @@ -512,8 +512,7 @@ func (q *fakeQuerier) GetProvisionerJobAgentByAuthToken(_ context.Context, authT q.mutex.Lock() defer q.mutex.Unlock() - for i := len(q.provisionerJobAgent) - 1; i >= 0; i-- { - agent := q.provisionerJobAgent[i] + for _, agent := range q.provisionerJobAgent { if agent.AuthToken.String() == authToken.String() { return agent, nil } @@ -535,22 +534,16 @@ func (q *fakeQuerier) GetProvisionerJobAgentByInstanceID(_ context.Context, inst return database.ProvisionerJobAgent{}, sql.ErrNoRows } -func (q *fakeQuerier) GetProvisionerJobAgentsByResourceIDs(_ context.Context, ids []uuid.UUID) ([]database.ProvisionerJobAgent, error) { +func (q *fakeQuerier) GetProvisionerJobAgentByResourceID(ctx context.Context, resourceID uuid.UUID) (database.ProvisionerJobAgent, 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.ProvisionerJobAgent{}, sql.ErrNoRows } func (q *fakeQuerier) GetProvisionerDaemonByID(_ context.Context, id uuid.UUID) (database.ProvisionerDaemon, error) { @@ -969,6 +962,21 @@ func (q *fakeQuerier) UpdateProvisionerDaemonByID(_ context.Context, arg databas return sql.ErrNoRows } +func (q *fakeQuerier) UpdateProvisionerJobAgentByID(ctx context.Context, arg database.UpdateProvisionerJobAgentByIDParams) 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() diff --git a/database/querier.go b/database/querier.go index e6d33a1a4002a..20eaf693d99bc 100644 --- a/database/querier.go +++ b/database/querier.go @@ -28,7 +28,7 @@ type querier interface { GetProvisionerDaemons(ctx context.Context) ([]ProvisionerDaemon, error) GetProvisionerJobAgentByAuthToken(ctx context.Context, authToken uuid.UUID) (ProvisionerJobAgent, error) GetProvisionerJobAgentByInstanceID(ctx context.Context, authInstanceID string) (ProvisionerJobAgent, error) - GetProvisionerJobAgentsByResourceIDs(ctx context.Context, ids []uuid.UUID) ([]ProvisionerJobAgent, error) + GetProvisionerJobAgentByResourceID(ctx context.Context, resourceID 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) @@ -63,6 +63,7 @@ type querier interface { InsertWorkspaceHistory(ctx context.Context, arg InsertWorkspaceHistoryParams) (WorkspaceHistory, error) UpdateAPIKeyByID(ctx context.Context, arg UpdateAPIKeyByIDParams) error UpdateProvisionerDaemonByID(ctx context.Context, arg UpdateProvisionerDaemonByIDParams) error + UpdateProvisionerJobAgentByID(ctx context.Context, arg UpdateProvisionerJobAgentByIDParams) error UpdateProvisionerJobByID(ctx context.Context, arg UpdateProvisionerJobByIDParams) error UpdateProvisionerJobWithCompleteByID(ctx context.Context, arg UpdateProvisionerJobWithCompleteByIDParams) error UpdateWorkspaceHistoryByID(ctx context.Context, arg UpdateWorkspaceHistoryByIDParams) error diff --git a/database/query.sql b/database/query.sql index 3b2574acb15f1..8e1d9cbc4b805 100644 --- a/database/query.sql +++ b/database/query.sql @@ -354,13 +354,13 @@ FROM WHERE job_id = $1; --- name: GetProvisionerJobAgentsByResourceIDs :many +-- name: GetProvisionerJobAgentByResourceID :one SELECT * FROM provisioner_job_agent WHERE - resource_id = ANY(@ids :: uuid [ ]); + resource_id = $1; -- name: InsertAPIKey :one INSERT INTO @@ -660,6 +660,14 @@ SET WHERE id = $1; +-- name: UpdateProvisionerJobAgentByID :exec +UPDATE + provisioner_job_agent +SET + updated_at = $2 +WHERE + id = $1; + -- name: UpdateWorkspaceHistoryByID :exec UPDATE workspace_history diff --git a/database/query.sql.go b/database/query.sql.go index 52475635e0785..aa724700fa662 100644 --- a/database/query.sql.go +++ b/database/query.sql.go @@ -673,47 +673,31 @@ func (q *sqlQuerier) GetProvisionerJobAgentByInstanceID(ctx context.Context, aut return i, err } -const getProvisionerJobAgentsByResourceIDs = `-- name: GetProvisionerJobAgentsByResourceIDs :many +const getProvisionerJobAgentByResourceID = `-- name: GetProvisionerJobAgentByResourceID :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 - resource_id = ANY($1 :: uuid [ ]) + resource_id = $1 ` -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 +func (q *sqlQuerier) GetProvisionerJobAgentByResourceID(ctx context.Context, resourceID uuid.UUID) (ProvisionerJobAgent, error) { + row := q.db.QueryRowContext(ctx, getProvisionerJobAgentByResourceID, resourceID) + 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 getProvisionerJobByID = `-- name: GetProvisionerJobByID :one @@ -2234,6 +2218,25 @@ func (q *sqlQuerier) UpdateProvisionerDaemonByID(ctx context.Context, arg Update return err } +const updateProvisionerJobAgentByID = `-- name: UpdateProvisionerJobAgentByID :exec +UPDATE + provisioner_job_agent +SET + updated_at = $2 +WHERE + id = $1 +` + +type UpdateProvisionerJobAgentByIDParams struct { + ID uuid.UUID `db:"id" json:"id"` + UpdatedAt sql.NullTime `db:"updated_at" json:"updated_at"` +} + +func (q *sqlQuerier) UpdateProvisionerJobAgentByID(ctx context.Context, arg UpdateProvisionerJobAgentByIDParams) error { + _, err := q.db.ExecContext(ctx, updateProvisionerJobAgentByID, arg.ID, arg.UpdatedAt) + return err +} + const updateProvisionerJobByID = `-- name: UpdateProvisionerJobByID :exec UPDATE provisioner_job 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 From 7180233995cb8c51fd9d7ee4e4d6e660129bb469 Mon Sep 17 00:00:00 2001 From: Kyle Carberry Date: Thu, 3 Mar 2022 16:48:35 +0000 Subject: [PATCH 03/22] Fix linting error --- cli/workspaceagent.go | 5 +++-- cli/workspaces.go | 1 + coderd/workspaceagent_test.go | 5 +++-- codersdk/workspaceagent.go | 5 +++-- database/databasefake/databasefake.go | 4 ++-- 5 files changed, 12 insertions(+), 8 deletions(-) diff --git a/cli/workspaceagent.go b/cli/workspaceagent.go index 75a462c0a100e..3246b21bee5f1 100644 --- a/cli/workspaceagent.go +++ b/cli/workspaceagent.go @@ -4,11 +4,12 @@ import ( "net/url" "os" - "github.com/coder/coder/agent" - "github.com/coder/coder/codersdk" "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 { 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/coderd/workspaceagent_test.go b/coderd/workspaceagent_test.go index 17d56b8c7a560..a7e6dc57376f4 100644 --- a/coderd/workspaceagent_test.go +++ b/coderd/workspaceagent_test.go @@ -5,6 +5,9 @@ import ( "testing" "time" + "github.com/google/uuid" + "github.com/stretchr/testify/require" + "cdr.dev/slog" "cdr.dev/slog/sloggers/slogtest" "github.com/coder/coder/agent" @@ -16,8 +19,6 @@ import ( "github.com/coder/coder/peerbroker" "github.com/coder/coder/provisioner/echo" "github.com/coder/coder/provisionersdk/proto" - "github.com/google/uuid" - "github.com/stretchr/testify/require" ) func TestWorkspaceAgentServe(t *testing.T) { diff --git a/codersdk/workspaceagent.go b/codersdk/workspaceagent.go index 57507e7c578c6..be451aa28b62e 100644 --- a/codersdk/workspaceagent.go +++ b/codersdk/workspaceagent.go @@ -12,14 +12,15 @@ import ( "golang.org/x/xerrors" "nhooyr.io/websocket" + "github.com/google/uuid" + "github.com/hashicorp/yamux" + "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" - "github.com/google/uuid" - "github.com/hashicorp/yamux" ) // AuthenticateWorkspaceAgentUsingGoogleCloudIdentity uses the Google Compute Engine Metadata API to diff --git a/database/databasefake/databasefake.go b/database/databasefake/databasefake.go index 65cbbdd528d9e..214e6f45bb5d8 100644 --- a/database/databasefake/databasefake.go +++ b/database/databasefake/databasefake.go @@ -534,7 +534,7 @@ func (q *fakeQuerier) GetProvisionerJobAgentByInstanceID(_ context.Context, inst return database.ProvisionerJobAgent{}, sql.ErrNoRows } -func (q *fakeQuerier) GetProvisionerJobAgentByResourceID(ctx context.Context, resourceID uuid.UUID) (database.ProvisionerJobAgent, error) { +func (q *fakeQuerier) GetProvisionerJobAgentByResourceID(_ context.Context, resourceID uuid.UUID) (database.ProvisionerJobAgent, error) { q.mutex.Lock() defer q.mutex.Unlock() @@ -962,7 +962,7 @@ func (q *fakeQuerier) UpdateProvisionerDaemonByID(_ context.Context, arg databas return sql.ErrNoRows } -func (q *fakeQuerier) UpdateProvisionerJobAgentByID(ctx context.Context, arg database.UpdateProvisionerJobAgentByIDParams) error { +func (q *fakeQuerier) UpdateProvisionerJobAgentByID(_ context.Context, arg database.UpdateProvisionerJobAgentByIDParams) error { q.mutex.Lock() defer q.mutex.Unlock() From 2f56c6dd93b23d7f915c2aa8e0b86165bab8803f Mon Sep 17 00:00:00 2001 From: Kyle Carberry Date: Fri, 4 Mar 2022 01:35:35 +0000 Subject: [PATCH 04/22] Rename routes to be simpler --- cli/projectcreate.go | 2 +- coderd/coderd.go | 106 +++++++++++++++--- coderd/coderdtest/coderdtest.go | 2 +- coderd/files.go | 6 +- coderd/files_test.go | 8 +- coderd/organizations.go | 78 +++++++++++++ coderd/projectimport.go | 11 ++ coderd/projects.go | 73 ++++++------ coderd/projectversion.go | 24 ++-- coderd/provisionerjobs_test.go | 2 +- coderd/users.go | 7 +- coderd/workspaces.go | 26 ----- codersdk/files.go | 17 +-- codersdk/files_test.go | 4 +- codersdk/organizations.go | 18 +++ codersdk/projectimport.go | 95 ---------------- codersdk/projects.go | 90 ++++----------- codersdk/projects_test.go | 4 +- codersdk/projectversions.go | 96 ++++++++++++++++ ...import_test.go => projectversions_test.go} | 0 ...{provisioners.go => provisionerdaemons.go} | 8 +- ...ers_test.go => provisionerdaemons_test.go} | 0 codersdk/users.go | 35 ++---- codersdk/workspaceagent.go | 15 ++- codersdk/workspaces.go | 36 +++--- database/dump.sql | 4 +- database/migrations/000002_projects.up.sql | 7 +- database/models.go | 14 +-- database/querier.go | 2 +- database/query.sql | 2 +- database/query.sql.go | 40 +++---- 31 files changed, 466 insertions(+), 366 deletions(-) create mode 100644 codersdk/organizations.go delete mode 100644 codersdk/projectimport.go create mode 100644 codersdk/projectversions.go rename codersdk/{projectimport_test.go => projectversions_test.go} (100%) rename codersdk/{provisioners.go => provisionerdaemons.go} (92%) rename codersdk/{provisioners_test.go => provisionerdaemons_test.go} (100%) diff --git a/cli/projectcreate.go b/cli/projectcreate.go index a1e5027c26715..7267af63ca9d5 100644 --- a/cli/projectcreate.go +++ b/cli/projectcreate.go @@ -133,7 +133,7 @@ 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 } diff --git a/coderd/coderd.go b/coderd/coderd.go index b86de63834e00..826a6601c4e69 100644 --- a/coderd/coderd.go +++ b/coderd/coderd.go @@ -60,35 +60,109 @@ func New(options *Options) (http.Handler, func()) { r.Post("/keys", api.postKeyForUser) }) }) - r.Route("/projects", func(r chi.Router) { + r.Route("/organization/{organization}", func(r chi.Router) { r.Use( httpmw.ExtractAPIKey(options.Database, nil), + httpmw.ExtractOrganizationParam(options.Database), + ) + r.Route("/projects", func(r chi.Router) { + r.Post("/", api.postProjectsByOrganization) + r.Get("/", api.projectsByOrganization) + r.Get("/{projectname}", api.projectByOrganizationAndName) + }) + }) + r.Route("/project/{project}", func(r chi.Router) { + r.Use( + httpmw.ExtractAPIKey(options.Database, nil), + 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.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) - }) - }) + }) }) }) + // Upload - Uploads a file. + + // CreateProject(organization) - Creates a project with a version. + // ImportProjectVersion(organization, project?) - If a project is provided, it's attached. If a project isn't, it's detached. + // ProjectVersions - Returns a list of project versions by project. + // ProjectVersionSchema(projectversion) - Return parameter schemas for a job. + // ProjectVersionParameters(projectversion) - Returns computed parameters for a job. + // ProjectVersionParameters(projectversion) - Returns computed parameters for a job. + // ProjectVersionLogs(projectversion) - Returns logs for an executing project version. + // ProjectVersionLogsAfter(projectversion, timestamp) - Streams logs that occur after a specific timestamp. + // ProjectVersionResources(projectversion, resources) - Returns resources to be created for a project version. + + // CreateWorkspace - Creates a workspace for a project. + // ProvisionWorkspace - Creates a new build. + // Workspaces - Returns all workspaces the user has access to. + // WorkspacesByProject - Returns workspaces inside a project. + // WorkspaceByName - Returns a workspace by name. + // Workspace - Returns a single workspace by ID. + // WorkspaceProvisions - Returns a timeline of provisions for a workspace. + // WorkspaceProvisionResources - List resources for a specific workspace version. + // WorkspaceProvisionResource - Get a specific resource. + // WorkspaceProvisionLogs - Returns a stream of logs. + // WorkspaceProvisionLogsAfter - Returns a stream of logs after. + // DialWorkspaceAgent - Creates the connection to a workspace agent. + // ListenWorkspaceAgent - Listens to the workspace agent as the ID. + // AuthWorkspaceAgentWithGoogleInstanceIdentity - Exchanges SA for token. + + // User - Returns the currently authenticated user. + // HasFirstUser - Returns whether the first user has been created. + // CreateFirstUser - Creates a new user and the organization provided. + // CreateUser - Creates a new user and adds them to the organization. + // CreateAPIKey - Creates a new API key. + // LoginWithPassword - Authenticates with email and password. + // Logout - Should clear the session token. + + // ProvisionerDaemons + // ListenProvisionerDaemon + + // OrganizationsByUser - Returns organizations by user. + + r.Route("/agent", func(r chi.Router) { + + }) + + r.Route("/daemon", func(r chi.Router) { + + }) + + r.Route("/job", func(r chi.Router) { + r.Use( + httpmw.ExtractAPIKey(options.Database, nil), + httpmw.ExtractOrganizationParam(options.Database), + ) + r.Post("/", api.postProjectImportByOrganization) + }) + + r.Route("/provisioner/job/{job}/resources", func(r chi.Router) { + + }) + + // ProjectVersionLogs + // Listing operations specific to resources should go under // their respective routes. eg. /orgs//workspaces r.Route("/workspaces", func(r chi.Router) { diff --git a/coderd/coderdtest/coderdtest.go b/coderd/coderdtest/coderdtest.go index 905bd85b879e2..01c5c2a38e315 100644 --- a/coderd/coderdtest/coderdtest.go +++ b/coderd/coderdtest/coderdtest.go @@ -167,7 +167,7 @@ func CreateInitialUser(t *testing.T, client *codersdk.Client) coderd.CreateIniti func CreateProjectImportJob(t *testing.T, client *codersdk.Client, organization string, res *echo.Responses) coderd.ProvisionerJob { 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{ StorageSource: file.Hash, diff --git a/coderd/files.go b/coderd/files.go index e25ccd8d749b0..ee8609c635f7d 100644 --- a/coderd/files.go +++ b/coderd/files.go @@ -14,7 +14,7 @@ import ( "github.com/coder/coder/httpmw" ) -type UploadFileResponse struct { +type UploadResponse struct { Hash string `json:"hash"` } @@ -45,7 +45,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 +64,7 @@ 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, }) } diff --git a/coderd/files_test.go b/coderd/files_test.go index ad00a6c5b656e..dc4d209e01a27 100644 --- a/coderd/files_test.go +++ b/coderd/files_test.go @@ -16,7 +16,7 @@ func TestPostUpload(t *testing.T) { t.Parallel() client := coderdtest.New(t, nil) _ = coderdtest.CreateInitialUser(t, client) - _, err := client.UploadFile(context.Background(), "bad", []byte{'a'}) + _, err := client.Upload(context.Background(), "bad", []byte{'a'}) require.Error(t, err) }) @@ -24,7 +24,7 @@ func TestPostUpload(t *testing.T) { t.Parallel() client := coderdtest.New(t, nil) _ = coderdtest.CreateInitialUser(t, client) - _, err := client.UploadFile(context.Background(), codersdk.ContentTypeTar, make([]byte, 1024)) + _, err := client.Upload(context.Background(), codersdk.ContentTypeTar, make([]byte, 1024)) require.NoError(t, err) }) @@ -33,9 +33,9 @@ func TestPostUpload(t *testing.T) { client := coderdtest.New(t, nil) _ = coderdtest.CreateInitialUser(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) }) } diff --git a/coderd/organizations.go b/coderd/organizations.go index 0f438274598b3..40caaee40fbdf 100644 --- a/coderd/organizations.go +++ b/coderd/organizations.go @@ -1,9 +1,19 @@ 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" + "github.com/coder/coder/httpmw" ) // Organization is the JSON representation of a Coder organization. @@ -14,6 +24,74 @@ type Organization struct { UpdatedAt time.Time `json:"updated_at" validate:"required"` } +// 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}) + 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/projectimport.go b/coderd/projectimport.go index 9955e01ba3319..38a3a446dede7 100644 --- a/coderd/projectimport.go +++ b/coderd/projectimport.go @@ -31,6 +31,17 @@ type CreateProjectImportJobRequest struct { ParameterValues []CreateParameterValueRequest `json:"parameter_values"` } +type CreateProjectVersion struct { + 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 []CreateParameterValueRequest `json:"parameter_values"` +} + // Create a new project import job! func (api *api) postProjectImportByOrganization(rw http.ResponseWriter, r *http.Request) { apiKey := httpmw.APIKey(r) diff --git a/coderd/projects.go b/coderd/projects.go index b14018800b4e3..927047166fe0f 100644 --- a/coderd/projects.go +++ b/coderd/projects.go @@ -97,37 +97,6 @@ func (api *api) projects(rw http.ResponseWriter, r *http.Request) { render.JSON(rw, r, convertProjects(projects, workspaceCounts)) } -// 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}) - 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)) -} - // Create a new project in an organization. func (api *api) postProjectsByOrganization(rw http.ResponseWriter, r *http.Request) { var createProject CreateProjectRequest @@ -184,12 +153,12 @@ func (api *api) postProjectsByOrganization(rw http.ResponseWriter, r *http.Reque 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, + ID: projectVersionID, + ProjectID: dbProject.ID, + CreatedAt: database.Now(), + UpdatedAt: database.Now(), + Name: namesgenerator.GetRandomName(1), + JobID: importJob.ID, }) if err != nil { return xerrors.Errorf("insert project version: %s", err) @@ -273,6 +242,36 @@ func (api *api) parametersByProject(rw http.ResponseWriter, r *http.Request) { render.JSON(rw, r, apiParameterValues) } +// Returns all workspaces for a specific project. +func (api *api) workspacesByProject(rw http.ResponseWriter, r *http.Request) { + apiKey := httpmw.APIKey(r) + project := httpmw.ProjectParam(r) + workspaces, err := api.Database.GetWorkspacesByProjectAndUserID(r.Context(), database.GetWorkspacesByProjectAndUserIDParams{ + OwnerID: apiKey.UserID, + ProjectID: project.ID, + }) + if errors.Is(err, sql.ErrNoRows) { + err = nil + } + if err != nil { + httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ + Message: fmt.Sprintf("get workspaces: %s", err), + }) + return + } + + apiWorkspaces := make([]Workspace, 0, len(workspaces)) + for _, workspace := range workspaces { + apiWorkspaces = append(apiWorkspaces, convertWorkspace(workspace)) + } + render.Status(r, http.StatusOK) + render.JSON(rw, r, apiWorkspaces) +} + +func (api *api) workspaceByProjectAndName(rw http.ResponseWriter, r *http.Request) { + +} + func convertProjects(projects []database.Project, workspaceCounts []database.GetWorkspaceOwnerCountsByProjectIDsRow) []Project { apiProjects := make([]Project, 0, len(projects)) for _, project := range projects { diff --git a/coderd/projectversion.go b/coderd/projectversion.go index c1b3ecdd29ace..255a773e78e49 100644 --- a/coderd/projectversion.go +++ b/coderd/projectversion.go @@ -18,12 +18,12 @@ import ( // 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"` + 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"` + JobID uuid.UUID `json:"import_job_id"` } // CreateProjectVersionRequest enables callers to create a new Project Version. @@ -103,11 +103,11 @@ func (api *api) postProjectVersionByOrganization(rw http.ResponseWriter, r *http 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, + ID: version.ID, + ProjectID: version.ProjectID, + CreatedAt: version.CreatedAt, + UpdatedAt: version.UpdatedAt, + Name: version.Name, + JobID: version.ImportJobID, } } diff --git a/coderd/provisionerjobs_test.go b/coderd/provisionerjobs_test.go index b33169a58fd1a..e5b2c018e89cb 100644 --- a/coderd/provisionerjobs_test.go +++ b/coderd/provisionerjobs_test.go @@ -73,7 +73,7 @@ func TestPostProvisionerImportJobByOrganization(t *testing.T) { Provision: echo.ProvisionComplete, }) 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(), user.Organization, coderd.CreateProjectImportJobRequest{ StorageSource: file.Hash, diff --git a/coderd/users.go b/coderd/users.go index a660f31fdfa94..713f9a4687dcb 100644 --- a/coderd/users.go +++ b/coderd/users.go @@ -39,9 +39,10 @@ type CreateInitialUserRequest struct { // CreateUserRequest provides options for creating a new user. 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"` + Organization string `json:"organization" validate:"required,username"` } // LoginWithPasswordRequest enables callers to authenticate with email and password. diff --git a/coderd/workspaces.go b/coderd/workspaces.go index 4ea1ba2706202..31539894716f9 100644 --- a/coderd/workspaces.go +++ b/coderd/workspaces.go @@ -145,32 +145,6 @@ func (*api) workspaceByUser(rw http.ResponseWriter, r *http.Request) { render.JSON(rw, r, convertWorkspace(workspace)) } -// Returns all workspaces for a specific project. -func (api *api) workspacesByProject(rw http.ResponseWriter, r *http.Request) { - apiKey := httpmw.APIKey(r) - project := httpmw.ProjectParam(r) - workspaces, err := api.Database.GetWorkspacesByProjectAndUserID(r.Context(), database.GetWorkspacesByProjectAndUserIDParams{ - OwnerID: apiKey.UserID, - ProjectID: project.ID, - }) - if errors.Is(err, sql.ErrNoRows) { - err = nil - } - if err != nil { - httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ - Message: fmt.Sprintf("get workspaces: %s", err), - }) - return - } - - apiWorkspaces := make([]Workspace, 0, len(workspaces)) - for _, workspace := range workspaces { - apiWorkspaces = append(apiWorkspaces, convertWorkspace(workspace)) - } - render.Status(r, http.StatusOK) - render.JSON(rw, r, apiWorkspaces) -} - // Converts the internal workspace representation to a public external-facing model. func convertWorkspace(workspace database.Workspace) Workspace { return Workspace(workspace) diff --git a/codersdk/files.go b/codersdk/files.go index c7eb642f76b68..d15e3f7b1beb6 100644 --- a/codersdk/files.go +++ b/codersdk/files.go @@ -3,9 +3,7 @@ package codersdk import ( "context" "encoding/json" - "fmt" "net/http" - "net/url" "github.com/coder/coder/coderd" ) @@ -14,22 +12,19 @@ const ( ContentTypeTar = "application/x-tar" ) -func (c *Client) UploadFile(ctx context.Context, contentType string, content []byte) (coderd.UploadFileResponse, error) { +// 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/upload", 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)) -} diff --git a/codersdk/files_test.go b/codersdk/files_test.go index 8a85cee963396..316ec4a92bc6e 100644 --- a/codersdk/files_test.go +++ b/codersdk/files_test.go @@ -15,14 +15,14 @@ func TestUpload(t *testing.T) { t.Run("Error", func(t *testing.T) { t.Parallel() client := coderdtest.New(t, nil) - _, err := client.UploadFile(context.Background(), "wow", []byte{}) + _, err := client.Upload(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'}) + _, err := client.Upload(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..be1c688b0f708 --- /dev/null +++ b/codersdk/organizations.go @@ -0,0 +1,18 @@ +package codersdk + +import ( + "context" + + "github.com/coder/coder/coderd" + "github.com/google/uuid" +) + +// OrganizationsByUser returns organizations for the provided user. +func (c *Client) OrganizationsByUser(ctx context.Context, user uuid.UUID) ([]coderd.Organization, error) { + return nil, nil +} + +// OrganizationByName returns an organization by case-insensitive name. +func (c *Client) OrganizationByName(ctx context.Context, user uuid.UUID, name string) (coderd.Organization, error) { + return coderd.Organization{}, nil +} 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/projects.go b/codersdk/projects.go index 3e627ae571ed4..9079a2ed0f5f7 100644 --- a/codersdk/projects.go +++ b/codersdk/projects.go @@ -6,46 +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) - if err != nil { - return coderd.Project{}, nil - } - defer res.Body.Close() - if res.StatusCode != http.StatusOK { - return coderd.Project{}, readBodyAsError(res) - } - var resp coderd.Project - 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) +func (c *Client) CreateProject(ctx context.Context, organization uuid.UUID, 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 } @@ -57,51 +25,33 @@ func (c *Client) CreateProject(ctx context.Context, organization string, request 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) - if err != nil { - return nil, err - } - defer res.Body.Close() - if res.StatusCode != http.StatusOK { - return nil, readBodyAsError(res) - } - var projectVersion []coderd.ProjectVersion - return projectVersion, json.NewDecoder(res.Body).Decode(&projectVersion) +// ProjectsByOrganization lists all projects inside of an organization. +func (c *Client) ProjectsByOrganization(ctx context.Context, organization uuid.UUID) ([]coderd.Project, error) { + return nil, nil } -// 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) - if err != nil { - return coderd.ProjectVersion{}, err - } - defer res.Body.Close() - if res.StatusCode != http.StatusOK { - return coderd.ProjectVersion{}, readBodyAsError(res) - } - var projectVersion coderd.ProjectVersion - return projectVersion, json.NewDecoder(res.Body).Decode(&projectVersion) +// ProjectByName finds a project inside the organization provided with a case-insensitive name. +func (c *Client) ProjectByName(ctx context.Context, organization uuid.UUID, name string) (coderd.Project, error) { + return coderd.Project{}, nil } -// 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) +// Project returns a single project. +func (c *Client) Project(ctx context.Context, project uuid.UUID) (coderd.Project, error) { + res, err := c.request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/project/%s", project), nil) if err != nil { - return coderd.ProjectVersion{}, err + return coderd.Project{}, nil } defer res.Body.Close() - if res.StatusCode != http.StatusCreated { - return coderd.ProjectVersion{}, readBodyAsError(res) + if res.StatusCode != http.StatusOK { + return coderd.Project{}, readBodyAsError(res) } - var projectVersion coderd.ProjectVersion - return projectVersion, json.NewDecoder(res.Body).Decode(&projectVersion) + var resp coderd.Project + return resp, json.NewDecoder(res.Body).Decode(&resp) } // 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) +func (c *Client) ProjectParameters(ctx context.Context, project uuid.UUID) ([]coderd.ParameterValue, error) { + res, err := c.request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/project/%s/parameters", project), nil) if err != nil { return nil, err } diff --git a/codersdk/projects_test.go b/codersdk/projects_test.go index 694dcb60136f0..df740d386d2c7 100644 --- a/codersdk/projects_test.go +++ b/codersdk/projects_test.go @@ -17,7 +17,7 @@ func TestProjects(t *testing.T) { t.Run("Error", func(t *testing.T) { t.Parallel() client := coderdtest.New(t, nil) - _, err := client.Projects(context.Background(), "") + _, err := client.ProjectsByOrganization(context.Background(), uuid.New()) require.Error(t, err) }) @@ -25,7 +25,7 @@ func TestProjects(t *testing.T) { t.Parallel() client := coderdtest.New(t, nil) _ = coderdtest.CreateInitialUser(t, client) - _, err := client.Projects(context.Background(), "") + _, err := client.ProjectsByOrganization(context.Background(), "") require.NoError(t, err) }) } diff --git a/codersdk/projectversions.go b/codersdk/projectversions.go new file mode 100644 index 0000000000000..546a06e59e01d --- /dev/null +++ b/codersdk/projectversions.go @@ -0,0 +1,96 @@ +package codersdk + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + "time" + + "github.com/google/uuid" + + "github.com/coder/coder/coderd" +) + +// ImportProjectVersion processes source-code and optionally associates the version with a project. +// Executing without a project is useful for validating source-code. +func (c *Client) ImportProjectVersion(ctx context.Context, organization uuid.UUID, req coderd.CreateProjectVersion) (coderd.ProjectVersion, error) { + res, err := c.request(ctx, http.MethodPost, fmt.Sprintf("/api/v2/organizations/%s/projectversion", 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) +} + +// ProjectVersions lists versions associated with a project. +func (c *Client) ProjectVersions(ctx context.Context, project uuid.UUID) ([]coderd.ProjectVersion, error) { + res, err := c.request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/project/%s/versions", project), nil) + if err != nil { + return nil, err + } + defer res.Body.Close() + if res.StatusCode != http.StatusOK { + return nil, readBodyAsError(res) + } + var projectVersion []coderd.ProjectVersion + return projectVersion, json.NewDecoder(res.Body).Decode(&projectVersion) +} + +// ProjectVersion returns a project version by ID. +func (c *Client) ProjectVersion(ctx context.Context, version uuid.UUID) (coderd.ProjectVersion, error) { + return coderd.ProjectVersion{}, 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) { + return coderd.ProjectVersion{}, nil +} + +// ProjectVersionSchema returns schemas for a project version by ID. +func (c *Client) ProjectVersionSchema(ctx context.Context, version uuid.UUID) ([]coderd.ParameterSchema, error) { + res, err := c.request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/projectversion/%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.ParameterSchema + return params, json.NewDecoder(res.Body).Decode(¶ms) +} + +// ProjectVersionParameters returns computed parameters for a project version. +func (c *Client) ProjectVersionComputedParameters(ctx context.Context, version uuid.UUID) ([]coderd.ComputedParameterValue, error) { + res, err := c.request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/projectimport/%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.ComputedParameterValue + 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.ProvisionerJobResource, error) { + return nil, nil +} + +// 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, "projectimport", organization, job, before) +} + +// ProjectVersionLogsAfter streams logs for a project version that occurred after a specific time. +func (c *Client) ProjectVersionLogsAfter(ctx context.Context, organization string, job uuid.UUID, after time.Time) (<-chan coderd.ProvisionerJobLog, error) { + return c.provisionerJobLogsAfter(ctx, "projectimport", organization, job, after) +} diff --git a/codersdk/projectimport_test.go b/codersdk/projectversions_test.go similarity index 100% rename from codersdk/projectimport_test.go rename to codersdk/projectversions_test.go diff --git a/codersdk/provisioners.go b/codersdk/provisionerdaemons.go similarity index 92% rename from codersdk/provisioners.go rename to codersdk/provisionerdaemons.go index 2cee00a626438..5046f9ff44438 100644 --- a/codersdk/provisioners.go +++ b/codersdk/provisionerdaemons.go @@ -22,7 +22,7 @@ import ( // 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) + res, err := c.request(ctx, http.MethodGet, "/api/v2/provisionerdaemons", nil) if err != nil { return nil, err } @@ -34,9 +34,9 @@ func (c *Client) ProvisionerDaemons(ctx context.Context) ([]coderd.ProvisionerDa return daemons, json.NewDecoder(res.Body).Decode(&daemons) } -// ProvisionerDaemonServe returns the gRPC service for a provisioner daemon implementation. -func (c *Client) ProvisionerDaemonServe(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/serve") if err != nil { return nil, xerrors.Errorf("parse url: %w", err) } diff --git a/codersdk/provisioners_test.go b/codersdk/provisionerdaemons_test.go similarity index 100% rename from codersdk/provisioners_test.go rename to codersdk/provisionerdaemons_test.go diff --git a/codersdk/users.go b/codersdk/users.go index 08b152f9cf8d0..08e27d17381ee 100644 --- a/codersdk/users.go +++ b/codersdk/users.go @@ -7,11 +7,16 @@ import ( "net/http" "github.com/coder/coder/coderd" + "github.com/google/uuid" ) -// HasInitialUser returns whether the initial user has already been -// created or not. -func (c *Client) HasInitialUser(ctx context.Context) (bool, error) { +var ( + // Me represents an empty UUID, which is used to represent the current entity. + Me = uuid.UUID{} +) + +// 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/user", nil) if err != nil { return false, err @@ -26,10 +31,9 @@ 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) { +// 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.CreateInitialUserRequest) (coderd.User, error) { res, err := c.request(ctx, http.MethodPost, "/api/v2/user", req) if err != nil { return coderd.User{}, err @@ -119,20 +123,3 @@ func (c *Client) User(ctx context.Context, id string) (coderd.User, error) { var user coderd.User 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) { - if id == "" { - id = "me" - } - res, err := c.request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/users/%s/organizations", id), nil) - if err != nil { - return nil, err - } - defer res.Body.Close() - if res.StatusCode != http.StatusOK { - return nil, readBodyAsError(res) - } - var orgs []coderd.Organization - return orgs, json.NewDecoder(res.Body).Decode(&orgs) -} diff --git a/codersdk/workspaceagent.go b/codersdk/workspaceagent.go index be451aa28b62e..0a8650054469e 100644 --- a/codersdk/workspaceagent.go +++ b/codersdk/workspaceagent.go @@ -23,11 +23,11 @@ import ( "github.com/coder/coder/provisionersdk" ) -// AuthenticateWorkspaceAgentUsingGoogleCloudIdentity uses the Google Compute Engine Metadata API to +// AuthWorkspaceAgentWithGoogleInstanceIdentity 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) AuthWorkspaceAgentWithGoogleInstanceIdentity(ctx context.Context, serviceAccount string, gcpClient *metadata.Client) (coderd.WorkspaceAgentAuthenticateResponse, error) { if serviceAccount == "" { // This is the default name specified by Google. serviceAccount = "default" @@ -54,8 +54,9 @@ func (c *Client) AuthenticateWorkspaceAgentUsingGoogleCloudIdentity(ctx context. return resp, json.NewDecoder(res.Body).Decode(&resp) } -func (c *Client) WorkspaceAgentConnect(ctx context.Context, organization string, job, resource uuid.UUID) (proto.DRPCPeerBrokerClient, error) { - serverURL, err := c.URL.Parse(fmt.Sprintf("/api/v2/workspaceprovision/%s/%s/resources/%s/agent", organization, job.String(), resource.String())) +// 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/workspaceresource/%s/dial", resource.String())) if err != nil { return nil, xerrors.Errorf("parse url: %w", err) } @@ -90,8 +91,10 @@ func (c *Client) WorkspaceAgentConnect(ctx context.Context, organization string, return proto.NewDRPCPeerBrokerClient(provisionersdk.Conn(session)), nil } -func (c *Client) WorkspaceAgentServe(ctx context.Context, opts *peer.ConnOptions) (*peerbroker.Listener, error) { - serverURL, err := c.URL.Parse("/api/v2/workspaceagent/serve") +// 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/workspaceagent/listen") if err != nil { return nil, xerrors.Errorf("parse url: %w", err) } diff --git a/codersdk/workspaces.go b/codersdk/workspaces.go index 4e7a97e4e614c..86ad03b81e905 100644 --- a/codersdk/workspaces.go +++ b/codersdk/workspaces.go @@ -13,12 +13,10 @@ import ( ) // Workspaces returns all workspaces the authenticated session has access to. -// If owner is specified, all workspaces for an organization will be returned. -// If owner is empty, all workspaces the caller has access to will be returned. -func (c *Client) Workspaces(ctx context.Context, user string) ([]coderd.Workspace, error) { - route := "/api/v2/workspaces" - if user != "" { - route += fmt.Sprintf("/%s", user) +func (c *Client) WorkspacesByUser(ctx context.Context, user uuid.UUID) ([]coderd.Workspace, error) { + route := fmt.Sprintf("/api/v2/user/%s/workspaces", user) + if user == Me { + route = fmt.Sprintf("/api/v2/user/me/workspaces", user) } res, err := c.request(ctx, http.MethodGet, route, nil) if err != nil { @@ -33,8 +31,8 @@ func (c *Client) Workspaces(ctx context.Context, user string) ([]coderd.Workspac } // 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) +func (c *Client) WorkspacesByProject(ctx context.Context, project uuid.UUID) ([]coderd.Workspace, error) { + res, err := c.request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/projects/%s/workspaces", project), nil) if err != nil { return nil, err } @@ -46,12 +44,14 @@ func (c *Client) WorkspacesByProject(ctx context.Context, organization, project return workspaces, json.NewDecoder(res.Body).Decode(&workspaces) } +// WorkspaceByName returns a workspace for a user that matches the case-insensitive name. +func (c *Client) WorkspaceByName(ctx context.Context, user uuid.UUID, name string) (coderd.Workspace, error) { + return coderd.Workspace{}, nil +} + // 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) +func (c *Client) Workspace(ctx context.Context, id uuid.UUID) (coderd.Workspace, error) { + res, err := c.request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/workspace/%s", id), nil) if err != nil { return coderd.Workspace{}, err } @@ -63,6 +63,16 @@ func (c *Client) Workspace(ctx context.Context, owner, name string) (coderd.Work return workspace, json.NewDecoder(res.Body).Decode(&workspace) } +// WorkspaceProvisions returns a historical list of provision operations for a workspace. +func (c *Client) WorkspaceProvisions(ctx context.Context, workspace uuid.UUID) ([]coderd.WorkspaceHistory, error) { + return nil, nil +} + +// WorkspaceProvision returns +func (c *Client) WorkspaceVersion(ctx context.Context, provision uuid.UUID) (coderd.WorkspaceHistory, error) { + return coderd.WorkspaceHistory{}, nil +} + // ListWorkspaceHistory returns historical data for workspace builds. func (c *Client) ListWorkspaceHistory(ctx context.Context, owner, workspace string) ([]coderd.WorkspaceHistory, error) { if owner == "" { diff --git a/database/dump.sql b/database/dump.sql index 23095aad1819f..16de657652a4c 100644 --- a/database/dump.sql +++ b/database/dump.sql @@ -165,12 +165,12 @@ CREATE TABLE project ( CREATE TABLE project_version ( id uuid NOT NULL, - project_id uuid NOT NULL, + project_id uuid, 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 ( diff --git a/database/migrations/000002_projects.up.sql b/database/migrations/000002_projects.up.sql index 64426143d94d9..2cf6147a5e5a0 100644 --- a/database/migrations/000002_projects.up.sql +++ b/database/migrations/000002_projects.up.sql @@ -33,7 +33,7 @@ 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), created_at timestamptz NOT NULL, updated_at timestamptz NOT NULL, -- Name is generated for ease of differentiation. @@ -42,9 +42,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/models.go b/database/models.go index bf4ffbbe30524..fc1160896e749 100644 --- a/database/models.go +++ b/database/models.go @@ -344,13 +344,13 @@ 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"` + 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 { diff --git a/database/querier.go b/database/querier.go index 20eaf693d99bc..64434bfe7a369 100644 --- a/database/querier.go +++ b/database/querier.go @@ -22,7 +22,7 @@ type querier interface { 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) + GetProjectVersionsByProjectID(ctx context.Context, projectID uuid.NullUUID) ([]ProjectVersion, error) GetProjectsByOrganizationIDs(ctx context.Context, ids []string) ([]Project, error) GetProvisionerDaemonByID(ctx context.Context, id uuid.UUID) (ProvisionerDaemon, error) GetProvisionerDaemons(ctx context.Context) ([]ProvisionerDaemon, error) diff --git a/database/query.sql b/database/query.sql index 8e1d9cbc4b805..a5005b90ec787 100644 --- a/database/query.sql +++ b/database/query.sql @@ -488,7 +488,7 @@ INSERT INTO updated_at, name, description, - import_job_id + job_id ) VALUES ($1, $2, $3, $4, $5, $6, $7) RETURNING *; diff --git a/database/query.sql.go b/database/query.sql.go index aa724700fa662..e8c16a9977917 100644 --- a/database/query.sql.go +++ b/database/query.sql.go @@ -427,7 +427,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, created_at, updated_at, name, description, job_id FROM project_version WHERE @@ -444,14 +444,14 @@ func (q *sqlQuerier) GetProjectVersionByID(ctx context.Context, id uuid.UUID) (P &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, created_at, updated_at, name, description, job_id FROM project_version WHERE @@ -460,8 +460,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) { @@ -474,21 +474,21 @@ func (q *sqlQuerier) GetProjectVersionByProjectIDAndName(ctx context.Context, ar &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, created_at, updated_at, name, description, job_id FROM project_version WHERE project_id = $1 ` -func (q *sqlQuerier) GetProjectVersionsByProjectID(ctx context.Context, projectID uuid.UUID) ([]ProjectVersion, error) { +func (q *sqlQuerier) GetProjectVersionsByProjectID(ctx context.Context, projectID uuid.NullUUID) ([]ProjectVersion, error) { rows, err := q.db.QueryContext(ctx, getProjectVersionsByProjectID, projectID) if err != nil { return nil, err @@ -504,7 +504,7 @@ func (q *sqlQuerier) GetProjectVersionsByProjectID(ctx context.Context, projectI &i.UpdatedAt, &i.Name, &i.Description, - &i.ImportJobID, + &i.JobID, ); err != nil { return nil, err } @@ -1688,20 +1688,20 @@ INSERT INTO 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) RETURNING id, project_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"` + 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) { @@ -1712,7 +1712,7 @@ func (q *sqlQuerier) InsertProjectVersion(ctx context.Context, arg InsertProject arg.UpdatedAt, arg.Name, arg.Description, - arg.ImportJobID, + arg.JobID, ) var i ProjectVersion err := row.Scan( @@ -1722,7 +1722,7 @@ func (q *sqlQuerier) InsertProjectVersion(ctx context.Context, arg InsertProject &i.UpdatedAt, &i.Name, &i.Description, - &i.ImportJobID, + &i.JobID, ) return i, err } From a30cf2295fef825653349f66757057d8edf7afa3 Mon Sep 17 00:00:00 2001 From: Kyle Carberry Date: Sat, 5 Mar 2022 06:46:11 +0000 Subject: [PATCH 05/22] Rename workspace history to workspace build --- cli/workspacecreate.go | 2 +- coderd/coderd.go | 10 +- coderd/coderdtest/coderdtest_test.go | 2 +- coderd/provisionerdaemons.go | 38 +-- coderd/provisionerjobs_test.go | 6 +- coderd/workspaceagent_test.go | 2 +- coderd/workspaceagentauth.go | 8 +- coderd/workspaceagentauth_test.go | 2 +- coderd/workspacehistory.go | 78 +++--- coderd/workspacehistory_test.go | 32 +-- codersdk/workspaces.go | 38 +-- codersdk/workspaces_test.go | 18 +- database/databasefake/databasefake.go | 66 ++--- database/dump.sql | 18 +- database/migrations/000003_workspaces.up.sql | 3 +- database/models.go | 2 +- database/querier.go | 12 +- database/query.sql | 24 +- database/query.sql.go | 174 ++++++------- httpmw/workspacehistoryparam.go | 40 +-- httpmw/workspacehistoryparam_test.go | 28 +-- provisionerd/proto/provisionerd.pb.go | 248 +++++++++---------- provisionerd/proto/provisionerd.proto | 2 +- provisionerd/provisionerd.go | 2 +- 24 files changed, 427 insertions(+), 428 deletions(-) diff --git a/cli/workspacecreate.go b/cli/workspacecreate.go index f4eae0ab5d846..44f6a8d6d055e 100644 --- a/cli/workspacecreate.go +++ b/cli/workspacecreate.go @@ -100,7 +100,7 @@ func workspaceCreate() *cobra.Command { if err != nil { return err } - history, err := client.CreateWorkspaceHistory(cmd.Context(), "", workspace.Name, coderd.CreateWorkspaceHistoryRequest{ + history, err := client.CreateWorkspaceBuild(cmd.Context(), "", workspace.Name, coderd.CreateWorkspaceBuildRequest{ ProjectVersionID: projectVersion.ID, Transition: database.WorkspaceTransitionStart, }) diff --git a/coderd/coderd.go b/coderd/coderd.go index 826a6601c4e69..0cde9a240ee8c 100644 --- a/coderd/coderd.go +++ b/coderd/coderd.go @@ -175,11 +175,11 @@ func New(options *Options) (http.Handler, func()) { 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.Post("/", api.postWorkspaceBuildByUser) + r.Get("/", api.workspaceBuildByUser) + r.Route("/{workspacebuild}", func(r chi.Router) { + r.Use(httpmw.ExtractWorkspaceBuildParam(options.Database)) + r.Get("/", api.workspaceBuildByName) }) }) }) diff --git a/coderd/coderdtest/coderdtest_test.go b/coderd/coderdtest/coderdtest_test.go index cab9fe5a9decc..3a2a05018660c 100644 --- a/coderd/coderdtest/coderdtest_test.go +++ b/coderd/coderdtest/coderdtest_test.go @@ -26,7 +26,7 @@ func TestNew(t *testing.T) { 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(), "me", workspace.Name, coderd.CreateWorkspaceHistoryRequest{ + history, err := client.CreateWorkspaceBuild(context.Background(), "me", workspace.Name, coderd.CreateWorkspaceBuildRequest{ ProjectVersionID: project.ActiveVersionID, Transition: database.WorkspaceTransitionStart, }) diff --git a/coderd/provisionerdaemons.go b/coderd/provisionerdaemons.go index b416ff1f66c93..0a9ab95e8aed3 100644 --- a/coderd/provisionerdaemons.go +++ b/coderd/provisionerdaemons.go @@ -113,8 +113,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. @@ -188,15 +188,15 @@ func (server *provisionerdServer) AcquireJob(ctx context.Context, _ *proto.Empty 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)) } @@ -231,17 +231,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, + WorkspaceBuildId: workspaceBuild.ID.String(), + WorkspaceName: workspace.Name, + State: workspaceBuild.ProvisionerState, + ParameterValues: protoParameters, Metadata: &sdkproto.Provision_Metadata{ CoderUrl: server.AccessURL.String(), WorkspaceTransition: transition, @@ -441,13 +441,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, }) 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_: } @@ -510,9 +510,9 @@ func (server *provisionerdServer) CompleteJob(ctx context.Context, completed *pr 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 +527,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, }) 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) + err = insertProvisionerJobResource(ctx, db, job.ID, workspaceBuild.Transition, protoResource) if err != nil { return xerrors.Errorf("insert provisioner job: %w", err) } diff --git a/coderd/provisionerjobs_test.go b/coderd/provisionerjobs_test.go index e5b2c018e89cb..70f6b68b4972d 100644 --- a/coderd/provisionerjobs_test.go +++ b/coderd/provisionerjobs_test.go @@ -224,7 +224,7 @@ func TestProvisionerJobLogsByName(t *testing.T) { 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) - history, err := client.CreateWorkspaceHistory(context.Background(), "", workspace.Name, coderd.CreateWorkspaceHistoryRequest{ + history, err := client.CreateWorkspaceBuild(context.Background(), "", workspace.Name, coderd.CreateWorkspaceBuildRequest{ ProjectVersionID: project.ActiveVersionID, Transition: database.WorkspaceTransitionStart, }) @@ -261,7 +261,7 @@ func TestProvisionerJobLogsByName(t *testing.T) { coderdtest.AwaitProjectImportJob(t, client, user.Organization, job.ID) workspace := coderdtest.CreateWorkspace(t, client, "me", project.ID) before := time.Now().UTC() - history, err := client.CreateWorkspaceHistory(context.Background(), "", workspace.Name, coderd.CreateWorkspaceHistoryRequest{ + history, err := client.CreateWorkspaceBuild(context.Background(), "", workspace.Name, coderd.CreateWorkspaceBuildRequest{ ProjectVersionID: project.ActiveVersionID, Transition: database.WorkspaceTransitionStart, }) @@ -302,7 +302,7 @@ func TestProvisionerJobLogsByName(t *testing.T) { coderdtest.AwaitProjectImportJob(t, client, user.Organization, job.ID) workspace := coderdtest.CreateWorkspace(t, client, "me", project.ID) before := database.Now() - history, err := client.CreateWorkspaceHistory(context.Background(), "", workspace.Name, coderd.CreateWorkspaceHistoryRequest{ + history, err := client.CreateWorkspaceBuild(context.Background(), "", workspace.Name, coderd.CreateWorkspaceBuildRequest{ ProjectVersionID: project.ActiveVersionID, Transition: database.WorkspaceTransitionStart, }) diff --git a/coderd/workspaceagent_test.go b/coderd/workspaceagent_test.go index a7e6dc57376f4..218dd1fec4701 100644 --- a/coderd/workspaceagent_test.go +++ b/coderd/workspaceagent_test.go @@ -52,7 +52,7 @@ func TestWorkspaceAgentServe(t *testing.T) { 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) - history, err := client.CreateWorkspaceHistory(context.Background(), "", workspace.Name, coderd.CreateWorkspaceHistoryRequest{ + history, err := client.CreateWorkspaceBuild(context.Background(), "", workspace.Name, coderd.CreateWorkspaceBuildRequest{ ProjectVersionID: project.ActiveVersionID, Transition: database.WorkspaceTransitionStart, }) diff --git a/coderd/workspaceagentauth.go b/coderd/workspaceagentauth.go index 3d1913c54eedd..ce3eb6e2eaf20 100644 --- a/coderd/workspaceagentauth.go +++ b/coderd/workspaceagentauth.go @@ -117,20 +117,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/workspaceagentauth_test.go b/coderd/workspaceagentauth_test.go index c48dfc75af1d1..8412fb3819550 100644 --- a/coderd/workspaceagentauth_test.go +++ b/coderd/workspaceagentauth_test.go @@ -91,7 +91,7 @@ func TestPostWorkspaceAgentAuthenticateGoogleInstanceIdentity(t *testing.T) { 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{ + firstHistory, err := client.CreateWorkspaceBuild(context.Background(), "", workspace.Name, coderd.CreateWorkspaceBuildRequest{ ProjectVersionID: project.ActiveVersionID, Transition: database.WorkspaceTransitionStart, }) diff --git a/coderd/workspacehistory.go b/coderd/workspacehistory.go index b94a0e99ff46d..919daf6cbce02 100644 --- a/coderd/workspacehistory.go +++ b/coderd/workspacehistory.go @@ -18,9 +18,9 @@ import ( "github.com/coder/coder/httpmw" ) -// WorkspaceHistory is an at-point representation of a workspace state. +// WorkspaceBuild is an at-point representation of a workspace state. // Iterate on before/after to determine a chronological history. -type WorkspaceHistory struct { +type WorkspaceBuild struct { ID uuid.UUID `json:"id"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` @@ -34,14 +34,14 @@ type WorkspaceHistory struct { ProvisionJobID uuid.UUID `json:"provision_job_id"` } -// CreateWorkspaceHistoryRequest provides options to update the latest workspace history. -type CreateWorkspaceHistoryRequest struct { +// 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"` } -func (api *api) postWorkspaceHistoryByUser(rw http.ResponseWriter, r *http.Request) { - var createBuild CreateWorkspaceHistoryRequest +func (api *api) postWorkspaceBuildByUser(rw http.ResponseWriter, r *http.Request) { + var createBuild CreateWorkspaceBuildRequest if !httpapi.Read(rw, r, &createBuild) { return } @@ -100,7 +100,7 @@ func (api *api) postWorkspaceHistoryByUser(rw http.ResponseWriter, r *http.Reque // 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) + priorHistory, err := api.Database.GetWorkspaceBuildByWorkspaceIDWithoutAfter(r.Context(), workspace.ID) if err == nil { priorJob, err := api.Database.GetProvisionerJobByID(r.Context(), priorHistory.ProvisionJobID) if err == nil && !convertProvisionerJob(priorJob).Status.Completed() { @@ -116,17 +116,17 @@ func (api *api) postWorkspaceHistoryByUser(rw http.ResponseWriter, r *http.Reque } } else if !errors.Is(err, sql.ErrNoRows) { httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ - Message: fmt.Sprintf("get prior workspace history: %s", err), + Message: fmt.Sprintf("get prior workspace build: %s", err), }) return } - var workspaceHistory database.WorkspaceHistory + var workspaceBuild database.WorkspaceBuild // 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{ + workspaceBuild, err = db.InsertWorkspaceBuild(r.Context(), database.InsertWorkspaceBuildParams{ ID: uuid.New(), CreatedAt: database.Now(), UpdatedAt: database.Now(), @@ -139,11 +139,11 @@ func (api *api) postWorkspaceHistoryByUser(rw http.ResponseWriter, r *http.Reque ProvisionJobID: provisionerJobID, }) if err != nil { - return xerrors.Errorf("insert workspace history: %w", err) + return xerrors.Errorf("insert workspace build: %w", err) } input, err := json.Marshal(workspaceProvisionJob{ - WorkspaceHistoryID: workspaceHistory.ID, + WorkspaceBuildID: workspaceBuild.ID, }) if err != nil { return xerrors.Errorf("marshal provision job: %w", err) @@ -167,17 +167,17 @@ func (api *api) postWorkspaceHistoryByUser(rw http.ResponseWriter, r *http.Reque if priorHistoryID.Valid { // Update the prior history entries "after" column. - err = db.UpdateWorkspaceHistoryByID(r.Context(), database.UpdateWorkspaceHistoryByIDParams{ + err = db.UpdateWorkspaceBuildByID(r.Context(), database.UpdateWorkspaceBuildByIDParams{ ID: priorHistory.ID, ProvisionerState: priorHistory.ProvisionerState, UpdatedAt: database.Now(), AfterID: uuid.NullUUID{ - UUID: workspaceHistory.ID, + UUID: workspaceBuild.ID, Valid: true, }, }) if err != nil { - return xerrors.Errorf("update prior workspace history: %w", err) + return xerrors.Errorf("update prior workspace build: %w", err) } } @@ -191,54 +191,54 @@ func (api *api) postWorkspaceHistoryByUser(rw http.ResponseWriter, r *http.Reque } render.Status(r, http.StatusCreated) - render.JSON(rw, r, convertWorkspaceHistory(workspaceHistory)) + render.JSON(rw, r, convertWorkspaceBuild(workspaceBuild)) } -// Returns all workspace history. This is not sorted. Use before/after to chronologically sort. -func (api *api) workspaceHistoryByUser(rw http.ResponseWriter, r *http.Request) { +// Returns all workspace build. This is not sorted. Use before/after to chronologically sort. +func (api *api) workspaceBuildByUser(rw http.ResponseWriter, r *http.Request) { workspace := httpmw.WorkspaceParam(r) - history, err := api.Database.GetWorkspaceHistoryByWorkspaceID(r.Context(), workspace.ID) + history, err := api.Database.GetWorkspaceBuildByWorkspaceID(r.Context(), workspace.ID) if errors.Is(err, sql.ErrNoRows) { err = nil - history = []database.WorkspaceHistory{} + history = []database.WorkspaceBuild{} } 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 } - apiHistory := make([]WorkspaceHistory, 0, len(history)) + apiHistory := make([]WorkspaceBuild, 0, len(history)) for _, history := range history { - apiHistory = append(apiHistory, convertWorkspaceHistory(history)) + apiHistory = append(apiHistory, convertWorkspaceBuild(history)) } render.Status(r, http.StatusOK) render.JSON(rw, r, apiHistory) } -func (*api) workspaceHistoryByName(rw http.ResponseWriter, r *http.Request) { - workspaceHistory := httpmw.WorkspaceHistoryParam(r) +func (*api) workspaceBuildByName(rw http.ResponseWriter, r *http.Request) { + workspaceBuild := httpmw.WorkspaceBuildParam(r) render.Status(r, http.StatusOK) - render.JSON(rw, r, convertWorkspaceHistory(workspaceHistory)) + render.JSON(rw, r, convertWorkspaceBuild(workspaceBuild)) } // Converts the internal history representation to a public external-facing model. -func convertWorkspaceHistory(workspaceHistory database.WorkspaceHistory) WorkspaceHistory { +func convertWorkspaceBuild(workspaceBuild database.WorkspaceBuild) WorkspaceBuild { //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, + 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, + ProvisionJobID: workspaceBuild.ProvisionJobID, }) } diff --git a/coderd/workspacehistory_test.go b/coderd/workspacehistory_test.go index f00ebdaff5679..a189eb432f909 100644 --- a/coderd/workspacehistory_test.go +++ b/coderd/workspacehistory_test.go @@ -16,7 +16,7 @@ import ( "github.com/coder/coder/provisionersdk/proto" ) -func TestPostWorkspaceHistoryByUser(t *testing.T) { +func TestPostWorkspaceBuildByUser(t *testing.T) { t.Parallel() t.Run("NoProjectVersion", func(t *testing.T) { t.Parallel() @@ -25,7 +25,7 @@ func TestPostWorkspaceHistoryByUser(t *testing.T) { 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{ + _, err := client.CreateWorkspaceBuild(context.Background(), "", workspace.Name, coderd.CreateWorkspaceBuildRequest{ ProjectVersionID: uuid.New(), Transition: database.WorkspaceTransitionStart, }) @@ -46,7 +46,7 @@ func TestPostWorkspaceHistoryByUser(t *testing.T) { 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{ + _, err := client.CreateWorkspaceBuild(context.Background(), "", workspace.Name, coderd.CreateWorkspaceBuildRequest{ ProjectVersionID: project.ActiveVersionID, Transition: database.WorkspaceTransitionStart, }) @@ -64,15 +64,15 @@ func TestPostWorkspaceHistoryByUser(t *testing.T) { 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! + // Close here so workspace build doesn't process! closeDaemon.Close() workspace := coderdtest.CreateWorkspace(t, client, "me", project.ID) - _, err := client.CreateWorkspaceHistory(context.Background(), "", workspace.Name, coderd.CreateWorkspaceHistoryRequest{ + _, err := client.CreateWorkspaceBuild(context.Background(), "", workspace.Name, coderd.CreateWorkspaceBuildRequest{ ProjectVersionID: project.ActiveVersionID, Transition: database.WorkspaceTransitionStart, }) require.NoError(t, err) - _, err = client.CreateWorkspaceHistory(context.Background(), "", workspace.Name, coderd.CreateWorkspaceHistoryRequest{ + _, err = client.CreateWorkspaceBuild(context.Background(), "", workspace.Name, coderd.CreateWorkspaceBuildRequest{ ProjectVersionID: project.ActiveVersionID, Transition: database.WorkspaceTransitionStart, }) @@ -91,26 +91,26 @@ func TestPostWorkspaceHistoryByUser(t *testing.T) { 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{ + firstHistory, err := client.CreateWorkspaceBuild(context.Background(), "", workspace.Name, coderd.CreateWorkspaceBuildRequest{ 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{ + secondHistory, err := client.CreateWorkspaceBuild(context.Background(), "", workspace.Name, coderd.CreateWorkspaceBuildRequest{ 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) + firstHistory, err = client.WorkspaceBuild(context.Background(), "", workspace.Name, firstHistory.Name) require.NoError(t, err) require.Equal(t, secondHistory.ID.String(), firstHistory.AfterID.String()) }) } -func TestWorkspaceHistoryByUser(t *testing.T) { +func TestWorkspaceBuildByUser(t *testing.T) { t.Parallel() t.Run("ListEmpty", func(t *testing.T) { t.Parallel() @@ -120,7 +120,7 @@ func TestWorkspaceHistoryByUser(t *testing.T) { 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) + history, err := client.ListWorkspaceBuild(context.Background(), "me", workspace.Name) require.NoError(t, err) require.NotNil(t, history) require.Len(t, history, 0) @@ -135,19 +135,19 @@ func TestWorkspaceHistoryByUser(t *testing.T) { 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{ + _, err := client.CreateWorkspaceBuild(context.Background(), "", workspace.Name, coderd.CreateWorkspaceBuildRequest{ ProjectVersionID: project.ActiveVersionID, Transition: database.WorkspaceTransitionStart, }) require.NoError(t, err) - history, err := client.ListWorkspaceHistory(context.Background(), "me", workspace.Name) + history, err := client.ListWorkspaceBuild(context.Background(), "me", workspace.Name) require.NoError(t, err) require.NotNil(t, history) require.Len(t, history, 1) }) } -func TestWorkspaceHistoryByName(t *testing.T) { +func TestWorkspaceBuildByName(t *testing.T) { t.Parallel() client := coderdtest.New(t, nil) user := coderdtest.CreateInitialUser(t, client) @@ -156,11 +156,11 @@ func TestWorkspaceHistoryByName(t *testing.T) { 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{ + history, err := client.CreateWorkspaceBuild(context.Background(), "", workspace.Name, coderd.CreateWorkspaceBuildRequest{ ProjectVersionID: project.ActiveVersionID, Transition: database.WorkspaceTransitionStart, }) require.NoError(t, err) - _, err = client.WorkspaceHistory(context.Background(), "me", workspace.Name, history.Name) + _, err = client.WorkspaceBuild(context.Background(), "me", workspace.Name, history.Name) require.NoError(t, err) } diff --git a/codersdk/workspaces.go b/codersdk/workspaces.go index 86ad03b81e905..990724606eb0b 100644 --- a/codersdk/workspaces.go +++ b/codersdk/workspaces.go @@ -64,17 +64,17 @@ func (c *Client) Workspace(ctx context.Context, id uuid.UUID) (coderd.Workspace, } // WorkspaceProvisions returns a historical list of provision operations for a workspace. -func (c *Client) WorkspaceProvisions(ctx context.Context, workspace uuid.UUID) ([]coderd.WorkspaceHistory, error) { +func (c *Client) WorkspaceProvisions(ctx context.Context, workspace uuid.UUID) ([]coderd.WorkspaceBuild, error) { return nil, nil } // WorkspaceProvision returns -func (c *Client) WorkspaceVersion(ctx context.Context, provision uuid.UUID) (coderd.WorkspaceHistory, error) { - return coderd.WorkspaceHistory{}, nil +func (c *Client) WorkspaceVersion(ctx context.Context, provision uuid.UUID) (coderd.WorkspaceBuild, error) { + return coderd.WorkspaceBuild{}, nil } -// ListWorkspaceHistory returns historical data for workspace builds. -func (c *Client) ListWorkspaceHistory(ctx context.Context, owner, workspace string) ([]coderd.WorkspaceHistory, error) { +// ListWorkspaceBuild returns historical data for workspace builds. +func (c *Client) ListWorkspaceBuild(ctx context.Context, owner, workspace string) ([]coderd.WorkspaceBuild, error) { if owner == "" { owner = "me" } @@ -86,13 +86,13 @@ 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. +// WorkspaceBuild returns a single workspace build 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) { +func (c *Client) WorkspaceBuild(ctx context.Context, owner, workspace, history string) (coderd.WorkspaceBuild, error) { if owner == "" { owner = "me" } @@ -101,14 +101,14 @@ func (c *Client) WorkspaceHistory(ctx context.Context, owner, workspace, history } res, err := c.request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/workspaces/%s/%s/version/%s", owner, workspace, history), nil) if err != nil { - return coderd.WorkspaceHistory{}, err + return coderd.WorkspaceBuild{}, err } defer res.Body.Close() if res.StatusCode != http.StatusOK { - return coderd.WorkspaceHistory{}, readBodyAsError(res) + 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) } // CreateWorkspace creates a new workspace for the project specified. @@ -128,21 +128,21 @@ func (c *Client) CreateWorkspace(ctx context.Context, user string, request coder return workspace, json.NewDecoder(res.Body).Decode(&workspace) } -// 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) { +// CreateWorkspaceBuild queues a new build to occur for a workspace. +func (c *Client) CreateWorkspaceBuild(ctx context.Context, owner, workspace string, request coderd.CreateWorkspaceBuildRequest) (coderd.WorkspaceBuild, error) { if owner == "" { owner = "me" } res, err := c.request(ctx, http.MethodPost, fmt.Sprintf("/api/v2/workspaces/%s/%s/version", owner, workspace), request) 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) + 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) { diff --git a/codersdk/workspaces_test.go b/codersdk/workspaces_test.go index 74d7a21e10bf2..100a1a9573b7d 100644 --- a/codersdk/workspaces_test.go +++ b/codersdk/workspaces_test.go @@ -70,12 +70,12 @@ func TestWorkspace(t *testing.T) { }) } -func TestListWorkspaceHistory(t *testing.T) { +func TestListWorkspaceBuild(t *testing.T) { t.Parallel() t.Run("Error", func(t *testing.T) { t.Parallel() client := coderdtest.New(t, nil) - _, err := client.ListWorkspaceHistory(context.Background(), "", "") + _, err := client.ListWorkspaceBuild(context.Background(), "", "") require.Error(t, err) }) @@ -86,17 +86,17 @@ func TestListWorkspaceHistory(t *testing.T) { 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) + _, err := client.ListWorkspaceBuild(context.Background(), "", workspace.Name) require.NoError(t, err) }) } -func TestWorkspaceHistory(t *testing.T) { +func TestWorkspaceBuild(t *testing.T) { t.Parallel() t.Run("Error", func(t *testing.T) { t.Parallel() client := coderdtest.New(t, nil) - _, err := client.WorkspaceHistory(context.Background(), "", "", "") + _, err := client.WorkspaceBuild(context.Background(), "", "", "") require.Error(t, err) }) @@ -109,7 +109,7 @@ func TestWorkspaceHistory(t *testing.T) { 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{ + _, err := client.CreateWorkspaceBuild(context.Background(), "", workspace.Name, coderd.CreateWorkspaceBuildRequest{ ProjectVersionID: project.ActiveVersionID, Transition: database.WorkspaceTransitionStart, }) @@ -136,12 +136,12 @@ func TestCreateWorkspace(t *testing.T) { }) } -func TestCreateWorkspaceHistory(t *testing.T) { +func TestCreateWorkspaceBuild(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{}) + _, err := client.CreateWorkspaceBuild(context.Background(), "", "", coderd.CreateWorkspaceBuildRequest{}) require.Error(t, err) }) @@ -154,7 +154,7 @@ func TestCreateWorkspaceHistory(t *testing.T) { 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{ + _, err := client.CreateWorkspaceBuild(context.Background(), "", workspace.Name, coderd.CreateWorkspaceBuildRequest{ ProjectVersionID: project.ActiveVersionID, Transition: database.WorkspaceTransitionStart, }) diff --git a/database/databasefake/databasefake.go b/database/databasefake/databasefake.go index 214e6f45bb5d8..39655221fa9ea 100644 --- a/database/databasefake/databasefake.go +++ b/database/databasefake/databasefake.go @@ -29,7 +29,7 @@ func New() database.Store { provisionerJobLog: make([]database.ProvisionerJobLog, 0), workspace: make([]database.Workspace, 0), provisionerJobResource: make([]database.ProvisionerJobResource, 0), - workspaceHistory: make([]database.WorkspaceHistory, 0), + workspaceBuild: make([]database.WorkspaceBuild, 0), provisionerJobAgent: make([]database.ProvisionerJobAgent, 0), } } @@ -56,7 +56,7 @@ type fakeQuerier struct { provisionerJobResource []database.ProvisionerJobResource provisionerJobLog []database.ProvisionerJobLog workspace []database.Workspace - workspaceHistory []database.WorkspaceHistory + workspaceBuild []database.WorkspaceBuild } // InTx doesn't rollback data properly for in-memory yet. @@ -210,41 +210,41 @@ 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) GetWorkspaceBuildByWorkspaceIDWithoutAfter(_ context.Context, workspaceID uuid.UUID) (database.WorkspaceBuild, error) { q.mutex.Lock() defer q.mutex.Unlock() - for _, workspaceHistory := range q.workspaceHistory { - if workspaceHistory.WorkspaceID.String() != workspaceID.String() { + for _, workspaceBuild := range q.workspaceBuild { + if workspaceBuild.WorkspaceID.String() != workspaceID.String() { continue } - if !workspaceHistory.AfterID.Valid { - return workspaceHistory, nil + if !workspaceBuild.AfterID.Valid { + return workspaceBuild, 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) GetWorkspaceBuildByWorkspaceID(_ 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) + history := make([]database.WorkspaceBuild, 0) + for _, workspaceBuild := range q.workspaceBuild { + if workspaceBuild.WorkspaceID.String() == workspaceID.String() { + history = append(history, workspaceBuild) } } if len(history) == 0 { @@ -253,20 +253,20 @@ func (q *fakeQuerier) GetWorkspaceHistoryByWorkspaceID(_ context.Context, worksp return history, nil } -func (q *fakeQuerier) GetWorkspaceHistoryByWorkspaceIDAndName(_ context.Context, arg database.GetWorkspaceHistoryByWorkspaceIDAndNameParams) (database.WorkspaceHistory, error) { +func (q *fakeQuerier) GetWorkspaceBuildByWorkspaceIDAndName(_ context.Context, arg database.GetWorkspaceBuildByWorkspaceIDAndNameParams) (database.WorkspaceBuild, error) { q.mutex.Lock() defer q.mutex.Unlock() - for _, workspaceHistory := range q.workspaceHistory { - if workspaceHistory.WorkspaceID.String() != arg.WorkspaceID.String() { + for _, workspaceBuild := range q.workspaceBuild { + if workspaceBuild.WorkspaceID.String() != arg.WorkspaceID.String() { continue } - if !strings.EqualFold(workspaceHistory.Name, arg.Name) { + if !strings.EqualFold(workspaceBuild.Name, arg.Name) { continue } - return workspaceHistory, nil + return workspaceBuild, nil } - return database.WorkspaceHistory{}, sql.ErrNoRows + return database.WorkspaceBuild{}, sql.ErrNoRows } func (q *fakeQuerier) GetWorkspacesByProjectAndUserID(_ context.Context, arg database.GetWorkspacesByProjectAndUserIDParams) ([]database.Workspace, error) { @@ -906,11 +906,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, @@ -923,8 +923,8 @@ func (q *fakeQuerier) InsertWorkspaceHistory(_ context.Context, arg database.Ins ProvisionJobID: arg.ProvisionJobID, 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 { @@ -1010,18 +1010,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 16de657652a4c..8c7337416cf82 100644 --- a/database/dump.sql +++ b/database/dump.sql @@ -262,7 +262,7 @@ CREATE TABLE workspace ( name character varying(64) NOT NULL ); -CREATE TABLE workspace_history ( +CREATE TABLE workspace_build ( id uuid NOT NULL, created_at timestamp with time zone NOT NULL, updated_at timestamp with time zone NOT NULL, @@ -325,11 +325,11 @@ ALTER TABLE ONLY provisioner_job_log ALTER TABLE ONLY provisioner_job_resource ADD CONSTRAINT provisioner_job_resource_id_key UNIQUE (id); -ALTER TABLE ONLY workspace_history - ADD CONSTRAINT workspace_history_id_key UNIQUE (id); +ALTER TABLE ONLY workspace_build + ADD CONSTRAINT workspace_build_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_workspace_id_name_key UNIQUE (workspace_id, name); ALTER TABLE ONLY workspace ADD CONSTRAINT workspace_id_key UNIQUE (id); @@ -352,11 +352,11 @@ ALTER TABLE ONLY provisioner_job_log 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_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); diff --git a/database/migrations/000003_workspaces.up.sql b/database/migrations/000003_workspaces.up.sql index 97e3083d31a5f..f2b23adedcb9f 100644 --- a/database/migrations/000003_workspaces.up.sql +++ b/database/migrations/000003_workspaces.up.sql @@ -14,8 +14,7 @@ CREATE TYPE workspace_transition AS ENUM ( 'delete' ); --- Workspace transition represents a change in workspace state. -CREATE TABLE workspace_history ( +CREATE TABLE workspace_build ( id uuid NOT NULL UNIQUE, created_at timestamptz NOT NULL, updated_at timestamptz NOT NULL, diff --git a/database/models.go b/database/models.go index fc1160896e749..1194a0ab423eb 100644 --- a/database/models.go +++ b/database/models.go @@ -442,7 +442,7 @@ type Workspace struct { Name string `db:"name" json:"name"` } -type WorkspaceHistory struct { +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"` diff --git a/database/querier.go b/database/querier.go index 64434bfe7a369..9b9628ecaccf4 100644 --- a/database/querier.go +++ b/database/querier.go @@ -36,12 +36,12 @@ type querier interface { GetUserByEmailOrUsername(ctx context.Context, arg GetUserByEmailOrUsernameParams) (User, error) GetUserByID(ctx context.Context, id string) (User, error) GetUserCount(ctx context.Context) (int64, error) + GetWorkspaceBuildByID(ctx context.Context, id 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) GetWorkspacesByUserID(ctx context.Context, ownerID string) ([]Workspace, error) @@ -60,13 +60,13 @@ type querier interface { 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) + InsertWorkspaceBuild(ctx context.Context, arg InsertWorkspaceBuildParams) (WorkspaceBuild, error) UpdateAPIKeyByID(ctx context.Context, arg UpdateAPIKeyByIDParams) error UpdateProvisionerDaemonByID(ctx context.Context, arg UpdateProvisionerDaemonByIDParams) error UpdateProvisionerJobAgentByID(ctx context.Context, arg UpdateProvisionerJobAgentByIDParams) error UpdateProvisionerJobByID(ctx context.Context, arg UpdateProvisionerJobByIDParams) error UpdateProvisionerJobWithCompleteByID(ctx context.Context, arg UpdateProvisionerJobWithCompleteByIDParams) error - UpdateWorkspaceHistoryByID(ctx context.Context, arg UpdateWorkspaceHistoryByIDParams) error + UpdateWorkspaceBuildByID(ctx context.Context, arg UpdateWorkspaceBuildByIDParams) error } var _ querier = (*sqlQuerier)(nil) diff --git a/database/query.sql b/database/query.sql index a5005b90ec787..500a13bcd7985 100644 --- a/database/query.sql +++ b/database/query.sql @@ -300,38 +300,38 @@ 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: GetWorkspaceBuildByWorkspaceIDAndName :one SELECT * FROM - workspace_history + 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 @@ -602,9 +602,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, @@ -668,9 +668,9 @@ SET WHERE id = $1; --- name: UpdateWorkspaceHistoryByID :exec +-- 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 e8c16a9977917..f1730b47d4f54 100644 --- a/database/query.sql.go +++ b/database/query.sql.go @@ -943,74 +943,20 @@ func (q *sqlQuerier) GetUserCount(ctx context.Context) (int64, error) { return count, 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 -} - -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 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, @@ -1028,24 +974,24 @@ func (q *sqlQuerier) GetWorkspaceHistoryByID(ctx context.Context, id uuid.UUID) 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 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, @@ -1073,24 +1019,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 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, @@ -1108,11 +1054,11 @@ func (q *sqlQuerier) GetWorkspaceHistoryByWorkspaceIDAndName(ctx context.Context 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 FROM - workspace_history + workspace_build WHERE workspace_id = $1 AND after_id IS NULL @@ -1120,9 +1066,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, @@ -1140,6 +1086,60 @@ func (q *sqlQuerier) GetWorkspaceHistoryByWorkspaceIDWithoutAfter(ctx context.Co 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 +} + const getWorkspaceOwnerCountsByProjectIDs = `-- name: GetWorkspaceOwnerCountsByProjectIDs :many SELECT project_id, @@ -2098,9 +2098,9 @@ func (q *sqlQuerier) InsertWorkspace(ctx context.Context, arg InsertWorkspacePar return i, err } -const insertWorkspaceHistory = `-- name: InsertWorkspaceHistory :one +const insertWorkspaceBuild = `-- name: InsertWorkspaceBuild :one INSERT INTO - workspace_history ( + workspace_build ( id, created_at, updated_at, @@ -2117,7 +2117,7 @@ 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 ` -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"` @@ -2131,8 +2131,8 @@ type InsertWorkspaceHistoryParams struct { 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, @@ -2145,7 +2145,7 @@ func (q *sqlQuerier) InsertWorkspaceHistory(ctx context.Context, arg InsertWorks arg.ProvisionJobID, arg.ProvisionerState, ) - var i WorkspaceHistory + var i WorkspaceBuild err := row.Scan( &i.ID, &i.CreatedAt, @@ -2287,9 +2287,9 @@ func (q *sqlQuerier) UpdateProvisionerJobWithCompleteByID(ctx context.Context, a return err } -const updateWorkspaceHistoryByID = `-- name: UpdateWorkspaceHistoryByID :exec +const updateWorkspaceBuildByID = `-- name: UpdateWorkspaceBuildByID :exec UPDATE - workspace_history + workspace_build SET updated_at = $2, after_id = $3, @@ -2298,15 +2298,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/httpmw/workspacehistoryparam.go b/httpmw/workspacehistoryparam.go index ff426faf23c83..c34156387f3b1 100644 --- a/httpmw/workspacehistoryparam.go +++ b/httpmw/workspacehistoryparam.go @@ -13,59 +13,59 @@ import ( "github.com/coder/coder/httpapi" ) -type workspaceHistoryParamContextKey struct{} +type workspaceBuildParamContextKey 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) +// 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 history param middleware not provided") + panic("developer error: workspace build param middleware not provided") } - return workspaceHistory + return workspaceBuild } -// ExtractWorkspaceHistoryParam grabs workspace history from the "workspacehistory" URL parameter. -func ExtractWorkspaceHistoryParam(db database.Store) func(http.Handler) http.Handler { +// 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) { workspace := WorkspaceParam(r) - workspaceHistoryName := chi.URLParam(r, "workspacehistory") - if workspaceHistoryName == "" { + workspaceBuildName := chi.URLParam(r, "workspacebuild") + if workspaceBuildName == "" { httpapi.Write(rw, http.StatusBadRequest, httpapi.Response{ - Message: "workspace history name must be provided", + Message: "workspace build name must be provided", }) return } - var workspaceHistory database.WorkspaceHistory + var workspaceBuild database.WorkspaceBuild var err error - if workspaceHistoryName == "latest" { - workspaceHistory, err = db.GetWorkspaceHistoryByWorkspaceIDWithoutAfter(r.Context(), workspace.ID) + if workspaceBuildName == "latest" { + workspaceBuild, err = db.GetWorkspaceBuildByWorkspaceIDWithoutAfter(r.Context(), workspace.ID) if errors.Is(err, sql.ErrNoRows) { httpapi.Write(rw, http.StatusNotFound, httpapi.Response{ - Message: "there is no workspace history", + Message: "there is no workspace build", }) return } } else { - workspaceHistory, err = db.GetWorkspaceHistoryByWorkspaceIDAndName(r.Context(), database.GetWorkspaceHistoryByWorkspaceIDAndNameParams{ + workspaceBuild, err = db.GetWorkspaceBuildByWorkspaceIDAndName(r.Context(), database.GetWorkspaceBuildByWorkspaceIDAndNameParams{ WorkspaceID: workspace.ID, - Name: workspaceHistoryName, + Name: workspaceBuildName, }) if errors.Is(err, sql.ErrNoRows) { httpapi.Write(rw, http.StatusNotFound, httpapi.Response{ - Message: fmt.Sprintf("workspace history %q does not exist", workspaceHistoryName), + Message: fmt.Sprintf("workspace build %q does not exist", workspaceBuildName), }) return } } if err != nil { httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ - Message: fmt.Sprintf("get workspace history: %s", err.Error()), + Message: fmt.Sprintf("get workspace build: %s", err.Error()), }) return } - ctx := context.WithValue(r.Context(), workspaceHistoryParamContextKey{}, workspaceHistory) + ctx := context.WithValue(r.Context(), workspaceBuildParamContextKey{}, workspaceBuild) next.ServeHTTP(rw, r.WithContext(ctx)) }) } diff --git a/httpmw/workspacehistoryparam_test.go b/httpmw/workspacehistoryparam_test.go index 063f2fd7be3ca..c30153c7948ec 100644 --- a/httpmw/workspacehistoryparam_test.go +++ b/httpmw/workspacehistoryparam_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) { @@ -78,7 +78,7 @@ func TestWorkspaceHistoryParam(t *testing.T) { httpmw.ExtractAPIKey(db, nil), httpmw.ExtractUserParam(db), httpmw.ExtractWorkspaceParam(db), - httpmw.ExtractWorkspaceHistoryParam(db), + httpmw.ExtractWorkspaceBuildParam(db), ) rtr.Get("/", nil) r, _ := setupAuthentication(db) @@ -98,12 +98,12 @@ func TestWorkspaceHistoryParam(t *testing.T) { httpmw.ExtractAPIKey(db, nil), httpmw.ExtractUserParam(db), httpmw.ExtractWorkspaceParam(db), - httpmw.ExtractWorkspaceHistoryParam(db), + httpmw.ExtractWorkspaceBuildParam(db), ) rtr.Get("/", nil) r, _ := setupAuthentication(db) - chi.RouteContext(r.Context()).URLParams.Add("workspacehistory", "nothin") + chi.RouteContext(r.Context()).URLParams.Add("workspacebuild", "nothin") rw := httptest.NewRecorder() rtr.ServeHTTP(rw, r) @@ -112,7 +112,7 @@ func TestWorkspaceHistoryParam(t *testing.T) { require.Equal(t, http.StatusNotFound, res.StatusCode) }) - t.Run("WorkspaceHistory", func(t *testing.T) { + t.Run("WorkspaceBuild", func(t *testing.T) { t.Parallel() db := databasefake.New() rtr := chi.NewRouter() @@ -120,21 +120,21 @@ func TestWorkspaceHistoryParam(t *testing.T) { httpmw.ExtractAPIKey(db, nil), httpmw.ExtractUserParam(db), httpmw.ExtractWorkspaceParam(db), - httpmw.ExtractWorkspaceHistoryParam(db), + httpmw.ExtractWorkspaceBuildParam(db), ) rtr.Get("/", func(rw http.ResponseWriter, r *http.Request) { - _ = httpmw.WorkspaceHistoryParam(r) + _ = httpmw.WorkspaceBuildParam(r) rw.WriteHeader(http.StatusOK) }) r, workspace := setupAuthentication(db) - workspaceHistory, 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", workspaceHistory.Name) + chi.RouteContext(r.Context()).URLParams.Add("workspacebuild", workspaceBuild.Name) rw := httptest.NewRecorder() rtr.ServeHTTP(rw, r) @@ -143,7 +143,7 @@ func TestWorkspaceHistoryParam(t *testing.T) { require.Equal(t, http.StatusOK, res.StatusCode) }) - t.Run("WorkspaceHistoryLatest", func(t *testing.T) { + t.Run("WorkspaceBuildLatest", func(t *testing.T) { t.Parallel() db := databasefake.New() rtr := chi.NewRouter() @@ -151,21 +151,21 @@ func TestWorkspaceHistoryParam(t *testing.T) { httpmw.ExtractAPIKey(db, nil), httpmw.ExtractUserParam(db), httpmw.ExtractWorkspaceParam(db), - httpmw.ExtractWorkspaceHistoryParam(db), + httpmw.ExtractWorkspaceBuildParam(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{ + _, 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", "latest") rw := httptest.NewRecorder() rtr.ServeHTTP(rw, r) diff --git a/provisionerd/proto/provisionerd.pb.go b/provisionerd/proto/provisionerd.pb.go index 8954b6a5ae16a..51855606811d4 100644 --- a/provisionerd/proto/provisionerd.pb.go +++ b/provisionerd/proto/provisionerd.pb.go @@ -603,11 +603,11 @@ type AcquiredJob_WorkspaceProvision struct { 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() { @@ -642,9 +642,9 @@ func (*AcquiredJob_WorkspaceProvision) Descriptor() ([]byte, []int) { return file_provisionerd_proto_provisionerd_proto_rawDescGZIP(), []int{1, 0} } -func (x *AcquiredJob_WorkspaceProvision) GetWorkspaceHistoryId() string { +func (x *AcquiredJob_WorkspaceProvision) GetWorkspaceBuildId() string { if x != nil { - return x.WorkspaceHistoryId + return x.WorkspaceBuildId } return "" } @@ -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, 0xc8, 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, @@ -951,127 +951,127 @@ var file_provisionerd_proto_provisionerd_proto_rawDesc = []byte{ 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, + 0x70, 0x6f, 0x72, 0x74, 0x1a, 0x84, 0x02, 0x0a, 0x12, 0x57, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, + 0x63, 0x65, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 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, - 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, + 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, 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, - 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, + 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, 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, 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, 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 ( diff --git a/provisionerd/proto/provisionerd.proto b/provisionerd/proto/provisionerd.proto index 55c546ef7676b..b867200f5a86c 100644 --- a/provisionerd/proto/provisionerd.proto +++ b/provisionerd/proto/provisionerd.proto @@ -12,7 +12,7 @@ message Empty {} // AcquiredJob is returned when a provisioner daemon has a job locked. message AcquiredJob { message WorkspaceProvision { - string workspace_history_id = 1; + string workspace_build_id = 1; string workspace_name = 2; repeated provisioner.ParameterValue parameter_values = 3; provisioner.Provision.Metadata metadata = 4; diff --git a/provisionerd/provisionerd.go b/provisionerd/provisionerd.go index e7f2bd691a694..a9f76159cd704 100644 --- a/provisionerd/provisionerd.go +++ b/provisionerd/provisionerd.go @@ -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.GetWorkspaceProvision().WorkspaceBuildId), ) _, err = p.client.UpdateJob(ctx, &proto.UpdateJobRequest{ From b67f8d43ebfe6262e0905efba8662002f6831a18 Mon Sep 17 00:00:00 2001 From: Kyle Carberry Date: Sat, 5 Mar 2022 16:55:43 +0000 Subject: [PATCH 06/22] Refactor HTTP middlewares to use UUIDs --- coderd/coderd.go | 215 +++++------------- database/databasefake/databasefake.go | 28 +-- database/dump.sql | 2 + database/migrations/000002_projects.up.sql | 1 + database/migrations/000004_jobs.up.sql | 1 + database/models.go | 26 ++- database/querier.go | 2 +- database/query.sql | 9 +- database/query.sql.go | 56 +++-- go.mod | 2 +- httpmw/apikey_test.go | 1 + httpmw/httpmw.go | 61 +++++ httpmw/organizationparam.go | 12 +- httpmw/organizationparam_test.go | 4 +- httpmw/projectparam.go | 19 +- httpmw/projectparam_test.go | 19 +- httpmw/projectversionparam.go | 23 +- httpmw/projectversionparam_test.go | 27 +-- httpmw/provisionerjobparam.go | 64 ------ httpmw/provisionerjobparam_test.go | 109 --------- ...historyparam.go => workspacebuildparam.go} | 36 +-- ...am_test.go => workspacebuildparam_test.go} | 52 +---- httpmw/workspaceparam.go | 23 +- httpmw/workspaceparam_test.go | 17 +- httpmw/workspaceresourceparam.go | 24 +- httpmw/workspaceresourceparam_test.go | 31 +-- 26 files changed, 272 insertions(+), 592 deletions(-) create mode 100644 httpmw/httpmw.go delete mode 100644 httpmw/provisionerjobparam.go delete mode 100644 httpmw/provisionerjobparam_test.go rename httpmw/{workspacehistoryparam.go => workspacebuildparam.go} (54%) rename httpmw/{workspacehistoryparam_test.go => workspacebuildparam_test.go} (73%) diff --git a/coderd/coderd.go b/coderd/coderd.go index 0cde9a240ee8c..a150ac63d8bb8 100644 --- a/coderd/coderd.go +++ b/coderd/coderd.go @@ -44,27 +44,28 @@ func New(options *Options) (http.Handler, func()) { 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.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("/keys", api.postKeyForUser) + r.Route("/user", func(r chi.Router) { + r.Get("/first", api.user) + r.Post("/first", api.user) + r.Group(func(r chi.Router) { + r.Use(httpmw.ExtractAPIKey(options.Database, nil)) + r.Post("/", nil) + 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.Route("/organization/{organization}", func(r chi.Router) { r.Use( httpmw.ExtractAPIKey(options.Database, nil), httpmw.ExtractOrganizationParam(options.Database), ) + r.Post("/users", nil) + r.Get("/provisionerdaemons", nil) r.Route("/projects", func(r chi.Router) { r.Post("/", api.postProjectsByOrganization) r.Get("/", api.projectsByOrganization) @@ -75,172 +76,70 @@ func New(options *Options) (http.Handler, func()) { r.Use( httpmw.ExtractAPIKey(options.Database, nil), httpmw.ExtractProjectParam(options.Database), + httpmw.ExtractOrganizationParam(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.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.Get("/parameters", api.parametersByProject) + r.Post("/parameters", api.postParametersByProject) + r.Get("/versions", api.projectVersionsByOrganization) + r.Post("/versions", api.postProjectVersionByOrganization) }) - - // Upload - Uploads a file. - - // CreateProject(organization) - Creates a project with a version. - // ImportProjectVersion(organization, project?) - If a project is provided, it's attached. If a project isn't, it's detached. - // ProjectVersions - Returns a list of project versions by project. - // ProjectVersionSchema(projectversion) - Return parameter schemas for a job. - // ProjectVersionParameters(projectversion) - Returns computed parameters for a job. - // ProjectVersionParameters(projectversion) - Returns computed parameters for a job. - // ProjectVersionLogs(projectversion) - Returns logs for an executing project version. - // ProjectVersionLogsAfter(projectversion, timestamp) - Streams logs that occur after a specific timestamp. - // ProjectVersionResources(projectversion, resources) - Returns resources to be created for a project version. - - // CreateWorkspace - Creates a workspace for a project. - // ProvisionWorkspace - Creates a new build. - // Workspaces - Returns all workspaces the user has access to. - // WorkspacesByProject - Returns workspaces inside a project. - // WorkspaceByName - Returns a workspace by name. - // Workspace - Returns a single workspace by ID. - // WorkspaceProvisions - Returns a timeline of provisions for a workspace. - // WorkspaceProvisionResources - List resources for a specific workspace version. - // WorkspaceProvisionResource - Get a specific resource. - // WorkspaceProvisionLogs - Returns a stream of logs. - // WorkspaceProvisionLogsAfter - Returns a stream of logs after. - // DialWorkspaceAgent - Creates the connection to a workspace agent. - // ListenWorkspaceAgent - Listens to the workspace agent as the ID. - // AuthWorkspaceAgentWithGoogleInstanceIdentity - Exchanges SA for token. - - // User - Returns the currently authenticated user. - // HasFirstUser - Returns whether the first user has been created. - // CreateFirstUser - Creates a new user and the organization provided. - // CreateUser - Creates a new user and adds them to the organization. - // CreateAPIKey - Creates a new API key. - // LoginWithPassword - Authenticates with email and password. - // Logout - Should clear the session token. - - // ProvisionerDaemons - // ListenProvisionerDaemon - - // OrganizationsByUser - Returns organizations by user. - - r.Route("/agent", func(r chi.Router) { - - }) - - r.Route("/daemon", func(r chi.Router) { - - }) - - r.Route("/job", func(r chi.Router) { + r.Route("/projectversion/{projectversion}", func(r chi.Router) { r.Use( httpmw.ExtractAPIKey(options.Database, nil), + httpmw.ExtractProjectVersionParam(options.Database), httpmw.ExtractOrganizationParam(options.Database), ) - r.Post("/", api.postProjectImportByOrganization) - }) - - r.Route("/provisioner/job/{job}/resources", func(r chi.Router) { + r.Get("/", nil) + r.Get("/schema", nil) + r.Get("/parameters", nil) + r.Get("/logs", nil) + r.Get("/resources", nil) }) - - // ProjectVersionLogs - - // 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.postWorkspaceBuildByUser) - r.Get("/", api.workspaceBuildByUser) - r.Route("/{workspacebuild}", func(r chi.Router) { - r.Use(httpmw.ExtractWorkspaceBuildParam(options.Database)) - r.Get("/", api.workspaceBuildByName) - }) - }) - }) + r.Route("/workspace/{workspace}", func(r chi.Router) { + r.Use( + httpmw.ExtractAPIKey(options.Database, nil), + httpmw.ExtractWorkspaceParam(options.Database), + httpmw.ExtractUserParam(options.Database), + ) + r.Get("/", nil) + r.Get("/builds", nil) + r.Post("/builds", nil) + }) + r.Route("/workspacebuild/{workspacebuild}", func(r chi.Router) { + r.Use( + httpmw.ExtractAPIKey(options.Database, nil), + httpmw.ExtractWorkspaceBuildParam(options.Database), + httpmw.ExtractWorkspaceParam(options.Database), + ) + r.Get("/logs", nil) + r.Get("/resources", nil) + r.Route("/resources/{workspaceresource}", func(r chi.Router) { + r.Use(httpmw.ExtractWorkspaceResourceParam(options.Database)) + r.Get("/", nil) + r.Get("/dial", nil) }) }) - r.Route("/workspaceagent", func(r chi.Router) { - r.Route("/authenticate", func(r chi.Router) { + r.Route("/auth", func(r chi.Router) { r.Post("/google-instance-identity", api.postAuthenticateWorkspaceAgentUsingGoogleInstanceIdentity) }) - r.Group(func(r chi.Router) { + r.Route("/me", func(r chi.Router) { r.Use(httpmw.ExtractWorkspaceAgent(options.Database)) - r.Get("/serve", api.workspaceAgentServe) + r.Get("/listen", nil) + }) + }) + r.Route("/provisionerdaemon", func(r chi.Router) { + r.Route("/me", func(r chi.Router) { + r.Get("/listen", api.provisionerDaemonsServe) }) }) - 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.Use( - httpmw.ExtractAPIKey(options.Database, nil), - httpmw.ExtractOrganizationParam(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.provisionerJobResourcesByID) - r.Get("/logs", api.provisionerJobLogsByID) - }) - }) - - r.Route("/workspaceprovision/{organization}", func(r chi.Router) { - r.Use( - httpmw.ExtractAPIKey(options.Database, nil), - httpmw.ExtractOrganizationParam(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("/resources", func(r chi.Router) { - r.Get("/", api.provisionerJobResourcesByID) - r.Route("/{workspaceresource}", func(r chi.Router) { - r.Use(httpmw.ExtractWorkspaceResourceParam(options.Database)) - r.Get("/", api.provisionerJobResourceByID) - r.Get("/agent", api.workspaceAgentConnectByResource) - }) - }) - }) - }) - - r.Route("/provisioners/daemons", func(r chi.Router) { - r.Get("/", api.provisionerDaemons) - r.Get("/serve", api.provisionerDaemonsServe) - }) }) r.NotFound(site.Handler(options.Logger).ServeHTTP) return r, api.websocketWaitGroup.Wait diff --git a/database/databasefake/databasefake.go b/database/databasefake/databasefake.go index 39655221fa9ea..08850f183698e 100644 --- a/database/databasefake/databasefake.go +++ b/database/databasefake/databasefake.go @@ -406,7 +406,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 +422,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) { @@ -740,13 +740,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 @@ -803,10 +804,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 diff --git a/database/dump.sql b/database/dump.sql index 8c7337416cf82..6e92fece82128 100644 --- a/database/dump.sql +++ b/database/dump.sql @@ -166,6 +166,7 @@ CREATE TABLE project ( CREATE TABLE project_version ( 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, @@ -177,6 +178,7 @@ 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 ); diff --git a/database/migrations/000002_projects.up.sql b/database/migrations/000002_projects.up.sql index 2cf6147a5e5a0..49222cd5d8104 100644 --- a/database/migrations/000002_projects.up.sql +++ b/database/migrations/000002_projects.up.sql @@ -34,6 +34,7 @@ CREATE TABLE project_version ( id uuid NOT NULL UNIQUE, -- This should be indexed. 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. diff --git a/database/migrations/000004_jobs.up.sql b/database/migrations/000004_jobs.up.sql index f8e1ca8db9fd5..851d3871ae26f 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, diff --git a/database/models.go b/database/models.go index 1194a0ab423eb..cebe4b9517e12 100644 --- a/database/models.go +++ b/database/models.go @@ -344,21 +344,23 @@ type Project struct { } type ProjectVersion struct { - ID uuid.UUID `db:"id" json:"id"` - ProjectID uuid.NullUUID `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"` - JobID uuid.UUID `db:"job_id" json:"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 { diff --git a/database/querier.go b/database/querier.go index 9b9628ecaccf4..7aad96af3e64a 100644 --- a/database/querier.go +++ b/database/querier.go @@ -22,7 +22,7 @@ type querier interface { 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.NullUUID) ([]ProjectVersion, error) + GetProjectVersionsByProjectID(ctx context.Context, dollar_1 uuid.UUID) ([]ProjectVersion, error) GetProjectsByOrganizationIDs(ctx context.Context, ids []string) ([]Project, error) GetProvisionerDaemonByID(ctx context.Context, id uuid.UUID) (ProvisionerDaemon, error) GetProvisionerDaemons(ctx context.Context) ([]ProvisionerDaemon, error) diff --git a/database/query.sql b/database/query.sql index 500a13bcd7985..5d6506d78d0de 100644 --- a/database/query.sql +++ b/database/query.sql @@ -179,7 +179,7 @@ SELECT FROM project_version WHERE - project_id = $1; + project_id = $1 :: uuid; -- name: GetProjectVersionByProjectIDAndName :one SELECT @@ -484,6 +484,7 @@ INSERT INTO project_version ( id, project_id, + organization_id, created_at, updated_at, name, @@ -491,7 +492,7 @@ INSERT INTO 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 @@ -535,9 +536,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 diff --git a/database/query.sql.go b/database/query.sql.go index f1730b47d4f54..9cace7103a686 100644 --- a/database/query.sql.go +++ b/database/query.sql.go @@ -427,7 +427,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, job_id + id, project_id, organization_id, created_at, updated_at, name, description, job_id FROM project_version WHERE @@ -440,6 +440,7 @@ 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, @@ -451,7 +452,7 @@ func (q *sqlQuerier) GetProjectVersionByID(ctx context.Context, id uuid.UUID) (P const getProjectVersionByProjectIDAndName = `-- name: GetProjectVersionByProjectIDAndName :one SELECT - id, project_id, created_at, updated_at, name, description, job_id + id, project_id, organization_id, created_at, updated_at, name, description, job_id FROM project_version WHERE @@ -470,6 +471,7 @@ func (q *sqlQuerier) GetProjectVersionByProjectIDAndName(ctx context.Context, ar err := row.Scan( &i.ID, &i.ProjectID, + &i.OrganizationID, &i.CreatedAt, &i.UpdatedAt, &i.Name, @@ -481,15 +483,15 @@ func (q *sqlQuerier) GetProjectVersionByProjectIDAndName(ctx context.Context, ar const getProjectVersionsByProjectID = `-- name: GetProjectVersionsByProjectID :many SELECT - id, project_id, created_at, updated_at, name, description, 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.NullUUID) ([]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,6 +502,7 @@ func (q *sqlQuerier) GetProjectVersionsByProjectID(ctx context.Context, projectI if err := rows.Scan( &i.ID, &i.ProjectID, + &i.OrganizationID, &i.CreatedAt, &i.UpdatedAt, &i.Name, @@ -561,7 +564,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 +578,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 +587,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 +605,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 { @@ -1684,6 +1689,7 @@ INSERT INTO project_version ( id, project_id, + organization_id, created_at, updated_at, name, @@ -1691,23 +1697,25 @@ INSERT INTO job_id ) VALUES - ($1, $2, $3, $4, $5, $6, $7) RETURNING id, project_id, created_at, updated_at, name, description, 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.NullUUID `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"` - JobID uuid.UUID `db:"job_id" json:"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, @@ -1718,6 +1726,7 @@ func (q *sqlQuerier) InsertProjectVersion(ctx context.Context, arg InsertProject err := row.Scan( &i.ID, &i.ProjectID, + &i.OrganizationID, &i.CreatedAt, &i.UpdatedAt, &i.Name, @@ -1729,22 +1738,24 @@ func (q *sqlQuerier) InsertProjectVersion(ctx context.Context, arg InsertProject 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), ) @@ -1753,6 +1764,7 @@ func (q *sqlQuerier) InsertProvisionerDaemon(ctx context.Context, arg InsertProv &i.ID, &i.CreatedAt, &i.UpdatedAt, + &i.OrganizationID, &i.Name, pq.Array(&i.Provisioners), ) diff --git a/go.mod b/go.mod index 077e969ae9c69..8ff903c44f292 100644 --- a/go.mod +++ b/go.mod @@ -55,7 +55,6 @@ require ( 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/shirou/gopsutil v3.21.11+incompatible github.com/spf13/cobra v1.3.0 github.com/stretchr/testify v1.7.0 github.com/tabbed/pqtype v0.1.1 @@ -148,6 +147,7 @@ require ( github.com/pion/udp v0.1.1 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/shirou/gopsutil v3.21.11+incompatible // indirect github.com/sirupsen/logrus v1.8.1 // indirect github.com/spf13/pflag v1.0.5 // indirect github.com/tklauser/go-sysconf v0.3.10 // indirect 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..1c4e6027c1b0a --- /dev/null +++ b/httpmw/httpmw.go @@ -0,0 +1,61 @@ +package httpmw + +import ( + "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" +) + +// 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 +} + +func fetchOrganization(rw http.ResponseWriter, r *http.Request, db database.Store, organizationID string) (database.Organization, database.OrganizationMember, bool) { + apiKey := APIKey(r) + organization, err := db.GetOrganizationByID(r.Context(), organizationID) + if err != nil { + httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ + Message: fmt.Sprintf("get organization: %s", err.Error()), + }) + return organization, database.OrganizationMember{}, false + } + organizationMember, err := db.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: "not a member of the organization", + }) + return organization, organizationMember, false + } + if err != nil { + httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ + Message: fmt.Sprintf("get organization member: %s", err.Error()), + }) + return organization, organizationMember, false + } + return organization, organizationMember, 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..84985f632a873 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) @@ -125,8 +116,8 @@ func TestProjectParam(t *testing.T) { 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 +131,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/workspacehistoryparam.go b/httpmw/workspacebuildparam.go similarity index 54% rename from httpmw/workspacehistoryparam.go rename to httpmw/workspacebuildparam.go index c34156387f3b1..29e88d8f20654 100644 --- a/httpmw/workspacehistoryparam.go +++ b/httpmw/workspacebuildparam.go @@ -28,35 +28,16 @@ func WorkspaceBuildParam(r *http.Request) database.WorkspaceBuild { 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) { - workspace := WorkspaceParam(r) - workspaceBuildName := chi.URLParam(r, "workspacebuild") - if workspaceBuildName == "" { - httpapi.Write(rw, http.StatusBadRequest, httpapi.Response{ - Message: "workspace build name must be provided", - }) + workspaceBuildID, parsed := parseUUID(rw, r, "workspacebuild") + if !parsed { return } - var workspaceBuild database.WorkspaceBuild - var err error - if workspaceBuildName == "latest" { - workspaceBuild, err = db.GetWorkspaceBuildByWorkspaceIDWithoutAfter(r.Context(), workspace.ID) - if errors.Is(err, sql.ErrNoRows) { - httpapi.Write(rw, http.StatusNotFound, httpapi.Response{ - Message: "there is no workspace build", - }) - return - } - } else { - workspaceBuild, err = db.GetWorkspaceBuildByWorkspaceIDAndName(r.Context(), database.GetWorkspaceBuildByWorkspaceIDAndNameParams{ - WorkspaceID: workspace.ID, - Name: workspaceBuildName, + 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), }) - if errors.Is(err, sql.ErrNoRows) { - httpapi.Write(rw, http.StatusNotFound, httpapi.Response{ - Message: fmt.Sprintf("workspace build %q does not exist", workspaceBuildName), - }) - return - } + return } if err != nil { httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ @@ -66,6 +47,9 @@ func ExtractWorkspaceBuildParam(db database.Store) func(http.Handler) http.Handl } 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 73% rename from httpmw/workspacehistoryparam_test.go rename to httpmw/workspacebuildparam_test.go index c30153c7948ec..7ef913770abc4 100644 --- a/httpmw/workspacehistoryparam_test.go +++ b/httpmw/workspacebuildparam_test.go @@ -74,12 +74,7 @@ func TestWorkspaceBuildParam(t *testing.T) { t.Parallel() db := databasefake.New() rtr := chi.NewRouter() - rtr.Use( - httpmw.ExtractAPIKey(db, nil), - httpmw.ExtractUserParam(db), - httpmw.ExtractWorkspaceParam(db), - httpmw.ExtractWorkspaceBuildParam(db), - ) + rtr.Use(httpmw.ExtractWorkspaceBuildParam(db)) rtr.Get("/", nil) r, _ := setupAuthentication(db) rw := httptest.NewRecorder() @@ -94,16 +89,11 @@ func TestWorkspaceBuildParam(t *testing.T) { t.Parallel() db := databasefake.New() rtr := chi.NewRouter() - rtr.Use( - httpmw.ExtractAPIKey(db, nil), - httpmw.ExtractUserParam(db), - httpmw.ExtractWorkspaceParam(db), - httpmw.ExtractWorkspaceBuildParam(db), - ) + rtr.Use(httpmw.ExtractWorkspaceBuildParam(db)) rtr.Get("/", nil) r, _ := setupAuthentication(db) - chi.RouteContext(r.Context()).URLParams.Add("workspacebuild", "nothin") + chi.RouteContext(r.Context()).URLParams.Add("workspacebuild", uuid.NewString()) rw := httptest.NewRecorder() rtr.ServeHTTP(rw, r) @@ -118,40 +108,8 @@ func TestWorkspaceBuildParam(t *testing.T) { rtr := chi.NewRouter() rtr.Use( httpmw.ExtractAPIKey(db, nil), - httpmw.ExtractUserParam(db), - httpmw.ExtractWorkspaceParam(db), httpmw.ExtractWorkspaceBuildParam(db), - ) - rtr.Get("/", func(rw http.ResponseWriter, r *http.Request) { - _ = httpmw.WorkspaceBuildParam(r) - rw.WriteHeader(http.StatusOK) - }) - - r, workspace := setupAuthentication(db) - 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("workspacebuild", workspaceBuild.Name) - rw := httptest.NewRecorder() - rtr.ServeHTTP(rw, r) - - res := rw.Result() - defer res.Body.Close() - require.Equal(t, http.StatusOK, res.StatusCode) - }) - - t.Run("WorkspaceBuildLatest", 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.ExtractWorkspaceBuildParam(db), ) rtr.Get("/", func(rw http.ResponseWriter, r *http.Request) { _ = httpmw.WorkspaceBuildParam(r) @@ -159,13 +117,13 @@ func TestWorkspaceBuildParam(t *testing.T) { }) r, workspace := setupAuthentication(db) - _, err := db.InsertWorkspaceBuild(context.Background(), database.InsertWorkspaceBuildParams{ + 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("workspacebuild", "latest") + chi.RouteContext(r.Context()).URLParams.Add("workspacebuild", workspaceBuild.ID.String()) rw := httptest.NewRecorder() rtr.ServeHTTP(rw, r) diff --git a/httpmw/workspaceparam.go b/httpmw/workspaceparam.go index dff494a24af23..8eba7e8e16ce0 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.StatusBadRequest, httpapi.Response{ + Message: "getting non-personal workspaces isn't supported yet", + }) + 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..e394270c58e9a 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() @@ -85,14 +81,10 @@ 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) - chi.RouteContext(r.Context()).URLParams.Add("workspace", "frog") + chi.RouteContext(r.Context()).URLParams.Add("workspace", uuid.NewString()) rw := httptest.NewRecorder() rtr.ServeHTTP(rw, r) @@ -107,7 +99,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 +112,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 index 2f19b153dc701..8f57932721863 100644 --- a/httpmw/workspaceresourceparam.go +++ b/httpmw/workspaceresourceparam.go @@ -7,9 +7,6 @@ import ( "fmt" "net/http" - "github.com/go-chi/chi/v5" - "github.com/google/uuid" - "github.com/coder/coder/database" "github.com/coder/coder/httpapi" ) @@ -29,18 +26,8 @@ func WorkspaceResourceParam(r *http.Request) database.ProvisionerJobResource { 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) { - resourceID := chi.URLParam(r, "workspaceresource") - if resourceID == "" { - httpapi.Write(rw, http.StatusBadRequest, httpapi.Response{ - Message: "workspace resource must be provided", - }) - return - } - resourceUUID, err := uuid.Parse(resourceID) - if err != nil { - httpapi.Write(rw, http.StatusBadRequest, httpapi.Response{ - Message: "resource id must be a uuid", - }) + resourceUUID, parsed := parseUUID(rw, r, "workspaceresource") + if !parsed { return } resource, err := db.GetProvisionerJobResourceByID(r.Context(), resourceUUID) @@ -56,6 +43,13 @@ func ExtractWorkspaceResourceParam(db database.Store) func(http.Handler) http.Ha }) return } + workspaceBuild := WorkspaceBuildParam(r) + if workspaceBuild.ProvisionJobID.String() != resource.JobID.String() { + httpapi.Write(rw, http.StatusUnauthorized, httpapi.Response{ + Message: "you don't own this resource", + }) + return + } ctx := context.WithValue(r.Context(), workspaceResourceParamContextKey{}, resource) next.ServeHTTP(rw, r.WithContext(ctx)) diff --git a/httpmw/workspaceresourceparam_test.go b/httpmw/workspaceresourceparam_test.go index 454a061c665e8..36f387f8464dd 100644 --- a/httpmw/workspaceresourceparam_test.go +++ b/httpmw/workspaceresourceparam_test.go @@ -20,12 +20,17 @@ func TestWorkspaceResourceParam(t *testing.T) { setup := func(db database.Store) (*http.Request, database.ProvisionerJobResource) { r := httptest.NewRequest("GET", "/", nil) + workspaceBuild, err := db.InsertWorkspaceBuild(r.Context(), database.InsertWorkspaceBuildParams{ + ID: uuid.New(), + }) + require.NoError(t, err) resource, err := db.InsertProvisionerJobResource(context.Background(), database.InsertProvisionerJobResourceParams{ ID: uuid.New(), }) 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 } @@ -34,9 +39,7 @@ func TestWorkspaceResourceParam(t *testing.T) { t.Parallel() db := databasefake.New() rtr := chi.NewRouter() - rtr.Use( - httpmw.ExtractWorkspaceResourceParam(db), - ) + rtr.Use(httpmw.ExtractWorkspaceResourceParam(db)) rtr.Get("/", nil) r, _ := setup(db) rw := httptest.NewRecorder() @@ -47,25 +50,6 @@ func TestWorkspaceResourceParam(t *testing.T) { 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.ExtractWorkspaceResourceParam(db), - ) - rtr.Get("/", nil) - - r, _ := setup(db) - chi.RouteContext(r.Context()).URLParams.Add("workspaceresource", "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() @@ -85,11 +69,12 @@ func TestWorkspaceResourceParam(t *testing.T) { require.Equal(t, http.StatusNotFound, res.StatusCode) }) - t.Run("ProvisionerJob", func(t *testing.T) { + t.Run("Found", func(t *testing.T) { t.Parallel() db := databasefake.New() rtr := chi.NewRouter() rtr.Use( + httpmw.ExtractWorkspaceBuildParam(db), httpmw.ExtractWorkspaceResourceParam(db), ) rtr.Get("/", func(rw http.ResponseWriter, r *http.Request) { From 7b0899c10b2bcf8f809a7cca17c22e2c50c347f7 Mon Sep 17 00:00:00 2001 From: Kyle Carberry Date: Sat, 5 Mar 2022 22:24:21 +0000 Subject: [PATCH 07/22] Cleanup routes --- coderd/coderd.go | 75 ++++++++++++------------ coderd/files.go | 29 +++++++++ coderd/files_test.go | 10 ++++ codersdk/files.go | 21 ++++++- codersdk/organization.go | 65 ++++++++++++++++++++ codersdk/organizations.go | 18 ------ codersdk/projects.go | 60 +++++++++++-------- codersdk/projectversions.go | 29 --------- codersdk/{users.go => user.go} | 0 codersdk/{users_test.go => user_test.go} | 0 codersdk/workspaces.go | 14 ----- 11 files changed, 198 insertions(+), 123 deletions(-) create mode 100644 codersdk/organization.go delete mode 100644 codersdk/organizations.go rename codersdk/{users.go => user.go} (100%) rename codersdk/{users_test.go => user_test.go} (100%) diff --git a/coderd/coderd.go b/coderd/coderd.go index a150ac63d8bb8..ac48cb1348f8d 100644 --- a/coderd/coderd.go +++ b/coderd/coderd.go @@ -41,31 +41,18 @@ func New(options *Options) (http.Handler, func()) { Message: "👋", }) }) - r.Post("/login", api.postLogin) - r.Post("/logout", api.postLogout) - - r.Route("/user", func(r chi.Router) { - r.Get("/first", api.user) - r.Post("/first", api.user) - r.Group(func(r chi.Router) { - r.Use(httpmw.ExtractAPIKey(options.Database, nil)) - r.Post("/", nil) - 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.Route("/files", func(r chi.Router) { + r.Use(httpmw.ExtractAPIKey(options.Database, nil)) + r.Post("/", api.postUpload) + r.Get("/{hash}", nil) }) - r.Route("/organization/{organization}", func(r chi.Router) { r.Use( httpmw.ExtractAPIKey(options.Database, nil), httpmw.ExtractOrganizationParam(options.Database), ) - r.Post("/users", nil) r.Get("/provisionerdaemons", nil) + r.Post("/projectversion", nil) r.Route("/projects", func(r chi.Router) { r.Post("/", api.postProjectsByOrganization) r.Get("/", api.projectsByOrganization) @@ -79,11 +66,13 @@ func New(options *Options) (http.Handler, func()) { httpmw.ExtractOrganizationParam(options.Database), ) r.Get("/", api.projectByOrganization) + r.Delete("/", nil) r.Get("/workspaces", api.workspacesByProject) r.Get("/parameters", api.parametersByProject) r.Post("/parameters", api.postParametersByProject) r.Get("/versions", api.projectVersionsByOrganization) - r.Post("/versions", api.postProjectVersionByOrganization) + r.Get("/versions/latest", nil) + r.Patch("/versions", nil) }) r.Route("/projectversion/{projectversion}", func(r chi.Router) { r.Use( @@ -98,6 +87,36 @@ func New(options *Options) (http.Handler, func()) { r.Get("/logs", nil) r.Get("/resources", nil) }) + r.Route("/provisionerdaemon", func(r chi.Router) { + r.Route("/me", func(r chi.Router) { + r.Get("/listen", api.provisionerDaemonsServe) + }) + }) + r.Route("/user", func(r chi.Router) { + r.Post("/login", api.postLogin) + r.Post("/logout", api.postLogout) + r.Get("/first", api.user) + r.Post("/first", api.user) + r.Group(func(r chi.Router) { + r.Use(httpmw.ExtractAPIKey(options.Database, nil)) + r.Post("/", nil) + 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.Route("/workspaceagent", func(r chi.Router) { + r.Route("/auth", func(r chi.Router) { + r.Post("/google-instance-identity", api.postAuthenticateWorkspaceAgentUsingGoogleInstanceIdentity) + }) + r.Route("/me", func(r chi.Router) { + r.Use(httpmw.ExtractWorkspaceAgent(options.Database)) + r.Get("/listen", nil) + }) + }) r.Route("/workspace/{workspace}", func(r chi.Router) { r.Use( httpmw.ExtractAPIKey(options.Database, nil), @@ -122,24 +141,6 @@ func New(options *Options) (http.Handler, func()) { r.Get("/dial", nil) }) }) - r.Route("/workspaceagent", func(r chi.Router) { - r.Route("/auth", func(r chi.Router) { - r.Post("/google-instance-identity", api.postAuthenticateWorkspaceAgentUsingGoogleInstanceIdentity) - }) - r.Route("/me", func(r chi.Router) { - r.Use(httpmw.ExtractWorkspaceAgent(options.Database)) - r.Get("/listen", nil) - }) - }) - r.Route("/provisionerdaemon", func(r chi.Router) { - r.Route("/me", func(r chi.Router) { - r.Get("/listen", api.provisionerDaemonsServe) - }) - }) - r.Route("/upload", func(r chi.Router) { - r.Use(httpmw.ExtractAPIKey(options.Database, nil)) - r.Post("/", api.postUpload) - }) }) r.NotFound(site.Handler(options.Logger).ServeHTTP) return r, api.websocketWaitGroup.Wait diff --git a/coderd/files.go b/coderd/files.go index ee8609c635f7d..1280e27e8f85d 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" @@ -68,3 +71,29 @@ func (api *api) postUpload(rw http.ResponseWriter, r *http.Request) { Hash: file.Hash, }) } + +func (api *api) download(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.WriteHeader(http.StatusOK) + rw.Header().Set("Content-Type", file.Mimetype) + rw.Write(file.Data) +} diff --git a/coderd/files_test.go b/coderd/files_test.go index dc4d209e01a27..893a3eeb90d34 100644 --- a/coderd/files_test.go +++ b/coderd/files_test.go @@ -39,3 +39,13 @@ func TestPostUpload(t *testing.T) { require.NoError(t, err) }) } + +func TestDownload(t *testing.T) { + t.Parallel() + t.Run("BadHash", func(t *testing.T) { + t.Parallel() + client := coderdtest.New(t, nil) + _ = coderdtest.CreateInitialUser(t, client) + + }) +} diff --git a/codersdk/files.go b/codersdk/files.go index d15e3f7b1beb6..b3278f54dd5a3 100644 --- a/codersdk/files.go +++ b/codersdk/files.go @@ -3,6 +3,8 @@ package codersdk import ( "context" "encoding/json" + "fmt" + "io" "net/http" "github.com/coder/coder/coderd" @@ -15,7 +17,7 @@ const ( // 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/upload", content, func(r *http.Request) { + res, err := c.request(ctx, http.MethodPost, "/api/v2/files", content, func(r *http.Request) { r.Header.Set("Content-Type", contentType) }) if err != nil { @@ -28,3 +30,20 @@ func (c *Client) Upload(ctx context.Context, contentType string, content []byte) var resp coderd.UploadResponse return resp, json.NewDecoder(res.Body).Decode(&resp) } + +// 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.MethodPost, 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/organization.go b/codersdk/organization.go new file mode 100644 index 0000000000000..32c55baa5a193 --- /dev/null +++ b/codersdk/organization.go @@ -0,0 +1,65 @@ +package codersdk + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + + "github.com/coder/coder/coderd" + "github.com/google/uuid" +) + +// OrganizationsByUser returns organizations for the provided user. +func (c *Client) OrganizationsByUser(ctx context.Context, user uuid.UUID) ([]coderd.Organization, error) { + return nil, nil +} + +// OrganizationByName returns an organization by case-insensitive name. +func (c *Client) OrganizationByName(ctx context.Context, user uuid.UUID, name string) (coderd.Organization, error) { + return coderd.Organization{}, nil +} + +// ProvisionerDaemonsByOrganization returns provisioner daemons available for an organization. +func (c *Client) ProvisionerDaemonsByOrganization(ctx context.Context, organization string) ([]coderd.ProvisionerDaemon, error) { + return nil, nil +} + +// 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 uuid.UUID, req coderd.CreateProjectVersion) (coderd.ProjectVersion, error) { + res, err := c.request(ctx, http.MethodPost, fmt.Sprintf("/api/v2/organizations/%s/projectversion", 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 uuid.UUID) ([]coderd.Project, error) { + return nil, nil +} + +// ProjectByName finds a project inside the organization provided with a case-insensitive name. +func (c *Client) ProjectByName(ctx context.Context, organization uuid.UUID, name string) (coderd.Project, error) { + return coderd.Project{}, nil +} diff --git a/codersdk/organizations.go b/codersdk/organizations.go deleted file mode 100644 index be1c688b0f708..0000000000000 --- a/codersdk/organizations.go +++ /dev/null @@ -1,18 +0,0 @@ -package codersdk - -import ( - "context" - - "github.com/coder/coder/coderd" - "github.com/google/uuid" -) - -// OrganizationsByUser returns organizations for the provided user. -func (c *Client) OrganizationsByUser(ctx context.Context, user uuid.UUID) ([]coderd.Organization, error) { - return nil, nil -} - -// OrganizationByName returns an organization by case-insensitive name. -func (c *Client) OrganizationByName(ctx context.Context, user uuid.UUID, name string) (coderd.Organization, error) { - return coderd.Organization{}, nil -} diff --git a/codersdk/projects.go b/codersdk/projects.go index 9079a2ed0f5f7..bc19c18fa330f 100644 --- a/codersdk/projects.go +++ b/codersdk/projects.go @@ -11,30 +11,6 @@ import ( "github.com/coder/coder/coderd" ) -// CreateProject creates a new project inside an organization. -func (c *Client) CreateProject(ctx context.Context, organization uuid.UUID, 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 uuid.UUID) ([]coderd.Project, error) { - return nil, nil -} - -// ProjectByName finds a project inside the organization provided with a case-insensitive name. -func (c *Client) ProjectByName(ctx context.Context, organization uuid.UUID, name string) (coderd.Project, error) { - return coderd.Project{}, nil -} - // Project returns a single project. func (c *Client) Project(ctx context.Context, project uuid.UUID) (coderd.Project, error) { res, err := c.request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/project/%s", project), nil) @@ -49,6 +25,20 @@ func (c *Client) Project(ctx context.Context, project uuid.UUID) (coderd.Project return resp, json.NewDecoder(res.Body).Decode(&resp) } +// WorkspacesByProject lists all workspaces for a specific project. +func (c *Client) WorkspacesByProject(ctx context.Context, project uuid.UUID) ([]coderd.Workspace, error) { + res, err := c.request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/projects/%s/workspaces", 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) +} + // ProjectParameters returns parameters scoped to a project. func (c *Client) ProjectParameters(ctx context.Context, project uuid.UUID) ([]coderd.ParameterValue, error) { res, err := c.request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/project/%s/parameters", project), nil) @@ -76,3 +66,25 @@ func (c *Client) CreateProjectParameter(ctx context.Context, organization, proje var param coderd.ParameterValue return param, json.NewDecoder(res.Body).Decode(¶m) } + +// 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/project/%s/versions", project), nil) + if err != nil { + return nil, err + } + defer res.Body.Close() + if res.StatusCode != http.StatusOK { + return nil, readBodyAsError(res) + } + var projectVersion []coderd.ProjectVersion + return projectVersion, json.NewDecoder(res.Body).Decode(&projectVersion) +} + +func (c *Client) DeleteProject(ctx context.Context, project uuid.UUID) error { + return nil +} + +func (c *Client) UpdateProjectVersion() { + +} diff --git a/codersdk/projectversions.go b/codersdk/projectversions.go index 546a06e59e01d..56d51c967f323 100644 --- a/codersdk/projectversions.go +++ b/codersdk/projectversions.go @@ -12,35 +12,6 @@ import ( "github.com/coder/coder/coderd" ) -// ImportProjectVersion processes source-code and optionally associates the version with a project. -// Executing without a project is useful for validating source-code. -func (c *Client) ImportProjectVersion(ctx context.Context, organization uuid.UUID, req coderd.CreateProjectVersion) (coderd.ProjectVersion, error) { - res, err := c.request(ctx, http.MethodPost, fmt.Sprintf("/api/v2/organizations/%s/projectversion", 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) -} - -// ProjectVersions lists versions associated with a project. -func (c *Client) ProjectVersions(ctx context.Context, project uuid.UUID) ([]coderd.ProjectVersion, error) { - res, err := c.request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/project/%s/versions", project), nil) - if err != nil { - return nil, err - } - defer res.Body.Close() - if res.StatusCode != http.StatusOK { - return nil, readBodyAsError(res) - } - var projectVersion []coderd.ProjectVersion - return projectVersion, json.NewDecoder(res.Body).Decode(&projectVersion) -} - // ProjectVersion returns a project version by ID. func (c *Client) ProjectVersion(ctx context.Context, version uuid.UUID) (coderd.ProjectVersion, error) { return coderd.ProjectVersion{}, nil diff --git a/codersdk/users.go b/codersdk/user.go similarity index 100% rename from codersdk/users.go rename to codersdk/user.go diff --git a/codersdk/users_test.go b/codersdk/user_test.go similarity index 100% rename from codersdk/users_test.go rename to codersdk/user_test.go diff --git a/codersdk/workspaces.go b/codersdk/workspaces.go index 990724606eb0b..8592988da4b53 100644 --- a/codersdk/workspaces.go +++ b/codersdk/workspaces.go @@ -30,20 +30,6 @@ func (c *Client) WorkspacesByUser(ctx context.Context, user uuid.UUID) ([]coderd return workspaces, json.NewDecoder(res.Body).Decode(&workspaces) } -// WorkspacesByProject lists all workspaces for a specific project. -func (c *Client) WorkspacesByProject(ctx context.Context, project uuid.UUID) ([]coderd.Workspace, error) { - res, err := c.request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/projects/%s/workspaces", 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) -} - // WorkspaceByName returns a workspace for a user that matches the case-insensitive name. func (c *Client) WorkspaceByName(ctx context.Context, user uuid.UUID, name string) (coderd.Workspace, error) { return coderd.Workspace{}, nil From db0ca76e81d3322503368e00a7b8fc66b896b9f1 Mon Sep 17 00:00:00 2001 From: Kyle Carberry Date: Sat, 5 Mar 2022 22:46:57 +0000 Subject: [PATCH 08/22] Compiles! --- .vscode/settings.json | 3 + cli/projectcreate_test.go | 4 +- cli/projectlist_test.go | 4 +- cli/workspacecreate_test.go | 2 +- coderd/cmd/root.go | 2 +- coderd/coderd.go | 8 +- coderd/coderdtest/coderdtest.go | 42 ++--- coderd/coderdtest/coderdtest_test.go | 2 +- coderd/files.go | 6 +- coderd/files_test.go | 28 +++- coderd/projectimport.go | 5 +- coderd/projectimport_test.go | 103 ++++++------ coderd/projects.go | 7 +- coderd/projects_test.go | 186 +++++++++++----------- coderd/projectversion.go | 19 ++- coderd/projectversion_test.go | 68 ++++---- coderd/provisionerdaemons.go | 4 +- coderd/provisionerjobs.go | 12 +- coderd/provisionerjobs_test.go | 230 +++++++++++++-------------- coderd/users.go | 4 +- coderd/users_test.go | 114 ++++++------- coderd/workspaceagent_test.go | 61 ++++--- coderd/workspaceagentauth_test.go | 142 ++++++++--------- coderd/workspacehistory.go | 4 +- coderd/workspacehistory_test.go | 14 +- coderd/workspaces_test.go | 106 ++++++------ codersdk/files.go | 2 +- codersdk/files_test.go | 28 ---- codersdk/projects_test.go | 179 --------------------- codersdk/projectversions.go | 2 +- codersdk/projectversions_test.go | 147 ----------------- codersdk/provisionerdaemons_test.go | 46 ------ codersdk/user.go | 8 +- codersdk/user_test.go | 133 ---------------- codersdk/workspaceagent_test.go | 37 ----- codersdk/workspaces_test.go | 163 ------------------- go.mod | 5 - go.sum | 10 -- 38 files changed, 594 insertions(+), 1346 deletions(-) delete mode 100644 codersdk/files_test.go delete mode 100644 codersdk/projects_test.go delete mode 100644 codersdk/projectversions_test.go delete mode 100644 codersdk/provisionerdaemons_test.go delete mode 100644 codersdk/user_test.go delete mode 100644 codersdk/workspaceagent_test.go delete mode 100644 codersdk/workspaces_test.go 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/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_test.go b/cli/projectlist_test.go index a1340989946b2..84549c0893afb 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,7 +33,7 @@ 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) diff --git a/cli/workspacecreate_test.go b/cli/workspacecreate_test.go index 812f59db38d7f..1c9df9e4d8de4 100644 --- a/cli/workspacecreate_test.go +++ b/cli/workspacecreate_test.go @@ -17,7 +17,7 @@ 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{ Parse: echo.ParseComplete, diff --git a/coderd/cmd/root.go b/coderd/cmd/root.go index c5acf3bba3bd0..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.ProvisionerDaemonServe, &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 ac48cb1348f8d..93f5948f6ddd8 100644 --- a/coderd/coderd.go +++ b/coderd/coderd.go @@ -43,8 +43,8 @@ func New(options *Options) (http.Handler, func()) { }) r.Route("/files", func(r chi.Router) { r.Use(httpmw.ExtractAPIKey(options.Database, nil)) - r.Post("/", api.postUpload) - r.Get("/{hash}", nil) + r.Get("/{hash}", api.fileByHash) + r.Post("/", api.postFiles) }) r.Route("/organization/{organization}", func(r chi.Router) { r.Use( @@ -95,8 +95,8 @@ func New(options *Options) (http.Handler, func()) { r.Route("/user", func(r chi.Router) { r.Post("/login", api.postLogin) r.Post("/logout", api.postLogout) - r.Get("/first", api.user) - r.Post("/first", api.user) + r.Get("/first", api.firstUser) + r.Post("/first", api.postFirstUser) r.Group(func(r chi.Router) { r.Use(httpmw.ExtractAPIKey(options.Database, nil)) r.Post("/", nil) diff --git a/coderd/coderdtest/coderdtest.go b/coderd/coderdtest/coderdtest.go index 01c5c2a38e315..c2650c8c2c22d 100644 --- a/coderd/coderdtest/coderdtest.go +++ b/coderd/coderdtest/coderdtest.go @@ -125,7 +125,7 @@ func NewProvisionerDaemon(t *testing.T, client *codersdk.Client) io.Closer { require.NoError(t, err) }() - closer := provisionerd.New(client.ProvisionerDaemonServe, &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 +140,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 { +func CreateFirstUser(t *testing.T, client *codersdk.Client) coderd.CreateInitialUserRequest { req := coderd.CreateInitialUserRequest{ Email: "testuser@coder.com", Username: "testuser", Password: "testpass", Organization: "testorg", } - _, err := client.CreateInitialUser(context.Background(), req) + _, err := client.CreateFirstUser(context.Background(), req) require.NoError(t, err) login, err := client.LoginWithPassword(context.Background(), coderd.LoginWithPasswordRequest{ @@ -165,17 +165,17 @@ func CreateInitialUser(t *testing.T, client *codersdk.Client) coderd.CreateIniti // 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 { - data, err := echo.Tar(res) - require.NoError(t, err) - file, err := client.Upload(context.Background(), codersdk.ContentTypeTar, data) - require.NoError(t, err) - job, err := client.CreateProjectImportJob(context.Background(), organization, coderd.CreateProjectImportJobRequest{ - StorageSource: file.Hash, - StorageMethod: database.ProvisionerStorageMethodFile, - Provisioner: database.ProvisionerTypeEcho, - }) - require.NoError(t, err) - return job + // data, err := echo.Tar(res) + // require.NoError(t, err) + // file, err := client.Upload(context.Background(), codersdk.ContentTypeTar, data) + // require.NoError(t, err) + // job, err := client.CreateProjectImportJob(context.Background(), organization, coderd.CreateProjectImportJobRequest{ + // StorageSource: file.Hash, + // StorageMethod: database.ProvisionerStorageMethodFile, + // Provisioner: database.ProvisionerTypeEcho, + // }) + // require.NoError(t, err) + return coderd.ProvisionerJob{} } // CreateProject creates a project with the "echo" provisioner for @@ -192,12 +192,12 @@ func CreateProject(t *testing.T, client *codersdk.Client, organization string, j // 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 - require.Eventually(t, func() bool { - var err error - provisionerJob, err = client.ProjectImportJob(context.Background(), organization, job) - require.NoError(t, err) - return provisionerJob.Status.Completed() - }, 5*time.Second, 25*time.Millisecond) + // require.Eventually(t, func() bool { + // var err error + // provisionerJob, err = client.ProjectImportJob(context.Background(), organization, job) + // require.NoError(t, err) + // return provisionerJob.Status.Completed() + // }, 5*time.Second, 25*time.Millisecond) return provisionerJob } diff --git a/coderd/coderdtest/coderdtest_test.go b/coderd/coderdtest/coderdtest_test.go index 3a2a05018660c..a322532bf03ed 100644 --- a/coderd/coderdtest/coderdtest_test.go +++ b/coderd/coderdtest/coderdtest_test.go @@ -20,7 +20,7 @@ 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) diff --git a/coderd/files.go b/coderd/files.go index 1280e27e8f85d..1e3cd16d0b5ae 100644 --- a/coderd/files.go +++ b/coderd/files.go @@ -21,7 +21,7 @@ type UploadResponse struct { Hash string `json:"hash"` } -func (api *api) postUpload(rw http.ResponseWriter, r *http.Request) { +func (api *api) postFiles(rw http.ResponseWriter, r *http.Request) { apiKey := httpmw.APIKey(r) contentType := r.Header.Get("Content-Type") @@ -72,7 +72,7 @@ func (api *api) postUpload(rw http.ResponseWriter, r *http.Request) { }) } -func (api *api) download(rw http.ResponseWriter, r *http.Request) { +func (api *api) fileByHash(rw http.ResponseWriter, r *http.Request) { hash := chi.URLParam(r, "hash") if hash == "" { httpapi.Write(rw, http.StatusBadRequest, httpapi.Response{ @@ -93,7 +93,7 @@ func (api *api) download(rw http.ResponseWriter, r *http.Request) { }) return } - rw.WriteHeader(http.StatusOK) 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 893a3eeb90d34..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,12 +11,12 @@ 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) + _ = coderdtest.CreateFirstUser(t, client) _, err := client.Upload(context.Background(), "bad", []byte{'a'}) require.Error(t, err) }) @@ -23,7 +24,7 @@ func TestPostUpload(t *testing.T) { t.Run("Insert", func(t *testing.T) { t.Parallel() client := coderdtest.New(t, nil) - _ = coderdtest.CreateInitialUser(t, client) + _ = coderdtest.CreateFirstUser(t, client) _, err := client.Upload(context.Background(), codersdk.ContentTypeTar, make([]byte, 1024)) require.NoError(t, err) }) @@ -31,7 +32,7 @@ func TestPostUpload(t *testing.T) { 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.Upload(context.Background(), codersdk.ContentTypeTar, data) require.NoError(t, err) @@ -42,10 +43,25 @@ func TestPostUpload(t *testing.T) { func TestDownload(t *testing.T) { t.Parallel() - t.Run("BadHash", func(t *testing.T) { + t.Run("NotFound", func(t *testing.T) { t.Parallel() client := coderdtest.New(t, nil) - _ = coderdtest.CreateInitialUser(t, client) + _ = 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/projectimport.go b/coderd/projectimport.go index 38a3a446dede7..bd24ae75bf3da 100644 --- a/coderd/projectimport.go +++ b/coderd/projectimport.go @@ -110,7 +110,8 @@ func (api *api) postProjectImportByOrganization(rw http.ResponseWriter, r *http. // Returns imported parameter schemas from a completed job! func (api *api) projectImportJobSchemasByID(rw http.ResponseWriter, r *http.Request) { - job := httpmw.ProvisionerJobParam(r) + // job := httpmw.ProvisionerJobParam(r) + var job database.ProvisionerJob if !convertProvisionerJob(job).Status.Completed() { httpapi.Write(rw, http.StatusPreconditionFailed, httpapi.Response{ Message: "Job hasn't completed!", @@ -138,7 +139,7 @@ func (api *api) projectImportJobSchemasByID(rw http.ResponseWriter, r *http.Requ // 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) + var job database.ProvisionerJob if !convertProvisionerJob(job).Status.Completed() { httpapi.Write(rw, http.StatusPreconditionFailed, httpapi.Response{ Message: "Job hasn't completed!", diff --git a/coderd/projectimport_test.go b/coderd/projectimport_test.go index e253f1ccdc929..534e25aee1120 100644 --- a/coderd/projectimport_test.go +++ b/coderd/projectimport_test.go @@ -1,16 +1,9 @@ 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" ) @@ -19,19 +12,19 @@ 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) + // 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) + user := coderdtest.CreateFirstUser(t, client) _ = coderdtest.CreateProjectImportJob(t, client, user.Organization, nil) }) } @@ -40,18 +33,18 @@ 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()) + // 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) + user := coderdtest.CreateFirstUser(t, client) coderdtest.NewProvisionerDaemon(t, client) job := coderdtest.CreateProjectImportJob(t, client, user.Organization, &echo.Responses{ Parse: []*proto.Parse_Response{{ @@ -69,10 +62,10 @@ func TestProjectImportJobSchemasByID(t *testing.T) { 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) + // schemas, err := client.ProjectImportJobSchemas(context.Background(), user.Organization, job.ID) + // require.NoError(t, err) + // require.NotNil(t, schemas) + // require.Len(t, schemas, 1) }) } @@ -80,18 +73,18 @@ 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()) + // 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) + user := coderdtest.CreateFirstUser(t, client) coderdtest.NewProvisionerDaemon(t, client) job := coderdtest.CreateProjectImportJob(t, client, user.Organization, &echo.Responses{ Parse: []*proto.Parse_Response{{ @@ -114,11 +107,11 @@ func TestProjectImportJobParametersByID(t *testing.T) { 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) + // 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) }) } @@ -126,18 +119,18 @@ 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()) + // 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) + user := coderdtest.CreateFirstUser(t, client) coderdtest.NewProvisionerDaemon(t, client) job := coderdtest.CreateProjectImportJob(t, client, user.Organization, &echo.Responses{ Parse: echo.ParseComplete, @@ -153,11 +146,11 @@ func TestProjectImportJobResourcesByID(t *testing.T) { }}, }) 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) + // 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 927047166fe0f..e1d8d3520ca67 100644 --- a/coderd/projects.go +++ b/coderd/projects.go @@ -153,8 +153,11 @@ func (api *api) postProjectsByOrganization(rw http.ResponseWriter, r *http.Reque return xerrors.Errorf("insert project: %s", err) } _, err = db.InsertProjectVersion(r.Context(), database.InsertProjectVersionParams{ - ID: projectVersionID, - ProjectID: dbProject.ID, + ID: projectVersionID, + ProjectID: uuid.NullUUID{ + UUID: dbProject.ID, + Valid: true, + }, CreatedAt: database.Now(), UpdatedAt: database.Now(), Name: namesgenerator.GetRandomName(1), diff --git a/coderd/projects_test.go b/coderd/projects_test.go index db1daafea4979..b2fdd5d44172d 100644 --- a/coderd/projects_test.go +++ b/coderd/projects_test.go @@ -16,66 +16,66 @@ import ( func TestProjects(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("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("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.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(), "") - require.NoError(t, err) - require.Len(t, projects, 1) - require.Equal(t, projects[0].WorkspaceOwnerCount, uint32(1)) - }) + // t.Run("ListWorkspaceOwnerCount", 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(), "") + // require.NoError(t, err) + // require.Len(t, projects, 1) + // require.Equal(t, projects[0].WorkspaceOwnerCount, uint32(1)) + // }) } func TestProjectsByOrganization(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("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.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("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) + // }) } func TestPostProjectsByOrganization(t *testing.T) { @@ -83,7 +83,7 @@ func TestPostProjectsByOrganization(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) job := coderdtest.CreateProjectImportJob(t, client, user.Organization, nil) _ = coderdtest.CreateProject(t, client, user.Organization, job.ID) }) @@ -91,7 +91,7 @@ func TestPostProjectsByOrganization(t *testing.T) { t.Run("AlreadyExists", func(t *testing.T) { t.Parallel() client := coderdtest.New(t, nil) - user := coderdtest.CreateInitialUser(t, client) + user := coderdtest.CreateFirstUser(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{ @@ -106,15 +106,15 @@ func TestPostProjectsByOrganization(t *testing.T) { 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) - }) + // 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) { @@ -122,7 +122,7 @@ func TestPostParametersByProject(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) 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{ @@ -137,33 +137,33 @@ func TestPostParametersByProject(t *testing.T) { func TestParametersByProject(t *testing.T) { t.Parallel() - t.Run("ListEmpty", 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("ListEmpty", 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) - require.NoError(t, err) - require.NotNil(t, params) - require.Len(t, params, 1) - }) + // 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) + // require.NoError(t, err) + // require.NotNil(t, params) + // require.Len(t, params, 1) + // }) } diff --git a/coderd/projectversion.go b/coderd/projectversion.go index 255a773e78e49..7d12013f4f86a 100644 --- a/coderd/projectversion.go +++ b/coderd/projectversion.go @@ -83,12 +83,15 @@ func (api *api) postProjectVersionByOrganization(rw http.ResponseWriter, r *http } 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, + ID: uuid.New(), + ProjectID: uuid.NullUUID{ + UUID: project.ID, + Valid: true, + }, + CreatedAt: database.Now(), + UpdatedAt: database.Now(), + Name: namesgenerator.GetRandomName(1), + JobID: job.ID, }) if err != nil { httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ @@ -104,10 +107,10 @@ func (api *api) postProjectVersionByOrganization(rw http.ResponseWriter, r *http func convertProjectVersion(version database.ProjectVersion) ProjectVersion { return ProjectVersion{ ID: version.ID, - ProjectID: version.ProjectID, + ProjectID: &version.ProjectID.UUID, CreatedAt: version.CreatedAt, UpdatedAt: version.UpdatedAt, Name: version.Name, - JobID: version.ImportJobID, + JobID: version.JobID, } } diff --git a/coderd/projectversion_test.go b/coderd/projectversion_test.go index e9937d4ba4f2c..1266c59ca4c9f 100644 --- a/coderd/projectversion_test.go +++ b/coderd/projectversion_test.go @@ -1,54 +1,48 @@ 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) - }) + // 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) - }) + // 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) - }) + // 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/provisionerdaemons.go b/coderd/provisionerdaemons.go index 0a9ab95e8aed3..790f794367a15 100644 --- a/coderd/provisionerdaemons.go +++ b/coderd/provisionerdaemons.go @@ -200,14 +200,14 @@ func (server *provisionerdServer) AcquireJob(ctx context.Context, _ *proto.Empty 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, diff --git a/coderd/provisionerjobs.go b/coderd/provisionerjobs.go index 3e2e665ee3fcc..d6ca11c74a439 100644 --- a/coderd/provisionerjobs.go +++ b/coderd/provisionerjobs.go @@ -18,7 +18,6 @@ import ( "github.com/coder/coder/database" "github.com/coder/coder/httpapi" - "github.com/coder/coder/httpmw" ) type ProvisionerJobStatus string @@ -79,9 +78,8 @@ type ProvisionerJobAgent struct { } func (*api) provisionerJobByID(rw http.ResponseWriter, r *http.Request) { - job := httpmw.ProvisionerJobParam(r) - render.Status(r, http.StatusOK) - render.JSON(rw, r, convertProvisionerJob(job)) + // render.Status(r, http.StatusOK) + // render.JSON(rw, r, convertProvisionerJob(job)) } // Returns provisioner logs based on query parameters. @@ -133,7 +131,7 @@ func (api *api) provisionerJobLogsByID(rw http.ResponseWriter, r *http.Request) before = database.Now() } - job := httpmw.ProvisionerJobParam(r) + var job database.ProvisionerJob if !follow { logs, err := api.Database.GetProvisionerLogsByIDBetween(r.Context(), database.GetProvisionerLogsByIDBetweenParams{ JobID: job.ID, @@ -241,7 +239,7 @@ func (api *api) provisionerJobLogsByID(rw http.ResponseWriter, r *http.Request) } func (api *api) provisionerJobResourcesByID(rw http.ResponseWriter, r *http.Request) { - job := httpmw.ProvisionerJobParam(r) + var job database.ProvisionerJob if !convertProvisionerJob(job).Status.Completed() { httpapi.Write(rw, http.StatusPreconditionFailed, httpapi.Response{ Message: "Job hasn't completed!", @@ -282,7 +280,7 @@ func (api *api) provisionerJobResourcesByID(rw http.ResponseWriter, r *http.Requ } func (api *api) provisionerJobResourceByID(rw http.ResponseWriter, r *http.Request) { - job := httpmw.ProvisionerJobParam(r) + var job database.ProvisionerJob if !convertProvisionerJob(job).Status.Completed() { httpapi.Write(rw, http.StatusPreconditionFailed, httpapi.Response{ Message: "Job hasn't completed!", diff --git a/coderd/provisionerjobs_test.go b/coderd/provisionerjobs_test.go index 70f6b68b4972d..9e65c63acef76 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,7 +9,6 @@ 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" @@ -20,77 +18,77 @@ func TestPostProvisionerImportJobByOrganization(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) - } + // 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.Upload(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) + // 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.Upload(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) }) } @@ -98,49 +96,49 @@ 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()) + // 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) + // 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) + user := coderdtest.CreateFirstUser(t, client) _ = coderdtest.NewProvisionerDaemon(t, client) job := coderdtest.CreateProjectImportJob(t, client, user.Organization, &echo.Responses{ Parse: []*proto.Parse_Response{{ @@ -163,11 +161,11 @@ func TestProvisionerJobParametersByID(t *testing.T) { 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, "") + // 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, "") }) } @@ -176,7 +174,7 @@ func TestProvisionerJobResourcesByID(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{ Parse: echo.ParseComplete, @@ -192,10 +190,10 @@ func TestProvisionerJobResourcesByID(t *testing.T) { }}, }) 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) + // 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) }) } @@ -204,7 +202,7 @@ func TestProvisionerJobLogsByName(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{ Parse: echo.ParseComplete, @@ -240,7 +238,7 @@ func TestProvisionerJobLogsByName(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{ Parse: echo.ParseComplete, @@ -281,7 +279,7 @@ func TestProvisionerJobLogsByName(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{ Parse: echo.ParseComplete, diff --git a/coderd/users.go b/coderd/users.go index 713f9a4687dcb..1cd37ed8d7c7b 100644 --- a/coderd/users.go +++ b/coderd/users.go @@ -62,7 +62,7 @@ type GenerateAPIKeyResponse struct { } // 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{ @@ -82,7 +82,7 @@ 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) { +func (api *api) postFirstUser(rw http.ResponseWriter, r *http.Request) { var createUser CreateInitialUserRequest if !httpapi.Read(rw, r, &createUser) { return diff --git a/coderd/users_test.go b/coderd/users_test.go index 5316b781a04b3..bf6d4b70ac9ae 100644 --- a/coderd/users_test.go +++ b/coderd/users_test.go @@ -17,19 +17,19 @@ func TestUser(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) + // 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) + // client := coderdtest.New(t, nil) + // _ = coderdtest.CreateInitialUser(t, client) + // has, err := client.HasInitialUser(context.Background()) + // require.NoError(t, err) + // require.True(t, has) }) } @@ -37,61 +37,61 @@ func TestPostUser(t *testing.T) { t.Parallel() t.Run("BadRequest", func(t *testing.T) { t.Parallel() - client := coderdtest.New(t, nil) - _, err := client.CreateInitialUser(context.Background(), coderd.CreateInitialUserRequest{}) - require.Error(t, err) + // client := coderdtest.New(t, nil) + // _, err := client.CreateInitialUser(context.Background(), coderd.CreateInitialUserRequest{}) + // require.Error(t, err) }) t.Run("AlreadyExists", 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", - }) - var apiErr *codersdk.Error - require.ErrorAs(t, err, &apiErr) - require.Equal(t, http.StatusConflict, apiErr.StatusCode()) + // 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", + // }) + // 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.CreateInitialUser(t, client) + _ = coderdtest.CreateFirstUser(t, client) }) } func TestPostUsers(t *testing.T) { t.Parallel() - t.Run("BadRequest", func(t *testing.T) { - t.Parallel() - client := coderdtest.New(t, nil) - _, err := client.CreateInitialUser(context.Background(), coderd.CreateInitialUserRequest{}) - 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", - }) - var apiErr *codersdk.Error - require.ErrorAs(t, err, &apiErr) - require.Equal(t, http.StatusConflict, apiErr.StatusCode()) - }) + // t.Run("BadRequest", func(t *testing.T) { + // t.Parallel() + // client := coderdtest.New(t, nil) + // _, err := client.CreateInitialUser(context.Background(), coderd.CreateInitialUserRequest{}) + // 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", + // }) + // 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.CreateInitialUser(t, client) + _ = coderdtest.CreateFirstUser(t, client) _, err := client.CreateUser(context.Background(), coderd.CreateUserRequest{ Email: "another@user.org", Username: "someone-else", @@ -104,19 +104,19 @@ 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) } func TestOrganizationsByUser(t *testing.T) { t.Parallel() - client := coderdtest.New(t, nil) - _ = coderdtest.CreateInitialUser(t, client) - orgs, err := client.UserOrganizations(context.Background(), "") - require.NoError(t, err) - require.NotNil(t, orgs) - require.Len(t, orgs, 1) + // client := coderdtest.New(t, nil) + // _ = coderdtest.CreateInitialUser(t, client) + // orgs, err := client.UserOrganizations(context.Background(), "") + // require.NoError(t, err) + // require.NotNil(t, orgs) + // require.Len(t, orgs, 1) } func TestPostKey(t *testing.T) { @@ -124,7 +124,7 @@ func TestPostKey(t *testing.T) { t.Run("InvalidUser", func(t *testing.T) { t.Parallel() client := coderdtest.New(t, nil) - _ = coderdtest.CreateInitialUser(t, client) + _ = coderdtest.CreateFirstUser(t, client) // Clear session token client.SessionToken = "" @@ -138,7 +138,7 @@ func TestPostKey(t *testing.T) { t.Run("Success", func(t *testing.T) { t.Parallel() client := coderdtest.New(t, nil) - _ = coderdtest.CreateInitialUser(t, client) + _ = coderdtest.CreateFirstUser(t, client) apiKey, err := client.CreateAPIKey(context.Background()) require.NotNil(t, apiKey) require.GreaterOrEqual(t, len(apiKey.Key), 2) @@ -163,7 +163,7 @@ func TestPostLogin(t *testing.T) { t.Run("BadPassword", func(t *testing.T) { t.Parallel() client := coderdtest.New(t, nil) - user := coderdtest.CreateInitialUser(t, client) + user := coderdtest.CreateFirstUser(t, client) _, err := client.LoginWithPassword(context.Background(), coderd.LoginWithPasswordRequest{ Email: user.Email, Password: "badpass", @@ -176,7 +176,7 @@ func TestPostLogin(t *testing.T) { t.Run("Success", func(t *testing.T) { t.Parallel() client := coderdtest.New(t, nil) - user := coderdtest.CreateInitialUser(t, client) + user := coderdtest.CreateFirstUser(t, client) _, err := client.LoginWithPassword(context.Background(), coderd.LoginWithPasswordRequest{ Email: user.Email, Password: user.Password, diff --git a/coderd/workspaceagent_test.go b/coderd/workspaceagent_test.go index 218dd1fec4701..5690416a4f7dc 100644 --- a/coderd/workspaceagent_test.go +++ b/coderd/workspaceagent_test.go @@ -3,20 +3,13 @@ package coderd_test import ( "context" "testing" - "time" "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" ) @@ -26,7 +19,7 @@ func TestWorkspaceAgentServe(t *testing.T) { t.Run("Success", func(t *testing.T) { t.Parallel() client := coderdtest.New(t, nil) - user := coderdtest.CreateInitialUser(t, client) + user := coderdtest.CreateFirstUser(t, client) daemonCloser := coderdtest.NewProvisionerDaemon(t, client) authToken := uuid.NewString() job := coderdtest.CreateProjectImportJob(t, client, user.Organization, &echo.Responses{ @@ -60,34 +53,34 @@ func TestWorkspaceAgentServe(t *testing.T) { coderdtest.AwaitWorkspaceProvisionJob(t, client, user.Organization, history.ProvisionJobID) daemonCloser.Close() - agentClient := codersdk.New(client.URL) - agentClient.SessionToken = authToken - agentCloser := agent.New(agentClient.WorkspaceAgentServe, &peer.ConnOptions{ - Logger: slogtest.Make(t, nil), - }) + // agentClient := codersdk.New(client.URL) + // agentClient.SessionToken = authToken + // agentCloser := agent.New(agentClient.WorkspaceAgentServe, &peer.ConnOptions{ + // Logger: slogtest.Make(t, nil), + // }) - var resources []coderd.ProvisionerJobResource - require.Eventually(t, func() bool { - resources, err = client.WorkspaceProvisionJobResources(context.Background(), user.Organization, history.ProvisionJobID) - require.NoError(t, err) - require.Len(t, resources, 1) - return !resources[0].Agent.UpdatedAt.IsZero() - }, 5*time.Second, 25*time.Millisecond) + // var resources []coderd.ProvisionerJobResource + // require.Eventually(t, func() bool { + // resources, err = client.WorkspaceProvisionJobResources(context.Background(), user.Organization, history.ProvisionJobID) + // require.NoError(t, err) + // require.Len(t, resources, 1) + // return !resources[0].Agent.UpdatedAt.IsZero() + // }, 5*time.Second, 25*time.Millisecond) - workspaceClient, err := client.WorkspaceAgentConnect(context.Background(), user.Organization, history.ProvisionJobID, resources[0].ID) - require.NoError(t, err) - 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) - _, err = conn.Ping() - require.NoError(t, err) + // workspaceClient, err := client.WorkspaceAgentConnect(context.Background(), user.Organization, history.ProvisionJobID, resources[0].ID) + // require.NoError(t, err) + // 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) + // _, err = conn.Ping() + // require.NoError(t, err) - workspaceClient.DRPCConn().Close() - conn.Close() - stream.Close() - agentCloser.Close() + // workspaceClient.DRPCConn().Close() + // conn.Close() + // stream.Close() + // agentCloser.Close() }) } diff --git a/coderd/workspaceagentauth_test.go b/coderd/workspaceagentauth_test.go index 8412fb3819550..fea2592b0bdaf 100644 --- a/coderd/workspaceagentauth_test.go +++ b/coderd/workspaceagentauth_test.go @@ -19,88 +19,82 @@ import ( "google.golang.org/api/idtoken" "google.golang.org/api/option" - "github.com/coder/coder/coderd" - "github.com/coder/coder/coderd/coderdtest" - "github.com/coder/coder/codersdk" "github.com/coder/coder/cryptorand" - "github.com/coder/coder/database" - "github.com/coder/coder/provisioner/echo" - "github.com/coder/coder/provisionersdk/proto" ) func TestPostWorkspaceAgentAuthenticateGoogleInstanceIdentity(t *testing.T) { t.Parallel() - t.Run("Expired", func(t *testing.T) { - t.Parallel() - instanceID := "instanceidentifier" - signedKey, keyID, privateKey := createSignedToken(t, instanceID, &jwt.MapClaims{}) - validator := createValidator(t, keyID, privateKey) - client := coderdtest.New(t, &coderdtest.Options{ - GoogleTokenValidator: validator, - }) - _, err := client.AuthenticateWorkspaceAgentUsingGoogleCloudIdentity(context.Background(), "", createMetadataClient(signedKey)) - var apiErr *codersdk.Error - require.ErrorAs(t, err, &apiErr) - require.Equal(t, http.StatusUnauthorized, apiErr.StatusCode()) - }) + // t.Run("Expired", func(t *testing.T) { + // t.Parallel() + // instanceID := "instanceidentifier" + // signedKey, keyID, privateKey := createSignedToken(t, instanceID, &jwt.MapClaims{}) + // validator := createValidator(t, keyID, privateKey) + // client := coderdtest.New(t, &coderdtest.Options{ + // GoogleTokenValidator: validator, + // }) + // _, err := client.AuthenticateWorkspaceAgentUsingGoogleCloudIdentity(context.Background(), "", createMetadataClient(signedKey)) + // var apiErr *codersdk.Error + // require.ErrorAs(t, err, &apiErr) + // require.Equal(t, http.StatusUnauthorized, apiErr.StatusCode()) + // }) - t.Run("InstanceNotFound", func(t *testing.T) { - t.Parallel() - instanceID := "instanceidentifier" - signedKey, keyID, privateKey := createSignedToken(t, instanceID, nil) - validator := createValidator(t, keyID, privateKey) - client := coderdtest.New(t, &coderdtest.Options{ - GoogleTokenValidator: validator, - }) - _, err := client.AuthenticateWorkspaceAgentUsingGoogleCloudIdentity(context.Background(), "", createMetadataClient(signedKey)) - var apiErr *codersdk.Error - require.ErrorAs(t, err, &apiErr) - require.Equal(t, http.StatusNotFound, apiErr.StatusCode()) - }) + // t.Run("InstanceNotFound", func(t *testing.T) { + // t.Parallel() + // instanceID := "instanceidentifier" + // signedKey, keyID, privateKey := createSignedToken(t, instanceID, nil) + // validator := createValidator(t, keyID, privateKey) + // client := coderdtest.New(t, &coderdtest.Options{ + // GoogleTokenValidator: validator, + // }) + // _, err := client.AuthenticateWorkspaceAgentUsingGoogleCloudIdentity(context.Background(), "", createMetadataClient(signedKey)) + // var apiErr *codersdk.Error + // require.ErrorAs(t, err, &apiErr) + // require.Equal(t, http.StatusNotFound, apiErr.StatusCode()) + // }) - t.Run("Success", func(t *testing.T) { - t.Parallel() - instanceID := "instanceidentifier" - signedKey, keyID, privateKey := createSignedToken(t, instanceID, nil) - validator := createValidator(t, keyID, privateKey) - client := coderdtest.New(t, &coderdtest.Options{ - GoogleTokenValidator: validator, - }) - 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: "somename", - Type: "someinstance", - Agent: &proto.Agent{ - Auth: &proto.Agent_GoogleInstanceIdentity{ - GoogleInstanceIdentity: &proto.GoogleInstanceIdentityAuth{ - InstanceId: instanceID, - }, - }, - }, - }}, - }, - }, - }}, - }) - 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.CreateWorkspaceBuild(context.Background(), "", workspace.Name, coderd.CreateWorkspaceBuildRequest{ - ProjectVersionID: project.ActiveVersionID, - Transition: database.WorkspaceTransitionStart, - }) - require.NoError(t, err) - coderdtest.AwaitWorkspaceProvisionJob(t, client, user.Organization, firstHistory.ProvisionJobID) + // t.Run("Success", func(t *testing.T) { + // t.Parallel() + // instanceID := "instanceidentifier" + // signedKey, keyID, privateKey := createSignedToken(t, instanceID, nil) + // validator := createValidator(t, keyID, privateKey) + // client := coderdtest.New(t, &coderdtest.Options{ + // GoogleTokenValidator: validator, + // }) + // 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: "somename", + // Type: "someinstance", + // Agent: &proto.Agent{ + // Auth: &proto.Agent_GoogleInstanceIdentity{ + // GoogleInstanceIdentity: &proto.GoogleInstanceIdentityAuth{ + // InstanceId: instanceID, + // }, + // }, + // }, + // }}, + // }, + // }, + // }}, + // }) + // 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.CreateWorkspaceBuild(context.Background(), "", workspace.Name, coderd.CreateWorkspaceBuildRequest{ + // ProjectVersionID: project.ActiveVersionID, + // Transition: database.WorkspaceTransitionStart, + // }) + // require.NoError(t, err) + // coderdtest.AwaitWorkspaceProvisionJob(t, client, user.Organization, firstHistory.ProvisionJobID) - _, err = client.AuthenticateWorkspaceAgentUsingGoogleCloudIdentity(context.Background(), "", createMetadataClient(signedKey)) - require.NoError(t, err) - }) + // _, err = client.AuthenticateWorkspaceAgentUsingGoogleCloudIdentity(context.Background(), "", createMetadataClient(signedKey)) + // require.NoError(t, err) + // }) } // Used to easily create an HTTP transport! diff --git a/coderd/workspacehistory.go b/coderd/workspacehistory.go index 919daf6cbce02..adefd65e33b86 100644 --- a/coderd/workspacehistory.go +++ b/coderd/workspacehistory.go @@ -64,7 +64,7 @@ func (api *api) postWorkspaceBuildByUser(rw http.ResponseWriter, r *http.Request }) return } - projectVersionJob, err := api.Database.GetProvisionerJobByID(r.Context(), projectVersion.ImportJobID) + 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), @@ -90,7 +90,7 @@ func (api *api) postWorkspaceBuildByUser(rw http.ResponseWriter, r *http.Request return } - project, err := api.Database.GetProjectByID(r.Context(), projectVersion.ProjectID) + project, err := api.Database.GetProjectByID(r.Context(), projectVersion.ProjectID.UUID) if err != nil { httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ Message: fmt.Sprintf("get project: %s", err), diff --git a/coderd/workspacehistory_test.go b/coderd/workspacehistory_test.go index a189eb432f909..09b60c78b0762 100644 --- a/coderd/workspacehistory_test.go +++ b/coderd/workspacehistory_test.go @@ -21,7 +21,7 @@ func TestPostWorkspaceBuildByUser(t *testing.T) { t.Run("NoProjectVersion", func(t *testing.T) { t.Parallel() client := coderdtest.New(t, nil) - user := coderdtest.CreateInitialUser(t, client) + user := coderdtest.CreateFirstUser(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) @@ -38,7 +38,7 @@ func TestPostWorkspaceBuildByUser(t *testing.T) { t.Run("ProjectVersionFailedImport", 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{ Provision: []*proto.Provision_Response{{}}, @@ -59,7 +59,7 @@ func TestPostWorkspaceBuildByUser(t *testing.T) { t.Run("AlreadyActive", func(t *testing.T) { t.Parallel() client := coderdtest.New(t, nil) - user := coderdtest.CreateInitialUser(t, client) + user := coderdtest.CreateFirstUser(t, client) closeDaemon := coderdtest.NewProvisionerDaemon(t, client) job := coderdtest.CreateProjectImportJob(t, client, user.Organization, nil) project := coderdtest.CreateProject(t, client, user.Organization, job.ID) @@ -85,7 +85,7 @@ func TestPostWorkspaceBuildByUser(t *testing.T) { t.Run("UpdatePriorAfterField", 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, nil) project := coderdtest.CreateProject(t, client, user.Organization, job.ID) @@ -115,7 +115,7 @@ func TestWorkspaceBuildByUser(t *testing.T) { t.Run("ListEmpty", 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, nil) project := coderdtest.CreateProject(t, client, user.Organization, job.ID) @@ -129,7 +129,7 @@ func TestWorkspaceBuildByUser(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, nil) project := coderdtest.CreateProject(t, client, user.Organization, job.ID) @@ -150,7 +150,7 @@ func TestWorkspaceBuildByUser(t *testing.T) { func TestWorkspaceBuildByName(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, nil) coderdtest.AwaitProjectImportJob(t, client, user.Organization, job.ID) diff --git a/coderd/workspaces_test.go b/coderd/workspaces_test.go index 5591a2947221f..1ff37bd3808af 100644 --- a/coderd/workspaces_test.go +++ b/coderd/workspaces_test.go @@ -15,27 +15,27 @@ import ( func TestWorkspaces(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("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) - }) + // 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) + // }) } func TestPostWorkspaceByUser(t *testing.T) { @@ -43,7 +43,7 @@ func TestPostWorkspaceByUser(t *testing.T) { t.Run("InvalidProject", func(t *testing.T) { t.Parallel() client := coderdtest.New(t, nil) - _ = coderdtest.CreateInitialUser(t, client) + _ = coderdtest.CreateFirstUser(t, client) _, err := client.CreateWorkspace(context.Background(), "", coderd.CreateWorkspaceRequest{ ProjectID: uuid.New(), Name: "workspace", @@ -57,7 +57,7 @@ func TestPostWorkspaceByUser(t *testing.T) { t.Run("NoProjectAccess", func(t *testing.T) { t.Parallel() client := coderdtest.New(t, nil) - user := coderdtest.CreateInitialUser(t, client) + user := coderdtest.CreateFirstUser(t, client) job := coderdtest.CreateProjectImportJob(t, client, user.Organization, nil) project := coderdtest.CreateProject(t, client, user.Organization, job.ID) @@ -89,7 +89,7 @@ func TestPostWorkspaceByUser(t *testing.T) { t.Run("AlreadyExists", func(t *testing.T) { t.Parallel() client := coderdtest.New(t, nil) - user := coderdtest.CreateInitialUser(t, client) + user := coderdtest.CreateFirstUser(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) @@ -106,7 +106,7 @@ func TestPostWorkspaceByUser(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) job := coderdtest.CreateProjectImportJob(t, client, user.Organization, nil) project := coderdtest.CreateProject(t, client, user.Organization, job.ID) _ = coderdtest.CreateWorkspace(t, client, "", project.ID) @@ -115,38 +115,38 @@ func TestPostWorkspaceByUser(t *testing.T) { func TestWorkspaceByUser(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) + // 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 TestWorkspacesByProject(t *testing.T) { t.Parallel() - t.Run("ListEmpty", 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) - }) + // t.Run("ListEmpty", 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) + // }) - 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.WorkspacesByProject(context.Background(), user.Organization, project.Name) - require.NoError(t, err) - require.NotNil(t, workspaces) - require.Len(t, workspaces, 1) - }) + // 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.WorkspacesByProject(context.Background(), user.Organization, project.Name) + // require.NoError(t, err) + // require.NotNil(t, workspaces) + // require.Len(t, workspaces, 1) + // }) } diff --git a/codersdk/files.go b/codersdk/files.go index b3278f54dd5a3..565859f87a007 100644 --- a/codersdk/files.go +++ b/codersdk/files.go @@ -33,7 +33,7 @@ func (c *Client) Upload(ctx context.Context, contentType string, content []byte) // 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.MethodPost, fmt.Sprintf("/api/v2/files/%s", hash), nil) + res, err := c.request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/files/%s", hash), nil) if err != nil { return nil, "", err } diff --git a/codersdk/files_test.go b/codersdk/files_test.go deleted file mode 100644 index 316ec4a92bc6e..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.Upload(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.Upload(context.Background(), codersdk.ContentTypeTar, []byte{'a'}) - require.NoError(t, err) - }) -} diff --git a/codersdk/projects_test.go b/codersdk/projects_test.go deleted file mode 100644 index df740d386d2c7..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.ProjectsByOrganization(context.Background(), uuid.New()) - require.Error(t, err) - }) - - t.Run("List", func(t *testing.T) { - t.Parallel() - client := coderdtest.New(t, nil) - _ = coderdtest.CreateInitialUser(t, client) - _, err := client.ProjectsByOrganization(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 index 56d51c967f323..5fb9f5758482b 100644 --- a/codersdk/projectversions.go +++ b/codersdk/projectversions.go @@ -58,7 +58,7 @@ func (c *Client) ProjectVersionResources(ctx context.Context, version uuid.UUID) // 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, "projectimport", organization, job, before) + return c.provisionerJobLogsBefore(ctx, "projectimport", "", version, before) } // ProjectVersionLogsAfter streams logs for a project version that occurred after a specific time. diff --git a/codersdk/projectversions_test.go b/codersdk/projectversions_test.go deleted file mode 100644 index 99039c29c68b0..0000000000000 --- a/codersdk/projectversions_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/provisionerdaemons_test.go b/codersdk/provisionerdaemons_test.go deleted file mode 100644 index 222a74dbac021..0000000000000 --- a/codersdk/provisionerdaemons_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.ProvisionerDaemonServe(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.ProvisionerDaemonServe(ctx) - require.NoError(t, err) - _, err = daemon.AcquireJob(ctx, &proto.Empty{}) - require.NoError(t, err) - }) -} diff --git a/codersdk/user.go b/codersdk/user.go index 08e27d17381ee..9931e9a76b4f5 100644 --- a/codersdk/user.go +++ b/codersdk/user.go @@ -17,7 +17,7 @@ var ( // 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/user", nil) + res, err := c.request(ctx, http.MethodGet, "/api/v2/user/first", nil) if err != nil { return false, err } @@ -34,7 +34,7 @@ func (c *Client) HasFirstUser(ctx context.Context) (bool, error) { // 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.CreateInitialUserRequest) (coderd.User, error) { - res, err := c.request(ctx, http.MethodPost, "/api/v2/user", req) + res, err := c.request(ctx, http.MethodPost, "/api/v2/user/first", req) if err != nil { return coderd.User{}, err } @@ -77,7 +77,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/user/login", req) if err != nil { return coderd.LoginWithPasswordResponse{}, err } @@ -98,7 +98,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/user/logout", nil) if err != nil { return err } diff --git a/codersdk/user_test.go b/codersdk/user_test.go deleted file mode 100644 index 74f4eca6ebda5..0000000000000 --- a/codersdk/user_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/workspaces_test.go b/codersdk/workspaces_test.go deleted file mode 100644 index 100a1a9573b7d..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 TestListWorkspaceBuild(t *testing.T) { - t.Parallel() - t.Run("Error", func(t *testing.T) { - t.Parallel() - client := coderdtest.New(t, nil) - _, err := client.ListWorkspaceBuild(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.ListWorkspaceBuild(context.Background(), "", workspace.Name) - require.NoError(t, err) - }) -} - -func TestWorkspaceBuild(t *testing.T) { - t.Parallel() - t.Run("Error", func(t *testing.T) { - t.Parallel() - client := coderdtest.New(t, nil) - _, err := client.WorkspaceBuild(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.CreateWorkspaceBuild(context.Background(), "", workspace.Name, coderd.CreateWorkspaceBuildRequest{ - 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 TestCreateWorkspaceBuild(t *testing.T) { - t.Parallel() - t.Run("Error", func(t *testing.T) { - t.Parallel() - client := coderdtest.New(t, nil) - _, err := client.CreateWorkspaceBuild(context.Background(), "", "", coderd.CreateWorkspaceBuildRequest{}) - 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.CreateWorkspaceBuild(context.Background(), "", workspace.Name, coderd.CreateWorkspaceBuildRequest{ - ProjectVersionID: project.ActiveVersionID, - Transition: database.WorkspaceTransitionStart, - }) - require.NoError(t, err) - }) -} diff --git a/go.mod b/go.mod index 8ff903c44f292..bc9967b17f141 100644 --- a/go.mod +++ b/go.mod @@ -92,7 +92,6 @@ require ( github.com/docker/docker v20.10.12+incompatible // indirect github.com/docker/go-connections v0.4.0 // indirect github.com/docker/go-units v0.4.0 // indirect - github.com/go-ole/go-ole v1.2.6 // indirect github.com/go-playground/locales v0.14.0 // indirect github.com/go-playground/universal-translator v0.18.0 // indirect github.com/gogo/protobuf v1.3.2 // indirect @@ -147,16 +146,12 @@ require ( github.com/pion/udp v0.1.1 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - github.com/shirou/gopsutil v3.21.11+incompatible // indirect github.com/sirupsen/logrus v1.8.1 // indirect github.com/spf13/pflag v1.0.5 // indirect - github.com/tklauser/go-sysconf v0.3.10 // indirect - github.com/tklauser/numcpus v0.4.0 // indirect github.com/vmihailenco/msgpack v4.0.4+incompatible // indirect github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect github.com/xeipuuv/gojsonschema v1.2.0 // indirect - github.com/yusufpapurcu/wmi v1.2.2 // indirect github.com/zclconf/go-cty v1.10.0 // indirect github.com/zeebo/errs v1.2.2 // indirect golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd // indirect diff --git a/go.sum b/go.sum index d28ed5a7262da..302226a3850e2 100644 --- a/go.sum +++ b/go.sum @@ -485,8 +485,6 @@ github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9 github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas= github.com/go-logr/logr v0.2.0/go.mod h1:z6/tIYblkpsD+a4lm/fGIIU9mZ+XfAiaFtq7xTgseGU= -github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= -github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= github.com/go-openapi/jsonpointer v0.19.2/go.mod h1:3akKfEdA7DF1sugOqz1dVQHBcuDBPKZGEoHC/NkiQRg= github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= github.com/go-openapi/jsonreference v0.19.2/go.mod h1:jMjeRr2HHw6nAVajTXJ4eiUwohSTlpa0o73RUL1owJc= @@ -1212,8 +1210,6 @@ github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAm github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= github.com/sergi/go-diff v1.2.0 h1:XU+rvMAioB0UC3q1MFrIQy4Vo5/4VsRDQQXHsEya6xQ= github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= -github.com/shirou/gopsutil v3.21.11+incompatible h1:+1+c1VGhc88SSonWP6foOcLhvnKlUeu/erjjvaPEYiI= -github.com/shirou/gopsutil v3.21.11+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA= github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4= github.com/shopspring/decimal v0.0.0-20200227202807-02e2044944cc/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= @@ -1279,10 +1275,6 @@ github.com/tabbed/pqtype v0.1.1 h1:PhEcb9JZ8jr7SUjJDFjRPxny0M8fkXZrxn/a9yQfoZg= github.com/tabbed/pqtype v0.1.1/go.mod h1:HLt2kLJPcUhODQkYn3mJkMHXVsuv3Z2n5NZEeKXL0Uk= github.com/tchap/go-patricia v2.2.6+incompatible/go.mod h1:bmLyhP68RS6kStMGxByiQ23RP/odRBOTVjwp2cDyi6I= github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= -github.com/tklauser/go-sysconf v0.3.10 h1:IJ1AZGZRWbY8T5Vfk04D9WOA5WSejdflXxP03OUqALw= -github.com/tklauser/go-sysconf v0.3.10/go.mod h1:C8XykCvCb+Gn0oNCWPIlcb0RuglQTYaQ2hGm7jmxEFk= -github.com/tklauser/numcpus v0.4.0 h1:E53Dm1HjH1/R2/aoCtXtPgzmElmn51aOkhCFSuZq//o= -github.com/tklauser/numcpus v0.4.0/go.mod h1:1+UI3pD8NW14VMwdgJNJ1ESk2UnwhAnz5hMwiKKqXCQ= github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM= @@ -1335,8 +1327,6 @@ github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= -github.com/yusufpapurcu/wmi v1.2.2 h1:KBNDSne4vP5mbSWnJbO+51IMOXJB67QiYCSBrubbPRg= -github.com/yusufpapurcu/wmi v1.2.2/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= github.com/yvasiyarov/go-metrics v0.0.0-20140926110328-57bccd1ccd43/go.mod h1:aX5oPXxHm3bOH+xeAttToC8pqch2ScQN/JoXYupl6xs= github.com/yvasiyarov/gorelic v0.0.0-20141212073537-a9bba5b9ab50/go.mod h1:NUSPSUX/bi6SeDMUh6brw0nXpxHnc96TguQh0+r/ssA= github.com/yvasiyarov/newrelic_platform_go v0.0.0-20140908184405-b21fdbd4370f/go.mod h1:GlGEuHIJweS1mbCqG+7vt2nvWLzLLnRHbXz5JKd/Qbg= From 79796811b2ce77eca05390415b04e7cb445dfb54 Mon Sep 17 00:00:00 2001 From: Kyle Carberry Date: Sun, 6 Mar 2022 17:12:31 +0000 Subject: [PATCH 09/22] Fix files and organizations --- cli/projectlist_test.go | 6 +- cli/workspacecreate_test.go | 6 +- coderd/coderd.go | 22 +- coderd/coderdtest/coderdtest.go | 40 +-- coderd/coderdtest/coderdtest_test.go | 8 +- coderd/files.go | 3 +- coderd/organizations.go | 244 ++++++++++++++++++ coderd/organizations_test.go | 178 +++++++++++++ coderd/projectimport_test.go | 34 +-- coderd/projects.go | 98 ------- coderd/projects_test.go | 88 ++----- coderd/projectversion.go | 50 ---- coderd/projectversion_test.go | 18 +- coderd/provisionerdaemons.go | 20 -- coderd/provisionerdaemons_test.go | 3 +- coderd/provisionerjobs_test.go | 60 ++--- coderd/users.go | 26 +- coderd/users_test.go | 8 +- coderd/workspaceagent_test.go | 12 +- coderd/workspaceagentauth_test.go | 8 +- coderd/workspacehistory_test.go | 40 +-- coderd/workspaces_test.go | 32 +-- .../{organization.go => organizations.go} | 52 ++-- codersdk/provisionerdaemons.go | 14 - codersdk/{user.go => users.go} | 35 ++- 25 files changed, 663 insertions(+), 442 deletions(-) create mode 100644 coderd/organizations_test.go rename codersdk/{organization.go => organizations.go} (58%) rename codersdk/{user.go => users.go} (76%) diff --git a/cli/projectlist_test.go b/cli/projectlist_test.go index 84549c0893afb..03ee7ad5e995d 100644 --- a/cli/projectlist_test.go +++ b/cli/projectlist_test.go @@ -35,10 +35,10 @@ func TestProjectList(t *testing.T) { client := coderdtest.New(t, nil) 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) + job := coderdtest.CreateProjectVersion(t, client, user.OrganizationID, nil) + coderdtest.AwaitProjectImportJob(t, client, user.OrganizationID, job.ID) _ = daemon.Close() - project := coderdtest.CreateProject(t, client, user.Organization, job.ID) + project := coderdtest.CreateProject(t, client, user.OrganizationID, job.ID) cmd, root := clitest.New(t, "projects", "list") clitest.SetupConfig(t, client, root) pty := ptytest.New(t) diff --git a/cli/workspacecreate_test.go b/cli/workspacecreate_test.go index 1c9df9e4d8de4..86ffb65aed051 100644 --- a/cli/workspacecreate_test.go +++ b/cli/workspacecreate_test.go @@ -19,7 +19,7 @@ func TestWorkspaceCreate(t *testing.T) { client := coderdtest.New(t, nil) user := coderdtest.CreateFirstUser(t, client) _ = coderdtest.NewProvisionerDaemon(t, client) - job := coderdtest.CreateProjectImportJob(t, client, user.Organization, &echo.Responses{ + job := 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.AwaitProjectImportJob(t, client, user.OrganizationID, job.ID) + project := coderdtest.CreateProject(t, client, user.OrganizationID, job.ID) cmd, root := clitest.New(t, "workspaces", "create", project.Name) clitest.SetupConfig(t, client, root) diff --git a/coderd/coderd.go b/coderd/coderd.go index 93f5948f6ddd8..a80f75225984c 100644 --- a/coderd/coderd.go +++ b/coderd/coderd.go @@ -44,22 +44,22 @@ func New(options *Options) (http.Handler, func()) { r.Route("/files", func(r chi.Router) { r.Use(httpmw.ExtractAPIKey(options.Database, nil)) r.Get("/{hash}", api.fileByHash) - r.Post("/", api.postFiles) + r.Post("/", api.postFile) }) - r.Route("/organization/{organization}", func(r chi.Router) { + r.Route("/organizations/{organization}", func(r chi.Router) { r.Use( httpmw.ExtractAPIKey(options.Database, nil), httpmw.ExtractOrganizationParam(options.Database), ) - r.Get("/provisionerdaemons", nil) - r.Post("/projectversion", nil) + 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("/project/{project}", func(r chi.Router) { + r.Route("/projects/{project}", func(r chi.Router) { r.Use( httpmw.ExtractAPIKey(options.Database, nil), httpmw.ExtractProjectParam(options.Database), @@ -74,7 +74,7 @@ func New(options *Options) (http.Handler, func()) { r.Get("/versions/latest", nil) r.Patch("/versions", nil) }) - r.Route("/projectversion/{projectversion}", func(r chi.Router) { + r.Route("/projectversions/{projectversion}", func(r chi.Router) { r.Use( httpmw.ExtractAPIKey(options.Database, nil), httpmw.ExtractProjectVersionParam(options.Database), @@ -87,12 +87,12 @@ func New(options *Options) (http.Handler, func()) { r.Get("/logs", nil) r.Get("/resources", nil) }) - r.Route("/provisionerdaemon", func(r chi.Router) { + r.Route("/provisionerdaemons", func(r chi.Router) { r.Route("/me", func(r chi.Router) { r.Get("/listen", api.provisionerDaemonsServe) }) }) - r.Route("/user", func(r chi.Router) { + r.Route("/users", func(r chi.Router) { r.Post("/login", api.postLogin) r.Post("/logout", api.postLogout) r.Get("/first", api.firstUser) @@ -108,7 +108,7 @@ func New(options *Options) (http.Handler, func()) { }) }) }) - r.Route("/workspaceagent", func(r chi.Router) { + r.Route("/workspaceagents", func(r chi.Router) { r.Route("/auth", func(r chi.Router) { r.Post("/google-instance-identity", api.postAuthenticateWorkspaceAgentUsingGoogleInstanceIdentity) }) @@ -117,7 +117,7 @@ func New(options *Options) (http.Handler, func()) { r.Get("/listen", nil) }) }) - r.Route("/workspace/{workspace}", func(r chi.Router) { + r.Route("/workspaces/{workspace}", func(r chi.Router) { r.Use( httpmw.ExtractAPIKey(options.Database, nil), httpmw.ExtractWorkspaceParam(options.Database), @@ -127,7 +127,7 @@ func New(options *Options) (http.Handler, func()) { r.Get("/builds", nil) r.Post("/builds", nil) }) - r.Route("/workspacebuild/{workspacebuild}", func(r chi.Router) { + r.Route("/workspacebuilds/{workspacebuild}", func(r chi.Router) { r.Use( httpmw.ExtractAPIKey(options.Database, nil), httpmw.ExtractWorkspaceBuildParam(options.Database), diff --git a/coderd/coderdtest/coderdtest.go b/coderd/coderdtest/coderdtest.go index c2650c8c2c22d..109ca0d072c77 100644 --- a/coderd/coderdtest/coderdtest.go +++ b/coderd/coderdtest/coderdtest.go @@ -142,14 +142,14 @@ func NewProvisionerDaemon(t *testing.T, client *codersdk.Client) io.Closer { // CreateFirstUser creates a user with preset credentials and authenticates // with the passed in codersdk client. -func CreateFirstUser(t *testing.T, client *codersdk.Client) coderd.CreateInitialUserRequest { - req := coderd.CreateInitialUserRequest{ +func CreateFirstUser(t *testing.T, client *codersdk.Client) coderd.CreateUserResponse { + req := coderd.CreateUserRequest{ Email: "testuser@coder.com", Username: "testuser", Password: "testpass", Organization: "testorg", } - _, err := client.CreateFirstUser(context.Background(), req) + resp, err := client.CreateFirstUser(context.Background(), req) require.NoError(t, err) login, err := client.LoginWithPassword(context.Background(), coderd.LoginWithPasswordRequest{ @@ -158,32 +158,32 @@ func CreateFirstUser(t *testing.T, client *codersdk.Client) coderd.CreateInitial }) require.NoError(t, err) client.SessionToken = login.SessionToken - return req + return resp } -// CreateProjectImportJob creates a project import provisioner job +// 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 { - // data, err := echo.Tar(res) - // require.NoError(t, err) - // file, err := client.Upload(context.Background(), codersdk.ContentTypeTar, data) - // require.NoError(t, err) - // job, err := client.CreateProjectImportJob(context.Background(), organization, coderd.CreateProjectImportJobRequest{ - // StorageSource: file.Hash, - // StorageMethod: database.ProvisionerStorageMethodFile, - // Provisioner: database.ProvisionerTypeEcho, - // }) - // require.NoError(t, err) - return 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.Upload(context.Background(), codersdk.ContentTypeTar, data) + require.NoError(t, err) + projectVersion, err := client.CreateProjectVersion(context.Background(), organization, coderd.CreateProjectVersion{ + StorageSource: file.Hash, + StorageMethod: database.ProvisionerStorageMethodFile, + Provisioner: database.ProvisionerTypeEcho, + }) + require.NoError(t, err) + 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 diff --git a/coderd/coderdtest/coderdtest_test.go b/coderd/coderdtest/coderdtest_test.go index a322532bf03ed..f41e0c74e6f6a 100644 --- a/coderd/coderdtest/coderdtest_test.go +++ b/coderd/coderdtest/coderdtest_test.go @@ -22,15 +22,15 @@ func TestNew(t *testing.T) { client := coderdtest.New(t, nil) 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) + job := coderdtest.CreateProjectVersion(t, client, user.OrganizationID, nil) + coderdtest.AwaitProjectImportJob(t, client, user.OrganizationID, job.ID) + project := coderdtest.CreateProject(t, client, user.OrganizationID, job.ID) workspace := coderdtest.CreateWorkspace(t, client, "me", project.ID) history, err := client.CreateWorkspaceBuild(context.Background(), "me", workspace.Name, coderd.CreateWorkspaceBuildRequest{ ProjectVersionID: project.ActiveVersionID, Transition: database.WorkspaceTransitionStart, }) require.NoError(t, err) - coderdtest.AwaitWorkspaceProvisionJob(t, client, user.Organization, history.ProvisionJobID) + coderdtest.AwaitWorkspaceProvisionJob(t, client, user.OrganizationID, history.ProvisionJobID) closer.Close() } diff --git a/coderd/files.go b/coderd/files.go index 1e3cd16d0b5ae..1d98ab4dc17ef 100644 --- a/coderd/files.go +++ b/coderd/files.go @@ -17,11 +17,12 @@ import ( "github.com/coder/coder/httpmw" ) +// UploadResponse contains the hash to reference the uploaded file. type UploadResponse struct { Hash string `json:"hash"` } -func (api *api) postFiles(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") diff --git a/coderd/organizations.go b/coderd/organizations.go index 40caaee40fbdf..00b2f8302f63b 100644 --- a/coderd/organizations.go +++ b/coderd/organizations.go @@ -10,6 +10,8 @@ import ( "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" @@ -24,6 +26,248 @@ 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 []CreateParameterValueRequest `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 *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 + 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) + } + } + + 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 { + 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: job.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)) +} + +// 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 { + 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: uuid.NullUUID{ + UUID: dbProject.ID, + Valid: true, + }, + CreatedAt: database.Now(), + UpdatedAt: database.Now(), + Name: namesgenerator.GetRandomName(1), + JobID: importJob.ID, + }) + 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) +} + // Lists all projects in an organization. func (api *api) projectsByOrganization(rw http.ResponseWriter, r *http.Request) { organization := httpmw.OrganizationParam(r) diff --git a/coderd/organizations_test.go b/coderd/organizations_test.go new file mode 100644 index 0000000000000..3efa0f5085ea9 --- /dev/null +++ b/coderd/organizations_test.go @@ -0,0 +1,178 @@ +package coderd_test + +import ( + "context" + "net/http" + "testing" + + "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/google/uuid" + "github.com/stretchr/testify/require" +) + +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.CreateProjectVersion{ + 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.CreateProjectVersion{ + 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.CreateProjectVersion{ + StorageMethod: database.ProvisionerStorageMethodFile, + StorageSource: file.Hash, + Provisioner: database.ProvisionerTypeEcho, + ParameterValues: []coderd.CreateParameterValueRequest{{ + 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/projectimport_test.go b/coderd/projectimport_test.go index 534e25aee1120..083f728a5037c 100644 --- a/coderd/projectimport_test.go +++ b/coderd/projectimport_test.go @@ -14,7 +14,7 @@ func TestPostProjectImportByOrganization(t *testing.T) { t.Parallel() // client := coderdtest.New(t, nil) // user := coderdtest.CreateInitialUser(t, client) - // _, err := client.CreateProjectImportJob(context.Background(), user.Organization, coderd.CreateProjectImportJobRequest{ + // _, err := client.CreateProjectImportJob(context.Background(), user.OrganizationID, coderd.CreateProjectImportJobRequest{ // StorageMethod: database.ProvisionerStorageMethodFile, // StorageSource: "bananas", // Provisioner: database.ProvisionerTypeEcho, @@ -25,7 +25,7 @@ func TestPostProjectImportByOrganization(t *testing.T) { t.Parallel() client := coderdtest.New(t, nil) user := coderdtest.CreateFirstUser(t, client) - _ = coderdtest.CreateProjectImportJob(t, client, user.Organization, nil) + _ = coderdtest.CreateProjectVersion(t, client, user.OrganizationID, nil) }) } @@ -35,8 +35,8 @@ func TestProjectImportJobSchemasByID(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) + // job := coderdtest.CreateProjectImportJob(t, client, user.OrganizationID, nil) + // _, err := client.ProjectImportJobSchemas(context.Background(), user.OrganizationID, job.ID) // var apiErr *codersdk.Error // require.ErrorAs(t, err, &apiErr) // require.Equal(t, http.StatusPreconditionFailed, apiErr.StatusCode()) @@ -46,7 +46,7 @@ func TestProjectImportJobSchemasByID(t *testing.T) { client := coderdtest.New(t, nil) user := coderdtest.CreateFirstUser(t, client) coderdtest.NewProvisionerDaemon(t, client) - job := coderdtest.CreateProjectImportJob(t, client, user.Organization, &echo.Responses{ + job := coderdtest.CreateProjectVersion(t, client, user.OrganizationID, &echo.Responses{ Parse: []*proto.Parse_Response{{ Type: &proto.Parse_Response_Complete{ Complete: &proto.Parse_Complete{ @@ -61,8 +61,8 @@ func TestProjectImportJobSchemasByID(t *testing.T) { }}, Provision: echo.ProvisionComplete, }) - coderdtest.AwaitProjectImportJob(t, client, user.Organization, job.ID) - // schemas, err := client.ProjectImportJobSchemas(context.Background(), user.Organization, job.ID) + coderdtest.AwaitProjectImportJob(t, client, user.OrganizationID, job.ID) + // schemas, err := client.ProjectImportJobSchemas(context.Background(), user.OrganizationID, job.ID) // require.NoError(t, err) // require.NotNil(t, schemas) // require.Len(t, schemas, 1) @@ -75,8 +75,8 @@ func TestProjectImportJobParametersByID(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) + // job := coderdtest.CreateProjectImportJob(t, client, user.OrganizationID, nil) + // _, err := client.ProjectImportJobSchemas(context.Background(), user.OrganizationID, job.ID) // var apiErr *codersdk.Error // require.ErrorAs(t, err, &apiErr) // require.Equal(t, http.StatusPreconditionFailed, apiErr.StatusCode()) @@ -86,7 +86,7 @@ func TestProjectImportJobParametersByID(t *testing.T) { client := coderdtest.New(t, nil) user := coderdtest.CreateFirstUser(t, client) coderdtest.NewProvisionerDaemon(t, client) - job := coderdtest.CreateProjectImportJob(t, client, user.Organization, &echo.Responses{ + job := coderdtest.CreateProjectVersion(t, client, user.OrganizationID, &echo.Responses{ Parse: []*proto.Parse_Response{{ Type: &proto.Parse_Response_Complete{ Complete: &proto.Parse_Complete{ @@ -106,8 +106,8 @@ func TestProjectImportJobParametersByID(t *testing.T) { }}, Provision: echo.ProvisionComplete, }) - coderdtest.AwaitProjectImportJob(t, client, user.Organization, job.ID) - // params, err := client.ProjectImportJobParameters(context.Background(), user.Organization, job.ID) + coderdtest.AwaitProjectImportJob(t, client, user.OrganizationID, job.ID) + // params, err := client.ProjectImportJobParameters(context.Background(), user.OrganizationID, job.ID) // require.NoError(t, err) // require.NotNil(t, params) // require.Len(t, params, 1) @@ -121,8 +121,8 @@ func TestProjectImportJobResourcesByID(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) + // job := coderdtest.CreateProjectImportJob(t, client, user.OrganizationID, nil) + // _, err := client.ProjectImportJobResources(context.Background(), user.OrganizationID, job.ID) // var apiErr *codersdk.Error // require.ErrorAs(t, err, &apiErr) // require.Equal(t, http.StatusPreconditionFailed, apiErr.StatusCode()) @@ -132,7 +132,7 @@ func TestProjectImportJobResourcesByID(t *testing.T) { client := coderdtest.New(t, nil) user := coderdtest.CreateFirstUser(t, client) coderdtest.NewProvisionerDaemon(t, client) - job := coderdtest.CreateProjectImportJob(t, client, user.Organization, &echo.Responses{ + job := coderdtest.CreateProjectVersion(t, client, user.OrganizationID, &echo.Responses{ Parse: echo.ParseComplete, Provision: []*proto.Provision_Response{{ Type: &proto.Provision_Response_Complete{ @@ -145,8 +145,8 @@ func TestProjectImportJobResourcesByID(t *testing.T) { }, }}, }) - coderdtest.AwaitProjectImportJob(t, client, user.Organization, job.ID) - // resources, err := client.ProjectImportJobResources(context.Background(), user.Organization, job.ID) + coderdtest.AwaitProjectImportJob(t, client, user.OrganizationID, job.ID) + // resources, err := client.ProjectImportJobResources(context.Background(), user.OrganizationID, job.ID) // require.NoError(t, err) // require.NotNil(t, resources) // require.Len(t, resources, 2) diff --git a/coderd/projects.go b/coderd/projects.go index e1d8d3520ca67..adb28a0960984 100644 --- a/coderd/projects.go +++ b/coderd/projects.go @@ -9,8 +9,6 @@ import ( "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" @@ -42,19 +40,6 @@ 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) @@ -97,89 +82,6 @@ func (api *api) projects(rw http.ResponseWriter, r *http.Request) { 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 - } - - 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: uuid.NullUUID{ - UUID: dbProject.ID, - Valid: true, - }, - CreatedAt: database.Now(), - UpdatedAt: database.Now(), - Name: namesgenerator.GetRandomName(1), - JobID: importJob.ID, - }) - 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) -} - // Returns a single project. func (*api) projectByOrganization(rw http.ResponseWriter, r *http.Request) { project := httpmw.ProjectParam(r) diff --git a/coderd/projects_test.go b/coderd/projects_test.go index b2fdd5d44172d..b7d02a1d1677b 100644 --- a/coderd/projects_test.go +++ b/coderd/projects_test.go @@ -2,14 +2,12 @@ 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" ) @@ -30,8 +28,8 @@ func TestProjects(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) + // job := coderdtest.CreateProjectImportJob(t, client, user.OrganizationID, nil) + // _ = coderdtest.CreateProject(t, client, user.OrganizationID, job.ID) // projects, err := client.Projects(context.Background(), "") // require.NoError(t, err) // require.Len(t, projects, 1) @@ -42,9 +40,9 @@ func TestProjects(t *testing.T) { // 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) + // job := coderdtest.CreateProjectImportJob(t, client, user.OrganizationID, nil) + // coderdtest.AwaitProjectImportJob(t, client, user.OrganizationID, job.ID) + // project := coderdtest.CreateProject(t, client, user.OrganizationID, job.ID) // _ = coderdtest.CreateWorkspace(t, client, "", project.ID) // _ = coderdtest.CreateWorkspace(t, client, "", project.ID) // projects, err := client.Projects(context.Background(), "") @@ -54,65 +52,15 @@ func TestProjects(t *testing.T) { // }) } -func TestProjectsByOrganization(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.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) - // }) -} - -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) - 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.CreateFirstUser(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, - }) - 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) + // job := coderdtest.CreateProjectImportJob(t, client, user.OrganizationID, nil) + // project := coderdtest.CreateProject(t, client, user.OrganizationID, job.ID) + // _, err := client.Project(context.Background(), user.OrganizationID, project.Name) // require.NoError(t, err) // }) } @@ -123,9 +71,9 @@ func TestPostParametersByProject(t *testing.T) { t.Parallel() client := coderdtest.New(t, nil) user := coderdtest.CreateFirstUser(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{ + job := coderdtest.CreateProjectVersion(t, client, user.OrganizationID, nil) + project := coderdtest.CreateProject(t, client, user.OrganizationID, job.ID) + _, err := client.CreateProjectParameter(context.Background(), user.OrganizationID, project.Name, coderd.CreateParameterValueRequest{ Name: "somename", SourceValue: "tomato", SourceScheme: database.ParameterSourceSchemeData, @@ -141,9 +89,9 @@ func TestParametersByProject(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) + // job := coderdtest.CreateProjectImportJob(t, client, user.OrganizationID, nil) + // project := coderdtest.CreateProject(t, client, user.OrganizationID, job.ID) + // params, err := client.ProjectParameters(context.Background(), user.OrganizationID, project.Name) // require.NoError(t, err) // require.NotNil(t, params) // }) @@ -152,16 +100,16 @@ func TestParametersByProject(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{ + // job := coderdtest.CreateProjectImportJob(t, client, user.OrganizationID, nil) + // project := coderdtest.CreateProject(t, client, user.OrganizationID, job.ID) + // _, err := client.CreateProjectParameter(context.Background(), user.OrganizationID, 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) + // params, err := client.ProjectParameters(context.Background(), user.OrganizationID, project.Name) // require.NoError(t, err) // require.NotNil(t, params) // require.Len(t, params, 1) diff --git a/coderd/projectversion.go b/coderd/projectversion.go index 7d12013f4f86a..d8f72e7cc6d62 100644 --- a/coderd/projectversion.go +++ b/coderd/projectversion.go @@ -9,7 +9,6 @@ import ( "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" @@ -26,11 +25,6 @@ type ProjectVersion struct { JobID 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) @@ -60,50 +54,6 @@ func (*api) projectVersionByOrganizationAndName(rw http.ResponseWriter, r *http. 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: uuid.NullUUID{ - UUID: project.ID, - Valid: true, - }, - CreatedAt: database.Now(), - UpdatedAt: database.Now(), - Name: namesgenerator.GetRandomName(1), - JobID: 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, diff --git a/coderd/projectversion_test.go b/coderd/projectversion_test.go index 1266c59ca4c9f..1ce6fb33b8fdf 100644 --- a/coderd/projectversion_test.go +++ b/coderd/projectversion_test.go @@ -10,9 +10,9 @@ func TestProjectVersionsByOrganization(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) + // job := coderdtest.CreateProjectImportJob(t, client, user.OrganizationID, nil) + // project := coderdtest.CreateProject(t, client, user.OrganizationID, job.ID) + // versions, err := client.ProjectVersions(context.Background(), user.OrganizationID, project.Name) // require.NoError(t, err) // require.NotNil(t, versions) // require.Len(t, versions, 1) @@ -25,9 +25,9 @@ func TestProjectVersionByOrganizationAndName(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()) + // job := coderdtest.CreateProjectImportJob(t, client, user.OrganizationID, nil) + // project := coderdtest.CreateProject(t, client, user.OrganizationID, job.ID) + // _, err := client.ProjectVersion(context.Background(), user.OrganizationID, project.Name, project.ActiveVersionID.String()) // require.NoError(t, err) // }) } @@ -38,9 +38,9 @@ func TestPostProjectVersionByOrganization(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{ + // job := coderdtest.CreateProjectImportJob(t, client, user.OrganizationID, nil) + // project := coderdtest.CreateProject(t, client, user.OrganizationID, job.ID) + // _, err := client.CreateProjectVersion(context.Background(), user.OrganizationID, project.Name, coderd.CreateProjectVersionRequest{ // ImportJobID: job.ID, // }) // require.NoError(t, err) diff --git a/coderd/provisionerdaemons.go b/coderd/provisionerdaemons.go index 790f794367a15..279f680d7d4ed 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,25 +32,6 @@ 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) { api.websocketWaitGroup.Add(1) diff --git a/coderd/provisionerdaemons_test.go b/coderd/provisionerdaemons_test.go index 22584d03b5853..a8e9fdf6ea6ce 100644 --- a/coderd/provisionerdaemons_test.go +++ b/coderd/provisionerdaemons_test.go @@ -16,9 +16,10 @@ func TestProvisionerDaemons(t *testing.T) { t.Parallel() client := coderdtest.New(t, nil) + user := coderdtest.CreateFirstUser(t, client) _ = coderdtest.NewProvisionerDaemon(t, client) require.Eventually(t, func() bool { - daemons, err := client.ProvisionerDaemons(context.Background()) + daemons, err := client.ProvisionerDaemonsByOrganization(context.Background(), user.OrganizationID) require.NoError(t, err) return len(daemons) > 0 }, 3*time.Second, 50*time.Millisecond) diff --git a/coderd/provisionerjobs_test.go b/coderd/provisionerjobs_test.go index 9e65c63acef76..f91af29676aa9 100644 --- a/coderd/provisionerjobs_test.go +++ b/coderd/provisionerjobs_test.go @@ -22,7 +22,7 @@ func TestPostProvisionerImportJobByOrganization(t *testing.T) { // user := coderdtest.CreateInitialUser(t, client) // _ = coderdtest.NewProvisionerDaemon(t, client) // before := time.Now() - // job := coderdtest.CreateProjectImportJob(t, client, user.Organization, &echo.Responses{ + // job := coderdtest.CreateProjectImportJob(t, client, user.OrganizationID, &echo.Responses{ // Parse: []*proto.Parse_Response{{ // Type: &proto.Parse_Response_Complete{ // Complete: &proto.Parse_Complete{ @@ -41,7 +41,7 @@ func TestPostProvisionerImportJobByOrganization(t *testing.T) { // }, // }}, // }) - // logs, err := client.ProjectImportJobLogsAfter(context.Background(), user.Organization, job.ID, before) + // logs, err := client.ProjectImportJobLogsAfter(context.Background(), user.OrganizationID, job.ID, before) // require.NoError(t, err) // for { // log, ok := <-logs @@ -73,7 +73,7 @@ func TestPostProvisionerImportJobByOrganization(t *testing.T) { // require.NoError(t, err) // file, err := client.Upload(context.Background(), codersdk.ContentTypeTar, data) // require.NoError(t, err) - // job, err := client.CreateProjectImportJob(context.Background(), user.Organization, coderd.CreateProjectImportJobRequest{ + // job, err := client.CreateProjectImportJob(context.Background(), user.OrganizationID, coderd.CreateProjectImportJobRequest{ // StorageSource: file.Hash, // StorageMethod: database.ProvisionerStorageMethodFile, // Provisioner: database.ProvisionerTypeEcho, @@ -85,8 +85,8 @@ func TestPostProvisionerImportJobByOrganization(t *testing.T) { // }}, // }) // require.NoError(t, err) - // job = coderdtest.AwaitProjectImportJob(t, client, user.Organization, job.ID) - // values, err := client.ProjectImportJobParameters(context.Background(), user.Organization, job.ID) + // job = coderdtest.AwaitProjectImportJob(t, client, user.OrganizationID, job.ID) + // values, err := client.ProjectImportJobParameters(context.Background(), user.OrganizationID, job.ID) // require.NoError(t, err) // require.Equal(t, "somevalue", values[0].SourceValue) }) @@ -98,8 +98,8 @@ func TestProvisionerJobParametersByID(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) + // job := coderdtest.CreateProjectImportJob(t, client, user.OrganizationID, nil) + // _, err := client.ProjectImportJobParameters(context.Background(), user.OrganizationID, job.ID) // var apiErr *codersdk.Error // require.ErrorAs(t, err, &apiErr) // require.Equal(t, http.StatusPreconditionFailed, apiErr.StatusCode()) @@ -110,7 +110,7 @@ func TestProvisionerJobParametersByID(t *testing.T) { // client := coderdtest.New(t, nil) // user := coderdtest.CreateInitialUser(t, client) // _ = coderdtest.NewProvisionerDaemon(t, client) - // job := coderdtest.CreateProjectImportJob(t, client, user.Organization, &echo.Responses{ + // job := coderdtest.CreateProjectImportJob(t, client, user.OrganizationID, &echo.Responses{ // Parse: []*proto.Parse_Response{{ // Type: &proto.Parse_Response_Complete{ // Complete: &proto.Parse_Complete{ @@ -129,8 +129,8 @@ func TestProvisionerJobParametersByID(t *testing.T) { // }}, // Provision: echo.ProvisionComplete, // }) - // job = coderdtest.AwaitProjectImportJob(t, client, user.Organization, job.ID) - // params, err := client.ProjectImportJobParameters(context.Background(), user.Organization, job.ID) + // job = coderdtest.AwaitProjectImportJob(t, client, user.OrganizationID, job.ID) + // params, err := client.ProjectImportJobParameters(context.Background(), user.OrganizationID, job.ID) // require.NoError(t, err) // require.Len(t, params, 1) }) @@ -140,7 +140,7 @@ func TestProvisionerJobParametersByID(t *testing.T) { client := coderdtest.New(t, nil) user := coderdtest.CreateFirstUser(t, client) _ = coderdtest.NewProvisionerDaemon(t, client) - job := coderdtest.CreateProjectImportJob(t, client, user.Organization, &echo.Responses{ + job := coderdtest.CreateProjectVersion(t, client, user.OrganizationID, &echo.Responses{ Parse: []*proto.Parse_Response{{ Type: &proto.Parse_Response_Complete{ Complete: &proto.Parse_Complete{ @@ -160,8 +160,8 @@ func TestProvisionerJobParametersByID(t *testing.T) { }}, Provision: echo.ProvisionComplete, }) - coderdtest.AwaitProjectImportJob(t, client, user.Organization, job.ID) - // params, err := client.ProjectImportJobParameters(context.Background(), user.Organization, job.ID) + coderdtest.AwaitProjectImportJob(t, client, user.OrganizationID, job.ID) + // params, err := client.ProjectImportJobParameters(context.Background(), user.OrganizationID, job.ID) // require.NoError(t, err) // require.Len(t, params, 1) // require.NotNil(t, params[0]) @@ -176,7 +176,7 @@ func TestProvisionerJobResourcesByID(t *testing.T) { client := coderdtest.New(t, nil) user := coderdtest.CreateFirstUser(t, client) _ = coderdtest.NewProvisionerDaemon(t, client) - job := coderdtest.CreateProjectImportJob(t, client, user.Organization, &echo.Responses{ + job := coderdtest.CreateProjectVersion(t, client, user.OrganizationID, &echo.Responses{ Parse: echo.ParseComplete, Provision: []*proto.Provision_Response{{ Type: &proto.Provision_Response_Complete{ @@ -189,8 +189,8 @@ func TestProvisionerJobResourcesByID(t *testing.T) { }, }}, }) - coderdtest.AwaitProjectImportJob(t, client, user.Organization, job.ID) - // resources, err := client.ProjectImportJobResources(context.Background(), user.Organization, job.ID) + coderdtest.AwaitProjectImportJob(t, client, user.OrganizationID, job.ID) + // resources, err := client.ProjectImportJobResources(context.Background(), user.OrganizationID, job.ID) // require.NoError(t, err) // // One for start, and one for stop! // require.Len(t, resources, 2) @@ -204,7 +204,7 @@ func TestProvisionerJobLogsByName(t *testing.T) { client := coderdtest.New(t, nil) user := coderdtest.CreateFirstUser(t, client) coderdtest.NewProvisionerDaemon(t, client) - job := coderdtest.CreateProjectImportJob(t, client, user.Organization, &echo.Responses{ + job := coderdtest.CreateProjectVersion(t, client, user.OrganizationID, &echo.Responses{ Parse: echo.ParseComplete, Provision: []*proto.Provision_Response{{ Type: &proto.Provision_Response_Log{ @@ -219,17 +219,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, job.ID) + coderdtest.AwaitProjectImportJob(t, client, user.OrganizationID, job.ID) workspace := coderdtest.CreateWorkspace(t, client, "me", project.ID) history, err := client.CreateWorkspaceBuild(context.Background(), "", workspace.Name, coderd.CreateWorkspaceBuildRequest{ ProjectVersionID: project.ActiveVersionID, Transition: database.WorkspaceTransitionStart, }) require.NoError(t, err) - coderdtest.AwaitProjectImportJob(t, client, user.Organization, history.ProvisionJobID) + coderdtest.AwaitProjectImportJob(t, client, user.OrganizationID, history.ProvisionJobID) // Return the log after completion! - logs, err := client.WorkspaceProvisionJobLogsBefore(context.Background(), user.Organization, history.ProvisionJobID, time.Time{}) + logs, err := client.WorkspaceProvisionJobLogsBefore(context.Background(), user.OrganizationID, history.ProvisionJobID, time.Time{}) require.NoError(t, err) require.NotNil(t, logs) require.Len(t, logs, 1) @@ -240,7 +240,7 @@ func TestProvisionerJobLogsByName(t *testing.T) { client := coderdtest.New(t, nil) user := coderdtest.CreateFirstUser(t, client) coderdtest.NewProvisionerDaemon(t, client) - job := coderdtest.CreateProjectImportJob(t, client, user.Organization, &echo.Responses{ + job := coderdtest.CreateProjectVersion(t, client, user.OrganizationID, &echo.Responses{ Parse: echo.ParseComplete, Provision: []*proto.Provision_Response{{ Type: &proto.Provision_Response_Log{ @@ -255,8 +255,8 @@ 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, job.ID) + coderdtest.AwaitProjectImportJob(t, client, user.OrganizationID, job.ID) workspace := coderdtest.CreateWorkspace(t, client, "me", project.ID) before := time.Now().UTC() history, err := client.CreateWorkspaceBuild(context.Background(), "", workspace.Name, coderd.CreateWorkspaceBuildRequest{ @@ -264,9 +264,9 @@ func TestProvisionerJobLogsByName(t *testing.T) { Transition: database.WorkspaceTransitionStart, }) require.NoError(t, err) - coderdtest.AwaitProjectImportJob(t, client, user.Organization, history.ProvisionJobID) + coderdtest.AwaitProjectImportJob(t, client, user.OrganizationID, history.ProvisionJobID) - logs, err := client.WorkspaceProvisionJobLogsAfter(context.Background(), user.Organization, history.ProvisionJobID, before) + logs, err := client.WorkspaceProvisionJobLogsAfter(context.Background(), user.OrganizationID, history.ProvisionJobID, before) require.NoError(t, err) log, ok := <-logs require.True(t, ok) @@ -281,7 +281,7 @@ func TestProvisionerJobLogsByName(t *testing.T) { client := coderdtest.New(t, nil) user := coderdtest.CreateFirstUser(t, client) coderdtest.NewProvisionerDaemon(t, client) - job := coderdtest.CreateProjectImportJob(t, client, user.Organization, &echo.Responses{ + job := coderdtest.CreateProjectVersion(t, client, user.OrganizationID, &echo.Responses{ Parse: echo.ParseComplete, Provision: []*proto.Provision_Response{{ Type: &proto.Provision_Response_Log{ @@ -296,8 +296,8 @@ 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, job.ID) + coderdtest.AwaitProjectImportJob(t, client, user.OrganizationID, job.ID) workspace := coderdtest.CreateWorkspace(t, client, "me", project.ID) before := database.Now() history, err := client.CreateWorkspaceBuild(context.Background(), "", workspace.Name, coderd.CreateWorkspaceBuildRequest{ @@ -305,7 +305,7 @@ func TestProvisionerJobLogsByName(t *testing.T) { Transition: database.WorkspaceTransitionStart, }) require.NoError(t, err) - logs, err := client.WorkspaceProvisionJobLogsAfter(context.Background(), user.Organization, history.ProvisionJobID, before) + logs, err := client.WorkspaceProvisionJobLogsAfter(context.Background(), user.OrganizationID, history.ProvisionJobID, before) require.NoError(t, err) log := <-logs require.Equal(t, "log-output", log.Output) diff --git a/coderd/users.go b/coderd/users.go index 1cd37ed8d7c7b..6f237cc8ad587 100644 --- a/coderd/users.go +++ b/coderd/users.go @@ -27,16 +27,6 @@ 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 { - 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. type CreateUserRequest struct { Email string `json:"email" validate:"required,email"` @@ -45,6 +35,12 @@ type CreateUserRequest struct { Organization string `json:"organization" validate:"required,username"` } +// CreateUserResponse contains IDs for newly created user info. +type CreateUserResponse struct { + UserID string `json:"user_id"` + OrganizationID string `json:"organization_id"` +} + // LoginWithPasswordRequest enables callers to authenticate with email and password. type LoginWithPasswordRequest struct { Email string `json:"email" validate:"required,email"` @@ -83,7 +79,7 @@ func (api *api) firstUser(rw http.ResponseWriter, r *http.Request) { // Creates the initial user for a Coder deployment. func (api *api) postFirstUser(rw http.ResponseWriter, r *http.Request) { - var createUser CreateInitialUserRequest + var createUser CreateUserRequest if !httpapi.Read(rw, r, &createUser) { return } @@ -112,6 +108,7 @@ func (api *api) postFirstUser(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(), @@ -125,7 +122,7 @@ func (api *api) postFirstUser(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(), @@ -154,7 +151,10 @@ func (api *api) postFirstUser(rw http.ResponseWriter, r *http.Request) { } render.Status(r, http.StatusCreated) - render.JSON(rw, r, convertUser(user)) + render.JSON(rw, r, CreateUserResponse{ + UserID: user.ID, + OrganizationID: organization.ID, + }) } // Creates a new user. diff --git a/coderd/users_test.go b/coderd/users_test.go index bf6d4b70ac9ae..7fd8b9b931050 100644 --- a/coderd/users_test.go +++ b/coderd/users_test.go @@ -163,9 +163,8 @@ func TestPostLogin(t *testing.T) { t.Run("BadPassword", func(t *testing.T) { t.Parallel() client := coderdtest.New(t, nil) - user := coderdtest.CreateFirstUser(t, client) _, err := client.LoginWithPassword(context.Background(), coderd.LoginWithPasswordRequest{ - Email: user.Email, + Email: "something", Password: "badpass", }) var apiErr *codersdk.Error @@ -176,10 +175,9 @@ func TestPostLogin(t *testing.T) { t.Run("Success", func(t *testing.T) { t.Parallel() client := coderdtest.New(t, nil) - user := coderdtest.CreateFirstUser(t, client) _, err := client.LoginWithPassword(context.Background(), coderd.LoginWithPasswordRequest{ - Email: user.Email, - Password: user.Password, + Email: "something", + Password: "wow", }) require.NoError(t, err) }) diff --git a/coderd/workspaceagent_test.go b/coderd/workspaceagent_test.go index 5690416a4f7dc..81c24fd52a373 100644 --- a/coderd/workspaceagent_test.go +++ b/coderd/workspaceagent_test.go @@ -22,7 +22,7 @@ func TestWorkspaceAgentServe(t *testing.T) { user := coderdtest.CreateFirstUser(t, client) daemonCloser := coderdtest.NewProvisionerDaemon(t, client) authToken := uuid.NewString() - job := coderdtest.CreateProjectImportJob(t, client, user.Organization, &echo.Responses{ + job := coderdtest.CreateProjectVersion(t, client, user.OrganizationID, &echo.Responses{ Parse: echo.ParseComplete, ProvisionDryRun: echo.ProvisionComplete, Provision: []*proto.Provision_Response{{ @@ -42,15 +42,15 @@ func TestWorkspaceAgentServe(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, job.ID) + coderdtest.AwaitProjectImportJob(t, client, user.OrganizationID, job.ID) workspace := coderdtest.CreateWorkspace(t, client, "me", project.ID) history, err := client.CreateWorkspaceBuild(context.Background(), "", workspace.Name, coderd.CreateWorkspaceBuildRequest{ ProjectVersionID: project.ActiveVersionID, Transition: database.WorkspaceTransitionStart, }) require.NoError(t, err) - coderdtest.AwaitWorkspaceProvisionJob(t, client, user.Organization, history.ProvisionJobID) + coderdtest.AwaitWorkspaceProvisionJob(t, client, user.OrganizationID, history.ProvisionJobID) daemonCloser.Close() // agentClient := codersdk.New(client.URL) @@ -61,13 +61,13 @@ func TestWorkspaceAgentServe(t *testing.T) { // var resources []coderd.ProvisionerJobResource // require.Eventually(t, func() bool { - // resources, err = client.WorkspaceProvisionJobResources(context.Background(), user.Organization, history.ProvisionJobID) + // resources, err = client.WorkspaceProvisionJobResources(context.Background(), user.OrganizationID, history.ProvisionJobID) // require.NoError(t, err) // require.Len(t, resources, 1) // return !resources[0].Agent.UpdatedAt.IsZero() // }, 5*time.Second, 25*time.Millisecond) - // workspaceClient, err := client.WorkspaceAgentConnect(context.Background(), user.Organization, history.ProvisionJobID, resources[0].ID) + // workspaceClient, err := client.WorkspaceAgentConnect(context.Background(), user.OrganizationID, history.ProvisionJobID, resources[0].ID) // require.NoError(t, err) // stream, err := workspaceClient.NegotiateConnection(context.Background()) // require.NoError(t, err) diff --git a/coderd/workspaceagentauth_test.go b/coderd/workspaceagentauth_test.go index fea2592b0bdaf..1e12744da1aab 100644 --- a/coderd/workspaceagentauth_test.go +++ b/coderd/workspaceagentauth_test.go @@ -62,7 +62,7 @@ func TestPostWorkspaceAgentAuthenticateGoogleInstanceIdentity(t *testing.T) { // }) // user := coderdtest.CreateInitialUser(t, client) // coderdtest.NewProvisionerDaemon(t, client) - // job := coderdtest.CreateProjectImportJob(t, client, user.Organization, &echo.Responses{ + // job := coderdtest.CreateProjectImportJob(t, client, user.OrganizationID, &echo.Responses{ // Parse: echo.ParseComplete, // Provision: []*proto.Provision_Response{{ // Type: &proto.Provision_Response_Complete{ @@ -82,15 +82,15 @@ 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, job.ID) + // coderdtest.AwaitProjectImportJob(t, client, user.OrganizationID, job.ID) // workspace := coderdtest.CreateWorkspace(t, client, "me", project.ID) // firstHistory, err := client.CreateWorkspaceBuild(context.Background(), "", workspace.Name, coderd.CreateWorkspaceBuildRequest{ // ProjectVersionID: project.ActiveVersionID, // Transition: database.WorkspaceTransitionStart, // }) // require.NoError(t, err) - // coderdtest.AwaitWorkspaceProvisionJob(t, client, user.Organization, firstHistory.ProvisionJobID) + // coderdtest.AwaitWorkspaceProvisionJob(t, client, user.OrganizationID, firstHistory.ProvisionJobID) // _, err = client.AuthenticateWorkspaceAgentUsingGoogleCloudIdentity(context.Background(), "", createMetadataClient(signedKey)) // require.NoError(t, err) diff --git a/coderd/workspacehistory_test.go b/coderd/workspacehistory_test.go index 09b60c78b0762..d1cb20fcfd786 100644 --- a/coderd/workspacehistory_test.go +++ b/coderd/workspacehistory_test.go @@ -22,8 +22,8 @@ func TestPostWorkspaceBuildByUser(t *testing.T) { t.Parallel() client := coderdtest.New(t, nil) user := coderdtest.CreateFirstUser(t, client) - job := coderdtest.CreateProjectImportJob(t, client, user.Organization, nil) - project := coderdtest.CreateProject(t, client, user.Organization, job.ID) + 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.Name, coderd.CreateWorkspaceBuildRequest{ ProjectVersionID: uuid.New(), @@ -40,11 +40,11 @@ func TestPostWorkspaceBuildByUser(t *testing.T) { client := coderdtest.New(t, nil) user := coderdtest.CreateFirstUser(t, client) coderdtest.NewProvisionerDaemon(t, client) - job := coderdtest.CreateProjectImportJob(t, client, user.Organization, &echo.Responses{ + job := coderdtest.CreateProjectVersion(t, client, user.OrganizationID, &echo.Responses{ Provision: []*proto.Provision_Response{{}}, }) - project := coderdtest.CreateProject(t, client, user.Organization, job.ID) - coderdtest.AwaitProjectImportJob(t, client, user.Organization, job.ID) + project := coderdtest.CreateProject(t, client, user.OrganizationID, job.ID) + coderdtest.AwaitProjectImportJob(t, client, user.OrganizationID, job.ID) workspace := coderdtest.CreateWorkspace(t, client, "me", project.ID) _, err := client.CreateWorkspaceBuild(context.Background(), "", workspace.Name, coderd.CreateWorkspaceBuildRequest{ ProjectVersionID: project.ActiveVersionID, @@ -61,9 +61,9 @@ func TestPostWorkspaceBuildByUser(t *testing.T) { client := coderdtest.New(t, nil) user := coderdtest.CreateFirstUser(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) + job := coderdtest.CreateProjectVersion(t, client, user.OrganizationID, nil) + project := coderdtest.CreateProject(t, client, user.OrganizationID, job.ID) + coderdtest.AwaitProjectImportJob(t, client, user.OrganizationID, job.ID) // Close here so workspace build doesn't process! closeDaemon.Close() workspace := coderdtest.CreateWorkspace(t, client, "me", project.ID) @@ -87,16 +87,16 @@ func TestPostWorkspaceBuildByUser(t *testing.T) { client := coderdtest.New(t, nil) user := coderdtest.CreateFirstUser(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) + job := coderdtest.CreateProjectVersion(t, client, user.OrganizationID, nil) + project := coderdtest.CreateProject(t, client, user.OrganizationID, job.ID) + coderdtest.AwaitProjectImportJob(t, client, user.OrganizationID, job.ID) workspace := coderdtest.CreateWorkspace(t, client, "me", project.ID) firstHistory, err := client.CreateWorkspaceBuild(context.Background(), "", workspace.Name, coderd.CreateWorkspaceBuildRequest{ ProjectVersionID: project.ActiveVersionID, Transition: database.WorkspaceTransitionStart, }) require.NoError(t, err) - coderdtest.AwaitWorkspaceProvisionJob(t, client, user.Organization, firstHistory.ProvisionJobID) + coderdtest.AwaitWorkspaceProvisionJob(t, client, user.OrganizationID, firstHistory.ProvisionJobID) secondHistory, err := client.CreateWorkspaceBuild(context.Background(), "", workspace.Name, coderd.CreateWorkspaceBuildRequest{ ProjectVersionID: project.ActiveVersionID, Transition: database.WorkspaceTransitionStart, @@ -117,8 +117,8 @@ func TestWorkspaceBuildByUser(t *testing.T) { client := coderdtest.New(t, nil) user := coderdtest.CreateFirstUser(t, client) coderdtest.NewProvisionerDaemon(t, client) - job := coderdtest.CreateProjectImportJob(t, client, user.Organization, nil) - project := coderdtest.CreateProject(t, client, user.Organization, job.ID) + 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) history, err := client.ListWorkspaceBuild(context.Background(), "me", workspace.Name) require.NoError(t, err) @@ -131,9 +131,9 @@ func TestWorkspaceBuildByUser(t *testing.T) { client := coderdtest.New(t, nil) user := coderdtest.CreateFirstUser(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) + job := coderdtest.CreateProjectVersion(t, client, user.OrganizationID, nil) + project := coderdtest.CreateProject(t, client, user.OrganizationID, job.ID) + coderdtest.AwaitProjectImportJob(t, client, user.OrganizationID, job.ID) workspace := coderdtest.CreateWorkspace(t, client, "me", project.ID) _, err := client.CreateWorkspaceBuild(context.Background(), "", workspace.Name, coderd.CreateWorkspaceBuildRequest{ ProjectVersionID: project.ActiveVersionID, @@ -152,9 +152,9 @@ func TestWorkspaceBuildByName(t *testing.T) { client := coderdtest.New(t, nil) user := coderdtest.CreateFirstUser(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) + job := coderdtest.CreateProjectVersion(t, client, user.OrganizationID, nil) + coderdtest.AwaitProjectImportJob(t, client, user.OrganizationID, job.ID) + project := coderdtest.CreateProject(t, client, user.OrganizationID, job.ID) workspace := coderdtest.CreateWorkspace(t, client, "me", project.ID) history, err := client.CreateWorkspaceBuild(context.Background(), "", workspace.Name, coderd.CreateWorkspaceBuildRequest{ ProjectVersionID: project.ActiveVersionID, diff --git a/coderd/workspaces_test.go b/coderd/workspaces_test.go index 1ff37bd3808af..f2edb491f2456 100644 --- a/coderd/workspaces_test.go +++ b/coderd/workspaces_test.go @@ -29,8 +29,8 @@ func TestWorkspaces(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) + // job := coderdtest.CreateProjectImportJob(t, client, user.OrganizationID, nil) + // project := coderdtest.CreateProject(t, client, user.OrganizationID, job.ID) // _ = coderdtest.CreateWorkspace(t, client, "", project.ID) // workspaces, err := client.Workspaces(context.Background(), "") // require.NoError(t, err) @@ -58,8 +58,8 @@ func TestPostWorkspaceByUser(t *testing.T) { t.Parallel() client := coderdtest.New(t, nil) user := coderdtest.CreateFirstUser(t, client) - job := coderdtest.CreateProjectImportJob(t, client, user.Organization, nil) - project := coderdtest.CreateProject(t, client, user.Organization, job.ID) + job := coderdtest.CreateProjectVersion(t, client, user.OrganizationID, nil) + project := coderdtest.CreateProject(t, client, user.OrganizationID, job.ID) anotherUser := coderd.CreateUserRequest{ Email: "another@user.org", @@ -90,8 +90,8 @@ func TestPostWorkspaceByUser(t *testing.T) { t.Parallel() client := coderdtest.New(t, nil) user := coderdtest.CreateFirstUser(t, client) - job := coderdtest.CreateProjectImportJob(t, client, user.Organization, nil) - project := coderdtest.CreateProject(t, client, user.Organization, job.ID) + 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, @@ -107,8 +107,8 @@ func TestPostWorkspaceByUser(t *testing.T) { t.Parallel() client := coderdtest.New(t, nil) user := coderdtest.CreateFirstUser(t, client) - job := coderdtest.CreateProjectImportJob(t, client, user.Organization, nil) - project := coderdtest.CreateProject(t, client, user.Organization, job.ID) + job := coderdtest.CreateProjectVersion(t, client, user.OrganizationID, nil) + project := coderdtest.CreateProject(t, client, user.OrganizationID, job.ID) _ = coderdtest.CreateWorkspace(t, client, "", project.ID) }) } @@ -117,8 +117,8 @@ func TestWorkspaceByUser(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) + // job := coderdtest.CreateProjectImportJob(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.Name) // require.NoError(t, err) @@ -130,9 +130,9 @@ func TestWorkspacesByProject(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) + // job := coderdtest.CreateProjectImportJob(t, client, user.OrganizationID, nil) + // project := coderdtest.CreateProject(t, client, user.OrganizationID, job.ID) + // workspaces, err := client.WorkspacesByProject(context.Background(), user.OrganizationID, project.Name) // require.NoError(t, err) // require.NotNil(t, workspaces) // }) @@ -141,10 +141,10 @@ func TestWorkspacesByProject(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) + // job := coderdtest.CreateProjectImportJob(t, client, user.OrganizationID, nil) + // project := coderdtest.CreateProject(t, client, user.OrganizationID, job.ID) // _ = coderdtest.CreateWorkspace(t, client, "", project.ID) - // workspaces, err := client.WorkspacesByProject(context.Background(), user.Organization, project.Name) + // workspaces, err := client.WorkspacesByProject(context.Background(), user.OrganizationID, project.Name) // require.NoError(t, err) // require.NotNil(t, workspaces) // require.Len(t, workspaces, 1) diff --git a/codersdk/organization.go b/codersdk/organizations.go similarity index 58% rename from codersdk/organization.go rename to codersdk/organizations.go index 32c55baa5a193..c5ab486e733de 100644 --- a/codersdk/organization.go +++ b/codersdk/organizations.go @@ -7,28 +7,26 @@ import ( "net/http" "github.com/coder/coder/coderd" - "github.com/google/uuid" ) -// OrganizationsByUser returns organizations for the provided user. -func (c *Client) OrganizationsByUser(ctx context.Context, user uuid.UUID) ([]coderd.Organization, error) { - return nil, nil -} - -// OrganizationByName returns an organization by case-insensitive name. -func (c *Client) OrganizationByName(ctx context.Context, user uuid.UUID, name string) (coderd.Organization, error) { - return coderd.Organization{}, nil -} - // ProvisionerDaemonsByOrganization returns provisioner daemons available for an organization. func (c *Client) ProvisionerDaemonsByOrganization(ctx context.Context, organization string) ([]coderd.ProvisionerDaemon, error) { - return nil, nil + 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 uuid.UUID, req coderd.CreateProjectVersion) (coderd.ProjectVersion, error) { - res, err := c.request(ctx, http.MethodPost, fmt.Sprintf("/api/v2/organizations/%s/projectversion", organization), req) +func (c *Client) CreateProjectVersion(ctx context.Context, organization string, req coderd.CreateProjectVersion) (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 } @@ -55,11 +53,29 @@ func (c *Client) CreateProject(ctx context.Context, organization string, request } // ProjectsByOrganization lists all projects inside of an organization. -func (c *Client) ProjectsByOrganization(ctx context.Context, organization uuid.UUID) ([]coderd.Project, error) { - return nil, nil +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 uuid.UUID, name string) (coderd.Project, error) { - return coderd.Project{}, nil +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/provisionerdaemons.go b/codersdk/provisionerdaemons.go index 5046f9ff44438..a6eb0f2c63a57 100644 --- a/codersdk/provisionerdaemons.go +++ b/codersdk/provisionerdaemons.go @@ -20,20 +20,6 @@ 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/provisionerdaemons", 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) -} - // 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/serve") diff --git a/codersdk/user.go b/codersdk/users.go similarity index 76% rename from codersdk/user.go rename to codersdk/users.go index 9931e9a76b4f5..af5dc6d2c3931 100644 --- a/codersdk/user.go +++ b/codersdk/users.go @@ -17,7 +17,7 @@ var ( // 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/user/first", nil) + res, err := c.request(ctx, http.MethodGet, "/api/v2/users/first", nil) if err != nil { return false, err } @@ -33,17 +33,17 @@ func (c *Client) HasFirstUser(ctx context.Context) (bool, error) { // 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.CreateInitialUserRequest) (coderd.User, error) { - res, err := c.request(ctx, http.MethodPost, "/api/v2/user/first", req) +func (c *Client) CreateFirstUser(ctx context.Context, req coderd.CreateUserRequest) (coderd.CreateUserResponse, error) { + res, err := c.request(ctx, http.MethodPost, "/api/v2/users/first", req) if err != nil { - return coderd.User{}, err + return coderd.CreateUserResponse{}, err } defer res.Body.Close() if res.StatusCode != http.StatusCreated { - return coderd.User{}, readBodyAsError(res) + return coderd.CreateUserResponse{}, readBodyAsError(res) } - var user coderd.User - return user, json.NewDecoder(res.Body).Decode(&user) + var resp coderd.CreateUserResponse + return resp, json.NewDecoder(res.Body).Decode(&resp) } // CreateUser creates a new user. @@ -77,7 +77,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/user/login", req) + res, err := c.request(ctx, http.MethodPost, "/api/v2/users/login", req) if err != nil { return coderd.LoginWithPasswordResponse{}, err } @@ -98,7 +98,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/user/logout", nil) + res, err := c.request(ctx, http.MethodPost, "/api/v2/users/logout", nil) if err != nil { return err } @@ -123,3 +123,20 @@ func (c *Client) User(ctx context.Context, id string) (coderd.User, error) { var user coderd.User return user, json.NewDecoder(res.Body).Decode(&user) } + +// 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" + } + res, err := c.request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/users/%s", id), nil) + if err != nil { + return nil, err + } + defer res.Body.Close() + if res.StatusCode > http.StatusOK { + return nil, readBodyAsError(res) + } + var orgs []coderd.Organization + return orgs, json.NewDecoder(res.Body).Decode(&orgs) +} From 1726eadca554a80f8fa90b685deed8f882d25ed7 Mon Sep 17 00:00:00 2001 From: Kyle Carberry Date: Sun, 6 Mar 2022 23:05:21 +0000 Subject: [PATCH 10/22] Fix querying --- cli/projects.go | 2 +- cli/workspacecreate.go | 2 +- coderd/coderd.go | 88 ++- coderd/coderdtest/coderdtest.go | 76 ++- coderd/coderdtest/coderdtest_test.go | 11 +- coderd/organizations.go | 24 +- coderd/organizations_test.go | 5 +- coderd/projectimport.go | 4 +- coderd/projectimport_test.go | 156 ----- coderd/projects.go | 121 ++-- coderd/projects_test.go | 161 ++--- coderd/projectversion.go | 66 -- coderd/projectversion_test.go | 48 -- coderd/projectversions.go | 154 +++++ coderd/projectversions_test.go | 203 ++++++ coderd/provisionerdaemons.go | 30 +- coderd/provisionerdaemons_test.go | 26 - coderd/provisionerjobs.go | 178 +---- coderd/provisionerjobs_test.go | 244 +------ coderd/users.go | 325 ++++++++- coderd/users_test.go | 413 +++++++++--- coderd/workspaceagent.go | 134 ---- coderd/workspaceagent_test.go | 86 --- coderd/workspaceagentauth_test.go | 176 ----- coderd/workspacebuilds.go | 95 +++ coderd/workspacebuilds_test.go | 147 +++++ coderd/workspacehistory.go | 244 ------- coderd/workspacehistory_test.go | 166 ----- ...eagentauth.go => workspaceresourceauth.go} | 28 +- coderd/workspaceresourceauth_test.go | 182 ++++++ coderd/workspaceresources.go | 236 +++++++ coderd/workspaceresources_test.go | 125 ++++ coderd/workspaces.go | 272 ++++++-- coderd/workspaces_test.go | 226 ++++--- codersdk/organizations.go | 13 + codersdk/projects.go | 29 +- codersdk/projectversions.go | 46 +- codersdk/provisionerdaemons.go | 11 +- codersdk/users.go | 108 ++- codersdk/workspacebuilds.go | 51 ++ codersdk/workspaceresourceauth.go | 44 ++ ...orkspaceagent.go => workspaceresources.go} | 45 +- codersdk/workspaces.go | 123 +--- database/databasefake/databasefake.go | 88 ++- database/dump.sql | 82 +-- database/migrations/000003_workspaces.up.sql | 17 - database/migrations/000004_jobs.up.sql | 28 +- database/models.go | 50 +- database/querier.go | 19 +- database/query.sql | 61 +- database/query.sql.go | 616 ++++++++++-------- httpmw/workspaceagent.go | 6 +- httpmw/workspaceagent_test.go | 2 +- httpmw/workspaceresourceparam.go | 31 +- httpmw/workspaceresourceparam_test.go | 18 +- peerbroker/proxy.go | 28 +- provisionerd/proto/provisionerd.pb.go | 251 ++++--- provisionerd/proto/provisionerd.proto | 12 +- provisionerd/provisionerd.go | 28 +- provisionerd/provisionerd_test.go | 16 +- 60 files changed, 3532 insertions(+), 2745 deletions(-) delete mode 100644 coderd/projectimport_test.go delete mode 100644 coderd/projectversion.go delete mode 100644 coderd/projectversion_test.go create mode 100644 coderd/projectversions.go create mode 100644 coderd/projectversions_test.go delete mode 100644 coderd/provisionerdaemons_test.go delete mode 100644 coderd/workspaceagent.go delete mode 100644 coderd/workspaceagent_test.go delete mode 100644 coderd/workspaceagentauth_test.go create mode 100644 coderd/workspacebuilds.go create mode 100644 coderd/workspacebuilds_test.go delete mode 100644 coderd/workspacehistory.go delete mode 100644 coderd/workspacehistory_test.go rename coderd/{workspaceagentauth.go => workspaceresourceauth.go} (77%) create mode 100644 coderd/workspaceresourceauth_test.go create mode 100644 coderd/workspaceresources.go create mode 100644 coderd/workspaceresources_test.go create mode 100644 codersdk/workspacebuilds.go create mode 100644 codersdk/workspaceresourceauth.go rename codersdk/{workspaceagent.go => workspaceresources.go} (64%) diff --git a/cli/projects.go b/cli/projects.go index 0f04dc05cd259..18300ae5db7f4 100644 --- a/cli/projects.go +++ b/cli/projects.go @@ -40,7 +40,7 @@ func projects() *cobra.Command { return cmd } -func displayProjectImportInfo(cmd *cobra.Command, parameterSchemas []coderd.ParameterSchema, parameterValues []coderd.ComputedParameterValue, resources []coderd.ProvisionerJobResource) error { +func displayProjectImportInfo(cmd *cobra.Command, parameterSchemas []coderd.ParameterSchema, parameterValues []coderd.ComputedParameterValue, resources []coderd.WorkspaceResource) error { schemaByID := map[string]coderd.ParameterSchema{} for _, schema := range parameterSchemas { schemaByID[schema.ID.String()] = schema diff --git a/cli/workspacecreate.go b/cli/workspacecreate.go index 44f6a8d6d055e..82b12ff844fe0 100644 --- a/cli/workspacecreate.go +++ b/cli/workspacecreate.go @@ -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.WorkspaceBuildJobLogsAfter(cmd.Context(), organization.Name, history.ProvisionJobID, time.Time{}) if err != nil { return err } diff --git a/coderd/coderd.go b/coderd/coderd.go index a80f75225984c..a279b41ea328f 100644 --- a/coderd/coderd.go +++ b/coderd/coderd.go @@ -51,6 +51,7 @@ func New(options *Options) (http.Handler, func()) { httpmw.ExtractAPIKey(options.Database, nil), httpmw.ExtractOrganizationParam(options.Database), ) + r.Get("/", api.organization) r.Get("/provisionerdaemons", api.provisionerDaemonsByOrganization) r.Post("/projectversions", api.postProjectVersionsByOrganization) r.Route("/projects", func(r chi.Router) { @@ -58,6 +59,10 @@ func New(options *Options) (http.Handler, func()) { r.Get("/", api.projectsByOrganization) r.Get("/{projectname}", api.projectByOrganizationAndName) }) + r.Get("/parameters", nil) + r.Post("/parameters", nil) + r.Patch("/parameters/{name}", nil) + r.Delete("/parameters/{name}", nil) }) r.Route("/projects/{project}", func(r chi.Router) { r.Use( @@ -65,14 +70,16 @@ func New(options *Options) (http.Handler, func()) { httpmw.ExtractProjectParam(options.Database), httpmw.ExtractOrganizationParam(options.Database), ) - r.Get("/", api.projectByOrganization) - r.Delete("/", nil) - r.Get("/workspaces", api.workspacesByProject) + r.Get("/", api.project) r.Get("/parameters", api.parametersByProject) r.Post("/parameters", api.postParametersByProject) - r.Get("/versions", api.projectVersionsByOrganization) - r.Get("/versions/latest", nil) - r.Patch("/versions", nil) + r.Patch("/parameters/{name}", nil) + r.Delete("/parameters/{name}", nil) + 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( @@ -81,51 +88,74 @@ func New(options *Options) (http.Handler, func()) { httpmw.ExtractOrganizationParam(options.Database), ) - r.Get("/", nil) - r.Get("/schema", nil) - r.Get("/parameters", nil) - r.Get("/logs", nil) - r.Get("/resources", nil) + 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.provisionerDaemonsServe) + r.Get("/listen", api.provisionerDaemonsListen) }) }) r.Route("/users", func(r chi.Router) { - r.Post("/login", api.postLogin) - r.Post("/logout", api.postLogout) 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("/", 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("/keys", api.postKeyForUser) + 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("/workspaceagents", func(r chi.Router) { + r.Route("/workspaceresources", func(r chi.Router) { r.Route("/auth", func(r chi.Router) { - r.Post("/google-instance-identity", api.postAuthenticateWorkspaceAgentUsingGoogleInstanceIdentity) + r.Post("/google-instance-identity", api.postWorkspaceAuthGoogleInstanceIdentity) }) - r.Route("/me", func(r chi.Router) { + r.Route("/agent", func(r chi.Router) { r.Use(httpmw.ExtractWorkspaceAgent(options.Database)) - r.Get("/listen", nil) + 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("/workspaces/{workspace}", func(r chi.Router) { r.Use( httpmw.ExtractAPIKey(options.Database, nil), httpmw.ExtractWorkspaceParam(options.Database), - httpmw.ExtractUserParam(options.Database), ) - r.Get("/", nil) - r.Get("/builds", nil) - r.Post("/builds", nil) + 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("/workspacebuilds/{workspacebuild}", func(r chi.Router) { r.Use( @@ -133,13 +163,9 @@ func New(options *Options) (http.Handler, func()) { httpmw.ExtractWorkspaceBuildParam(options.Database), httpmw.ExtractWorkspaceParam(options.Database), ) - r.Get("/logs", nil) - r.Get("/resources", nil) - r.Route("/resources/{workspaceresource}", func(r chi.Router) { - r.Use(httpmw.ExtractWorkspaceResourceParam(options.Database)) - r.Get("/", nil) - r.Get("/dial", nil) - }) + 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 109ca0d072c77..2e3266adbc119 100644 --- a/coderd/coderdtest/coderdtest.go +++ b/coderd/coderdtest/coderdtest.go @@ -142,8 +142,8 @@ func NewProvisionerDaemon(t *testing.T, client *codersdk.Client) io.Closer { // CreateFirstUser creates a user with preset credentials and authenticates // with the passed in codersdk client. -func CreateFirstUser(t *testing.T, client *codersdk.Client) coderd.CreateUserResponse { - req := coderd.CreateUserRequest{ +func CreateFirstUser(t *testing.T, client *codersdk.Client) coderd.CreateFirstUserResponse { + req := coderd.CreateFirstUserRequest{ Email: "testuser@coder.com", Username: "testuser", Password: "testpass", @@ -161,6 +161,28 @@ func CreateFirstUser(t *testing.T, client *codersdk.Client) coderd.CreateUserRes return resp } +// 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. @@ -190,27 +212,47 @@ func CreateProject(t *testing.T, client *codersdk.Client, organization string, v } // 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 - // require.Eventually(t, func() bool { - // var err error - // provisionerJob, err = client.ProjectImportJob(context.Background(), organization, job) - // require.NoError(t, err) - // return provisionerJob.Status.Completed() - // }, 5*time.Second, 25*time.Millisecond) - return 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 + workspaceBuild, err = client.WorkspaceBuild(context.Background(), build) + require.NoError(t, err) + return workspaceBuild.Job.CompletedAt != nil + }, 5*time.Second, 25*time.Millisecond) + 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 f41e0c74e6f6a..6f4cb6127e25b 100644 --- a/coderd/coderdtest/coderdtest_test.go +++ b/coderd/coderdtest/coderdtest_test.go @@ -22,15 +22,16 @@ func TestNew(t *testing.T) { client := coderdtest.New(t, nil) user := coderdtest.CreateFirstUser(t, client) closer := coderdtest.NewProvisionerDaemon(t, client) - job := coderdtest.CreateProjectVersion(t, client, user.OrganizationID, nil) - coderdtest.AwaitProjectImportJob(t, client, user.OrganizationID, job.ID) - project := coderdtest.CreateProject(t, client, user.OrganizationID, 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.CreateWorkspaceBuild(context.Background(), "me", workspace.Name, coderd.CreateWorkspaceBuildRequest{ + 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.OrganizationID, history.ProvisionJobID) + coderdtest.AwaitWorkspaceBuildJob(t, client, build.ID) + coderdtest.AwaitWorkspaceAgents(t, client, build.ID) closer.Close() } diff --git a/coderd/organizations.go b/coderd/organizations.go index 00b2f8302f63b..42ba97f29092e 100644 --- a/coderd/organizations.go +++ b/coderd/organizations.go @@ -52,6 +52,12 @@ type CreateProjectRequest struct { VersionID uuid.UUID `json:"project_version_id" validate:"required"` } +func (api *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) { @@ -109,6 +115,7 @@ func (api *api) postProjectVersionsByOrganization(rw http.ResponseWriter, r *htt } 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 { @@ -128,7 +135,7 @@ func (api *api) postProjectVersionsByOrganization(rw http.ResponseWriter, r *htt } } - job, err := api.Database.InsertProvisionerJob(r.Context(), database.InsertProvisionerJobParams{ + provisionerJob, err = api.Database.InsertProvisionerJob(r.Context(), database.InsertProvisionerJobParams{ ID: jobID, CreatedAt: database.Now(), UpdatedAt: database.Now(), @@ -160,7 +167,7 @@ func (api *api) postProjectVersionsByOrganization(rw http.ResponseWriter, r *htt UpdatedAt: database.Now(), Name: namesgenerator.GetRandomName(1), Description: "", - JobID: job.ID, + JobID: provisionerJob.ID, }) if err != nil { return xerrors.Errorf("insert project version: %w", err) @@ -175,7 +182,7 @@ func (api *api) postProjectVersionsByOrganization(rw http.ResponseWriter, r *htt } render.Status(r, http.StatusCreated) - render.JSON(rw, r, convertProjectVersion(projectVersion)) + render.JSON(rw, r, convertProjectVersion(projectVersion, convertProvisionerJob(provisionerJob))) } // Create a new project in an organization. @@ -227,7 +234,6 @@ func (api *api) postProjectsByOrganization(rw http.ResponseWriter, r *http.Reque 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(), @@ -235,21 +241,17 @@ func (api *api) postProjectsByOrganization(rw http.ResponseWriter, r *http.Reque OrganizationID: organization.ID, Name: createProject.Name, Provisioner: importJob.Provisioner, - ActiveVersionID: projectVersionID, + ActiveVersionID: projectVersion.ID, }) if err != nil { return xerrors.Errorf("insert project: %s", err) } - _, err = db.InsertProjectVersion(r.Context(), database.InsertProjectVersionParams{ - ID: projectVersionID, + err = db.UpdateProjectVersionByID(r.Context(), database.UpdateProjectVersionByIDParams{ + ID: projectVersion.ID, ProjectID: uuid.NullUUID{ UUID: dbProject.ID, Valid: true, }, - CreatedAt: database.Now(), - UpdatedAt: database.Now(), - Name: namesgenerator.GetRandomName(1), - JobID: importJob.ID, }) if err != nil { return xerrors.Errorf("insert project version: %s", err) diff --git a/coderd/organizations_test.go b/coderd/organizations_test.go index 3efa0f5085ea9..0af786f2e3a73 100644 --- a/coderd/organizations_test.go +++ b/coderd/organizations_test.go @@ -5,13 +5,14 @@ import ( "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/google/uuid" - "github.com/stretchr/testify/require" ) func TestProvisionerDaemonsByOrganization(t *testing.T) { diff --git a/coderd/projectimport.go b/coderd/projectimport.go index bd24ae75bf3da..55f922c6ebbce 100644 --- a/coderd/projectimport.go +++ b/coderd/projectimport.go @@ -112,7 +112,7 @@ func (api *api) postProjectImportByOrganization(rw http.ResponseWriter, r *http. func (api *api) projectImportJobSchemasByID(rw http.ResponseWriter, r *http.Request) { // job := httpmw.ProvisionerJobParam(r) var job database.ProvisionerJob - if !convertProvisionerJob(job).Status.Completed() { + if convertProvisionerJob(job).CompletedAt == nil { httpapi.Write(rw, http.StatusPreconditionFailed, httpapi.Response{ Message: "Job hasn't completed!", }) @@ -140,7 +140,7 @@ func (api *api) projectImportJobSchemasByID(rw http.ResponseWriter, r *http.Requ func (api *api) projectImportJobParametersByID(rw http.ResponseWriter, r *http.Request) { apiKey := httpmw.APIKey(r) var job database.ProvisionerJob - if !convertProvisionerJob(job).Status.Completed() { + if !job.CompletedAt.Valid { httpapi.Write(rw, http.StatusPreconditionFailed, httpapi.Response{ Message: "Job hasn't completed!", }) diff --git a/coderd/projectimport_test.go b/coderd/projectimport_test.go deleted file mode 100644 index 083f728a5037c..0000000000000 --- a/coderd/projectimport_test.go +++ /dev/null @@ -1,156 +0,0 @@ -package coderd_test - -import ( - "testing" - - "github.com/coder/coder/coderd/coderdtest" - "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.OrganizationID, 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.CreateFirstUser(t, client) - _ = coderdtest.CreateProjectVersion(t, client, user.OrganizationID, 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.OrganizationID, nil) - // _, err := client.ProjectImportJobSchemas(context.Background(), user.OrganizationID, 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.CreateFirstUser(t, client) - coderdtest.NewProvisionerDaemon(t, client) - job := 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.AwaitProjectImportJob(t, client, user.OrganizationID, job.ID) - // schemas, err := client.ProjectImportJobSchemas(context.Background(), user.OrganizationID, 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.OrganizationID, nil) - // _, err := client.ProjectImportJobSchemas(context.Background(), user.OrganizationID, 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.CreateFirstUser(t, client) - coderdtest.NewProvisionerDaemon(t, client) - job := 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.AwaitProjectImportJob(t, client, user.OrganizationID, job.ID) - // params, err := client.ProjectImportJobParameters(context.Background(), user.OrganizationID, 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.OrganizationID, nil) - // _, err := client.ProjectImportJobResources(context.Background(), user.OrganizationID, 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.CreateFirstUser(t, client) - coderdtest.NewProvisionerDaemon(t, client) - job := 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", - }}, - }, - }, - }}, - }) - coderdtest.AwaitProjectImportJob(t, client, user.OrganizationID, job.ID) - // resources, err := client.ProjectImportJobResources(context.Background(), user.OrganizationID, 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 adb28a0960984..30cccefa9ccf1 100644 --- a/coderd/projects.go +++ b/coderd/projects.go @@ -7,6 +7,7 @@ import ( "net/http" "time" + "github.com/go-chi/chi/v5" "github.com/go-chi/render" "github.com/google/uuid" @@ -40,35 +41,10 @@ type Project struct { WorkspaceOwnerCount uint32 `json:"workspace_owner_count"` } -// 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 } @@ -78,16 +54,13 @@ func (api *api) projects(rw http.ResponseWriter, r *http.Request) { }) return } - render.Status(r, http.StatusOK) - render.JSON(rw, r, convertProjects(projects, workspaceCounts)) -} - -// Returns a single project. -func (*api) projectByOrganization(rw http.ResponseWriter, r *http.Request) { - project := httpmw.ProjectParam(r) + count := uint32(0) + if len(workspaceCounts) > 0 { + count = uint32(workspaceCounts[0].Count) + } render.Status(r, http.StatusOK) - render.JSON(rw, r, project) + render.JSON(rw, r, convertProject(project, count)) } // Creates parameters for a project. @@ -147,34 +120,82 @@ func (api *api) parametersByProject(rw http.ResponseWriter, r *http.Request) { render.JSON(rw, r, apiParameterValues) } -// Returns all workspaces for a specific project. -func (api *api) workspacesByProject(rw http.ResponseWriter, r *http.Request) { - apiKey := httpmw.APIKey(r) +func (api *api) projectVersionsByProject(rw http.ResponseWriter, r *http.Request) { project := httpmw.ProjectParam(r) - workspaces, err := api.Database.GetWorkspacesByProjectAndUserID(r.Context(), database.GetWorkspacesByProjectAndUserIDParams{ - OwnerID: apiKey.UserID, - ProjectID: project.ID, - }) + + 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 workspaces: %s", err), + Message: fmt.Sprintf("get project version: %s", err), }) return } + 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 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)) + 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 + } + apiVersion = append(apiVersion, convertProjectVersion(version, convertProvisionerJob(job))) } render.Status(r, http.StatusOK) - render.JSON(rw, r, apiWorkspaces) + render.JSON(rw, r, apiVersion) } -func (api *api) workspaceByProjectAndName(rw http.ResponseWriter, r *http.Request) { +func (api *api) projectVersionByName(rw http.ResponseWriter, r *http.Request) { + project := httpmw.ProjectParam(r) + 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 + } + if err != nil { + httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ + Message: fmt.Sprintf("get project version by name: %s", err), + }) + return + } + 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 convertProjects(projects []database.Project, workspaceCounts []database.GetWorkspaceOwnerCountsByProjectIDsRow) []Project { diff --git a/coderd/projects_test.go b/coderd/projects_test.go index b7d02a1d1677b..6d4f2b99bb211 100644 --- a/coderd/projects_test.go +++ b/coderd/projects_test.go @@ -2,67 +2,29 @@ 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 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.OrganizationID, nil) - // _ = coderdtest.CreateProject(t, client, user.OrganizationID, 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.Parallel() - // client := coderdtest.New(t, nil) - // user := coderdtest.CreateInitialUser(t, client) - // coderdtest.NewProvisionerDaemon(t, client) - // job := coderdtest.CreateProjectImportJob(t, client, user.OrganizationID, nil) - // coderdtest.AwaitProjectImportJob(t, client, user.OrganizationID, job.ID) - // project := coderdtest.CreateProject(t, client, user.OrganizationID, job.ID) - // _ = coderdtest.CreateWorkspace(t, client, "", project.ID) - // _ = coderdtest.CreateWorkspace(t, client, "", project.ID) - // projects, err := client.Projects(context.Background(), "") - // require.NoError(t, err) - // require.Len(t, projects, 1) - // require.Equal(t, projects[0].WorkspaceOwnerCount, uint32(1)) - // }) -} - -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.OrganizationID, nil) - // project := coderdtest.CreateProject(t, client, user.OrganizationID, job.ID) - // _, err := client.Project(context.Background(), user.OrganizationID, project.Name) - // require.NoError(t, err) - // }) + 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) + project := coderdtest.CreateProject(t, client, user.OrganizationID, version.ID) + _, err := client.Project(context.Background(), project.ID) + require.NoError(t, err) + }) } func TestPostParametersByProject(t *testing.T) { @@ -73,7 +35,7 @@ func TestPostParametersByProject(t *testing.T) { user := coderdtest.CreateFirstUser(t, client) job := coderdtest.CreateProjectVersion(t, client, user.OrganizationID, nil) project := coderdtest.CreateProject(t, client, user.OrganizationID, job.ID) - _, err := client.CreateProjectParameter(context.Background(), user.OrganizationID, project.Name, coderd.CreateParameterValueRequest{ + _, err := client.CreateProjectParameter(context.Background(), project.ID, coderd.CreateParameterValueRequest{ Name: "somename", SourceValue: "tomato", SourceScheme: database.ParameterSourceSchemeData, @@ -85,33 +47,74 @@ func TestPostParametersByProject(t *testing.T) { func TestParametersByProject(t *testing.T) { t.Parallel() - // t.Run("ListEmpty", func(t *testing.T) { - // t.Parallel() - // client := coderdtest.New(t, nil) - // user := coderdtest.CreateInitialUser(t, client) - // job := coderdtest.CreateProjectImportJob(t, client, user.OrganizationID, nil) - // project := coderdtest.CreateProject(t, client, user.OrganizationID, job.ID) - // params, err := client.ProjectParameters(context.Background(), user.OrganizationID, project.Name) - // require.NoError(t, err) - // require.NotNil(t, params) - // }) + t.Run("ListEmpty", func(t *testing.T) { + t.Parallel() + client := coderdtest.New(t, nil) + coderdtest.NewProvisionerDaemon(t, client) + user := coderdtest.CreateFirstUser(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) + params, err := client.ProjectParameters(context.Background(), project.ID) + 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.OrganizationID, nil) - // project := coderdtest.CreateProject(t, client, user.OrganizationID, job.ID) - // _, err := client.CreateProjectParameter(context.Background(), user.OrganizationID, 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.OrganizationID, project.Name) - // require.NoError(t, err) - // require.NotNil(t, params) - // require.Len(t, params, 1) - // }) + 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) + project := coderdtest.CreateProject(t, client, user.OrganizationID, version.ID) + _, err := client.CreateProjectParameter(context.Background(), project.ID, coderd.CreateParameterValueRequest{ + Name: "example", + SourceValue: "source-value", + SourceScheme: database.ParameterSourceSchemeData, + DestinationScheme: database.ParameterDestinationSchemeEnvironmentVariable, + }) + require.NoError(t, err) + params, err := client.ProjectParameters(context.Background(), project.ID) + require.NoError(t, err) + require.NotNil(t, params) + require.Len(t, params, 1) + }) +} + +func TestProjectVersionsByProject(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) + project := coderdtest.CreateProject(t, client, user.OrganizationID, version.ID) + versions, err := client.ProjectVersionsByProject(context.Background(), project.ID) + require.NoError(t, err) + require.Len(t, versions, 1) + }) +} + +func TestProjectVersionByName(t *testing.T) { + t.Parallel() + t.Run("NotFound", 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.ProjectVersionByName(context.Background(), project.ID, "nothing") + 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.ProjectVersionByName(context.Background(), project.ID, version.Name) + require.NoError(t, err) + }) } diff --git a/coderd/projectversion.go b/coderd/projectversion.go deleted file mode 100644 index d8f72e7cc6d62..0000000000000 --- a/coderd/projectversion.go +++ /dev/null @@ -1,66 +0,0 @@ -package coderd - -import ( - "database/sql" - "errors" - "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" -) - -// 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"` - JobID uuid.UUID `json:"import_job_id"` -} - -// 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)) -} - -func convertProjectVersion(version database.ProjectVersion) ProjectVersion { - return ProjectVersion{ - ID: version.ID, - ProjectID: &version.ProjectID.UUID, - CreatedAt: version.CreatedAt, - UpdatedAt: version.UpdatedAt, - Name: version.Name, - JobID: version.JobID, - } -} diff --git a/coderd/projectversion_test.go b/coderd/projectversion_test.go deleted file mode 100644 index 1ce6fb33b8fdf..0000000000000 --- a/coderd/projectversion_test.go +++ /dev/null @@ -1,48 +0,0 @@ -package coderd_test - -import ( - "testing" -) - -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.OrganizationID, nil) - // project := coderdtest.CreateProject(t, client, user.OrganizationID, job.ID) - // versions, err := client.ProjectVersions(context.Background(), user.OrganizationID, 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.OrganizationID, nil) - // project := coderdtest.CreateProject(t, client, user.OrganizationID, job.ID) - // _, err := client.ProjectVersion(context.Background(), user.OrganizationID, 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.OrganizationID, nil) - // project := coderdtest.CreateProject(t, client, user.OrganizationID, job.ID) - // _, err := client.CreateProjectVersion(context.Background(), user.OrganizationID, 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..430e1ca130b35 --- /dev/null +++ b/coderd/projectversions.go @@ -0,0 +1,154 @@ +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, + } +} + +func convertProjectVersionParameterSchema(schema database.ParameterSchema) ProjectVersionParameterSchema { + return ProjectVersionParameterSchema(schema) +} diff --git a/coderd/projectversions_test.go b/coderd/projectversions_test.go new file mode 100644 index 0000000000000..8cc934e7b7bbc --- /dev/null +++ b/coderd/projectversions_test.go @@ -0,0 +1,203 @@ +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", + }}, + }, + }, + }}, + }) + logs, err := client.ProjectVersionLogsAfter(context.Background(), 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 279f680d7d4ed..dfd9b13ed52d6 100644 --- a/coderd/provisionerdaemons.go +++ b/coderd/provisionerdaemons.go @@ -33,7 +33,7 @@ import ( type ProvisionerDaemon database.ProvisionerDaemon // 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() @@ -162,7 +162,7 @@ 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 { @@ -216,8 +216,8 @@ func (server *provisionerdServer) AcquireJob(ctx context.Context, _ *proto.Empty return nil, failJob(fmt.Sprint("convert workspace transition: %w", err)) } - protoJob.Type = &proto.AcquiredJob_WorkspaceProvision_{ - WorkspaceProvision: &proto.AcquiredJob_WorkspaceProvision{ + protoJob.Type = &proto.AcquiredJob_WorkspaceBuild_{ + WorkspaceBuild: &proto.AcquiredJob_WorkspaceBuild{ WorkspaceBuildId: workspaceBuild.ID.String(), WorkspaceName: workspace.Name, State: workspaceBuild.ProvisionerState, @@ -412,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 @@ -424,7 +424,7 @@ func (server *provisionerdServer) FailJob(ctx context.Context, failJob *proto.Fa 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 build state: %w", err) @@ -461,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) } @@ -483,7 +483,7 @@ 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 { @@ -510,14 +510,14 @@ func (server *provisionerdServer) CompleteJob(ctx context.Context, completed *pr 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 build: %w", err) } // This could be a bulk insert to improve performance. - for _, protoResource := range jobType.WorkspaceProvision.Resources { - err = insertProvisionerJobResource(ctx, db, job.ID, workspaceBuild.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) } @@ -535,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, @@ -578,7 +578,7 @@ func insertProvisionerJobResource(ctx context.Context, db database.Store, jobID } } - _, err := db.InsertProvisionerJobAgent(ctx, database.InsertProvisionerJobAgentParams{ + _, err := db.InsertWorkspaceAgent(ctx, database.InsertWorkspaceAgentParams{ ID: resource.AgentID.UUID, CreatedAt: database.Now(), ResourceID: resource.ID, diff --git a/coderd/provisionerdaemons_test.go b/coderd/provisionerdaemons_test.go deleted file mode 100644 index a8e9fdf6ea6ce..0000000000000 --- a/coderd/provisionerdaemons_test.go +++ /dev/null @@ -1,26 +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) - user := coderdtest.CreateFirstUser(t, client) - _ = coderdtest.NewProvisionerDaemon(t, client) - require.Eventually(t, func() bool { - daemons, err := client.ProvisionerDaemonsByOrganization(context.Background(), user.OrganizationID) - 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 d6ca11c74a439..62f7b6f64ed0e 100644 --- a/coderd/provisionerjobs.go +++ b/coderd/provisionerjobs.go @@ -12,7 +12,6 @@ import ( "github.com/go-chi/render" "github.com/google/uuid" - "golang.org/x/xerrors" "cdr.dev/slog" @@ -20,35 +19,27 @@ import ( "github.com/coder/coder/httpapi" ) +// 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"` - Agent *ProvisionerJobAgent `json:"agent,omitempty"` -} - -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) { - // 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() } - var job database.ProvisionerJob if !follow { logs, err := api.Database.GetProvisionerLogsByIDBetween(r.Context(), database.GetProvisionerLogsByIDBetweenParams{ JobID: job.ID, @@ -231,90 +196,51 @@ 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) provisionerJobResourcesByID(rw http.ResponseWriter, r *http.Request) { - var job database.ProvisionerJob - if !convertProvisionerJob(job).Status.Completed() { +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.GetProvisionerJobResourcesByJobID(r.Context(), job.ID) - if err != nil { - httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ - Message: fmt.Sprintf("get provisioner job resources: %s", err), - }) - return - } - apiResources := make([]ProvisionerJobResource, 0) - for _, resource := range resources { - if !resource.AgentID.Valid { - apiResources = append(apiResources, convertProvisionerJobResource(resource, nil)) - continue - } - agent, err := api.Database.GetProvisionerJobAgentByResourceID(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 := convertProvisionerJobAgent(agent) - if err != nil { - httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ - Message: fmt.Sprintf("convert provisioner job agent: %s", err), - }) - return - } - apiResources = append(apiResources, convertProvisionerJobResource(resource, &apiAgent)) - } - render.Status(r, http.StatusOK) - render.JSON(rw, r, apiResources) -} - -func (api *api) provisionerJobResourceByID(rw http.ResponseWriter, r *http.Request) { - var job database.ProvisionerJob - if !convertProvisionerJob(job).Status.Completed() { - 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 } - resources, err := api.Database.GetProvisionerJobResourcesByJobID(r.Context(), job.ID) if err != nil { httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ Message: fmt.Sprintf("get provisioner job resources: %s", err), }) return } - apiResources := make([]ProvisionerJobResource, 0) + apiResources := make([]WorkspaceResource, 0) for _, resource := range resources { if !resource.AgentID.Valid { - apiResources = append(apiResources, convertProvisionerJobResource(resource, nil)) + apiResources = append(apiResources, convertWorkspaceResource(resource, nil)) continue } - agent, err := api.Database.GetProvisionerJobAgentByResourceID(r.Context(), resource.ID) + 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 := convertProvisionerJobAgent(agent) + 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, convertProvisionerJobResource(resource, &apiAgent)) + apiResources = append(apiResources, convertWorkspaceResource(resource, &apiAgent)) } render.Status(r, http.StatusOK) render.JSON(rw, r, apiResources) @@ -332,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 } @@ -354,56 +275,25 @@ 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 } -func convertProvisionerJobResource(resource database.ProvisionerJobResource, agent *ProvisionerJobAgent) ProvisionerJobResource { - return ProvisionerJobResource{ - ID: resource.ID, - CreatedAt: resource.CreatedAt, - JobID: resource.JobID, - Transition: resource.Transition, - Type: resource.Type, - Name: resource.Name, - Agent: agent, - } -} - -func convertProvisionerJobAgent(agent database.ProvisionerJobAgent) (ProvisionerJobAgent, error) { - var envs map[string]string - if agent.EnvironmentVariables.Valid { - err := json.Unmarshal(agent.EnvironmentVariables.RawMessage, &envs) - if err != nil { - return ProvisionerJobAgent{}, xerrors.Errorf("unmarshal: %w", err) - } - } - return ProvisionerJobAgent{ - ID: agent.ID, - CreatedAt: agent.CreatedAt, - UpdatedAt: agent.UpdatedAt.Time, - ResourceID: agent.ResourceID, - InstanceID: agent.AuthInstanceID.String, - StartupScript: agent.StartupScript.String, - EnvironmentVariables: envs, - }, nil -} - func provisionerJobLogsChannel(jobID uuid.UUID) string { return fmt.Sprintf("provisioner-log-logs:%s", jobID) } diff --git a/coderd/provisionerjobs_test.go b/coderd/provisionerjobs_test.go index f91af29676aa9..e790a317bcf2c 100644 --- a/coderd/provisionerjobs_test.go +++ b/coderd/provisionerjobs_test.go @@ -14,233 +14,14 @@ import ( "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.OrganizationID, &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.OrganizationID, 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.Upload(context.Background(), codersdk.ContentTypeTar, data) - // require.NoError(t, err) - // job, err := client.CreateProjectImportJob(context.Background(), user.OrganizationID, 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.OrganizationID, job.ID) - // values, err := client.ProjectImportJobParameters(context.Background(), user.OrganizationID, 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.OrganizationID, nil) - // _, err := client.ProjectImportJobParameters(context.Background(), user.OrganizationID, 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.OrganizationID, &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.OrganizationID, job.ID) - // params, err := client.ProjectImportJobParameters(context.Background(), user.OrganizationID, 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.CreateFirstUser(t, client) - _ = coderdtest.NewProvisionerDaemon(t, client) - job := 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", - 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.OrganizationID, job.ID) - // params, err := client.ProjectImportJobParameters(context.Background(), user.OrganizationID, 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.CreateFirstUser(t, client) - _ = coderdtest.NewProvisionerDaemon(t, client) - job := 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: "hello", - Type: "ec2_instance", - }}, - }, - }, - }}, - }) - coderdtest.AwaitProjectImportJob(t, client, user.OrganizationID, job.ID) - // resources, err := client.ProjectImportJobResources(context.Background(), user.OrganizationID, 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.Parallel() - client := coderdtest.New(t, nil) - user := coderdtest.CreateFirstUser(t, client) - coderdtest.NewProvisionerDaemon(t, client) - job := 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: "log-output", - }, - }, - }, { - Type: &proto.Provision_Response_Complete{ - Complete: &proto.Provision_Complete{}, - }, - }}, - }) - project := coderdtest.CreateProject(t, client, user.OrganizationID, job.ID) - coderdtest.AwaitProjectImportJob(t, client, user.OrganizationID, job.ID) - workspace := coderdtest.CreateWorkspace(t, client, "me", project.ID) - history, err := client.CreateWorkspaceBuild(context.Background(), "", workspace.Name, coderd.CreateWorkspaceBuildRequest{ - ProjectVersionID: project.ActiveVersionID, - Transition: database.WorkspaceTransitionStart, - }) - require.NoError(t, err) - coderdtest.AwaitProjectImportJob(t, client, user.OrganizationID, history.ProvisionJobID) - // Return the log after completion! - logs, err := client.WorkspaceProvisionJobLogsBefore(context.Background(), user.OrganizationID, history.ProvisionJobID, time.Time{}) - require.NoError(t, err) - require.NotNil(t, logs) - require.Len(t, logs, 1) - }) - t.Run("StreamAfterComplete", func(t *testing.T) { t.Parallel() client := coderdtest.New(t, nil) user := coderdtest.CreateFirstUser(t, client) coderdtest.NewProvisionerDaemon(t, client) - job := coderdtest.CreateProjectVersion(t, client, user.OrganizationID, &echo.Responses{ + version := coderdtest.CreateProjectVersion(t, client, user.OrganizationID, &echo.Responses{ Parse: echo.ParseComplete, Provision: []*proto.Provision_Response{{ Type: &proto.Provision_Response_Log{ @@ -255,18 +36,18 @@ func TestProvisionerJobLogsByName(t *testing.T) { }, }}, }) - project := coderdtest.CreateProject(t, client, user.OrganizationID, job.ID) - coderdtest.AwaitProjectImportJob(t, client, user.OrganizationID, 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.CreateWorkspaceBuild(context.Background(), "", workspace.Name, coderd.CreateWorkspaceBuildRequest{ + 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.OrganizationID, history.ProvisionJobID) + coderdtest.AwaitWorkspaceBuildJob(t, client, build.ID) - logs, err := client.WorkspaceProvisionJobLogsAfter(context.Background(), user.OrganizationID, history.ProvisionJobID, before) + logs, err := client.WorkspaceBuildLogsAfter(context.Background(), build.ID, before) require.NoError(t, err) log, ok := <-logs require.True(t, ok) @@ -281,7 +62,7 @@ func TestProvisionerJobLogsByName(t *testing.T) { client := coderdtest.New(t, nil) user := coderdtest.CreateFirstUser(t, client) coderdtest.NewProvisionerDaemon(t, client) - job := coderdtest.CreateProjectVersion(t, client, user.OrganizationID, &echo.Responses{ + version := coderdtest.CreateProjectVersion(t, client, user.OrganizationID, &echo.Responses{ Parse: echo.ParseComplete, Provision: []*proto.Provision_Response{{ Type: &proto.Provision_Response_Log{ @@ -296,20 +77,19 @@ func TestProvisionerJobLogsByName(t *testing.T) { }, }}, }) - project := coderdtest.CreateProject(t, client, user.OrganizationID, job.ID) - coderdtest.AwaitProjectImportJob(t, client, user.OrganizationID, 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.CreateWorkspaceBuild(context.Background(), "", workspace.Name, coderd.CreateWorkspaceBuildRequest{ + 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.OrganizationID, history.ProvisionJobID, before) + logs, err := client.WorkspaceBuildLogsAfter(context.Background(), build.ID, before) require.NoError(t, err) log := <-logs require.Equal(t, "log-output", log.Output) - // Make sure the channel automatically closes! _, ok := <-logs require.False(t, ok) }) diff --git a/coderd/users.go b/coderd/users.go index 6f237cc8ad587..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,20 +28,26 @@ type User struct { Username string `json:"username" validate:"required"` } -// CreateUserRequest provides options for creating a new user. -type CreateUserRequest 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"` } -// CreateUserResponse contains IDs for newly created user info. -type CreateUserResponse struct { +// 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"` + OrganizationID string `json:"organization_id" validate:"required"` +} + // LoginWithPasswordRequest enables callers to authenticate with email and password. type LoginWithPasswordRequest struct { Email string `json:"email" validate:"required,email"` @@ -57,6 +64,16 @@ 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) firstUser(rw http.ResponseWriter, r *http.Request) { userCount, err := api.Database.GetUserCount(r.Context()) @@ -79,7 +96,7 @@ func (api *api) firstUser(rw http.ResponseWriter, r *http.Request) { // Creates the initial user for a Coder deployment. func (api *api) postFirstUser(rw http.ResponseWriter, r *http.Request) { - var createUser CreateUserRequest + var createUser CreateFirstUserRequest if !httpapi.Read(rw, r, &createUser) { return } @@ -151,7 +168,7 @@ func (api *api) postFirstUser(rw http.ResponseWriter, r *http.Request) { } render.Status(r, http.StatusCreated) - render.JSON(rw, r, CreateUserResponse{ + render.JSON(rw, r, CreateFirstUserResponse{ UserID: user.ID, OrganizationID: organization.ID, }) @@ -159,6 +176,8 @@ func (api *api) postFirstUser(rw http.ResponseWriter, r *http.Request) { // 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 @@ -180,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{ @@ -188,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 } @@ -241,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 @@ -319,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) @@ -376,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 7fd8b9b931050..fb948c8208b79 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) + client := coderdtest.New(t, nil) + _, 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.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.CreateInitialUser(t, client) - // has, err := client.HasInitialUser(context.Background()) - // require.NoError(t, err) - // require.True(t, has) + 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) + client := coderdtest.New(t, nil) + _, 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", - // }) - // var apiErr *codersdk.Error - // require.ErrorAs(t, err, &apiErr) - // require.Equal(t, http.StatusConflict, apiErr.StatusCode()) + client := coderdtest.New(t, nil) + 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.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.CreateFirstUser(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.Parallel() - // client := coderdtest.New(t, nil) - // _, err := client.CreateInitialUser(context.Background(), coderd.CreateInitialUserRequest{}) - // 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", - // }) - // var apiErr *codersdk.Error - // require.ErrorAs(t, err, &apiErr) - // require.Equal(t, http.StatusConflict, apiErr.StatusCode()) - // }) + t.Run("NoAuth", func(t *testing.T) { + t.Parallel() + client := coderdtest.New(t, nil) + _, 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) + 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.CreateFirstUser(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) }) @@ -111,25 +211,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(), "") - // require.NoError(t, err) - // require.NotNil(t, orgs) - // require.Len(t, orgs, 1) + client := coderdtest.New(t, nil) + _ = coderdtest.CreateFirstUser(t, client) + orgs, err := client.OrganizationsByUser(context.Background(), "") + require.NoError(t, err) + require.NotNil(t, orgs) + require.Len(t, orgs, 1) +} + +func TestOrganizationByUserAndName(t *testing.T) { + t.Parallel() + t.Run("NoExist", func(t *testing.T) { + t.Parallel() + client := coderdtest.New(t, nil) + 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()) + }) + + 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("Valid", func(t *testing.T) { + t.Parallel() + client := coderdtest.New(t, nil) + 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 TestPostKey(t *testing.T) { +func TestPostOrganizationsByUser(t *testing.T) { + t.Parallel() + t.Run("Conflict", func(t *testing.T) { + client := coderdtest.New(t, nil) + 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.StatusConflict, apiErr.StatusCode()) + }) + + t.Run("Create", func(t *testing.T) { + client := coderdtest.New(t, nil) + _ = 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) - // Clear session token client.SessionToken = "" - // ...and request an API key - _, err := client.CreateAPIKey(context.Background()) + _, err := client.CreateAPIKey(context.Background(), "") var apiErr *codersdk.Error require.ErrorAs(t, err, &apiErr) require.Equal(t, http.StatusUnauthorized, apiErr.StatusCode()) @@ -139,73 +301,120 @@ func TestPostKey(t *testing.T) { t.Parallel() client := coderdtest.New(t, nil) _ = coderdtest.CreateFirstUser(t, client) - apiKey, err := client.CreateAPIKey(context.Background()) + apiKey, err := client.CreateAPIKey(context.Background(), "") require.NotNil(t, apiKey) require.GreaterOrEqual(t, len(apiKey.Key), 2) require.NoError(t, err) }) } -func TestPostLogin(t *testing.T) { +func TestPostWorkspacesByUser(t *testing.T) { t.Parallel() - t.Run("InvalidUser", func(t *testing.T) { + t.Run("InvalidProject", func(t *testing.T) { t.Parallel() client := coderdtest.New(t, nil) - _, err := client.LoginWithPassword(context.Background(), coderd.LoginWithPasswordRequest{ - Email: "my@email.org", - Password: "password", + _ = 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.StatusUnauthorized, apiErr.StatusCode()) + require.Equal(t, http.StatusBadRequest, apiErr.StatusCode()) }) - t.Run("BadPassword", func(t *testing.T) { + t.Run("NoProjectAccess", func(t *testing.T) { t.Parallel() client := coderdtest.New(t, nil) - _, err := client.LoginWithPassword(context.Background(), coderd.LoginWithPasswordRequest{ - Email: "something", - Password: "badpass", + 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) + version := coderdtest.CreateProjectVersion(t, other, org.ID, nil) + project := coderdtest.CreateProject(t, other, org.ID, version.ID) + + _, err = client.CreateWorkspace(context.Background(), "", coderd.CreateWorkspaceRequest{ + ProjectID: project.ID, + Name: "workspace", + }) + require.Error(t, err) var apiErr *codersdk.Error require.ErrorAs(t, err, &apiErr) require.Equal(t, http.StatusUnauthorized, apiErr.StatusCode()) }) - t.Run("Success", func(t *testing.T) { + t.Run("AlreadyExists", func(t *testing.T) { t.Parallel() client := coderdtest.New(t, nil) - _, err := client.LoginWithPassword(context.Background(), coderd.LoginWithPasswordRequest{ - Email: "something", - Password: "wow", + 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.NoError(t, err) + require.Error(t, err) + var apiErr *codersdk.Error + require.ErrorAs(t, err, &apiErr) + require.Equal(t, http.StatusConflict, apiErr.StatusCode()) + }) + + t.Run("Create", func(t *testing.T) { + t.Parallel() + client := coderdtest.New(t, 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) }) } -func TestPostLogout(t *testing.T) { +func TestWorkspacesByUser(t *testing.T) { t.Parallel() - - t.Run("ClearCookie", func(t *testing.T) { + t.Run("ListEmpty", 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") - - 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") + 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) + }) +} - 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 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/workspaceagent.go b/coderd/workspaceagent.go deleted file mode 100644 index f70c663416f5c..0000000000000 --- a/coderd/workspaceagent.go +++ /dev/null @@ -1,134 +0,0 @@ -package coderd - -import ( - "database/sql" - "fmt" - "io" - "net/http" - "time" - - "github.com/hashicorp/yamux" - "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" -) - -func (api *api) workspaceAgentConnectByResource(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.GetProvisionerJobAgentByResourceID(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) workspaceAgentServe(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.UpdateProvisionerJobAgentByID(r.Context(), database.UpdateProvisionerJobAgentByIDParams{ - 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.UpdateProvisionerJobAgentByID(r.Context(), database.UpdateProvisionerJobAgentByIDParams{ - ID: agent.ID, - UpdatedAt: sql.NullTime{ - Time: database.Now(), - Valid: true, - }, - }) - if err != nil { - _ = conn.Close(websocket.StatusAbnormalClosure, err.Error()) - return - } - } - } -} diff --git a/coderd/workspaceagent_test.go b/coderd/workspaceagent_test.go deleted file mode 100644 index 81c24fd52a373..0000000000000 --- a/coderd/workspaceagent_test.go +++ /dev/null @@ -1,86 +0,0 @@ -package coderd_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" - "github.com/coder/coder/provisioner/echo" - "github.com/coder/coder/provisionersdk/proto" -) - -func TestWorkspaceAgentServe(t *testing.T) { - t.Parallel() - t.Run("Success", func(t *testing.T) { - t.Parallel() - client := coderdtest.New(t, nil) - user := coderdtest.CreateFirstUser(t, client) - daemonCloser := coderdtest.NewProvisionerDaemon(t, client) - authToken := uuid.NewString() - job := 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, job.ID) - coderdtest.AwaitProjectImportJob(t, client, user.OrganizationID, job.ID) - workspace := coderdtest.CreateWorkspace(t, client, "me", project.ID) - history, err := client.CreateWorkspaceBuild(context.Background(), "", workspace.Name, coderd.CreateWorkspaceBuildRequest{ - ProjectVersionID: project.ActiveVersionID, - Transition: database.WorkspaceTransitionStart, - }) - require.NoError(t, err) - coderdtest.AwaitWorkspaceProvisionJob(t, client, user.OrganizationID, history.ProvisionJobID) - daemonCloser.Close() - - // agentClient := codersdk.New(client.URL) - // agentClient.SessionToken = authToken - // agentCloser := agent.New(agentClient.WorkspaceAgentServe, &peer.ConnOptions{ - // Logger: slogtest.Make(t, nil), - // }) - - // var resources []coderd.ProvisionerJobResource - // require.Eventually(t, func() bool { - // resources, err = client.WorkspaceProvisionJobResources(context.Background(), user.OrganizationID, history.ProvisionJobID) - // require.NoError(t, err) - // require.Len(t, resources, 1) - // return !resources[0].Agent.UpdatedAt.IsZero() - // }, 5*time.Second, 25*time.Millisecond) - - // workspaceClient, err := client.WorkspaceAgentConnect(context.Background(), user.OrganizationID, history.ProvisionJobID, resources[0].ID) - // require.NoError(t, err) - // 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) - // _, err = conn.Ping() - // require.NoError(t, err) - - // workspaceClient.DRPCConn().Close() - // conn.Close() - // stream.Close() - // agentCloser.Close() - }) -} diff --git a/coderd/workspaceagentauth_test.go b/coderd/workspaceagentauth_test.go deleted file mode 100644 index 1e12744da1aab..0000000000000 --- a/coderd/workspaceagentauth_test.go +++ /dev/null @@ -1,176 +0,0 @@ -package coderd_test - -import ( - "bytes" - "context" - "crypto/rand" - "crypto/rsa" - "encoding/base64" - "encoding/json" - "io/ioutil" - "math/big" - "net/http" - "testing" - "time" - - "cloud.google.com/go/compute/metadata" - "github.com/golang-jwt/jwt" - "github.com/stretchr/testify/require" - "google.golang.org/api/idtoken" - "google.golang.org/api/option" - - "github.com/coder/coder/cryptorand" -) - -func TestPostWorkspaceAgentAuthenticateGoogleInstanceIdentity(t *testing.T) { - t.Parallel() - // t.Run("Expired", func(t *testing.T) { - // t.Parallel() - // instanceID := "instanceidentifier" - // signedKey, keyID, privateKey := createSignedToken(t, instanceID, &jwt.MapClaims{}) - // validator := createValidator(t, keyID, privateKey) - // client := coderdtest.New(t, &coderdtest.Options{ - // GoogleTokenValidator: validator, - // }) - // _, err := client.AuthenticateWorkspaceAgentUsingGoogleCloudIdentity(context.Background(), "", createMetadataClient(signedKey)) - // var apiErr *codersdk.Error - // require.ErrorAs(t, err, &apiErr) - // require.Equal(t, http.StatusUnauthorized, apiErr.StatusCode()) - // }) - - // t.Run("InstanceNotFound", func(t *testing.T) { - // t.Parallel() - // instanceID := "instanceidentifier" - // signedKey, keyID, privateKey := createSignedToken(t, instanceID, nil) - // validator := createValidator(t, keyID, privateKey) - // client := coderdtest.New(t, &coderdtest.Options{ - // GoogleTokenValidator: validator, - // }) - // _, err := client.AuthenticateWorkspaceAgentUsingGoogleCloudIdentity(context.Background(), "", createMetadataClient(signedKey)) - // var apiErr *codersdk.Error - // require.ErrorAs(t, err, &apiErr) - // require.Equal(t, http.StatusNotFound, apiErr.StatusCode()) - // }) - - // t.Run("Success", func(t *testing.T) { - // t.Parallel() - // instanceID := "instanceidentifier" - // signedKey, keyID, privateKey := createSignedToken(t, instanceID, nil) - // validator := createValidator(t, keyID, privateKey) - // client := coderdtest.New(t, &coderdtest.Options{ - // GoogleTokenValidator: validator, - // }) - // user := coderdtest.CreateInitialUser(t, client) - // coderdtest.NewProvisionerDaemon(t, client) - // job := coderdtest.CreateProjectImportJob(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: "somename", - // Type: "someinstance", - // Agent: &proto.Agent{ - // Auth: &proto.Agent_GoogleInstanceIdentity{ - // GoogleInstanceIdentity: &proto.GoogleInstanceIdentityAuth{ - // InstanceId: instanceID, - // }, - // }, - // }, - // }}, - // }, - // }, - // }}, - // }) - // project := coderdtest.CreateProject(t, client, user.OrganizationID, job.ID) - // coderdtest.AwaitProjectImportJob(t, client, user.OrganizationID, job.ID) - // workspace := coderdtest.CreateWorkspace(t, client, "me", project.ID) - // firstHistory, err := client.CreateWorkspaceBuild(context.Background(), "", workspace.Name, coderd.CreateWorkspaceBuildRequest{ - // ProjectVersionID: project.ActiveVersionID, - // Transition: database.WorkspaceTransitionStart, - // }) - // require.NoError(t, err) - // coderdtest.AwaitWorkspaceProvisionJob(t, client, user.OrganizationID, firstHistory.ProvisionJobID) - - // _, err = client.AuthenticateWorkspaceAgentUsingGoogleCloudIdentity(context.Background(), "", createMetadataClient(signedKey)) - // require.NoError(t, err) - // }) -} - -// Used to easily create an HTTP transport! -type roundTripper func(req *http.Request) (*http.Response, error) - -func (r roundTripper) RoundTrip(req *http.Request) (*http.Response, error) { - return r(req) -} - -// Create's a new Google metadata client to authenticate. -func createMetadataClient(signedKey string) *metadata.Client { - return metadata.NewClient(&http.Client{ - Transport: roundTripper(func(r *http.Request) (*http.Response, error) { - return &http.Response{ - StatusCode: http.StatusOK, - Body: ioutil.NopCloser(bytes.NewReader([]byte(signedKey))), - Header: make(http.Header), - }, nil - }), - }) -} - -// Create's a signed JWT with a randomly generated private key. -func createSignedToken(t *testing.T, instanceID string, claims *jwt.MapClaims) (signedKey string, keyID string, privateKey *rsa.PrivateKey) { - keyID, err := cryptorand.String(12) - require.NoError(t, err) - if claims == nil { - claims = &jwt.MapClaims{ - "exp": time.Now().AddDate(1, 0, 0).Unix(), - "google": map[string]interface{}{ - "compute_engine": map[string]string{ - "instance_id": instanceID, - }, - }, - } - } - token := jwt.NewWithClaims(jwt.SigningMethodRS256, claims) - token.Header["kid"] = keyID - privateKey, err = rsa.GenerateKey(rand.Reader, 2048) - require.NoError(t, err) - signedKey, err = token.SignedString(privateKey) - require.NoError(t, err) - return signedKey, keyID, privateKey -} - -// Create's a validator that verifies against the provided private key. -// In a production scenario, the validator calls against the Google OAuth API -// to obtain certificates. -func createValidator(t *testing.T, keyID string, privateKey *rsa.PrivateKey) *idtoken.Validator { - // Taken from: https://github.com/googleapis/google-api-go-client/blob/4bb729045d611fa77bdbeb971f6a1204ba23161d/idtoken/validate.go#L57-L75 - type jwk struct { - Kid string `json:"kid"` - N string `json:"n"` - E string `json:"e"` - } - type certResponse struct { - Keys []jwk `json:"keys"` - } - - validator, err := idtoken.NewValidator(context.Background(), option.WithHTTPClient(&http.Client{ - Transport: roundTripper(func(r *http.Request) (*http.Response, error) { - data, err := json.Marshal(certResponse{ - Keys: []jwk{{ - Kid: keyID, - N: base64.RawURLEncoding.EncodeToString(privateKey.N.Bytes()), - E: base64.RawURLEncoding.EncodeToString(new(big.Int).SetInt64(int64(privateKey.E)).Bytes()), - }}, - }) - require.NoError(t, err) - return &http.Response{ - StatusCode: http.StatusOK, - Body: ioutil.NopCloser(bytes.NewReader(data)), - Header: make(http.Header), - }, nil - }), - })) - require.NoError(t, err) - return validator -} diff --git a/coderd/workspacebuilds.go b/coderd/workspacebuilds.go new file mode 100644 index 0000000000000..4f8dfbeac3c1e --- /dev/null +++ b/coderd/workspacebuilds.go @@ -0,0 +1,95 @@ +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 { + 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..da1e8655c04d4 --- /dev/null +++ b/coderd/workspacebuilds_test.go @@ -0,0 +1,147 @@ +package coderd_test + +import ( + "context" + "net/http" + "testing" + "time" + + "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" + "github.com/stretchr/testify/require" +) + +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) + logs, err := client.WorkspaceBuildLogsAfter(context.Background(), 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 adefd65e33b86..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" -) - -// 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"` - ProvisionJobID uuid.UUID `json:"provision_job_id"` -} - -// 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"` -} - -func (api *api) postWorkspaceBuildByUser(rw http.ResponseWriter, r *http.Request) { - var createBuild CreateWorkspaceBuildRequest - 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.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 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.UUID) - 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.GetWorkspaceBuildByWorkspaceIDWithoutAfter(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 build: %s", err), - }) - return - } - - var workspaceBuild database.WorkspaceBuild - // 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() - workspaceBuild, err = db.InsertWorkspaceBuild(r.Context(), database.InsertWorkspaceBuildParams{ - 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 build: %w", err) - } - - input, err := json.Marshal(workspaceProvisionJob{ - WorkspaceBuildID: workspaceBuild.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.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: err.Error(), - }) - return - } - - render.Status(r, http.StatusCreated) - render.JSON(rw, r, convertWorkspaceBuild(workspaceBuild)) -} - -// Returns all workspace build. This is not sorted. Use before/after to chronologically sort. -func (api *api) workspaceBuildByUser(rw http.ResponseWriter, r *http.Request) { - workspace := httpmw.WorkspaceParam(r) - - history, err := api.Database.GetWorkspaceBuildByWorkspaceID(r.Context(), workspace.ID) - if errors.Is(err, sql.ErrNoRows) { - err = nil - history = []database.WorkspaceBuild{} - } - if err != nil { - httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ - Message: fmt.Sprintf("get workspace build: %s", err), - }) - return - } - - apiHistory := make([]WorkspaceBuild, 0, len(history)) - for _, history := range history { - apiHistory = append(apiHistory, convertWorkspaceBuild(history)) - } - - render.Status(r, http.StatusOK) - render.JSON(rw, r, apiHistory) -} - -func (*api) workspaceBuildByName(rw http.ResponseWriter, r *http.Request) { - workspaceBuild := httpmw.WorkspaceBuildParam(r) - render.Status(r, http.StatusOK) - render.JSON(rw, r, convertWorkspaceBuild(workspaceBuild)) -} - -// Converts the internal history representation to a public external-facing model. -func convertWorkspaceBuild(workspaceBuild database.WorkspaceBuild) 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, - ProvisionJobID: workspaceBuild.ProvisionJobID, - }) -} diff --git a/coderd/workspacehistory_test.go b/coderd/workspacehistory_test.go deleted file mode 100644 index d1cb20fcfd786..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 TestPostWorkspaceBuildByUser(t *testing.T) { - t.Parallel() - t.Run("NoProjectVersion", 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, "me", project.ID) - _, err := client.CreateWorkspaceBuild(context.Background(), "", workspace.Name, coderd.CreateWorkspaceBuildRequest{ - 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.CreateFirstUser(t, client) - coderdtest.NewProvisionerDaemon(t, client) - job := coderdtest.CreateProjectVersion(t, client, user.OrganizationID, &echo.Responses{ - Provision: []*proto.Provision_Response{{}}, - }) - project := coderdtest.CreateProject(t, client, user.OrganizationID, job.ID) - coderdtest.AwaitProjectImportJob(t, client, user.OrganizationID, job.ID) - workspace := coderdtest.CreateWorkspace(t, client, "me", project.ID) - _, err := client.CreateWorkspaceBuild(context.Background(), "", workspace.Name, 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.StatusPreconditionFailed, apiErr.StatusCode()) - }) - - t.Run("AlreadyActive", func(t *testing.T) { - t.Parallel() - client := coderdtest.New(t, nil) - user := coderdtest.CreateFirstUser(t, client) - closeDaemon := coderdtest.NewProvisionerDaemon(t, client) - job := coderdtest.CreateProjectVersion(t, client, user.OrganizationID, nil) - project := coderdtest.CreateProject(t, client, user.OrganizationID, job.ID) - coderdtest.AwaitProjectImportJob(t, client, user.OrganizationID, job.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.Name, coderd.CreateWorkspaceBuildRequest{ - ProjectVersionID: project.ActiveVersionID, - Transition: database.WorkspaceTransitionStart, - }) - require.NoError(t, err) - _, err = client.CreateWorkspaceBuild(context.Background(), "", workspace.Name, 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.StatusConflict, apiErr.StatusCode()) - }) - - t.Run("UpdatePriorAfterField", func(t *testing.T) { - t.Parallel() - client := coderdtest.New(t, nil) - user := coderdtest.CreateFirstUser(t, client) - coderdtest.NewProvisionerDaemon(t, client) - job := coderdtest.CreateProjectVersion(t, client, user.OrganizationID, nil) - project := coderdtest.CreateProject(t, client, user.OrganizationID, job.ID) - coderdtest.AwaitProjectImportJob(t, client, user.OrganizationID, job.ID) - workspace := coderdtest.CreateWorkspace(t, client, "me", project.ID) - firstHistory, err := client.CreateWorkspaceBuild(context.Background(), "", workspace.Name, coderd.CreateWorkspaceBuildRequest{ - ProjectVersionID: project.ActiveVersionID, - Transition: database.WorkspaceTransitionStart, - }) - require.NoError(t, err) - coderdtest.AwaitWorkspaceProvisionJob(t, client, user.OrganizationID, firstHistory.ProvisionJobID) - secondHistory, err := client.CreateWorkspaceBuild(context.Background(), "", workspace.Name, coderd.CreateWorkspaceBuildRequest{ - ProjectVersionID: project.ActiveVersionID, - Transition: database.WorkspaceTransitionStart, - }) - require.NoError(t, err) - require.Equal(t, firstHistory.ID.String(), secondHistory.BeforeID.String()) - - firstHistory, err = client.WorkspaceBuild(context.Background(), "", workspace.Name, firstHistory.Name) - require.NoError(t, err) - require.Equal(t, secondHistory.ID.String(), firstHistory.AfterID.String()) - }) -} - -func TestWorkspaceBuildByUser(t *testing.T) { - t.Parallel() - t.Run("ListEmpty", func(t *testing.T) { - t.Parallel() - client := coderdtest.New(t, nil) - user := coderdtest.CreateFirstUser(t, client) - coderdtest.NewProvisionerDaemon(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) - history, err := client.ListWorkspaceBuild(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.CreateFirstUser(t, client) - coderdtest.NewProvisionerDaemon(t, client) - job := coderdtest.CreateProjectVersion(t, client, user.OrganizationID, nil) - project := coderdtest.CreateProject(t, client, user.OrganizationID, job.ID) - coderdtest.AwaitProjectImportJob(t, client, user.OrganizationID, job.ID) - workspace := coderdtest.CreateWorkspace(t, client, "me", project.ID) - _, err := client.CreateWorkspaceBuild(context.Background(), "", workspace.Name, coderd.CreateWorkspaceBuildRequest{ - ProjectVersionID: project.ActiveVersionID, - Transition: database.WorkspaceTransitionStart, - }) - require.NoError(t, err) - history, err := client.ListWorkspaceBuild(context.Background(), "me", workspace.Name) - require.NoError(t, err) - require.NotNil(t, history) - require.Len(t, history, 1) - }) -} - -func TestWorkspaceBuildByName(t *testing.T) { - t.Parallel() - client := coderdtest.New(t, nil) - user := coderdtest.CreateFirstUser(t, client) - coderdtest.NewProvisionerDaemon(t, client) - job := coderdtest.CreateProjectVersion(t, client, user.OrganizationID, nil) - coderdtest.AwaitProjectImportJob(t, client, user.OrganizationID, job.ID) - project := coderdtest.CreateProject(t, client, user.OrganizationID, job.ID) - workspace := coderdtest.CreateWorkspace(t, client, "me", project.ID) - history, err := client.CreateWorkspaceBuild(context.Background(), "", workspace.Name, coderd.CreateWorkspaceBuildRequest{ - ProjectVersionID: project.ActiveVersionID, - Transition: database.WorkspaceTransitionStart, - }) - require.NoError(t, err) - _, err = client.WorkspaceBuild(context.Background(), "me", workspace.Name, history.Name) - require.NoError(t, err) -} diff --git a/coderd/workspaceagentauth.go b/coderd/workspaceresourceauth.go similarity index 77% rename from coderd/workspaceagentauth.go rename to coderd/workspaceresourceauth.go index ce3eb6e2eaf20..1f906cbbb14a4 100644 --- a/coderd/workspaceagentauth.go +++ b/coderd/workspaceresourceauth.go @@ -25,30 +25,10 @@ type WorkspaceAgentAuthenticateResponse struct { SessionToken string `json:"session_token"` } -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"` -} - // 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 @@ -76,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), @@ -89,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), @@ -103,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), }) diff --git a/coderd/workspaceresourceauth_test.go b/coderd/workspaceresourceauth_test.go new file mode 100644 index 0000000000000..5c1263f77696f --- /dev/null +++ b/coderd/workspaceresourceauth_test.go @@ -0,0 +1,182 @@ +package coderd_test + +import ( + "bytes" + "context" + "crypto/rand" + "crypto/rsa" + "encoding/base64" + "encoding/json" + "io/ioutil" + "math/big" + "net/http" + "testing" + "time" + + "cloud.google.com/go/compute/metadata" + "github.com/golang-jwt/jwt" + "github.com/stretchr/testify/require" + "google.golang.org/api/idtoken" + "google.golang.org/api/option" + + "github.com/coder/coder/coderd" + "github.com/coder/coder/coderd/coderdtest" + "github.com/coder/coder/codersdk" + "github.com/coder/coder/cryptorand" + "github.com/coder/coder/database" + "github.com/coder/coder/provisioner/echo" + "github.com/coder/coder/provisionersdk/proto" +) + +func TestPostWorkspaceAuthGoogleInstanceIdentity(t *testing.T) { + t.Parallel() + t.Run("Expired", func(t *testing.T) { + t.Parallel() + instanceID := "instanceidentifier" + signedKey, keyID, privateKey := createSignedToken(t, instanceID, &jwt.MapClaims{}) + validator := createValidator(t, keyID, privateKey) + client := coderdtest.New(t, &coderdtest.Options{ + GoogleTokenValidator: validator, + }) + _, err := client.AuthWorkspaceGoogleInstanceIdentity(context.Background(), "", createMetadataClient(signedKey)) + var apiErr *codersdk.Error + require.ErrorAs(t, err, &apiErr) + require.Equal(t, http.StatusUnauthorized, apiErr.StatusCode()) + }) + + t.Run("InstanceNotFound", func(t *testing.T) { + t.Parallel() + instanceID := "instanceidentifier" + signedKey, keyID, privateKey := createSignedToken(t, instanceID, nil) + validator := createValidator(t, keyID, privateKey) + client := coderdtest.New(t, &coderdtest.Options{ + GoogleTokenValidator: validator, + }) + _, err := client.AuthWorkspaceGoogleInstanceIdentity(context.Background(), "", createMetadataClient(signedKey)) + var apiErr *codersdk.Error + require.ErrorAs(t, err, &apiErr) + require.Equal(t, http.StatusNotFound, apiErr.StatusCode()) + }) + + t.Run("Success", func(t *testing.T) { + t.Parallel() + instanceID := "instanceidentifier" + signedKey, keyID, privateKey := createSignedToken(t, instanceID, nil) + validator := createValidator(t, keyID, privateKey) + client := coderdtest.New(t, &coderdtest.Options{ + GoogleTokenValidator: validator, + }) + 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: "somename", + Type: "someinstance", + Agent: &proto.Agent{ + Auth: &proto.Agent_GoogleInstanceIdentity{ + GoogleInstanceIdentity: &proto.GoogleInstanceIdentityAuth{ + InstanceId: instanceID, + }, + }, + }, + }}, + }, + }, + }}, + }) + 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) + + _, err = client.AuthWorkspaceGoogleInstanceIdentity(context.Background(), "", createMetadataClient(signedKey)) + require.NoError(t, err) + }) +} + +// Used to easily create an HTTP transport! +type roundTripper func(req *http.Request) (*http.Response, error) + +func (r roundTripper) RoundTrip(req *http.Request) (*http.Response, error) { + return r(req) +} + +// Create's a new Google metadata client to authenticate. +func createMetadataClient(signedKey string) *metadata.Client { + return metadata.NewClient(&http.Client{ + Transport: roundTripper(func(r *http.Request) (*http.Response, error) { + return &http.Response{ + StatusCode: http.StatusOK, + Body: ioutil.NopCloser(bytes.NewReader([]byte(signedKey))), + Header: make(http.Header), + }, nil + }), + }) +} + +// Create's a signed JWT with a randomly generated private key. +func createSignedToken(t *testing.T, instanceID string, claims *jwt.MapClaims) (signedKey string, keyID string, privateKey *rsa.PrivateKey) { + keyID, err := cryptorand.String(12) + require.NoError(t, err) + if claims == nil { + claims = &jwt.MapClaims{ + "exp": time.Now().AddDate(1, 0, 0).Unix(), + "google": map[string]interface{}{ + "compute_engine": map[string]string{ + "instance_id": instanceID, + }, + }, + } + } + token := jwt.NewWithClaims(jwt.SigningMethodRS256, claims) + token.Header["kid"] = keyID + privateKey, err = rsa.GenerateKey(rand.Reader, 2048) + require.NoError(t, err) + signedKey, err = token.SignedString(privateKey) + require.NoError(t, err) + return signedKey, keyID, privateKey +} + +// Create's a validator that verifies against the provided private key. +// In a production scenario, the validator calls against the Google OAuth API +// to obtain certificates. +func createValidator(t *testing.T, keyID string, privateKey *rsa.PrivateKey) *idtoken.Validator { + // Taken from: https://github.com/googleapis/google-api-go-client/blob/4bb729045d611fa77bdbeb971f6a1204ba23161d/idtoken/validate.go#L57-L75 + type jwk struct { + Kid string `json:"kid"` + N string `json:"n"` + E string `json:"e"` + } + type certResponse struct { + Keys []jwk `json:"keys"` + } + + validator, err := idtoken.NewValidator(context.Background(), option.WithHTTPClient(&http.Client{ + Transport: roundTripper(func(r *http.Request) (*http.Response, error) { + data, err := json.Marshal(certResponse{ + Keys: []jwk{{ + Kid: keyID, + N: base64.RawURLEncoding.EncodeToString(privateKey.N.Bytes()), + E: base64.RawURLEncoding.EncodeToString(new(big.Int).SetInt64(int64(privateKey.E)).Bytes()), + }}, + }) + require.NoError(t, err) + return &http.Response{ + StatusCode: http.StatusOK, + Body: ioutil.NopCloser(bytes.NewReader(data)), + Header: make(http.Header), + }, nil + }), + })) + require.NoError(t, err) + return validator +} 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..a346ebeb9c2da --- /dev/null +++ b/coderd/workspaceresources_test.go @@ -0,0 +1,125 @@ +package coderd_test + +import ( + "context" + "testing" + + "cdr.dev/slog" + "cdr.dev/slog/sloggers/slogtest" + "github.com/google/uuid" + "github.com/stretchr/testify/require" + + "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 31539894716f9..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,134 +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", - }}, + + 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 build: %s", err), }) return } - if !errors.Is(err, sql.ErrNoRows) { + + 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("get workspace by name: %s", err.Error()), + Message: 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, - }) + render.Status(r, http.StatusCreated) + render.JSON(rw, r, convertWorkspaceBuild(workspaceBuild, convertProvisionerJob(provisionerJob))) +} + +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("insert workspace: %s", err), + 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.StatusCreated) - render.JSON(rw, r, convertWorkspace(workspace)) + render.Status(r, http.StatusOK) + render.JSON(rw, r, convertWorkspaceBuild(workspaceBuild, convertProvisionerJob(job))) } -// Returns a single workspace. -func (*api) workspaceByUser(rw http.ResponseWriter, r *http.Request) { +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) { + 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 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))) } -// 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 f2edb491f2456..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.OrganizationID, nil) - // project := coderdtest.CreateProject(t, client, user.OrganizationID, 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.CreateFirstUser(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.CreateFirstUser(t, client) - job := coderdtest.CreateProjectVersion(t, client, user.OrganizationID, nil) - project := coderdtest.CreateProject(t, client, user.OrganizationID, 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, + 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.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, + 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.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) + 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.OrganizationID, nil) - // project := coderdtest.CreateProject(t, client, user.OrganizationID, 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.Parallel() - // client := coderdtest.New(t, nil) - // user := coderdtest.CreateInitialUser(t, client) - // job := coderdtest.CreateProjectImportJob(t, client, user.OrganizationID, nil) - // project := coderdtest.CreateProject(t, client, user.OrganizationID, job.ID) - // workspaces, err := client.WorkspacesByProject(context.Background(), user.OrganizationID, project.Name) - // require.NoError(t, err) - // require.NotNil(t, workspaces) - // }) + t.Run("NotFound", 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.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.Parallel() - // client := coderdtest.New(t, nil) - // user := coderdtest.CreateInitialUser(t, client) - // job := coderdtest.CreateProjectImportJob(t, client, user.OrganizationID, nil) - // project := coderdtest.CreateProject(t, client, user.OrganizationID, job.ID) - // _ = coderdtest.CreateWorkspace(t, client, "", project.ID) - // workspaces, err := client.WorkspacesByProject(context.Background(), user.OrganizationID, project.Name) - // require.NoError(t, err) - // require.NotNil(t, workspaces) - // require.Len(t, workspaces, 1) - // }) + 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) + 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) + }) } diff --git a/codersdk/organizations.go b/codersdk/organizations.go index c5ab486e733de..3cea65b084bb5 100644 --- a/codersdk/organizations.go +++ b/codersdk/organizations.go @@ -9,6 +9,19 @@ import ( "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) diff --git a/codersdk/projects.go b/codersdk/projects.go index bc19c18fa330f..f3f4ce9072269 100644 --- a/codersdk/projects.go +++ b/codersdk/projects.go @@ -13,7 +13,7 @@ import ( // Project returns a single project. func (c *Client) Project(ctx context.Context, project uuid.UUID) (coderd.Project, error) { - res, err := c.request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/project/%s", project), nil) + res, err := c.request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/projects/%s", project), nil) if err != nil { return coderd.Project{}, nil } @@ -41,7 +41,7 @@ func (c *Client) WorkspacesByProject(ctx context.Context, project uuid.UUID) ([] // ProjectParameters returns parameters scoped to a project. func (c *Client) ProjectParameters(ctx context.Context, project uuid.UUID) ([]coderd.ParameterValue, error) { - res, err := c.request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/project/%s/parameters", project), nil) + res, err := c.request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/projects/%s/parameters", project), nil) if err != nil { return nil, err } @@ -54,8 +54,8 @@ func (c *Client) ProjectParameters(ctx context.Context, project uuid.UUID) ([]co } // 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) +func (c *Client) CreateProjectParameter(ctx context.Context, project uuid.UUID, req coderd.CreateParameterValueRequest) (coderd.ParameterValue, error) { + res, err := c.request(ctx, http.MethodPost, fmt.Sprintf("/api/v2/projects/%s/parameters", project), req) if err != nil { return coderd.ParameterValue{}, err } @@ -69,7 +69,7 @@ func (c *Client) CreateProjectParameter(ctx context.Context, organization, proje // 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/project/%s/versions", project), nil) + res, err := c.request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/projects/%s/versions", project), nil) if err != nil { return nil, err } @@ -81,10 +81,17 @@ func (c *Client) ProjectVersionsByProject(ctx context.Context, project uuid.UUID return projectVersion, json.NewDecoder(res.Body).Decode(&projectVersion) } -func (c *Client) DeleteProject(ctx context.Context, project uuid.UUID) error { - return nil -} - -func (c *Client) UpdateProjectVersion() { - +// 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 + } + defer res.Body.Close() + if res.StatusCode != http.StatusOK { + return coderd.ProjectVersion{}, readBodyAsError(res) + } + var projectVersion coderd.ProjectVersion + return projectVersion, json.NewDecoder(res.Body).Decode(&projectVersion) } diff --git a/codersdk/projectversions.go b/codersdk/projectversions.go index 5fb9f5758482b..c244a016a48d0 100644 --- a/codersdk/projectversions.go +++ b/codersdk/projectversions.go @@ -13,19 +13,22 @@ import ( ) // ProjectVersion returns a project version by ID. -func (c *Client) ProjectVersion(ctx context.Context, version uuid.UUID) (coderd.ProjectVersion, error) { - return coderd.ProjectVersion{}, 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) { - return coderd.ProjectVersion{}, nil +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.ParameterSchema, error) { - res, err := c.request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/projectversion/%s/schema", version), nil) + res, err := c.request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/projectversions/%s/schema", version), nil) if err != nil { return nil, err } @@ -38,8 +41,8 @@ func (c *Client) ProjectVersionSchema(ctx context.Context, version uuid.UUID) ([ } // ProjectVersionParameters returns computed parameters for a project version. -func (c *Client) ProjectVersionComputedParameters(ctx context.Context, version uuid.UUID) ([]coderd.ComputedParameterValue, error) { - res, err := c.request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/projectimport/%s/parameters", version), nil) +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 } @@ -47,21 +50,30 @@ func (c *Client) ProjectVersionComputedParameters(ctx context.Context, version u if res.StatusCode != http.StatusOK { return nil, readBodyAsError(res) } - var params []coderd.ComputedParameterValue + 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.ProvisionerJobResource, error) { - return nil, nil +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, "projectimport", "", version, before) + 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, organization string, job uuid.UUID, after time.Time) (<-chan coderd.ProvisionerJobLog, error) { - return c.provisionerJobLogsAfter(ctx, "projectimport", organization, job, after) +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/provisionerdaemons.go b/codersdk/provisionerdaemons.go index a6eb0f2c63a57..446399d3eece3 100644 --- a/codersdk/provisionerdaemons.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" @@ -22,7 +21,7 @@ import ( // 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/serve") + serverURL, err := c.URL.Parse("/api/v2/provisionerdaemons/me/listen") if err != nil { return nil, xerrors.Errorf("parse url: %w", err) } @@ -49,12 +48,12 @@ func (c *Client) ListenProvisionerDaemon(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 } @@ -68,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/users.go b/codersdk/users.go index af5dc6d2c3931..1973471dfbcdc 100644 --- a/codersdk/users.go +++ b/codersdk/users.go @@ -7,12 +7,6 @@ import ( "net/http" "github.com/coder/coder/coderd" - "github.com/google/uuid" -) - -var ( - // Me represents an empty UUID, which is used to represent the current entity. - Me = uuid.UUID{} ) // HasFirstUser returns whether the first user has been created. @@ -33,16 +27,16 @@ func (c *Client) HasFirstUser(ctx context.Context) (bool, error) { // 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.CreateUserRequest) (coderd.CreateUserResponse, error) { +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.CreateUserResponse{}, err + return coderd.CreateFirstUserResponse{}, err } defer res.Body.Close() if res.StatusCode != http.StatusCreated { - return coderd.CreateUserResponse{}, readBodyAsError(res) + return coderd.CreateFirstUserResponse{}, readBodyAsError(res) } - var resp coderd.CreateUserResponse + var resp coderd.CreateFirstUserResponse return resp, json.NewDecoder(res.Body).Decode(&resp) } @@ -60,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 } @@ -129,7 +126,7 @@ func (c *Client) OrganizationsByUser(ctx context.Context, id string) ([]coderd.O if id == "" { id = "me" } - res, err := c.request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/users/%s", id), nil) + res, err := c.request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/users/%s/organizations", id), nil) if err != nil { return nil, err } @@ -140,3 +137,86 @@ func (c *Client) OrganizationsByUser(ctx context.Context, id string) ([]coderd.O 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/workspacebuilds.go b/codersdk/workspacebuilds.go new file mode 100644 index 0000000000000..5e03b82e5611f --- /dev/null +++ b/codersdk/workspacebuilds.go @@ -0,0 +1,51 @@ +package codersdk + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + "time" + + "github.com/coder/coder/coderd" + "github.com/google/uuid" +) + +// 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/workspaceresourceauth.go b/codersdk/workspaceresourceauth.go new file mode 100644 index 0000000000000..c2522444eeced --- /dev/null +++ b/codersdk/workspaceresourceauth.go @@ -0,0 +1,44 @@ +package codersdk + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + + "cloud.google.com/go/compute/metadata" + "golang.org/x/xerrors" + + "github.com/coder/coder/coderd" +) + +// 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) AuthWorkspaceGoogleInstanceIdentity(ctx context.Context, serviceAccount string, gcpClient *metadata.Client) (coderd.WorkspaceAgentAuthenticateResponse, error) { + if serviceAccount == "" { + // This is the default name specified by Google. + serviceAccount = "default" + } + if gcpClient == nil { + gcpClient = metadata.NewClient(c.httpClient) + } + // "format=full" is required, otherwise the responding payload will be missing "instance_id". + jwt, err := gcpClient.Get(fmt.Sprintf("instance/service-accounts/%s/identity?audience=coder&format=full", serviceAccount)) + if err != nil { + return coderd.WorkspaceAgentAuthenticateResponse{}, xerrors.Errorf("get metadata identity: %w", err) + } + res, err := c.request(ctx, http.MethodPost, "/api/v2/workspaceresources/auth/google-instance-identity", coderd.GoogleInstanceIdentityToken{ + JSONWebToken: jwt, + }) + if err != nil { + return coderd.WorkspaceAgentAuthenticateResponse{}, err + } + defer res.Body.Close() + if res.StatusCode != http.StatusOK { + return coderd.WorkspaceAgentAuthenticateResponse{}, readBodyAsError(res) + } + var resp coderd.WorkspaceAgentAuthenticateResponse + return resp, json.NewDecoder(res.Body).Decode(&resp) +} diff --git a/codersdk/workspaceagent.go b/codersdk/workspaceresources.go similarity index 64% rename from codersdk/workspaceagent.go rename to codersdk/workspaceresources.go index 0a8650054469e..652ab8fcb68c2 100644 --- a/codersdk/workspaceagent.go +++ b/codersdk/workspaceresources.go @@ -8,55 +8,34 @@ import ( "net/http" "net/http/cookiejar" - "cloud.google.com/go/compute/metadata" - "golang.org/x/xerrors" - "nhooyr.io/websocket" - - "github.com/google/uuid" - "github.com/hashicorp/yamux" - "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" + "github.com/google/uuid" + "github.com/hashicorp/yamux" + "golang.org/x/xerrors" + "nhooyr.io/websocket" ) -// AuthWorkspaceAgentWithGoogleInstanceIdentity 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) AuthWorkspaceAgentWithGoogleInstanceIdentity(ctx context.Context, serviceAccount string, gcpClient *metadata.Client) (coderd.WorkspaceAgentAuthenticateResponse, error) { - if serviceAccount == "" { - // This is the default name specified by Google. - serviceAccount = "default" - } - if gcpClient == nil { - gcpClient = metadata.NewClient(c.httpClient) - } - // "format=full" is required, otherwise the responding payload will be missing "instance_id". - jwt, err := gcpClient.Get(fmt.Sprintf("instance/service-accounts/%s/identity?audience=coder&format=full", serviceAccount)) - 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{ - JSONWebToken: jwt, - }) +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.WorkspaceAgentAuthenticateResponse{}, err + return coderd.WorkspaceResource{}, err } defer res.Body.Close() if res.StatusCode != http.StatusOK { - return coderd.WorkspaceAgentAuthenticateResponse{}, readBodyAsError(res) + return coderd.WorkspaceResource{}, readBodyAsError(res) } - var resp coderd.WorkspaceAgentAuthenticateResponse - return resp, json.NewDecoder(res.Body).Decode(&resp) + 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/workspaceresource/%s/dial", resource.String())) + 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) } @@ -94,7 +73,7 @@ func (c *Client) DialWorkspaceAgent(ctx context.Context, resource uuid.UUID) (pr // 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/workspaceagent/listen") + serverURL, err := c.URL.Parse("/api/v2/workspaceresources/agent") if err != nil { return nil, xerrors.Errorf("parse url: %w", err) } diff --git a/codersdk/workspaces.go b/codersdk/workspaces.go index 8592988da4b53..9aacf60d39db2 100644 --- a/codersdk/workspaces.go +++ b/codersdk/workspaces.go @@ -5,39 +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. -func (c *Client) WorkspacesByUser(ctx context.Context, user uuid.UUID) ([]coderd.Workspace, error) { - route := fmt.Sprintf("/api/v2/user/%s/workspaces", user) - if user == Me { - route = fmt.Sprintf("/api/v2/user/me/workspaces", 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) -} - -// WorkspaceByName returns a workspace for a user that matches the case-insensitive name. -func (c *Client) WorkspaceByName(ctx context.Context, user uuid.UUID, name string) (coderd.Workspace, error) { - return coderd.Workspace{}, nil -} - -// Workspace returns a single workspace by owner and name. +// 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/workspace/%s", id), nil) + res, err := c.request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/workspaces/%s", id), nil) if err != nil { return coderd.Workspace{}, err } @@ -49,22 +25,8 @@ func (c *Client) Workspace(ctx context.Context, id uuid.UUID) (coderd.Workspace, return workspace, json.NewDecoder(res.Body).Decode(&workspace) } -// WorkspaceProvisions returns a historical list of provision operations for a workspace. -func (c *Client) WorkspaceProvisions(ctx context.Context, workspace uuid.UUID) ([]coderd.WorkspaceBuild, error) { - return nil, nil -} - -// WorkspaceProvision returns -func (c *Client) WorkspaceVersion(ctx context.Context, provision uuid.UUID) (coderd.WorkspaceBuild, error) { - return coderd.WorkspaceBuild{}, nil -} - -// ListWorkspaceBuild returns historical data for workspace builds. -func (c *Client) ListWorkspaceBuild(ctx context.Context, owner, workspace string) ([]coderd.WorkspaceBuild, 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,93 +38,42 @@ func (c *Client) ListWorkspaceBuild(ctx context.Context, owner, workspace string return workspaceBuild, json.NewDecoder(res.Body).Decode(&workspaceBuild) } -// WorkspaceBuild returns a single workspace build for a workspace. -// If history is "", the latest version is returned. -func (c *Client) WorkspaceBuild(ctx context.Context, owner, workspace, history string) (coderd.WorkspaceBuild, 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.WorkspaceBuild{}, err } defer res.Body.Close() - if res.StatusCode != http.StatusOK { + if res.StatusCode != http.StatusCreated { return coderd.WorkspaceBuild{}, readBodyAsError(res) } var workspaceBuild coderd.WorkspaceBuild return workspaceBuild, json.NewDecoder(res.Body).Decode(&workspaceBuild) } -// 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 - } - 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) -} - -// CreateWorkspaceBuild queues a new build to occur for a workspace. -func (c *Client) CreateWorkspaceBuild(ctx context.Context, owner, workspace string, request coderd.CreateWorkspaceBuildRequest) (coderd.WorkspaceBuild, 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.WorkspaceBuild{}, err } defer res.Body.Close() - if res.StatusCode != http.StatusCreated { + if res.StatusCode != http.StatusOK { return coderd.WorkspaceBuild{}, readBodyAsError(res) } 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 - } - 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) -} - -// 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) -} - -func (c *Client) WorkspaceProvisionJobResources(ctx context.Context, organization string, job uuid.UUID) ([]coderd.ProvisionerJobResource, error) { - res, err := c.request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/workspaceprovision/%s/%s/resources", organization, job), nil) - if err != nil { - return nil, err + return coderd.WorkspaceBuild{}, err } defer res.Body.Close() if res.StatusCode != http.StatusOK { - return nil, readBodyAsError(res) + return coderd.WorkspaceBuild{}, readBodyAsError(res) } - var resources []coderd.ProvisionerJobResource - return resources, json.NewDecoder(res.Body).Decode(&resources) + var workspaceBuild coderd.WorkspaceBuild + return workspaceBuild, json.NewDecoder(res.Body).Decode(&workspaceBuild) } diff --git a/database/databasefake/databasefake.go b/database/databasefake/databasefake.go index 08850f183698e..738b62b4c5e82 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), + provisionerJobResource: make([]database.WorkspaceResource, 0), workspaceBuild: make([]database.WorkspaceBuild, 0), - provisionerJobAgent: make([]database.ProvisionerJobAgent, 0), + provisionerJobAgent: make([]database.WorkspaceAgent, 0), } } @@ -52,8 +52,8 @@ 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 workspaceBuild []database.WorkspaceBuild @@ -222,6 +222,18 @@ func (q *fakeQuerier) GetWorkspaceBuildByID(_ context.Context, id uuid.UUID) (da return database.WorkspaceBuild{}, sql.ErrNoRows } +func (q *fakeQuerier) GetWorkspaceBuildByJobID(ctx context.Context, jobID uuid.UUID) (database.WorkspaceBuild, error) { + q.mutex.Lock() + defer q.mutex.Unlock() + + for _, build := range q.workspaceBuild { + if build.JobID.String() == jobID.String() { + return build, nil + } + } + return database.WorkspaceBuild{}, sql.ErrNoRows +} + func (q *fakeQuerier) GetWorkspaceBuildByWorkspaceIDWithoutAfter(_ context.Context, workspaceID uuid.UUID) (database.WorkspaceBuild, error) { q.mutex.Lock() defer q.mutex.Unlock() @@ -508,7 +520,7 @@ func (q *fakeQuerier) GetProvisionerDaemons(_ context.Context) ([]database.Provi return q.provisionerDaemons, nil } -func (q *fakeQuerier) GetProvisionerJobAgentByAuthToken(_ context.Context, authToken uuid.UUID) (database.ProvisionerJobAgent, error) { +func (q *fakeQuerier) GetWorkspaceAgentByAuthToken(_ context.Context, authToken uuid.UUID) (database.WorkspaceAgent, error) { q.mutex.Lock() defer q.mutex.Unlock() @@ -517,10 +529,10 @@ func (q *fakeQuerier) GetProvisionerJobAgentByAuthToken(_ context.Context, authT return agent, nil } } - return database.ProvisionerJobAgent{}, sql.ErrNoRows + return database.WorkspaceAgent{}, sql.ErrNoRows } -func (q *fakeQuerier) GetProvisionerJobAgentByInstanceID(_ context.Context, instanceID string) (database.ProvisionerJobAgent, error) { +func (q *fakeQuerier) GetWorkspaceAgentByInstanceID(_ context.Context, instanceID string) (database.WorkspaceAgent, error) { q.mutex.Lock() defer q.mutex.Unlock() @@ -531,10 +543,10 @@ func (q *fakeQuerier) GetProvisionerJobAgentByInstanceID(_ context.Context, inst return agent, nil } } - return database.ProvisionerJobAgent{}, sql.ErrNoRows + return database.WorkspaceAgent{}, sql.ErrNoRows } -func (q *fakeQuerier) GetProvisionerJobAgentByResourceID(_ context.Context, resourceID uuid.UUID) (database.ProvisionerJobAgent, error) { +func (q *fakeQuerier) GetWorkspaceAgentByResourceID(_ context.Context, resourceID uuid.UUID) (database.WorkspaceAgent, error) { q.mutex.Lock() defer q.mutex.Unlock() @@ -543,7 +555,7 @@ func (q *fakeQuerier) GetProvisionerJobAgentByResourceID(_ context.Context, reso return agent, nil } } - return database.ProvisionerJobAgent{}, sql.ErrNoRows + return database.WorkspaceAgent{}, sql.ErrNoRows } func (q *fakeQuerier) GetProvisionerDaemonByID(_ context.Context, id uuid.UUID) (database.ProvisionerDaemon, error) { @@ -572,7 +584,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() @@ -581,14 +593,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 @@ -601,6 +613,26 @@ func (q *fakeQuerier) GetProvisionerJobResourcesByJobID(_ context.Context, jobID return resources, nil } +func (q *fakeQuerier) GetProvisionerJobsByIDs(ctx 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() @@ -834,12 +866,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, @@ -855,12 +887,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, @@ -922,7 +954,7 @@ func (q *fakeQuerier) InsertWorkspaceBuild(_ context.Context, arg database.Inser BeforeID: arg.BeforeID, Transition: arg.Transition, Initiator: arg.Initiator, - ProvisionJobID: arg.ProvisionJobID, + JobID: arg.JobID, ProvisionerState: arg.ProvisionerState, } q.workspaceBuild = append(q.workspaceBuild, workspaceBuild) @@ -948,6 +980,22 @@ func (q *fakeQuerier) UpdateAPIKeyByID(_ context.Context, arg database.UpdateAPI return sql.ErrNoRows } +func (q *fakeQuerier) UpdateProjectVersionByID(ctx 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() @@ -964,7 +1012,7 @@ func (q *fakeQuerier) UpdateProvisionerDaemonByID(_ context.Context, arg databas return sql.ErrNoRows } -func (q *fakeQuerier) UpdateProvisionerJobAgentByID(_ context.Context, arg database.UpdateProvisionerJobAgentByIDParams) error { +func (q *fakeQuerier) UpdateWorkspaceAgentByID(_ context.Context, arg database.UpdateWorkspaceAgentByIDParams) error { q.mutex.Lock() defer q.mutex.Unlock() diff --git a/database/dump.sql b/database/dump.sql index 6e92fece82128..ec19ea0eef954 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 ( @@ -201,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, @@ -223,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, @@ -264,6 +241,19 @@ CREATE TABLE workspace ( name character varying(64) NOT NULL ); +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, @@ -276,7 +266,17 @@ CREATE TABLE workspace_build ( 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 @@ -312,24 +312,24 @@ 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_agent + ADD CONSTRAINT workspace_agent_id_key UNIQUE (id); 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); @@ -339,20 +339,23 @@ 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_build ADD CONSTRAINT workspace_build_project_version_id_fkey FOREIGN KEY (project_version_id) REFERENCES project_version(id) ON DELETE CASCADE; @@ -363,3 +366,6 @@ ALTER TABLE ONLY workspace_build 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/000003_workspaces.up.sql b/database/migrations/000003_workspaces.up.sql index f2b23adedcb9f..b06b10362c4e0 100644 --- a/database/migrations/000003_workspaces.up.sql +++ b/database/migrations/000003_workspaces.up.sql @@ -14,20 +14,3 @@ CREATE TYPE workspace_transition AS ENUM ( 'delete' ); -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 - 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 851d3871ae26f..ef56b80b30dc3 100644 --- a/database/migrations/000004_jobs.up.sql +++ b/database/migrations/000004_jobs.up.sql @@ -11,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'); @@ -56,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, @@ -67,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, @@ -145,3 +143,21 @@ CREATE TABLE parameter_value ( -- Prevents duplicates for parameters in the same scope. UNIQUE(name, scope, scope_id) ); + +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 cebe4b9517e12..ae2544d3646bc 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 { @@ -381,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"` @@ -403,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"` @@ -444,6 +421,19 @@ type Workspace struct { Name string `db:"name" json:"name"` } +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"` @@ -456,5 +446,15 @@ type WorkspaceBuild 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 7aad96af3e64a..46048cd3a9a68 100644 --- a/database/querier.go +++ b/database/querier.go @@ -26,23 +26,25 @@ type querier interface { GetProjectsByOrganizationIDs(ctx context.Context, ids []string) ([]Project, error) GetProvisionerDaemonByID(ctx context.Context, id uuid.UUID) (ProvisionerDaemon, error) GetProvisionerDaemons(ctx context.Context) ([]ProvisionerDaemon, error) - GetProvisionerJobAgentByAuthToken(ctx context.Context, authToken uuid.UUID) (ProvisionerJobAgent, error) - GetProvisionerJobAgentByInstanceID(ctx context.Context, authInstanceID string) (ProvisionerJobAgent, error) - GetProvisionerJobAgentByResourceID(ctx context.Context, resourceID 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) GetWorkspaceOwnerCountsByProjectIDs(ctx context.Context, ids []uuid.UUID) ([]GetWorkspaceOwnerCountsByProjectIDsRow, error) + GetWorkspaceResourceByID(ctx context.Context, id uuid.UUID) (WorkspaceResource, error) + GetWorkspaceResourcesByJobID(ctx context.Context, jobID uuid.UUID) ([]WorkspaceResource, error) GetWorkspacesByProjectAndUserID(ctx context.Context, arg GetWorkspacesByProjectAndUserIDParams) ([]Workspace, error) GetWorkspacesByUserID(ctx context.Context, ownerID string) ([]Workspace, error) InsertAPIKey(ctx context.Context, arg InsertAPIKeyParams) (APIKey, error) @@ -55,17 +57,18 @@ 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) + 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 - UpdateProvisionerJobAgentByID(ctx context.Context, arg UpdateProvisionerJobAgentByIDParams) error UpdateProvisionerJobByID(ctx context.Context, arg UpdateProvisionerJobByIDParams) error UpdateProvisionerJobWithCompleteByID(ctx context.Context, arg UpdateProvisionerJobWithCompleteByIDParams) error + UpdateWorkspaceAgentByID(ctx context.Context, arg UpdateWorkspaceAgentByIDParams) error UpdateWorkspaceBuildByID(ctx context.Context, arg UpdateWorkspaceBuildByIDParams) error } diff --git a/database/query.sql b/database/query.sql index 5d6506d78d0de..9917a2e45e818 100644 --- a/database/query.sql +++ b/database/query.sql @@ -226,19 +226,19 @@ SELECT FROM provisioner_daemon; --- name: GetProvisionerJobAgentByAuthToken :one +-- name: GetWorkspaceAgentByAuthToken :one SELECT * FROM - provisioner_job_agent + workspace_agent WHERE auth_token = $1; --- name: GetProvisionerJobAgentByInstanceID :one +-- name: GetWorkspaceAgentByInstanceID :one SELECT * FROM - provisioner_job_agent + workspace_agent WHERE auth_instance_id = @auth_instance_id :: text ORDER BY @@ -252,6 +252,14 @@ FROM WHERE id = $1; +-- name: GetProvisionerJobsByIDs :many +SELECT + * +FROM + provisioner_job +WHERE + id = ANY(@ids :: uuid [ ]); + -- name: GetWorkspaceByID :one SELECT * @@ -310,6 +318,16 @@ WHERE LIMIT 1; +-- name: GetWorkspaceBuildByJobID :one +SELECT + * +FROM + workspace_build +WHERE + job_id = $1 +LIMIT + 1; + -- name: GetWorkspaceBuildByWorkspaceIDAndName :one SELECT * @@ -338,27 +356,27 @@ WHERE 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: GetProvisionerJobAgentByResourceID :one +-- name: GetWorkspaceAgentByResourceID :one SELECT * FROM - provisioner_job_agent + workspace_agent WHERE resource_id = $1; @@ -465,9 +483,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, @@ -586,9 +604,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, @@ -615,7 +633,7 @@ INSERT INTO name, transition, initiator, - provision_job_id, + job_id, provisioner_state ) VALUES @@ -633,6 +651,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 @@ -661,9 +688,9 @@ SET WHERE id = $1; --- name: UpdateProvisionerJobAgentByID :exec +-- name: UpdateWorkspaceAgentByID :exec UPDATE - provisioner_job_agent + workspace_agent SET updated_at = $2 WHERE diff --git a/database/query.sql.go b/database/query.sql.go index 9cace7103a686..ed4b05cfad090 100644 --- a/database/query.sql.go +++ b/database/query.sql.go @@ -622,89 +622,6 @@ func (q *sqlQuerier) GetProvisionerDaemons(ctx context.Context) ([]ProvisionerDa return items, nil } -const getProvisionerJobAgentByAuthToken = `-- name: GetProvisionerJobAgentByAuthToken :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_token = $1 -` - -func (q *sqlQuerier) GetProvisionerJobAgentByAuthToken(ctx context.Context, authToken uuid.UUID) (ProvisionerJobAgent, error) { - row := q.db.QueryRowContext(ctx, getProvisionerJobAgentByAuthToken, authToken) - 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 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 getProvisionerJobAgentByResourceID = `-- name: GetProvisionerJobAgentByResourceID :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 - resource_id = $1 -` - -func (q *sqlQuerier) GetProvisionerJobAgentByResourceID(ctx context.Context, resourceID uuid.UUID) (ProvisionerJobAgent, error) { - row := q.db.QueryRowContext(ctx, getProvisionerJobAgentByResourceID, resourceID) - 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 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 @@ -737,56 +654,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 } @@ -948,9 +849,92 @@ func (q *sqlQuerier) GetUserCount(ctx context.Context) (int64, error) { return count, err } +const getWorkspaceAgentByAuthToken = `-- name: GetWorkspaceAgentByAuthToken :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 + auth_token = $1 +` + +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.ResourceID, + &i.AuthToken, + &i.AuthInstanceID, + &i.EnvironmentVariables, + &i.StartupScript, + &i.InstanceMetadata, + &i.ResourceMetadata, + ) + return i, err +} + +const getWorkspaceAgentByInstanceID = `-- name: GetWorkspaceAgentByInstanceID :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 + auth_instance_id = $1 :: text +ORDER BY + created_at DESC +` + +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 +} + +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.ResourceID, + &i.AuthToken, + &i.AuthInstanceID, + &i.EnvironmentVariables, + &i.StartupScript, + &i.InstanceMetadata, + &i.ResourceMetadata, + ) + return i, err +} + 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_build WHERE @@ -974,14 +958,45 @@ func (q *sqlQuerier) GetWorkspaceBuildByID(ctx context.Context, id uuid.UUID) (W &i.Transition, &i.Initiator, &i.ProvisionerState, - &i.ProvisionJobID, + &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, + &i.UpdatedAt, + &i.WorkspaceID, + &i.ProjectVersionID, + &i.Name, + &i.BeforeID, + &i.AfterID, + &i.Transition, + &i.Initiator, + &i.ProvisionerState, + &i.JobID, ) return i, err } 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_build WHERE @@ -1009,7 +1024,7 @@ func (q *sqlQuerier) GetWorkspaceBuildByWorkspaceID(ctx context.Context, workspa &i.Transition, &i.Initiator, &i.ProvisionerState, - &i.ProvisionJobID, + &i.JobID, ); err != nil { return nil, err } @@ -1026,7 +1041,7 @@ func (q *sqlQuerier) GetWorkspaceBuildByWorkspaceID(ctx context.Context, workspa 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_build WHERE @@ -1054,14 +1069,14 @@ func (q *sqlQuerier) GetWorkspaceBuildByWorkspaceIDAndName(ctx context.Context, &i.Transition, &i.Initiator, &i.ProvisionerState, - &i.ProvisionJobID, + &i.JobID, ) return i, err } 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_build WHERE @@ -1086,7 +1101,7 @@ func (q *sqlQuerier) GetWorkspaceBuildByWorkspaceIDWithoutAfter(ctx context.Cont &i.Transition, &i.Initiator, &i.ProvisionerState, - &i.ProvisionJobID, + &i.JobID, ) return i, err } @@ -1186,6 +1201,70 @@ func (q *sqlQuerier) GetWorkspaceOwnerCountsByProjectIDs(ctx context.Context, id return items, nil } +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, job_id, transition, type, name, agent_id +FROM + workspace_resource +WHERE + job_id = $1 +` + +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 []WorkspaceResource + for rows.Next() { + var i WorkspaceResource + if err := rows.Scan( + &i.ID, + &i.CreatedAt, + &i.JobID, + &i.Transition, + &i.Type, + &i.Name, + &i.AgentID, + ); 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 getWorkspacesByProjectAndUserID = `-- name: GetWorkspacesByProjectAndUserID :many SELECT id, created_at, updated_at, owner_id, project_id, name @@ -1836,66 +1915,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 @@ -1954,54 +1973,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 ( @@ -2110,6 +2081,66 @@ func (q *sqlQuerier) InsertWorkspace(ctx context.Context, arg InsertWorkspacePar return i, err } +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_build ( @@ -2122,11 +2153,11 @@ 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 InsertWorkspaceBuildParams struct { @@ -2139,7 +2170,7 @@ type InsertWorkspaceBuildParams 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"` } @@ -2154,7 +2185,7 @@ func (q *sqlQuerier) InsertWorkspaceBuild(ctx context.Context, arg InsertWorkspa arg.Name, arg.Transition, arg.Initiator, - arg.ProvisionJobID, + arg.JobID, arg.ProvisionerState, ) var i WorkspaceBuild @@ -2170,7 +2201,55 @@ func (q *sqlQuerier) InsertWorkspaceBuild(ctx context.Context, arg InsertWorkspa &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 } @@ -2209,43 +2288,45 @@ func (q *sqlQuerier) UpdateAPIKeyByID(ctx context.Context, arg UpdateAPIKeyByIDP return err } -const updateProvisionerDaemonByID = `-- name: UpdateProvisionerDaemonByID :exec +const updateProjectVersionByID = `-- name: UpdateProjectVersionByID :exec UPDATE - provisioner_daemon + project_version SET - updated_at = $2, - provisioners = $3 + project_id = $2, + updated_at = $3 WHERE id = $1 ` -type UpdateProvisionerDaemonByIDParams struct { - ID uuid.UUID `db:"id" json:"id"` - UpdatedAt sql.NullTime `db:"updated_at" json:"updated_at"` - Provisioners []ProvisionerType `db:"provisioners" json:"provisioners"` +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) UpdateProvisionerDaemonByID(ctx context.Context, arg UpdateProvisionerDaemonByIDParams) error { - _, err := q.db.ExecContext(ctx, updateProvisionerDaemonByID, arg.ID, arg.UpdatedAt, pq.Array(arg.Provisioners)) +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 updateProvisionerJobAgentByID = `-- name: UpdateProvisionerJobAgentByID :exec +const updateProvisionerDaemonByID = `-- name: UpdateProvisionerDaemonByID :exec UPDATE - provisioner_job_agent + provisioner_daemon SET - updated_at = $2 + updated_at = $2, + provisioners = $3 WHERE id = $1 ` -type UpdateProvisionerJobAgentByIDParams struct { - ID uuid.UUID `db:"id" json:"id"` - UpdatedAt sql.NullTime `db:"updated_at" json:"updated_at"` +type UpdateProvisionerDaemonByIDParams struct { + ID uuid.UUID `db:"id" json:"id"` + UpdatedAt sql.NullTime `db:"updated_at" json:"updated_at"` + Provisioners []ProvisionerType `db:"provisioners" json:"provisioners"` } -func (q *sqlQuerier) UpdateProvisionerJobAgentByID(ctx context.Context, arg UpdateProvisionerJobAgentByIDParams) error { - _, err := q.db.ExecContext(ctx, updateProvisionerJobAgentByID, arg.ID, arg.UpdatedAt) +func (q *sqlQuerier) UpdateProvisionerDaemonByID(ctx context.Context, arg UpdateProvisionerDaemonByIDParams) error { + _, err := q.db.ExecContext(ctx, updateProvisionerDaemonByID, arg.ID, arg.UpdatedAt, pq.Array(arg.Provisioners)) return err } @@ -2299,6 +2380,25 @@ func (q *sqlQuerier) UpdateProvisionerJobWithCompleteByID(ctx context.Context, a return err } +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_build diff --git a/httpmw/workspaceagent.go b/httpmw/workspaceagent.go index 85a46ed0001cb..13fbf556b026e 100644 --- a/httpmw/workspaceagent.go +++ b/httpmw/workspaceagent.go @@ -16,8 +16,8 @@ import ( type workspaceAgentContextKey struct{} // WorkspaceAgent returns the workspace agent from the ExtractAgent handler. -func WorkspaceAgent(r *http.Request) database.ProvisionerJobAgent { - user, ok := r.Context().Value(workspaceAgentContextKey{}).(database.ProvisionerJobAgent) +func WorkspaceAgent(r *http.Request) database.WorkspaceAgent { + user, ok := r.Context().Value(workspaceAgentContextKey{}).(database.WorkspaceAgent) if !ok { panic("developer error: agent middleware not provided") } @@ -42,7 +42,7 @@ func ExtractWorkspaceAgent(db database.Store) func(http.Handler) http.Handler { }) return } - agent, err := db.GetProvisionerJobAgentByAuthToken(r.Context(), token) + 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{ diff --git a/httpmw/workspaceagent_test.go b/httpmw/workspaceagent_test.go index b36d74c9c23dc..fc14db017a1be 100644 --- a/httpmw/workspaceagent_test.go +++ b/httpmw/workspaceagent_test.go @@ -57,7 +57,7 @@ func TestWorkspaceAgent(t *testing.T) { rw.WriteHeader(http.StatusOK) }) r, token := setup(db) - _, err := db.InsertProvisionerJobAgent(context.Background(), database.InsertProvisionerJobAgentParams{ + _, err := db.InsertWorkspaceAgent(context.Background(), database.InsertWorkspaceAgentParams{ ID: uuid.New(), AuthToken: token, }) diff --git a/httpmw/workspaceresourceparam.go b/httpmw/workspaceresourceparam.go index 8f57932721863..101e6dc2e4b7b 100644 --- a/httpmw/workspaceresourceparam.go +++ b/httpmw/workspaceresourceparam.go @@ -9,13 +9,14 @@ import ( "github.com/coder/coder/database" "github.com/coder/coder/httpapi" + "github.com/go-chi/chi/v5" ) type workspaceResourceParamContextKey struct{} // ProvisionerJobParam returns the project from the ExtractProjectParam handler. -func WorkspaceResourceParam(r *http.Request) database.ProvisionerJobResource { - resource, ok := r.Context().Value(workspaceResourceParamContextKey{}).(database.ProvisionerJobResource) +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") } @@ -30,7 +31,7 @@ func ExtractWorkspaceResourceParam(db database.Store) func(http.Handler) http.Ha if !parsed { return } - resource, err := db.GetProvisionerJobResourceByID(r.Context(), resourceUUID) + 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", @@ -43,15 +44,31 @@ func ExtractWorkspaceResourceParam(db database.Store) func(http.Handler) http.Ha }) return } - workspaceBuild := WorkspaceBuildParam(r) - if workspaceBuild.ProvisionJobID.String() != resource.JobID.String() { - httpapi.Write(rw, http.StatusUnauthorized, httpapi.Response{ - Message: "you don't own this resource", + + 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 index 36f387f8464dd..d746f7db98b97 100644 --- a/httpmw/workspaceresourceparam_test.go +++ b/httpmw/workspaceresourceparam_test.go @@ -18,14 +18,21 @@ import ( func TestWorkspaceResourceParam(t *testing.T) { t.Parallel() - setup := func(db database.Store) (*http.Request, database.ProvisionerJobResource) { + setup := func(db database.Store) (*http.Request, database.WorkspaceResource) { r := httptest.NewRequest("GET", "/", nil) - workspaceBuild, err := db.InsertWorkspaceBuild(r.Context(), database.InsertWorkspaceBuildParams{ - ID: uuid.New(), + job, err := db.InsertProvisionerJob(context.Background(), database.InsertProvisionerJobParams{ + ID: uuid.New(), + Type: database.ProvisionerJobTypeWorkspaceBuild, }) require.NoError(t, err) - resource, err := db.InsertProvisionerJobResource(context.Background(), database.InsertProvisionerJobResourceParams{ - ID: uuid.New(), + 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) @@ -74,7 +81,6 @@ func TestWorkspaceResourceParam(t *testing.T) { db := databasefake.New() rtr := chi.NewRouter() rtr.Use( - httpmw.ExtractWorkspaceBuildParam(db), httpmw.ExtractWorkspaceResourceParam(db), ) rtr.Get("/", func(rw http.ResponseWriter, r *http.Request) { diff --git a/peerbroker/proxy.go b/peerbroker/proxy.go index 5a340ed855845..eddebabba1bc0 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.NegotiateConnection_ServerToClient - 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] @@ -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/provisionerd/proto/provisionerd.pb.go b/provisionerd/proto/provisionerd.pb.go index 51855606811d4..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,7 +598,7 @@ 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 @@ -610,8 +610,8 @@ type AcquiredJob_WorkspaceProvision struct { 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) GetWorkspaceBuildId() string { +func (x *AcquiredJob_WorkspaceBuild) GetWorkspaceBuildId() string { if x != nil { 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, 0xc8, 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,19 +940,18 @@ 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, 0x84, 0x02, 0x0a, 0x12, 0x57, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, - 0x63, 0x65, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x2c, 0x0a, 0x12, 0x77, + 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, @@ -973,41 +972,39 @@ var file_provisionerd_proto_provisionerd_proto_rawDesc = []byte{ 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, + 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, 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, 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, + 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, + 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, 0xd3, 0x03, 0x0a, 0x0c, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x4a, 0x6f, + 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, 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, 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, + 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, 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, @@ -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 b867200f5a86c..150878c7d5b13 100644 --- a/provisionerd/proto/provisionerd.proto +++ b/provisionerd/proto/provisionerd.proto @@ -11,7 +11,7 @@ message Empty {} // AcquiredJob is returned when a provisioner daemon has a job locked. message AcquiredJob { - message WorkspaceProvision { + message WorkspaceBuild { string workspace_build_id = 1; string workspace_name = 2; repeated provisioner.ParameterValue parameter_values = 3; @@ -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 a9f76159cd704..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_build_id", job.GetWorkspaceProvision().WorkspaceBuildId), + 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{}, }, }, From 29a019a422148946ee1e02971b5670114c552f49 Mon Sep 17 00:00:00 2001 From: Kyle Carberry Date: Sun, 6 Mar 2022 23:20:40 +0000 Subject: [PATCH 11/22] Fix agent lock --- agent/agent.go | 13 +- cli/login.go | 4 +- cli/login_test.go | 25 +--- cli/projectcreate.go | 33 +++-- cli/projectlist.go | 2 +- cli/projectlist_test.go | 6 +- cli/projects.go | 4 +- cli/root.go | 2 +- cli/workspaceagent.go | 4 +- cli/workspacecreate.go | 16 +-- cli/workspacecreate_test.go | 6 +- coderd/coderdtest/coderdtest.go | 2 +- coderd/files.go | 2 +- coderd/organizations.go | 2 +- coderd/organizations_test.go | 6 +- coderd/projectimport.go | 168 -------------------------- coderd/projectversions.go | 4 - coderd/users_test.go | 3 +- coderd/workspacebuilds.go | 1 + coderd/workspacebuilds_test.go | 3 +- coderd/workspaceresources_test.go | 5 +- codersdk/organizations.go | 2 +- codersdk/projectversions.go | 4 +- codersdk/workspacebuilds.go | 3 +- codersdk/workspaceresources.go | 9 +- database/databasefake/databasefake.go | 6 +- httpmw/httpmw.go | 31 ----- httpmw/workspaceresourceparam.go | 3 +- 28 files changed, 80 insertions(+), 289 deletions(-) delete mode 100644 coderd/projectimport.go diff --git a/agent/agent.go b/agent/agent.go index d8dba42a47b0b..253613a66a3cd 100644 --- a/agent/agent.go +++ b/agent/agent.go @@ -77,9 +77,10 @@ type server struct { clientDialer Dialer 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 } @@ -279,11 +280,16 @@ func (s *server) run(ctx context.Context) { s.run(ctx) return } + s.connCloseWait.Add(1) 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 +331,6 @@ func (s *server) Close() error { close(s.closed) s.closeCancel() _ = s.sshServer.Close() + s.connCloseWait.Wait() return nil } 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 7267af63ca9d5..2e401a9e3b4a3 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.CreateParameterValueRequest) (*coderd.ProjectVersion, error) { spin := spinner.New(spinner.CharSets[5], 100*time.Millisecond) spin.Writer = cmd.OutOrStdout() spin.Suffix = " Uploading current directory..." @@ -139,7 +139,7 @@ func validateProjectVersionSource(cmd *cobra.Command, client *codersdk.Client, o } 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 } @@ -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/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 03ee7ad5e995d..db2392bf2797d 100644 --- a/cli/projectlist_test.go +++ b/cli/projectlist_test.go @@ -35,10 +35,10 @@ func TestProjectList(t *testing.T) { client := coderdtest.New(t, nil) user := coderdtest.CreateFirstUser(t, client) daemon := coderdtest.NewProvisionerDaemon(t, client) - job := coderdtest.CreateProjectVersion(t, client, user.OrganizationID, nil) - coderdtest.AwaitProjectImportJob(t, client, user.OrganizationID, job.ID) + version := coderdtest.CreateProjectVersion(t, client, user.OrganizationID, nil) + coderdtest.AwaitProjectVersionJob(t, client, version.ID) _ = daemon.Close() - project := coderdtest.CreateProject(t, client, user.OrganizationID, 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 18300ae5db7f4..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.WorkspaceResource) 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 index 3246b21bee5f1..e8dc4e2da2487 100644 --- a/cli/workspaceagent.go +++ b/cli/workspaceagent.go @@ -39,7 +39,7 @@ func workspaceAgent() *cobra.Command { } switch { case probe.GCP(): - response, err := client.AuthenticateWorkspaceAgentUsingGoogleCloudIdentity(cmd.Context(), "", nil) + response, err := client.AuthWorkspaceGoogleInstanceIdentity(cmd.Context(), "", nil) if err != nil { return xerrors.Errorf("authenticate workspace with gcp: %w", err) } @@ -49,7 +49,7 @@ func workspaceAgent() *cobra.Command { } } client.SessionToken = sessionToken - closer := agent.New(client.WorkspaceAgentServe, nil) + closer := agent.New(client.ListenWorkspaceAgent, nil) <-cmd.Context().Done() return closer.Close() }, diff --git a/cli/workspacecreate.go b/cli/workspacecreate.go index 82b12ff844fe0..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.CreateWorkspaceBuild(cmd.Context(), "", workspace.Name, coderd.CreateWorkspaceBuildRequest{ + 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.WorkspaceBuildJobLogsAfter(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 86ffb65aed051..6709da6aa057e 100644 --- a/cli/workspacecreate_test.go +++ b/cli/workspacecreate_test.go @@ -19,7 +19,7 @@ func TestWorkspaceCreate(t *testing.T) { client := coderdtest.New(t, nil) user := coderdtest.CreateFirstUser(t, client) _ = coderdtest.NewProvisionerDaemon(t, client) - job := coderdtest.CreateProjectVersion(t, client, user.OrganizationID, &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.OrganizationID, job.ID) - project := coderdtest.CreateProject(t, client, user.OrganizationID, 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/coderd/coderdtest/coderdtest.go b/coderd/coderdtest/coderdtest.go index 2e3266adbc119..8dd936e1c8154 100644 --- a/coderd/coderdtest/coderdtest.go +++ b/coderd/coderdtest/coderdtest.go @@ -191,7 +191,7 @@ func CreateProjectVersion(t *testing.T, client *codersdk.Client, organization st require.NoError(t, err) file, err := client.Upload(context.Background(), codersdk.ContentTypeTar, data) require.NoError(t, err) - projectVersion, err := client.CreateProjectVersion(context.Background(), organization, coderd.CreateProjectVersion{ + projectVersion, err := client.CreateProjectVersion(context.Background(), organization, coderd.CreateProjectVersionRequest{ StorageSource: file.Hash, StorageMethod: database.ProvisionerStorageMethodFile, Provisioner: database.ProvisionerTypeEcho, diff --git a/coderd/files.go b/coderd/files.go index 1d98ab4dc17ef..2d753b44132d0 100644 --- a/coderd/files.go +++ b/coderd/files.go @@ -96,5 +96,5 @@ func (api *api) fileByHash(rw http.ResponseWriter, r *http.Request) { } rw.Header().Set("Content-Type", file.Mimetype) rw.WriteHeader(http.StatusOK) - rw.Write(file.Data) + _, _ = rw.Write(file.Data) } diff --git a/coderd/organizations.go b/coderd/organizations.go index 42ba97f29092e..a81bca51a9a6b 100644 --- a/coderd/organizations.go +++ b/coderd/organizations.go @@ -52,7 +52,7 @@ type CreateProjectRequest struct { VersionID uuid.UUID `json:"project_version_id" validate:"required"` } -func (api *api) organization(rw http.ResponseWriter, r *http.Request) { +func (*api) organization(rw http.ResponseWriter, r *http.Request) { organization := httpmw.OrganizationParam(r) render.Status(r, http.StatusOK) render.JSON(rw, r, convertOrganization(organization)) diff --git a/coderd/organizations_test.go b/coderd/organizations_test.go index 0af786f2e3a73..4347a15f299a4 100644 --- a/coderd/organizations_test.go +++ b/coderd/organizations_test.go @@ -40,7 +40,7 @@ func TestPostProjectVersionsByOrganization(t *testing.T) { client := coderdtest.New(t, nil) user := coderdtest.CreateFirstUser(t, client) projectID := uuid.New() - _, err := client.CreateProjectVersion(context.Background(), user.OrganizationID, coderd.CreateProjectVersion{ + _, err := client.CreateProjectVersion(context.Background(), user.OrganizationID, coderd.CreateProjectVersionRequest{ ProjectID: &projectID, StorageMethod: database.ProvisionerStorageMethodFile, StorageSource: "hash", @@ -55,7 +55,7 @@ func TestPostProjectVersionsByOrganization(t *testing.T) { t.Parallel() client := coderdtest.New(t, nil) user := coderdtest.CreateFirstUser(t, client) - _, err := client.CreateProjectVersion(context.Background(), user.OrganizationID, coderd.CreateProjectVersion{ + _, err := client.CreateProjectVersion(context.Background(), user.OrganizationID, coderd.CreateProjectVersionRequest{ StorageMethod: database.ProvisionerStorageMethodFile, StorageSource: "hash", Provisioner: database.ProvisionerTypeEcho, @@ -77,7 +77,7 @@ func TestPostProjectVersionsByOrganization(t *testing.T) { 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.CreateProjectVersion{ + _, err = client.CreateProjectVersion(context.Background(), user.OrganizationID, coderd.CreateProjectVersionRequest{ StorageMethod: database.ProvisionerStorageMethodFile, StorageSource: file.Hash, Provisioner: database.ProvisionerTypeEcho, diff --git a/coderd/projectimport.go b/coderd/projectimport.go deleted file mode 100644 index 55f922c6ebbce..0000000000000 --- a/coderd/projectimport.go +++ /dev/null @@ -1,168 +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"` -} - -type CreateProjectVersion struct { - 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 []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) - var job database.ProvisionerJob - if convertProvisionerJob(job).CompletedAt == nil { - 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) - var job database.ProvisionerJob - 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) -} diff --git a/coderd/projectversions.go b/coderd/projectversions.go index 430e1ca130b35..a32c8db7e6d16 100644 --- a/coderd/projectversions.go +++ b/coderd/projectversions.go @@ -148,7 +148,3 @@ func convertProjectVersion(version database.ProjectVersion, job ProvisionerJob) Job: job, } } - -func convertProjectVersionParameterSchema(schema database.ParameterSchema) ProjectVersionParameterSchema { - return ProjectVersionParameterSchema(schema) -} diff --git a/coderd/users_test.go b/coderd/users_test.go index fb948c8208b79..f28302bbff428 100644 --- a/coderd/users_test.go +++ b/coderd/users_test.go @@ -255,12 +255,12 @@ func TestOrganizationByUserAndName(t *testing.T) { _, err = client.OrganizationByName(context.Background(), "", org.Name) require.NoError(t, err) }) - } func TestPostOrganizationsByUser(t *testing.T) { t.Parallel() t.Run("Conflict", func(t *testing.T) { + t.Parallel() client := coderdtest.New(t, nil) user := coderdtest.CreateFirstUser(t, client) org, err := client.Organization(context.Background(), user.OrganizationID) @@ -274,6 +274,7 @@ func TestPostOrganizationsByUser(t *testing.T) { }) t.Run("Create", func(t *testing.T) { + t.Parallel() client := coderdtest.New(t, nil) _ = coderdtest.CreateFirstUser(t, client) _, err := client.CreateOrganization(context.Background(), "", coderd.CreateOrganizationRequest{ diff --git a/coderd/workspacebuilds.go b/coderd/workspacebuilds.go index 4f8dfbeac3c1e..6db8dbf21ea43 100644 --- a/coderd/workspacebuilds.go +++ b/coderd/workspacebuilds.go @@ -67,6 +67,7 @@ func (api *api) workspaceBuildLogs(rw http.ResponseWriter, r *http.Request) { } func convertWorkspaceBuild(workspaceBuild database.WorkspaceBuild, job ProvisionerJob) WorkspaceBuild { + //nolint:unconvert return WorkspaceBuild(WorkspaceBuild{ ID: workspaceBuild.ID, CreatedAt: workspaceBuild.CreatedAt, diff --git a/coderd/workspacebuilds_test.go b/coderd/workspacebuilds_test.go index da1e8655c04d4..16743be33af35 100644 --- a/coderd/workspacebuilds_test.go +++ b/coderd/workspacebuilds_test.go @@ -6,13 +6,14 @@ import ( "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" - "github.com/stretchr/testify/require" ) func TestWorkspaceBuild(t *testing.T) { diff --git a/coderd/workspaceresources_test.go b/coderd/workspaceresources_test.go index a346ebeb9c2da..2b7476a2ef8ae 100644 --- a/coderd/workspaceresources_test.go +++ b/coderd/workspaceresources_test.go @@ -4,11 +4,12 @@ import ( "context" "testing" - "cdr.dev/slog" - "cdr.dev/slog/sloggers/slogtest" "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" diff --git a/codersdk/organizations.go b/codersdk/organizations.go index 3cea65b084bb5..9a496bbb0dc1c 100644 --- a/codersdk/organizations.go +++ b/codersdk/organizations.go @@ -38,7 +38,7 @@ func (c *Client) ProvisionerDaemonsByOrganization(ctx context.Context, organizat // 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.CreateProjectVersion) (coderd.ProjectVersion, error) { +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 diff --git a/codersdk/projectversions.go b/codersdk/projectversions.go index c244a016a48d0..40530ff68eb08 100644 --- a/codersdk/projectversions.go +++ b/codersdk/projectversions.go @@ -27,7 +27,7 @@ func (c *Client) ProjectVersion(ctx context.Context, id uuid.UUID) (coderd.Proje } // ProjectVersionSchema returns schemas for a project version by ID. -func (c *Client) ProjectVersionSchema(ctx context.Context, version uuid.UUID) ([]coderd.ParameterSchema, error) { +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 @@ -36,7 +36,7 @@ func (c *Client) ProjectVersionSchema(ctx context.Context, version uuid.UUID) ([ if res.StatusCode != http.StatusOK { return nil, readBodyAsError(res) } - var params []coderd.ParameterSchema + var params []coderd.ProjectVersionParameterSchema return params, json.NewDecoder(res.Body).Decode(¶ms) } diff --git a/codersdk/workspacebuilds.go b/codersdk/workspacebuilds.go index 5e03b82e5611f..f62eea4a0e5df 100644 --- a/codersdk/workspacebuilds.go +++ b/codersdk/workspacebuilds.go @@ -7,8 +7,9 @@ import ( "net/http" "time" - "github.com/coder/coder/coderd" "github.com/google/uuid" + + "github.com/coder/coder/coderd" ) // WorkspaceBuild returns a single workspace build for a workspace. diff --git a/codersdk/workspaceresources.go b/codersdk/workspaceresources.go index 652ab8fcb68c2..cececb60354f7 100644 --- a/codersdk/workspaceresources.go +++ b/codersdk/workspaceresources.go @@ -8,16 +8,17 @@ import ( "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" - "github.com/google/uuid" - "github.com/hashicorp/yamux" - "golang.org/x/xerrors" - "nhooyr.io/websocket" ) func (c *Client) WorkspaceResource(ctx context.Context, id uuid.UUID) (coderd.WorkspaceResource, error) { diff --git a/database/databasefake/databasefake.go b/database/databasefake/databasefake.go index 738b62b4c5e82..8530048d8cfa6 100644 --- a/database/databasefake/databasefake.go +++ b/database/databasefake/databasefake.go @@ -222,7 +222,7 @@ func (q *fakeQuerier) GetWorkspaceBuildByID(_ context.Context, id uuid.UUID) (da return database.WorkspaceBuild{}, sql.ErrNoRows } -func (q *fakeQuerier) GetWorkspaceBuildByJobID(ctx context.Context, jobID uuid.UUID) (database.WorkspaceBuild, error) { +func (q *fakeQuerier) GetWorkspaceBuildByJobID(_ context.Context, jobID uuid.UUID) (database.WorkspaceBuild, error) { q.mutex.Lock() defer q.mutex.Unlock() @@ -613,7 +613,7 @@ func (q *fakeQuerier) GetWorkspaceResourcesByJobID(_ context.Context, jobID uuid return resources, nil } -func (q *fakeQuerier) GetProvisionerJobsByIDs(ctx context.Context, ids []uuid.UUID) ([]database.ProvisionerJob, error) { +func (q *fakeQuerier) GetProvisionerJobsByIDs(_ context.Context, ids []uuid.UUID) ([]database.ProvisionerJob, error) { q.mutex.Lock() defer q.mutex.Unlock() @@ -980,7 +980,7 @@ func (q *fakeQuerier) UpdateAPIKeyByID(_ context.Context, arg database.UpdateAPI return sql.ErrNoRows } -func (q *fakeQuerier) UpdateProjectVersionByID(ctx context.Context, arg database.UpdateProjectVersionByIDParams) error { +func (q *fakeQuerier) UpdateProjectVersionByID(_ context.Context, arg database.UpdateProjectVersionByIDParams) error { q.mutex.Lock() defer q.mutex.Unlock() diff --git a/httpmw/httpmw.go b/httpmw/httpmw.go index 1c4e6027c1b0a..003b76da1e79e 100644 --- a/httpmw/httpmw.go +++ b/httpmw/httpmw.go @@ -1,15 +1,12 @@ package httpmw import ( - "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" ) @@ -31,31 +28,3 @@ func parseUUID(rw http.ResponseWriter, r *http.Request, param string) (uuid.UUID } return parsed, true } - -func fetchOrganization(rw http.ResponseWriter, r *http.Request, db database.Store, organizationID string) (database.Organization, database.OrganizationMember, bool) { - apiKey := APIKey(r) - organization, err := db.GetOrganizationByID(r.Context(), organizationID) - if err != nil { - httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ - Message: fmt.Sprintf("get organization: %s", err.Error()), - }) - return organization, database.OrganizationMember{}, false - } - organizationMember, err := db.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: "not a member of the organization", - }) - return organization, organizationMember, false - } - if err != nil { - httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ - Message: fmt.Sprintf("get organization member: %s", err.Error()), - }) - return organization, organizationMember, false - } - return organization, organizationMember, true -} diff --git a/httpmw/workspaceresourceparam.go b/httpmw/workspaceresourceparam.go index 101e6dc2e4b7b..a02d556866315 100644 --- a/httpmw/workspaceresourceparam.go +++ b/httpmw/workspaceresourceparam.go @@ -7,9 +7,10 @@ import ( "fmt" "net/http" + "github.com/go-chi/chi/v5" + "github.com/coder/coder/database" "github.com/coder/coder/httpapi" - "github.com/go-chi/chi/v5" ) type workspaceResourceParamContextKey struct{} From bdea34558657ddb2145d6edf8b0e9266b6f294c0 Mon Sep 17 00:00:00 2001 From: Kyle Carberry Date: Sun, 6 Mar 2022 23:29:49 +0000 Subject: [PATCH 12/22] Cleanup database abstraction --- coderd/organizations.go | 3 +- database/databasefake/databasefake.go | 30 ++------------- database/querier.go | 3 +- database/query.sql | 13 +------ database/query.sql.go | 53 ++------------------------- 5 files changed, 12 insertions(+), 90 deletions(-) diff --git a/coderd/organizations.go b/coderd/organizations.go index a81bca51a9a6b..2ef39721e1c01 100644 --- a/coderd/organizations.go +++ b/coderd/organizations.go @@ -270,10 +270,9 @@ func (api *api) postProjectsByOrganization(rw http.ResponseWriter, r *http.Reque render.JSON(rw, r, project) } -// 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}) + projects, err := api.Database.GetProjectsByOrganization(r.Context(), organization.ID) if errors.Is(err, sql.ErrNoRows) { err = nil } diff --git a/database/databasefake/databasefake.go b/database/databasefake/databasefake.go index 8530048d8cfa6..f709f6920d4b3 100644 --- a/database/databasefake/databasefake.go +++ b/database/databasefake/databasefake.go @@ -281,26 +281,6 @@ func (q *fakeQuerier) GetWorkspaceBuildByWorkspaceIDAndName(_ context.Context, a return database.WorkspaceBuild{}, sql.ErrNoRows } -func (q *fakeQuerier) GetWorkspacesByProjectAndUserID(_ context.Context, arg database.GetWorkspacesByProjectAndUserIDParams) ([]database.Workspace, error) { - q.mutex.Lock() - defer q.mutex.Unlock() - - workspaces := make([]database.Workspace, 0) - for _, workspace := range q.workspace { - if workspace.OwnerID != arg.OwnerID { - continue - } - if workspace.ProjectID.String() != arg.ProjectID.String() { - continue - } - workspaces = append(workspaces, workspace) - } - if len(workspaces) == 0 { - return nil, sql.ErrNoRows - } - return workspaces, nil -} - func (q *fakeQuerier) GetWorkspacesByUserID(_ context.Context, ownerID string) ([]database.Workspace, error) { q.mutex.Lock() defer q.mutex.Unlock() @@ -475,17 +455,15 @@ 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) GetProjectsByOrganization(_ context.Context, id 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 == id { + projects = append(projects, project) + break } } if len(projects) == 0 { diff --git a/database/querier.go b/database/querier.go index 46048cd3a9a68..b7f71c4a6fabb 100644 --- a/database/querier.go +++ b/database/querier.go @@ -23,7 +23,7 @@ type querier interface { GetProjectVersionByID(ctx context.Context, id uuid.UUID) (ProjectVersion, error) GetProjectVersionByProjectIDAndName(ctx context.Context, arg GetProjectVersionByProjectIDAndNameParams) (ProjectVersion, error) GetProjectVersionsByProjectID(ctx context.Context, dollar_1 uuid.UUID) ([]ProjectVersion, error) - GetProjectsByOrganizationIDs(ctx context.Context, ids []string) ([]Project, error) + GetProjectsByOrganization(ctx context.Context, organizationID string) ([]Project, error) GetProvisionerDaemonByID(ctx context.Context, id uuid.UUID) (ProvisionerDaemon, error) GetProvisionerDaemons(ctx context.Context) ([]ProvisionerDaemon, error) GetProvisionerJobByID(ctx context.Context, id uuid.UUID) (ProvisionerJob, error) @@ -45,7 +45,6 @@ type querier interface { GetWorkspaceOwnerCountsByProjectIDs(ctx context.Context, ids []uuid.UUID) ([]GetWorkspaceOwnerCountsByProjectIDsRow, error) GetWorkspaceResourceByID(ctx context.Context, id uuid.UUID) (WorkspaceResource, error) GetWorkspaceResourcesByJobID(ctx context.Context, jobID uuid.UUID) ([]WorkspaceResource, error) - GetWorkspacesByProjectAndUserID(ctx context.Context, arg GetWorkspacesByProjectAndUserIDParams) ([]Workspace, 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) diff --git a/database/query.sql b/database/query.sql index 9917a2e45e818..a3fa1a20d495a 100644 --- a/database/query.sql +++ b/database/query.sql @@ -157,13 +157,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 @@ -287,15 +287,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, diff --git a/database/query.sql.go b/database/query.sql.go index ed4b05cfad090..c7f71dd337173 100644 --- a/database/query.sql.go +++ b/database/query.sql.go @@ -522,17 +522,17 @@ func (q *sqlQuerier) GetProjectVersionsByProjectID(ctx context.Context, dollar_1 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 } @@ -1265,51 +1265,6 @@ func (q *sqlQuerier) GetWorkspaceResourcesByJobID(ctx context.Context, jobID uui return items, nil } -const getWorkspacesByProjectAndUserID = `-- name: GetWorkspacesByProjectAndUserID :many -SELECT - id, created_at, updated_at, owner_id, project_id, name -FROM - workspace -WHERE - owner_id = $1 - AND project_id = $2 -` - -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) - if err != nil { - return nil, err - } - defer rows.Close() - var items []Workspace - for rows.Next() { - var i Workspace - if err := rows.Scan( - &i.ID, - &i.CreatedAt, - &i.UpdatedAt, - &i.OwnerID, - &i.ProjectID, - &i.Name, - ); 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 getWorkspacesByUserID = `-- name: GetWorkspacesByUserID :many SELECT id, created_at, updated_at, owner_id, project_id, name From e903072c41116c9f10f4e07031bdb3d7286ce173 Mon Sep 17 00:00:00 2001 From: Kyle Carberry Date: Mon, 7 Mar 2022 00:40:53 +0000 Subject: [PATCH 13/22] Add parameters --- coderd/coderd.go | 16 +-- coderd/organizations.go | 2 +- coderd/organizations_test.go | 2 +- coderd/parameters.go | 190 +++++++++++++++++++++++++ coderd/parameters_test.go | 120 ++++++++++++++++ coderd/projects.go | 73 ---------- coderd/projects_test.go | 55 ------- codersdk/parameters.go | 48 +++++++ codersdk/projects.go | 42 ------ database/databasefake/databasefake.go | 34 +++++ database/dump.sql | 4 +- database/migrations/000004_jobs.up.sql | 4 +- database/models.go | 2 +- database/querier.go | 2 + database/query.sql | 18 +++ database/query.sql.go | 56 +++++++- 16 files changed, 479 insertions(+), 189 deletions(-) create mode 100644 coderd/parameters.go create mode 100644 coderd/parameters_test.go create mode 100644 codersdk/parameters.go diff --git a/coderd/coderd.go b/coderd/coderd.go index a279b41ea328f..6a21929411eb2 100644 --- a/coderd/coderd.go +++ b/coderd/coderd.go @@ -59,10 +59,14 @@ func New(options *Options) (http.Handler, func()) { r.Get("/", api.projectsByOrganization) r.Get("/{projectname}", api.projectByOrganizationAndName) }) - r.Get("/parameters", nil) - r.Post("/parameters", nil) - r.Patch("/parameters/{name}", nil) - r.Delete("/parameters/{name}", nil) + }) + 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/{project}", func(r chi.Router) { r.Use( @@ -71,10 +75,6 @@ func New(options *Options) (http.Handler, func()) { httpmw.ExtractOrganizationParam(options.Database), ) r.Get("/", api.project) - r.Get("/parameters", api.parametersByProject) - r.Post("/parameters", api.postParametersByProject) - r.Patch("/parameters/{name}", nil) - r.Delete("/parameters/{name}", nil) r.Route("/versions", func(r chi.Router) { r.Get("/", api.projectVersionsByProject) r.Patch("/versions", nil) diff --git a/coderd/organizations.go b/coderd/organizations.go index 2ef39721e1c01..c8f3e4fc975b9 100644 --- a/coderd/organizations.go +++ b/coderd/organizations.go @@ -36,7 +36,7 @@ type CreateProjectVersionRequest struct { 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"` + ParameterValues []CreateParameterRequest `json:"parameter_values"` } // CreateProjectRequest provides options when creating a project. diff --git a/coderd/organizations_test.go b/coderd/organizations_test.go index 4347a15f299a4..b37ac9419d14c 100644 --- a/coderd/organizations_test.go +++ b/coderd/organizations_test.go @@ -81,7 +81,7 @@ func TestPostProjectVersionsByOrganization(t *testing.T) { StorageMethod: database.ProvisionerStorageMethodFile, StorageSource: file.Hash, Provisioner: database.ProvisionerTypeEcho, - ParameterValues: []coderd.CreateParameterValueRequest{{ + ParameterValues: []coderd.CreateParameterRequest{{ Name: "example", SourceValue: "value", SourceScheme: database.ParameterSourceSchemeData, diff --git a/coderd/parameters.go b/coderd/parameters.go new file mode 100644 index 0000000000000..1e74bd56167f6 --- /dev/null +++ b/coderd/parameters.go @@ -0,0 +1,190 @@ +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, id, valid := readScopeAndID(rw, r) + if !valid { + return + } + parameterValue, err := api.Database.GetParameterValueByScopeAndName(r.Context(), database.GetParameterValueByScopeAndNameParams{ + Scope: scope, + ScopeID: id, + 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: id, + 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, id, valid := readScopeAndID(rw, r) + if !valid { + return + } + parameterValues, err := api.Database.GetParameterValuesByScope(r.Context(), database.GetParameterValuesByScopeParams{ + Scope: scope, + ScopeID: id, + }) + 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, id, valid := readScopeAndID(rw, r) + if !valid { + return + } + name := chi.URLParam(r, "name") + parameterValue, err := api.Database.GetParameterValueByScopeAndName(r.Context(), database.GetParameterValueByScopeAndNameParams{ + Scope: scope, + ScopeID: id, + 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) (scope database.ParameterScope, id string, valid bool) { + 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 + } + + id = chi.URLParam(r, "id") + valid = true + return +} diff --git a/coderd/parameters_test.go b/coderd/parameters_test.go new file mode 100644 index 0000000000000..c8a1182bbfeb2 --- /dev/null +++ b/coderd/parameters_test.go @@ -0,0 +1,120 @@ +package coderd_test + +import ( + "context" + "net/http" + "testing" + + "github.com/coder/coder/coderd" + "github.com/coder/coder/coderd/coderdtest" + "github.com/coder/coder/codersdk" + "github.com/coder/coder/database" + "github.com/stretchr/testify/require" +) + +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/projects.go b/coderd/projects.go index 30cccefa9ccf1..9369ed1fec203 100644 --- a/coderd/projects.go +++ b/coderd/projects.go @@ -16,17 +16,6 @@ import ( "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. @@ -63,63 +52,6 @@ func (api *api) project(rw http.ResponseWriter, r *http.Request) { render.JSON(rw, r, convertProject(project, count)) } -// Creates parameters for a project. -// This should validate the calling user has permissions! -func (api *api) postParametersByProject(rw http.ResponseWriter, r *http.Request) { - project := httpmw.ProjectParam(r) - var createRequest CreateParameterValueRequest - if !httpapi.Read(rw, r, &createRequest) { - 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), - }) - 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{} - } - if err != nil { - httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ - Message: fmt.Sprintf("get parameter values: %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) -} - func (api *api) projectVersionsByProject(rw http.ResponseWriter, r *http.Request) { project := httpmw.ProjectParam(r) @@ -229,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 6d4f2b99bb211..7d82dfbf4b1f4 100644 --- a/coderd/projects_test.go +++ b/coderd/projects_test.go @@ -7,10 +7,8 @@ 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 TestProject(t *testing.T) { @@ -27,59 +25,6 @@ func TestProject(t *testing.T) { }) } -func TestPostParametersByProject(t *testing.T) { - t.Parallel() - 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) - _, err := client.CreateProjectParameter(context.Background(), project.ID, coderd.CreateParameterValueRequest{ - Name: "somename", - SourceValue: "tomato", - SourceScheme: database.ParameterSourceSchemeData, - DestinationScheme: database.ParameterDestinationSchemeEnvironmentVariable, - }) - require.NoError(t, err) - }) -} - -func TestParametersByProject(t *testing.T) { - t.Parallel() - t.Run("ListEmpty", func(t *testing.T) { - t.Parallel() - client := coderdtest.New(t, nil) - coderdtest.NewProvisionerDaemon(t, client) - user := coderdtest.CreateFirstUser(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) - params, err := client.ProjectParameters(context.Background(), project.ID) - require.NoError(t, err) - require.NotNil(t, params) - }) - - 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) - project := coderdtest.CreateProject(t, client, user.OrganizationID, version.ID) - _, err := client.CreateProjectParameter(context.Background(), project.ID, coderd.CreateParameterValueRequest{ - Name: "example", - SourceValue: "source-value", - SourceScheme: database.ParameterSourceSchemeData, - DestinationScheme: database.ParameterDestinationSchemeEnvironmentVariable, - }) - require.NoError(t, err) - params, err := client.ProjectParameters(context.Background(), project.ID) - require.NoError(t, err) - require.NotNil(t, params) - require.Len(t, params, 1) - }) -} - func TestProjectVersionsByProject(t *testing.T) { t.Parallel() t.Run("Get", func(t *testing.T) { 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/projects.go b/codersdk/projects.go index f3f4ce9072269..7d2116865e9f4 100644 --- a/codersdk/projects.go +++ b/codersdk/projects.go @@ -25,48 +25,6 @@ func (c *Client) Project(ctx context.Context, project uuid.UUID) (coderd.Project return resp, json.NewDecoder(res.Body).Decode(&resp) } -// WorkspacesByProject lists all workspaces for a specific project. -func (c *Client) WorkspacesByProject(ctx context.Context, project uuid.UUID) ([]coderd.Workspace, error) { - res, err := c.request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/projects/%s/workspaces", 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) -} - -// ProjectParameters returns parameters scoped to a project. -func (c *Client) ProjectParameters(ctx context.Context, project uuid.UUID) ([]coderd.ParameterValue, error) { - res, err := c.request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/projects/%s/parameters", 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, project uuid.UUID, req coderd.CreateParameterValueRequest) (coderd.ParameterValue, error) { - res, err := c.request(ctx, http.MethodPost, fmt.Sprintf("/api/v2/projects/%s/parameters", 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) -} - // 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) diff --git a/database/databasefake/databasefake.go b/database/databasefake/databasefake.go index f709f6920d4b3..7e206eda4af73 100644 --- a/database/databasefake/databasefake.go +++ b/database/databasefake/databasefake.go @@ -92,6 +92,21 @@ func (q *fakeQuerier) AcquireProvisionerJob(_ context.Context, arg database.Acqu return database.ProvisionerJob{}, sql.ErrNoRows } +func (q *fakeQuerier) DeleteParameterValueByID(ctx 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() @@ -455,6 +470,25 @@ func (q *fakeQuerier) GetParameterSchemasByJobID(_ context.Context, jobID uuid.U return parameters, nil } +func (q *fakeQuerier) GetParameterValueByScopeAndName(ctx 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, id string) ([]database.Project, error) { q.mutex.Lock() defer q.mutex.Unlock() diff --git a/database/dump.sql b/database/dump.sql index ec19ea0eef954..479b878b30a6a 100644 --- a/database/dump.sql +++ b/database/dump.sql @@ -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 @@ -292,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); diff --git a/database/migrations/000004_jobs.up.sql b/database/migrations/000004_jobs.up.sql index ef56b80b30dc3..8e4ebda49119e 100644 --- a/database/migrations/000004_jobs.up.sql +++ b/database/migrations/000004_jobs.up.sql @@ -132,16 +132,16 @@ 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 ( diff --git a/database/models.go b/database/models.go index ae2544d3646bc..7a40b3649091b 100644 --- a/database/models.go +++ b/database/models.go @@ -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"` diff --git a/database/querier.go b/database/querier.go index b7f71c4a6fabb..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,6 +18,7 @@ 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) diff --git a/database/query.sql b/database/query.sql index a3fa1a20d495a..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 * diff --git a/database/query.sql.go b/database/query.sql.go index c7f71dd337173..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, @@ -1628,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 { @@ -1658,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, From 3d0f7ad30b0496548247956ec0db8532ee36c36c Mon Sep 17 00:00:00 2001 From: Kyle Carberry Date: Mon, 7 Mar 2022 00:47:52 +0000 Subject: [PATCH 14/22] Fix linting errors --- coderd/parameters.go | 27 +++++++++++++-------------- coderd/parameters_test.go | 3 ++- database/databasefake/databasefake.go | 8 ++++---- 3 files changed, 19 insertions(+), 19 deletions(-) diff --git a/coderd/parameters.go b/coderd/parameters.go index 1e74bd56167f6..5b0d8e7efde9b 100644 --- a/coderd/parameters.go +++ b/coderd/parameters.go @@ -49,13 +49,13 @@ func (api *api) postParameter(rw http.ResponseWriter, r *http.Request) { if !httpapi.Read(rw, r, &createRequest) { return } - scope, id, valid := readScopeAndID(rw, r) + scope, scopeID, valid := readScopeAndID(rw, r) if !valid { return } - parameterValue, err := api.Database.GetParameterValueByScopeAndName(r.Context(), database.GetParameterValueByScopeAndNameParams{ + _, err := api.Database.GetParameterValueByScopeAndName(r.Context(), database.GetParameterValueByScopeAndNameParams{ Scope: scope, - ScopeID: id, + ScopeID: scopeID, Name: createRequest.Name, }) if err == nil { @@ -70,13 +70,13 @@ func (api *api) postParameter(rw http.ResponseWriter, r *http.Request) { }) return } - parameterValue, err = api.Database.InsertParameterValue(r.Context(), database.InsertParameterValueParams{ + parameterValue, err := api.Database.InsertParameterValue(r.Context(), database.InsertParameterValueParams{ ID: uuid.New(), Name: createRequest.Name, CreatedAt: database.Now(), UpdatedAt: database.Now(), Scope: scope, - ScopeID: id, + ScopeID: scopeID, SourceScheme: createRequest.SourceScheme, SourceValue: createRequest.SourceValue, DestinationScheme: createRequest.DestinationScheme, @@ -93,13 +93,13 @@ func (api *api) postParameter(rw http.ResponseWriter, r *http.Request) { } func (api *api) parameters(rw http.ResponseWriter, r *http.Request) { - scope, id, valid := readScopeAndID(rw, r) + scope, scopeID, valid := readScopeAndID(rw, r) if !valid { return } parameterValues, err := api.Database.GetParameterValuesByScope(r.Context(), database.GetParameterValuesByScopeParams{ Scope: scope, - ScopeID: id, + ScopeID: scopeID, }) if errors.Is(err, sql.ErrNoRows) { err = nil @@ -120,14 +120,14 @@ func (api *api) parameters(rw http.ResponseWriter, r *http.Request) { } func (api *api) deleteParameter(rw http.ResponseWriter, r *http.Request) { - scope, id, valid := readScopeAndID(rw, r) + 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: id, + ScopeID: scopeID, Name: name, }) if errors.Is(err, sql.ErrNoRows) { @@ -167,7 +167,8 @@ func convertParameterValue(parameterValue database.ParameterValue) Parameter { } } -func readScopeAndID(rw http.ResponseWriter, r *http.Request) (scope database.ParameterScope, id string, valid bool) { +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 @@ -181,10 +182,8 @@ func readScopeAndID(rw http.ResponseWriter, r *http.Request) (scope database.Par httpapi.Write(rw, http.StatusBadRequest, httpapi.Response{ Message: fmt.Sprintf("invalid scope %q", scope), }) - return + return scope, "", false } - id = chi.URLParam(r, "id") - valid = true - return + return scope, chi.URLParam(r, "id"), true } diff --git a/coderd/parameters_test.go b/coderd/parameters_test.go index c8a1182bbfeb2..06fb5757739bc 100644 --- a/coderd/parameters_test.go +++ b/coderd/parameters_test.go @@ -5,11 +5,12 @@ import ( "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/stretchr/testify/require" ) func TestPostParameter(t *testing.T) { diff --git a/database/databasefake/databasefake.go b/database/databasefake/databasefake.go index 7e206eda4af73..24fd970a3a033 100644 --- a/database/databasefake/databasefake.go +++ b/database/databasefake/databasefake.go @@ -92,7 +92,7 @@ func (q *fakeQuerier) AcquireProvisionerJob(_ context.Context, arg database.Acqu return database.ProvisionerJob{}, sql.ErrNoRows } -func (q *fakeQuerier) DeleteParameterValueByID(ctx context.Context, id uuid.UUID) error { +func (q *fakeQuerier) DeleteParameterValueByID(_ context.Context, id uuid.UUID) error { q.mutex.Lock() defer q.mutex.Unlock() @@ -470,7 +470,7 @@ func (q *fakeQuerier) GetParameterSchemasByJobID(_ context.Context, jobID uuid.U return parameters, nil } -func (q *fakeQuerier) GetParameterValueByScopeAndName(ctx context.Context, arg database.GetParameterValueByScopeAndNameParams) (database.ParameterValue, error) { +func (q *fakeQuerier) GetParameterValueByScopeAndName(_ context.Context, arg database.GetParameterValueByScopeAndNameParams) (database.ParameterValue, error) { q.mutex.Lock() defer q.mutex.Unlock() @@ -489,13 +489,13 @@ func (q *fakeQuerier) GetParameterValueByScopeAndName(ctx context.Context, arg d return database.ParameterValue{}, sql.ErrNoRows } -func (q *fakeQuerier) GetProjectsByOrganization(_ context.Context, id string) ([]database.Project, error) { +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 { - if project.OrganizationID == id { + if project.OrganizationID == organizationID { projects = append(projects, project) break } From abd4b50061ee60a6089af85e7e8dc12be1594950 Mon Sep 17 00:00:00 2001 From: Kyle Carberry Date: Mon, 7 Mar 2022 00:52:32 +0000 Subject: [PATCH 15/22] Fix log race --- peer/conn.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 } From 83eb9cae103eac9dbae44a7ad9c4454b48091f41 Mon Sep 17 00:00:00 2001 From: Kyle Carberry Date: Mon, 7 Mar 2022 01:00:05 +0000 Subject: [PATCH 16/22] Lock on close wait --- agent/agent.go | 2 ++ peerbroker/proxy.go | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/agent/agent.go b/agent/agent.go index 253613a66a3cd..a121b2150947f 100644 --- a/agent/agent.go +++ b/agent/agent.go @@ -280,7 +280,9 @@ 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) } } diff --git a/peerbroker/proxy.go b/peerbroker/proxy.go index 84a75f7e874a4..e732eb295c2c8 100644 --- a/peerbroker/proxy.go +++ b/peerbroker/proxy.go @@ -200,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)) } From ebf2f77f5481d52fbae416f006c5e3ce30f81c1f Mon Sep 17 00:00:00 2001 From: Kyle Carberry Date: Mon, 7 Mar 2022 01:17:23 +0000 Subject: [PATCH 17/22] Fix log cleanup --- coderd/projectversions_test.go | 4 +++- coderd/provisionerjobs_test.go | 8 ++++++-- coderd/workspacebuilds_test.go | 4 +++- site/api.ts | 4 ++-- 4 files changed, 14 insertions(+), 6 deletions(-) diff --git a/coderd/projectversions_test.go b/coderd/projectversions_test.go index 8cc934e7b7bbc..b6efc2ad687a6 100644 --- a/coderd/projectversions_test.go +++ b/coderd/projectversions_test.go @@ -196,7 +196,9 @@ func TestProjectVersionLogs(t *testing.T) { }, }}, }) - logs, err := client.ProjectVersionLogsAfter(context.Background(), version.ID, before) + 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/provisionerjobs_test.go b/coderd/provisionerjobs_test.go index e790a317bcf2c..7dacaaef57a9b 100644 --- a/coderd/provisionerjobs_test.go +++ b/coderd/provisionerjobs_test.go @@ -47,7 +47,9 @@ func TestProvisionerJobLogs(t *testing.T) { require.NoError(t, err) coderdtest.AwaitWorkspaceBuildJob(t, client, build.ID) - logs, err := client.WorkspaceBuildLogsAfter(context.Background(), build.ID, 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) @@ -86,7 +88,9 @@ func TestProvisionerJobLogs(t *testing.T) { Transition: database.WorkspaceTransitionStart, }) require.NoError(t, err) - logs, err := client.WorkspaceBuildLogsAfter(context.Background(), build.ID, before) + 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, "log-output", log.Output) diff --git a/coderd/workspacebuilds_test.go b/coderd/workspacebuilds_test.go index 16743be33af35..3dc62333670fb 100644 --- a/coderd/workspacebuilds_test.go +++ b/coderd/workspacebuilds_test.go @@ -141,7 +141,9 @@ func TestWorkspaceBuildLogs(t *testing.T) { Transition: database.WorkspaceTransitionStart, }) require.NoError(t, err) - logs, err := client.WorkspaceBuildLogsAfter(context.Background(), build.ID, before) + 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/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", }) From 74e8f43b44c1683295b3e3e663380996b3dcf93d Mon Sep 17 00:00:00 2001 From: Kyle Carberry Date: Mon, 7 Mar 2022 01:33:11 +0000 Subject: [PATCH 18/22] Fix e2e tests --- .github/workflows/coder.yaml | 2 ++ coderd/provisionerjobs_test.go | 34 ++++++++++++++++++++++++++++++++++ develop.sh | 2 +- images/coder/run.sh | 2 +- site/e2e/globalSetup.ts | 2 +- site/pages/projects/index.tsx | 4 +++- 6 files changed, 42 insertions(+), 4 deletions(-) diff --git a/.github/workflows/coder.yaml b/.github/workflows/coder.yaml index fad4245c3c838..8be5dc53c4a30 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/coderd/provisionerjobs_test.go b/coderd/provisionerjobs_test.go index 7dacaaef57a9b..828062bcf7014 100644 --- a/coderd/provisionerjobs_test.go +++ b/coderd/provisionerjobs_test.go @@ -97,4 +97,38 @@ func TestProvisionerJobLogs(t *testing.T) { _, ok := <-logs require.False(t, ok) }) + + 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_Log{ + Log: &proto.Log{ + Level: proto.LogLevel_INFO, + Output: "log-output", + }, + }, + }, { + Type: &proto.Provision_Response_Complete{ + Complete: &proto.Provision_Complete{}, + }, + }}, + }) + 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) + logs, err := client.WorkspaceBuildLogsBefore(context.Background(), build.ID, time.Now()) + require.NoError(t, err) + require.Len(t, logs, 1) + }) } 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/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/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 From a9a19b792ed3d08558e5c53311faeb961844eedb Mon Sep 17 00:00:00 2001 From: Kyle Carberry Date: Mon, 7 Mar 2022 01:51:48 +0000 Subject: [PATCH 19/22] Fix upstream version of opencensus-go --- go.mod | 7 ++----- go.sum | 8 ++++---- 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/go.mod b/go.mod index bc9967b17f141..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 diff --git a/go.sum b/go.sum index 302226a3850e2..586f91508d8e2 100644 --- a/go.sum +++ b/go.sum @@ -893,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= @@ -1098,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= From 9597b0c79aafd1cffa7e5572a3f375379833aa1d Mon Sep 17 00:00:00 2001 From: Kyle Carberry Date: Sun, 6 Mar 2022 20:17:47 -0600 Subject: [PATCH 20/22] Update coderdtest.go --- coderd/coderdtest/coderdtest.go | 8 -------- 1 file changed, 8 deletions(-) diff --git a/coderd/coderdtest/coderdtest.go b/coderd/coderdtest/coderdtest.go index 8dd936e1c8154..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{} } From 878738ceb42fc5cd066ac72f5ea1350ca05fc6d6 Mon Sep 17 00:00:00 2001 From: Kyle Carberry Date: Mon, 7 Mar 2022 02:34:15 +0000 Subject: [PATCH 21/22] Fix coverpkg --- .github/workflows/coder.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/coder.yaml b/.github/workflows/coder.yaml index 8be5dc53c4a30..72cf026d09212 100644 --- a/.github/workflows/coder.yaml +++ b/.github/workflows/coder.yaml @@ -157,7 +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 + -coverpkg=./...,github.com/coder/coder/codersdk -timeout=3m -count=$GOCOUNT -race -short -failfast - name: Upload DataDog Trace @@ -173,7 +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 + -coverpkg=./...,github.com/coder/coder/codersdk -count=1 -race -parallel=2 -failfast - name: Upload DataDog Trace From 429391413b7834ff63de2ddff2f0d8d3cd0677f5 Mon Sep 17 00:00:00 2001 From: Kyle Carberry Date: Mon, 7 Mar 2022 02:49:11 +0000 Subject: [PATCH 22/22] Fix codecov ignore --- codecov.yml | 6 ++++- httpmw/projectparam_test.go | 17 ++++++++++++++ httpmw/workspaceparam.go | 4 ++-- httpmw/workspaceparam_test.go | 25 +++++++++++++++++++++ httpmw/workspaceresourceparam_test.go | 32 ++++++++++++++++++++++----- 5 files changed, 76 insertions(+), 8 deletions(-) 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/httpmw/projectparam_test.go b/httpmw/projectparam_test.go index 84985f632a873..c8ea119a25125 100644 --- a/httpmw/projectparam_test.go +++ b/httpmw/projectparam_test.go @@ -110,6 +110,23 @@ 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() diff --git a/httpmw/workspaceparam.go b/httpmw/workspaceparam.go index 8eba7e8e16ce0..4c1f852d97015 100644 --- a/httpmw/workspaceparam.go +++ b/httpmw/workspaceparam.go @@ -46,8 +46,8 @@ func ExtractWorkspaceParam(db database.Store) func(http.Handler) http.Handler { apiKey := APIKey(r) if apiKey.UserID != workspace.OwnerID { - httpapi.Write(rw, http.StatusBadRequest, httpapi.Response{ - Message: "getting non-personal workspaces isn't supported yet", + httpapi.Write(rw, http.StatusUnauthorized, httpapi.Response{ + Message: "getting non-personal workspaces isn't supported", }) return } diff --git a/httpmw/workspaceparam_test.go b/httpmw/workspaceparam_test.go index e394270c58e9a..e68cc80920947 100644 --- a/httpmw/workspaceparam_test.go +++ b/httpmw/workspaceparam_test.go @@ -93,6 +93,31 @@ func TestWorkspaceParam(t *testing.T) { 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.ExtractWorkspaceParam(db), + ) + rtr.Get("/", nil) + r, _ := setup(db) + 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.StatusUnauthorized, res.StatusCode) + }) + t.Run("Found", func(t *testing.T) { t.Parallel() db := databasefake.New() diff --git a/httpmw/workspaceresourceparam_test.go b/httpmw/workspaceresourceparam_test.go index d746f7db98b97..8ac404f84b9f3 100644 --- a/httpmw/workspaceresourceparam_test.go +++ b/httpmw/workspaceresourceparam_test.go @@ -18,11 +18,11 @@ import ( func TestWorkspaceResourceParam(t *testing.T) { t.Parallel() - setup := func(db database.Store) (*http.Request, database.WorkspaceResource) { + 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: database.ProvisionerJobTypeWorkspaceBuild, + Type: jobType, }) require.NoError(t, err) workspaceBuild, err := db.InsertWorkspaceBuild(context.Background(), database.InsertWorkspaceBuildParams{ @@ -48,7 +48,7 @@ func TestWorkspaceResourceParam(t *testing.T) { rtr := chi.NewRouter() rtr.Use(httpmw.ExtractWorkspaceResourceParam(db)) rtr.Get("/", nil) - r, _ := setup(db) + r, _ := setup(db, database.ProvisionerJobTypeWorkspaceBuild) rw := httptest.NewRecorder() rtr.ServeHTTP(rw, r) @@ -66,7 +66,7 @@ func TestWorkspaceResourceParam(t *testing.T) { ) rtr.Get("/", nil) - r, _ := setup(db) + r, _ := setup(db, database.ProvisionerJobTypeWorkspaceBuild) chi.RouteContext(r.Context()).URLParams.Add("workspaceresource", uuid.NewString()) rw := httptest.NewRecorder() rtr.ServeHTTP(rw, r) @@ -76,6 +76,28 @@ func TestWorkspaceResourceParam(t *testing.T) { 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() @@ -88,7 +110,7 @@ func TestWorkspaceResourceParam(t *testing.T) { rw.WriteHeader(http.StatusOK) }) - r, job := setup(db) + r, job := setup(db, database.ProvisionerJobTypeWorkspaceBuild) chi.RouteContext(r.Context()).URLParams.Add("workspaceresource", job.ID.String()) rw := httptest.NewRecorder() rtr.ServeHTTP(rw, r)