From 879eaaa7eecbd024c4f200ba49e0825db4265436 Mon Sep 17 00:00:00 2001 From: Ahmad Ibrahim Date: Thu, 30 May 2024 10:33:19 -0700 Subject: [PATCH 1/3] chore: init go module --- go.mod | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 go.mod diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..cc7b420 --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module github.com/spectrocloud-labs/prompts-tui + +go 1.22.3 From 54e1a6f7cfa6ce216f80bb06d35773ee7925d7ab Mon Sep 17 00:00:00 2001 From: Ahmad Ibrahim Date: Thu, 30 May 2024 11:13:29 -0700 Subject: [PATCH 2/3] feat: add prompts implementation and test --- go.mod | 29 + go.sum | 138 ++++ prompts/mocks/prompt_mocks.go | 74 +++ prompts/prompts.go | 643 +++++++++++++++++++ prompts/prompts_test.go | 1123 +++++++++++++++++++++++++++++++++ 5 files changed, 2007 insertions(+) create mode 100644 go.sum create mode 100644 prompts/mocks/prompt_mocks.go create mode 100644 prompts/prompts.go create mode 100644 prompts/prompts_test.go diff --git a/go.mod b/go.mod index cc7b420..e3d9ab6 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,32 @@ module github.com/spectrocloud-labs/prompts-tui go 1.22.3 + +require ( + emperror.dev/errors v0.8.1 + github.com/Masterminds/semver v1.5.0 + github.com/pterm/pterm v0.12.79 + golang.org/x/crypto v0.23.0 + golang.org/x/exp v0.0.0-20240529005216-23cca8864a10 + k8s.io/apimachinery v0.30.1 +) + +require ( + atomicgo.dev/cursor v0.2.0 // indirect + atomicgo.dev/keyboard v0.2.9 // indirect + atomicgo.dev/schedule v0.1.0 // indirect + github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 // indirect + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/gookit/color v1.5.4 // indirect + github.com/lithammer/fuzzysearch v1.1.8 // indirect + github.com/mattn/go-runewidth v0.0.15 // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/rivo/uniseg v0.4.4 // indirect + github.com/stretchr/testify v1.9.0 // indirect + github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect + go.uber.org/multierr v1.11.0 // indirect + golang.org/x/sys v0.20.0 // indirect + golang.org/x/term v0.20.0 // indirect + golang.org/x/text v0.15.0 // indirect + k8s.io/utils v0.0.0-20240102154912-e7106e64919e // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..f800e78 --- /dev/null +++ b/go.sum @@ -0,0 +1,138 @@ +atomicgo.dev/assert v0.0.2 h1:FiKeMiZSgRrZsPo9qn/7vmr7mCsh5SZyXY4YGYiYwrg= +atomicgo.dev/assert v0.0.2/go.mod h1:ut4NcI3QDdJtlmAxQULOmA13Gz6e2DWbSAS8RUOmNYQ= +atomicgo.dev/cursor v0.2.0 h1:H6XN5alUJ52FZZUkI7AlJbUc1aW38GWZalpYRPpoPOw= +atomicgo.dev/cursor v0.2.0/go.mod h1:Lr4ZJB3U7DfPPOkbH7/6TOtJ4vFGHlgj1nc+n900IpU= +atomicgo.dev/keyboard v0.2.9 h1:tOsIid3nlPLZ3lwgG8KZMp/SFmr7P0ssEN5JUsm78K8= +atomicgo.dev/keyboard v0.2.9/go.mod h1:BC4w9g00XkxH/f1HXhW2sXmJFOCWbKn9xrOunSFtExQ= +atomicgo.dev/schedule v0.1.0 h1:nTthAbhZS5YZmgYbb2+DH8uQIZcTlIrd4eYr3UQxEjs= +atomicgo.dev/schedule v0.1.0/go.mod h1:xeUa3oAkiuHYh8bKiQBRojqAMq3PXXbJujjb0hw8pEU= +emperror.dev/errors v0.8.1 h1:UavXZ5cSX/4u9iyvH6aDcuGkVjeexUGJ7Ij7G4VfQT0= +emperror.dev/errors v0.8.1/go.mod h1:YcRvLPh626Ubn2xqtoprejnA5nFha+TJ+2vew48kWuE= +github.com/MarvinJWendt/testza v0.1.0/go.mod h1:7AxNvlfeHP7Z/hDQ5JtE3OKYT3XFUeLCDE2DQninSqs= +github.com/MarvinJWendt/testza v0.2.1/go.mod h1:God7bhG8n6uQxwdScay+gjm9/LnO4D3kkcZX4hv9Rp8= +github.com/MarvinJWendt/testza v0.2.8/go.mod h1:nwIcjmr0Zz+Rcwfh3/4UhBp7ePKVhuBExvZqnKYWlII= +github.com/MarvinJWendt/testza v0.2.10/go.mod h1:pd+VWsoGUiFtq+hRKSU1Bktnn+DMCSrDrXDpX2bG66k= +github.com/MarvinJWendt/testza v0.2.12/go.mod h1:JOIegYyV7rX+7VZ9r77L/eH6CfJHHzXjB69adAhzZkI= +github.com/MarvinJWendt/testza v0.3.0/go.mod h1:eFcL4I0idjtIx8P9C6KkAuLgATNKpX4/2oUqKc6bF2c= +github.com/MarvinJWendt/testza v0.4.2/go.mod h1:mSdhXiKH8sg/gQehJ63bINcCKp7RtYewEjXsvsVUPbE= +github.com/MarvinJWendt/testza v0.5.2 h1:53KDo64C1z/h/d/stCYCPY69bt/OSwjq5KpFNwi+zB4= +github.com/MarvinJWendt/testza v0.5.2/go.mod h1:xu53QFE5sCdjtMCKk8YMQ2MnymimEctc4n3EjyIYvEY= +github.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3QEww= +github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y= +github.com/atomicgo/cursor v0.0.1/go.mod h1:cBON2QmmrysudxNBFthvMtN32r3jxVRIvzkUiF/RuIk= +github.com/containerd/console v1.0.3/go.mod h1:7LqA/THxQ86k76b8c/EMSiaJ3h1eZkMkXar0TQ1gf3U= +github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 h1:q2hJAaP1k2wIvVRd/hEHD7lacgqrCPS+k8g1MndzfWY= +github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81/go.mod h1:YynlIjWYF8myEu6sdkwKIvGQq+cOckRm6So2avqoYAk= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/gookit/color v1.4.2/go.mod h1:fqRyamkC1W8uxl+lxCQxOT09l/vYfZ+QeiX3rKQHCoQ= +github.com/gookit/color v1.5.0/go.mod h1:43aQb+Zerm/BWh2GnrgOQm7ffz7tvQXEKV6BFMl7wAo= +github.com/gookit/color v1.5.4 h1:FZmqs7XOyGgCAxmWyPslpiok1k05wmY3SJTytgvYFs0= +github.com/gookit/color v1.5.4/go.mod h1:pZJOeOS8DM43rXbp4AZo1n9zCU2qjpcRko0b6/QJi9w= +github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= +github.com/klauspost/cpuid/v2 v2.0.10/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c= +github.com/klauspost/cpuid/v2 v2.0.12/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c= +github.com/klauspost/cpuid/v2 v2.2.3 h1:sxCkb+qR91z4vsqw4vGGZlDgPz3G7gjaLyK3V8y70BU= +github.com/klauspost/cpuid/v2 v2.2.3/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/lithammer/fuzzysearch v1.1.8 h1:/HIuJnjHuXS8bKaiTMeeDlW2/AyIWk2brx1V8LFgLN4= +github.com/lithammer/fuzzysearch v1.1.8/go.mod h1:IdqeyBClc3FFqSzYq/MXESsS4S0FsZ5ajtkr5xPLts4= +github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= +github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pterm/pterm v0.12.27/go.mod h1:PhQ89w4i95rhgE+xedAoqous6K9X+r6aSOI2eFF7DZI= +github.com/pterm/pterm v0.12.29/go.mod h1:WI3qxgvoQFFGKGjGnJR849gU0TsEOvKn5Q8LlY1U7lg= +github.com/pterm/pterm v0.12.30/go.mod h1:MOqLIyMOgmTDz9yorcYbcw+HsgoZo3BQfg2wtl3HEFE= +github.com/pterm/pterm v0.12.31/go.mod h1:32ZAWZVXD7ZfG0s8qqHXePte42kdz8ECtRyEejaWgXU= +github.com/pterm/pterm v0.12.33/go.mod h1:x+h2uL+n7CP/rel9+bImHD5lF3nM9vJj80k9ybiiTTE= +github.com/pterm/pterm v0.12.36/go.mod h1:NjiL09hFhT/vWjQHSj1athJpx6H8cjpHXNAK5bUw8T8= +github.com/pterm/pterm v0.12.40/go.mod h1:ffwPLwlbXxP+rxT0GsgDTzS3y3rmpAO1NMjUkGTYf8s= +github.com/pterm/pterm v0.12.79 h1:lH3yrYMhdpeqX9y5Ep1u7DejyHy7NSQg9qrBjF9dFT4= +github.com/pterm/pterm v0.12.79/go.mod h1:1v/gzOF1N0FsjbgTHZ1wVycRkKiatFvJSJC4IGaQAAo= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis= +github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +github.com/sergi/go-diff v1.2.0 h1:XU+rvMAioB0UC3q1MFrIQy4Vo5/4VsRDQQXHsEya6xQ= +github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778/go.mod h1:2MuV+tbUrU1zIOPMxZ5EncGwgmMJsa+9ucAQZXxsObs= +github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= +github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI= +golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= +golang.org/x/exp v0.0.0-20240529005216-23cca8864a10 h1:vpzMC/iZhYFAjJzHU0Cfuq+w1vLLsF2vLkDrPjzKYck= +golang.org/x/exp v0.0.0-20240529005216-23cca8864a10/go.mod h1:XtvwrStGgqGPLc4cjQfWqZHG1YFdYs6swckp8vpsjnc= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211013075003-97ac67df715c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220319134239-a9b59b0215f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y= +golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/term v0.20.0 h1:VnkxpohqXaOBYJtBmEppKUG6mXpi+4O6purfc2+sMhw= +golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk= +golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +k8s.io/apimachinery v0.30.1 h1:ZQStsEfo4n65yAdlGTfP/uSHMQSoYzU/oeEbkmF7P2U= +k8s.io/apimachinery v0.30.1/go.mod h1:iexa2somDaxdnj7bha06bhb43Zpa6eWH8N8dbqVjTUc= +k8s.io/utils v0.0.0-20240102154912-e7106e64919e h1:eQ/4ljkx21sObifjzXwlPKpdGLrCfRziVtos3ofG/sQ= +k8s.io/utils v0.0.0-20240102154912-e7106e64919e/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= diff --git a/prompts/mocks/prompt_mocks.go b/prompts/mocks/prompt_mocks.go new file mode 100644 index 0000000..907109e --- /dev/null +++ b/prompts/mocks/prompt_mocks.go @@ -0,0 +1,74 @@ +package mocks + +import ( + "fmt" + "strings" + + "golang.org/x/exp/slices" +) + +type MockTUI struct { + ReturnVals []string + Errs []error + validate func(string) error +} + +func (m *MockTUI) GetBool(prompt string, defaultVal bool) (bool, error) { + val, err := m.run() + if err != nil { + return false, err + } + val = strings.ToLower(val) + if !slices.Contains([]string{"", "y", "n"}, val) { + return false, fmt.Errorf("GetBool: invalid input: %s", val) + } + if val == "" { + return defaultVal, err + } + return val == "y", err +} + +func (m *MockTUI) GetText(prompt, defaultVal, mask string, optional bool, validate func(string) error) (string, error) { + if validate != nil { + m.validate = validate + } + return m.run() +} + +func (m *MockTUI) GetSelection(prompt string, options []string) (string, error) { + val, err := m.run() + if err != nil { + return val, err + } + if !slices.Contains(options, val) { + return val, fmt.Errorf("GetSelection: input %s not found in options %s", val, options) + } + return val, nil +} + +func (m *MockTUI) GetMultiSelection(prompt string, options []string, minSelections int) ([]string, error) { + vals := make([]string, 0) + for i := 0; i < minSelections; i++ { + val, err := m.run() + if err != nil { + return nil, err + } + vals = append(vals, val) + } + return vals, nil +} + +func (m *MockTUI) run() (val string, err error) { + val, m.ReturnVals = m.ReturnVals[0], m.ReturnVals[1:] + if m.validate != nil { + validateErr := m.validate(val) + if validateErr != nil { + m.Errs = []error{validateErr} + } + m.validate = nil // reset for subsequent prompts + } + if m.Errs != nil { + err, m.Errs = m.Errs[0], m.Errs[1:] + } + return val, err +} diff --git a/prompts/prompts.go b/prompts/prompts.go new file mode 100644 index 0000000..40bba4f --- /dev/null +++ b/prompts/prompts.go @@ -0,0 +1,643 @@ +package prompts + +import ( + "fmt" + "net" + "net/url" + "os" + "regexp" + "strconv" + "strings" + + "emperror.dev/errors" + "github.com/Masterminds/semver" + "github.com/pterm/pterm" + "golang.org/x/crypto/ssh" + "golang.org/x/exp/slices" + "k8s.io/apimachinery/pkg/util/validation" +) + +const ( + // Adapted from: http://stackoverflow.com/questions/10306690/domain-name-validation-with-regex/26987741#26987741 + domain = "([a-zA-Z0-9]{1,63}|[a-zA-Z0-9][a-zA-Z0-9\\-]{0,61}[a-zA-Z0-9])(\\.[a-zA-Z0-9]{1,63}|\\.[a-zA-Z0-9][a-zA-Z0-9\\-]{0,61}[a-zA-Z0-9]){0,10}\\.([a-zA-Z0-9][a-zA-Z0-9\\-]{0,61}[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\\-]{0,30}[a-zA-Z0-9]\\.[a-zA-Z]{2,})" + + topLevelDomain = "\\.[a-zA-Z0-9][a-zA-Z0-9\\-_]{0,61}" + + // Adapted from: https://stackoverflow.com/a/36760050/7898074, https://stackoverflow.com/a/12968117/7898074 + ip = "((25[0-5]|(2[0-4]|1\\d|[1-9]|)\\d)\\.?\\b){4}" + port = ":([1-9][0-9]{0,3}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])" +) + +var ( + logger = pterm.DefaultLogger + + // Exported to enable monkey-patching + Tui TUI = PtermTUI{} + + ValidationError = errors.New("validation failed") + InputMandatoryError = errors.New("input is mandatory") + + // Exported regex patterns for use with ReadTextRegex + KindClusterRegex = "^[a-z0-9]{1}[a-z0-9-]{0,30}[a-z0-9]{1}$" + MaasApiRegex = "^.*\\/MAAS$" + // Source: https://github.com/kubevirt/containerized-data-importer/blob/main/pkg/apiserver/webhooks/util.go#L122C3-L122C3 + CDIImageRegistryRegex = "^(docker|oci-archive):\\/\\/([a-z0-9.\\-\\/]+)([:]{0})$" + // Allowed chars are alphanumerics plus '.', '-', and '_', but cannot start or end with a symbol. + // Additionally, 2+ consecutive symbols are disallowed. + UsernameRegex = "[a-zA-Z0-9]+(?:\\.[a-zA-Z0-9]+)*(?:-[a-zA-Z0-9]+)*(?:_[a-zA-Z0-9]+)*" + PaletteResourceNameRegex = "[a-z][a-z0-9-]{1,31}[a-z0-9]" + VSphereUsernameRegex = "^" + UsernameRegex + "@" + domain + "$" + CPUReqRegex = "(^\\d+\\.?\\d*[M,G]Hz)" + MemoryReqRegex = "(^\\d+\\.?\\d*[M,G,T]i)" + DiskReqRegex = "(^\\d+\\.?\\d*[M,G,T]i)" + ArtifactRefRegex = "^[a-z0-9_.\\-\\/]+(:.*)?(@sha256:.*)?$" + UUIDRegex = "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$" + + noProxyExceptions = []string{"*", "localhost", "kubernetes"} + domainRegex = regexp.MustCompile("^" + domain + "$") + tldRegex = regexp.MustCompile("^" + topLevelDomain + "$") + noProxyDomainRegex = regexp.MustCompile("^" + "\\." + domain + "$") + domainPortRegex = regexp.MustCompile("^" + domain + port + "$") + ipPortRegex = regexp.MustCompile("^" + ip + port + "$") +) + +type TUI interface { + GetBool(prompt string, defaultVal bool) (bool, error) + GetText(prompt, defaultVal, mask string, optional bool, validate func(string) error) (string, error) + GetSelection(prompt string, options []string) (string, error) + GetMultiSelection(prompt string, options []string, minSelections int) ([]string, error) +} + +type PtermTUI struct{} + +// GetBool prompts a bool from the user while automatically appending a ? character to the end of +// the prompt message. +func (p PtermTUI) GetBool(prompt string, defaultVal bool) (bool, error) { + return pterm.DefaultInteractiveConfirm. + WithDefaultText(prompt + "?"). + WithDefaultValue(defaultVal). + WithOnInterruptFunc(exit). + Show() +} + +func (p PtermTUI) GetText(prompt, defaultVal, mask string, optional bool, validate func(string) error) (string, error) { + for { + if optional { + prompt = fmt.Sprintf("%s (optional, hit enter to skip)", prompt) + } + + // shoddy workaround for https://github.com/pterm/pterm/issues/560 + // inputs longer than the terminal width are handled better with multiline + // enabled... but the prompt is still repeated after every key press + var multiline bool + if len(defaultVal) > 60 { + multiline = true + } + + s, err := pterm.DefaultInteractiveTextInput. + WithDefaultValue(defaultVal). + WithMask(mask). + WithMultiLine(multiline). + WithOnInterruptFunc(exit). + Show(prompt) + if err != nil { + return "", err + } + + if err := validate(s); err != nil { + logger.Info("Validation failed", logger.Args("input", s, "error", err.Error())) + continue + } + return s, nil + } +} + +func (p PtermTUI) GetSelection(prompt string, options []string) (string, error) { + return pterm.DefaultInteractiveSelect. + WithDefaultText(prompt). + WithOptions(options). + WithOnInterruptFunc(exit). + Show() +} + +func (p PtermTUI) GetMultiSelection(prompt string, options []string, minSelections int) ([]string, error) { + for { + selections, err := pterm.DefaultInteractiveMultiselect. + WithDefaultText(prompt). + WithOptions(options). + Show() + if err != nil { + return nil, err + } + + if len(selections) < minSelections { + logger.Info("Minimum selection required", logger.Args(minSelections)) + continue + } + return selections, nil + } +} + +// --------- +// Selection +// --------- + +type ChoiceItem struct { + ID string + Name string +} + +func exit() { + logger.Fatal("Exiting CLI...") +} + +func Select(prompt string, options []string) (string, error) { + if len(options) == 0 { + return "", fmt.Errorf("failure in Select: no options available: %s", prompt) + } + choice, err := Tui.GetSelection(prompt, options) + if err != nil { + return "", errors.Wrap(err, "failure in Select") + } + return choice, nil +} + +func SelectID(prompt string, items []ChoiceItem) (*ChoiceItem, error) { + if len(items) == 0 { + return nil, fmt.Errorf("failure in SelectID: no options available: %s", prompt) + } + + options := make([]string, 0) + optionsMap := make(map[string]*ChoiceItem, 0) + + for _, i := range items { + i := i + options = append(options, i.Name) + optionsMap[i.Name] = &i + } + + choice, err := Tui.GetSelection(prompt, options) + if err != nil { + return nil, errors.Wrap(err, "failure in SelectID") + } + + return optionsMap[choice], nil +} + +func MultiSelect(prompt string, options []string, minSelections int) ([]string, error) { + if len(options) == 0 { + return nil, fmt.Errorf("failure in MultiSelect: no options available: %s", prompt) + } + selections, err := Tui.GetMultiSelection(prompt, options, minSelections) + if err != nil { + return nil, errors.Wrap(err, "failure in MultiSelect") + } + return selections, nil +} + +// ----- +// Input +// ----- + +func ReadBool(prompt string, defaultVal bool) (bool, error) { + b, err := Tui.GetBool(prompt, defaultVal) + if err != nil { + return b, errors.Wrap(err, "failure in ReadBool") + } + return b, nil +} + +func ReadInt(prompt, defaultVal string, minVal, maxVal int) (int, error) { + validate := func(input string) error { + i, err := strconv.Atoi(input) + if err != nil { + return err + } + if minVal > 0 && i < minVal { + return fmt.Errorf("minimum is %d", minVal) + } else if maxVal > 0 && i > maxVal { + return fmt.Errorf("maximum is %d", maxVal) + } + return nil + } + + s, err := Tui.GetText(prompt, defaultVal, "", false, validate) + if err != nil { + return -1, errors.Wrap(err, "failure in ReadInt") + } + return strconv.Atoi(s) +} + +func ReadText(label, defaultVal string, optional bool, maxLen int) (string, error) { + s, err := Tui.GetText(label, defaultVal, "", optional, validateStringFunc(optional, maxLen)) + if err != nil { + return s, errors.Wrap(err, "failure in ReadText") + } + return strings.TrimSpace(s), nil +} + +func ReadTextRegex(label, defaultVal, errMsg, regexPattern string) (string, error) { + + validate := func(input string) error { + if input == "" { + return InputMandatoryError + } + r, err := regexp.Compile(regexPattern) + if err != nil { + return errors.Wrap(err, errMsg) + } + m := r.Find([]byte(input)) + if string(m) == input { + return nil + } + return fmt.Errorf("input %s does not match regex %s; %s", input, regexPattern, errMsg) + } + + s, err := Tui.GetText(label, defaultVal, "", false, validate) + if err != nil { + return s, errors.Wrap(err, "failure in ReadTextRegex") + } + return s, nil +} + +func ReadSemVer(label, defaultVal, errMsg string) (string, error) { + + validate := func(input string) error { + if input == "" { + return InputMandatoryError + } + if !strings.HasPrefix(input, "v") { + return fmt.Errorf("input %s must start with a 'v'; %s", input, errMsg) + } + r, err := regexp.Compile(semver.SemVerRegex) + if err != nil { + return errors.Wrap(err, errMsg) + } + m := r.Find([]byte(input)) + if string(m) == input { + return nil + } + return fmt.Errorf("input %s does not match regex %s; %s", input, semver.SemVerRegex, errMsg) + } + + s, err := Tui.GetText(label, defaultVal, "", false, validate) + if err != nil { + return s, errors.Wrap(err, "failure in ReadSemVer") + } + return s, nil +} + +func ReadPassword(label, defaultVal string, optional bool, maxLen int) (string, error) { + s, err := Tui.GetText(label, defaultVal, "*", optional, validateStringFunc(optional, maxLen)) + if err != nil { + return s, errors.Wrap(err, "failure in ReadPassword") + } + return s, nil +} + +func ReadBasicCreds(usernamePrompt, passwordPrompt, defaultUsername, defaultPassword string, optional, maskUser bool) (string, string, error) { + var username string + var err error + + if maskUser { + username, err = ReadPassword(usernamePrompt, defaultUsername, optional, -1) + if err != nil { + return "", "", err + } + } else { + username, err = ReadText(usernamePrompt, defaultUsername, optional, -1) + if err != nil { + return "", "", err + } + } + password, err := ReadPassword(passwordPrompt, defaultPassword, optional, -1) + if err != nil { + return "", "", err + } + + return username, password, nil +} + +func ReadURL(label, defaultVal, errMsg string, optional bool) (string, error) { + + validate := func(input string) error { + if input == "" { + if !optional { + return InputMandatoryError + } else { + return nil + } + } + + _, err := url.ParseRequestURI(input) + if err != nil { + return errors.Wrap(err, errMsg) + } + + u, err := url.Parse(input) + if err != nil || u.Scheme == "" || u.Host == "" { + return errors.Wrap(err, errMsg) + } + return nil + } + + s, err := Tui.GetText(label, defaultVal, "", optional, validate) + if err != nil { + return s, errors.Wrap(err, "failure in ReadURL") + } + + s = strings.TrimRight(s, "/") + return s, nil +} + +func ReadURLRegex(label, defaultVal, errMsg, regexPattern string) (string, error) { + + validate := func(input string) error { + r, err := regexp.Compile(regexPattern) + if err != nil { + return errors.Wrap(err, errMsg) + } + m := r.Find([]byte(input)) + if string(m) != input { + return fmt.Errorf("input %s does not match regex %s; %s", input, regexPattern, errMsg) + } + + _, err = url.ParseRequestURI(input) + if err != nil { + return errors.Wrap(err, errMsg) + } + + u, err := url.Parse(input) + if err != nil { + return errors.Wrap(err, errMsg) + } else if u.Scheme == "" || u.Host == "" { + return errors.New(errMsg) + } + return nil + } + + s, err := Tui.GetText(label, defaultVal, "", false, validate) + if err != nil { + return s, errors.Wrap(err, "failure in ReadURLRegex") + } + + s = strings.TrimRight(s, "/") + return s, nil +} + +func ReadDomains(label, defaultVal, errMsg string, optional bool, maxVals int) (string, error) { + + validate := func(input string) error { + if input == "" { + if !optional { + return InputMandatoryError + } else { + return nil + } + } + vals := strings.Split(input, ",") + if maxVals > 0 && len(vals) > maxVals { + return fmt.Errorf("%s: maximum domains: %d", errMsg, maxVals) + } + for _, v := range vals { + if !(domainRegex.Match([]byte(v)) && validateDomain(v)) { + return errors.New(errMsg) + } + } + return nil + } + + s, err := Tui.GetText(label, defaultVal, "", optional, validate) + if err != nil { + return s, errors.Wrap(err, "failure in ReadDomains") + } + return s, nil +} + +func ReadIPs(label, defaultVal, errMsg string, optional bool, maxVals int) (string, error) { + + validate := func(input string) error { + if input == "" { + if !optional { + return InputMandatoryError + } else { + return nil + } + } + vals := strings.Split(input, ",") + if maxVals > 0 && len(vals) > maxVals { + return fmt.Errorf("%s: maximum IPs: %d", errMsg, maxVals) + } + for _, v := range vals { + if ip := net.ParseIP(v); ip == nil { + return errors.New(errMsg) + } + } + return nil + } + + s, err := Tui.GetText(label, defaultVal, "", optional, validate) + if err != nil { + return s, errors.Wrap(err, "failure in ReadIP") + } + return s, nil +} + +func ReadDomainsOrIPs(label, defaultVal, errMsg string, optional bool, maxVals int) (string, error) { + + validate := func(input string) error { + if input == "" { + if !optional { + return InputMandatoryError + } else { + return nil + } + } + vals := strings.Split(input, ",") + if maxVals > 0 && len(vals) > maxVals { + return fmt.Errorf("%s: maximum domains or IPs: %d", errMsg, maxVals) + } + for _, v := range vals { + ip := net.ParseIP(v) + isIPWithPort := ipPortRegex.Match([]byte(v)) + isDomain := domainRegex.Match([]byte(v)) && validateDomain(v) + if ip != nil || isIPWithPort || isDomain { + continue + } + return fmt.Errorf("%s: %s is neither an IP, IP:port, or an FQDN", errMsg, v) + } + return nil + } + + s, err := Tui.GetText(label, defaultVal, "", optional, validate) + if err != nil { + return s, errors.Wrap(err, "failure in ReadDomainsOrIPs") + } + return s, nil +} + +func ReadDomainOrIPNoPort(label, defaultVal, errMsg string, optional bool) (string, error) { + + validate := func(input string) error { + if input == "" { + if !optional { + return InputMandatoryError + } else { + return nil + } + } + + ip := net.ParseIP(input) + isDomain := domainRegex.Match([]byte(input)) && validateDomain(input) + if ip != nil || isDomain { + return nil + } + return fmt.Errorf("%s: %s is neither an IP or an FQDN", errMsg, input) + } + + s, err := Tui.GetText(label, defaultVal, "", optional, validate) + if err != nil { + return s, errors.Wrap(err, "failure in ReadDomainOrIPNoPort") + } + return s, nil +} + +func ReadCIDRs(label, defaultVal, errMsg string, optional bool, maxVals int) (string, error) { + + validate := func(input string) error { + if input == "" { + if !optional { + return InputMandatoryError + } else { + return nil + } + } + vals := strings.Split(input, ",") + if maxVals > 0 && len(vals) > maxVals { + return fmt.Errorf("%s: maximum CIDRs: %d", errMsg, maxVals) + } + for _, v := range vals { + if _, _, err := net.ParseCIDR(v); err != nil { + return errors.Wrap(err, errMsg) + } + } + return nil + } + + s, err := Tui.GetText(label, defaultVal, "", optional, validate) + if err != nil { + return s, errors.Wrap(err, "failure in ReadCIDRs") + } + return s, nil +} + +func ReadFilePath(label, defaultVal, errMsg string, optional bool) (string, error) { + + validate := func(input string) error { + if input == "" { + if !optional { + return InputMandatoryError + } else { + return nil + } + } + fileInfo, err := os.Stat(input) + if err != nil { + return errors.Wrap(err, errMsg) + } + if fileInfo.IsDir() { + return fmt.Errorf("%s: input %s is a directory, not a file", errMsg, input) + } + return nil + } + + s, err := Tui.GetText(label, defaultVal, "", optional, validate) + if err != nil { + return s, errors.Wrap(err, "failure in ReadFilePath") + } + return s, nil +} + +func ReadK8sName(label, defaultVal string, optional bool) (string, error) { + + validate := func(input string) error { + if err := validateStringFunc(optional, -1)(input); err != nil { + return err + } + return validateK8sName(input, optional) + } + + s, err := Tui.GetText(label, defaultVal, "", optional, validate) + if err != nil { + return s, errors.Wrap(err, "failure in ReadK8sName") + } + return s, nil +} + +// See: https://pkg.go.dev/golang.org/x/net/http/httpproxy#Config +func ValidateNoProxy(s string) error { + if s == "" { + return nil + } + + vBytes := []byte(s) + ip := net.ParseIP(s) + isIPWithPort := ipPortRegex.Match(vBytes) + isDomain := domainRegex.Match(vBytes) && validateDomain(s) + isDomainWithPort := domainPortRegex.Match(vBytes) + isNoProxyDomain := noProxyDomainRegex.Match(vBytes) && validateDomain(s) + isTopLevelDomain := tldRegex.Match(vBytes) + _, _, cidrErr := net.ParseCIDR(s) + isException := slices.Contains(noProxyExceptions, s) + + if ip != nil || isIPWithPort || cidrErr == nil || isDomain || isDomainWithPort || isNoProxyDomain || isTopLevelDomain || isException { + return nil + } + + logger.Error("invalid no_proxy input", logger.Args(s, "is neither an IP, CIDR, domain, '*', domain:port, or IP:port")) + return ValidationError +} + +func ValidateSSHPublicKey(s string) error { + if s == "" { + return nil + } + _, _, _, _, err := ssh.ParseAuthorizedKey([]byte(s)) + if err != nil { + logger.Error("invalid SSH public key", logger.Args("input", s, "error", err)) + return ValidationError + } + return nil +} + +// validateDomain ensures that no occurrence of consecutive dashes are found. +// This is necessary in addition to the domain regex, since go doesn't support negative lookaheads. +func validateDomain(domain string) bool { + return !strings.Contains(domain, "--") +} + +func validateStringFunc(optional bool, maxLen int) func(input string) error { + return func(input string) error { + if !optional && input == "" { + return InputMandatoryError + } + fieldLen := len(input) + if maxLen > 0 && fieldLen > maxLen { + return fmt.Errorf("maximum length of %d chars exceeded. input length: %d", maxLen, fieldLen) + } + return nil + } +} + +func validateK8sName(name string, optional bool) error { + if optional && name == "" { + return nil + } + if errs := validation.IsQualifiedName(name); errs != nil { + return errors.New(strings.Join(errs, ", ")) + } + if errs := validation.IsDNS1123Subdomain(name); errs != nil { + return errors.New(strings.Join(errs, ", ")) + } + return nil +} diff --git a/prompts/prompts_test.go b/prompts/prompts_test.go new file mode 100644 index 0000000..cc4c80e --- /dev/null +++ b/prompts/prompts_test.go @@ -0,0 +1,1123 @@ +package prompts + +import ( + "errors" + "reflect" + "regexp" + "strings" + "testing" + + "github.com/spectrocloud-labs/prompts-tui/prompts/mocks" +) + +func TestReadBool(t *testing.T) { + subtests := []struct { + name string + tui *mocks.MockTUI + defaultVal bool + expectedData bool + expectedErr error + }{ + { + name: "Read Yes (lower)", + tui: &mocks.MockTUI{ + ReturnVals: []string{"y"}, + Errs: nil, + }, + expectedData: true, + expectedErr: nil, + }, + { + name: "Read Yes (upper)", + tui: &mocks.MockTUI{ + ReturnVals: []string{"Y"}, + Errs: nil, + }, + expectedData: true, + expectedErr: nil, + }, + { + name: "Read No (lower)", + tui: &mocks.MockTUI{ + ReturnVals: []string{"n"}, + Errs: nil, + }, + expectedData: false, + expectedErr: nil, + }, + { + name: "Read No (upper)", + tui: &mocks.MockTUI{ + ReturnVals: []string{"N"}, + Errs: nil, + }, + expectedData: false, + expectedErr: nil, + }, + { + name: "Read Default (true)", + tui: &mocks.MockTUI{ + ReturnVals: []string{""}, + Errs: nil, + }, + defaultVal: true, + expectedData: true, + expectedErr: nil, + }, + { + name: "Read Default (false)", + tui: &mocks.MockTUI{ + ReturnVals: []string{""}, + Errs: nil, + }, + defaultVal: false, + expectedData: false, + expectedErr: nil, + }, + } + for _, subtest := range subtests { + t.Run(subtest.name, func(t *testing.T) { + Tui = subtest.tui + + data, err := ReadBool("", subtest.defaultVal) + if !reflect.DeepEqual(data, subtest.expectedData) { + t.Errorf("expected (%t), got (%t)", subtest.expectedData, data) + } + if err != nil && err.Error() != subtest.expectedErr.Error() { + t.Errorf("expected error (%v), got error (%v)", subtest.expectedErr, err) + } + }) + } +} + +func TestReadInt(t *testing.T) { + subtests := []struct { + name string + tui *mocks.MockTUI + min int + max int + expectedData int + expectedErr error + }{ + { + name: "Read Int (pass)", + tui: &mocks.MockTUI{ + ReturnVals: []string{"1"}, + Errs: nil, + }, + expectedData: 1, + expectedErr: nil, + }, + { + name: "Read Int (fail)", + tui: &mocks.MockTUI{ + ReturnVals: []string{"2"}, + Errs: []error{errors.New("fail")}, + }, + expectedData: -1, + expectedErr: errors.New("failure in ReadInt: fail"), + }, + { + name: "Read Int (fail_min)", + tui: &mocks.MockTUI{ + ReturnVals: []string{"2"}, + Errs: nil, + }, + min: 10, + expectedData: -1, + expectedErr: errors.New("failure in ReadInt: minimum is 10"), + }, + { + name: "Read Int (fail_max)", + tui: &mocks.MockTUI{ + ReturnVals: []string{"2"}, + Errs: nil, + }, + max: 1, + expectedData: -1, + expectedErr: errors.New("failure in ReadInt: maximum is 1"), + }, + { + name: "Read Int (fail_extra)", + tui: &mocks.MockTUI{ + ReturnVals: []string{"-1", "a"}, + Errs: nil, + }, + max: 1, + expectedData: -1, + expectedErr: errors.New("failure in ReadInt: maximum is 1: strconv.ParseInt: parsing \"a\": invalid syntax"), + }, + } + for _, subtest := range subtests { + t.Run(subtest.name, func(t *testing.T) { + Tui = subtest.tui + + data, err := ReadInt("", "", subtest.min, subtest.max) + if !reflect.DeepEqual(data, subtest.expectedData) { + t.Errorf("expected (%d), got (%d)", subtest.expectedData, data) + } + if err != nil && err.Error() != subtest.expectedErr.Error() { + t.Errorf("expected error (%v), got error (%v)", subtest.expectedErr, err) + } + }) + } +} + +func TestReadText(t *testing.T) { + subtests := []struct { + name string + tui *mocks.MockTUI + isOptional bool + maxLen int + expectedData string + expectedErr error + }{ + { + name: "Read Text (pass)", + tui: &mocks.MockTUI{ + ReturnVals: []string{"foo"}, + Errs: nil, + }, + expectedData: "foo", + expectedErr: nil, + }, + { + name: "Read Text (fail)", + tui: &mocks.MockTUI{ + ReturnVals: []string{"foo"}, + Errs: []error{errors.New("fail")}, + }, + expectedData: "foo", + expectedErr: errors.New("failure in ReadText: fail"), + }, + { + name: "Read Text (fail_len)", + tui: &mocks.MockTUI{ + ReturnVals: []string{"foo"}, + Errs: nil, + }, + maxLen: 2, + expectedData: "foo", + expectedErr: errors.New("failure in ReadText: maximum length of 2 chars exceeded. input length: 3"), + }, + { + name: "Read Text (fail_optional)", + tui: &mocks.MockTUI{ + ReturnVals: []string{""}, + Errs: nil, + }, + isOptional: false, + expectedData: "", + expectedErr: errors.New("failure in ReadText: input is mandatory"), + }, + { + name: "Read Text (fail_extra)", + tui: &mocks.MockTUI{ + ReturnVals: []string{"", ""}, + Errs: []error{errors.New("fail")}, + }, + isOptional: true, + expectedData: "", + expectedErr: errors.New("failure in ReadText: fail"), + }, + } + for _, subtest := range subtests { + t.Run(subtest.name, func(t *testing.T) { + Tui = subtest.tui + + data, err := ReadText("", "", subtest.isOptional, subtest.maxLen) + if !reflect.DeepEqual(data, subtest.expectedData) { + t.Errorf("expected (%s), got (%s)", subtest.expectedData, data) + } + if err != nil && err.Error() != subtest.expectedErr.Error() { + t.Errorf("expected error (%v), got error (%v)", subtest.expectedErr, err) + } + }) + } +} + +func TestReadDomains(t *testing.T) { + subtests := []struct { + name string + tui *mocks.MockTUI + errMsg string + isOptional bool + maxVals int + expectedData string + expectedErr error + }{ + { + name: "ReadDomains (pass_spectro)", + tui: &mocks.MockTUI{ + ReturnVals: []string{"spectrocloud.dev"}, + }, + maxVals: 1, + expectedData: "spectrocloud.dev", + }, + } + for _, subtest := range subtests { + t.Run(subtest.name, func(t *testing.T) { + Tui = subtest.tui + + data, err := ReadDomains("", "", subtest.errMsg, subtest.isOptional, subtest.maxVals) + if !reflect.DeepEqual(data, subtest.expectedData) { + t.Errorf("expected (%s), got (%s)", subtest.expectedData, data) + } + if err != nil && err.Error() != subtest.expectedErr.Error() { + t.Errorf("expected error (%v), got error (%v)", subtest.expectedErr, err) + } + }) + } +} + +func TestReadDomainsOrIPs(t *testing.T) { + subtests := []struct { + name string + tui *mocks.MockTUI + errMsg string + isOptional bool + maxVals int + expectedData string + expectedErr error + }{ + { + name: "ReadDomainsOrIPs (IP pass)", + tui: &mocks.MockTUI{ + ReturnVals: []string{"10.10.10.10"}, + }, + maxVals: 1, + expectedData: "10.10.10.10", + }, + { + name: "ReadDomainsOrIPs (IP fail)", + tui: &mocks.MockTUI{ + ReturnVals: []string{"10.10.10.10.10"}, + Errs: []error{errors.New("fail")}, + }, + maxVals: 1, + expectedData: "10.10.10.10.10", + expectedErr: errors.New("failure in ReadDomainsOrIPs: fail"), + }, + { + name: "ReadDomainsOrIPs (Domain pass basic)", + tui: &mocks.MockTUI{ + ReturnVals: []string{"vcenter.spectrocloud.dev"}, + }, + maxVals: 1, + expectedData: "vcenter.spectrocloud.dev", + }, + { + name: "ReadDomainsOrIPs (Domain pass long)", + tui: &mocks.MockTUI{ + ReturnVals: []string{"0.lab.vcenter.spectrocloud.dev"}, + }, + maxVals: 1, + expectedData: "0.lab.vcenter.spectrocloud.dev", + }, + { + name: "ReadDomainsOrIPs (Domain pass long with dashes)", + tui: &mocks.MockTUI{ + ReturnVals: []string{"0.lab.v-center.spectro-cloud.dev"}, + }, + maxVals: 1, + expectedData: "0.lab.v-center.spectro-cloud.dev", + }, + { + name: "ReadDomainsOrIPs (Domain pass short)", + tui: &mocks.MockTUI{ + ReturnVals: []string{"to.io"}, + }, + maxVals: 1, + expectedData: "to.io", + }, + { + name: "ReadDomainsOrIPs (Domain pass dashes)", + tui: &mocks.MockTUI{ + ReturnVals: []string{"ps-vcenter-02.ps.labs.local"}, + }, + maxVals: 1, + expectedData: "ps-vcenter-02.ps.labs.local", + }, + { + name: "ReadDomainsOrIPs (Domain pass multiple sub-domains)", + tui: &mocks.MockTUI{ + ReturnVals: []string{"vcenter.spectrocloud.foo.bar.baz.dev"}, + }, + maxVals: 1, + expectedData: "vcenter.spectrocloud.foo.bar.baz.dev", + }, + { + name: "ReadDomainsOrIPs (Domain fail leading dash)", + tui: &mocks.MockTUI{ + ReturnVals: []string{"-vcenter.spectrocloud.dev"}, + Errs: []error{errors.New("fail")}, + }, + maxVals: 1, + errMsg: "invalid domain", + expectedData: "-vcenter.spectrocloud.dev", + expectedErr: errors.New("failure in ReadDomainsOrIPs: invalid domain: -vcenter.spectrocloud.dev is neither an IP, IP:port, or an FQDN"), + }, + { + name: "ReadDomainsOrIPs (Domain fail trailing dash)", + tui: &mocks.MockTUI{ + ReturnVals: []string{"vcenter.spectrocloud.dev-"}, + Errs: []error{errors.New("fail")}, + }, + maxVals: 1, + errMsg: "invalid domain", + expectedData: "vcenter.spectrocloud.dev-", + expectedErr: errors.New("failure in ReadDomainsOrIPs: invalid domain: vcenter.spectrocloud.dev- is neither an IP, IP:port, or an FQDN"), + }, + { + name: "ReadDomainsOrIPs (Domain fail consecutive dashes)", + tui: &mocks.MockTUI{ + ReturnVals: []string{"vcenter.spectro--cloud.dev"}, + Errs: []error{errors.New("fail")}, + }, + maxVals: 1, + errMsg: "invalid domain", + expectedData: "vcenter.spectro--cloud.dev", + expectedErr: errors.New("failure in ReadDomainsOrIPs: invalid domain: vcenter.spectro--cloud.dev is neither an IP, IP:port, or an FQDN"), + }, + { + name: "ReadDomainsOrIPs (Domain fail dot dash)", + tui: &mocks.MockTUI{ + ReturnVals: []string{"vcenter.-spectrocloud.dev"}, + Errs: []error{errors.New("fail")}, + }, + maxVals: 1, + errMsg: "invalid domain", + expectedData: "vcenter.-spectrocloud.dev", + expectedErr: errors.New("failure in ReadDomainsOrIPs: invalid domain: vcenter.-spectrocloud.dev is neither an IP, IP:port, or an FQDN"), + }, + { + name: "ReadDomainsOrIPs (Domain fail dash dot)", + tui: &mocks.MockTUI{ + ReturnVals: []string{"vcenter-.spectrocloud.dev"}, + Errs: []error{errors.New("fail")}, + }, + maxVals: 1, + errMsg: "invalid domain", + expectedData: "vcenter-.spectrocloud.dev", + expectedErr: errors.New("failure in ReadDomainsOrIPs: invalid domain: vcenter-.spectrocloud.dev is neither an IP, IP:port, or an FQDN"), + }, + { + name: "ReadDomainsOrIPs (Domain fail invalid char)", + tui: &mocks.MockTUI{ + ReturnVals: []string{"vcenter.spectro*cloud.dev"}, + Errs: []error{errors.New("fail")}, + }, + maxVals: 1, + errMsg: "invalid domain", + expectedData: "vcenter.spectro*cloud.dev", + expectedErr: errors.New("failure in ReadDomainsOrIPs: invalid domain: vcenter.spectro*cloud.dev is neither an IP, IP:port, or an FQDN"), + }, + { + name: "ReadDomainsOrIPs (fail_optional)", + tui: &mocks.MockTUI{ + ReturnVals: []string{""}, + }, + maxVals: 1, + isOptional: false, + errMsg: InputMandatoryError.Error(), + expectedData: "", + expectedErr: errors.New("failure in ReadDomainsOrIPs: input is mandatory"), + }, + { + name: "ReadDomainsOrIPs (pass_max_vals)", + tui: &mocks.MockTUI{ + ReturnVals: []string{"foo.com,bar.io,baz.ca"}, + }, + maxVals: 3, + isOptional: false, + expectedData: "foo.com,bar.io,baz.ca", + }, + { + name: "ReadDomainsOrIPs (fail_max_vals)", + tui: &mocks.MockTUI{ + ReturnVals: []string{"foo.com,bar.io"}, + }, + maxVals: 1, + isOptional: false, + errMsg: "invalid domains or IPs", + expectedData: "foo.com,bar.io", + expectedErr: errors.New("failure in ReadDomainsOrIPs: invalid domains or IPs: maximum domains or IPs: 1"), + }, + { + name: "ReadDomainsOrIPs (fail_extra)", + tui: &mocks.MockTUI{ + ReturnVals: []string{"", ""}, + Errs: []error{errors.New("fail")}, + }, + isOptional: true, + maxVals: 1, + expectedErr: errors.New("failure in ReadDomainsOrIPs: fail"), + }, + { + name: "ReadDomainsOrIPs (pass_spectro)", + tui: &mocks.MockTUI{ + ReturnVals: []string{"spectrocloud.dev"}, + }, + maxVals: 1, + expectedData: "spectrocloud.dev", + }, + } + for _, subtest := range subtests { + t.Run(subtest.name, func(t *testing.T) { + Tui = subtest.tui + + data, err := ReadDomainsOrIPs("", "", subtest.errMsg, subtest.isOptional, subtest.maxVals) + if !reflect.DeepEqual(data, subtest.expectedData) { + t.Errorf("expected (%s), got (%s)", subtest.expectedData, data) + } + if err != nil && err.Error() != subtest.expectedErr.Error() { + t.Errorf("expected error (%v), got error (%v)", subtest.expectedErr, err) + } + }) + } +} + +func TestReadNoProxy(t *testing.T) { + defaultNoProxy := "127.0.0.1,192.168.0.0/16,10.0.0.0/16,10.96.0.0/12,169.254.169.254,169.254.0.0/24,localhost,kubernetes,kubernetes.default,kubernetes.default.svc,kubernetes.default.svc.cluster,kubernetes.default.svc.cluster.local,.svc,.svc.cluster,.svc.cluster.local,.svc.cluster.local,.company.local" + validate := func(s string) error { + for _, x := range strings.Split(s, ",") { + if err := ValidateNoProxy(x); err != nil { + return err + } + } + return nil + } + subtests := []struct { + name string + tui *mocks.MockTUI + isOptional bool + expectedData string + expectedErr error + }{ + { + name: "Read No Proxy (pass DefaultNoProxy)", + tui: &mocks.MockTUI{ + ReturnVals: []string{defaultNoProxy}, + }, + expectedData: defaultNoProxy, + }, + { + name: "Read No Proxy (pass IP)", + tui: &mocks.MockTUI{ + ReturnVals: []string{"10.10.10.10"}, + }, + expectedData: "10.10.10.10", + }, + { + name: "Read No Proxy (pass IP port)", + tui: &mocks.MockTUI{ + ReturnVals: []string{"10.10.10.10:80"}, + }, + expectedData: "10.10.10.10:80", + }, + { + name: "Read No Proxy (pass CIDR)", + tui: &mocks.MockTUI{ + ReturnVals: []string{"10.10.10.10/24"}, + }, + expectedData: "10.10.10.10/24", + }, + { + name: "Read No Proxy (pass domain w/ leading period)", + tui: &mocks.MockTUI{ + ReturnVals: []string{".vcenter.spectrocloud.dev"}, + }, + expectedData: ".vcenter.spectrocloud.dev", + }, + { + name: "Read No Proxy (pass exception)", + tui: &mocks.MockTUI{ + ReturnVals: []string{"localhost"}, + }, + expectedData: "localhost", + }, + { + name: "Read No Proxy (fail_validation)", + tui: &mocks.MockTUI{ + ReturnVals: []string{"abc"}, + Errs: []error{errors.New("fail")}, + }, + expectedData: "abc", + expectedErr: ValidationError, + }, + } + for _, subtest := range subtests { + t.Run(subtest.name, func(t *testing.T) { + Tui = subtest.tui + data, err := Tui.GetText("", "", "", subtest.isOptional, validate) + if !reflect.DeepEqual(data, subtest.expectedData) { + t.Errorf("expected (%s), got (%s)", subtest.expectedData, data) + } + if err != nil && err.Error() != subtest.expectedErr.Error() { + t.Errorf("expected error (%v), got error (%v)", subtest.expectedErr, err) + } + }) + } +} + +func TestReadK8sName(t *testing.T) { + subtests := []struct { + name string + tui *mocks.MockTUI + errMsg string + isOptional bool + expectedData string + expectedErr error + }{ + { + name: "Read K8sName (pass)", + tui: &mocks.MockTUI{ + ReturnVals: []string{"foo"}, + Errs: nil, + }, + expectedData: "foo", + expectedErr: nil, + }, + { + name: "Read K8sName (fail)", + tui: &mocks.MockTUI{ + ReturnVals: []string{"foo"}, + Errs: []error{errors.New("fail")}, + }, + expectedData: "foo", + expectedErr: errors.New("failure in ReadK8sName: fail"), + }, + { + name: "Read K8sName (fail_name)", + tui: &mocks.MockTUI{ + ReturnVals: []string{".invalidName"}, + Errs: nil, + }, + expectedData: ".invalidName", + expectedErr: errors.New("failure in ReadK8sName: name part must consist of alphanumeric characters, '-', '_' or '.', and must start and end with an alphanumeric character (e.g. 'MyName', or 'my.name', or '123-abc', regex used for validation is '([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9]')"), + }, + { + name: "Read K8sName (fail_name2)", + tui: &mocks.MockTUI{ + ReturnVals: []string{"UPPER"}, + Errs: nil, + }, + expectedData: "UPPER", + expectedErr: errors.New("failure in ReadK8sName: a lowercase RFC 1123 subdomain must consist of lower case alphanumeric characters, '-' or '.', and must start and end with an alphanumeric character (e.g. 'example.com', regex used for validation is '[a-z0-9]([-a-z0-9]*[a-z0-9])?(\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*')"), + }, + { + name: "Read K8sName (fail_len)", + tui: &mocks.MockTUI{ + ReturnVals: []string{"aaaaaaaaaa-aaaaaaaaaa-aaaaaaaaaa-aaaaaaaaaa-aaaaaaaaaa-aaaaaaaaaa"}, + Errs: nil, + }, + expectedData: "aaaaaaaaaa-aaaaaaaaaa-aaaaaaaaaa-aaaaaaaaaa-aaaaaaaaaa-aaaaaaaaaa", + expectedErr: errors.New("failure in ReadK8sName: name part must be no more than 63 characters"), + }, + { + name: "Read K8sName (optional_not_provided)", + tui: &mocks.MockTUI{ + ReturnVals: []string{""}, + Errs: nil, + }, + isOptional: true, + expectedData: "", + expectedErr: nil, + }, + { + name: "Read K8sName (fail_optional)", + tui: &mocks.MockTUI{ + ReturnVals: []string{""}, + Errs: nil, + }, + isOptional: false, + errMsg: InputMandatoryError.Error(), + expectedData: "", + expectedErr: errors.New("failure in ReadK8sName: input is mandatory"), + }, + } + for _, subtest := range subtests { + t.Run(subtest.name, func(t *testing.T) { + Tui = subtest.tui + + data, err := ReadK8sName("", "", subtest.isOptional) + if !reflect.DeepEqual(data, subtest.expectedData) { + t.Errorf("expected (%s), got (%s)", subtest.expectedData, data) + } + if err != nil && err.Error() != subtest.expectedErr.Error() { + t.Errorf("expected error (%v), got error (%v)", subtest.expectedErr, err) + } + }) + } +} + +func TestReadPassword(t *testing.T) { + subtests := []struct { + name string + tui *mocks.MockTUI + errMsg string + isOptional bool + maxLen int + expectedData string + expectedErr error + }{ + { + name: "Read Password (pass)", + tui: &mocks.MockTUI{ + ReturnVals: []string{"foo"}, + Errs: nil, + }, + expectedData: "foo", + expectedErr: nil, + }, + { + name: "Read Password (fail)", + tui: &mocks.MockTUI{ + ReturnVals: []string{"foo"}, + Errs: []error{errors.New("fail")}, + }, + expectedData: "foo", + expectedErr: errors.New("failure in ReadPassword: fail"), + }, + { + name: "Read Password (fail_len)", + tui: &mocks.MockTUI{ + ReturnVals: []string{"foo"}, + Errs: nil, + }, + maxLen: 2, + expectedData: "foo", + expectedErr: errors.New("failure in ReadPassword: maximum length of 2 chars exceeded. input length: 3"), + }, + } + for _, subtest := range subtests { + t.Run(subtest.name, func(t *testing.T) { + Tui = subtest.tui + + data, err := ReadPassword("", "", subtest.isOptional, subtest.maxLen) + if !reflect.DeepEqual(data, subtest.expectedData) { + t.Errorf("expected (%s), got (%s)", subtest.expectedData, data) + } + if err != nil && err.Error() != subtest.expectedErr.Error() { + t.Errorf("expected error (%v), got error (%v)", subtest.expectedErr, err) + } + }) + } +} + +func TestSemVer(t *testing.T) { + subtests := []struct { + name string + tui *mocks.MockTUI + errMsg string + expectedData string + expectedErr error + }{ + { + name: "ReadSemVer (fail)", + tui: &mocks.MockTUI{ + ReturnVals: []string{"0.0.1"}, + Errs: []error{errors.New("fail")}, + }, + errMsg: "invalid Helm chart version", + expectedData: "0.0.1", + expectedErr: errors.New("failure in ReadSemVer: input 0.0.1 must start with a 'v'; invalid Helm chart version"), + }, + { + name: "ReadSemVer (pass)", + tui: &mocks.MockTUI{ + ReturnVals: []string{"v0.0.1"}, + Errs: nil, + }, + expectedData: "v0.0.1", + expectedErr: nil, + }, + } + for _, subtest := range subtests { + t.Run(subtest.name, func(t *testing.T) { + Tui = subtest.tui + + data, err := ReadSemVer("", "", subtest.errMsg) + if !reflect.DeepEqual(data, subtest.expectedData) { + t.Errorf("expected (%s), got (%s)", subtest.expectedData, data) + } + if err != nil && err.Error() != subtest.expectedErr.Error() { + t.Errorf("expected error (%v), got error (%v)", subtest.expectedErr, err) + } + }) + } +} + +func TestReadTextRegex(t *testing.T) { + subtests := []struct { + name string + tui *mocks.MockTUI + errMsg string + regexPattern string + expectedData string + expectedErr error + }{ + { + name: "Read TextRegex (pass)", + tui: &mocks.MockTUI{ + ReturnVals: []string{"foo"}, + Errs: nil, + }, + regexPattern: KindClusterRegex, + expectedData: "foo", + expectedErr: nil, + }, + { + name: "Read TextRegex (fail)", + tui: &mocks.MockTUI{ + ReturnVals: []string{"foo"}, + Errs: []error{errors.New("fail")}, + }, + regexPattern: KindClusterRegex, + expectedData: "foo", + expectedErr: errors.New("failure in ReadTextRegex: fail"), + }, + { + name: "Read TextRegex (fail_regex)", + tui: &mocks.MockTUI{ + ReturnVals: []string{"foo@"}, + Errs: nil, + }, + regexPattern: KindClusterRegex, + expectedData: "foo@", + errMsg: "error", + expectedErr: errors.New("failure in ReadTextRegex: input foo@ does not match regex ^[a-z0-9]{1}[a-z0-9-]{0,30}[a-z0-9]{1}$; error"), + }, + { + name: "Read TextRegex (fail_regex_long)", + tui: &mocks.MockTUI{ + ReturnVals: []string{"fffffffffffffffffffffffffffffffff"}, + Errs: nil, + }, + regexPattern: KindClusterRegex, + expectedData: "fffffffffffffffffffffffffffffffff", + errMsg: "error", + expectedErr: errors.New("failure in ReadTextRegex: input fffffffffffffffffffffffffffffffff does not match regex ^[a-z0-9]{1}[a-z0-9-]{0,30}[a-z0-9]{1}$; error"), + }, + { + name: "Read TextRegex (fail_regex_first_char_alpha)", + tui: &mocks.MockTUI{ + ReturnVals: []string{"-ff"}, + Errs: nil, + }, + regexPattern: KindClusterRegex, + expectedData: "-ff", + errMsg: "error", + expectedErr: errors.New("failure in ReadTextRegex: input -ff does not match regex ^[a-z0-9]{1}[a-z0-9-]{0,30}[a-z0-9]{1}$; error"), + }, + { + name: "Read TextRegex (fail_regex_last_char_alpha)", + tui: &mocks.MockTUI{ + ReturnVals: []string{"ff-"}, + Errs: nil, + }, + regexPattern: KindClusterRegex, + expectedData: "ff-", + errMsg: "error", + expectedErr: errors.New("failure in ReadTextRegex: input ff- does not match regex ^[a-z0-9]{1}[a-z0-9-]{0,30}[a-z0-9]{1}$; error"), + }, + { + name: "Read TextRegex (fail_regex_short)", + tui: &mocks.MockTUI{ + ReturnVals: []string{"f"}, + Errs: nil, + }, + regexPattern: KindClusterRegex, + expectedData: "f", + errMsg: "error", + expectedErr: errors.New("failure in ReadTextRegex: input f does not match regex ^[a-z0-9]{1}[a-z0-9-]{0,30}[a-z0-9]{1}$; error"), + }, + { + name: "Read TextRegex (invalid_regex)", + tui: &mocks.MockTUI{ + ReturnVals: []string{"abc"}, + Errs: nil, + }, + regexPattern: "?!", + errMsg: "fail", + expectedData: "abc", + expectedErr: errors.New("failure in ReadTextRegex: fail: error parsing regexp: missing argument to repetition operator: `?`"), + }, + { + name: "Read TextRegex (invalid_input)", + tui: &mocks.MockTUI{ + ReturnVals: []string{""}, + Errs: nil, + }, + regexPattern: "", + expectedData: "", + expectedErr: errors.New("failure in ReadTextRegex: input is mandatory"), + }, + } + for _, subtest := range subtests { + t.Run(subtest.name, func(t *testing.T) { + Tui = subtest.tui + + data, err := ReadTextRegex("", "", subtest.errMsg, subtest.regexPattern) + if !reflect.DeepEqual(data, subtest.expectedData) { + t.Errorf("expected (%s), got (%s)", subtest.expectedData, data) + } + if err != nil && err.Error() != subtest.expectedErr.Error() { + t.Errorf("expected error (%v), got error (%v)", subtest.expectedErr, err) + } + }) + } +} + +func TestReadURLRegex(t *testing.T) { + subtests := []struct { + name string + tui *mocks.MockTUI + errMsg string + regexPattern string + expectedData string + expectedErr error + }{ + { + name: "ReadURLRegex (pass)", + tui: &mocks.MockTUI{ + ReturnVals: []string{"https://company-maas.com/MAAS"}, + Errs: nil, + }, + regexPattern: MaasApiRegex, + expectedData: "https://company-maas.com/MAAS", + expectedErr: nil, + }, + { + name: "ReadURLRegex (fail)", + tui: &mocks.MockTUI{ + ReturnVals: []string{"foo"}, + Errs: []error{errors.New("fail")}, + }, + regexPattern: MaasApiRegex, + errMsg: "invalid MAAS URL", + expectedData: "foo", + expectedErr: errors.New("failure in ReadURLRegex: input foo does not match regex ^.*\\/MAAS$; invalid MAAS URL"), + }, + { + name: "ReadURLRegex (fail_url)", + tui: &mocks.MockTUI{ + ReturnVals: []string{"company-maas.com/MAAS"}, + }, + regexPattern: MaasApiRegex, + expectedData: "company-maas.com/MAAS", + errMsg: "fail", + expectedErr: errors.New("failure in ReadURLRegex: fail: parse \"company-maas.com/MAAS\": invalid URI for request"), + }, + { + name: "ReadURLRegex (fail_regex)", + tui: &mocks.MockTUI{ + ReturnVals: []string{"foo@"}, + Errs: nil, + }, + regexPattern: MaasApiRegex, + errMsg: "error", + expectedData: "foo@", + expectedErr: errors.New("failure in ReadURLRegex: input foo@ does not match regex ^.*\\/MAAS$; error"), + }, + { + name: "ReadURLRegex (invalid_regex)", + tui: &mocks.MockTUI{ + ReturnVals: []string{"abc"}, + Errs: nil, + }, + regexPattern: "?!", + errMsg: "fail", + expectedData: "abc", + expectedErr: errors.New("failure in ReadURLRegex: fail: error parsing regexp: missing argument to repetition operator: `?`"), + }, + } + for _, subtest := range subtests { + t.Run(subtest.name, func(t *testing.T) { + Tui = subtest.tui + + data, err := ReadURLRegex("", "", subtest.errMsg, subtest.regexPattern) + if !reflect.DeepEqual(data, subtest.expectedData) { + t.Errorf("expected (%s), got (%s)", subtest.expectedData, data) + } + if err != nil && err.Error() != subtest.expectedErr.Error() { + t.Errorf("expected error (%v), got error (%v)", subtest.expectedErr, err) + } + }) + } +} + +func TestSelect(t *testing.T) { + subtests := []struct { + name string + tui *mocks.MockTUI + expectedData string + expectedErr error + }{ + { + name: "Select (pass)", + tui: &mocks.MockTUI{ + ReturnVals: []string{"foo"}, + Errs: nil, + }, + expectedData: "foo", + expectedErr: nil, + }, + { + name: "Select (fail)", + tui: &mocks.MockTUI{ + ReturnVals: []string{"foo"}, + Errs: []error{errors.New("fail")}, + }, + expectedData: "", + expectedErr: errors.New("failure in Select: fail"), + }, + { + name: "Select (fail_no_items)", + tui: &mocks.MockTUI{ + Errs: []error{errors.New("failure in Select: no options available: Select (fail_no_items)")}, + }, + expectedData: "", + expectedErr: errors.New("failure in Select: no options available: Select (fail_no_items)"), + }, + } + for _, subtest := range subtests { + t.Run(subtest.name, func(t *testing.T) { + Tui = subtest.tui + + data, err := Select(subtest.name, subtest.tui.ReturnVals) + if !reflect.DeepEqual(data, subtest.expectedData) { + t.Errorf("expected (%s), got (%s)", subtest.expectedData, data) + } + if err != nil && err.Error() != subtest.expectedErr.Error() { + t.Errorf("expected error (%v), got error (%v)", subtest.expectedErr, err) + } + }) + } +} + +func TestSelectID(t *testing.T) { + zero := &ChoiceItem{ID: "0", Name: "zero"} + options := []ChoiceItem{*zero} + + subtests := []struct { + name string + tui *mocks.MockTUI + options []ChoiceItem + expectedData *ChoiceItem + expectedErr error + }{ + { + name: "SelectID (pass)", + tui: &mocks.MockTUI{ + ReturnVals: []string{"zero"}, + Errs: nil, + }, + options: options, + expectedData: zero, + expectedErr: nil, + }, + { + name: "SelectID (fail)", + tui: &mocks.MockTUI{ + ReturnVals: []string{"zero"}, + Errs: []error{errors.New("fail")}, + }, + options: options, + expectedData: nil, + expectedErr: errors.New("failure in SelectID: fail"), + }, + { + name: "SelectID (fail_no_items)", + tui: &mocks.MockTUI{ + Errs: []error{errors.New("failure in SelectID: no options available: SelectID (fail_no_items)")}, + }, + expectedData: nil, + expectedErr: errors.New("failure in SelectID: no options available: SelectID (fail_no_items)"), + }, + } + for _, subtest := range subtests { + t.Run(subtest.name, func(t *testing.T) { + Tui = subtest.tui + + data, err := SelectID(subtest.name, subtest.options) + if !reflect.DeepEqual(data, subtest.expectedData) { + t.Errorf("expected (%s), got (%s)", subtest.expectedData, data) + } + if err != nil && err.Error() != subtest.expectedErr.Error() { + t.Errorf("expected error (%v), got error (%v)", subtest.expectedErr, err) + } + }) + } +} + +func TestValidateNoProxy(t *testing.T) { + subtests := []struct { + name string + noProxy string + expectedErr error + }{ + { + name: "ValidateNoProxy (empty_string)", + noProxy: "", + expectedErr: nil, + }, + { + name: "ValidateNoProxy (pass_default)", + noProxy: "127.0.0.1,192.168.0.0/16,10.0.0.0/16,10.96.0.0/12,169.254.169.254,169.254.0.0/24,localhost,kubernetes,kubernetes.default,kubernetes.default.svc,kubernetes.default.svc.cluster,kubernetes.default.svc.cluster.local,.svc,.svc.cluster,.svc.cluster.local,.svc.cluster.local,.company.local", + expectedErr: nil, + }, + { + name: "ValidateNoProxy (fail)", + noProxy: "notanoproxy", + expectedErr: ValidationError, + }, + } + for _, subtest := range subtests { + t.Run(subtest.name, func(t *testing.T) { + for _, s := range strings.Split(subtest.noProxy, ",") { + err := ValidateNoProxy(s) + if err != nil && err.Error() != subtest.expectedErr.Error() { + t.Errorf("expected error (%v), got error (%v)", subtest.expectedErr, err) + } + } + }) + } +} + +func TestArtifactRefRegex(t *testing.T) { + regex := regexp.MustCompile(ArtifactRefRegex) + + testCases := []struct { + input string + expectedMatch bool + }{ + { + input: "valid/digest/artifact/path@sha256:abcdef", + expectedMatch: true, + }, + { + input: "valid/tag/artifact/path:v1.0.0", + expectedMatch: true, + }, + { + input: "valid/no/tag/or/digest", + expectedMatch: true, + }, + { + input: "Invalid/Path/To/Artifact", + expectedMatch: false, + }, + { + input: "invalid/digest/artifact/path@sha253:abcdef", + expectedMatch: false, + }, + } + + for _, tc := range testCases { + actual := regex.MatchString(tc.input) + if actual != tc.expectedMatch { + t.Errorf("input: %s, expected: %v, actual: %v", tc.input, tc.expectedMatch, actual) + } + } +} From 20fc72fb3d9ba9836571393c76fa1a15ff64e36e Mon Sep 17 00:00:00 2001 From: Ahmad Ibrahim Date: Thu, 30 May 2024 11:21:14 -0700 Subject: [PATCH 3/3] ci: setup ci --- .github/workflows/bulwark-gitleaks.yaml | 39 ++++++++++++++++ .github/workflows/bulwark-golicenses.yaml | 31 +++++++++++++ .github/workflows/bulwark-gosec.yaml | 49 ++++++++++++++++++++ .github/workflows/bulwark-govulncheck.yaml | 27 +++++++++++ .github/workflows/ci.yaml | 19 ++++++++ .gitignore | 4 ++ .golangci.yaml | 53 ++++++++++++++++++++++ Makefile | 48 ++++++++++++++++++++ bin/.gitkeep | 0 prompts/prompts.go | 35 +++----------- prompts/prompts_test.go | 2 + 11 files changed, 278 insertions(+), 29 deletions(-) create mode 100644 .github/workflows/bulwark-gitleaks.yaml create mode 100644 .github/workflows/bulwark-golicenses.yaml create mode 100644 .github/workflows/bulwark-gosec.yaml create mode 100644 .github/workflows/bulwark-govulncheck.yaml create mode 100644 .github/workflows/ci.yaml create mode 100644 .gitignore create mode 100644 .golangci.yaml create mode 100644 Makefile create mode 100644 bin/.gitkeep diff --git a/.github/workflows/bulwark-gitleaks.yaml b/.github/workflows/bulwark-gitleaks.yaml new file mode 100644 index 0000000..caa9451 --- /dev/null +++ b/.github/workflows/bulwark-gitleaks.yaml @@ -0,0 +1,39 @@ +name: BulwarkGitLeaks + +on: + pull_request: + workflow_dispatch: + +concurrency: + group: gitleaks-${{ github.ref }} + cancel-in-progress: true + +jobs: + gitleaks-pr-scan: + runs-on: ubuntu-latest + container: + image: gcr.io/spectro-dev-public/bulwark/gitleaks:latest + env: + REPO: ${{ github.event.repository.name }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITLEAKS_CONFIG: /workspace/config.toml + steps: + + - name: run-bulwark-gitleaks-scan + shell: sh + env: + BRANCH: ${{ github.head_ref || github.ref_name }} + run: /workspace/bulwark -name CodeSASTGitLeaks -organization spectrocloud-labs -target $REPO -tags "branch:$BRANCH,options:--log-opts origin..HEAD" + + - name: check-result + shell: sh + run: | + resultPath=./$REPO/gitleaks.json + cat $resultPath | grep -v \"Match\"\: | grep -v \"Secret\"\: + total_failed_tests=`cat $resultPath | grep \"Fingerprint\"\: | wc -l` + if [ "$total_failed_tests" -gt 0 ]; then + echo "GitLeaks validation check failed with above findings..." + exit 1 + else + echo "GitLeaks validation check passed" + fi diff --git a/.github/workflows/bulwark-golicenses.yaml b/.github/workflows/bulwark-golicenses.yaml new file mode 100644 index 0000000..410fd85 --- /dev/null +++ b/.github/workflows/bulwark-golicenses.yaml @@ -0,0 +1,31 @@ +name: GoLicenses + +on: + pull_request: + workflow_dispatch: + +concurrency: + group: golicenses-${{ github.ref }} + cancel-in-progress: true + +jobs: + golicense-pr-scan: + runs-on: ubuntu-latest + steps: + - name: install-git + run: sudo apt-get install -y git + + - name: install-golicenses + run: GOBIN=/usr/local/bin go install github.com/google/go-licenses@v1.0.0 + + - name: checkout + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4 + + - name: Set up Go + uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # v4 + with: + go-version: '1.22' + + - name: golicense-scan + run: | + go-licenses check ./... diff --git a/.github/workflows/bulwark-gosec.yaml b/.github/workflows/bulwark-gosec.yaml new file mode 100644 index 0000000..33cb30f --- /dev/null +++ b/.github/workflows/bulwark-gosec.yaml @@ -0,0 +1,49 @@ +name: BulwarkGoSec + +on: + pull_request: + workflow_dispatch: + +concurrency: + group: gosec-${{ github.ref }} + cancel-in-progress: true + +jobs: + gosec-pr-scan: + runs-on: ubuntu-latest + container: + image: gcr.io/spectro-dev-public/bulwark/gosec:latest + env: + REPO: ${{ github.event.repository.name }} + steps: + + - name: Set up Go + uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # v4 + with: + go-version: '1.22' + + - name: checkout + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4 + + - name: run-gosec-scan + shell: sh + env: + BRANCH: ${{ github.head_ref || github.ref_name }} + GO111MODULE: on + run: /workspace/bulwark -name CodeSASTGoSec -verbose -organization spectrocloud-labs -target $REPO -tags "branch:$BRANCH" + + - name: check-result + shell: sh + run: | + resultPath=$REPO-result.json + issues=$(cat $resultPath | jq -r '.Stats.found') + echo "Found ${issues} issues" + echo "Issues by Rule ID" + jq -r '.Issues | group_by (.rule_id)[] | {rule: .[0].rule_id, count: length}' $resultPath + if [ "$issues" -gt 0 ]; then + echo "GoSec SAST scan failed with below findings..." + cat $resultPath + exit 1 + else + echo "GoSec SAST scan passed" + fi diff --git a/.github/workflows/bulwark-govulncheck.yaml b/.github/workflows/bulwark-govulncheck.yaml new file mode 100644 index 0000000..d298ec2 --- /dev/null +++ b/.github/workflows/bulwark-govulncheck.yaml @@ -0,0 +1,27 @@ +name: GoVulnCheck + +on: + pull_request: + workflow_dispatch: + +concurrency: + group: govulncheck-${{ github.ref }} + cancel-in-progress: true + +jobs: + govulncheck-pr-scan: + runs-on: ubuntu-latest + container: + image: gcr.io/spectro-images-public/golang:1.22-alpine + steps: + - name: install-govulncheck + run: GOBIN=/usr/local/bin go install golang.org/x/vuln/cmd/govulncheck@latest + + - name: checkout + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4 + + - name: govulncheck-scan + run: | + go version + govulncheck -mode source ./... + diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml new file mode 100644 index 0000000..c09fd0b --- /dev/null +++ b/.github/workflows/ci.yaml @@ -0,0 +1,19 @@ +on: + push: + workflow_dispatch: + +jobs: + lint-and-test: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v3 + - name: Unshallow + run: git fetch --prune --unshallow + - name: Set up Go + uses: actions/setup-go@v3 + with: + go-version: 1.22 + - name: Test + run: make test + diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3a2ce8b --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +bin/* +!bin/.gitkeep +_build +.DS_Store diff --git a/.golangci.yaml b/.golangci.yaml new file mode 100644 index 0000000..e236b1b --- /dev/null +++ b/.golangci.yaml @@ -0,0 +1,53 @@ +## golangci-lint v1.55.2 + +# References: +# - https://golangci-lint.run/usage/linters/ +# - https://gist.github.com/maratori/47a4d00457a92aa426dbd48a18776322 + +run: + timeout: 10m # default 1m + +linters-settings: + gosimple: + go: "1.21" # default 1.13 + govet: + enable-all: true + disable: + - fieldalignment # too strict + - shadow # too strict + staticcheck: + go: "1.21" # default 1.13 + + # Non-default + cyclop: + max-complexity: 12 # maximal code complexity to report; default 10 + package-average: 0.0 # maximal average package complexity to report; default 0.0 + gocognit: + min-complexity: 30 # minimal code complexity to report; default: 30 + +linters: + disable-all: true + enable: + ## enabled by default + - errcheck # Errcheck is a program for checking for unchecked errors in go programs. These unchecked errors can be critical bugs in some cases + - ineffassign # Detects when assignments to existing variables are not used + - typecheck # Like the front-end of a Go compiler, parses and type-checks Go code + - gosimple # Linter for Go source code that specializes in simplifying a code + - govet # Vet examines Go source code and reports suspicious constructs, such as Printf calls whose arguments do not align with the format string + - unused # Checks Go code for unused constants, variables, functions and types + - staticcheck # Staticcheck is a go vet on steroids, applying a ton of static analysis checks + ## disabled by default + - cyclop # checks function and package cyclomatic complexity + - gocognit # Computes and checks the cognitive complexity of functions + +issues: + max-issues-per-linter: 0 + max-same-issues: 0 + exclude-rules: + - path: _test\.go + linters: + - errcheck + - gosimple + - ineffassign + - staticcheck + - unused diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..ca7c960 --- /dev/null +++ b/Makefile @@ -0,0 +1,48 @@ +# If you update this file, please follow: +# https://suva.sh/posts/well-documented-makefiles/ + +.DEFAULT_GOAL:=help + +# binary versions +BIN_DIR ?= ./bin +GOLANGCI_VERSION ?= 1.55.2 + +GOOS ?= $(shell go env GOOS) +GOARCH ?= $(shell go env GOARCH) + +##@ Help Targets +help: ## Display this help + @awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m\033[0m\n"} /^[a-zA-Z_-]+:.*?##/ { printf " \033[36m%-15s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[0m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST) + +##@ Test Targets +.PHONY: test +test: static ## Run tests + @mkdir -p _build/cov + go test -covermode=atomic -coverpkg=./... -coverprofile _build/cov/coverage.out ./... -timeout 120m + +##@ Static Analysis Targets +static: fmt lint vet +fmt: ## Run go fmt against code + go fmt ./... +lint: golangci-lint ## Run golangci-lint + $(GOLANGCI_LINT) run +vet: ## Run go vet against code + go vet ./... + +## Tools & binaries +golangci-lint: + if ! test -f $(BIN_DIR)/golangci-lint-linux-amd64; then \ + curl -LOs https://github.com/golangci/golangci-lint/releases/download/v$(GOLANGCI_VERSION)/golangci-lint-$(GOLANGCI_VERSION)-linux-amd64.tar.gz; \ + tar -zxf golangci-lint-$(GOLANGCI_VERSION)-linux-amd64.tar.gz; \ + mv golangci-lint-$(GOLANGCI_VERSION)-*/golangci-lint $(BIN_DIR)/golangci-lint-linux-amd64; \ + chmod +x $(BIN_DIR)/golangci-lint-linux-amd64; \ + rm -rf ./golangci-lint-$(GOLANGCI_VERSION)-linux-amd64*; \ + fi + if ! test -f $(BIN_DIR)/golangci-lint-$(GOOS)-$(GOARCH); then \ + curl -LOs https://github.com/golangci/golangci-lint/releases/download/v$(GOLANGCI_VERSION)/golangci-lint-$(GOLANGCI_VERSION)-$(GOOS)-$(GOARCH).tar.gz; \ + tar -zxf golangci-lint-$(GOLANGCI_VERSION)-$(GOOS)-$(GOARCH).tar.gz; \ + mv golangci-lint-$(GOLANGCI_VERSION)-*/golangci-lint $(BIN_DIR)/golangci-lint-$(GOOS)-$(GOARCH); \ + chmod +x $(BIN_DIR)/golangci-lint-$(GOOS)-$(GOARCH); \ + rm -rf ./golangci-lint-$(GOLANGCI_VERSION)-$(GOOS)-$(GOARCH)*; \ + fi +GOLANGCI_LINT=$(BIN_DIR)/golangci-lint-$(GOOS)-$(GOARCH) diff --git a/bin/.gitkeep b/bin/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/prompts/prompts.go b/prompts/prompts.go index 40bba4f..bca6af3 100644 --- a/prompts/prompts.go +++ b/prompts/prompts.go @@ -39,19 +39,8 @@ var ( // Exported regex patterns for use with ReadTextRegex KindClusterRegex = "^[a-z0-9]{1}[a-z0-9-]{0,30}[a-z0-9]{1}$" - MaasApiRegex = "^.*\\/MAAS$" - // Source: https://github.com/kubevirt/containerized-data-importer/blob/main/pkg/apiserver/webhooks/util.go#L122C3-L122C3 - CDIImageRegistryRegex = "^(docker|oci-archive):\\/\\/([a-z0-9.\\-\\/]+)([:]{0})$" - // Allowed chars are alphanumerics plus '.', '-', and '_', but cannot start or end with a symbol. - // Additionally, 2+ consecutive symbols are disallowed. - UsernameRegex = "[a-zA-Z0-9]+(?:\\.[a-zA-Z0-9]+)*(?:-[a-zA-Z0-9]+)*(?:_[a-zA-Z0-9]+)*" - PaletteResourceNameRegex = "[a-z][a-z0-9-]{1,31}[a-z0-9]" - VSphereUsernameRegex = "^" + UsernameRegex + "@" + domain + "$" - CPUReqRegex = "(^\\d+\\.?\\d*[M,G]Hz)" - MemoryReqRegex = "(^\\d+\\.?\\d*[M,G,T]i)" - DiskReqRegex = "(^\\d+\\.?\\d*[M,G,T]i)" - ArtifactRefRegex = "^[a-z0-9_.\\-\\/]+(:.*)?(@sha256:.*)?$" - UUIDRegex = "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$" + ArtifactRefRegex = "^[a-z0-9_.\\-\\/]+(:.*)?(@sha256:.*)?$" + UUIDRegex = "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$" noProxyExceptions = []string{"*", "localhost", "kubernetes"} domainRegex = regexp.MustCompile("^" + domain + "$") @@ -70,8 +59,7 @@ type TUI interface { type PtermTUI struct{} -// GetBool prompts a bool from the user while automatically appending a ? character to the end of -// the prompt message. +// GetBool prompts a bool from the user while automatically appending a ? character to the end of the prompt message. func (p PtermTUI) GetBool(prompt string, defaultVal bool) (bool, error) { return pterm.DefaultInteractiveConfirm. WithDefaultText(prompt + "?"). @@ -86,9 +74,9 @@ func (p PtermTUI) GetText(prompt, defaultVal, mask string, optional bool, valida prompt = fmt.Sprintf("%s (optional, hit enter to skip)", prompt) } - // shoddy workaround for https://github.com/pterm/pterm/issues/560 - // inputs longer than the terminal width are handled better with multiline - // enabled... but the prompt is still repeated after every key press + // workaround for https://github.com/pterm/pterm/issues/560: + // inputs longer than the terminal width are handled better with + // multiline enabled, but the prompt is still repeated after every key press var multiline bool if len(defaultVal) > 60 { multiline = true @@ -237,7 +225,6 @@ func ReadText(label, defaultVal string, optional bool, maxLen int) (string, erro } func ReadTextRegex(label, defaultVal, errMsg, regexPattern string) (string, error) { - validate := func(input string) error { if input == "" { return InputMandatoryError @@ -261,7 +248,6 @@ func ReadTextRegex(label, defaultVal, errMsg, regexPattern string) (string, erro } func ReadSemVer(label, defaultVal, errMsg string) (string, error) { - validate := func(input string) error { if input == "" { return InputMandatoryError @@ -319,7 +305,6 @@ func ReadBasicCreds(usernamePrompt, passwordPrompt, defaultUsername, defaultPass } func ReadURL(label, defaultVal, errMsg string, optional bool) (string, error) { - validate := func(input string) error { if input == "" { if !optional { @@ -351,7 +336,6 @@ func ReadURL(label, defaultVal, errMsg string, optional bool) (string, error) { } func ReadURLRegex(label, defaultVal, errMsg, regexPattern string) (string, error) { - validate := func(input string) error { r, err := regexp.Compile(regexPattern) if err != nil { @@ -386,7 +370,6 @@ func ReadURLRegex(label, defaultVal, errMsg, regexPattern string) (string, error } func ReadDomains(label, defaultVal, errMsg string, optional bool, maxVals int) (string, error) { - validate := func(input string) error { if input == "" { if !optional { @@ -415,7 +398,6 @@ func ReadDomains(label, defaultVal, errMsg string, optional bool, maxVals int) ( } func ReadIPs(label, defaultVal, errMsg string, optional bool, maxVals int) (string, error) { - validate := func(input string) error { if input == "" { if !optional { @@ -444,7 +426,6 @@ func ReadIPs(label, defaultVal, errMsg string, optional bool, maxVals int) (stri } func ReadDomainsOrIPs(label, defaultVal, errMsg string, optional bool, maxVals int) (string, error) { - validate := func(input string) error { if input == "" { if !optional { @@ -477,7 +458,6 @@ func ReadDomainsOrIPs(label, defaultVal, errMsg string, optional bool, maxVals i } func ReadDomainOrIPNoPort(label, defaultVal, errMsg string, optional bool) (string, error) { - validate := func(input string) error { if input == "" { if !optional { @@ -503,7 +483,6 @@ func ReadDomainOrIPNoPort(label, defaultVal, errMsg string, optional bool) (stri } func ReadCIDRs(label, defaultVal, errMsg string, optional bool, maxVals int) (string, error) { - validate := func(input string) error { if input == "" { if !optional { @@ -532,7 +511,6 @@ func ReadCIDRs(label, defaultVal, errMsg string, optional bool, maxVals int) (st } func ReadFilePath(label, defaultVal, errMsg string, optional bool) (string, error) { - validate := func(input string) error { if input == "" { if !optional { @@ -559,7 +537,6 @@ func ReadFilePath(label, defaultVal, errMsg string, optional bool) (string, erro } func ReadK8sName(label, defaultVal string, optional bool) (string, error) { - validate := func(input string) error { if err := validateStringFunc(optional, -1)(input); err != nil { return err diff --git a/prompts/prompts_test.go b/prompts/prompts_test.go index cc4c80e..a3a577d 100644 --- a/prompts/prompts_test.go +++ b/prompts/prompts_test.go @@ -870,6 +870,8 @@ func TestReadTextRegex(t *testing.T) { } func TestReadURLRegex(t *testing.T) { + MaasApiRegex := "^.*\\/MAAS$" + subtests := []struct { name string tui *mocks.MockTUI