From 096f50e985c1ba0c74dbbba54283f3d2b73a8b36 Mon Sep 17 00:00:00 2001 From: Mateusz Szostok Date: Mon, 10 Jul 2023 18:50:50 +0200 Subject: [PATCH] Add enhancements --- cmd/cli/cmd/install.go | 20 +++-- go.mod | 12 ++- go.sum | 22 ++++-- internal/cli/install/config.go | 4 +- internal/cli/install/helm/config.go | 50 ++++--------- internal/cli/install/helm/install.go | 85 +++++++++++---------- internal/cli/install/helm/status.go | 43 ++++++++--- internal/cli/install/helm/version.go | 7 -- internal/cli/install/install.go | 108 +++++++++++++++++++-------- internal/cli/install/iox/stdout.go | 38 ++++++++++ internal/cli/printer/status.go | 13 ++++ internal/kubex/config.go | 89 ++++++++++++++++++++++ 12 files changed, 349 insertions(+), 142 deletions(-) create mode 100644 internal/cli/install/iox/stdout.go create mode 100644 internal/kubex/config.go diff --git a/cmd/cli/cmd/install.go b/cmd/cli/cmd/install.go index 7ab3d4ebdf..f4e5ca0a4d 100644 --- a/cmd/cli/cmd/install.go +++ b/cmd/cli/cmd/install.go @@ -7,11 +7,11 @@ import ( "github.com/pkg/errors" "github.com/spf13/cobra" - "sigs.k8s.io/controller-runtime/pkg/client/config" "github.com/kubeshop/botkube/internal/cli" "github.com/kubeshop/botkube/internal/cli/heredoc" "github.com/kubeshop/botkube/internal/cli/install" + "github.com/kubeshop/botkube/internal/kubex" ) // NewInstall returns a cobra.Command for installing Botkube. @@ -32,30 +32,36 @@ func NewInstall() *cobra.Command { # Install Botkube from local git repository. Needs to be run from the main directory. install --repo @local`, cli.Name), RunE: func(cmd *cobra.Command, args []string) error { - k8sCfg, err := config.GetConfig() // fixme kubex + config, err := kubex.LoadRestConfigWithMetaInformation() + if err != nil { + return err + } if err != nil { return errors.Wrap(err, "while creating k8s config") } - return install.Install(cmd.Context(), os.Stdout, k8sCfg, opts) + return install.Install(cmd.Context(), os.Stdout, config, opts) }, } flags := installCmd.Flags() + kubex.RegisterKubeconfigFlag(flags) + // common params for install and upgrade operation - flags.StringVar(&opts.HelmParams.Version, "version", install.LatestVersionTag, "Botkube version. Possible values @latest, 0.3.0, ...") + flags.StringVar(&opts.HelmParams.Version, "version", install.LatestVersionTag, "Botkube version. Possible values @latest, 1.2.0, ...") flags.StringVar(&opts.HelmParams.Namespace, "namespace", install.Namespace, "Botkube installation namespace.") flags.StringVar(&opts.HelmParams.ReleaseName, "release-name", install.ReleaseName, "Botkube Helm chart release name.") + flags.StringVar(&opts.HelmParams.ChartName, "chart-name", "botkube", "Botkube Helm chart name.") flags.StringVar(&opts.HelmParams.RepoLocation, "repo", install.HelmRepoStable, fmt.Sprintf("Botkube Helm chart repository location. It can be relative path to current working directory or URL. Use %s tag to select repository which holds the stable Helm chart versions.", install.StableVersionTag)) flags.BoolVar(&opts.HelmParams.DryRun, "dry-run", false, "Simulate an install") flags.BoolVar(&opts.HelmParams.Force, "force", false, "Force resource updates through a replacement strategy") flags.BoolVar(&opts.HelmParams.DisableHooks, "no-hooks", false, "Disable pre/post install/upgrade hooks") flags.BoolVar(&opts.HelmParams.DisableOpenAPIValidation, "disable-openapi-validation", false, "If set, it will not validate rendered templates against the Kubernetes OpenAPI Schema") flags.BoolVar(&opts.HelmParams.SkipCRDs, "skip-crds", false, "If set, no CRDs will be installed.") - flags.DurationVar(&opts.HelmParams.Timeout, "timeout", 300*time.Second, "time to wait for any individual Kubernetes operation (like Jobs for hooks)") - flags.BoolVar(&opts.HelmParams.Wait, "wait", false, "If set, will wait until all Pods, PVCs, Services, and minimum number of Pods of a Deployment, StatefulSet, or ReplicaSet are in a ready state before marking the release as successful. It will wait for as long as --timeout") - flags.BoolVar(&opts.HelmParams.WaitForJobs, "wait-for-jobs", false, "If set and --wait enabled, will wait until all Jobs have been completed before marking the release as successful. It will wait for as long as --timeout") + flags.DurationVar(&opts.HelmParams.Timeout, "timeout", 5*time.Minute, "time to wait for any individual Kubernetes operation (like Jobs for hooks)") + flags.BoolVar(&opts.HelmParams.Wait, "wait", true, "If set, will wait until all Pods, PVCs, Services, and minimum number of Pods of a Deployment, StatefulSet, or ReplicaSet are in a ready state before marking the release as successful. It will wait for as long as --timeout") + flags.BoolVar(&opts.HelmParams.WaitForJobs, "wait-for-jobs", true, "If set and --wait enabled, will wait until all Jobs have been completed before marking the release as successful. It will wait for as long as --timeout") flags.BoolVar(&opts.HelmParams.Atomic, "atomic", false, "If set, process rolls back changes made in case of failed install/upgrade. The --wait flag will be set automatically if --atomic is used") flags.BoolVar(&opts.HelmParams.SubNotes, "render-subchart-notes", false, "If set, render subchart notes along with the parent") flags.StringVar(&opts.HelmParams.Description, "description", "", "add a custom description") diff --git a/go.mod b/go.mod index 2132630f51..d116df2bd2 100644 --- a/go.mod +++ b/go.mod @@ -11,7 +11,7 @@ require ( github.com/aws/aws-sdk-go v1.44.122 github.com/briandowns/spinner v1.23.0 github.com/bwmarrin/discordgo v0.25.0 - github.com/dustin/go-humanize v1.0.0 + github.com/dustin/go-humanize v1.0.1 github.com/fatih/color v1.13.0 github.com/go-playground/locales v0.14.0 github.com/go-playground/universal-translator v0.18.0 @@ -46,12 +46,13 @@ require ( github.com/sha1sum/aws_signing_client v0.0.0-20200229211254-f7815c59d5c1 github.com/sirupsen/logrus v1.9.0 github.com/slack-go/slack v0.12.2 - github.com/spf13/cobra v1.6.1 + github.com/spf13/cobra v1.7.0 github.com/spf13/pflag v1.0.5 github.com/spiffe/spire v1.5.3 github.com/stretchr/testify v1.8.4 github.com/vrischmann/envconfig v1.3.0 github.com/xeipuuv/gojsonschema v1.2.0 + go.szostok.io/version v1.2.0 golang.org/x/exp v0.0.0-20230307190834-24139beb5833 golang.org/x/oauth2 v0.5.0 golang.org/x/sync v0.2.0 @@ -87,6 +88,7 @@ require ( github.com/Masterminds/squirrel v1.5.4 // indirect github.com/alexflint/go-scalar v1.1.0 // indirect github.com/asaskevich/govalidator v0.0.0-20200428143746-21a406dcc535 // indirect + github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d // indirect github.com/blang/semver v3.5.1+incompatible // indirect @@ -143,7 +145,7 @@ require ( github.com/hashicorp/go-safetemp v1.0.0 // indirect github.com/hashicorp/yamux v0.0.0-20211028200310-0bc27b27de87 // indirect github.com/imdario/mergo v0.3.13 // indirect - github.com/inconshreveable/mousetrap v1.0.1 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/jmespath/go-jmespath v0.4.0 // indirect github.com/jmoiron/sqlx v1.3.5 // indirect github.com/josharian/intern v1.0.0 // indirect @@ -162,13 +164,14 @@ require ( github.com/lestrrat-go/option v1.0.1 // indirect github.com/lib/pq v1.10.9 // indirect 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.7 // indirect github.com/mattermost/go-i18n v1.11.1-0.20211013152124-5c415071e404 // indirect github.com/mattermost/ldap v0.0.0-20201202150706-ee0e6284187d // indirect github.com/mattermost/logr v1.0.13 // indirect github.com/mattermost/logr/v2 v2.0.15 // indirect github.com/mattn/go-colorable v0.1.13 // indirect - github.com/mattn/go-runewidth v0.0.13 // indirect + github.com/mattn/go-runewidth v0.0.14 // indirect github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b // indirect github.com/minio/md5-simd v1.1.2 // indirect @@ -187,6 +190,7 @@ require ( github.com/modern-go/reflect2 v1.0.2 // indirect github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 // indirect github.com/morikuni/aec v1.0.0 // indirect + github.com/muesli/termenv v0.15.1 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/oklog/run v1.1.0 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect diff --git a/go.sum b/go.sum index 73bb694e15..a0a1ee5b04 100644 --- a/go.sum +++ b/go.sum @@ -390,6 +390,8 @@ github.com/aws/aws-sdk-go-v2/service/sts v1.6.1/go.mod h1:hLZ/AnkIKHLuPGjEiyghNE github.com/aws/aws-sdk-go-v2/service/sts v1.7.2/go.mod h1:8EzeIqfWt2wWT4rJVu3f21TfrhJ8AEMzVybRNSb/b4g= github.com/aws/smithy-go v1.7.0/go.mod h1:SObp3lf9smib00L/v3U2eAKG8FyQ7iLrJnQiAmR5n+E= github.com/aws/smithy-go v1.8.0/go.mod h1:SObp3lf9smib00L/v3U2eAKG8FyQ7iLrJnQiAmR5n+E= +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/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4= github.com/aymerick/raymond v2.0.3-0.20180322193309-b565731e1464+incompatible/go.mod h1:osfaiScAUVup+UC9Nfq76eWqDhXlp+4UYaA8uhTBO6g= github.com/beevik/etree v1.1.0/go.mod h1:r8Aw8JqVegEf0w2fDnATrX9VpkMcyFeM0FhwO62wh+A= @@ -692,8 +694,9 @@ github.com/dsnet/compress v0.0.1/go.mod h1:Aw8dCMJ7RioblQeTqt88akK31OvO8Dhf5Jflh github.com/dsnet/compress v0.0.2-0.20210315054119-f66993602bf5/go.mod h1:qssHWj60/X5sZFNxpG4HBPDHVqxNm4DfnCKgrbZOT+s= github.com/dsnet/golib v0.0.0-20171103203638-1ea166775780/go.mod h1:Lj+Z9rebOhdfkVLjJ8T6VcRQv3SXugXy999NBtR9aFY= github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= -github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo= 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/dvyukov/go-fuzz v0.0.0-20210429054444-fca39067bc72/go.mod h1:11Gm+ccJnvAhCNLlf5+cS9KjtbaD5I5zaZpFMsTHWTw= github.com/dyatlov/go-opengraph v0.0.0-20210112100619-dae8665a5b09 h1:AQLr//nh20BzN3hIWj2+/Gt3FwSs8Nwo/nz4hMIcLPg= github.com/dyatlov/go-opengraph v0.0.0-20210112100619-dae8665a5b09/go.mod h1:nYia/MIs9OyvXXYboPmNOj0gVWo97Wx0sde+ZuKkoM4= @@ -1218,8 +1221,8 @@ github.com/imdario/mergo v0.3.13 h1:lFzP57bqS/wsqKssCGmtLAb8A0wKjLGrve2q3PPVcBk= github.com/imdario/mergo v0.3.13/go.mod h1:4lJ1jqUDcsbIECGy0RUJAXNIhg+6ocWgb1ALK2O4oXg= github.com/imkira/go-interpol v1.1.0/go.mod h1:z0h2/2T3XF8kyEPpRgJ3kmNv+C43p+I/CoI+jC3w2iA= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= -github.com/inconshreveable/mousetrap v1.0.1 h1:U3uMjPSQEBMNp1lFxmllqCPM6P5u/Xq7Pgzkat/bFNc= -github.com/inconshreveable/mousetrap v1.0.1/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/infracloudio/msbotbuilder-go v0.2.5 h1:/FmQPW387kUAiLn7lngcCbBgvuYMUtZhA6Ry0t3R/HY= github.com/infracloudio/msbotbuilder-go v0.2.5/go.mod h1:kdhU1DN2E6cvo7we1EBOAHjU0kaIJPNYzzvdlHh+yhI= github.com/iris-contrib/blackfriday v2.0.0+incompatible/go.mod h1:UzZ2bDEoaSGPbkg6SAB4att1aAwTmVIx/5gCVqeyUdI= @@ -1436,6 +1439,8 @@ github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de h1:9TO3cAIGXtEhnIaL+V+BEER86oLrvS+kWobKpbJuye0= github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de/go.mod h1:zAbeS9B/r2mtpb6U+EI2rYA5OAXxsYw6wTamcNW+zcE= +github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= +github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/lunixbochs/vtclean v1.0.0/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI= github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= @@ -1509,8 +1514,9 @@ github.com/mattn/go-runewidth v0.0.7/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= github.com/mattn/go-runewidth v0.0.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= -github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU= github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU= +github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mattn/go-shellwords v1.0.3/go.mod h1:3xCvwCdWdlDJUrvuMn7Wuy9eWs4pE8vqg+NOMyg4B2o= github.com/mattn/go-shellwords v1.0.12 h1:M2zGm7EW6UQJvDeQxo4T51eKPurbeFbe8WtebGE2xrk= github.com/mattn/go-shellwords v1.0.12/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y= @@ -1611,6 +1617,8 @@ 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/smartcrop v0.2.1-0.20181030220600-548bbf0c0965/go.mod h1:i2fCI/UorTfgEpPPLWiFBv4pye+YAG78RwcQLUkocpI= github.com/muesli/smartcrop v0.3.0/go.mod h1:i2fCI/UorTfgEpPPLWiFBv4pye+YAG78RwcQLUkocpI= +github.com/muesli/termenv v0.15.1 h1:UzuTb/+hhlBugQz28rpzey4ZuKcZ03MeKsoG7IJZIxs= +github.com/muesli/termenv v0.15.1/go.mod h1:HeAQPTzpfs016yGtA4g00CsdYnVLJvxsS4ANqrZs2sQ= 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= @@ -1961,8 +1969,8 @@ github.com/spf13/cobra v1.0.0/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHN github.com/spf13/cobra v1.1.3/go.mod h1:pGADOWyqRD/YMrPZigI/zbliZ2wVD/23d+is3pSWzOo= github.com/spf13/cobra v1.2.1/go.mod h1:ExllRjgxM/piMAM+3tAZvg8fsklGAf3tPfi+i8t68Nk= github.com/spf13/cobra v1.4.0/go.mod h1:Wo4iy3BUC+X2Fybo0PDqwJIv3dNRiZLHQymsfxlB84g= -github.com/spf13/cobra v1.6.1 h1:o94oiPyS4KD1mPy2fmcYYHHfCxLqYjJOhGsCHFZtEzA= -github.com/spf13/cobra v1.6.1/go.mod h1:IOw/AERYS7UzyrGinqmz6HLUo219MORXGxhbaJUqzrY= +github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I= +github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0= github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= github.com/spf13/pflag v0.0.0-20170130214245-9ff6c6923cff/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= @@ -2176,6 +2184,8 @@ go.opentelemetry.io/otel/trace v1.14.0/go.mod h1:8avnQLK+CG77yNLUae4ea2JDQ6iT+go go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= go.starlark.net v0.0.0-20200306205701-8dd3e2ee1dd5 h1:+FNtrFTmVw0YZGpBGX56XDee331t6JAXeK2bcyhLOOc= go.starlark.net v0.0.0-20200306205701-8dd3e2ee1dd5/go.mod h1:nmDLcffg48OtT/PSW0Hg7FvpRQsQh5OSqIylirxKC7o= +go.szostok.io/version v1.2.0 h1:8eMMdfsonjbibwZRLJ8TnrErY8bThFTQsZYV16mcXms= +go.szostok.io/version v1.2.0/go.mod h1:EiU0gPxaXb6MZ+apSN0WgDO6F4JXyC99k9PIXf2k2E8= go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= diff --git a/internal/cli/install/config.go b/internal/cli/install/config.go index 97eb2f1512..c0fd81b92e 100644 --- a/internal/cli/install/config.go +++ b/internal/cli/install/config.go @@ -18,11 +18,11 @@ const ( // HelmRepoStable URL of the stable Botkube Helm charts repository. HelmRepoStable = "https://charts.botkube.io/" // LocalChartsPath path to Helm charts in botkube repository. - LocalChartsPath = "helm/botkube/" + LocalChartsPath = "./helm/botkube/" ) // Config holds parameters for Botkube installation on cluster. type Config struct { - // Helm client opts + Kubeconfig string HelmParams helm.Config } diff --git a/internal/cli/install/helm/config.go b/internal/cli/install/helm/config.go index bc087ba8ae..979b10d70e 100644 --- a/internal/cli/install/helm/config.go +++ b/internal/cli/install/helm/config.go @@ -7,55 +7,37 @@ import ( ) const ( - // RepositoryCache Helm cache for repositories - RepositoryCache = "/tmp/helm" + repositoryCache = "/tmp/helm" helmDriver = "secrets" ) +// Config holds Helm configuration parameters. type Config struct { ReleaseName string ChartName string Version string RepoLocation string - // Namespace is the namespace in which this operation should be performed. - Namespace string - // SkipCRDs skips installing CRDs when install flag is enabled during upgrade - SkipCRDs bool - // Timeout is the timeout for this operation - Timeout time.Duration - // Wait determines whether the wait operation should be performed after the upgrade is requested. - Wait bool - // WaitForJobs determines whether the wait operation for the Jobs should be performed after the upgrade is requested. - WaitForJobs bool - // DisableHooks disables hook processing if set to true. - DisableHooks bool - // DryRun controls whether the operation is prepared, but not executed. - // If `true`, the upgrade is prepared but not performed. - DryRun bool - // Force will, if set to `true`, ignore certain warnings and perform the upgrade anyway. - // - // This should be used with caution. - Force bool - // Atomic, if true, will roll back on failure. - Atomic bool - // SubNotes determines whether sub-notes are rendered in the chart. - SubNotes bool - // Description is the description of this operation - Description string - // DisableOpenAPIValidation controls whether OpenAPI validation is enforced. + Namespace string + SkipCRDs bool + Timeout time.Duration + Wait bool + WaitForJobs bool + DisableHooks bool + DryRun bool + Force bool + Atomic bool + SubNotes bool + Description string DisableOpenAPIValidation bool - // Get missing dependencies - DependencyUpdate bool + DependencyUpdate bool + Values values.Options UpgradeConfig - - Values values.Options } +// UpgradeConfig holds upgrade related settings. type UpgradeConfig struct { - // ResetValues will reset the values to the chart's built-ins rather than merging with existing. ResetValues bool - // ReuseValues will re-use the user's last supplied values. ReuseValues bool } diff --git a/internal/cli/install/helm/install.go b/internal/cli/install/helm/install.go index bc9e24a562..d3f278f674 100644 --- a/internal/cli/install/helm/install.go +++ b/internal/cli/install/helm/install.go @@ -20,13 +20,20 @@ import ( "k8s.io/cli-runtime/pkg/genericclioptions" "k8s.io/client-go/rest" + "github.com/kubeshop/botkube/internal/cli/install/iox" + "github.com/kubeshop/botkube/internal/cli/printer" "github.com/kubeshop/botkube/internal/ptr" ) +// Run provides single function signature both for install and upgrade. +type Run func(ctx context.Context, relName string, chart *chart.Chart, vals map[string]any) (*release.Release, error) + +// Helm provides option to or update install Helm charts. type Helm struct { helmCfg *action.Configuration } +// NewHelm returns a new Helm instance. func NewHelm(k8sCfg *rest.Config, forNamespace string) (*Helm, error) { configuration, err := getConfiguration(k8sCfg, forNamespace) if err != nil { @@ -35,9 +42,8 @@ func NewHelm(k8sCfg *rest.Config, forNamespace string) (*Helm, error) { return &Helm{helmCfg: configuration}, nil } -type Run func(ctx context.Context, relName string, chart *chart.Chart, vals map[string]any) (*release.Release, error) - -func (c *Helm) Install(ctx context.Context, opts Config) (*release.Release, error) { +// Install installs a given Helm chart. +func (c *Helm) Install(ctx context.Context, status *printer.StatusPrinter, opts Config) (*release.Release, error) { histClient := action.NewHistory(c.helmCfg) histClient.Max = 1 _, err := histClient.Run(opts.ReleaseName) @@ -50,7 +56,9 @@ func (c *Helm) Install(ctx context.Context, opts Config) (*release.Release, erro } var upgrade bool - err = survey.AskOne(prompt, &upgrade) + + questionIndent := iox.NewIndentStdoutWriter("?", 1) // we indent questions by 1 space to match the step layout + err = survey.AskOne(prompt, &upgrade, survey.WithStdio(os.Stdin, questionIndent, os.Stderr)) if err != nil { return nil, fmt.Errorf("while confiriming upgrade: %v", err) } @@ -66,7 +74,8 @@ func (c *Helm) Install(ctx context.Context, opts Config) (*release.Release, erro return nil, fmt.Errorf("while getting Helm release history: %v", err) } - loadedChart, err := c.GetChart(opts.RepoLocation, opts.ChartName, opts.Version) + status.Step("Loading %s Helm chart", opts.ChartName) + loadedChart, err := c.getChart(opts.RepoLocation, opts.ChartName, opts.Version) if err != nil { return nil, fmt.Errorf("while loading Helm chart: %v", err) } @@ -77,6 +86,7 @@ func (c *Helm) Install(ctx context.Context, opts Config) (*release.Release, erro return nil, err } + status.Step("Installing %s Helm chart", opts.ChartName) // We may run into in issue temporary network issues. var rel *release.Release err = retry.Do(func() error { @@ -90,6 +100,33 @@ func (c *Helm) Install(ctx context.Context, opts Config) (*release.Release, erro return rel, nil } +func (c *Helm) getChart(repoLocation string, chartName string, version string) (*chart.Chart, error) { + location := chartName + chartOptions := action.ChartPathOptions{ + RepoURL: repoLocation, + Version: version, + } + + if isLocalDir(repoLocation) { + location = path.Join(repoLocation, chartName) + chartOptions.RepoURL = "" + } + + chartPath, err := chartOptions.LocateChart(location, &helmcli.EnvSettings{ + RepositoryCache: repositoryCache, + }) + if err != nil { + return nil, err + } + + chartData, err := loader.Load(chartPath) + if err != nil { + return nil, err + } + + return chartData, nil +} + func (c *Helm) installAction(opts Config) Run { installCli := action.NewInstall(c.helmCfg) @@ -138,39 +175,6 @@ func (c *Helm) upgradeAction(opts Config) Run { } } -func (c *Helm) GetChart(repoLocation string, chartName string, version string) (*chart.Chart, error) { - location := chartName - chartOptions := action.ChartPathOptions{ - RepoURL: repoLocation, - Version: version, - } - - if isLocalDir(repoLocation) { - location = path.Join(repoLocation, chartName) - chartOptions.RepoURL = "" - } - - chartPath, err := chartOptions.LocateChart(location, &helmcli.EnvSettings{ - RepositoryCache: RepositoryCache, - }) - if err != nil { - return nil, err - } - - chartData, err := loader.Load(chartPath) - if err != nil { - return nil, err - } - - return chartData, nil -} - -func isLocalDir(in string) bool { - f, err := os.Stat(in) - return err == nil && f.IsDir() -} - -// getConfiguration returns Helm action.Configuration. func getConfiguration(k8sCfg *rest.Config, forNamespace string) (*action.Configuration, error) { actionConfig := new(action.Configuration) helmCfg := &genericclioptions.ConfigFlags{ @@ -192,3 +196,8 @@ func getConfiguration(k8sCfg *rest.Config, forNamespace string) (*action.Configu return actionConfig, nil } + +func isLocalDir(in string) bool { + f, err := os.Stat(in) + return err == nil && f.IsDir() +} diff --git a/internal/cli/install/helm/status.go b/internal/cli/install/helm/status.go index 925f27a545..b360921b1e 100644 --- a/internal/cli/install/helm/status.go +++ b/internal/cli/install/helm/status.go @@ -1,29 +1,48 @@ package helm import ( - "bytes" "fmt" "time" + "github.com/muesli/reflow/indent" + "go.szostok.io/version/style" "helm.sh/helm/v3/pkg/release" + + "github.com/kubeshop/botkube/internal/cli/printer" ) -// GetStringStatusFromRelease returns release description similar to what Helm does, +var releaseGoTpl = ` + {{ Key "Name" }} {{ .Name | Val }} + {{ Key "Namespace" }} {{ .Namespace | Val }} + {{ Key "Last Deployed" }} {{ .LastDeployed | FmtDate | Val }} + {{ Key "Revision" }} {{ .Revision | Val }} + {{ Key "Description" }} {{ .Description | Val }} +` + +// PrintReleaseStatus returns release description similar to what Helm does, // based on https://github.com/helm/helm/blob/f31d4fb3aacabf6102b3ec9214b3433a3dbf1812/cmd/helm/status.go#L126C1-L138C3 -func GetStringStatusFromRelease(r *release.Release) string { +func PrintReleaseStatus(status *printer.StatusPrinter, r *release.Release) error { if r == nil { - return "" + return nil } - var buff bytes.Buffer - buff.WriteString(fmt.Sprintf("NAME: %s\n", r.Name)) + renderer := style.NewGoTemplateRender(style.DefaultConfig(releaseGoTpl)) + + properties := make(map[string]string) + properties["Name"] = r.Name if !r.Info.LastDeployed.IsZero() { - buff.WriteString(fmt.Sprintf("LAST DEPLOYED: %s\n", r.Info.LastDeployed.Format(time.ANSIC))) + properties["LastDeployed"] = r.Info.LastDeployed.Format(time.ANSIC) + } + properties["Namespace"] = r.Namespace + properties["Status"] = r.Info.Status.String() + properties["Revision"] = fmt.Sprintf("%d", r.Version) + properties["Description"] = r.Info.Description + + desc, err := renderer.Render(properties, true) + if err != nil { + return err } - buff.WriteString(fmt.Sprintf("NAMESPACE: %s\n", r.Namespace)) - buff.WriteString(fmt.Sprintf("STATUS: %s\n", r.Info.Status.String())) - buff.WriteString(fmt.Sprintf("REVISION: %d\n", r.Version)) - buff.WriteString(fmt.Sprintf("DESCRIPTION: %s\n", r.Info.Description)) - return buff.String() + status.InfoWithBody("Release details:", indent.String(desc, 4)) + return nil } diff --git a/internal/cli/install/helm/version.go b/internal/cli/install/helm/version.go index c8ea0592a0..3d1272e0b1 100644 --- a/internal/cli/install/helm/version.go +++ b/internal/cli/install/helm/version.go @@ -53,10 +53,3 @@ func GetLatestVersion(repoURL string, chart string) (string, error) { return entry[0].Version, nil } - -// ValuesFromString converts yaml string into map[string]interface{} -func ValuesFromString(values string) (map[string]interface{}, error) { - v := map[string]interface{}{} - err := yaml.Unmarshal([]byte(values), &v) - return v, err -} diff --git a/internal/cli/install/install.go b/internal/cli/install/install.go index e28a2f7fcb..4161e25eb8 100644 --- a/internal/cli/install/install.go +++ b/internal/cli/install/install.go @@ -4,8 +4,9 @@ import ( "context" "fmt" "io" - "strings" + "github.com/muesli/reflow/indent" + "go.szostok.io/version/style" corev1 "k8s.io/api/core/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -13,13 +14,13 @@ import ( "k8s.io/client-go/rest" "github.com/kubeshop/botkube/internal/cli" - "github.com/kubeshop/botkube/internal/cli/heredoc" "github.com/kubeshop/botkube/internal/cli/install/helm" "github.com/kubeshop/botkube/internal/cli/printer" + "github.com/kubeshop/botkube/internal/kubex" ) // Install installs Botkube Helm chart into cluster. -func Install(ctx context.Context, w io.Writer, k8sCfg *rest.Config, opts Config) (err error) { +func Install(ctx context.Context, w io.Writer, k8sCfg *kubex.ConfigWithMeta, opts Config) (err error) { status := printer.NewStatus(w, "Installing Botkube on cluster...") defer func() { status.End(err == nil) @@ -27,65 +28,108 @@ func Install(ctx context.Context, w io.Writer, k8sCfg *rest.Config, opts Config) switch opts.HelmParams.RepoLocation { case StableVersionTag: + status.Debugf("Resolved %s tag into %s...", StableVersionTag, HelmRepoStable) opts.HelmParams.RepoLocation = HelmRepoStable - if opts.HelmParams.Version == LatestVersionTag { - ver, err := helm.GetLatestVersion(opts.HelmParams.RepoLocation, opts.HelmParams.ChartName) - if err != nil { - return err - } - opts.HelmParams.Version = ver - } case LocalVersionTag: + status.Debugf("Resolved %s tag into %s...", LocalVersionTag, LocalChartsPath) opts.HelmParams.RepoLocation = LocalChartsPath + opts.HelmParams.Version = "" + } + + if opts.HelmParams.Version == LatestVersionTag { + ver, err := helm.GetLatestVersion(opts.HelmParams.RepoLocation, opts.HelmParams.ChartName) + if err != nil { + return err + } + status.Debugf("Resolved %s tag into %s...", LatestVersionTag, ver) + opts.HelmParams.Version = ver } - if cli.VerboseMode.IsEnabled() { - status.InfoWithBody("Installation details:", installDetails(opts)) + if err = printInstallationDetails(k8sCfg, opts, status); err != nil { + return err } - helmInstaller, err := helm.NewHelm(k8sCfg, opts.HelmParams.Namespace) + helmInstaller, err := helm.NewHelm(k8sCfg.K8s, opts.HelmParams.Namespace) if err != nil { return err } status.Step("Creating namespace %s", opts.HelmParams.Namespace) - err = ensureNamespaceCreated(ctx, k8sCfg, opts.HelmParams.Namespace) + err = ensureNamespaceCreated(ctx, k8sCfg.K8s, opts.HelmParams.Namespace) + status.End(err == nil) if err != nil { return err } - //log.SetOutput(io.Discard) - status.Step("Installing %s Helm chart", opts.HelmParams.ChartName) - rel, err := helmInstaller.Install(ctx, opts.HelmParams) + rel, err := helmInstaller.Install(ctx, status, opts.HelmParams) status.End(err == nil) if err != nil { return err } - if cli.VerboseMode.IsEnabled() { - desc := helm.GetStringStatusFromRelease(rel) - fmt.Fprintln(w, desc) + if err := helm.PrintReleaseStatus(status, rel); err != nil { + return err } - welcomeMessage(w) + return printSuccessInstallMessage(opts.HelmParams.Version, w) +} + +var successInstallGoTpl = ` + + │ Botkube {{ .Version | Bold }} installed successfully! + │ To read more how to use CLI, check out the documentation on {{ .DocsURL | Underline | Blue }} +` + +func printSuccessInstallMessage(version string, w io.Writer) error { + renderer := style.NewGoTemplateRender(style.DefaultConfig(successInstallGoTpl)) + + props := map[string]string{ + "DocsURL": "https://docs.botkube.io/cli/getting-started/#first-use", + "Version": version, + } + + out, err := renderer.Render(props, cli.IsSmartTerminal(w)) + if err != nil { + return err + } + + _, err = fmt.Fprintln(w, out) + if err != nil { + return err + } return nil } -func welcomeMessage(w io.Writer) { - msg := heredoc.Docf(` - Botkube installed successfully! - - To read more how to use CLI, check out the documentation on https://docs.botkube.io/docs/cli/getting-started#first-use.`) - fmt.Fprintln(w, msg) +var infoFieldsGoTpl = `{{ AdjustKeyWidth . }} + {{- range $item := (. | Extra) }} + {{ $item.Key | Key }} {{ $item.Value | Val }} + {{- end}} + +` + +type Custom struct { + // Fields are printed in the same order as defined in struct. + Version string `pretty:"Version"` + HelmRepo string `pretty:"Helm repository"` + K8sCtx string `pretty:"Kubernetes Context"` } -func installDetails(opts Config) string { - out := &strings.Builder{} - fmt.Fprintf(out, "\tVersion: %s\n", opts.HelmParams.Version) - fmt.Fprintf(out, "\tHelm repository: %s\n", opts.HelmParams.RepoLocation) +func printInstallationDetails(cfg *kubex.ConfigWithMeta, opts Config, status *printer.StatusPrinter) error { + renderer := style.NewGoTemplateRender(style.DefaultConfig(infoFieldsGoTpl)) - return out.String() + out, err := renderer.Render(Custom{ + Version: opts.HelmParams.Version, + HelmRepo: opts.HelmParams.RepoLocation, + K8sCtx: cfg.CurrentContext, + }, cli.IsSmartTerminal(status.Writer())) + if err != nil { + return err + } + + status.InfoWithBody("Installation details:", indent.String(out, 4)) + + return nil } // ensureNamespaceCreated creates a k8s namespaces. If it already exists it does nothing. diff --git a/internal/cli/install/iox/stdout.go b/internal/cli/install/iox/stdout.go new file mode 100644 index 0000000000..671eaed76e --- /dev/null +++ b/internal/cli/install/iox/stdout.go @@ -0,0 +1,38 @@ +package iox + +import ( + "os" + "strings" + + "github.com/gookit/color" +) + +// IndentStdoutWriter adds configured indent for messages starting with configured prefix. +type IndentStdoutWriter struct { + triggerPrefix string + indent int +} + +// NewIndentStdoutWriter returns a new IndentStdoutWriter instance. +func NewIndentStdoutWriter(triggerPrefix string, indent int) *IndentStdoutWriter { + return &IndentStdoutWriter{triggerPrefix: triggerPrefix, indent: indent} +} + +// Fd returns the integer Unix file descriptor referencing the open file. +func (s *IndentStdoutWriter) Fd() uintptr { + return os.Stdout.Fd() +} + +// Write writes len(b) bytes from b to os.Stdout. +func (s *IndentStdoutWriter) Write(p []byte) (n int, err error) { + if strings.HasPrefix(color.ClearCode(string(p)), s.triggerPrefix) { + // we add indent only to messages that starts with a known prefix + // as a result we don't alter messages which are terminal special codes, e.g. to clear the screen. + _, err := os.Stdout.Write([]byte(strings.Repeat(" ", s.indent))) + if err != nil { + return 0, err + } + } + + return os.Stdout.Write(p) +} diff --git a/internal/cli/printer/status.go b/internal/cli/printer/status.go index 54f7a98010..bb4085d3d4 100644 --- a/internal/cli/printer/status.go +++ b/internal/cli/printer/status.go @@ -103,6 +103,19 @@ func (s *StatusPrinter) Infof(format string, a ...interface{}) { fmt.Fprintf(s.w, " • %s\n", fmt.Sprintf(format, a...)) } +// Debugf prints a given debug message without spinner animation. +// It prints it only if verbose flag was specified. +func (s *StatusPrinter) Debugf(format string, a ...interface{}) { + if !cli.VerboseMode.IsEnabled() { + return + } + + // Ensure that previously started step is finished. Without that we will mess up our output. + s.End(true) + + fmt.Fprintf(s.w, " • %s\n", fmt.Sprintf(format, a...)) +} + // InfoWithBody prints a given info with a given body and without spinner animation. func (s *StatusPrinter) InfoWithBody(header, body string) { // Ensure that previously started step is finished. Without that we will mess up our output. diff --git a/internal/kubex/config.go b/internal/kubex/config.go new file mode 100644 index 0000000000..009b09ad90 --- /dev/null +++ b/internal/kubex/config.go @@ -0,0 +1,89 @@ +package kubex + +import ( + "fmt" + "os" + "os/user" + "path/filepath" + + "github.com/spf13/pflag" + "k8s.io/client-go/rest" + "k8s.io/client-go/tools/clientcmd" +) + +var kubeconfig string + +// RegisterKubeconfigFlag registers `--kubeconfig` flag. +func RegisterKubeconfigFlag(flags *pflag.FlagSet) { + flags.StringVar(&kubeconfig, clientcmd.RecommendedConfigPathFlag, "", "Paths to a kubeconfig. Only required if out-of-cluster.") +} + +type ConfigWithMeta struct { + K8s *rest.Config + CurrentContext string +} + +// LoadRestConfigWithMetaInformation loads a REST Config. Config precedence: +// +// * --kubeconfig flag pointing at a file +// +// * KUBECONFIG environment variable pointing at a file +// +// * In-cluster config if running in cluster +// +// * $HOME/.kube/config if exists. +// +// code inspired by sigs.k8s.io/controller-runtime@v0.13.1/pkg/client/config/config.go +func LoadRestConfigWithMetaInformation() (*ConfigWithMeta, error) { + // 1. --kubeconfig flag + if kubeconfig != "" { + c := clientcmd.NewNonInteractiveDeferredLoadingClientConfig(&clientcmd.ClientConfigLoadingRules{ExplicitPath: kubeconfig}, nil) + return transform(c) + } + + // 2. KUBECONFIG environment variable pointing at a file + kubeconfigPath := os.Getenv(clientcmd.RecommendedConfigPathEnvVar) + if len(kubeconfigPath) == 0 { + if c, err := rest.InClusterConfig(); err == nil { + return &ConfigWithMeta{ + K8s: c, + CurrentContext: "In cluster", + }, nil + } + } + + // 3. In-cluster config if running in cluster + // 4. $HOME/.kube/config if exists + // 5. user.HomeDir/.kube/config if exists + // + // NOTE: For default config file locations, upstream only checks + // $HOME for the user's home directory, but we can also try + // os/user.HomeDir when $HOME is unset. + // + // TODO(jlanford): could this be done upstream? + loadingRules := clientcmd.NewDefaultClientConfigLoadingRules() + if _, ok := os.LookupEnv("HOME"); !ok { + u, err := user.Current() + if err != nil { + return nil, fmt.Errorf("could not get current user: %w", err) + } + loadingRules.Precedence = append(loadingRules.Precedence, filepath.Join(u.HomeDir, clientcmd.RecommendedHomeDir, clientcmd.RecommendedFileName)) + } + + return transform(clientcmd.NewNonInteractiveDeferredLoadingClientConfig(loadingRules, nil)) +} + +func transform(c clientcmd.ClientConfig) (*ConfigWithMeta, error) { + rawConfig, err := c.RawConfig() + if err != nil { + return nil, err + } + clientConfig, err := c.ClientConfig() + if err != nil { + return nil, err + } + return &ConfigWithMeta{ + K8s: clientConfig, + CurrentContext: rawConfig.CurrentContext, + }, nil +}