|
| 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