Skip to content

Commit 6867852

Browse files
authored
feat: migrate database stack prompts from survey to huh (#130)
Replace survey-based AskQuestions in database.go with extracted huh form builder functions (DatabaseEngineForm, DatabaseAuroraForm, DatabaseInstanceClassForm, DatabaseMultiAZForm). Add teatest-based tests for all four form builders. Closes #122
1 parent 62ebcb6 commit 6867852

2 files changed

Lines changed: 236 additions & 72 deletions

File tree

stacks/database.go

Lines changed: 129 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,13 @@ import (
66
"fmt"
77
"strings"
88

9-
"github.com/AlecAivazis/survey/v2"
109
"github.com/apppackio/apppack/bridge"
1110
"github.com/apppackio/apppack/ui"
1211
"github.com/aws/aws-sdk-go-v2/aws"
1312
"github.com/aws/aws-sdk-go-v2/service/cloudformation/types"
1413
"github.com/aws/aws-sdk-go-v2/service/rds"
1514
rdstypes "github.com/aws/aws-sdk-go-v2/service/rds/types"
15+
"github.com/charmbracelet/huh"
1616
"github.com/sirupsen/logrus"
1717
"github.com/spf13/pflag"
1818
)
@@ -275,12 +275,8 @@ func (a *DatabaseStack) UpdateFromFlags(flags *pflag.FlagSet) error {
275275
}
276276

277277
func (a *DatabaseStack) AskQuestions(cfg aws.Config) error {
278-
var questions []*ui.QuestionExtra
279-
280278
var err error
281279

282-
var aurora bool
283-
284280
if a.Stack == nil {
285281
err = AskForCluster(
286282
cfg,
@@ -292,39 +288,19 @@ func (a *DatabaseStack) AskQuestions(cfg aws.Config) error {
292288
return err
293289
}
294290

295-
questions = append(questions, []*ui.QuestionExtra{
296-
{
297-
Verbose: "What engine should this Database use?",
298-
HelpText: "",
299-
Question: &survey.Question{
300-
Name: "Engine",
301-
Prompt: &survey.Select{
302-
Message: "Type",
303-
Options: []string{"postgres", "mysql"},
304-
FilterMessage: "",
305-
Default: "postgres",
306-
},
307-
Validate: survey.Required,
308-
},
309-
},
310-
{
311-
Verbose: "Should this Database use the Aurora engine variant?",
312-
HelpText: "Aurora provides many benefits over the standard engines, but is not available on very small instance sizes. For more info see https://aws.amazon.com/rds/aurora/.",
313-
WriteTo: &ui.BooleanOptionProxy{Value: &aurora},
314-
Question: &survey.Question{
315-
Prompt: &survey.Select{
316-
Message: "Aurora",
317-
Options: []string{"yes", "no"},
318-
FilterMessage: "",
319-
Default: ui.BooleanAsYesNo(aurora),
320-
},
321-
Validate: survey.Required,
322-
},
323-
},
324-
}...)
325-
if err = ui.AskQuestions(questions, a.Parameters); err != nil {
291+
// Engine prompt
292+
engineForm, enginePtr := DatabaseEngineForm(a.Parameters.Engine)
293+
if err = engineForm.Run(); err != nil {
294+
return err
295+
}
296+
a.Parameters.Engine = *enginePtr
297+
298+
// Aurora prompt
299+
auroraForm, auroraPtr := DatabaseAuroraForm(false)
300+
if err = auroraForm.Run(); err != nil {
326301
return err
327302
}
303+
aurora := ui.YesNoToBool(*auroraPtr)
328304

329305
ui.StartSpinner()
330306

@@ -342,8 +318,6 @@ func (a *DatabaseStack) AskQuestions(cfg aws.Config) error {
342318
if err != nil {
343319
return err
344320
}
345-
346-
questions = []*ui.QuestionExtra{}
347321
}
348322

349323
ui.StartSpinner()
@@ -356,40 +330,123 @@ func (a *DatabaseStack) AskQuestions(cfg aws.Config) error {
356330

357331
ui.Spinner.Stop()
358332
ui.Spinner.Suffix = ""
359-
questions = append(questions, []*ui.QuestionExtra{
360-
{
361-
Verbose: "What instance class should be used for this Database?",
362-
HelpText: "Enter the Database instance class. For more info see https://aws.amazon.com/rds/pricing/.",
363-
Question: &survey.Question{
364-
Name: "InstanceClass",
365-
Prompt: &survey.Select{
366-
Message: "Instance Class",
367-
Options: instanceClasses,
368-
FilterMessage: "",
369-
Default: a.Parameters.InstanceClass,
370-
},
371-
Validate: survey.Required,
372-
},
373-
},
374-
{
375-
Verbose: "Should this Database be setup in multiple availability zones?",
376-
HelpText: "Multiple availability zones (AZs) provide more resilience in the case of an AZ outage, " +
377-
"but double the cost at AWS. In the case of Aurora databases, enabling multiple availability zones will give you access to a read-replica." +
378-
"For more info see https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/Concepts.MultiAZ.html.",
379-
WriteTo: &ui.BooleanOptionProxy{Value: &a.Parameters.MultiAZ},
380-
Question: &survey.Question{
381-
Prompt: &survey.Select{
382-
Message: "Multi AZ",
383-
Options: []string{"yes", "no"},
384-
FilterMessage: "",
385-
Default: ui.BooleanAsYesNo(a.Parameters.MultiAZ),
386-
},
387-
Validate: survey.Required,
388-
},
389-
},
390-
}...)
391-
392-
return ui.AskQuestions(questions, a.Parameters)
333+
334+
// Instance class prompt
335+
instanceClassForm, instanceClassPtr := DatabaseInstanceClassForm(instanceClasses, a.Parameters.InstanceClass)
336+
if err = instanceClassForm.Run(); err != nil {
337+
return err
338+
}
339+
a.Parameters.InstanceClass = *instanceClassPtr
340+
341+
// Multi-AZ prompt
342+
multiAZForm, multiAZPtr := DatabaseMultiAZForm(a.Parameters.MultiAZ)
343+
if err = multiAZForm.Run(); err != nil {
344+
return err
345+
}
346+
a.Parameters.MultiAZ = ui.YesNoToBool(*multiAZPtr)
347+
348+
return nil
349+
}
350+
351+
// DatabaseEngineForm builds the interactive form for selecting the database engine.
352+
// Returns the form and a pointer to the selected engine value.
353+
func DatabaseEngineForm(defaultEngine string) (*huh.Form, *string) {
354+
if defaultEngine == "" {
355+
defaultEngine = "postgres"
356+
}
357+
selected := defaultEngine
358+
359+
options := []huh.Option[string]{
360+
huh.NewOption("postgres", "postgres"),
361+
huh.NewOption("mysql", "mysql"),
362+
}
363+
if defaultEngine == "mysql" {
364+
options[1] = options[1].Selected(true)
365+
} else {
366+
options[0] = options[0].Selected(true)
367+
}
368+
369+
form := huh.NewForm(
370+
huh.NewGroup(
371+
huh.NewNote().
372+
Title("What engine should this Database use?"),
373+
huh.NewSelect[string]().
374+
Title("Type").
375+
Options(options...).
376+
Value(&selected),
377+
),
378+
)
379+
380+
return form, &selected
381+
}
382+
383+
// DatabaseAuroraForm builds the interactive form for selecting Aurora mode.
384+
// Returns the form and a pointer to the selected "yes"/"no" value.
385+
func DatabaseAuroraForm(defaultAurora bool) (*huh.Form, *string) {
386+
selected := ui.BooleanAsYesNo(defaultAurora)
387+
388+
form := huh.NewForm(
389+
huh.NewGroup(
390+
huh.NewNote().
391+
Title("Should this Database use the Aurora engine variant?").
392+
Description("Aurora provides many benefits over the standard engines, but is not available on very small\ninstance sizes. For more info see https://aws.amazon.com/rds/aurora/."),
393+
huh.NewSelect[string]().
394+
Title("Aurora").
395+
Options(ui.YesNoOptions(defaultAurora)...).
396+
Value(&selected),
397+
),
398+
)
399+
400+
return form, &selected
401+
}
402+
403+
// DatabaseInstanceClassForm builds the interactive form for selecting a database instance class.
404+
// Returns the form and a pointer to the selected instance class.
405+
func DatabaseInstanceClassForm(instanceClasses []string, defaultClass string) (*huh.Form, *string) {
406+
selected := defaultClass
407+
408+
options := make([]huh.Option[string], len(instanceClasses))
409+
for i, c := range instanceClasses {
410+
opt := huh.NewOption(c, c)
411+
if c == defaultClass {
412+
opt = opt.Selected(true)
413+
}
414+
options[i] = opt
415+
}
416+
417+
form := huh.NewForm(
418+
huh.NewGroup(
419+
huh.NewNote().
420+
Title("What instance class should be used for this Database?").
421+
Description("Enter the Database instance class. For more info see https://aws.amazon.com/rds/pricing/."),
422+
huh.NewSelect[string]().
423+
Title("Instance Class").
424+
Options(options...).
425+
Value(&selected),
426+
),
427+
)
428+
429+
return form, &selected
430+
}
431+
432+
// DatabaseMultiAZForm builds the interactive form for selecting multi-AZ mode.
433+
// Returns the form and a pointer to the selected "yes"/"no" value.
434+
func DatabaseMultiAZForm(defaultMultiAZ bool) (*huh.Form, *string) {
435+
selected := ui.BooleanAsYesNo(defaultMultiAZ)
436+
437+
form := huh.NewForm(
438+
huh.NewGroup(
439+
huh.NewNote().
440+
Title("Should this Database be setup in multiple availability zones?").
441+
Description("Multiple availability zones (AZs) provide more resilience in the case of an AZ outage,\nbut double the cost at AWS. In the case of Aurora databases, enabling multiple availability\nzones will give you access to a read-replica. For more info see\nhttps://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/Concepts.MultiAZ.html."),
442+
huh.NewSelect[string]().
443+
Title("Multi AZ").
444+
Options(ui.YesNoOptions(defaultMultiAZ)...).
445+
Value(&selected),
446+
),
447+
)
448+
449+
return form, &selected
393450
}
394451

395452
func (*DatabaseStack) StackName(name *string) *string {

stacks/database_test.go

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
package stacks
2+
3+
import (
4+
"testing"
5+
6+
"github.com/apppackio/apppack/ui/uitest"
7+
)
8+
9+
func TestDatabaseEngineForm_DefaultPostgres(t *testing.T) {
10+
form, selectedPtr := DatabaseEngineForm("postgres")
11+
tm := uitest.RunForm(t, form)
12+
uitest.SelectFirst(tm) // pass Note
13+
uitest.SelectFirst(tm) // accept default (postgres)
14+
uitest.WaitDone(t, tm)
15+
16+
if *selectedPtr != "postgres" {
17+
t.Errorf("expected 'postgres', got %q", *selectedPtr)
18+
}
19+
}
20+
21+
func TestDatabaseEngineForm_SelectMySQL(t *testing.T) {
22+
form, selectedPtr := DatabaseEngineForm("postgres")
23+
tm := uitest.RunForm(t, form)
24+
uitest.SelectFirst(tm) // pass Note
25+
uitest.SelectNth(tm, 1) // select mysql
26+
uitest.WaitDone(t, tm)
27+
28+
if *selectedPtr != "mysql" {
29+
t.Errorf("expected 'mysql', got %q", *selectedPtr)
30+
}
31+
}
32+
33+
func TestDatabaseAuroraForm_DefaultNo(t *testing.T) {
34+
form, selectedPtr := DatabaseAuroraForm(false)
35+
tm := uitest.RunForm(t, form)
36+
uitest.SelectFirst(tm) // pass Note
37+
uitest.SelectFirst(tm) // accept default (no)
38+
uitest.WaitDone(t, tm)
39+
40+
if *selectedPtr != "no" {
41+
t.Errorf("expected 'no', got %q", *selectedPtr)
42+
}
43+
}
44+
45+
func TestDatabaseAuroraForm_DefaultYes(t *testing.T) {
46+
form, selectedPtr := DatabaseAuroraForm(true)
47+
tm := uitest.RunForm(t, form)
48+
uitest.SelectFirst(tm) // pass Note
49+
uitest.SelectFirst(tm) // accept default (yes)
50+
uitest.WaitDone(t, tm)
51+
52+
if *selectedPtr != "yes" {
53+
t.Errorf("expected 'yes', got %q", *selectedPtr)
54+
}
55+
}
56+
57+
func TestDatabaseInstanceClassForm_SelectDefault(t *testing.T) {
58+
classes := []string{"db.t4g.medium", "db.t4g.large", "db.r6g.large"}
59+
60+
form, selectedPtr := DatabaseInstanceClassForm(classes, "db.t4g.medium")
61+
tm := uitest.RunForm(t, form)
62+
uitest.SelectFirst(tm) // pass Note
63+
uitest.SelectFirst(tm) // accept default
64+
uitest.WaitDone(t, tm)
65+
66+
if *selectedPtr != "db.t4g.medium" {
67+
t.Errorf("expected 'db.t4g.medium', got %q", *selectedPtr)
68+
}
69+
}
70+
71+
func TestDatabaseInstanceClassForm_SelectSecond(t *testing.T) {
72+
classes := []string{"db.t4g.medium", "db.t4g.large", "db.r6g.large"}
73+
74+
form, selectedPtr := DatabaseInstanceClassForm(classes, "db.t4g.medium")
75+
tm := uitest.RunForm(t, form)
76+
uitest.SelectFirst(tm) // pass Note
77+
uitest.SelectNth(tm, 1) // select second option
78+
uitest.WaitDone(t, tm)
79+
80+
if *selectedPtr != "db.t4g.large" {
81+
t.Errorf("expected 'db.t4g.large', got %q", *selectedPtr)
82+
}
83+
}
84+
85+
func TestDatabaseMultiAZForm_DefaultNo(t *testing.T) {
86+
form, selectedPtr := DatabaseMultiAZForm(false)
87+
tm := uitest.RunForm(t, form)
88+
uitest.SelectFirst(tm) // pass Note
89+
uitest.SelectFirst(tm) // accept default (no)
90+
uitest.WaitDone(t, tm)
91+
92+
if *selectedPtr != "no" {
93+
t.Errorf("expected 'no', got %q", *selectedPtr)
94+
}
95+
}
96+
97+
func TestDatabaseMultiAZForm_DefaultYes(t *testing.T) {
98+
form, selectedPtr := DatabaseMultiAZForm(true)
99+
tm := uitest.RunForm(t, form)
100+
uitest.SelectFirst(tm) // pass Note
101+
uitest.SelectFirst(tm) // accept default (yes)
102+
uitest.WaitDone(t, tm)
103+
104+
if *selectedPtr != "yes" {
105+
t.Errorf("expected 'yes', got %q", *selectedPtr)
106+
}
107+
}

0 commit comments

Comments
 (0)