mirror of
https://github.com/coder/coder.git
synced 2026-06-02 20:48:20 +00:00
feat: expose premium trial form via CLI (#15054)
This PR closes https://github.com/coder/coder/issues/14856
This commit is contained in:
@@ -425,7 +425,7 @@ jobs:
|
||||
--first-user-username coder \
|
||||
--first-user-email pr${{ env.PR_NUMBER }}@coder.com \
|
||||
--first-user-password $password \
|
||||
--first-user-trial \
|
||||
--first-user-trial=false \
|
||||
--use-token-as-session \
|
||||
https://${{ env.PR_HOSTNAME }}
|
||||
|
||||
|
||||
+124
-5
@@ -267,12 +267,59 @@ func (r *RootCmd) login() *serpent.Command {
|
||||
trial = v == "yes" || v == "y"
|
||||
}
|
||||
|
||||
var trialInfo codersdk.CreateFirstUserTrialInfo
|
||||
if trial {
|
||||
if trialInfo.FirstName == "" {
|
||||
trialInfo.FirstName, err = promptTrialInfo(inv, "firstName")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if trialInfo.LastName == "" {
|
||||
trialInfo.LastName, err = promptTrialInfo(inv, "lastName")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if trialInfo.PhoneNumber == "" {
|
||||
trialInfo.PhoneNumber, err = promptTrialInfo(inv, "phoneNumber")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if trialInfo.JobTitle == "" {
|
||||
trialInfo.JobTitle, err = promptTrialInfo(inv, "jobTitle")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if trialInfo.CompanyName == "" {
|
||||
trialInfo.CompanyName, err = promptTrialInfo(inv, "companyName")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if trialInfo.Country == "" {
|
||||
trialInfo.Country, err = promptCountry(inv)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if trialInfo.Developers == "" {
|
||||
trialInfo.Developers, err = promptDevelopers(inv)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_, err = client.CreateFirstUser(ctx, codersdk.CreateFirstUserRequest{
|
||||
Email: email,
|
||||
Username: username,
|
||||
Name: name,
|
||||
Password: password,
|
||||
Trial: trial,
|
||||
Email: email,
|
||||
Username: username,
|
||||
Name: name,
|
||||
Password: password,
|
||||
Trial: trial,
|
||||
TrialInfo: trialInfo,
|
||||
})
|
||||
if err != nil {
|
||||
return xerrors.Errorf("create initial user: %w", err)
|
||||
@@ -449,3 +496,75 @@ func openURL(inv *serpent.Invocation, urlToOpen string) error {
|
||||
|
||||
return browser.OpenURL(urlToOpen)
|
||||
}
|
||||
|
||||
func promptTrialInfo(inv *serpent.Invocation, fieldName string) (string, error) {
|
||||
value, err := cliui.Prompt(inv, cliui.PromptOptions{
|
||||
Text: fmt.Sprintf("Please enter %s:", pretty.Sprint(cliui.DefaultStyles.Field, fieldName)),
|
||||
Validate: func(s string) error {
|
||||
if strings.TrimSpace(s) == "" {
|
||||
return xerrors.Errorf("%s is required", fieldName)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
if errors.Is(err, cliui.Canceled) {
|
||||
return "", nil
|
||||
}
|
||||
return "", err
|
||||
}
|
||||
return value, nil
|
||||
}
|
||||
|
||||
func promptDevelopers(inv *serpent.Invocation) (string, error) {
|
||||
options := []string{"1-100", "101-500", "501-1000", "1001-2500", "2500+"}
|
||||
selection, err := cliui.Select(inv, cliui.SelectOptions{
|
||||
Options: options,
|
||||
HideSearch: false,
|
||||
Message: "Select the number of developers:",
|
||||
})
|
||||
if err != nil {
|
||||
return "", xerrors.Errorf("select developers: %w", err)
|
||||
}
|
||||
return selection, nil
|
||||
}
|
||||
|
||||
func promptCountry(inv *serpent.Invocation) (string, error) {
|
||||
countries := []string{
|
||||
"Afghanistan", "Åland Islands", "Albania", "Algeria", "American Samoa", "Andorra", "Angola", "Anguilla", "Antarctica", "Antigua and Barbuda",
|
||||
"Argentina", "Armenia", "Aruba", "Australia", "Austria", "Azerbaijan", "Bahamas", "Bahrain", "Bangladesh", "Barbados",
|
||||
"Belarus", "Belgium", "Belize", "Benin", "Bermuda", "Bhutan", "Bolivia, Plurinational State of", "Bonaire, Sint Eustatius and Saba", "Bosnia and Herzegovina", "Botswana",
|
||||
"Bouvet Island", "Brazil", "British Indian Ocean Territory", "Brunei Darussalam", "Bulgaria", "Burkina Faso", "Burundi", "Cambodia", "Cameroon", "Canada",
|
||||
"Cape Verde", "Cayman Islands", "Central African Republic", "Chad", "Chile", "China", "Christmas Island", "Cocos (Keeling) Islands", "Colombia", "Comoros",
|
||||
"Congo", "Congo, the Democratic Republic of the", "Cook Islands", "Costa Rica", "Côte d'Ivoire", "Croatia", "Cuba", "Curaçao", "Cyprus", "Czech Republic",
|
||||
"Denmark", "Djibouti", "Dominica", "Dominican Republic", "Ecuador", "Egypt", "El Salvador", "Equatorial Guinea", "Eritrea", "Estonia",
|
||||
"Ethiopia", "Falkland Islands (Malvinas)", "Faroe Islands", "Fiji", "Finland", "France", "French Guiana", "French Polynesia", "French Southern Territories", "Gabon",
|
||||
"Gambia", "Georgia", "Germany", "Ghana", "Gibraltar", "Greece", "Greenland", "Grenada", "Guadeloupe", "Guam",
|
||||
"Guatemala", "Guernsey", "Guinea", "Guinea-Bissau", "Guyana", "Haiti", "Heard Island and McDonald Islands", "Holy See (Vatican City State)", "Honduras", "Hong Kong",
|
||||
"Hungary", "Iceland", "India", "Indonesia", "Iran, Islamic Republic of", "Iraq", "Ireland", "Isle of Man", "Israel", "Italy",
|
||||
"Jamaica", "Japan", "Jersey", "Jordan", "Kazakhstan", "Kenya", "Kiribati", "Korea, Democratic People's Republic of", "Korea, Republic of", "Kuwait",
|
||||
"Kyrgyzstan", "Lao People's Democratic Republic", "Latvia", "Lebanon", "Lesotho", "Liberia", "Libya", "Liechtenstein", "Lithuania", "Luxembourg",
|
||||
"Macao", "Macedonia, the Former Yugoslav Republic of", "Madagascar", "Malawi", "Malaysia", "Maldives", "Mali", "Malta", "Marshall Islands", "Martinique",
|
||||
"Mauritania", "Mauritius", "Mayotte", "Mexico", "Micronesia, Federated States of", "Moldova, Republic of", "Monaco", "Mongolia", "Montenegro", "Montserrat",
|
||||
"Morocco", "Mozambique", "Myanmar", "Namibia", "Nauru", "Nepal", "Netherlands", "New Caledonia", "New Zealand", "Nicaragua",
|
||||
"Niger", "Nigeria", "Niue", "Norfolk Island", "Northern Mariana Islands", "Norway", "Oman", "Pakistan", "Palau", "Palestine, State of",
|
||||
"Panama", "Papua New Guinea", "Paraguay", "Peru", "Philippines", "Pitcairn", "Poland", "Portugal", "Puerto Rico", "Qatar",
|
||||
"Réunion", "Romania", "Russian Federation", "Rwanda", "Saint Barthélemy", "Saint Helena, Ascension and Tristan da Cunha", "Saint Kitts and Nevis", "Saint Lucia", "Saint Martin (French part)", "Saint Pierre and Miquelon",
|
||||
"Saint Vincent and the Grenadines", "Samoa", "San Marino", "Sao Tome and Principe", "Saudi Arabia", "Senegal", "Serbia", "Seychelles", "Sierra Leone", "Singapore",
|
||||
"Sint Maarten (Dutch part)", "Slovakia", "Slovenia", "Solomon Islands", "Somalia", "South Africa", "South Georgia and the South Sandwich Islands", "South Sudan", "Spain", "Sri Lanka",
|
||||
"Sudan", "Suriname", "Svalbard and Jan Mayen", "Swaziland", "Sweden", "Switzerland", "Syrian Arab Republic", "Taiwan, Province of China", "Tajikistan", "Tanzania, United Republic of",
|
||||
"Thailand", "Timor-Leste", "Togo", "Tokelau", "Tonga", "Trinidad and Tobago", "Tunisia", "Turkey", "Turkmenistan", "Turks and Caicos Islands",
|
||||
"Tuvalu", "Uganda", "Ukraine", "United Arab Emirates", "United Kingdom", "United States", "United States Minor Outlying Islands", "Uruguay", "Uzbekistan", "Vanuatu",
|
||||
"Venezuela, Bolivarian Republic of", "Vietnam", "Virgin Islands, British", "Virgin Islands, U.S.", "Wallis and Futuna", "Western Sahara", "Yemen", "Zambia", "Zimbabwe",
|
||||
}
|
||||
|
||||
selection, err := cliui.Select(inv, cliui.SelectOptions{
|
||||
Options: countries,
|
||||
Message: "Select the country:",
|
||||
HideSearch: false,
|
||||
})
|
||||
if err != nil {
|
||||
return "", xerrors.Errorf("select country: %w", err)
|
||||
}
|
||||
return selection, nil
|
||||
}
|
||||
|
||||
+96
-1
@@ -96,6 +96,58 @@ func TestLogin(t *testing.T) {
|
||||
"password", coderdtest.FirstUserParams.Password,
|
||||
"password", coderdtest.FirstUserParams.Password, // confirm
|
||||
"trial", "yes",
|
||||
"firstName", coderdtest.TrialUserParams.FirstName,
|
||||
"lastName", coderdtest.TrialUserParams.LastName,
|
||||
"phoneNumber", coderdtest.TrialUserParams.PhoneNumber,
|
||||
"jobTitle", coderdtest.TrialUserParams.JobTitle,
|
||||
"companyName", coderdtest.TrialUserParams.CompanyName,
|
||||
// `developers` and `country` `cliui.Select` automatically selects the first option during tests.
|
||||
}
|
||||
for i := 0; i < len(matches); i += 2 {
|
||||
match := matches[i]
|
||||
value := matches[i+1]
|
||||
pty.ExpectMatch(match)
|
||||
pty.WriteLine(value)
|
||||
}
|
||||
pty.ExpectMatch("Welcome to Coder")
|
||||
<-doneChan
|
||||
ctx := testutil.Context(t, testutil.WaitShort)
|
||||
resp, err := client.LoginWithPassword(ctx, codersdk.LoginWithPasswordRequest{
|
||||
Email: coderdtest.FirstUserParams.Email,
|
||||
Password: coderdtest.FirstUserParams.Password,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
client.SetSessionToken(resp.SessionToken)
|
||||
me, err := client.User(ctx, codersdk.Me)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, coderdtest.FirstUserParams.Username, me.Username)
|
||||
assert.Equal(t, coderdtest.FirstUserParams.Name, me.Name)
|
||||
assert.Equal(t, coderdtest.FirstUserParams.Email, me.Email)
|
||||
})
|
||||
|
||||
t.Run("InitialUserTTYWithNoTrial", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
client := coderdtest.New(t, nil)
|
||||
// The --force-tty flag is required on Windows, because the `isatty` library does not
|
||||
// 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", "--force-tty", client.URL.String())
|
||||
pty := ptytest.New(t).Attach(root)
|
||||
go func() {
|
||||
defer close(doneChan)
|
||||
err := root.Run()
|
||||
assert.NoError(t, err)
|
||||
}()
|
||||
|
||||
matches := []string{
|
||||
"first user?", "yes",
|
||||
"username", coderdtest.FirstUserParams.Username,
|
||||
"name", coderdtest.FirstUserParams.Name,
|
||||
"email", coderdtest.FirstUserParams.Email,
|
||||
"password", coderdtest.FirstUserParams.Password,
|
||||
"password", coderdtest.FirstUserParams.Password, // confirm
|
||||
"trial", "no",
|
||||
}
|
||||
for i := 0; i < len(matches); i += 2 {
|
||||
match := matches[i]
|
||||
@@ -142,6 +194,12 @@ func TestLogin(t *testing.T) {
|
||||
"password", coderdtest.FirstUserParams.Password,
|
||||
"password", coderdtest.FirstUserParams.Password, // confirm
|
||||
"trial", "yes",
|
||||
"firstName", coderdtest.TrialUserParams.FirstName,
|
||||
"lastName", coderdtest.TrialUserParams.LastName,
|
||||
"phoneNumber", coderdtest.TrialUserParams.PhoneNumber,
|
||||
"jobTitle", coderdtest.TrialUserParams.JobTitle,
|
||||
"companyName", coderdtest.TrialUserParams.CompanyName,
|
||||
// `developers` and `country` `cliui.Select` automatically selects the first option during tests.
|
||||
}
|
||||
for i := 0; i < len(matches); i += 2 {
|
||||
match := matches[i]
|
||||
@@ -185,6 +243,12 @@ func TestLogin(t *testing.T) {
|
||||
"password", coderdtest.FirstUserParams.Password,
|
||||
"password", coderdtest.FirstUserParams.Password, // confirm
|
||||
"trial", "yes",
|
||||
"firstName", coderdtest.TrialUserParams.FirstName,
|
||||
"lastName", coderdtest.TrialUserParams.LastName,
|
||||
"phoneNumber", coderdtest.TrialUserParams.PhoneNumber,
|
||||
"jobTitle", coderdtest.TrialUserParams.JobTitle,
|
||||
"companyName", coderdtest.TrialUserParams.CompanyName,
|
||||
// `developers` and `country` `cliui.Select` automatically selects the first option during tests.
|
||||
}
|
||||
for i := 0; i < len(matches); i += 2 {
|
||||
match := matches[i]
|
||||
@@ -220,6 +284,17 @@ func TestLogin(t *testing.T) {
|
||||
)
|
||||
pty := ptytest.New(t).Attach(inv)
|
||||
w := clitest.StartWithWaiter(t, inv)
|
||||
pty.ExpectMatch("firstName")
|
||||
pty.WriteLine(coderdtest.TrialUserParams.FirstName)
|
||||
pty.ExpectMatch("lastName")
|
||||
pty.WriteLine(coderdtest.TrialUserParams.LastName)
|
||||
pty.ExpectMatch("phoneNumber")
|
||||
pty.WriteLine(coderdtest.TrialUserParams.PhoneNumber)
|
||||
pty.ExpectMatch("jobTitle")
|
||||
pty.WriteLine(coderdtest.TrialUserParams.JobTitle)
|
||||
pty.ExpectMatch("companyName")
|
||||
pty.WriteLine(coderdtest.TrialUserParams.CompanyName)
|
||||
// `developers` and `country` `cliui.Select` automatically selects the first option during tests.
|
||||
pty.ExpectMatch("Welcome to Coder")
|
||||
w.RequireSuccess()
|
||||
ctx := testutil.Context(t, testutil.WaitShort)
|
||||
@@ -248,6 +323,17 @@ func TestLogin(t *testing.T) {
|
||||
)
|
||||
pty := ptytest.New(t).Attach(inv)
|
||||
w := clitest.StartWithWaiter(t, inv)
|
||||
pty.ExpectMatch("firstName")
|
||||
pty.WriteLine(coderdtest.TrialUserParams.FirstName)
|
||||
pty.ExpectMatch("lastName")
|
||||
pty.WriteLine(coderdtest.TrialUserParams.LastName)
|
||||
pty.ExpectMatch("phoneNumber")
|
||||
pty.WriteLine(coderdtest.TrialUserParams.PhoneNumber)
|
||||
pty.ExpectMatch("jobTitle")
|
||||
pty.WriteLine(coderdtest.TrialUserParams.JobTitle)
|
||||
pty.ExpectMatch("companyName")
|
||||
pty.WriteLine(coderdtest.TrialUserParams.CompanyName)
|
||||
// `developers` and `country` `cliui.Select` automatically selects the first option during tests.
|
||||
pty.ExpectMatch("Welcome to Coder")
|
||||
w.RequireSuccess()
|
||||
ctx := testutil.Context(t, testutil.WaitShort)
|
||||
@@ -299,12 +385,21 @@ func TestLogin(t *testing.T) {
|
||||
// Validate that we reprompt for matching passwords.
|
||||
pty.ExpectMatch("Passwords do not match")
|
||||
pty.ExpectMatch("Enter a " + pretty.Sprint(cliui.DefaultStyles.Field, "password"))
|
||||
|
||||
pty.WriteLine(coderdtest.FirstUserParams.Password)
|
||||
pty.ExpectMatch("Confirm")
|
||||
pty.WriteLine(coderdtest.FirstUserParams.Password)
|
||||
pty.ExpectMatch("trial")
|
||||
pty.WriteLine("yes")
|
||||
pty.ExpectMatch("firstName")
|
||||
pty.WriteLine(coderdtest.TrialUserParams.FirstName)
|
||||
pty.ExpectMatch("lastName")
|
||||
pty.WriteLine(coderdtest.TrialUserParams.LastName)
|
||||
pty.ExpectMatch("phoneNumber")
|
||||
pty.WriteLine(coderdtest.TrialUserParams.PhoneNumber)
|
||||
pty.ExpectMatch("jobTitle")
|
||||
pty.WriteLine(coderdtest.TrialUserParams.JobTitle)
|
||||
pty.ExpectMatch("companyName")
|
||||
pty.WriteLine(coderdtest.TrialUserParams.CompanyName)
|
||||
pty.ExpectMatch("Welcome to Coder")
|
||||
<-doneChan
|
||||
})
|
||||
|
||||
@@ -653,6 +653,16 @@ var FirstUserParams = codersdk.CreateFirstUserRequest{
|
||||
Name: "Test User",
|
||||
}
|
||||
|
||||
var TrialUserParams = codersdk.CreateFirstUserTrialInfo{
|
||||
FirstName: "John",
|
||||
LastName: "Doe",
|
||||
PhoneNumber: "9999999999",
|
||||
JobTitle: "Engineer",
|
||||
CompanyName: "Acme Inc",
|
||||
Country: "United States",
|
||||
Developers: "10-50",
|
||||
}
|
||||
|
||||
// CreateFirstUser creates a user with preset credentials and authenticates
|
||||
// with the passed in codersdk client.
|
||||
func CreateFirstUser(t testing.TB, client *codersdk.Client) codersdk.CreateFirstUserResponse {
|
||||
|
||||
+1
-1
@@ -164,7 +164,7 @@ fatal() {
|
||||
|
||||
if [ ! -f "${PROJECT_ROOT}/.coderv2/developsh-did-first-setup" ]; then
|
||||
# Try to create the initial admin user.
|
||||
if "${CODER_DEV_SHIM}" login http://127.0.0.1:3000 --first-user-username=admin --first-user-email=admin@coder.com --first-user-password="${password}" --first-user-full-name="Admin User" --first-user-trial=true; then
|
||||
if "${CODER_DEV_SHIM}" login http://127.0.0.1:3000 --first-user-username=admin --first-user-email=admin@coder.com --first-user-password="${password}" --first-user-full-name="Admin User" --first-user-trial=false; then
|
||||
# Only create this file if an admin user was successfully
|
||||
# created, otherwise we won't retry on a later attempt.
|
||||
touch "${PROJECT_ROOT}/.coderv2/developsh-did-first-setup"
|
||||
|
||||
Generated
+1
-1
@@ -964,7 +964,7 @@ export const countries = [
|
||||
flag: "🇻🇪",
|
||||
},
|
||||
{
|
||||
name: "Viet Nam",
|
||||
name: "Vietnam",
|
||||
flag: "🇻🇳",
|
||||
},
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user