diff --git a/artifactory/commands/container/dockerfileutils/parser.go b/artifactory/commands/container/dockerfileutils/parser.go index c3a00734..b17059ef 100644 --- a/artifactory/commands/container/dockerfileutils/parser.go +++ b/artifactory/commands/container/dockerfileutils/parser.go @@ -47,7 +47,7 @@ func newDockerfileParser() *dockerfileParser { // - FROM --platform=linux/amd64 ubuntu:20.04 // - FROM --platform=linux/amd64 ubuntu:20.04 AS builder // - FROM builder (skipped - references previous stage) -func ParseDockerfileBaseImages(dockerfilePath string) ([]ocicontainer.BaseImage, error) { +func ParseDockerfileBaseImages(dockerfilePath string) ([]ocicontainer.DockerImage, error) { file, err := os.Open(dockerfilePath) if err != nil { return nil, err @@ -103,8 +103,8 @@ func readDockerfileLines(file *os.File) ([]string, error) { } // extractBaseImages processes all lines and extracts base images from FROM instructions -func (p *dockerfileParser) extractBaseImages(lines []string) []ocicontainer.BaseImage { - var baseImages []ocicontainer.BaseImage +func (p *dockerfileParser) extractBaseImages(lines []string) []ocicontainer.DockerImage { + var baseImages []ocicontainer.DockerImage for _, line := range lines { if !isFromInstruction(line) { @@ -130,7 +130,7 @@ func (p *dockerfileParser) extractBaseImages(lines []string) []ocicontainer.Base } p.seenImages[fromInfo.image] = true - baseImages = append(baseImages, ocicontainer.BaseImage{ + baseImages = append(baseImages, ocicontainer.DockerImage{ Image: fromInfo.image, OS: fromInfo.os, Architecture: fromInfo.arch, diff --git a/artifactory/commands/container/pull.go b/artifactory/commands/container/pull.go index 6c6005fc..07a8c382 100644 --- a/artifactory/commands/container/pull.go +++ b/artifactory/commands/container/pull.go @@ -6,6 +6,7 @@ import ( "github.com/jfrog/jfrog-cli-core/v2/common/build" "github.com/jfrog/jfrog-cli-core/v2/utils/config" "github.com/jfrog/jfrog-client-go/utils/errorutils" + "github.com/jfrog/jfrog-client-go/utils/log" ) type PullCommand struct { @@ -76,7 +77,12 @@ func (pc *PullCommand) Run() error { if err != nil || buildInfoModule == nil { return err } - return build.SaveBuildInfo(buildName, buildNumber, project, buildInfoModule) + err = build.SaveBuildInfo(buildName, buildNumber, project, buildInfoModule) + if err != nil { + return err + } + log.Info("Successfully Saved build info for " + buildName + "/" + buildNumber) + return nil } func (pc *PullCommand) CommandName() string { diff --git a/artifactory/commands/helm/utils_test.go b/artifactory/commands/helm/utils_test.go index d10599e3..ef6e908b 100644 --- a/artifactory/commands/helm/utils_test.go +++ b/artifactory/commands/helm/utils_test.go @@ -600,9 +600,9 @@ func TestAppendModuleInExistingBuildInfo(t *testing.T) { }, } moduleToAdd := &entities.Module{ - Id: "test:1.0.0", + Id: "test:1.0.0", Dependencies: []entities.Dependency{}, - Artifacts: []entities.Artifact{}, + Artifacts: []entities.Artifact{}, } appendModuleInExistingBuildInfo(buildInfo, moduleToAdd) assert.Len(t, buildInfo.Modules, 1) diff --git a/artifactory/commands/ocicontainer/docker_artifacts.go b/artifactory/commands/ocicontainer/docker_artifacts.go new file mode 100644 index 00000000..5aa9f91c --- /dev/null +++ b/artifactory/commands/ocicontainer/docker_artifacts.go @@ -0,0 +1,97 @@ +package ocicontainer + +import ( + "fmt" + "github.com/jfrog/jfrog-cli-artifactory/artifactory/commands/repository" + "github.com/jfrog/jfrog-client-go/artifactory" + "github.com/jfrog/jfrog-client-go/utils/errorutils" + + buildinfo "github.com/jfrog/build-info-go/entities" + "github.com/jfrog/jfrog-client-go/artifactory/services/utils" + "github.com/jfrog/jfrog-client-go/utils/log" +) + +type DockerArtifactsBuilder struct { + serviceManager artifactory.ArtifactoryServicesManager + isImagePushed bool + imageTag string + repositoryDetails *DockerRepositoryDetails +} + +// NewDockerArtifactsBuilder creates a new builder for docker build command +func NewDockerArtifactsBuilder(serviceManager artifactory.ArtifactoryServicesManager, imageTag string, isImagePushed bool) *DockerArtifactsBuilder { + return &DockerArtifactsBuilder{ + serviceManager: serviceManager, + isImagePushed: isImagePushed, + imageTag: imageTag, + } +} + +// getArtifacts collects artifacts for the pushed image +func (dab *DockerArtifactsBuilder) getArtifacts() (artifacts []buildinfo.Artifact, leadSha string, resultsToApplyProps []utils.ResultItem, err error) { + var resultItems []utils.ResultItem + if !dab.isImagePushed { + log.Debug("Image was not pushed, skipping artifact collection!") + return + } + if dab.isImagePushed { + leadSha, resultItems, resultsToApplyProps, err = dab.collectDetailsForPushedImage(dab.imageTag) + if err != nil { + return artifacts, leadSha, resultsToApplyProps, err + } + artifacts = dab.createArtifactsFromResults(resultItems) + } + return artifacts, leadSha, resultsToApplyProps, err +} + +// collectDetailsForPushedImage collects layer details for a pushed image +func (dab *DockerArtifactsBuilder) collectDetailsForPushedImage(imageRef string) (string, []utils.ResultItem, []utils.ResultItem, error) { + log.Debug(fmt.Sprintf("Building artifacts for the pushed image %s", dab.imageTag)) + remoteRepo, dockerManifestType, leadSha, err := GetRemoteRepoAndManifestTypeWithLeadSha(imageRef, dab.serviceManager) + if err != nil { + return "", []utils.ResultItem{}, []utils.ResultItem{}, err + } + searchableRepository, repositoryDetails, err := GetSearchableRepositoryAndDetails(remoteRepo, dab.serviceManager) + if err != nil { + return "", []utils.ResultItem{}, []utils.ResultItem{}, err + } + dab.repositoryDetails = repositoryDetails + layers, resultsToApplyProps, err := NewDockerManifestHandler(dab.serviceManager).FetchLayersOfPushedImage(imageRef, searchableRepository, dockerManifestType) + if err != nil { + return "", []utils.ResultItem{}, []utils.ResultItem{}, errorutils.CheckError(err) + } + log.Debug(fmt.Sprintf("Collected %d layers, %d folders for props", len(layers), len(resultsToApplyProps))) + return leadSha, layers, resultsToApplyProps, err +} + +// createArtifactsFromResults converts search results to artifacts +func (dab *DockerArtifactsBuilder) createArtifactsFromResults(results []utils.ResultItem) []buildinfo.Artifact { + deduplicated := deduplicateResultsBySha256(results) + artifacts := make([]buildinfo.Artifact, 0, len(deduplicated)) + for _, result := range deduplicated { + artifacts = append(artifacts, result.ToArtifact()) + } + return artifacts +} + +// GetOriginalDeploymentRepo returns the repository where the image was pushed +func (dab *DockerArtifactsBuilder) GetOriginalDeploymentRepo() string { + if dab.repositoryDetails == nil { + return "" + } + if dab.repositoryDetails.RepoType == repository.Virtual { + return dab.repositoryDetails.DefaultDeploymentRepo + } + return dab.repositoryDetails.Key +} + +// filterLayersFromVirtualRepo filters layers to only include those from the pushed repository +func filterLayersFromVirtualRepo(items []utils.ResultItem, pushedRepo string) []utils.ResultItem { + filteredLayers := make([]utils.ResultItem, 0, len(items)) + for _, item := range items { + if item.Repo == pushedRepo { + filteredLayers = append(filteredLayers, item) + } + } + return filteredLayers +} diff --git a/artifactory/commands/ocicontainer/docker_build.go b/artifactory/commands/ocicontainer/docker_build.go index ca1d31bc..22e38e26 100644 --- a/artifactory/commands/ocicontainer/docker_build.go +++ b/artifactory/commands/ocicontainer/docker_build.go @@ -17,41 +17,33 @@ import ( // DockerBuildInfoBuilder is a simplified builder for docker build command type DockerBuildInfoBuilder struct { - buildName string - buildNumber string - project string - module string - serviceManager artifactory.ArtifactoryServicesManager - imageTag string - baseImages []BaseImage - isImagePushed bool - cmdArgs []string - repositoryDetails dockerRepositoryDetails - searchableLayerForApplyingProps []utils.ResultItem + buildName string + buildNumber string + project string + module string + serviceManager artifactory.ArtifactoryServicesManager + imageTag string + baseImages []DockerImage + isImagePushed bool + cmdArgs []string } -type dockerRepositoryDetails struct { +type DockerRepositoryDetails struct { Key string `json:"key"` RepoType string `json:"rclass"` DefaultDeploymentRepo string `json:"defaultDeploymentRepo"` } -type BaseImage struct { - Image string - OS string - Architecture string -} - -type manifestType string +type ManifestType string const ( - ManifestList manifestType = "list.manifest.json" - Manifest manifestType = "manifest.json" + ManifestList ManifestType = "list.manifest.json" + Manifest ManifestType = "manifest.json" ) // NewDockerBuildInfoBuilder creates a new builder for docker build command func NewDockerBuildInfoBuilder(buildName, buildNumber, project string, module string, serviceManager artifactory.ArtifactoryServicesManager, - imageTag string, baseImages []BaseImage, isImagePushed bool, cmdArgs []string) *DockerBuildInfoBuilder { + imageTag string, baseImages []DockerImage, isImagePushed bool, cmdArgs []string) *DockerBuildInfoBuilder { biImage := NewImage(imageTag) @@ -79,21 +71,23 @@ func NewDockerBuildInfoBuilder(buildName, buildNumber, project string, module st // Build orchestrates the collection of dependencies and artifacts for the docker build func (dbib *DockerBuildInfoBuilder) Build() error { + log.Debug(fmt.Sprintf("Starting docker build-info collection for %s/%s", dbib.buildName, dbib.buildNumber)) if err := build.SaveBuildGeneralDetails(dbib.buildName, dbib.buildNumber, dbib.project); err != nil { return err } - dependencies, err := dbib.getDependencies() + dependencies, err := NewDockerDependenciesBuilder(dbib.baseImages, dbib.serviceManager).getDependencies() if err != nil { log.Warn(fmt.Sprintf("Failed to get dependencies for '%s'. Error: %v", dbib.buildName, err)) } - artifacts, leadSha, err := dbib.getArtifacts() + artifactBuilder := NewDockerArtifactsBuilder(dbib.serviceManager, dbib.imageTag, dbib.isImagePushed) + artifacts, leadSha, resultsToApplyProps, err := artifactBuilder.getArtifacts() if err != nil { log.Warn(fmt.Sprintf("Failed to get artifacts for '%s'. Error: %v", dbib.buildName, err)) } - err = dbib.applyBuildProps(dbib.searchableLayerForApplyingProps) + err = dbib.applyBuildProps(resultsToApplyProps, artifactBuilder.GetOriginalDeploymentRepo()) if err != nil { log.Warn(fmt.Sprintf("Failed to apply build prop. Error: %v", err)) } @@ -108,6 +102,7 @@ func (dbib *DockerBuildInfoBuilder) Build() error { Artifacts: artifacts, }}} + log.Debug(fmt.Sprintf("Saving build info for %s/%s", dbib.buildName, dbib.buildNumber)) if err = build.SaveBuildInfo(dbib.buildName, dbib.buildNumber, dbib.project, buildInfo); err != nil { return errorutils.CheckErrorf("failed to save build info for '%s/%s': %s", dbib.buildName, dbib.buildNumber, err.Error()) } @@ -116,12 +111,15 @@ func (dbib *DockerBuildInfoBuilder) Build() error { } // applyBuildProps applies build properties to the artifacts -func (dbib *DockerBuildInfoBuilder) applyBuildProps(items []utils.ResultItem) (err error) { +func (dbib *DockerBuildInfoBuilder) applyBuildProps(items []utils.ResultItem, pushedRepo string) (err error) { props, err := build.CreateBuildProperties(dbib.buildName, dbib.buildNumber, dbib.project) if err != nil { return } - pushedRepo := dbib.getPushedRepo() + if pushedRepo == "" { + log.Warn("Pushed repository is empty, skipping applying build properties.") + return nil + } filteredLayers := filterLayersFromVirtualRepo(items, pushedRepo) if len(filteredLayers) == 0 { log.Debug(fmt.Sprintf("Filtered layers length is 0 after filtering with pushedRepo: %s, All layers: %v", pushedRepo, items)) diff --git a/artifactory/commands/ocicontainer/docker_build_artifacts.go b/artifactory/commands/ocicontainer/docker_build_artifacts.go deleted file mode 100644 index 3b7afc42..00000000 --- a/artifactory/commands/ocicontainer/docker_build_artifacts.go +++ /dev/null @@ -1,66 +0,0 @@ -package ocicontainer - -import ( - "fmt" - - buildinfo "github.com/jfrog/build-info-go/entities" - "github.com/jfrog/jfrog-client-go/artifactory/services/utils" - "github.com/jfrog/jfrog-client-go/utils/log" -) - -// getArtifacts collects artifacts for the pushed image -func (dbib *DockerBuildInfoBuilder) getArtifacts() (artifacts []buildinfo.Artifact, leadSha string, err error) { - var resultItems []utils.ResultItem - if dbib.isImagePushed { - log.Debug(fmt.Sprintf("Building artifacts for the pushed image %s", dbib.imageTag)) - leadSha, resultItems, err = dbib.collectDetailsForPushedImage(dbib.imageTag) - if err != nil { - return artifacts, leadSha, err - } - artifacts = dbib.createArtifactsFromResults(resultItems) - } - return artifacts, leadSha, err -} - -// collectDetailsForPushedImage collects layer details for a pushed image -func (dbib *DockerBuildInfoBuilder) collectDetailsForPushedImage(imageRef string) (string, []utils.ResultItem, error) { - remoteRepo, dockerManifestType, leadSha, err := dbib.getRemoteRepoAndManifestTypeWithLeadSha(imageRef) - if err != nil { - return "", []utils.ResultItem{}, err - } - searchableRepository, err := dbib.getSearchableRepository(remoteRepo) - if err != nil { - return "", []utils.ResultItem{}, err - } - layers, err := dbib.fetchLayersOfPushedImage(imageRef, searchableRepository, dockerManifestType) - return leadSha, layers, err -} - -// createArtifactsFromResults converts search results to artifacts -func (dbib *DockerBuildInfoBuilder) createArtifactsFromResults(results []utils.ResultItem) []buildinfo.Artifact { - deduplicated := deduplicateResultsBySha256(results) - artifacts := make([]buildinfo.Artifact, 0, len(deduplicated)) - for _, result := range deduplicated { - artifacts = append(artifacts, result.ToArtifact()) - } - return artifacts -} - -// getPushedRepo returns the repository where the image was pushed -func (dbib *DockerBuildInfoBuilder) getPushedRepo() string { - if dbib.repositoryDetails.RepoType == "virtual" { - return dbib.repositoryDetails.DefaultDeploymentRepo - } - return dbib.repositoryDetails.Key -} - -// filterLayersFromVirtualRepo filters layers to only include those from the pushed repository -func filterLayersFromVirtualRepo(items []utils.ResultItem, pushedRepo string) []utils.ResultItem { - filteredLayers := make([]utils.ResultItem, 0, len(items)) - for _, item := range items { - if item.Repo == pushedRepo { - filteredLayers = append(filteredLayers, item) - } - } - return filteredLayers -} diff --git a/artifactory/commands/ocicontainer/docker_build_dependencies.go b/artifactory/commands/ocicontainer/docker_build_dependencies.go deleted file mode 100644 index 312b27e3..00000000 --- a/artifactory/commands/ocicontainer/docker_build_dependencies.go +++ /dev/null @@ -1,122 +0,0 @@ -package ocicontainer - -import ( - "errors" - "fmt" - "sync" - - buildinfo "github.com/jfrog/build-info-go/entities" - "github.com/jfrog/jfrog-client-go/artifactory/services/utils" -) - -// getDependencies collects dependencies for all base images in parallel -func (dbib *DockerBuildInfoBuilder) getDependencies() ([]buildinfo.Dependency, error) { - var wg sync.WaitGroup - errChan := make(chan error, len(dbib.baseImages)) - dependencyResultChan := make(chan []utils.ResultItem, len(dbib.baseImages)) - - for _, baseImage := range dbib.baseImages { - wg.Add(1) - go func(img BaseImage) { - defer wg.Done() - resultItems, err := dbib.collectDetailsForBaseImage(img) - if err != nil { - errChan <- err - return - } - dependencyResultChan <- resultItems - }(baseImage) - } - - wg.Wait() - close(errChan) - close(dependencyResultChan) - - var errorList []error - for err := range errChan { - errorList = append(errorList, err) - } - - if len(errorList) > 0 { - return []buildinfo.Dependency{}, fmt.Errorf("errors occurred during build info collection: %v", errors.Join(errorList...)) - } - - var allDependencyResultItems []utils.ResultItem - for resultItems := range dependencyResultChan { - allDependencyResultItems = append(allDependencyResultItems, resultItems...) - } - - return dbib.createDependenciesFromResults(allDependencyResultItems), nil -} - -// collectDetailsForBaseImage collects layer details for a single base image -func (dbib *DockerBuildInfoBuilder) collectDetailsForBaseImage(baseImage BaseImage) ([]utils.ResultItem, error) { - remoteRepo, dockerManifestType, _, err := dbib.getRemoteRepoAndManifestTypeWithLeadSha(baseImage.Image) - if err != nil { - return []utils.ResultItem{}, err - } - manifestSha, err := dbib.manifestDetails(baseImage) - if err != nil { - return []utils.ResultItem{}, err - } - searchableRepository, err := dbib.getSearchableRepository(remoteRepo) - if err != nil { - return []utils.ResultItem{}, err - } - - image := NewImage(baseImage.Image) - imageTag, err := image.GetImageTag() - if err != nil { - return []utils.ResultItem{}, err - } - imageName, err := image.GetImageShortName() - if err != nil { - return []utils.ResultItem{}, err - } - - // Use interface to get base path, then apply repo-type modifications - handler := dbib.getManifestHandler(dockerManifestType) - basePath := handler.BuildSearchPaths(imageName, imageTag, manifestSha) - layerPaths := dbib.applyRepoTypeModifications(basePath) - - layers, err := dbib.searchForImageLayersInPath(imageName, searchableRepository, layerPaths) - if err != nil { - return []utils.ResultItem{}, err - } - - if dbib.repositoryDetails.RepoType == "remote" { - var markerLayers []string - markerLayers, layers = getMarkerLayerShasFromSearchResult(layers) - markerLayersDetails := handleMarkerLayersForDockerBuild(markerLayers, dbib.serviceManager, dbib.repositoryDetails.Key, imageName) - layers = append(layers, markerLayersDetails...) - } - - return layers, nil -} - -// applyRepoTypeModifications applies repository-type-specific path modifications -func (dbib *DockerBuildInfoBuilder) applyRepoTypeModifications(basePath string) []string { - // for remote repositories, the image path is prefixed with "library/" - if dbib.repositoryDetails.RepoType == "remote" { - return []string{modifyPathForRemoteRepo(basePath)} - } - - // virtual repository can contain remote repository and local repository - // multi-platform images are stored in local under folders like sha256:xyz format - // but in remote it's stored in folders like library/sha256__xyz format - if dbib.repositoryDetails.RepoType == "virtual" { - return append([]string{modifyPathForRemoteRepo(basePath)}, basePath) - } - - return []string{basePath} -} - -// createDependenciesFromResults converts search results to dependencies -func (dbib *DockerBuildInfoBuilder) createDependenciesFromResults(results []utils.ResultItem) []buildinfo.Dependency { - deduplicated := deduplicateResultsBySha256(results) - dependencies := make([]buildinfo.Dependency, 0, len(deduplicated)) - for _, result := range deduplicated { - dependencies = append(dependencies, result.ToDependency()) - } - return dependencies -} diff --git a/artifactory/commands/ocicontainer/docker_build_manifest.go b/artifactory/commands/ocicontainer/docker_build_manifest.go deleted file mode 100644 index 3a4400cc..00000000 --- a/artifactory/commands/ocicontainer/docker_build_manifest.go +++ /dev/null @@ -1,151 +0,0 @@ -package ocicontainer - -import ( - "fmt" - - "github.com/google/go-containerregistry/pkg/authn" - "github.com/google/go-containerregistry/pkg/name" - "github.com/google/go-containerregistry/pkg/v1/remote" - "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" -) - -// ManifestHandler interface for handling different manifest types -type ManifestHandler interface { - // FetchLayers fetches layers for a pushed image from Artifactory - FetchLayers(imageRef, repository string) ([]utils.ResultItem, error) - // BuildSearchPaths builds the search path for base image layers - BuildSearchPaths(imageName, imageTag, manifestDigest string) string -} - -// SingleManifestHandler handles single manifest images -type SingleManifestHandler struct { - builder *DockerBuildInfoBuilder -} - -// FatManifestHandler handles fat manifest (multi-platform) images -type FatManifestHandler struct { - builder *DockerBuildInfoBuilder -} - -// getManifestHandler returns the appropriate handler based on manifest type -func (dbib *DockerBuildInfoBuilder) getManifestHandler(dockerManifestType manifestType) ManifestHandler { - switch dockerManifestType { - case ManifestList: - return &FatManifestHandler{builder: dbib} - case Manifest: - return &SingleManifestHandler{builder: dbib} - default: - return nil - } -} - -// fetchLayersOfPushedImage dispatches to the appropriate manifest handler -func (dbib *DockerBuildInfoBuilder) fetchLayersOfPushedImage(imageRef, repository string, dockerManifestType manifestType) ([]utils.ResultItem, error) { - handler := dbib.getManifestHandler(dockerManifestType) - if handler == nil { - return []utils.ResultItem{}, errorutils.CheckErrorf("unknown/other manifest type provided: %s", dockerManifestType) - } - return handler.FetchLayers(imageRef, repository) -} - -// SINGLE MANIFEST HANDLER IMPLEMENTATION - -// FetchLayers fetches layers for a single manifest image -func (h *SingleManifestHandler) FetchLayers(imageRef string, repository string) ([]utils.ResultItem, error) { - image := NewImage(imageRef) - imageTag, err := image.GetImageTag() - if err != nil { - return []utils.ResultItem{}, err - } - imageName, err := image.GetImageShortName() - if err != nil { - return []utils.ResultItem{}, err - } - expectedImagePath := imageName + "/" + imageTag - h.builder.searchableLayerForApplyingProps = append(h.builder.searchableLayerForApplyingProps, utils.ResultItem{ - Repo: repository, - Path: expectedImagePath, - Type: "folder", - }) - layers, err := h.builder.searchArtifactoryForFilesByPath(repository, []string{expectedImagePath}) - if err != nil { - return []utils.ResultItem{}, err - } - return layers, nil -} - -// BuildSearchPaths returns the search path for a single manifest image (imageName/imageTag) -func (h *SingleManifestHandler) BuildSearchPaths(imageName, imageTag, manifestDigest string) string { - return fmt.Sprintf("%s/%s", imageName, imageTag) -} - -// FAT MANIFEST HANDLER IMPLEMENTATION - -// FetchLayers fetches layers for a fat manifest (multi-platform) image -func (h *FatManifestHandler) FetchLayers(imageRef string, repository string) ([]utils.ResultItem, error) { - ref, err := name.ParseReference(imageRef) - if err != nil { - return []utils.ResultItem{}, fmt.Errorf("parsing reference %s: %w", imageRef, err) - } - manifestShas := h.getManifestShaListForImage(ref) - return h.getLayersForManifestSha(imageRef, manifestShas, repository) -} - -// BuildSearchPaths returns the search path for a fat manifest image (imageName/sha256:xxx) -func (h *FatManifestHandler) BuildSearchPaths(imageName, imageTag, manifestDigest string) string { - return fmt.Sprintf("%s/%s", imageName, manifestDigest) -} - -// getManifestShaListForImage retrieves all platform manifest SHAs from a fat manifest -func (h *FatManifestHandler) getManifestShaListForImage(imageReference name.Reference) []string { - index, err := remote.Index(imageReference, remote.WithAuthFromKeychain(authn.DefaultKeychain)) - if err != nil { - log.Warn(fmt.Sprintf("Failed to get image index for image: %s. Error: %s", imageReference.Name(), err.Error())) - return []string{} - } - manifestList, err := index.IndexManifest() - if err != nil { - log.Warn(fmt.Sprintf("Failed to get manifest list for image: %s. Error: %s", imageReference.Name(), err.Error())) - return []string{} - } - manifestShas := make([]string, 0, len(manifestList.Manifests)) - for _, descriptor := range manifestList.Manifests { - manifestShas = append(manifestShas, descriptor.Digest.String()) - } - return manifestShas -} - -// getLayersForManifestSha searches for layers across all manifest SHAs -func (h *FatManifestHandler) getLayersForManifestSha(imageRef string, manifestShas []string, repository string) ([]utils.ResultItem, error) { - searchablePathForManifest := h.createSearchablePathForDockerManifestContents(imageRef, manifestShas) - - for _, path := range searchablePathForManifest { - h.builder.searchableLayerForApplyingProps = append(h.builder.searchableLayerForApplyingProps, utils.ResultItem{ - Repo: repository, - Path: path, - Type: "folder", - }) - } - - layers, err := h.builder.searchArtifactoryForFilesByPath(repository, searchablePathForManifest) - if err != nil { - return []utils.ResultItem{}, err - } - return layers, nil -} - -// createSearchablePathForDockerManifestContents builds search paths like imageName/sha256:xxx -func (h *FatManifestHandler) createSearchablePathForDockerManifestContents(imageRef string, manifestShas []string) []string { - imageName, err := NewImage(imageRef).GetImageShortName() - if err != nil { - log.Warn(fmt.Sprintf("Failed to get image name: %s. Error: %s while creating searchable paths for docker manifest contents.", imageRef, err.Error())) - return []string{} - } - searchablePaths := make([]string, 0, len(manifestShas)) - for _, manifestSha := range manifestShas { - searchablePaths = append(searchablePaths, fmt.Sprintf("%s/%s", imageName, manifestSha)) - } - return searchablePaths -} diff --git a/artifactory/commands/ocicontainer/docker_dependencies.go b/artifactory/commands/ocicontainer/docker_dependencies.go new file mode 100644 index 00000000..f2bd9a83 --- /dev/null +++ b/artifactory/commands/ocicontainer/docker_dependencies.go @@ -0,0 +1,183 @@ +package ocicontainer + +import ( + "errors" + "fmt" + "github.com/jfrog/jfrog-cli-artifactory/artifactory/commands/repository" + "strings" + "sync" + + 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/log" +) + +type DockerDependenciesBuilder struct { + dockerImages []DockerImage + serviceManager artifactory.ArtifactoryServicesManager +} + +func NewDockerDependenciesBuilder(dockerImages []DockerImage, serviceManager artifactory.ArtifactoryServicesManager) *DockerDependenciesBuilder { + return &DockerDependenciesBuilder{ + dockerImages: dockerImages, + serviceManager: serviceManager, + } +} + +// getDependencies collects dependencies for all base images in parallel +func (ddp *DockerDependenciesBuilder) getDependencies() ([]buildinfo.Dependency, error) { + var wg sync.WaitGroup + errChan := make(chan error, len(ddp.dockerImages)) + dependencyResultChan := make(chan []utils.ResultItem, len(ddp.dockerImages)) + + for _, baseImage := range ddp.dockerImages { + wg.Add(1) + go func(img DockerImage) { + defer wg.Done() + resultItems, err := ddp.collectDetailsForBaseImage(img) + if err != nil { + errChan <- err + return + } + dependencyResultChan <- resultItems + }(baseImage) + } + + wg.Wait() + close(errChan) + close(dependencyResultChan) + + var errorList []error + for err := range errChan { + errorList = append(errorList, err) + } + + if len(errorList) > 0 { + return []buildinfo.Dependency{}, fmt.Errorf("errors occurred during build info collection: %v", errors.Join(errorList...)) + } + + var allDependencyResultItems []utils.ResultItem + for resultItems := range dependencyResultChan { + allDependencyResultItems = append(allDependencyResultItems, resultItems...) + } + + return ddp.createDependenciesFromResults(allDependencyResultItems), nil +} + +// collectDetailsForBaseImage collects layer details for a single base image +func (ddp *DockerDependenciesBuilder) collectDetailsForBaseImage(baseImage DockerImage) ([]utils.ResultItem, error) { + log.Debug(fmt.Sprintf("Collecting details for image: %s", baseImage.Image)) + + image := NewImage(baseImage.Image) + imageTag, err := image.GetImageTag() + if err != nil { + return []utils.ResultItem{}, err + } + imageName, err := image.GetImageShortName() + if err != nil { + return []utils.ResultItem{}, err + } + + // Handle digest-based images (sha256:xxx) - skip unnecessary API calls + if strings.HasPrefix(imageTag, "sha256:") { + return ddp.collectDetailsForDigestBasedImage(image, imageName, imageTag) + } + + // Tag-based image flow + remoteRepo, dockerManifestType, _, err := GetRemoteRepoAndManifestTypeWithLeadSha(baseImage.Image, ddp.serviceManager) + if err != nil { + return []utils.ResultItem{}, err + } + manifestSha, err := baseImage.GetManifestDetails() + if err != nil { + return []utils.ResultItem{}, err + } + searchableRepository, repositoryDetails, err := GetSearchableRepositoryAndDetails(remoteRepo, ddp.serviceManager) + if err != nil { + return []utils.ResultItem{}, err + } + log.Debug(fmt.Sprintf("SearchableRepository: %s, Type: %s", remoteRepo, repositoryDetails.RepoType)) + + // Use interface to get base path, then apply repo-type modifications + handler := NewDockerManifestHandler(ddp.serviceManager).GetManifestHandler(dockerManifestType) + basePath := handler.BuildSearchPaths(imageName, imageTag, manifestSha) + layerPaths := ddp.applyRepoTypeModifications(basePath, *repositoryDetails) + + layers, err := SearchForImageLayersInPath(imageName, searchableRepository, layerPaths, ddp.serviceManager) + if err != nil { + return []utils.ResultItem{}, err + } + + if repositoryDetails.RepoType == repository.Remote { + var markerLayers []string + markerLayers, layers = getMarkerLayerShasFromSearchResult(layers) + markerLayersDetails := handleMarkerLayersForDockerBuild(markerLayers, ddp.serviceManager, repositoryDetails.Key, imageName) + layers = append(layers, markerLayersDetails...) + } + + return layers, nil +} + +// collectDetailsForDigestBasedImage handles images pulled by digest (@sha256:xxx) +func (ddp *DockerDependenciesBuilder) collectDetailsForDigestBasedImage(image *Image, imageName, digest string) ([]utils.ResultItem, error) { + log.Debug(fmt.Sprintf("Collecting details for digest-based image: %s", image.Name())) + + remoteRepo, err := image.GetRemoteRepo(ddp.serviceManager) + if err != nil { + return []utils.ResultItem{}, err + } + searchableRepository, repositoryDetails, err := GetSearchableRepositoryAndDetails(remoteRepo, ddp.serviceManager) + if err != nil { + return []utils.ResultItem{}, err + } + log.Debug(fmt.Sprintf("SearchableRepository: %s, Type: %s", searchableRepository, repositoryDetails.RepoType)) + + // Find manifest path by digest property using AQL + manifestPath, err := SearchManifestPathByDigest(searchableRepository, digest, ddp.serviceManager) + if err != nil { + return []utils.ResultItem{}, err + } + log.Debug(fmt.Sprintf("Found manifest at path: %s", manifestPath)) + + layers, err := SearchForImageLayersInPath(imageName, searchableRepository, []string{manifestPath}, ddp.serviceManager) + if err != nil { + return []utils.ResultItem{}, err + } + + if repositoryDetails.RepoType == repository.Remote { + var markerLayers []string + markerLayers, layers = getMarkerLayerShasFromSearchResult(layers) + markerLayersDetails := handleMarkerLayersForDockerBuild(markerLayers, ddp.serviceManager, repositoryDetails.Key, imageName) + layers = append(layers, markerLayersDetails...) + } + + return layers, nil +} + +// applyRepoTypeModifications applies repository-type-specific path modifications +func (ddp *DockerDependenciesBuilder) applyRepoTypeModifications(basePath string, repositoryDetails DockerRepositoryDetails) []string { + // for remote repositories, the image path is prefixed with "library/" + if repositoryDetails.RepoType == repository.Remote { + return []string{modifyPathForRemoteRepo(basePath)} + } + + // virtual repository can contain remote repository and local repository + // multi-platform images are stored in local under folders like sha256:xyz format + // but in remote it's stored in folders like library/sha256__xyz format + if repositoryDetails.RepoType == repository.Virtual { + return append([]string{modifyPathForRemoteRepo(basePath)}, basePath) + } + + return []string{basePath} +} + +// createDependenciesFromResults converts search results to dependencies +func (ddp *DockerDependenciesBuilder) createDependenciesFromResults(results []utils.ResultItem) []buildinfo.Dependency { + deduplicated := deduplicateResultsBySha256(results) + dependencies := make([]buildinfo.Dependency, 0, len(deduplicated)) + for _, result := range deduplicated { + dependencies = append(dependencies, result.ToDependency()) + } + return dependencies +} diff --git a/artifactory/commands/ocicontainer/docker_manifest.go b/artifactory/commands/ocicontainer/docker_manifest.go new file mode 100644 index 00000000..9203e213 --- /dev/null +++ b/artifactory/commands/ocicontainer/docker_manifest.go @@ -0,0 +1,236 @@ +package ocicontainer + +import ( + "fmt" + v1 "github.com/google/go-containerregistry/pkg/v1" + "github.com/jfrog/jfrog-client-go/artifactory" + "runtime" + + "github.com/google/go-containerregistry/pkg/authn" + "github.com/google/go-containerregistry/pkg/name" + "github.com/google/go-containerregistry/pkg/v1/remote" + "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" +) + +const ( + osDarwin = "darwin" + osLinux = "linux" +) + +type DockerImage struct { + Image string + OS string + Architecture string +} + +// GetManifestDetails gets the manifest SHA for a base image, considering platform (OS/architecture) +func (baseImage DockerImage) GetManifestDetails() (string, error) { + imageRef := baseImage.Image + ref, err := name.ParseReference(imageRef) + if err != nil { + return "", fmt.Errorf("parsing reference %s: %w", imageRef, err) + } + var osName, osArch string + + if baseImage.OS != "" && baseImage.Architecture != "" { + osName = baseImage.OS + osArch = baseImage.Architecture + } else { + osName = runtime.GOOS + if osName == osDarwin { + osName = osLinux + } + osArch = runtime.GOARCH + } + + remoteImage, err := remote.Image(ref, remote.WithAuthFromKeychain(authn.DefaultKeychain), remote.WithPlatform(v1.Platform{OS: osName, Architecture: osArch})) + if err != nil { + return "", errorutils.CheckError(err) + } + if remoteImage == nil { + return "", fmt.Errorf("error fetching manifest for %s", imageRef) + } + + manifestShaDigest, err := remoteImage.Digest() + if err != nil { + return "", fmt.Errorf("error getting manifest digest for %s: %w", imageRef, err) + } + return manifestShaDigest.String(), nil +} + +func GetManifestTypeAndLeadSha(imageRef string) (ManifestType, string, error) { + ref, err := name.ParseReference(imageRef) + if err != nil { + return "", "", errorutils.CheckErrorf("parsing reference %s: %w", imageRef, err) + } + desc, err := remote.Head(ref, remote.WithAuthFromKeychain(authn.DefaultKeychain)) + if err != nil { + return "", "", errorutils.CheckErrorf("getting remote manifest for %s: %w", imageRef, err) + } + log.Debug(fmt.Sprintf("Manifest type: %s, digest: %s", desc.MediaType, desc.Digest.Hex)) + + if desc.MediaType.IsIndex() { + return ManifestList, desc.Digest.Hex, nil + } else if desc.MediaType.IsImage() { + return Manifest, desc.Digest.Hex, nil + } + return "", "", errorutils.CheckErrorf("unsupported manifest media type: %s", desc.MediaType) +} + +// ManifestHandler interface for handling different manifest types +type ManifestHandler interface { + // FetchLayers fetches layers for a pushed image from Artifactory, also return folders, whose elements are eligible to apply props on + FetchLayers(imageRef, repository string) ([]utils.ResultItem, []utils.ResultItem, error) + // BuildSearchPaths builds the search path for base image layers + BuildSearchPaths(imageName, imageTag, manifestDigest string) string +} + +type DockerManifestHandler struct { + serviceManager artifactory.ArtifactoryServicesManager +} + +// SingleManifestHandler handles single manifest images +type SingleManifestHandler struct { + *DockerManifestHandler +} + +// FatManifestHandler handles fat manifest (multi-platform) images +type FatManifestHandler struct { + *DockerManifestHandler +} + +func NewDockerManifestHandler(serviceManager artifactory.ArtifactoryServicesManager) *DockerManifestHandler { + return &DockerManifestHandler{serviceManager: serviceManager} +} + +// GetManifestHandler returns the appropriate handler based on manifest type +func (dmh *DockerManifestHandler) GetManifestHandler(dockerManifestType ManifestType) ManifestHandler { + switch dockerManifestType { + case ManifestList: + return &FatManifestHandler{dmh} + case Manifest: + return &SingleManifestHandler{dmh} + default: + return nil + } +} + +// FetchLayersOfPushedImage dispatches to the appropriate manifest handler +func (dmh *DockerManifestHandler) FetchLayersOfPushedImage(imageRef, repository string, dockerManifestType ManifestType) ([]utils.ResultItem, []utils.ResultItem, error) { + log.Debug(fmt.Sprintf("Fetching layers for the pushed image %s", imageRef)) + handler := dmh.GetManifestHandler(dockerManifestType) + if handler == nil { + return []utils.ResultItem{}, []utils.ResultItem{}, + errorutils.CheckErrorf("unknown/other manifest type provided: %s", dockerManifestType) + } + return handler.FetchLayers(imageRef, repository) +} + +// SINGLE MANIFEST HANDLER IMPLEMENTATION + +// FetchLayers fetches layers for a single manifest image +func (h *SingleManifestHandler) FetchLayers(imageRef string, repository string) ([]utils.ResultItem, []utils.ResultItem, error) { + log.Debug(fmt.Sprintf("Fetching layers for single manifest image: %s", imageRef)) + var folderToApplyProps []utils.ResultItem + image := NewImage(imageRef) + imageTag, err := image.GetImageTag() + if err != nil { + return []utils.ResultItem{}, []utils.ResultItem{}, err + } + imageName, err := image.GetImageShortName() + if err != nil { + return []utils.ResultItem{}, []utils.ResultItem{}, err + } + expectedImagePath := imageName + "/" + imageTag + folderToApplyProps = append(folderToApplyProps, utils.ResultItem{ + Repo: repository, + Path: expectedImagePath, + Type: "folder", + }) + layers, err := searchArtifactoryForFilesByPath(repository, []string{expectedImagePath}, h.serviceManager) + if err != nil { + return []utils.ResultItem{}, folderToApplyProps, err + } + log.Debug(fmt.Sprintf("Found %d layers at path %s", len(layers), expectedImagePath)) + return layers, folderToApplyProps, nil +} + +// BuildSearchPaths returns the search path for a single manifest image (imageName/imageTag) +func (h *SingleManifestHandler) BuildSearchPaths(imageName, imageTag, manifestDigest string) string { + return fmt.Sprintf("%s/%s", imageName, imageTag) +} + +// FAT MANIFEST HANDLER IMPLEMENTATION + +// FetchLayers fetches layers for a fat manifest (multi-platform) image +func (h *FatManifestHandler) FetchLayers(imageRef string, repository string) ([]utils.ResultItem, []utils.ResultItem, error) { + log.Debug(fmt.Sprintf("Fetching layers for fat manifest image: %s", imageRef)) + ref, err := name.ParseReference(imageRef) + if err != nil { + return []utils.ResultItem{}, []utils.ResultItem{}, fmt.Errorf("parsing reference %s: %w", imageRef, err) + } + manifestShas, err := h.getManifestShaListForImage(ref) + if err != nil { + return []utils.ResultItem{}, []utils.ResultItem{}, err + } + return h.getLayersForManifestSha(imageRef, manifestShas, repository) +} + +// BuildSearchPaths returns the search path for a fat manifest image (imageName/sha256:xxx) +func (h *FatManifestHandler) BuildSearchPaths(imageName, imageTag, manifestDigest string) string { + return fmt.Sprintf("%s/%s", imageName, manifestDigest) +} + +// getManifestShaListForImage retrieves all platform manifest SHAs from a fat manifest +func (h *FatManifestHandler) getManifestShaListForImage(imageReference name.Reference) ([]string, error) { + index, err := remote.Index(imageReference, remote.WithAuthFromKeychain(authn.DefaultKeychain)) + if err != nil { + return []string{}, errorutils.CheckErrorf("Failed to get image index for image: %s. Error: %s", imageReference.Name(), err.Error()) + } + manifestList, err := index.IndexManifest() + if err != nil { + return []string{}, errorutils.CheckErrorf("Failed to get manifest list for image: %s. Error: %s", imageReference.Name(), err.Error()) + } + manifestShas := make([]string, 0, len(manifestList.Manifests)) + for _, descriptor := range manifestList.Manifests { + manifestShas = append(manifestShas, descriptor.Digest.String()) + } + log.Debug(fmt.Sprintf("Found %d platform manifests", len(manifestShas))) + return manifestShas, nil +} + +// getLayersForManifestSha searches for layers across all manifest SHAs +func (h *FatManifestHandler) getLayersForManifestSha(imageRef string, manifestShas []string, repository string) ([]utils.ResultItem, []utils.ResultItem, error) { + var foldersToApplyProps []utils.ResultItem + searchablePathForManifest := h.createSearchablePathForDockerManifestContents(imageRef, manifestShas) + + for _, path := range searchablePathForManifest { + foldersToApplyProps = append(foldersToApplyProps, utils.ResultItem{ + Repo: repository, + Path: path, + Type: "folder", + }) + } + + layers, err := searchArtifactoryForFilesByPath(repository, searchablePathForManifest, h.serviceManager) + if err != nil { + return []utils.ResultItem{}, foldersToApplyProps, err + } + return layers, foldersToApplyProps, nil +} + +// createSearchablePathForDockerManifestContents builds search paths like imageName/sha256:xxx +func (h *FatManifestHandler) createSearchablePathForDockerManifestContents(imageRef string, manifestShas []string) []string { + imageName, err := NewImage(imageRef).GetImageShortName() + if err != nil { + log.Warn(fmt.Sprintf("Failed to get image name: %s. Error: %s while creating searchable paths for docker manifest contents.", imageRef, err.Error())) + return []string{} + } + searchablePaths := make([]string, 0, len(manifestShas)) + for _, manifestSha := range manifestShas { + searchablePaths = append(searchablePaths, fmt.Sprintf("%s/%s", imageName, manifestSha)) + } + return searchablePaths +} diff --git a/artifactory/commands/ocicontainer/docker_build_utils.go b/artifactory/commands/ocicontainer/docker_utils.go similarity index 71% rename from artifactory/commands/ocicontainer/docker_build_utils.go rename to artifactory/commands/ocicontainer/docker_utils.go index fc574e26..f1bd3152 100644 --- a/artifactory/commands/ocicontainer/docker_build_utils.go +++ b/artifactory/commands/ocicontainer/docker_utils.go @@ -5,15 +5,9 @@ import ( "fmt" "io" "net/http" - "runtime" "strings" "sync" - v1 "github.com/google/go-containerregistry/pkg/v1" - - "github.com/google/go-containerregistry/pkg/authn" - "github.com/google/go-containerregistry/pkg/name" - "github.com/google/go-containerregistry/pkg/v1/remote" "github.com/jfrog/jfrog-client-go/artifactory" "github.com/jfrog/jfrog-client-go/artifactory/services/utils" "github.com/jfrog/jfrog-client-go/utils/errorutils" @@ -29,74 +23,38 @@ const ( remoteRepositoryType = "remote" ) -// getRemoteRepoAndManifestTypeWithLeadSha determines the repository, manifest type, and lead SHA for an image -func (dbib *DockerBuildInfoBuilder) getRemoteRepoAndManifestTypeWithLeadSha(imageRef string) (string, manifestType, string, error) { +// GetRemoteRepoAndManifestTypeWithLeadSha determines the repository, manifest type, and lead SHA for an image +func GetRemoteRepoAndManifestTypeWithLeadSha(imageRef string, serviceManager artifactory.ArtifactoryServicesManager) (string, ManifestType, string, error) { image := NewImage(imageRef) - repository, manifestFileName, leadSha, err := image.GetRemoteRepoAndManifestTypeAndLeadSha(dbib.serviceManager) + repository, err := image.GetRemoteRepo(serviceManager) if err != nil { return "", "", "", err } - switch manifestFileName { - case string(ManifestList): - return repository, ManifestList, leadSha, nil - case string(Manifest): - return repository, Manifest, leadSha, nil - default: - return "", "", "", errorutils.CheckErrorf("unknown/other artifact type: %s", manifestFileName) - } -} - -// manifestDetails gets the manifest SHA for a base image, considering platform (OS/architecture) -func (dbib *DockerBuildInfoBuilder) manifestDetails(baseImage BaseImage) (string, error) { - imageRef := baseImage.Image - ref, err := name.ParseReference(imageRef) - if err != nil { - return "", fmt.Errorf("parsing reference %s: %w", imageRef, err) - } - var osName, osArch string - - if baseImage.OS != "" && baseImage.Architecture != "" { - osName = baseImage.OS - osArch = baseImage.Architecture - } else { - osName = runtime.GOOS - if osName == "darwin" { - osName = "linux" - } - osArch = runtime.GOARCH - } - - remoteImage, err := remote.Image(ref, remote.WithAuthFromKeychain(authn.DefaultKeychain), remote.WithPlatform(v1.Platform{OS: osName, Architecture: osArch})) - if err != nil || remoteImage == nil { - return "", fmt.Errorf("error fetching manifest for %s: %w", imageRef, err) - } - - manifestShaDigest, err := remoteImage.Digest() + manifestType, leadSha, err := GetManifestTypeAndLeadSha(imageRef) if err != nil { - return "", fmt.Errorf("error getting manifest digest for %s: %w", imageRef, err) + return "", "", "", err } - return manifestShaDigest.String(), nil + return repository, manifestType, leadSha, nil } -// getSearchableRepository resolves the repository name based on type (adds -cache for remote repos) -func (dbib *DockerBuildInfoBuilder) getSearchableRepository(repositoryName string) (string, error) { - repositoryDetails := &dockerRepositoryDetails{} - err := dbib.serviceManager.GetRepository(repositoryName, &repositoryDetails) +// GetSearchableRepositoryAndDetails resolves the repository name based on type (adds -cache for remote repos) +func GetSearchableRepositoryAndDetails(repositoryName string, serviceManager artifactory.ArtifactoryServicesManager) (string, *DockerRepositoryDetails, error) { + repositoryDetails := &DockerRepositoryDetails{} + err := serviceManager.GetRepository(repositoryName, &repositoryDetails) if err != nil { - return "", err + return "", nil, err } - dbib.repositoryDetails = *repositoryDetails - if dbib.repositoryDetails.RepoType == "" || dbib.repositoryDetails.Key == "" { - return "", errorutils.CheckErrorf("repository details are incomplete: %+v", dbib.repositoryDetails) + if repositoryDetails.RepoType == "" || repositoryDetails.Key == "" { + return "", nil, errorutils.CheckErrorf("repository details are incomplete: %+v", repositoryDetails) } - if dbib.repositoryDetails.RepoType == remoteRepositoryType { - return dbib.repositoryDetails.Key + "-cache", nil + if repositoryDetails.RepoType == remoteRepositoryType { + return repositoryDetails.Key + "-cache", repositoryDetails, nil } - return dbib.repositoryDetails.Key, nil + return repositoryDetails.Key, repositoryDetails, nil } // searchArtifactoryForFilesByPath performs AQL query with exact path matching -func (dbib *DockerBuildInfoBuilder) searchArtifactoryForFilesByPath(repository string, paths []string) ([]utils.ResultItem, error) { +func searchArtifactoryForFilesByPath(repository string, paths []string, serviceManager artifactory.ArtifactoryServicesManager) ([]utils.ResultItem, error) { if len(paths) == 0 { return []utils.ResultItem{}, nil } @@ -121,7 +79,7 @@ func (dbib *DockerBuildInfoBuilder) searchArtifactoryForFilesByPath(repository s repository, strings.Join(pathConditions, ",\n ")) // Execute AQL search - allResults, err := executeAqlQuery(dbib.serviceManager, aqlQuery) + allResults, err := executeAqlQuery(serviceManager, aqlQuery) if err != nil { return []utils.ResultItem{}, fmt.Errorf("failed to search Artifactory for layers by path: %w", err) } @@ -129,11 +87,11 @@ func (dbib *DockerBuildInfoBuilder) searchArtifactoryForFilesByPath(repository s return allResults, nil } -// searchForImageLayersInPath performs AQL query with $match/$nmatch patterns +// SearchForImageLayersInPath performs AQL query with $match/$nmatch patterns // this function looks for the uploaded layers in docker-repo/imageName/path* provided and neglects the _uploads folder // upload folder contains actual uploaded layer which are copied to their final location by docker // adding properties in uploaded folder is redundant to form tree structure in build info page -func (dbib *DockerBuildInfoBuilder) searchForImageLayersInPath(imageName, repository string, paths []string) ([]utils.ResultItem, error) { +func SearchForImageLayersInPath(imageName, repository string, paths []string, serviceManager artifactory.ArtifactoryServicesManager) ([]utils.ResultItem, error) { excludePath := fmt.Sprintf("%s/%s", imageName, uploadsFolder) var allResults []utils.ResultItem var err error @@ -159,7 +117,7 @@ func (dbib *DockerBuildInfoBuilder) searchForImageLayersInPath(imageName, reposi repository, path, excludePath) // Execute AQL search - allResults, err = executeAqlQuery(dbib.serviceManager, aqlQuery) + allResults, err = executeAqlQuery(serviceManager, aqlQuery) if err != nil { return []utils.ResultItem{}, fmt.Errorf("failed to search Artifactory for layers in path: %w", err) } @@ -171,11 +129,44 @@ func (dbib *DockerBuildInfoBuilder) searchForImageLayersInPath(imageName, reposi return allResults, nil } +// SearchManifestPathByDigest finds the manifest path using AQL property search on @docker.manifest.digest +func SearchManifestPathByDigest(repo, digest string, serviceManager artifactory.ArtifactoryServicesManager) (string, error) { + aqlQuery := fmt.Sprintf(`items.find({ + "repo": "%s", + "name": "manifest.json", + "@sha256": "%s", + "@docker.manifest.digest": "%s" + }).include("path")`, repo, strings.TrimPrefix(digest, "sha256:"), digest) + + results, err := executeAqlQuery(serviceManager, aqlQuery) + if err != nil { + return "", err + } + if len(results) == 0 { + return "", fmt.Errorf("no manifest found for digest: %s in repo: %s", digest, repo) + } + + return results[0].Path, nil +} + // modifyPathForRemoteRepo adds library/ prefix and converts sha256: to sha256__ func modifyPathForRemoteRepo(path string) string { return fmt.Sprintf("%s/%s", remoteRepoLibraryPrefix, strings.Replace(path, sha256Prefix, sha256RemoteFormat, 1)) } +// normalizeLayerSha removes sha256: or sha256__ prefix if present, returning just the hex digest +func normalizeLayerSha(layerSha string) string { + // Handle sha256:xxx format + if strings.HasPrefix(layerSha, sha256Prefix) { + return strings.TrimPrefix(layerSha, sha256Prefix) + } + // Handle sha256__xxx format (used in remote repos) + if strings.HasPrefix(layerSha, sha256RemoteFormat) { + return strings.TrimPrefix(layerSha, sha256RemoteFormat) + } + return layerSha +} + // deduplicateResultsBySha256 removes duplicate results based on SHA256 func deduplicateResultsBySha256(results []utils.ResultItem) []utils.ResultItem { encountered := make(map[string]bool) @@ -237,10 +228,10 @@ func getMarkerLayerShasFromSearchResult(searchResults []utils.ResultItem) ([]str // handleMarkerLayersForDockerBuild downloads marker layers into the remote cache repository func handleMarkerLayersForDockerBuild(markerLayerShas []string, serviceManager artifactory.ArtifactoryServicesManager, remoteRepo, imageShortName string) []utils.ResultItem { - log.Debug("Handling marker layers for shas: ", strings.Join(markerLayerShas, ", ")) if len(markerLayerShas) == 0 { return nil } + log.Debug("Handling marker layers for shas: ", strings.Join(markerLayerShas, ", ")) baseUrl := serviceManager.GetConfig().GetServiceDetails().GetUrl() var wg sync.WaitGroup @@ -270,7 +261,9 @@ func handleMarkerLayersForDockerBuild(markerLayerShas []string, serviceManager a // downloadSingleMarkerLayer downloads a single marker layer into the remote cache repository func downloadSingleMarkerLayer(layerSha, remoteRepo, imageName, baseUrl string, serviceManager artifactory.ArtifactoryServicesManager) *utils.ResultItem { log.Debug(fmt.Sprintf("Downloading marker %s layer into remote repository cache...", layerSha)) - endpoint := "api/docker/" + remoteRepo + "/v2/" + imageName + "/blobs/" + "sha256:" + layerSha + // Normalize layerSha - remove sha256: or sha256__ prefix if present + normalizedSha := normalizeLayerSha(layerSha) + endpoint := "api/docker/" + remoteRepo + "/v2/" + imageName + "/blobs/" + "sha256:" + normalizedSha clientDetails := serviceManager.GetConfig().GetServiceDetails().CreateHttpClientDetails() resp, body, err := serviceManager.Client().SendHead(baseUrl+endpoint, &clientDetails) diff --git a/artifactory/commands/ocicontainer/docker_build_utils_test.go b/artifactory/commands/ocicontainer/docker_utils_test.go similarity index 84% rename from artifactory/commands/ocicontainer/docker_build_utils_test.go rename to artifactory/commands/ocicontainer/docker_utils_test.go index 43a68326..41ef629f 100644 --- a/artifactory/commands/ocicontainer/docker_build_utils_test.go +++ b/artifactory/commands/ocicontainer/docker_utils_test.go @@ -1,6 +1,7 @@ package ocicontainer import ( + "strings" "testing" "github.com/jfrog/jfrog-client-go/artifactory/services/utils" @@ -370,12 +371,17 @@ func TestGetBiProperties(t *testing.T) { func TestGetPushedRepo(t *testing.T) { tests := []struct { name string - repoDetails dockerRepositoryDetails + repoDetails *DockerRepositoryDetails expected string }{ + { + name: "nil repository details", + repoDetails: nil, + expected: "", + }, { name: "local repository", - repoDetails: dockerRepositoryDetails{ + repoDetails: &DockerRepositoryDetails{ Key: "docker-local", RepoType: "local", }, @@ -383,7 +389,7 @@ func TestGetPushedRepo(t *testing.T) { }, { name: "remote repository", - repoDetails: dockerRepositoryDetails{ + repoDetails: &DockerRepositoryDetails{ Key: "docker-remote", RepoType: "remote", }, @@ -391,7 +397,7 @@ func TestGetPushedRepo(t *testing.T) { }, { name: "virtual repository", - repoDetails: dockerRepositoryDetails{ + repoDetails: &DockerRepositoryDetails{ Key: "docker-virtual", RepoType: "virtual", DefaultDeploymentRepo: "docker-local-deploy", @@ -402,11 +408,11 @@ func TestGetPushedRepo(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - builder := &DockerBuildInfoBuilder{ + builder := &DockerArtifactsBuilder{ repositoryDetails: tt.repoDetails, } - result := builder.getPushedRepo() + result := builder.GetOriginalDeploymentRepo() assert.Equal(t, tt.expected, result) }) } @@ -445,11 +451,10 @@ func TestApplyRepoTypeModifications(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - builder := &DockerBuildInfoBuilder{ - repositoryDetails: dockerRepositoryDetails{RepoType: tt.repoType}, - } + builder := &DockerDependenciesBuilder{} + repoDetails := DockerRepositoryDetails{RepoType: tt.repoType} - result := builder.applyRepoTypeModifications(tt.basePath) + result := builder.applyRepoTypeModifications(tt.basePath, repoDetails) assert.Equal(t, tt.expectedLen, len(result)) assert.Equal(t, tt.expected, result) }) @@ -457,11 +462,11 @@ func TestApplyRepoTypeModifications(t *testing.T) { } func TestGetManifestHandler(t *testing.T) { - builder := &DockerBuildInfoBuilder{} + manifestHandler := &DockerManifestHandler{} tests := []struct { name string - manifestType manifestType + manifestType ManifestType expectNil bool handlerType string }{ @@ -479,13 +484,13 @@ func TestGetManifestHandler(t *testing.T) { }, { name: "unknown type returns nil", - manifestType: manifestType("unknown"), + manifestType: ManifestType("unknown"), expectNil: true, handlerType: "", }, { name: "empty type returns nil", - manifestType: manifestType(""), + manifestType: ManifestType(""), expectNil: true, handlerType: "", }, @@ -493,7 +498,7 @@ func TestGetManifestHandler(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - handler := builder.getManifestHandler(tt.manifestType) + handler := manifestHandler.GetManifestHandler(tt.manifestType) if tt.expectNil { assert.Nil(t, handler) @@ -555,7 +560,7 @@ func TestCreateSearchablePathForDockerManifestContents(t *testing.T) { } func TestCreateDependenciesFromResults(t *testing.T) { - builder := &DockerBuildInfoBuilder{} + builder := &DockerDependenciesBuilder{} tests := []struct { name string @@ -603,7 +608,7 @@ func TestCreateDependenciesFromResults(t *testing.T) { } func TestCreateArtifactsFromResults(t *testing.T) { - builder := &DockerBuildInfoBuilder{} + builder := &DockerArtifactsBuilder{} tests := []struct { name string @@ -650,55 +655,58 @@ func TestCreateArtifactsFromResults(t *testing.T) { } func TestManifestTypeConstants(t *testing.T) { - assert.Equal(t, manifestType("list.manifest.json"), ManifestList) - assert.Equal(t, manifestType("manifest.json"), Manifest) + assert.Equal(t, ManifestType("list.manifest.json"), ManifestList) + assert.Equal(t, ManifestType("manifest.json"), Manifest) } func TestFetchLayersOfPushedImage_UnknownManifestType(t *testing.T) { - builder := &DockerBuildInfoBuilder{} + handler := &DockerManifestHandler{} // Test error handling for unknown manifest type - result, err := builder.fetchLayersOfPushedImage("test-image", "test-repo", manifestType("unknown")) + layers, foldersToApplyProps, err := handler.FetchLayersOfPushedImage("test-image", "test-repo", ManifestType("unknown")) assert.Error(t, err) assert.Contains(t, err.Error(), "unknown/other manifest type") - assert.Empty(t, result) + assert.Empty(t, layers) + assert.Empty(t, foldersToApplyProps) } func TestFetchLayersOfPushedImage_EmptyManifestType(t *testing.T) { - builder := &DockerBuildInfoBuilder{} + handler := &DockerManifestHandler{} // Test error handling for empty manifest type - result, err := builder.fetchLayersOfPushedImage("test-image", "test-repo", manifestType("")) + layers, foldersToApplyProps, err := handler.FetchLayersOfPushedImage("test-image", "test-repo", ManifestType("")) assert.Error(t, err) assert.Contains(t, err.Error(), "unknown/other manifest type") - assert.Empty(t, result) + assert.Empty(t, layers) + assert.Empty(t, foldersToApplyProps) } func TestGetArtifacts_ImageNotPushed(t *testing.T) { - builder := &DockerBuildInfoBuilder{ + builder := &DockerArtifactsBuilder{ isImagePushed: false, imageTag: "test:latest", } - artifacts, leadSha, err := builder.getArtifacts() + artifacts, leadSha, resultsToApplyProps, err := builder.getArtifacts() assert.NoError(t, err) assert.Empty(t, artifacts) assert.Empty(t, leadSha) + assert.Empty(t, resultsToApplyProps) } func TestGetArtifacts_ImagePushed(t *testing.T) { // This test verifies that when isImagePushed is true, the function attempts to collect artifacts // Without proper serviceManager setup, it will error, but we verify the code path is taken - builder := &DockerBuildInfoBuilder{ + builder := &DockerArtifactsBuilder{ isImagePushed: true, imageTag: "test:latest", } // Without proper setup, this will error, but we verify the path is taken - artifacts, leadSha, err := builder.getArtifacts() + artifacts, leadSha, resultsToApplyProps, err := builder.getArtifacts() // Error expected without proper mocking, but function should attempt collection // We just verify the function executes (error is expected without serviceManager) @@ -710,4 +718,47 @@ func TestGetArtifacts_ImagePushed(t *testing.T) { _ = artifacts } _ = leadSha + _ = resultsToApplyProps +} + +func TestNormalizeLayerSha(t *testing.T) { + tests := []struct { + name string + input string + expected string + }{ + {"plain hex digest", "6b59a28fa20117e6048ad0616b8d8c901877ef15ff4c7f18db04e4f01f43bc39", "6b59a28fa20117e6048ad0616b8d8c901877ef15ff4c7f18db04e4f01f43bc39"}, + {"sha256: prefix", "sha256:6b59a28fa20117e6048ad0616b8d8c901877ef15ff4c7f18db04e4f01f43bc39", "6b59a28fa20117e6048ad0616b8d8c901877ef15ff4c7f18db04e4f01f43bc39"}, + {"sha256__ prefix", "sha256__6b59a28fa20117e6048ad0616b8d8c901877ef15ff4c7f18db04e4f01f43bc39", "6b59a28fa20117e6048ad0616b8d8c901877ef15ff4c7f18db04e4f01f43bc39"}, + {"empty string", "", ""}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := normalizeLayerSha(tt.input) + assert.Equal(t, tt.expected, result) + }) + } +} + +func TestDigestBasedImageDetection(t *testing.T) { + // Test that digest-based images are correctly identified + tests := []struct { + name string + imageTag string + isDigest bool + }{ + {"tag-based image", "latest", false}, + {"version tag", "v1.0.0", false}, + {"digest-based image", "sha256:abc123def456", true}, + {"full digest", "sha256:4a2047b0e69af48c94821afb84ded71dee018059ac708e0e8f3e687e22726cd2", true}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Verify our detection logic matches expected behavior + isDigest := strings.HasPrefix(tt.imageTag, "sha256:") + assert.Equal(t, tt.isDigest, isDigest) + }) + } } diff --git a/artifactory/commands/ocicontainer/image.go b/artifactory/commands/ocicontainer/image.go index 9499daba..252f125e 100644 --- a/artifactory/commands/ocicontainer/image.go +++ b/artifactory/commands/ocicontainer/image.go @@ -46,13 +46,19 @@ func (image *Image) GetImageLongNameWithTag() (string, error) { 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 +// Get image base name by removing the prefixed registry hostname and the tag/digest. +// e.g.: https://my-registry/docker-local/hello-world:latest -> docker-local/hello-world +// e.g.: https://my-registry/docker-local/hello-world@sha256:12334 -> docker-local/hello-world func (image *Image) GetImageLongName() (string, error) { imageName, err := image.GetImageLongNameWithTag() if err != nil { return "", err } + // Check for digest first (@sha256:...) + if digestIndex := strings.Index(imageName, "@"); digestIndex != -1 { + return imageName[:digestIndex], nil + } + // Otherwise strip the tag tagIndex := strings.Index(imageName, ":") return imageName[:tagIndex], nil } @@ -64,13 +70,18 @@ func (image *Image) validateTag() error { 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 +// Get image base name by removing the prefixed registry hostname and the tag/digest. +// e.g.: https://my-registry/docker-local/hello-world:latest -> hello-world +// e.g.: https://my-registry/docker-local/hello-world@sha256:12334 -> hello-world func (image *Image) GetImageShortName() (string, error) { imageName, err := image.GetImageShortNameWithTag() if err != nil { return "", err } + // Check for digest first (@sha256:...) + if digestIndex := strings.Index(imageName, "@"); digestIndex != -1 { + return imageName[:digestIndex], nil + } tagIndex := strings.LastIndex(imageName, ":") if tagIndex != -1 { return imageName[:tagIndex], nil @@ -108,13 +119,18 @@ func (image *Image) GetImageLongNameWithoutRepoWithTag() (string, error) { return longName, nil } -// Get image tag name of an image. -// e.g.: https://my-registry/docker-local/hello-world:latest. -> latest +// Get image tag or digest of an image. +// e.g.: https://my-registry/docker-local/hello-world:latest -> latest +// e.g.: https://my-registry/docker-local/hello-world@sha256:12334 -> sha256:12334 func (image *Image) GetImageTag() (string, error) { imageName, err := image.GetImageLongNameWithTag() if err != nil { return "", err } + // Check for digest first (@sha256:...) + if digestIndex := strings.Index(imageName, "@"); digestIndex != -1 { + return imageName[digestIndex+1:], nil + } tagIndex := strings.Index(imageName, ":") if tagIndex == -1 { return "", errorutils.CheckErrorf("unexpected image name '%s'. Failed to get image tag.", image.Name()) @@ -167,48 +183,6 @@ func (image *Image) GetRemoteRepo(serviceManager artifactory.ArtifactoryServices return "", errors.New("couldn't find 'X-Artifactory-Docker-Registry' header docker repository in artifactory") } -// Returns the physical Artifactory repository name of the pulled/pushed image, by reading a response header from Artifactory. -func (image *Image) GetRemoteRepoAndManifestTypeAndLeadSha(serviceManager artifactory.ArtifactoryServicesManager) (string, string, 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 - } - // Build the request URL. - endpoint := buildRequestUrl(longImageName, imageTag, containerRegistryUrl, isSecureProtocol(serviceManager)) - 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.StatusForbidden { - return "", "", "", errorutils.CheckErrorf("%s for image '%s'", getStatusForbiddenErrorMessage(), image.name) - } - if resp.StatusCode != http.StatusOK { - return "", "", "", errorutils.CheckErrorf("error while getting docker repository name for image '%s'. Artifactory response: %s", image.name, resp.Status) - } - - var dockerRepo, dockerManifestType, dockerLeadSha []string - if dockerRepo = resp.Header["X-Artifactory-Docker-Registry"]; len(dockerRepo) == 0 { - return "", "", "", errorutils.CheckErrorf("couldn't find 'X-Artifactory-Docker-Registry' header for image '%s'", image.name) - } - if dockerManifestType = resp.Header["X-Artifactory-Filename"]; len(dockerManifestType) == 0 { - return "", "", "", errorutils.CheckErrorf("couldn't find 'X-Artifactory-Filename' header for image '%s'", image.name) - } - if dockerLeadSha = resp.Header["X-Checksum-Sha256"]; len(dockerLeadSha) == 0 { - return "", "", "", errorutils.CheckErrorf("couldn't find 'X-Checksum-Sha256' header for image '%s'", image.name) - } - return dockerRepo[0], dockerManifestType[0], dockerLeadSha[0], nil -} - func isSecureProtocol(serviceManager artifactory.ArtifactoryServicesManager) bool { return strings.HasPrefix(serviceManager.GetConfig().GetServiceDetails().GetUrl(), "https") } diff --git a/artifactory/commands/ocicontainer/image_test.go b/artifactory/commands/ocicontainer/image_test.go index 6d0c5aff..561bdfad 100644 --- a/artifactory/commands/ocicontainer/image_test.go +++ b/artifactory/commands/ocicontainer/image_test.go @@ -18,6 +18,11 @@ func TestGetImageLongName(t *testing.T) { {"domain/path:1.0", "path"}, {"domain/path/in/artifactory:1.0", "path/in/artifactory"}, {"domain/path/in/artifactory", "path/in/artifactory"}, + // Digest-based images + {"domain:8080/path@sha256:abc123", "path"}, + {"domain:8080/path/in/artifactory@sha256:abc123def456", "path/in/artifactory"}, + {"domain/path@sha256:abc123", "path"}, + {"domain/path/in/artifactory@sha256:abc123def456", "path/in/artifactory"}, } for _, v := range imageTags { @@ -44,6 +49,11 @@ func TestGetImageShortName(t *testing.T) { {"domain/path:1.0", "path"}, {"domain/path/in/artifactory:1.0", "artifactory"}, {"domain/path/in/artifactory", "artifactory"}, + // Digest-based images + {"domain:8080/path@sha256:abc123", "path"}, + {"domain:8080/path/in/artifactory@sha256:abc123def456", "artifactory"}, + {"domain/path@sha256:abc123", "path"}, + {"domain/path/in/artifactory@sha256:abc123def456", "artifactory"}, } for _, v := range imageTags { @@ -70,6 +80,9 @@ func TestGetImageLongNameWithTag(t *testing.T) { {"domain/path:1.0", "path:1.0"}, {"domain/path/in/artifactory:1.0", "path/in/artifactory:1.0"}, {"domain/path/in/artifactory", "path/in/artifactory:latest"}, + // Digest-based images + {"domain:8080/path@sha256:abc123", "path@sha256:abc123"}, + {"domain/path/in/artifactory@sha256:abc123def456", "path/in/artifactory@sha256:abc123def456"}, } for _, v := range imageTags { @@ -116,6 +129,9 @@ func TestGetImageShortNameWithTag(t *testing.T) { {"domain/path:1.0", "path:1.0"}, {"domain/path/in/artifactory:1.0", "artifactory:1.0"}, {"domain/path/in/artifactory", "artifactory:latest"}, + // Digest-based images + {"domain:8080/path@sha256:abc123", "path@sha256:abc123"}, + {"domain/path/in/artifactory@sha256:abc123def456", "artifactory@sha256:abc123def456"}, } for _, v := range imageTags { @@ -156,6 +172,30 @@ func TestResolveRegistryFromTag(t *testing.T) { } } +func TestGetImageTag(t *testing.T) { + var imageTags = []struct { + in string + expected string + }{ + // Tag-based images + {"domain:8080/path:1.0", "1.0"}, + {"domain:8080/path/in/artifactory:1.0", "1.0"}, + {"domain:8080/path/in/artifactory", "latest"}, + {"domain/path:v2.0.0", "v2.0.0"}, + // Digest-based images - should return full digest + {"domain:8080/path@sha256:abc123def456", "sha256:abc123def456"}, + {"domain/path/in/artifactory@sha256:4a2047b0e69af48c94821afb84ded71dee018059ac708e0e8f3e687e22726cd2", "sha256:4a2047b0e69af48c94821afb84ded71dee018059ac708e0e8f3e687e22726cd2"}, + } + + for _, v := range imageTags { + result, err := NewImage(v.in).GetImageTag() + assert.NoError(t, err) + if result != v.expected { + t.Errorf("GetImageTag(\"%s\") => '%s', want '%s'", v.in, result, v.expected) + } + } +} + func TestDockerClientApiVersionRegex(t *testing.T) { var versionStrings = []struct { in string diff --git a/artifactory/commands/ocicontainer/localagent.go b/artifactory/commands/ocicontainer/localagent.go index 4f786eaa..0d173647 100644 --- a/artifactory/commands/ocicontainer/localagent.go +++ b/artifactory/commands/ocicontainer/localagent.go @@ -49,6 +49,15 @@ func (labib *localAgentBuildInfoBuilder) SetSkipTaggingLayers(skipTaggingLayers // Create build-info for a docker image. func (labib *localAgentBuildInfoBuilder) Build(module string) (*buildinfo.BuildInfo, error) { + longImageName, err := labib.buildInfoBuilder.image.GetImageLongNameWithTag() + if err != nil { + return nil, err + } + // Check if this is a digest-based image reference (contains @sha256:) + if digestIndex := strings.Index(longImageName, "@"); digestIndex != -1 { + return labib.handleImageByDigest() + } + // Search for image build-info. candidateLayers, manifest, err := labib.searchImage() if err != nil { @@ -62,6 +71,35 @@ func (labib *localAgentBuildInfoBuilder) Build(module string) (*buildinfo.BuildI return labib.buildInfoBuilder.createBuildInfo(labib.commandType, manifest, candidateLayers, module) } +func (labib *localAgentBuildInfoBuilder) handleImageByDigest() (*buildinfo.BuildInfo, error) { + log.Debug("Processing digest-based image pull for: " + labib.buildInfoBuilder.image.name) + dockerImage := DockerImage{ + Image: labib.buildInfoBuilder.image.Name(), + } + dependencies, err := NewDockerDependenciesBuilder([]DockerImage{dockerImage}, labib.buildInfoBuilder.serviceManager).getDependencies() + if err != nil { + return nil, err + } + + dockerImageTag, err := labib.buildInfoBuilder.image.GetImageShortNameWithTag() + if err != nil { + return nil, err + } + + imageProperties := map[string]string{ + "docker.image.id": labib.buildInfoBuilder.imageSha2, + "docker.image.tag": labib.buildInfoBuilder.image.Name(), + } + + buildInfo := &buildinfo.BuildInfo{Modules: []buildinfo.Module{{ + Id: dockerImageTag, + Type: buildinfo.Docker, + Properties: imageProperties, + Dependencies: dependencies, + }}} + return buildInfo, nil +} + // 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()