Skip to content

Commit 52be561

Browse files
feat: introducing plain prompts (#37)
* feat: introducing plain prompts plain prompts do not leverage survey package. survey package does not behave well with stdin redirection so it is quite hard to test an interactive installation. with plain prompts this should be straight forward to integrate with tools like `expect`. this commit also adds an end-to-end test that uses plain prompts. * Update cmd/helmvm/token.go Co-authored-by: Ethan Mosbaugh <[email protected]> * chore: using interfaces for prompt structs --------- Co-authored-by: Ethan Mosbaugh <[email protected]>
1 parent d419fca commit 52be561

File tree

15 files changed

+319
-167
lines changed

15 files changed

+319
-167
lines changed

.github/workflows/e2e.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,14 @@ jobs:
99
tests:
1010
- TestBuildBundle
1111
- TestEmbedAndInstall
12-
- TestInstallSingleNodeAndUpgradeToEmbed
1312
- TestMultiNodeInstallation
1413
- TestSingleNodeInstallation
1514
- TestTokenBasedMultiNodeInstallation
1615
- TestSingleNodeInstallationRockyLinux8
1716
- TestSingleNodeInstallationDebian12
1817
- TestSingleNodeInstallationCentos8Stream
1918
- TestVersion
19+
- TestMultiNodeInteractiveInstallation
2020
steps:
2121
- name: Move Docker aside
2222
run: |

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,7 @@ e2e-test: helmvm-linux-amd64
144144
mkdir -p output/tmp
145145
rm -rf output/tmp/id_rsa*
146146
ssh-keygen -t rsa -N "" -C "Integration Test Key" -f output/tmp/id_rsa
147-
go test -timeout 10m -v ./e2e -run $(TEST_NAME)$
147+
go test -timeout 30m -v ./e2e -run $(TEST_NAME)$
148148

149149
.PHONY: create-e2e-workflows
150150
create-e2e-workflows:

cmd/helmvm/install.go

Lines changed: 20 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ import (
1212
"strings"
1313
"time"
1414

15-
"github.com/AlecAivazis/survey/v2"
1615
"github.com/k0sproject/k0sctl/pkg/apis/k0sctl.k0sproject.io/v1beta1/cluster"
1716
"github.com/k0sproject/rig"
1817
"github.com/k0sproject/rig/log"
@@ -26,6 +25,7 @@ import (
2625
"github.com/replicatedhq/helmvm/pkg/goods"
2726
"github.com/replicatedhq/helmvm/pkg/infra"
2827
pb "github.com/replicatedhq/helmvm/pkg/progressbar"
28+
"github.com/replicatedhq/helmvm/pkg/prompts"
2929
)
3030

3131
// runPostApply is meant to run things that can't be run automatically with
@@ -150,25 +150,19 @@ func copyUserProvidedConfig(c *cli.Context) error {
150150

151151
// overwriteExistingConfig asks user if they want to overwrite the existing cluster
152152
// configuration file.
153-
func overwriteExistingConfig() (bool, error) {
154-
var useCurrent = &survey.Confirm{
155-
Message: "Do you want to create a new cluster configuration ?",
156-
Default: false,
157-
}
153+
func overwriteExistingConfig() bool {
158154
logrus.Warn("A cluster configuration file was found. This means you already")
159-
logrus.Warn("have created a cluster configured. You can either use the existing")
160-
logrus.Warn("configuration or create a new one (the original configuration will")
161-
logrus.Warn("be backed up).")
162-
var answer bool
163-
if err := survey.AskOne(useCurrent, &answer); err != nil {
164-
return false, err
165-
}
166-
return answer, nil
155+
logrus.Warn("have created and configured a cluster. You can either use the")
156+
logrus.Warn("existing configuration or create a new one (the original config")
157+
logrus.Warn("will be backed up).")
158+
return prompts.New().Confirm(
159+
"Do you want to create a new cluster configuration ?", false,
160+
)
167161
}
168162

169163
// ensureK0sctlConfig ensures that a k0sctl.yaml file exists in the configuration
170164
// directory. If none exists then this directs the user to a wizard to create one.
171-
func ensureK0sctlConfig(c *cli.Context, nodes []infra.Node, prompt bool) error {
165+
func ensureK0sctlConfig(c *cli.Context, nodes []infra.Node, useprompt bool) error {
172166
multi := c.Bool("multi-node") || len(nodes) > 0
173167
if !multi && runtime.GOOS != "linux" {
174168
return fmt.Errorf("single node clusters only supported on linux")
@@ -182,12 +176,10 @@ func ensureK0sctlConfig(c *cli.Context, nodes []infra.Node, prompt bool) error {
182176
}
183177
if _, err := os.Stat(cfgpath); err == nil {
184178
if len(nodes) == 0 {
185-
if !prompt {
179+
if !useprompt {
186180
return updateConfigBundle(c.Context, bundledir)
187181
}
188-
if over, err := overwriteExistingConfig(); err != nil {
189-
return fmt.Errorf("unable to process answers: %w", err)
190-
} else if !over {
182+
if !overwriteExistingConfig() {
191183
return updateConfigBundle(c.Context, bundledir)
192184
}
193185
}
@@ -276,26 +268,20 @@ func dumpApplyLogs() {
276268

277269
// applyK0sctl runs the k0sctl apply command and waits for it to finish. If
278270
// no configuration is found one is generated.
279-
func applyK0sctl(c *cli.Context, prompt bool, nodes []infra.Node) error {
271+
func applyK0sctl(c *cli.Context, useprompt bool, nodes []infra.Node) error {
280272
logrus.Infof("Processing cluster configuration")
281-
if err := ensureK0sctlConfig(c, nodes, prompt); err != nil {
273+
if err := ensureK0sctlConfig(c, nodes, useprompt); err != nil {
282274
return fmt.Errorf("unable to create config file: %w", err)
283275
}
284276
logrus.Infof("Applying cluster configuration")
285277
if err := runK0sctlApply(c.Context); err != nil {
286278
logrus.Errorf("Installation or upgrade failed.")
287-
if !prompt {
279+
if !useprompt {
288280
dumpApplyLogs()
289281
return fmt.Errorf("unable to apply cluster: %w", err)
290282
}
291-
var useCurrent = &survey.Confirm{
292-
Message: "Do you wish to visualize the logs?",
293-
Default: true,
294-
}
295-
var answer bool
296-
if err := survey.AskOne(useCurrent, &answer); err != nil {
297-
return fmt.Errorf("unable to process answers: %w", err)
298-
} else if answer {
283+
msg := "Do you wish to visualize the logs?"
284+
if prompts.New().Confirm(msg, true) {
299285
dumpApplyLogs()
300286
}
301287
return fmt.Errorf("unable to apply cluster: %w", err)
@@ -348,7 +334,7 @@ var installCommand = &cli.Command{
348334
logrus.Warnf("Run '%s node --help' for more information.", defaults.BinaryName())
349335
return fmt.Errorf("decentralized install detected")
350336
}
351-
prompt := !c.Bool("no-prompt")
337+
useprompt := !c.Bool("no-prompt")
352338
logrus.Infof("Materializing binaries")
353339
if err := goods.Materialize(); err != nil {
354340
return fmt.Errorf("unable to materialize binaries: %w", err)
@@ -358,11 +344,11 @@ var installCommand = &cli.Command{
358344
var nodes []infra.Node
359345
if dir := c.String("infra"); dir != "" {
360346
logrus.Infof("Processing infrastructure manifests")
361-
if nodes, err = infra.Apply(c.Context, dir, prompt); err != nil {
347+
if nodes, err = infra.Apply(c.Context, dir, useprompt); err != nil {
362348
return fmt.Errorf("unable to create infra: %w", err)
363349
}
364350
}
365-
if err := applyK0sctl(c, prompt, nodes); err != nil {
351+
if err := applyK0sctl(c, useprompt, nodes); err != nil {
366352
return fmt.Errorf("unable update cluster: %w", err)
367353
}
368354
}
@@ -374,7 +360,7 @@ var installCommand = &cli.Command{
374360
ccfg := defaults.PathToConfig("k0sctl.yaml")
375361
kcfg := defaults.PathToConfig("kubeconfig")
376362
os.Setenv("KUBECONFIG", kcfg)
377-
if applier, err := addons.NewApplier(prompt, true); err != nil {
363+
if applier, err := addons.NewApplier(useprompt, true); err != nil {
378364
return fmt.Errorf("unable to create applier: %w", err)
379365
} else if err := applier.Apply(c.Context); err != nil {
380366
return fmt.Errorf("unable to apply addons: %w", err)

cmd/helmvm/token.go

Lines changed: 4 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,11 @@ import (
99
"runtime"
1010
"time"
1111

12-
"github.com/AlecAivazis/survey/v2"
1312
"github.com/sirupsen/logrus"
1413
"github.com/urfave/cli/v2"
1514

1615
"github.com/replicatedhq/helmvm/pkg/defaults"
16+
"github.com/replicatedhq/helmvm/pkg/prompts"
1717
)
1818

1919
var tokenCommands = &cli.Command{
@@ -53,7 +53,7 @@ var tokenCreateCommand = &cli.Command{
5353
if role != "worker" && role != "controller" {
5454
return fmt.Errorf("invalid role %q", role)
5555
}
56-
prompt := !c.Bool("no-prompt")
56+
useprompt := !c.Bool("no-prompt")
5757
cfgpath := defaults.PathToConfig("k0sctl.yaml")
5858
if _, err := os.Stat(cfgpath); err != nil {
5959
if os.IsNotExist(err) {
@@ -69,17 +69,8 @@ var tokenCreateCommand = &cli.Command{
6969
logrus.Warn("Through the centralized management you can manage all your")
7070
logrus.Warn("cluster nodes from a single location. If you decide to move")
7171
logrus.Warn("on the centralized management won't be available anymore")
72-
if prompt {
73-
question := &survey.Confirm{
74-
Message: "Do you want to use continue ?",
75-
Default: true,
76-
}
77-
var moveOn bool
78-
if err := survey.AskOne(question, &moveOn); err != nil {
79-
return fmt.Errorf("unable to ask for confirmation: %w", err)
80-
} else if !moveOn {
81-
return nil
82-
}
72+
if useprompt && !prompts.New().Confirm("Do you want to continue ?", true) {
73+
return nil
8374
}
8475
}
8576
dur := c.Duration("expiry").String()

e2e/cluster/cluster.go

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,10 +27,9 @@ lxc.mount.entry = /dev/kmsg dev/kmsg none defaults,bind,create=file`
2727
const checkInternet = `#!/bin/bash
2828
timeout 5 bash -c 'cat < /dev/null > /dev/tcp/www.replicated.com/80'
2929
if [ $? == 0 ]; then
30-
echo "Internet connectivity is up"
3130
exit 0
3231
fi
33-
echo "Internet connectivity is down"
32+
echo "Internet connection is down"
3433
exit 1
3534
`
3635

e2e/embed_test.go

Lines changed: 0 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -33,36 +33,3 @@ func TestEmbedAndInstall(t *testing.T) {
3333
t.Fatalf("fail to create deployment with pvc: %v", err)
3434
}
3535
}
36-
37-
func TestInstallSingleNodeAndUpgradeToEmbed(t *testing.T) {
38-
t.Parallel()
39-
tc := cluster.NewTestCluster(&cluster.Input{
40-
T: t,
41-
Nodes: 1,
42-
Image: "ubuntu/jammy",
43-
SSHPublicKey: "../output/tmp/id_rsa.pub",
44-
SSHPrivateKey: "../output/tmp/id_rsa",
45-
HelmVMPath: "../output/bin/helmvm",
46-
})
47-
defer tc.Destroy()
48-
t.Log("installing ssh in node 0")
49-
line := []string{"apt", "install", "openssh-server", "-y"}
50-
if _, _, err := RunCommandOnNode(t, tc, 0, line); err != nil {
51-
t.Fatalf("fail to install ssh on node %s: %v", tc.Nodes[0], err)
52-
}
53-
t.Log("installing helmvm on node 0")
54-
line = []string{"single-node-install.sh"}
55-
if _, _, err := RunCommandOnNode(t, tc, 0, line); err != nil {
56-
t.Fatalf("fail to install helmvm on node 0: %v", err)
57-
}
58-
t.Log("installing helmvm embed with memcached on node 0")
59-
line = []string{"embed-and-install.sh"}
60-
if _, _, err := RunCommandOnNode(t, tc, 0, line); err != nil {
61-
t.Fatalf("fail to install embed helmvm on node 0: %v", err)
62-
}
63-
t.Log("creating deployment mounting pvc")
64-
line = []string{"deploy-with-pvc.sh"}
65-
if _, _, err := RunCommandOnNode(t, tc, 0, line); err != nil {
66-
t.Fatalf("fail to create deployment with pvc: %v", err)
67-
}
68-
}

e2e/install_test.go

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,3 +193,39 @@ func TestSingleNodeInstallationCentos8Stream(t *testing.T) {
193193
t.Fatalf("fail to create deployment with pvc: %v", err)
194194
}
195195
}
196+
197+
func TestMultiNodeInteractiveInstallation(t *testing.T) {
198+
t.Parallel()
199+
t.Log("creating cluster")
200+
tc := cluster.NewTestCluster(&cluster.Input{
201+
T: t,
202+
Nodes: 3,
203+
Image: "ubuntu/jammy",
204+
SSHPublicKey: "../output/tmp/id_rsa.pub",
205+
SSHPrivateKey: "../output/tmp/id_rsa",
206+
HelmVMPath: "../output/bin/helmvm",
207+
})
208+
defer tc.Destroy()
209+
for i := range tc.Nodes {
210+
t.Logf("installing ssh on node %d", i)
211+
line := []string{"apt", "install", "openssh-server", "-y"}
212+
if _, _, err := RunCommandOnNode(t, tc, i, line); err != nil {
213+
t.Fatalf("fail to install ssh on node %d: %v", i, err)
214+
}
215+
}
216+
t.Logf("installing expect on node 0")
217+
line := []string{"apt", "install", "expect", "-y"}
218+
if _, _, err := RunCommandOnNode(t, tc, 0, line); err != nil {
219+
t.Fatalf("fail to install expect on node 0: %v", err)
220+
}
221+
t.Log("running multi node interactive install from node 0")
222+
line = []string{"interactive-multi-node-install.exp"}
223+
if _, _, err := RunCommandOnNode(t, tc, 0, line); err != nil {
224+
t.Fatalf("fail to install helmvm from node 0: %v", err)
225+
}
226+
t.Log("waiting for cluster nodes to report ready")
227+
line = []string{"wait-for-ready-nodes.sh", "3"}
228+
if _, _, err := RunCommandOnNode(t, tc, 0, line); err != nil {
229+
t.Fatalf("nodes not reporting ready: %v", err)
230+
}
231+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
#!/usr/bin/env expect
2+
3+
proc configure_node {address} {
4+
set timeout 30
5+
expect "Node address:" { send "$address\r" }
6+
expect "SSH user:" { send "root\r" }
7+
expect "SSH port:" { send "22\r" }
8+
expect "Type one of the options above:" { send "controller+worker\r" }
9+
expect "Type one of the options above:" { send "/root/.ssh/id_rsa\r" }
10+
}
11+
12+
set env(HELMVM_PLAIN_PROMPTS) "true"
13+
spawn helmvm install --multi-node
14+
configure_node "10.0.0.2"
15+
expect -re "Add another node?.*:" { send "y\r" }
16+
configure_node "10.0.0.3"
17+
expect -re "Add another node?.*:" { send "y\r" }
18+
configure_node "10.0.0.4"
19+
expect -re "Add another node?.*:" { send "n\r" }
20+
set timeout 600
21+
expect "Load balancer address:" { send "\r" }
22+
expect "Enter a new Admin Console password:" { send "password\r" }
23+
expect "You can now access your cluster with kubectl"

e2e/scripts/zz-scripts.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,5 @@ package scripts
55

66
import "embed"
77

8-
//go:embed *.sh
8+
//go:embed *
99
var FS embed.FS

pkg/addons/adminconsole/adminconsole.go

Lines changed: 6 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ import (
77
"fmt"
88
"strings"
99

10-
"github.com/AlecAivazis/survey/v2"
1110
"github.com/sirupsen/logrus"
1211
"golang.org/x/mod/semver"
1312
"helm.sh/helm/v3/pkg/action"
@@ -16,6 +15,7 @@ import (
1615
"helm.sh/helm/v3/pkg/release"
1716

1817
"github.com/replicatedhq/helmvm/pkg/addons/adminconsole/charts"
18+
"github.com/replicatedhq/helmvm/pkg/prompts"
1919
)
2020

2121
const (
@@ -37,24 +37,15 @@ type AdminConsole struct {
3737
config *action.Configuration
3838
logger action.DebugLog
3939
namespace string
40-
prompt bool
40+
useprompt bool
4141
}
4242

4343
func (a *AdminConsole) askPassword() (string, error) {
44-
if !a.prompt {
44+
if !a.useprompt {
4545
logrus.Warnf("Admin Console password set to: password")
4646
return "password", nil
4747
}
48-
question := &survey.Password{Message: "Enter a new Admin Console password:"}
49-
var pass string
50-
for pass == "" {
51-
if err := survey.AskOne(question, &pass); err != nil {
52-
return "", fmt.Errorf("unable to ask for password: %w", err)
53-
} else if pass == "" {
54-
logrus.Warn("Password cannot be empty")
55-
}
56-
}
57-
return pass, nil
48+
return prompts.New().Password("Enter a new Admin Console password:"), nil
5849
}
5950

6051
func (a *AdminConsole) Version() (map[string]string, error) {
@@ -166,7 +157,7 @@ func (a *AdminConsole) installedRelease(ctx context.Context) (*release.Release,
166157
return releases[0], nil
167158
}
168159

169-
func New(ns string, prompt bool, log action.DebugLog) (*AdminConsole, error) {
160+
func New(ns string, useprompt bool, log action.DebugLog) (*AdminConsole, error) {
170161
env := cli.New()
171162
env.SetNamespace(ns)
172163
config := &action.Configuration{}
@@ -177,7 +168,7 @@ func New(ns string, prompt bool, log action.DebugLog) (*AdminConsole, error) {
177168
namespace: ns,
178169
config: config,
179170
logger: log,
180-
prompt: prompt,
171+
useprompt: useprompt,
181172
customization: AdminConsoleCustomization{},
182173
}, nil
183174
}

0 commit comments

Comments
 (0)