diff --git a/cmd/cli/cmd/install.go b/cmd/cli/cmd/install.go index 73e28433b..5ebb8d944 100644 --- a/cmd/cli/cmd/install.go +++ b/cmd/cli/cmd/install.go @@ -54,7 +54,7 @@ func NewInstall() *cobra.Command { 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.ChartName, "chart-name", install.HelmChartName, "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") diff --git a/internal/cli/install/config.go b/internal/cli/install/config.go index 5bbcf62e4..addf52670 100644 --- a/internal/cli/install/config.go +++ b/internal/cli/install/config.go @@ -19,8 +19,10 @@ const ( ReleaseName = "botkube" // HelmRepoStable URL of the stable Botkube Helm charts repository. HelmRepoStable = "https://charts.botkube.io/" + // HelmChartName represents Botkube Helm chart name in a given Helm repository. + HelmChartName = "botkube" // LocalChartsPath path to Helm charts in botkube repository. - LocalChartsPath = "./helm/botkube/" + LocalChartsPath = "./helm/" ) // Config holds parameters for Botkube installation on cluster. diff --git a/internal/cli/install/helm/install.go b/internal/cli/install/helm/install.go index e41eae491..7eae546a3 100644 --- a/internal/cli/install/helm/install.go +++ b/internal/cli/install/helm/install.go @@ -5,7 +5,7 @@ import ( "errors" "fmt" "os" - "path" + "strings" "time" "github.com/AlecAivazis/survey/v2" @@ -20,6 +20,7 @@ import ( "k8s.io/cli-runtime/pkg/genericclioptions" "k8s.io/client-go/rest" + "github.com/kubeshop/botkube/internal/cli" "github.com/kubeshop/botkube/internal/cli/install/iox" "github.com/kubeshop/botkube/internal/cli/printer" "github.com/kubeshop/botkube/internal/ptr" @@ -116,7 +117,8 @@ func (c *Helm) getChart(repoLocation string, chartName string, version string) ( } if isLocalDir(repoLocation) { - location = path.Join(repoLocation, chartName) + repoLocation = strings.TrimSuffix(repoLocation, "/") + location = fmt.Sprintf("%s/%s", repoLocation, chartName) chartOptions.RepoURL = "" } @@ -192,7 +194,11 @@ func getConfiguration(k8sCfg *rest.Config, forNamespace string) (*action.Configu } debugLog := func(format string, v ...interface{}) { - // noop + if cli.VerboseMode.IsTracing() { + fmt.Print(" Helm log: ") // if enabled, we need to nest that under Helm step which was already printed with 2 spaces. + fmt.Printf(format, v...) + fmt.Println() + } } err := actionConfig.Init(helmCfg, forNamespace, helmDriver, debugLog) diff --git a/internal/cli/install/install.go b/internal/cli/install/install.go index d6ff5f1ad..ebf33b15e 100644 --- a/internal/cli/install/install.go +++ b/internal/cli/install/install.go @@ -78,16 +78,16 @@ func Install(ctx context.Context, w io.Writer, k8sCfg *kubex.ConfigWithMeta, opt return err } + timeBeforeInstall := time.Now() parallel, _ := errgroup.WithContext(ctxWithTimeout) podScheduledIndicator := make(chan string) podWaitResult := make(chan error, 1) parallel.Go(func() error { - err := kubex.WaitForPod(ctxWithTimeout, clientset, opts.HelmParams.Namespace, opts.HelmParams.ReleaseName, kubex.PodReady(podScheduledIndicator, time.Now())) + err := kubex.WaitForPod(ctxWithTimeout, clientset, opts.HelmParams.Namespace, opts.HelmParams.ReleaseName, kubex.PodReady(podScheduledIndicator, timeBeforeInstall)) podWaitResult <- err return nil }) - rel, err := helmInstaller.Install(ctxWithTimeout, status, opts.HelmParams) if err != nil { return err @@ -107,7 +107,7 @@ func Install(ctx context.Context, w io.Writer, k8sCfg *kubex.ConfigWithMeta, opt select { case podName = <-podScheduledIndicator: status.End(true) - case <-time.After(opts.Timeout): + case <-ctxWithTimeout.Done(): return fmt.Errorf("Timed out waiting for Pod") } @@ -123,10 +123,6 @@ func Install(ctx context.Context, w io.Writer, k8sCfg *kubex.ConfigWithMeta, opt podName, ) - parallel.Go(func() error { - logsPrinter.Start(ctxWithTimeout, status) - return nil - }) parallel.Go(func() error { for { select { @@ -140,6 +136,8 @@ func Install(ctx context.Context, w io.Writer, k8sCfg *kubex.ConfigWithMeta, opt } }) + status.InfoWithBody("Streaming logs...", indent.String(fmt.Sprintf("Pod: %s\n", podName), 4)) + parallel.Go(func() error { for { select { @@ -147,10 +145,10 @@ func Install(ctx context.Context, w io.Writer, k8sCfg *kubex.ConfigWithMeta, opt return ctxWithTimeout.Err() case entry, ok := <-messages: if !ok { - logsPrinter.Stop() + status.Infof("Finished logs streaming") return nil } - logsPrinter.AppendLogEntry(string(entry)) + logsPrinter.PrintLine(string(entry)) } } }) @@ -204,8 +202,9 @@ var failedInstallGoTpl = ` │ │ kubectl logs -n {{ .Namespace }} pod/{{ .PodName }} - │ To receive assistance, please join our Slack community at {{ .SlackURL | Underline | Blue }}. - │ We'll be glad to help you get Botkube up and running! + │ To resolve the issue, see Botkube documentation on {{ .DocsURL | Underline | Blue }}. + | If that doesn't help, join our Slack community at {{ .SlackURL | Underline | Blue }}. + │ We'll be glad to get your Botkube up and running! ` func printFailedInstallMessage(version string, namespace string, name string, w io.Writer) error { @@ -213,6 +212,7 @@ func printFailedInstallMessage(version string, namespace string, name string, w props := map[string]string{ "SlackURL": "https://join.botkube.io", + "DocsURL": "https://docs.botkube.io", "Version": version, "Namespace": namespace, "PodName": name, diff --git a/internal/cli/install/logs/json_parser.go b/internal/cli/install/logs/json_parser.go index 49bdcf481..85f32ddb6 100644 --- a/internal/cli/install/logs/json_parser.go +++ b/internal/cli/install/logs/json_parser.go @@ -32,7 +32,7 @@ func (j *JSONParser) ParseLineIntoCharm(line string) ([]any, charmlog.Level) { sort.Strings(keys) for _, k := range keys { switch k { - case "level", "msg", "time": // already process + case "level", "msg", "time": // already processed continue case "component", "url": if !cli.VerboseMode.IsEnabled() { diff --git a/internal/cli/install/logs/printer.go b/internal/cli/install/logs/printer.go index dc8d51947..f9855c6ae 100644 --- a/internal/cli/install/logs/printer.go +++ b/internal/cli/install/logs/printer.go @@ -1,20 +1,17 @@ package logs import ( - "context" "fmt" "os" - "strings" "github.com/charmbracelet/log" charmlog "github.com/charmbracelet/log" "github.com/morikuni/aec" - "github.com/muesli/reflow/indent" "github.com/kubeshop/botkube/internal/cli" - "github.com/kubeshop/botkube/internal/cli/printer" ) +// Printer knows how to print Botkube logs. type Printer struct { podName string newLog chan string @@ -36,55 +33,23 @@ func NewPrinter(podName string) *Printer { } } -// Start starts the log streaming process. -func (f *Printer) Start(ctx context.Context, status *printer.StatusPrinter) { - status.InfoWithBody("Streaming logs...", indent.String(fmt.Sprintf("Pod: %s", f.podName), 4)) - fmt.Println() - - for { - select { - case <-f.stop: - return - case <-ctx.Done(): - status.Infof("Requested logs streaming cancel...") - return - case entry := <-f.newLog: - f.printLogs(entry) - } - } -} - -// AppendLogEntry appends a log entry to the printer. -func (f *Printer) AppendLogEntry(entry string) { - if strings.TrimSpace(entry) == "" { - return - } - select { - case f.newLog <- entry: - default: - } -} - -// Stop stops the printer. -func (f *Printer) Stop() { - close(f.stop) -} - -func (f *Printer) printLogs(item string) { - fields, lvl := f.parser.ParseLineIntoCharm(item) - if fields == nil { - f.printLogLine(item) +func (f *Printer) PrintLine(line string) { + fields, lvl := f.parser.ParseLineIntoCharm(line) + if fields == nil { // it was not recognized as JSON log entry, so let's print it as plain text. + f.printLogLine(line) return } if lvl == charmlog.DebugLevel && !cli.VerboseMode.IsEnabled() { return } + fmt.Print(aec.EraseLine(aec.EraseModes.Tail)) fmt.Print(aec.Column(6)) f.logger.With(fields...).Print(nil) } func (f *Printer) printLogLine(line string) { + fmt.Print(aec.EraseLine(aec.EraseModes.Tail)) fmt.Print(aec.Column(6)) fmt.Print(line) } diff --git a/internal/cli/printer/status.go b/internal/cli/printer/status.go index be4cb5970..e240982ed 100644 --- a/internal/cli/printer/status.go +++ b/internal/cli/printer/status.go @@ -7,6 +7,7 @@ import ( "time" "github.com/fatih/color" + "github.com/morikuni/aec" "k8s.io/apimachinery/pkg/util/duration" "github.com/kubeshop/botkube/internal/cli" @@ -105,6 +106,7 @@ func (s *StatusPrinter) Infof(format string, a ...interface{}) { // Ensure that previously started step is finished. Without that we will mess up our output. s.End(true) + fmt.Fprint(s.w, aec.Column(0)) fmt.Fprintf(s.w, " • %s\n", fmt.Sprintf(format, a...)) } @@ -118,6 +120,7 @@ func (s *StatusPrinter) Debugf(format string, a ...interface{}) { // Ensure that previously started step is finished. Without that we will mess up our output. s.End(true) + fmt.Fprint(s.w, aec.Column(0)) fmt.Fprintf(s.w, " • %s\n", fmt.Sprintf(format, a...)) } @@ -126,5 +129,6 @@ func (s *StatusPrinter) InfoWithBody(header, body string) { // Ensure that previously started step is finished. Without that we will mess up our output. s.End(true) + fmt.Fprint(s.w, aec.Column(0)) fmt.Fprintf(s.w, " • %s\n%s", header, body) } diff --git a/internal/kubex/wait.go b/internal/kubex/wait.go index 926eb30ef..477b75ef6 100644 --- a/internal/kubex/wait.go +++ b/internal/kubex/wait.go @@ -37,7 +37,7 @@ var ( errPodRestartedWithError = errors.New("pod restarted with non zero exit code") ) -// PodReady returns true if the Pod is read. +// PodReady returns true if the Pod is ready. func PodReady(podScheduledIndicator chan string, since time.Time) func(event watch.Event) (bool, error) { informed := false sinceK8sTime := metav1.NewTime(since) diff --git a/pkg/config/testdata/TestLoadConfigSuccess/config.golden.yaml b/pkg/config/testdata/TestLoadConfigSuccess/config.golden.yaml index 709e917c5..717a7f331 100644 --- a/pkg/config/testdata/TestLoadConfigSuccess/config.golden.yaml +++ b/pkg/config/testdata/TestLoadConfigSuccess/config.golden.yaml @@ -156,6 +156,7 @@ settings: log: level: error disableColors: false + formatter: "" informersResyncPeriod: 30m0s kubeconfig: kubeconfig-from-env saCredentialsPathPrefix: "" diff --git a/pkg/execute/config_test.go b/pkg/execute/config_test.go index b3eee52a9..bd036ff2e 100644 --- a/pkg/execute/config_test.go +++ b/pkg/execute/config_test.go @@ -64,6 +64,7 @@ func TestConfigExecutorShowConfig(t *testing.T) { log: level: "" disableColors: false + formatter: "" informersResyncPeriod: 0s kubeconfig: "" saCredentialsPathPrefix: ""