From c3bae6704b29189a8f42c8d74ba147c13358cc1b Mon Sep 17 00:00:00 2001 From: Kyle Carberry Date: Fri, 4 Feb 2022 12:11:24 -0600 Subject: [PATCH] feat: Add parameter querying to the API (#143) --- .vscode/settings.json | 1 + coderd/coderd.go | 4 ++ coderd/parameters.go | 107 ++++++++++++++++++++++++++++++++++++++ coderd/projects.go | 18 +++++++ coderd/projects_test.go | 32 ++++++++++++ codersdk/projects.go | 28 ++++++++++ codersdk/projects_test.go | 35 +++++++++++++ 7 files changed, 225 insertions(+) create mode 100644 coderd/parameters.go diff --git a/.vscode/settings.json b/.vscode/settings.json index 3f149bf5d5..3b78316937 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -40,6 +40,7 @@ "nolint", "nosec", "oneof", + "parameterscopeid", "protobuf", "provisionerd", "provisionersdk", diff --git a/coderd/coderd.go b/coderd/coderd.go index 2a0f255523..a3e1c28d3a 100644 --- a/coderd/coderd.go +++ b/coderd/coderd.go @@ -62,6 +62,10 @@ func New(options *Options) http.Handler { 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("/history", func(r chi.Router) { r.Get("/", api.projectHistoryByOrganization) r.Post("/", api.postProjectHistoryByOrganization) diff --git a/coderd/parameters.go b/coderd/parameters.go new file mode 100644 index 0000000000..bc0ed4e665 --- /dev/null +++ b/coderd/parameters.go @@ -0,0 +1,107 @@ +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" +) + +// CreateParameterValueRequest is used to create a new parameter value for a scope. +type CreateParameterValueRequest struct { + Name string `json:"name"` + SourceValue string `json:"source_value"` + SourceScheme database.ParameterSourceScheme `json:"source_scheme"` + DestinationScheme database.ParameterDestinationScheme `json:"destination_scheme"` + DestinationValue string `json:"destination_value"` +} + +// ParameterValue represents a set value for the scope. +type ParameterValue struct { + ID uuid.UUID `json:"id"` + Name string `json:"name"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + Scope database.ParameterScope `json:"scope"` + ScopeID string `json:"scope_id"` + SourceScheme database.ParameterSourceScheme `json:"source_scheme"` + DestinationScheme database.ParameterDestinationScheme `json:"destination_scheme"` + DestinationValue string `json:"destination_value"` +} + +// Abstracts creating parameters into a single request/response format. +// Callers are in charge of validating the requester has permissions to +// perform the creation. +func postParameterValueForScope(rw http.ResponseWriter, r *http.Request, db database.Store, scope database.ParameterScope, scopeID string) { + var createRequest CreateParameterValueRequest + if !httpapi.Read(rw, r, &createRequest) { + return + } + parameterValue, err := db.InsertParameterValue(r.Context(), database.InsertParameterValueParams{ + ID: uuid.New(), + Name: createRequest.Name, + CreatedAt: database.Now(), + UpdatedAt: database.Now(), + Scope: scope, + ScopeID: scopeID, + SourceScheme: createRequest.SourceScheme, + SourceValue: createRequest.SourceValue, + DestinationScheme: createRequest.DestinationScheme, + DestinationValue: createRequest.DestinationValue, + }) + 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) +} + +// Abstracts returning parameters for a scope into a standardized +// request/response format. Callers are responsible for checking +// requester permissions. +func parametersForScope(rw http.ResponseWriter, r *http.Request, db database.Store, req database.GetParameterValuesByScopeParams) { + parameterValues, err := db.GetParameterValuesByScope(r.Context(), req) + 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 convertParameterValue(parameterValue database.ParameterValue) ParameterValue { + return ParameterValue{ + ID: parameterValue.ID, + Name: parameterValue.Name, + CreatedAt: parameterValue.CreatedAt, + UpdatedAt: parameterValue.UpdatedAt, + Scope: parameterValue.Scope, + ScopeID: parameterValue.ScopeID, + SourceScheme: parameterValue.SourceScheme, + DestinationScheme: parameterValue.DestinationScheme, + DestinationValue: parameterValue.DestinationValue, + } +} diff --git a/coderd/projects.go b/coderd/projects.go index b7e161adcd..e0ed677fa6 100644 --- a/coderd/projects.go +++ b/coderd/projects.go @@ -149,3 +149,21 @@ func (api *api) workspacesByProject(rw http.ResponseWriter, r *http.Request) { render.Status(r, http.StatusOK) render.JSON(rw, r, apiWorkspaces) } + +// Creates parameters for a project. +// This should validate the calling user has permissions! +func (api *api) postParametersByProject(rw http.ResponseWriter, r *http.Request) { + project := httpmw.ProjectParam(r) + + postParameterValueForScope(rw, r, api.Database, database.ParameterScopeProject, project.ID.String()) +} + +// Lists parameters for a project. +func (api *api) parametersByProject(rw http.ResponseWriter, r *http.Request) { + project := httpmw.ProjectParam(r) + + parametersForScope(rw, r, api.Database, database.GetParameterValuesByScopeParams{ + Scope: database.ParameterScopeProject, + ScopeID: project.ID.String(), + }) +} diff --git a/coderd/projects_test.go b/coderd/projects_test.go index 98261ba105..46dc7b2058 100644 --- a/coderd/projects_test.go +++ b/coderd/projects_test.go @@ -92,4 +92,36 @@ func TestProjects(t *testing.T) { _, err = server.Client.Project(context.Background(), user.Organization, project.Name) require.NoError(t, err) }) + + t.Run("Parameters", func(t *testing.T) { + t.Parallel() + server := coderdtest.New(t) + user := server.RandomInitialUser(t) + project, err := server.Client.CreateProject(context.Background(), user.Organization, coderd.CreateProjectRequest{ + Name: "someproject", + Provisioner: database.ProvisionerTypeTerraform, + }) + require.NoError(t, err) + _, err = server.Client.ProjectParameters(context.Background(), user.Organization, project.Name) + require.NoError(t, err) + }) + + t.Run("CreateParameter", func(t *testing.T) { + t.Parallel() + server := coderdtest.New(t) + user := server.RandomInitialUser(t) + project, err := server.Client.CreateProject(context.Background(), user.Organization, coderd.CreateProjectRequest{ + Name: "someproject", + Provisioner: database.ProvisionerTypeTerraform, + }) + require.NoError(t, err) + _, err = server.Client.CreateProjectParameter(context.Background(), user.Organization, project.Name, coderd.CreateParameterValueRequest{ + Name: "hi", + SourceValue: "tomato", + SourceScheme: database.ParameterSourceSchemeData, + DestinationScheme: database.ParameterDestinationSchemeEnvironmentVariable, + DestinationValue: "moo", + }) + require.NoError(t, err) + }) } diff --git a/codersdk/projects.go b/codersdk/projects.go index a4281849c2..6a1304893b 100644 --- a/codersdk/projects.go +++ b/codersdk/projects.go @@ -98,3 +98,31 @@ func (c *Client) CreateProjectHistory(ctx context.Context, organization, project var projectVersion coderd.ProjectHistory return projectVersion, json.NewDecoder(res.Body).Decode(&projectVersion) } + +// ProjectParameters returns parameters scoped to a project. +func (c *Client) ProjectParameters(ctx context.Context, organization, project string) ([]coderd.ParameterValue, error) { + res, err := c.request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/projects/%s/%s/parameters", organization, project), nil) + if err != nil { + return nil, err + } + defer res.Body.Close() + if res.StatusCode != http.StatusOK { + return nil, readBodyAsError(res) + } + var params []coderd.ParameterValue + return params, json.NewDecoder(res.Body).Decode(¶ms) +} + +// CreateProjectParameter creates a new parameter value scoped to a project. +func (c *Client) CreateProjectParameter(ctx context.Context, organization, project string, req coderd.CreateParameterValueRequest) (coderd.ParameterValue, error) { + res, err := c.request(ctx, http.MethodPost, fmt.Sprintf("/api/v2/projects/%s/%s/parameters", organization, project), req) + if err != nil { + return coderd.ParameterValue{}, err + } + defer res.Body.Close() + if res.StatusCode != http.StatusCreated { + return coderd.ParameterValue{}, readBodyAsError(res) + } + var param coderd.ParameterValue + return param, json.NewDecoder(res.Body).Decode(¶m) +} diff --git a/codersdk/projects_test.go b/codersdk/projects_test.go index a30146b7b9..0b66b12fd1 100644 --- a/codersdk/projects_test.go +++ b/codersdk/projects_test.go @@ -128,4 +128,39 @@ func TestProjects(t *testing.T) { _, err = server.Client.ProjectHistory(context.Background(), user.Organization, project.Name, history.Name) require.NoError(t, err) }) + + t.Run("Parameters", func(t *testing.T) { + t.Parallel() + server := coderdtest.New(t) + user := server.RandomInitialUser(t) + project, err := server.Client.CreateProject(context.Background(), user.Organization, coderd.CreateProjectRequest{ + Name: "someproject", + Provisioner: database.ProvisionerTypeTerraform, + }) + require.NoError(t, err) + params, err := server.Client.ProjectParameters(context.Background(), user.Organization, project.Name) + require.NoError(t, err) + require.NotNil(t, params) + require.Len(t, params, 0) + }) + + t.Run("CreateParameter", func(t *testing.T) { + t.Parallel() + server := coderdtest.New(t) + user := server.RandomInitialUser(t) + project, err := server.Client.CreateProject(context.Background(), user.Organization, coderd.CreateProjectRequest{ + Name: "someproject", + Provisioner: database.ProvisionerTypeTerraform, + }) + require.NoError(t, err) + param, err := server.Client.CreateProjectParameter(context.Background(), user.Organization, project.Name, coderd.CreateParameterValueRequest{ + Name: "hi", + SourceValue: "tomato", + SourceScheme: database.ParameterSourceSchemeData, + DestinationScheme: database.ParameterDestinationSchemeEnvironmentVariable, + DestinationValue: "moo", + }) + require.NoError(t, err) + require.Equal(t, "hi", param.Name) + }) }