Skip to content

Commit e5f302f

Browse files
committed
merge: main into branch
2 parents 968d626 + c4a9be9 commit e5f302f

31 files changed

+1259
-81
lines changed

cli/features.go

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package cli
22

33
import (
4+
"bytes"
45
"encoding/json"
56
"fmt"
67
"strings"
@@ -53,12 +54,14 @@ func featuresList() *cobra.Command {
5354
return xerrors.Errorf("render table: %w", err)
5455
}
5556
case "json":
56-
outBytes, err := json.Marshal(entitlements)
57+
buf := new(bytes.Buffer)
58+
enc := json.NewEncoder(buf)
59+
enc.SetIndent("", " ")
60+
err = enc.Encode(entitlements)
5761
if err != nil {
5862
return xerrors.Errorf("marshal features to JSON: %w", err)
5963
}
60-
61-
out = string(outBytes)
64+
out = buf.String()
6265
default:
6366
return xerrors.Errorf(`unknown output format %q, only "table" and "json" are supported`, outputFormat)
6467
}

coderd/coderd.go

Lines changed: 7 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,7 @@
11
package coderd
22

33
import (
4-
"context"
54
"crypto/x509"
6-
"fmt"
75
"io"
86
"net/http"
97
"net/url"
@@ -68,6 +66,7 @@ type Options struct {
6866
TracerProvider *sdktrace.TracerProvider
6967
AutoImportTemplates []AutoImportTemplate
7068
LicenseHandler http.Handler
69+
FeaturesService FeaturesService
7170
}
7271

7372
// New constructs a Coder API handler.
@@ -97,6 +96,9 @@ func New(options *Options) *API {
9796
if options.LicenseHandler == nil {
9897
options.LicenseHandler = licenses()
9998
}
99+
if options.FeaturesService == nil {
100+
options.FeaturesService = featuresService{}
101+
}
100102

101103
siteCacheDir := options.CacheDir
102104
if siteCacheDir != "" {
@@ -125,11 +127,8 @@ func New(options *Options) *API {
125127
apiKeyMiddleware := httpmw.ExtractAPIKey(options.Database, oauthConfigs, false)
126128

127129
r.Use(
128-
func(next http.Handler) http.Handler {
129-
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
130-
next.ServeHTTP(middleware.NewWrapResponseWriter(w, r.ProtoMajor), r)
131-
})
132-
},
130+
httpmw.Recover(api.Logger),
131+
httpmw.Logger(api.Logger),
133132
httpmw.Prometheus(options.PrometheusRegistry),
134133
)
135134

@@ -159,7 +158,6 @@ func New(options *Options) *API {
159158
r.Use(
160159
// Specific routes can specify smaller limits.
161160
httpmw.RateLimitPerMinute(options.APIRateLimit),
162-
debugLogRequest(api.Logger),
163161
tracing.HTTPMW(api.TracerProvider, "coderd.http"),
164162
)
165163
r.Get("/", func(w http.ResponseWriter, r *http.Request) {
@@ -406,7 +404,7 @@ func New(options *Options) *API {
406404
})
407405
r.Route("/entitlements", func(r chi.Router) {
408406
r.Use(apiKeyMiddleware)
409-
r.Get("/", entitlements)
407+
r.Get("/", api.FeaturesService.EntitlementsAPI)
410408
})
411409
r.Route("/licenses", func(r chi.Router) {
412410
r.Use(apiKeyMiddleware)
@@ -438,15 +436,6 @@ func (api *API) Close() error {
438436
return api.workspaceAgentCache.Close()
439437
}
440438

441-
func debugLogRequest(log slog.Logger) func(http.Handler) http.Handler {
442-
return func(next http.Handler) http.Handler {
443-
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
444-
log.Debug(context.Background(), fmt.Sprintf("%s %s", r.Method, r.URL.Path))
445-
next.ServeHTTP(rw, r)
446-
})
447-
}
448-
}
449-
450439
func compressHandler(h http.Handler) http.Handler {
451440
cmp := middleware.NewCompressor(5,
452441
"text/*",

coderd/database/databasefake/databasefake.go

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -246,6 +246,19 @@ func (q *fakeQuerier) GetUserCount(_ context.Context) (int64, error) {
246246
return int64(len(q.users)), nil
247247
}
248248

249+
func (q *fakeQuerier) GetActiveUserCount(_ context.Context) (int64, error) {
250+
q.mutex.RLock()
251+
defer q.mutex.RUnlock()
252+
253+
active := int64(0)
254+
for _, u := range q.users {
255+
if u.Status == database.UserStatusActive {
256+
active++
257+
}
258+
}
259+
return active, nil
260+
}
261+
249262
func (q *fakeQuerier) GetUsers(_ context.Context, params database.GetUsersParams) ([]database.User, error) {
250263
q.mutex.RLock()
251264
defer q.mutex.RUnlock()
@@ -2322,6 +2335,21 @@ func (q *fakeQuerier) GetLicenses(_ context.Context) ([]database.License, error)
23222335
return results, nil
23232336
}
23242337

2338+
func (q *fakeQuerier) GetUnexpiredLicenses(_ context.Context) ([]database.License, error) {
2339+
q.mutex.RLock()
2340+
defer q.mutex.RUnlock()
2341+
2342+
now := time.Now()
2343+
var results []database.License
2344+
for _, l := range q.licenses {
2345+
if l.Exp.After(now) {
2346+
results = append(results, l)
2347+
}
2348+
}
2349+
sort.Slice(results, func(i, j int) bool { return results[i].ID < results[j].ID })
2350+
return results, nil
2351+
}
2352+
23252353
func (q *fakeQuerier) DeleteLicense(_ context.Context, id int32) (int32, error) {
23262354
q.mutex.Lock()
23272355
defer q.mutex.Unlock()

coderd/database/querier.go

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

coderd/database/queries.sql.go

Lines changed: 51 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

coderd/database/queries/licenses.sql

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,12 @@ SELECT *
1313
FROM licenses
1414
ORDER BY (id);
1515

16+
-- name: GetUnexpiredLicenses :many
17+
SELECT *
18+
FROM licenses
19+
WHERE exp > NOW()
20+
ORDER BY (id);
21+
1622
-- name: DeleteLicense :one
1723
DELETE
1824
FROM licenses

coderd/database/queries/users.sql

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,14 @@ SELECT
2828
FROM
2929
users;
3030

31+
-- name: GetActiveUserCount :one
32+
SELECT
33+
COUNT(*)
34+
FROM
35+
users
36+
WHERE
37+
status = 'active'::public.user_status;
38+
3139
-- name: InsertUser :one
3240
INSERT INTO
3341
users (

coderd/features.go

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,20 @@ import (
77
"github.com/coder/coder/codersdk"
88
)
99

10-
func entitlements(rw http.ResponseWriter, _ *http.Request) {
10+
// FeaturesService is the interface for interacting with enterprise features.
11+
type FeaturesService interface {
12+
EntitlementsAPI(w http.ResponseWriter, r *http.Request)
13+
14+
// TODO
15+
// Get returns the implementations for feature interfaces. Parameter `s `must be a pointer to a
16+
// struct type containing feature interfaces as fields. The FeatureService sets all fields to
17+
// the correct implementations depending on whether the features are turned on.
18+
// Get(s any) error
19+
}
20+
21+
type featuresService struct{}
22+
23+
func (featuresService) EntitlementsAPI(rw http.ResponseWriter, _ *http.Request) {
1124
features := make(map[string]codersdk.Feature)
1225
for _, f := range codersdk.FeatureNames {
1326
features[f] = codersdk.Feature{

coderd/features_internal_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ func TestEntitlements(t *testing.T) {
1818
t.Parallel()
1919
r := httptest.NewRequest("GET", "https://example.com/api/v2/entitlements", nil)
2020
rw := httptest.NewRecorder()
21-
entitlements(rw, r)
21+
featuresService{}.EntitlementsAPI(rw, r)
2222
resp := rw.Result()
2323
defer resp.Body.Close()
2424
assert.Equal(t, http.StatusOK, resp.StatusCode)

coderd/httpapi/httpapi.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,18 @@ func Forbidden(rw http.ResponseWriter) {
5959
})
6060
}
6161

62+
func InternalServerError(rw http.ResponseWriter, err error) {
63+
var details string
64+
if err != nil {
65+
details = err.Error()
66+
}
67+
68+
Write(rw, http.StatusInternalServerError, codersdk.Response{
69+
Message: "An internal server error occurred.",
70+
Detail: details,
71+
})
72+
}
73+
6274
// Write outputs a standardized format to an HTTP response body.
6375
func Write(rw http.ResponseWriter, status int, response interface{}) {
6476
buf := &bytes.Buffer{}

coderd/httpapi/httpapi_test.go

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,46 @@ import (
1010

1111
"github.com/stretchr/testify/assert"
1212
"github.com/stretchr/testify/require"
13+
"golang.org/x/xerrors"
1314

1415
"github.com/coder/coder/coderd/httpapi"
1516
"github.com/coder/coder/codersdk"
1617
)
1718

19+
func TestInternalServerError(t *testing.T) {
20+
t.Parallel()
21+
22+
t.Run("NoError", func(t *testing.T) {
23+
t.Parallel()
24+
w := httptest.NewRecorder()
25+
httpapi.InternalServerError(w, nil)
26+
27+
var resp codersdk.Response
28+
err := json.NewDecoder(w.Body).Decode(&resp)
29+
require.NoError(t, err)
30+
require.Equal(t, http.StatusInternalServerError, w.Code)
31+
require.NotEmpty(t, resp.Message)
32+
require.Empty(t, resp.Detail)
33+
})
34+
35+
t.Run("WithError", func(t *testing.T) {
36+
t.Parallel()
37+
var (
38+
w = httptest.NewRecorder()
39+
httpErr = xerrors.New("error!")
40+
)
41+
42+
httpapi.InternalServerError(w, httpErr)
43+
44+
var resp codersdk.Response
45+
err := json.NewDecoder(w.Body).Decode(&resp)
46+
require.NoError(t, err)
47+
require.Equal(t, http.StatusInternalServerError, w.Code)
48+
require.NotEmpty(t, resp.Message)
49+
require.Equal(t, httpErr.Error(), resp.Detail)
50+
})
51+
}
52+
1853
func TestWrite(t *testing.T) {
1954
t.Parallel()
2055
t.Run("NoErrors", func(t *testing.T) {

coderd/httpapi/request.go

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
package httpapi
2+
3+
import "net/http"
4+
5+
const (
6+
// XForwardedHostHeader is a header used by proxies to indicate the
7+
// original host of the request.
8+
XForwardedHostHeader = "X-Forwarded-Host"
9+
)
10+
11+
// RequestHost returns the name of the host from the request. It prioritizes
12+
// 'X-Forwarded-Host' over r.Host since most requests are being proxied.
13+
func RequestHost(r *http.Request) string {
14+
host := r.Header.Get(XForwardedHostHeader)
15+
if host != "" {
16+
return host
17+
}
18+
19+
return r.Host
20+
}
21+
22+
func IsWebsocketUpgrade(r *http.Request) bool {
23+
vs := r.Header.Values("Upgrade")
24+
for _, v := range vs {
25+
if v == "websocket" {
26+
return true
27+
}
28+
}
29+
return false
30+
}

0 commit comments

Comments
 (0)