diff --git a/README.md b/README.md index 6f1610056..19c08db7b 100644 --- a/README.md +++ b/README.md @@ -225,6 +225,20 @@ - [Export Release Bundle](#export-release-bundle) - [Import Release Bundle](#import-release-bundle) - [Remote Delete Release Bundle](#remote-delete-release-bundle) + - [Lifecycle APIs](#lifecycle-apis) + - [Creating Lifecycle Service Manager](#creating-lifeCycle-service-manager) + - [Creating Lifecycle Details](#creating-lifeCycle-details) + - [Creating Lifecycle Service Config](#creating-lifeCycle-service-config) + - [Creating New Lifecycle Service Manager](#creating-new-lifeCycle-service-manager) + - [Using Lifecycle Services](#using-lifeCycle-services) + - [Creating a Release Bundle From AQL](#creating-a-release-bundle-from-aql) + - [Evidence APIs](#evidence-apis) + - [Creating Evidence Service Manager](#creating-evidence-service-manager) + - [Creating Evidence Details](#creating-evidence-details) + - [Creating Evidence Service Config](#creating-evidence-service-config) + - [Creating New Evidence Service Manager](#creating-new-evidence-service-manager) + - [Using Evidence Services](#using-evidence-services) + - [Upload Evidence](#upload-evidence) ## General @@ -2808,3 +2822,48 @@ dryRun := true resp, err := serviceManager.RemoteDeleteReleaseBundle(params, dryRun) ``` +## Evidence APIs + +### Creating Evidence Service Manager + +#### Creating Evidence Details + +```go +evdDetails := auth.NewEvidenceDetails() +evdDetails.SetUrl("http://localhost:8081/evidence") +evdDetails.SetAccessToken("access-token") +// if client certificates are required +evdDetails.SetClientCertPath("path/to/.cer") +evdDetails.SetClientCertKeyPath("path/to/.key") +``` + +#### Creating Evidence Service Config + +```go +serviceConfig, err := config.NewConfigBuilder(). + SetServiceDetails(evdDetails). + SetCertificatesPath(evdDetails.GetClientCertPath()). + // Optionally overwrite the default HTTP retries, which is set to 3. + SetHttpRetries(3). + Build() +``` + +#### Creating New Evidence Service Manager + +```go +evidenceManager, err := evidence.New(serviceConfig) +``` + +### Using Evidence Services + +#### Upload Evidence + +```go +envelopeBytes := []byte("envelope") + +evidenceDetails := evidenceService.EvidenceDetails{ + SubjectUri: "subjectUri", + DSSEFileRaw: &envelopeBytes, +} +body, err = evideceManager.UploadEvidence(evidenceDetails) +``` diff --git a/evidence/auth/evidencedetails.go b/evidence/auth/evidencedetails.go new file mode 100644 index 000000000..b7c46992e --- /dev/null +++ b/evidence/auth/evidencedetails.go @@ -0,0 +1,17 @@ +package auth + +import ( + "github.com/jfrog/jfrog-client-go/auth" +) + +func NewEvidenceDetails() auth.ServiceDetails { + return &evidenceDetails{} +} + +type evidenceDetails struct { + auth.CommonConfigFields +} + +func (rt *evidenceDetails) GetVersion() (string, error) { + panic("Failed: Method is not implemented") +} diff --git a/evidence/evidence_test.go b/evidence/evidence_test.go new file mode 100644 index 000000000..5fa3dc34a --- /dev/null +++ b/evidence/evidence_test.go @@ -0,0 +1,59 @@ +package evidence + +import ( + "encoding/json" + artifactoryAuth "github.com/jfrog/jfrog-client-go/artifactory/auth" + evidence "github.com/jfrog/jfrog-client-go/evidence/services" + "github.com/jfrog/jfrog-client-go/http/jfroghttpclient" + "github.com/stretchr/testify/assert" + "net/http" + "net/http/httptest" + "testing" +) + +var dsseRaw = []byte("someData") + +var evidenceData = evidence.EvidenceDetails{ + SubjectUri: "someUri", + DSSEFileRaw: dsseRaw, +} + +func TestEvidenceServicesManager_UploadEvidence(t *testing.T) { + handlerFunc, requestNum := createDefaultHandlerFunc(t) + + mockServer, evdService := createMockServer(t, handlerFunc) + defer mockServer.Close() + + _, err := evdService.UploadEvidence(evidenceData) + assert.NoError(t, err) + assert.Equal(t, 0, *requestNum) +} + +func createMockServer(t *testing.T, testHandler http.HandlerFunc) (*httptest.Server, *evidence.EvidenceService) { + testServer := httptest.NewServer(testHandler) + + rtDetails := artifactoryAuth.NewArtifactoryDetails() + rtDetails.SetUrl(testServer.URL + "/") + + client, err := jfroghttpclient.JfrogClientBuilder().Build() + assert.NoError(t, err) + return testServer, evidence.NewEvidenceService(rtDetails, client) +} + +func createDefaultHandlerFunc(t *testing.T) (http.HandlerFunc, *int) { + requestNum := 0 + return func(w http.ResponseWriter, r *http.Request) { + if r.RequestURI == "/api/v1/subject" { + w.WriteHeader(http.StatusOK) + requestNum++ + writeMockStatusResponse(t, w, dsseRaw) + } + }, &requestNum +} + +func writeMockStatusResponse(t *testing.T, w http.ResponseWriter, payload []byte) { + content, err := json.Marshal(payload) + assert.NoError(t, err) + _, err = w.Write(content) + assert.NoError(t, err) +} diff --git a/evidence/manager.go b/evidence/manager.go new file mode 100644 index 000000000..e4a8ce45c --- /dev/null +++ b/evidence/manager.go @@ -0,0 +1,41 @@ +package evidence + +import ( + "github.com/jfrog/jfrog-client-go/config" + "github.com/jfrog/jfrog-client-go/evidence/services" + "github.com/jfrog/jfrog-client-go/http/jfroghttpclient" +) + +type EvidenceServicesManager struct { + client *jfroghttpclient.JfrogHttpClient + config config.Config +} + +func New(config config.Config) (*EvidenceServicesManager, error) { + details := config.GetServiceDetails() + var err error + manager := &EvidenceServicesManager{config: config} + manager.client, err = jfroghttpclient.JfrogClientBuilder(). + SetCertificatesPath(config.GetCertificatesPath()). + SetInsecureTls(config.IsInsecureTls()). + SetClientCertPath(details.GetClientCertPath()). + SetClientCertKeyPath(details.GetClientCertKeyPath()). + AppendPreRequestInterceptor(details.RunPreRequestFunctions). + SetContext(config.GetContext()). + SetDialTimeout(config.GetDialTimeout()). + SetOverallRequestTimeout(config.GetOverallRequestTimeout()). + SetRetries(config.GetHttpRetries()). + SetRetryWaitMilliSecs(config.GetHttpRetryWaitMilliSecs()). + Build() + + return manager, err +} + +func (esm *EvidenceServicesManager) Client() *jfroghttpclient.JfrogHttpClient { + return esm.client +} + +func (esm *EvidenceServicesManager) UploadEvidence(evidenceDetails services.EvidenceDetails) ([]byte, error) { + evidenceService := services.NewEvidenceService(esm.config.GetServiceDetails(), esm.client) + return evidenceService.UploadEvidence(evidenceDetails) +} diff --git a/evidence/services/create.go b/evidence/services/create.go new file mode 100644 index 000000000..f9d316da2 --- /dev/null +++ b/evidence/services/create.go @@ -0,0 +1,33 @@ +package services + +import "path" + +const ( + evidenceCreateAPI = "api/v1/subject" +) + +type createEvidenceOperation struct { + evidenceBody EvidenceCreationBody +} + +func (ce *createEvidenceOperation) getOperationRestApi() string { + return path.Join(evidenceCreateAPI, ce.evidenceBody.SubjectUri) +} + +func (ce *createEvidenceOperation) getRequestBody() []byte { + return ce.evidenceBody.DSSEFileRaw +} + +func (es *EvidenceService) UploadEvidence(evidenceDetails EvidenceDetails) ([]byte, error) { + operation := createEvidenceOperation{ + evidenceBody: EvidenceCreationBody{ + EvidenceDetails: evidenceDetails, + }, + } + body, err := es.doOperation(&operation) + return body, err +} + +type EvidenceCreationBody struct { + EvidenceDetails +} diff --git a/evidence/services/operation.go b/evidence/services/operation.go new file mode 100644 index 000000000..c59beff12 --- /dev/null +++ b/evidence/services/operation.go @@ -0,0 +1,51 @@ +package services + +import ( + rtUtils "github.com/jfrog/jfrog-client-go/artifactory/services/utils" + "github.com/jfrog/jfrog-client-go/auth" + "github.com/jfrog/jfrog-client-go/http/jfroghttpclient" + "github.com/jfrog/jfrog-client-go/utils/errorutils" + "net/http" + "net/url" +) + +type EvidenceService struct { + client *jfroghttpclient.JfrogHttpClient + evidenceDetails *auth.ServiceDetails +} + +func NewEvidenceService(evidenceDetails auth.ServiceDetails, client *jfroghttpclient.JfrogHttpClient) *EvidenceService { + return &EvidenceService{evidenceDetails: &evidenceDetails, client: client} +} + +func (es *EvidenceService) GetEvidenceDetails() auth.ServiceDetails { + return *es.evidenceDetails +} + +type EvidenceOperation interface { + getOperationRestApi() string + getRequestBody() []byte +} + +func (es *EvidenceService) doOperation(operation EvidenceOperation) ([]byte, error) { + u := url.URL{Path: operation.getOperationRestApi()} + requestFullUrl, err := url.Parse(es.GetEvidenceDetails().GetUrl() + u.String()) + if err != nil { + return []byte{}, errorutils.CheckError(err) + } + + httpClientDetails := es.GetEvidenceDetails().CreateHttpClientDetails() + rtUtils.SetContentType("application/json", &httpClientDetails.Headers) + + resp, body, err := es.client.SendPost(requestFullUrl.String(), operation.getRequestBody(), &httpClientDetails) + if err != nil { + return []byte{}, err + } + + return body, errorutils.CheckResponseStatusWithBody(resp, body, http.StatusOK, http.StatusCreated) +} + +type EvidenceDetails struct { + SubjectUri string `json:"subject_uri"` + DSSEFileRaw []byte `json:"dsse_file_raw"` +} diff --git a/go.mod b/go.mod index f0feb0acf..49ca3fba6 100644 --- a/go.mod +++ b/go.mod @@ -11,7 +11,7 @@ require ( github.com/gookit/color v1.5.4 github.com/jfrog/archiver/v3 v3.6.0 github.com/jfrog/build-info-go v1.9.26 - github.com/jfrog/gofrog v1.7.1 + github.com/jfrog/gofrog v1.7.2 github.com/stretchr/testify v1.9.0 github.com/xanzy/ssh-agent v0.3.3 golang.org/x/crypto v0.23.0 @@ -43,7 +43,7 @@ require ( github.com/pierrec/lz4/v4 v4.1.21 // indirect github.com/pjbgf/sha1cd v0.3.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - github.com/rivo/uniseg v0.4.3 // indirect + github.com/rivo/uniseg v0.4.7 // indirect github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect github.com/skeema/knownhosts v1.2.2 // indirect github.com/ulikunitz/xz v0.5.11 // indirect diff --git a/go.sum b/go.sum index ca5deea59..73c0c0e43 100644 --- a/go.sum +++ b/go.sum @@ -54,8 +54,8 @@ github.com/jfrog/archiver/v3 v3.6.0 h1:OVZ50vudkIQmKMgA8mmFF9S0gA47lcag22N13iV3F github.com/jfrog/archiver/v3 v3.6.0/go.mod h1:fCAof46C3rAXgZurS8kNRNdSVMKBbZs+bNNhPYxLldI= github.com/jfrog/build-info-go v1.9.26 h1:1Ddc6+Ecvhc+UMnKhRVG1jGM6fYNwA49207azTBGBc8= github.com/jfrog/build-info-go v1.9.26/go.mod h1:8T7/ajM9aGshvgpwCtXwIFpyF/R6CEn4W+/FLryNXWw= -github.com/jfrog/gofrog v1.7.1 h1:ME1Meg4hukAT/7X6HUQCVSe4DNjMZACCP8aCY37EW/w= -github.com/jfrog/gofrog v1.7.1/go.mod h1:X7bjfWoQDN0Z4FQGbE91j3gbPP7Urwzm4Z8tkvrlbRI= +github.com/jfrog/gofrog v1.7.2 h1:VkAaA/9tmbw27IqgUOmaZWnO6ATUqL3vRzDnsROKATw= +github.com/jfrog/gofrog v1.7.2/go.mod h1:WJFk88SR9Sr9mKl1bQBig7DmSdXiBGKV3WhL9O6jL9w= github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4= github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= @@ -84,8 +84,8 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/rivo/uniseg v0.4.3 h1:utMvzDsuh3suAEnhH0RdHmoPbU648o6CvXxTx4SBMOw= -github.com/rivo/uniseg v0.4.3/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= +github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8= github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= diff --git a/tests/timeout_test.go b/tests/timeout_test.go index 660bdb611..ea7304654 100644 --- a/tests/timeout_test.go +++ b/tests/timeout_test.go @@ -1,6 +1,8 @@ package tests import ( + "github.com/jfrog/jfrog-client-go/evidence" + "github.com/jfrog/jfrog-client-go/evidence/services" "net/http" "net/http/httptest" "testing" @@ -18,6 +20,7 @@ import ( distributionAuth "github.com/jfrog/jfrog-client-go/distribution/auth" distributionServices "github.com/jfrog/jfrog-client-go/distribution/services" + evidenceAuth "github.com/jfrog/jfrog-client-go/evidence/auth" lifecycleAuth "github.com/jfrog/jfrog-client-go/lifecycle/auth" lifecycleServices "github.com/jfrog/jfrog-client-go/lifecycle/services" pipelinesAuth "github.com/jfrog/jfrog-client-go/pipelines/auth" @@ -44,6 +47,7 @@ func TestTimeout(t *testing.T) { t.Run("testLifecycleTimeout", testLifecycleTimeout) t.Run("testPipelinesTimeout", testPipelinesTimeout) t.Run("testXrayTimeout", testXrayTimeout) + t.Run("testEvidenceTimeout", testEvidenceTimeout) } func testAccessTimeout(t *testing.T) { @@ -110,6 +114,28 @@ func testLifecycleTimeout(t *testing.T) { assert.ErrorContains(t, err, "context deadline exceeded") } +func testEvidenceTimeout(t *testing.T) { + // Create mock server + url, cleanup := createSleepyRequestServer() + defer cleanup() + + // Create services manager configuring to work with the mock server + details := evidenceAuth.NewEvidenceDetails() + details.SetUrl(url) + servicesManager, err := evidence.New(createServiceConfigWithTimeout(t, details)) + assert.NoError(t, err) + + dsseFile := []byte("dsse file") + serviceDetails := services.EvidenceDetails{ + SubjectUri: "repository/path/to/file.txt", + DSSEFileRaw: dsseFile, + } + + // Expect timeout + _, err = servicesManager.UploadEvidence(serviceDetails) + assert.ErrorContains(t, err, "context deadline exceeded") +} + func testPipelinesTimeout(t *testing.T) { // Create mock server url, cleanup := createSleepyRequestServer() diff --git a/tests/utils_test.go b/tests/utils_test.go index 16fd2010b..feb157850 100644 --- a/tests/utils_test.go +++ b/tests/utils_test.go @@ -869,7 +869,7 @@ func isEnterprisePlus() (bool, error) { if err != nil { return false, err } - return value == "Enterprise Plus", nil + return strings.Contains(value, "Enterprise Plus"), nil } func createRepoConfigValidationFunc(repoKey string, expectedConfig interface{}) clientutils.ExecutionHandlerFunc { @@ -921,7 +921,7 @@ func createRepoConfigValidationFunc(repoKey string, expectedConfig interface{}) func validateRepoConfig(t *testing.T, repoKey string, params interface{}) { retryExecutor := &clientutils.RetryExecutor{ - MaxRetries: 30, + MaxRetries: 5, // RetriesIntervalMilliSecs in milliseconds RetriesIntervalMilliSecs: 10 * 1000, ErrorMessage: "Waiting for Artifactory to evaluate repository operation...", diff --git a/utils/archiveutils.go b/utils/archiveutils.go index 63f31c302..b4489dd59 100644 --- a/utils/archiveutils.go +++ b/utils/archiveutils.go @@ -17,16 +17,15 @@ import ( // bypassInspection - Set to true to bypass archive inspection against ZipSlip // Extract an archive file to the 'localPath'. func ExtractArchive(localPath, localFileName, originFileName, logMsgPrefix string, bypassInspection bool) error { - unarchiver := &unarchive.Unarchiver{ - BypassInspection: bypassInspection, - } - if !unarchiver.IsSupportedArchive(originFileName) { + if !unarchive.IsSupportedArchive(originFileName) { return nil } + extractionPath, err := getExtractionPath(localPath) if err != nil { return err } + var archivePath string if !strings.HasPrefix(localFileName, localPath) { archivePath = filepath.Join(localPath, localFileName) @@ -37,11 +36,17 @@ func ExtractArchive(localPath, localFileName, originFileName, logMsgPrefix strin if err != nil { return err } + err = os.MkdirAll(extractionPath, 0755) if errorutils.CheckError(err) != nil { return err } + log.Info(logMsgPrefix+"Extracting archive:", archivePath, "to", extractionPath) + + unarchiver := &unarchive.Unarchiver{ + BypassInspection: bypassInspection, + } return errorutils.CheckError(extract(archivePath, originFileName, extractionPath, unarchiver)) }