Skip to content

Commit 8c02058

Browse files
committed
feat: Add the option to generate a trial license during setup
This allows users to generate a 30 day free license during setup to test out Enterprise features.
1 parent 894953d commit 8c02058

28 files changed

+325
-72
lines changed

.github/workflows/typos.toml

+1
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ MacOS = "macOS"
99
doas = "doas"
1010
darcula = "darcula"
1111
Hashi = "Hashi"
12+
trialer = "trialer"
1213

1314
[files]
1415
extend-exclude = [

.golangci.yaml

+2
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,8 @@ linters-settings:
123123

124124
misspell:
125125
locale: US
126+
ignore-words:
127+
- trialer
126128

127129
nestif:
128130
min-complexity: 4 # Min complexity of if statements (def 5, goal 4)

.vscode/settings.json

+1
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,7 @@
127127
"tfstate",
128128
"tios",
129129
"tparallel",
130+
"trialer",
130131
"trimprefix",
131132
"tsdial",
132133
"tslogger",

cli/login.go

+17-4
Original file line numberDiff line numberDiff line change
@@ -38,10 +38,13 @@ func init() {
3838
}
3939

4040
func login() *cobra.Command {
41+
const firstUserTrialEnv = "CODER_FIRST_USER_TRIAL"
42+
4143
var (
4244
email string
4345
username string
4446
password string
47+
trial bool
4548
)
4649
cmd := &cobra.Command{
4750
Use: "login <url>",
@@ -162,11 +165,20 @@ func login() *cobra.Command {
162165
}
163166
}
164167

168+
if !cmd.Flags().Changed("first-user-trial") && os.Getenv(firstUserTrialEnv) == "" {
169+
v, _ := cliui.Prompt(cmd, cliui.PromptOptions{
170+
Text: "Start a 30-day trial of Enterprise?",
171+
IsConfirm: true,
172+
Default: "yes",
173+
})
174+
trial = v == "yes" || v == "y"
175+
}
176+
165177
_, err = client.CreateFirstUser(cmd.Context(), codersdk.CreateFirstUserRequest{
166-
Email: email,
167-
Username: username,
168-
OrganizationName: username,
169-
Password: password,
178+
Email: email,
179+
Username: username,
180+
Password: password,
181+
Trial: trial,
170182
})
171183
if err != nil {
172184
return xerrors.Errorf("create initial user: %w", err)
@@ -251,6 +263,7 @@ func login() *cobra.Command {
251263
cliflag.StringVarP(cmd.Flags(), &email, "first-user-email", "", "CODER_FIRST_USER_EMAIL", "", "Specifies an email address to use if creating the first user for the deployment.")
252264
cliflag.StringVarP(cmd.Flags(), &username, "first-user-username", "", "CODER_FIRST_USER_USERNAME", "", "Specifies a username to use if creating the first user for the deployment.")
253265
cliflag.StringVarP(cmd.Flags(), &password, "first-user-password", "", "CODER_FIRST_USER_PASSWORD", "", "Specifies a password to use if creating the first user for the deployment.")
266+
cliflag.BoolVarP(cmd.Flags(), &trial, "first-user-trial", "", firstUserTrialEnv, false, "Specifies whether a trial license should be provisioned for the Coder deployment or not.")
254267
return cmd
255268
}
256269

cli/login_test.go

+3
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ func TestLogin(t *testing.T) {
5656
"email", "user@coder.com",
5757
"password", "password",
5858
"password", "password", // Confirm.
59+
"trial", "yes",
5960
}
6061
for i := 0; i < len(matches); i += 2 {
6162
match := matches[i]
@@ -127,6 +128,8 @@ func TestLogin(t *testing.T) {
127128
pty.WriteLine("pass")
128129
pty.ExpectMatch("Confirm")
129130
pty.WriteLine("pass")
131+
pty.ExpectMatch("trial")
132+
pty.WriteLine("yes")
130133
pty.ExpectMatch("Welcome to Coder")
131134
<-doneChan
132135
})

cli/resetpassword_test.go

+3-4
Original file line numberDiff line numberDiff line change
@@ -60,10 +60,9 @@ func TestResetPassword(t *testing.T) {
6060
client := codersdk.New(accessURL)
6161

6262
_, err = client.CreateFirstUser(ctx, codersdk.CreateFirstUserRequest{
63-
Email: email,
64-
Username: username,
65-
Password: oldPassword,
66-
OrganizationName: "example",
63+
Email: email,
64+
Username: username,
65+
Password: oldPassword,
6766
})
6867
require.NoError(t, err)
6968

cli/server_test.go

+3-4
Original file line numberDiff line numberDiff line change
@@ -71,10 +71,9 @@ func TestServer(t *testing.T) {
7171
client := codersdk.New(accessURL)
7272

7373
_, err = client.CreateFirstUser(ctx, codersdk.CreateFirstUserRequest{
74-
Email: "some@one.com",
75-
Username: "example",
76-
Password: "password",
77-
OrganizationName: "example",
74+
Email: "some@one.com",
75+
Username: "example",
76+
Password: "password",
7877
})
7978
require.NoError(t, err)
8079
cancelFunc()

coderd/coderd.go

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package coderd
22

33
import (
4+
"context"
45
"crypto/tls"
56
"crypto/x509"
67
"fmt"
@@ -86,7 +87,7 @@ type Options struct {
8687
AutoImportTemplates []AutoImportTemplate
8788
GitAuthConfigs []*gitauth.Config
8889
RealIPConfig *httpmw.RealIPConfig
89-
90+
TrialGenerator func(ctx context.Context, email string) error
9091
// TLSCertificates is used to mesh DERP servers securely.
9192
TLSCertificates []tls.Certificate
9293
TailnetCoordinator tailnet.Coordinator

coderd/coderdtest/coderdtest.go

+5-4
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@ type Options struct {
9494
Auditor audit.Auditor
9595
TLSCertificates []tls.Certificate
9696
GitAuthConfigs []*gitauth.Config
97+
TrialGenerator func(context.Context, string) error
9798

9899
// IncludeProvisionerDaemon when true means to start an in-memory provisionerD
99100
IncludeProvisionerDaemon bool
@@ -258,6 +259,7 @@ func NewOptions(t *testing.T, options *Options) (func(http.Handler), context.Can
258259
Authorizer: options.Authorizer,
259260
Telemetry: telemetry.NewNoop(),
260261
TLSCertificates: options.TLSCertificates,
262+
TrialGenerator: options.TrialGenerator,
261263
DERPMap: &tailcfg.DERPMap{
262264
Regions: map[int]*tailcfg.DERPRegion{
263265
1: {
@@ -348,10 +350,9 @@ func NewProvisionerDaemon(t *testing.T, coderAPI *coderd.API) io.Closer {
348350
}
349351

350352
var FirstUserParams = codersdk.CreateFirstUserRequest{
351-
Email: "testuser@coder.com",
352-
Username: "testuser",
353-
Password: "testpass",
354-
OrganizationName: "testorg",
353+
Email: "testuser@coder.com",
354+
Username: "testuser",
355+
Password: "testpass",
355356
}
356357

357358
// CreateFirstUser creates a user with preset credentials and authenticates

coderd/database/dump.sql

+2-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
ALTER TABLE licenses DROP COLUMN uuid;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
ALTER TABLE licenses ADD COLUMN uuid uuid;

coderd/database/models.go

+2-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

coderd/database/queries.sql.go

+18-8
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

coderd/database/queries/licenses.sql

+3-2
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,11 @@ INSERT INTO
33
licenses (
44
uploaded_at,
55
jwt,
6-
exp
6+
exp,
7+
uuid
78
)
89
VALUES
9-
($1, $2, $3) RETURNING *;
10+
($1, $2, $3, $4) RETURNING *;
1011

1112
-- name: GetLicenses :many
1213
SELECT *

coderd/telemetry/telemetry.go

+25
Original file line numberDiff line numberDiff line change
@@ -446,6 +446,17 @@ func (r *remoteReporter) createSnapshot() (*Snapshot, error) {
446446
}
447447
return nil
448448
})
449+
eg.Go(func() error {
450+
licenses, err := r.options.Database.GetUnexpiredLicenses(ctx)
451+
if err != nil {
452+
return xerrors.Errorf("get licenses: %w", err)
453+
}
454+
snapshot.Licenses = make([]License, 0, len(licenses))
455+
for _, license := range licenses {
456+
snapshot.Licenses = append(snapshot.Licenses, ConvertLicense(license))
457+
}
458+
return nil
459+
})
449460

450461
err := eg.Wait()
451462
if err != nil {
@@ -622,6 +633,14 @@ func ConvertTemplateVersion(version database.TemplateVersion) TemplateVersion {
622633
return snapVersion
623634
}
624635

636+
// ConvertLicense anonymizes a license.
637+
func ConvertLicense(license database.License) License {
638+
return License{
639+
UploadedAt: license.UploadedAt,
640+
UUID: license.Uuid.UUID,
641+
}
642+
}
643+
625644
// Snapshot represents a point-in-time anonymized database dump.
626645
// Data is aggregated by latest on the server-side, so partial data
627646
// can be sent without issue.
@@ -631,6 +650,7 @@ type Snapshot struct {
631650
APIKeys []APIKey `json:"api_keys"`
632651
ParameterSchemas []ParameterSchema `json:"parameter_schemas"`
633652
ProvisionerJobs []ProvisionerJob `json:"provisioner_jobs"`
653+
Licenses []License `json:"licenses"`
634654
Templates []Template `json:"templates"`
635655
TemplateVersions []TemplateVersion `json:"template_versions"`
636656
Users []User `json:"users"`
@@ -791,6 +811,11 @@ type ParameterSchema struct {
791811
ValidationCondition string `json:"validation_condition"`
792812
}
793813

814+
type License struct {
815+
UploadedAt time.Time `json:"uploaded_at"`
816+
UUID uuid.UUID `json:"uuid"`
817+
}
818+
794819
type noopReporter struct{}
795820

796821
func (*noopReporter) Report(_ *Snapshot) {}

coderd/telemetry/telemetry_test.go

+12
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"net/http/httptest"
88
"net/url"
99
"testing"
10+
"time"
1011

1112
"github.com/go-chi/chi"
1213
"github.com/google/uuid"
@@ -87,9 +88,20 @@ func TestTelemetry(t *testing.T) {
8788
CreatedAt: database.Now(),
8889
})
8990
require.NoError(t, err)
91+
_, err = db.InsertLicense(ctx, database.InsertLicenseParams{
92+
UploadedAt: database.Now(),
93+
JWT: "",
94+
Exp: database.Now().Add(time.Hour),
95+
Uuid: uuid.NullUUID{
96+
UUID: uuid.New(),
97+
Valid: true,
98+
},
99+
})
100+
require.NoError(t, err)
90101
snapshot := collectSnapshot(t, db)
91102
require.Len(t, snapshot.ParameterSchemas, 1)
92103
require.Len(t, snapshot.ProvisionerJobs, 1)
104+
require.Len(t, snapshot.Licenses, 1)
93105
require.Len(t, snapshot.Templates, 1)
94106
require.Len(t, snapshot.TemplateVersions, 1)
95107
require.Len(t, snapshot.Users, 1)

coderd/users.go

+11
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,17 @@ func (api *API) postFirstUser(rw http.ResponseWriter, r *http.Request) {
8080
return
8181
}
8282

83+
if createUser.Trial && api.TrialGenerator != nil {
84+
err = api.TrialGenerator(ctx, createUser.Email)
85+
if err != nil {
86+
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
87+
Message: "Failed to generate trial",
88+
Detail: err.Error(),
89+
})
90+
return
91+
}
92+
}
93+
8394
user, organizationID, err := api.CreateUser(ctx, api.Database, CreateUserRequest{
8495
CreateUserRequest: codersdk.CreateUserRequest{
8596
Email: createUser.Email,

0 commit comments

Comments
 (0)