Repository: ratify-project/ratify (go)

Issue:
Warning message is printed to stdout by CLI
### What happened in your environment?

After running
```sh
ratify verify -c config.json -s mcr.microsoft.com/oss/deislabs/ratify-base:v1.2.0 > log.json
```
the `ratify` CLI saves the following content to `log.json`.
```json
Warning: Digest should be used instead of tagged reference. The resolved digest may not point to the same signed artifact, since tags are mutable.
{
  "verifierReports": [
    {
      "subject": "mcr.microsoft.com/oss/deislabs/ratify-base:v1.2.0",
      "referenceDigest": "sha256:664dbce8187af59ee9a156b10f1ae66c0ab74b2d356bcce6ae3bfbffc90ddcf2",
      "artifactType": "application/vnd.cncf.notary.signature",
      "verifierReports": [
        {
          "isSuccess": false,
          "message": "Original Error: (Original Error: (valid certificates must be provided, only CA certificates or self-signed signing certificates are supported), Error: verify plugin failure, Code: VERIFY_PLUGIN_FAILURE, Plugin Name: notation, Component Type: verifier, Documentation: https://ratify.dev/docs/troubleshoot/verifier/notation, Detail: failed to verify signature of digest), Error: verify reference failure, Code: VERIFY_REFERENCE_FAILURE, Plugin Name: notation, Component Type: verifier",
          "name": "notation",
          "extensions": null
        }
      ],
      "nestedReports": []
    },
    {
      "subject": "mcr.microsoft.com/oss/deislabs/ratify-base:v1.2.0",
      "referenceDigest": "sha256:6557162adb2a50ac98b52477ce8959858ba3bafbb94f346fcf764ac0c2aa8346",
      "artifactType": "application/vnd.in-toto+json",
      "verifierReports": [],
      "nestedReports": []
    },
    {
      "subject": "mcr.microsoft.com/oss/deislabs/ratify-base:v1.2.0",
      "referenceDigest": "sha256:f281be7185446aa5bd346b3ee859061c95199830cfd42ce289cda2994205076f",
      "artifactType": "application/spdx+json",
      "verifierReports": [],
      "nestedReports": [
        {
          "subject": "mcr.microsoft.com/oss/deislabs/ratify-base@sha256:f281be7185446aa5bd346b3ee859061c95199830cfd42ce289cda2994205076f",
          "referenceDigest": "sha256:9a330411e967bde20bd41702fa6cdb32ab27183f53cd3a17af3ebac41d3112b2",
          "artifactType": "application/vnd.cncf.notary.signature",
          "verifierReports": [
            {
              "isSuccess": false,
              "message": "Original Error: (Original Error: (valid certificates must be provided, only CA certificates or self-signed signing certificates are supported), Error: verify plugin failure, Code: VERIFY_PLUGIN_FAILURE, Plugin Name: notation, Component Type: verifier, Documentation: https://ratify.dev/docs/troubleshoot/verifier/notation, Detail: failed to verify signature of digest), Error: verify reference failure, Code: VERIFY_REFERENCE_FAILURE, Plugin Name: notation, Component Type: verifier",
              "name": "notation",
              "extensions": null
            }
          ],
          "nestedReports": []
        }
      ]
    },
    {
      "subject": "mcr.microsoft.com/oss/deislabs/ratify-base:v1.2.0",
      "referenceDigest": "sha256:ac86395350279f460c6bc08eb7875583c7365c423ebf9a7ac5a7a0f86f87924e",
      "artifactType": "application/spdx+json",
      "verifierReports": [],
      "nestedReports": [
        {
          "subject": "mcr.microsoft.com/oss/deislabs/ratify-base@sha256:ac86395350279f460c6bc08eb7875583c7365c423ebf9a7ac5a7a0f86f87924e",
          "referenceDigest": "sha256:d93c4208945899f65c50f03024892a6f106344a0759eb1168e43d497d1582e40",
          "artifactType": "application/vnd.cncf.notary.signature",
          "verifierReports": [
            {
              "isSuccess": false,
              "message": "Original Error: (Original Error: (valid certificates must be provided, only CA certificates or self-signed signing certificates are supported), Error: verify plugin failure, Code: VERIFY_PLUGIN_FAILURE, Plugin Name: notation, Component Type: verifier, Documentation: https://ratify.dev/docs/troubleshoot/verifier/notation, Detail: failed to verify signature of digest), Error: verify reference failure, Code: VERIFY_REFERENCE_FAILURE, Plugin Name: notation, Component Type: verifier",
              "name": "notation",
              "extensions": null
            }
          ],
          "nestedReports": []
        }
      ]
    }
  ]
}
```
As we can observe, `log.json` contains a warning line and thus it renders `log.json` not a JSON object.

