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:
Kyle Carberry
2022-11-16 17:09:49 -06:00
committed by GitHub
parent b6703b11c6
commit fb9ca7b830
29 changed files with 332 additions and 79 deletions

View File

@ -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

View File

@ -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

View File

@ -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.';

View File

@ -0,0 +1 @@
ALTER TABLE licenses DROP COLUMN uuid;

View File

@ -0,0 +1 @@
ALTER TABLE licenses ADD COLUMN uuid uuid;

View File

@ -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 {

View File

@ -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
}

View File

@ -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 *

View File

@ -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) {}

View File

@ -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)

View File

@ -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,

View File

@ -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)