Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 14 additions & 7 deletions buildtools/cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -931,31 +931,38 @@ 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
}
}
}
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)
}
Expand Down
169 changes: 169 additions & 0 deletions docker_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 <repository>/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.")
Expand Down
4 changes: 3 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Expand Down
Loading