Skip to content

RTDEV-56127 - Support annotate command for release bundle #1110

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 4 commits into
base: dev
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3003,6 +3003,39 @@ resp, err := serviceManager.RemoteDeleteReleaseBundle(params, dryRun)
exists, err := serviceManager.ReleaseBundleExists(rbName, rbVersion, projectKey)
```

#### Annotate Release Bundle
```go
rbDetails := ReleaseBundleDetails{"rbName", "rbVersion"}
queryParams := CommonOptionalQueryParams{}
queryParams.ProjectKey = "project"

cmd := NewReleaseBundleAnnotateCommand()
serverDetails := &config.ServerDetails{
ArtifactoryUrl: "https://artifactory.example.com",
}
cmd.SetServerDetails(serverDetails)
annotateParams := lifecycle.AnnotateOperationParams{
RbTag: lifecycle.RbAnnotationTag{
Tag: "bundle-tag",
Exist: true,
},
RbProps: lifecycle.RbAnnotationProps{
Properties:props.ToMap(),
Exists: false,
},
RbDetails: rbDetails,
QueryParams: queryParams,
PropertyParams: lifecycle.CommonPropParams{
Path: "manifest-path"
RepoKey: "repokey"
},
ArtifactoryUrl: services.ArtCommonParams{
Url: cmd.ServerDetails().ArtifactoryUrl,
},
}

