mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
feat: Add the option to generate a trial license during setup (#5110)
This allows users to generate a 30 day free license during setup to test out Enterprise features.
This commit is contained in:
@@ -17,6 +17,7 @@ import (
|
||||
"github.com/coder/coder/enterprise/audit"
|
||||
"github.com/coder/coder/enterprise/audit/backends"
|
||||
"github.com/coder/coder/enterprise/coderd"
|
||||
"github.com/coder/coder/enterprise/trialer"
|
||||
"github.com/coder/coder/tailnet"
|
||||
|
||||
agpl "github.com/coder/coder/cli"
|
||||
@@ -57,6 +58,8 @@ func server() *cobra.Command {
|
||||
)
|
||||
}
|
||||
|
||||
options.TrialGenerator = trialer.New(options.Database, "https://v2-licensor.coder.com/trial", coderd.Keys)
|
||||
|
||||
o := &coderd.Options{
|
||||
AuditLogging: options.DeploymentConfig.AuditLogging.Value,
|
||||
BrowserOnly: options.DeploymentConfig.BrowserOnly.Value,
|
||||
|
||||
@@ -54,7 +54,7 @@ func Entitlements(
|
||||
|
||||
// Here we loop through licenses to detect enabled features.
|
||||
for _, l := range licenses {
|
||||
claims, err := validateDBLicense(l, keys)
|
||||
claims, err := ParseClaims(l.JWT, keys)
|
||||
if err != nil {
|
||||
logger.Debug(ctx, "skipping invalid license",
|
||||
slog.F("id", l.ID), slog.Error(err))
|
||||
@@ -270,8 +270,8 @@ type Claims struct {
|
||||
Features Features `json:"features"`
|
||||
}
|
||||
|
||||
// Parse consumes a license and returns the claims.
|
||||
func Parse(l string, keys map[string]ed25519.PublicKey) (jwt.MapClaims, error) {
|
||||
// ParseRaw consumes a license and returns the claims.
|
||||
func ParseRaw(l string, keys map[string]ed25519.PublicKey) (jwt.MapClaims, error) {
|
||||
tok, err := jwt.Parse(
|
||||
l,
|
||||
keyFunc(keys),
|
||||
@@ -293,11 +293,11 @@ func Parse(l string, keys map[string]ed25519.PublicKey) (jwt.MapClaims, error) {
|
||||
return nil, xerrors.New("unable to parse Claims")
|
||||
}
|
||||
|
||||
// validateDBLicense validates a database.License record, and if valid, returns the claims. If
|
||||
// ParseClaims validates a database.License record, and if valid, returns the claims. If
|
||||
// unparsable or invalid, it returns an error
|
||||
func validateDBLicense(l database.License, keys map[string]ed25519.PublicKey) (*Claims, error) {
|
||||
func ParseClaims(rawJWT string, keys map[string]ed25519.PublicKey) (*Claims, error) {
|
||||
tok, err := jwt.ParseWithClaims(
|
||||
l.JWT,
|
||||
rawJWT,
|
||||
&Claims{},
|
||||
keyFunc(keys),
|
||||
jwt.WithValidMethods(ValidMethods),
|
||||
|
||||
@@ -15,6 +15,7 @@ import (
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/golang-jwt/jwt/v4"
|
||||
"github.com/google/uuid"
|
||||
"golang.org/x/xerrors"
|
||||
|
||||
"cdr.dev/slog"
|
||||
@@ -59,7 +60,7 @@ func (api *API) postLicense(rw http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
claims, err := license.Parse(addLicense.License, api.Keys)
|
||||
rawClaims, err := license.ParseRaw(addLicense.License, api.Keys)
|
||||
if err != nil {
|
||||
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
|
||||
Message: "Invalid license",
|
||||
@@ -67,7 +68,7 @@ func (api *API) postLicense(rw http.ResponseWriter, r *http.Request) {
|
||||
})
|
||||
return
|
||||
}
|
||||
exp, ok := claims["exp"].(float64)
|
||||
exp, ok := rawClaims["exp"].(float64)
|
||||
if !ok {
|
||||
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
|
||||
Message: "Invalid license",
|
||||
@@ -77,10 +78,24 @@ func (api *API) postLicense(rw http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
expTime := time.Unix(int64(exp), 0)
|
||||
|
||||
claims, err := license.ParseClaims(addLicense.License, api.Keys)
|
||||
if err != nil {
|
||||
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
|
||||
Message: "Invalid license",
|
||||
Detail: err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
id, err := uuid.Parse(claims.ID)
|
||||
dl, err := api.Database.InsertLicense(ctx, database.InsertLicenseParams{
|
||||
UploadedAt: database.Now(),
|
||||
JWT: addLicense.License,
|
||||
Exp: expTime,
|
||||
Uuid: uuid.NullUUID{
|
||||
UUID: id,
|
||||
Valid: err == nil,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
|
||||
@@ -103,7 +118,7 @@ func (api *API) postLicense(rw http.ResponseWriter, r *http.Request) {
|
||||
// don't fail the HTTP request, since we did write it successfully to the database
|
||||
}
|
||||
|
||||
httpapi.Write(ctx, rw, http.StatusCreated, convertLicense(dl, claims))
|
||||
httpapi.Write(ctx, rw, http.StatusCreated, convertLicense(dl, rawClaims))
|
||||
}
|
||||
|
||||
func (api *API) licenses(rw http.ResponseWriter, r *http.Request) {
|
||||
@@ -189,6 +204,7 @@ func (api *API) deleteLicense(rw http.ResponseWriter, r *http.Request) {
|
||||
func convertLicense(dl database.License, c jwt.MapClaims) codersdk.License {
|
||||
return codersdk.License{
|
||||
ID: dl.ID,
|
||||
UUID: dl.Uuid.UUID,
|
||||
UploadedAt: dl.UploadedAt,
|
||||
Claims: c,
|
||||
}
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
package trialer
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/ed25519"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"golang.org/x/xerrors"
|
||||
|
||||
"github.com/coder/coder/coderd/database"
|
||||
"github.com/coder/coder/enterprise/coderd/license"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type request struct {
|
||||
DeploymentID string `json:"deployment_id"`
|
||||
Email string `json:"email"`
|
||||
}
|
||||
|
||||
// New creates a handler that can issue trial licenses!
|
||||
func New(db database.Store, url string, keys map[string]ed25519.PublicKey) func(ctx context.Context, email string) error {
|
||||
return func(ctx context.Context, email string) error {
|
||||
deploymentID, err := db.GetDeploymentID(ctx)
|
||||
if err != nil {
|
||||
return xerrors.Errorf("get deployment id: %w", err)
|
||||
}
|
||||
data, err := json.Marshal(request{
|
||||
DeploymentID: deploymentID,
|
||||
Email: email,
|
||||
})
|
||||
if err != nil {
|
||||
return xerrors.Errorf("marshal: %w", err)
|
||||
}
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(data))
|
||||
if err != nil {
|
||||
return xerrors.Errorf("create license request: %w", err)
|
||||
}
|
||||
res, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return xerrors.Errorf("perform license request: %w", err)
|
||||
}
|
||||
defer res.Body.Close()
|
||||
raw, err := io.ReadAll(res.Body)
|
||||
if err != nil {
|
||||
return xerrors.Errorf("read license: %w", err)
|
||||
}
|
||||
rawClaims, err := license.ParseRaw(string(raw), keys)
|
||||
if err != nil {
|
||||
return xerrors.Errorf("parse license: %w", err)
|
||||
}
|
||||
exp, ok := rawClaims["exp"].(float64)
|
||||
if !ok {
|
||||
return xerrors.New("invalid license missing exp claim")
|
||||
}
|
||||
expTime := time.Unix(int64(exp), 0)
|
||||
|
||||
claims, err := license.ParseClaims(string(raw), keys)
|
||||
if err != nil {
|
||||
return xerrors.Errorf("parse claims: %w", err)
|
||||
}
|
||||
id, err := uuid.Parse(claims.ID)
|
||||
_, err = db.InsertLicense(ctx, database.InsertLicenseParams{
|
||||
UploadedAt: database.Now(),
|
||||
JWT: string(raw),
|
||||
Exp: expTime,
|
||||
Uuid: uuid.NullUUID{
|
||||
UUID: id,
|
||||
Valid: err == nil,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return xerrors.Errorf("insert license: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package trialer_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/coder/coder/coderd/database/databasefake"
|
||||
"github.com/coder/coder/enterprise/coderd/coderdenttest"
|
||||
"github.com/coder/coder/enterprise/trialer"
|
||||
)
|
||||
|
||||
func TestTrialer(t *testing.T) {
|
||||
t.Parallel()
|
||||
license := coderdenttest.GenerateLicense(t, coderdenttest.LicenseOptions{
|
||||
Trial: true,
|
||||
})
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
_, _ = w.Write([]byte(license))
|
||||
}))
|
||||
defer srv.Close()
|
||||
db := databasefake.New()
|
||||
|
||||
gen := trialer.New(db, srv.URL, coderdenttest.Keys)
|
||||
err := gen(context.Background(), "kyle@coder.com")
|
||||
require.NoError(t, err)
|
||||
licenses, err := db.GetLicenses(context.Background())
|
||||
require.NoError(t, err)
|
||||
require.Len(t, licenses, 1)
|
||||
}
|
||||
Reference in New Issue
Block a user