@@ -396,6 +396,175 @@ func TestPushFatManifestImage(t *testing.T) {
396396 assert .True (t , totalResults > 1 )
397397}
398398
399+ // runNestedPathDockerBuildTest is a helper function for testing docker build --push with nested paths.
400+ // It handles common setup, build execution, validation, and cleanup.
401+ // platforms: empty string for single platform, or comma-separated platforms like "linux/amd64,linux/arm64"
402+ func runNestedPathDockerBuildTest (t * testing.T , buildNameSuffix , imageSuffix , nestedPath , platforms string ) {
403+ buildName := buildNameSuffix + tests .DockerBuildName
404+ buildNumber := "1"
405+
406+ // Extract hostname from ContainerRegistry (remove protocol if present)
407+ registryHost := * tests .ContainerRegistry
408+ if parsedURL , err := url .Parse (registryHost ); err == nil && parsedURL .Host != "" {
409+ registryHost = parsedURL .Host
410+ }
411+
412+ // Construct image name with nested path: repo/nestedPath/image
413+ nestedImageName := path .Join (registryHost , tests .OciLocalRepo , nestedPath , imageSuffix )
414+ imageTag := nestedImageName + ":v1"
415+
416+ // Create test workspace
417+ workspace , err := filepath .Abs (tests .Out )
418+ assert .NoError (t , err )
419+ assert .NoError (t , fileutils .CreateDirIfNotExist (workspace ))
420+
421+ // Construct base image with hostname
422+ baseImage := path .Join (registryHost , tests .OciRemoteRepo , "alpine:latest" )
423+
424+ // Create Dockerfile
425+ dockerfileContent := fmt .Sprintf (`FROM %s
426+ RUN echo "Built for nested path test"
427+ CMD ["echo", "Hello from nested path"]` , baseImage )
428+
429+ dockerfilePath := filepath .Join (workspace , "Dockerfile" )
430+ assert .NoError (t , os .WriteFile (dockerfilePath , []byte (dockerfileContent ), 0644 ))
431+
432+ // Cleanup old build
433+ inttestutils .DeleteBuild (serverDetails .ArtifactoryUrl , buildName , artHttpDetails )
434+ defer inttestutils .DeleteBuild (serverDetails .ArtifactoryUrl , buildName , artHttpDetails )
435+
436+ // Clean build before test
437+ runJfrogCli (t , "rt" , "bc" , buildName , buildNumber )
438+
439+ // Run docker build --push (single or multiplatform based on platforms parameter)
440+ if platforms != "" {
441+ runJfrogCli (t , "docker" , "buildx" , "build" , "--platform" , platforms ,
442+ "-t" , imageTag , "-f" , dockerfilePath , "--push" , "--build-name=" + buildName , "--build-number=" + buildNumber , workspace )
443+ } else {
444+ runJfrogCli (t , "docker" , "build" , "-t" , imageTag , "-f" , dockerfilePath , "--push" ,
445+ "--build-name=" + buildName , "--build-number=" + buildNumber , workspace )
446+ }
447+
448+ // Publish build info
449+ runJfrogCli (t , "rt" , "build-publish" , buildName , buildNumber )
450+
451+ // Validate the published build-info exists
452+ publishedBuildInfo , found , err := tests .GetBuildInfo (serverDetails , buildName , buildNumber )
453+ assert .NoError (t , err )
454+ assert .True (t , found , "build info was expected to be found" )
455+ assert .True (t , len (publishedBuildInfo .BuildInfo .Modules ) >= 1 , "Expected at least 1 module in build info" )
456+
457+ // Validate build-name & build-number properties in all image layers at nested path
458+ searchSpec := spec .NewBuilder ().Pattern (tests .OciLocalRepo + "/" + nestedPath + "/*" ).Build (buildName ).Recursive (true ).BuildSpec ()
459+ searchCmd := generic .NewSearchCommand ()
460+ searchCmd .SetServerDetails (serverDetails ).SetSpec (searchSpec )
461+ reader , err := searchCmd .Search ()
462+ assert .NoError (t , err )
463+ totalResults , err := reader .Length ()
464+ assert .NoError (t , err )
465+ assert .True (t , totalResults > 1 , "Expected layers to be found at nested path " + nestedPath + "/" )
466+
467+ // Cleanup image from Artifactory
468+ inttestutils .ContainerTestCleanup (t , serverDetails , artHttpDetails , nestedPath + "/" + imageSuffix , buildName , tests .OciLocalRepo )
469+ }
470+
471+ // TestDockerBuildPushWithNestedPath tests docker build --push with nested paths like repo/myorg/image.
472+ // This validates that layer fetching works correctly for single platform images with nested paths.
473+ func TestDockerBuildPushWithNestedPath (t * testing.T ) {
474+ cleanup := initDockerBuildTest (t )
475+ defer cleanup ()
476+ runNestedPathDockerBuildTest (t , "docker-build-nested" , "test-single-nested" , "myorg" , "" )
477+ }
478+
479+ // TestPushFatManifestImageWithNestedPath tests pushing fat-manifest (multi-platform) images with nested paths.
480+ // This validates that layer fetching works correctly for paths like <repository>/myorg/image
481+ // which was failing before the fix to FatManifestHandler.createSearchablePathForDockerManifestContents.
482+ func TestPushFatManifestImageWithNestedPath (t * testing.T ) {
483+ cleanup := initDockerBuildTest (t )
484+ defer cleanup ()
485+ runNestedPathDockerBuildTest (t , "push-fat-manifest-nested" , "test-multiarch-nested" , "myorg" , "linux/amd64,linux/arm64" )
486+ }
487+
488+ // TestDockerBuildWithNestedPathBaseImage tests that dependencies are correctly collected
489+ // when using a nested path image as a base layer in a Dockerfile.
490+ func TestDockerBuildWithNestedPathBaseImage (t * testing.T ) {
491+ cleanup := initDockerBuildTest (t )
492+ defer cleanup ()
493+
494+ // Extract hostname from ContainerRegistry
495+ registryHost := * tests .ContainerRegistry
496+ if parsedURL , err := url .Parse (registryHost ); err == nil && parsedURL .Host != "" {
497+ registryHost = parsedURL .Host
498+ }
499+
500+ // Step 1: Push a base image to a nested path (myorg/base-image)
501+ baseImageBuildName := "base-nested" + tests .DockerBuildName
502+ baseImageBuildNumber := "1"
503+ nestedBasePath := "myorg"
504+ baseImageName := path .Join (registryHost , tests .OciLocalRepo , nestedBasePath , "base-image" )
505+ baseImageTag := baseImageName + ":v1"
506+
507+ workspace , err := filepath .Abs (tests .Out )
508+ assert .NoError (t , err )
509+ assert .NoError (t , fileutils .CreateDirIfNotExist (workspace ))
510+
511+ // Create base Dockerfile
512+ alpineBase := path .Join (registryHost , tests .OciRemoteRepo , "alpine:latest" )
513+ baseDockerfile := fmt .Sprintf (`FROM %s
514+ RUN echo "This is the nested base image"
515+ CMD ["echo", "base"]` , alpineBase )
516+
517+ baseDockerfilePath := filepath .Join (workspace , "Dockerfile.base" )
518+ assert .NoError (t , os .WriteFile (baseDockerfilePath , []byte (baseDockerfile ), 0644 ))
519+
520+ // Push base image to nested path
521+ inttestutils .DeleteBuild (serverDetails .ArtifactoryUrl , baseImageBuildName , artHttpDetails )
522+ defer inttestutils .DeleteBuild (serverDetails .ArtifactoryUrl , baseImageBuildName , artHttpDetails )
523+
524+ runJfrogCli (t , "rt" , "bc" , baseImageBuildName , baseImageBuildNumber )
525+ runJfrogCli (t , "docker" , "build" , "-t" , baseImageTag , "-f" , baseDockerfilePath , "--push" ,
526+ "--build-name=" + baseImageBuildName , "--build-number=" + baseImageBuildNumber , workspace )
527+ runJfrogCli (t , "rt" , "build-publish" , baseImageBuildName , baseImageBuildNumber )
528+
529+ // Step 2: Build a new image using the nested path base image
530+ childBuildName := "child-nested" + tests .DockerBuildName
531+ childBuildNumber := "1"
532+ childImageName := path .Join (registryHost , tests .OciLocalRepo , "child-image" )
533+ childImageTag := childImageName + ":v1"
534+
535+ // Create child Dockerfile that uses the nested path base image
536+ childDockerfile := fmt .Sprintf (`FROM %s
537+ RUN echo "This is the child image using nested base"
538+ CMD ["echo", "child"]` , baseImageTag )
539+
540+ childDockerfilePath := filepath .Join (workspace , "Dockerfile.child" )
541+ assert .NoError (t , os .WriteFile (childDockerfilePath , []byte (childDockerfile ), 0644 ))
542+
543+ // Build child image
544+ inttestutils .DeleteBuild (serverDetails .ArtifactoryUrl , childBuildName , artHttpDetails )
545+ defer inttestutils .DeleteBuild (serverDetails .ArtifactoryUrl , childBuildName , artHttpDetails )
546+
547+ runJfrogCli (t , "rt" , "bc" , childBuildName , childBuildNumber )
548+ runJfrogCli (t , "docker" , "build" , "-t" , childImageTag , "-f" , childDockerfilePath , "--push" ,
549+ "--build-name=" + childBuildName , "--build-number=" + childBuildNumber , workspace )
550+ runJfrogCli (t , "rt" , "build-publish" , childBuildName , childBuildNumber )
551+
552+ // Step 3: Validate build info has dependencies from the nested path base image
553+ publishedBuildInfo , found , err := tests .GetBuildInfo (serverDetails , childBuildName , childBuildNumber )
554+ assert .NoError (t , err )
555+ assert .True (t , found , "build info was expected to be found" )
556+ assert .True (t , len (publishedBuildInfo .BuildInfo .Modules ) >= 1 , "Expected at least 1 module in build info" )
557+
558+ // Check that dependencies exist (these come from the base image layers)
559+ module := publishedBuildInfo .BuildInfo .Modules [0 ]
560+ assert .True (t , len (module .Dependencies ) > 0 ,
561+ "Expected dependencies from nested path base image (myorg/base-image). " )
562+
563+ // Cleanup
564+ inttestutils .ContainerTestCleanup (t , serverDetails , artHttpDetails , nestedBasePath + "/base-image" , baseImageBuildName , tests .OciLocalRepo )
565+ inttestutils .ContainerTestCleanup (t , serverDetails , artHttpDetails , "child-image" , childBuildName , tests .OciLocalRepo )
566+ }
567+
399568func TestPushMultiTaggedImage (t * testing.T ) {
400569 if ! * tests .TestDocker {
401570 t .Skip ("Skipping test. To run it, add the '-test.docker=true' option." )
0 commit comments