mirror of
https://github.com/coder/coder.git
synced 2025-07-08 11:39:50 +00: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:
@ -94,7 +94,7 @@ type Options struct {
|
||||
AutoImportTemplates []AutoImportTemplate
|
||||
GitAuthConfigs []*gitauth.Config
|
||||
RealIPConfig *httpmw.RealIPConfig
|
||||
|
||||
TrialGenerator func(ctx context.Context, email string) error
|
||||
// TLSCertificates is used to mesh DERP servers securely.
|
||||
TLSCertificates []tls.Certificate
|
||||
TailnetCoordinator tailnet.Coordinator
|
||||
|
@ -94,6 +94,7 @@ type Options struct {
|
||||
Auditor audit.Auditor
|
||||
TLSCertificates []tls.Certificate
|
||||
GitAuthConfigs []*gitauth.Config
|
||||
TrialGenerator func(context.Context, string) error
|
||||
|
||||
// IncludeProvisionerDaemon when true means to start an in-memory provisionerD
|
||||
IncludeProvisionerDaemon bool
|
||||
@ -258,6 +259,7 @@ func NewOptions(t *testing.T, options *Options) (func(http.Handler), context.Can
|
||||
Authorizer: options.Authorizer,
|
||||
Telemetry: telemetry.NewNoop(),
|
||||
TLSCertificates: options.TLSCertificates,
|
||||
TrialGenerator: options.TrialGenerator,
|
||||
DERPMap: &tailcfg.DERPMap{
|
||||
Regions: map[int]*tailcfg.DERPRegion{
|
||||
1: {
|
||||
@ -383,10 +385,9 @@ func NewExternalProvisionerDaemon(t *testing.T, client *codersdk.Client, org uui
|
||||
}
|
||||
|
||||
var FirstUserParams = codersdk.CreateFirstUserRequest{
|
||||
Email: "testuser@coder.com",
|
||||
Username: "testuser",
|
||||
Password: "testpass",
|
||||
OrganizationName: "testorg",
|
||||
Email: "testuser@coder.com",
|
||||
Username: "testuser",
|
||||
Password: "testpass",
|
||||
}
|
||||
|
||||
// CreateFirstUser creates a user with preset credentials and authenticates
|
||||
|
3
coderd/database/dump.sql
generated
3
coderd/database/dump.sql
generated
@ -200,7 +200,8 @@ CREATE TABLE licenses (
|
||||
id integer NOT NULL,
|
||||
uploaded_at timestamp with time zone NOT NULL,
|
||||
jwt text NOT NULL,
|
||||
exp timestamp with time zone NOT NULL
|
||||
exp timestamp with time zone NOT NULL,
|
||||
uuid uuid
|
||||
);
|
||||
|
||||
COMMENT ON COLUMN licenses.exp IS 'exp tracks the claim of the same name in the JWT, and we include it here so that we can easily query for licenses that have not yet expired.';
|
||||
|
1
coderd/database/migrations/000080_license_ids.down.sql
Normal file
1
coderd/database/migrations/000080_license_ids.down.sql
Normal file
@ -0,0 +1 @@
|
||||
ALTER TABLE licenses DROP COLUMN uuid;
|
1
coderd/database/migrations/000080_license_ids.up.sql
Normal file
1
coderd/database/migrations/000080_license_ids.up.sql
Normal file
@ -0,0 +1 @@
|
||||
ALTER TABLE licenses ADD COLUMN uuid uuid;
|
@ -468,7 +468,8 @@ type License struct {
|
||||
UploadedAt time.Time `db:"uploaded_at" json:"uploaded_at"`
|
||||
JWT string `db:"jwt" json:"jwt"`
|
||||
// exp tracks the claim of the same name in the JWT, and we include it here so that we can easily query for licenses that have not yet expired.
|
||||
Exp time.Time `db:"exp" json:"exp"`
|
||||
Exp time.Time `db:"exp" json:"exp"`
|
||||
Uuid uuid.NullUUID `db:"uuid" json:"uuid"`
|
||||
}
|
||||
|
||||
type Organization struct {
|
||||
|
@ -1402,7 +1402,7 @@ func (q *sqlQuerier) DeleteLicense(ctx context.Context, id int32) (int32, error)
|
||||
}
|
||||
|
||||
const getLicenses = `-- name: GetLicenses :many
|
||||
SELECT id, uploaded_at, jwt, exp
|
||||
SELECT id, uploaded_at, jwt, exp, uuid
|
||||
FROM licenses
|
||||
ORDER BY (id)
|
||||
`
|
||||
@ -1421,6 +1421,7 @@ func (q *sqlQuerier) GetLicenses(ctx context.Context) ([]License, error) {
|
||||
&i.UploadedAt,
|
||||
&i.JWT,
|
||||
&i.Exp,
|
||||
&i.Uuid,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -1436,7 +1437,7 @@ func (q *sqlQuerier) GetLicenses(ctx context.Context) ([]License, error) {
|
||||
}
|
||||
|
||||
const getUnexpiredLicenses = `-- name: GetUnexpiredLicenses :many
|
||||
SELECT id, uploaded_at, jwt, exp
|
||||
SELECT id, uploaded_at, jwt, exp, uuid
|
||||
FROM licenses
|
||||
WHERE exp > NOW()
|
||||
ORDER BY (id)
|
||||
@ -1456,6 +1457,7 @@ func (q *sqlQuerier) GetUnexpiredLicenses(ctx context.Context) ([]License, error
|
||||
&i.UploadedAt,
|
||||
&i.JWT,
|
||||
&i.Exp,
|
||||
&i.Uuid,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -1475,26 +1477,34 @@ INSERT INTO
|
||||
licenses (
|
||||
uploaded_at,
|
||||
jwt,
|
||||
exp
|
||||
exp,
|
||||
uuid
|
||||
)
|
||||
VALUES
|
||||
($1, $2, $3) RETURNING id, uploaded_at, jwt, exp
|
||||
($1, $2, $3, $4) RETURNING id, uploaded_at, jwt, exp, uuid
|
||||
`
|
||||
|
||||
type InsertLicenseParams struct {
|
||||
UploadedAt time.Time `db:"uploaded_at" json:"uploaded_at"`
|
||||
JWT string `db:"jwt" json:"jwt"`
|
||||
Exp time.Time `db:"exp" json:"exp"`
|
||||
UploadedAt time.Time `db:"uploaded_at" json:"uploaded_at"`
|
||||
JWT string `db:"jwt" json:"jwt"`
|
||||
Exp time.Time `db:"exp" json:"exp"`
|
||||
Uuid uuid.NullUUID `db:"uuid" json:"uuid"`
|
||||
}
|
||||
|
||||
func (q *sqlQuerier) InsertLicense(ctx context.Context, arg InsertLicenseParams) (License, error) {
|
||||
row := q.db.QueryRowContext(ctx, insertLicense, arg.UploadedAt, arg.JWT, arg.Exp)
|
||||
row := q.db.QueryRowContext(ctx, insertLicense,
|
||||
arg.UploadedAt,
|
||||
arg.JWT,
|
||||
arg.Exp,
|
||||
arg.Uuid,
|
||||
)
|
||||
var i License
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.UploadedAt,
|
||||
&i.JWT,
|
||||
&i.Exp,
|
||||
&i.Uuid,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
@ -3,10 +3,11 @@ INSERT INTO
|
||||
licenses (
|
||||
uploaded_at,
|
||||
jwt,
|
||||
exp
|
||||
exp,
|
||||
uuid
|
||||
)
|
||||
VALUES
|
||||
($1, $2, $3) RETURNING *;
|
||||
($1, $2, $3, $4) RETURNING *;
|
||||
|
||||
-- name: GetLicenses :many
|
||||
SELECT *
|
||||
|
@ -446,6 +446,17 @@ func (r *remoteReporter) createSnapshot() (*Snapshot, error) {
|
||||
}
|
||||
return nil
|
||||
})
|
||||
eg.Go(func() error {
|
||||
licenses, err := r.options.Database.GetUnexpiredLicenses(ctx)
|
||||
if err != nil {
|
||||
return xerrors.Errorf("get licenses: %w", err)
|
||||
}
|
||||
snapshot.Licenses = make([]License, 0, len(licenses))
|
||||
for _, license := range licenses {
|
||||
snapshot.Licenses = append(snapshot.Licenses, ConvertLicense(license))
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
err := eg.Wait()
|
||||
if err != nil {
|
||||
@ -622,6 +633,14 @@ func ConvertTemplateVersion(version database.TemplateVersion) TemplateVersion {
|
||||
return snapVersion
|
||||
}
|
||||
|
||||
// ConvertLicense anonymizes a license.
|
||||
func ConvertLicense(license database.License) License {
|
||||
return License{
|
||||
UploadedAt: license.UploadedAt,
|
||||
UUID: license.Uuid.UUID,
|
||||
}
|
||||
}
|
||||
|
||||
// Snapshot represents a point-in-time anonymized database dump.
|
||||
// Data is aggregated by latest on the server-side, so partial data
|
||||
// can be sent without issue.
|
||||
@ -631,6 +650,7 @@ type Snapshot struct {
|
||||
APIKeys []APIKey `json:"api_keys"`
|
||||
ParameterSchemas []ParameterSchema `json:"parameter_schemas"`
|
||||
ProvisionerJobs []ProvisionerJob `json:"provisioner_jobs"`
|
||||
Licenses []License `json:"licenses"`
|
||||
Templates []Template `json:"templates"`
|
||||
TemplateVersions []TemplateVersion `json:"template_versions"`
|
||||
Users []User `json:"users"`
|
||||
@ -791,6 +811,11 @@ type ParameterSchema struct {
|
||||
ValidationCondition string `json:"validation_condition"`
|
||||
}
|
||||
|
||||
type License struct {
|
||||
UploadedAt time.Time `json:"uploaded_at"`
|
||||
UUID uuid.UUID `json:"uuid"`
|
||||
}
|
||||
|
||||
type noopReporter struct{}
|
||||
|
||||
func (*noopReporter) Report(_ *Snapshot) {}
|
||||
|
@ -7,6 +7,7 @@ import (
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/go-chi/chi"
|
||||
"github.com/google/uuid"
|
||||
@ -87,9 +88,20 @@ func TestTelemetry(t *testing.T) {
|
||||
CreatedAt: database.Now(),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
_, err = db.InsertLicense(ctx, database.InsertLicenseParams{
|
||||
UploadedAt: database.Now(),
|
||||
JWT: "",
|
||||
Exp: database.Now().Add(time.Hour),
|
||||
Uuid: uuid.NullUUID{
|
||||
UUID: uuid.New(),
|
||||
Valid: true,
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
snapshot := collectSnapshot(t, db)
|
||||
require.Len(t, snapshot.ParameterSchemas, 1)
|
||||
require.Len(t, snapshot.ProvisionerJobs, 1)
|
||||
require.Len(t, snapshot.Licenses, 1)
|
||||
require.Len(t, snapshot.Templates, 1)
|
||||
require.Len(t, snapshot.TemplateVersions, 1)
|
||||
require.Len(t, snapshot.Users, 1)
|
||||
|
@ -80,6 +80,17 @@ func (api *API) postFirstUser(rw http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
if createUser.Trial && api.TrialGenerator != nil {
|
||||
err = api.TrialGenerator(ctx, createUser.Email)
|
||||
if err != nil {
|
||||
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
|
||||
Message: "Failed to generate trial",
|
||||
Detail: err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
user, organizationID, err := api.CreateUser(ctx, api.Database, CreateUserRequest{
|
||||
CreateUserRequest: codersdk.CreateUserRequest{
|
||||
Email: createUser.Email,
|
||||
|
@ -49,10 +49,9 @@ func TestFirstUser(t *testing.T) {
|
||||
defer cancel()
|
||||
|
||||
_, err := client.CreateFirstUser(ctx, codersdk.CreateFirstUserRequest{
|
||||
Email: "some@email.com",
|
||||
Username: "exampleuser",
|
||||
Password: "password",
|
||||
OrganizationName: "someorg",
|
||||
Email: "some@email.com",
|
||||
Username: "exampleuser",
|
||||
Password: "password",
|
||||
})
|
||||
var apiErr *codersdk.Error
|
||||
require.ErrorAs(t, err, &apiErr)
|
||||
@ -65,6 +64,30 @@ func TestFirstUser(t *testing.T) {
|
||||
_ = coderdtest.CreateFirstUser(t, client)
|
||||
})
|
||||
|
||||
t.Run("Trial", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
called := make(chan struct{})
|
||||
client := coderdtest.New(t, &coderdtest.Options{
|
||||
TrialGenerator: func(ctx context.Context, s string) error {
|
||||
close(called)
|
||||
return nil
|
||||
},
|
||||
})
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
||||
defer cancel()
|
||||
|
||||
req := codersdk.CreateFirstUserRequest{
|
||||
Email: "testuser@coder.com",
|
||||
Username: "testuser",
|
||||
Password: "testpass",
|
||||
Trial: true,
|
||||
}
|
||||
_, err := client.CreateFirstUser(ctx, req)
|
||||
require.NoError(t, err)
|
||||
<-called
|
||||
})
|
||||
|
||||
t.Run("LastSeenAt", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
||||
@ -192,10 +215,9 @@ func TestPostLogin(t *testing.T) {
|
||||
defer cancel()
|
||||
|
||||
req := codersdk.CreateFirstUserRequest{
|
||||
Email: "testuser@coder.com",
|
||||
Username: "testuser",
|
||||
Password: "testpass",
|
||||
OrganizationName: "testorg",
|
||||
Email: "testuser@coder.com",
|
||||
Username: "testuser",
|
||||
Password: "testpass",
|
||||
}
|
||||
_, err := client.CreateFirstUser(ctx, req)
|
||||
require.NoError(t, err)
|
||||
@ -249,10 +271,9 @@ func TestPostLogin(t *testing.T) {
|
||||
defer cancel()
|
||||
|
||||
req := codersdk.CreateFirstUserRequest{
|
||||
Email: "testuser@coder.com",
|
||||
Username: "testuser",
|
||||
Password: "testpass",
|
||||
OrganizationName: "testorg",
|
||||
Email: "testuser@coder.com",
|
||||
Username: "testuser",
|
||||
Password: "testpass",
|
||||
}
|
||||
_, err := client.CreateFirstUser(ctx, req)
|
||||
require.NoError(t, err)
|
||||
|
Reference in New Issue
Block a user