diff --git a/artifactory/commands/container/containermanagerbase.go b/artifactory/commands/container/containermanagerbase.go index 48f988bfc..c52f97c85 100644 --- a/artifactory/commands/container/containermanagerbase.go +++ b/artifactory/commands/container/containermanagerbase.go @@ -4,7 +4,7 @@ import ( "github.com/jfrog/jfrog-cli-core/v2/artifactory/utils" "github.com/jfrog/jfrog-cli-core/v2/artifactory/utils/container" "github.com/jfrog/jfrog-cli-core/v2/utils/config" - "github.com/jfrog/jfrog-cli-core/v2/utils/coreutils" + clientutils "github.com/jfrog/jfrog-client-go/utils" "github.com/jfrog/jfrog-client-go/utils/errorutils" ) @@ -59,7 +59,7 @@ func (ccb *ContainerCommandBase) IsGetRepoSupported() (bool, error) { if err != nil { return false, err } - err = coreutils.ValidateMinimumVersion(coreutils.Artifactory, currentVersion, MinRtVersionForRepoFetching) + err = clientutils.ValidateMinimumVersion(clientutils.Artifactory, currentVersion, MinRtVersionForRepoFetching) return err == nil, nil } diff --git a/artifactory/commands/golang/go.go b/artifactory/commands/golang/go.go index 3575c6329..22dda146f 100644 --- a/artifactory/commands/golang/go.go +++ b/artifactory/commands/golang/go.go @@ -9,9 +9,9 @@ import ( "github.com/jfrog/jfrog-cli-core/v2/utils/config" "github.com/jfrog/jfrog-cli-core/v2/utils/coreutils" goutils "github.com/jfrog/jfrog-cli-core/v2/utils/golang" - rtutils "github.com/jfrog/jfrog-client-go/artifactory/services/utils" "github.com/jfrog/jfrog-client-go/auth" "github.com/jfrog/jfrog-client-go/http/httpclient" + rtutils "github.com/jfrog/jfrog-client-go/utils" "github.com/jfrog/jfrog-client-go/utils/errorutils" "github.com/jfrog/jfrog-client-go/utils/io/fileutils" "github.com/jfrog/jfrog-client-go/utils/log" @@ -261,7 +261,7 @@ func getPackageFilePathFromArtifactory(packageName, rtTargetRepo string, authArt // PackageName string should be in the following format: /@V/.info OR latest.info // For example the jfrog/jfrog-cli/@v/master.info packageName will return the corresponding canonical version (vX.Y.Z) string for the jfrog-cli master branch. func getPackageVersion(repoName, packageName string, details auth.ServiceDetails) (string, error) { - artifactoryApiUrl, err := rtutils.BuildArtifactoryUrl(details.GetUrl(), "api/go/"+repoName, make(map[string]string)) + artifactoryApiUrl, err := rtutils.BuildUrl(details.GetUrl(), "api/go/"+repoName, make(map[string]string)) if err != nil { return "", err } diff --git a/artifactory/commands/golang/gopublish.go b/artifactory/commands/golang/gopublish.go index a9278097f..3d65525fa 100644 --- a/artifactory/commands/golang/gopublish.go +++ b/artifactory/commands/golang/gopublish.go @@ -1,13 +1,13 @@ package golang import ( - "github.com/jfrog/jfrog-cli-core/v2/utils/coreutils" "os/exec" "github.com/jfrog/build-info-go/build" commandutils "github.com/jfrog/jfrog-cli-core/v2/artifactory/commands/utils" "github.com/jfrog/jfrog-cli-core/v2/artifactory/utils" goutils "github.com/jfrog/jfrog-cli-core/v2/utils/golang" + clientutils "github.com/jfrog/jfrog-client-go/utils" "github.com/jfrog/jfrog-client-go/utils/errorutils" ) @@ -82,7 +82,7 @@ func (gpc *GoPublishCommand) Run() error { if err != nil { return err } - err = coreutils.ValidateMinimumVersion(coreutils.Artifactory, artifactoryVersion, minSupportedArtifactoryVersion) + err = clientutils.ValidateMinimumVersion(clientutils.Artifactory, artifactoryVersion, minSupportedArtifactoryVersion) if err != nil { return err } diff --git a/artifactory/commands/transferconfig/transferconfig.go b/artifactory/commands/transferconfig/transferconfig.go index 1b37ed67c..c0e1da318 100644 --- a/artifactory/commands/transferconfig/transferconfig.go +++ b/artifactory/commands/transferconfig/transferconfig.go @@ -544,7 +544,7 @@ func (tcc *TransferConfigCommand) validateMinVersion() (sourceArtifactoryVersion } // Validate minimal Artifactory version in the source server - err = coreutils.ValidateMinimumVersion(coreutils.Artifactory, sourceArtifactoryVersion, minTransferConfigArtifactoryVersion) + err = clientutils.ValidateMinimumVersion(clientutils.Artifactory, sourceArtifactoryVersion, minTransferConfigArtifactoryVersion) if err != nil { return } @@ -565,7 +565,7 @@ func (tcc *TransferConfigCommand) validateServerPrerequisites() (err error) { } // Check connectivity to JFrog Access if the source Artifactory version is >= 7.0.0 - if versionErr := coreutils.ValidateMinimumVersion(coreutils.Projects, sourceArtifactoryVersion, commandsUtils.MinJFrogProjectsArtifactoryVersion); versionErr == nil { + if versionErr := clientutils.ValidateMinimumVersion(clientutils.Projects, sourceArtifactoryVersion, commandsUtils.MinJFrogProjectsArtifactoryVersion); versionErr == nil { if err = tcc.ValidateAccessServerConnection(tcc.SourceServerDetails, tcc.SourceAccessManager); err != nil { return } diff --git a/artifactory/commands/transferconfigmerge/transferconfigmerge.go b/artifactory/commands/transferconfigmerge/transferconfigmerge.go index 8ef78eb1e..fa3171c8c 100644 --- a/artifactory/commands/transferconfigmerge/transferconfigmerge.go +++ b/artifactory/commands/transferconfigmerge/transferconfigmerge.go @@ -9,9 +9,9 @@ import ( commandsUtils "github.com/jfrog/jfrog-cli-core/v2/artifactory/commands/utils" "github.com/jfrog/jfrog-cli-core/v2/artifactory/utils" "github.com/jfrog/jfrog-cli-core/v2/utils/config" - "github.com/jfrog/jfrog-cli-core/v2/utils/coreutils" accessServices "github.com/jfrog/jfrog-client-go/access/services" "github.com/jfrog/jfrog-client-go/artifactory/services" + clientutils "github.com/jfrog/jfrog-client-go/utils" "github.com/jfrog/jfrog-client-go/utils/log" "golang.org/x/exp/slices" ) @@ -99,7 +99,7 @@ func (tcmc *TransferConfigMergeCommand) initServiceManagersAndValidateServers() return } // Check if JFrog Projects supported by Source Artifactory version - versionErr := coreutils.ValidateMinimumVersion(coreutils.Projects, sourceArtifactoryVersion, commandsUtils.MinJFrogProjectsArtifactoryVersion) + versionErr := clientutils.ValidateMinimumVersion(clientutils.Projects, sourceArtifactoryVersion, commandsUtils.MinJFrogProjectsArtifactoryVersion) if versionErr != nil { // Projects not supported by Source Artifactory version return diff --git a/artifactory/commands/transferfiles/transfer.go b/artifactory/commands/transferfiles/transfer.go index c597a85f4..fb45a01dc 100644 --- a/artifactory/commands/transferfiles/transfer.go +++ b/artifactory/commands/transferfiles/transfer.go @@ -17,8 +17,10 @@ import ( "github.com/jfrog/jfrog-cli-core/v2/artifactory/utils" "github.com/jfrog/jfrog-cli-core/v2/utils/config" "github.com/jfrog/jfrog-cli-core/v2/utils/coreutils" + usageReporter "github.com/jfrog/jfrog-cli-core/v2/utils/usage" serviceUtils "github.com/jfrog/jfrog-client-go/artifactory/services/utils" "github.com/jfrog/jfrog-client-go/artifactory/usage" + clientutils "github.com/jfrog/jfrog-client-go/utils" "github.com/jfrog/jfrog-client-go/utils/errorutils" "github.com/jfrog/jfrog-client-go/utils/io/fileutils" "github.com/jfrog/jfrog-client-go/utils/log" @@ -234,7 +236,7 @@ func (tdc *TransferFilesCommand) initStateManager(allSourceLocalRepos, sourceBui } func (tdc *TransferFilesCommand) reportTransferFilesUsage() { - log.Debug(usage.ReportUsagePrefix + "Sending Transfer Files info...") + log.Debug(usageReporter.ReportUsagePrefix, "Sending Transfer Files info...") sourceStorageInfo, err := tdc.sourceStorageInfoManager.GetStorageInfo() if err != nil { log.Debug(err.Error()) @@ -708,7 +710,7 @@ func validateDataTransferPluginMinimumVersion(currentVersion string) error { if strings.Contains(currentVersion, "SNAPSHOT") { return nil } - return coreutils.ValidateMinimumVersion(coreutils.DataTransfer, currentVersion, dataTransferPluginMinVersion) + return clientutils.ValidateMinimumVersion(clientutils.DataTransfer, currentVersion, dataTransferPluginMinVersion) } // Verify connection to the source Artifactory instance, and that the user plugin is installed, responsive, and stands in the minimal version requirement. diff --git a/artifactory/commands/transferfiles/transfer_test.go b/artifactory/commands/transferfiles/transfer_test.go index e940d19b9..2b745159b 100644 --- a/artifactory/commands/transferfiles/transfer_test.go +++ b/artifactory/commands/transferfiles/transfer_test.go @@ -23,7 +23,8 @@ import ( "github.com/jfrog/jfrog-cli-core/v2/utils/tests" "github.com/jfrog/jfrog-client-go/artifactory" "github.com/jfrog/jfrog-client-go/artifactory/services" - clientUtils "github.com/jfrog/jfrog-client-go/artifactory/services/utils" + artifactoryUtils "github.com/jfrog/jfrog-client-go/artifactory/services/utils" + clientutils "github.com/jfrog/jfrog-client-go/utils" "github.com/stretchr/testify/assert" ) @@ -117,7 +118,7 @@ func testValidateDataTransferPluginMinimumVersion(t *testing.T, curVersion strin pluginVersion = curVersion err := getAndValidateDataTransferPlugin(srcPluginManager) if errorExpected { - assert.EqualError(t, err, coreutils.ValidateMinimumVersion(coreutils.DataTransfer, curVersion, dataTransferPluginMinVersion).Error()) + assert.EqualError(t, err, clientutils.ValidateMinimumVersion(clientutils.DataTransfer, curVersion, dataTransferPluginMinVersion).Error()) return } assert.NoError(t, err) @@ -319,7 +320,7 @@ func TestGetAllLocalRepositories(t *testing.T) { case "/api/storageinfo": // Response for GetStorageInfo w.WriteHeader(http.StatusOK) - response := &clientUtils.StorageInfo{RepositoriesSummaryList: []clientUtils.RepositorySummary{ + response := &artifactoryUtils.StorageInfo{RepositoriesSummaryList: []artifactoryUtils.RepositorySummary{ {RepoKey: "repo-1"}, {RepoKey: "repo-2"}, {RepoKey: "federated-repo-1"}, {RepoKey: "federated-repo-2"}, {RepoKey: "artifactory-build-info", PackageType: "BuildInfo"}, {RepoKey: "proj-build-info", PackageType: "BuildInfo"}}, diff --git a/artifactory/commands/utils/npmcmdutils.go b/artifactory/commands/utils/npmcmdutils.go index 3b9f94db9..a9c3dcc7f 100644 --- a/artifactory/commands/utils/npmcmdutils.go +++ b/artifactory/commands/utils/npmcmdutils.go @@ -15,6 +15,7 @@ import ( "github.com/jfrog/jfrog-cli-core/v2/utils/coreutils" "github.com/jfrog/jfrog-client-go/auth" "github.com/jfrog/jfrog-client-go/http/httpclient" + clientutils "github.com/jfrog/jfrog-client-go/utils" "github.com/jfrog/jfrog-client-go/utils/errorutils" "github.com/jfrog/jfrog-client-go/utils/log" ) @@ -54,7 +55,7 @@ func validateArtifactoryVersionForNpmCmds(artDetails *auth.ServiceDetails) error } // Validate version. - return coreutils.ValidateMinimumVersion(coreutils.Artifactory, versionStr, minSupportedArtifactoryVersionForNpmCmds) + return clientutils.ValidateMinimumVersion(clientutils.Artifactory, versionStr, minSupportedArtifactoryVersionForNpmCmds) } func getNpmAuthFromArtifactory(artDetails *auth.ServiceDetails) (npmAuth string, err error) { diff --git a/artifactory/utils/container/containermanager.go b/artifactory/utils/container/containermanager.go index 3ea833d48..e5139b5d5 100644 --- a/artifactory/utils/container/containermanager.go +++ b/artifactory/utils/container/containermanager.go @@ -11,6 +11,7 @@ import ( "github.com/jfrog/jfrog-cli-core/v2/utils/config" "github.com/jfrog/jfrog-cli-core/v2/utils/coreutils" "github.com/jfrog/jfrog-client-go/auth" + "github.com/jfrog/jfrog-client-go/utils" "github.com/jfrog/jfrog-client-go/utils/errorutils" "github.com/jfrog/jfrog-client-go/utils/log" ) @@ -257,5 +258,5 @@ func ValidateClientApiVersion() error { log.Error("The Docker client Api version is expected to be 'major.minor'. The actual output is:", content) return errorutils.CheckError(err) } - return coreutils.ValidateMinimumVersion(coreutils.DockerApi, content, MinSupportedApiVersion) + return utils.ValidateMinimumVersion(utils.DockerApi, content, MinSupportedApiVersion) } diff --git a/common/commands/command.go b/common/commands/command.go index bac9435a1..074876d42 100644 --- a/common/commands/command.go +++ b/common/commands/command.go @@ -4,8 +4,8 @@ import ( "github.com/jfrog/jfrog-cli-core/v2/artifactory/utils" "github.com/jfrog/jfrog-cli-core/v2/utils/config" "github.com/jfrog/jfrog-cli-core/v2/utils/coreutils" + usageReporter "github.com/jfrog/jfrog-cli-core/v2/utils/usage" "github.com/jfrog/jfrog-client-go/artifactory/usage" - clientutils "github.com/jfrog/jfrog-client-go/utils" "github.com/jfrog/jfrog-client-go/utils/log" ) @@ -31,22 +31,18 @@ func Exec(command Command) error { func reportUsage(command Command, channel chan<- bool) { defer signalReportUsageFinished(channel) - reportUsage, err := clientutils.GetBoolEnvValue(coreutils.ReportUsage, true) - if err != nil { - log.Debug(usage.ReportUsagePrefix + err.Error()) - return - } + reportUsage := usageReporter.ShouldReportUsage() if reportUsage { serverDetails, err := command.ServerDetails() if err != nil { - log.Debug(usage.ReportUsagePrefix + err.Error()) + log.Debug(usageReporter.ReportUsagePrefix, err.Error()) return } if serverDetails != nil && serverDetails.ArtifactoryUrl != "" { - log.Debug(usage.ReportUsagePrefix + "Sending info...") + log.Debug(usageReporter.ReportUsagePrefix, "Sending info...") serviceManager, err := utils.CreateServiceManager(serverDetails, -1, 0, false) if err != nil { - log.Debug(usage.ReportUsagePrefix + err.Error()) + log.Debug(usageReporter.ReportUsagePrefix, err.Error()) return } err = usage.SendReportUsage(coreutils.GetCliUserAgent(), command.CommandName(), serviceManager) diff --git a/go.mod b/go.mod index d8791b5f8..dc575a9ae 100644 --- a/go.mod +++ b/go.mod @@ -12,7 +12,7 @@ require ( github.com/jedib0t/go-pretty/v6 v6.4.6 github.com/jfrog/build-info-go v1.9.8 github.com/jfrog/gofrog v1.3.0 - github.com/jfrog/jfrog-client-go v1.31.5 + github.com/jfrog/jfrog-client-go v1.31.6 github.com/magiconair/properties v1.8.7 github.com/manifoldco/promptui v0.9.0 github.com/owenrumney/go-sarif/v2 v2.2.0 diff --git a/go.sum b/go.sum index 63a72106b..937fd2ed8 100644 --- a/go.sum +++ b/go.sum @@ -200,8 +200,8 @@ github.com/jfrog/build-info-go v1.9.8 h1:D8/ga+YgQpqp/CJj2zteS4/twmSy8zvm1v9lCd2 github.com/jfrog/build-info-go v1.9.8/go.mod h1:t31QRpH5xUJKw8XkQlAA+Aq7aanyS1rrzpcK8xSNVts= github.com/jfrog/gofrog v1.3.0 h1:o4zgsBZE4QyDbz2M7D4K6fXPTBJht+8lE87mS9bw7Gk= github.com/jfrog/gofrog v1.3.0/go.mod h1:IFMc+V/yf7rA5WZ74CSbXe+Lgf0iApEQLxRZVzKRUR0= -github.com/jfrog/jfrog-client-go v1.31.5 h1:dYVgIJzMwX+EU9GEELKPSHFLyfW6UrrjZWMEZtAyx6A= -github.com/jfrog/jfrog-client-go v1.31.5/go.mod h1:icb00ZJN/mMMNkQduHDkzpqsXH9Flwi3f3COYexq3Nc= +github.com/jfrog/jfrog-client-go v1.31.6 h1:uWuyT4BDm9s5ES6oDTBny9Gl6yf8iKFjcbmHSHQZrDc= +github.com/jfrog/jfrog-client-go v1.31.6/go.mod h1:icb00ZJN/mMMNkQduHDkzpqsXH9Flwi3f3COYexq3Nc= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4= diff --git a/utils/coreutils/utils.go b/utils/coreutils/utils.go index a7bae4e0f..f1c2b7fb1 100644 --- a/utils/coreutils/utils.go +++ b/utils/coreutils/utils.go @@ -11,7 +11,6 @@ import ( "runtime" "strings" - "github.com/jfrog/gofrog/version" "github.com/jfrog/jfrog-client-go/utils" "github.com/jfrog/jfrog-client-go/utils/errorutils" "github.com/jfrog/jfrog-client-go/utils/io/fileutils" @@ -24,16 +23,6 @@ const ( JFrogHelpUrl = JFrogComUrl + "help/r/" ) -type MinVersionProduct string - -const ( - Artifactory MinVersionProduct = "JFrog Artifactory" - Xray MinVersionProduct = "JFrog Xray" - DataTransfer MinVersionProduct = "Data Transfer" - DockerApi MinVersionProduct = "Docker API" - Projects MinVersionProduct = "JFrog Projects" -) - const ( // ReleasesRemoteEnv should be used for downloading the CLI dependencies (extractor jars, analyzerManager etc.) through an Artifactory remote // repository, instead of downloading directly from releases.jfrog.io. The remote repository should be @@ -44,8 +33,7 @@ const ( // Its functionality was similar to ReleasesRemoteEnv, but it proxies releases.jfrog.io/artifactory/oss-release-local instead. DeprecatedExtractorsRemoteEnv = "JFROG_CLI_EXTRACTORS_REMOTE" // JFrog releases URL - JfrogReleasesUrl = "https://releases.jfrog.io/artifactory/" - MinimumVersionMsg = "You are using %s version %s, while this operation requires version %s or higher." + JfrogReleasesUrl = "https://releases.jfrog.io/artifactory/" ) // Error modes (how should the application behave when the CheckError function is invoked): @@ -561,13 +549,6 @@ func GetJfrogTransferDir() (string, error) { return filepath.Join(homeDir, JfrogTransferDirName), nil } -func ValidateMinimumVersion(product MinVersionProduct, currentVersion, minimumVersion string) error { - if !version.NewVersion(currentVersion).AtLeast(minimumVersion) { - return errorutils.CheckErrorf(MinimumVersionMsg, product, currentVersion, minimumVersion) - } - return nil -} - func GetServerIdAndRepo(remoteEnv string) (serverID string, repoName string, err error) { serverAndRepo := os.Getenv(remoteEnv) if serverAndRepo == "" { diff --git a/utils/usage/usage.go b/utils/usage/usage.go new file mode 100644 index 000000000..df1a20292 --- /dev/null +++ b/utils/usage/usage.go @@ -0,0 +1,230 @@ +package usage + +import ( + "fmt" + + "golang.org/x/sync/errgroup" + + "github.com/jfrog/jfrog-cli-core/v2/artifactory/utils" + "github.com/jfrog/jfrog-cli-core/v2/utils/config" + "github.com/jfrog/jfrog-cli-core/v2/utils/coreutils" + xray "github.com/jfrog/jfrog-cli-core/v2/xray/commands/utils" + "github.com/jfrog/jfrog-client-go/artifactory/usage" + clientutils "github.com/jfrog/jfrog-client-go/utils" + "github.com/jfrog/jfrog-client-go/utils/errorutils" + "github.com/jfrog/jfrog-client-go/utils/log" + ecosysusage "github.com/jfrog/jfrog-client-go/utils/usage" + xrayusage "github.com/jfrog/jfrog-client-go/xray/usage" +) + +const ( + ReportUsagePrefix = "Usage Report:" + clientIdAttributeName = "clientId" +) + +type UsageReporter struct { + ProductId string + serverDetails *config.ServerDetails + reportWaitGroup *errgroup.Group + + sendToEcosystem bool + sendToXray bool + sendToArtifactory bool +} + +type ReportFeature struct { + FeatureId string + ClientId string + Attributes []ReportUsageAttribute +} + +type ReportUsageAttribute struct { + AttributeName string + AttributeValue string +} + +func NewUsageReporter(productId string, serverDetails *config.ServerDetails) *UsageReporter { + return &UsageReporter{ + ProductId: productId, + serverDetails: serverDetails, + reportWaitGroup: new(errgroup.Group), + sendToEcosystem: true, + sendToXray: true, + sendToArtifactory: true, + } +} + +func ShouldReportUsage() (reportUsage bool) { + reportUsage, err := clientutils.GetBoolEnvValue(coreutils.ReportUsage, true) + if err != nil { + log.Debug(ReportUsagePrefix + err.Error()) + return false + } + return reportUsage +} + +func (ur *UsageReporter) SetSendToEcosystem(send bool) *UsageReporter { + ur.sendToEcosystem = send + return ur +} + +func (ur *UsageReporter) SetSendToXray(send bool) *UsageReporter { + ur.sendToXray = send + return ur +} + +func (ur *UsageReporter) SetSendToArtifactory(send bool) *UsageReporter { + ur.sendToArtifactory = send + return ur +} + +// Report usage to Artifactory, Xray and Ecosystem +func (ur *UsageReporter) Report(features ...ReportFeature) { + if !ShouldReportUsage() { + log.Debug("Usage info is disabled.") + return + } + log.Debug(ReportUsagePrefix, "Sending info...") + if ur.sendToEcosystem { + ur.reportWaitGroup.Go(func() error { + return ur.reportToEcosystem(features...) + }) + } + if ur.sendToXray { + ur.reportWaitGroup.Go(func() error { + return ur.reportToXray(features...) + }) + } + if ur.sendToArtifactory { + ur.reportWaitGroup.Go(func() error { + return ur.reportToArtifactory(features...) + }) + } +} + +func (ur *UsageReporter) WaitForResponses() (err error) { + if err = ur.reportWaitGroup.Wait(); err != nil { + err = fmt.Errorf("%s %s", ReportUsagePrefix, err.Error()) + } + return +} + +func (ur *UsageReporter) reportToEcosystem(features ...ReportFeature) (err error) { + if ur.serverDetails.Url == "" { + err = errorutils.CheckErrorf("platform Url is not set") + return + } + reports, err := ur.convertAttributesToEcosystemReports(features...) + if len(reports) == 0 || err != nil { + err = errorutils.CheckErrorf("Nothing to send.") + return + } + return ecosysusage.SendEcosystemUsageReports(reports...) +} + +func (ur *UsageReporter) reportToXray(features ...ReportFeature) (err error) { + if ur.serverDetails.XrayUrl == "" { + err = errorutils.CheckErrorf("Xray Url is not set.") + return + } + serviceManager, err := xray.CreateXrayServiceManager(ur.serverDetails) + if err != nil { + return + } + events := ur.convertAttributesToXrayEvents(features...) + if len(events) == 0 { + err = errorutils.CheckErrorf("Nothing to send.") + return + } + return xrayusage.SendXrayUsageEvents(*serviceManager, events...) +} + +func (ur *UsageReporter) reportToArtifactory(features ...ReportFeature) (err error) { + if ur.serverDetails.ArtifactoryUrl == "" { + err = errorutils.CheckErrorf("Artifactory Url is not set..") + return + } + serviceManager, err := utils.CreateServiceManager(ur.serverDetails, -1, 0, false) + if err != nil { + return + } + converted := ur.convertAttributesToArtifactoryFeatures(features...) + if len(converted) == 0 { + err = errorutils.CheckErrorf("Nothing to send.") + return + } + return usage.ReportUsageToArtifactory(ur.ProductId, serviceManager, converted...) +} + +func convertAttributesToMap(reportFeature ReportFeature) (converted map[string]string) { + if len(reportFeature.Attributes) == 0 { + return + } + converted = make(map[string]string, len(reportFeature.Attributes)) + for _, attribute := range reportFeature.Attributes { + if attribute.AttributeName != "" { + converted[attribute.AttributeName] = attribute.AttributeValue + } + } + return +} + +func (ur *UsageReporter) convertAttributesToArtifactoryFeatures(reportFeatures ...ReportFeature) (features []usage.Feature) { + for _, feature := range reportFeatures { + featureInfo := usage.Feature{ + FeatureId: feature.FeatureId, + ClientId: feature.ClientId, + Attributes: convertAttributesToMap(feature), + } + features = append(features, featureInfo) + } + return +} + +func (ur *UsageReporter) convertAttributesToXrayEvents(reportFeatures ...ReportFeature) (events []xrayusage.ReportXrayEventData) { + for _, feature := range reportFeatures { + convertedAttributes := []xrayusage.ReportUsageAttribute{} + for _, attribute := range feature.Attributes { + convertedAttributes = append(convertedAttributes, xrayusage.ReportUsageAttribute{ + AttributeName: attribute.AttributeName, + AttributeValue: attribute.AttributeValue, + }) + } + if feature.ClientId != "" { + // Add clientId as attribute + convertedAttributes = append(convertedAttributes, xrayusage.ReportUsageAttribute{ + AttributeName: clientIdAttributeName, + AttributeValue: feature.ClientId, + }) + } + events = append(events, xrayusage.CreateUsageEvent( + ur.ProductId, feature.FeatureId, convertedAttributes..., + )) + } + return +} + +func (ur *UsageReporter) convertAttributesToEcosystemReports(reportFeatures ...ReportFeature) (reports []ecosysusage.ReportEcosystemUsageData, err error) { + accountId := ur.serverDetails.Url + clientToFeaturesMap := map[string][]string{} + // Combine + for _, feature := range reportFeatures { + if feature.FeatureId == "" { + continue + } + if features, contains := clientToFeaturesMap[feature.ClientId]; contains { + clientToFeaturesMap[feature.ClientId] = append(features, feature.FeatureId) + } else { + clientToFeaturesMap[feature.ClientId] = []string{feature.FeatureId} + } + } + // Create data + for clientId, features := range clientToFeaturesMap { + var report ecosysusage.ReportEcosystemUsageData + if report, err = ecosysusage.CreateUsageData(ur.ProductId, accountId, clientId, features...); err != nil { + return + } + reports = append(reports, report) + } + return +} diff --git a/utils/usage/usage_test.go b/utils/usage/usage_test.go new file mode 100644 index 000000000..d67ceef25 --- /dev/null +++ b/utils/usage/usage_test.go @@ -0,0 +1,237 @@ +package usage + +import ( + "bytes" + "fmt" + "net/http" + "net/http/httptest" + "testing" + + "github.com/jfrog/jfrog-cli-core/v2/utils/config" + "github.com/jfrog/jfrog-client-go/artifactory/usage" + ecosysusage "github.com/jfrog/jfrog-client-go/utils/usage" + xrayusage "github.com/jfrog/jfrog-client-go/xray/usage" + "github.com/stretchr/testify/assert" +) + +const ( + productName = "test-product" + serverUrl = "server-url" +) + +var ( + features = []ReportFeature{ + { + FeatureId: "featureId2", + ClientId: "clientId", + Attributes: []ReportUsageAttribute{ + {AttributeName: "attribute", AttributeValue: "value"}, + }, + }, + {FeatureId: "featureId", ClientId: "clientId2"}, + {FeatureId: "featureId"}, + } + artifactoryFeatures = []usage.Feature{ + { + FeatureId: "featureId2", + ClientId: "clientId", + Attributes: map[string]string{"attribute": "value"}, + }, + {FeatureId: "featureId", ClientId: "clientId2"}, + {FeatureId: "featureId"}, + } + xrayEvents = []xrayusage.ReportXrayEventData{ + { + ProductId: productName, + EventId: "server_" + productName + "_featureId2", + Attributes: map[string]string{ + "clientId": "clientId", + "attribute": "value", + }, + Origin: "API_CLI", + }, + { + ProductId: productName, + EventId: "server_" + productName + "_featureId", + Attributes: map[string]string{"clientId": "clientId2"}, + Origin: "API_CLI", + }, + { + ProductId: productName, + EventId: "server_" + productName + "_featureId", + Origin: "API_CLI", + }, + } + ecosystemData = []ecosysusage.ReportEcosystemUsageData{ + { + ProductId: productName, + AccountId: serverUrl, + ClientId: "clientId", + Features: []string{"featureId2"}, + }, + { + ProductId: productName, + AccountId: serverUrl, + ClientId: "clientId2", + Features: []string{"featureId"}, + }, + { + ProductId: productName, + AccountId: serverUrl, + Features: []string{"featureId"}, + }, + } +) + +func TestConvertToArtifactoryUsage(t *testing.T) { + reporter := NewUsageReporter(productName, &config.ServerDetails{ArtifactoryUrl: serverUrl + "/"}) + for i := 0; i < len(features); i++ { + assert.Equal(t, artifactoryFeatures[i], reporter.convertAttributesToArtifactoryFeatures(features[i])[0]) + } +} + +func TestConvertToXrayUsage(t *testing.T) { + reporter := NewUsageReporter(productName, &config.ServerDetails{XrayUrl: serverUrl + "/"}) + for i := 0; i < len(features); i++ { + assert.Equal(t, xrayEvents[i], reporter.convertAttributesToXrayEvents(features[i])[0]) + } +} + +func TestConvertToEcosystemUsage(t *testing.T) { + reporter := NewUsageReporter(productName, &config.ServerDetails{Url: serverUrl}) + for i := 0; i < len(features); i++ { + report, err := reporter.convertAttributesToEcosystemReports(features[i]) + assert.NoError(t, err) + assert.Equal(t, ecosystemData[i], report[0]) + } +} + +func TestReportArtifactoryUsage(t *testing.T) { + const commandName = "test-command" + server := httptest.NewServer(createArtifactoryUsageHandler(t, productName, commandName)) + defer server.Close() + serverDetails := &config.ServerDetails{ArtifactoryUrl: server.URL + "/"} + + reporter := NewUsageReporter(productName, serverDetails).SetSendToEcosystem(false).SetSendToXray(false) + + reporter.Report(ReportFeature{ + FeatureId: commandName, + }) + assert.NoError(t, reporter.WaitForResponses()) +} + +func TestReportArtifactoryUsageError(t *testing.T) { + reporter := NewUsageReporter("", &config.ServerDetails{}).SetSendToEcosystem(false).SetSendToXray(false) + reporter.Report(ReportFeature{ + FeatureId: "", + }) + assert.Error(t, reporter.WaitForResponses()) + + server := httptest.NewServer(create404UsageHandler(t)) + defer server.Close() + reporter = NewUsageReporter("", &config.ServerDetails{ArtifactoryUrl: server.URL + "/"}).SetSendToEcosystem(false).SetSendToXray(false) + reporter.Report(ReportFeature{ + FeatureId: "", + }) + assert.Error(t, reporter.WaitForResponses()) +} + +func createArtifactoryUsageHandler(t *testing.T, productName, commandName string) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + if r.RequestURI == "/api/system/version" { + w.WriteHeader(http.StatusOK) + _, err := w.Write([]byte(`{"version":"6.9.0"}`)) + assert.NoError(t, err) + return + } + if r.RequestURI == "/api/system/usage" { + // Check request + buf := new(bytes.Buffer) + _, err := buf.ReadFrom(r.Body) + assert.NoError(t, err) + assert.Equal(t, fmt.Sprintf(`{"productId":"%s","features":[{"featureId":"%s"}]}`, productName, commandName), buf.String()) + + // Send response OK + w.WriteHeader(http.StatusOK) + _, err = w.Write([]byte("{}")) + assert.NoError(t, err) + } + } +} + +func TestReportXrayUsage(t *testing.T) { + const productName = "test-product" + const commandName = "test-command" + const clientName = "test-client" + + server := httptest.NewServer(createXrayUsageHandler(t, productName, commandName, clientName)) + defer server.Close() + serverDetails := &config.ServerDetails{XrayUrl: server.URL + "/"} + + reporter := NewUsageReporter(productName, serverDetails).SetSendToEcosystem(false).SetSendToArtifactory(false) + + reporter.Report(ReportFeature{ + FeatureId: commandName, + ClientId: clientName, + }) + assert.NoError(t, reporter.WaitForResponses()) +} + +func TestReportXrayError(t *testing.T) { + reporter := NewUsageReporter("", &config.ServerDetails{}).SetSendToEcosystem(false).SetSendToArtifactory(false) + reporter.Report(ReportFeature{ + FeatureId: "", + }) + assert.Error(t, reporter.WaitForResponses()) + + server := httptest.NewServer(create404UsageHandler(t)) + defer server.Close() + reporter = NewUsageReporter("", &config.ServerDetails{ArtifactoryUrl: server.URL + "/"}).SetSendToEcosystem(false).SetSendToArtifactory(false) + reporter.Report(ReportFeature{ + FeatureId: "", + }) + assert.Error(t, reporter.WaitForResponses()) +} + +func createXrayUsageHandler(t *testing.T, productId, commandName, clientId string) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + if r.RequestURI == "/api/v1/system/version" { + w.WriteHeader(http.StatusOK) + _, err := w.Write([]byte(`{"xray_version":"6.9.0"}`)) + assert.NoError(t, err) + return + } + if r.RequestURI == "/api/v1/usage/events/send" { + // Check request + buf := new(bytes.Buffer) + _, err := buf.ReadFrom(r.Body) + assert.NoError(t, err) + featureId := xrayusage.GetExpectedXrayEventName(productId, commandName) + assert.Equal(t, fmt.Sprintf(`[{"data":{"clientId":"%s"},"product_name":"%s","event_name":"%s","origin":"API_CLI"}]`, clientId, productId, featureId), buf.String()) + + // Send response OK + w.WriteHeader(http.StatusOK) + _, err = w.Write([]byte("{}")) + assert.NoError(t, err) + } + } +} + +func TestReportEcosystemUsageError(t *testing.T) { + // No features + reporter := NewUsageReporter("", &config.ServerDetails{}).SetSendToArtifactory(false).SetSendToXray(false) + reporter.Report() + assert.Error(t, reporter.WaitForResponses()) + // Empty features + reporter.Report(ReportFeature{ + FeatureId: "", + ClientId: "client", + }) + assert.Error(t, reporter.WaitForResponses()) +} + +func create404UsageHandler(t *testing.T) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusNotFound) + } +} diff --git a/xray/commands/audit/generic/auditmanager.go b/xray/commands/audit/generic/auditmanager.go index 55015a9ce..8b535f7a3 100644 --- a/xray/commands/audit/generic/auditmanager.go +++ b/xray/commands/audit/generic/auditmanager.go @@ -17,6 +17,7 @@ import ( "github.com/jfrog/jfrog-cli-core/v2/xray/audit/yarn" commandsutils "github.com/jfrog/jfrog-cli-core/v2/xray/commands/utils" xrayutils "github.com/jfrog/jfrog-cli-core/v2/xray/utils" + clientutils "github.com/jfrog/jfrog-client-go/utils" "github.com/jfrog/jfrog-client-go/utils/errorutils" "github.com/jfrog/jfrog-client-go/utils/log" "github.com/jfrog/jfrog-client-go/xray" @@ -124,7 +125,7 @@ func RunAudit(auditParams *Params) (results *Results, err error) { if err != nil { return } - if err = coreutils.ValidateMinimumVersion(coreutils.Xray, auditParams.xrayVersion, commandsutils.GraphScanMinXrayVersion); err != nil { + if err = clientutils.ValidateMinimumVersion(clientutils.Xray, auditParams.xrayVersion, commandsutils.GraphScanMinXrayVersion); err != nil { return } results.ExtendedScanResults.EntitledForJas, err = isEntitledForJas(xrayManager, auditParams.xrayVersion) @@ -154,7 +155,7 @@ func RunAudit(auditParams *Params) (results *Results, err error) { } func isEntitledForJas(xrayManager *xray.XrayServicesManager, xrayVersion string) (entitled bool, err error) { - if e := coreutils.ValidateMinimumVersion(coreutils.Xray, xrayVersion, xrayutils.EntitlementsMinVersion); e != nil { + if e := clientutils.ValidateMinimumVersion(clientutils.Xray, xrayVersion, xrayutils.EntitlementsMinVersion); e != nil { log.Debug(e) return } diff --git a/xray/commands/scan/buildscan.go b/xray/commands/scan/buildscan.go index b336f8717..11aa3617f 100644 --- a/xray/commands/scan/buildscan.go +++ b/xray/commands/scan/buildscan.go @@ -4,9 +4,9 @@ import ( "errors" rtutils "github.com/jfrog/jfrog-cli-core/v2/artifactory/utils" "github.com/jfrog/jfrog-cli-core/v2/utils/config" - "github.com/jfrog/jfrog-cli-core/v2/utils/coreutils" "github.com/jfrog/jfrog-cli-core/v2/xray/commands/utils" xrutils "github.com/jfrog/jfrog-cli-core/v2/xray/utils" + clientutils "github.com/jfrog/jfrog-client-go/utils" "github.com/jfrog/jfrog-client-go/utils/log" "github.com/jfrog/jfrog-client-go/xray" "github.com/jfrog/jfrog-client-go/xray/services" @@ -76,12 +76,12 @@ func (bsc *BuildScanCommand) Run() (err error) { if err != nil { return err } - err = coreutils.ValidateMinimumVersion(coreutils.Xray, xrayVersion, BuildScanMinVersion) + err = clientutils.ValidateMinimumVersion(clientutils.Xray, xrayVersion, BuildScanMinVersion) if err != nil { return err } if bsc.includeVulnerabilities { - err = coreutils.ValidateMinimumVersion(coreutils.Xray, xrayVersion, BuildScanIncludeVulnerabilitiesMinVersion) + err = clientutils.ValidateMinimumVersion(clientutils.Xray, xrayVersion, BuildScanIncludeVulnerabilitiesMinVersion) if err != nil { return errors.New("build-scan command with '--vuln' flag is not supported on your current Xray version. " + err.Error()) } diff --git a/xray/commands/scan/dockerscan.go b/xray/commands/scan/dockerscan.go index 98b4de13b..cf840a4d7 100644 --- a/xray/commands/scan/dockerscan.go +++ b/xray/commands/scan/dockerscan.go @@ -4,8 +4,8 @@ import ( "bytes" "fmt" "github.com/jfrog/jfrog-cli-core/v2/common/spec" - "github.com/jfrog/jfrog-cli-core/v2/utils/coreutils" "github.com/jfrog/jfrog-cli-core/v2/xray/commands/utils" + clientutils "github.com/jfrog/jfrog-client-go/utils" "github.com/jfrog/jfrog-client-go/utils/errorutils" "github.com/jfrog/jfrog-client-go/utils/io/fileutils" "github.com/jfrog/jfrog-client-go/utils/log" @@ -46,7 +46,7 @@ func (dsc *DockerScanCommand) Run() (err error) { if err != nil { return err } - err = coreutils.ValidateMinimumVersion(coreutils.Xray, xrayVersion, DockerScanMinXrayVersion) + err = clientutils.ValidateMinimumVersion(clientutils.Xray, xrayVersion, DockerScanMinXrayVersion) if err != nil { return err } diff --git a/xray/commands/scan/scan.go b/xray/commands/scan/scan.go index 0bd67b67f..e918da3be 100644 --- a/xray/commands/scan/scan.go +++ b/xray/commands/scan/scan.go @@ -171,14 +171,14 @@ func (scanCmd *ScanCommand) Run() (err error) { } // Validate Xray minimum version for graph scan command - err = coreutils.ValidateMinimumVersion(coreutils.Xray, xrayVersion, utils.GraphScanMinXrayVersion) + err = clientutils.ValidateMinimumVersion(clientutils.Xray, xrayVersion, utils.GraphScanMinXrayVersion) if err != nil { return err } if scanCmd.bypassArchiveLimits { // Validate Xray minimum version for BypassArchiveLimits flag for indexer - err = coreutils.ValidateMinimumVersion(coreutils.Xray, xrayVersion, utils.BypassArchiveLimitsMinXrayVersion) + err = clientutils.ValidateMinimumVersion(clientutils.Xray, xrayVersion, utils.BypassArchiveLimitsMinXrayVersion) if err != nil { return err } diff --git a/xray/commands/utils/utils.go b/xray/commands/utils/utils.go index 2d5f5e92d..b3a4ccc2d 100644 --- a/xray/commands/utils/utils.go +++ b/xray/commands/utils/utils.go @@ -6,6 +6,7 @@ import ( "github.com/jfrog/jfrog-cli-core/v2/utils/coreutils" "github.com/jfrog/jfrog-cli-core/v2/xray/utils" clientconfig "github.com/jfrog/jfrog-client-go/config" + clientutils "github.com/jfrog/jfrog-client-go/utils" "github.com/jfrog/jfrog-client-go/utils/errorutils" "github.com/jfrog/jfrog-client-go/utils/log" "github.com/jfrog/jfrog-client-go/xray" @@ -101,7 +102,7 @@ func RunScanGraphAndGetResults(params *ScanGraphParams) (*services.ScanResponse, return nil, err } - err = coreutils.ValidateMinimumVersion(coreutils.Xray, params.xrayVersion, ScanTypeMinXrayVersion) + err = clientutils.ValidateMinimumVersion(clientutils.Xray, params.xrayVersion, ScanTypeMinXrayVersion) if err != nil { // Remove scan type param if Xray version is under the minimum supported version params.xrayGraphScanParams.ScanType = ""