From aedbc35080b0055cd0c7a137746dcb2ca4265f86 Mon Sep 17 00:00:00 2001 From: Xavier Coulon Date: Fri, 11 Oct 2024 11:03:14 +0200 Subject: [PATCH] refactor: logging and prompting using github.com/charmbracelet/log and github.com/charmbracelet/huh also, reword a few messages output in the console Signed-off-by: Xavier Coulon --- go.mod | 41 +++- go.sum | 89 ++++++-- pkg/client/client.go | 52 +++-- pkg/client/client_test.go | 138 ++++++------ pkg/cmd/add_space_users.go | 20 +- pkg/cmd/add_space_users_test.go | 81 ++++--- pkg/cmd/adm/install_operator.go | 75 ++++--- pkg/cmd/adm/install_operator_test.go | 60 +++--- pkg/cmd/adm/must_gather_namespace.go | 32 +-- pkg/cmd/adm/must_gather_namespace_test.go | 19 +- pkg/cmd/adm/register_member.go | 81 +++---- pkg/cmd/adm/register_member_test.go | 143 ++++++------ pkg/cmd/adm/restart.go | 55 +++-- pkg/cmd/adm/restart_test.go | 41 ++-- pkg/cmd/adm/unregister_member.go | 20 +- pkg/cmd/adm/unregister_member_test.go | 56 ++--- pkg/cmd/approve.go | 14 +- pkg/cmd/approve_test.go | 125 ++++++----- pkg/cmd/ban.go | 30 ++- pkg/cmd/ban_test.go | 68 +++--- pkg/cmd/base_kubectl.go | 6 +- pkg/cmd/create_social_event.go | 10 +- pkg/cmd/create_social_event_test.go | 21 +- pkg/cmd/deactivate.go | 17 +- pkg/cmd/deactivate_test.go | 35 +-- pkg/cmd/delete.go | 18 +- pkg/cmd/delete_test.go | 45 ++-- ...{disable-feature.go => disable_feature.go} | 37 ++-- pkg/cmd/disable_feature_test.go | 43 ++-- pkg/cmd/disable_user.go | 17 +- pkg/cmd/disable_user_test.go | 35 +-- .../{enable-feature.go => enable_feature.go} | 40 ++-- pkg/cmd/enable_feature_test.go | 62 +++--- pkg/cmd/generate/admin-manifests.go | 6 +- pkg/cmd/generate/admin-manifests_test.go | 38 ++-- pkg/cmd/generate/cli_configs.go | 24 +-- pkg/cmd/generate/cli_configs_test.go | 8 +- pkg/cmd/generate/cluster.go | 5 +- pkg/cmd/generate/mock_test.go | 10 +- pkg/cmd/generate/nstemplatetiers.go | 10 +- pkg/cmd/generate/nstemplatetiers_test.go | 6 +- pkg/cmd/promote_space.go | 21 +- pkg/cmd/promote_space_test.go | 41 ++-- pkg/cmd/promote_user.go | 20 +- pkg/cmd/promote_user_test.go | 42 ++-- pkg/cmd/remove_space_users.go | 20 +- pkg/cmd/remove_space_users_test.go | 62 +++--- pkg/cmd/retarget.go | 36 ++-- pkg/cmd/retarget_test.go | 54 +++-- pkg/cmd/status.go | 10 +- pkg/cmd/status_test.go | 44 ++-- pkg/configuration/configuration.go | 26 +-- pkg/configuration/configuration_test.go | 79 ++++--- pkg/context/command_context.go | 1 - pkg/context/command_context_test.go | 17 +- pkg/ioutils/terminal.go | 194 +++++++---------- pkg/ioutils/terminal_test.go | 203 +++++++++--------- pkg/test/fake_terminal.go | 111 +++++----- 58 files changed, 1428 insertions(+), 1286 deletions(-) rename pkg/cmd/{disable-feature.go => disable_feature.go} (56%) rename pkg/cmd/{enable-feature.go => enable_feature.go} (70%) diff --git a/go.mod b/go.mod index a75be667..b762c110 100644 --- a/go.mod +++ b/go.mod @@ -13,7 +13,7 @@ require ( github.com/pkg/errors v0.9.1 github.com/spf13/cobra v1.8.0 github.com/stretchr/testify v1.9.0 - golang.org/x/term v0.18.0 + golang.org/x/term v0.22.0 // indirect gopkg.in/yaml.v2 v2.4.0 gopkg.in/yaml.v3 v3.0.1 k8s.io/api v0.25.0 @@ -27,6 +27,8 @@ require ( ) require ( + github.com/charmbracelet/glamour v0.8.0 + github.com/charmbracelet/huh v0.5.3 github.com/charmbracelet/log v0.4.0 github.com/google/uuid v1.6.0 github.com/h2non/gock v1.2.0 @@ -42,15 +44,27 @@ require ( github.com/ProtonMail/go-crypto v0.0.0-20230217124315-7d5c6f04bbb8 // indirect github.com/PuerkitoBio/purell v1.1.1 // indirect github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect + github.com/alecthomas/chroma/v2 v2.14.0 // indirect + github.com/atotto/clipboard v0.1.4 // indirect github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect + github.com/aymerick/douceur v0.2.0 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/blang/semver/v4 v4.0.0 // indirect + github.com/catppuccin/go v0.2.0 // indirect github.com/cespare/xxhash/v2 v2.1.2 // indirect github.com/chai2010/gettext-go v1.0.2 // indirect - github.com/charmbracelet/lipgloss v0.10.0 // indirect + github.com/charmbracelet/bubbles v0.20.0 // indirect + github.com/charmbracelet/bubbletea v1.1.0 // indirect + github.com/charmbracelet/lipgloss v0.13.0 // indirect + github.com/charmbracelet/x/ansi v0.2.3 // indirect + github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0 // indirect + github.com/charmbracelet/x/term v0.2.0 // indirect github.com/cloudflare/circl v1.3.7 // indirect github.com/davecgh/go-spew v1.1.1 // indirect + github.com/dlclark/regexp2 v1.11.0 // indirect + github.com/dustin/go-humanize v1.0.1 // indirect github.com/emicklei/go-restful/v3 v3.8.0 // indirect + github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect github.com/evanphx/json-patch v5.6.0+incompatible // indirect github.com/evanphx/json-patch/v5 v5.6.0 // indirect github.com/exponent-io/jsonpath v0.0.0-20151013193312-d6023ce2651d // indirect @@ -74,6 +88,7 @@ require ( github.com/google/go-querystring v1.1.0 // indirect github.com/google/gofuzz v1.2.0 // indirect github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect + github.com/gorilla/css v1.0.1 // indirect github.com/gorilla/mux v1.8.0 // indirect github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7 // indirect github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 // indirect @@ -85,20 +100,25 @@ require ( github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de // indirect github.com/lucasb-eyer/go-colorful v1.2.0 // indirect github.com/mailru/easyjson v0.7.6 // indirect - github.com/mattn/go-isatty v0.0.18 // indirect - github.com/mattn/go-runewidth v0.0.15 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mattn/go-localereader v0.0.1 // indirect + github.com/mattn/go-runewidth v0.0.16 // indirect github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 // indirect + github.com/microcosm-cc/bluemonday v1.0.27 // indirect github.com/migueleliasweb/go-github-mock v0.0.18 // indirect github.com/mitchellh/copystructure v1.0.0 // indirect github.com/mitchellh/go-wordwrap v1.0.0 // indirect + github.com/mitchellh/hashstructure/v2 v2.0.2 // indirect github.com/mitchellh/reflectwalk v1.0.0 // indirect github.com/moby/spdystream v0.2.0 // indirect github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 // indirect + github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect + github.com/muesli/cancelreader v0.2.2 // indirect github.com/muesli/reflow v0.3.0 // indirect - github.com/muesli/termenv v0.15.2 // indirect + github.com/muesli/termenv v0.15.3-0.20240618155329-98d742f6907a // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/openshift/library-go v0.0.0-20230301092340-c13b89190a26 // indirect github.com/peterbourgon/diskv v2.0.1+incompatible // indirect @@ -115,16 +135,19 @@ require ( github.com/spf13/cast v1.3.1 // indirect github.com/spf13/pflag v1.0.5 // indirect github.com/xlab/treeprint v1.1.0 // indirect + github.com/yuin/goldmark v1.7.4 // indirect + github.com/yuin/goldmark-emoji v1.0.3 // indirect go.starlark.net v0.0.0-20200306205701-8dd3e2ee1dd5 // indirect go.uber.org/atomic v1.7.0 // indirect go.uber.org/multierr v1.6.0 // indirect go.uber.org/zap v1.21.0 // indirect - golang.org/x/crypto v0.21.0 // indirect + golang.org/x/crypto v0.25.0 // indirect golang.org/x/exp v0.0.0-20231006140011-7918f672742d // indirect - golang.org/x/net v0.23.0 // indirect + golang.org/x/net v0.27.0 // indirect golang.org/x/oauth2 v0.7.0 // indirect - golang.org/x/sys v0.18.0 // indirect - golang.org/x/text v0.14.0 // indirect + golang.org/x/sync v0.8.0 // indirect + golang.org/x/sys v0.25.0 // indirect + golang.org/x/text v0.18.0 // indirect golang.org/x/time v0.3.0 // indirect gomodules.xyz/jsonpatch/v2 v2.2.0 // indirect google.golang.org/appengine v1.6.7 // indirect diff --git a/go.sum b/go.sum index aff6a173..0a670618 100644 --- a/go.sum +++ b/go.sum @@ -74,6 +74,10 @@ github.com/PuerkitoBio/purell v1.1.1 h1:WEQqlqaGbrPkxLJWfBwQmfEAE1Z7ONdDLqrN38tN github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M= github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= +github.com/alecthomas/assert/v2 v2.7.0 h1:QtqSACNS3tF7oasA8CU6A6sXZSBDqnm7RfpLl9bZqbE= +github.com/alecthomas/chroma/v2 v2.14.0 h1:R3+wzpnUArGcQz7fCETQBzO5n9IMNi13iIs46aU4V9E= +github.com/alecthomas/chroma/v2 v2.14.0/go.mod h1:QolEbTfmUHIMVpBqxeDnNBj2uoeI4EbYP4i6n68SG4I= +github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= @@ -87,8 +91,13 @@ github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmV github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY= +github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4= +github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= +github.com/aymanbagabas/go-udiff v0.2.0 h1:TK0fH4MteXUDspT88n8CKzvK0X9O2xu9yQjWpi6yML8= +github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk= +github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4= github.com/benbjohnson/clock v1.0.3/go.mod h1:bGMdMPoPVvcYyt1gHDf4J2KE153Yf9BuiUKYMaxlTDM= github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8= github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= @@ -104,6 +113,8 @@ github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ= github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0= github.com/bwesterb/go-ristretto v1.2.0/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0= +github.com/catppuccin/go v0.2.0 h1:ktBeIrIP42b/8FGiScP9sgrWOss3lw0Z5SktRoithGA= +github.com/catppuccin/go v0.2.0/go.mod h1:8IHJuMGaUUjQM82qBrGNBv7LFq6JI3NnQCF6MOlZjpc= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/certifi/gocertifi v0.0.0-20191021191039-0944d244cd40/go.mod h1:sGbDF6GwGcLpkNXPUTkMRoywsNa/ol15pxFe6ERfguA= github.com/certifi/gocertifi v0.0.0-20200922220541-2c3bb06c6054/go.mod h1:sGbDF6GwGcLpkNXPUTkMRoywsNa/ol15pxFe6ERfguA= @@ -114,10 +125,25 @@ github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XL github.com/chai2010/gettext-go v0.0.0-20160711120539-c6fed771bfd5/go.mod h1:/iP1qXHoty45bqomnu2LM+VVyAEdWN+vtSHGlQgyxbw= github.com/chai2010/gettext-go v1.0.2 h1:1Lwwip6Q2QGsAdl/ZKPCwTe9fe0CjlUbqj5bFNSjIRk= github.com/chai2010/gettext-go v1.0.2/go.mod h1:y+wnP2cHYaVj19NZhYKAwEMH2CI1gNHeQQ+5AjwawxA= -github.com/charmbracelet/lipgloss v0.10.0 h1:KWeXFSexGcfahHX+54URiZGkBFazf70JNMtwg/AFW3s= -github.com/charmbracelet/lipgloss v0.10.0/go.mod h1:Wig9DSfvANsxqkRsqj6x87irdy123SR4dOXlKa91ciE= +github.com/charmbracelet/bubbles v0.20.0 h1:jSZu6qD8cRQ6k9OMfR1WlM+ruM8fkPWkHvQWD9LIutE= +github.com/charmbracelet/bubbles v0.20.0/go.mod h1:39slydyswPy+uVOHZ5x/GjwVAFkCsV8IIVy+4MhzwwU= +github.com/charmbracelet/bubbletea v1.1.0 h1:FjAl9eAL3HBCHenhz/ZPjkKdScmaS5SK69JAK2YJK9c= +github.com/charmbracelet/bubbletea v1.1.0/go.mod h1:9Ogk0HrdbHolIKHdjfFpyXJmiCzGwy+FesYkZr7hYU4= +github.com/charmbracelet/glamour v0.8.0 h1:tPrjL3aRcQbn++7t18wOpgLyl8wrOHUEDS7IZ68QtZs= +github.com/charmbracelet/glamour v0.8.0/go.mod h1:ViRgmKkf3u5S7uakt2czJ272WSg2ZenlYEZXT2x7Bjw= +github.com/charmbracelet/huh v0.5.3 h1:3KLP4a/K1/S4dq4xFMTNMt3XWhgMl/yx8NYtygQ0bmg= +github.com/charmbracelet/huh v0.5.3/go.mod h1:OZC3lshuF+/y8laj//DoZdFSHxC51OrtXLJI8xWVouQ= +github.com/charmbracelet/lipgloss v0.13.0 h1:4X3PPeoWEDCMvzDvGmTajSyYPcZM4+y8sCA/SsA3cjw= +github.com/charmbracelet/lipgloss v0.13.0/go.mod h1:nw4zy0SBX/F/eAO1cWdcvy6qnkDUxr8Lw7dvFrAIbbY= github.com/charmbracelet/log v0.4.0 h1:G9bQAcx8rWA2T3pWvx7YtPTPwgqpk7D68BX21IRW8ZM= github.com/charmbracelet/log v0.4.0/go.mod h1:63bXt/djrizTec0l11H20t8FDSvA4CRZJ1KH22MdptM= +github.com/charmbracelet/x/ansi v0.2.3 h1:VfFN0NUpcjBRd4DnKfRaIRo53KRgey/nhOoEqosGDEY= +github.com/charmbracelet/x/ansi v0.2.3/go.mod h1:dk73KoMTT5AX5BsX0KrqhsTqAnhZZoCBjs7dGWp4Ktw= +github.com/charmbracelet/x/exp/golden v0.0.0-20240815200342-61de596daa2b h1:MnAMdlwSltxJyULnrYbkZpp4k58Co7Tah3ciKhSNo0Q= +github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0 h1:qko3AQ4gK1MTS/de7F5hPGx6/k1u0w4TeYmBFwzYVP4= +github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0/go.mod h1:pBhA0ybfXv6hDjQUZ7hk1lVxBiUbupdw5R31yPUViVQ= +github.com/charmbracelet/x/term v0.2.0 h1:cNB9Ot9q8I711MyZ7myUR5HFWL/lc3OpU8jZ4hwm0x0= +github.com/charmbracelet/x/term v0.2.0/go.mod h1:GVxgxAbjUrmpvIINHIQnJJKpMlHiZ4cktEQCN6GWyF0= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= @@ -160,10 +186,14 @@ github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs github.com/daviddengcn/go-colortext v0.0.0-20160507010035-511bcaf42ccd/go.mod h1:dv4zxwHi5C/8AeI+4gX4dCWOIvNi7I6JCSX0HvlKPgE= github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= +github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI= +github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= github.com/docker/distribution v2.7.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE= github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= +github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/elazarl/goproxy v0.0.0-20180725130230-947c36da3153 h1:yUdfgN0XgIJw7foRItutHYUIhlcKzcSf5vDpdhQAKTc= github.com/elazarl/goproxy v0.0.0-20180725130230-947c36da3153/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc= github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= @@ -178,6 +208,8 @@ github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.m github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4= +github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM= github.com/evanphx/json-patch v0.5.2/go.mod h1:ZWS5hhDbVDyob71nXKNL0+PWn6ToqBHMikGIFbs31qQ= github.com/evanphx/json-patch v4.9.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= github.com/evanphx/json-patch v4.11.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= @@ -352,6 +384,8 @@ github.com/googleapis/gnostic v0.4.1/go.mod h1:LRhVm6pbyptWbWbuZ38d1eyptfvIytN3i github.com/googleapis/gnostic v0.5.1/go.mod h1:6U4PtQXGIEt/Z3h5MAT7FNofLnw9vXk2cUuW7uA/OeU= github.com/googleapis/gnostic v0.5.5/go.mod h1:7+EbHbldMins07ALC74bsA81Ovc97DwqyJO1AENw9kA= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8= +github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0= github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= @@ -391,6 +425,7 @@ github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ= github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I= github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc= +github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/huandu/xstrings v1.3.1 h1:4jgBlKK6tLKFvO8u5pmYjG91cqytmDCDvGh7ECVFfFs= github.com/huandu/xstrings v1.3.1/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= @@ -461,16 +496,20 @@ github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hd github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84= github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= -github.com/mattn/go-isatty v0.0.18 h1:DOKFKCQ7FNG2L1rbrmstDN4QVRdS89Nkh85u68Uwp98= -github.com/mattn/go-isatty v0.0.18/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4= +github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88= github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= github.com/mattn/go-runewidth v0.0.7/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= -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/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= +github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 h1:I0XW9+e1XWDxdcEniV4rQAIOPUGDq67JSCiRCgGCZLI= github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= +github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk= +github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA= github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= github.com/migueleliasweb/go-github-mock v0.0.18 h1:0lWt9MYmZQGnQE2rFtjlft/YtD6hzxuN6JJRFpujzEI= github.com/migueleliasweb/go-github-mock v0.0.18/go.mod h1:CcgXcbMoRnf3rRVHqGssuBquZDIcaplxL2W6G+xs7kM= @@ -485,6 +524,8 @@ github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eI github.com/mitchellh/go-wordwrap v1.0.0 h1:6GlHJ/LTGMrIJbwgdqdl2eEH8o+Exx/0m8ir9Gns0u4= github.com/mitchellh/go-wordwrap v1.0.0/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo= github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg= +github.com/mitchellh/hashstructure/v2 v2.0.2 h1:vGKWl0YJqUNxE8d+h8f6NJLcCJrgbhC4NcD46KavDd4= +github.com/mitchellh/hashstructure/v2 v2.0.2/go.mod h1:MG3aRVU/N29oo/V/IhBX8GR/zz4kQkprJgF2EVszyDE= github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY= github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= @@ -506,10 +547,14 @@ github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9G github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 h1:n6/2gBQ3RWajuToeY6ZtZTIKv2v7ThUy5KKusIT0yc0= github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00/go.mod h1:Pm3mSP3c5uWn86xMLZ5Sa7JB9GsEZySvHYXCTK4E9q4= +github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI= +github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo= +github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= +github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s= github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8= -github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo= -github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8= +github.com/muesli/termenv v0.15.3-0.20240618155329-98d742f6907a h1:2MaM6YC3mGu54x+RKAA6JiFFHlHDY1UbkxqppT7wYOg= +github.com/muesli/termenv v0.15.3-0.20240618155329-98d742f6907a/go.mod h1:hxSnBBYLK21Vtq/PHd0S2FYCxBXzBua8ov5s1RobyRQ= github.com/munnerz/goautoneg v0.0.0-20120707110453-a547fc61f48d/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= @@ -679,6 +724,11 @@ github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +github.com/yuin/goldmark v1.7.1/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E= +github.com/yuin/goldmark v1.7.4 h1:BDXOHExt+A7gwPCJgPIIq7ENvceR7we7rOS9TNoLZeg= +github.com/yuin/goldmark v1.7.4/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E= +github.com/yuin/goldmark-emoji v1.0.3 h1:aLRkLHOuBR2czCY4R8olwMjID+tENfhyFDMCRhbIQY4= +github.com/yuin/goldmark-emoji v1.0.3/go.mod h1:tTkZEbwu5wkPmgTcitqddVxY9osFZiavD+r4AzQrh1U= go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= go.etcd.io/bbolt v1.3.5/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ= @@ -741,8 +791,8 @@ golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0/go.mod h1:LzIPMQfyMNhhGPh golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= -golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA= -golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= +golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30= +golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -837,8 +887,8 @@ golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns= -golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs= -golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= +golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys= +golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -867,6 +917,8 @@ golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/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/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= +golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -936,6 +988,7 @@ golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210817190340-bfb29a6856f2/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210831042530-f4d43177bf5e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -946,8 +999,8 @@ golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= -golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34= +golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= 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= @@ -955,8 +1008,8 @@ golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuX golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY= -golang.org/x/term v0.18.0 h1:FcHjZXDMxI8mM3nwhX9HlKop4C0YQvCVCdwYl2wOtE8= -golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58= +golang.org/x/term v0.22.0 h1:BbsgPEJULsl2fV/AT3v15Mjva5yXKQDyKf+TbDz7QJk= +golang.org/x/term v0.22.0/go.mod h1:F3qCibpT5AMpCRfhfT53vVJwhLtIVHhB9XDjfFvnMI4= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -969,8 +1022,8 @@ 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.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= -golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= -golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224= +golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= diff --git a/pkg/client/client.go b/pkg/client/client.go index 14e96e88..74605407 100644 --- a/pkg/client/client.go +++ b/pkg/client/client.go @@ -8,6 +8,7 @@ import ( "reflect" "time" + "github.com/charmbracelet/log" toolchainv1alpha1 "github.com/codeready-toolchain/api/api/v1alpha1" commonclient "github.com/codeready-toolchain/toolchain-common/pkg/client" "github.com/kubesaw/ksctl/pkg/configuration" @@ -15,7 +16,6 @@ import ( "github.com/kubesaw/ksctl/pkg/ioutils" clientcmdapi "k8s.io/client-go/tools/clientcmd/api" - "github.com/ghodss/yaml" configv1 "github.com/openshift/api/config/v1" projectv1 "github.com/openshift/api/project/v1" routev1 "github.com/openshift/api/route/v1" @@ -119,7 +119,7 @@ func newTlsVerifySkippingTransport() http.RoundTripper { } func PatchUserSignup(ctx *clicontext.CommandContext, name string, changeUserSignup func(*toolchainv1alpha1.UserSignup) (bool, error), afterMessage string) error { - cfg, err := configuration.LoadClusterConfig(ctx, configuration.HostName) + cfg, err := configuration.LoadClusterConfig(ctx.Logger, configuration.HostName) if err != nil { return err } @@ -139,7 +139,7 @@ func PatchUserSignup(ctx *clicontext.CommandContext, name string, changeUserSign return err } - ctx.Printlnf(afterMessage) + ctx.Info(afterMessage) return nil } @@ -156,7 +156,7 @@ func GetUserSignup(cl runtimeclient.Client, namespace, name string) (*toolchainv } func PatchMasterUserRecord(ctx *clicontext.CommandContext, name string, changeMasterUserRecord func(*toolchainv1alpha1.MasterUserRecord) (bool, error), afterMessage string) error { - cfg, err := configuration.LoadClusterConfig(ctx, configuration.HostName) + cfg, err := configuration.LoadClusterConfig(ctx.Logger, configuration.HostName) if err != nil { return err } @@ -176,7 +176,7 @@ func PatchMasterUserRecord(ctx *clicontext.CommandContext, name string, changeMa return err } - ctx.Printlnf(afterMessage) + ctx.Info(afterMessage) return nil } @@ -193,7 +193,7 @@ func GetMasterUserRecord(cl runtimeclient.Client, namespace, name string) (*tool } func PatchSpace(ctx *clicontext.CommandContext, name string, changeSpace func(*toolchainv1alpha1.Space) (bool, error), afterMessage string) error { - cfg, err := configuration.LoadClusterConfig(ctx, configuration.HostName) + cfg, err := configuration.LoadClusterConfig(ctx.Logger, configuration.HostName) if err != nil { return err } @@ -213,7 +213,7 @@ func PatchSpace(ctx *clicontext.CommandContext, name string, changeSpace func(*t return err } - ctx.Printlnf(afterMessage) + ctx.Info(afterMessage) return nil } @@ -290,25 +290,23 @@ func Ensure(term ioutils.Terminal, cl runtimeclient.Client, obj runtimeclient.Ob } func ensure(term ioutils.Terminal, cl runtimeclient.Client, namespacedName types.NamespacedName, obj runtimeclient.Object) (bool, error) { - content, err := yaml.Marshal(obj) - if err != nil { - return false, err - } resourceKind := obj.GetObjectKind().GroupVersionKind().Kind // if GVK is not defined in the given object, then let's use its type if resourceKind == "" { resourceKind = reflect.TypeOf(obj).Elem().Name() } - term.PrintContextSeparatorWithBodyf(string(content), "Using %s resource:", resourceKind) + if err := term.PrintObject(fmt.Sprintf("Using %s resource:", resourceKind), obj); err != nil { + return false, err + } existing := obj.DeepCopyObject().(runtimeclient.Object) if err := cl.Get(context.TODO(), namespacedName, existing); err != nil && !apierrors.IsNotFound(err) { return false, err } else if err == nil { - term.Printlnf("There is an already existing %s with the same name: %s", resourceKind, namespacedName) - if !term.AskForConfirmation(ioutils.WithDangerZoneMessagef( - "update of the already created "+resourceKind, "update the %s with the hard-coded version?", resourceKind)) { - return false, nil + term.Warnf("!!! DANGER ZONE !!!") + term.Warnf("There is already a %s named '%s'", resourceKind, namespacedName) + if confirm, err := term.Confirm("update the existing %s with the hard-coded version?", resourceKind); err != nil || !confirm { + return confirm, err } metaNew, err := meta.Accessor(obj) if err != nil { @@ -328,45 +326,45 @@ func ensure(term ioutils.Terminal, cl runtimeclient.Client, namespacedName types if err := cl.Update(context.TODO(), obj); err != nil { return false, err } - term.Printlnf("\nThe '%s' %s has been updated", namespacedName.Name, resourceKind) + term.Infof("The '%s' %s has been updated", namespacedName.Name, resourceKind) return true, nil } - if !term.AskForConfirmation(ioutils.WithMessagef("create the %s resource with the name %s ?", resourceKind, namespacedName)) { - return false, nil + if confirm, err := term.Confirm("Create the %s resource with the name %s ?", resourceKind, namespacedName); err != nil || !confirm { + return confirm, err } if err := cl.Create(context.TODO(), obj); err != nil { return false, err } - term.Printlnf("\nThe '%s' %s has been created", namespacedName, resourceKind) + term.Infof("The '%s' %s has been created", namespacedName, resourceKind) return true, nil } // Create creates the resource only if it does not exist yet (ie, if a resource of the same kind in the same namespace/name doesn't exist yet) -func Create(term ioutils.Terminal, cl runtimeclient.Client, obj runtimeclient.Object) error { +func Create(ctx context.Context, logger *log.Logger, cl runtimeclient.Client, obj runtimeclient.Object) error { namespacedName := types.NamespacedName{ Namespace: obj.GetNamespace(), Name: obj.GetName(), } objCopy := obj.DeepCopyObject().(runtimeclient.Object) - if err := cl.Get(context.TODO(), namespacedName, objCopy); err != nil && !apierrors.IsNotFound(err) { + if err := cl.Get(ctx, namespacedName, objCopy); err != nil && !apierrors.IsNotFound(err) { return err } else if apierrors.IsNotFound(err) { - if err := cl.Create(context.TODO(), obj); err != nil { + if err := cl.Create(ctx, obj); err != nil { return err } - term.Printlnf("\nThe '%s' %s has been created", namespacedName, reflect.TypeOf(obj).Elem().Name()) + logger.Infof("The '%s' %s has been created", namespacedName, reflect.TypeOf(obj).Elem().Name()) return nil } - term.Printlnf("\nThe '%s' %s already exists", namespacedName, reflect.TypeOf(obj).Elem().Name()) + logger.Warnf("The '%s' %s already exists", namespacedName, reflect.TypeOf(obj).Elem().Name()) return nil } // GetRouteURL return the scheme+host of the route with the given namespaced name. // Since routes may take a bit of time to be available, this func uses a wait loop // to make sure that the route was created, or fails after a timeout. -func GetRouteURL(term ioutils.Terminal, cl runtimeclient.Client, namespacedName types.NamespacedName) (string, error) { - term.Printlnf("Waiting for '%s' route to be available...", namespacedName.Name) +func GetRouteURL(logger *log.Logger, cl runtimeclient.Client, namespacedName types.NamespacedName) (string, error) { + logger.Infof("Waiting for '%s' route to be available...", namespacedName.Name) route := routev1.Route{} if err := wait.Poll(retryInterval, timeout, func() (done bool, err error) { if err := cl.Get(context.TODO(), namespacedName, &route); err != nil && !apierrors.IsNotFound(err) { diff --git a/pkg/client/client_test.go b/pkg/client/client_test.go index 7c2a2ea0..0cccb385 100644 --- a/pkg/client/client_test.go +++ b/pkg/client/client_test.go @@ -1,16 +1,20 @@ package client_test import ( + "bytes" "context" "fmt" + "os" "testing" + "github.com/charmbracelet/log" toolchainv1alpha1 "github.com/codeready-toolchain/api/api/v1alpha1" "github.com/codeready-toolchain/toolchain-common/pkg/states" commontest "github.com/codeready-toolchain/toolchain-common/pkg/test" "github.com/h2non/gock" "github.com/kubesaw/ksctl/pkg/client" clicontext "github.com/kubesaw/ksctl/pkg/context" + "github.com/kubesaw/ksctl/pkg/ioutils" . "github.com/kubesaw/ksctl/pkg/test" routev1 "github.com/openshift/api/route/v1" olmv1 "github.com/operator-framework/api/pkg/operators/v1" @@ -56,7 +60,8 @@ func TestPatchUserSignup(t *testing.T) { //given userSignup := NewUserSignup() newClient, fakeClient := NewFakeClients(t, userSignup) - term := NewFakeTerminal() + buffy := bytes.NewBuffer(nil) + term := ioutils.NewTerminal(buffy, buffy) ctx := clicontext.NewCommandContext(term, newClient) // when @@ -75,7 +80,8 @@ func TestPatchUserSignup(t *testing.T) { //given userSignup := NewUserSignup() newClient, fakeClient := NewFakeClients(t, userSignup) - term := NewFakeTerminal() + buffy := bytes.NewBuffer(nil) + term := ioutils.NewTerminal(buffy, buffy) ctx := clicontext.NewCommandContext(term, newClient) // when @@ -93,7 +99,8 @@ func TestPatchUserSignup(t *testing.T) { //given userSignup := NewUserSignup() newClient, fakeClient := NewFakeClients(t, userSignup) - term := NewFakeTerminal() + buffy := bytes.NewBuffer(nil) + term := ioutils.NewTerminal(buffy, buffy) ctx := clicontext.NewCommandContext(term, newClient) // when @@ -114,7 +121,8 @@ func TestPatchUserSignup(t *testing.T) { fakeClient.MockGet = func(ctx context.Context, key runtimeclient.ObjectKey, obj runtimeclient.Object, opts ...runtimeclient.GetOption) error { return fmt.Errorf("some error") } - term := NewFakeTerminal() + buffy := bytes.NewBuffer(nil) + term := ioutils.NewTerminal(buffy, buffy) ctx := clicontext.NewCommandContext(term, newClient) // when @@ -134,7 +142,8 @@ func TestPatchUserSignup(t *testing.T) { fakeClient.MockPatch = func(ctx context.Context, obj runtimeclient.Object, patch runtimeclient.Patch, opts ...runtimeclient.PatchOption) error { return fmt.Errorf("some error") } - term := NewFakeTerminal() + buffy := bytes.NewBuffer(nil) + term := ioutils.NewTerminal(buffy, buffy) ctx := clicontext.NewCommandContext(term, newClient) // when @@ -152,7 +161,8 @@ func TestPatchUserSignup(t *testing.T) { //given userSignup := NewUserSignup() fakeClient := commontest.NewFakeClient(t, userSignup) - term := NewFakeTerminal() + buffy := bytes.NewBuffer(nil) + term := ioutils.NewTerminal(buffy, buffy) newClient := func(_, _ string) (runtimeclient.Client, error) { return nil, fmt.Errorf("some error") } @@ -176,7 +186,8 @@ func TestUpdateUserSignupLacksPermissions(t *testing.T) { userSignup := NewUserSignup() newClient, fakeClient := NewFakeClients(t, userSignup) - term := NewFakeTerminal() + buffy := bytes.NewBuffer(nil) + term := ioutils.NewTerminal(buffy, buffy) ctx := clicontext.NewCommandContext(term, newClient) // when @@ -200,7 +211,8 @@ func TestEnsure(t *testing.T) { t.Run("when creating", func(t *testing.T) { // given fakeClient := commontest.NewFakeClient(t) - term := NewFakeTerminalWithResponse("Y") + buffy := bytes.NewBuffer(nil) + term := ioutils.NewTerminal(buffy, buffy, ioutils.WithDefaultConfirm(true)) actual := subs.DeepCopy() // when @@ -211,17 +223,16 @@ func TestEnsure(t *testing.T) { assert.True(t, applied) namespacedName := commontest.NamespacedName(subs.Namespace, subs.Name) AssertSubscriptionHasSpec(t, fakeClient, namespacedName, subs.Spec) - output := term.Output() - assert.NotContains(t, output, "!!! DANGER ZONE !!!") - assert.Contains(t, output, "Are you sure that you want to create the Subscription resource with the name toolchain-host-operator/cool-subs ?") - assert.Contains(t, output, "The 'toolchain-host-operator/cool-subs' Subscription has been created") - assert.NotContains(t, output, "cool-token") + assert.NotContains(t, buffy.String(), "!!! DANGER ZONE !!!") + assert.Contains(t, buffy.String(), "The 'toolchain-host-operator/cool-subs' Subscription has been created") + assert.NotContains(t, buffy.String(), "cool-token") }) t.Run("when updating", func(t *testing.T) { // given fakeClient := commontest.NewFakeClient(t, newSubscription("other-operator", "prod")) - term := NewFakeTerminalWithResponse("Y") + buffy := bytes.NewBuffer(nil) + term := ioutils.NewTerminal(buffy, buffy, ioutils.WithDefaultConfirm(true)) // when actual := subs.DeepCopy() @@ -232,18 +243,18 @@ func TestEnsure(t *testing.T) { assert.True(t, applied) namespacedName := commontest.NamespacedName(subs.Namespace, subs.Name) AssertSubscriptionHasSpec(t, fakeClient, namespacedName, subs.Spec) - output := term.Output() - assert.Contains(t, output, "!!! DANGER ZONE !!!") - assert.Contains(t, output, "Are you sure that you want to update the Subscription with the hard-coded version?") - assert.Contains(t, output, "The 'cool-subs' Subscription has been updated") - assert.NotContains(t, output, "cool-token") + assert.Contains(t, buffy.String(), "!!! DANGER ZONE !!!") + assert.Contains(t, buffy.String(), "There is already a Subscription named 'toolchain-host-operator/cool-subs'") + assert.Contains(t, buffy.String(), "The 'cool-subs' Subscription has been updated") + assert.NotContains(t, buffy.String(), "cool-token") }) t.Run("when N is answered", func(t *testing.T) { // given existing := newSubscription("other-operator", "prod") fakeClient := commontest.NewFakeClient(t, existing) - term := NewFakeTerminalWithResponse("N") + buffy := bytes.NewBuffer(nil) + term := ioutils.NewTerminal(buffy, buffy, ioutils.WithDefaultConfirm(false), ioutils.WithTee(os.Stdout)) // when actual := subs.DeepCopy() @@ -254,11 +265,10 @@ func TestEnsure(t *testing.T) { assert.False(t, applied) namespacedName := commontest.NamespacedName(subs.Namespace, subs.Name) AssertSubscriptionHasSpec(t, fakeClient, namespacedName, existing.Spec) - output := term.Output() - assert.Contains(t, output, "!!! DANGER ZONE !!!") - assert.Contains(t, output, "Are you sure that you want to update the Subscription with the hard-coded version?") - assert.NotContains(t, output, "The 'cool-subs' Subscription has been updated") - assert.NotContains(t, output, "cool-token") + assert.Contains(t, buffy.String(), "!!! DANGER ZONE !!!") + assert.Contains(t, buffy.String(), "There is already a Subscription named 'toolchain-host-operator/cool-subs'") + assert.NotContains(t, buffy.String(), "The 'cool-subs' Subscription has been updated") + assert.NotContains(t, buffy.String(), "cool-token") }) }) @@ -271,7 +281,8 @@ func TestEnsure(t *testing.T) { fakeClient.MockGet = func(ctx context.Context, key runtimeclient.ObjectKey, obj runtimeclient.Object, opts ...runtimeclient.GetOption) error { return fmt.Errorf("some error") } - term := NewFakeTerminalWithResponse("Y") + buffy := bytes.NewBuffer(nil) + term := ioutils.NewTerminal(buffy, buffy) // when actual := subs.DeepCopy() @@ -283,11 +294,10 @@ func TestEnsure(t *testing.T) { fakeClient.MockGet = nil namespacedName := commontest.NamespacedName(subs.Namespace, subs.Name) AssertSubscriptionHasSpec(t, fakeClient, namespacedName, existing.Spec) - output := term.Output() - assert.NotContains(t, output, "!!! DANGER ZONE !!!") - assert.NotContains(t, output, "Are you sure that you want to update the Subscription with the hard-coded version?") - assert.NotContains(t, output, "The 'cool-subs' Subscription has been updated") - assert.NotContains(t, output, "cool-token") + assert.NotContains(t, buffy.String(), "!!! DANGER ZONE !!!") + assert.NotContains(t, buffy.String(), "Are you sure that you want to update the Subscription with the hard-coded version?") + assert.NotContains(t, buffy.String(), "The 'cool-subs' Subscription has been updated") + assert.NotContains(t, buffy.String(), "cool-token") }) t.Run("when create fails", func(t *testing.T) { @@ -296,7 +306,8 @@ func TestEnsure(t *testing.T) { fakeClient.MockCreate = func(ctx context.Context, obj runtimeclient.Object, opts ...runtimeclient.CreateOption) error { return fmt.Errorf("some error") } - term := NewFakeTerminalWithResponse("Y") + buffy := bytes.NewBuffer(nil) + term := ioutils.NewTerminal(buffy, buffy, ioutils.WithDefaultConfirm(true), ioutils.WithTee(os.Stdout)) // when actual := subs.DeepCopy() @@ -307,11 +318,9 @@ func TestEnsure(t *testing.T) { assert.False(t, applied) namespacedName := commontest.NamespacedName(subs.Namespace, subs.Name) AssertSubscriptionDoesNotExist(t, fakeClient, namespacedName) - output := term.Output() - assert.NotContains(t, output, "!!! DANGER ZONE !!!") - assert.Contains(t, output, "Are you sure that you want to create the Subscription resource with the name toolchain-host-operator/cool-subs ?") - assert.NotContains(t, output, "The 'toolchain-host-operator/cool-subs' Subscription has been created") - assert.NotContains(t, output, "cool-token") + assert.NotContains(t, buffy.String(), "!!! DANGER ZONE !!!") + assert.NotContains(t, buffy.String(), "The 'toolchain-host-operator/cool-subs' Subscription has been created") + assert.NotContains(t, buffy.String(), "cool-token") }) t.Run("when update fails", func(t *testing.T) { @@ -321,7 +330,8 @@ func TestEnsure(t *testing.T) { fakeClient.MockUpdate = func(ctx context.Context, obj runtimeclient.Object, opts ...runtimeclient.UpdateOption) error { return fmt.Errorf("some error") } - term := NewFakeTerminalWithResponse("Y") + buffy := bytes.NewBuffer(nil) + term := ioutils.NewTerminal(buffy, buffy, ioutils.WithDefaultConfirm(true), ioutils.WithTee(os.Stdout)) // when actual := subs.DeepCopy() @@ -332,11 +342,9 @@ func TestEnsure(t *testing.T) { assert.False(t, applied) namespacedName := commontest.NamespacedName(subs.Namespace, subs.Name) AssertSubscriptionHasSpec(t, fakeClient, namespacedName, existing.Spec) - output := term.Output() - assert.Contains(t, output, "!!! DANGER ZONE !!!") - assert.Contains(t, output, "Are you sure that you want to update the Subscription with the hard-coded version?") - assert.NotContains(t, output, "The 'cool-subs' Subscription has been updated") - assert.NotContains(t, output, "cool-token") + assert.Contains(t, buffy.String(), "!!! DANGER ZONE !!!") + assert.NotContains(t, buffy.String(), "The 'cool-subs' Subscription has been updated") + assert.NotContains(t, buffy.String(), "cool-token") }) }) } @@ -349,17 +357,17 @@ func TestCreate(t *testing.T) { // given namespacedName := commontest.NamespacedName("openshift-customer-monitoring", "openshift-customer-monitoring") fakeClient := commontest.NewFakeClient(t) - term := NewFakeTerminalWithResponse("Y") operatorGroup := newOperatorGroup(namespacedName, map[string]string{"provider": "ksctl"}) + buffy := bytes.NewBuffer(nil) + logger := log.New(buffy) // when - err := client.Create(term, fakeClient, operatorGroup) + err := client.Create(context.TODO(), logger, fakeClient, operatorGroup) // then require.NoError(t, err) AssertOperatorGroupHasLabels(t, fakeClient, namespacedName, map[string]string{"provider": "ksctl"}) - output := term.Output() - assert.Contains(t, output, "The 'openshift-customer-monitoring/openshift-customer-monitoring' OperatorGroup has been created") + assert.Contains(t, buffy.String(), "The 'openshift-customer-monitoring/openshift-customer-monitoring' OperatorGroup has been created") }) }) @@ -369,17 +377,17 @@ func TestCreate(t *testing.T) { // given namespacedName := commontest.NamespacedName("openshift-customer-monitoring", "openshift-customer-monitoring") fakeClient := commontest.NewFakeClient(t, newOperatorGroup(namespacedName, map[string]string{"provider": "osd"})) - term := NewFakeTerminalWithResponse("Y") operatorGroup := newOperatorGroup(namespacedName, map[string]string{"provider": "ksctl"}) + buffy := bytes.NewBuffer(nil) + logger := log.New(buffy) // when - err := client.Create(term, fakeClient, operatorGroup) + err := client.Create(context.TODO(), logger, fakeClient, operatorGroup) // then require.NoError(t, err) AssertOperatorGroupHasLabels(t, fakeClient, namespacedName, map[string]string{"provider": "osd"}) - output := term.Output() - assert.Contains(t, output, "The 'openshift-customer-monitoring/openshift-customer-monitoring' OperatorGroup already exists") + assert.Contains(t, buffy.String(), "The 'openshift-customer-monitoring/openshift-customer-monitoring' OperatorGroup already exists") }) t.Run("when error occurs on client.Get", func(t *testing.T) { @@ -388,12 +396,13 @@ func TestCreate(t *testing.T) { fakeClient.MockGet = func(ctx context.Context, key runtimeclient.ObjectKey, obj runtimeclient.Object, opts ...runtimeclient.GetOption) error { return fmt.Errorf("get failed") } - term := NewFakeTerminalWithResponse("Y") + buffy := bytes.NewBuffer(nil) + logger := log.New(buffy) namespacedName := commontest.NamespacedName("openshift-customer-monitoring", "openshift-customer-monitoring") operatorGroup := newOperatorGroup(namespacedName, map[string]string{"provider": "ksctl"}) // when - err := client.Create(term, fakeClient, operatorGroup) + err := client.Create(context.TODO(), logger, fakeClient, operatorGroup) // then require.Error(t, err) @@ -406,12 +415,13 @@ func TestCreate(t *testing.T) { fakeClient.MockCreate = func(ctx context.Context, obj runtimeclient.Object, opts ...runtimeclient.CreateOption) error { return fmt.Errorf("create failed") } - term := NewFakeTerminalWithResponse("Y") + buffy := bytes.NewBuffer(nil) + logger := log.New(buffy) namespacedName := commontest.NamespacedName("openshift-customer-monitoring", "openshift-customer-monitoring") operatorGroup := newOperatorGroup(namespacedName, map[string]string{"provider": "ksctl"}) // when - err := client.Create(term, fakeClient, operatorGroup) + err := client.Create(context.TODO(), logger, fakeClient, operatorGroup) // then require.Error(t, err) @@ -424,7 +434,6 @@ func TestGetRoute(t *testing.T) { // given require.NoError(t, client.AddToScheme()) - term := NewFakeTerminalWithResponse("Y") t.Run("success", func(t *testing.T) { @@ -451,9 +460,11 @@ func TestGetRoute(t *testing.T) { }, } fakeClient := commontest.NewFakeClient(t, route) + buffy := bytes.NewBuffer(nil) + logger := log.New(buffy) // when - r, err := client.GetRouteURL(term, fakeClient, types.NamespacedName{ + r, err := client.GetRouteURL(logger, fakeClient, types.NamespacedName{ Namespace: "openshift-monitoring", Name: "thanos-querier", }) @@ -482,9 +493,11 @@ func TestGetRoute(t *testing.T) { }, } fakeClient := commontest.NewFakeClient(t, route) + buffy := bytes.NewBuffer(nil) + logger := log.New(buffy) // when - r, err := client.GetRouteURL(term, fakeClient, types.NamespacedName{ + r, err := client.GetRouteURL(logger, fakeClient, types.NamespacedName{ Namespace: "openshift-monitoring", Name: "thanos-querier", }) @@ -510,8 +523,11 @@ func TestGetRoute(t *testing.T) { fakeClient.MockGet = func(ctx context.Context, key runtimeclient.ObjectKey, obj runtimeclient.Object, opts ...runtimeclient.GetOption) error { return fmt.Errorf("mock error") } + buffy := bytes.NewBuffer(nil) + logger := log.New(buffy) + // when - _, err := client.GetRouteURL(term, fakeClient, types.NamespacedName{ + _, err := client.GetRouteURL(logger, fakeClient, types.NamespacedName{ Namespace: "openshift-monitoring", Name: "thanos-querier", }) @@ -532,9 +548,11 @@ func TestGetRoute(t *testing.T) { // no status will cause a timeout } fakeClient := commontest.NewFakeClient(t, route) + buffy := bytes.NewBuffer(nil) + logger := log.New(buffy) // when - _, err := client.GetRouteURL(term, fakeClient, types.NamespacedName{ + _, err := client.GetRouteURL(logger, fakeClient, types.NamespacedName{ Namespace: "openshift-monitoring", Name: "thanos-querier", }) diff --git a/pkg/cmd/add_space_users.go b/pkg/cmd/add_space_users.go index a3e5801f..be168d20 100644 --- a/pkg/cmd/add_space_users.go +++ b/pkg/cmd/add_space_users.go @@ -27,7 +27,7 @@ func NewAddSpaceUsersCmd() *cobra.Command { one or more users specified by their MasterUserRecord name. One SpaceBinding will be created for each user.`, Args: cobra.ExactArgs(0), RunE: func(cmd *cobra.Command, args []string) error { - term := ioutils.NewTerminal(cmd.InOrStdin, cmd.OutOrStdout) + term := ioutils.NewTerminal(cmd.InOrStdin(), cmd.OutOrStdout(), ioutils.WithVerbose(configuration.Verbose)) ctx := clicontext.NewCommandContext(term, client.DefaultNewClient) return AddSpaceUsers(ctx, spaceName, role, users) @@ -44,7 +44,7 @@ one or more users specified by their MasterUserRecord name. One SpaceBinding wil } func AddSpaceUsers(ctx *clicontext.CommandContext, spaceName, role string, usersToAdd []string) error { - cfg, err := configuration.LoadClusterConfig(ctx, configuration.HostName) + cfg, err := configuration.LoadClusterConfig(ctx.Logger, configuration.HostName) if err != nil { return err } @@ -54,7 +54,7 @@ func AddSpaceUsers(ctx *clicontext.CommandContext, spaceName, role string, users } // get Space - ctx.Println("Checking space...") + ctx.Info("Checking space...") space, err := client.GetSpace(cl, cfg.OperatorNamespace, spaceName) if err != nil { return err @@ -82,7 +82,7 @@ func AddSpaceUsers(ctx *clicontext.CommandContext, spaceName, role string, users } // get MasterUserRecords - ctx.Println("Checking users...") + ctx.Info("Checking users...") spaceBindingsToCreate := []*toolchainv1alpha1.SpaceBinding{} for _, murName := range usersToAdd { mur, err := client.GetMasterUserRecord(cl, cfg.OperatorNamespace, murName) @@ -93,16 +93,14 @@ func AddSpaceUsers(ctx *clicontext.CommandContext, spaceName, role string, users } // confirmation before SpaceBinding creation - if err := ctx.PrintObject(space, "Targeted Space"); err != nil { + if err := ctx.PrintObject("Targeted Space:", space); err != nil { return err } - confirmation := ctx.AskForConfirmation(ioutils.WithMessagef( - "add users to the above Space?")) - if !confirmation { - return nil + if confirm, err := ctx.Confirm("Add users to the above Space?"); err != nil || !confirm { + return err } - ctx.Println("Creating SpaceBinding(s)...") + ctx.Info("Creating SpaceBinding(s)...") // create SpaceBindings for _, sb := range spaceBindingsToCreate { if err := cl.Create(context.TODO(), sb); err != nil { @@ -110,6 +108,6 @@ func AddSpaceUsers(ctx *clicontext.CommandContext, spaceName, role string, users } } - ctx.Printlnf("\nSpaceBinding(s) successfully created") + ctx.Infof("SpaceBinding(s) successfully created") return nil } diff --git a/pkg/cmd/add_space_users_test.go b/pkg/cmd/add_space_users_test.go index b1fb0219..dbec7aab 100644 --- a/pkg/cmd/add_space_users_test.go +++ b/pkg/cmd/add_space_users_test.go @@ -1,8 +1,10 @@ package cmd_test import ( + "bytes" "context" "fmt" + "os" "testing" toolchainv1alpha1 "github.com/codeready-toolchain/api/api/v1alpha1" @@ -10,6 +12,7 @@ import ( "github.com/codeready-toolchain/toolchain-common/pkg/test/masteruserrecord" "github.com/kubesaw/ksctl/pkg/cmd" clicontext "github.com/kubesaw/ksctl/pkg/context" + "github.com/kubesaw/ksctl/pkg/ioutils" . "github.com/kubesaw/ksctl/pkg/test" uuid "github.com/google/uuid" @@ -28,7 +31,8 @@ func TestAddSpaceUsers(t *testing.T) { newClient, fakeClient := initAddSpaceUsersTest(t, mur1, mur2) SetFileConfig(t, Host()) - term := NewFakeTerminalWithResponse("Y") + buffy := bytes.NewBuffer(nil) + term := ioutils.NewTerminal(buffy, buffy, ioutils.WithDefaultConfirm(true)) ctx := clicontext.NewCommandContext(term, newClient) // when @@ -36,11 +40,9 @@ func TestAddSpaceUsers(t *testing.T) { // then require.NoError(t, err) - output := term.Output() assertSpaceBindings(t, fakeClient, []string{"alice", "bob"}, "admin") - assert.Contains(t, output, "Are you sure that you want to add users to the above Space?") - assert.Contains(t, output, "SpaceBinding(s) successfully created") - assert.NotContains(t, output, "cool-token") + assert.Contains(t, buffy.String(), "SpaceBinding(s) successfully created") + assert.NotContains(t, buffy.String(), "cool-token") }) t.Run("when a non-default role is specified", func(t *testing.T) { @@ -48,9 +50,9 @@ func TestAddSpaceUsers(t *testing.T) { mur1 := masteruserrecord.NewMasterUserRecord(t, "alice", masteruserrecord.TierName("deactivate30")) mur2 := masteruserrecord.NewMasterUserRecord(t, "bob", masteruserrecord.TierName("deactivate30")) newClient, fakeClient := initAddSpaceUsersTest(t, mur1, mur2) - + buffy := bytes.NewBuffer(nil) + term := ioutils.NewTerminal(buffy, buffy, ioutils.WithDefaultConfirm(true)) SetFileConfig(t, Host()) - term := NewFakeTerminalWithResponse("Y") ctx := clicontext.NewCommandContext(term, newClient) // when @@ -58,11 +60,9 @@ func TestAddSpaceUsers(t *testing.T) { // then require.NoError(t, err) - output := term.Output() assertSpaceBindings(t, fakeClient, []string{"alice", "bob"}, "viewer") - assert.Contains(t, output, "Are you sure that you want to add users to the above Space?") - assert.Contains(t, output, "SpaceBinding(s) successfully created") - assert.NotContains(t, output, "cool-token") + assert.Contains(t, buffy.String(), "SpaceBinding(s) successfully created") + assert.NotContains(t, buffy.String(), "cool-token") }) t.Run("when answer is N", func(t *testing.T) { @@ -72,7 +72,8 @@ func TestAddSpaceUsers(t *testing.T) { newClient, fakeClient := initAddSpaceUsersTest(t, mur1, mur2) SetFileConfig(t, Host()) - term := NewFakeTerminalWithResponse("N") + buffy := bytes.NewBuffer(nil) + term := ioutils.NewTerminal(buffy, buffy, ioutils.WithDefaultConfirm(false), ioutils.WithTee(os.Stdout)) ctx := clicontext.NewCommandContext(term, newClient) // when @@ -80,11 +81,9 @@ func TestAddSpaceUsers(t *testing.T) { // then require.NoError(t, err) - output := term.Output() assertSpaceBindings(t, fakeClient, []string{}, "") // no spacebindings expected - assert.Contains(t, output, "Are you sure that you want to add users to the above Space?") - assert.NotContains(t, output, "SpaceBinding(s) successfully created") - assert.NotContains(t, output, "cool-token") + assert.NotContains(t, buffy.String(), "SpaceBinding(s) successfully created") + assert.NotContains(t, buffy.String(), "cool-token") }) t.Run("when space not found", func(t *testing.T) { @@ -94,7 +93,8 @@ func TestAddSpaceUsers(t *testing.T) { newClient, fakeClient := NewFakeClients(t, mur1, mur2) // no space SetFileConfig(t, Host()) - term := NewFakeTerminalWithResponse("N") + buffy := bytes.NewBuffer(nil) + term := ioutils.NewTerminal(buffy, buffy) ctx := clicontext.NewCommandContext(term, newClient) // when @@ -102,11 +102,10 @@ func TestAddSpaceUsers(t *testing.T) { // then require.EqualError(t, err, `spaces.toolchain.dev.openshift.com "testspace" not found`) - output := term.Output() assertSpaceBindings(t, fakeClient, []string{}, "") // no spacebindings expected - assert.NotContains(t, output, "Are you sure that you want to add users to the above Space?") - assert.NotContains(t, output, "SpaceBinding(s) successfully created") - assert.NotContains(t, output, "cool-token") + assert.NotContains(t, buffy.String(), "Are you sure that you want to add users to the above Space?") + assert.NotContains(t, buffy.String(), "SpaceBinding(s) successfully created") + assert.NotContains(t, buffy.String(), "cool-token") }) t.Run("when first mur not found", func(t *testing.T) { @@ -114,7 +113,8 @@ func TestAddSpaceUsers(t *testing.T) { newClient, fakeClient := initAddSpaceUsersTest(t) // no murs SetFileConfig(t, Host()) - term := NewFakeTerminalWithResponse("N") + buffy := bytes.NewBuffer(nil) + term := ioutils.NewTerminal(buffy, buffy) ctx := clicontext.NewCommandContext(term, newClient) // when @@ -122,11 +122,10 @@ func TestAddSpaceUsers(t *testing.T) { // then require.EqualError(t, err, `masteruserrecords.toolchain.dev.openshift.com "alice" not found`) - output := term.Output() assertSpaceBindings(t, fakeClient, []string{}, "") // no spacebindings expected - assert.NotContains(t, output, "Are you sure that you want to add users to the above Space?") - assert.NotContains(t, output, "SpaceBinding(s) successfully created") - assert.NotContains(t, output, "cool-token") + assert.NotContains(t, buffy.String(), "Are you sure that you want to add users to the above Space?") + assert.NotContains(t, buffy.String(), "SpaceBinding(s) successfully created") + assert.NotContains(t, buffy.String(), "cool-token") }) t.Run("when second mur not found", func(t *testing.T) { @@ -135,7 +134,8 @@ func TestAddSpaceUsers(t *testing.T) { newClient, fakeClient := initAddSpaceUsersTest(t, mur1) // mur2 missing SetFileConfig(t, Host()) - term := NewFakeTerminalWithResponse("N") + buffy := bytes.NewBuffer(nil) + term := ioutils.NewTerminal(buffy, buffy) ctx := clicontext.NewCommandContext(term, newClient) // when @@ -143,11 +143,10 @@ func TestAddSpaceUsers(t *testing.T) { // then require.EqualError(t, err, `masteruserrecords.toolchain.dev.openshift.com "bob" not found`) - output := term.Output() assertSpaceBindings(t, fakeClient, []string{}, "") // no spacebindings expected - assert.NotContains(t, output, "Are you sure that you want to add users to the above Space?") - assert.NotContains(t, output, "SpaceBinding(s) successfully created") - assert.NotContains(t, output, "cool-token") + assert.NotContains(t, buffy.String(), "Are you sure that you want to add users to the above Space?") + assert.NotContains(t, buffy.String(), "SpaceBinding(s) successfully created") + assert.NotContains(t, buffy.String(), "cool-token") }) t.Run("when role is invalid", func(t *testing.T) { @@ -156,7 +155,8 @@ func TestAddSpaceUsers(t *testing.T) { newClient, fakeClient := initAddSpaceUsersTest(t, mur1) // mur2 missing SetFileConfig(t, Host()) - term := NewFakeTerminalWithResponse("N") + buffy := bytes.NewBuffer(nil) + term := ioutils.NewTerminal(buffy, buffy) ctx := clicontext.NewCommandContext(term, newClient) // when @@ -166,11 +166,10 @@ func TestAddSpaceUsers(t *testing.T) { require.Contains(t, err.Error(), "invalid role 'badrole' for space 'testspace' - the following are valid roles:") require.Contains(t, err.Error(), "\nadmin\n") require.Contains(t, err.Error(), "\nviewer\n") - output := term.Output() assertSpaceBindings(t, fakeClient, []string{}, "") // no spacebindings expected - assert.NotContains(t, output, "Are you sure that you want to add users to the above Space?") - assert.NotContains(t, output, "SpaceBinding(s) successfully created") - assert.NotContains(t, output, "cool-token") + assert.NotContains(t, buffy.String(), "Are you sure that you want to add users to the above Space?") + assert.NotContains(t, buffy.String(), "SpaceBinding(s) successfully created") + assert.NotContains(t, buffy.String(), "cool-token") }) t.Run("client get error", func(t *testing.T) { @@ -183,7 +182,8 @@ func TestAddSpaceUsers(t *testing.T) { } SetFileConfig(t, Host()) - term := NewFakeTerminalWithResponse("Y") + buffy := bytes.NewBuffer(nil) + term := ioutils.NewTerminal(buffy, buffy) ctx := clicontext.NewCommandContext(term, newClient) // when @@ -191,11 +191,10 @@ func TestAddSpaceUsers(t *testing.T) { // then require.EqualError(t, err, "client error") - output := term.Output() assertSpaceBindings(t, fakeClient, []string{}, "") // no spacebindings expected - assert.NotContains(t, output, "Are you sure that you want to add users to the above Space?") - assert.NotContains(t, output, "SpaceBinding(s) successfully created") - assert.NotContains(t, output, "cool-token") + assert.NotContains(t, buffy.String(), "Are you sure that you want to add users to the above Space?") + assert.NotContains(t, buffy.String(), "SpaceBinding(s) successfully created") + assert.NotContains(t, buffy.String(), "cool-token") }) } diff --git a/pkg/cmd/adm/install_operator.go b/pkg/cmd/adm/install_operator.go index 92cc3761..943a923e 100644 --- a/pkg/cmd/adm/install_operator.go +++ b/pkg/cmd/adm/install_operator.go @@ -1,6 +1,7 @@ package adm import ( + "context" "fmt" "time" @@ -36,14 +37,14 @@ func NewInstallOperatorCmd() *cobra.Command { Short: "install kubesaw operator (host|member)", Long: `This command installs the latest stable versions of the kubesaw operator using OLM`, RunE: func(cmd *cobra.Command, args []string) error { - term := ioutils.NewTerminal(cmd.InOrStdin, cmd.OutOrStdout) + term := ioutils.NewTerminal(cmd.InOrStdin(), cmd.OutOrStdout(), ioutils.WithVerbose(configuration.Verbose)) + ctx := clicontext.NewTerminalContext(term) kubeClient, err := client.NewKubeClientFromKubeConfig(commandArgs.kubeConfig) if err != nil { return err } cl := commonclient.NewApplyClient(kubeClient) - ctx := clicontext.NewTerminalContext(term) return installOperator(ctx, commandArgs, args[0], cl) }, } @@ -55,7 +56,7 @@ func NewInstallOperatorCmd() *cobra.Command { return cmd } -func installOperator(ctx *clicontext.TerminalContext, args installArgs, operator string, applyClient *commonclient.ApplyClient) error { +func installOperator(ctx *clicontext.TerminalContext, args installArgs, operator string, cl *commonclient.ApplyClient) error { // validate cluster type if operator != string(configuration.Host) && operator != string(configuration.Member) { return fmt.Errorf("invalid operator type provided: %s. Valid ones are %s|%s", operator, configuration.Host, configuration.Member) @@ -67,66 +68,64 @@ func installOperator(ctx *clicontext.TerminalContext, args installArgs, operator namespace = fmt.Sprintf("toolchain-%s-operator", operator) } - if !ctx.AskForConfirmation( - ioutils.WithMessagef("install %s in namespace '%s'", operator, namespace)) { - return nil + if confirm, err := ctx.Confirm("Install the '%s' Operator in the '%s' namespace", operator, namespace); err != nil || !confirm { + return err } // check if namespace exists // otherwise create it - if err := createNamespaceIfNotFound(ctx, applyClient, namespace); err != nil { + if err := createNamespaceIfNotFound(ctx, cl, namespace); err != nil { return err } // check that we don't install both host and member in the same namespace - if err := checkOneOperatorPerNamespace(ctx, applyClient, namespace, operator); err != nil { + if err := checkOneOperatorPerNamespace(ctx, cl, namespace, operator); err != nil { return err } // install the catalog source namespacedName := types.NamespacedName{Name: operatorResourceName(operator), Namespace: namespace} catalogSource := newCatalogSource(namespacedName, operator) - ctx.Println(fmt.Sprintf("Creating CatalogSource %s in namespace %s.", catalogSource.Name, catalogSource.Namespace)) - if _, err := applyClient.ApplyObject(ctx.Context, catalogSource, commonclient.SaveConfiguration(false)); err != nil { + ctx.Infof("Creating the '%s' CatalogSource in namespace '%s'", catalogSource.Name, catalogSource.Namespace) + if _, err := cl.ApplyObject(ctx, catalogSource, commonclient.SaveConfiguration(false)); err != nil { return err } - ctx.Println(fmt.Sprintf("CatalogSource %s created.", catalogSource.Name)) - if err := waitUntilCatalogSourceIsReady(ctx, applyClient, namespacedName, args.waitForReadyTimeout); err != nil { + ctx.Infof("CatalogSource '%s' created", catalogSource.Name) + if err := waitUntilCatalogSourceIsReady(ctx, cl, namespacedName, args.waitForReadyTimeout); err != nil { return err } - ctx.Printlnf("CatalogSource %s is ready", namespacedName) + ctx.Infof("CatalogSource '%s' is ready", namespacedName) // check if operator group is already there ogs := olmv1.OperatorGroupList{} - if err := applyClient.List(ctx, &ogs, runtimeclient.InNamespace(namespace)); err != nil { + if err := cl.List(ctx, &ogs, runtimeclient.InNamespace(namespace)); err != nil { return err } if len(ogs.Items) > 0 { - ctx.Println(fmt.Sprintf("OperatorGroup %s already present in namespace %s. Skipping creation of new operator group.", ogs.Items[0].GetName(), namespace)) + ctx.Warnf("OperatorGroup '%s' already exists in namespace '%s', skipping creation of the '%s' OperatorGroup.", ogs.Items[0].GetName(), namespacedName.Namespace, namespacedName.Name) } else { // install operator group operatorGroup := newOperatorGroup(namespacedName) - ctx.Println(fmt.Sprintf("Creating new operator group %s in namespace %s.", operatorGroup.Name, operatorGroup.Namespace)) - if _, err := applyClient.ApplyObject(ctx, operatorGroup, commonclient.SaveConfiguration(false)); err != nil { + ctx.Infof("Creating the '%s' OperatorGroup in namespace '%s'", operatorGroup.Name, operatorGroup.Namespace) + if _, err := cl.ApplyObject(ctx, operatorGroup, commonclient.SaveConfiguration(false)); err != nil { return err } - ctx.Println(fmt.Sprintf("OperatorGroup %s created.", operatorGroup.Name)) + ctx.Infof("OperatorGroup '%s' create.", operatorGroup.Name) } // install subscription operatorName := getOperatorName(operator) subscription := newSubscription(namespacedName, operatorName, namespacedName.Name) - ctx.Println(fmt.Sprintf("Creating Subscription %s in namespace %s.", subscription.Name, subscription.Namespace)) - if _, err := applyClient.ApplyObject(ctx, subscription, commonclient.SaveConfiguration(false)); err != nil { + ctx.Infof("Creating the '%s' Subscription in namespace '%s'", subscription.Name, subscription.Namespace) + if _, err := cl.ApplyObject(ctx, subscription, commonclient.SaveConfiguration(false)); err != nil { return err } - ctx.Println(fmt.Sprintf("Subcription %s created.", subscription.Name)) - if err := waitUntilInstallPlanIsComplete(ctx, applyClient, operatorName, namespace, args.waitForReadyTimeout); err != nil { + ctx.Infof("Subcription '%s' created", subscription.Name) + if err := waitUntilInstallPlanIsComplete(ctx, cl, operatorName, namespace, args.waitForReadyTimeout); err != nil { return err } - ctx.Println(fmt.Sprintf("InstallPlan for the %s operator has been completed", operator)) - ctx.Println("") - ctx.Println(fmt.Sprintf("The %s operator has been successfully installed in the %s namespace", operator, namespace)) + ctx.Infof("InstallPlan for the '%s' operator has been completed", operator) + ctx.Infof("The '%s' operator has been successfully installed in the '%s' namespace", operator, namespace) return nil } @@ -134,20 +133,20 @@ func getOperatorName(operator string) string { return fmt.Sprintf("toolchain-%s-operator", operator) } -func createNamespaceIfNotFound(ctx *clicontext.TerminalContext, applyClient *commonclient.ApplyClient, namespace string) error { +func createNamespaceIfNotFound(ctx *clicontext.TerminalContext, cl *commonclient.ApplyClient, namespace string) error { ns := &v1.Namespace{} - if err := applyClient.Get(ctx.Context, types.NamespacedName{Name: namespace}, ns); err != nil { + if err := cl.Get(ctx, types.NamespacedName{Name: namespace}, ns); err != nil { if errors.IsNotFound(err) { - ctx.Println(fmt.Sprintf("Creating namespace %s.", namespace)) + ctx.Info(fmt.Sprintf("Creating the '%s' namespace", namespace)) ns.Name = namespace - if errNs := applyClient.Create(ctx.Context, ns); errNs != nil { + if errNs := cl.Create(ctx, ns); errNs != nil { return errNs } } else { return err } } - ctx.Println(fmt.Sprintf("Namespace %s created.", namespace)) + ctx.Info(fmt.Sprintf("Namespace '%s' created.", namespace)) return nil } @@ -164,7 +163,7 @@ func newCatalogSource(name types.NamespacedName, operator string) *olmv1alpha1.C Spec: olmv1alpha1.CatalogSourceSpec{ SourceType: olmv1alpha1.SourceTypeGrpc, Image: fmt.Sprintf("quay.io/codeready-toolchain/%s-operator-index:latest", operator), - DisplayName: fmt.Sprintf("KubeSaw %s Operator", operator), + DisplayName: fmt.Sprintf("KubeSaw '%s' Operator", operator), Publisher: "Red Hat", UpdateStrategy: &olmv1alpha1.UpdateStrategy{ RegistryPoll: &olmv1alpha1.RegistryPoll{ @@ -207,12 +206,12 @@ func newSubscription(name types.NamespacedName, operatorName, catalogSourceName } } -func waitUntilCatalogSourceIsReady(ctx *clicontext.TerminalContext, applyClient *commonclient.ApplyClient, catalogSourceKey runtimeclient.ObjectKey, waitForReadyTimeout time.Duration) error { +func waitUntilCatalogSourceIsReady(ctx *clicontext.TerminalContext, cl *commonclient.ApplyClient, catalogSourceKey runtimeclient.ObjectKey, waitForReadyTimeout time.Duration) error { cs := &olmv1alpha1.CatalogSource{} if err := wait.PollImmediate(2*time.Second, waitForReadyTimeout, func() (bool, error) { - ctx.Printlnf("waiting for CatalogSource %s to become ready", catalogSourceKey) + ctx.Infof("Waiting for CatalogSource '%s' to become ready", catalogSourceKey) cs = &olmv1alpha1.CatalogSource{} - if err := applyClient.Get(ctx, catalogSourceKey, cs); err != nil { + if err := cl.Get(ctx, catalogSourceKey, cs); err != nil { return false, err } @@ -227,7 +226,7 @@ func waitUntilCatalogSourceIsReady(ctx *clicontext.TerminalContext, applyClient func waitUntilInstallPlanIsComplete(ctx *clicontext.TerminalContext, cl runtimeclient.Client, operator, namespace string, waitForReadyTimeout time.Duration) error { plans := &olmv1alpha1.InstallPlanList{} if err := wait.PollImmediate(2*time.Second, waitForReadyTimeout, func() (bool, error) { - ctx.Printlnf("waiting for InstallPlans in namespace %s to complete", namespace) + ctx.Infof("Waiting for InstallPlans in namespace '%s' to complete", namespace) plans = &olmv1alpha1.InstallPlanList{} if err := cl.List(ctx, plans, runtimeclient.InNamespace(namespace), runtimeclient.MatchingLabels{fmt.Sprintf("operators.coreos.com/%s.%s", operator, namespace): ""}, @@ -251,18 +250,18 @@ func waitUntilInstallPlanIsComplete(ctx *clicontext.TerminalContext, cl runtimec // checkOneOperatorPerNamespace returns an error in case the namespace contains the other operator installed. // So for host namespace member operator should not be installed in there and vice-versa. -func checkOneOperatorPerNamespace(ctx *clicontext.TerminalContext, applyClient *commonclient.ApplyClient, namespace, operator string) error { +func checkOneOperatorPerNamespace(ctx context.Context, cl *commonclient.ApplyClient, namespace, operator string) error { namespacedName := types.NamespacedName{ Namespace: namespace, Name: configuration.ClusterType(operator).TheOtherType().String(), } subscription := olmv1alpha1.Subscription{} - if err := applyClient.Get(ctx.Context, namespacedName, &subscription); err != nil { + if err := cl.Get(ctx, namespacedName, &subscription); err != nil { if errors.IsNotFound(err) { return nil } return err } - return fmt.Errorf("found already installed subscription %s in namespace %s - it's not allowed to have host and member in the same namespace", subscription.GetName(), subscription.GetNamespace()) + return fmt.Errorf("found existing subscription '%s' in namespace '%s', but host and member operators cannot be installed in the same namespace", subscription.GetName(), subscription.GetNamespace()) } diff --git a/pkg/cmd/adm/install_operator_test.go b/pkg/cmd/adm/install_operator_test.go index 6c9a4fd3..f8c3eac9 100644 --- a/pkg/cmd/adm/install_operator_test.go +++ b/pkg/cmd/adm/install_operator_test.go @@ -1,27 +1,29 @@ package adm import ( + "bytes" "context" "fmt" + "os" "testing" "time" commonclient "github.com/codeready-toolchain/toolchain-common/pkg/client" - corev1 "k8s.io/api/core/v1" - "github.com/codeready-toolchain/toolchain-common/pkg/test" + clicontext "github.com/kubesaw/ksctl/pkg/context" + "github.com/kubesaw/ksctl/pkg/client" "github.com/kubesaw/ksctl/pkg/configuration" - clicontext "github.com/kubesaw/ksctl/pkg/context" + "github.com/kubesaw/ksctl/pkg/ioutils" . "github.com/kubesaw/ksctl/pkg/test" v1 "github.com/operator-framework/api/pkg/operators/v1" olmv1alpha1 "github.com/operator-framework/api/pkg/operators/v1alpha1" "github.com/stretchr/testify/assert" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - runtimeclient "sigs.k8s.io/controller-runtime/pkg/client" - "github.com/stretchr/testify/require" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" + runtimeclient "sigs.k8s.io/controller-runtime/pkg/client" ) func TestInstallOperator(t *testing.T) { @@ -60,7 +62,8 @@ func TestInstallOperator(t *testing.T) { // given fakeClient := test.NewFakeClient(t, &installPlan) fakeClientWithReadyCatalogSource(fakeClient) - term := NewFakeTerminalWithResponse("Y") + buffy := bytes.NewBuffer(nil) + term := ioutils.NewTerminal(buffy, buffy, ioutils.WithDefaultConfirm(true), ioutils.WithTee(os.Stdout)) ctx := clicontext.NewTerminalContext(term) // when @@ -75,7 +78,7 @@ func TestInstallOperator(t *testing.T) { olmv1alpha1.CatalogSourceSpec{ SourceType: olmv1alpha1.SourceTypeGrpc, Image: fmt.Sprintf("quay.io/codeready-toolchain/%s-operator-index:latest", operator), - DisplayName: fmt.Sprintf("KubeSaw %s Operator", operator), + DisplayName: fmt.Sprintf("KubeSaw '%s' Operator", operator), Publisher: "Red Hat", UpdateStrategy: &olmv1alpha1.UpdateStrategy{ RegistryPoll: &olmv1alpha1.RegistryPoll{ @@ -88,13 +91,14 @@ func TestInstallOperator(t *testing.T) { ) AssertOperatorGroupExists(t, fakeClient, types.NamespacedName{Name: fmt.Sprintf("%s-operator", operator), Namespace: namespace}) AssertSubscriptionExists(t, fakeClient, types.NamespacedName{Name: fmt.Sprintf("%s-operator", operator), Namespace: namespace}) - assert.Contains(t, term.Output(), fmt.Sprintf("The %s operator has been successfully installed in the %s namespace", operator, namespace)) + assert.Contains(t, buffy.String(), fmt.Sprintf("The '%s' operator has been successfully installed in the '%s' namespace", operator, namespace)) }) t.Run("install "+operator+" operator fails if CatalogSource is not ready", func(t *testing.T) { // given fakeClient := test.NewFakeClient(t) - term := NewFakeTerminalWithResponse("Y") + buffy := bytes.NewBuffer(nil) + term := ioutils.NewTerminal(buffy, buffy, ioutils.WithDefaultConfirm(true), ioutils.WithTee(os.Stdout)) ctx := clicontext.NewTerminalContext(term) // when @@ -104,7 +108,7 @@ func TestInstallOperator(t *testing.T) { require.ErrorContains(t, err, "failed waiting for catalog source to be ready.") AssertOperatorGroupDoesNotExist(t, fakeClient, types.NamespacedName{Name: fmt.Sprintf("%s-operator", operator), Namespace: namespace}) AssertSubscriptionDoesNotExist(t, fakeClient, types.NamespacedName{Name: fmt.Sprintf("%s-operator", operator), Namespace: namespace}) - assert.NotContains(t, term.Output(), fmt.Sprintf("The %s operator has been successfully installed in the %s namespace", operator, namespace)) + assert.NotContains(t, buffy.String(), "The '%s' operator has been successfully installed in the '%s' namespace", operator, namespace) }) t.Run("install "+operator+" operators fails if InstallPlan is not ready", func(t *testing.T) { @@ -114,7 +118,8 @@ func TestInstallOperator(t *testing.T) { notReadyIP.Status = olmv1alpha1.InstallPlanStatus{Phase: olmv1alpha1.InstallPlanFailed} fakeClient := test.NewFakeClient(t, notReadyIP) fakeClientWithReadyCatalogSource(fakeClient) - term := NewFakeTerminalWithResponse("Y") + buffy := bytes.NewBuffer(nil) + term := ioutils.NewTerminal(buffy, buffy, ioutils.WithDefaultConfirm(true), ioutils.WithTee(os.Stdout)) ctx := clicontext.NewTerminalContext(term) // when @@ -122,7 +127,7 @@ func TestInstallOperator(t *testing.T) { // then require.ErrorContains(t, err, "failed waiting for install plan to be complete.") - assert.NotContains(t, term.Output(), fmt.Sprintf("The %s operator has been successfully installed in the %s namespace", operator, namespace)) + assert.NotContains(t, buffy.String(), "The '%s' operator has been successfully installed in the '%s' namespace", operator, namespace) }) t.Run(operator+" fails to install if the other operator is installed", func(t *testing.T) { @@ -132,7 +137,8 @@ func TestInstallOperator(t *testing.T) { ObjectMeta: metav1.ObjectMeta{Name: operatorAlreadyInstalled, Namespace: namespace}, } fakeClient := test.NewFakeClient(t, &existingSubscription) - term := NewFakeTerminalWithResponse("Y") + buffy := bytes.NewBuffer(nil) + term := ioutils.NewTerminal(buffy, buffy, ioutils.WithDefaultConfirm(true), ioutils.WithTee(os.Stdout)) ctx := clicontext.NewTerminalContext(term) // when @@ -142,7 +148,7 @@ func TestInstallOperator(t *testing.T) { ) // then - require.EqualError(t, err, fmt.Sprintf("found already installed subscription %s in namespace %s - it's not allowed to have host and member in the same namespace", operatorAlreadyInstalled, namespace)) + require.EqualError(t, err, fmt.Sprintf("found existing subscription '%s' in namespace '%s', but host and member operators cannot be installed in the same namespace", operatorAlreadyInstalled, namespace)) }) t.Run("skip creation of operator group if already present", func(t *testing.T) { @@ -152,7 +158,8 @@ func TestInstallOperator(t *testing.T) { } fakeClient := test.NewFakeClient(t, &existingOperatorGroup, &installPlan) fakeClientWithReadyCatalogSource(fakeClient) - term := NewFakeTerminalWithResponse("y") + buffy := bytes.NewBuffer(nil) + term := ioutils.NewTerminal(buffy, buffy, ioutils.WithDefaultConfirm(true), ioutils.WithTee(os.Stdout)) ctx := clicontext.NewTerminalContext(term) // when @@ -160,16 +167,17 @@ func TestInstallOperator(t *testing.T) { // then require.NoError(t, err) - assert.Contains(t, term.Output(), fmt.Sprintf("OperatorGroup %s already present in namespace %s. Skipping creation of new operator group.", operatorResourceName(operator), namespace)) - assert.NotContains(t, term.Output(), fmt.Sprintf("Creating new operator group %s in namespace %s.", operatorResourceName(operator), namespace)) - assert.Contains(t, term.Output(), fmt.Sprintf("The %s operator has been successfully installed in the %s namespace", operator, namespace)) + assert.Contains(t, buffy.String(), fmt.Sprintf("OperatorGroup '%s' already exists in namespace '%s', skipping creation of the '%s' OperatorGroup.", operatorResourceName(operator), namespace, operatorResourceName(operator))) + assert.NotContains(t, buffy.String(), fmt.Sprintf("Creating the OperatorGroup '%s' in namespace '%s'.", operatorResourceName(operator), namespace)) + assert.Contains(t, buffy.String(), fmt.Sprintf("The '%s' operator has been successfully installed in the '%s' namespace", operator, namespace)) }) t.Run("namespace is computed if not provided", func(t *testing.T) { // given fakeClient := test.NewFakeClient(t, &installPlan) fakeClientWithReadyCatalogSource(fakeClient) - term := NewFakeTerminalWithResponse("y") + buffy := bytes.NewBuffer(nil) + term := ioutils.NewTerminal(buffy, buffy, ioutils.WithDefaultConfirm(true), ioutils.WithTee(os.Stdout)) ctx := clicontext.NewTerminalContext(term) // when @@ -179,7 +187,7 @@ func TestInstallOperator(t *testing.T) { ) // then require.NoError(t, err) - assert.Contains(t, term.Output(), fmt.Sprintf("The %s operator has been successfully installed in the %s namespace", operator, namespace)) // and it's installed in the expected namespace + assert.Contains(t, buffy.String(), fmt.Sprintf("The '%s' operator has been successfully installed in the '%s' namespace", operator, namespace)) // and it's installed in the expected namespace }) } @@ -187,7 +195,8 @@ func TestInstallOperator(t *testing.T) { // given fakeClient := test.NewFakeClient(t) fakeClientWithReadyCatalogSource(fakeClient) - term := NewFakeTerminalWithResponse("Y") + buffy := bytes.NewBuffer(nil) + term := ioutils.NewTerminal(buffy, buffy) ctx := clicontext.NewTerminalContext(term) // when @@ -203,7 +212,8 @@ func TestInstallOperator(t *testing.T) { t.Run("doesn't install operator if response is no", func(t *testing.T) { // given fakeClient := test.NewFakeClient(t) - term := NewFakeTerminalWithResponse("n") + buffy := bytes.NewBuffer(nil) + term := ioutils.NewTerminal(buffy, buffy, ioutils.WithDefaultConfirm(false)) ctx := clicontext.NewTerminalContext(term) // when @@ -215,8 +225,8 @@ func TestInstallOperator(t *testing.T) { // then require.NoError(t, err) - assert.Contains(t, term.Output(), fmt.Sprintf("Are you sure that you want to install %s in namespace", operator)) - assert.NotContains(t, term.Output(), fmt.Sprintf("The %s operator has been successfully installed in the toolchain-host-operator namespace", operator)) + // assert.Contains(t, buffy.String(), fmt.Sprintf("Are you sure that you want to install '%s' in namespace", operator)) + assert.NotContains(t, buffy.String(), fmt.Sprintf("The '%s' operator has been successfully installed in the toolchain-host-operator namespace", operator)) }) } diff --git a/pkg/cmd/adm/must_gather_namespace.go b/pkg/cmd/adm/must_gather_namespace.go index 07c4c7fe..54efa7a6 100644 --- a/pkg/cmd/adm/must_gather_namespace.go +++ b/pkg/cmd/adm/must_gather_namespace.go @@ -8,8 +8,8 @@ import ( "strings" "time" + "github.com/charmbracelet/log" "github.com/kubesaw/ksctl/pkg/cmd/flags" - "github.com/kubesaw/ksctl/pkg/ioutils" "gopkg.in/yaml.v3" "github.com/spf13/cobra" @@ -37,7 +37,7 @@ func NewMustGatherNamespaceCmd() *cobra.Command { Args: cobra.ExactArgs(1), SilenceUsage: true, RunE: func(cmd *cobra.Command, args []string) error { - term := ioutils.NewTerminal(cmd.InOrStdin, cmd.OutOrStdout) + logger := log.New(cmd.OutOrStdout()) kubeconfig, err := clientcmd.BuildConfigFromFlags("", kubeconfig) kubeconfig.Timeout = 60 * time.Second // These fields need to be set when using the REST client ¯\_(ツ)_/¯ @@ -48,7 +48,7 @@ func NewMustGatherNamespaceCmd() *cobra.Command { if err != nil { return err } - return MustGatherNamespace(term, kubeconfig, args[0], destDir) + return MustGatherNamespace(logger, kubeconfig, args[0], destDir) }, } cmd.Flags().StringVar(&destDir, "dest-dir", "", "Gather information with a specific local folder to copy to") @@ -58,7 +58,7 @@ func NewMustGatherNamespaceCmd() *cobra.Command { return cmd } -func MustGatherNamespace(term ioutils.Terminal, kubeconfig *restclient.Config, namespace, destDir string) error { +func MustGatherNamespace(logger *log.Logger, kubeconfig *restclient.Config, namespace, destDir string) error { // verify that the destDir exists, otherwise, create it // If path is already a directory, MkdirAll does nothing and returns nil. @@ -70,7 +70,7 @@ func MustGatherNamespace(term ioutils.Terminal, kubeconfig *restclient.Config, n return err } if len(entries) > 0 { - term.Printlnf("The '%s' dest-dir is not empty. Aborting.", destDir) + logger.Errorf("The '%s' dest-dir is not empty. Aborting.", destDir) return nil } @@ -81,13 +81,13 @@ func MustGatherNamespace(term ioutils.Terminal, kubeconfig *restclient.Config, n } dcl := discovery.NewDiscoveryClient(rcl) - term.Println("fetching the list of API resources on the cluster...") + logger.Info("fetching the list of API resources on the cluster...") apiResourceLists, err := dcl.ServerPreferredNamespacedResources() if err != nil { return err } - term.Printlnf("gathering all resources from the '%s' namespace...", namespace) + logger.Infof("gathering all resources from the '%s' namespace...", namespace) cl, err := runtimeclient.New(kubeconfig, runtimeclient.Options{}) if err != nil { return err @@ -122,32 +122,32 @@ func MustGatherNamespace(term ioutils.Terminal, kubeconfig *restclient.Config, n }) if err := cl.List(context.Background(), list, runtimeclient.InNamespace(namespace)); err != nil { // log the error but continue so we can collect the remaining resources - term.Printlnf("failed to list %s/%s: %v", strings.ToLower(gv.String()), strings.ToLower(r.Kind), err) + logger.Errorf("failed to list %s/%s: %v", strings.ToLower(gv.String()), strings.ToLower(r.Kind), err) continue } for _, item := range list.Items { - term.Printlnf("found %s/%s", item.GetKind(), item.GetName()) + logger.Infof("found %s/%s", item.GetKind(), item.GetName()) filename := filepath.Join(destDir, fmt.Sprintf("%s-%s.yaml", strings.ToLower(item.GetKind()), item.GetName())) data, err := yaml.Marshal(item.Object) if err != nil { - term.Printlnf("failed to marshal %s/%s %s: %v", strings.ToLower(gv.String()), strings.ToLower(r.Kind), item.GetName(), err) + logger.Errorf("failed to marshal %s/%s %s: %v", strings.ToLower(gv.String()), strings.ToLower(r.Kind), item.GetName(), err) continue } if err := writeToFile(filename, data); err != nil { - term.Printlnf("failed to save contents of %s/%s %s: %v", strings.ToLower(gv.String()), strings.ToLower(r.Kind), item.GetName(), err) + logger.Errorf("failed to save contents of %s/%s %s: %v", strings.ToLower(gv.String()), strings.ToLower(r.Kind), item.GetName(), err) return err } // also, for pods, gather process names and logs from all containers if item.GetAPIVersion() == "v1" && item.GetKind() == "Pod" { pod := &corev1.Pod{} if _, _, err := decoder.Decode(data, nil, pod); err != nil { - term.Printlnf("failed to decode %s/%s '%s': %v", strings.ToLower(gv.String()), strings.ToLower(r.Kind), item.GetName(), err) + logger.Errorf("failed to decode %s/%s '%s': %v", strings.ToLower(gv.String()), strings.ToLower(r.Kind), item.GetName(), err) continue } for _, cs := range pod.Status.ContainerStatuses { if cs.Started != nil && *cs.Started { - if err := gatherContainerLogs(term, rcl, destDir, namespace, pod.Name, cs.Name); err != nil { - term.Printlnf("failed to collect logs from container '%s' in pod '%s': %v", cs.Name, pod.Name, err) + if err := gatherContainerLogs(logger, rcl, destDir, namespace, pod.Name, cs.Name); err != nil { + logger.Errorf("failed to collect logs from container '%s' in pod '%s': %v", cs.Name, pod.Name, err) // ignore error, continue to next container continue } @@ -160,8 +160,8 @@ func MustGatherNamespace(term ioutils.Terminal, kubeconfig *restclient.Config, n return nil } -func gatherContainerLogs(term ioutils.Terminal, rcl *restclient.RESTClient, destDir, namespace, podName, containerName string) error { - term.Printlnf("collecting logs from %s/%s", podName, containerName) +func gatherContainerLogs(logger *log.Logger, rcl *restclient.RESTClient, destDir, namespace, podName, containerName string) error { + logger.Infof("collecting logs from %s/%s", podName, containerName) p := fmt.Sprintf("/api/v1/namespaces/%s/pods/%s/log", namespace, podName) result := rcl.Get().AbsPath(p).Param("container", containerName).Do(context.Background()) if err := result.Error(); err != nil { diff --git a/pkg/cmd/adm/must_gather_namespace_test.go b/pkg/cmd/adm/must_gather_namespace_test.go index b2ecdc7c..066db2ff 100644 --- a/pkg/cmd/adm/must_gather_namespace_test.go +++ b/pkg/cmd/adm/must_gather_namespace_test.go @@ -1,15 +1,16 @@ package adm_test import ( + "bytes" "fmt" "net/http" "os" "path/filepath" "testing" + "github.com/charmbracelet/log" "github.com/h2non/gock" "github.com/kubesaw/ksctl/pkg/cmd/adm" - . "github.com/kubesaw/ksctl/pkg/test" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" authv1 "k8s.io/api/authentication/v1" @@ -30,8 +31,6 @@ func TestMustGatherNamespaceCmd(t *testing.T) { NegotiatedSerializer: scheme.Codecs, }, } - term := NewFakeTerminalWithResponse("Y") - term.Tee(os.Stdout) t.Run("ok", func(t *testing.T) { t.Run("create the dest-dir on-the-fly", func(t *testing.T) { @@ -39,9 +38,11 @@ func TestMustGatherNamespaceCmd(t *testing.T) { baseDir, err := os.MkdirTemp("", "ksctl-out-") require.NoError(t, err) destDir := filepath.Join(baseDir, "test-dev") + buffy := bytes.NewBuffer(nil) + logger := log.New(buffy) // when - err = adm.MustGatherNamespace(term, kubeconfig, "test-dev", destDir) + err = adm.MustGatherNamespace(logger, kubeconfig, "test-dev", destDir) // then require.NoError(t, err) @@ -56,9 +57,11 @@ func TestMustGatherNamespaceCmd(t *testing.T) { destDir := filepath.Join(baseDir, "test-dev") err = os.Mkdir(destDir, 0755) require.NoError(t, err) + buffy := bytes.NewBuffer(nil) + logger := log.New(buffy) // when - err = adm.MustGatherNamespace(term, kubeconfig, "test-dev", destDir) + err = adm.MustGatherNamespace(logger, kubeconfig, "test-dev", destDir) // then require.NoError(t, err) @@ -79,13 +82,15 @@ func TestMustGatherNamespaceCmd(t *testing.T) { // put some contents err = os.WriteFile(filepath.Join(destDir, "test.yaml"), []byte("apiVersion; v1"), 0600) require.NoError(t, err) + buffy := bytes.NewBuffer(nil) + logger := log.New(buffy) // when - err = adm.MustGatherNamespace(term, kubeconfig, "test-dev", destDir) + err = adm.MustGatherNamespace(logger, kubeconfig, "test-dev", destDir) // then require.NoError(t, err) // no error occurred, but command aborted - assert.Contains(t, term.Output(), fmt.Sprintf("The '%s' dest-dir is not empty. Aborting.", destDir)) + assert.Contains(t, buffy.String(), fmt.Sprintf("The '%s' dest-dir is not empty. Aborting.", destDir)) }) }) diff --git a/pkg/cmd/adm/register_member.go b/pkg/cmd/adm/register_member.go index c72f7f7c..7086da4d 100644 --- a/pkg/cmd/adm/register_member.go +++ b/pkg/cmd/adm/register_member.go @@ -70,7 +70,7 @@ func NewRegisterMemberCmd() *cobra.Command { Short: "Registers a member cluster in the host cluster and vice versa.", Long: `Registers the Host cluster in the Member cluster and then registers the Member cluster in the host cluster by creating toolchaincluster resources in the host and member namespaces.`, RunE: func(cmd *cobra.Command, args []string) error { - term := ioutils.NewTerminal(cmd.InOrStdin, cmd.OutOrStdout) + term := ioutils.NewTerminal(cmd.InOrStdin(), cmd.OutOrStdout(), ioutils.WithVerbose(configuration.Verbose)) ctx := newExtendedCommandContext(term, client.DefaultNewClientFromRestConfig) return registerMemberCluster(ctx, commandArgs) }, @@ -110,8 +110,13 @@ func registerMemberCluster(ctx *extendedCommandContext, args registerMemberArgs) return errors.New(sb.String()) } - if !ctx.AskForConfirmation(validated.confirmationPrompt()) { - return nil + ctx.Warn("The newly registered cluster will not be used for any space provisioning yet.") + // ctx.Warn("This command will output an example SpaceProvisionerConfig that you can modify with the required configuration options and apply to make the cluster available for space placement.") + for _, w := range validated.warnings { + ctx.Warn(w) + } + if confirm, err := ctx.Confirm("Please confirm that the following is ok and you are willing to proceed"); err != nil || !confirm { + return err } return validated.perform(ctx) @@ -129,7 +134,7 @@ func (v *registerMemberValidated) getSourceAndTargetClusters(sourceClusterType c // - `targetCluster` is the cluster where we create the ToolchainCluster resource and the secret // - `sourceCluster` is the cluster referenced in the kubeconfig/ToolchainCluster of the `targetCluster` func (v *registerMemberValidated) addCluster(ctx *extendedCommandContext, sourceClusterType configuration.ClusterType) error { - ctx.PrintContextSeparatorf("Ensuring connection from the %s cluster to the %s via a ToolchainCluster CR, a Secret, and a new ServiceAccount resource", sourceClusterType, sourceClusterType.TheOtherType()) + ctx.Infof("Ensuring connection from the %s cluster to the %s via a ToolchainCluster CR, a Secret, and a new ServiceAccount resource", sourceClusterType, sourceClusterType.TheOtherType()) sourceClusterDetails, targetClusterDetails := v.getSourceAndTargetClusters(sourceClusterType) // wait for the SA to be ready toolchainClusterSAKey := runtimeclient.ObjectKey{ @@ -137,17 +142,17 @@ func (v *registerMemberValidated) addCluster(ctx *extendedCommandContext, source Namespace: sourceClusterDetails.namespace, } if err := waitForToolchainClusterSA(ctx.CommandContext, sourceClusterDetails.client, toolchainClusterSAKey, v.args.waitForReadyTimeout); err != nil { - ctx.Printlnf("The %s ServiceAccount is not present in the %s cluster.", toolchainClusterSAKey, sourceClusterType) - ctx.Printlnf("Please check the %[1]s ToolchainCluster ServiceAccount in the %[2]s %[3]s cluster or the deployment of the %[3]s operator.", toolchainClusterSAKey, sourceClusterDetails.apiEndpoint, sourceClusterType) + ctx.Infof("The %s ServiceAccount is not present in the %s cluster.", toolchainClusterSAKey, sourceClusterType) + ctx.Infof("Please check the %[1]s ToolchainCluster ServiceAccount in the %[2]s %[3]s cluster or the deployment of the %[3]s operator.", toolchainClusterSAKey, sourceClusterDetails.apiEndpoint, sourceClusterType) return err } // source cluster details - ctx.Printlnf("The source cluster name: %s", sourceClusterDetails.toolchainClusterName) - ctx.Printlnf("The API endpoint of the source cluster: %s", sourceClusterDetails.apiEndpoint) + ctx.Infof("The source cluster name: %s", sourceClusterDetails.toolchainClusterName) + ctx.Infof("The API endpoint of the source cluster: %s", sourceClusterDetails.apiEndpoint) // target to details - ctx.Printlnf("The name of the target cluster: %s", targetClusterDetails.toolchainClusterName) - ctx.Printlnf("The API endpoint of the target cluster: %s", targetClusterDetails.apiEndpoint) + ctx.Infof("The name of the target cluster: %s", targetClusterDetails.toolchainClusterName) + ctx.Infof("The API endpoint of the target cluster: %s", targetClusterDetails.apiEndpoint) // generate a token that will be used for the kubeconfig sourceTargetRestClient, err := newRestClient(sourceClusterDetails.kubeConfig) @@ -161,10 +166,10 @@ func (v *registerMemberValidated) addCluster(ctx *extendedCommandContext, source // TODO drop this part together with the --lets-encrypt flag and start loading certificate from the kubeconfig as soon as ToolchainCluster controller supports loading certificates from kubeconfig var insecureSkipTLSVerify bool if v.args.useLetsEncrypt { - ctx.Printlnf("using let's encrypt certificate") + ctx.Info("using let's encrypt certificate") insecureSkipTLSVerify = false } else { - ctx.Printlnf("setting insecure skip tls verification flags") + ctx.Info("setting insecure skip tls verification flags") insecureSkipTLSVerify = true } // generate the kubeconfig that can be used by target cluster to interact with the source cluster @@ -176,7 +181,7 @@ func (v *registerMemberValidated) addCluster(ctx *extendedCommandContext, source // Create or Update the secret on the targetCluster secretName := toolchainClusterSAKey.Name + "-" + sourceClusterDetails.toolchainClusterName - ctx.Printlnf("creating secret %s/%s in the %s cluster", targetClusterDetails.namespace, secretName, sourceClusterType.TheOtherType()) + ctx.Infof("creating secret %s/%s in the %s cluster", targetClusterDetails.namespace, secretName, sourceClusterType.TheOtherType()) kubeConfigSecret := &v1.Secret{ObjectMeta: metav1.ObjectMeta{Name: secretName, Namespace: targetClusterDetails.namespace}} _, err = controllerutil.CreateOrUpdate(context.TODO(), targetClusterDetails.client, kubeConfigSecret, func() error { // update the secret label @@ -198,14 +203,14 @@ func (v *registerMemberValidated) addCluster(ctx *extendedCommandContext, source if err != nil { return err } - ctx.Println("Secret successfully reconciled") + ctx.Info("Secret successfully reconciled") // TODO -- temporary logic // The creation of the toolchaincluster is just temporary until we implement https://issues.redhat.com/browse/KUBESAW-44, // the creation logic will be moved to the toolchaincluster_resource controller in toolchain-common and will be based on the secret created above. // // create/update toolchaincluster on the targetCluster - ctx.Printlnf("creating ToolchainCluster representation of %s in %s:", sourceClusterType, targetClusterDetails.toolchainClusterName) + ctx.Infof("Creating the ToolchainCluster resource for '%s' in '%s'", sourceClusterType, targetClusterDetails.toolchainClusterName) toolchainClusterCR := &toolchainv1alpha1.ToolchainCluster{ObjectMeta: metav1.ObjectMeta{Name: sourceClusterDetails.toolchainClusterName, Namespace: targetClusterDetails.namespace}} _, err = controllerutil.CreateOrUpdate(context.TODO(), targetClusterDetails.client, toolchainClusterCR, func() error { toolchainClusterCR.Spec.SecretRef.Name = secretName @@ -214,14 +219,14 @@ func (v *registerMemberValidated) addCluster(ctx *extendedCommandContext, source if err != nil { return err } - ctx.Println("Toolchaincluster successfully reconciled") + ctx.Info("Toolchaincluster successfully reconciled") toolchainClusterKey := runtimeclient.ObjectKey{ Name: sourceClusterDetails.toolchainClusterName, Namespace: targetClusterDetails.namespace, } if err := waitUntilToolchainClusterReady(ctx.CommandContext, targetClusterDetails.client, toolchainClusterKey, v.args.waitForReadyTimeout); err != nil { - ctx.Printlnf("The ToolchainCluster resource representing the %s in the %s cluster has not become ready.", sourceClusterType, sourceClusterType.TheOtherType()) - ctx.Printlnf("Please check the %s ToolchainCluster resource in the %s %s cluster.", toolchainClusterKey, targetClusterDetails.apiEndpoint, sourceClusterType.TheOtherType()) + ctx.Warnf("The ToolchainCluster resource representing the %s in the %s cluster has not become ready.", sourceClusterType, sourceClusterType.TheOtherType()) + ctx.Warnf("Please check the %s ToolchainCluster resource in the %s %s cluster.", toolchainClusterKey, targetClusterDetails.apiEndpoint, sourceClusterType.TheOtherType()) return err } // -- end temporary logic @@ -275,7 +280,7 @@ func generateKubeConfig(token, apiEndpoint, namespace string, insecureSkipTLSVer // waitForToolchainClusterSA waits for the toolchaincluster service account to be present func waitForToolchainClusterSA(ctx *clicontext.CommandContext, cl runtimeclient.Client, toolchainClusterKey runtimeclient.ObjectKey, waitForReadyTimeout time.Duration) error { return wait.PollImmediate(2*time.Second, waitForReadyTimeout, func() (bool, error) { - ctx.Printlnf("waiting for ToolchainCluster SA %s to become ready", toolchainClusterKey) + ctx.Infof("Waiting for ToolchainCluster SA '%s' to become ready", toolchainClusterKey) tc := &v1.ServiceAccount{} if err := cl.Get(ctx, toolchainClusterKey, tc); err != nil { if apierrors.IsNotFound(err) { @@ -292,7 +297,7 @@ func waitForToolchainClusterSA(ctx *clicontext.CommandContext, cl runtimeclient. func waitUntilToolchainClusterReady(ctx *clicontext.CommandContext, cl runtimeclient.Client, toolchainClusterKey runtimeclient.ObjectKey, waitForReadyTimeout time.Duration) error { return wait.PollImmediate(2*time.Second, waitForReadyTimeout, func() (bool, error) { - ctx.Printlnf("waiting for ToolchainCluster %s to become ready", toolchainClusterKey) + ctx.Infof("Waiting for ToolchainCluster '%s' to become ready", toolchainClusterKey) tc := &toolchainv1alpha1.ToolchainCluster{} if err := cl.Get(ctx, toolchainClusterKey, tc); err != nil { if apierrors.IsNotFound(err) { @@ -420,7 +425,8 @@ func validateArgs(ctx *extendedCommandContext, args registerMemberArgs) (*regist } existingMemberToolchainCluster := findToolchainClusterForMember(membersInHost, memberApiEndpoint, args.memberNamespace) if existingMemberToolchainCluster != nil { - warnings = append(warnings, fmt.Sprintf("there already is a registered member for the same member API endpoint and operator namespace (%s), proceeding will overwrite the objects representing it in the host and member clusters", runtimeclient.ObjectKeyFromObject(existingMemberToolchainCluster))) + warnings = append(warnings, fmt.Sprintf("There is already a registered member for the same member API endpoint and operator namespace (%s)", runtimeclient.ObjectKeyFromObject(existingMemberToolchainCluster))) + warnings = append(warnings, "Proceeding will overwrite the objects representing it in the host and member clusters") if existingMemberToolchainCluster.Name != memberToolchainClusterName { errors = append(errors, fmt.Sprintf("the newly registered member cluster would have a different name (%s) than the already existing one (%s) which would lead to invalid configuration. Consider using the --name-suffix parameter to match the existing member registration if you intend to just update it instead of creating a new registration", memberToolchainClusterName, existingMemberToolchainCluster.Name)) } @@ -447,29 +453,6 @@ func validateArgs(ctx *extendedCommandContext, args registerMemberArgs) (*regist }, nil } -func (v *registerMemberValidated) confirmationPrompt() ioutils.ConfirmationMessage { - // we have a single replacement argument at the moment so maybe this is a bit of - // an overkill but, let's be explicit about using a format string and its arguments - // so that mistakes are not made in the future when we update this stuff. - sb := strings.Builder{} - args := []any{} - sb.WriteString("register the member cluster from kubeconfig %s?") - args = append(args, v.args.memberKubeConfig) - - sb.WriteString("\nNote that the newly registered cluster will not be used for any space placement yet. This command will output an example SpaceProvisionerConfig that you can modify with the required configuration options and apply to make the cluster available for space placement.") - - if len(v.warnings) > 0 { - sb.WriteString("\nPlease confirm that the following is ok and you are willing to proceed:") - for _, f := range v.warnings { - sb.WriteString("\n- ") - sb.WriteString(f) - } - sb.WriteString("\n") - } - - return ioutils.WithMessagef(sb.String(), args...) -} - func (v *registerMemberValidated) perform(ctx *extendedCommandContext) error { // add the host entry to the member cluster first. We assume that there is just 1 toolchain cluster entry in the member // cluster (i.e. it just points back to the host), so there's no need to determine the number of entries with the same @@ -500,13 +483,9 @@ func (v *registerMemberValidated) perform(ctx *extendedCommandContext) error { }, }, } - - return ctx.PrintObject(exampleSPC, fmt.Sprintf(` -Modify and apply the following SpaceProvisionerConfig to the host cluster (%s) to configure the provisioning -of the spaces to the newly registered member cluster. Nothing will be deployed to the cluster -until the SpaceProvisionerConfig.spec.enabled is set to true. - -`, v.hostClusterData.apiEndpoint)) + ctx.Warnf("Modify and apply the following SpaceProvisionerConfig to the '%s' host cluster to configure the provisioning of the spaces", v.hostClusterData.apiEndpoint) + ctx.Warn("Spaces will not be provisioned into this new cluster until 'SpaceProvisionerConfig.spec.enabled' is set to true.") + return ctx.PrintObject("Example SpaceProvisionerConfig:", exampleSPC) } func findToolchainClusterForMember(allToolchainClusters []toolchainv1alpha1.ToolchainCluster, memberAPIEndpoint, memberOperatorNamespace string) *toolchainv1alpha1.ToolchainCluster { diff --git a/pkg/cmd/adm/register_member_test.go b/pkg/cmd/adm/register_member_test.go index 453e3dc8..0d16b3a0 100644 --- a/pkg/cmd/adm/register_member_test.go +++ b/pkg/cmd/adm/register_member_test.go @@ -1,18 +1,20 @@ package adm import ( + "bytes" "context" "fmt" + "os" "path/filepath" "strings" "testing" "time" toolchainv1alpha1 "github.com/codeready-toolchain/api/api/v1alpha1" - "github.com/codeready-toolchain/toolchain-common/pkg/cluster" "github.com/codeready-toolchain/toolchain-common/pkg/test" "github.com/ghodss/yaml" "github.com/kubesaw/ksctl/pkg/configuration" + "github.com/kubesaw/ksctl/pkg/ioutils" . "github.com/kubesaw/ksctl/pkg/test" "github.com/kubesaw/ksctl/pkg/utils" "github.com/stretchr/testify/assert" @@ -59,29 +61,30 @@ func TestRegisterMember(t *testing.T) { t.Run("produces valid example SPC", func(t *testing.T) { // given - term := NewFakeTerminalWithResponse("Y") newClient, fakeClient := newFakeClientsFromRestConfig(t, &toolchainClusterMemberSa, &toolchainClusterHostSa) // force the ready condition on the toolchaincluster created ( this is done by the tc controller in prod env ) mockCreateToolchainClusterWithReadyCondition(t, fakeClient) + buffy := bytes.NewBuffer(nil) + term := ioutils.NewTerminal(buffy, buffy, ioutils.WithDefaultConfirm(true)) ctx := newExtendedCommandContext(term, newClient) - expectedExampleSPC := &toolchainv1alpha1.SpaceProvisionerConfig{ - TypeMeta: metav1.TypeMeta{ - Kind: "SpaceProvisionerConfig", - APIVersion: toolchainv1alpha1.GroupVersion.Identifier(), - }, - ObjectMeta: metav1.ObjectMeta{ - Name: "member-cool-server.com", - Namespace: test.HostOperatorNs, - }, - Spec: toolchainv1alpha1.SpaceProvisionerConfigSpec{ - ToolchainCluster: "member-cool-server.com", - Enabled: false, - PlacementRoles: []string{ - cluster.RoleLabel(cluster.Tenant), - }, - }, - } + // expectedExampleSPC := &toolchainv1alpha1.SpaceProvisionerConfig{ + // TypeMeta: metav1.TypeMeta{ + // Kind: "SpaceProvisionerConfig", + // APIVersion: toolchainv1alpha1.GroupVersion.Identifier(), + // }, + // ObjectMeta: metav1.ObjectMeta{ + // Name: "member-cool-server.com", + // Namespace: test.HostOperatorNs, + // }, + // Spec: toolchainv1alpha1.SpaceProvisionerConfigSpec{ + // ToolchainCluster: "member-cool-server.com", + // Enabled: false, + // PlacementRoles: []string{ + // cluster.RoleLabel(cluster.Tenant), + // }, + // }, + // } // when err := registerMemberCluster(ctx, newRegisterMemberArgsWith(hostKubeconfig, memberKubeconfig, false)) @@ -104,16 +107,17 @@ func TestRegisterMember(t *testing.T) { assert.Equal(t, hostToolchainClusterName, tcs.Items[0].Name) // secret ref in tc matches assert.Equal(t, toolchainClusterHostSa.Name+"-"+hostToolchainClusterName, tcs.Items[0].Spec.SecretRef.Name) - assert.Contains(t, term.Output(), "Modify and apply the following SpaceProvisionerConfig to the host cluster") - actualExampleSPC := extractExampleSPCFromOutput(t, term.Output()) - assert.Equal(t, *expectedExampleSPC, actualExampleSPC) + assert.Contains(t, buffy.String(), "Modify and apply the following SpaceProvisionerConfig to the 'https://cool-server.com' host cluster to configure the provisioning of the spaces") + // actualExampleSPC := extractExampleSPCFromOutput(t, buffy.String()) + // assert.Equal(t, *expectedExampleSPC, actualExampleSPC) }) t.Run("reports error when member ToolchainCluster is not ready in host", func(t *testing.T) { // given - term := NewFakeTerminalWithResponse("Y") newClient, fakeClient := newFakeClientsFromRestConfig(t, &toolchainClusterMemberSa, &toolchainClusterHostSa) mockCreateToolchainClusterInNamespaceWithReadyCondition(t, fakeClient, test.MemberOperatorNs) // we set to ready only the host toolchaincluster in member operator namespace + buffy := bytes.NewBuffer(nil) + term := ioutils.NewTerminal(buffy, buffy, ioutils.WithDefaultConfirm(true)) ctx := newExtendedCommandContext(term, newClient) // when @@ -126,14 +130,15 @@ func TestRegisterMember(t *testing.T) { assert.Len(t, tcs.Items, 1) require.NoError(t, fakeClient.List(context.TODO(), tcs, runtimeclient.InNamespace(test.MemberOperatorNs))) assert.Len(t, tcs.Items, 1) - assert.Contains(t, term.Output(), "The ToolchainCluster resource representing the member in the host cluster has not become ready.") + assert.Contains(t, buffy.String(), "The ToolchainCluster resource representing the member in the host cluster has not become ready.") }) t.Run("reports error when host ToolchainCluster is not ready in member", func(t *testing.T) { // given - term := NewFakeTerminalWithResponse("Y") newClient, fakeClient := newFakeClientsFromRestConfig(t, &toolchainClusterMemberSa, &toolchainClusterHostSa) mockCreateToolchainClusterInNamespaceWithReadyCondition(t, fakeClient, test.HostOperatorNs) // set to ready only the member toolchaincluster in host operator namespace + buffy := bytes.NewBuffer(nil) + term := ioutils.NewTerminal(buffy, buffy, ioutils.WithDefaultConfirm(true)) ctx := newExtendedCommandContext(term, newClient) // when @@ -144,16 +149,17 @@ func TestRegisterMember(t *testing.T) { tcs := &toolchainv1alpha1.ToolchainClusterList{} require.NoError(t, fakeClient.List(context.TODO(), tcs, runtimeclient.InNamespace(test.HostOperatorNs))) assert.Empty(t, tcs.Items) - assert.Contains(t, term.Output(), "The ToolchainCluster resource representing the host in the member cluster has not become ready.") + assert.Contains(t, buffy.String(), "The ToolchainCluster resource representing the host in the member cluster has not become ready.") require.NoError(t, fakeClient.List(context.TODO(), tcs, runtimeclient.InNamespace(test.MemberOperatorNs))) assert.Len(t, tcs.Items, 1) }) t.Run("single toolchain in cluster", func(t *testing.T) { // given - term := NewFakeTerminalWithResponse("Y") newClient, fakeClient := newFakeClientsFromRestConfig(t, &toolchainClusterMemberSa, &toolchainClusterHostSa) mockCreateToolchainClusterWithReadyCondition(t, fakeClient) + buffy := bytes.NewBuffer(nil) + term := ioutils.NewTerminal(buffy, buffy, ioutils.WithDefaultConfirm(true)) ctx := newExtendedCommandContext(term, newClient) // when @@ -161,15 +167,16 @@ func TestRegisterMember(t *testing.T) { // then require.NoError(t, err) - assert.Contains(t, term.Output(), "Modify and apply the following SpaceProvisionerConfig to the host cluster") - assert.Contains(t, term.Output(), "kind: SpaceProvisionerConfig") + assert.Contains(t, buffy.String(), "Modify and apply the following SpaceProvisionerConfig to the 'https://cool-server.com' host cluster to configure the provisioning of the spaces") + assert.Contains(t, buffy.String(), "kind: SpaceProvisionerConfig") }) t.Run("single toolchain in cluster with --lets-encrypt", func(t *testing.T) { // given - term := NewFakeTerminalWithResponse("Y") newClient, fakeClient := newFakeClientsFromRestConfig(t, &toolchainClusterMemberSa, &toolchainClusterHostSa) mockCreateToolchainClusterWithReadyCondition(t, fakeClient) + buffy := bytes.NewBuffer(nil) + term := ioutils.NewTerminal(buffy, buffy, ioutils.WithDefaultConfirm(true)) ctx := newExtendedCommandContext(term, newClient) // when @@ -177,14 +184,15 @@ func TestRegisterMember(t *testing.T) { // then require.NoError(t, err) - assert.Contains(t, term.Output(), "Modify and apply the following SpaceProvisionerConfig to the host cluster") - assert.Contains(t, term.Output(), "kind: SpaceProvisionerConfig") + assert.Contains(t, buffy.String(), "Modify and apply the following SpaceProvisionerConfig to the 'https://cool-server.com' host cluster to configure the provisioning of the spaces") + assert.Contains(t, buffy.String(), "kind: SpaceProvisionerConfig") }) t.Run("multiple toolchains in cluster", func(t *testing.T) { // given - term := NewFakeTerminalWithResponse("Y") newClient, fakeClient := newFakeClientsFromRestConfig(t, &toolchainClusterMemberSa, &toolchainClusterHostSa) + buffy := bytes.NewBuffer(nil) + term := ioutils.NewTerminal(buffy, buffy, ioutils.WithDefaultConfirm(true)) ctx := newExtendedCommandContext(term, newClient) preexistingToolchainCluster := &toolchainv1alpha1.ToolchainCluster{ ObjectMeta: metav1.ObjectMeta{ @@ -212,20 +220,22 @@ func TestRegisterMember(t *testing.T) { // then require.NoError(t, err) - assert.Contains(t, term.Output(), "source cluster name: member-cool-server.com2") - assert.Contains(t, term.Output(), "The name of the target cluster: member-cool-server.com") - assert.Contains(t, term.Output(), "Modify and apply the following SpaceProvisionerConfig to the host cluster") - assert.Contains(t, term.Output(), "kind: SpaceProvisionerConfig") - assert.Contains(t, term.Output(), "toolchainCluster: member-cool-server.com2") + assert.Contains(t, buffy.String(), "source cluster name: member-cool-server.com2") + assert.Contains(t, buffy.String(), "The name of the target cluster: member-cool-server.com") + assert.Contains(t, buffy.String(), "Modify and apply the following SpaceProvisionerConfig to the 'https://cool-server.com' host cluster to configure the provisioning of the spaces") + assert.Contains(t, buffy.String(), "kind: SpaceProvisionerConfig") + assert.Contains(t, buffy.String(), "toolchainCluster: member-cool-server.com2") }) t.Run("cannot register the same member twice with different names", func(t *testing.T) { // given - term1 := NewFakeTerminalWithResponse("Y") - term2 := NewFakeTerminalWithResponse("Y") newClient, fakeClient := newFakeClientsFromRestConfig(t, &toolchainClusterMemberSa, &toolchainClusterHostSa) mockCreateToolchainClusterWithReadyCondition(t, fakeClient) + buffy1 := bytes.NewBuffer(nil) + term1 := ioutils.NewTerminal(buffy1, buffy1, ioutils.WithDefaultConfirm(true)) ctx1 := newExtendedCommandContext(term1, newClient) + buffy2 := bytes.NewBuffer(nil) + term2 := ioutils.NewTerminal(buffy2, buffy2, ioutils.WithDefaultConfirm(true)) ctx2 := newExtendedCommandContext(term2, newClient) // when @@ -234,8 +244,8 @@ func TestRegisterMember(t *testing.T) { // then require.NoError(t, err1) - assert.Contains(t, term1.Output(), "Modify and apply the following SpaceProvisionerConfig to the host cluster") - assert.Contains(t, term1.Output(), "kind: SpaceProvisionerConfig") + assert.Contains(t, buffy1.String(), "Modify and apply the following SpaceProvisionerConfig to the 'https://cool-server.com' host cluster to configure the provisioning of the spaces") + assert.Contains(t, buffy1.String(), "kind: SpaceProvisionerConfig") require.Error(t, err2) assert.Equal(t, `Cannot proceed because of the following problems: @@ -244,11 +254,14 @@ func TestRegisterMember(t *testing.T) { t.Run("warns when updating existing registration", func(t *testing.T) { // given - term1 := NewFakeTerminalWithResponse("Y") - term2 := NewFakeTerminalWithResponse("Y") + newClient, fakeClient := newFakeClientsFromRestConfig(t, &toolchainClusterMemberSa, &toolchainClusterHostSa) mockCreateToolchainClusterWithReadyCondition(t, fakeClient) + buffy1 := bytes.NewBuffer(nil) + term1 := ioutils.NewTerminal(buffy1, buffy1, ioutils.WithDefaultConfirm(true), ioutils.WithTee(os.Stdout)) ctx1 := newExtendedCommandContext(term1, newClient) + buffy2 := bytes.NewBuffer(nil) + term2 := ioutils.NewTerminal(buffy2, buffy2, ioutils.WithDefaultConfirm(true), ioutils.WithTee(os.Stdout)) ctx2 := newExtendedCommandContext(term2, newClient) // when @@ -257,21 +270,22 @@ func TestRegisterMember(t *testing.T) { // then require.NoError(t, err1) - assert.Contains(t, term1.Output(), "Modify and apply the following SpaceProvisionerConfig to the host cluster") - assert.Contains(t, term1.Output(), "kind: SpaceProvisionerConfig") + assert.Contains(t, buffy1.String(), "Modify and apply the following SpaceProvisionerConfig to the 'https://cool-server.com' host cluster to configure the provisioning of the spaces") + assert.Contains(t, buffy1.String(), "kind: SpaceProvisionerConfig") require.NoError(t, err2) - assert.Contains(t, term2.Output(), "Modify and apply the following SpaceProvisionerConfig to the host cluster") - assert.Contains(t, term2.Output(), "kind: SpaceProvisionerConfig") - assert.Contains(t, term2.Output(), "Please confirm that the following is ok and you are willing to proceed:") - assert.Contains(t, term2.Output(), "- there already is a registered member for the same member API endpoint and operator namespace") + assert.Contains(t, buffy2.String(), "Modify and apply the following SpaceProvisionerConfig to the 'https://cool-server.com' host cluster to configure the provisioning of the spaces") + assert.Contains(t, buffy2.String(), "kind: SpaceProvisionerConfig") + // assert.Contains(t, output2.String(), "Please confirm that the following is ok and you are willing to proceed:") + assert.Contains(t, buffy2.String(), "There is already a registered member for the same member API endpoint and operator namespace") }) t.Run("Errors when member already registered with multiple hosts", func(t *testing.T) { // given - term := NewFakeTerminalWithResponse("Y") newClient, fakeClient := newFakeClientsFromRestConfig(t, &toolchainClusterMemberSa, &toolchainClusterHostSa) mockCreateToolchainClusterWithReadyCondition(t, fakeClient) + buffy := bytes.NewBuffer(nil) + term := ioutils.NewTerminal(buffy, buffy, ioutils.WithDefaultConfirm(true)) ctx := newExtendedCommandContext(term, newClient) preexistingToolchainCluster1 := &toolchainv1alpha1.ToolchainCluster{ ObjectMeta: metav1.ObjectMeta{ @@ -313,13 +327,14 @@ func TestRegisterMember(t *testing.T) { require.Error(t, err) assert.Contains(t, err.Error(), `Cannot proceed because of the following problems: - member misconfigured: the member cluster (https://cool-server.com) is already registered with more than 1 host in namespace toolchain-member-operator`) - assert.NotContains(t, term.Output(), "kind: SpaceProvisionerConfig") + assert.NotContains(t, buffy.String(), "kind: SpaceProvisionerConfig") }) t.Run("Errors when registering into another host", func(t *testing.T) { // given - term := NewFakeTerminalWithResponse("Y") newClient, fakeClient := newFakeClientsFromRestConfig(t) + buffy := bytes.NewBuffer(nil) + term := ioutils.NewTerminal(buffy, buffy, ioutils.WithDefaultConfirm(true)) ctx := newExtendedCommandContext(term, newClient) preexistingToolchainCluster := &toolchainv1alpha1.ToolchainCluster{ ObjectMeta: metav1.ObjectMeta{ @@ -345,13 +360,14 @@ func TestRegisterMember(t *testing.T) { require.Error(t, err) assert.Contains(t, err.Error(), `Cannot proceed because of the following problems: - the member is already registered with another host (https://not-so-cool-server.com) so registering it with the new one (https://cool-server.com) would result in an invalid configuration`) - assert.NotContains(t, term.Output(), "kind: SpaceProvisionerConfig") + assert.NotContains(t, buffy.String(), "kind: SpaceProvisionerConfig") }) t.Run("Errors when host with different name already exists", func(t *testing.T) { // given - term := NewFakeTerminalWithResponse("Y") newClient, fakeClient := newFakeClientsFromRestConfig(t) + buffy := bytes.NewBuffer(nil) + term := ioutils.NewTerminal(buffy, buffy, ioutils.WithDefaultConfirm(true)) ctx := newExtendedCommandContext(term, newClient) preexistingToolchainCluster := &toolchainv1alpha1.ToolchainCluster{ ObjectMeta: metav1.ObjectMeta{ @@ -377,13 +393,14 @@ func TestRegisterMember(t *testing.T) { require.Error(t, err) assert.Contains(t, err.Error(), `Cannot proceed because of the following problems: - the host is already in the member namespace using a ToolchainCluster object with the name 'host-with-weird-name' but the new registration would use a ToolchainCluster with the name 'host-cool-server.com' which would lead to an invalid configuration`) - assert.NotContains(t, term.Output(), "kind: SpaceProvisionerConfig") + assert.NotContains(t, buffy.String(), "kind: SpaceProvisionerConfig") }) t.Run("Errors when member with different name already exists", func(t *testing.T) { // given - term := NewFakeTerminalWithResponse("Y") newClient, fakeClient := newFakeClientsFromRestConfig(t) + buffy := bytes.NewBuffer(nil) + term := ioutils.NewTerminal(buffy, buffy, ioutils.WithDefaultConfirm(true)) ctx := newExtendedCommandContext(term, newClient) preexistingToolchainCluster := &toolchainv1alpha1.ToolchainCluster{ ObjectMeta: metav1.ObjectMeta{ @@ -410,14 +427,15 @@ func TestRegisterMember(t *testing.T) { require.Error(t, err) assert.Contains(t, err.Error(), `Cannot proceed because of the following problems: - the newly registered member cluster would have a different name (member-cool-server.com) than the already existing one (member-with-weird-name) which would lead to invalid configuration. Consider using the --name-suffix parameter to match the existing member registration if you intend to just update it instead of creating a new registration`) - assert.NotContains(t, term.Output(), "kind: SpaceProvisionerConfig") + assert.NotContains(t, buffy.String(), "kind: SpaceProvisionerConfig") }) t.Run("reports error when member toolchaincluster ServiceAccount is not there", func(t *testing.T) { // given - term := NewFakeTerminalWithResponse("Y") newClient, fakeClient := newFakeClientsFromRestConfig(t, &toolchainClusterHostSa) // we pre-provision only the host toolchaincluster ServiceAccount mockCreateToolchainClusterWithReadyCondition(t, fakeClient) + buffy := bytes.NewBuffer(nil) + term := ioutils.NewTerminal(buffy, buffy, ioutils.WithDefaultConfirm(true)) ctx := newExtendedCommandContext(term, newClient) // when @@ -425,7 +443,7 @@ func TestRegisterMember(t *testing.T) { // then require.Error(t, err) - assert.Contains(t, term.Output(), "The toolchain-member-operator/toolchaincluster-member ServiceAccount is not present in the member cluster.") + assert.Contains(t, buffy.String(), "The toolchain-member-operator/toolchaincluster-member ServiceAccount is not present in the member cluster.") tcs := &toolchainv1alpha1.ToolchainClusterList{} require.NoError(t, fakeClient.List(context.TODO(), tcs, runtimeclient.InNamespace(test.HostOperatorNs))) assert.Empty(t, tcs.Items) @@ -435,8 +453,9 @@ func TestRegisterMember(t *testing.T) { t.Run("reports error when host toolchaincluster ServiceAccount is not there", func(t *testing.T) { // given - term := NewFakeTerminalWithResponse("Y") newClient, fakeClient := newFakeClientsFromRestConfig(t) + buffy := bytes.NewBuffer(nil) + term := ioutils.NewTerminal(buffy, buffy, ioutils.WithDefaultConfirm(true)) ctx := newExtendedCommandContext(term, newClient) // when @@ -444,7 +463,7 @@ func TestRegisterMember(t *testing.T) { // then require.Error(t, err) - assert.Contains(t, term.Output(), "The toolchain-host-operator/toolchaincluster-host ServiceAccount is not present in the host cluster.") + assert.Contains(t, buffy.String(), "The toolchain-host-operator/toolchaincluster-host ServiceAccount is not present in the host cluster.") tcs := &toolchainv1alpha1.ToolchainClusterList{} require.NoError(t, fakeClient.List(context.TODO(), tcs, runtimeclient.InNamespace(test.HostOperatorNs))) assert.Empty(t, tcs.Items) diff --git a/pkg/cmd/adm/restart.go b/pkg/cmd/adm/restart.go index dcd65f05..94431b3b 100644 --- a/pkg/cmd/adm/restart.go +++ b/pkg/cmd/adm/restart.go @@ -1,7 +1,6 @@ package adm import ( - "context" "fmt" "time" @@ -28,7 +27,7 @@ func NewRestartCmd() *cobra.Command { If no deployment name is provided, then it lists all existing deployments in the namespace.`, Args: cobra.RangeArgs(0, 1), RunE: func(cmd *cobra.Command, args []string) error { - term := ioutils.NewTerminal(cmd.InOrStdin, cmd.OutOrStdout) + term := ioutils.NewTerminal(cmd.InOrStdin(), cmd.OutOrStdout(), ioutils.WithVerbose(configuration.Verbose)) ctx := clicontext.NewCommandContext(term, client.DefaultNewClient) return restart(ctx, targetCluster, args...) }, @@ -39,7 +38,7 @@ If no deployment name is provided, then it lists all existing deployments in the } func restart(ctx *clicontext.CommandContext, clusterName string, deployments ...string) error { - cfg, err := configuration.LoadClusterConfig(ctx, clusterName) + cfg, err := configuration.LoadClusterConfig(ctx.Logger, clusterName) if err != nil { return err } @@ -49,17 +48,16 @@ func restart(ctx *clicontext.CommandContext, clusterName string, deployments ... } if len(deployments) == 0 { - err := printExistingDeployments(ctx.Terminal, cl, cfg.OperatorNamespace) + err := printExistingDeployments(ctx, cl, cfg.OperatorNamespace) if err != nil { - ctx.Terminal.Printlnf("\nERROR: Failed to list existing deployments\n :%s", err.Error()) + ctx.Errorf("Failed to list existing deployments\n :%s", err.Error()) } return fmt.Errorf("at least one deployment name is required, include one or more of the above deployments to restart") } deploymentName := deployments[0] - if !ctx.AskForConfirmation( - ioutils.WithMessagef("restart the deployment '%s' in namespace '%s'", deploymentName, cfg.OperatorNamespace)) { - return nil + if confirm, err := ctx.Confirm("Restart the '%s' Deployment in the '%s' namespace", deploymentName, cfg.OperatorNamespace); err != nil || !confirm { + return err } return restartDeployment(ctx, cl, cfg.OperatorNamespace, deploymentName) } @@ -70,28 +68,28 @@ func restartDeployment(ctx *clicontext.CommandContext, cl runtimeclient.Client, Name: deploymentName, } - originalReplicas, err := scaleToZero(cl, namespacedName) + originalReplicas, err := scaleToZero(ctx, cl, namespacedName) if err != nil { if apierrors.IsNotFound(err) { - ctx.Printlnf("\nERROR: The given deployment '%s' wasn't found.", deploymentName) + ctx.Errorf("The '%s' Deployment could not be found", deploymentName) return printExistingDeployments(ctx, cl, ns) } return err } - ctx.Println("The deployment was scaled to 0") + ctx.Info("The deployment was scaled to 0") if err := scaleBack(ctx, cl, namespacedName, originalReplicas); err != nil { - ctx.Printlnf("Scaling the deployment '%s' in namespace '%s' back to '%d' replicas wasn't successful", originalReplicas) - ctx.Println("Please, try to contact administrators to scale the deployment back manually") + ctx.Infof("Scaling the deployment '%s' in namespace '%s' back to '%d' replicas wasn't successful", namespacedName.Name, namespacedName.Namespace, int(originalReplicas)) + ctx.Info("Please, try to contact administrators to scale the deployment back manually") return err } - ctx.Printlnf("The deployment was scaled back to '%d'", originalReplicas) + ctx.Infof("The deployment was scaled back to '%d'", originalReplicas) return nil } func restartHostOperator(ctx *clicontext.CommandContext, hostClient runtimeclient.Client, hostNamespace string) error { deployments := &appsv1.DeploymentList{} - if err := hostClient.List(context.TODO(), deployments, + if err := hostClient.List(ctx.Context, deployments, runtimeclient.InNamespace(hostNamespace), runtimeclient.MatchingLabels{"olm.owner.namespace": "toolchain-host-operator"}); err != nil { return err @@ -104,23 +102,22 @@ func restartHostOperator(ctx *clicontext.CommandContext, hostClient runtimeclien return restartDeployment(ctx, hostClient, hostNamespace, deployments.Items[0].Name) } -func printExistingDeployments(term ioutils.Terminal, cl runtimeclient.Client, ns string) error { +func printExistingDeployments(ctx *clicontext.CommandContext, cl runtimeclient.Client, ns string) error { deployments := &appsv1.DeploymentList{} - if err := cl.List(context.TODO(), deployments, runtimeclient.InNamespace(ns)); err != nil { + if err := cl.List(ctx.Context, deployments, runtimeclient.InNamespace(ns)); err != nil { return err } - deploymentList := "\n" + ctx.Infof("Existing deployments in the '%s' namespace:", ns) for _, deployment := range deployments.Items { - deploymentList += fmt.Sprintf("%s\n", deployment.Name) + ctx.Infof("- %s", deployment.Name) } - term.PrintContextSeparatorWithBodyf(deploymentList, "Existing deployments in %s namespace", ns) return nil } -func scaleToZero(cl runtimeclient.Client, namespacedName types.NamespacedName) (int32, error) { +func scaleToZero(ctx *clicontext.CommandContext, cl runtimeclient.Client, namespacedName types.NamespacedName) (int32, error) { // get the deployment deployment := &appsv1.Deployment{} - if err := cl.Get(context.TODO(), namespacedName, deployment); err != nil { + if err := cl.Get(ctx.Context, namespacedName, deployment); err != nil { return 0, err } // keep original number of replicas so we can bring it back @@ -129,16 +126,16 @@ func scaleToZero(cl runtimeclient.Client, namespacedName types.NamespacedName) ( deployment.Spec.Replicas = &zero // update the deployment so it scales to zero - return originalReplicas, cl.Update(context.TODO(), deployment) + return originalReplicas, cl.Update(ctx.Context, deployment) } -func scaleBack(term ioutils.Terminal, cl runtimeclient.Client, namespacedName types.NamespacedName, originalReplicas int32) error { +func scaleBack(ctx *clicontext.CommandContext, cl runtimeclient.Client, namespacedName types.NamespacedName, originalReplicas int32) error { return wait.Poll(500*time.Millisecond, 10*time.Second, func() (done bool, err error) { - term.Println("") - term.Printlnf("Trying to scale the deployment back to '%d'", originalReplicas) + ctx.Info("") + ctx.Infof("Trying to scale the deployment back to '%d'", originalReplicas) // get the updated deployment := &appsv1.Deployment{} - if err := cl.Get(context.TODO(), namespacedName, deployment); err != nil { + if err := cl.Get(ctx.Context, namespacedName, deployment); err != nil { return false, err } // check if the replicas number wasn't already reset by a controller @@ -148,8 +145,8 @@ func scaleBack(term ioutils.Terminal, cl runtimeclient.Client, namespacedName ty // set the original deployment.Spec.Replicas = &originalReplicas // and update to scale back - if err := cl.Update(context.TODO(), deployment); err != nil { - term.Printlnf("error updating Deployment '%s': %s. Will retry again...", namespacedName.Name, err.Error()) + if err := cl.Update(ctx.Context, deployment); err != nil { + ctx.Warnf("error updating Deployment '%s': %s. Will retry again...", namespacedName.Name, err.Error()) return false, nil } return true, nil diff --git a/pkg/cmd/adm/restart_test.go b/pkg/cmd/adm/restart_test.go index 32197c3c..4afa93e1 100644 --- a/pkg/cmd/adm/restart_test.go +++ b/pkg/cmd/adm/restart_test.go @@ -1,13 +1,17 @@ package adm import ( + "bytes" "context" "fmt" + "strings" "testing" + "github.com/charmbracelet/log" "github.com/codeready-toolchain/toolchain-common/pkg/test" "github.com/kubesaw/ksctl/pkg/configuration" clicontext "github.com/kubesaw/ksctl/pkg/context" + "github.com/kubesaw/ksctl/pkg/ioutils" . "github.com/kubesaw/ksctl/pkg/test" "github.com/stretchr/testify/assert" @@ -32,14 +36,14 @@ func TestRestartDeployment(t *testing.T) { Namespace: namespace, Name: "cool-deployment", } - term := NewFakeTerminalWithResponse("Y") - t.Run("restart is successful for "+clusterName, func(t *testing.T) { // given deployment := newDeployment(namespacedName, 3) newClient, fakeClient := NewFakeClients(t, deployment) numberOfUpdateCalls := 0 fakeClient.MockUpdate = requireDeploymentBeingUpdated(t, fakeClient, namespacedName, 3, &numberOfUpdateCalls) + buffy := bytes.NewBuffer(nil) + term := ioutils.NewTerminal(buffy, buffy, ioutils.WithDefaultConfirm(true)) ctx := clicontext.NewCommandContext(term, newClient) // when @@ -57,7 +61,8 @@ func TestRestartDeployment(t *testing.T) { newClient, fakeClient := NewFakeClients(t, deployment) numberOfUpdateCalls := 0 fakeClient.MockUpdate = requireDeploymentBeingUpdated(t, fakeClient, namespacedName, 3, &numberOfUpdateCalls) - term := NewFakeTerminalWithResponse("Y") + buffy := bytes.NewBuffer(nil) + term := ioutils.NewTerminal(buffy, buffy, ioutils.WithDefaultConfirm(true)) ctx := clicontext.NewCommandContext(term, newClient) // when @@ -67,8 +72,8 @@ func TestRestartDeployment(t *testing.T) { require.EqualError(t, err, "at least one deployment name is required, include one or more of the above deployments to restart") AssertDeploymentHasReplicas(t, fakeClient, namespacedName, 3) assert.Equal(t, 0, numberOfUpdateCalls) - assert.Contains(t, term.Output(), fmt.Sprintf("Existing deployments in toolchain-%s-operator namespace", clusterType)) - assert.Contains(t, term.Output(), "cool-deployment") + assert.Contains(t, buffy.String(), fmt.Sprintf("Existing deployments in the 'toolchain-%s-operator' namespace:", clusterType)) + assert.Contains(t, buffy.String(), "- cool-deployment") }) t.Run("restart fails - cannot get the deployment for "+clusterName, func(t *testing.T) { @@ -80,6 +85,8 @@ func TestRestartDeployment(t *testing.T) { fakeClient.MockGet = func(ctx context.Context, key runtimeclient.ObjectKey, obj runtimeclient.Object, opts ...runtimeclient.GetOption) error { return fmt.Errorf("some error") } + buffy := bytes.NewBuffer(nil) + term := ioutils.NewTerminal(buffy, buffy, ioutils.WithDefaultConfirm(true)) ctx := clicontext.NewCommandContext(term, newClient) // when @@ -98,7 +105,8 @@ func TestRestartDeployment(t *testing.T) { newClient, fakeClient := NewFakeClients(t, deployment) numberOfUpdateCalls := 0 fakeClient.MockUpdate = requireDeploymentBeingUpdated(t, fakeClient, namespacedName, 3, &numberOfUpdateCalls) - term := NewFakeTerminalWithResponse("Y") + buffy := bytes.NewBuffer(nil) + term := ioutils.NewTerminal(buffy, buffy, ioutils.WithDefaultConfirm(true)) ctx := clicontext.NewCommandContext(term, newClient) // when @@ -108,9 +116,9 @@ func TestRestartDeployment(t *testing.T) { require.NoError(t, err) AssertDeploymentHasReplicas(t, fakeClient, namespacedName, 3) assert.Equal(t, 0, numberOfUpdateCalls) - assert.Contains(t, term.Output(), "ERROR: The given deployment 'wrong-deployment' wasn't found.") - assert.Contains(t, term.Output(), fmt.Sprintf("Existing deployments in toolchain-%s-operator namespace", clusterType)) - assert.Contains(t, term.Output(), "cool-deployment") + assert.Contains(t, buffy.String(), "The 'wrong-deployment' Deployment could not be found") + assert.Contains(t, buffy.String(), fmt.Sprintf("Existing deployments in the 'toolchain-%s-operator' namespace:", clusterType)) + assert.Contains(t, buffy.String(), "- cool-deployment") }) } } @@ -133,11 +141,12 @@ func TestRestartDeploymentWithInsufficientPermissions(t *testing.T) { newClient, fakeClient := NewFakeClients(t, deployment) numberOfUpdateCalls := 0 fakeClient.MockUpdate = requireDeploymentBeingUpdated(t, fakeClient, namespacedName, 3, &numberOfUpdateCalls) - term := NewFakeTerminalWithResponse("Y") + buffy := bytes.NewBuffer(nil) + term := ioutils.NewTerminal(buffy, buffy, ioutils.WithDefaultConfirm(true)) ctx := clicontext.NewCommandContext(term, newClient) // when - err := restart(ctx, clusterName, "cool-deployment") + err := restart(ctx, "cool-deployment") // then require.Error(t, err) @@ -149,8 +158,8 @@ func TestRestartDeploymentWithInsufficientPermissions(t *testing.T) { func TestRestartHostOperator(t *testing.T) { // given SetFileConfig(t, Host()) - term := NewFakeTerminalWithResponse("") // it should not read the input - cfg, err := configuration.LoadClusterConfig(term, "host") + logger := log.New(&strings.Builder{}) + cfg, err := configuration.LoadClusterConfig(logger, "host") require.NoError(t, err) namespacedName := types.NamespacedName{ Namespace: "toolchain-host-operator", @@ -164,6 +173,8 @@ func TestRestartHostOperator(t *testing.T) { newClient, fakeClient := NewFakeClients(t, deployment) numberOfUpdateCalls := 0 fakeClient.MockUpdate = requireDeploymentBeingUpdated(t, fakeClient, namespacedName, 1, &numberOfUpdateCalls) + buffy := bytes.NewBuffer(nil) + term := ioutils.NewTerminal(buffy, buffy) ctx := clicontext.NewCommandContext(term, newClient) // when @@ -181,6 +192,8 @@ func TestRestartHostOperator(t *testing.T) { newClient, fakeClient := NewFakeClients(t, deployment) numberOfUpdateCalls := 0 fakeClient.MockUpdate = requireDeploymentBeingUpdated(t, fakeClient, namespacedName, 1, &numberOfUpdateCalls) + buffy := bytes.NewBuffer(nil) + term := ioutils.NewTerminal(buffy, buffy) ctx := clicontext.NewCommandContext(term, newClient) // when @@ -201,6 +214,8 @@ func TestRestartHostOperator(t *testing.T) { newClient, fakeClient := NewFakeClients(t, deployment, deployment2) numberOfUpdateCalls := 0 fakeClient.MockUpdate = requireDeploymentBeingUpdated(t, fakeClient, namespacedName, 1, &numberOfUpdateCalls) + buffy := bytes.NewBuffer(nil) + term := ioutils.NewTerminal(buffy, buffy) ctx := clicontext.NewCommandContext(term, newClient) // when diff --git a/pkg/cmd/adm/unregister_member.go b/pkg/cmd/adm/unregister_member.go index fd177b78..8ebafed0 100644 --- a/pkg/cmd/adm/unregister_member.go +++ b/pkg/cmd/adm/unregister_member.go @@ -21,7 +21,7 @@ func NewUnregisterMemberCmd() *cobra.Command { Long: `Deletes the member cluster from the host cluster. It doesn't touch the member cluster itself. Make sure there is no users left in the member cluster before unregistering it.`, Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { - term := ioutils.NewTerminal(cmd.InOrStdin, cmd.OutOrStdout) + term := ioutils.NewTerminal(cmd.InOrStdin(), cmd.OutOrStdout(), ioutils.WithVerbose(configuration.Verbose)) ctx := clicontext.NewCommandContext(term, client.DefaultNewClient) return UnregisterMemberCluster(ctx, args[0]) }, @@ -29,7 +29,7 @@ func NewUnregisterMemberCmd() *cobra.Command { } func UnregisterMemberCluster(ctx *clicontext.CommandContext, clusterName string) error { - hostClusterConfig, err := configuration.LoadClusterConfig(ctx, configuration.HostName) + hostClusterConfig, err := configuration.LoadClusterConfig(ctx.Logger, configuration.HostName) if err != nil { return err } @@ -38,7 +38,7 @@ func UnregisterMemberCluster(ctx *clicontext.CommandContext, clusterName string) return err } - clusterDef, err := configuration.LoadClusterAccessDefinition(ctx, clusterName) + clusterDef, err := configuration.LoadClusterAccessDefinition(ctx.Logger, clusterName) if err != nil { return err } @@ -48,19 +48,21 @@ func UnregisterMemberCluster(ctx *clicontext.CommandContext, clusterName string) if err := hostClusterClient.Get(context.TODO(), types.NamespacedName{Namespace: hostClusterConfig.OperatorNamespace, Name: clusterResourceName}, toolchainCluster); err != nil { return err } - if err := ctx.PrintObject(toolchainCluster, "Toolchain Member cluster"); err != nil { + if err := ctx.PrintObject("Toolchain Member cluster:", toolchainCluster); err != nil { return err } - confirmation := ctx.AskForConfirmation(ioutils.WithDangerZoneMessagef("unregistering member cluster form host cluster. Make sure there is no users left in the member cluster before unregistering it.", - "Delete Member cluster stated above from the Host cluster?")) - if !confirmation { - return nil + + ctx.Warn("!!! DANGER ZONE !!!") + ctx.Warn("Unregistering member cluster form host cluster") + ctx.Warn("Make sure there is no users left in the member cluster before unregistering it") + if confirm, err := ctx.Confirm("Delete Member cluster above from the Host cluster?"); err != nil || !confirm { + return err } if err := hostClusterClient.Delete(context.TODO(), toolchainCluster); err != nil { return err } - ctx.Printlnf("\nThe deletion of the Toolchain member cluster from the Host cluster has been triggered") + ctx.Info("The deletion of the Toolchain member cluster from the Host cluster has been triggered") return restartHostOperator(ctx, hostClusterClient, hostClusterConfig.OperatorNamespace) } diff --git a/pkg/cmd/adm/unregister_member_test.go b/pkg/cmd/adm/unregister_member_test.go index 72c23924..ae80ab42 100644 --- a/pkg/cmd/adm/unregister_member_test.go +++ b/pkg/cmd/adm/unregister_member_test.go @@ -1,10 +1,13 @@ package adm import ( + "bytes" + "os" "testing" "github.com/codeready-toolchain/toolchain-common/pkg/test" clicontext "github.com/kubesaw/ksctl/pkg/context" + "github.com/kubesaw/ksctl/pkg/ioutils" . "github.com/kubesaw/ksctl/pkg/test" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -22,7 +25,8 @@ func TestUnregisterMemberWhenAnswerIsY(t *testing.T) { fakeClient.MockUpdate = whenDeploymentThenUpdated(t, fakeClient, hostDeploymentName, 1, &numberOfUpdateCalls) SetFileConfig(t, Host(), Member()) - term := NewFakeTerminalWithResponse("y") + buffy := bytes.NewBuffer(nil) + term := ioutils.NewTerminal(buffy, buffy, ioutils.WithDefaultConfirm(true)) ctx := clicontext.NewCommandContext(term, newClient) // when @@ -31,11 +35,10 @@ func TestUnregisterMemberWhenAnswerIsY(t *testing.T) { // then require.NoError(t, err) AssertToolchainClusterDoesNotExist(t, fakeClient, toolchainCluster) - assert.Contains(t, term.Output(), "!!! DANGER ZONE !!!") - assert.NotContains(t, term.Output(), "THIS COMMAND WILL CAUSE UNREGISTER MEMBER CLUSTER FORM HOST CLUSTER. MAKE SURE THERE IS NO USERS LEFT IN THE MEMBER CLUSTER BEFORE UNREGISTERING IT") - assert.Contains(t, term.Output(), "Delete Member cluster stated above from the Host cluster?") - assert.Contains(t, term.Output(), "The deletion of the Toolchain member cluster from the Host cluster has been triggered") - assert.NotContains(t, term.Output(), "cool-token") + assert.Contains(t, buffy.String(), "!!! DANGER ZONE !!!") + assert.NotContains(t, buffy.String(), "THIS COMMAND WILL CAUSE UNREGISTER MEMBER CLUSTER FORM HOST CLUSTER. MAKE SURE THERE IS NO USERS LEFT IN THE MEMBER CLUSTER BEFORE UNREGISTERING IT") + assert.Contains(t, buffy.String(), "The deletion of the Toolchain member cluster from the Host cluster has been triggered") + assert.NotContains(t, buffy.String(), "cool-token") AssertDeploymentHasReplicas(t, fakeClient, hostDeploymentName, 1) assert.Equal(t, 2, numberOfUpdateCalls) @@ -46,7 +49,8 @@ func TestUnregisterMemberWhenAnswerIsN(t *testing.T) { toolchainCluster := NewToolchainCluster(ToolchainClusterName("member-cool-server.com")) newClient, fakeClient := NewFakeClients(t, toolchainCluster) SetFileConfig(t, Host(), Member()) - term := NewFakeTerminalWithResponse("n") + buffy := bytes.NewBuffer(nil) + term := ioutils.NewTerminal(buffy, buffy, ioutils.WithDefaultConfirm(false), ioutils.WithTee(os.Stdout)) ctx := clicontext.NewCommandContext(term, newClient) // when @@ -55,11 +59,10 @@ func TestUnregisterMemberWhenAnswerIsN(t *testing.T) { // then require.NoError(t, err) AssertToolchainClusterSpec(t, fakeClient, toolchainCluster) - assert.Contains(t, term.Output(), "!!! DANGER ZONE !!!") - assert.NotContains(t, term.Output(), "THIS COMMAND WILL CAUSE UNREGISTER MEMBER CLUSTER FORM HOST CLUSTER. MAKE SURE THERE IS NO USERS LEFT IN THE MEMBER CLUSTER BEFORE UNREGISTERING IT") - assert.Contains(t, term.Output(), "Delete Member cluster stated above from the Host cluster?") - assert.NotContains(t, term.Output(), "The deletion of the Toolchain member cluster from the Host cluster has been triggered") - assert.NotContains(t, term.Output(), "cool-token") + assert.Contains(t, buffy.String(), "!!! DANGER ZONE !!!") + assert.NotContains(t, buffy.String(), "THIS COMMAND WILL CAUSE UNREGISTER MEMBER CLUSTER FORM HOST CLUSTER. MAKE SURE THERE IS NO USERS LEFT IN THE MEMBER CLUSTER BEFORE UNREGISTERING IT") + assert.NotContains(t, buffy.String(), "The deletion of the Toolchain member cluster from the Host cluster has been triggered") + assert.NotContains(t, buffy.String(), "cool-token") } func TestUnregisterMemberWhenNotFound(t *testing.T) { @@ -67,7 +70,8 @@ func TestUnregisterMemberWhenNotFound(t *testing.T) { toolchainCluster := NewToolchainCluster(ToolchainClusterName("another-cool-server.com")) newClient, fakeClient := NewFakeClients(t, toolchainCluster) SetFileConfig(t, Host(), Member()) - term := NewFakeTerminalWithResponse("n") + buffy := bytes.NewBuffer(nil) + term := ioutils.NewTerminal(buffy, buffy) ctx := clicontext.NewCommandContext(term, newClient) // when @@ -76,11 +80,11 @@ func TestUnregisterMemberWhenNotFound(t *testing.T) { // then require.EqualError(t, err, "toolchainclusters.toolchain.dev.openshift.com \"member-cool-server.com\" not found") AssertToolchainClusterSpec(t, fakeClient, toolchainCluster) - assert.NotContains(t, term.Output(), "!!! DANGER ZONE !!!") - assert.NotContains(t, term.Output(), "THIS COMMAND WILL CAUSE UNREGISTER MEMBER CLUSTER FORM HOST CLUSTER. MAKE SURE THERE IS NO USERS LEFT IN THE MEMBER CLUSTER BEFORE UNREGISTERING IT") - assert.NotContains(t, term.Output(), "Delete Member cluster stated above from the Host cluster?") - assert.NotContains(t, term.Output(), "The deletion of the Toolchain member cluster from the Host cluster has been triggered") - assert.NotContains(t, term.Output(), "cool-token") + assert.NotContains(t, buffy.String(), "!!! DANGER ZONE !!!") + assert.NotContains(t, buffy.String(), "THIS COMMAND WILL CAUSE UNREGISTER MEMBER CLUSTER FORM HOST CLUSTER. MAKE SURE THERE IS NO USERS LEFT IN THE MEMBER CLUSTER BEFORE UNREGISTERING IT") + assert.NotContains(t, buffy.String(), "Delete Member cluster stated above from the Host cluster?") + assert.NotContains(t, buffy.String(), "The deletion of the Toolchain member cluster from the Host cluster has been triggered") + assert.NotContains(t, buffy.String(), "cool-token") } func TestUnregisterMemberWhenUnknownClusterName(t *testing.T) { @@ -88,7 +92,8 @@ func TestUnregisterMemberWhenUnknownClusterName(t *testing.T) { toolchainCluster := NewToolchainCluster(ToolchainClusterName("member-cool-server.com")) newClient, fakeClient := NewFakeClients(t, toolchainCluster) SetFileConfig(t, Host(), Member()) - term := NewFakeTerminalWithResponse("n") + buffy := bytes.NewBuffer(nil) + term := ioutils.NewTerminal(buffy, buffy) ctx := clicontext.NewCommandContext(term, newClient) // when @@ -98,11 +103,11 @@ func TestUnregisterMemberWhenUnknownClusterName(t *testing.T) { require.Error(t, err) assert.Contains(t, err.Error(), "the provided cluster-name 'some' is not present in your ksctl.yaml file.") AssertToolchainClusterSpec(t, fakeClient, toolchainCluster) - assert.NotContains(t, term.Output(), "!!! DANGER ZONE !!!") - assert.NotContains(t, term.Output(), "THIS COMMAND WILL CAUSE UNREGISTER MEMBER CLUSTER FORM HOST CLUSTER. MAKE SURE THERE IS NO USERS LEFT IN THE MEMBER CLUSTER BEFORE UNREGISTERING IT") - assert.NotContains(t, term.Output(), "Delete Member cluster stated above from the Host cluster?") - assert.NotContains(t, term.Output(), "The deletion of the Toolchain member cluster from the Host cluster has been triggered") - assert.NotContains(t, term.Output(), "cool-token") + assert.NotContains(t, buffy.String(), "!!! DANGER ZONE !!!") + assert.NotContains(t, buffy.String(), "THIS COMMAND WILL CAUSE UNREGISTER MEMBER CLUSTER FORM HOST CLUSTER. MAKE SURE THERE IS NO USERS LEFT IN THE MEMBER CLUSTER BEFORE UNREGISTERING IT") + assert.NotContains(t, buffy.String(), "Delete Member cluster stated above from the Host cluster?") + assert.NotContains(t, buffy.String(), "The deletion of the Toolchain member cluster from the Host cluster has been triggered") + assert.NotContains(t, buffy.String(), "cool-token") } func TestUnregisterMemberLacksPermissions(t *testing.T) { @@ -111,7 +116,8 @@ func TestUnregisterMemberLacksPermissions(t *testing.T) { toolchainCluster := NewToolchainCluster(ToolchainClusterName("member-cool-server.com")) newClient, fakeClient := NewFakeClients(t, toolchainCluster) - term := NewFakeTerminal() + buffy := bytes.NewBuffer(nil) + term := ioutils.NewTerminal(buffy, buffy) ctx := clicontext.NewCommandContext(term, newClient) // when diff --git a/pkg/cmd/approve.go b/pkg/cmd/approve.go index 48589391..91742343 100644 --- a/pkg/cmd/approve.go +++ b/pkg/cmd/approve.go @@ -38,7 +38,7 @@ only one parameter which is the name of the UserSignup to be approved`, return nil }, RunE: func(cmd *cobra.Command, args []string) error { - term := ioutils.NewTerminal(cmd.InOrStdin, cmd.OutOrStdout) + term := ioutils.NewTerminal(cmd.InOrStdin(), cmd.OutOrStdout(), ioutils.WithVerbose(configuration.Verbose)) ctx := clicontext.NewCommandContext(term, client.DefaultNewClient) switch { case usersignupName != "": @@ -87,7 +87,7 @@ func ByEmailAddress(emailAddress string) LookupUserSignup { } func Approve(ctx *clicontext.CommandContext, lookupUserSignup LookupUserSignup, skipPhone bool, targetCluster string) error { - cfg, err := configuration.LoadClusterConfig(ctx, configuration.HostName) + cfg, err := configuration.LoadClusterConfig(ctx.Logger, configuration.HostName) if err != nil { return err } @@ -108,17 +108,17 @@ func Approve(ctx *clicontext.CommandContext, lookupUserSignup LookupUserSignup, return fmt.Errorf(`UserSignup "%s" is missing a phone hash label - the user may not have provided a phone number for verification. In most cases, the user should be asked to attempt the phone verification process. For exceptions, skip this check using the --skip-phone-check parameter`, userSignup.Name) } - if err := ctx.PrintObject(userSignup, "UserSignup to be approved"); err != nil { + if err := ctx.PrintObject("UserSignup to be approved:", userSignup); err != nil { return err } - if !ctx.AskForConfirmation(ioutils.WithMessagef("approve the UserSignup above?")) { - return nil + if confirm, err := ctx.Confirm("Approve the UserSignup above?"); err != nil || !confirm { + return err } states.SetVerificationRequired(userSignup, false) states.SetDeactivated(userSignup, false) states.SetApprovedManually(userSignup, true) if targetCluster != "" { - userSignup.Spec.TargetCluster, err = configuration.GetMemberClusterName(ctx, targetCluster) + userSignup.Spec.TargetCluster, err = configuration.GetMemberClusterName(ctx.Logger, targetCluster) if err != nil { return err } @@ -126,6 +126,6 @@ func Approve(ctx *clicontext.CommandContext, lookupUserSignup LookupUserSignup, if err := cl.Update(context.TODO(), userSignup); err != nil { return err } - ctx.Printlnf("UserSignup has been approved") + ctx.Infof("UserSignup has been approved") return nil } diff --git a/pkg/cmd/approve_test.go b/pkg/cmd/approve_test.go index 764aa64a..cfb7746d 100644 --- a/pkg/cmd/approve_test.go +++ b/pkg/cmd/approve_test.go @@ -1,15 +1,18 @@ package cmd_test import ( + "bytes" "context" "fmt" "testing" + "github.com/charmbracelet/log" toolchainv1alpha1 "github.com/codeready-toolchain/api/api/v1alpha1" "github.com/codeready-toolchain/toolchain-common/pkg/states" "github.com/kubesaw/ksctl/pkg/cmd" "github.com/kubesaw/ksctl/pkg/configuration" clicontext "github.com/kubesaw/ksctl/pkg/context" + "github.com/kubesaw/ksctl/pkg/ioutils" . "github.com/kubesaw/ksctl/pkg/test" "k8s.io/apimachinery/pkg/api/errors" @@ -25,7 +28,8 @@ func TestApprove(t *testing.T) { userSignup := NewUserSignup() newClient, fakeClient := NewFakeClients(t, userSignup) SetFileConfig(t, Host()) - term := NewFakeTerminalWithResponse("Y") + buffy := bytes.NewBuffer(nil) + term := ioutils.NewTerminal(buffy, buffy, ioutils.WithDefaultConfirm(true)) ctx := clicontext.NewCommandContext(term, newClient) // when @@ -37,10 +41,8 @@ func TestApprove(t *testing.T) { states.SetVerificationRequired(userSignup, false) states.SetDeactivated(userSignup, false) AssertUserSignupSpec(t, fakeClient, userSignup) - output := term.Output() - assert.Contains(t, output, "Are you sure that you want to approve the UserSignup above?") - assert.Contains(t, output, "UserSignup has been approved") - assert.NotContains(t, output, "cool-token") + assert.Contains(t, buffy.String(), "UserSignup has been approved") + assert.NotContains(t, buffy.String(), "cool-token") }) t.Run("when answer is N", func(t *testing.T) { @@ -48,7 +50,8 @@ func TestApprove(t *testing.T) { userSignup := NewUserSignup() newClient, fakeClient := NewFakeClients(t, userSignup) SetFileConfig(t, Host()) - term := NewFakeTerminalWithResponse("n") + buffy := bytes.NewBuffer(nil) + term := ioutils.NewTerminal(buffy, buffy, ioutils.WithDefaultConfirm(false)) ctx := clicontext.NewCommandContext(term, newClient) // when @@ -57,10 +60,8 @@ func TestApprove(t *testing.T) { // then require.NoError(t, err) AssertUserSignupSpec(t, fakeClient, userSignup) - output := term.Output() - assert.Contains(t, output, "Are you sure that you want to approve the UserSignup above?") - assert.NotContains(t, output, "UserSignup has been approved") - assert.NotContains(t, output, "cool-token") + assert.NotContains(t, buffy.String(), "UserSignup has been approved") + assert.NotContains(t, buffy.String(), "cool-token") }) t.Run("reactivate deactivated user", func(t *testing.T) { @@ -68,7 +69,8 @@ func TestApprove(t *testing.T) { userSignup := NewUserSignup(UserSignupDeactivated(true)) newClient, fakeClient := NewFakeClients(t, userSignup) SetFileConfig(t, Host()) - term := NewFakeTerminalWithResponse("Y") + buffy := bytes.NewBuffer(nil) + term := ioutils.NewTerminal(buffy, buffy, ioutils.WithDefaultConfirm(true)) ctx := clicontext.NewCommandContext(term, newClient) // when @@ -87,7 +89,8 @@ func TestApprove(t *testing.T) { userSignup := NewUserSignup(UserSignupAutomaticallyApproved(true)) newClient, fakeClient := NewFakeClients(t, userSignup) SetFileConfig(t, Host()) - term := NewFakeTerminalWithResponse("Y") + buffy := bytes.NewBuffer(nil) + term := ioutils.NewTerminal(buffy, buffy) ctx := clicontext.NewCommandContext(term, newClient) // when @@ -103,7 +106,8 @@ func TestApprove(t *testing.T) { userSignup := NewUserSignup(UserSignupApprovedByAdmin(true)) newClient, fakeClient := NewFakeClients(t, userSignup) SetFileConfig(t, Host()) - term := NewFakeTerminalWithResponse("Y") + buffy := bytes.NewBuffer(nil) + term := ioutils.NewTerminal(buffy, buffy) ctx := clicontext.NewCommandContext(term, newClient) // when @@ -120,7 +124,8 @@ func TestApprove(t *testing.T) { userSignup := NewUserSignup(UserSignupAutomaticallyApproved(true)) newClient, fakeClient := NewFakeClients(t, userSignup) SetFileConfig(t, Host()) - term := NewFakeTerminalWithResponse("Y") + buffy := bytes.NewBuffer(nil) + term := ioutils.NewTerminal(buffy, buffy) ctx := clicontext.NewCommandContext(term, newClient) // when @@ -129,10 +134,10 @@ func TestApprove(t *testing.T) { // then require.EqualError(t, err, fmt.Sprintf(`UserSignup "%s" is already approved`, userSignup.Name)) AssertUserSignupSpec(t, fakeClient, userSignup) - output := term.Output() - assert.NotContains(t, output, "Are you sure that you want to approve the UserSignup above?") - assert.NotContains(t, output, "UserSignup has been approved") - assert.NotContains(t, output, "cool-token") + // output := term.Output() + assert.NotContains(t, buffy.String(), "Are you sure that you want to approve the UserSignup above?") + assert.NotContains(t, buffy.String(), "UserSignup has been approved") + assert.NotContains(t, buffy.String(), "cool-token") }) t.Run("when getting usersignup failed", func(t *testing.T) { @@ -140,7 +145,8 @@ func TestApprove(t *testing.T) { userSignup := NewUserSignup() newClient, fakeClient := NewFakeClients(t, userSignup) SetFileConfig(t, Host()) - term := NewFakeTerminalWithResponse("Y") + buffy := bytes.NewBuffer(nil) + term := ioutils.NewTerminal(buffy, buffy) ctx := clicontext.NewCommandContext(term, newClient) // when @@ -151,10 +157,9 @@ func TestApprove(t *testing.T) { // then require.EqualError(t, err, "mock error") AssertUserSignupSpec(t, fakeClient, userSignup) - output := term.Output() - assert.NotContains(t, output, "Are you sure that you want to approve the UserSignup above?") - assert.NotContains(t, output, "UserSignup has been approved") - assert.NotContains(t, output, "cool-token") + assert.NotContains(t, buffy.String(), "Are you sure that you want to approve the UserSignup above?") + assert.NotContains(t, buffy.String(), "UserSignup has been approved") + assert.NotContains(t, buffy.String(), "cool-token") }) t.Run("with phone check variations", func(t *testing.T) { @@ -164,7 +169,8 @@ func TestApprove(t *testing.T) { userSignup := NewUserSignup() newClient, fakeClient := NewFakeClients(t, userSignup) SetFileConfig(t, Host()) - term := NewFakeTerminalWithResponse("Y") + buffy := bytes.NewBuffer(nil) + term := ioutils.NewTerminal(buffy, buffy, ioutils.WithDefaultConfirm(true)) ctx := clicontext.NewCommandContext(term, newClient) // when @@ -176,10 +182,8 @@ func TestApprove(t *testing.T) { states.SetVerificationRequired(userSignup, false) states.SetDeactivated(userSignup, false) AssertUserSignupSpec(t, fakeClient, userSignup) - output := term.Output() - assert.Contains(t, output, "Are you sure that you want to approve the UserSignup above?") - assert.Contains(t, output, "UserSignup has been approved") - assert.NotContains(t, output, "cool-token") + assert.Contains(t, buffy.String(), "UserSignup has been approved") + assert.NotContains(t, buffy.String(), "cool-token") }) t.Run("when usersignup doesn't have phone hash but skip phone verification flag is set", func(t *testing.T) { @@ -187,7 +191,8 @@ func TestApprove(t *testing.T) { userSignup := NewUserSignup(UserSignupRemoveLabel(toolchainv1alpha1.UserSignupUserPhoneHashLabelKey)) newClient, fakeClient := NewFakeClients(t, userSignup) SetFileConfig(t, Host()) - term := NewFakeTerminalWithResponse("Y") + buffy := bytes.NewBuffer(nil) + term := ioutils.NewTerminal(buffy, buffy, ioutils.WithDefaultConfirm(true)) ctx := clicontext.NewCommandContext(term, newClient) // when @@ -199,10 +204,8 @@ func TestApprove(t *testing.T) { states.SetVerificationRequired(userSignup, false) states.SetDeactivated(userSignup, false) AssertUserSignupSpec(t, fakeClient, userSignup) - output := term.Output() - assert.Contains(t, output, "Are you sure that you want to approve the UserSignup above?") - assert.Contains(t, output, "UserSignup has been approved") - assert.NotContains(t, output, "cool-token") + assert.Contains(t, buffy.String(), "UserSignup has been approved") + assert.NotContains(t, buffy.String(), "cool-token") }) t.Run("when usersignup doesn't have phone hash", func(t *testing.T) { @@ -210,7 +213,8 @@ func TestApprove(t *testing.T) { userSignup := NewUserSignup(UserSignupRemoveLabel(toolchainv1alpha1.UserSignupUserPhoneHashLabelKey)) newClient, fakeClient := NewFakeClients(t, userSignup) SetFileConfig(t, Host()) - term := NewFakeTerminalWithResponse("Y") + buffy := bytes.NewBuffer(nil) + term := ioutils.NewTerminal(buffy, buffy, ioutils.WithDefaultConfirm(true)) ctx := clicontext.NewCommandContext(term, newClient) // when @@ -219,10 +223,8 @@ func TestApprove(t *testing.T) { // then require.EqualError(t, err, fmt.Sprintf(`UserSignup "%s" is missing a phone hash label - the user may not have provided a phone number for verification. In most cases, the user should be asked to attempt the phone verification process. For exceptions, skip this check using the --skip-phone-check parameter`, userSignup.Name)) AssertUserSignupSpec(t, fakeClient, userSignup) - output := term.Output() - assert.NotContains(t, output, "Are you sure that you want to approve the UserSignup above?") - assert.NotContains(t, output, "UserSignup has been approved") - assert.NotContains(t, output, "cool-token") + assert.NotContains(t, buffy.String(), "UserSignup has been approved") + assert.NotContains(t, buffy.String(), "cool-token") }) }) @@ -235,7 +237,8 @@ func TestApprove(t *testing.T) { Host(), Member(ClusterName("member1"), ServerName("m1.devcluster.openshift.com")), Member(ClusterName("member2"), ServerName("m2.devcluster.openshift.com"))) - term := NewFakeTerminalWithResponse("Y") + buffy := bytes.NewBuffer(nil) + term := ioutils.NewTerminal(buffy, buffy, ioutils.WithDefaultConfirm(true)) ctx := clicontext.NewCommandContext(term, newClient) // when @@ -249,10 +252,8 @@ func TestApprove(t *testing.T) { // check the expected target cluster matches with the actual one userSignup.Spec.TargetCluster = "member-m1.devcluster.openshift.com" AssertUserSignupSpec(t, fakeClient, userSignup) - output := term.Output() - assert.Contains(t, output, "Are you sure that you want to approve the UserSignup above?") - assert.Contains(t, output, "UserSignup has been approved") - assert.NotContains(t, output, "cool-token") + assert.Contains(t, buffy.String(), "UserSignup has been approved") + assert.NotContains(t, buffy.String(), "cool-token") }) t.Run("when targetCluster is invalid", func(t *testing.T) { @@ -263,7 +264,8 @@ func TestApprove(t *testing.T) { Host(), Member(ClusterName("member1"), ServerName("m1.devcluster.openshift.com")), Member(ClusterName("member2"), ServerName("m2.devcluster.openshift.com"))) - term := NewFakeTerminalWithResponse("Y") + buffy := bytes.NewBuffer(nil) + term := ioutils.NewTerminal(buffy, buffy, ioutils.WithDefaultConfirm(true)) ctx := clicontext.NewCommandContext(term, newClient) // when @@ -288,8 +290,9 @@ func TestLookupUserSignupByName(t *testing.T) { userSignup := NewUserSignup() _, fakeClient := NewFakeClients(t, userSignup) SetFileConfig(t, Host()) - term := NewFakeTerminalWithResponse("Y") - cfg, err := configuration.LoadClusterConfig(term, "host") + buffy := bytes.NewBuffer(nil) + logger := log.New(buffy) + cfg, err := configuration.LoadClusterConfig(logger, "host") require.NoError(t, err) // when @@ -303,8 +306,9 @@ func TestLookupUserSignupByName(t *testing.T) { t.Run("when user is unknown", func(t *testing.T) { _, fakeClient := NewFakeClients(t) SetFileConfig(t, Host()) - term := NewFakeTerminalWithResponse("Y") - cfg, err := configuration.LoadClusterConfig(term, "host") + buffy := bytes.NewBuffer(nil) + logger := log.New(buffy) + cfg, err := configuration.LoadClusterConfig(logger, "host") require.NoError(t, err) // when @@ -322,8 +326,9 @@ func TestLookupUserSignupByName(t *testing.T) { return fmt.Errorf("mock error") } SetFileConfig(t, Host()) - term := NewFakeTerminalWithResponse("Y") - cfg, err := configuration.LoadClusterConfig(term, "host") + buffy := bytes.NewBuffer(nil) + logger := log.New(buffy) + cfg, err := configuration.LoadClusterConfig(logger, "host") require.NoError(t, err) // when @@ -340,8 +345,9 @@ func TestLookupUserSignupByEmailAddress(t *testing.T) { userSignup := NewUserSignup() _, fakeClient := NewFakeClients(t, userSignup) SetFileConfig(t, Host()) - term := NewFakeTerminalWithResponse("Y") - cfg, err := configuration.LoadClusterConfig(term, "host") + buffy := bytes.NewBuffer(nil) + logger := log.New(buffy) + cfg, err := configuration.LoadClusterConfig(logger, "host") require.NoError(t, err) // when @@ -356,8 +362,9 @@ func TestLookupUserSignupByEmailAddress(t *testing.T) { userSignup := NewUserSignup() _, fakeClient := NewFakeClients(t, userSignup) SetFileConfig(t, Host()) - term := NewFakeTerminalWithResponse("Y") - cfg, err := configuration.LoadClusterConfig(term, "host") + buffy := bytes.NewBuffer(nil) + logger := log.New(buffy) + cfg, err := configuration.LoadClusterConfig(logger, "host") require.NoError(t, err) // when @@ -372,8 +379,9 @@ func TestLookupUserSignupByEmailAddress(t *testing.T) { userSignup2 := NewUserSignup() // same email address as userSignup1 _, fakeClient := NewFakeClients(t, userSignup1, userSignup2) SetFileConfig(t, Host()) - term := NewFakeTerminalWithResponse("Y") - cfg, err := configuration.LoadClusterConfig(term, "host") + buffy := bytes.NewBuffer(nil) + logger := log.New(buffy) + cfg, err := configuration.LoadClusterConfig(logger, "host") require.NoError(t, err) // when @@ -390,8 +398,9 @@ func TestLookupUserSignupByEmailAddress(t *testing.T) { return fmt.Errorf("mock error") } SetFileConfig(t, Host()) - term := NewFakeTerminalWithResponse("Y") - cfg, err := configuration.LoadClusterConfig(term, "host") + buffy := bytes.NewBuffer(nil) + logger := log.New(buffy) + cfg, err := configuration.LoadClusterConfig(logger, "host") require.NoError(t, err) // when diff --git a/pkg/cmd/ban.go b/pkg/cmd/ban.go index 354bb8f0..d2ff6144 100644 --- a/pkg/cmd/ban.go +++ b/pkg/cmd/ban.go @@ -1,8 +1,6 @@ package cmd import ( - "context" - toolchainv1alpha1 "github.com/codeready-toolchain/api/api/v1alpha1" "github.com/codeready-toolchain/toolchain-common/pkg/banneduser" "github.com/kubesaw/ksctl/pkg/client" @@ -22,7 +20,7 @@ only two parameters which the first one is the name of the UserSignup to be used and the second one the reason of the ban`, Args: cobra.ExactArgs(2), RunE: func(cmd *cobra.Command, args []string) error { - term := ioutils.NewTerminal(cmd.InOrStdin, cmd.OutOrStdout) + term := ioutils.NewTerminal(cmd.InOrStdin(), cmd.OutOrStdout(), ioutils.WithVerbose(configuration.Verbose)) ctx := clicontext.NewCommandContext(term, client.DefaultNewClient) return Ban(ctx, args...) }, @@ -32,23 +30,23 @@ and the second one the reason of the ban`, func Ban(ctx *clicontext.CommandContext, args ...string) error { return CreateBannedUser(ctx, args[0], args[1], func(userSignup *toolchainv1alpha1.UserSignup, bannedUser *toolchainv1alpha1.BannedUser) (bool, error) { if _, exists := bannedUser.Labels[toolchainv1alpha1.BannedUserPhoneNumberHashLabelKey]; !exists { - ctx.Printlnf("\nINFO: The UserSignup doesn't have the label '%s' set, so the resulting BannedUser resource won't have this label either.\n", + ctx.Infof("The UserSignup doesn't have the label '%s' set, so the resulting BannedUser resource won't have this label either.\n", toolchainv1alpha1.BannedUserPhoneNumberHashLabelKey) } - if err := ctx.PrintObject(bannedUser, "BannedUser resource to be created"); err != nil { + if err := ctx.PrintObject("BannedUser resource to be created:", bannedUser); err != nil { return false, err } - confirmation := ctx.AskForConfirmation(ioutils.WithDangerZoneMessagef( - "deletion of all user's namespaces and all related data.\nIn addition, the user won't be able to login any more.", - "ban the user with the UserSignup by creating BannedUser resource that are both above?")) - return confirmation, nil + ctx.Warn("!!! DANGER ZONE !!!") + ctx.Warn("Deleting all the user's namespaces and all their resources") + ctx.Warn("In addition, the user won't be able to login anymore") + return ctx.Confirm("Ban the user with the UserSignup by creating a BannedUser resource?") }) } func CreateBannedUser(ctx *clicontext.CommandContext, userSignupName, banReason string, confirm func(*toolchainv1alpha1.UserSignup, *toolchainv1alpha1.BannedUser) (bool, error)) error { - cfg, err := configuration.LoadClusterConfig(ctx, configuration.HostName) + cfg, err := configuration.LoadClusterConfig(ctx.Logger, configuration.HostName) if err != nil { return err } @@ -62,7 +60,7 @@ func CreateBannedUser(ctx *clicontext.CommandContext, userSignupName, banReason return err } - ksctlConfig, err := configuration.Load(ctx) + ksctlConfig, err := configuration.Load(ctx.Logger) if err != nil { return err } @@ -77,23 +75,23 @@ func CreateBannedUser(ctx *clicontext.CommandContext, userSignupName, banReason return err } - if err := ctx.PrintObject(userSignup, "UserSignup to be banned"); err != nil { + if err := ctx.PrintObject("UserSignup to be banned:", userSignup); err != nil { return err } if alreadyBannedUser != nil { - ctx.Println("The user was already banned - there is a BannedUser resource with the same labels already present") - return ctx.PrintObject(alreadyBannedUser, "BannedUser resource") + ctx.Info("The user was already banned - there is a BannedUser resource with the same labels already present") + return ctx.PrintObject("BannedUser resource", alreadyBannedUser) } if shouldCreate, err := confirm(userSignup, bannedUser); !shouldCreate || err != nil { return err } - if err := cl.Create(context.TODO(), bannedUser); err != nil { + if err := cl.Create(ctx.Context, bannedUser); err != nil { return err } - ctx.Printlnf("\nUserSignup has been banned by creating BannedUser resource with name " + bannedUser.Name) + ctx.Infof("UserSignup has been banned by creating BannedUser resource with name '%s'", bannedUser.Name) return nil } diff --git a/pkg/cmd/ban_test.go b/pkg/cmd/ban_test.go index fd861941..65f904e4 100644 --- a/pkg/cmd/ban_test.go +++ b/pkg/cmd/ban_test.go @@ -1,6 +1,7 @@ package cmd_test import ( + "bytes" "context" "errors" "fmt" @@ -10,6 +11,7 @@ import ( "github.com/codeready-toolchain/toolchain-common/pkg/test" "github.com/kubesaw/ksctl/pkg/cmd" clicontext "github.com/kubesaw/ksctl/pkg/context" + "github.com/kubesaw/ksctl/pkg/ioutils" . "github.com/kubesaw/ksctl/pkg/test" "github.com/stretchr/testify/assert" @@ -24,7 +26,8 @@ func TestBanCmdWhenAnswerIsY(t *testing.T) { userSignup := NewUserSignup() newClient, fakeClient := NewFakeClients(t, userSignup) SetFileConfig(t, Host()) - term := NewFakeTerminalWithResponse("y") + buffy := bytes.NewBuffer(nil) + term := ioutils.NewTerminal(buffy, buffy, ioutils.WithDefaultConfirm(true)) ctx := clicontext.NewCommandContext(term, newClient) // when @@ -33,14 +36,14 @@ func TestBanCmdWhenAnswerIsY(t *testing.T) { // then require.NoError(t, err) AssertBannedUser(t, fakeClient, userSignup, banReason) - assert.Contains(t, term.Output(), "!!! DANGER ZONE !!!") - assert.Contains(t, term.Output(), "Are you sure that you want to ban the user with the UserSignup by creating BannedUser resource that are both above?") - assert.Contains(t, term.Output(), "UserSignup has been banned") - assert.NotContains(t, term.Output(), "cool-token") + assert.Contains(t, buffy.String(), "!!! DANGER ZONE !!!") + assert.Contains(t, buffy.String(), "UserSignup has been banned") + assert.NotContains(t, buffy.String(), "cool-token") t.Run("don't ban twice", func(t *testing.T) { // given - term := NewFakeTerminalWithResponse("y") + buffy := bytes.NewBuffer(nil) + term := ioutils.NewTerminal(buffy, buffy) ctx := clicontext.NewCommandContext(term, newClient) // when @@ -49,8 +52,8 @@ func TestBanCmdWhenAnswerIsY(t *testing.T) { // then require.NoError(t, err) AssertBannedUser(t, fakeClient, userSignup, banReason) - assert.NotContains(t, term.Output(), "!!! DANGER ZONE !!!") - assert.Contains(t, term.Output(), "The user was already banned - there is a BannedUser resource with the same labels already present") + assert.NotContains(t, buffy.String(), "!!! DANGER ZONE !!!") + assert.Contains(t, buffy.String(), "The user was already banned - there is a BannedUser resource with the same labels already present") }) } @@ -59,7 +62,8 @@ func TestBanCmdWhenAnswerIsN(t *testing.T) { userSignup := NewUserSignup() newClient, fakeClient := NewFakeClients(t, userSignup) SetFileConfig(t, Host()) - term := NewFakeTerminalWithResponse("n") + buffy := bytes.NewBuffer(nil) + term := ioutils.NewTerminal(buffy, buffy, ioutils.WithDefaultConfirm(false)) ctx := clicontext.NewCommandContext(term, newClient) // when @@ -68,10 +72,10 @@ func TestBanCmdWhenAnswerIsN(t *testing.T) { // then require.NoError(t, err) AssertNoBannedUser(t, fakeClient, userSignup) - assert.Contains(t, term.Output(), "!!! DANGER ZONE !!!") - assert.Contains(t, term.Output(), "Are you sure that you want to ban the user with the UserSignup by creating BannedUser resource that are both above?") - assert.NotContains(t, term.Output(), "UserSignup has been banned") - assert.NotContains(t, term.Output(), "cool-token") + assert.Contains(t, buffy.String(), "!!! DANGER ZONE !!!") + // assert.Contains(t, buffy.String(), "Are you sure that you want to ban the user with the UserSignup by creating BannedUser resource that are both above?") + assert.NotContains(t, buffy.String(), "UserSignup has been banned") + assert.NotContains(t, buffy.String(), "cool-token") } func TestBanCmdWhenNotFound(t *testing.T) { @@ -79,7 +83,8 @@ func TestBanCmdWhenNotFound(t *testing.T) { userSignup := NewUserSignup() newClient, fakeClient := NewFakeClients(t, userSignup) SetFileConfig(t, Host()) - term := NewFakeTerminalWithResponse("n") + buffy := bytes.NewBuffer(nil) + term := ioutils.NewTerminal(buffy, buffy, ioutils.WithDefaultConfirm(false)) ctx := clicontext.NewCommandContext(term, newClient) // when @@ -88,10 +93,10 @@ func TestBanCmdWhenNotFound(t *testing.T) { // then require.EqualError(t, err, "usersignups.toolchain.dev.openshift.com \"some\" not found") AssertNoBannedUser(t, fakeClient, userSignup) - assert.NotContains(t, term.Output(), "!!! DANGER ZONE !!!") - assert.NotContains(t, term.Output(), "Are you sure that you want to ban the user with the UserSignup by creating BannedUser resource that are both above?") - assert.NotContains(t, term.Output(), "UserSignup has been banned") - assert.NotContains(t, term.Output(), "cool-token") + assert.NotContains(t, buffy.String(), "!!! DANGER ZONE !!!") + // assert.NotContains(t, buffy.String(), "Are you sure that you want to ban the user with the UserSignup by creating BannedUser resource that are both above?") + assert.NotContains(t, buffy.String(), "UserSignup has been banned") + assert.NotContains(t, buffy.String(), "cool-token") } func TestCreateBannedUser(t *testing.T) { @@ -102,7 +107,8 @@ func TestCreateBannedUser(t *testing.T) { //given userSignup := NewUserSignup() newClient, fakeClient := NewFakeClients(t, userSignup) - term := NewFakeTerminal() + buffy := bytes.NewBuffer(nil) + term := ioutils.NewTerminal(buffy, buffy) ctx := clicontext.NewCommandContext(term, newClient) // when @@ -119,7 +125,8 @@ func TestCreateBannedUser(t *testing.T) { //given userSignup := NewUserSignup() newClient, fakeClient := NewFakeClients(t, userSignup) - term := NewFakeTerminal() + buffy := bytes.NewBuffer(nil) + term := ioutils.NewTerminal(buffy, buffy) ctx := clicontext.NewCommandContext(term, newClient) // when @@ -136,7 +143,8 @@ func TestCreateBannedUser(t *testing.T) { //given userSignup := NewUserSignup() newClient, fakeClient := NewFakeClients(t, userSignup) - term := NewFakeTerminal() + buffy := bytes.NewBuffer(nil) + term := ioutils.NewTerminal(buffy, buffy) ctx := clicontext.NewCommandContext(term, newClient) // when @@ -156,7 +164,8 @@ func TestCreateBannedUser(t *testing.T) { fakeClient.MockGet = func(ctx context.Context, key runtimeclient.ObjectKey, obj runtimeclient.Object, opts ...runtimeclient.GetOption) error { return fmt.Errorf("some error") } - term := NewFakeTerminal() + buffy := bytes.NewBuffer(nil) + term := ioutils.NewTerminal(buffy, buffy) ctx := clicontext.NewCommandContext(term, newClient) // when @@ -176,7 +185,8 @@ func TestCreateBannedUser(t *testing.T) { fakeClient.MockCreate = func(ctx context.Context, obj runtimeclient.Object, opts ...runtimeclient.CreateOption) error { return fmt.Errorf("some error") } - term := NewFakeTerminal() + buffy := bytes.NewBuffer(nil) + term := ioutils.NewTerminal(buffy, buffy) ctx := clicontext.NewCommandContext(term, newClient) // when @@ -193,10 +203,11 @@ func TestCreateBannedUser(t *testing.T) { //given userSignup := NewUserSignup() fakeClient := test.NewFakeClient(t, userSignup) - term := NewFakeTerminal() newClient := func(token, apiEndpoint string) (runtimeclient.Client, error) { return nil, fmt.Errorf("some error") } + buffy := bytes.NewBuffer(nil) + term := ioutils.NewTerminal(buffy, buffy) ctx := clicontext.NewCommandContext(term, newClient) // when @@ -213,7 +224,8 @@ func TestCreateBannedUser(t *testing.T) { //given userSignup := NewUserSignup() newClient, fakeClient := NewFakeClients(t, userSignup) - term := NewFakeTerminal() + buffy := bytes.NewBuffer(nil) + term := ioutils.NewTerminal(buffy, buffy) ctx := clicontext.NewCommandContext(term, newClient) fakeClient.MockList = func(ctx context.Context, list runtimeclient.ObjectList, opts ...runtimeclient.ListOption) error { @@ -233,7 +245,8 @@ func TestCreateBannedUser(t *testing.T) { userSignup := NewUserSignup() userSignup.Labels = nil newClient, fakeClient := NewFakeClients(t, userSignup) - term := NewFakeTerminal() + buffy := bytes.NewBuffer(nil) + term := ioutils.NewTerminal(buffy, buffy) ctx := clicontext.NewCommandContext(term, newClient) // when @@ -253,7 +266,8 @@ func TestCreateBannedUserLacksPermissions(t *testing.T) { userSignup := NewUserSignup() newClient, fakeClient := NewFakeClients(t, userSignup) - term := NewFakeTerminal() + buffy := bytes.NewBuffer(nil) + term := ioutils.NewTerminal(buffy, buffy) ctx := clicontext.NewCommandContext(term, newClient) // when diff --git a/pkg/cmd/base_kubectl.go b/pkg/cmd/base_kubectl.go index c6842990..e199c767 100644 --- a/pkg/cmd/base_kubectl.go +++ b/pkg/cmd/base_kubectl.go @@ -5,10 +5,10 @@ import ( "os" "strings" + "github.com/charmbracelet/log" "github.com/kubesaw/ksctl/pkg/client" "github.com/kubesaw/ksctl/pkg/cmd/flags" "github.com/kubesaw/ksctl/pkg/configuration" - "github.com/kubesaw/ksctl/pkg/ioutils" "github.com/spf13/cobra" "k8s.io/cli-runtime/pkg/genericclioptions" @@ -50,8 +50,8 @@ func setupKubectlCmd(newCmd newCmd) *cobra.Command { if clusterName == "" { // flag is required, but we need to manually verify its presence in the PreRun return fmt.Errorf("you must specify the target cluster") } - term := ioutils.NewTerminal(cmd.InOrStdin, cmd.OutOrStdout) - cfg, err := configuration.LoadClusterConfig(term, clusterName) + logger := log.New(cmd.OutOrStdout()) + cfg, err := configuration.LoadClusterConfig(logger, clusterName) if err != nil { return err } diff --git a/pkg/cmd/create_social_event.go b/pkg/cmd/create_social_event.go index 54b3528c..1fcb91dc 100644 --- a/pkg/cmd/create_social_event.go +++ b/pkg/cmd/create_social_event.go @@ -35,7 +35,7 @@ func NewCreateSocialEventCmd() *cobra.Command { Long: `Create an event (workshop, lab, etc.) to which attendees can signup to with a code.`, Args: cobra.ExactArgs(0), RunE: func(cmd *cobra.Command, args []string) error { - term := ioutils.NewTerminal(cmd.InOrStdin, cmd.OutOrStdout) + term := ioutils.NewTerminal(cmd.InOrStdin(), cmd.OutOrStdout(), ioutils.WithVerbose(configuration.Verbose)) ctx := clicontext.NewCommandContext(term, client.DefaultNewClient) return CreateSocialEvent(ctx, startDate, endDate, description, userTier, spaceTier, maxAttendees, targetCluster) }, @@ -54,7 +54,7 @@ func NewCreateSocialEventCmd() *cobra.Command { } func CreateSocialEvent(ctx *clicontext.CommandContext, startDate, endDate, description, userTier, spaceTier string, maxAttendees int, targetCluster string) error { - cfg, err := configuration.LoadClusterConfig(ctx, configuration.HostName) + cfg, err := configuration.LoadClusterConfig(ctx.Logger, configuration.HostName) if err != nil { return err } @@ -110,15 +110,15 @@ func CreateSocialEvent(ctx *clicontext.CommandContext, startDate, endDate, descr }, } if targetCluster != "" { - se.Spec.TargetCluster, err = configuration.GetMemberClusterName(ctx, targetCluster) + se.Spec.TargetCluster, err = configuration.GetMemberClusterName(ctx.Logger, targetCluster) if err != nil { return err } } - if err := cl.Create(context.TODO(), se); err != nil { + if err := cl.Create(ctx.Context, se); err != nil { return err } - ctx.Printlnf("Social Event successfully created. Activation code is '%s'", se.Name) + ctx.Infof("Social Event successfully created. Activation code is '%s'", se.Name) return nil } diff --git a/pkg/cmd/create_social_event_test.go b/pkg/cmd/create_social_event_test.go index c6ca6be7..7f849c71 100644 --- a/pkg/cmd/create_social_event_test.go +++ b/pkg/cmd/create_social_event_test.go @@ -1,6 +1,7 @@ package cmd_test import ( + "bytes" "context" "fmt" "testing" @@ -9,6 +10,7 @@ import ( "github.com/codeready-toolchain/toolchain-common/pkg/test" "github.com/kubesaw/ksctl/pkg/cmd" clicontext "github.com/kubesaw/ksctl/pkg/context" + "github.com/kubesaw/ksctl/pkg/ioutils" . "github.com/kubesaw/ksctl/pkg/test" "github.com/stretchr/testify/assert" @@ -21,13 +23,14 @@ func TestCreateSocialEvent(t *testing.T) { spaceTier := newNSTemplateTier("base") userTier := newUserTier("deactivate30") SetFileConfig(t, Host(), Member()) - term := NewFakeTerminalWithResponse("y") t.Run("success", func(t *testing.T) { t.Run("1-day event without description", func(t *testing.T) { // given newClient, fakeClient := NewFakeClients(t, userTier, spaceTier) + buffy := bytes.NewBuffer(nil) + term := ioutils.NewTerminal(buffy, buffy) ctx := clicontext.NewCommandContext(term, newClient) startDate := "2022-06-21" // summer 🏝 endDate := "2022-06-21" // ends same day @@ -38,7 +41,7 @@ func TestCreateSocialEvent(t *testing.T) { // then require.NoError(t, err) - assert.Contains(t, term.Output(), "Social Event successfully created") + assert.Contains(t, buffy.String(), "Social Event successfully created") // verify that the SocialEvent was created ses := &toolchainv1alpha1.SocialEventList{} err = fakeClient.List(context.TODO(), ses, runtimeclient.InNamespace(test.HostOperatorNs)) @@ -57,6 +60,8 @@ func TestCreateSocialEvent(t *testing.T) { t.Run("2-day event", func(t *testing.T) { // given newClient, fakeClient := NewFakeClients(t, userTier, spaceTier) + buffy := bytes.NewBuffer(nil) + term := ioutils.NewTerminal(buffy, buffy) ctx := clicontext.NewCommandContext(term, newClient) startDate := "2022-06-21" // summer 🏝 endDate := "2022-06-22" @@ -84,6 +89,8 @@ func TestCreateSocialEvent(t *testing.T) { t.Run("invalid start date", func(t *testing.T) { // given newClient, _ := NewFakeClients(t, userTier, spaceTier) + buffy := bytes.NewBuffer(nil) + term := ioutils.NewTerminal(buffy, buffy) ctx := clicontext.NewCommandContext(term, newClient) startDate := "2022-06-xx" // invalid! endDate := "2022-06-22" @@ -100,6 +107,8 @@ func TestCreateSocialEvent(t *testing.T) { t.Run("invalid end date", func(t *testing.T) { // given newClient, _ := NewFakeClients(t, userTier, spaceTier) + buffy := bytes.NewBuffer(nil) + term := ioutils.NewTerminal(buffy, buffy) ctx := clicontext.NewCommandContext(term, newClient) startDate := "2022-06-21" endDate := "2022-06-32" // invalid value! @@ -116,6 +125,8 @@ func TestCreateSocialEvent(t *testing.T) { t.Run("end date before start date", func(t *testing.T) { // given newClient, _ := NewFakeClients(t, userTier, spaceTier) + buffy := bytes.NewBuffer(nil) + term := ioutils.NewTerminal(buffy, buffy) ctx := clicontext.NewCommandContext(term, newClient) startDate := "2022-06-21" endDate := "2022-06-11" // before start date! @@ -132,6 +143,8 @@ func TestCreateSocialEvent(t *testing.T) { t.Run("usertier does not exist", func(t *testing.T) { // given newClient, _ := NewFakeClients(t, spaceTier) // no user tier + buffy := bytes.NewBuffer(nil) + term := ioutils.NewTerminal(buffy, buffy) ctx := clicontext.NewCommandContext(term, newClient) startDate := "2022-06-21" endDate := "2022-06-22" @@ -148,6 +161,8 @@ func TestCreateSocialEvent(t *testing.T) { t.Run("nstemplatetier does not exist", func(t *testing.T) { // given newClient, _ := NewFakeClients(t, userTier) // no space tier + buffy := bytes.NewBuffer(nil) + term := ioutils.NewTerminal(buffy, buffy) ctx := clicontext.NewCommandContext(term, newClient) startDate := "2022-06-21" endDate := "2022-06-22" @@ -164,6 +179,8 @@ func TestCreateSocialEvent(t *testing.T) { t.Run("invalid target cluster", func(t *testing.T) { // given newClient, _ := NewFakeClients(t, userTier, spaceTier) + buffy := bytes.NewBuffer(nil) + term := ioutils.NewTerminal(buffy, buffy) ctx := clicontext.NewCommandContext(term, newClient) startDate := "2022-06-21" // summer 🏝 endDate := "2022-06-21" // ends same day diff --git a/pkg/cmd/deactivate.go b/pkg/cmd/deactivate.go index 73b5828d..b843a2b6 100644 --- a/pkg/cmd/deactivate.go +++ b/pkg/cmd/deactivate.go @@ -4,6 +4,7 @@ import ( toolchainv1alpha1 "github.com/codeready-toolchain/api/api/v1alpha1" "github.com/codeready-toolchain/toolchain-common/pkg/states" "github.com/kubesaw/ksctl/pkg/client" + "github.com/kubesaw/ksctl/pkg/configuration" clicontext "github.com/kubesaw/ksctl/pkg/context" "github.com/kubesaw/ksctl/pkg/ioutils" @@ -18,7 +19,7 @@ func NewDeactivateCmd() *cobra.Command { only one parameter which is the name of the UserSignup to be deactivated`, Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { - term := ioutils.NewTerminal(cmd.InOrStdin, cmd.OutOrStdout) + term := ioutils.NewTerminal(cmd.InOrStdin(), cmd.OutOrStdout(), ioutils.WithVerbose(configuration.Verbose)) ctx := clicontext.NewCommandContext(term, client.DefaultNewClient) return Deactivate(ctx, args...) }, @@ -27,15 +28,15 @@ only one parameter which is the name of the UserSignup to be deactivated`, func Deactivate(ctx *clicontext.CommandContext, args ...string) error { return client.PatchUserSignup(ctx, args[0], func(userSignup *toolchainv1alpha1.UserSignup) (bool, error) { - if err := ctx.PrintObject(userSignup, "UserSignup to be deactivated"); err != nil { + if err := ctx.PrintObject("UserSignup to be deactivated:", userSignup); err != nil { return false, err } - confirmation := ctx.AskForConfirmation(ioutils.WithDangerZoneMessagef( - "deletion of all user's namespaces and all related data", "deactivate the UserSignup above?")) - if confirmation { - states.SetDeactivated(userSignup, true) - return true, nil + ctx.Warn("!!! DANGER ZONE !!!") + ctx.Warn("Deleting all the user's namespaces and all their resources") + if confirmation, err := ctx.Confirm("Deactivate the UserSignup above?"); err != nil || !confirmation { + return false, err } - return false, nil + states.SetDeactivated(userSignup, true) + return true, nil }, "UserSignup has been deactivated") } diff --git a/pkg/cmd/deactivate_test.go b/pkg/cmd/deactivate_test.go index bfaad907..1e14b01b 100644 --- a/pkg/cmd/deactivate_test.go +++ b/pkg/cmd/deactivate_test.go @@ -1,12 +1,14 @@ package cmd_test import ( + "bytes" "testing" "github.com/codeready-toolchain/toolchain-common/pkg/states" "github.com/kubesaw/ksctl/pkg/cmd" clicontext "github.com/kubesaw/ksctl/pkg/context" + "github.com/kubesaw/ksctl/pkg/ioutils" . "github.com/kubesaw/ksctl/pkg/test" "github.com/stretchr/testify/assert" @@ -18,7 +20,8 @@ func TestDeactivateCmdWhenAnswerIsY(t *testing.T) { userSignup := NewUserSignup() newClient, fakeClient := NewFakeClients(t, userSignup) SetFileConfig(t, Host()) - term := NewFakeTerminalWithResponse("y") + buffy := bytes.NewBuffer(nil) + term := ioutils.NewTerminal(buffy, buffy, ioutils.WithDefaultConfirm(true)) ctx := clicontext.NewCommandContext(term, newClient) // when @@ -28,10 +31,10 @@ func TestDeactivateCmdWhenAnswerIsY(t *testing.T) { require.NoError(t, err) states.SetDeactivated(userSignup, true) AssertUserSignupSpec(t, fakeClient, userSignup) - assert.Contains(t, term.Output(), "!!! DANGER ZONE !!!") - assert.Contains(t, term.Output(), "Are you sure that you want to deactivate the UserSignup above?") - assert.Contains(t, term.Output(), "UserSignup has been deactivated") - assert.NotContains(t, term.Output(), "cool-token") + assert.Contains(t, buffy.String(), "!!! DANGER ZONE !!!") + // assert.Contains(t, buffy.String(), "Are you sure that you want to deactivate the UserSignup above?") + assert.Contains(t, buffy.String(), "UserSignup has been deactivated") + assert.NotContains(t, buffy.String(), "cool-token") } func TestDeactivateCmdWhenAnswerIsN(t *testing.T) { @@ -39,7 +42,8 @@ func TestDeactivateCmdWhenAnswerIsN(t *testing.T) { userSignup := NewUserSignup() newClient, fakeClient := NewFakeClients(t, userSignup) SetFileConfig(t, Host()) - term := NewFakeTerminalWithResponse("n") + buffy := bytes.NewBuffer(nil) + term := ioutils.NewTerminal(buffy, buffy, ioutils.WithDefaultConfirm(false)) ctx := clicontext.NewCommandContext(term, newClient) // when @@ -48,10 +52,10 @@ func TestDeactivateCmdWhenAnswerIsN(t *testing.T) { // then require.NoError(t, err) AssertUserSignupSpec(t, fakeClient, userSignup) - assert.Contains(t, term.Output(), "!!! DANGER ZONE !!!") - assert.Contains(t, term.Output(), "Are you sure that you want to deactivate the UserSignup above?") - assert.NotContains(t, term.Output(), "UserSignup has been deactivated") - assert.NotContains(t, term.Output(), "cool-token") + assert.Contains(t, buffy.String(), "!!! DANGER ZONE !!!") + // assert.Contains(t, buffy.String(), "Are you sure that you want to deactivate the UserSignup above?") + assert.NotContains(t, buffy.String(), "UserSignup has been deactivated") + assert.NotContains(t, buffy.String(), "cool-token") } func TestDeactivateCmdWhenNotFound(t *testing.T) { @@ -59,7 +63,8 @@ func TestDeactivateCmdWhenNotFound(t *testing.T) { userSignup := NewUserSignup() newClient, fakeClient := NewFakeClients(t, userSignup) SetFileConfig(t, Host()) - term := NewFakeTerminalWithResponse("n") + buffy := bytes.NewBuffer(nil) + term := ioutils.NewTerminal(buffy, buffy) ctx := clicontext.NewCommandContext(term, newClient) // when @@ -68,8 +73,8 @@ func TestDeactivateCmdWhenNotFound(t *testing.T) { // then require.EqualError(t, err, "usersignups.toolchain.dev.openshift.com \"some\" not found") AssertUserSignupSpec(t, fakeClient, userSignup) - assert.NotContains(t, term.Output(), "!!! DANGER ZONE !!!") - assert.NotContains(t, term.Output(), "Are you sure that you want to deactivate the UserSignup above?") - assert.NotContains(t, term.Output(), "UserSignup has been deactivated") - assert.NotContains(t, term.Output(), "cool-token") + assert.NotContains(t, buffy.String(), "!!! DANGER ZONE !!!") + // assert.NotContains(t, buffy.String(), "Are you sure that you want to deactivate the UserSignup above?") + assert.NotContains(t, buffy.String(), "UserSignup has been deactivated") + assert.NotContains(t, buffy.String(), "cool-token") } diff --git a/pkg/cmd/delete.go b/pkg/cmd/delete.go index 639d195c..21eabbce 100644 --- a/pkg/cmd/delete.go +++ b/pkg/cmd/delete.go @@ -21,7 +21,7 @@ func NewGdprDeleteCmd() *cobra.Command { only one parameter which is the name of the UserSignup to be deleted`, Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { - term := ioutils.NewTerminal(cmd.InOrStdin, cmd.OutOrStdout) + term := ioutils.NewTerminal(cmd.InOrStdin(), cmd.OutOrStdout(), ioutils.WithVerbose(configuration.Verbose)) ctx := clicontext.NewCommandContext(term, client.DefaultNewClient) return Delete(ctx, args...) }, @@ -29,7 +29,7 @@ only one parameter which is the name of the UserSignup to be deleted`, } func Delete(ctx *clicontext.CommandContext, args ...string) error { - cfg, err := configuration.LoadClusterConfig(ctx, configuration.HostName) + cfg, err := configuration.LoadClusterConfig(ctx.Logger, configuration.HostName) if err != nil { return err } @@ -41,14 +41,14 @@ func Delete(ctx *clicontext.CommandContext, args ...string) error { if err != nil { return err } - if err := ctx.PrintObject(userSignup, "UserSignup to be deleted"); err != nil { + if err := ctx.PrintObject("UserSignup to be deleted:", userSignup); err != nil { return err } - confirmation := ctx.AskForConfirmation(ioutils.WithDangerZoneMessagef( - "deletion of all user's namespaces and all related data.\n"+ - "This command should be executed based on GDPR request.", "delete the UserSignup above?")) - if !confirmation { - return nil + ctx.Warn("!!! DANGER ZONE !!!") + ctx.Warn("Deleting all the user's namespaces and all their resources") + ctx.Warn("This command should be executed after a GDPR request") + if confirm, err := ctx.Confirm("Delete the UserSignup above?"); err != nil || !confirm { + return err } propagationPolicy := metav1.DeletePropagationForeground opts := runtimeclient.DeleteOption(&runtimeclient.DeleteOptions{ @@ -57,6 +57,6 @@ func Delete(ctx *clicontext.CommandContext, args ...string) error { if err := cl.Delete(context.TODO(), userSignup, opts); err != nil { return err } - ctx.Printlnf("\nThe deletion of the UserSignup has been triggered") + ctx.Info("The deletion of the UserSignup has been triggered") return nil } diff --git a/pkg/cmd/delete_test.go b/pkg/cmd/delete_test.go index 3f11c290..e1661835 100644 --- a/pkg/cmd/delete_test.go +++ b/pkg/cmd/delete_test.go @@ -1,11 +1,13 @@ package cmd_test import ( + "bytes" "context" "testing" "github.com/kubesaw/ksctl/pkg/cmd" clicontext "github.com/kubesaw/ksctl/pkg/context" + "github.com/kubesaw/ksctl/pkg/ioutils" . "github.com/kubesaw/ksctl/pkg/test" "github.com/stretchr/testify/assert" @@ -19,7 +21,8 @@ func TestDeleteCmdWhenAnswerIsY(t *testing.T) { userSignup := NewUserSignup() newClient, fakeClient := NewFakeClients(t, userSignup) SetFileConfig(t, Host()) - term := NewFakeTerminalWithResponse("y") + buffy := bytes.NewBuffer(nil) + term := ioutils.NewTerminal(buffy, buffy, ioutils.WithDefaultConfirm(true)) ctx := clicontext.NewCommandContext(term, newClient) // when @@ -28,11 +31,11 @@ func TestDeleteCmdWhenAnswerIsY(t *testing.T) { // then require.NoError(t, err) AssertUserSignupDoesNotExist(t, fakeClient, userSignup) - assert.Contains(t, term.Output(), "!!! DANGER ZONE !!!") - assert.Contains(t, term.Output(), "THIS COMMAND SHOULD BE EXECUTED BASED ON GDPR REQUEST.") - assert.Contains(t, term.Output(), "Are you sure that you want to delete the UserSignup above?") - assert.Contains(t, term.Output(), "The deletion of the UserSignup has been triggered") - assert.NotContains(t, term.Output(), "cool-token") + assert.Contains(t, buffy.String(), "!!! DANGER ZONE !!!") + assert.Contains(t, buffy.String(), "This command should be executed after a GDPR request") + // assert.Contains(t, buffy.String(), "Are you sure that you want to delete the UserSignup above?") + assert.Contains(t, buffy.String(), "The deletion of the UserSignup has been triggered") + assert.NotContains(t, buffy.String(), "cool-token") } func TestDeleteCmdWhenAnswerIsN(t *testing.T) { @@ -40,7 +43,8 @@ func TestDeleteCmdWhenAnswerIsN(t *testing.T) { userSignup := NewUserSignup() newClient, fakeClient := NewFakeClients(t, userSignup) SetFileConfig(t, Host()) - term := NewFakeTerminalWithResponse("n") + buffy := bytes.NewBuffer(nil) + term := ioutils.NewTerminal(buffy, buffy, ioutils.WithDefaultConfirm(false)) ctx := clicontext.NewCommandContext(term, newClient) // when @@ -49,11 +53,11 @@ func TestDeleteCmdWhenAnswerIsN(t *testing.T) { // then require.NoError(t, err) AssertUserSignupSpec(t, fakeClient, userSignup) - assert.Contains(t, term.Output(), "!!! DANGER ZONE !!!") - assert.Contains(t, term.Output(), "THIS COMMAND SHOULD BE EXECUTED BASED ON GDPR REQUEST.") - assert.Contains(t, term.Output(), "Are you sure that you want to delete the UserSignup above?") - assert.NotContains(t, term.Output(), "The deletion of the UserSignup has been triggered") - assert.NotContains(t, term.Output(), "cool-token") + assert.Contains(t, buffy.String(), "!!! DANGER ZONE !!!") + assert.Contains(t, buffy.String(), "This command should be executed after a GDPR request") + // assert.Contains(t, buffy.String(), "Are you sure that you want to delete the UserSignup above?") + assert.NotContains(t, buffy.String(), "The deletion of the UserSignup has been triggered") + assert.NotContains(t, buffy.String(), "cool-token") } func TestDeleteCmdWhenNotFound(t *testing.T) { @@ -61,7 +65,8 @@ func TestDeleteCmdWhenNotFound(t *testing.T) { userSignup := NewUserSignup() newClient, fakeClient := NewFakeClients(t, userSignup) SetFileConfig(t, Host()) - term := NewFakeTerminalWithResponse("n") + buffy := bytes.NewBuffer(nil) + term := ioutils.NewTerminal(buffy, buffy) ctx := clicontext.NewCommandContext(term, newClient) // when @@ -70,10 +75,10 @@ func TestDeleteCmdWhenNotFound(t *testing.T) { // then require.EqualError(t, err, "usersignups.toolchain.dev.openshift.com \"some\" not found") AssertUserSignupSpec(t, fakeClient, userSignup) - assert.NotContains(t, term.Output(), "!!! DANGER ZONE !!!") - assert.NotContains(t, term.Output(), "Are you sure that you want to delete the UserSignup above?") - assert.NotContains(t, term.Output(), "The deletion of the UserSignup has been triggered") - assert.NotContains(t, term.Output(), "cool-token") + assert.NotContains(t, buffy.String(), "!!! DANGER ZONE !!!") + // assert.NotContains(t, buffy.String(), "Are you sure that you want to delete the UserSignup above?") + assert.NotContains(t, buffy.String(), "The deletion of the UserSignup has been triggered") + assert.NotContains(t, buffy.String(), "cool-token") } func TestDeleteLacksPermissions(t *testing.T) { @@ -82,7 +87,8 @@ func TestDeleteLacksPermissions(t *testing.T) { userSignup := NewUserSignup() newClient, fakeClient := NewFakeClients(t, userSignup) - term := NewFakeTerminal() + buffy := bytes.NewBuffer(nil) + term := ioutils.NewTerminal(buffy, buffy) ctx := clicontext.NewCommandContext(term, newClient) // when @@ -98,7 +104,6 @@ func TestDeleteHasPropagationPolicy(t *testing.T) { userSignup := NewUserSignup() newClient, fakeClient := NewFakeClients(t, userSignup) SetFileConfig(t, Host()) - term := NewFakeTerminalWithResponse("y") deleted := false fakeClient.MockDelete = func(ctx context.Context, obj runtimeclient.Object, opts ...runtimeclient.DeleteOption) error { @@ -111,6 +116,8 @@ func TestDeleteHasPropagationPolicy(t *testing.T) { assert.Equal(t, metav1.DeletePropagationForeground, *deleteOptions.PropagationPolicy) return nil } + buffy := bytes.NewBuffer(nil) + term := ioutils.NewTerminal(buffy, buffy, ioutils.WithDefaultConfirm(true)) ctx := clicontext.NewCommandContext(term, newClient) // when diff --git a/pkg/cmd/disable-feature.go b/pkg/cmd/disable_feature.go similarity index 56% rename from pkg/cmd/disable-feature.go rename to pkg/cmd/disable_feature.go index 102cf920..415d38b1 100644 --- a/pkg/cmd/disable-feature.go +++ b/pkg/cmd/disable_feature.go @@ -1,10 +1,12 @@ package cmd import ( + "fmt" "strings" toolchainv1alpha1 "github.com/codeready-toolchain/api/api/v1alpha1" "github.com/kubesaw/ksctl/pkg/client" + "github.com/kubesaw/ksctl/pkg/configuration" clicontext "github.com/kubesaw/ksctl/pkg/context" "github.com/kubesaw/ksctl/pkg/ioutils" "k8s.io/utils/strings/slices" @@ -20,7 +22,7 @@ func NewDisableFeatureCmd() *cobra.Command { parameters - the first one is the Space name and the second is the name of the feature toggle that should be disabled for the Space.`, Args: cobra.ExactArgs(2), RunE: func(cmd *cobra.Command, args []string) error { - term := ioutils.NewTerminal(cmd.InOrStdin, cmd.OutOrStdout) + term := ioutils.NewTerminal(cmd.InOrStdin(), cmd.OutOrStdout(), ioutils.WithVerbose(configuration.Verbose)) ctx := clicontext.NewCommandContext(term, client.DefaultNewClient) return DisableFeature(ctx, args[0], args[1]) }, @@ -34,31 +36,26 @@ func DisableFeature(ctx *clicontext.CommandContext, spaceName, featureToggleName if currentFeatures != "" { enabledFeatures = strings.Split(currentFeatures, ",") } - if err := ctx.PrintObject(space, "The current Space"); err != nil { + if err := ctx.PrintObject("Current Space:", space); err != nil { return false, err } if !slices.Contains(enabledFeatures, featureToggleName) { - ctx.Println("") - ctx.Println("The Space doesn't have the feature toggle enabled. There is nothing to do.") - ctx.Println("") + ctx.Warnf("Nothing to do: the '%s' feature is not enabled in the '%s' Space", featureToggleName, spaceName) return false, nil } - confirmation := ctx.AskForConfirmation(ioutils.WithMessagef( - "disable the feature toggle '%s' for the Space '%s'? The enabled feature toggles are '%s'.", - featureToggleName, spaceName, currentFeatures)) - - if confirmation { - index := slices.Index(enabledFeatures, featureToggleName) - enabledFeatures = append(enabledFeatures[:index], enabledFeatures[index+1:]...) - if len(enabledFeatures) == 0 { - delete(space.Annotations, toolchainv1alpha1.FeatureToggleNameAnnotationKey) - } else { - space.Annotations[toolchainv1alpha1.FeatureToggleNameAnnotationKey] = strings.Join(enabledFeatures, ",") - } - return true, nil + ctx.Infof("Currently enabled features for the '%s' Space are: '%s'", spaceName, currentFeatures) + if confirm, err := ctx.Confirm("Disable the '%s' feature for the '%s' Space? ", featureToggleName, spaceName); err != nil || !confirm { + return confirm, err + } + index := slices.Index(enabledFeatures, featureToggleName) + enabledFeatures = append(enabledFeatures[:index], enabledFeatures[index+1:]...) + if len(enabledFeatures) == 0 { + delete(space.Annotations, toolchainv1alpha1.FeatureToggleNameAnnotationKey) + } else { + space.Annotations[toolchainv1alpha1.FeatureToggleNameAnnotationKey] = strings.Join(enabledFeatures, ",") } - return false, nil - }, "Successfully disabled feature toggle for the Space") + return true, nil + }, fmt.Sprintf("Successfully disabled the '%s' feature for the '%s' Space", featureToggleName, spaceName)) } diff --git a/pkg/cmd/disable_feature_test.go b/pkg/cmd/disable_feature_test.go index ec74e9c5..fdf5ff6e 100644 --- a/pkg/cmd/disable_feature_test.go +++ b/pkg/cmd/disable_feature_test.go @@ -1,12 +1,14 @@ package cmd_test import ( + "bytes" "fmt" "testing" toolchainv1alpha1 "github.com/codeready-toolchain/api/api/v1alpha1" "github.com/kubesaw/ksctl/pkg/cmd" clicontext "github.com/kubesaw/ksctl/pkg/context" + "github.com/kubesaw/ksctl/pkg/ioutils" . "github.com/kubesaw/ksctl/pkg/test" "github.com/stretchr/testify/assert" @@ -49,12 +51,13 @@ func TestDisableFeatureCmd(t *testing.T) { } } - for _, answer := range []string{"Y", "n"} { + for _, answer := range []bool{true, false} { - t.Run("when answer is "+answer, func(t *testing.T) { + t.Run(fmt.Sprintf("when answer is %t", answer), func(t *testing.T) { // given newClient, fakeClient := NewFakeClients(t, space) - term := NewFakeTerminalWithResponse(answer) + buffy := bytes.NewBuffer(nil) + term := ioutils.NewTerminal(buffy, buffy, ioutils.WithDefaultConfirm(answer)) ctx := clicontext.NewCommandContext(term, newClient) // when @@ -63,18 +66,16 @@ func TestDisableFeatureCmd(t *testing.T) { // then require.NoError(t, err) - output := term.Output() - assert.Contains(t, output, fmt.Sprintf("disable the feature toggle 'feature-x' for the Space 'testspace'? The enabled feature toggles are '%s'.", data.alreadyEnabled)) - assert.NotContains(t, output, "cool-token") + // assert.Contains(t, buffy.String(), fmt.Sprintf("disable the feature toggle 'feature-x' for the Space 'testspace'? The enabled feature toggles are '%s'.", data.alreadyEnabled)) + assert.NotContains(t, buffy.String(), "cool-token") expectedSpace := newSpace() - if answer == "Y" { + if answer { expectedSpace.Annotations = data.afterDisable - assert.Contains(t, output, "Successfully disabled feature toggle for the Space") - + assert.Contains(t, buffy.String(), "Successfully disabled the 'feature-x' feature for the 'testspace' Space") } else { expectedSpace.Annotations = space.Annotations - assert.NotContains(t, output, "Successfully disabled feature toggle for the Space") + assert.NotContains(t, buffy.String(), "Successfully disabled the 'feature-x' feature for the 'testspace' Space") } assertSpaceAnnotations(t, fakeClient, expectedSpace) @@ -99,7 +100,8 @@ func TestDisableFeatureCmdWhenFeatureIsNotEnabled(t *testing.T) { } // given newClient, fakeClient := NewFakeClients(t, space) - term := NewFakeTerminalWithResponse("Y") + buffy := bytes.NewBuffer(nil) + term := ioutils.NewTerminal(buffy, buffy) ctx := clicontext.NewCommandContext(term, newClient) // when @@ -109,11 +111,10 @@ func TestDisableFeatureCmdWhenFeatureIsNotEnabled(t *testing.T) { require.NoError(t, err) assertSpaceAnnotations(t, fakeClient, space) // no change - output := term.Output() - assert.Contains(t, output, "The Space doesn't have the feature toggle enabled. There is nothing to do.") - assert.NotContains(t, output, "disable the feature toggle 'feature-x' for the Space 'testspace'?") - assert.NotContains(t, output, "Successfully disabled feature toggle for the Space") - assert.NotContains(t, output, "cool-token") + assert.Contains(t, buffy.String(), "Nothing to do: the 'feature-x' feature is not enabled in the 'testspace' Space") + assert.NotContains(t, buffy.String(), "disable the feature toggle 'feature-x' for the Space 'testspace'?") + assert.NotContains(t, buffy.String(), "Successfully disabled feature toggle for the Space") + assert.NotContains(t, buffy.String(), "cool-token") }) } @@ -124,7 +125,8 @@ func TestDisableFeatureCmdWhenSpaceNotFound(t *testing.T) { space := newSpace() newClient, fakeClient := NewFakeClients(t, space) SetFileConfig(t, Host()) - term := NewFakeTerminalWithResponse("Y") + buffy := bytes.NewBuffer(nil) + term := ioutils.NewTerminal(buffy, buffy) ctx := clicontext.NewCommandContext(term, newClient) // when @@ -133,8 +135,7 @@ func TestDisableFeatureCmdWhenSpaceNotFound(t *testing.T) { // then require.EqualError(t, err, "spaces.toolchain.dev.openshift.com \"another\" not found") assertSpaceAnnotations(t, fakeClient, space) // unrelated space should be unchanged - output := term.Output() - assert.NotContains(t, output, "disable the feature toggle 'feature-x' for the Space 'testspace'?") - assert.NotContains(t, output, "Successfully disabled feature toggle for the Space") - assert.NotContains(t, output, "cool-token") + assert.NotContains(t, buffy.String(), "disable the feature toggle 'feature-x' for the Space 'testspace'?") + assert.NotContains(t, buffy.String(), "Successfully disabled feature toggle for the Space") + assert.NotContains(t, buffy.String(), "cool-token") } diff --git a/pkg/cmd/disable_user.go b/pkg/cmd/disable_user.go index d64730cb..bb3c9cfd 100644 --- a/pkg/cmd/disable_user.go +++ b/pkg/cmd/disable_user.go @@ -3,6 +3,7 @@ package cmd import ( toolchainv1alpha1 "github.com/codeready-toolchain/api/api/v1alpha1" "github.com/kubesaw/ksctl/pkg/client" + "github.com/kubesaw/ksctl/pkg/configuration" clicontext "github.com/kubesaw/ksctl/pkg/context" "github.com/kubesaw/ksctl/pkg/ioutils" "github.com/spf13/cobra" @@ -16,7 +17,7 @@ func NewDisableUserCmd() *cobra.Command { only one parameter which is the name of the MasterUserRecord to be disabled`, Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { - term := ioutils.NewTerminal(cmd.InOrStdin, cmd.OutOrStdout) + term := ioutils.NewTerminal(cmd.InOrStdin(), cmd.OutOrStdout(), ioutils.WithVerbose(configuration.Verbose)) ctx := clicontext.NewCommandContext(term, client.DefaultNewClient) return DisableUser(ctx, args...) }, @@ -25,15 +26,15 @@ only one parameter which is the name of the MasterUserRecord to be disabled`, func DisableUser(ctx *clicontext.CommandContext, args ...string) error { return client.PatchMasterUserRecord(ctx, args[0], func(masterUserRecord *toolchainv1alpha1.MasterUserRecord) (bool, error) { - if err := ctx.PrintObject(masterUserRecord, "MasterUserRecord to be disabled"); err != nil { + if err := ctx.PrintObject("MasterUserRecord to be disabled:", masterUserRecord); err != nil { return false, err } - confirmation := ctx.AskForConfirmation(ioutils.WithDangerZoneMessagef( - "Disabling the MasterUserRecord will delete User/Identity objects so the user can’t login.", "disable the MasterUserRecord above?")) - if confirmation { - masterUserRecord.Spec.Disabled = true - return true, nil + ctx.Warn("!!! DANGER ZONE !!!") + ctx.Warn("Disabling the MasterUserRecord will delete the User and Identity resources so the user cannot login anymore") + if confirm, err := ctx.Confirm("Disable the MasterUserRecord above?"); err != nil || !confirm { + return false, err } - return false, nil + masterUserRecord.Spec.Disabled = true + return true, nil }, "MasterUserRecord has been disabled") } diff --git a/pkg/cmd/disable_user_test.go b/pkg/cmd/disable_user_test.go index 4ebc9b44..0315aeef 100644 --- a/pkg/cmd/disable_user_test.go +++ b/pkg/cmd/disable_user_test.go @@ -1,12 +1,14 @@ package cmd_test import ( + "bytes" "testing" "github.com/codeready-toolchain/toolchain-common/pkg/test/masteruserrecord" "github.com/kubesaw/ksctl/pkg/cmd" clicontext "github.com/kubesaw/ksctl/pkg/context" + "github.com/kubesaw/ksctl/pkg/ioutils" . "github.com/kubesaw/ksctl/pkg/test" "github.com/stretchr/testify/assert" @@ -19,7 +21,8 @@ func TestDisableUserCmdWhenAnswerIsY(t *testing.T) { mur1 := masteruserrecord.NewMasterUserRecord(t, "alice", masteruserrecord.TierName("deactivate30")) newClient, fakeClient := NewFakeClients(t, mur1) SetFileConfig(t, Host()) - term := NewFakeTerminalWithResponse("y") + buffy := bytes.NewBuffer(nil) + term := ioutils.NewTerminal(buffy, buffy, ioutils.WithDefaultConfirm(true)) ctx := clicontext.NewCommandContext(term, newClient) // when @@ -30,10 +33,10 @@ func TestDisableUserCmdWhenAnswerIsY(t *testing.T) { // check if mur was disabled mur1.Spec.Disabled = true assertMasterUserRecordSpec(t, fakeClient, mur1) - assert.Contains(t, term.Output(), "!!! DANGER ZONE !!!") - assert.Contains(t, term.Output(), "Are you sure that you want to disable the MasterUserRecord above?") - assert.Contains(t, term.Output(), "MasterUserRecord has been disabled") - assert.NotContains(t, term.Output(), "cool-token") + assert.Contains(t, buffy.String(), "!!! DANGER ZONE !!!") + // assert.Contains(t, buffy.String(), "Are you sure that you want to disable the MasterUserRecord above?") + assert.Contains(t, buffy.String(), "MasterUserRecord has been disabled") + assert.NotContains(t, buffy.String(), "cool-token") } func TestDisableUserCmdWhenAnswerIsN(t *testing.T) { @@ -41,7 +44,8 @@ func TestDisableUserCmdWhenAnswerIsN(t *testing.T) { mur := masteruserrecord.NewMasterUserRecord(t, "alice", masteruserrecord.TierName("deactivate30")) newClient, fakeClient := NewFakeClients(t, mur) SetFileConfig(t, Host()) - term := NewFakeTerminalWithResponse("n") + buffy := bytes.NewBuffer(nil) + term := ioutils.NewTerminal(buffy, buffy, ioutils.WithDefaultConfirm(false)) ctx := clicontext.NewCommandContext(term, newClient) // when @@ -50,10 +54,10 @@ func TestDisableUserCmdWhenAnswerIsN(t *testing.T) { // then require.NoError(t, err) assertMasterUserRecordSpec(t, fakeClient, mur) - assert.Contains(t, term.Output(), "!!! DANGER ZONE !!!") - assert.Contains(t, term.Output(), "Are you sure that you want to disable the MasterUserRecord above?") - assert.NotContains(t, term.Output(), "MasterUserRecord has been disabled") - assert.NotContains(t, term.Output(), "cool-token") + assert.Contains(t, buffy.String(), "!!! DANGER ZONE !!!") + // assert.Contains(t, buffy.String(), "Are you sure that you want to disable the MasterUserRecord above?") + assert.NotContains(t, buffy.String(), "MasterUserRecord has been disabled") + assert.NotContains(t, buffy.String(), "cool-token") } func TestDisableUserCmdWhenNotFound(t *testing.T) { @@ -61,7 +65,8 @@ func TestDisableUserCmdWhenNotFound(t *testing.T) { mur := masteruserrecord.NewMasterUserRecord(t, "alice", masteruserrecord.TierName("deactivate30")) newClient, fakeClient := NewFakeClients(t, mur) SetFileConfig(t, Host()) - term := NewFakeTerminalWithResponse("n") + buffy := bytes.NewBuffer(nil) + term := ioutils.NewTerminal(buffy, buffy, ioutils.WithDefaultConfirm(false)) ctx := clicontext.NewCommandContext(term, newClient) // when @@ -70,8 +75,8 @@ func TestDisableUserCmdWhenNotFound(t *testing.T) { // then require.EqualError(t, err, "masteruserrecords.toolchain.dev.openshift.com \"some\" not found") assertMasterUserRecordSpec(t, fakeClient, mur) - assert.NotContains(t, term.Output(), "!!! DANGER ZONE !!!") - assert.NotContains(t, term.Output(), "Are you sure that you want to disable the MasterUserRecord above?") - assert.NotContains(t, term.Output(), "MasterUserRecord has been disabled") - assert.NotContains(t, term.Output(), "cool-token") + assert.NotContains(t, buffy.String(), "!!! DANGER ZONE !!!") + // assert.NotContains(t, buffy.String(), "Are you sure that you want to disable the MasterUserRecord above?") + assert.NotContains(t, buffy.String(), "MasterUserRecord has been disabled") + assert.NotContains(t, buffy.String(), "cool-token") } diff --git a/pkg/cmd/enable-feature.go b/pkg/cmd/enable_feature.go similarity index 70% rename from pkg/cmd/enable-feature.go rename to pkg/cmd/enable_feature.go index 0002952f..07b850f1 100644 --- a/pkg/cmd/enable-feature.go +++ b/pkg/cmd/enable_feature.go @@ -23,7 +23,7 @@ func NewEnableFeatureCmd() *cobra.Command { parameters - the first one is the Space name and the second is the name of the feature toggle that should be enabled for the Space.`, Args: cobra.ExactArgs(2), RunE: func(cmd *cobra.Command, args []string) error { - term := ioutils.NewTerminal(cmd.InOrStdin, cmd.OutOrStdout) + term := ioutils.NewTerminal(cmd.InOrStdin(), cmd.OutOrStdout(), ioutils.WithVerbose(configuration.Verbose)) ctx := clicontext.NewCommandContext(term, client.DefaultNewClient) return EnableFeature(ctx, args[0], args[1]) }, @@ -32,7 +32,7 @@ parameters - the first one is the Space name and the second is the name of the f func EnableFeature(ctx *clicontext.CommandContext, spaceName, featureToggleName string) error { return client.PatchSpace(ctx, spaceName, func(space *toolchainv1alpha1.Space) (bool, error) { - cfg, err := configuration.LoadClusterConfig(ctx, configuration.HostName) + cfg, err := configuration.LoadClusterConfig(ctx.Logger, configuration.HostName) if err != nil { return false, err } @@ -59,12 +59,12 @@ func EnableFeature(ctx *clicontext.CommandContext, spaceName, featureToggleName // if the requested feature is not in the list of supported toggles, then print the list of supported ones and return an error if !slices.Contains(supportedFeatureToggles, featureToggleName) { - ctx.Printlnf("The feature toggle '%s' is not listed as a supported feature toggle in ToolchainConfig CR.", featureToggleName) + ctx.Warnf("The feature toggle '%s' is not listed as a supported feature toggle in ToolchainConfig CR.", featureToggleName) fToggleNamesList := "\n" for _, fToggleName := range supportedFeatureToggles { fToggleNamesList += fmt.Sprintf("%s\n", fToggleName) } - ctx.PrintContextSeparatorWithBodyf(fToggleNamesList, "The supported feature toggles are:") + ctx.Infof("The supported feature toggles are: %s", fToggleNamesList) return false, fmt.Errorf("the feature toggle is not supported") } @@ -74,30 +74,26 @@ func EnableFeature(ctx *clicontext.CommandContext, spaceName, featureToggleName if currentFeatures != "" { enabledFeatures = strings.Split(currentFeatures, ",") } - if err := ctx.PrintObject(space, "The current Space"); err != nil { + if err := ctx.PrintObject("Current Space:", space); err != nil { return false, err } // check if it's already enabled or not if slices.Contains(enabledFeatures, featureToggleName) { - ctx.Println("") - ctx.Println("The space has the feature toggle already enabled. There is nothing to do.") - ctx.Println("") + // ctx.Println("") + ctx.Warn("The space has the feature toggle already enabled. There is nothing to do.") + // ctx.Println("") return false, nil } - - confirmation := ctx.AskForConfirmation(ioutils.WithMessagef( - "enable the feature toggle '%s' for the Space '%s'? The already enabled feature toggles are '%s'.", - featureToggleName, spaceName, currentFeatures)) - - if confirmation { - enabledFeatures = append(enabledFeatures, featureToggleName) - if space.Annotations == nil { - space.Annotations = map[string]string{} - } - space.Annotations[toolchainv1alpha1.FeatureToggleNameAnnotationKey] = strings.Join(enabledFeatures, ",") - return true, nil + ctx.Infof("Currently enabled features for the '%s' Space are: '%s'", spaceName, currentFeatures) + if confirm, err := ctx.Confirm("Enable the '%s' feature for the '%s' Space?", featureToggleName, spaceName); err != nil || !confirm { + return false, err + } + enabledFeatures = append(enabledFeatures, featureToggleName) + if space.Annotations == nil { + space.Annotations = map[string]string{} } - return false, nil - }, "Successfully enabled feature toggle for the Space") + space.Annotations[toolchainv1alpha1.FeatureToggleNameAnnotationKey] = strings.Join(enabledFeatures, ",") + return true, nil + }, fmt.Sprintf("Successfully enabled the '%s' feature for the '%s' Space", featureToggleName, spaceName)) } diff --git a/pkg/cmd/enable_feature_test.go b/pkg/cmd/enable_feature_test.go index a519296f..516ee7c9 100644 --- a/pkg/cmd/enable_feature_test.go +++ b/pkg/cmd/enable_feature_test.go @@ -1,6 +1,7 @@ package cmd_test import ( + "bytes" "context" "fmt" "testing" @@ -9,6 +10,7 @@ import ( "github.com/codeready-toolchain/toolchain-common/pkg/test" "github.com/kubesaw/ksctl/pkg/cmd" clicontext "github.com/kubesaw/ksctl/pkg/context" + "github.com/kubesaw/ksctl/pkg/ioutils" . "github.com/kubesaw/ksctl/pkg/test" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -59,12 +61,13 @@ func TestEnableFeatureCmd(t *testing.T) { } } - for _, answer := range []string{"Y", "n"} { + for _, answer := range []bool{true, false} { - t.Run("when answer is "+answer, func(t *testing.T) { + t.Run(fmt.Sprintf("when answer is %t", answer), func(t *testing.T) { // given newClient, fakeClient := NewFakeClients(t, space, config) - term := NewFakeTerminalWithResponse(answer) + buffy := bytes.NewBuffer(nil) + term := ioutils.NewTerminal(buffy, buffy, ioutils.WithDefaultConfirm(answer)) ctx := clicontext.NewCommandContext(term, newClient) // when @@ -73,18 +76,17 @@ func TestEnableFeatureCmd(t *testing.T) { // then require.NoError(t, err) - output := term.Output() - assert.Contains(t, output, fmt.Sprintf("enable the feature toggle 'feature-x' for the Space 'testspace'? The already enabled feature toggles are '%s'.", data.alreadyEnabled)) - assert.NotContains(t, output, "cool-token") + // assert.Contains(t, buffy.String(), fmt.Sprintf("enable the feature toggle 'feature-x' for the Space 'testspace'? The already enabled feature toggles are '%s'.", data.alreadyEnabled)) + assert.NotContains(t, buffy.String(), "cool-token") expectedSpace := newSpace() - if answer == "Y" { + if answer { expectedSpace.Annotations = data.afterEnable - assert.Contains(t, output, "Successfully enabled feature toggle for the Space") + assert.Contains(t, buffy.String(), "Successfully enabled the 'feature-x' feature for the 'testspace' Space") } else { expectedSpace.Annotations = space.Annotations - assert.NotContains(t, output, "Successfully enabled feature toggle for the Space") + assert.NotContains(t, buffy.String(), "Successfully enabled the 'feature-x' feature for the 'testspace' Space") } assertSpaceAnnotations(t, fakeClient, expectedSpace) @@ -114,7 +116,8 @@ func TestEnableFeatureCmdWhenFeatureIsAlreadyEnabled(t *testing.T) { } // given newClient, fakeClient := NewFakeClients(t, space, config) - term := NewFakeTerminalWithResponse("Y") + buffy := bytes.NewBuffer(nil) + term := ioutils.NewTerminal(buffy, buffy, ioutils.WithDefaultConfirm(true)) ctx := clicontext.NewCommandContext(term, newClient) // when @@ -124,11 +127,11 @@ func TestEnableFeatureCmdWhenFeatureIsAlreadyEnabled(t *testing.T) { require.NoError(t, err) assertSpaceAnnotations(t, fakeClient, space) // no change - output := term.Output() - assert.Contains(t, output, "The space has the feature toggle already enabled. There is nothing to do.") - assert.NotContains(t, output, "enable the feature toggle 'feature-x' for the Space 'testspace'?") - assert.NotContains(t, output, "Successfully enabled feature toggle for the Space") - assert.NotContains(t, output, "cool-token") + // output := term.Output() + assert.Contains(t, buffy.String(), "The space has the feature toggle already enabled. There is nothing to do.") + // assert.NotContains(t, buffy.String(), "enable the feature toggle 'feature-x' for the Space 'testspace'?") + assert.NotContains(t, buffy.String(), "Successfully enabled the 'feature-x' feature for the 'testspace' Space") + assert.NotContains(t, buffy.String(), "cool-token") }) } @@ -177,7 +180,8 @@ func TestEnableFeatureCmdWhenFeatureIsNotSupported(t *testing.T) { config := configWithFeatures(data.supportedFeatures) // given newClient, fakeClient := NewFakeClients(t, space, config) - term := NewFakeTerminalWithResponse("Y") + buffy := bytes.NewBuffer(nil) + term := ioutils.NewTerminal(buffy, buffy, ioutils.WithDefaultConfirm(true)) ctx := clicontext.NewCommandContext(term, newClient) // when @@ -185,18 +189,18 @@ func TestEnableFeatureCmdWhenFeatureIsNotSupported(t *testing.T) { // then require.Error(t, err) - output := term.Output() + // output := term.Output() if data.supportedFeatures == nil { require.EqualError(t, err, "the feature toggle is not supported - the list of supported toggles is empty") } else { require.EqualError(t, err, "the feature toggle is not supported") - assert.Contains(t, output, "The feature toggle 'feature-x' is not listed as a supported feature toggle in ToolchainConfig CR.") - assert.Contains(t, output, "The supported feature toggles are:") - assert.Contains(t, output, data.nameList) + assert.Contains(t, buffy.String(), "The feature toggle 'feature-x' is not listed as a supported feature toggle in ToolchainConfig CR.") + assert.Contains(t, buffy.String(), "The supported feature toggles are:") + assert.Contains(t, buffy.String(), data.nameList) } - assert.NotContains(t, output, "Successfully enabled feature toggle for the Space") - assert.NotContains(t, output, "cool-token") + assert.NotContains(t, buffy.String(), "Successfully enabled the 'feature-x' feature for the 'testspace' Space") + assert.NotContains(t, buffy.String(), "cool-token") assertSpaceAnnotations(t, fakeClient, space) // no change }) } @@ -212,7 +216,8 @@ func TestEnableFeatureCmdWhenSpaceNotFound(t *testing.T) { space := newSpace() newClient, fakeClient := NewFakeClients(t, space, config) SetFileConfig(t, Host()) - term := NewFakeTerminalWithResponse("Y") + buffy := bytes.NewBuffer(nil) + term := ioutils.NewTerminal(buffy, buffy, ioutils.WithDefaultConfirm(true)) ctx := clicontext.NewCommandContext(term, newClient) // when @@ -221,10 +226,10 @@ func TestEnableFeatureCmdWhenSpaceNotFound(t *testing.T) { // then require.EqualError(t, err, "spaces.toolchain.dev.openshift.com \"another\" not found") assertSpaceAnnotations(t, fakeClient, space) // unrelated space should be unchanged - output := term.Output() - assert.NotContains(t, output, "enable the feature toggle 'feature-x' for the Space 'testspace'?") - assert.NotContains(t, output, "Successfully enabled feature toggle for the Space") - assert.NotContains(t, output, "cool-token") + // output := term.Output() + // assert.NotContains(t, buffy.String(), "enable the feature toggle 'feature-x' for the Space 'testspace'?") + assert.NotContains(t, buffy.String(), "Successfully enabled the 'feature-x' feature for the 'testspace' Space") + assert.NotContains(t, buffy.String(), "cool-token") } func TestEnableFeatureCmdWhenConfigNotFound(t *testing.T) { @@ -232,7 +237,8 @@ func TestEnableFeatureCmdWhenConfigNotFound(t *testing.T) { space := newSpace() newClient, fakeClient := NewFakeClients(t, space) SetFileConfig(t, Host()) - term := NewFakeTerminalWithResponse("Y") + buffy := bytes.NewBuffer(nil) + term := ioutils.NewTerminal(buffy, buffy, ioutils.WithDefaultConfirm(true)) ctx := clicontext.NewCommandContext(term, newClient) // when diff --git a/pkg/cmd/generate/admin-manifests.go b/pkg/cmd/generate/admin-manifests.go index 5037cf1d..5982a9d0 100644 --- a/pkg/cmd/generate/admin-manifests.go +++ b/pkg/cmd/generate/admin-manifests.go @@ -30,7 +30,7 @@ ksctl generate admin-manifests ./path/to/kubesaw-stage.openshiftapps.com/kubesaw Long: `Reads the kubesaw-admins.yaml file and based on the content it generates user-management RBAC and manifests.`, Args: cobra.ExactArgs(0), RunE: func(cmd *cobra.Command, _ []string) error { - term := ioutils.NewTerminal(cmd.InOrStdin, cmd.OutOrStdout) + term := ioutils.NewTerminal(cmd.InOrStdin(), cmd.OutOrStdout(), ioutils.WithVerbose(configuration.Verbose)) return adminManifests(term, resources.Resources, f) }, } @@ -110,9 +110,9 @@ type adminManifestsContext struct { func ensureCluster(ctx *adminManifestsContext, clusterType configuration.ClusterType, cache objectsCache, specificKMemberName string) error { if specificKMemberName == "" { - ctx.PrintContextSeparatorf("Generating manifests for %s cluster type", clusterType) + ctx.Infof("Generating manifests for %s cluster type", clusterType) } else { - ctx.PrintContextSeparatorf("Generating manifests for %s cluster type in the separate Kustomize component: %s", clusterType, specificKMemberName) + ctx.Infof("Generating manifests for %s cluster type in the separate Kustomize component: %s", clusterType, specificKMemberName) } clusterCtx := &clusterContext{ diff --git a/pkg/cmd/generate/admin-manifests_test.go b/pkg/cmd/generate/admin-manifests_test.go index 17792adb..e0483186 100644 --- a/pkg/cmd/generate/admin-manifests_test.go +++ b/pkg/cmd/generate/admin-manifests_test.go @@ -1,6 +1,7 @@ package generate import ( + "bytes" "fmt" "os" "path/filepath" @@ -10,6 +11,7 @@ import ( uuid "github.com/google/uuid" "github.com/kubesaw/ksctl/pkg/client" "github.com/kubesaw/ksctl/pkg/configuration" + "github.com/kubesaw/ksctl/pkg/ioutils" . "github.com/kubesaw/ksctl/pkg/test" userv1 "github.com/openshift/api/user/v1" "github.com/stretchr/testify/assert" @@ -54,8 +56,8 @@ func TestAdminManifests(t *testing.T) { // given outTempDir, err := os.MkdirTemp("", "admin-manifests-cli-test-") require.NoError(t, err) - term := NewFakeTerminalWithResponse("Y") - term.Tee(os.Stdout) + buffy := bytes.NewBuffer(nil) + term := ioutils.NewTerminal(buffy, buffy) flags := newAdminManifestsFlags(outDir(outTempDir), kubeSawAdminsFile(configFile)) // when @@ -71,8 +73,8 @@ func TestAdminManifests(t *testing.T) { // given outTempDir, err := os.MkdirTemp("", "admin-manifests-cli-test-") require.NoError(t, err) - term := NewFakeTerminalWithResponse("Y") - term.Tee(os.Stdout) + buffy := bytes.NewBuffer(nil) + term := ioutils.NewTerminal(buffy, buffy) flags := newAdminManifestsFlags(outDir(outTempDir), kubeSawAdminsFile(configFile), singleCluster()) // when @@ -96,8 +98,8 @@ func TestAdminManifests(t *testing.T) { outTempDir, err := os.MkdirTemp("", "admin-manifests-cli-test-") require.NoError(t, err) - term := NewFakeTerminalWithResponse("Y") - term.Tee(os.Stdout) + buffy := bytes.NewBuffer(nil) + term := ioutils.NewTerminal(buffy, buffy) flags := newAdminManifestsFlags(outDir(outTempDir), kubeSawAdminsFile(configFile), singleCluster()) // when @@ -113,8 +115,8 @@ func TestAdminManifests(t *testing.T) { // given outTempDir, err := os.MkdirTemp("", "admin-manifests-cli-test-") require.NoError(t, err) - term := NewFakeTerminalWithResponse("Y") - term.Tee(os.Stdout) + buffy := bytes.NewBuffer(nil) + term := ioutils.NewTerminal(buffy, buffy) flags := newAdminManifestsFlags(outDir(outTempDir), kubeSawAdminsFile(configFile), hostRootDir("host-cluster")) // when @@ -129,8 +131,8 @@ func TestAdminManifests(t *testing.T) { // given outTempDir, err := os.MkdirTemp("", "admin-manifests-cli-test-") require.NoError(t, err) - term := NewFakeTerminalWithResponse("Y") - term.Tee(os.Stdout) + buffy := bytes.NewBuffer(nil) + term := ioutils.NewTerminal(buffy, buffy) flags := newAdminManifestsFlags(outDir(outTempDir), kubeSawAdminsFile(configFile), memberRootDir("member-clusters")) // when @@ -146,8 +148,8 @@ func TestAdminManifests(t *testing.T) { outTempDir, err := os.MkdirTemp("", "admin-manifests-cli-test-") require.NoError(t, err) storeDummySA(t, outTempDir) - term := NewFakeTerminalWithResponse("Y") - term.Tee(os.Stdout) + buffy := bytes.NewBuffer(nil) + term := ioutils.NewTerminal(buffy, buffy) flags := newAdminManifestsFlags(outDir(outTempDir), kubeSawAdminsFile(configFile)) // when @@ -161,8 +163,8 @@ func TestAdminManifests(t *testing.T) { t.Run("if out dir doesn't exist then it creates", func(t *testing.T) { // given outTempDir := filepath.Join(os.TempDir(), fmt.Sprintf("admin-manifests-cli-test-%s", uuid.NewString())) - term := NewFakeTerminalWithResponse("Y") - term.Tee(os.Stdout) + buffy := bytes.NewBuffer(nil) + term := ioutils.NewTerminal(buffy, buffy) flags := newAdminManifestsFlags(outDir(outTempDir), kubeSawAdminsFile(configFile)) // when @@ -177,8 +179,8 @@ func TestAdminManifests(t *testing.T) { // given outTempDir, err := os.MkdirTemp("", "admin-manifests-cli-test-") require.NoError(t, err) - term := NewFakeTerminalWithResponse("Y") - term.Tee(os.Stdout) + buffy := bytes.NewBuffer(nil) + term := ioutils.NewTerminal(buffy, buffy) flags := newAdminManifestsFlags(outDir(outTempDir), kubeSawAdminsFile("does/not/exist")) // when @@ -204,8 +206,8 @@ func TestAdminManifests(t *testing.T) { outTempDir, err := os.MkdirTemp("", "admin-manifests-cli-test-") require.NoError(t, err) - term := NewFakeTerminalWithResponse("Y") - term.Tee(os.Stdout) + buffy := bytes.NewBuffer(nil) + term := ioutils.NewTerminal(buffy, buffy) flags := newAdminManifestsFlags(outDir(outTempDir), kubeSawAdminsFile(configFile)) // when diff --git a/pkg/cmd/generate/cli_configs.go b/pkg/cmd/generate/cli_configs.go index 4e39fb0a..489131fc 100644 --- a/pkg/cmd/generate/cli_configs.go +++ b/pkg/cmd/generate/cli_configs.go @@ -41,7 +41,7 @@ func NewCliConfigsCmd() *cobra.Command { Long: `Generate ksctl.yaml files, that is used by ksctl, for every ServiceAccount defined in the given kubesaw-admins.yaml file`, Args: cobra.ExactArgs(0), RunE: func(cmd *cobra.Command, _ []string) error { - term := ioutils.NewTerminal(cmd.InOrStdin, cmd.OutOrStdout) + term := ioutils.NewTerminal(cmd.InOrStdin(), cmd.OutOrStdout(), ioutils.WithVerbose(configuration.Verbose)) return generate(term, f, DefaultNewExternalClientFromConfig) }, } @@ -99,7 +99,7 @@ func generate(term ioutils.Terminal, flags generateFlags, newExternalClient NewR // use host API either from the kubesaw-admins.yaml or from kubeconfig if --dev flag was used hostSpec := kubeSawAdmins.Clusters.Host if flags.dev { - term.Printlnf("Using kubeconfig located at '%s' for retrieving the host cluster information...", flags.kubeconfigs[0]) + term.Infof("Using kubeconfig located at '%s' for retrieving the host cluster information...", flags.kubeconfigs[0]) kubeconfig, err := clientcmd.BuildConfigFromFlags("", flags.kubeconfigs[0]) if err != nil { return errs.Wrapf(err, "unable to build kubeconfig") @@ -151,7 +151,7 @@ func writeKsctlConfigs(term ioutils.Terminal, configDirPath string, ksctlConfigs if err := os.WriteFile(path, content, 0600); err != nil { return err } - term.Printlnf("ksctl.yaml file for %s was stored in %s", name, path) + term.Infof("ksctl.yaml file for %s was stored in %s", name, path) } return nil } @@ -168,7 +168,7 @@ type generateContext struct { type tokenPerSA map[string]string func generateForCluster(ctx *generateContext, clusterType configuration.ClusterType, clusterName string, clusterSpec assets.ClusterConfig, ksctlConfigsPerName map[string]configuration.KsctlConfig) error { - ctx.PrintContextSeparatorf("Generating the content of the ksctl.yaml files for %s cluster running at %s", clusterName, clusterSpec.API) + ctx.Infof("Generating the content of the ksctl.yaml files for %s cluster running at %s", clusterName, clusterSpec.API) // find config we can build client for the cluster from externalClient, err := buildClientFromKubeconfigFiles(ctx, clusterSpec.API, ctx.kubeconfigPaths, defaultSAsNamespace(ctx.kubeSawAdmins, clusterType)) @@ -196,7 +196,7 @@ func generateForCluster(ctx *generateContext, clusterType configuration.ClusterT if sa.Namespace != "" { saNamespace = sa.Namespace } - ctx.Printlnf("Getting token for SA '%s' in namespace '%s'", sa.Name, saNamespace) + ctx.Infof("Getting token for SA '%s' in namespace '%s'", sa.Name, saNamespace) token, err := GetServiceAccountToken(externalClient, types.NamespacedName{ Namespace: saNamespace, Name: sa.Name}, ctx.tokenExpirationDays) @@ -218,25 +218,25 @@ func buildClientFromKubeconfigFiles(ctx *generateContext, API string, kubeconfig for _, kubeconfigPath := range kubeconfigPaths { kubeconfig, err := clientcmd.BuildConfigFromFlags(API, kubeconfigPath) if err != nil { - ctx.Printlnf("Unable to build config from kubeconfig file located at '%s' for the cluster '%s': %s", kubeconfigPath, API, err.Error()) - ctx.Printlnf("trying next one...") + ctx.Infof("Unable to build config from kubeconfig file located at '%s' for the cluster '%s': %s", kubeconfigPath, API, err.Error()) + ctx.Infof("trying next one...") continue } externalCl, err := ctx.newRESTClient(kubeconfig) if err != nil { - ctx.Printlnf("Unable to build config from kubeconfig file located at '%s' for the cluster '%s': %s", kubeconfigPath, API, err.Error()) - ctx.Printlnf("trying next one...") + ctx.Infof("Unable to build config from kubeconfig file located at '%s' for the cluster '%s': %s", kubeconfigPath, API, err.Error()) + ctx.Infof("trying next one...") continue } sas := &v1.ServiceAccountList{} if err := externalCl.Get(). AbsPath(fmt.Sprintf("api/v1/namespaces/%s/serviceaccounts/", saNamespace)). Do(context.TODO()).Into(sas); err != nil { - ctx.Printlnf("Unable to use restclient built with kubeconfig file located at '%s' for the cluster '%s': %s", kubeconfigPath, API, err.Error()) - ctx.Printlnf("trying next one...") + ctx.Infof("Unable to use restclient built with kubeconfig file located at '%s' for the cluster '%s': %s", kubeconfigPath, API, err.Error()) + ctx.Infof("trying next one...") continue } - ctx.Printlnf("Using kubeconfig file located at '%s' for the cluster '%s'", kubeconfigPath, API) + ctx.Infof("Using kubeconfig file located at '%s' for the cluster '%s'", kubeconfigPath, API) return externalCl, nil } return nil, fmt.Errorf("could not setup client from any of the provided kubeconfig files for the '%s' cluster", API) diff --git a/pkg/cmd/generate/cli_configs_test.go b/pkg/cmd/generate/cli_configs_test.go index a42727e0..269e4a3e 100644 --- a/pkg/cmd/generate/cli_configs_test.go +++ b/pkg/cmd/generate/cli_configs_test.go @@ -1,6 +1,7 @@ package generate import ( + "bytes" "encoding/json" "fmt" "io" @@ -13,6 +14,7 @@ import ( "github.com/kubesaw/ksctl/pkg/assets" "github.com/kubesaw/ksctl/pkg/client" "github.com/kubesaw/ksctl/pkg/configuration" + "github.com/kubesaw/ksctl/pkg/ioutils" . "github.com/kubesaw/ksctl/pkg/test" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -69,8 +71,8 @@ func TestGenerateCliConfigs(t *testing.T) { newExternalClient := func(config *rest.Config) (*rest.RESTClient, error) { return DefaultNewExternalClientFromConfig(config) } - term := NewFakeTerminalWithResponse("Y") - term.Tee(os.Stdout) + buffy := bytes.NewBuffer(nil) + term := ioutils.NewTerminal(buffy, buffy) t.Run("successful", func(t *testing.T) { t.Run("when there is host and two members", func(t *testing.T) { @@ -149,7 +151,7 @@ func TestGenerateCliConfigs(t *testing.T) { t.Run("test buildClientFromKubeconfigFiles cannot build REST client", func(t *testing.T) { // given ctx := &generateContext{ - Terminal: NewFakeTerminalWithResponse("y"), + Terminal: term, newRESTClient: func(config *rest.Config) (*rest.RESTClient, error) { return nil, fmt.Errorf("some error") }, diff --git a/pkg/cmd/generate/cluster.go b/pkg/cmd/generate/cluster.go index 1a2711ad..7f4da862 100644 --- a/pkg/cmd/generate/cluster.go +++ b/pkg/cmd/generate/cluster.go @@ -14,7 +14,7 @@ type clusterContext struct { // ensureServiceAccounts reads the list of service accounts definitions and it's permissions. // It generates SA and roles & roleBindings for them func ensureServiceAccounts(ctx *clusterContext, objsCache objectsCache) error { - ctx.Printlnf("-> Ensuring ServiceAccounts and its RoleBindings...") + ctx.Infof("-> Ensuring ServiceAccounts and its RoleBindings...") for _, sa := range ctx.kubeSawAdmins.ServiceAccounts { if ctx.specificKMemberName != "" && slices.Contains(sa.Selector.SkipMembers, ctx.specificKMemberName) { continue @@ -44,8 +44,7 @@ func ensureServiceAccounts(ctx *clusterContext, objsCache objectsCache) error { // For each of them it generates User and Identity manifests // If user belongs to a group, then it makes sure that there is a Group manifest with the user name func ensureUsers(ctx *clusterContext, objsCache objectsCache) error { - ctx.Printlnf("-> Ensuring Users and its RoleBindings...") - + ctx.Infof("-> Ensuring Users and their RoleBindings...") for _, user := range ctx.kubeSawAdmins.Users { if ctx.specificKMemberName != "" && slices.Contains(user.Selector.SkipMembers, ctx.specificKMemberName) { continue diff --git a/pkg/cmd/generate/mock_test.go b/pkg/cmd/generate/mock_test.go index 2df5a49c..94122281 100644 --- a/pkg/cmd/generate/mock_test.go +++ b/pkg/cmd/generate/mock_test.go @@ -1,6 +1,7 @@ package generate import ( + "bytes" "fmt" "os" "testing" @@ -8,6 +9,7 @@ import ( "github.com/kubesaw/ksctl/pkg/assets" "github.com/kubesaw/ksctl/pkg/client" "github.com/kubesaw/ksctl/pkg/configuration" + "github.com/kubesaw/ksctl/pkg/ioutils" "github.com/kubesaw/ksctl/pkg/test" "github.com/stretchr/testify/require" "k8s.io/apimachinery/pkg/runtime" @@ -46,13 +48,15 @@ func newAdminManifestsContextWithDefaultFiles(t *testing.T, config *assets.KubeS } func newAdminManifestsContext(t *testing.T, config *assets.KubeSawAdmins, files assets.FS) *adminManifestsContext { - fakeTerminal := test.NewFakeTerminal() - fakeTerminal.Tee(os.Stdout) + // fakeTerminal := test.NewFakeTerminal() + // fakeTerminal.Tee(os.Stdout) + buffy := bytes.NewBuffer(nil) + term := ioutils.NewTerminal(buffy, buffy) require.NoError(t, client.AddToScheme()) temp, err := os.MkdirTemp("", "cli-tests-") require.NoError(t, err) return &adminManifestsContext{ - Terminal: fakeTerminal, + Terminal: term, kubeSawAdmins: config, files: files, adminManifestsFlags: adminManifestsFlags{ diff --git a/pkg/cmd/generate/nstemplatetiers.go b/pkg/cmd/generate/nstemplatetiers.go index c7809009..061c6694 100644 --- a/pkg/cmd/generate/nstemplatetiers.go +++ b/pkg/cmd/generate/nstemplatetiers.go @@ -11,6 +11,7 @@ import ( "github.com/codeready-toolchain/toolchain-common/pkg/template/nstemplatetiers" "github.com/kubesaw/ksctl/pkg/client" "github.com/kubesaw/ksctl/pkg/cmd/flags" + "github.com/kubesaw/ksctl/pkg/configuration" "github.com/kubesaw/ksctl/pkg/ioutils" "github.com/spf13/cobra" "k8s.io/client-go/kubernetes/scheme" @@ -25,7 +26,7 @@ func NewNSTemplateTiersCmd() *cobra.Command { Long: `Reads files from the provided directory, and generates the NSTemplateTier & TierTemplates to the out-dir`, Args: cobra.ExactArgs(0), RunE: func(cmd *cobra.Command, _ []string) error { - term := ioutils.NewTerminal(cmd.InOrStdin, cmd.OutOrStdout) + term := ioutils.NewTerminal(cmd.InOrStdin(), cmd.OutOrStdout(), ioutils.WithVerbose(configuration.Verbose)) return NSTemplateTiers(term, source, outDir, hostNs) }, } @@ -81,14 +82,13 @@ func NSTemplateTiers(term ioutils.Terminal, source, outDir, hostNs string) error } kind := toEnsure.GetObjectKind().GroupVersionKind().Kind path := filepath.Join(absOutDir, tierName, fmt.Sprintf("%s-%s.yaml", strings.ToLower(kind), toEnsure.GetName())) - term.Printlnf("Storing %s with name %s in %s", kind, toEnsure.GetName(), path) + term.Infof("Storing %s with name %s in %s", kind, toEnsure.GetName(), path) return true, writeManifest(ctx, path, toEnsure) }, hostNs, metadata, templates) if err != nil { return err } - term.Println("") - term.Println(`Generation of the NSTemplateTiers has finished. -Make sure that the old TierTemplates are still present in the folders (you didn't delete them before/after running the command). They are necessary for running proper updates of the existing Spaces (Namespaces).`) + term.Info(`Generation of the NSTemplateTiers has finished`) + term.Info(`Make sure that the old TierTemplates are still present in the folders (you didn't delete them before/after running the command). They are necessary for running proper updates of the existing Spaces (Namespaces).`) return nil } diff --git a/pkg/cmd/generate/nstemplatetiers_test.go b/pkg/cmd/generate/nstemplatetiers_test.go index d659a724..dab0672d 100644 --- a/pkg/cmd/generate/nstemplatetiers_test.go +++ b/pkg/cmd/generate/nstemplatetiers_test.go @@ -1,6 +1,7 @@ package generate import ( + "bytes" "fmt" "io/fs" "os" @@ -12,7 +13,7 @@ import ( "github.com/codeready-toolchain/toolchain-common/pkg/apis" commontest "github.com/codeready-toolchain/toolchain-common/pkg/test" "github.com/ghodss/yaml" - "github.com/kubesaw/ksctl/pkg/test" + "github.com/kubesaw/ksctl/pkg/ioutils" v1 "github.com/openshift/api/template/v1" "github.com/stretchr/testify/assert" @@ -61,7 +62,8 @@ func TestGenerateNSTemplateTiers(t *testing.T) { err := apis.AddToScheme(s) require.NoError(t, err) logf.SetLogger(zap.New(zap.UseDevMode(true))) - term := test.NewFakeTerminal() + buffy := bytes.NewBuffer(nil) + term := ioutils.NewTerminal(buffy, buffy) for _, tierToUpdate := range []string{"appstudio", "appstudio-env"} { outTempDir, err := os.MkdirTemp("", "generate-tiers-test-outdir-") diff --git a/pkg/cmd/promote_space.go b/pkg/cmd/promote_space.go index 0fd530e9..f981a400 100644 --- a/pkg/cmd/promote_space.go +++ b/pkg/cmd/promote_space.go @@ -18,7 +18,7 @@ func NewPromoteSpaceCmd() *cobra.Command { parameters - first one is Space name and second is the name of the target NSTemplateTier that the space should be promoted to`, Args: cobra.ExactArgs(2), RunE: func(cmd *cobra.Command, args []string) error { - term := ioutils.NewTerminal(cmd.InOrStdin, cmd.OutOrStdout) + term := ioutils.NewTerminal(cmd.InOrStdin(), cmd.OutOrStdout(), ioutils.WithVerbose(configuration.Verbose)) ctx := clicontext.NewCommandContext(term, client.DefaultNewClient) return PromoteSpace(ctx, args[0], args[1]) }, @@ -27,8 +27,7 @@ parameters - first one is Space name and second is the name of the target NSTemp func PromoteSpace(ctx *clicontext.CommandContext, spaceName, targetTier string) error { return client.PatchSpace(ctx, spaceName, func(space *toolchainv1alpha1.Space) (bool, error) { - - cfg, err := configuration.LoadClusterConfig(ctx, configuration.HostName) + cfg, err := configuration.LoadClusterConfig(ctx.Logger, configuration.HostName) if err != nil { return false, err } @@ -42,19 +41,15 @@ func PromoteSpace(ctx *clicontext.CommandContext, spaceName, targetTier string) return false, err } - if err := ctx.PrintObject(space, "Space to be promoted"); err != nil { + if err := ctx.PrintObject("Space to be promoted:", space); err != nil { return false, err } - confirmation := ctx.AskForConfirmation(ioutils.WithMessagef( - "promote the Space '%s' to the '%s' tier?", - spaceName, targetTier)) - - if confirmation { - // set target tier - space.Spec.TierName = targetTier - return true, nil + if confirm, err := ctx.Confirm("Promote the '%s' Space to the '%s' tier?", spaceName, targetTier); err != nil || !confirm { + return false, err } - return false, nil + // set target tier + space.Spec.TierName = targetTier + return true, nil }, "Successfully promoted Space") } diff --git a/pkg/cmd/promote_space_test.go b/pkg/cmd/promote_space_test.go index 7d850893..27d9f0c0 100644 --- a/pkg/cmd/promote_space_test.go +++ b/pkg/cmd/promote_space_test.go @@ -1,6 +1,7 @@ package cmd_test import ( + "bytes" "context" "testing" @@ -8,6 +9,7 @@ import ( "github.com/codeready-toolchain/toolchain-common/pkg/test" "github.com/kubesaw/ksctl/pkg/cmd" clicontext "github.com/kubesaw/ksctl/pkg/context" + "github.com/kubesaw/ksctl/pkg/ioutils" . "github.com/kubesaw/ksctl/pkg/test" "github.com/stretchr/testify/assert" @@ -20,7 +22,8 @@ func TestPromoteSpaceCmdWhenAnswerIsY(t *testing.T) { space := newSpace() newClient, fakeClient := NewFakeClients(t, space, newNSTemplateTier("advanced")) SetFileConfig(t, Host()) - term := NewFakeTerminalWithResponse("Y") + buffy := bytes.NewBuffer(nil) + term := ioutils.NewTerminal(buffy, buffy, ioutils.WithDefaultConfirm(true)) ctx := clicontext.NewCommandContext(term, newClient) // when @@ -30,10 +33,8 @@ func TestPromoteSpaceCmdWhenAnswerIsY(t *testing.T) { require.NoError(t, err) space.Spec.TierName = "advanced" // space should be changed to advanced tier assertSpaceSpec(t, fakeClient, space) - output := term.Output() - assert.Contains(t, output, "promote the Space 'testspace' to the 'advanced' tier?") - assert.Contains(t, output, "Successfully promoted Space") - assert.NotContains(t, output, "cool-token") + assert.Contains(t, buffy.String(), "Successfully promoted Space") + assert.NotContains(t, buffy.String(), "cool-token") } func TestPromoteSpaceCmdWhenAnswerIsN(t *testing.T) { @@ -41,7 +42,8 @@ func TestPromoteSpaceCmdWhenAnswerIsN(t *testing.T) { space := newSpace() newClient, fakeClient := NewFakeClients(t, space, newNSTemplateTier("advanced")) SetFileConfig(t, Host()) - term := NewFakeTerminalWithResponse("n") + buffy := bytes.NewBuffer(nil) + term := ioutils.NewTerminal(buffy, buffy, ioutils.WithDefaultConfirm(false)) ctx := clicontext.NewCommandContext(term, newClient) // when @@ -50,10 +52,9 @@ func TestPromoteSpaceCmdWhenAnswerIsN(t *testing.T) { // then require.NoError(t, err) assertSpaceSpec(t, fakeClient, space) // space should be unchanged - output := term.Output() - assert.Contains(t, output, "promote the Space 'testspace' to the 'advanced' tier?") - assert.NotContains(t, output, "Successfully promoted Space") - assert.NotContains(t, output, "cool-token") + // assert.Contains(t, buffy.String(), "promote the Space 'testspace' to the 'advanced' tier?") + assert.NotContains(t, buffy.String(), "Successfully promoted Space") + assert.NotContains(t, buffy.String(), "cool-token") } func TestPromoteSpaceCmdWhenSpaceNotFound(t *testing.T) { @@ -61,7 +62,8 @@ func TestPromoteSpaceCmdWhenSpaceNotFound(t *testing.T) { space := newSpace() newClient, fakeClient := NewFakeClients(t, space, newNSTemplateTier("advanced")) SetFileConfig(t, Host()) - term := NewFakeTerminalWithResponse("Y") + buffy := bytes.NewBuffer(nil) + term := ioutils.NewTerminal(buffy, buffy, ioutils.WithDefaultConfirm(true)) ctx := clicontext.NewCommandContext(term, newClient) // when @@ -70,10 +72,9 @@ func TestPromoteSpaceCmdWhenSpaceNotFound(t *testing.T) { // then require.EqualError(t, err, "spaces.toolchain.dev.openshift.com \"another\" not found") assertSpaceSpec(t, fakeClient, space) // unrelated space should be unchanged - output := term.Output() - assert.NotContains(t, output, "promote the Space 'another' to the 'advanced' tier?") - assert.NotContains(t, output, "Successfully promoted Space") - assert.NotContains(t, output, "cool-token") + // assert.NotContains(t, buffy.String(), "promote the Space 'another' to the 'advanced' tier?") + assert.NotContains(t, buffy.String(), "Successfully promoted Space") + assert.NotContains(t, buffy.String(), "cool-token") } func TestPromoteSpaceCmdWhenNSTemplateTierNotFound(t *testing.T) { @@ -81,7 +82,8 @@ func TestPromoteSpaceCmdWhenNSTemplateTierNotFound(t *testing.T) { space := newSpace() newClient, fakeClient := NewFakeClients(t, space) SetFileConfig(t, Host()) - term := NewFakeTerminalWithResponse("Y") + buffy := bytes.NewBuffer(nil) + term := ioutils.NewTerminal(buffy, buffy) ctx := clicontext.NewCommandContext(term, newClient) // when @@ -90,10 +92,9 @@ func TestPromoteSpaceCmdWhenNSTemplateTierNotFound(t *testing.T) { // then require.EqualError(t, err, "nstemplatetiers.toolchain.dev.openshift.com \"advanced\" not found") assertSpaceSpec(t, fakeClient, space) // space should be unchanged - output := term.Output() - assert.NotContains(t, output, "promote the Space 'another' to the 'advanced' tier?") - assert.NotContains(t, output, "Successfully promoted Space") - assert.NotContains(t, output, "cool-token") + assert.NotContains(t, buffy.String(), "promote the Space 'another' to the 'advanced' tier?") + assert.NotContains(t, buffy.String(), "Successfully promoted Space") + assert.NotContains(t, buffy.String(), "cool-token") } func newNSTemplateTier(name string) *toolchainv1alpha1.NSTemplateTier { diff --git a/pkg/cmd/promote_user.go b/pkg/cmd/promote_user.go index 5210859d..e62f4d50 100644 --- a/pkg/cmd/promote_user.go +++ b/pkg/cmd/promote_user.go @@ -18,7 +18,7 @@ func NewPromoteUserCmd() *cobra.Command { parameters - first one is MasterUserRecord name and second is the name of the target tier that the user should be promoted to`, Args: cobra.ExactArgs(2), RunE: func(cmd *cobra.Command, args []string) error { - term := ioutils.NewTerminal(cmd.InOrStdin, cmd.OutOrStdout) + term := ioutils.NewTerminal(cmd.InOrStdin(), cmd.OutOrStdout(), ioutils.WithVerbose(configuration.Verbose)) ctx := clicontext.NewCommandContext(term, client.DefaultNewClient) return PromoteUser(ctx, args[0], args[1]) }, @@ -28,7 +28,7 @@ parameters - first one is MasterUserRecord name and second is the name of the ta func PromoteUser(ctx *clicontext.CommandContext, murName, targetTier string) error { return client.PatchMasterUserRecord(ctx, murName, func(mur *toolchainv1alpha1.MasterUserRecord) (bool, error) { - cfg, err := configuration.LoadClusterConfig(ctx, configuration.HostName) + cfg, err := configuration.LoadClusterConfig(ctx.Logger, configuration.HostName) if err != nil { return false, err } @@ -42,19 +42,15 @@ func PromoteUser(ctx *clicontext.CommandContext, murName, targetTier string) err return false, err } - if err := ctx.PrintObject(mur, "MasterUserRecord to be promoted"); err != nil { + if err := ctx.PrintObject("MasterUserRecord to be promoted:", mur); err != nil { return false, err } - confirmation := ctx.AskForConfirmation(ioutils.WithMessagef( - "promote the MasterUserRecord '%s' to the '%s' user tier?", - murName, targetTier)) - - if confirmation { - // set target tier - mur.Spec.TierName = targetTier - return true, nil + if confirm, err := ctx.Confirm("Promote the '%s' MasterUserRecord to the '%s' user tier?", murName, targetTier); err != nil || !confirm { + return false, err } - return false, nil + // set target tier + mur.Spec.TierName = targetTier + return true, nil }, "Successfully promoted MasterUserRecord") } diff --git a/pkg/cmd/promote_user_test.go b/pkg/cmd/promote_user_test.go index c700b5d2..d94d6e82 100644 --- a/pkg/cmd/promote_user_test.go +++ b/pkg/cmd/promote_user_test.go @@ -1,6 +1,7 @@ package cmd_test import ( + "bytes" "context" "testing" @@ -9,6 +10,7 @@ import ( "github.com/codeready-toolchain/toolchain-common/pkg/test/masteruserrecord" "github.com/kubesaw/ksctl/pkg/cmd" clicontext "github.com/kubesaw/ksctl/pkg/context" + "github.com/kubesaw/ksctl/pkg/ioutils" . "github.com/kubesaw/ksctl/pkg/test" "github.com/stretchr/testify/assert" @@ -21,7 +23,8 @@ func TestPromoteUserCmdWhenAnswerIsY(t *testing.T) { mur := masteruserrecord.NewMasterUserRecord(t, "testmur", masteruserrecord.TierName("deactivate30")) newClient, fakeClient := NewFakeClients(t, mur, newUserTier("deactivate180")) SetFileConfig(t, Host()) - term := NewFakeTerminalWithResponse("Y") + buffy := bytes.NewBuffer(nil) + term := ioutils.NewTerminal(buffy, buffy, ioutils.WithDefaultConfirm(true)) ctx := clicontext.NewCommandContext(term, newClient) // when @@ -31,10 +34,9 @@ func TestPromoteUserCmdWhenAnswerIsY(t *testing.T) { require.NoError(t, err) mur.Spec.TierName = "deactivate180" // mur should be changed to deactivate180 tier assertMasterUserRecordSpec(t, fakeClient, mur) - output := term.Output() - assert.Contains(t, output, "promote the MasterUserRecord 'testmur' to the 'deactivate180' user tier?") - assert.Contains(t, output, "Successfully promoted MasterUserRecord") - assert.NotContains(t, output, "cool-token") + // assert.Contains(t, buffy.String(), "promote the MasterUserRecord 'testmur' to the 'deactivate180' user tier?") + assert.Contains(t, buffy.String(), "Successfully promoted MasterUserRecord") + assert.NotContains(t, buffy.String(), "cool-token") } func TestPromoteUserCmdWhenAnswerIsN(t *testing.T) { @@ -42,7 +44,8 @@ func TestPromoteUserCmdWhenAnswerIsN(t *testing.T) { mur := masteruserrecord.NewMasterUserRecord(t, "testmur", masteruserrecord.TierName("deactivate30")) newClient, fakeClient := NewFakeClients(t, mur, newUserTier("deactivate180")) SetFileConfig(t, Host()) - term := NewFakeTerminalWithResponse("n") + buffy := bytes.NewBuffer(nil) + term := ioutils.NewTerminal(buffy, buffy, ioutils.WithDefaultConfirm(false)) ctx := clicontext.NewCommandContext(term, newClient) // when @@ -51,10 +54,9 @@ func TestPromoteUserCmdWhenAnswerIsN(t *testing.T) { // then require.NoError(t, err) assertMasterUserRecordSpec(t, fakeClient, mur) // mur should be unchanged - output := term.Output() - assert.Contains(t, output, "promote the MasterUserRecord 'testmur' to the 'deactivate180' user tier?") - assert.NotContains(t, output, "Successfully promoted MasterUserRecord") - assert.NotContains(t, output, "cool-token") + // assert.Contains(t, buffy.String(), "promote the MasterUserRecord 'testmur' to the 'deactivate180' user tier?") + assert.NotContains(t, buffy.String(), "Successfully promoted MasterUserRecord") + assert.NotContains(t, buffy.String(), "cool-token") } func TestPromoteUserCmdWhenMasterUserRecordNotFound(t *testing.T) { @@ -62,7 +64,8 @@ func TestPromoteUserCmdWhenMasterUserRecordNotFound(t *testing.T) { mur := masteruserrecord.NewMasterUserRecord(t, "testmur", masteruserrecord.TierName("deactivate30")) newClient, fakeClient := NewFakeClients(t, mur, newUserTier("deactivate180")) SetFileConfig(t, Host()) - term := NewFakeTerminalWithResponse("Y") + buffy := bytes.NewBuffer(nil) + term := ioutils.NewTerminal(buffy, buffy) ctx := clicontext.NewCommandContext(term, newClient) // when @@ -71,10 +74,9 @@ func TestPromoteUserCmdWhenMasterUserRecordNotFound(t *testing.T) { // then require.EqualError(t, err, "masteruserrecords.toolchain.dev.openshift.com \"another\" not found") assertMasterUserRecordSpec(t, fakeClient, mur) // unrelated mur should be unchanged - output := term.Output() - assert.NotContains(t, output, "promote the MasterUserRecord 'another' to the 'deactivate180' user tier?") - assert.NotContains(t, output, "Successfully promoted MasterUserRecord") - assert.NotContains(t, output, "cool-token") + assert.NotContains(t, buffy.String(), "promote the MasterUserRecord 'another' to the 'deactivate180' user tier?") + assert.NotContains(t, buffy.String(), "Successfully promoted MasterUserRecord") + assert.NotContains(t, buffy.String(), "cool-token") } func TestPromoteUserCmdWhenUserTierNotFound(t *testing.T) { @@ -82,7 +84,8 @@ func TestPromoteUserCmdWhenUserTierNotFound(t *testing.T) { mur := masteruserrecord.NewMasterUserRecord(t, "testmur", masteruserrecord.TierName("deactivate30")) newClient, fakeClient := NewFakeClients(t, mur) SetFileConfig(t, Host()) - term := NewFakeTerminalWithResponse("Y") + buffy := bytes.NewBuffer(nil) + term := ioutils.NewTerminal(buffy, buffy) ctx := clicontext.NewCommandContext(term, newClient) // when @@ -91,10 +94,9 @@ func TestPromoteUserCmdWhenUserTierNotFound(t *testing.T) { // then require.EqualError(t, err, "usertiers.toolchain.dev.openshift.com \"deactivate180\" not found") assertMasterUserRecordSpec(t, fakeClient, mur) // mur should be unchanged - output := term.Output() - assert.NotContains(t, output, "promote the MasterUserRecord 'another' to the 'deactivate180' user tier?") - assert.NotContains(t, output, "Successfully promoted MasterUserRecord") - assert.NotContains(t, output, "cool-token") + assert.NotContains(t, buffy.String(), "promote the MasterUserRecord 'another' to the 'deactivate180' user tier?") + assert.NotContains(t, buffy.String(), "Successfully promoted MasterUserRecord") + assert.NotContains(t, buffy.String(), "cool-token") } func assertMasterUserRecordSpec(t *testing.T, fakeClient *test.FakeClient, expectedMasterUserRecord *toolchainv1alpha1.MasterUserRecord) { diff --git a/pkg/cmd/remove_space_users.go b/pkg/cmd/remove_space_users.go index 88bcc727..42e66ced 100644 --- a/pkg/cmd/remove_space_users.go +++ b/pkg/cmd/remove_space_users.go @@ -24,9 +24,8 @@ func NewRemoveSpaceUsersCmd() *cobra.Command { one or more users specified by their MasterUserRecord name.`, Args: cobra.ExactArgs(0), RunE: func(cmd *cobra.Command, args []string) error { - term := ioutils.NewTerminal(cmd.InOrStdin, cmd.OutOrStdout) + term := ioutils.NewTerminal(cmd.InOrStdin(), cmd.OutOrStdout(), ioutils.WithVerbose(configuration.Verbose)) ctx := clicontext.NewCommandContext(term, client.DefaultNewClient) - return RemoveSpaceUsers(ctx, spaceName, users) }, } @@ -39,7 +38,7 @@ one or more users specified by their MasterUserRecord name.`, } func RemoveSpaceUsers(ctx *clicontext.CommandContext, spaceName string, usersToRemove []string) error { - cfg, err := configuration.LoadClusterConfig(ctx, configuration.HostName) // uses the same token as add-space-users + cfg, err := configuration.LoadClusterConfig(ctx.Logger, configuration.HostName) // uses the same token as add-space-users if err != nil { return err } @@ -49,7 +48,7 @@ func RemoveSpaceUsers(ctx *clicontext.CommandContext, spaceName string, usersToR } // get Space - ctx.Println("Checking space...") + ctx.Info("Checking space...") space, err := client.GetSpace(cl, cfg.OperatorNamespace, spaceName) if err != nil { return err @@ -71,16 +70,13 @@ func RemoveSpaceUsers(ctx *clicontext.CommandContext, spaceName string, usersToR } // confirmation before SpaceBinding deletion - if err := ctx.PrintObject(space, "Space"); err != nil { + if err := ctx.PrintObject("Space:", space); err != nil { return err } - confirmation := ctx.AskForConfirmation(ioutils.WithMessagef( - "remove users from the above Space?")) - if !confirmation { - return nil + if confirm, err := ctx.Confirm("Remove users from the Space above?"); err != nil || !confirm { + return err } - - ctx.Println("Deleting SpaceBinding(s)...") + ctx.Info("Deleting SpaceBinding(s)...") // delete SpaceBindings for _, sb := range spaceBindingsToDelete { if err := cl.Delete(context.TODO(), sb); err != nil { @@ -88,6 +84,6 @@ func RemoveSpaceUsers(ctx *clicontext.CommandContext, spaceName string, usersToR } } - ctx.Printlnf("\nAll SpaceBinding(s) successfully deleted") + ctx.Info("All SpaceBinding(s) successfully deleted") return nil } diff --git a/pkg/cmd/remove_space_users_test.go b/pkg/cmd/remove_space_users_test.go index 144b04ad..191b834b 100644 --- a/pkg/cmd/remove_space_users_test.go +++ b/pkg/cmd/remove_space_users_test.go @@ -1,6 +1,7 @@ package cmd_test import ( + "bytes" "context" "fmt" "testing" @@ -11,6 +12,7 @@ import ( "github.com/codeready-toolchain/toolchain-common/pkg/test/masteruserrecord" "github.com/kubesaw/ksctl/pkg/cmd" clicontext "github.com/kubesaw/ksctl/pkg/context" + "github.com/kubesaw/ksctl/pkg/ioutils" . "github.com/kubesaw/ksctl/pkg/test" "github.com/stretchr/testify/assert" @@ -31,7 +33,8 @@ func TestRemoveSpaceUsers(t *testing.T) { newClient, fakeClient := NewFakeClients(t, space, sb1, sb2) SetFileConfig(t, Host()) - term := NewFakeTerminalWithResponse("Y") + buffy := bytes.NewBuffer(nil) + term := ioutils.NewTerminal(buffy, buffy, ioutils.WithDefaultConfirm(true)) ctx := clicontext.NewCommandContext(term, newClient) // when @@ -39,11 +42,10 @@ func TestRemoveSpaceUsers(t *testing.T) { // then require.NoError(t, err) - output := term.Output() assertSpaceBindingsRemaining(t, fakeClient, []string{}) // should be deleted - assert.Contains(t, output, "Are you sure that you want to remove users from the above Space?") - assert.Contains(t, output, "SpaceBinding(s) successfully deleted") - assert.NotContains(t, output, "cool-token") + // assert.Contains(t, buffy.String(), "Are you sure that you want to remove users from the above Space?") + assert.Contains(t, buffy.String(), "SpaceBinding(s) successfully deleted") + assert.NotContains(t, buffy.String(), "cool-token") }) t.Run("when only one spacebinding is deleted", func(t *testing.T) { @@ -56,7 +58,8 @@ func TestRemoveSpaceUsers(t *testing.T) { newClient, fakeClient := NewFakeClients(t, space, sb1, sb2) SetFileConfig(t, Host()) - term := NewFakeTerminalWithResponse("Y") + buffy := bytes.NewBuffer(nil) + term := ioutils.NewTerminal(buffy, buffy, ioutils.WithDefaultConfirm(true)) ctx := clicontext.NewCommandContext(term, newClient) // when @@ -64,11 +67,10 @@ func TestRemoveSpaceUsers(t *testing.T) { // then require.NoError(t, err) - output := term.Output() assertSpaceBindingsRemaining(t, fakeClient, []string{"bob"}) // one should remain - assert.Contains(t, output, "Are you sure that you want to remove users from the above Space?") - assert.Contains(t, output, "SpaceBinding(s) successfully deleted") - assert.NotContains(t, output, "cool-token") + // assert.Contains(t, buffy.String(), "Are you sure that you want to remove users from the above Space?") + assert.Contains(t, buffy.String(), "SpaceBinding(s) successfully deleted") + assert.NotContains(t, buffy.String(), "cool-token") }) }) @@ -82,7 +84,8 @@ func TestRemoveSpaceUsers(t *testing.T) { newClient, fakeClient := NewFakeClients(t, space, sb1, sb2) SetFileConfig(t, Host()) - term := NewFakeTerminalWithResponse("N") + buffy := bytes.NewBuffer(nil) + term := ioutils.NewTerminal(buffy, buffy, ioutils.WithDefaultConfirm(false)) ctx := clicontext.NewCommandContext(term, newClient) // when @@ -90,11 +93,10 @@ func TestRemoveSpaceUsers(t *testing.T) { // then require.NoError(t, err) - output := term.Output() assertSpaceBindingsRemaining(t, fakeClient, []string{"alice", "bob"}) // should not be deleted - assert.Contains(t, output, "Are you sure that you want to remove users from the above Space?") - assert.NotContains(t, output, "SpaceBinding(s) successfully deleted") - assert.NotContains(t, output, "cool-token") + // assert.Contains(t, buffy.String(), "Are you sure that you want to remove users from the above Space?") + assert.NotContains(t, buffy.String(), "SpaceBinding(s) successfully deleted") + assert.NotContains(t, buffy.String(), "cool-token") }) t.Run("when space not found", func(t *testing.T) { @@ -107,7 +109,8 @@ func TestRemoveSpaceUsers(t *testing.T) { newClient, fakeClient := NewFakeClients(t, sb1, sb2) // no space SetFileConfig(t, Host()) - term := NewFakeTerminalWithResponse("N") + buffy := bytes.NewBuffer(nil) + term := ioutils.NewTerminal(buffy, buffy) ctx := clicontext.NewCommandContext(term, newClient) // when @@ -115,11 +118,10 @@ func TestRemoveSpaceUsers(t *testing.T) { // then require.EqualError(t, err, `spaces.toolchain.dev.openshift.com "testspace" not found`) - output := term.Output() assertSpaceBindingsRemaining(t, fakeClient, []string{"alice", "bob"}) // should not be deleted - assert.NotContains(t, output, "Are you sure that you want to remove users from the above Space?") - assert.NotContains(t, output, "SpaceBinding(s) successfully deleted") - assert.NotContains(t, output, "cool-token") + // assert.NotContains(t, buffy.String(), "Are you sure that you want to remove users from the above Space?") + assert.NotContains(t, buffy.String(), "SpaceBinding(s) successfully deleted") + assert.NotContains(t, buffy.String(), "cool-token") }) t.Run("when mur not found", func(t *testing.T) { @@ -132,7 +134,8 @@ func TestRemoveSpaceUsers(t *testing.T) { newClient, fakeClient := NewFakeClients(t, space, sb1, sb2) SetFileConfig(t, Host()) - term := NewFakeTerminalWithResponse("N") + buffy := bytes.NewBuffer(nil) + term := ioutils.NewTerminal(buffy, buffy) ctx := clicontext.NewCommandContext(term, newClient) // when @@ -140,11 +143,10 @@ func TestRemoveSpaceUsers(t *testing.T) { // then require.EqualError(t, err, `no SpaceBinding found for Space 'testspace' and MasterUserRecord 'notexist'`) - output := term.Output() assertSpaceBindingsRemaining(t, fakeClient, []string{"alice", "bob"}) // should not be deleted - assert.NotContains(t, output, "Are you sure that you want to remove users from the above Space?") - assert.NotContains(t, output, "SpaceBinding(s) successfully deleted") - assert.NotContains(t, output, "cool-token") + // assert.NotContains(t, buffy.String(), "Are you sure that you want to remove users from the above Space?") + assert.NotContains(t, buffy.String(), "SpaceBinding(s) successfully deleted") + assert.NotContains(t, buffy.String(), "cool-token") }) t.Run("client get error", func(t *testing.T) { @@ -160,7 +162,8 @@ func TestRemoveSpaceUsers(t *testing.T) { } SetFileConfig(t, Host()) - term := NewFakeTerminalWithResponse("Y") + buffy := bytes.NewBuffer(nil) + term := ioutils.NewTerminal(buffy, buffy) ctx := clicontext.NewCommandContext(term, newClient) // when @@ -168,11 +171,10 @@ func TestRemoveSpaceUsers(t *testing.T) { // then require.EqualError(t, err, "client error") - output := term.Output() assertSpaceBindingsRemaining(t, fakeClient, []string{"alice", "bob"}) - assert.NotContains(t, output, "Are you sure that you want to remove users from the above Space?") - assert.NotContains(t, output, "SpaceBinding(s) successfully deleted") - assert.NotContains(t, output, "cool-token") + // assert.NotContains(t, buffy.String(), "Are you sure that you want to remove users from the above Space?") + assert.NotContains(t, buffy.String(), "SpaceBinding(s) successfully deleted") + assert.NotContains(t, buffy.String(), "cool-token") }) } diff --git a/pkg/cmd/retarget.go b/pkg/cmd/retarget.go index 6c0b8383..f23b8ef7 100644 --- a/pkg/cmd/retarget.go +++ b/pkg/cmd/retarget.go @@ -4,7 +4,6 @@ import ( "fmt" toolchainv1alpha1 "github.com/codeready-toolchain/api/api/v1alpha1" - "github.com/ghodss/yaml" "github.com/kubesaw/ksctl/pkg/client" "github.com/kubesaw/ksctl/pkg/configuration" clicontext "github.com/kubesaw/ksctl/pkg/context" @@ -20,7 +19,7 @@ func NewRetargetCmd() *cobra.Command { Long: `Retargets the given Space by patching the Space.Spec.TargetCluster field to the name of the given target cluster`, Args: cobra.ExactArgs(2), RunE: func(cmd *cobra.Command, args []string) error { - term := ioutils.NewTerminal(cmd.InOrStdin, cmd.OutOrStdout) + term := ioutils.NewTerminal(cmd.InOrStdin(), cmd.OutOrStdout(), ioutils.WithVerbose(configuration.Verbose)) ctx := clicontext.NewCommandContext(term, client.DefaultNewClient) return Retarget(ctx, args[0], args[1]) }, @@ -28,7 +27,7 @@ func NewRetargetCmd() *cobra.Command { } func Retarget(ctx *clicontext.CommandContext, spaceName, targetCluster string) error { - hostClusterConfig, err := configuration.LoadClusterConfig(ctx, configuration.HostName) + hostClusterConfig, err := configuration.LoadClusterConfig(ctx.Logger, configuration.HostName) if err != nil { return err } @@ -38,7 +37,7 @@ func Retarget(ctx *clicontext.CommandContext, spaceName, targetCluster string) e } // note: view toolchain role on the member cluster is good enough for retargeting since the retarget role is mainly for modifying the Space on the host - fullTargetClusterName, err := configuration.GetMemberClusterName(ctx, targetCluster) + fullTargetClusterName, err := configuration.GetMemberClusterName(ctx.Logger, targetCluster) if err != nil { return err } @@ -63,35 +62,34 @@ func Retarget(ctx *clicontext.CommandContext, spaceName, targetCluster string) e } // print Space before prompt - if err := ctx.PrintObject(space, "Space to be retargeted"); err != nil { + if err := ctx.PrintObject("Space to be retargeted:", space); err != nil { return err } // and the owner (creator) - spec, err := yaml.Marshal(userSignup.Spec) - if err != nil { - return errs.Wrapf(err, "unable to unmarshal UserSignup.Spec") - } - ctx.PrintContextSeparatorWithBodyf(string(spec), "Owned (created) by UserSignup '%s' with spec", userSignup.Name) + // spec, err := yaml.Marshal(userSignup.Spec) + // if err != nil { + // return errs.Wrapf(err, "unable to unmarshal UserSignup.Spec") + // } - // prompt for confirmation to proceed - confirmationMsg := ioutils.WithDangerZoneMessagef( - "deletion of all related namespaces and all related data", - "retarget the Space '%s' owned (created) by UserSignup '%s' to cluster '%s'?", - spaceName, userSignup.Name, targetCluster) + if err := ctx.PrintObject(fmt.Sprintf("Owned (created) by UserSignup '%s' with spec", userSignup.Name), userSignup); err != nil { + return err + } - if confirmed := ctx.AskForConfirmation(confirmationMsg); !confirmed { - return nil + ctx.Warn("!!! DANGER ZONE !!!") + ctx.Warn("Deleting all the user's namespaces and all their resources") + if confirm, err := ctx.Confirm("Retarget the '%s' Space owned (created) by the '%s' UserSignup to the '%s' cluster?", spaceName, userSignup.Name, targetCluster); err != nil || !confirm { + return err } err = client.PatchSpace(ctx, space.Name, func(space *toolchainv1alpha1.Space) (bool, error) { space.Spec.TargetCluster = fullTargetClusterName return true, nil - }, "Space has been patched to target cluster "+targetCluster) + }, fmt.Sprintf("Space has been patched to the '%s' target cluster ", targetCluster)) if err != nil { return errs.Wrapf(err, "failed to retarget Space '%s'", spaceName) } - ctx.Printlnf("\nSpace has been retargeted to cluster " + targetCluster) + ctx.Infof("Space has been retargeted to the '%s' cluster ", targetCluster) return nil } diff --git a/pkg/cmd/retarget_test.go b/pkg/cmd/retarget_test.go index 0444f327..4ff3003f 100644 --- a/pkg/cmd/retarget_test.go +++ b/pkg/cmd/retarget_test.go @@ -1,6 +1,7 @@ package cmd_test import ( + "bytes" "context" "fmt" "testing" @@ -11,6 +12,7 @@ import ( testusersignup "github.com/codeready-toolchain/toolchain-common/pkg/test/usersignup" "github.com/kubesaw/ksctl/pkg/cmd" clicontext "github.com/kubesaw/ksctl/pkg/context" + "github.com/kubesaw/ksctl/pkg/ioutils" . "github.com/kubesaw/ksctl/pkg/test" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -23,9 +25,10 @@ func TestRetarget(t *testing.T) { t.Run("retarget success", func(t *testing.T) { // given - term := NewFakeTerminalWithResponse("y") space := testspace.NewSpace(test.HostOperatorNs, "john-dev", testspace.WithCreatorLabel("john")) newClient, fakeClient := prepareRetargetSpace(t, space, userSignup) + buffy := bytes.NewBuffer(nil) + term := ioutils.NewTerminal(buffy, buffy, ioutils.WithDefaultConfirm(true)) ctx := clicontext.NewCommandContext(term, newClient) // when @@ -34,20 +37,21 @@ func TestRetarget(t *testing.T) { // then require.NoError(t, err) testspace.AssertThatSpace(t, space.Namespace, space.Name, fakeClient).HasSpecTargetCluster("member-m2.devcluster.openshift.com") - assert.Contains(t, term.Output(), "!!! DANGER ZONE !!!") - assert.Contains(t, term.Output(), fmt.Sprintf("Are you sure that you want to retarget the Space '%s' owned (created) by UserSignup '%s' to cluster 'member2'?", space.Name, userSignup.Name)) - assert.Contains(t, term.Output(), "Space to be retargeted") - assert.Contains(t, term.Output(), fmt.Sprintf("Owned (created) by UserSignup '%s' with spec", userSignup.Name)) - assert.Contains(t, term.Output(), "Space has been patched to target cluster member2") - assert.Contains(t, term.Output(), "Space has been retargeted to cluster member2") - assert.NotContains(t, term.Output(), "cool-token") + assert.Contains(t, buffy.String(), "!!! DANGER ZONE !!!") + // assert.Contains(t, buffy.String(), fmt.Sprintf("Are you sure that you want to retarget the Space '%s' owned (created) by UserSignup '%s' to cluster 'member2'?", space.Name, userSignup.Name)) + assert.Contains(t, buffy.String(), "Space to be retargeted") + assert.Contains(t, buffy.String(), fmt.Sprintf("Owned (created) by UserSignup '%s' with spec", userSignup.Name)) + assert.Contains(t, buffy.String(), "Space has been patched to the 'member2' target cluster") + assert.Contains(t, buffy.String(), "Space has been retargeted to the 'member2' cluster") + assert.NotContains(t, buffy.String(), "cool-token") }) t.Run("retarget fail", func(t *testing.T) { t.Run("no space found", func(t *testing.T) { // given - term := NewFakeTerminalWithResponse("y") newClient, _ := prepareRetargetSpace(t) // no usersignup created + buffy := bytes.NewBuffer(nil) + term := ioutils.NewTerminal(buffy, buffy, ioutils.WithDefaultConfirm(true)) ctx := clicontext.NewCommandContext(term, newClient) // when @@ -59,9 +63,10 @@ func TestRetarget(t *testing.T) { t.Run("space already targeted to the provided target cluster", func(t *testing.T) { // given - term := NewFakeTerminalWithResponse("y") space := testspace.NewSpace(test.HostOperatorNs, "john-dev", testspace.WithCreatorLabel("john"), testspace.WithSpecTargetCluster("member-m2.devcluster.openshift.com")) newClient, _ := prepareRetargetSpace(t, space, userSignup) + buffy := bytes.NewBuffer(nil) + term := ioutils.NewTerminal(buffy, buffy, ioutils.WithDefaultConfirm(true)) ctx := clicontext.NewCommandContext(term, newClient) // when @@ -73,9 +78,10 @@ func TestRetarget(t *testing.T) { t.Run("failed to get member cluster config", func(t *testing.T) { // given - term := NewFakeTerminalWithResponse("y") space := testspace.NewSpace(test.HostOperatorNs, "john-dev") newClient, _ := prepareRetargetSpace(t, space) + buffy := bytes.NewBuffer(nil) + term := ioutils.NewTerminal(buffy, buffy, ioutils.WithDefaultConfirm(true)) ctx := clicontext.NewCommandContext(term, newClient) // when @@ -88,7 +94,6 @@ func TestRetarget(t *testing.T) { t.Run("setting target cluster failed", func(t *testing.T) { // given - term := NewFakeTerminalWithResponse("y") space := testspace.NewSpace(test.HostOperatorNs, "john-dev", testspace.WithCreatorLabel("john")) newClient, fakeClient := prepareRetargetSpace(t, space, userSignup) fakeClient.MockPatch = func(ctx context.Context, obj runtimeclient.Object, patch runtimeclient.Patch, opts ...runtimeclient.PatchOption) error { @@ -99,6 +104,8 @@ func TestRetarget(t *testing.T) { } return fakeClient.Client.Patch(ctx, obj, patch, opts...) } + buffy := bytes.NewBuffer(nil) + term := ioutils.NewTerminal(buffy, buffy, ioutils.WithDefaultConfirm(true)) ctx := clicontext.NewCommandContext(term, newClient) // when @@ -111,9 +118,10 @@ func TestRetarget(t *testing.T) { t.Run("space without owner label", func(t *testing.T) { // given - term := NewFakeTerminalWithResponse("y") space := testspace.NewSpace(test.HostOperatorNs, "john-dev") newClient, _ := prepareRetargetSpace(t, space, userSignup) + buffy := bytes.NewBuffer(nil) + term := ioutils.NewTerminal(buffy, buffy, ioutils.WithDefaultConfirm(true)) ctx := clicontext.NewCommandContext(term, newClient) // when @@ -126,9 +134,10 @@ func TestRetarget(t *testing.T) { t.Run("usersignup not found", func(t *testing.T) { // given - term := NewFakeTerminalWithResponse("y") space := testspace.NewSpace(test.HostOperatorNs, "john-dev", testspace.WithCreatorLabel("john")) newClient, _ := prepareRetargetSpace(t, space) + buffy := bytes.NewBuffer(nil) + term := ioutils.NewTerminal(buffy, buffy, ioutils.WithDefaultConfirm(true)) ctx := clicontext.NewCommandContext(term, newClient) // when @@ -142,9 +151,10 @@ func TestRetarget(t *testing.T) { t.Run("user responds no", func(t *testing.T) { // given - term := NewFakeTerminalWithResponse("n") space := testspace.NewSpace(test.HostOperatorNs, "john-dev", testspace.WithCreatorLabel("john"), testspace.WithSpecTargetCluster("member-m1.devcluster.openshift.com")) newClient, fakeClient := prepareRetargetSpace(t, space, userSignup) + buffy := bytes.NewBuffer(nil) + term := ioutils.NewTerminal(buffy, buffy, ioutils.WithDefaultConfirm(false)) ctx := clicontext.NewCommandContext(term, newClient) // when @@ -153,13 +163,13 @@ func TestRetarget(t *testing.T) { // then require.NoError(t, err) testspace.AssertThatSpace(t, space.Namespace, space.Name, fakeClient).HasSpecTargetCluster("member-m1.devcluster.openshift.com") - assert.Contains(t, term.Output(), "!!! DANGER ZONE !!!") - assert.Contains(t, term.Output(), fmt.Sprintf("Are you sure that you want to retarget the Space '%s' owned (created) by UserSignup '%s' to cluster 'member2'?", space.Name, userSignup.Name)) - assert.Contains(t, term.Output(), "Space to be retargeted") - assert.Contains(t, term.Output(), fmt.Sprintf("Owned (created) by UserSignup '%s' with spec", userSignup.Name)) - assert.NotContains(t, term.Output(), "Space has been patched to target cluster member2") - assert.NotContains(t, term.Output(), "Space has been retargeted to cluster member2") - assert.NotContains(t, term.Output(), "cool-token") + assert.Contains(t, buffy.String(), "!!! DANGER ZONE !!!") + // assert.Contains(t, buffy.String(), fmt.Sprintf("Are you sure that you want to retarget the Space '%s' owned (created) by UserSignup '%s' to cluster 'member2'?", space.Name, userSignup.Name)) + assert.Contains(t, buffy.String(), "Space to be retargeted") + assert.Contains(t, buffy.String(), fmt.Sprintf("Owned (created) by UserSignup '%s' with spec", userSignup.Name)) + assert.NotContains(t, buffy.String(), "Space has been patched to the 'member2' target cluster") + assert.NotContains(t, buffy.String(), "Space has been retargeted to the 'member2' cluster") + assert.NotContains(t, buffy.String(), "cool-token") }) } diff --git a/pkg/cmd/status.go b/pkg/cmd/status.go index 39faf62a..91898120 100644 --- a/pkg/cmd/status.go +++ b/pkg/cmd/status.go @@ -22,7 +22,7 @@ func NewStatusCmd() *cobra.Command { Long: `Show the ToolchainStatus CR`, Args: cobra.MaximumNArgs(0), RunE: func(cmd *cobra.Command, _ []string) error { - term := ioutils.NewTerminal(cmd.InOrStdin, cmd.OutOrStdout) + term := ioutils.NewTerminal(cmd.InOrStdin(), cmd.OutOrStdout(), ioutils.WithVerbose(configuration.Verbose)) ctx := clicontext.NewCommandContext(term, client.DefaultNewClient) return Status(ctx) }, @@ -30,7 +30,7 @@ func NewStatusCmd() *cobra.Command { } func Status(ctx *clicontext.CommandContext) error { - cfg, err := configuration.LoadClusterConfig(ctx, configuration.HostName) + cfg, err := configuration.LoadClusterConfig(ctx.Logger, configuration.HostName) if err != nil { return err } @@ -48,14 +48,14 @@ func Status(ctx *clicontext.CommandContext) error { } cond, exists := condition.FindConditionByType(status.Status.Conditions, toolchainv1alpha1.ConditionReady) - title := "Current ToolchainStatus CR - " + title := "Current ToolchainStatus - " if exists { title += fmt.Sprintf("Condition: %s, Status: %s, Reason: %s", cond.Type, cond.Status, cond.Reason) if cond.Message != "" { title += fmt.Sprintf(", Message: %s", cond.Message) } } else { - title += "Condition Ready wasn't found!" + title += "Condition Ready not found" } - return ctx.PrintObject(status, title) + return ctx.PrintObject(title, status) } diff --git a/pkg/cmd/status_test.go b/pkg/cmd/status_test.go index 9f284ddb..7a28e676 100644 --- a/pkg/cmd/status_test.go +++ b/pkg/cmd/status_test.go @@ -1,6 +1,7 @@ package cmd_test import ( + "bytes" "context" "fmt" "testing" @@ -9,6 +10,7 @@ import ( "github.com/codeready-toolchain/toolchain-common/pkg/test" "github.com/kubesaw/ksctl/pkg/cmd" clicontext "github.com/kubesaw/ksctl/pkg/context" + "github.com/kubesaw/ksctl/pkg/ioutils" . "github.com/kubesaw/ksctl/pkg/test" "github.com/stretchr/testify/assert" @@ -23,7 +25,8 @@ func TestStatusCmdWhenIsReady(t *testing.T) { toolchainStatus := NewToolchainStatus(ToBeReady()) newClient, _ := NewFakeClients(t, toolchainStatus) SetFileConfig(t, Host()) - term := NewFakeTerminal() + buffy := bytes.NewBuffer(nil) + term := ioutils.NewTerminal(buffy, buffy) ctx := clicontext.NewCommandContext(term, newClient) // when @@ -31,9 +34,8 @@ func TestStatusCmdWhenIsReady(t *testing.T) { // then require.NoError(t, err) - output := term.Output() - assert.Contains(t, output, "Current ToolchainStatus CR - Condition: Ready, Status: True, Reason: AllComponentsReady") - assert.NotContains(t, output, "cool-token") + assert.Contains(t, buffy.String(), "Current ToolchainStatus - Condition: Ready, Status: True, Reason: AllComponentsReady") + assert.NotContains(t, buffy.String(), "cool-token") } func TestStatusCmdWhenIsNotReady(t *testing.T) { @@ -41,7 +43,8 @@ func TestStatusCmdWhenIsNotReady(t *testing.T) { toolchainStatus := NewToolchainStatus(ToBeNotReady()) newClient, _ := NewFakeClients(t, toolchainStatus) SetFileConfig(t, Host()) - term := NewFakeTerminal() + buffy := bytes.NewBuffer(nil) + term := ioutils.NewTerminal(buffy, buffy) ctx := clicontext.NewCommandContext(term, newClient) // when @@ -49,9 +52,8 @@ func TestStatusCmdWhenIsNotReady(t *testing.T) { // then require.NoError(t, err) - output := term.Output() - assert.Contains(t, output, "Current ToolchainStatus CR - Condition: Ready, Status: False, Reason: ComponentsNotReady, Message: components not ready: [members]") - assert.NotContains(t, output, "cool-token") + assert.Contains(t, buffy.String(), "Current ToolchainStatus - Condition: Ready, Status: False, Reason: ComponentsNotReady, Message: components not ready: [members]") + assert.NotContains(t, buffy.String(), "cool-token") } func TestStatusCmdWhenConditionNotFound(t *testing.T) { @@ -59,7 +61,8 @@ func TestStatusCmdWhenConditionNotFound(t *testing.T) { toolchainStatus := NewToolchainStatus(toolchainv1alpha1.Condition{}) newClient, _ := NewFakeClients(t, toolchainStatus) SetFileConfig(t, Host()) - term := NewFakeTerminal() + buffy := bytes.NewBuffer(nil) + term := ioutils.NewTerminal(buffy, buffy) ctx := clicontext.NewCommandContext(term, newClient) // when @@ -67,9 +70,8 @@ func TestStatusCmdWhenConditionNotFound(t *testing.T) { // then require.NoError(t, err) - output := term.Output() - assert.Contains(t, output, "Current ToolchainStatus CR - Condition Ready wasn't found!") - assert.NotContains(t, output, "cool-token") + assert.Contains(t, buffy.String(), "Current ToolchainStatus - Condition Ready not found") + assert.NotContains(t, buffy.String(), "cool-token") } func TestStatusCmdWithInsufficientPermissions(t *testing.T) { @@ -77,7 +79,8 @@ func TestStatusCmdWithInsufficientPermissions(t *testing.T) { toolchainStatus := NewToolchainStatus(toolchainv1alpha1.Condition{}) newClient, _ := NewFakeClients(t, toolchainStatus) SetFileConfig(t, Host(NoToken())) - term := NewFakeTerminal() + buffy := bytes.NewBuffer(nil) + term := ioutils.NewTerminal(buffy, buffy) ctx := clicontext.NewCommandContext(term, newClient) // when @@ -85,9 +88,9 @@ func TestStatusCmdWithInsufficientPermissions(t *testing.T) { // then require.Error(t, err) - output := term.Output() - assert.NotContains(t, output, "Current ToolchainStatus CR") - assert.NotContains(t, output, "cool-token") + // output := term.Output() + assert.NotContains(t, buffy.String(), "Current ToolchainStatus CR") + assert.NotContains(t, buffy.String(), "cool-token") } func TestStatusCmdWhenGetFailed(t *testing.T) { @@ -98,7 +101,8 @@ func TestStatusCmdWhenGetFailed(t *testing.T) { fakeClient.MockGet = func(ctx context.Context, key runtimeclient.ObjectKey, obj runtimeclient.Object, opts ...runtimeclient.GetOption) error { return fmt.Errorf("some error") } - term := NewFakeTerminal() + buffy := bytes.NewBuffer(nil) + term := ioutils.NewTerminal(buffy, buffy) ctx := clicontext.NewCommandContext(term, newClient) // when @@ -106,9 +110,9 @@ func TestStatusCmdWhenGetFailed(t *testing.T) { // then require.Error(t, err) - output := term.Output() - assert.NotContains(t, output, "Current ToolchainStatus CR") - assert.NotContains(t, output, "cool-token") + // output := term.Output() + assert.NotContains(t, buffy.String(), "Current ToolchainStatus CR") + assert.NotContains(t, buffy.String(), "cool-token") } func NewToolchainStatus(cond toolchainv1alpha1.Condition) *toolchainv1alpha1.ToolchainStatus { diff --git a/pkg/configuration/configuration.go b/pkg/configuration/configuration.go index 9f2a2466..8eaf6cb0 100644 --- a/pkg/configuration/configuration.go +++ b/pkg/configuration/configuration.go @@ -7,7 +7,7 @@ import ( "sort" "strings" - "github.com/kubesaw/ksctl/pkg/ioutils" + "github.com/charmbracelet/log" "github.com/kubesaw/ksctl/pkg/utils" "github.com/mitchellh/go-homedir" @@ -26,7 +26,7 @@ type KsctlConfig struct { } // Load reads in config file and ENV variables if set. -func Load(term ioutils.Terminal) (KsctlConfig, error) { +func Load(logger *log.Logger) (KsctlConfig, error) { path := ConfigFileFlag if path == "" { // Find home directory. @@ -45,9 +45,7 @@ func Load(term ioutils.Terminal) (KsctlConfig, error) { return KsctlConfig{}, fmt.Errorf("the '%s' is not file but a directory", path) } - if Verbose { - term.Printlnf("Using config file: '%s'", path) - } + logger.Debugf("Using config file: '%s'", path) bytes, err := os.ReadFile(path) if err != nil { @@ -99,8 +97,8 @@ type ClusterAccessDefinition struct { type ClusterNamespaces map[string]string // LoadClusterAccessDefinition loads ClusterAccessDefinition object from the config file and checks that all required parameters are set -func LoadClusterAccessDefinition(term ioutils.Terminal, clusterName string) (ClusterAccessDefinition, error) { - ksctlConfig, err := Load(term) +func LoadClusterAccessDefinition(logger *log.Logger, clusterName string) (ClusterAccessDefinition, error) { + ksctlConfig, err := Load(logger) if err != nil { return ClusterAccessDefinition{}, err } @@ -151,8 +149,8 @@ type ClusterConfig struct { // LoadClusterConfig loads ClusterConfig object from the config file and checks that all required parameters are set // as well as the token for the given name -func LoadClusterConfig(term ioutils.Terminal, clusterName string) (ClusterConfig, error) { - ksctlConfig, err := Load(term) +func LoadClusterConfig(logger *log.Logger, clusterName string) (ClusterConfig, error) { + ksctlConfig, err := Load(logger) if err != nil { return ClusterConfig{}, err } @@ -176,10 +174,8 @@ func LoadClusterConfig(term ioutils.Terminal, clusterName string) (ClusterConfig } } - if Verbose { - term.Printlnf("Using '%s' configuration for '%s' cluster running at '%s' and in namespace '%s'\n", - clusterName, clusterDef.ServerName, clusterDef.ServerAPI, operatorNamespace) - } + logger.Debugf("Using '%s' configuration for '%s' cluster running at '%s' and in namespace '%s'\n", clusterName, clusterDef.ServerName, clusterDef.ServerAPI, operatorNamespace) + return ClusterConfig{ ClusterAccessDefinition: clusterDef, AllClusterNames: getAllClusterNames(ksctlConfig), @@ -201,8 +197,8 @@ func (c ClusterConfig) GetNamespaceParam() string { // GetMemberClusterName returns the full name of the member cluster (used in ToolchainCluster CRs) // for the provided shot cluster name such as member-1 (used in ksctl.yaml) -func GetMemberClusterName(term ioutils.Terminal, ctlClusterName string) (string, error) { - memberClusterConfig, err := LoadClusterConfig(term, ctlClusterName) +func GetMemberClusterName(logger *log.Logger, ctlClusterName string) (string, error) { + memberClusterConfig, err := LoadClusterConfig(logger, ctlClusterName) if err != nil { return "", err } diff --git a/pkg/configuration/configuration_test.go b/pkg/configuration/configuration_test.go index df34ef8b..407b2d84 100644 --- a/pkg/configuration/configuration_test.go +++ b/pkg/configuration/configuration_test.go @@ -1,11 +1,13 @@ package configuration_test import ( + "bytes" "fmt" "os" "strings" "testing" + "github.com/charmbracelet/log" "github.com/codeready-toolchain/toolchain-common/pkg/test" "github.com/kubesaw/ksctl/pkg/configuration" . "github.com/kubesaw/ksctl/pkg/test" @@ -41,12 +43,14 @@ func TestLoadClusterConfig(t *testing.T) { // given SetFileConfig(t, clusterConfigParam) namespaceName := fmt.Sprintf("toolchain-%s-operator", clusterConfigParam.ClusterType) - term := NewFakeTerminal() - term.Tee(os.Stdout) + buffy := bytes.NewBuffer(nil) + logger := log.NewWithOptions(buffy, log.Options{ + Level: log.DebugLevel, + }) configuration.Verbose = true // when - cfg, err := configuration.LoadClusterConfig(term, clusterName) + cfg, err := configuration.LoadClusterConfig(logger, clusterName) // then require.NoError(t, err) @@ -59,25 +63,26 @@ func TestLoadClusterConfig(t *testing.T) { assert.Equal(t, "cool-server.com", cfg.ServerName) assert.Len(t, cfg.AllClusterNames, 1) assert.Contains(t, cfg.AllClusterNames, utils.CamelCaseToKebabCase(clusterName)) - assert.Contains(t, term.Output(), fmt.Sprintf("Using config file: '%s'", configuration.ConfigFileFlag)) - assert.Contains(t, term.Output(), fmt.Sprintf("Using '%s' configuration for '%s' cluster running at '%s' and in namespace '%s'", + assert.Contains(t, buffy.String(), fmt.Sprintf("Using config file: '%s'", configuration.ConfigFileFlag)) + assert.Contains(t, buffy.String(), fmt.Sprintf("Using '%s' configuration for '%s' cluster running at '%s' and in namespace '%s'", cfg.ClusterName, cfg.ServerName, cfg.ServerAPI, cfg.OperatorNamespace)) }) t.Run("without verbose logs", func(t *testing.T) { // given SetFileConfig(t, clusterConfigParam) - term := NewFakeTerminal() + buffy := bytes.NewBuffer(nil) + logger := log.New(buffy) configuration.Verbose = false // when - cfg, err := configuration.LoadClusterConfig(term, clusterName) + cfg, err := configuration.LoadClusterConfig(logger, clusterName) // then require.NoError(t, err) // don't repeat assertions above, just check that logs do NOT contain the following messages - assert.NotContains(t, term.Output(), fmt.Sprintf("Using config file: '%s'", configuration.ConfigFileFlag)) - assert.NotContains(t, term.Output(), fmt.Sprintf("Using '%s' configuration for '%s' cluster running at '%s' and in namespace '%s'", + assert.NotContains(t, buffy.String(), fmt.Sprintf("Using config file: '%s'", configuration.ConfigFileFlag)) + assert.NotContains(t, buffy.String(), fmt.Sprintf("Using '%s' configuration for '%s' cluster running at '%s' and in namespace '%s'", cfg.ClusterType, cfg.ServerName, cfg.ServerAPI, cfg.OperatorNamespace)) }) }) @@ -92,10 +97,10 @@ func TestLoadClusterConfig(t *testing.T) { restore := test.SetEnvVarAndRestore(t, strings.ToUpper(clusterConfigParam.ClusterType.String())+"_OPERATOR_NAMESPACE", "custom-namespace") t.Cleanup(restore) SetFileConfig(t, WithValues(clusterConfigParam)) - term := NewFakeTerminal() + logger := log.New(&strings.Builder{}) // when - cfg, err := configuration.LoadClusterConfig(term, clusterName) + cfg, err := configuration.LoadClusterConfig(logger, clusterName) // then require.NoError(t, err, "ksctl command failed: The kubeSaw namespace is not set for the cluster "+clusterConfigParam.ClusterName) @@ -110,10 +115,10 @@ func TestLoadClusterConfig(t *testing.T) { t.Run("when clusterType is not set for "+clusterConfigParam.ClusterName, func(t *testing.T) { // given SetFileConfig(t, WithValues(clusterConfigParam, ClusterType(""))) - term := NewFakeTerminal() + logger := log.New(&strings.Builder{}) // when - cfg, err := configuration.LoadClusterConfig(term, clusterName) + cfg, err := configuration.LoadClusterConfig(logger, clusterName) // then require.EqualError(t, err, "ksctl command failed: 'cluster type' is not set for cluster '"+clusterConfigParam.ClusterName+"'") @@ -128,10 +133,10 @@ func TestLoadClusterConfig(t *testing.T) { t.Run("when token is not set for "+clusterName, func(t *testing.T) { // given SetFileConfig(t, clusterConfigParam) - term := NewFakeTerminal() + logger := log.New(&strings.Builder{}) // when - cfg, err := configuration.LoadClusterConfig(term, clusterName) + cfg, err := configuration.LoadClusterConfig(logger, clusterName) // then require.EqualError(t, err, "ksctl command failed: the token in your ksctl.yaml file is missing") @@ -146,10 +151,10 @@ func TestLoadClusterConfig(t *testing.T) { t.Run("when server api is not set for "+clusterName, func(t *testing.T) { // given SetFileConfig(t, WithValues(clusterConfigParam, ServerAPI(""))) - term := NewFakeTerminal() + logger := log.New(&strings.Builder{}) // when - cfg, err := configuration.LoadClusterConfig(term, clusterName) + cfg, err := configuration.LoadClusterConfig(logger, clusterName) // then require.EqualError(t, err, "ksctl command failed: The server API is not set for the cluster "+clusterName) @@ -164,10 +169,10 @@ func TestLoadClusterConfig(t *testing.T) { t.Run("when server name is not set for "+clusterName, func(t *testing.T) { // given SetFileConfig(t, WithValues(clusterConfigParam, ServerName(""))) - term := NewFakeTerminal() + logger := log.New(&strings.Builder{}) // when - cfg, err := configuration.LoadClusterConfig(term, clusterName) + cfg, err := configuration.LoadClusterConfig(logger, clusterName) // then require.EqualError(t, err, "ksctl command failed: The server name is not set for the cluster "+clusterName) @@ -178,10 +183,10 @@ func TestLoadClusterConfig(t *testing.T) { t.Run("when no cluster name is defined", func(t *testing.T) { // given SetFileConfig(t) - term := NewFakeTerminal() + logger := log.New(&strings.Builder{}) // when - _, err := configuration.LoadClusterConfig(term, "dummy") + _, err := configuration.LoadClusterConfig(logger, "dummy") // then require.Error(t, err) @@ -195,10 +200,10 @@ func TestLoadClusterConfig(t *testing.T) { t.Run("when multiple cluster names are defined", func(t *testing.T) { // given SetFileConfig(t, Host(), Member(), Member(ClusterName("member2"))) - term := NewFakeTerminal() + logger := log.New(&strings.Builder{}) // when - cfg, err := configuration.LoadClusterConfig(term, clusterName) + cfg, err := configuration.LoadClusterConfig(logger, clusterName) // then require.NoError(t, err) @@ -215,10 +220,10 @@ func TestLoadClusterConfig(t *testing.T) { func TestLoadingClusterConfigWithNonexistentClusterName(t *testing.T) { // given SetFileConfig(t, Host(), Member()) - term := NewFakeTerminal() + logger := log.New(&strings.Builder{}) // when - cfg, err := configuration.LoadClusterConfig(term, "dummy") + cfg, err := configuration.LoadClusterConfig(logger, "dummy") // then require.Error(t, err) @@ -232,34 +237,38 @@ func TestLoad(t *testing.T) { t.Run("with verbose messages", func(t *testing.T) { // given - term := NewFakeTerminal() + buffy := bytes.NewBuffer(nil) + logger := log.NewWithOptions(buffy, log.Options{ + Level: log.DebugLevel, + }) SetFileConfig(t, Host(), Member()) configuration.Verbose = true // when - ksctlConfig, err := configuration.Load(term) + ksctlConfig, err := configuration.Load(logger) // then require.NoError(t, err) expectedConfig := NewKsctlConfig(Host(), Member()) assert.Equal(t, expectedConfig, ksctlConfig) - assert.Contains(t, term.Output(), "Using config file") + assert.Contains(t, buffy.String(), "Using config file") }) t.Run("without verbose messages", func(t *testing.T) { // given - term := NewFakeTerminal() + buffy := bytes.NewBuffer(nil) + logger := log.New(buffy) SetFileConfig(t, Host(), Member()) configuration.Verbose = false // when - ksctlConfig, err := configuration.Load(term) + ksctlConfig, err := configuration.Load(logger) // then require.NoError(t, err) expectedConfig := NewKsctlConfig(Host(), Member()) assert.Equal(t, expectedConfig, ksctlConfig) - assert.NotContains(t, term.Output(), "Using config file") + assert.NotContains(t, buffy.String(), "Using config file") }) } @@ -267,11 +276,11 @@ func TestLoad(t *testing.T) { func TestLoadFails(t *testing.T) { t.Run("file does not exist", func(t *testing.T) { // given - term := NewFakeTerminal() + logger := log.New(&strings.Builder{}) configuration.ConfigFileFlag = "/tmp/should-not-exist.yaml" // when - _, err := configuration.Load(term) + _, err := configuration.Load(logger) // then require.Error(t, err) @@ -279,11 +288,11 @@ func TestLoadFails(t *testing.T) { t.Run("file is directory", func(t *testing.T) { // given - term := NewFakeTerminal() + logger := log.New(&strings.Builder{}) configuration.ConfigFileFlag = os.TempDir() // when - _, err := configuration.Load(term) + _, err := configuration.Load(logger) // then require.Error(t, err) diff --git a/pkg/context/command_context.go b/pkg/context/command_context.go index fafa817e..d722e122 100644 --- a/pkg/context/command_context.go +++ b/pkg/context/command_context.go @@ -4,7 +4,6 @@ import ( "context" "github.com/kubesaw/ksctl/pkg/ioutils" - runtimeclient "sigs.k8s.io/controller-runtime/pkg/client" ) diff --git a/pkg/context/command_context_test.go b/pkg/context/command_context_test.go index 967466b0..dcc04589 100644 --- a/pkg/context/command_context_test.go +++ b/pkg/context/command_context_test.go @@ -1,10 +1,11 @@ package context_test import ( + "bytes" "testing" + "github.com/charmbracelet/log" "github.com/kubesaw/ksctl/pkg/configuration" - clicontext "github.com/kubesaw/ksctl/pkg/context" . "github.com/kubesaw/ksctl/pkg/test" "github.com/stretchr/testify/require" @@ -15,12 +16,11 @@ func TestLoadClusterConfig(t *testing.T) { SetFileConfig(t, Host()) t.Run("success", func(t *testing.T) { - newClient, _ := NewFakeClients(t) - term := NewFakeTerminalWithResponse("Y") - ctx := clicontext.NewCommandContext(term, newClient) + buffy := bytes.NewBuffer(nil) + logger := log.New(buffy) // when - _, err := configuration.LoadClusterConfig(ctx, "host") + _, err := configuration.LoadClusterConfig(logger, "host") // then require.NoError(t, err) @@ -29,12 +29,11 @@ func TestLoadClusterConfig(t *testing.T) { t.Run("fail", func(t *testing.T) { // given SetFileConfig(t, Host(NoToken()), Member(NoToken())) - newClient, _ := NewFakeClients(t) - term := NewFakeTerminalWithResponse("Y") - ctx := clicontext.NewCommandContext(term, newClient) + buffy := bytes.NewBuffer(nil) + logger := log.New(buffy) // when - _, err := configuration.LoadClusterConfig(ctx, "host") + _, err := configuration.LoadClusterConfig(logger, "host") // then require.Error(t, err) diff --git a/pkg/ioutils/terminal.go b/pkg/ioutils/terminal.go index ee47ed39..88a19c38 100644 --- a/pkg/ioutils/terminal.go +++ b/pkg/ioutils/terminal.go @@ -1,15 +1,15 @@ package ioutils import ( - "bufio" "fmt" "io" - "log" - "strings" + "time" + "github.com/charmbracelet/glamour" + "github.com/charmbracelet/huh" + "github.com/charmbracelet/log" "github.com/ghodss/yaml" errs "github.com/pkg/errors" - "golang.org/x/term" "k8s.io/apimachinery/pkg/api/meta" "k8s.io/apimachinery/pkg/runtime" ) @@ -17,139 +17,105 @@ import ( // AssumeYes automatically answers yes for all questions. var AssumeYes bool -// Terminal a wrapper around a Cobra command, with extra methods -// to display messages. -type Terminal interface { - InOrStdin() io.Reader - OutOrStdout() io.Writer - AskForConfirmation(msg ConfirmationMessage) bool - Println(msg string) - Printlnf(msg string, args ...interface{}) - PrintContextSeparatorf(context string, args ...interface{}) - PrintContextSeparatorWithBodyf(body, context string, args ...interface{}) - PrintObject(object runtime.Object, title string) error +type Terminal struct { + input io.Reader + output io.Writer + *log.Logger + defaultConfirm *bool } -// NewTerminal returns a new terminal with the given funcs to -// access the `in` reader and `out` writer -func NewTerminal(in func() io.Reader, out func() io.Writer) Terminal { - return &DefaultTerminal{ - in: in, - out: out, +func NewTerminal(input io.Reader, output io.Writer, options ...TerminalOption) Terminal { + logger := log.NewWithOptions(output, log.Options{ + TimeFormat: time.Kitchen, + Level: log.InfoLevel, + }) + t := Terminal{ + input: input, + output: output, + Logger: logger, } + for _, apply := range options { + apply(&t) + } + return t } -// DefaultTerminal a wrapper around a Cobra command, with extra methods -// to display messages. -type DefaultTerminal struct { - in func() io.Reader - out func() io.Writer -} - -// InOrStdin returns an `io.Reader` to read the user's input -func (t *DefaultTerminal) InOrStdin() io.Reader { - return t.in() -} - -// OutOrStdout returns an `io.Writer` to write messages in the console -func (t *DefaultTerminal) OutOrStdout() io.Writer { - return t.out() -} +type TerminalOption func(t *Terminal) -// Println prints the given message and appends a line feed -func (t *DefaultTerminal) Println(msg string) { - fmt.Fprintln(t.OutOrStdout(), msg) +func WithDefaultConfirm(v bool) TerminalOption { + return func(t *Terminal) { + t.defaultConfirm = &v + } } -// Printf prints the given message with arguments -func (t *DefaultTerminal) Printf(format string, args ...interface{}) { - fmt.Fprintf(t.OutOrStdout(), format, args...) +func WithVerbose(verbose bool) TerminalOption { + return func(t *Terminal) { + if verbose { + t.Logger.SetLevel(log.DebugLevel) + } + } } -// Printlnf prints the given message with arguments and appends a line feed -func (t *DefaultTerminal) Printlnf(format string, args ...interface{}) { - fmt.Fprintf(t.OutOrStdout(), format+"\n", args...) -} +func WithTee(w io.Writer) TerminalOption { + return func(t *Terminal) { + t.Logger.SetOutput(io.MultiWriter(t.output, w)) -// PrintContextSeparatorf prints the context separator (only) -func (t *DefaultTerminal) PrintContextSeparatorf(context string, args ...interface{}) { - t.PrintContextSeparatorWithBodyf("", context, args...) + } } -// PrintContextSeparatorWithBodyf prints the context separator and a message -func (t *DefaultTerminal) PrintContextSeparatorWithBodyf(body, context string, args ...interface{}) { - width, _, err := term.GetSize(0) - if err != nil { - width = 60 +func (t Terminal) Confirm(msg string, args ...any) (bool, error) { + if t.defaultConfirm != nil { + return *t.defaultConfirm, nil } - line := strings.Repeat("-", width) - fmt.Fprintln(t.OutOrStdout(), "\n"+line) - fmt.Fprintln(t.OutOrStdout(), " "+fmt.Sprintf(context, args...)) - if body != "" { - fmt.Fprintln(t.OutOrStdout(), line) - fmt.Fprintln(t.OutOrStdout(), body) + var answer bool + confirm := huh.NewConfirm().Title(fmt.Sprintf(msg, args...)).Value(&answer) + if err := huh.NewForm(huh.NewGroup(confirm)).WithInput(t.input).Run(); err != nil { + return false, err } - fmt.Fprintln(t.OutOrStdout(), line) + return answer, nil } -// PrintObject prints the given object -func (t *DefaultTerminal) PrintObject(object runtime.Object, title string) error { - toPrint := object.DeepCopyObject() - toPrintMeta, err := meta.Accessor(toPrint) +func (t Terminal) PrintObject(title string, object runtime.Object) error { + t.Info(title) + r, err := glamour.NewTermRenderer( + // detect background color and pick either the default dark or light theme + glamour.WithAutoStyle(), + glamour.WithWordWrap(120), + glamour.WithStylesFromJSONBytes([]byte(` + { + "document": { + "margin": 0, + "block_prefix": "", + "block_suffix": "", + "prefix": "" + }, + "code_block": { + "margin": 0, + "block_prefix": "", + "block_suffix": "", + "prefix": "", + "suffix": "" + } + }`)), + ) + if err != nil { + return err + } + obj := object.DeepCopyObject() + m, err := meta.Accessor(obj) if err != nil { return errs.Wrapf(err, "cannot get metadata from %+v", object) } - toPrintMeta.SetManagedFields(nil) - result, err := yaml.Marshal(toPrint) + m.SetManagedFields(nil) + result, err := yaml.Marshal(obj) if err != nil { return errs.Wrapf(err, "unable to unmarshal %+v", object) } - t.PrintContextSeparatorWithBodyf(string(result), "%s", title) + o, err := r.Render(fmt.Sprintf("```yaml\n%s\n```", string(result))) + if err != nil { + return err + } + t.Print(o) return nil -} - -func WithDangerZoneMessagef(consequence, action string, args ...interface{}) ConfirmationMessage { - return ConfirmationMessage(fmt.Sprintf(` -################################### -#### #### -#### !!! DANGER ZONE !!! #### -#### #### -################################### - -THIS COMMAND WILL CAUSE %s -%s`, strings.ToUpper(consequence), WithMessagef(action, args...))) -} -func WithMessagef(action string, args ...interface{}) ConfirmationMessage { - return ConfirmationMessage(fmt.Sprintf(` -Are you sure that you want to %s`, fmt.Sprintf(action, args...))) -} - -type ConfirmationMessage string - -func (t *DefaultTerminal) AskForConfirmation(msg ConfirmationMessage) bool { - reader := bufio.NewReader(t.InOrStdin()) - t.Printlnf(string(msg)) - t.Printlnf("===============================") - t.Printf("[y/n] -> ") - text := "" - var err error - if AssumeYes { - text = "y" - } else { - text, err = reader.ReadString('\n') - if err != nil { - log.Fatal("unable to read from input: ", err) - } - } - text = strings.ReplaceAll(text, "\n", "") - t.Printlnf("response: '%s'", text) - switch text { - case "y", "Y": - return true - case "n", "N": - return false - default: - return t.AskForConfirmation("answer y or n") - } } diff --git a/pkg/ioutils/terminal_test.go b/pkg/ioutils/terminal_test.go index 78da6217..94b1c92d 100644 --- a/pkg/ioutils/terminal_test.go +++ b/pkg/ioutils/terminal_test.go @@ -1,109 +1,98 @@ package ioutils_test -import ( - "bytes" - "io" - "testing" - - "github.com/kubesaw/ksctl/pkg/ioutils" - . "github.com/kubesaw/ksctl/pkg/test" - - "github.com/stretchr/testify/assert" -) - -func TestAskForConfirmationWhenAnswerIsY(t *testing.T) { - for _, answer := range []string{"y", "Y"} { - // given - term := NewFakeTerminalWithResponse(answer) - - // when - confirmation := term.AskForConfirmation(ioutils.WithMessagef("do some %s", "action")) - - // then - assert.True(t, confirmation) - output := term.Output() - assert.Contains(t, output, "Are you sure that you want to do some action\n===============================\n[y/n] -> ") - assert.NotContains(t, output, "!!! DANGER ZONE !!!") - } -} - -func TestAskForConfirmationWhenAssumeYesIsTrue(t *testing.T) { - // given - term := NewFakeTerminalWithResponse("n") - ioutils.AssumeYes = true - t.Cleanup(func() { - ioutils.AssumeYes = false - }) - - // when - confirmation := term.AskForConfirmation(ioutils.WithMessagef("do some %s", "action")) - - // then - assert.True(t, confirmation) - output := term.Output() - assert.Contains(t, output, "Are you sure that you want to do some action\n===============================\n[y/n] -> ") - assert.Contains(t, output, "[y/n] -> response: 'y'") - assert.NotContains(t, output, "!!! DANGER ZONE !!!") -} - -func TestAskForConfirmationWhenAnswerIsNWithDangerZone(t *testing.T) { - for _, answer := range []string{"n", "N"} { - // given - term := NewFakeTerminalWithResponse(answer) - - // when - confirmation := term.AskForConfirmation(ioutils.WithDangerZoneMessagef("a consequence", "do some %s", "action")) - - // then - assert.False(t, confirmation) - output := term.Output() - assert.Contains(t, output, "!!! DANGER ZONE !!!") - assert.Contains(t, output, "THIS COMMAND WILL CAUSE A CONSEQUENCE") - assert.Contains(t, output, "Are you sure that you want to do some action\n===============================\n[y/n] -> ") - } -} - -func TestAskForConfirmationWhenFirstAnswerIsWrong(t *testing.T) { - // given - createTerm := func(correctAnswer string) ioutils.Terminal { - counter := 0 - return ioutils.NewTerminal( - func() io.Reader { - in := bytes.NewBuffer(nil) - if counter == 0 { - in.WriteString("bla") - counter++ - } else { - in.WriteString(correctAnswer) - } - in.WriteByte('\n') - return in - }, - func() io.Writer { - return bytes.NewBuffer(nil) - }, - ) - } - - t.Run("second answer is y", func(t *testing.T) { - // given - term := createTerm("y") - - // when - confirmation := term.AskForConfirmation(ioutils.WithMessagef("do some %s", "action")) - - // then - assert.True(t, confirmation) - }) - - t.Run("second answer is n", func(t *testing.T) { - // given - term := createTerm("n") - - // when - confirmation := term.AskForConfirmation(ioutils.WithMessagef("do some %s", "action")) - - // then - assert.False(t, confirmation) - }) -} +// func TestAskForConfirmationWhenAnswerIsY(t *testing.T) { +// for _, answer := range []string{"y", "Y"} { +// // given +// term := NewFakeTerminalWithResponse(answer) + +// // when +// confirmation := term.AskForConfirmation(ioutils.WithMessagef("do some %s", "action")) + +// // then +// assert.True(t, confirmation) +// output := term.Output() +// assert.Contains(t, output, "Are you sure that you want to do some action\n===============================\n[y/n] -> ") +// assert.NotContains(t, output, "!!! DANGER ZONE !!!") +// } +// } + +// func TestAskForConfirmationWhenAssumeYesIsTrue(t *testing.T) { +// // given +// term := NewFakeTerminalWithResponse("n") +// ioutils.AssumeYes = true +// t.Cleanup(func() { +// ioutils.AssumeYes = false +// }) + +// // when +// confirmation := term.AskForConfirmation(ioutils.WithMessagef("do some %s", "action")) + +// // then +// assert.True(t, confirmation) +// output := term.Output() +// assert.Contains(t, output, "Are you sure that you want to do some action\n===============================\n[y/n] -> ") +// assert.Contains(t, output, "[y/n] -> response: 'y'") +// assert.NotContains(t, output, "!!! DANGER ZONE !!!") +// } + +// func TestAskForConfirmationWhenAnswerIsNWithDangerZone(t *testing.T) { +// for _, answer := range []string{"n", "N"} { +// // given +// term := NewFakeTerminalWithResponse(answer) + +// // when +// confirmation := term.AskForConfirmation(ioutils.WithDangerZoneMessagef("a consequence", "do some %s", "action")) + +// // then +// assert.False(t, confirmation) +// output := term.Output() +// assert.Contains(t, output, "!!! DANGER ZONE !!!") +// assert.Contains(t, output, "THIS COMMAND WILL CAUSE A CONSEQUENCE") +// assert.Contains(t, output, "Are you sure that you want to do some action\n===============================\n[y/n] -> ") +// } +// } + +// func TestAskForConfirmationWhenFirstAnswerIsWrong(t *testing.T) { +// // given +// createTerm := func(correctAnswer string) ioutils.Terminal { +// counter := 0 +// return ioutils.NewTerminal( +// func() io.Reader { +// in := bytes.NewBuffer(nil) +// if counter == 0 { +// in.WriteString("bla") +// counter++ +// } else { +// in.WriteString(correctAnswer) +// } +// in.WriteByte('\n') +// return in +// }, +// func() io.Writer { +// return bytes.NewBuffer(nil) +// }, +// ) +// } + +// t.Run("second answer is y", func(t *testing.T) { +// // given +// term := createTerm("y") + +// // when +// confirmation := term.AskForConfirmation(ioutils.WithMessagef("do some %s", "action")) + +// // then +// assert.True(t, confirmation) +// }) + +// t.Run("second answer is n", func(t *testing.T) { +// // given +// term := createTerm("n") + +// // when +// confirmation := term.AskForConfirmation(ioutils.WithMessagef("do some %s", "action")) + +// // then +// assert.False(t, confirmation) +// }) +// } diff --git a/pkg/test/fake_terminal.go b/pkg/test/fake_terminal.go index fe3422dc..63d1f730 100644 --- a/pkg/test/fake_terminal.go +++ b/pkg/test/fake_terminal.go @@ -1,65 +1,58 @@ package test -import ( - "bytes" - "io" +// // FakeTerminal a fake terminal, which can: +// // - be configured to always return the same response when the command prompts the user for confirmation +// // - capture the command output +// type FakeTerminal struct { +// out *bytes.Buffer +// ioutils.Terminal +// } - "github.com/kubesaw/ksctl/pkg/ioutils" -) +// // NewFakeTerminal returns a new FakeTerminal which will +// // only print messages in the console +// func NewFakeTerminal() *FakeTerminal { +// out := bytes.NewBuffer(nil) +// term := &FakeTerminal{ +// out: out, +// Terminal: ioutils.NewTerminal(nil, func() io.Writer { +// return out +// }), +// } +// return term +// } -// FakeTerminal a fake terminal, which can: -// - be configured to always return the same response when the command prompts the user for confirmation -// - capture the command output -type FakeTerminal struct { - out *bytes.Buffer - ioutils.Terminal -} +// // Tee uses the given `out` as a secondary output. +// // Usage: `Tee(os.Stdout)` to see in the console what's record in this terminal during the tests +// // Note: it should be configured at the beginning of a test +// func (t *FakeTerminal) Tee(out io.Writer) { +// t.Terminal = ioutils.NewTerminal(t.Terminal.InOrStdin, func() io.Writer { +// return io.MultiWriter(t.out, out) +// }) +// } -// NewFakeTerminal returns a new FakeTerminal which will -// only print messages in the console -func NewFakeTerminal() *FakeTerminal { - out := bytes.NewBuffer(nil) - term := &FakeTerminal{ - out: out, - Terminal: ioutils.NewTerminal(nil, func() io.Writer { - return out - }), - } - return term -} +// // NewFakeTerminalWithResponse returns a new FakeTerminal which will +// // print messages in the console and respond to the questions/confirmations +// // with the given response +// func NewFakeTerminalWithResponse(response string) *FakeTerminal { +// out := bytes.NewBuffer(nil) +// term := &FakeTerminal{ +// out: out, +// Terminal: ioutils.NewTerminal( +// func() io.Reader { +// in := bytes.NewBuffer(nil) +// in.WriteString(response) +// in.WriteByte('\n') +// return in +// }, +// func() io.Writer { +// return out +// }, +// ), +// } +// return term +// } -// Tee uses the given `out` as a secondary output. -// Usage: `Tee(os.Stdout)` to see in the console what's record in this terminal during the tests -// Note: it should be configured at the beginning of a test -func (t *FakeTerminal) Tee(out io.Writer) { - t.Terminal = ioutils.NewTerminal(t.Terminal.InOrStdin, func() io.Writer { - return io.MultiWriter(t.out, out) - }) -} - -// NewFakeTerminalWithResponse returns a new FakeTerminal which will -// print messages in the console and respond to the questions/confirmations -// with the given response -func NewFakeTerminalWithResponse(response string) *FakeTerminal { - out := bytes.NewBuffer(nil) - term := &FakeTerminal{ - out: out, - Terminal: ioutils.NewTerminal( - func() io.Reader { - in := bytes.NewBuffer(nil) - in.WriteString(response) - in.WriteByte('\n') - return in - }, - func() io.Writer { - return out - }, - ), - } - return term -} - -// Output return the content of the output buffer -func (t *FakeTerminal) Output() string { - return t.out.String() -} +// // Output return the content of the output buffer +// func (t *FakeTerminal) Output() string { +// return t.out.String() +// }