From 698ce163cc44b61ce98ab3393bcff008ce0a2b91 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 21 Aug 2024 18:53:43 +0000 Subject: [PATCH] Bump github.com/jfrog/jfrog-cli-core/v2 from 2.55.1 to 2.55.3 Bumps [github.com/jfrog/jfrog-cli-core/v2](https://github.com/jfrog/jfrog-cli-core) from 2.55.1 to 2.55.3. - [Release notes](https://github.com/jfrog/jfrog-cli-core/releases) - [Commits](https://github.com/jfrog/jfrog-cli-core/compare/v2.55.1...v2.55.3) --- updated-dependencies: - dependency-name: github.com/jfrog/jfrog-cli-core/v2 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- go.mod | 2 +- go.sum | 4 +- .../commandssummaries/buildinfosummary.go | 131 ++++- .../commands/commandssummaries/utils.go | 2 + .../artifactory/utils/container/buildinfo.go | 489 ++++++++++++++++++ .../utils/container/containermanager.go | 262 ++++++++++ .../v2/artifactory/utils/container/image.go | 156 ++++++ .../artifactory/utils/container/localagent.go | 196 +++++++ .../artifactory/utils/container/manifest.go | 98 ++++ .../utils/container/remoteagent.go | 125 +++++ .../v2/utils/coreutils/coreconsts.go | 4 +- vendor/modules.txt | 3 +- 12 files changed, 1441 insertions(+), 31 deletions(-) create mode 100644 vendor/github.com/jfrog/jfrog-cli-core/v2/artifactory/utils/container/buildinfo.go create mode 100644 vendor/github.com/jfrog/jfrog-cli-core/v2/artifactory/utils/container/containermanager.go create mode 100644 vendor/github.com/jfrog/jfrog-cli-core/v2/artifactory/utils/container/image.go create mode 100644 vendor/github.com/jfrog/jfrog-cli-core/v2/artifactory/utils/container/localagent.go create mode 100644 vendor/github.com/jfrog/jfrog-cli-core/v2/artifactory/utils/container/manifest.go create mode 100644 vendor/github.com/jfrog/jfrog-cli-core/v2/artifactory/utils/container/remoteagent.go diff --git a/go.mod b/go.mod index d46e3ce2..a1ac4c16 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,7 @@ go 1.22.3 require ( github.com/Masterminds/semver v1.5.0 - github.com/jfrog/jfrog-cli-core/v2 v2.55.1 + github.com/jfrog/jfrog-cli-core/v2 v2.55.3 github.com/jfrog/jfrog-client-go v1.44.2 gopkg.in/yaml.v3 v3.0.1 ) diff --git a/go.sum b/go.sum index 0b0f2e43..4b58026d 100644 --- a/go.sum +++ b/go.sum @@ -96,8 +96,8 @@ github.com/jfrog/build-info-go v1.9.34 h1:bPnW58VpclbpBe/x8XEu/2BIviEOoJrJ5PkRRc github.com/jfrog/build-info-go v1.9.34/go.mod h1:6mdtqjREK76bHNODXakqKR/+ksJ9dvfLS7H57BZtnLY= github.com/jfrog/gofrog v1.7.5 h1:dFgtEDefJdlq9cqTRoe09RLxS5Bxbe1Ev5+E6SmZHcg= github.com/jfrog/gofrog v1.7.5/go.mod h1:jyGiCgiqSSR7k86hcUSu67XVvmvkkgWTmPsH25wI298= -github.com/jfrog/jfrog-cli-core/v2 v2.55.1 h1:l8za3FqU833g2t0OUOCVw8MuBX8HAMNatPA+aYJPsVE= -github.com/jfrog/jfrog-cli-core/v2 v2.55.1/go.mod h1:Ju0hfP+5KyVPxoDW0iPMutmGo7wrDwk33APWKsJM96E= +github.com/jfrog/jfrog-cli-core/v2 v2.55.3 h1:/PWlB8yxJlUjB9TdhQ5TIFjMMDXujNy5WJBFOOUoldc= +github.com/jfrog/jfrog-cli-core/v2 v2.55.3/go.mod h1:2/Ccqq0ayMqIuH5AAoneX0CowwdrNWQcs5aKz8iDYkE= github.com/jfrog/jfrog-client-go v1.44.2 h1:5t8tx6NOth6Xq24SdF3MYSd6vo0bTibW93nads2DEuY= github.com/jfrog/jfrog-client-go v1.44.2/go.mod h1:f5Jfv+RGKVr4smOp4a4pxyBKdlpLG7R894kx2XW+w8c= github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4= diff --git a/vendor/github.com/jfrog/jfrog-cli-core/v2/artifactory/commands/commandssummaries/buildinfosummary.go b/vendor/github.com/jfrog/jfrog-cli-core/v2/artifactory/commands/commandssummaries/buildinfosummary.go index a870bae0..acd3710c 100644 --- a/vendor/github.com/jfrog/jfrog-cli-core/v2/artifactory/commands/commandssummaries/buildinfosummary.go +++ b/vendor/github.com/jfrog/jfrog-cli-core/v2/artifactory/commands/commandssummaries/buildinfosummary.go @@ -4,6 +4,7 @@ import ( "fmt" buildInfo "github.com/jfrog/build-info-go/entities" "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/commandsummary" "path" "strings" @@ -59,22 +60,12 @@ func (bis *BuildInfoSummary) buildInfoTable(builds []*buildInfo.BuildInfo) strin func (bis *BuildInfoSummary) buildInfoModules(builds []*buildInfo.BuildInfo) string { var markdownBuilder strings.Builder - markdownBuilder.WriteString("\n\n ### Modules Published As Part of This Build \n\n") + markdownBuilder.WriteString("\n### Modules Published As Part of This Build\n") var shouldGenerate bool for _, build := range builds { - for _, module := range build.Modules { - if len(module.Artifacts) == 0 { - continue - } - - switch module.Type { - case buildInfo.Docker, buildInfo.Maven, buildInfo.Npm, buildInfo.Go, buildInfo.Generic, buildInfo.Terraform: - markdownBuilder.WriteString(bis.generateModuleMarkdown(module)) - shouldGenerate = true - default: - // Skip unsupported module types. - continue - } + if modulesMarkdown := bis.generateModulesMarkdown(build.Modules...); modulesMarkdown != "" { + markdownBuilder.WriteString(modulesMarkdown) + shouldGenerate = true } } @@ -85,19 +76,47 @@ func (bis *BuildInfoSummary) buildInfoModules(builds []*buildInfo.BuildInfo) str return markdownBuilder.String() } -func parseBuildTime(timestamp string) string { - // Parse the timestamp string into a time.Time object - buildInfoTime, err := time.Parse(buildInfo.TimeFormat, timestamp) - if err != nil { - return "N/A" +func (bis *BuildInfoSummary) generateModulesMarkdown(modules ...buildInfo.Module) string { + var modulesMarkdown strings.Builder + parentToModulesMap := groupModulesByParent(modules) + if len(parentToModulesMap) == 0 { + return "" } - // Format the time in a more human-readable format and save it in a variable - return buildInfoTime.Format(timeFormat) + + for parentModuleID, parentModules := range parentToModulesMap { + modulesMarkdown.WriteString(fmt.Sprintf("#### %s\n
", parentModuleID))
+		isMultiModule := len(parentModules) > 1
+
+		for _, module := range parentModules {
+			if isMultiModule && parentModuleID == module.Id {
+				// Skip the parent module if there are multiple modules, as it will be displayed as a header
+				continue
+			}
+			modulesMarkdown.WriteString(bis.generateModuleArtifactsTree(&module, isMultiModule))
+		}
+		modulesMarkdown.WriteString("
\n") + } + return modulesMarkdown.String() } -func (bis *BuildInfoSummary) generateModuleMarkdown(module buildInfo.Module) string { - var moduleMarkdown strings.Builder - moduleMarkdown.WriteString(fmt.Sprintf("\n #### %s \n", module.Id)) +func (bis *BuildInfoSummary) generateModuleArtifactsTree(module *buildInfo.Module, shouldCollapseArtifactsTree bool) string { + artifactsTree := bis.createArtifactsTree(module) + if shouldCollapseArtifactsTree { + return bis.generateModuleCollapsibleSection(module, artifactsTree) + } + return artifactsTree +} + +func (bis *BuildInfoSummary) generateModuleCollapsibleSection(module *buildInfo.Module, sectionContent string) string { + switch module.Type { + case buildInfo.Docker: + return createCollapsibleSection(createDockerMultiArchTitle(module, bis.platformUrl), sectionContent) + default: + return createCollapsibleSection(module.Id, sectionContent) + } +} + +func (bis *BuildInfoSummary) createArtifactsTree(module *buildInfo.Module) string { artifactsTree := utils.NewFileTree() for _, artifact := range module.Artifacts { artifactUrlInArtifactory := bis.generateArtifactUrl(artifact) @@ -108,8 +127,7 @@ func (bis *BuildInfoSummary) generateModuleMarkdown(module buildInfo.Module) str artifactTreePath := path.Join(artifact.OriginalDeploymentRepo, artifact.Path) artifactsTree.AddFile(artifactTreePath, artifactUrlInArtifactory) } - moduleMarkdown.WriteString("\n\n
" + artifactsTree.String() + "
") - return moduleMarkdown.String() + return artifactsTree.String() } func (bis *BuildInfoSummary) generateArtifactUrl(artifact buildInfo.Artifact) string { @@ -118,3 +136,64 @@ func (bis *BuildInfoSummary) generateArtifactUrl(artifact buildInfo.Artifact) st } return generateArtifactUrl(bis.platformUrl, path.Join(artifact.OriginalDeploymentRepo, artifact.Path), bis.majorVersion) } + +// groupModulesByParent groups modules that share the same parent ID into a map where the key is the parent ID and the value is a slice of those modules. +func groupModulesByParent(modules []buildInfo.Module) map[string][]buildInfo.Module { + parentToModulesMap := make(map[string][]buildInfo.Module, len(modules)) + for _, module := range modules { + if len(module.Artifacts) == 0 || !isSupportedModule(&module) { + continue + } + + parentID := module.Parent + // If the module has no parent, that means it is the parent module itself, so we can use its ID as the parent ID. + if parentID == "" { + parentID = module.Id + } + parentToModulesMap[parentID] = append(parentToModulesMap[parentID], module) + } + return parentToModulesMap +} + +func isSupportedModule(module *buildInfo.Module) bool { + switch module.Type { + case buildInfo.Maven, buildInfo.Npm, buildInfo.Go, buildInfo.Generic, buildInfo.Terraform: + return true + case buildInfo.Docker: + // Skip attestations that are added as a module for multi-arch docker builds + return !strings.HasPrefix(module.Id, container.AttestationsModuleIdPrefix) + default: + return false + } +} + +func parseBuildTime(timestamp string) string { + // Parse the timestamp string into a time.Time object + buildInfoTime, err := time.Parse(buildInfo.TimeFormat, timestamp) + if err != nil { + return "N/A" + } + // Format the time in a more human-readable format and save it in a variable + return buildInfoTime.Format(timeFormat) +} + +func createDockerMultiArchTitle(module *buildInfo.Module, platformUrl string) string { + // Extract the parent image name from the module ID (e.g. my-image:1.0 -> my-image) + parentImageName := strings.Split(module.Parent, ":")[0] + + // Get the relevant SHA256 + var sha256 string + for _, artifact := range module.Artifacts { + if artifact.Name == container.ManifestJsonFile { + sha256 = artifact.Sha256 + break + } + } + // Create a link to the Docker package in Artifactory UI + dockerModuleLink := fmt.Sprintf(artifactoryDockerPackagesUiFormat, strings.TrimSuffix(platformUrl, "/"), "%2F%2F"+parentImageName, sha256) + return fmt.Sprintf("%s (🐸 View)", module.Id, dockerModuleLink) +} + +func createCollapsibleSection(title, content string) string { + return fmt.Sprintf("
%s\n%s
", title, content) +} diff --git a/vendor/github.com/jfrog/jfrog-cli-core/v2/artifactory/commands/commandssummaries/utils.go b/vendor/github.com/jfrog/jfrog-cli-core/v2/artifactory/commands/commandssummaries/utils.go index 75417663..465ca4dd 100644 --- a/vendor/github.com/jfrog/jfrog-cli-core/v2/artifactory/commands/commandssummaries/utils.go +++ b/vendor/github.com/jfrog/jfrog-cli-core/v2/artifactory/commands/commandssummaries/utils.go @@ -8,6 +8,8 @@ import ( const ( artifactory7UiFormat = "%sui/repos/tree/General/%s?clearFilter=true" artifactory6UiFormat = "%sartifactory/webapp/#/artifacts/browse/tree/General/%s" + + artifactoryDockerPackagesUiFormat = "%s/ui/packages/docker:%s/sha256__%s" ) func generateArtifactUrl(rtUrl, pathInRt string, majorVersion int) string { diff --git a/vendor/github.com/jfrog/jfrog-cli-core/v2/artifactory/utils/container/buildinfo.go b/vendor/github.com/jfrog/jfrog-cli-core/v2/artifactory/utils/container/buildinfo.go new file mode 100644 index 00000000..a3a30118 --- /dev/null +++ b/vendor/github.com/jfrog/jfrog-cli-core/v2/artifactory/utils/container/buildinfo.go @@ -0,0 +1,489 @@ +package container + +import ( + "encoding/json" + ioutils "github.com/jfrog/gofrog/io" + "os" + "path" + "strings" + + buildinfo "github.com/jfrog/build-info-go/entities" + + artutils "github.com/jfrog/jfrog-cli-core/v2/artifactory/utils" + "github.com/jfrog/jfrog-cli-core/v2/common/build" + "github.com/jfrog/jfrog-client-go/artifactory" + "github.com/jfrog/jfrog-client-go/artifactory/services" + "github.com/jfrog/jfrog-client-go/artifactory/services/utils" + "github.com/jfrog/jfrog-client-go/utils/errorutils" + "github.com/jfrog/jfrog-client-go/utils/io/content" + "github.com/jfrog/jfrog-client-go/utils/log" +) + +const ( + Pull CommandType = "pull" + Push CommandType = "push" + foreignLayerMediaType string = "application/vnd.docker.image.rootfs.foreign.diff.tar.gzip" + imageNotFoundErrorMessage string = "Could not find docker image in Artifactory, expecting image tag: %s" + markerLayerSuffix string = ".marker" + attestationManifestRefType string = "attestation-manifest" + unknownPlatformPlaceholder string = "unknown" + + ManifestJsonFile = "manifest.json" + AttestationsModuleIdPrefix string = "attestations" +) + +// Docker image build info builder. +type Builder interface { + Build(module string) (*buildinfo.BuildInfo, error) + UpdateArtifactsAndDependencies() error + GetLayers() *[]utils.ResultItem +} + +type buildInfoBuilder struct { + image *Image + repositoryDetails RepositoryDetails + buildName string + buildNumber string + project string + serviceManager artifactory.ArtifactoryServicesManager + imageSha2 string + // If true, don't set layers props in Artifactory. + skipTaggingLayers bool + imageLayers []utils.ResultItem +} + +// Create instance of docker build info builder. +func newBuildInfoBuilder(image *Image, repository, buildName, buildNumber, project string, serviceManager artifactory.ArtifactoryServicesManager) (*buildInfoBuilder, error) { + var err error + builder := &buildInfoBuilder{} + builder.repositoryDetails.key = repository + builder.repositoryDetails.isRemote, err = artutils.IsRemoteRepo(repository, serviceManager) + if err != nil { + return nil, err + } + builder.image = image + builder.buildName = buildName + builder.buildNumber = buildNumber + builder.project = project + builder.serviceManager = serviceManager + return builder, nil +} + +type RepositoryDetails struct { + key string + isRemote bool +} + +func (builder *buildInfoBuilder) setImageSha2(imageSha2 string) { + builder.imageSha2 = imageSha2 +} + +func (builder *buildInfoBuilder) GetLayers() *[]utils.ResultItem { + return &builder.imageLayers +} + +func (builder *buildInfoBuilder) getSearchableRepo() string { + if builder.repositoryDetails.isRemote { + return builder.repositoryDetails.key + "-cache" + } + return builder.repositoryDetails.key +} + +// Set build properties on image layers in Artifactory. +func setBuildProperties(buildName, buildNumber, project string, imageLayers []utils.ResultItem, serviceManager artifactory.ArtifactoryServicesManager) (err error) { + props, err := build.CreateBuildProperties(buildName, buildNumber, project) + if err != nil { + return + } + pathToFile, err := writeLayersToFile(imageLayers) + if err != nil { + return + } + reader := content.NewContentReader(pathToFile, content.DefaultKey) + defer ioutils.Close(reader, &err) + _, err = serviceManager.SetProps(services.PropsParams{Reader: reader, Props: props}) + return +} + +// Download the content of layer search result. +func downloadLayer(searchResult utils.ResultItem, result interface{}, serviceManager artifactory.ArtifactoryServicesManager, repo string) error { + // Search results may include artifacts from the remote-cache repository. + // When artifact is expired, it cannot be downloaded from the remote-cache. + // To solve this, change back the search results' repository, to its origin remote/virtual. + searchResult.Repo = repo + return artutils.RemoteUnmarshal(serviceManager, searchResult.GetItemRelativePath(), result) +} + +func writeLayersToFile(layers []utils.ResultItem) (filePath string, err error) { + writer, err := content.NewContentWriter("results", true, false) + if err != nil { + return + } + defer ioutils.Close(writer, &err) + for _, layer := range layers { + writer.Write(layer) + } + filePath = writer.GetFilePath() + return +} + +// Return - manifest artifacts as buildinfo.Artifact struct. +func getManifestArtifact(manifest *utils.ResultItem) (artifact buildinfo.Artifact) { + return buildinfo.Artifact{ + Name: ManifestJsonFile, + Type: "json", + Checksum: buildinfo.Checksum{Sha1: manifest.Actual_Sha1, Md5: manifest.Actual_Md5, Sha256: manifest.Sha256}, + Path: path.Join(manifest.Path, manifest.Name), + OriginalDeploymentRepo: manifest.Repo, + } +} + +// Return - fat manifest artifacts as buildinfo.Artifact struct. +func getFatManifestArtifact(fatManifest *utils.ResultItem) (artifact buildinfo.Artifact) { + return buildinfo.Artifact{ + Name: "list.manifest.json", + Type: "json", + Checksum: buildinfo.Checksum{Sha1: fatManifest.Actual_Sha1, Md5: fatManifest.Actual_Md5, Sha256: fatManifest.Sha256}, + Path: path.Join(fatManifest.Path, fatManifest.Name), + OriginalDeploymentRepo: fatManifest.Repo, + } +} + +// Return - manifest dependency as buildinfo.Dependency struct. +func getManifestDependency(searchResults *utils.ResultItem) (dependency buildinfo.Dependency) { + return buildinfo.Dependency{ + Id: ManifestJsonFile, + Type: "json", + Checksum: buildinfo.Checksum{Sha1: searchResults.Actual_Sha1, Md5: searchResults.Actual_Md5, Sha256: searchResults.Sha256}, + } +} + +// Read the file which contains the following format: 'IMAGE-TAG-IN-ARTIFACTORY'@sha256'SHA256-OF-THE-IMAGE-MANIFEST'. +func GetImageTagWithDigest(filePath string) (*Image, string, error) { + var buildxMetaData buildxMetaData + data, err := os.ReadFile(filePath) + if errorutils.CheckError(err) != nil { + log.Debug("os.ReadFile failed with '%s'\n", err) + return nil, "", err + } + err = json.Unmarshal(data, &buildxMetaData) + if err != nil { + log.Debug("failed unmarshalling buildxMetaData file with error: " + err.Error() + ". falling back to Kanico/OC file format...") + } + // Try to read buildx metadata file. + if buildxMetaData.ImageName != "" && buildxMetaData.ImageSha256 != "" { + return NewImage(buildxMetaData.ImageName), buildxMetaData.ImageSha256, nil + } + // Try read Kaniko/oc file. + splittedData := strings.Split(string(data), `@`) + if len(splittedData) != 2 { + return nil, "", errorutils.CheckErrorf(`unexpected file format "` + filePath + `". The file should include one line in the following format: image-tag@sha256`) + } + tag, sha256 := splittedData[0], strings.Trim(splittedData[1], "\n") + if tag == "" || sha256 == "" { + err = errorutils.CheckErrorf(`missing image-tag/sha256 in file: "` + filePath + `"`) + if err != nil { + return nil, "", err + } + } + return NewImage(tag), sha256, nil +} + +type buildxMetaData struct { + ImageName string `json:"image.name"` + ImageSha256 string `json:"containerimage.digest"` +} + +// Search for manifest digest in fat manifest, which contains specific platforms. +func searchManifestDigest(imageOs, imageArch string, manifestList []ManifestDetails) (digest string) { + for _, manifest := range manifestList { + if manifest.Platform.Os == imageOs && manifest.Platform.Architecture == imageArch { + digest = manifest.Digest + break + } + } + return +} + +// Returns a map of: layer-digest -> layer-search-result +func performSearch(imagePathPattern string, serviceManager artifactory.ArtifactoryServicesManager) (resultMap map[string]*utils.ResultItem, err error) { + searchParams := services.NewSearchParams() + searchParams.CommonParams = &utils.CommonParams{} + searchParams.Pattern = imagePathPattern + var reader *content.ContentReader + reader, err = serviceManager.SearchFiles(searchParams) + if err != nil { + return nil, err + } + defer ioutils.Close(reader, &err) + resultMap = make(map[string]*utils.ResultItem) + for resultItem := new(utils.ResultItem); reader.NextRecord(resultItem) == nil; resultItem = new(utils.ResultItem) { + resultMap[resultItem.Name] = resultItem + } + err = reader.GetError() + return +} + +// Returns a map of: image-sha2 -> image-layers +func performMultiPlatformImageSearch(imagePathPattern string, serviceManager artifactory.ArtifactoryServicesManager) (resultMap map[string][]*utils.ResultItem, err error) { + searchParams := services.NewSearchParams() + searchParams.CommonParams = &utils.CommonParams{} + searchParams.Pattern = imagePathPattern + searchParams.Recursive = true + var reader *content.ContentReader + reader, err = serviceManager.SearchFiles(searchParams) + if err != nil { + return nil, err + } + defer ioutils.Close(reader, &err) + pathToSha2 := make(map[string]string) + pathToImageLayers := make(map[string][]*utils.ResultItem) + resultMap = make(map[string][]*utils.ResultItem) + for resultItem := new(utils.ResultItem); reader.NextRecord(resultItem) == nil; resultItem = new(utils.ResultItem) { + pathToImageLayers[resultItem.Path] = append(pathToImageLayers[resultItem.Path], resultItem) + if resultItem.Name == ManifestJsonFile { + pathToSha2[resultItem.Path] = "sha256:" + resultItem.Sha256 + } + } + for k, v := range pathToSha2 { + resultMap[v] = append(resultMap[v], pathToImageLayers[k]...) + } + err = reader.GetError() + return +} + +// Digest of type sha256:30daa5c11544632449b01f450bebfef6b89644e9e683258ed05797abe7c32a6e to +// sha256__30daa5c11544632449b01f450bebfef6b89644e9e683258ed05797abe7c32a6e +func digestToLayer(digest string) string { + return strings.Replace(digest, ":", "__", 1) +} + +// Get the number of dependencies layers from the config. +func (configLayer *configLayer) getNumberOfDependentLayers() int { + layersNum := len(configLayer.History) + newImageLayers := true + for i := len(configLayer.History) - 1; i >= 0; i-- { + if newImageLayers { + layersNum-- + } + if !newImageLayers && configLayer.History[i].EmptyLayer { + layersNum-- + } + createdBy := configLayer.History[i].CreatedBy + if strings.Contains(createdBy, "ENTRYPOINT") || strings.Contains(createdBy, "MAINTAINER") { + newImageLayers = false + } + } + return layersNum +} + +func removeDuplicateLayers(imageMLayers []layer) []layer { + res := imageMLayers[:0] + // Use map to record duplicates as we find them. + encountered := map[string]bool{} + for _, v := range imageMLayers { + if !encountered[v.Digest] { + res = append(res, v) + encountered[v.Digest] = true + } + } + return res +} + +func toNoneMarkerLayer(layer string) string { + imageId := strings.Replace(layer, "__", ":", 1) + return strings.Replace(imageId, ".marker", "", 1) +} + +type CommandType string + +// Create an image's build info from manifest.json. +func (builder *buildInfoBuilder) createBuildInfo(commandType CommandType, manifest *manifest, candidateLayers map[string]*utils.ResultItem, module string) (*buildinfo.BuildInfo, error) { + if manifest == nil { + return nil, nil + } + imageProperties := map[string]string{ + "docker.image.id": builder.imageSha2, + "docker.image.tag": builder.image.Name(), + } + if module == "" { + var err error + if module, err = builder.image.GetImageShortNameWithTag(); err != nil { + return nil, err + } + } + // Manifest may hold 'empty layers'. As a result, promotion will fail to promote the same layer more than once. + manifest.Layers = removeDuplicateLayers(manifest.Layers) + var artifacts []buildinfo.Artifact + var dependencies []buildinfo.Dependency + var err error + switch commandType { + case Pull: + dependencies = builder.createPullBuildProperties(manifest, candidateLayers) + case Push: + artifacts, dependencies, builder.imageLayers, err = builder.createPushBuildProperties(manifest, candidateLayers) + if err != nil { + return nil, err + } + if !builder.skipTaggingLayers { + if err := setBuildProperties(builder.buildName, builder.buildNumber, builder.project, builder.imageLayers, builder.serviceManager); err != nil { + return nil, err + } + } + } + buildInfo := &buildinfo.BuildInfo{Modules: []buildinfo.Module{{ + Id: module, + Type: buildinfo.Docker, + Properties: imageProperties, + Artifacts: artifacts, + Dependencies: dependencies, + }}} + return buildInfo, nil +} + +// Create the image's build info from list.manifest.json. +func (builder *buildInfoBuilder) createMultiPlatformBuildInfo(fatManifest *FatManifest, searchResultFatManifest *utils.ResultItem, candidateImages map[string][]*utils.ResultItem, baseModuleId string) (*buildinfo.BuildInfo, error) { + imageProperties := map[string]string{ + "docker.image.tag": builder.image.Name(), + } + if baseModuleId == "" { + imageName, err := builder.image.GetImageShortNameWithTag() + if err != nil { + return nil, err + } + baseModuleId = imageName + } + // Add layers. + builder.imageLayers = append(builder.imageLayers, *searchResultFatManifest) + // Create fat-manifest module + buildInfo := &buildinfo.BuildInfo{Modules: []buildinfo.Module{{ + Id: baseModuleId, + Type: buildinfo.Docker, + Properties: imageProperties, + Artifacts: []buildinfo.Artifact{getFatManifestArtifact(searchResultFatManifest)}, + }}} + // Create all image arch modules + for _, manifest := range fatManifest.Manifests { + image := candidateImages[manifest.Digest] + var artifacts []buildinfo.Artifact + for _, layer := range image { + builder.imageLayers = append(builder.imageLayers, *layer) + if layer.Name == ManifestJsonFile { + artifacts = append(artifacts, getManifestArtifact(layer)) + } else { + artifacts = append(artifacts, layer.ToArtifact()) + } + } + buildInfo.Modules = append(buildInfo.Modules, buildinfo.Module{ + Id: getModuleIdByManifest(manifest, baseModuleId), + Type: buildinfo.Docker, + Artifacts: artifacts, + Parent: baseModuleId, + }) + } + return buildInfo, setBuildProperties(builder.buildName, builder.buildNumber, builder.project, builder.imageLayers, builder.serviceManager) +} + +// Construct the manifest's module ID by its type (attestation) or its platform. +func getModuleIdByManifest(manifest ManifestDetails, baseModuleId string) string { + if manifest.Annotations.ReferenceType == attestationManifestRefType { + return path.Join(AttestationsModuleIdPrefix, baseModuleId) + } + if manifest.Platform.Os != unknownPlatformPlaceholder && manifest.Platform.Architecture != unknownPlatformPlaceholder { + return path.Join(manifest.Platform.Os, manifest.Platform.Architecture, baseModuleId) + } + return baseModuleId +} + +func (builder *buildInfoBuilder) createPushBuildProperties(imageManifest *manifest, candidateLayers map[string]*utils.ResultItem) (artifacts []buildinfo.Artifact, dependencies []buildinfo.Dependency, imageLayers []utils.ResultItem, err error) { + // Add artifacts. + artifacts = append(artifacts, getManifestArtifact(candidateLayers[ManifestJsonFile])) + artifacts = append(artifacts, candidateLayers[digestToLayer(builder.imageSha2)].ToArtifact()) + + // Add layers. + imageLayers = append(imageLayers, *candidateLayers[ManifestJsonFile]) + imageLayers = append(imageLayers, *candidateLayers[digestToLayer(builder.imageSha2)]) + + totalLayers := len(imageManifest.Layers) + totalDependencies, err := builder.totalDependencies(candidateLayers[digestToLayer(builder.imageSha2)]) + if err != nil { + return nil, nil, nil, err + } + + // Add image layers as artifacts and dependencies. + for i := 0; i < totalLayers; i++ { + layerFileName := digestToLayer(imageManifest.Layers[i].Digest) + item, layerExists := candidateLayers[layerFileName] + if !layerExists { + err := handleForeignLayer(imageManifest.Layers[i].MediaType, layerFileName) + if err != nil { + return nil, nil, nil, err + } + continue + } + + // Decide if the layer is also a dependency. + if i < totalDependencies { + dependencies = append(dependencies, item.ToDependency()) + } + artifacts = append(artifacts, item.ToArtifact()) + imageLayers = append(imageLayers, *item) + } + return +} + +func (builder *buildInfoBuilder) createPullBuildProperties(imageManifest *manifest, imageLayers map[string]*utils.ResultItem) []buildinfo.Dependency { + configDependencies, err := getDependenciesFromManifestConfig(imageLayers, builder.imageSha2) + if err != nil { + log.Debug(err.Error()) + return nil + } + + layerDependencies, err := getDependenciesFromManifestLayer(imageLayers, imageManifest) + if err != nil { + log.Debug(err.Error()) + return nil + } + + return append(configDependencies, layerDependencies...) +} + +func getDependenciesFromManifestConfig(candidateLayers map[string]*utils.ResultItem, imageSha2 string) ([]buildinfo.Dependency, error) { + var dependencies []buildinfo.Dependency + manifestSearchResults, found := candidateLayers[ManifestJsonFile] + if !found { + return nil, errorutils.CheckErrorf("failed to collect build-info. The manifest.json was not found in Artifactory") + } + + dependencies = append(dependencies, getManifestDependency(manifestSearchResults)) + imageDetails, found := candidateLayers[digestToLayer(imageSha2)] + if !found { + return nil, errorutils.CheckErrorf("failed to collect build-info. Image '" + imageSha2 + "' was not found in Artifactory") + } + + return append(dependencies, imageDetails.ToDependency()), nil +} + +func getDependenciesFromManifestLayer(layers map[string]*utils.ResultItem, imageManifest *manifest) ([]buildinfo.Dependency, error) { + var dependencies []buildinfo.Dependency + for i := 0; i < len(imageManifest.Layers); i++ { + layerFileName := digestToLayer(imageManifest.Layers[i].Digest) + item, layerExists := layers[layerFileName] + if !layerExists { + if err := handleForeignLayer(imageManifest.Layers[i].MediaType, layerFileName); err != nil { + return nil, err + } + continue + } + dependencies = append(dependencies, item.ToDependency()) + } + return dependencies, nil +} + +func (builder *buildInfoBuilder) totalDependencies(image *utils.ResultItem) (int, error) { + configurationLayer := new(configLayer) + if err := downloadLayer(*image, &configurationLayer, builder.serviceManager, builder.repositoryDetails.key); err != nil { + return 0, err + } + return configurationLayer.getNumberOfDependentLayers(), nil +} diff --git a/vendor/github.com/jfrog/jfrog-cli-core/v2/artifactory/utils/container/containermanager.go b/vendor/github.com/jfrog/jfrog-cli-core/v2/artifactory/utils/container/containermanager.go new file mode 100644 index 00000000..e5139b5d --- /dev/null +++ b/vendor/github.com/jfrog/jfrog-cli-core/v2/artifactory/utils/container/containermanager.go @@ -0,0 +1,262 @@ +package container + +import ( + "bytes" + "fmt" + "os" + "os/exec" + "regexp" + "strings" + + "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" +) + +// Search for docker API version format pattern e.g. 1.40 +var ApiVersionRegex = regexp.MustCompile(`^(\d+)\.(\d+)$`) + +// Docker API version 1.31 is compatible with Docker version 17.07.0, according to https://docs.docker.com/engine/api/#api-version-matrix +const MinSupportedApiVersion string = "1.31" + +// Docker login error message +const LoginFailureMessage string = "%s login failed for: %s.\n%s image must be in the form: registry-domain/path-in-repository/image-name:version." + +func NewManager(containerManagerType ContainerManagerType) ContainerManager { + return &containerManager{Type: containerManagerType} +} + +type ContainerManagerType int + +const ( + DockerClient ContainerManagerType = iota + Podman +) + +func (cmt ContainerManagerType) String() string { + return [...]string{"docker", "podman"}[cmt] +} + +// Container image +type ContainerManager interface { + // Image ID is basically the image's SHA256 + Id(image *Image) (string, error) + OsCompatibility(image *Image) (string, string, error) + RunNativeCmd(cmdParams []string) error + GetContainerManagerType() ContainerManagerType +} + +type containerManager struct { + Type ContainerManagerType +} + +type ContainerManagerLoginConfig struct { + ServerDetails *config.ServerDetails +} + +// Run native command of the container buildtool +func (containerManager *containerManager) RunNativeCmd(cmdParams []string) error { + cmd := &nativeCmd{cmdParams: cmdParams, containerManager: containerManager.Type} + return cmd.RunCmd() +} + +// Get image ID +func (containerManager *containerManager) Id(image *Image) (string, error) { + cmd := &getImageIdCmd{image: image, containerManager: containerManager.Type} + content, err := cmd.RunCmd() + if err != nil { + return "", err + } + return strings.Split(content, "\n")[0], nil +} + +// Return the OS and architecture on which the image runs e.g. (linux, amd64, nil). +func (containerManager *containerManager) OsCompatibility(image *Image) (string, string, error) { + cmd := &getImageSystemCompatibilityCmd{image: image, containerManager: containerManager.Type} + log.Debug("Running image inspect...") + content, err := cmd.RunCmd() + if err != nil { + return "", "", err + } + content = strings.Trim(content, "\n") + firstSeparator := strings.Index(content, ",") + if firstSeparator == -1 { + return "", "", errorutils.CheckErrorf("couldn't find OS and architecture of image:" + image.name) + } + return content[:firstSeparator], content[firstSeparator+1:], err +} + +func (containerManager *containerManager) GetContainerManagerType() ContainerManagerType { + return containerManager.Type +} + +// Image push command +type nativeCmd struct { + cmdParams []string + containerManager ContainerManagerType +} + +func (nc *nativeCmd) GetCmd() *exec.Cmd { + return exec.Command(nc.containerManager.String(), nc.cmdParams...) +} + +func (nc *nativeCmd) RunCmd() error { + command := nc.GetCmd() + command.Stderr = os.Stderr + command.Stdout = os.Stderr + if nc.containerManager == DockerClient { + command.Env = append(os.Environ(), "DOCKER_SCAN_SUGGEST=false") + } + return command.Run() +} + +// Image get image id command +type getImageIdCmd struct { + image *Image + containerManager ContainerManagerType +} + +func (getImageId *getImageIdCmd) GetCmd() *exec.Cmd { + var cmd []string + cmd = append(cmd, "images") + cmd = append(cmd, "--format", "{{.ID}}") + cmd = append(cmd, "--no-trunc") + cmd = append(cmd, getImageId.image.name) + return exec.Command(getImageId.containerManager.String(), cmd...) +} + +func (getImageId *getImageIdCmd) RunCmd() (string, error) { + command := getImageId.GetCmd() + buffer := bytes.NewBuffer([]byte{}) + command.Stderr = buffer + command.Stdout = buffer + err := command.Run() + return buffer.String(), err +} + +// Get image system compatibility details +type getImageSystemCompatibilityCmd struct { + image *Image + containerManager ContainerManagerType +} + +func (getImageSystemCompatibilityCmd *getImageSystemCompatibilityCmd) GetCmd() *exec.Cmd { + var cmd []string + cmd = append(cmd, "image") + cmd = append(cmd, "inspect") + cmd = append(cmd, getImageSystemCompatibilityCmd.image.name) + cmd = append(cmd, "--format") + cmd = append(cmd, "{{ .Os}},{{ .Architecture}}") + return exec.Command(getImageSystemCompatibilityCmd.containerManager.String(), cmd...) +} + +func (getImageSystemCompatibilityCmd *getImageSystemCompatibilityCmd) RunCmd() (string, error) { + command := getImageSystemCompatibilityCmd.GetCmd() + buffer := bytes.NewBuffer([]byte{}) + command.Stderr = buffer + command.Stdout = buffer + err := command.Run() + return buffer.String(), err +} + +// Login command +type LoginCmd struct { + DockerRegistry string + Username string + Password string + containerManager ContainerManagerType +} + +func (loginCmd *LoginCmd) GetCmd() *exec.Cmd { + if coreutils.IsWindows() { + return exec.Command("cmd", "/C", "echo", "%CONTAINER_MANAGER_PASS%|", "docker", "login", loginCmd.DockerRegistry, "--username", loginCmd.Username, "--password-stdin") + } + cmd := "echo $CONTAINER_MANAGER_PASS " + fmt.Sprintf(`| `+loginCmd.containerManager.String()+` login %s --username="%s" --password-stdin`, loginCmd.DockerRegistry, loginCmd.Username) + return exec.Command("sh", "-c", cmd) +} + +func (loginCmd *LoginCmd) RunCmd() error { + command := loginCmd.GetCmd() + command.Stderr = os.Stderr + command.Stdout = os.Stderr + command.Env = os.Environ() + command.Env = append(command.Env, "CONTAINER_MANAGER_PASS="+loginCmd.Password) + return command.Run() +} + +// First we'll try to log in assuming a proxy-less tag (e.g. "registry-address/docker-repo/image:ver"). +// If fails, we will try assuming a reverse proxy tag (e.g. "registry-address-docker-repo/image:ver"). +func ContainerManagerLogin(image *Image, config *ContainerManagerLoginConfig, containerManager ContainerManagerType) error { + imageRegistry, err := image.GetRegistry() + if err != nil { + return err + } + username := config.ServerDetails.User + password := config.ServerDetails.Password + // If access-token exists, perform login with it. + if config.ServerDetails.AccessToken != "" { + log.Debug("Using access-token details in " + containerManager.String() + "-login command.") + if username == "" { + username = auth.ExtractUsernameFromAccessToken(config.ServerDetails.AccessToken) + } + password = config.ServerDetails.AccessToken + } + // Perform login. + cmd := &LoginCmd{DockerRegistry: imageRegistry, Username: username, Password: password, containerManager: containerManager} + err = cmd.RunCmd() + if exitCode := coreutils.GetExitCode(err, 0, 0, false); exitCode == coreutils.ExitCodeNoError { + // Login succeeded + return nil + } + log.Debug(containerManager.String()+" login while assuming proxy-less failed:", err) + indexOfSlash := strings.Index(imageRegistry, "/") + if indexOfSlash < 0 { + return errorutils.CheckErrorf(LoginFailureMessage, containerManager.String(), imageRegistry, containerManager.String()) + } + cmd = &LoginCmd{DockerRegistry: imageRegistry[:indexOfSlash], Username: config.ServerDetails.User, Password: config.ServerDetails.Password} + err = cmd.RunCmd() + if err != nil { + // Login failed for both attempts + return errorutils.CheckErrorf(LoginFailureMessage, + containerManager.String(), fmt.Sprintf("%s, %s", imageRegistry, imageRegistry[:indexOfSlash]), containerManager.String()+" "+err.Error()) + } + // Login succeeded + return nil +} + +// Version command +// Docker-client provides an API for interacting with the Docker daemon. This cmd should be used for docker client only. +type VersionCmd struct{} + +func (versionCmd *VersionCmd) GetCmd() *exec.Cmd { + var cmd []string + cmd = append(cmd, "docker") + cmd = append(cmd, "version") + cmd = append(cmd, "--format", "{{.Client.APIVersion}}") + return exec.Command(cmd[0], cmd[1:]...) +} + +func (versionCmd *VersionCmd) RunCmd() (string, error) { + command := versionCmd.GetCmd() + buffer := bytes.NewBuffer([]byte{}) + command.Stderr = buffer + command.Stdout = buffer + err := command.Run() + return buffer.String(), err +} + +func ValidateClientApiVersion() error { + cmd := &VersionCmd{} + // 'docker version' may return 1 in case of errors from daemon. We should ignore this kind of errors. + content, err := cmd.RunCmd() + content = strings.TrimSpace(content) + if !ApiVersionRegex.Match([]byte(content)) { + // The Api version is expected to be 'major.minor'. Anything else should return an error. + log.Error("The Docker client Api version is expected to be 'major.minor'. The actual output is:", content) + return errorutils.CheckError(err) + } + return utils.ValidateMinimumVersion(utils.DockerApi, content, MinSupportedApiVersion) +} diff --git a/vendor/github.com/jfrog/jfrog-cli-core/v2/artifactory/utils/container/image.go b/vendor/github.com/jfrog/jfrog-cli-core/v2/artifactory/utils/container/image.go new file mode 100644 index 00000000..7737ca21 --- /dev/null +++ b/vendor/github.com/jfrog/jfrog-cli-core/v2/artifactory/utils/container/image.go @@ -0,0 +1,156 @@ +package container + +import ( + "net/http" + "path" + "strings" + + "github.com/jfrog/jfrog-client-go/artifactory" + "github.com/jfrog/jfrog-client-go/utils/errorutils" + "github.com/jfrog/jfrog-client-go/utils/log" +) + +type Image struct { + // Image name includes the registry domain, image base name and image tag e.g.: my-registry:port/docker-local/hello-world:latest. + name string +} + +func NewImage(imageTag string) *Image { + return &Image{name: imageTag} +} + +// Get image name +func (image *Image) Name() string { + return image.name +} + +// Get image name from tag by removing the prefixed registry hostname. +// e.g.: https://my-registry/docker-local/hello-world:latest. -> docker-local/hello-world:latest +func (image *Image) GetImageLongNameWithTag() (string, error) { + if err := image.validateTag(); err != nil { + return "", err + } + indexOfLastSlash := strings.Index(image.name, "/") + indexOfLastColon := strings.LastIndex(image.name, ":") + if indexOfLastColon < 0 || indexOfLastColon < indexOfLastSlash { + log.Info("The image '" + image.name + "' does not include tag. Using the 'latest' tag.") + image.name += ":latest" + } + return image.name[indexOfLastSlash+1:], nil +} + +// Get image base name by removing the prefixed registry hostname and the tag. +// e.g.: https://my-registry/docker-local/hello-world:latest. -> docker-local/hello-world +func (image *Image) GetImageLongName() (string, error) { + imageName, err := image.GetImageLongNameWithTag() + if err != nil { + return "", err + } + tagIndex := strings.Index(imageName, ":") + return imageName[:tagIndex], nil +} + +func (image *Image) validateTag() error { + if !strings.Contains(image.name, "/") { + return errorutils.CheckErrorf("The image '%s' is missing '/' which indicates the image name/tag", image.name) + } + return nil +} + +// Get image base name by removing the prefixed registry hostname and the tag. +// e.g.: https://my-registry/docker-local/hello-world:latest. -> hello-world +func (image *Image) GetImageShortName() (string, error) { + imageName, err := image.GetImageShortNameWithTag() + if err != nil { + return "", err + } + tagIndex := strings.LastIndex(imageName, ":") + if tagIndex != -1 { + return imageName[:tagIndex], nil + } + return imageName, nil +} + +// Get image base name by removing the prefixed registry hostname. +// e.g.: https://my-registry/docker-local/hello-world:latest. -> hello-world:latest +func (image *Image) GetImageShortNameWithTag() (string, error) { + imageName, err := image.GetImageLongNameWithTag() + if err != nil { + return "", err + } + indexOfSlash := strings.LastIndex(imageName, "/") + if indexOfSlash != -1 { + return imageName[indexOfSlash+1:], nil + + } + return imageName, nil +} + +// Get image tag name of an image. +// e.g.: https://my-registry/docker-local/hello-world:latest. -> latest +func (image *Image) GetImageTag() (string, error) { + imageName, err := image.GetImageLongNameWithTag() + if err != nil { + return "", err + } + tagIndex := strings.Index(imageName, ":") + if tagIndex == -1 { + return "", errorutils.CheckErrorf("unexpected image name '%s'. Failed to get image tag.", image.Name()) + } + return imageName[tagIndex+1:], nil +} + +func (image *Image) GetRegistry() (string, error) { + if err := image.validateTag(); err != nil { + return "", err + } + indexOfLastSlash := strings.Index(image.name, "/") + if indexOfLastSlash == -1 { + return "", errorutils.CheckErrorf("unexpected image name '%s'. Failed to get registry.", image.Name()) + } + return image.name[:indexOfLastSlash], nil +} + +// Returns the physical Artifactory repository name of the pulled/pushed image, by reading a response header from Artifactory. +func (image *Image) GetRemoteRepo(serviceManager artifactory.ArtifactoryServicesManager) (string, error) { + containerRegistryUrl, err := image.GetRegistry() + if err != nil { + return "", err + } + longImageName, err := image.GetImageLongName() + if err != nil { + return "", err + } + imageTag, err := image.GetImageTag() + if err != nil { + return "", err + } + var isSecure bool + if rtUrl := serviceManager.GetConfig().GetServiceDetails().GetUrl(); strings.HasPrefix(rtUrl, "https") { + isSecure = true + } + // Build the request URL. + endpoint := buildRequestUrl(longImageName, imageTag, containerRegistryUrl, isSecure) + artHttpDetails := serviceManager.GetConfig().GetServiceDetails().CreateHttpClientDetails() + artHttpDetails.Headers["accept"] = "application/vnd.docker.distribution.manifest.v1+prettyjws, application/json, application/vnd.oci.image.manifest.v1+json, application/vnd.docker.distribution.manifest.v2+json, application/vnd.docker.distribution.manifest.list.v2+json, application/vnd.oci.image.index.v1+json" + resp, _, err := serviceManager.Client().SendHead(endpoint, &artHttpDetails) + if err != nil { + return "", err + } + if resp.StatusCode != http.StatusOK { + return "", errorutils.CheckErrorf("error while getting docker repository name. Artifactory response: " + resp.Status) + } + if dockerRepo := resp.Header["X-Artifactory-Docker-Registry"]; len(dockerRepo) != 0 { + return dockerRepo[0], nil + } + return "", errorutils.CheckErrorf("couldn't find 'X-Artifactory-Docker-Registry' header docker repository in artifactory") +} + +// Returns the name of the repository containing the image in Artifactory. +func buildRequestUrl(longImageName, imageTag, containerRegistryUrl string, https bool) string { + endpoint := path.Join(containerRegistryUrl, "v2", longImageName, "manifests", imageTag) + if https { + return "https://" + endpoint + } + return "http://" + endpoint +} diff --git a/vendor/github.com/jfrog/jfrog-cli-core/v2/artifactory/utils/container/localagent.go b/vendor/github.com/jfrog/jfrog-cli-core/v2/artifactory/utils/container/localagent.go new file mode 100644 index 00000000..9987dfc7 --- /dev/null +++ b/vendor/github.com/jfrog/jfrog-cli-core/v2/artifactory/utils/container/localagent.go @@ -0,0 +1,196 @@ +package container + +import ( + "fmt" + "net/http" + "path" + "strings" + + buildinfo "github.com/jfrog/build-info-go/entities" + "github.com/jfrog/jfrog-client-go/artifactory" + "github.com/jfrog/jfrog-client-go/artifactory/services/utils" + "github.com/jfrog/jfrog-client-go/utils/errorutils" + "github.com/jfrog/jfrog-client-go/utils/log" +) + +// Build-info builder for local agents tools such as: Docker or Podman. +type localAgentbuildInfoBuilder struct { + buildInfoBuilder *buildInfoBuilder + // Name of the container CLI tool e.g. docker + containerManager ContainerManager + commandType CommandType +} + +// Create new build info builder container CLI tool +func NewLocalAgentBuildInfoBuilder(image *Image, repository, buildName, buildNumber, project string, serviceManager artifactory.ArtifactoryServicesManager, commandType CommandType, containerManager ContainerManager) (*localAgentbuildInfoBuilder, error) { + imageSha2, err := containerManager.Id(image) + if err != nil { + return nil, err + } + builder, err := newBuildInfoBuilder(image, repository, buildName, buildNumber, project, serviceManager) + if err != nil { + return nil, err + } + builder.setImageSha2(imageSha2) + return &localAgentbuildInfoBuilder{ + buildInfoBuilder: builder, + containerManager: containerManager, + commandType: commandType, + }, err +} + +func (labib *localAgentbuildInfoBuilder) GetLayers() *[]utils.ResultItem { + return &labib.buildInfoBuilder.imageLayers +} + +func (labib *localAgentbuildInfoBuilder) SetSkipTaggingLayers(skipTaggingLayers bool) { + labib.buildInfoBuilder.skipTaggingLayers = skipTaggingLayers +} + +// Create build-info for a docker image. +func (labib *localAgentbuildInfoBuilder) Build(module string) (*buildinfo.BuildInfo, error) { + // Search for image build-info. + candidateLayers, manifest, err := labib.searchImage() + if err != nil { + log.Warn("Failed to collect build-info. No layer(s) was found for image:'" + labib.buildInfoBuilder.image.name + "'. Hint, try to delete the image from the local cache and rerun the command") + log.Debug(err.Error()) + return nil, nil + } else { + log.Debug("Found manifest.json with the following layers to create build-info:", candidateLayers) + } + // Create build-info from search results. + return labib.buildInfoBuilder.createBuildInfo(labib.commandType, manifest, candidateLayers, module) +} + +// Search an image in Artifactory and validate its sha2 with local image. +func (labib *localAgentbuildInfoBuilder) searchImage() (map[string]*utils.ResultItem, *manifest, error) { + longImageName, err := labib.buildInfoBuilder.image.GetImageLongNameWithTag() + if err != nil { + return nil, nil, err + } + imagePath := strings.Replace(longImageName, ":", "/", 1) + manifestPathsCandidates := getManifestPaths(imagePath, labib.buildInfoBuilder.getSearchableRepo(), labib.commandType) + log.Debug("Start searching for image manifest.json") + for _, path := range manifestPathsCandidates { + log.Debug(`Searching in:"` + path + `"`) + resultMap, err := labib.search(path) + if err != nil { + return nil, nil, err + } + manifest, err := getManifest(resultMap, labib.buildInfoBuilder.serviceManager, labib.buildInfoBuilder.repositoryDetails.key) + if err != nil { + return nil, nil, err + } + if manifest != nil && labib.isVerifiedManifest(manifest) { + return resultMap, manifest, nil + } + } + return nil, nil, errorutils.CheckErrorf(imageNotFoundErrorMessage, labib.buildInfoBuilder.image.name) +} + +// Search image layers in artifactory by the provided image path in artifactory. +// If fat-manifest is found, use it to find our image in Artifactory. +func (labib *localAgentbuildInfoBuilder) search(imagePathPattern string) (resultMap map[string]*utils.ResultItem, err error) { + resultMap, err = performSearch(imagePathPattern, labib.buildInfoBuilder.serviceManager) + if err != nil { + log.Debug("Failed to search marker layer. Error:", err.Error()) + return + } + // Validate there are no .marker layers. + totalDownloaded, err := downloadMarkerLayersToRemoteCache(resultMap, labib.buildInfoBuilder) + if err != nil { + log.Debug("Failed to download marker layer. Error:", err.Error()) + return nil, err + } + if totalDownloaded > 0 { + // Search again after .marker layer were downloaded. + if resultMap, err = performSearch(imagePathPattern, labib.buildInfoBuilder.serviceManager); err != nil { + log.Debug("Failed to research layers after download marker layers. Error:", err.Error()) + return + } + } + // Check if search results contain multi-architecture images (fat-manifest). + if searchResult, ok := resultMap["list.manifest.json"]; labib.commandType == Pull && ok { + // In case of a fat-manifest, Artifactory will create two folders. + // One folder named as the image tag, which contains the fat manifest. + // The second folder, named as image's manifest digest, contains the image layers and the image's manifest. + log.Debug("Found list.manifest.json (fat-manifest). Searching for the image manifest digest in list.manifest.json") + var digest string + digest, err = labib.getImageDigestFromFatManifest(*searchResult) + if err == nil && digest != "" { + // Remove tag from pattern, place the manifest digest instead. + imagePathPattern = strings.Replace(imagePathPattern, "/*", "", 1) + imagePathPattern = path.Join(imagePathPattern[:strings.LastIndex(imagePathPattern, "/")], strings.Replace(digest, ":", "__", 1), "*") + // Retry search. + return labib.search(imagePathPattern) + } + log.Debug("Couldn't find matching digest in list.manifest.json") + } + return resultMap, err +} + +// Verify manifest by comparing sha256, which references to the image digest. If there is no match, return nil. +func (labib *localAgentbuildInfoBuilder) isVerifiedManifest(imageManifest *manifest) bool { + if imageManifest.Config.Digest != labib.buildInfoBuilder.imageSha2 { + log.Debug(`Found incorrect manifest.json file. Expects digest "` + labib.buildInfoBuilder.imageSha2 + `" found "` + imageManifest.Config.Digest) + return false + } + return true +} + +func (labib *localAgentbuildInfoBuilder) getImageDigestFromFatManifest(fatManifest utils.ResultItem) (string, error) { + var fatManifestContent *FatManifest + if err := downloadLayer(fatManifest, &fatManifestContent, labib.buildInfoBuilder.serviceManager, labib.buildInfoBuilder.repositoryDetails.key); err != nil { + log.Debug(`failed to unmarshal fat-manifest`) + return "", err + } + imageOs, imageArch, err := labib.containerManager.OsCompatibility(labib.buildInfoBuilder.image) + if err != nil { + return "", err + } + return searchManifestDigest(imageOs, imageArch, fatManifestContent.Manifests), nil +} + +// When a client tries to pull an image from a remote repository in Artifactory and the client has some the layers cached locally on the disk, +// then Artifactory will not download these layers into the remote repository cache. Instead, it will mark the layer artifacts with .marker suffix files in the remote cache. +// This function download all the marker layers into the remote cache repository. +func downloadMarkerLayersToRemoteCache(resultMap map[string]*utils.ResultItem, builder *buildInfoBuilder) (int, error) { + if !builder.repositoryDetails.isRemote || len(resultMap) == 0 { + return 0, nil + } + totalDownloaded := 0 + remoteRepo := builder.repositoryDetails.key + imageName, err := builder.image.GetImageShortName() + if err != nil { + return 0, err + } + clientDetails := builder.serviceManager.GetConfig().GetServiceDetails().CreateHttpClientDetails() + // Search for marker layers + for _, layerData := range resultMap { + if strings.HasSuffix(layerData.Name, markerLayerSuffix) { + log.Debug(fmt.Sprintf("Downloading %s layer into remote repository cache...", layerData.Name)) + baseUrl := builder.serviceManager.GetConfig().GetServiceDetails().GetUrl() + endpoint := "api/docker/" + remoteRepo + "/v2/" + imageName + "/blobs/" + toNoneMarkerLayer(layerData.Name) + resp, body, err := builder.serviceManager.Client().SendHead(baseUrl+endpoint, &clientDetails) + if err != nil { + log.Debug("Failed to download marker layer. Error:", err.Error()) + return totalDownloaded, err + } + if err = errorutils.CheckResponseStatusWithBody(resp, body, http.StatusOK); err != nil { + log.Debug("Failed to download marker layer. HTTP stats code:", resp.StatusCode) + return totalDownloaded, err + } + totalDownloaded++ + } + } + return totalDownloaded, nil +} + +func handleForeignLayer(layerMediaType, layerFileName string) error { + // Allow missing layer to be of a foreign type. + if layerMediaType == foreignLayerMediaType { + log.Info(fmt.Sprintf("Foreign layer: %s is missing in Artifactory and therefore will not be added to the build-info.", layerFileName)) + return nil + } + return errorutils.CheckErrorf("Could not find layer: " + layerFileName + " in Artifactory") +} diff --git a/vendor/github.com/jfrog/jfrog-cli-core/v2/artifactory/utils/container/manifest.go b/vendor/github.com/jfrog/jfrog-cli-core/v2/artifactory/utils/container/manifest.go new file mode 100644 index 00000000..210d5916 --- /dev/null +++ b/vendor/github.com/jfrog/jfrog-cli-core/v2/artifactory/utils/container/manifest.go @@ -0,0 +1,98 @@ +package container + +import ( + "path" + "strings" + + "github.com/jfrog/jfrog-client-go/artifactory" + "github.com/jfrog/jfrog-client-go/artifactory/services/utils" +) + +// To unmarshal config layer file +type configLayer struct { + History []history `json:"history,omitempty"` +} + +type history struct { + Created string `json:"created,omitempty"` + CreatedBy string `json:"created_by,omitempty"` + EmptyLayer bool `json:"empty_layer,omitempty"` +} + +// To unmarshal manifest.json file +type manifest struct { + Config manifestConfig `json:"config,omitempty"` + Layers []layer `json:"layers,omitempty"` +} + +type manifestConfig struct { + Digest string `json:"digest,omitempty"` +} + +type layer struct { + Digest string `json:"digest,omitempty"` + MediaType string `json:"mediaType,omitempty"` +} + +type FatManifest struct { + Manifests []ManifestDetails `json:"manifests"` +} + +type ManifestDetails struct { + Digest string `json:"digest"` + Platform Platform `json:"platform"` + Annotations Annotations `json:"annotations"` +} + +type Platform struct { + Architecture string `json:"architecture"` + Os string `json:"os"` +} + +// Annotations for attestation manifests. +type Annotations struct { + ReferenceDigest string `json:"vnd.docker.reference.digest"` + ReferenceType string `json:"vnd.docker.reference.type"` +} + +// Return all the search patterns in which manifest can be found. +func getManifestPaths(imagePath, repo string, commandType CommandType) []string { + // pattern 1: reverse proxy e.g. ecosysjfrog-docker-local.jfrog.io. + paths := []string{path.Join(repo, imagePath, "*")} + // pattern 2: proxy-less e.g. orgab.jfrog.team/docker-local. + endOfRepoNameIndex := strings.Index(imagePath[1:], "/") + proxylessTag := imagePath[endOfRepoNameIndex+1:] + paths = append(paths, path.Join(repo, proxylessTag, "*")) + // If image path includes more than 3 slashes, Artifactory doesn't store this image under 'library', thus we should not look further. + if commandType != Push && strings.Count(imagePath, "/") <= 3 { + // pattern 3: reverse proxy - this time with 'library' as part of the path. + paths = append(paths, path.Join(repo, "library", imagePath, "*")) + // pattern 4: Assume proxy-less - this time with 'library' as part of the path. + paths = append(paths, path.Join(repo, "library", proxylessTag, "*")) + } + return paths +} + +func getManifest(resultMap map[string]*utils.ResultItem, serviceManager artifactory.ArtifactoryServicesManager, repo string) (imageManifest *manifest, err error) { + if len(resultMap) == 0 { + return + } + manifestSearchResult, ok := resultMap["manifest.json"] + if !ok { + return + } + err = downloadLayer(*manifestSearchResult, &imageManifest, serviceManager, repo) + return +} + +func getFatManifest(resultMap map[string]*utils.ResultItem, serviceManager artifactory.ArtifactoryServicesManager, repo string) (imageFatManifest *FatManifest, err error) { + if len(resultMap) == 0 { + return + } + manifestSearchResult, ok := resultMap["list.manifest.json"] + if !ok { + return + } + err = downloadLayer(*manifestSearchResult, &imageFatManifest, serviceManager, repo) + return +} diff --git a/vendor/github.com/jfrog/jfrog-cli-core/v2/artifactory/utils/container/remoteagent.go b/vendor/github.com/jfrog/jfrog-cli-core/v2/artifactory/utils/container/remoteagent.go new file mode 100644 index 00000000..a808ca02 --- /dev/null +++ b/vendor/github.com/jfrog/jfrog-cli-core/v2/artifactory/utils/container/remoteagent.go @@ -0,0 +1,125 @@ +package container + +import ( + "strings" + + buildinfo "github.com/jfrog/build-info-go/entities" + "github.com/jfrog/jfrog-client-go/artifactory" + "github.com/jfrog/jfrog-client-go/artifactory/services/utils" + "github.com/jfrog/jfrog-client-go/utils/errorutils" + "github.com/jfrog/jfrog-client-go/utils/log" +) + +// Build-info builder for remote agents tools such as: Kaniko, OpenShift CLI (oc), or buildx. +type RemoteAgentBuildInfoBuilder struct { + buildInfoBuilder *buildInfoBuilder + manifestSha2 string +} + +func NewRemoteAgentBuildInfoBuilder(image *Image, repository, buildName, buildNumber, project string, serviceManager artifactory.ArtifactoryServicesManager, manifestSha256 string) (*RemoteAgentBuildInfoBuilder, error) { + builder, err := newBuildInfoBuilder(image, repository, buildName, buildNumber, project, serviceManager) + return &RemoteAgentBuildInfoBuilder{ + buildInfoBuilder: builder, + manifestSha2: manifestSha256, + }, err +} + +func (rabib *RemoteAgentBuildInfoBuilder) GetLayers() *[]utils.ResultItem { + return &rabib.buildInfoBuilder.imageLayers +} + +func (rabib *RemoteAgentBuildInfoBuilder) Build(module string) (*buildinfo.BuildInfo, error) { + // Search for and image in Artifactory. + results, err := rabib.searchImage() + if err != nil { + return nil, err + } + // Create build-info based on image manifest. + if results["manifest.json"] != nil { + searchResults, manifest, err := rabib.handleManifest(results) + if err != nil { + return nil, err + } + return rabib.buildInfoBuilder.createBuildInfo(Push, manifest, searchResults, module) + } + // Create build-info based on image fat-manifest. + multiPlatformImages, fatManifestDetails, fatManifest, err := rabib.handleFatManifestImage(results) + if err != nil { + return nil, err + } + return rabib.buildInfoBuilder.createMultiPlatformBuildInfo(fatManifest, fatManifestDetails, multiPlatformImages, module) +} + +// Search for image manifest and layers in Artifactory. +func (rabib *RemoteAgentBuildInfoBuilder) handleManifest(resultMap map[string]*utils.ResultItem) (map[string]*utils.ResultItem, *manifest, error) { + if manifest, ok := resultMap["manifest.json"]; ok { + err := rabib.isVerifiedManifest(manifest) + if err != nil { + return nil, nil, err + + } + manifest, err := getManifest(resultMap, rabib.buildInfoBuilder.serviceManager, rabib.buildInfoBuilder.repositoryDetails.key) + if err != nil { + return nil, nil, err + } + // // Manifest may hold 'empty layers'. As a result, promotion will fail to promote the same layer more than once. + rabib.buildInfoBuilder.imageSha2 = manifest.Config.Digest + log.Debug("Found manifest.json. Proceeding to create build-info.") + return resultMap, manifest, nil + } + return nil, nil, errorutils.CheckErrorf(`couldn't find image "` + rabib.buildInfoBuilder.image.name + `" manifest in Artifactory`) +} + +func (rabib *RemoteAgentBuildInfoBuilder) handleFatManifestImage(results map[string]*utils.ResultItem) (map[string][]*utils.ResultItem, *utils.ResultItem, *FatManifest, error) { + if fatManifestResult, ok := results["list.manifest.json"]; ok { + log.Debug("Found list.manifest.json. Proceeding to create build-info.") + fatManifestRootPath := getFatManifestRoot(fatManifestResult.GetItemRelativeLocation()) + "/*" + fatManifest, err := getFatManifest(results, rabib.buildInfoBuilder.serviceManager, rabib.buildInfoBuilder.repositoryDetails.key) + if err != nil { + return nil, nil, nil, err + } + multiPlatformImages, err := performMultiPlatformImageSearch(fatManifestRootPath, rabib.buildInfoBuilder.serviceManager) + return multiPlatformImages, fatManifestResult, fatManifest, err + } + return nil, nil, nil, errorutils.CheckErrorf(`couldn't find image "` + rabib.buildInfoBuilder.image.name + `" fat manifest in Artifactory`) +} + +// Search image manifest or fat-manifest of and image. +func (rabib *RemoteAgentBuildInfoBuilder) searchImage() (resultMap map[string]*utils.ResultItem, err error) { + longImageName, err := rabib.buildInfoBuilder.image.GetImageLongNameWithTag() + if err != nil { + return nil, err + } + imagePath := strings.Replace(longImageName, ":", "/", 1) + + // Search image's manifest. + manifestPathsCandidates := getManifestPaths(imagePath, rabib.buildInfoBuilder.getSearchableRepo(), Push) + log.Debug("Start searching for image manifest.json") + for _, path := range manifestPathsCandidates { + log.Debug(`Searching in:"` + path + `"`) + resultMap, err = performSearch(path, rabib.buildInfoBuilder.serviceManager) + if err != nil { + return nil, err + } + if resultMap == nil { + continue + } + if resultMap["list.manifest.json"] != nil || resultMap["manifest.json"] != nil { + return resultMap, nil + } + } + return nil, errorutils.CheckErrorf(imageNotFoundErrorMessage, rabib.buildInfoBuilder.image.name) +} + +// Verify manifest's sha256. If there is no match, return nil. +func (rabib *RemoteAgentBuildInfoBuilder) isVerifiedManifest(imageManifest *utils.ResultItem) error { + if imageManifest.GetProperty("docker.manifest.digest") != rabib.manifestSha2 { + return errorutils.CheckErrorf(`Found incorrect manifest.json file. Expects digest "` + rabib.manifestSha2 + `" found "` + imageManifest.GetProperty("docker.manifest.digest")) + } + return nil +} + +func getFatManifestRoot(fatManifestPath string) string { + fatManifestPath = strings.TrimSuffix(fatManifestPath, "/") + return fatManifestPath[:strings.LastIndex(fatManifestPath, "/")] +} diff --git a/vendor/github.com/jfrog/jfrog-cli-core/v2/utils/coreutils/coreconsts.go b/vendor/github.com/jfrog/jfrog-cli-core/v2/utils/coreutils/coreconsts.go index 6f9ec2da..a10643e7 100644 --- a/vendor/github.com/jfrog/jfrog-cli-core/v2/utils/coreutils/coreconsts.go +++ b/vendor/github.com/jfrog/jfrog-cli-core/v2/utils/coreutils/coreconsts.go @@ -44,11 +44,13 @@ const ( LogTimestamp = "JFROG_CLI_LOG_TIMESTAMP" ReportUsage = "JFROG_CLI_REPORT_USAGE" DependenciesDir = "JFROG_CLI_DEPENDENCIES_DIR" - TransitiveDownload = "JFROG_CLI_TRANSITIVE_DOWNLOAD_EXPERIMENTAL" FailNoOp = "JFROG_CLI_FAIL_NO_OP" OutputDirPathEnv = "JFROG_CLI_COMMAND_SUMMARY_OUTPUT_DIR" CI = "CI" ServerID = "JFROG_CLI_SERVER_ID" + TransitiveDownload = "JFROG_CLI_TRANSITIVE_DOWNLOAD" + // Deprecated and replaced with TransitiveDownload + TransitiveDownloadExperimental = "JFROG_CLI_TRANSITIVE_DOWNLOAD_EXPERIMENTAL" ) // Although these vars are constant, they are defined inside a vars section and not a constants section because the tests modify these values. diff --git a/vendor/modules.txt b/vendor/modules.txt index 718c8526..b0d96b03 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -219,12 +219,13 @@ github.com/jfrog/gofrog/parallel github.com/jfrog/gofrog/stringutils github.com/jfrog/gofrog/unarchive github.com/jfrog/gofrog/version -# github.com/jfrog/jfrog-cli-core/v2 v2.55.1 +# github.com/jfrog/jfrog-cli-core/v2 v2.55.3 ## explicit; go 1.22.3 github.com/jfrog/jfrog-cli-core/v2/artifactory/commands/commandssummaries github.com/jfrog/jfrog-cli-core/v2/artifactory/commands/generic 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/artifactory/utils/container github.com/jfrog/jfrog-cli-core/v2/commandsummary github.com/jfrog/jfrog-cli-core/v2/common/build github.com/jfrog/jfrog-cli-core/v2/common/format