diff --git a/buildtools/cli.go b/buildtools/cli.go index d9d8c1bd8..54d2abe37 100644 --- a/buildtools/cli.go +++ b/buildtools/cli.go @@ -931,14 +931,15 @@ func goCmdVerification(c *cli.Context) (string, error) { func dockerCmd(c *cli.Context) error { args := cliutils.ExtractCommand(c) - var cmd, image string + var cmd, cmdArg string // We may have prior flags before push/pull commands for the docker client. + // cmdArg is the second non-flag argument: image name for pull/push/scan, subcommand for buildx for _, arg := range args { if !strings.HasPrefix(arg, "-") { if cmd == "" { cmd = arg } else { - image = arg + cmdArg = arg break } } @@ -946,16 +947,22 @@ func dockerCmd(c *cli.Context) error { var err error switch cmd { case "pull": - err = pullCmd(c, image) + err = pullCmd(c, cmdArg) case "push": - err = pushCmd(c, image) + err = pushCmd(c, cmdArg) case "login": err = loginCmd(c) case "scan": - return dockerScanCmd(c, image) - // Handle both build and buildx with same handler - case "build", "buildx": + return dockerScanCmd(c, cmdArg) + case "build": err = buildCmd(c) + case "buildx": + // Only intercept "buildx build", pass through other buildx subcommands (create, ls, rm, etc.) + if cmdArg == "build" { + err = buildCmd(c) + } else { + err = dockerNativeCmd(c) + } default: err = dockerNativeCmd(c) } diff --git a/docker_test.go b/docker_test.go index 6635d0827..a8fb5cc6a 100644 --- a/docker_test.go +++ b/docker_test.go @@ -396,6 +396,175 @@ func TestPushFatManifestImage(t *testing.T) { assert.True(t, totalResults > 1) } +// runNestedPathDockerBuildTest is a helper function for testing docker build --push with nested paths. +// It handles common setup, build execution, validation, and cleanup. +// platforms: empty string for single platform, or comma-separated platforms like "linux/amd64,linux/arm64" +func runNestedPathDockerBuildTest(t *testing.T, buildNameSuffix, imageSuffix, nestedPath, platforms string) { + buildName := buildNameSuffix + tests.DockerBuildName + buildNumber := "1" + + // Extract hostname from ContainerRegistry (remove protocol if present) + registryHost := *tests.ContainerRegistry + if parsedURL, err := url.Parse(registryHost); err == nil && parsedURL.Host != "" { + registryHost = parsedURL.Host + } + + // Construct image name with nested path: repo/nestedPath/image + nestedImageName := path.Join(registryHost, tests.OciLocalRepo, nestedPath, imageSuffix) + imageTag := nestedImageName + ":v1" + + // Create test workspace + workspace, err := filepath.Abs(tests.Out) + assert.NoError(t, err) + assert.NoError(t, fileutils.CreateDirIfNotExist(workspace)) + + // Construct base image with hostname + baseImage := path.Join(registryHost, tests.OciRemoteRepo, "alpine:latest") + + // Create Dockerfile + dockerfileContent := fmt.Sprintf(`FROM %s +RUN echo "Built for nested path test" +CMD ["echo", "Hello from nested path"]`, baseImage) + + dockerfilePath := filepath.Join(workspace, "Dockerfile") + assert.NoError(t, os.WriteFile(dockerfilePath, []byte(dockerfileContent), 0644)) + + // Cleanup old build + inttestutils.DeleteBuild(serverDetails.ArtifactoryUrl, buildName, artHttpDetails) + defer inttestutils.DeleteBuild(serverDetails.ArtifactoryUrl, buildName, artHttpDetails) + + // Clean build before test + runJfrogCli(t, "rt", "bc", buildName, buildNumber) + + // Run docker build --push (single or multiplatform based on platforms parameter) + if platforms != "" { + runJfrogCli(t, "docker", "buildx", "build", "--platform", platforms, + "-t", imageTag, "-f", dockerfilePath, "--push", "--build-name="+buildName, "--build-number="+buildNumber, workspace) + } else { + runJfrogCli(t, "docker", "build", "-t", imageTag, "-f", dockerfilePath, "--push", + "--build-name="+buildName, "--build-number="+buildNumber, workspace) + } + + // Publish build info + runJfrogCli(t, "rt", "build-publish", buildName, buildNumber) + + // Validate the published build-info exists + publishedBuildInfo, found, err := tests.GetBuildInfo(serverDetails, buildName, buildNumber) + assert.NoError(t, err) + assert.True(t, found, "build info was expected to be found") + assert.True(t, len(publishedBuildInfo.BuildInfo.Modules) >= 1, "Expected at least 1 module in build info") + + // Validate build-name & build-number properties in all image layers at nested path + searchSpec := spec.NewBuilder().Pattern(tests.OciLocalRepo + "/" + nestedPath + "/*").Build(buildName).Recursive(true).BuildSpec() + searchCmd := generic.NewSearchCommand() + searchCmd.SetServerDetails(serverDetails).SetSpec(searchSpec) + reader, err := searchCmd.Search() + assert.NoError(t, err) + totalResults, err := reader.Length() + assert.NoError(t, err) + assert.True(t, totalResults > 1, "Expected layers to be found at nested path "+nestedPath+"/") + + // Cleanup image from Artifactory + inttestutils.ContainerTestCleanup(t, serverDetails, artHttpDetails, nestedPath+"/"+imageSuffix, buildName, tests.OciLocalRepo) +} + +// TestDockerBuildPushWithNestedPath tests docker build --push with nested paths like repo/myorg/image. +// This validates that layer fetching works correctly for single platform images with nested paths. +func TestDockerBuildPushWithNestedPath(t *testing.T) { + cleanup := initDockerBuildTest(t) + defer cleanup() + runNestedPathDockerBuildTest(t, "docker-build-nested", "test-single-nested", "myorg", "") +} + +// TestPushFatManifestImageWithNestedPath tests pushing fat-manifest (multi-platform) images with nested paths. +// This validates that layer fetching works correctly for paths like /myorg/image +// which was failing before the fix to FatManifestHandler.createSearchablePathForDockerManifestContents. +func TestPushFatManifestImageWithNestedPath(t *testing.T) { + cleanup := initDockerBuildTest(t) + defer cleanup() + runNestedPathDockerBuildTest(t, "push-fat-manifest-nested", "test-multiarch-nested", "myorg", "linux/amd64,linux/arm64") +} + +// TestDockerBuildWithNestedPathBaseImage tests that dependencies are correctly collected +// when using a nested path image as a base layer in a Dockerfile. +func TestDockerBuildWithNestedPathBaseImage(t *testing.T) { + cleanup := initDockerBuildTest(t) + defer cleanup() + + // Extract hostname from ContainerRegistry + registryHost := *tests.ContainerRegistry + if parsedURL, err := url.Parse(registryHost); err == nil && parsedURL.Host != "" { + registryHost = parsedURL.Host + } + + // Step 1: Push a base image to a nested path (myorg/base-image) + baseImageBuildName := "base-nested" + tests.DockerBuildName + baseImageBuildNumber := "1" + nestedBasePath := "myorg" + baseImageName := path.Join(registryHost, tests.OciLocalRepo, nestedBasePath, "base-image") + baseImageTag := baseImageName + ":v1" + + workspace, err := filepath.Abs(tests.Out) + assert.NoError(t, err) + assert.NoError(t, fileutils.CreateDirIfNotExist(workspace)) + + // Create base Dockerfile + alpineBase := path.Join(registryHost, tests.OciRemoteRepo, "alpine:latest") + baseDockerfile := fmt.Sprintf(`FROM %s +RUN echo "This is the nested base image" +CMD ["echo", "base"]`, alpineBase) + + baseDockerfilePath := filepath.Join(workspace, "Dockerfile.base") + assert.NoError(t, os.WriteFile(baseDockerfilePath, []byte(baseDockerfile), 0644)) + + // Push base image to nested path + inttestutils.DeleteBuild(serverDetails.ArtifactoryUrl, baseImageBuildName, artHttpDetails) + defer inttestutils.DeleteBuild(serverDetails.ArtifactoryUrl, baseImageBuildName, artHttpDetails) + + runJfrogCli(t, "rt", "bc", baseImageBuildName, baseImageBuildNumber) + runJfrogCli(t, "docker", "build", "-t", baseImageTag, "-f", baseDockerfilePath, "--push", + "--build-name="+baseImageBuildName, "--build-number="+baseImageBuildNumber, workspace) + runJfrogCli(t, "rt", "build-publish", baseImageBuildName, baseImageBuildNumber) + + // Step 2: Build a new image using the nested path base image + childBuildName := "child-nested" + tests.DockerBuildName + childBuildNumber := "1" + childImageName := path.Join(registryHost, tests.OciLocalRepo, "child-image") + childImageTag := childImageName + ":v1" + + // Create child Dockerfile that uses the nested path base image + childDockerfile := fmt.Sprintf(`FROM %s +RUN echo "This is the child image using nested base" +CMD ["echo", "child"]`, baseImageTag) + + childDockerfilePath := filepath.Join(workspace, "Dockerfile.child") + assert.NoError(t, os.WriteFile(childDockerfilePath, []byte(childDockerfile), 0644)) + + // Build child image + inttestutils.DeleteBuild(serverDetails.ArtifactoryUrl, childBuildName, artHttpDetails) + defer inttestutils.DeleteBuild(serverDetails.ArtifactoryUrl, childBuildName, artHttpDetails) + + runJfrogCli(t, "rt", "bc", childBuildName, childBuildNumber) + runJfrogCli(t, "docker", "build", "-t", childImageTag, "-f", childDockerfilePath, "--push", + "--build-name="+childBuildName, "--build-number="+childBuildNumber, workspace) + runJfrogCli(t, "rt", "build-publish", childBuildName, childBuildNumber) + + // Step 3: Validate build info has dependencies from the nested path base image + publishedBuildInfo, found, err := tests.GetBuildInfo(serverDetails, childBuildName, childBuildNumber) + assert.NoError(t, err) + assert.True(t, found, "build info was expected to be found") + assert.True(t, len(publishedBuildInfo.BuildInfo.Modules) >= 1, "Expected at least 1 module in build info") + + // Check that dependencies exist (these come from the base image layers) + module := publishedBuildInfo.BuildInfo.Modules[0] + assert.True(t, len(module.Dependencies) > 0, + "Expected dependencies from nested path base image (myorg/base-image). ") + + // Cleanup + inttestutils.ContainerTestCleanup(t, serverDetails, artHttpDetails, nestedBasePath+"/base-image", baseImageBuildName, tests.OciLocalRepo) + inttestutils.ContainerTestCleanup(t, serverDetails, artHttpDetails, "child-image", childBuildName, tests.OciLocalRepo) +} + func TestPushMultiTaggedImage(t *testing.T) { if !*tests.TestDocker { t.Skip("Skipping test. To run it, add the '-test.docker=true' option.") diff --git a/go.mod b/go.mod index 826e04e5c..17436d2f6 100644 --- a/go.mod +++ b/go.mod @@ -19,7 +19,7 @@ require ( github.com/jfrog/build-info-go v1.13.1-0.20260130140656-2d0d5593fccf github.com/jfrog/gofrog v1.7.6 github.com/jfrog/jfrog-cli-application v1.0.2-0.20260202095705-3657239cde88 - github.com/jfrog/jfrog-cli-artifactory v0.8.1-0.20260204122910-dcfb1d59b122 + github.com/jfrog/jfrog-cli-artifactory v0.8.1-0.20260206111107-7a05b41040dd github.com/jfrog/jfrog-cli-core/v2 v2.60.1-0.20260202114755-cec5fd702f50 github.com/jfrog/jfrog-cli-evidence v0.8.3-0.20260202100913-d9ee9476845a github.com/jfrog/jfrog-cli-platform-services v1.10.1-0.20251205121610-171eb9b0000e @@ -259,6 +259,8 @@ replace github.com/ktrysmt/go-bitbucket => github.com/ktrysmt/go-bitbucket v0.9. //replace github.com/jfrog/jfrog-cli-core/v2 => ../jfrog-cli-core +// replace github.com/jfrog/jfrog-cli-artifactory => github.com/fluxxBot/jfrog-cli-artifactory v0.0.0-20260130044429-464a5025d08a + //replace github.com/jfrog/build-info-go => github.com/fluxxBot/build-info-go v1.10.10-0.20260105070825-d3f36f619ba5 //replace github.com/jfrog/jfrog-cli-core/v2 => github.com/fluxxBot/jfrog-cli-core/v2 v2.58.1-0.20260105065921-c6488910f44c diff --git a/go.sum b/go.sum index 4191e2e77..c8431fe46 100644 --- a/go.sum +++ b/go.sum @@ -419,8 +419,8 @@ github.com/jfrog/jfrog-apps-config v1.0.1 h1:mtv6k7g8A8BVhlHGlSveapqf4mJfonwvXYL github.com/jfrog/jfrog-apps-config v1.0.1/go.mod h1:8AIIr1oY9JuH5dylz2S6f8Ym2MaadPLR6noCBO4C22w= github.com/jfrog/jfrog-cli-application v1.0.2-0.20260202095705-3657239cde88 h1:KP6SmrduuGMMsMfHhBATv5I3W1iTtjBojtqEkPHBWk4= github.com/jfrog/jfrog-cli-application v1.0.2-0.20260202095705-3657239cde88/go.mod h1:xum2HquWO5uExa/A7MQs3TgJJVEeoqTR+6Z4mfBr1Xw= -github.com/jfrog/jfrog-cli-artifactory v0.8.1-0.20260204122910-dcfb1d59b122 h1:fapP2b7hTJbrYuy8P/WSo84nwzYK5FnRaxNonD2NOvc= -github.com/jfrog/jfrog-cli-artifactory v0.8.1-0.20260204122910-dcfb1d59b122/go.mod h1:1GKFtQs/5/3HRp1JyMHMQVT9lxzXR+YecO6dWdRThNs= +github.com/jfrog/jfrog-cli-artifactory v0.8.1-0.20260206111107-7a05b41040dd h1:ZKRwlPEkSmul7fBKat7qBVU3tSxd6xg2GbxQixK4COg= +github.com/jfrog/jfrog-cli-artifactory v0.8.1-0.20260206111107-7a05b41040dd/go.mod h1:1GKFtQs/5/3HRp1JyMHMQVT9lxzXR+YecO6dWdRThNs= github.com/jfrog/jfrog-cli-core/v2 v2.60.1-0.20260202114755-cec5fd702f50 h1:u3NvevCPC5ygeOMv39XSlpIOCt7PhLtFWR2XZ0QkKaU= github.com/jfrog/jfrog-cli-core/v2 v2.60.1-0.20260202114755-cec5fd702f50/go.mod h1:+Hnaikp/xCSPD/q7txxRy4Zc0wzjW/usrCSf+6uONSQ= github.com/jfrog/jfrog-cli-evidence v0.8.3-0.20260202100913-d9ee9476845a h1:lTOAhUjKcOmM/0Kbj4V+I/VHPlW7YNAhIEVpGnCM5mI=