Skip to content

Commit 8902370

Browse files
authored
feature/add conan support (jfrog#321)
1 parent 20b9565 commit 8902370

File tree

10 files changed

+2111
-21
lines changed

10 files changed

+2111
-21
lines changed
Lines changed: 280 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,280 @@
1+
package conan
2+
3+
import (
4+
"fmt"
5+
"strconv"
6+
"strings"
7+
"time"
8+
9+
"github.com/jfrog/build-info-go/entities"
10+
"github.com/jfrog/jfrog-cli-core/v2/artifactory/utils"
11+
"github.com/jfrog/jfrog-cli-core/v2/utils/config"
12+
"github.com/jfrog/jfrog-client-go/artifactory/services"
13+
specutils "github.com/jfrog/jfrog-client-go/artifactory/services/utils"
14+
"github.com/jfrog/jfrog-client-go/utils/io/content"
15+
"github.com/jfrog/jfrog-client-go/utils/log"
16+
)
17+
18+
// ConanPackageInfo holds parsed Conan package reference information.
19+
// Supports both Conan 2.x (name/version) and 1.x (name/version@user/channel) formats.
20+
type ConanPackageInfo struct {
21+
Name string
22+
Version string
23+
User string
24+
Channel string
25+
}
26+
27+
// ArtifactCollector collects Conan artifacts from Artifactory.
28+
type ArtifactCollector struct {
29+
serverDetails *config.ServerDetails
30+
targetRepo string
31+
}
32+
33+
// NewArtifactCollector creates a new artifact collector.
34+
func NewArtifactCollector(serverDetails *config.ServerDetails, targetRepo string) *ArtifactCollector {
35+
return &ArtifactCollector{
36+
serverDetails: serverDetails,
37+
targetRepo: targetRepo,
38+
}
39+
}
40+
41+
// CollectArtifacts searches Artifactory for Conan artifacts matching the package reference.
42+
func (ac *ArtifactCollector) CollectArtifacts(packageRef string) ([]entities.Artifact, error) {
43+
if ac.serverDetails == nil {
44+
return nil, fmt.Errorf("server details not initialized")
45+
}
46+
47+
pkgInfo, err := ParsePackageReference(packageRef)
48+
if err != nil {
49+
return nil, err
50+
}
51+
52+
return ac.searchArtifacts(buildArtifactQuery(ac.targetRepo, pkgInfo))
53+
}
54+
55+
// CollectArtifactsForPath collects artifacts from a specific path.
56+
// Used to collect only artifacts that were uploaded in the current build.
57+
// The path should be exact (e.g., "_/multideps/1.0.0/_/revision/export")
58+
func (ac *ArtifactCollector) CollectArtifactsForPath(exactPath string) ([]entities.Artifact, error) {
59+
if ac.serverDetails == nil {
60+
return nil, fmt.Errorf("server details not initialized")
61+
}
62+
63+
// Use exact path match - artifacts are directly in the path, not subfolders
64+
query := fmt.Sprintf(`{"repo": "%s", "path": "%s"}`, ac.targetRepo, exactPath)
65+
return ac.searchArtifacts(query)
66+
}
67+
68+
// searchArtifacts executes an AQL query and returns matching artifacts.
69+
func (ac *ArtifactCollector) searchArtifacts(aqlQuery string) ([]entities.Artifact, error) {
70+
servicesManager, err := utils.CreateServiceManager(ac.serverDetails, -1, 0, false)
71+
if err != nil {
72+
return nil, fmt.Errorf("create services manager: %w", err)
73+
}
74+
75+
searchParams := services.SearchParams{
76+
CommonParams: &specutils.CommonParams{
77+
Aql: specutils.Aql{ItemsFind: aqlQuery},
78+
},
79+
}
80+
81+
reader, err := servicesManager.SearchFiles(searchParams)
82+
if err != nil {
83+
return nil, fmt.Errorf("search files: %w", err)
84+
}
85+
defer closeReader(reader)
86+
87+
return parseSearchResults(reader), nil
88+
}
89+
90+
// parseSearchResults converts AQL search results to artifacts.
91+
func parseSearchResults(reader *content.ContentReader) []entities.Artifact {
92+
var artifacts []entities.Artifact
93+
94+
for item := new(specutils.ResultItem); reader.NextRecord(item) == nil; item = new(specutils.ResultItem) {
95+
artifact := entities.Artifact{
96+
Name: item.Name,
97+
Path: item.Path,
98+
Checksum: entities.Checksum{
99+
Sha1: item.Actual_Sha1,
100+
Sha256: item.Sha256,
101+
Md5: item.Actual_Md5,
102+
},
103+
}
104+
artifacts = append(artifacts, artifact)
105+
}
106+
107+
return artifacts
108+
}
109+
110+
// ParsePackageReference parses a Conan package reference string into structured info.
111+
// Supports both formats:
112+
// - Conan 2.x: name/version (e.g., "zlib/1.2.13")
113+
// - Conan 1.x: name/version@user/channel (e.g., "zlib/1.2.13@_/_")
114+
func ParsePackageReference(ref string) (*ConanPackageInfo, error) {
115+
ref = strings.TrimSpace(ref)
116+
117+
// Check for @user/channel format (Conan 1.x style)
118+
if idx := strings.Index(ref, "@"); idx != -1 {
119+
nameVersion := ref[:idx]
120+
userChannel := ref[idx+1:]
121+
122+
nameParts := strings.SplitN(nameVersion, "/", 2)
123+
channelParts := strings.SplitN(userChannel, "/", 2)
124+
125+
if len(nameParts) != 2 || len(channelParts) != 2 {
126+
return nil, fmt.Errorf("invalid package reference: %s", ref)
127+
}
128+
129+
return &ConanPackageInfo{
130+
Name: nameParts[0],
131+
Version: nameParts[1],
132+
User: channelParts[0],
133+
Channel: channelParts[1],
134+
}, nil
135+
}
136+
137+
// Simple name/version format (Conan 2.x style)
138+
parts := strings.SplitN(ref, "/", 2)
139+
if len(parts) != 2 {
140+
return nil, fmt.Errorf("invalid package reference: %s", ref)
141+
}
142+
143+
return &ConanPackageInfo{
144+
Name: parts[0],
145+
Version: parts[1],
146+
User: "_",
147+
Channel: "_",
148+
}, nil
149+
}
150+
151+
// buildArtifactQuery creates an AQL query for Conan artifacts.
152+
// Conan stores artifacts in different path formats depending on version:
153+
// - Conan 2.x: _/name/version/_/revision/...
154+
// - Conan 1.x: user/name/version/channel/revision/...
155+
func buildArtifactQuery(repo string, pkg *ConanPackageInfo) string {
156+
if pkg.User == "_" && pkg.Channel == "_" {
157+
return fmt.Sprintf(`{"repo": "%s", "path": {"$match": "_/%s/%s/_/*"}}`,
158+
repo, pkg.Name, pkg.Version)
159+
}
160+
return fmt.Sprintf(`{"repo": "%s", "path": {"$match": "%s/%s/%s/%s/*"}}`,
161+
repo, pkg.User, pkg.Name, pkg.Version, pkg.Channel)
162+
}
163+
164+
// BuildPropertySetter sets build properties on Conan artifacts in Artifactory.
165+
// This is required to link artifacts to build info in Artifactory UI.
166+
type BuildPropertySetter struct {
167+
serverDetails *config.ServerDetails
168+
targetRepo string
169+
buildName string
170+
buildNumber string
171+
projectKey string
172+
}
173+
174+
// NewBuildPropertySetter creates a new build property setter.
175+
func NewBuildPropertySetter(serverDetails *config.ServerDetails, targetRepo, buildName, buildNumber, projectKey string) *BuildPropertySetter {
176+
return &BuildPropertySetter{
177+
serverDetails: serverDetails,
178+
targetRepo: targetRepo,
179+
buildName: buildName,
180+
buildNumber: buildNumber,
181+
projectKey: projectKey,
182+
}
183+
}
184+
185+
// SetProperties sets build properties on the given artifacts in a single batch operation.
186+
// This uses the same approach as Docker - writing all items to a temp file and making
187+
// one SetProps call, which is much more efficient than individual calls per artifact.
188+
func (bps *BuildPropertySetter) SetProperties(artifacts []entities.Artifact) error {
189+
if len(artifacts) == 0 || bps.serverDetails == nil {
190+
return nil
191+
}
192+
193+
servicesManager, err := utils.CreateServiceManager(bps.serverDetails, -1, 0, false)
194+
if err != nil {
195+
return fmt.Errorf("create services manager: %w", err)
196+
}
197+
198+
// Convert artifacts to ResultItem format for batch processing
199+
resultItems := bps.convertToResultItems(artifacts)
200+
if len(resultItems) == 0 {
201+
return nil
202+
}
203+
204+
// Write all items to a temp file (like Docker does)
205+
pathToFile, err := bps.writeItemsToFile(resultItems)
206+
if err != nil {
207+
return fmt.Errorf("write items to file: %w", err)
208+
}
209+
210+
// Create reader and set properties in one batch call
211+
reader := content.NewContentReader(pathToFile, content.DefaultKey)
212+
defer closeReader(reader)
213+
214+
timestamp := strconv.FormatInt(time.Now().UnixMilli(), 10)
215+
props := bps.formatBuildProperties(timestamp)
216+
217+
_, err = servicesManager.SetProps(services.PropsParams{Reader: reader, Props: props, UseDebugLogs: true, IsRecursive: true})
218+
if err != nil {
219+
return fmt.Errorf("set properties: %w", err)
220+
}
221+
222+
log.Info(fmt.Sprintf("Set build properties on %d Conan artifacts (batch)", len(artifacts)))
223+
return nil
224+
}
225+
226+
// convertToResultItems converts build-info artifacts to ResultItem format for SetProps.
227+
func (bps *BuildPropertySetter) convertToResultItems(artifacts []entities.Artifact) []specutils.ResultItem {
228+
var items []specutils.ResultItem
229+
for _, artifact := range artifacts {
230+
items = append(items, specutils.ResultItem{
231+
Repo: bps.targetRepo,
232+
Path: artifact.Path,
233+
Name: artifact.Name,
234+
Actual_Sha1: artifact.Sha1,
235+
Actual_Md5: artifact.Md5,
236+
Sha256: artifact.Sha256,
237+
})
238+
}
239+
return items
240+
}
241+
242+
// writeItemsToFile writes result items to a temp file for batch processing.
243+
func (bps *BuildPropertySetter) writeItemsToFile(items []specutils.ResultItem) (string, error) {
244+
writer, err := content.NewContentWriter("results", true, false)
245+
if err != nil {
246+
return "", err
247+
}
248+
defer func() {
249+
if closeErr := writer.Close(); closeErr != nil {
250+
log.Debug(fmt.Sprintf("Failed to close writer: %s", closeErr))
251+
}
252+
}()
253+
254+
for _, item := range items {
255+
writer.Write(item)
256+
}
257+
return writer.GetFilePath(), nil
258+
}
259+
260+
// formatBuildProperties creates the build properties string.
261+
// Only includes build.name, build.number, build.timestamp (and optional build.project).
262+
func (bps *BuildPropertySetter) formatBuildProperties(timestamp string) string {
263+
props := fmt.Sprintf("build.name=%s;build.number=%s;build.timestamp=%s",
264+
bps.buildName, bps.buildNumber, timestamp)
265+
266+
if bps.projectKey != "" {
267+
props += fmt.Sprintf(";build.project=%s", bps.projectKey)
268+
}
269+
270+
return props
271+
}
272+
273+
// closeReader safely closes a content reader.
274+
func closeReader(reader *content.ContentReader) {
275+
if reader != nil {
276+
if err := reader.Close(); err != nil {
277+
log.Debug(fmt.Sprintf("Failed to close reader: %s", err))
278+
}
279+
}
280+
}

0 commit comments

Comments
 (0)