Skip to content

Commit

Permalink
feat: Add the option to generate a trial license during setup (#5110)
Browse files Browse the repository at this point in the history
This allows users to generate a 30 day free license during setup to
test out Enterprise features.
  • Loading branch information
kylecarbs committed Nov 16, 2022
1 parent b6703b1 commit fb9ca7b
Show file tree
Hide file tree
Showing 29 changed files with 332 additions and 79 deletions.
1 change: 1 addition & 0 deletions .github/workflows/typos.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ MacOS = "macOS"
doas = "doas"
darcula = "darcula"
Hashi = "Hashi"
trialer = "trialer"

[files]
extend-exclude = [
Expand Down
2 changes: 2 additions & 0 deletions .golangci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,8 @@ linters-settings:

misspell:
locale: US
ignore-words:
- trialer

nestif:
min-complexity: 4 # Min complexity of if statements (def 5, goal 4)
Expand Down
1 change: 1 addition & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,7 @@
"tfstate",
"tios",
"tparallel",
"trialer",
"trimprefix",
"tsdial",
"tslogger",
Expand Down
21 changes: 17 additions & 4 deletions cli/login.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,10 +38,13 @@ func init() {
}

func login() *cobra.Command {
const firstUserTrialEnv = "CODER_FIRST_USER_TRIAL"

var (
email string
username string
password string
trial bool
)
cmd := &cobra.Command{
Use: "login <url>",
Expand Down Expand Up @@ -162,11 +165,20 @@ func login() *cobra.Command {
}
}

if !cmd.Flags().Changed("first-user-trial") && os.Getenv(firstUserTrialEnv) == "" {
v, _ := cliui.Prompt(cmd, cliui.PromptOptions{
Text: "Start a 30-day trial of Enterprise?",
IsConfirm: true,
Default: "yes",
})
trial = v == "yes" || v == "y"
}

_, err = client.CreateFirstUser(cmd.Context(), codersdk.CreateFirstUserRequest{
Email: email,
Username: username,
OrganizationName: username,
Password: password,
Email: email,
Username: username,
Password: password,
Trial: trial,
})
if err != nil {
return xerrors.Errorf("create initial user: %w", err)
Expand Down Expand Up @@ -251,6 +263,7 @@ func login() *cobra.Command {
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.")
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.")
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.")
cliflag.BoolVarP(cmd.Flags(), &trial, "first-user-trial", "", firstUserTrialEnv, false, "Specifies whether a trial license should be provisioned for the Coder deployment or not.")
return cmd
}

Expand Down
5 changes: 4 additions & 1 deletion cli/login_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ func TestLogin(t *testing.T) {
"email", "user@coder.com",
"password", "password",
"password", "password", // Confirm.
"trial", "yes",
}
for i := 0; i < len(matches); i += 2 {
match := matches[i]
Expand All @@ -74,7 +75,7 @@ func TestLogin(t *testing.T) {
// accurately detect Windows ptys when they are not attached to a process:
// https://github.com/mattn/go-isatty/issues/59
doneChan := make(chan struct{})
root, _ := clitest.New(t, "login", client.URL.String(), "--first-user-username", "testuser", "--first-user-email", "user@coder.com", "--first-user-password", "password")
root, _ := clitest.New(t, "login", client.URL.String(), "--first-user-username", "testuser", "--first-user-email", "user@coder.com", "--first-user-password", "password", "--first-user-trial")
pty := ptytest.New(t)
root.SetIn(pty.Input())
root.SetOut(pty.Output())
Expand Down Expand Up @@ -127,6 +128,8 @@ func TestLogin(t *testing.T) {
pty.WriteLine("pass")
pty.ExpectMatch("Confirm")
pty.WriteLine("pass")
pty.ExpectMatch("trial")
pty.WriteLine("yes")
pty.ExpectMatch("Welcome to Coder")
<-doneChan
})
Expand Down
7 changes: 3 additions & 4 deletions cli/resetpassword_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,10 +60,9 @@ func TestResetPassword(t *testing.T) {
client := codersdk.New(accessURL)

_, err = client.CreateFirstUser(ctx, codersdk.CreateFirstUserRequest{
Email: email,
Username: username,
Password: oldPassword,
OrganizationName: "example",
Email: email,
Username: username,
Password: oldPassword,
})
require.NoError(t, err)

Expand Down
7 changes: 3 additions & 4 deletions cli/server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,10 +71,9 @@ func TestServer(t *testing.T) {
client := codersdk.New(accessURL)

_, err = client.CreateFirstUser(ctx, codersdk.CreateFirstUserRequest{
Email: "some@one.com",
Username: "example",
Password: "password",
OrganizationName: "example",
Email: "some@one.com",
Username: "example",
Password: "password",
})
require.NoError(t, err)
cancelFunc()
Expand Down
2 changes: 1 addition & 1 deletion coderd/coderd.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
9 changes: 5 additions & 4 deletions coderd/coderdtest/coderdtest.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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: {
Expand Down Expand Up @@ -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
Expand Down
3 changes: 2 additions & 1 deletion coderd/database/dump.sql

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions coderd/database/migrations/000080_license_ids.down.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ALTER TABLE licenses DROP COLUMN uuid;
1 change: 1 addition & 0 deletions coderd/database/migrations/000080_license_ids.up.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ALTER TABLE licenses ADD COLUMN uuid uuid;
3 changes: 2 additions & 1 deletion coderd/database/models.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

26 changes: 18 additions & 8 deletions coderd/database/queries.sql.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 3 additions & 2 deletions coderd/database/queries/licenses.sql
Original file line number Diff line number Diff line change
Expand Up @@ -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 *
Expand Down
25 changes: 25 additions & 0 deletions coderd/telemetry/telemetry.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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.
Expand All @@ -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"`
Expand Down Expand Up @@ -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) {}
Expand Down
12 changes: 12 additions & 0 deletions coderd/telemetry/telemetry_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"net/http/httptest"
"net/url"
"testing"
"time"

"github.com/go-chi/chi"
"github.com/google/uuid"
Expand Down Expand Up @@ -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)
Expand Down
11 changes: 11 additions & 0 deletions coderd/users.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down

0 comments on commit fb9ca7b

Please sign in to comment.