resp, err := serviceManager.AnnotateReleaseBundle(params)
```
## Evidence APIs

### Creating Evidence Service Manager
Expand Down
110 changes: 110 additions & 0 deletions lifecycle/lifecycle_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@ package lifecycle

import (
"encoding/json"
"fmt"
artifactoryAuth "github.com/jfrog/jfrog-client-go/artifactory/auth"
"github.com/jfrog/jfrog-client-go/artifactory/services/utils"
"github.com/jfrog/jfrog-client-go/http/jfroghttpclient"
lifecycle "github.com/jfrog/jfrog-client-go/lifecycle/services"
"github.com/stretchr/testify/assert"
Expand All @@ -12,6 +14,11 @@ import (
"time"
)

const (
manifestName = "release-bundle.json.evd"
releaseBundlesV2 = "release-bundles-v2"
)

var testRb = lifecycle.ReleaseBundleDetails{
ReleaseBundleName: "bundle-test",
ReleaseBundleVersion: "1.2.3",
Expand Down Expand Up @@ -285,3 +292,106 @@ func TestIsReleaseBundleExistWithProject(t *testing.T) {
assert.NoError(t, err)
assert.False(t, exist)
}

func TestReleaseBundleAnnotate(t *testing.T) {
mockServer, rbService := createMockServer(t, func(w http.ResponseWriter, r *http.Request) {
if r.RequestURI == "/"+lifecycle.GetReleaseBundleSetTagApi(testRb) {
w.WriteHeader(http.StatusOK)
_, err := w.Write([]byte(`{
{
"repository_key": "release-bundles-v2",
"release_bundle_name": "bundle-test",
"release_bundle_version": "1.2.3",
"release_bundle_tag" : "bundle-tag"
}
}`))
assert.NoError(t, err)
}
if r.RequestURI == "/artifactory/"+lifecycle.PropertiesBaseApi {
w.WriteHeader(http.StatusCreated) // Status 201
writeMockStatusResponse(t, w, map[string]interface{}{"message": "Created", "status": "201"})
}
})
defer mockServer.Close()
properties := "environment=qa335;buildNumber=335"
annotateOperationParams := buildAnnotationOperationParams(testRb, "default", "bundle-tag", properties, manifestName, mockServer.URL)
err := rbService.AnnotateReleaseBundle(annotateOperationParams)
assert.NoError(t, err)
}

func testAnnotate(t *testing.T, projectKey, tag, properties string, expectError bool) {
mockServer, rbService := createMockServer(t, func(w http.ResponseWriter, r *http.Request) {
if r.RequestURI == "/"+lifecycle.GetReleaseBundleSetTagApi(testRb) {
w.WriteHeader(http.StatusOK)
}
if r.RequestURI == "/artifactory/"+lifecycle.PropertiesBaseApi {
w.WriteHeader(http.StatusCreated) // Status 201
writeMockStatusResponse(t, w, map[string]interface{}{"message": "Created", "status": "201"})
}
})
defer mockServer.Close()
annotateOperationParams := buildAnnotationOperationParams(testRb, projectKey, tag, properties, manifestName, mockServer.URL)
err := rbService.AnnotateReleaseBundle(annotateOperationParams)
if expectError {
assert.Error(t, err)
return
}
assert.NoError(t, err)
}

func TestReleaseBundleAnnotationCases(t *testing.T) {
testCases := []struct {
name string
projectKey string
tag string
properties string
expectError bool
}{
{"tag, properties and project is empty", "", "bundle-tag", "prop1=1;prop2=2", false},
{"tag, property, project is default", "default", "bundle-tag", "prop1=1;prop2=2", false},
{"tag is empty", "default", "", "prop1=1;prop2=2", false},
{"property is empty, project is default", "default", "bundle-tag", "", false},
{"property is empty", "project", "bundle-tag", "", false},
{"property is one pair, tag isn't empty", "project", "bundle-tag", "prop1=1", false},
{"property is one pair, tag is empty", "project", "", "prop1=1", false},
}

for _, test := range testCases {
t.Run(test.name, func(t *testing.T) {
testAnnotate(t, test.projectKey, test.tag, test.properties, test.expectError)
})
}
}

func buildAnnotationOperationParams(rbDetails lifecycle.ReleaseBundleDetails, projectKey, bundleTag, properties, manifestName, artUrl string) lifecycle.AnnotateOperationParams {
props, _ := utils.ParseProperties(properties)
annotateOperationParams := lifecycle.AnnotateOperationParams{
RbTag: lifecycle.RbAnnotationTag{
Tag: bundleTag,
Exist: true,
},
RbProps: lifecycle.RbAnnotationProps{
Properties: props.ToMap(),
Exist: true,
},
RbDetails: rbDetails,
QueryParams: lifecycle.CommonOptionalQueryParams{
ProjectKey: projectKey,
},
PropertyParams: lifecycle.CommonPropParams{
Path: fmt.Sprintf("%s/%s/%s", rbDetails.ReleaseBundleName, rbDetails.ReleaseBundleVersion, manifestName),
RepoKey: resolveRepoKey(projectKey),
},
ArtifactoryUrl: lifecycle.ArtCommonParams{
Url: artUrl + "/artifactory/",
},
}
return annotateOperationParams
}

func resolveRepoKey(project string) string {
if project == "" || project == "default" {
return releaseBundlesV2
}
return fmt.Sprintf("%s-%s", project, releaseBundlesV2)
}
5 changes: 5 additions & 0 deletions lifecycle/manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -138,3 +138,8 @@ func (lcs *LifecycleServicesManager) IsReleaseBundleExist(rbName, rbVersion, pro
rbService := lifecycle.NewReleaseBundlesService(lcs.config.GetServiceDetails(), lcs.client)
return rbService.ReleaseBundleExists(rbName, rbVersion, projectKey)
}

func (lcs *LifecycleServicesManager) AnnotateReleaseBundle(params lifecycle.AnnotateOperationParams) error {
rbService := lifecycle.NewReleaseBundlesService(lcs.config.GetServiceDetails(), lcs.client)
return rbService.AnnotateReleaseBundle(params)
}
185 changes: 185 additions & 0 deletions lifecycle/services/annotate.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
package services

import (
"encoding/json"
"github.com/jfrog/jfrog-client-go/utils"
"github.com/jfrog/jfrog-client-go/utils/distribution"
"github.com/jfrog/jfrog-client-go/utils/errorutils"
"github.com/jfrog/jfrog-client-go/utils/io/httputils"
"github.com/jfrog/jfrog-client-go/utils/log"
"net/http"
"path"
)

const (
Tag = "tag"
PropertiesBaseApi = "api/artifactproperties"
)

type Property struct {
Name string `json:"name"`
}

type PropertyBody struct {
MultiValue bool `json:"multiValue"`
Property Property `json:"property"`
SelectedValues []string `json:"selectedValues"`
}

type InitialProperties struct {
Properties map[string][]string `json:"properties"`
}

type CommonPropParams struct {
Path string `json:"path"`
Recursive bool `json:"recursive"`
RepoKey string `json:"repoKey"`
}

type ArtCommonParams struct {
Url string `json:"url"`
}

type AnnotateOperationParams struct {
RbTag RbAnnotationTag
RbProps RbAnnotationProps
RbDetails ReleaseBundleDetails
QueryParams CommonOptionalQueryParams
PropertyParams CommonPropParams
ArtifactoryUrl ArtCommonParams
}

type RbAnnotationTag struct {
Tag string `json:"tag,omitempty"`
Exist bool `json:"exist"`
}

type RbAnnotationProps struct {
Properties map[string][]string `json:"properties,omitempty"`
Exist bool `json:"exist"`
}

func (rbs *ReleaseBundlesService) AnnotateReleaseBundle(params AnnotateOperationParams) error {
return rbs.annotateReleaseBundle(params)
}

func GetReleaseBundleSetTagApi(rbDetails ReleaseBundleDetails) string {
return path.Join(releaseBundleBaseApi, records, rbDetails.ReleaseBundleName, rbDetails.ReleaseBundleVersion, Tag)
}

func (rbs *ReleaseBundlesService) setTag(params AnnotateOperationParams, api string, httpClientsDetails httputils.HttpClientDetails) error {
if !params.RbTag.Exist {
log.Info("Tag doesn't exist")
return nil
}

projParam := distribution.GetProjectQueryParam(params.QueryParams.ProjectKey)
tagContent, err := json.Marshal(params.RbTag)
if err != nil {
return errorutils.CheckError(err)
}

setTagFullUrl, err := utils.BuildUrl(rbs.GetLifecycleDetails().GetUrl(), api, projParam)
if err != nil {
return err
}

resp, body, err := rbs.client.SendPut(setTagFullUrl, tagContent, &httpClientsDetails)
if err != nil {
return err
}

if err = errorutils.CheckResponseStatusWithBody(resp, body, http.StatusOK); err != nil {
return err
}
return err
}

func (rbs *ReleaseBundlesService) setProps(params AnnotateOperationParams, api string, httpClientsDetails httputils.HttpClientDetails) error {
if !params.RbProps.Exist {
log.Info("Properties doesn't exist")
return nil
}

var err error
propsContent, err := json.Marshal(params.RbProps)
if err != nil {
return errorutils.CheckError(err)
}

var initialStruct InitialProperties
err = json.Unmarshal(propsContent, &initialStruct)
if err != nil {
return errorutils.CheckError(err)
}

propsFullUrl, err := utils.BuildUrl(params.ArtifactoryUrl.Url, api, GetPropQueryParam(params.PropertyParams))
if err != nil {
return errorutils.CheckError(err)
}

for key, values := range initialStruct.Properties {
propertyBody := PropertyBody{
MultiValue: false,
Property: Property{Name: key},
SelectedValues: values,
}

var propertyContent []byte
propertyContent, err = json.Marshal(propertyBody)
if err != nil {
return errorutils.CheckError(err)
}

var resp *http.Response
var respBody []byte
resp, respBody, err = rbs.client.SendPost(propsFullUrl, propertyContent, &httpClientsDetails)
if err != nil {
return errorutils.CheckError(err)
}

if err = errorutils.CheckResponseStatusWithBody(resp, respBody, http.StatusCreated, http.StatusOK); err != nil {
return errorutils.CheckError(err)
}
}

return nil
}

func (rbs *ReleaseBundlesService) annotateReleaseBundle(params AnnotateOperationParams) error {

httpClientsDetails := rbs.GetLifecycleDetails().CreateHttpClientDetails()
httpClientsDetails.SetContentTypeApplicationJson()

var err error
err = rbs.setTag(params, GetReleaseBundleSetTagApi(params.RbDetails), httpClientsDetails)
if err != nil {
log.Info("Failed to set tag: " + params.RbTag.Tag)
}

err = rbs.setProps(params, PropertiesBaseApi, httpClientsDetails)
if err != nil {
log.Info("Failed to set properties")
}

return nil
}

func GetPropQueryParam(params CommonPropParams) map[string]string {
queryParams := make(map[string]string)
if params.Path != "" {
queryParams["path"] = params.Path
}

if params.RepoKey != "" {
queryParams["repoKey"] = params.RepoKey
}

if params.Recursive {
queryParams["recursive"] = "true"
} else {
queryParams["recursive"] = "false"
}

return queryParams
}