fix: remove string TTL from workspace error responses (#3257)

- Rewrites some error messages to better integrate with the frontend (ttl_ms -> time until shutdown)
- Makes codersdk.ValidationError implement the error interface
- Only return validations if the error was a validation error, return detail otherwise (e.g. database error)
This commit is contained in:
Cian Johnston 2022-07-27 22:20:02 +01:00 committed by GitHub
parent 36ffdce065
commit 27ea415b6c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 34 additions and 27 deletions

View File

@ -288,7 +288,7 @@ func (api *API) postWorkspacesByOrganization(rw http.ResponseWriter, r *http.Req
dbTTL, err := validWorkspaceTTLMillis(createWorkspace.TTLMillis, time.Duration(template.MaxTtl))
if err != nil {
httpapi.Write(rw, http.StatusBadRequest, codersdk.Response{
Message: "Invalid Workspace TTL.",
Message: "Invalid Workspace Time to Shutdown.",
Validations: []codersdk.ValidationError{{Field: "ttl_ms", Detail: err.Error()}},
})
return
@ -523,8 +523,6 @@ func (api *API) putWorkspaceTTL(rw http.ResponseWriter, r *http.Request) {
return
}
var validErrs []codersdk.ValidationError
err := api.Database.InTx(func(s database.Store) error {
template, err := s.GetTemplateByID(r.Context(), workspace.TemplateID)
if err != nil {
@ -536,29 +534,31 @@ func (api *API) putWorkspaceTTL(rw http.ResponseWriter, r *http.Request) {
dbTTL, err := validWorkspaceTTLMillis(req.TTLMillis, time.Duration(template.MaxTtl))
if err != nil {
validErrs = append(validErrs, codersdk.ValidationError{Field: "ttl_ms", Detail: err.Error()})
return err
return codersdk.ValidationError{Field: "ttl_ms", Detail: err.Error()}
}
if err := s.UpdateWorkspaceTTL(r.Context(), database.UpdateWorkspaceTTLParams{
ID: workspace.ID,
Ttl: dbTTL,
}); err != nil {
return xerrors.Errorf("update workspace TTL: %w", err)
return xerrors.Errorf("update workspace time until shutdown: %w", err)
}
return nil
})
if err != nil {
code := http.StatusInternalServerError
if len(validErrs) > 0 {
code = http.StatusBadRequest
resp := codersdk.Response{
Message: "Error updating workspace time until shutdown.",
}
httpapi.Write(rw, code, codersdk.Response{
Message: "Error updating workspace time until shutdown!",
Validations: validErrs,
Detail: err.Error(),
})
var validErr codersdk.ValidationError
if errors.As(err, &validErr) {
resp.Validations = []codersdk.ValidationError{validErr}
httpapi.Write(rw, http.StatusBadRequest, resp)
return
}
resp.Detail = err.Error()
httpapi.Write(rw, http.StatusInternalServerError, resp)
return
}
@ -895,15 +895,15 @@ func validWorkspaceTTLMillis(millis *int64, max time.Duration) (sql.NullInt64, e
dur := time.Duration(*millis) * time.Millisecond
truncated := dur.Truncate(time.Minute)
if truncated < time.Minute {
return sql.NullInt64{}, xerrors.New("ttl must be at least one minute")
return sql.NullInt64{}, xerrors.New("time until shutdown must be at least one minute")
}
if truncated > 24*7*time.Hour {
return sql.NullInt64{}, xerrors.New("ttl must be less than 7 days")
return sql.NullInt64{}, xerrors.New("time until shutdown must be less than 7 days")
}
if truncated > max {
return sql.NullInt64{}, xerrors.Errorf("ttl must be below template maximum %s", max.String())
return sql.NullInt64{}, xerrors.Errorf("time until shutdown must be below template maximum %s", max.String())
}
return sql.NullInt64{

View File

@ -207,7 +207,7 @@ func TestPostWorkspacesByOrganization(t *testing.T) {
require.Equal(t, http.StatusBadRequest, apiErr.StatusCode())
require.Len(t, apiErr.Validations, 1)
require.Equal(t, apiErr.Validations[0].Field, "ttl_ms")
require.Equal(t, apiErr.Validations[0].Detail, "ttl must be at least one minute")
require.Equal(t, "time until shutdown must be at least one minute", apiErr.Validations[0].Detail)
})
t.Run("AboveMax", func(t *testing.T) {
@ -220,7 +220,7 @@ func TestPostWorkspacesByOrganization(t *testing.T) {
req := codersdk.CreateWorkspaceRequest{
TemplateID: template.ID,
Name: "testing",
TTLMillis: ptr.Ref((24*7*time.Hour + time.Minute).Milliseconds()),
TTLMillis: ptr.Ref(template.MaxTTLMillis + time.Minute.Milliseconds()),
}
_, err := client.CreateWorkspace(context.Background(), template.OrganizationID, req)
require.Error(t, err)
@ -229,7 +229,7 @@ func TestPostWorkspacesByOrganization(t *testing.T) {
require.Equal(t, http.StatusBadRequest, apiErr.StatusCode())
require.Len(t, apiErr.Validations, 1)
require.Equal(t, apiErr.Validations[0].Field, "ttl_ms")
require.Equal(t, apiErr.Validations[0].Detail, "ttl must be less than 7 days")
require.Equal(t, "time until shutdown must be less than 7 days", apiErr.Validations[0].Detail)
})
})
@ -934,7 +934,7 @@ func TestWorkspaceUpdateTTL(t *testing.T) {
{
name: "below minimum ttl",
ttlMillis: ptr.Ref((30 * time.Second).Milliseconds()),
expectedError: "ttl must be at least one minute",
expectedError: "time until shutdown must be at least one minute",
},
{
name: "minimum ttl",
@ -949,12 +949,12 @@ func TestWorkspaceUpdateTTL(t *testing.T) {
{
name: "above maximum ttl",
ttlMillis: ptr.Ref((24*7*time.Hour + time.Minute).Milliseconds()),
expectedError: "ttl must be less than 7 days",
expectedError: "time until shutdown must be less than 7 days",
},
{
name: "above template maximum ttl",
ttlMillis: ptr.Ref((12 * time.Hour).Milliseconds()),
expectedError: "ttl_ms: ttl must be below template maximum 8h0m0s",
expectedError: "ttl_ms: time until shutdown must be below template maximum 8h0m0s",
modifyTemplate: func(ctr *codersdk.CreateTemplateRequest) { ctr.MaxTTLMillis = ptr.Ref((8 * time.Hour).Milliseconds()) },
},
}

View File

@ -1,6 +1,7 @@
package codersdk
import (
"fmt"
"net"
"golang.org/x/xerrors"
@ -32,6 +33,12 @@ type ValidationError struct {
Detail string `json:"detail" validate:"required"`
}
func (e ValidationError) Error() string {
return fmt.Sprintf("field: %s detail: %s", e.Field, e.Detail)
}
var _ error = (*ValidationError)(nil)
// IsConnectionErr is a convenience function for checking if the source of an
// error is due to a 'connection refused', 'no such host', etc.
func IsConnectionErr(err error) bool {

View File

@ -192,7 +192,7 @@ func (c *Client) UpdateWorkspaceTTL(ctx context.Context, id uuid.UUID, req Updat
path := fmt.Sprintf("/api/v2/workspaces/%s/ttl", id.String())
res, err := c.Request(ctx, http.MethodPut, path, req)
if err != nil {
return xerrors.Errorf("update workspace ttl: %w", err)
return xerrors.Errorf("update workspace time until shutdown: %w", err)
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
@ -212,7 +212,7 @@ func (c *Client) PutExtendWorkspace(ctx context.Context, id uuid.UUID, req PutEx
path := fmt.Sprintf("/api/v2/workspaces/%s/extend", id.String())
res, err := c.Request(ctx, http.MethodPut, path, req)
if err != nil {
return xerrors.Errorf("extend workspace ttl: %w", err)
return xerrors.Errorf("extend workspace time until shutdown: %w", err)
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK && res.StatusCode != http.StatusNotModified {

View File

@ -250,7 +250,7 @@ export interface PutExtendWorkspaceRequest {
readonly deadline: string
}
// From codersdk/error.go:10:6
// From codersdk/error.go:11:6
export interface Response {
readonly message: string
readonly detail?: string
@ -386,7 +386,7 @@ export interface UsersRequest extends Pagination {
readonly q?: string
}
// From codersdk/error.go:30:6
// From codersdk/error.go:31:6
export interface ValidationError {
readonly field: string
readonly detail: string