From b6678c2e185523b5bd604c1736c6ef18a9122b88 Mon Sep 17 00:00:00 2001 From: Aldo Lacuku Date: Wed, 25 Oct 2023 11:26:26 +0200 Subject: [PATCH] update(output): complete rework of the output system Old flags "--verbose" and "--disable-styling" have been deprecated. Two new flags configure the output system: * --log-level can be one of info, warn debug or trace. * --log-format can be one of color, text, json. The output is done using a logger that is used across all commands. Having a unique logger guarantees a consistent format of the output. Signed-off-by: Aldo Lacuku --- Makefile | 2 +- cmd/artifact/follow/follow.go | 26 +- cmd/artifact/info/info.go | 5 +- cmd/artifact/install/install.go | 27 +- cmd/artifact/install/install_suite_test.go | 3 +- cmd/artifact/install/install_test.go | 24 +- cmd/cli_test.go | 2 +- cmd/index/add/add.go | 11 +- cmd/index/add/add_suite_test.go | 3 +- cmd/index/add/add_test.go | 18 +- cmd/index/remove/remove.go | 12 +- cmd/index/update/update.go | 10 +- cmd/registry/auth/basic/basic.go | 6 +- cmd/registry/auth/basic/basic_suite_test.go | 3 +- cmd/registry/auth/basic/basic_test.go | 26 +- cmd/registry/auth/gcp/gcp.go | 7 +- cmd/registry/auth/oauth/oauth.go | 12 +- cmd/registry/auth/oauth/oauth_suite_test.go | 3 +- cmd/registry/auth/oauth/oauth_test.go | 13 +- cmd/registry/pull/pull.go | 10 +- cmd/registry/pull/pull_suite_test.go | 3 +- cmd/registry/pull/pull_test.go | 20 +- cmd/registry/push/push.go | 7 +- cmd/registry/push/push_suite_test.go | 3 +- cmd/registry/push/push_test.go | 45 ++- cmd/registry/registry.go | 2 + cmd/root.go | 12 +- cmd/testdata/help.txt | 8 +- cmd/testdata/noargsnoflags.txt | 8 +- cmd/testdata/wrongflag.txt | 9 +- cmd/version/version.go | 3 - cmd/version/version_test.go | 1 - go.mod | 2 +- internal/follower/follower.go | 74 ++--- internal/utils/credentials.go | 6 +- main.go | 8 +- pkg/install/tls/generator.go | 8 +- pkg/install/tls/handler.go | 5 +- pkg/options/common.go | 52 +-- pkg/options/enum.go | 58 ++++ pkg/options/enum_test.go | 96 ++++++ pkg/options/logFormat.go | 59 ++++ pkg/options/logFormat_test.go | 88 ++++++ pkg/options/logLevel.go | 61 ++++ pkg/options/logLevel_test.go | 87 +++++ pkg/options/options_suite_test.go | 28 ++ pkg/output/output.go | 161 ++++------ pkg/output/output_test.go | 333 +++++++++++++++++--- pkg/output/tracker.go | 26 +- 49 files changed, 1082 insertions(+), 414 deletions(-) create mode 100644 pkg/options/enum.go create mode 100644 pkg/options/enum_test.go create mode 100644 pkg/options/logFormat.go create mode 100644 pkg/options/logFormat_test.go create mode 100644 pkg/options/logLevel.go create mode 100644 pkg/options/logLevel_test.go create mode 100644 pkg/options/options_suite_test.go diff --git a/Makefile b/Makefile index 5259d529..85d0b8df 100644 --- a/Makefile +++ b/Makefile @@ -34,7 +34,7 @@ test: .PHONY: gci gci: ifeq (, $(shell which gci)) - @go install github.com/daixiang0/gci@v0.9.0 + @go install github.com/daixiang0/gci@v0.11.1 GCI=$(GOBIN)/gci else GCI=$(shell which gci) diff --git a/cmd/artifact/follow/follow.go b/cmd/artifact/follow/follow.go index f9c4cff5..92e7a266 100644 --- a/cmd/artifact/follow/follow.go +++ b/cmd/artifact/follow/follow.go @@ -253,6 +253,7 @@ Examples: // RunArtifactFollow executes the business logic for the artifact follow command. func (o *artifactFollowOptions) RunArtifactFollow(ctx context.Context, args []string) error { + logger := o.Printer.Logger // Retrieve configuration for follower configuredFollower, err := config.Follower() if err != nil { @@ -278,15 +279,13 @@ func (o *artifactFollowOptions) RunArtifactFollow(ctx context.Context, args []st } var wg sync.WaitGroup - // Disable styling - o.Printer.DisableStylingf() // For each artifact create a follower. var followers = make(map[string]*follower.Follower, 0) for _, a := range args { if o.cron != "" { - o.Printer.Info.Printfln("Creating follower for %q, with check using cron %s", a, o.cron) + logger.Info("Creating follower", logger.Args("artifact", a, "cron", o.cron)) } else { - o.Printer.Info.Printfln("Creating follower for %q, with check every %s", a, o.every.String()) + logger.Info("Creating follower", logger.Args("artifact", a, "check every", o.every.String())) } ref, err := o.IndexCache.ResolveReference(a) if err != nil { @@ -305,7 +304,6 @@ func (o *artifactFollowOptions) RunArtifactFollow(ctx context.Context, args []st PluginsDir: o.pluginsDir, ArtifactReference: ref, PlainHTTP: o.PlainHTTP, - Verbose: o.IsVerbose(), CloseChan: o.closeChan, TmpDir: o.tmpDir, FalcoVersions: o.versions, @@ -319,11 +317,9 @@ func (o *artifactFollowOptions) RunArtifactFollow(ctx context.Context, args []st wg.Add(1) followers[ref] = fol } - // Enable styling - o.Printer.EnableStyling() for k, f := range followers { - o.Printer.Info.Printfln("Starting follower for %q", k) + logger.Info("Starting follower", logger.Args("artifact", k)) go f.Follow(ctx) } @@ -331,7 +327,7 @@ func (o *artifactFollowOptions) RunArtifactFollow(ctx context.Context, args []st <-ctx.Done() // We are done, shutdown the followers. - o.Printer.DefaultText.Printfln("closing followers...") + logger.Info("Closing followers...") close(o.closeChan) // Wait for the followers to shutdown or that the timer expires. @@ -344,9 +340,9 @@ func (o *artifactFollowOptions) RunArtifactFollow(ctx context.Context, args []st select { case <-doneChan: - o.Printer.DefaultText.Printfln("followers correctly stopped.") + logger.Info("Followers correctly stopped.") case <-time.After(timeout): - o.Printer.DefaultText.Printfln("Timed out waiting for followers to exit") + logger.Info("Timed out waiting for followers to exit") } return nil @@ -433,11 +429,11 @@ type backoffTransport struct { func (bt *backoffTransport) RoundTrip(req *http.Request) (*http.Response, error) { var err error var resp *http.Response - + logger := bt.Printer.Logger bt.startTime = time.Now() bt.attempts = 0 - bt.Printer.Verbosef("Retrieving versions from Falco (timeout %s) ...", bt.Config.MaxDelay) + logger.Debug(fmt.Sprintf("Retrieving versions from Falco (timeout %s) ...", bt.Config.MaxDelay)) for { resp, err = bt.Base.RoundTrip(req) @@ -452,10 +448,10 @@ func (bt *backoffTransport) RoundTrip(req *http.Request) (*http.Response, error) return resp, fmt.Errorf("timeout occurred while retrieving versions from Falco") } - bt.Printer.Verbosef("error: %s. Trying again in %s", err.Error(), sleep.String()) + logger.Debug(fmt.Sprintf("error: %s. Trying again in %s", err.Error(), sleep.String())) time.Sleep(sleep) } else { - bt.Printer.Verbosef("Successfully retrieved versions from Falco ...") + logger.Debug("Successfully retrieved versions from Falco") return resp, err } diff --git a/cmd/artifact/info/info.go b/cmd/artifact/info/info.go index 91b1a235..4a3c9a16 100644 --- a/cmd/artifact/info/info.go +++ b/cmd/artifact/info/info.go @@ -62,6 +62,7 @@ func NewArtifactInfoCmd(ctx context.Context, opt *options.Common) *cobra.Command func (o *artifactInfoOptions) RunArtifactInfo(ctx context.Context, args []string) error { var data [][]string + logger := o.Printer.Logger client, err := ociutils.Client(true) if err != nil { @@ -75,7 +76,7 @@ func (o *artifactInfoOptions) RunArtifactInfo(ctx context.Context, args []string if err != nil { entry, ok := o.IndexCache.MergedIndexes.EntryByName(name) if !ok { - o.Printer.Warning.Printfln("cannot find %q, skipping", name) + logger.Warn("Cannot find artifact, skipping", logger.Args("name", name)) continue } ref = fmt.Sprintf("%s/%s", entry.Registry, entry.Repository) @@ -93,7 +94,7 @@ func (o *artifactInfoOptions) RunArtifactInfo(ctx context.Context, args []string tags, err := repo.Tags(ctx) if err != nil && !errors.Is(err, context.Canceled) { - o.Printer.Warning.Printfln("cannot retrieve tags from t %q, %v", ref, err) + logger.Warn("Cannot retrieve tags from", logger.Args("ref", ref, "reason", err.Error())) continue } else if errors.Is(err, context.Canceled) { // When the context is canceled we exit, since we receive a termination signal. diff --git a/cmd/artifact/install/install.go b/cmd/artifact/install/install.go index 46144c17..ff263ac4 100644 --- a/cmd/artifact/install/install.go +++ b/cmd/artifact/install/install.go @@ -22,6 +22,7 @@ import ( "path/filepath" "runtime" + "github.com/pterm/pterm" "github.com/spf13/cobra" "github.com/spf13/viper" @@ -183,6 +184,9 @@ Examples: // RunArtifactInstall executes the business logic for the artifact install command. func (o *artifactInstallOptions) RunArtifactInstall(ctx context.Context, args []string) error { + var sp *pterm.SpinnerPrinter + + logger := o.Printer.Logger // Retrieve configuration for installer configuredInstaller, err := config.Installer() if err != nil { @@ -244,7 +248,7 @@ func (o *artifactInstallOptions) RunArtifactInstall(ctx context.Context, args [] var refs []string if o.resolveDeps { // Solve dependencies - o.Printer.Info.Println("Resolving dependencies ...") + logger.Info("Resolving dependencies ...") refs, err = ResolveDeps(resolver, args...) if err != nil { return err @@ -253,7 +257,7 @@ func (o *artifactInstallOptions) RunArtifactInstall(ctx context.Context, args [] refs = args } - o.Printer.Info.Printfln("Installing the following artifacts: %v", refs) + logger.Info("Installing artifacts", logger.Args("refs", refs)) for _, ref := range refs { ref, err = o.IndexCache.ResolveReference(ref) @@ -261,7 +265,7 @@ func (o *artifactInstallOptions) RunArtifactInstall(ctx context.Context, args [] return err } - o.Printer.Info.Printfln("Preparing to pull %q", ref) + logger.Info("Preparing to pull artifact", logger.Args("ref", ref)) if err := puller.CheckAllowedType(ctx, ref, o.allowedTypes.Types); err != nil { return err @@ -290,12 +294,12 @@ func (o *artifactInstallOptions) RunArtifactInstall(ctx context.Context, args [] // the exact digest that we just pulled, even if the tag gets overwritten in the meantime. digestRef := fmt.Sprintf("%s@%s", repo, result.RootDigest) - o.Printer.Info.Printfln("Verifying signature for %s", digestRef) + logger.Info("Verifying signature for artifact", logger.Args("digest", digestRef)) err = signature.Verify(ctx, digestRef, sig) if err != nil { return fmt.Errorf("error while verifying signature for %s: %w", digestRef, err) } - o.Printer.Info.Printfln("Signature successfully verified!") + logger.Info("Signature successfully verified!") } var destDir string @@ -312,14 +316,18 @@ func (o *artifactInstallOptions) RunArtifactInstall(ctx context.Context, args [] return fmt.Errorf("cannot use directory %q as install destination: %w", destDir, err) } - sp, _ := o.Printer.Spinner.Start(fmt.Sprintf("INFO: Extracting and installing %q %q", result.Type, result.Filename)) + logger.Info("Extracting and installing artifact", logger.Args("type", result.Type, "file", result.Filename)) + + if !o.Printer.DisableStyling { + sp, _ = o.Printer.Spinner.Start("Extracting and installing") + } + result.Filename = filepath.Join(tmpDir, result.Filename) f, err := os.Open(result.Filename) if err != nil { return err } - // Extract artifact and move it to its destination directory _, err = utils.ExtractTarGz(f, destDir) if err != nil { @@ -331,7 +339,10 @@ func (o *artifactInstallOptions) RunArtifactInstall(ctx context.Context, args [] return err } - sp.Success(fmt.Sprintf("Artifact successfully installed in %q", destDir)) + if sp != nil { + _ = sp.Stop() + } + logger.Info("Artifact successfully installed", logger.Args("name", ref, "type", result.Type, "digest", result.Digest, "directory", destDir)) } return nil diff --git a/cmd/artifact/install/install_suite_test.go b/cmd/artifact/install/install_suite_test.go index d3ecb5c4..f1ba2dbf 100644 --- a/cmd/artifact/install/install_suite_test.go +++ b/cmd/artifact/install/install_suite_test.go @@ -71,7 +71,6 @@ var _ = BeforeSuite(func() { // Create and configure the common options. opt = commonoptions.NewOptions() opt.Initialize(commonoptions.WithWriter(output)) - opt.Printer.DisableStylingf() // Create the oras registry. orasRegistry, err = testutils.NewOrasRegistry(registry, true) @@ -98,5 +97,5 @@ var _ = AfterSuite(func() { func executeRoot(args []string) error { rootCmd.SetArgs(args) rootCmd.SetOut(output) - return cmd.Execute(rootCmd, opt.Printer) + return cmd.Execute(rootCmd, opt) } diff --git a/cmd/artifact/install/install_test.go b/cmd/artifact/install/install_test.go index bd726f35..f37de8ca 100644 --- a/cmd/artifact/install/install_test.go +++ b/cmd/artifact/install/install_test.go @@ -51,8 +51,8 @@ Flags: Global Flags: --config string config file to be used for falcoctl (default "/etc/falcoctl/falcoctl.yaml") - --disable-styling Disable output styling such as spinners, progress bars and colors. Styling is automatically disabled if not attacched to a tty (default false) - -v, --verbose Enable verbose logs (default false) + --log-format string Set formatting for logs (color, text, json) (default "color") + --log-level string Set level for logs (info, warn, debug, trace) (default "info") ` @@ -170,7 +170,7 @@ var artifactInstallTests = Describe("install", func() { args = []string{artifactCmd, installCmd, "--config", configFile} }) installAssertFailedBehavior(artifactInstallUsage, - "ERRO: no artifacts to install, please configure artifacts or pass them as arguments to this command") + "ERROR no artifacts to install, please configure artifacts or pass them as arguments to this command") }) When("unreachable registry", func() { @@ -181,7 +181,7 @@ var artifactInstallTests = Describe("install", func() { Expect(err).To(BeNil()) args = []string{artifactCmd, installCmd, "noregistry/testrules", "--plain-http", "--config", configFile} }) - installAssertFailedBehavior(artifactInstallUsage, `ERRO: unable to fetch reference`) + installAssertFailedBehavior(artifactInstallUsage, `ERROR unable to fetch reference`) }) When("invalid repository", func() { @@ -193,7 +193,7 @@ var artifactInstallTests = Describe("install", func() { Expect(err).To(BeNil()) args = []string{artifactCmd, installCmd, newReg, "--plain-http", "--config", configFile} }) - installAssertFailedBehavior(artifactInstallUsage, fmt.Sprintf("ERRO: unable to fetch reference %q", newReg)) + installAssertFailedBehavior(artifactInstallUsage, fmt.Sprintf("ERROR unable to fetch reference %q", newReg)) }) When("with disallowed types (rulesfile)", func() { @@ -222,7 +222,7 @@ var artifactInstallTests = Describe("install", func() { "--config", configFilePath, "--allowed-types", "rulesfile"} }) - installAssertFailedBehavior(artifactInstallUsage, "ERRO: cannot download artifact of type \"plugin\": type not permitted") + installAssertFailedBehavior(artifactInstallUsage, "ERROR cannot download artifact of type \"plugin\": type not permitted") }) When("with disallowed types (plugin)", func() { @@ -251,7 +251,7 @@ var artifactInstallTests = Describe("install", func() { "--config", configFilePath, "--allowed-types", "plugin"} }) - installAssertFailedBehavior(artifactInstallUsage, "ERRO: cannot download artifact of type \"rulesfile\": type not permitted") + installAssertFailedBehavior(artifactInstallUsage, "ERROR cannot download artifact of type \"rulesfile\": type not permitted") }) When("an unknown type is used", func() { @@ -281,7 +281,7 @@ var artifactInstallTests = Describe("install", func() { "--config", configFilePath, "--allowed-types", "plugin," + wrongType} }) - installAssertFailedBehavior(artifactInstallUsage, fmt.Sprintf("ERRO: invalid argument \"plugin,%s\" for \"--allowed-types\" flag: "+ + installAssertFailedBehavior(artifactInstallUsage, fmt.Sprintf("ERROR invalid argument \"plugin,%s\" for \"--allowed-types\" flag: "+ "not valid token %q: must be one of \"rulesfile\", \"plugin\"", wrongType, wrongType)) }) @@ -315,7 +315,7 @@ var artifactInstallTests = Describe("install", func() { }) It("check that fails and the usage is not printed", func() { - expectedError := fmt.Sprintf("ERRO: cannot use directory %q "+ + expectedError := fmt.Sprintf("ERROR cannot use directory %q "+ "as install destination: %s is not writable", destDir, destDir) Expect(err).To(HaveOccurred()) Expect(output).ShouldNot(gbytes.Say(regexp.QuoteMeta(artifactInstallUsage))) @@ -353,7 +353,7 @@ var artifactInstallTests = Describe("install", func() { }) It("check that fails and the usage is not printed", func() { - expectedError := fmt.Sprintf("ERRO: cannot use directory %q "+ + expectedError := fmt.Sprintf("ERROR cannot use directory %q "+ "as install destination: %s doesn't exists", destDir, destDir) Expect(err).To(HaveOccurred()) Expect(output).ShouldNot(gbytes.Say(regexp.QuoteMeta(artifactInstallUsage))) @@ -391,7 +391,7 @@ var artifactInstallTests = Describe("install", func() { }) It("check that fails and the usage is not printed", func() { - expectedError := fmt.Sprintf("ERRO: cannot use directory %q "+ + expectedError := fmt.Sprintf("ERROR cannot use directory %q "+ "as install destination: %s is not writable", destDir, destDir) Expect(err).To(HaveOccurred()) Expect(output).ShouldNot(gbytes.Say(regexp.QuoteMeta(artifactInstallUsage))) @@ -429,7 +429,7 @@ var artifactInstallTests = Describe("install", func() { }) It("check that fails and the usage is not printed", func() { - expectedError := fmt.Sprintf("ERRO: cannot use directory %q "+ + expectedError := fmt.Sprintf("ERROR cannot use directory %q "+ "as install destination: %s doesn't exists", destDir, destDir) Expect(err).To(HaveOccurred()) Expect(output).ShouldNot(gbytes.Say(regexp.QuoteMeta(artifactInstallUsage))) diff --git a/cmd/cli_test.go b/cmd/cli_test.go index bc38d3e8..280b2f46 100644 --- a/cmd/cli_test.go +++ b/cmd/cli_test.go @@ -75,7 +75,7 @@ var tests = []testCase{ func run(t *testing.T, test *testCase) { // Setup - c := New(context.Background(), &options.Common{}) + c := New(context.Background(), options.NewOptions()) o := bytes.NewBufferString("") c.SetOut(o) c.SetErr(o) diff --git a/cmd/index/add/add.go b/cmd/index/add/add.go index f820a645..228e22ef 100644 --- a/cmd/index/add/add.go +++ b/cmd/index/add/add.go @@ -56,6 +56,7 @@ func NewIndexAddCmd(ctx context.Context, opt *options.Common) *cobra.Command { // RunIndexAdd implements the index add command. func (o *IndexAddOptions) RunIndexAdd(ctx context.Context, args []string) error { var err error + logger := o.Printer.Logger name := args[0] url := args[1] @@ -64,24 +65,24 @@ func (o *IndexAddOptions) RunIndexAdd(ctx context.Context, args []string) error backend = args[2] } - o.Printer.Verbosef("Creating in-memory cache using indexes file %q and indexes directory %q", config.IndexesFile, config.IndexesDir) + logger.Debug("Creating in-memory cache using", logger.Args("indexes file", config.IndexesFile, "indexes directory", config.IndexesDir)) indexCache, err := cache.New(ctx, config.IndexesFile, config.IndexesDir) if err != nil { return fmt.Errorf("unable to create index cache: %w", err) } - o.Printer.Info.Printfln("Adding index") + logger.Info("Adding index", logger.Args("name", name, "path", url)) if err = indexCache.Add(ctx, name, backend, url); err != nil { return fmt.Errorf("unable to add index: %w", err) } - o.Printer.Verbosef("Writing cache to disk") + logger.Debug("Writing cache to disk") if _, err = indexCache.Write(); err != nil { return fmt.Errorf("unable to write cache to disk: %w", err) } - o.Printer.Verbosef("Adding new index entry to configuration file %q", o.ConfigFile) + logger.Debug("Adding new index entry to configuration", logger.Args("file", o.ConfigFile)) if err = config.AddIndexes([]config.Index{{ Name: name, URL: url, @@ -90,7 +91,7 @@ func (o *IndexAddOptions) RunIndexAdd(ctx context.Context, args []string) error return fmt.Errorf("index entry %q: %w", name, err) } - o.Printer.Success.Printfln("Index %q successfully added", name) + logger.Info("Index successfully added") return nil } diff --git a/cmd/index/add/add_suite_test.go b/cmd/index/add/add_suite_test.go index 8b41d3e1..52eb7655 100644 --- a/cmd/index/add/add_suite_test.go +++ b/cmd/index/add/add_suite_test.go @@ -67,7 +67,6 @@ var _ = BeforeSuite(func() { // Create and configure the common options. opt = commonoptions.NewOptions() opt.Initialize(commonoptions.WithWriter(output)) - opt.Printer.DisableStylingf() // Create temporary directory used to save the configuration file. configFile, err = testutils.CreateEmptyFile("falcoctl.yaml") @@ -84,5 +83,5 @@ var _ = AfterSuite(func() { func executeRoot(args []string) error { rootCmd.SetArgs(args) rootCmd.SetOut(output) - return cmd.Execute(rootCmd, opt.Printer) + return cmd.Execute(rootCmd, opt) } diff --git a/cmd/index/add/add_test.go b/cmd/index/add/add_test.go index f17f9542..6aef43d4 100644 --- a/cmd/index/add/add_test.go +++ b/cmd/index/add/add_test.go @@ -33,9 +33,9 @@ Flags: -h, --help help for add Global Flags: - --config string config file to be used for falcoctl (default "/etc/falcoctl/falcoctl.yaml") - --disable-styling Disable output styling such as spinners, progress bars and colors. Styling is automatically disabled if not attacched to a tty (default false) --v, --verbose Enable verbose logs (default false) + --config string config file to be used for falcoctl (default "/etc/falcoctl/falcoctl.yaml") + --log-format string Set formatting for logs (color, text, json) (default "color") + --log-level string Set level for logs (info, warn, debug, trace) (default "info") ` //nolint:lll // no need to check for line length. @@ -48,9 +48,9 @@ Flags: -h, --help help for add Global Flags: - --config string config file to be used for falcoctl (default "/etc/falcoctl/falcoctl.yaml") - --disable-styling Disable output styling such as spinners, progress bars and colors. Styling is automatically disabled if not attacched to a tty (default false) - -v, --verbose Enable verbose logs (default false) + --config string config file to be used for falcoctl (default "/etc/falcoctl/falcoctl.yaml") + --log-format string Set formatting for logs (color, text, json) (default "color") + --log-level string Set level for logs (info, warn, debug, trace) (default "info") ` var addAssertFailedBehavior = func(usage, specificError string) { @@ -97,14 +97,14 @@ var indexAddTests = Describe("add", func() { BeforeEach(func() { args = []string{indexCmd, addCmd, "--config", configFile, indexName} }) - addAssertFailedBehavior(indexAddUsage, "ERRO: accepts between 2 and 3 arg(s), received 1") + addAssertFailedBehavior(indexAddUsage, "ERROR accepts between 2 and 3 arg(s), received 1") }) When("with invalid URL", func() { BeforeEach(func() { args = []string{indexCmd, addCmd, "--config", configFile, indexName, "NOTAPROTOCAL://something"} }) - addAssertFailedBehavior(indexAddUsage, "ERRO: unable to add index: unable to fetch index \"testName\""+ + addAssertFailedBehavior(indexAddUsage, "ERROR unable to add index: unable to fetch index \"testName\""+ " with URL \"NOTAPROTOCAL://something\": unable to fetch index: cannot fetch index: Get "+ "\"notaprotocal://something\": unsupported protocol scheme \"notaprotocal\"") }) @@ -113,7 +113,7 @@ var indexAddTests = Describe("add", func() { BeforeEach(func() { args = []string{indexCmd, addCmd, "--config", configFile, indexName, "http://noindex", "notabackend"} }) - addAssertFailedBehavior(indexAddUsage, "ERRO: unable to add index: unable to fetch index \"testName\" "+ + addAssertFailedBehavior(indexAddUsage, "ERROR unable to add index: unable to fetch index \"testName\" "+ "with URL \"http://noindex\": unsupported index backend type: notabackend") }) }) diff --git a/cmd/index/remove/remove.go b/cmd/index/remove/remove.go index 88ebce05..ce1f9c29 100644 --- a/cmd/index/remove/remove.go +++ b/cmd/index/remove/remove.go @@ -54,30 +54,32 @@ func NewIndexRemoveCmd(ctx context.Context, opt *options.Common) *cobra.Command } func (o *indexRemoveOptions) RunIndexRemove(ctx context.Context, args []string) error { - o.Printer.Verbosef("Creating in-memory cache using indexes file %q and indexes directory %q", config.IndexesFile, config.IndexesDir) + logger := o.Printer.Logger + + logger.Debug("Creating in-memory cache using", logger.Args("indexes file", config.IndexesFile, "indexes directory", config.IndexesDir)) indexCache, err := cache.New(ctx, config.IndexesFile, config.IndexesDir) if err != nil { return fmt.Errorf("unable to create index cache: %w", err) } for _, name := range args { - o.Printer.Info.Printfln("Removing index %q", name) + logger.Info("Removing index", logger.Args("name", name)) if err = indexCache.Remove(name); err != nil { return fmt.Errorf("unable to remove index: %w", err) } } - o.Printer.Verbosef("Writing cache to disk") + logger.Debug("Writing cache to disk") if _, err = indexCache.Write(); err != nil { return fmt.Errorf("unable to write cache to disk: %w", err) } - o.Printer.Verbosef("Removing indexes entries from configuration file %q", o.ConfigFile) + logger.Debug("Removing indexes entries from configuration", logger.Args("file", o.ConfigFile)) if err = config.RemoveIndexes(args, o.ConfigFile); err != nil { return err } - o.Printer.Success.Printfln("Indexes successfully removed") + logger.Info("Indexes successfully removed") return nil } diff --git a/cmd/index/update/update.go b/cmd/index/update/update.go index af63826a..dd570691 100644 --- a/cmd/index/update/update.go +++ b/cmd/index/update/update.go @@ -52,25 +52,27 @@ func NewIndexUpdateCmd(ctx context.Context, opt *options.Common) *cobra.Command } func (o *indexUpdateOptions) RunIndexUpdate(ctx context.Context, args []string) error { - o.Printer.Verbosef("Creating in-memory cache using indexes file %q and indexes directory %q", config.IndexesFile, config.IndexesDir) + logger := o.Printer.Logger + + logger.Debug("Creating in-memory cache using", logger.Args("indexes file", config.IndexesFile, "indexes directory", config.IndexesDir)) indexCache, err := cache.New(ctx, config.IndexesFile, config.IndexesDir) if err != nil { return fmt.Errorf("unable to create index cache: %w", err) } for _, arg := range args { - o.Printer.Info.Printfln("Updating index %q", arg) + logger.Info("Updating index file", logger.Args("name", arg)) if err := indexCache.Update(ctx, arg); err != nil { return fmt.Errorf("an error occurred while updating index %q: %w", arg, err) } } - o.Printer.Verbosef("Writing cache to disk") + logger.Debug("Writing cache to disk") if _, err = indexCache.Write(); err != nil { return fmt.Errorf("unable to write cache to disk: %w", err) } - o.Printer.Success.Printfln("Indexes successfully updated") + logger.Info("Indexes successfully updated") return nil } diff --git a/cmd/registry/auth/basic/basic.go b/cmd/registry/auth/basic/basic.go index fbc8d0e0..86e0c039 100644 --- a/cmd/registry/auth/basic/basic.go +++ b/cmd/registry/auth/basic/basic.go @@ -58,7 +58,7 @@ func NewBasicCmd(ctx context.Context, opt *options.Common) *cobra.Command { // RunBasic executes the business logic for the basic command. func (o *loginOptions) RunBasic(ctx context.Context, args []string) error { reg := args[0] - + logger := o.Printer.Logger user, token, err := utils.GetCredentials(o.Printer) if err != nil { return err @@ -78,8 +78,8 @@ func (o *loginOptions) RunBasic(ctx context.Context, args []string) error { if err := basic.Login(ctx, client, credentialStore, reg, user, token); err != nil { return err } - o.Printer.Verbosef("credentials added to credential store") - o.Printer.Success.Println("Login succeeded") + logger.Debug("Credentials added", logger.Args("credential store", config.RegistryCredentialConfPath())) + logger.Info("Login succeeded", logger.Args("registry", reg, "user", user)) return nil } diff --git a/cmd/registry/auth/basic/basic_suite_test.go b/cmd/registry/auth/basic/basic_suite_test.go index f6d60693..8d0dadec 100644 --- a/cmd/registry/auth/basic/basic_suite_test.go +++ b/cmd/registry/auth/basic/basic_suite_test.go @@ -102,7 +102,6 @@ var _ = BeforeSuite(func() { // Create and configure the common options. opt = commonoptions.NewOptions() opt.Initialize(commonoptions.WithWriter(output)) - opt.Printer.DisableStylingf() // Start the local registry. go func() { @@ -131,5 +130,5 @@ var _ = AfterSuite(func() { func executeRoot(args []string) error { rootCmd.SetArgs(args) rootCmd.SetOut(output) - return cmd.Execute(rootCmd, opt.Printer) + return cmd.Execute(rootCmd, opt) } diff --git a/cmd/registry/auth/basic/basic_test.go b/cmd/registry/auth/basic/basic_test.go index 7f42118d..2519ca50 100644 --- a/cmd/registry/auth/basic/basic_test.go +++ b/cmd/registry/auth/basic/basic_test.go @@ -107,32 +107,8 @@ var registryAuthBasicTests = Describe("auth", func() { args = []string{registryCmd, authCmd, basicCmd} }) registryAuthBasicAssertFailedBehavior(registryAuthBasicUsage, - "ERRO: accepts 1 arg(s), received 0") + "ERROR accepts 1 arg(s), received 0") }) - - /* - When("wrong credentials", func() { - BeforeEach(func() { - - ptyFile, ttyFile, err := pty.Open() - Expect(err).To(BeNil()) - - os.Stdin = ttyFile - input := `username1 - password1 - ` - _, err = ptyFile.Write([]byte(input)) - Expect(err).To(BeNil()) - - http.DefaultTransport.(*http.Transport).TLSClientConfig = &tls.Config{InsecureSkipVerify: true} - - args = []string{registryCmd, authCmd, basicCmd, "--config", configFile, registryBasic} - }) - - registryAuthBasicAssertFailedBehavior(registryAuthBasicUsage, - "ERRO: accepts 0 arg(s), received 0") - }) - */ }) }) diff --git a/cmd/registry/auth/gcp/gcp.go b/cmd/registry/auth/gcp/gcp.go index 8d9e56f7..0f2d332b 100644 --- a/cmd/registry/auth/gcp/gcp.go +++ b/cmd/registry/auth/gcp/gcp.go @@ -66,20 +66,21 @@ func NewGcpCmd(ctx context.Context, opt *options.Common) *cobra.Command { // RunGcp executes the business logic for the gcp command. func (o *RegistryGcpOptions) RunGcp(ctx context.Context, args []string) error { var err error + logger := o.Printer.Logger reg := args[0] if err = gcp.Login(ctx, reg); err != nil { return err } - o.Printer.Success.Printfln("GCP authentication successful for %q", reg) + logger.Info("GCP authentication successful", logger.Args("registry", reg)) - o.Printer.Verbosef("Adding new gcp entry to configuration file %q", o.ConfigFile) + logger.Debug("Adding new gcp entry to configuration", logger.Args("file", o.ConfigFile)) if err = config.AddGcp([]config.GcpAuth{{ Registry: reg, }}, o.ConfigFile); err != nil { return fmt.Errorf("index entry %q: %w", reg, err) } - o.Printer.Success.Printfln("GCP authentication entry for %q successfully added in configuration file", reg) + logger.Info("GCG authentication entry successfully added", logger.Args("registry", reg, "confgi file", o.ConfigFile)) return nil } diff --git a/cmd/registry/auth/oauth/oauth.go b/cmd/registry/auth/oauth/oauth.go index c9824a69..a36889ab 100644 --- a/cmd/registry/auth/oauth/oauth.go +++ b/cmd/registry/auth/oauth/oauth.go @@ -17,6 +17,7 @@ package oauth import ( "context" + "fmt" "github.com/spf13/cobra" "golang.org/x/oauth2/clientcredentials" @@ -24,6 +25,7 @@ import ( "github.com/falcosecurity/falcoctl/internal/config" "github.com/falcosecurity/falcoctl/internal/login/oauth" "github.com/falcosecurity/falcoctl/pkg/options" + "github.com/falcosecurity/falcoctl/pkg/output" ) const ( @@ -67,17 +69,15 @@ func NewOauthCmd(ctx context.Context, opt *options.Common) *cobra.Command { cmd.Flags().StringVar(&o.Conf.TokenURL, "token-url", "", "token URL used to get access and refresh tokens") if err := cmd.MarkFlagRequired("token-url"); err != nil { - o.Printer.Error.Println("unable to mark flag \"token-url\" as required") - return nil + output.ExitOnErr(o.Printer, fmt.Errorf("unable to mark flag \"token-url\" as required")) } cmd.Flags().StringVar(&o.Conf.ClientID, "client-id", "", "client ID of the OAuth2.0 app") if err := cmd.MarkFlagRequired("client-id"); err != nil { - o.Printer.Error.Println("unable to mark flag \"client-id\" as required") - return nil + output.ExitOnErr(o.Printer, fmt.Errorf("unable to mark flag \"client-id\" as required")) } cmd.Flags().StringVar(&o.Conf.ClientSecret, "client-secret", "", "client secret of the OAuth2.0 app") if err := cmd.MarkFlagRequired("client-secret"); err != nil { - o.Printer.Error.Println("unable to mark flag \"client-secret\" as required") + output.ExitOnErr(o.Printer, fmt.Errorf("unable to mark flag \"client-secret\" as required")) return nil } cmd.Flags().StringSliceVar(&o.Conf.Scopes, "scopes", nil, "comma separeted list of scopes for which requesting access") @@ -91,6 +91,6 @@ func (o *RegistryOauthOptions) RunOAuth(ctx context.Context, args []string) erro if err := oauth.Login(ctx, reg, &o.Conf); err != nil { return err } - o.Printer.Success.Printfln("client credentials correctly saved in %q", config.ClientCredentialsFile) + o.Printer.Logger.Info("Client credentials correctly saved", o.Printer.Logger.Args("file", config.ClientCredentialsFile)) return nil } diff --git a/cmd/registry/auth/oauth/oauth_suite_test.go b/cmd/registry/auth/oauth/oauth_suite_test.go index c7a09698..e7977654 100644 --- a/cmd/registry/auth/oauth/oauth_suite_test.go +++ b/cmd/registry/auth/oauth/oauth_suite_test.go @@ -84,7 +84,6 @@ var _ = BeforeSuite(func() { // Create and configure the common options. opt = commonoptions.NewOptions() opt.Initialize(commonoptions.WithWriter(output)) - opt.Printer.DisableStylingf() // Create the oras registry. orasRegistry, err = testutils.NewOrasRegistry(registry, true) @@ -115,5 +114,5 @@ var _ = AfterSuite(func() { func executeRoot(args []string) error { rootCmd.SetArgs(args) rootCmd.SetOut(output) - return cmd.Execute(rootCmd, opt.Printer) + return cmd.Execute(rootCmd, opt) } diff --git a/cmd/registry/auth/oauth/oauth_test.go b/cmd/registry/auth/oauth/oauth_test.go index 0ff6534d..9ae37eae 100644 --- a/cmd/registry/auth/oauth/oauth_test.go +++ b/cmd/registry/auth/oauth/oauth_test.go @@ -65,8 +65,9 @@ Flags: Global Flags: --config string config file to be used for falcoctl (default "/etc/falcoctl/falcoctl.yaml") - --disable-styling Disable output styling such as spinners, progress bars and colors. Styling is automatically disabled if not attacched to a tty (default false) - -v, --verbose Enable verbose logs (default false) + --log-format string Set formatting for logs (color, text, json) (default "color") + --log-level string Set level for logs (info, warn, debug, trace) (default "info") + ` //nolint:unused // false positive @@ -132,7 +133,7 @@ var registryAuthOAuthTests = Describe("auth", func() { args = []string{registryCmd, authCmd, oauthCmd} }) registryAuthOAuthAssertFailedBehavior(registryAuthOAuthUsage, - "ERRO: accepts 1 arg(s), received 0") + "ERROR accepts 1 arg(s), received 0") }) When("wrong client id", func() { @@ -152,7 +153,7 @@ var registryAuthOAuthTests = Describe("auth", func() { } }) registryAuthOAuthAssertFailedBehavior(registryAuthOAuthUsage, - `ERRO: wrong client credentials, unable to retrieve token`) + `ERROR wrong client credentials, unable to retrieve token`) }) When("wrong client secret", func() { @@ -172,7 +173,7 @@ var registryAuthOAuthTests = Describe("auth", func() { } }) registryAuthOAuthAssertFailedBehavior(registryAuthOAuthUsage, - `ERRO: wrong client credentials, unable to retrieve token`) + `ERROR wrong client credentials, unable to retrieve token`) }) }) @@ -199,7 +200,7 @@ var registryAuthOAuthTests = Describe("auth", func() { It("should successed", func() { Expect(output).Should(gbytes.Say(regexp.QuoteMeta( - `INFO: client credentials correctly saved in`))) + `INFO Client credentials correctly saved`))) }) }) diff --git a/cmd/registry/pull/pull.go b/cmd/registry/pull/pull.go index 7f713fe6..4fbd1e03 100644 --- a/cmd/registry/pull/pull.go +++ b/cmd/registry/pull/pull.go @@ -88,6 +88,7 @@ func NewPullCmd(ctx context.Context, opt *options.Common) *cobra.Command { if err != nil { return err } + o.Common.Initialize() return nil }, RunE: func(cmd *cobra.Command, args []string) error { @@ -103,6 +104,7 @@ func NewPullCmd(ctx context.Context, opt *options.Common) *cobra.Command { // RunPull executes the business logic for the pull command. func (o *pullOptions) RunPull(ctx context.Context, args []string) error { + logger := o.Printer.Logger ref := args[0] registry, err := utils.GetRegistryFromRef(ref) @@ -120,12 +122,12 @@ func (o *pullOptions) RunPull(ctx context.Context, args []string) error { return err } - o.Printer.Info.Printfln("Preparing to pull artifact %q", args[0]) + logger.Info("Preparing to pull artifact", logger.Args("name", args[0])) if o.destDir == "" { - o.Printer.Info.Printfln("Pulling artifact in the current directory") + logger.Info("Pulling artifact in the current directory") } else { - o.Printer.Info.Printfln("Pulling artifact in %q directory", o.destDir) + logger.Info("Pulling artifact in", logger.Args("directory", o.destDir)) } os, arch := runtime.GOOS, runtime.GOARCH @@ -138,7 +140,7 @@ func (o *pullOptions) RunPull(ctx context.Context, args []string) error { return err } - o.Printer.Success.Printfln("Artifact of type %q pulled. Digest: %q", res.Type, res.Digest) + logger.Info("Artifact pulled", logger.Args("name", args[0], "type", res.Type, "digest", res.Digest)) return nil } diff --git a/cmd/registry/pull/pull_suite_test.go b/cmd/registry/pull/pull_suite_test.go index 89b521f9..d057e488 100644 --- a/cmd/registry/pull/pull_suite_test.go +++ b/cmd/registry/pull/pull_suite_test.go @@ -70,7 +70,6 @@ var _ = BeforeSuite(func() { // Create and configure the common options. opt = commonoptions.NewOptions() opt.Initialize(commonoptions.WithWriter(output)) - opt.Printer.DisableStylingf() // Create the oras registry. orasRegistry, err = testutils.NewOrasRegistry(registry, true) @@ -97,5 +96,5 @@ var _ = AfterSuite(func() { func executeRoot(args []string) error { rootCmd.SetArgs(args) rootCmd.SetOut(output) - return cmd.Execute(rootCmd, opt.Printer) + return cmd.Execute(rootCmd, opt) } diff --git a/cmd/registry/pull/pull_test.go b/cmd/registry/pull/pull_test.go index 854a0a78..ad29fd01 100644 --- a/cmd/registry/pull/pull_test.go +++ b/cmd/registry/pull/pull_test.go @@ -144,7 +144,7 @@ var registryPullTests = Describe("pull", func() { BeforeEach(func() { args = []string{registryCmd, pullCmd} }) - pullAssertFailedBehavior(registryPullUsage, "ERRO: accepts 1 arg(s), received 0") + pullAssertFailedBehavior(registryPullUsage, "ERROR accepts 1 arg(s), received 0") }) When("unreachable registry", func() { @@ -155,7 +155,7 @@ var registryPullTests = Describe("pull", func() { Expect(err).To(BeNil()) args = []string{registryCmd, pullCmd, "noregistry/testrules", "--plain-http", "--config", configFile} }) - pullAssertFailedBehavior(registryPullUsage, "ERRO: unable to connect to remote registry") + pullAssertFailedBehavior(registryPullUsage, "ERROR unable to connect to remote registry") }) When("invalid repository", func() { @@ -167,7 +167,7 @@ var registryPullTests = Describe("pull", func() { Expect(err).To(BeNil()) args = []string{registryCmd, pullCmd, newReg, "--plain-http", "--config", configFile} }) - pullAssertFailedBehavior(registryPullUsage, fmt.Sprintf("ERRO: %s: not found", newReg)) + pullAssertFailedBehavior(registryPullUsage, fmt.Sprintf("ERROR %s: not found", newReg)) }) When("unwritable --dest-dir", func() { @@ -200,7 +200,7 @@ var registryPullTests = Describe("pull", func() { artName := tmp[0] tag := tmp[1] expectedError := fmt.Sprintf( - "ERRO: unable to pull artifact generic-repo with %s tag from repo %s: failed to create file", + "ERROR unable to pull artifact generic-repo with %s tag from repo %s: failed to create file", tag, artName) Expect(err).To(HaveOccurred()) Expect(output).ShouldNot(gbytes.Say(regexp.QuoteMeta(registryPullUsage))) @@ -233,10 +233,8 @@ var registryPullTests = Describe("pull", func() { }) It("check that fails and the usage is not printed", func() { - expectedError := fmt.Sprintf( - "ERRO: unable to push artifact failed to ensure directories of the target path: mkdir %s: permission denied\n"+ - "ERRO: unable to pull artifact %s with tag %s from repo %s: failed to ensure directories of the target path: mkdir %s: permission denied", - destDir, artifact, tag, artifact, destDir) + expectedError := fmt.Sprintf("ERROR unable to pull artifact %s with tag %s from repo %s: failed to ensure directories of the target path: "+ + "mkdir %s: permission denied", artifact, tag, artifact, destDir) Expect(err).To(HaveOccurred()) Expect(output).ShouldNot(gbytes.Say(regexp.QuoteMeta(registryPullUsage))) Expect(output).Should(gbytes.Say(regexp.QuoteMeta(expectedError))) @@ -264,7 +262,7 @@ var registryPullTests = Describe("pull", func() { }) It("check that fails and the usage is not printed", func() { - expectedError := fmt.Sprintf("ERRO: %s: not found", registry+repo+"@"+wrongDigest) + expectedError := fmt.Sprintf("ERROR %s: not found", registry+repo+"@"+wrongDigest) Expect(err).To(HaveOccurred()) Expect(output).ShouldNot(gbytes.Say(regexp.QuoteMeta(registryPullUsage))) Expect(output).Should(gbytes.Say(regexp.QuoteMeta(expectedError))) @@ -282,7 +280,7 @@ var registryPullTests = Describe("pull", func() { }) It("check that fails and the usage is not printed", func() { - expectedError := fmt.Sprintf("ERRO: cannot extract registry name from ref %q", ref) + expectedError := fmt.Sprintf("ERROR cannot extract registry name from ref %q", ref) Expect(err).To(HaveOccurred()) Expect(output).ShouldNot(gbytes.Say(regexp.QuoteMeta(registryPullUsage))) Expect(output).Should(gbytes.Say(regexp.QuoteMeta(expectedError))) @@ -298,7 +296,7 @@ var registryPullTests = Describe("pull", func() { Expect(err).To(BeNil()) args = []string{registryCmd, pullCmd, newReg, "--plain-http", "--config", configFile} }) - pullAssertFailedBehavior(registryPullUsage, fmt.Sprintf("ERRO: unable to create new repository with ref %s: "+ + pullAssertFailedBehavior(registryPullUsage, fmt.Sprintf("ERROR unable to create new repository with ref %s: "+ "invalid reference: invalid digest; invalid checksum digest format\n", newReg)) }) diff --git a/cmd/registry/push/push.go b/cmd/registry/push/push.go index 15fe0e0c..ce785d33 100644 --- a/cmd/registry/push/push.go +++ b/cmd/registry/push/push.go @@ -124,6 +124,7 @@ func (o *pushOptions) runPush(ctx context.Context, args []string) error { // When creating the tar.gz archives we need to remove them after we are done. // We save the temporary dir where they live here. var toBeDeleted string + logger := o.Printer.Logger registry, err := utils.GetRegistryFromRef(ref) if err != nil { @@ -140,12 +141,12 @@ func (o *pushOptions) runPush(ctx context.Context, args []string) error { return err } - o.Printer.Info.Printfln("Preparing to push artifact %q of type %q", args[0], o.ArtifactType) + logger.Info("Preparing to push artifact", o.Printer.Logger.Args("name", args[0], "type", o.ArtifactType)) // Make sure to remove temporary working dir. defer func() { if err := os.RemoveAll(toBeDeleted); err != nil { - o.Printer.Warning.Printfln("Unable to remove temporary dir %q: %s", toBeDeleted, err.Error()) + logger.Warn("Unable to remove temporary dir", logger.Args("name", toBeDeleted, "error", err.Error())) } }() @@ -202,7 +203,7 @@ func (o *pushOptions) runPush(ctx context.Context, args []string) error { return err } - o.Printer.Success.Printfln("Artifact pushed. Digest: %q", res.Digest) + logger.Info("Artifact pushed", logger.Args("name", args[0], "type", res.Type, "digest", res.Digest)) return nil } diff --git a/cmd/registry/push/push_suite_test.go b/cmd/registry/push/push_suite_test.go index 08900a41..761973b7 100644 --- a/cmd/registry/push/push_suite_test.go +++ b/cmd/registry/push/push_suite_test.go @@ -71,7 +71,6 @@ var _ = BeforeSuite(func() { // Create and configure the common options. opt = commonoptions.NewOptions() opt.Initialize(commonoptions.WithWriter(output)) - opt.Printer.DisableStylingf() // Create the oras registry. orasRegistry, err = testutils.NewOrasRegistry(registry, true) @@ -97,5 +96,5 @@ var _ = AfterSuite(func() { func executeRoot(args []string) error { rootCmd.SetArgs(args) rootCmd.SetOut(output) - return cmd.Execute(rootCmd, opt.Printer) + return cmd.Execute(rootCmd, opt) } diff --git a/cmd/registry/push/push_test.go b/cmd/registry/push/push_test.go index bcb9bf80..4afc4fba 100644 --- a/cmd/registry/push/push_test.go +++ b/cmd/registry/push/push_test.go @@ -49,10 +49,9 @@ Flags: --version string set the version of the artifact Global Flags: - --config string config file to be used for falcoctl (default "/etc/falcoctl/falcoctl.yaml") - --disable-styling Disable output styling such as spinners, progress bars and colors. Styling is automatically disabled if not attacched to a tty (default false) - -v, --verbose Enable verbose logs (default false) - + --config string config file to be used for falcoctl (default "/etc/falcoctl/falcoctl.yaml") + --log-format string Set formatting for logs (color, text, json) (default "color") + --log-level string Set level for logs (info, warn, debug, trace) (default "info") ` //nolint:lll,unused // no need to check for line length. @@ -104,9 +103,9 @@ Flags: --version string set the version of the artifact Global Flags: - --config string config file to be used for falcoctl (default "/etc/falcoctl/falcoctl.yaml") - --disable-styling Disable output styling such as spinners, progress bars and colors. Styling is automatically disabled if not attacched to a tty (default false) - -v, --verbose Enable verbose logs (default false) + --config string config file to be used for falcoctl (default "/etc/falcoctl/falcoctl.yaml") + --log-format string Set formatting for logs (color, text, json) (default "color") + --log-level string Set level for logs (info, warn, debug, trace) (default "info") ` //nolint:unused // false positive @@ -156,8 +155,8 @@ var registryPushTests = Describe("push", func() { }) It("should match the saved one", func() { - - Expect(output).Should(gbytes.Say(regexp.QuoteMeta(registryPushHelp))) + outputMsg := string(output.Contents()) + Expect(outputMsg).Should(Equal(registryPushHelp)) }) }) @@ -174,21 +173,21 @@ var registryPushTests = Describe("push", func() { BeforeEach(func() { args = []string{registryCmd, pushCmd, "--config", configFile, rulesRepo, rulesfiletgz, "--type", "rulesfile"} }) - pushAssertFailedBehavior(registryPushUsage, "ERRO: required flag(s) \"version\" not set\n") + pushAssertFailedBehavior(registryPushUsage, "ERROR required flag(s) \"version\" not set") }) When("without rulesfile", func() { BeforeEach(func() { args = []string{registryCmd, pushCmd, "--config", configFile, rulesRepo, "--type", "rulesfile"} }) - pushAssertFailedBehavior(registryPushUsage, "ERRO: requires at least 2 arg(s), only received 1\n") + pushAssertFailedBehavior(registryPushUsage, "ERROR requires at least 2 arg(s), only received 1") }) When("without registry", func() { BeforeEach(func() { args = []string{registryCmd, pushCmd, "--config", configFile, rulesfiletgz, "--type", "rulesfile"} }) - pushAssertFailedBehavior(registryPushUsage, "ERRO: requires at least 2 arg(s), only received 1\n") + pushAssertFailedBehavior(registryPushUsage, "ERROR requires at least 2 arg(s), only received 1") }) When("multiple rulesfiles", func() { @@ -196,7 +195,7 @@ var registryPushTests = Describe("push", func() { args = []string{registryCmd, pushCmd, rulesRepo, "--config", configFile, rulesfiletgz, rulesfiletgz, "--type", "rulesfile", "--version", "1.1.1", "--plain-http"} }) - pushAssertFailedBehavior(registryPushUsage, "ERRO: expecting 1 rulesfile object received 2: invalid number of rulesfiles\n") + pushAssertFailedBehavior(registryPushUsage, "ERROR expecting 1 rulesfile object received 2: invalid number of rulesfiles") }) When("unreachable registry", func() { @@ -204,7 +203,7 @@ var registryPushTests = Describe("push", func() { args = []string{registryCmd, pushCmd, "noregistry/testrules", "--config", configFile, rulesfiletgz, "--type", "rulesfile", "--version", "1.1.1", "--plain-http"} }) - pushAssertFailedBehavior(registryPushUsage, "ERRO: unable to connect to remote "+ + pushAssertFailedBehavior(registryPushUsage, "ERROR unable to connect to remote "+ "registry \"noregistry\": Get \"http://noregistry/v2/\": dial tcp: lookup noregistry") }) @@ -212,7 +211,7 @@ var registryPushTests = Describe("push", func() { BeforeEach(func() { args = []string{registryCmd, pushCmd, registry, rulesfiletgz, "--config", configFile, "--type", "rulesfile", "--version", "1.1.1", "--plain-http"} }) - pushAssertFailedBehavior(registryPushUsage, fmt.Sprintf("ERRO: cannot extract registry name from ref %q", registry)) + pushAssertFailedBehavior(registryPushUsage, fmt.Sprintf("ERROR cannot extract registry name from ref %q", registry)) }) When("invalid repository", func() { @@ -220,7 +219,7 @@ var registryPushTests = Describe("push", func() { BeforeEach(func() { args = []string{registryCmd, pushCmd, newReg, rulesfiletgz, "--config", configFile, "--type", "rulesfile", "--version", "1.1.1", "--plain-http"} }) - pushAssertFailedBehavior(registryPushUsage, fmt.Sprintf("ERRO: unable to create new repository with ref %s: "+ + pushAssertFailedBehavior(registryPushUsage, fmt.Sprintf("ERROR unable to create new repository with ref %s: "+ "invalid reference: invalid digest; invalid checksum digest format\n", newReg)) }) @@ -229,7 +228,7 @@ var registryPushTests = Describe("push", func() { args = []string{registryCmd, pushCmd, rulesRepo, rulesfiletgz, "--config", configFile, "--type", "rulesfile", "--version", "1.1.1", "--plain-http", "--requires", "wrongreq"} }) - pushAssertFailedBehavior(registryPushUsage, "ERRO: cannot parse \"wrongreq\"\n") + pushAssertFailedBehavior(registryPushUsage, "ERROR cannot parse \"wrongreq\"") }) When("invalid dependency", func() { @@ -237,7 +236,7 @@ var registryPushTests = Describe("push", func() { args = []string{registryCmd, pushCmd, rulesRepo, rulesfiletgz, "--config", configFile, "--type", "rulesfile", "--version", "1.1.1", "--plain-http", "--depends-on", "wrongdep"} }) - pushAssertFailedBehavior(registryPushUsage, "ERRO: cannot parse \"wrongdep\": invalid artifact reference "+ + pushAssertFailedBehavior(registryPushUsage, "ERROR cannot parse \"wrongdep\": invalid artifact reference "+ "(must be in the format \"name:version\")\n") }) @@ -245,8 +244,8 @@ var registryPushTests = Describe("push", func() { BeforeEach(func() { args = []string{registryCmd, pushCmd, pluginsRepo, plugintgz, "--config", configFile, "--type", "plugin", "--version", "1.1.1", "--plain-http"} }) - pushAssertFailedBehavior(registryPushUsage, "ERRO: \"filepaths\" length (1) must match \"platforms\" "+ - "length (0): number of filepaths and platform should be the same\n") + pushAssertFailedBehavior(registryPushUsage, "ERROR \"filepaths\" length (1) must match \"platforms\" "+ + "length (0): number of filepaths and platform should be the same") }) When("wrong plugin type", func() { @@ -254,7 +253,7 @@ var registryPushTests = Describe("push", func() { args = []string{registryCmd, pushCmd, pluginsRepo, pluginsRepo, "--config", configFile, "--type", "wrongType", "--version", "1.1.1", "--plain-http"} }) - pushAssertFailedBehavior(registryPushUsage, "ERRO: invalid argument \"wrongType\" for \"--type\" flag: must be one of \"rulesfile\", \"plugin\"\n") + pushAssertFailedBehavior(registryPushUsage, "ERROR invalid argument \"wrongType\" for \"--type\" flag: must be one of \"rulesfile\", \"plugin\"") }) }) @@ -295,7 +294,7 @@ var registryPushTests = Describe("push", func() { // We do not check the error here since we are checking it before // pulling the artifact. By("checking no error in output") - Expect(output).ShouldNot(gbytes.Say("ERRO:")) + Expect(output).ShouldNot(gbytes.Say("ERROR")) Expect(output).ShouldNot(gbytes.Say("Unable to remove temporary dir")) By("checking descriptor") @@ -471,7 +470,7 @@ var registryPushTests = Describe("push", func() { // We do not check the error here since we are checking it before // pulling the artifact. By("checking no error in output") - Expect(output).ShouldNot(gbytes.Say("ERRO:")) + Expect(output).ShouldNot(gbytes.Say("ERROR")) Expect(output).ShouldNot(gbytes.Say("Unable to remove temporary dir")) By("checking descriptor") diff --git a/cmd/registry/registry.go b/cmd/registry/registry.go index a35fcd9d..30cccc2a 100644 --- a/cmd/registry/registry.go +++ b/cmd/registry/registry.go @@ -36,6 +36,8 @@ func NewRegistryCmd(ctx context.Context, opt *commonoptions.Common) *cobra.Comma Long: "Interact with OCI registries", SilenceErrors: true, PersistentPreRunE: func(cmd *cobra.Command, args []string) error { + // Initialize the options. + opt.Initialize() // Load configuration from ENV variables and/or config file. return config.Load(opt.ConfigFile) }, diff --git a/cmd/root.go b/cmd/root.go index d1d105de..186f91ca 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -26,7 +26,6 @@ import ( "github.com/falcosecurity/falcoctl/cmd/tls" "github.com/falcosecurity/falcoctl/cmd/version" "github.com/falcosecurity/falcoctl/pkg/options" - "github.com/falcosecurity/falcoctl/pkg/output" ) const ( @@ -48,7 +47,14 @@ func New(ctx context.Context, opt *options.Common) *cobra.Command { Use: "falcoctl", Short: "The official CLI tool for working with Falco and its ecosystem components", Long: longRootCmd, + SilenceErrors: true, DisableAutoGenTag: true, + PersistentPreRun: func(cmd *cobra.Command, args []string) { + // Initialize the common options for all subcommands. + // Subcommands con overwrite the default settings by calling initialize with + // different options. + opt.Initialize() + }, } // Global flags @@ -65,10 +71,10 @@ func New(ctx context.Context, opt *options.Common) *cobra.Command { } // Execute configures the signal handlers and runs the command. -func Execute(cmd *cobra.Command, printer *output.Printer) error { +func Execute(cmd *cobra.Command, opt *options.Common) error { // we do not log the error here since we expect that each subcommand // handles the errors by itself. err := cmd.Execute() - printer.CheckErr(err) + opt.Printer.CheckErr(err) return err } diff --git a/cmd/testdata/help.txt b/cmd/testdata/help.txt index c94979f7..5fdd1c4f 100644 --- a/cmd/testdata/help.txt +++ b/cmd/testdata/help.txt @@ -21,9 +21,9 @@ Available Commands: version Print the falcoctl version information Flags: - --config string config file to be used for falcoctl (default "/etc/falcoctl/falcoctl.yaml") - --disable-styling Disable output styling such as spinners, progress bars and colors. Styling is automatically disabled if not attacched to a tty (default false) - -h, --help help for falcoctl - -v, --verbose Enable verbose logs (default false) + --config string config file to be used for falcoctl (default "/etc/falcoctl/falcoctl.yaml") + -h, --help help for falcoctl + --log-format string Set formatting for logs (color, text, json) (default "color") + --log-level string Set level for logs (info, warn, debug, trace) (default "info") Use "falcoctl [command] --help" for more information about a command. diff --git a/cmd/testdata/noargsnoflags.txt b/cmd/testdata/noargsnoflags.txt index c94979f7..5fdd1c4f 100644 --- a/cmd/testdata/noargsnoflags.txt +++ b/cmd/testdata/noargsnoflags.txt @@ -21,9 +21,9 @@ Available Commands: version Print the falcoctl version information Flags: - --config string config file to be used for falcoctl (default "/etc/falcoctl/falcoctl.yaml") - --disable-styling Disable output styling such as spinners, progress bars and colors. Styling is automatically disabled if not attacched to a tty (default false) - -h, --help help for falcoctl - -v, --verbose Enable verbose logs (default false) + --config string config file to be used for falcoctl (default "/etc/falcoctl/falcoctl.yaml") + -h, --help help for falcoctl + --log-format string Set formatting for logs (color, text, json) (default "color") + --log-level string Set level for logs (info, warn, debug, trace) (default "info") Use "falcoctl [command] --help" for more information about a command. diff --git a/cmd/testdata/wrongflag.txt b/cmd/testdata/wrongflag.txt index 429693f0..84363da1 100644 --- a/cmd/testdata/wrongflag.txt +++ b/cmd/testdata/wrongflag.txt @@ -1,4 +1,3 @@ -Error: unknown flag: --wrong Usage: falcoctl [command] @@ -12,10 +11,10 @@ Available Commands: version Print the falcoctl version information Flags: - --config string config file to be used for falcoctl (default "/etc/falcoctl/falcoctl.yaml") - --disable-styling Disable output styling such as spinners, progress bars and colors. Styling is automatically disabled if not attacched to a tty (default false) - -h, --help help for falcoctl - -v, --verbose Enable verbose logs (default false) + --config string config file to be used for falcoctl (default "/etc/falcoctl/falcoctl.yaml") + -h, --help help for falcoctl + --log-format string Set formatting for logs (color, text, json) (default "color") + --log-level string Set level for logs (info, warn, debug, trace) (default "info") Use "falcoctl [command] --help" for more information about a command. diff --git a/cmd/version/version.go b/cmd/version/version.go index 7934fa1f..ef97ff2b 100644 --- a/cmd/version/version.go +++ b/cmd/version/version.go @@ -119,20 +119,17 @@ func (o *options) Run(v *version) error { case yamlFormat: marshaled, err := yaml.Marshal(v) if err != nil { - o.Printer.Error.Println(err.Error()) return err } o.Printer.DefaultText.Printf("%s:\n%s\n", "Client Version", string(marshaled)) case jsonFormat: marshaled, err := json.MarshalIndent(v, "", " ") if err != nil { - o.Printer.Error.Println(err.Error()) return err } o.Printer.DefaultText.Printf("%s:\n%s \n", "Client Version", string(marshaled)) default: // We should never hit this case. - o.Printer.Error.Printf("options of the version command were not validated: --output=%q should have been rejected", o.Output) return fmt.Errorf("options of the version command were not validated: --output=%q should have been rejected", o.Output) } diff --git a/cmd/version/version_test.go b/cmd/version/version_test.go index 5a240810..de39ea6d 100644 --- a/cmd/version/version_test.go +++ b/cmd/version/version_test.go @@ -150,7 +150,6 @@ var _ = Describe("Version", func() { Context("run method", func() { It("should print the error message", func() { Expect(opt.Run(version)).Error().Should(HaveOccurred()) - Expect(writer).Should(gbytes.Say("options of the version command were not validated")) }) }) }) diff --git a/go.mod b/go.mod index c3f02854..2b133809 100644 --- a/go.mod +++ b/go.mod @@ -12,6 +12,7 @@ require ( github.com/go-oauth2/oauth2/v4 v4.5.2 github.com/golang-jwt/jwt v3.2.2+incompatible github.com/google/go-containerregistry v0.16.1 + github.com/gookit/color v1.5.4 github.com/mitchellh/mapstructure v1.5.0 github.com/onsi/ginkgo/v2 v2.10.0 github.com/onsi/gomega v1.27.8 @@ -163,7 +164,6 @@ require ( github.com/google/uuid v1.3.1 // indirect github.com/googleapis/enterprise-certificate-proxy v0.2.5 // indirect github.com/googleapis/gax-go/v2 v2.12.0 // indirect - github.com/gookit/color v1.5.4 // indirect github.com/gorilla/handlers v1.5.1 // indirect github.com/gorilla/mux v1.8.0 // indirect github.com/hashicorp/go-cleanhttp v0.5.2 // indirect diff --git a/internal/follower/follower.go b/internal/follower/follower.go index ff2f3031..4f6526be 100644 --- a/internal/follower/follower.go +++ b/internal/follower/follower.go @@ -29,6 +29,7 @@ import ( "time" "github.com/blang/semver" + "github.com/pterm/pterm" "github.com/robfig/cron/v3" "oras.land/oras-go/v2/registry" @@ -52,8 +53,9 @@ type Follower struct { currentDigest string *ocipuller.Puller *Config - *output.Printer + logger *pterm.Logger config.FalcoVersions + *output.Printer } // Config configuration options for the Follower. @@ -71,8 +73,6 @@ type Config struct { ArtifactReference string // PlainHTTP is set to true if all registry interaction must be in plain http. PlainHTTP bool - // Verbose enables the verbose logs. - Verbose bool // TmpDir directory where to save temporary files. TmpDir string // FalcoVersions is a struct containing all the required Falco versions that this follower @@ -118,15 +118,13 @@ func New(ref string, printer *output.Printer, conf *Config) (*Follower, error) { return nil, fmt.Errorf("unable to create temporary directory: %w", err) } - customPrinter := printer.WithScope(ref) - return &Follower{ ref: ref, tag: tag, tmpDir: tmpDir, Puller: puller, Config: conf, - Printer: customPrinter, + logger: printer.Logger, FalcoVersions: conf.FalcoVersions, }, nil } @@ -142,7 +140,7 @@ func (f *Follower) Follow(ctx context.Context) { select { case <-f.CloseChan: f.cleanUp() - fmt.Printf("follower for %q stopped\n", f.ref) + f.logger.Info("Follower stopped", f.logger.Args("followerName", f.ref)) // Notify that the follower is done. f.WaitGroup.Done() return @@ -155,109 +153,111 @@ func (f *Follower) Follow(ctx context.Context) { func (f *Follower) follow(ctx context.Context) { // First thing get the descriptor from remote repo. - f.Verbosef("fetching descriptor from remote repository...") + f.logger.Debug("Fetching descriptor from remote repository...", f.logger.Args("followerName", f.ref)) desc, err := f.Descriptor(ctx, f.ref) if err != nil { - f.Error.Printfln("an error occurred while fetching descriptor from remote repository: %v", err) + f.logger.Debug(fmt.Sprintf("an error occurred while fetching descriptor from remote repository: %v", err)) return } - f.Verbosef("descriptor correctly fetched") + f.logger.Debug("Descriptor correctly fetched", f.logger.Args("followerName", f.ref)) // If we have already processed then do nothing. // TODO(alacuku): check that the file also exists to cover the case when someone has removed the file. if desc.Digest.String() == f.currentDigest { - f.Verbosef("nothing to do, artifact already up to date.") + f.logger.Debug("Nothing to do, artifact already up to date.", f.logger.Args("followerName", f.ref)) return } - f.Info.Printfln("found new version under tag %q", f.tag) + f.logger.Info("Found new artifact version", f.logger.Args("followerName", f.ref, "tag", f.tag)) // Pull config layer to check falco versions artifactConfig, err := f.PullConfigLayer(ctx, f.ref) if err != nil { - f.Error.Printfln("unable to pull config layer for ref %q: %v", f.ref, err) + f.logger.Error("Unable to pull config layer", f.logger.Args("followerName", f.ref, "reason", err.Error())) return } err = f.checkRequirements(artifactConfig) if err != nil { - f.Error.Printfln("unmet requirements for ref %q: %v", f.ref, err) + f.logger.Error("Unmet requirements", f.logger.Args("followerName", f.ref, "reason", err.Error())) return } - f.Verbosef("pulling artifact from remote repository...") + f.logger.Debug("Pulling artifact", f.logger.Args("followerName", f.ref)) // Pull the artifact from the repository. filePaths, res, err := f.pull(ctx) if err != nil { - f.Error.Printfln("an error occurred while pulling artifact from remote repository: %v", err) + f.logger.Error("Unable to pull artifact", f.logger.Args("followerName", f.ref, "reason", err.Error())) return } - f.Verbosef("artifact correctly pulled") + f.logger.Debug("Artifact correctly pulled", f.logger.Args("followerName", f.ref)) dstDir := f.destinationDir(res) // Check if directory exists and is writable. err = utils.ExistsAndIsWritable(dstDir) if err != nil { - f.Error.Printfln("cannot use directory %q as install destination: %v", dstDir, err) + f.logger.Error("Invalid destination", f.logger.Args("followerName", f.ref, "directory", dstDir, "reason", err.Error())) return } // Install the artifacts if necessary. for _, path := range filePaths { baseName := filepath.Base(path) - f.Verbosef("installing file %q...", baseName) + f.logger.Debug("Installing file", f.logger.Args("followerName", f.ref, "fileName", baseName)) dstPath := filepath.Join(dstDir, baseName) // Check if the file exists. - f.Verbosef("checking if file %q already exists in %q", baseName, dstDir) + f.logger.Debug("Checking if file already exists", f.logger.Args("followerName", f.ref, "fileName", baseName, "directory", dstDir)) exists, err := fileExists(dstPath) if err != nil { - f.Error.Printfln("an error occurred while checking %q existence: %v", baseName, err) + f.logger.Error("Unable to check existence for file", f.logger.Args("followerName", f.ref, "fileName", baseName, "reason", err.Error())) return } if !exists { - f.Verbosef("file %q does not exist in %q, moving it", baseName, dstDir) + f.logger.Debug("Moving file", f.logger.Args("followerName", f.ref, "fileName", baseName, "destDirectory", dstDir)) if err = utils.Move(path, dstPath); err != nil { - f.Error.Printfln("an error occurred while moving file %q to %q: %v", baseName, dstDir, err) + f.logger.Error("Unable to move file", f.logger.Args("followerName", f.ref, "fileName", baseName, "destDirectory", dstDir, "reason", err.Error())) return } - f.Verbosef("file %q correctly installed", path) + f.logger.Debug("File correctly installed", f.logger.Args("followerName", f.ref, "path", path)) // It's done, move to the next file. continue } - f.Verbosef("file %q already exists in %q, checking if it is equal to the existing one", baseName, dstDir) + f.logger.Debug(fmt.Sprintf("file %q already exists in %q, checking if it is equal to the existing one", baseName, dstDir), + f.logger.Args("followerName", f.ref)) // Check if the files are equal. eq, err := equal([]string{path, dstPath}) if err != nil { - f.Error.Printfln("an error occurred while comparing files %q and %q: %v", path, dstPath, err) + f.logger.Error("Unable to compare files", f.logger.Args("followerName", f.ref, "newFile", path, "existingFile", dstPath, "reason", err.Error())) return } if !eq { - f.Verbosef("overwriting file %q with file %q", dstPath, path) + f.logger.Debug(fmt.Sprintf("Overwriting file %q with file %q", dstPath, path), f.logger.Args("followerName", f.ref)) if err = utils.Move(path, dstPath); err != nil { - f.Error.Printfln("an error occurred while overwriting file %q: %v", dstPath, err) + f.logger.Error("Unable to overwrite file", f.logger.Args("followerName", f.ref, "existingFile", dstPath, "reason", err.Error())) return } } else { - f.Verbosef("the two file are equal, nothing to be done") + f.logger.Debug("The two file are equal, nothing to be done") } } - f.Info.Printfln("artifact with tag %q correctly installed", f.tag) + f.logger.Info("Artifact correctly installed", + f.logger.Args("followerName", f.ref, "artifactName", f.ref, "type", res.Type, "digest", res.Digest, "directory", dstDir)) f.currentDigest = desc.Digest.String() } // pull downloads, extracts, and installs the artifact. func (f *Follower) pull(ctx context.Context) (filePaths []string, res *oci.RegistryResult, err error) { - f.Verbosef("check if pulling an allowed type of artifact") + f.logger.Debug("Check if pulling an allowed type of artifact", f.logger.Args("followerName", f.ref)) if err := f.Puller.CheckAllowedType(ctx, f.ref, f.Config.AllowedTypes.Types); err != nil { return nil, nil, err } // Pull the artifact from the repository. - f.Verbosef("pulling artifact %q", f.ref) + f.logger.Debug("Pulling artifact %q", f.logger.Args("followerName", f.ref, "artifactName", f.ref)) res, err = f.Pull(ctx, f.ref, f.tmpDir, runtime.GOOS, runtime.GOARCH) if err != nil { return filePaths, res, fmt.Errorf("unable to pull artifact %q: %w", f.ref, err) @@ -272,15 +272,15 @@ func (f *Follower) pull(ctx context.Context) (filePaths []string, res *oci.Regis // Verify the signature if needed if f.Config.Signature != nil { - f.Verbosef("verifying signature for %s", digestRef) + f.logger.Debug("Verifying signature", f.logger.Args("followerName", f.ref, "digest", digestRef)) err = signature.Verify(ctx, digestRef, f.Config.Signature) if err != nil { return filePaths, res, fmt.Errorf("could not verify signature for %s: %w", res.RootDigest, err) } - f.Verbosef("signature successfully verified") + f.logger.Debug("Signature successfully verified") } - f.Verbosef("extracting artifact") + f.logger.Debug("Extracting artifact", f.logger.Args("followerName", f.ref)) res.Filename = filepath.Join(f.tmpDir, res.Filename) file, err := os.Open(res.Filename) @@ -294,7 +294,7 @@ func (f *Follower) pull(ctx context.Context) (filePaths []string, res *oci.Regis return filePaths, res, fmt.Errorf("unable to extract %q to %q: %w", res.Filename, f.tmpDir, err) } - f.Verbosef("cleaning up leftovers files") + f.logger.Debug("Cleaning up leftovers files", f.logger.Args("followerName", f.ref)) err = os.Remove(res.Filename) if err != nil { return filePaths, res, fmt.Errorf("unable to remove file %q: %w", res.Filename, err) @@ -364,7 +364,7 @@ func (f *Follower) checkRequirements(artifactConfig *oci.ArtifactConfig) error { func (f *Follower) cleanUp() { if err := os.RemoveAll(f.tmpDir); err != nil { - f.DefaultText.Printfln("an error occurred while removing working directory %q:%v", f.tmpDir, err) + f.logger.Warn("Unable to clean working directory", f.logger.Args("followerName", f.ref, "directory", f.tmpDir, "reason", err)) } } diff --git a/internal/utils/credentials.go b/internal/utils/credentials.go index a726470c..7ca6a3c5 100644 --- a/internal/utils/credentials.go +++ b/internal/utils/credentials.go @@ -29,20 +29,18 @@ import ( func GetCredentials(p *output.Printer) (username, password string, err error) { reader := bufio.NewReader(os.Stdin) - p.DefaultText.Print("Username: ") + p.DefaultText.Print(p.FormatTitleAsLoggerInfo("Enter username:")) username, err = reader.ReadString('\n') if err != nil { return "", "", err } - p.DefaultText.Print("Password: ") + p.Logger.Info("Enter password: ") bytePassword, err := term.ReadPassword(int(os.Stdin.Fd())) if err != nil { return "", "", err } - p.DefaultText.Println() - password = string(bytePassword) return strings.TrimSpace(username), strings.TrimSpace(password), nil } diff --git a/main.go b/main.go index f491ddf8..d9b9a416 100644 --- a/main.go +++ b/main.go @@ -36,7 +36,11 @@ func main() { // If the ctx is marked as done then we reset the signals. go func() { <-ctx.Done() - opt.Printer.Info.Println("Received signal, terminating...") + // Stop all the printers if any is active + if opt.Printer != nil && opt.Printer.ProgressBar != nil && opt.Printer.ProgressBar.IsActive { + _, _ = opt.Printer.ProgressBar.Stop() + } + opt.Printer.Logger.Info("Received signal, terminating...") stop() }() @@ -44,7 +48,7 @@ func main() { rootCmd := cmd.New(ctx, opt) // Execute the command. - if err := cmd.Execute(rootCmd, opt.Printer); err != nil { + if err := cmd.Execute(rootCmd, opt); err != nil { os.Exit(1) } os.Exit(0) diff --git a/pkg/install/tls/generator.go b/pkg/install/tls/generator.go index 308e7ba0..b89dc6ec 100644 --- a/pkg/install/tls/generator.go +++ b/pkg/install/tls/generator.go @@ -28,7 +28,7 @@ import ( "path/filepath" "time" - "github.com/falcosecurity/falcoctl/pkg/output" + "github.com/pterm/pterm" ) // A GRPCTLS represents a TLS Generator for Falco. @@ -242,7 +242,7 @@ func (g *GRPCTLS) GenerateClient(caTemplate *x509.Certificate, caKey DSAKey, not } // FlushToDisk is used to persist the cert material from a GRPCTLS to disk given a path. -func (g *GRPCTLS) FlushToDisk(path string, logger *output.Printer) error { +func (g *GRPCTLS) FlushToDisk(path string, logger *pterm.Logger) error { p, err := satisfyDir(path) if err != nil { return fmt.Errorf("invalid path: %w", err) @@ -251,13 +251,13 @@ func (g *GRPCTLS) FlushToDisk(path string, logger *output.Printer) error { for _, name := range certsFilenames { f := filepath.Join(path, name) - logger.Info.Printf("Saving %s to %s\n", name, path) + logger.Info("Saving file", logger.Args("name", name, "directory", path)) if err := os.WriteFile(f, g.certs[name].Bytes(), 0o600); err != nil { return fmt.Errorf("unable to write %q: %w", name, err) } } - logger.Info.Println("Done generating the TLS certificates") + logger.Info("Done generating the TLS certificates") return nil } diff --git a/pkg/install/tls/handler.go b/pkg/install/tls/handler.go index 79ae2a0f..979b87db 100644 --- a/pkg/install/tls/handler.go +++ b/pkg/install/tls/handler.go @@ -40,6 +40,7 @@ type Options struct { // Run executes the business logic of the `install tls` command. func (o *Options) Run() error { + logger := o.Common.Printer.Logger // If the output path is not given then get the current working directory. if o.Path == "" { cwd, err := os.Getwd() @@ -49,7 +50,7 @@ func (o *Options) Run() error { o.Path = cwd } - o.Common.Printer.Info.Printf("Generating certificates in %s directory\n", o.Path) + logger.Info("Generating certificates", logger.Args("directory", o.Path)) keyGenerator := NewKeyGenerator(DSAType(o.Algorithm)) @@ -84,5 +85,5 @@ func (o *Options) Run() error { return err } - return generator.FlushToDisk(o.Path, o.Common.Printer) + return generator.FlushToDisk(o.Path, logger) } diff --git a/pkg/options/common.go b/pkg/options/common.go index 9b377320..dca5dfb4 100644 --- a/pkg/options/common.go +++ b/pkg/options/common.go @@ -18,6 +18,7 @@ package options import ( "io" + "github.com/pterm/pterm" "github.com/spf13/pflag" "github.com/falcosecurity/falcoctl/internal/config" @@ -31,37 +32,35 @@ import ( type Common struct { // Printer used by all commands to output messages. Printer *output.Printer - // printerScope contains the data of the optional scope of a prefix. - // It used to add a prefix to the output of a printer. - printerScope string // writer is used to write the output of the printer. writer io.Writer // Used to store the verbose flag, and then passed to the printer. + // Deprecated: will be removed in the future verbose bool // Disable the styling if set to true. + // Deprecated: will be removed in the future disableStyling bool // Config file. It must not be possible to be reinitialized by subcommands, // using the Initialize function. It will be attached as global flags. ConfigFile string // IndexCache caches the entries for the configured indexes. IndexCache *cache.Cache + + logLevel *LogLevel + logFormat *LogFormat } // NewOptions returns a new Common struct. func NewOptions() *Common { - return &Common{} + return &Common{ + logLevel: NewLogLevel(), + logFormat: NewLogFormat(), + } } // Configs type of the configs accepted by the Initialize function. type Configs func(options *Common) -// WithPrinterScope sets the scope for the printer. -func WithPrinterScope(scope string) Configs { - return func(options *Common) { - options.printerScope = scope - } -} - // WithWriter sets the writer for the printer. func WithWriter(writer io.Writer) Configs { return func(options *Common) { @@ -83,20 +82,35 @@ func (o *Common) Initialize(cfgs ...Configs) { cfg(o) } - // create the printer. The value of verbose is a flag value. - o.Printer = output.NewPrinter(o.printerScope, o.disableStyling, o.verbose, o.writer) -} + // TODO(alacuku): remove once we remove the old flags + var logLevel pterm.LogLevel + if o.verbose { + logLevel = pterm.LogLevelDebug + } else { + logLevel = o.logLevel.ToPtermLogLevel() + } -// IsVerbose used to check if the verbose flag is set or not. -func (o *Common) IsVerbose() bool { - return o.verbose + var logFormatter pterm.LogFormatter + if o.disableStyling { + logFormatter = pterm.LogFormatterJSON + } else { + logFormatter = o.logFormat.ToPtermFormatter() + } + + // create the printer. The value of verbose is a flag value. + o.Printer = output.NewPrinter(logLevel, logFormatter, o.writer) } // AddFlags registers the common flags. func (o *Common) AddFlags(flags *pflag.FlagSet) { flags.BoolVarP(&o.verbose, "verbose", "v", false, "Enable verbose logs (default false)") + // Mark the verbose flag as deprecated. + _ = flags.MarkDeprecated("verbose", "please use --log-level") flags.BoolVar(&o.disableStyling, "disable-styling", false, "Disable output styling such as spinners, progress bars and colors. "+ - "Styling is automatically disabled if not attacched to a tty (default false)") - // Add global config + "Styling is automatically disabled if not attached to a tty (default false)") + // Mark the disableStyling as deprecated. + _ = flags.MarkDeprecated("disable-styling", "please use --log-format") flags.StringVar(&o.ConfigFile, "config", config.ConfigPath, "config file to be used for falcoctl") + flags.Var(o.logFormat, "log-format", "Set formatting for logs (color, text, json)") + flags.Var(o.logLevel, "log-level", "Set level for logs (info, warn, debug, trace)") } diff --git a/pkg/options/enum.go b/pkg/options/enum.go new file mode 100644 index 00000000..a9e53143 --- /dev/null +++ b/pkg/options/enum.go @@ -0,0 +1,58 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright (C) 2023 The Falco Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package options + +import ( + "fmt" + "strings" + + "golang.org/x/exp/slices" +) + +// Enum implements the flag interface. It can be used as a base for new flags that +// can have a limited set of values. +type Enum struct { + allowed []string + value string +} + +// NewEnum returns an enum struct. The firs argument is a set of values allowed for the flag. +// The second argument is the default value of the flag. +func NewEnum(allowed []string, d string) *Enum { + return &Enum{ + allowed: allowed, + value: d, + } +} + +// String returns the value. +func (e *Enum) String() string { + return e.value +} + +// Set the value for the flag. +func (e *Enum) Set(p string) error { + if !slices.Contains(e.allowed, p) { + return fmt.Errorf("invalid argument %q, please provide one of (%s)", p, strings.Join(e.allowed, ", ")) + } + e.value = p + return nil +} + +// Type returns the type of the flag. +func (e *Enum) Type() string { + return "string" +} diff --git a/pkg/options/enum_test.go b/pkg/options/enum_test.go new file mode 100644 index 00000000..f36efe3e --- /dev/null +++ b/pkg/options/enum_test.go @@ -0,0 +1,96 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright (C) 2023 The Falco Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package options + +import ( + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Describe("Enum", func() { + var ( + allowed = []string{"val1", "val2", "val3"} + defValue = "val1" + enum *Enum + ) + + BeforeEach(func() { + enum = NewEnum(allowed, defValue) + }) + + JustAfterEach(func() { + enum = nil + }) + + Context("NewEnum Func", func() { + It("should return a not nil struct", func() { + Expect(enum).ShouldNot(BeNil()) + }) + + It("should set the default values", func() { + Expect(enum.value).Should(Equal(defValue)) + }) + + It("should set the allowed values", func() { + Expect(enum.allowed).Should(Equal(allowed)) + }) + }) + + Context("Set Func", func() { + var val string + var err error + newVal := "val2" + newValWrong := "WrongVal" + + JustBeforeEach(func() { + err = enum.Set(val) + }) + + Context("Setting an allowed value", func() { + BeforeEach(func() { + val = newVal + }) + + It("Should set the correct val", func() { + Expect(err).ShouldNot(HaveOccurred()) + Expect(enum.value).Should(Equal(newVal)) + }) + }) + + Context("Setting a not allowed value", func() { + BeforeEach(func() { + val = newValWrong + }) + + It("Should error", func() { + Expect(err).Should(HaveOccurred()) + }) + }) + }) + + Context("String Func", func() { + It("Should return the setted value", func() { + Expect(enum.String()).Should(Equal(defValue)) + }) + }) + + Context("Type Func", func() { + It("Should return the string type", func() { + Expect(enum.Type()).Should(Equal("string")) + }) + }) + +}) diff --git a/pkg/options/logFormat.go b/pkg/options/logFormat.go new file mode 100644 index 00000000..a8e8b951 --- /dev/null +++ b/pkg/options/logFormat.go @@ -0,0 +1,59 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright (C) 2023 The Falco Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package options + +import ( + "github.com/pterm/pterm" +) + +const ( + // LogFormatColor formatting option for logs. + LogFormatColor = "color" + // LogFormatText formatting option for logs. + LogFormatText = "text" + // LogFormatJSON formatting otion for logs. + LogFormatJSON = "json" +) + +var logFormats = []string{LogFormatColor, LogFormatText, LogFormatJSON} + +// LogFormat data structure for log-format flag. +type LogFormat struct { + *Enum +} + +// NewLogFormat returns a new Enum configured for the log formats flag. +func NewLogFormat() *LogFormat { + return &LogFormat{ + Enum: NewEnum(logFormats, LogFormatColor), + } +} + +// ToPtermFormatter converts the current formatter to pterm.LogFormatter. +func (lg *LogFormat) ToPtermFormatter() pterm.LogFormatter { + var formatter pterm.LogFormatter + + switch lg.value { + case LogFormatColor: + formatter = pterm.LogFormatterColorful + case LogFormatText: + pterm.DisableColor() + formatter = pterm.LogFormatterColorful + case LogFormatJSON: + formatter = pterm.LogFormatterJSON + } + return formatter +} diff --git a/pkg/options/logFormat_test.go b/pkg/options/logFormat_test.go new file mode 100644 index 00000000..fed1a6dd --- /dev/null +++ b/pkg/options/logFormat_test.go @@ -0,0 +1,88 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright (C) 2023 The Falco Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package options + +import ( + "github.com/gookit/color" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + "github.com/pterm/pterm" +) + +var _ = Describe("LogFormat", func() { + var ( + logFormatter *LogFormat + ) + BeforeEach(func() { + logFormatter = NewLogFormat() + }) + + Context("NewLogFormat Func", func() { + It("should return a new logFormatter", func() { + Expect(logFormatter).ShouldNot(BeNil()) + Expect(logFormatter.value).Should(Equal(LogFormatColor)) + Expect(logFormatter.allowed).Should(Equal(logFormats)) + }) + }) + + Context("ToPtermFormatter Func", func() { + var output pterm.LogFormatter + + JustBeforeEach(func() { + output = logFormatter.ToPtermFormatter() + }) + + Context("Color", func() { + BeforeEach(func() { + Expect(logFormatter.Set(LogFormatColor)).ShouldNot(HaveOccurred()) + }) + + It("should return the color logFormatter", func() { + Expect(output).Should(Equal(pterm.LogFormatterColorful)) + Expect(pterm.PrintColor).Should(BeTrue()) + Expect(color.Enable).Should(BeTrue()) + }) + }) + + Context("Text", func() { + BeforeEach(func() { + Expect(logFormatter.Set(LogFormatText)).ShouldNot(HaveOccurred()) + }) + + AfterEach(func() { + pterm.EnableColor() + }) + + It("should return the text logFormatter", func() { + Expect(output).Should(Equal(pterm.LogFormatterColorful)) + Expect(pterm.PrintColor).Should(BeFalse()) + Expect(color.Enable).Should(BeFalse()) + }) + }) + + Context("JSON", func() { + BeforeEach(func() { + Expect(logFormatter.Set(LogFormatJSON)).ShouldNot(HaveOccurred()) + }) + + It("should return the json logFormatter", func() { + Expect(output).Should(Equal(pterm.LogFormatterJSON)) + Expect(pterm.PrintColor).Should(BeTrue()) + Expect(color.Enable).Should(BeTrue()) + }) + }) + }) +}) diff --git a/pkg/options/logLevel.go b/pkg/options/logLevel.go new file mode 100644 index 00000000..b574696a --- /dev/null +++ b/pkg/options/logLevel.go @@ -0,0 +1,61 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright (C) 2023 The Falco Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package options + +import ( + "github.com/pterm/pterm" +) + +const ( + // LogLevelInfo level option for logs. + LogLevelInfo = "info" + // LogLevelWarn level opiton for logs. + LogLevelWarn = "warn" + // LogLevelDebug level option for logs. + LogLevelDebug = "debug" + // LogLevelTrace level option for logs. + LogLevelTrace = "trace" +) + +var logLevels = []string{LogLevelInfo, LogLevelWarn, LogLevelDebug, LogLevelTrace} + +// LogLevel data structure for log-level flag. +type LogLevel struct { + *Enum +} + +// NewLogLevel returns a new Enum configured for the log level flag. +func NewLogLevel() *LogLevel { + return &LogLevel{ + Enum: NewEnum(logLevels, LogLevelInfo), + } +} + +// ToPtermLogLevel converts the current log level to pterm.LogLevel. +func (ll *LogLevel) ToPtermLogLevel() pterm.LogLevel { + var level pterm.LogLevel + switch ll.value { + case LogLevelInfo: + level = pterm.LogLevelInfo + case LogLevelWarn: + level = pterm.LogLevelWarn + case LogLevelDebug: + level = pterm.LogLevelDebug + case LogLevelTrace: + level = pterm.LogLevelTrace + } + return level +} diff --git a/pkg/options/logLevel_test.go b/pkg/options/logLevel_test.go new file mode 100644 index 00000000..5f7b579b --- /dev/null +++ b/pkg/options/logLevel_test.go @@ -0,0 +1,87 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright (C) 2023 The Falco Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package options + +import ( + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + "github.com/pterm/pterm" +) + +var _ = Describe("LogLevel", func() { + var ( + logLevel *LogLevel + ) + BeforeEach(func() { + logLevel = NewLogLevel() + }) + + Context("NewLogLevel Func", func() { + It("should return a new LogLevel", func() { + Expect(logLevel).ShouldNot(BeNil()) + Expect(logLevel.value).Should(Equal(LogLevelInfo)) + Expect(logLevel.allowed).Should(Equal(logLevels)) + }) + }) + + Context("ToPtermLogLevel Func", func() { + var output pterm.LogLevel + + JustBeforeEach(func() { + output = logLevel.ToPtermLogLevel() + }) + + Context("Info", func() { + BeforeEach(func() { + Expect(logLevel.Set(LogLevelInfo)).ShouldNot(HaveOccurred()) + }) + + It("should return the Info level", func() { + Expect(output).Should(Equal(pterm.LogLevelInfo)) + }) + }) + + Context("Warn", func() { + BeforeEach(func() { + Expect(logLevel.Set(LogLevelWarn)).ShouldNot(HaveOccurred()) + }) + + It("should return the Warn level", func() { + Expect(output).Should(Equal(pterm.LogLevelWarn)) + }) + }) + + Context("Debug", func() { + BeforeEach(func() { + Expect(logLevel.Set(LogLevelDebug)).ShouldNot(HaveOccurred()) + }) + + It("should return the Debug level", func() { + Expect(output).Should(Equal(pterm.LogLevelDebug)) + }) + }) + + Context("Trace", func() { + BeforeEach(func() { + Expect(logLevel.Set(LogLevelTrace)).ShouldNot(HaveOccurred()) + }) + + It("should return the Info level", func() { + Expect(output).Should(Equal(pterm.LogLevelTrace)) + }) + }) + }) +}) diff --git a/pkg/options/options_suite_test.go b/pkg/options/options_suite_test.go new file mode 100644 index 00000000..f4e4c6a7 --- /dev/null +++ b/pkg/options/options_suite_test.go @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright (C) 2023 The Falco Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package options + +import ( + "testing" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +func TestOptions(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Options Suite") +} diff --git a/pkg/output/output.go b/pkg/output/output.go index 40be3e25..3f073e51 100644 --- a/pkg/output/output.go +++ b/pkg/output/output.go @@ -16,6 +16,7 @@ package output import ( + "bytes" "fmt" "io" "os" @@ -40,100 +41,73 @@ const ( var spinnerCharset = []string{"⠈⠁", "⠈⠑", "⠈⠱", "⠈⡱", "⢀⡱", "⢄⡱", "⢄⡱", "⢆⡱", "⢎⡱", "⢎⡰", "⢎⡠", "⢎⡀", "⢎⠁", "⠎⠁", "⠊⠁"} +// NewProgressBar returns a new progress bar printer. +func NewProgressBar() pterm.ProgressbarPrinter { + return *pterm.DefaultProgressbar. + WithTitleStyle(pterm.NewStyle(pterm.FgDefault)). + WithBarStyle(pterm.NewStyle(pterm.FgDefault)). + WithBarCharacter("#"). + WithLastCharacter("#"). + WithShowElapsedTime(false). + WithShowCount(false). + WithMaxWidth(90). + WithRemoveWhenDone(true) +} + // Printer used by all commands to output messages. // If a commands needs a new format for its output add it here. type Printer struct { - Info *pterm.PrefixPrinter - Success *pterm.PrefixPrinter - Warning *pterm.PrefixPrinter - Error *pterm.PrefixPrinter - - DefaultText *pterm.BasicTextPrinter - TablePrinter *pterm.TablePrinter - - ProgressBar *pterm.ProgressbarPrinter - - Spinner *pterm.SpinnerPrinter - + Logger *pterm.Logger + DefaultText *pterm.BasicTextPrinter + TablePrinter *pterm.TablePrinter + ProgressBar *pterm.ProgressbarPrinter + Spinner *pterm.SpinnerPrinter DisableStyling bool - verbose bool } // NewPrinter returns a printer ready to be used. -func NewPrinter(scope string, disableStyling, verbose bool, writer io.Writer) *Printer { +func NewPrinter(logLevel pterm.LogLevel, logFormatter pterm.LogFormatter, writer io.Writer) *Printer { + var disableStyling bool // If we are not in a tty then make sure that the disableStyling variable is set to true since // we use it elsewhere to check if we are in a tty or not. We force the disableStyling to true // only if it is set to false and we are not in a tty. Otherwise let it as it is, false if the // user has not set it (default) otherwise true. - if !disableStyling && !isatty.IsTerminal(os.Stdout.Fd()) { + if (logFormatter != pterm.LogFormatterJSON && !isatty.IsTerminal(os.Stdout.Fd())) || logFormatter == pterm.LogFormatterJSON { disableStyling = true } - generic := &pterm.PrefixPrinter{MessageStyle: pterm.NewStyle(pterm.FgDefault)} + logger := pterm.DefaultLogger. + WithLevel(logLevel).WithFormatter(logFormatter). + WithMaxWidth(150) + basicText := &pterm.BasicTextPrinter{} - progressBar := pterm.DefaultProgressbar. - WithTitleStyle(pterm.NewStyle(pterm.FgDefault)). - WithBarStyle(pterm.NewStyle(pterm.FgDefault)). - WithBarCharacter("#"). - WithLastCharacter("#"). - WithShowElapsedTime(false) + tablePrinter := pterm.DefaultTable.WithHasHeader().WithSeparator("\t") spinner := &pterm.SpinnerPrinter{ Sequence: spinnerCharset, Style: pterm.NewStyle(pterm.FgDefault), Delay: time.Millisecond * 100, MessageStyle: pterm.NewStyle(pterm.FgDefault), - RemoveWhenDone: false, + RemoveWhenDone: true, ShowTimer: true, TimerRoundingFactor: time.Second, TimerStyle: &pterm.ThemeDefault.TimerStyle, } printer := Printer{ - verbose: verbose, - Info: generic.WithPrefix(pterm.Prefix{ - Text: "INFO", - Style: pterm.NewStyle(pterm.FgDefault), - }), - - Success: generic.WithPrefix(pterm.Prefix{ - Text: "INFO", - Style: pterm.NewStyle(pterm.FgLightGreen), - }), - - Warning: generic.WithPrefix(pterm.Prefix{ - Text: "WARN", - Style: pterm.NewStyle(pterm.FgYellow), - }), - - Error: generic.WithPrefix(pterm.Prefix{ - Text: "ERRO", - Style: pterm.NewStyle(pterm.FgRed), - }), - - DefaultText: basicText, - - ProgressBar: progressBar, - - TablePrinter: tablePrinter, - - Spinner: spinner, - + DefaultText: basicText, + TablePrinter: tablePrinter, + Spinner: spinner, DisableStyling: disableStyling, + Logger: logger, } - // Populate the printers for the spinner. We use the same one define in the printer. - printer.Spinner.FailPrinter = printer.Error - printer.Spinner.WarningPrinter = printer.Warning - printer.Spinner.SuccessPrinter = printer.Info - printer.Spinner.InfoPrinter = printer.Info - // We disable styling when the program is not attached to a tty or when requested by the user. if disableStyling { pterm.DisableStyling() } - return printer.WithScope(scope).WithWriter(writer) + return printer.WithWriter(writer) } // CheckErr prints a user-friendly error based on the active printer. @@ -143,20 +117,28 @@ func (p *Printer) CheckErr(err error) { case err == nil: return - // Print the error through the spinner, if active. + // Stop the spinner, if active. case p != nil && p.Spinner.IsActive: handlerFunc = func(msg string) { - p.Spinner.Fail(msg) + _ = p.Spinner.Stop() + p.Logger.Error(msg) + } + // Stop the progress bar, if active. + case p != nil && p.ProgressBar != nil && p.ProgressBar.IsActive: + + handlerFunc = func(msg string) { + _, _ = p.ProgressBar.Stop() + p.Logger.Error(msg) } // If the printer is initialized then print the error through it. case p != nil: handlerFunc = func(msg string) { - msg = strings.TrimPrefix(msg, "error: ") - p.Error.Println(strings.TrimRight(msg, "\n")) + p.Logger.Error(msg) } // Otherwise, restore the default behavior. + // It should never happen. default: handlerFunc = func(msg string) { fmt.Printf("%s (it seems that the printer has not been initialized, that's why you are seeing this message", msg) @@ -166,13 +148,6 @@ func (p *Printer) CheckErr(err error) { handlerFunc(err.Error()) } -// Verbosef outputs verbose messages if the verbose flags is set. -func (p *Printer) Verbosef(format string, args ...interface{}) { - if p.verbose { - p.Info.Printfln(strings.TrimRight(format, "\n"), args...) - } -} - // PrintTable is a helper used to print data in table format. func (p *Printer) PrintTable(header TableHeader, data [][]string) error { var table [][]string @@ -196,50 +171,14 @@ func (p *Printer) PrintTable(header TableHeader, data [][]string) error { // WithWriter sets the writer for the current printer. func (p Printer) WithWriter(writer io.Writer) *Printer { if writer != nil { - p.Info = p.Info.WithWriter(writer) - p.Success = p.Success.WithWriter(writer) - p.Warning = p.Warning.WithWriter(writer) - p.Error = p.Error.WithWriter(writer) p.Spinner = p.Spinner.WithWriter(writer) p.DefaultText = p.DefaultText.WithWriter(writer) - p.ProgressBar = p.ProgressBar.WithWriter(writer) p.TablePrinter = p.TablePrinter.WithWriter(writer) + p.Logger = p.Logger.WithWriter(writer) } - return &p } -// WithScope sets the scope for the current printer. -func (p Printer) WithScope(scope string) *Printer { - if scope != "" { - s := pterm.Scope{Text: scope, Style: pterm.NewStyle(pterm.FgGray)} - - p.Info = p.Info.WithScope(s) - p.Error = p.Error.WithScope(s) - p.Warning = p.Warning.WithScope(s) - - p.Spinner.FailPrinter = p.Error - p.Spinner.InfoPrinter = p.Info - p.Spinner.SuccessPrinter = p.Info - p.Spinner.WarningPrinter = p.Warning - } - - return &p -} - -// DisableStylingf disables styling globally for all existing printers. -func (p *Printer) DisableStylingf() { - pterm.DisableStyling() -} - -// EnableStyling enables styling globally for all existing printers. -func (p *Printer) EnableStyling() { - pterm.EnableStyling() - if p.DisableStyling { - pterm.DisableColor() - } -} - // ExitOnErr aborts the execution in case of errors, and prints the error using the configured printer. func ExitOnErr(p *Printer, err error) { if err != nil { @@ -247,3 +186,11 @@ func ExitOnErr(p *Printer, err error) { os.Exit(1) } } + +// FormatTitleAsLoggerInfo returns the msg formatted as been printed by +// the Info logger. +func (p *Printer) FormatTitleAsLoggerInfo(msg string) string { + buf := &bytes.Buffer{} + p.Logger.WithWriter(buf).Info(msg) + return strings.TrimRight(buf.String(), "\n") +} diff --git a/pkg/output/output_test.go b/pkg/output/output_test.go index 4fe0fe8d..e96a8a90 100644 --- a/pkg/output/output_test.go +++ b/pkg/output/output_test.go @@ -17,98 +17,335 @@ package output import ( "bytes" + "errors" + "fmt" "io" + "github.com/gookit/color" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" + "github.com/onsi/gomega/gbytes" + "github.com/pterm/pterm" ) -var _ = Describe("Output", func() { +var _ = Describe("NewPrinter func", func() { var ( - printer *Printer - scope string - verbose bool - writer io.Writer + printer *Printer + logFormatter pterm.LogFormatter + logLevel pterm.LogLevel + writer io.Writer ) JustBeforeEach(func() { - printer = NewPrinter(scope, false, verbose, writer) + printer = NewPrinter(logLevel, logFormatter, writer) }) JustAfterEach(func() { printer = nil - scope = "" - verbose = false + logFormatter = 1000 + logLevel = 1000 writer = nil }) - Context("a new printer is created", func() { - Context("with scope", func() { + Context("with writer", func() { + BeforeEach(func() { + writer = &bytes.Buffer{} + }) + + It("should correctly set the writer for each printer object", func() { + Expect(printer.Logger.Writer).Should(Equal(writer)) + Expect(printer.Spinner.Writer).Should(Equal(writer)) + Expect(printer.DefaultText.Writer).Should(Equal(writer)) + Expect(printer.TablePrinter.Writer).Should(Equal(writer)) + }) + }) + + Context("with log-level", func() { + Describe("info", func() { + BeforeEach(func() { + logLevel = pterm.LogLevelInfo + }) + It("should correctly set the log level to info", func() { + Expect(printer.Logger.Level).Should(Equal(pterm.LogLevelInfo)) + }) + }) + + Describe("warn", func() { BeforeEach(func() { - scope = "CustomScope" + logLevel = pterm.LogLevelWarn }) + It("should correctly set the log level to warn", func() { + Expect(printer.Logger.Level).Should(Equal(pterm.LogLevelWarn)) + }) + }) - It("should correctly set the scope for each printer object", func() { - Expect(printer.Error.Scope.Text).Should(Equal(scope)) - Expect(printer.Info.Scope.Text).Should(Equal(scope)) - Expect(printer.Warning.Scope.Text).Should(Equal(scope)) + Describe("debug", func() { + BeforeEach(func() { + logLevel = pterm.LogLevelDebug + }) + It("should correctly set the log level to debug", func() { + Expect(printer.Logger.Level).Should(Equal(pterm.LogLevelDebug)) }) }) - Context("with writer", func() { + Describe("error", func() { BeforeEach(func() { - writer = &bytes.Buffer{} + logLevel = pterm.LogLevelError }) + It("should correctly set the log level to error", func() { + Expect(printer.Logger.Level).Should(Equal(pterm.LogLevelError)) + }) + }) + }) - It("should correctly set the writer for each printer object", func() { - Expect(printer.Error.Writer).Should(Equal(writer)) - Expect(printer.Info.Writer).Should(Equal(writer)) - Expect(printer.Warning.Writer).Should(Equal(writer)) - Expect(printer.DefaultText.Writer).Should(Equal(writer)) + Context("with log-formatter", func() { + Describe("colorful", func() { + BeforeEach(func() { + logFormatter = pterm.LogFormatterColorful + }) + It("should correctly set the log formatter to colorful", func() { + Expect(printer.Logger.Formatter).Should(Equal(pterm.LogFormatterColorful)) }) }) - Context("with verbose", func() { + Describe("json", func() { BeforeEach(func() { - verbose = true + logFormatter = pterm.LogFormatterJSON + }) + + It("should correctly set the log level to json", func() { + Expect(printer.Logger.Formatter).Should(Equal(pterm.LogFormatterJSON)) + }) + + It("should correctly disable styling at pterm package level", func() { + Expect(pterm.RawOutput).Should(BeTrue()) + }) + + It("should correctly disable color at pterm package level", func() { + Expect(pterm.PrintColor).Should(BeFalse()) }) - It("should correctly set the verbose variable to true", func() { - Expect(printer.verbose).Should(BeTrue()) + + It("should correctly disable color at color package level", func() { + Expect(color.Enable).Should(BeFalse()) }) }) }) +}) + +var _ = Describe("CheckErr func", func() { + var ( + buf *gbytes.Buffer + printer *Printer + err error + ) + + JustBeforeEach(func() { + printer.CheckErr(err) + }) + + JustAfterEach(func() { + printer = nil + Expect(buf.Clear()).ShouldNot(HaveOccurred()) + err = nil + }) + + Context("printer is nil", func() { + BeforeEach(func() { + buf = gbytes.NewBuffer() + // Set printer to nil. + printer = nil + err = errors.New("printer is set to nil") + }) + + It("should print using fmt", func() { + Expect(buf).ShouldNot(gbytes.Say( + fmt.Sprintf("%s (it seems that the printer has not been initialized, that's why you are seeing this message)", err.Error()))) + }) + }) + + Context("only printer is active and defined", func() { + BeforeEach(func() { + buf = gbytes.NewBuffer() + // Set printer to nil. + printer = NewPrinter(pterm.LogLevelInfo, pterm.LogFormatterColorful, buf) + // Make sure that no other printers are active. + Expect(printer.ProgressBar).Should(BeNil()) + Expect(printer.Spinner.IsActive).Should(BeFalse()) + err = errors.New("only printers without effects") + }) + + It("should print using fmt", func() { + Expect(buf).Should(gbytes.Say(err.Error())) + }) + }) - Context("testing output using the verbose function", func() { - var ( - msg = "Testing verbose mode" - customWriter *bytes.Buffer - ) + Context("error is nil", func() { + BeforeEach(func() { + buf = gbytes.NewBuffer() + // Set printer to nil. + printer = NewPrinter(pterm.LogLevelInfo, pterm.LogFormatterColorful, buf) + // Make sure that no other printers are active. + Expect(printer.ProgressBar).Should(BeNil()) + Expect(printer.Spinner.IsActive).Should(BeFalse()) + err = nil + }) + + It("should print nothing", func() { + Expect(len(buf.Contents())).Should(BeZero()) + }) + }) + Context("spinner is active", func() { BeforeEach(func() { - // set the output writer. - customWriter = &bytes.Buffer{} - writer = customWriter + buf = gbytes.NewBuffer() + printer = NewPrinter(pterm.LogLevelInfo, pterm.LogFormatterColorful, buf) + printer.Spinner, _ = printer.Spinner.Start() + // Check that the spinner is active. + Expect(printer.Spinner.IsActive).Should(BeTrue()) + err = errors.New("spinner is active") }) - JustBeforeEach(func() { - // call the output function - printer.Verbosef("%s", msg) + It("should print using logger", func() { + Expect(buf).Should(gbytes.Say(err.Error())) }) - Context("verbose mode is disabled", func() { - It("should not output the message", func() { - Expect(customWriter.String()).Should(BeEmpty()) - }) + It("should stop the spinner", func() { + Expect(printer.Spinner.IsActive).Should(BeFalse()) }) + }) - Context("verbose mode is enabled", func() { - BeforeEach(func() { - verbose = true - }) + Context("spinner progress bar is active", func() { + BeforeEach(func() { + buf = gbytes.NewBuffer() + printer = NewPrinter(pterm.LogLevelInfo, pterm.LogFormatterColorful, buf) + printer.ProgressBar, _ = NewProgressBar().Start() + // Check that the progress bar is active. + Expect(printer.ProgressBar.IsActive).Should(BeTrue()) + err = errors.New("progress bar is active") + }) - It("should output the message", func() { - Expect(customWriter.String()).Should(ContainSubstring(msg)) - }) + It("should print using logger", func() { + Expect(buf).Should(gbytes.Say(err.Error())) + }) + + It("should stop the progress bar", func() { + Expect(printer.ProgressBar.IsActive).Should(BeFalse()) + }) + }) + +}) + +var _ = Describe("PrintTable func", func() { + var ( + buf *gbytes.Buffer + printer *Printer + header TableHeader + err error + ) + + JustBeforeEach(func() { + printer = NewPrinter(pterm.LogLevelInfo, pterm.LogFormatterColorful, buf) + err = printer.PrintTable(header, nil) + }) + + JustAfterEach(func() { + printer = nil + Expect(buf.Clear()).ShouldNot(HaveOccurred()) + header = 1000 + }) + + Context("artifact search header", func() { + BeforeEach(func() { + buf = gbytes.NewBuffer() + header = ArtifactSearch + }) + + It("should print header", func() { + header := []string{"INDEX", "ARTIFACT", "TYPE", "REGISTRY", "REPOSITORY"} + for _, col := range header { + Expect(buf).Should(gbytes.Say(col)) + } + }) + }) + + Context("index list header", func() { + BeforeEach(func() { + buf = gbytes.NewBuffer() + header = IndexList + }) + + It("should print header", func() { + header := []string{"NAME", "URL", "ADDED", "UPDATED"} + for _, col := range header { + Expect(buf).Should(gbytes.Say(col)) + } + }) + }) + + Context("index list header", func() { + BeforeEach(func() { + buf = gbytes.NewBuffer() + header = ArtifactInfo + }) + + It("should print header", func() { + header := []string{"REF", "TAGS"} + for _, col := range header { + Expect(buf).Should(gbytes.Say(col)) + } + }) + }) + + Context("header is not defined", func() { + BeforeEach(func() { + buf = gbytes.NewBuffer() + header = 1000 + }) + + It("should error", func() { + Expect(err).ShouldNot(BeNil()) + }) + }) + +}) + +var _ = Describe("FormatTitleAsLoggerInfo func", func() { + var ( + printer *Printer + msg string + formattedMsg string + ) + + JustBeforeEach(func() { + printer = NewPrinter(pterm.LogLevelInfo, pterm.LogFormatterColorful, nil) + formattedMsg = printer.FormatTitleAsLoggerInfo(msg) + }) + + JustAfterEach(func() { + printer = nil + }) + + Context("message without trailing new line", func() { + BeforeEach(func() { + msg = "Testing message without new line" + }) + + It("should format according to the INFO logger", func() { + output := fmt.Sprintf("INFO %s", msg) + Expect(formattedMsg).Should(ContainSubstring(output)) + }) + }) + + Context("message with trailing new line", func() { + BeforeEach(func() { + msg = "Testing message with new line" + }) + + It("should format according to the INFO logger and remove newline", func() { + output := fmt.Sprintf("INFO %s", msg) + Expect(formattedMsg).Should(ContainSubstring(output)) + Expect(formattedMsg).ShouldNot(ContainSubstring("\n")) }) }) + }) diff --git a/pkg/output/tracker.go b/pkg/output/tracker.go index b9383119..b827e70a 100644 --- a/pkg/output/tracker.go +++ b/pkg/output/tracker.go @@ -57,28 +57,30 @@ func NewProgressTracker(printer *Printer, target oras.Target, msg string) *Progr // Push reimplements the Push function of the oras.Target interface adding the needed logic for the progress bar. func (t *ProgressTracker) Push(ctx context.Context, expected v1.Descriptor, content io.Reader) error { //nolint:gocritic,lll // needed to implement the oras.Target interface - var progressBar *pterm.ProgressbarPrinter d := expected.Digest.Encoded()[:12] + t.Logger.Info(fmt.Sprintf("%s layer %s", t.msg, d)) + if !t.Printer.DisableStyling { - progressBar, _ = t.ProgressBar.WithTotal(int(expected.Size)).WithTitle(fmt.Sprintf(" INFO %s %s:", t.msg, d)).WithShowCount(false).Start() - } else { - t.Info.Printfln("%s %s", t.msg, d) + t.ProgressBar, _ = NewProgressBar(). + WithTotal(int(expected.Size)). + WithTitle(fmt.Sprintf("%s layer %s", t.msg, d)). + Start() } reader := &trackedReader{ Reader: content, descriptor: expected, - progressBar: progressBar, - } - err := t.Target.Push(ctx, expected, reader) - if !t.Printer.DisableStyling { - _, _ = progressBar.Stop() + progressBar: t.ProgressBar, } - if err != nil { - t.Error.Printfln("unable to push artifact %s", err) + if err := t.Target.Push(ctx, expected, reader); err != nil { return err } + + if !t.Printer.DisableStyling { + _, _ = t.ProgressBar.Stop() + } + return nil } @@ -90,7 +92,7 @@ func (t *ProgressTracker) Exists(ctx context.Context, target v1.Descriptor) (boo return ok, err } if ok { - t.Info.Printfln("%s: layer already exists", d) + t.Logger.Info(fmt.Sprintf("%s: layer already exists", d)) } return ok, err }