### What did you expect to happen?

Print the warning message in the `stderr` instead of `stdout`.

### What version of Kubernetes are you running?

N/A

### What version of Ratify are you running?

v1.2.0

### Anything else you would like to add?

It is the `ratify` CLI, not running in k8s.

### Are you willing to submit PRs to contribute to this bug fix?

- [ ] Yes, I am willing to implement it.

Pull request:
fix: warning message is printed to stdout by CLI
# Description

## What this PR does / why we need it:
Currently we are printing warning log out to stdout which pollutes the json verification output. 
This PR contains following update: 
- Warning should be logged using the logger. 
- added a unit test file for cli cmd. However the cli command currently is not unit testable, the test currently only validates the command exist.

## Which issue(s) this PR fixes *(optional, using `fixes #<issue number>(, fixes #<issue_number>, ...)` format, will close the issue(s) when the PR gets merged)*:
Fixes #1565

## Type of change

Please delete options that are not relevant.

- [X ] Bug fix (non-breaking change which fixes an issue)

# How Has This Been Tested?

Validated the  output only contains the verification result json. The warning is now logged ot stderr.

![image](https://github.com/user-attachments/assets/d985c79b-1f61-4934-8d0a-5850e3d2557e)

# Checklist:

- [ ] Does the affected code have corresponding tests?
- [ ] Are the changes documented, not just with inline documentation, but also with conceptual documentation such as an overview of a new feature, or task-based documentation like a tutorial? Consider if this change should be announced on your project blog.
- [ ] Does this introduce breaking changes that would require an announcement or bumping the major version?
- [ ] Do all new files have appropriate license header?

# Post Merge Requirements
- [ ] MAINTAINERS: manually trigger the "Publish Package" workflow after merging any PR that indicates `Helm Chart Change`


Patch:
diff --git a/cmd/ratify/cmd/discover.go b/cmd/ratify/cmd/discover.go
index 66d9d09..858f12b 100644
--- a/cmd/ratify/cmd/discover.go
+++ b/cmd/ratify/cmd/discover.go
@@ -96,10 +96,6 @@ func discover(opts discoverCmdOptions) error {
 		return err
 	}
 
-	if subRef.Digest == "" {
-		fmt.Println(taggedReferenceWarning)
-	}
-
 	cf, err := config.Load(opts.configFilePath)
 	if err != nil {
 		return err
@@ -109,6 +105,10 @@ func discover(opts discoverCmdOptions) error {
 		return err
 	}
 
+	if subRef.Digest == "" {
+		logger.GetLogger(context.Background(), logOpt).Warn(taggedReferenceWarning)
+	}
+
 	rootImage := treeprint.NewWithRoot(subRef.String())
 
 	stores, err := sf.CreateStoresFromConfig(cf.StoresConfig, config.GetDefaultPluginPath())
diff --git a/cmd/ratify/cmd/referrer.go b/cmd/ratify/cmd/referrer.go
index 67ebe2d..8921c5e 100644
--- a/cmd/ratify/cmd/referrer.go
+++ b/cmd/ratify/cmd/referrer.go
@@ -97,7 +97,7 @@ func NewCmdShowRefManifest(argv ...string) *cobra.Command {
 
 	cmd := &cobra.Command{
 		Use:     "show-manifest [OPTIONS]",
-		Short:   "show rference manifest at a digest",
+		Short:   "show reference manifest at a digest",
 		Example: eg,
 		Args:    cobra.NoArgs,
 		RunE: func(_ *cobra.Command, _ []string) error {
@@ -184,10 +184,6 @@ func showRefManifest(opts referrerCmdOptions) error {
 		return err
 	}
 
-	if subRef.Digest == "" {
-		fmt.Println(taggedReferenceWarning)
-	}
-
 	digest, err := utils.ParseDigest(opts.digest)
 	if err != nil {
 		return err
@@ -198,6 +194,10 @@ func showRefManifest(opts referrerCmdOptions) error {
 		return err
 	}
 
+	if subRef.Digest == "" {
+		logger.GetLogger(context.Background(), logOpt).Warn(taggedReferenceWarning)
+	}
+
 	stores, err := sf.CreateStoresFromConfig(cf.StoresConfig, config.GetDefaultPluginPath())
 
 	if err != nil {
diff --git a/cmd/ratify/cmd/verify.go b/cmd/ratify/cmd/verify.go
index 1e745e9..c189fa6 100644
--- a/cmd/ratify/cmd/verify.go
+++ b/cmd/ratify/cmd/verify.go
@@ -18,7 +18,6 @@ package cmd
 import (
 	"context"
 	"errors"
-	"fmt"
 
 	"github.com/ratify-project/ratify/config"
 	"github.com/ratify-project/ratify/internal/constants"
@@ -36,6 +35,10 @@ const (
 	verifyUse = "verify"
 )
 
+var logOpt = logger.Option{
+	ComponentType: logger.CommandLine,
+}
+
 type verifyCmdOptions struct {
 	configFilePath string
 	subject        string
@@ -65,12 +68,6 @@ func NewCmdVerify(_ ...string) *cobra.Command {
 	return cmd
 }
 
-func TestVerify(subject string) {
-	_ = verify((verifyCmdOptions{
-		subject: subject,
-	}))
-}
-
 func verify(opts verifyCmdOptions) error {
 	if opts.subject == "" {
 		return errors.New("subject parameter is required")
@@ -81,10 +78,6 @@ func verify(opts verifyCmdOptions) error {
 		return err
 	}
 
-	if subRef.Digest == "" {
-		fmt.Println(taggedReferenceWarning)
-	}
-
 	cf, err := config.Load(opts.configFilePath)
 	if err != nil {
 		return err
@@ -94,6 +87,10 @@ func verify(opts verifyCmdOptions) error {
 		return err
 	}
 
+	if subRef.Digest == "" {
+		logger.GetLogger(context.Background(), logOpt).Warn(taggedReferenceWarning)
+	}
+
 	stores, err := sf.CreateStoresFromConfig(cf.StoresConfig, config.GetDefaultPluginPath())
 
 	if err != nil {
diff --git a/internal/logger/logger.go b/internal/logger/logger.go
index 6721cd6..ff050ce 100644
--- a/internal/logger/logger.go
+++ b/internal/logger/logger.go
@@ -61,6 +61,8 @@ const (
 	Executor componentType = "executor"
 	// Server is the component type for the Ratify http server.
 	Server componentType = "server"
+	// CommandLine is the component type for the Ratify command line.
+	CommandLine componentType = "commandLine"
 	// ReferrerStore is the component type for the referrer store.
 	ReferrerStore componentType = "referrerStore"
 	// Cache is the component type for the cache.


Test patch:
diff --git a/cmd/ratify/cmd/cmd_test.go b/cmd/ratify/cmd/cmd_test.go
new file mode 100644
index 0000000..49149dc
--- /dev/null
+++ b/cmd/ratify/cmd/cmd_test.go
@@ -0,0 +1,83 @@
+/*
+Copyright The Ratify 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 cmd
+
+import (
+	"strings"
+	"testing"
+)
+
+const (
+	configFilePath = "../../../config/config.json"
+	subject        = "localhost:5000/net-monitor:v1"
+	storeName      = "oras"
+	digest         = "sha256:17490f904cf278d4314a1ccba407fc8fd00fb45303589b8cc7f5174ac35554f4"
+)
+
+func TestVerify(t *testing.T) {
+	err := verify((verifyCmdOptions{
+		subject:        subject,
+		artifactTypes:  []string{""},
+		configFilePath: configFilePath,
+	}))
+
+	// TODO: make ratify cli more unit testable
+	// unit test should not have dependency for real image
+	if !strings.Contains(err.Error(), "plugin not found") {
+		t.Errorf("error expected")
+	}
+}
+
+func TestDiscover(t *testing.T) {
+	err := discover((discoverCmdOptions{
+		subject:        subject,
+		artifactTypes:  []string{""},
+		configFilePath: configFilePath,
+	}))
+
+	// TODO: make ratify cli more unit testable
+	// unit test should not need to resolve real image
+	if !strings.Contains(err.Error(), "referrer store failure") {
+		t.Errorf("error expected")
+	}
+}
+
+func TestShowRefManifest(t *testing.T) {
+	err := showRefManifest((referrerCmdOptions{
+		subject:        subject,
+		configFilePath: configFilePath,
+		storeName:      storeName,
+		digest:         digest,
+	}))
+
+	// TODO: make ratify cli more unit testable
+	// unit test should not need to resolve real image
+	if !strings.Contains(err.Error(), "failed to resolve subject descriptor") {
+		t.Errorf("error expected")
+	}
+
+	// validate show blob returns error
+	err = showBlob((referrerCmdOptions{
+		subject:        subject,
+		configFilePath: configFilePath,
+		storeName:      storeName,
+		digest:         "invalid-digest",
+	}))
+
+	if !strings.Contains(err.Error(), "the digest of the subject is invalid") {
+		t.Errorf("error expected")
+	}
+}