Skip to content

Commit 1b6431b

Browse files
committed
feat: add force refresh of license entitlements
1 parent 94cbc2a commit 1b6431b

File tree

6 files changed

+68
-0
lines changed

6 files changed

+68
-0
lines changed

codersdk/deployment.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,7 @@ type Entitlements struct {
103103
HasLicense bool `json:"has_license"`
104104
Trial bool `json:"trial"`
105105
RequireTelemetry bool `json:"require_telemetry"`
106+
RefreshedAt time.Time `json:"refreshed_at"`
106107
}
107108

108109
func (c *Client) Entitlements(ctx context.Context) (Entitlements, error) {

enterprise/coderd/coderd.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,7 @@ func New(ctx context.Context, options *Options) (_ *API, err error) {
130130
})
131131
r.Route("/licenses", func(r chi.Router) {
132132
r.Use(apiKeyMiddleware)
133+
r.Post("/refresh-entitlements", api.postRefreshEntitlements)
133134
r.Post("/", api.postLicense)
134135
r.Get("/", api.licenses)
135136
r.Delete("/{id}", api.deleteLicense)

enterprise/coderd/license/license.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -225,6 +225,7 @@ func Entitlements(
225225
entitlements.Features[featureName] = feature
226226
}
227227
}
228+
entitlements.RefreshedAt = now
228229

229230
return entitlements, nil
230231
}

enterprise/coderd/licenses.go

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
_ "embed"
99
"encoding/base64"
1010
"encoding/json"
11+
"fmt"
1112
"net/http"
1213
"strconv"
1314
"strings"
@@ -150,6 +151,65 @@ func (api *API) postLicense(rw http.ResponseWriter, r *http.Request) {
150151
httpapi.Write(ctx, rw, http.StatusCreated, convertLicense(dl, rawClaims))
151152
}
152153

154+
// postRefreshEntitlements forces an `updateEntitlements` call and publishes
155+
// a message to the PubsubEventLicenses topic to force other replicas
156+
// to update their entitlements.
157+
// Updates happen automatically on a timer, however that time is every 10 minutes,
158+
// and we want to be able to force an update immediately in some cases.
159+
//
160+
// @Summary Update license entitlements
161+
// @ID update-license-entitlements
162+
// @Security CoderSessionToken
163+
// @Accept json
164+
// @Produce json
165+
// @Tags Organizations
166+
// @Success 201 {object} codersdk.Response
167+
// @Router /licenses/refresh-entitlements [post]
168+
func (api *API) postRefreshEntitlements(rw http.ResponseWriter, r *http.Request) {
169+
var (
170+
ctx = r.Context()
171+
)
172+
173+
// If the user cannot create a new license, then they cannot refresh entitlements.
174+
// Refreshing entitlements is a way to force a refresh of the license, so it is
175+
// equivalent to creating a new license.
176+
if !api.AGPL.Authorize(r, rbac.ActionCreate, rbac.ResourceLicense) {
177+
httpapi.Forbidden(rw)
178+
return
179+
}
180+
181+
// Prevent abuse by limiting how often we allow a forced refresh.
182+
now := time.Now()
183+
if diff := now.Sub(api.entitlements.RefreshedAt); diff < time.Minute {
184+
wait := time.Minute - diff
185+
rw.Header().Set("Retry-After", strconv.Itoa(int(wait.Seconds())))
186+
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
187+
Message: fmt.Sprintf("Entitlements already recently refreshed, please wait %d seconds to force a new refresh", wait),
188+
Detail: fmt.Sprintf("Last refresh at %s", now.UTC().String()),
189+
})
190+
return
191+
}
192+
193+
err := api.updateEntitlements(ctx)
194+
if err != nil {
195+
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
196+
Message: "Failed to update entitlements",
197+
Detail: err.Error(),
198+
})
199+
return
200+
}
201+
202+
err = api.Pubsub.Publish(PubsubEventLicenses, []byte("refresh"))
203+
if err != nil {
204+
api.Logger.Error(context.Background(), "failed to publish forced entitlement update", slog.Error(err))
205+
// don't fail the HTTP request, since we did write it successfully to the database
206+
}
207+
208+
httpapi.Write(ctx, rw, http.StatusOK, codersdk.Response{
209+
Message: "Entitlements updated",
210+
})
211+
}
212+
153213
// @Summary Get licenses
154214
// @ID get-licenses
155215
// @Security CoderSessionToken

site/src/api/typesGenerated.ts

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

site/src/testHelpers/entities.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1450,6 +1450,7 @@ export const MockEntitlements: TypesGen.Entitlements = {
14501450
features: withDefaultFeatures({}),
14511451
require_telemetry: false,
14521452
trial: false,
1453+
refreshed_at:"2022-05-20T16:45:57.122Z",
14531454
}
14541455

14551456
export const MockEntitlementsWithWarnings: TypesGen.Entitlements = {
@@ -1458,6 +1459,7 @@ export const MockEntitlementsWithWarnings: TypesGen.Entitlements = {
14581459
has_license: true,
14591460
trial: false,
14601461
require_telemetry: false,
1462+
refreshed_at:"2022-05-20T16:45:57.122Z",
14611463
features: withDefaultFeatures({
14621464
user_limit: {
14631465
enabled: true,
@@ -1482,6 +1484,7 @@ export const MockEntitlementsWithAuditLog: TypesGen.Entitlements = {
14821484
has_license: true,
14831485
require_telemetry: false,
14841486
trial: false,
1487+
refreshed_at:"2022-05-20T16:45:57.122Z",
14851488
features: withDefaultFeatures({
14861489
audit_log: {
14871490
enabled: true,
@@ -1496,6 +1499,7 @@ export const MockEntitlementsWithScheduling: TypesGen.Entitlements = {
14961499
has_license: true,
14971500
require_telemetry: false,
14981501
trial: false,
1502+
refreshed_at:"2022-05-20T16:45:57.122Z",
14991503
features: withDefaultFeatures({
15001504
advanced_template_scheduling: {
15011505
enabled: true,

0 commit comments

Comments
 (0)