diff --git a/artifactory/cli/cli.go b/artifactory/cli/cli.go index 42b72213..b2c24d98 100644 --- a/artifactory/cli/cli.go +++ b/artifactory/cli/cli.go @@ -1,6 +1,10 @@ package cli import ( + "os" + "strconv" + "strings" + ioutils "github.com/jfrog/gofrog/io" "github.com/jfrog/jfrog-cli-artifactory/artifactory/commands/buildinfo" "github.com/jfrog/jfrog-cli-artifactory/artifactory/commands/container" @@ -67,9 +71,6 @@ import ( "github.com/jfrog/jfrog-client-go/utils/errorutils" "github.com/jfrog/jfrog-client-go/utils/log" "github.com/pkg/errors" - "os" - "strconv" - "strings" ) const ( @@ -483,6 +484,7 @@ func containerPushCmd(c *components.Context, containerManagerType containerutils imageTag := c.GetArgumentAt(0) targetRepo := c.GetArgumentAt(1) skipLogin := c.GetBoolFlagValue("skip-login") + validateSha := c.GetBoolFlagValue("validate-sha") buildConfiguration, err := common.CreateBuildConfigurationWithModule(c) if err != nil { @@ -494,7 +496,7 @@ func containerPushCmd(c *components.Context, containerManagerType containerutils return } printDeploymentView, detailedSummary := log.IsStdErrTerminal(), c.GetBoolFlagValue("detailed-summary") - dockerPushCommand.SetThreads(threads).SetDetailedSummary(detailedSummary || printDeploymentView).SetCmdParams([]string{"push", imageTag}).SetSkipLogin(skipLogin).SetBuildConfiguration(buildConfiguration).SetRepo(targetRepo).SetServerDetails(artDetails).SetImageTag(imageTag) + dockerPushCommand.SetThreads(threads).SetDetailedSummary(detailedSummary || printDeploymentView).SetCmdParams([]string{"push", imageTag}).SetSkipLogin(skipLogin).SetBuildConfiguration(buildConfiguration).SetRepo(targetRepo).SetServerDetails(artDetails).SetImageTag(imageTag).SetValidateSha(validateSha) err = commandWrappers.ShowDockerDeprecationMessageIfNeeded(containerManagerType, dockerPushCommand.IsGetRepoSupported) if err != nil { return diff --git a/artifactory/commands/container/containermanagerbase.go b/artifactory/commands/container/containermanagerbase.go index 9169d83d..9f690558 100644 --- a/artifactory/commands/container/containermanagerbase.go +++ b/artifactory/commands/container/containermanagerbase.go @@ -19,6 +19,7 @@ type ContainerCommandBase struct { repo string buildConfiguration *build.BuildConfiguration serverDetails *config.ServerDetails + validateSha bool } func (ccb *ContainerCommandBase) ImageTag() string { @@ -30,6 +31,15 @@ func (ccb *ContainerCommandBase) SetImageTag(imageTag string) *ContainerCommandB return ccb } +func (ccb *ContainerCommandBase) SetValidateSha(validateSha bool) *ContainerCommandBase { + ccb.validateSha = validateSha + return ccb +} + +func (ccb *ContainerCommandBase) IsValidateSha() bool { + return ccb.validateSha +} + // Returns the repository name that contains this image. func (ccb *ContainerCommandBase) GetRepo() (string, error) { // The repository name is saved after first calling this function. diff --git a/artifactory/commands/container/push.go b/artifactory/commands/container/push.go index 1179c4f4..2af33e82 100644 --- a/artifactory/commands/container/push.go +++ b/artifactory/commands/container/push.go @@ -1,17 +1,19 @@ package container import ( + "fmt" "path" commandsutils "github.com/jfrog/jfrog-cli-core/v2/artifactory/commands/utils" "github.com/jfrog/jfrog-cli-core/v2/artifactory/utils" - "github.com/jfrog/jfrog-cli-core/v2/artifactory/utils/container" + containerutils "github.com/jfrog/jfrog-cli-core/v2/artifactory/utils/container" "github.com/jfrog/jfrog-cli-core/v2/common/build" "github.com/jfrog/jfrog-cli-core/v2/utils/config" servicesutils "github.com/jfrog/jfrog-client-go/artifactory/services/utils" clientutils "github.com/jfrog/jfrog-client-go/utils" "github.com/jfrog/jfrog-client-go/utils/errorutils" "github.com/jfrog/jfrog-client-go/utils/io/content" + "github.com/jfrog/jfrog-client-go/utils/log" ) type PushCommand struct { @@ -21,7 +23,7 @@ type PushCommand struct { result *commandsutils.Result } -func NewPushCommand(containerManagerType container.ContainerManagerType) *PushCommand { +func NewPushCommand(containerManagerType containerutils.ContainerManagerType) *PushCommand { return &PushCommand{ ContainerCommand: ContainerCommand{ containerManagerType: containerManagerType, @@ -47,6 +49,15 @@ func (pc *PushCommand) IsDetailedSummary() bool { return pc.detailedSummary } +func (pc *PushCommand) SetValidateSha(validateSha bool) *PushCommand { + pc.ContainerCommandBase.SetValidateSha(validateSha) + return pc +} + +func (pc *PushCommand) IsValidateSha() bool { + return pc.ContainerCommandBase.IsValidateSha() +} + func (pc *PushCommand) Result() *commandsutils.Result { return pc.result } @@ -60,8 +71,8 @@ func (pc *PushCommand) Run() error { if err := pc.init(); err != nil { return err } - if pc.containerManagerType == container.DockerClient { - err := container.ValidateClientApiVersion() + if pc.containerManagerType == containerutils.DockerClient { + err := containerutils.ValidateClientApiVersion() if err != nil { return err } @@ -75,11 +86,12 @@ func (pc *PushCommand) Run() error { return err } // Perform push. - cm := container.NewManager(pc.containerManagerType) + cm := containerutils.NewManager(pc.containerManagerType) err = cm.RunNativeCmd(pc.cmdParams) if err != nil { return err } + toCollect, err := pc.buildConfiguration.IsCollectBuildInfo() if err != nil { return err @@ -103,10 +115,57 @@ func (pc *PushCommand) Run() error { if err != nil { return err } - builder, err := container.NewLocalAgentBuildInfoBuilder(pc.image, repo, buildName, buildNumber, pc.BuildConfiguration().GetProject(), serviceManager, container.Push, cm) + + // If SHA validation is enabled, log it + if pc.IsValidateSha() { + log.Info("Performing SHA-based validation for Docker push...") + // Get image SHA from the container manager + imageSha256, err := cm.Id(pc.image) + if err != nil { + return err + } + log.Debug("Using image SHA256 for validation: " + imageSha256) + + // Use RemoteAgentBuildInfoBuilder for SHA-based validation + remoteBuilder, err := containerutils.NewRemoteAgentBuildInfoBuilder(pc.image, repo, buildName, buildNumber, pc.BuildConfiguration().GetProject(), serviceManager, imageSha256) + if err != nil { + return err + } + + if toCollect { + if err := build.SaveBuildGeneralDetails(buildName, buildNumber, pc.buildConfiguration.GetProject()); err != nil { + return err + } + buildInfoModule, err := remoteBuilder.Build(pc.BuildConfiguration().GetModule()) + if err != nil { + return err + } + if buildInfoModule == nil { + return errorutils.CheckError(fmt.Errorf("failed to create build info module: module is nil")) + } + if err = build.SaveBuildInfo(buildName, buildNumber, pc.BuildConfiguration().GetProject(), buildInfoModule); err != nil { + return errorutils.CheckError(fmt.Errorf("failed to save build info: %w", err)) + } + } + + if pc.IsDetailedSummary() { + if !toCollect { + // No build info collection triggered yet, build it now for the summary + if _, err = remoteBuilder.Build(""); err != nil { + return errorutils.CheckError(fmt.Errorf("failed to build summary info: %w", err)) + } + } + return pc.layersMapToFileTransferDetails(serverDetails.ArtifactoryUrl, remoteBuilder.GetLayers()) + } + return nil + } + + // Standard path: use LocalAgentBuildInfoBuilder for tag-based validation + builder, err := containerutils.NewLocalAgentBuildInfoBuilder(pc.image, repo, buildName, buildNumber, pc.BuildConfiguration().GetProject(), serviceManager, containerutils.Push, cm) if err != nil { return err } + if toCollect { if err := build.SaveBuildGeneralDetails(buildName, buildNumber, pc.buildConfiguration.GetProject()); err != nil { return err @@ -119,10 +178,11 @@ func (pc *PushCommand) Run() error { return err } } + if pc.IsDetailedSummary() { if !toCollect { // The build-info collection hasn't been triggered at this point, and we do need it for handling the detailed summary. - // We are therefore skipping setting mage build name/number props before running build-info collection. + // We are therefore skipping setting image build name/number props before running build-info collection. builder.SetSkipTaggingLayers(true) _, err = builder.Build("") if err != nil { @@ -131,6 +191,7 @@ func (pc *PushCommand) Run() error { } return pc.layersMapToFileTransferDetails(serverDetails.ArtifactoryUrl, builder.GetLayers()) } + return nil } @@ -163,3 +224,5 @@ func (pc *PushCommand) CommandName() string { func (pc *PushCommand) ServerDetails() (*config.ServerDetails, error) { return pc.serverDetails, nil } + +// Backward compatibility: If --validate-sha is not set, the legacy tag-based validation path is used, ensuring no change for existing users. diff --git a/cliutils/flagkit/flags.go b/cliutils/flagkit/flags.go index 7b3c1bcd..a7ce503a 100644 --- a/cliutils/flagkit/flags.go +++ b/cliutils/flagkit/flags.go @@ -1,11 +1,12 @@ package flagkit import ( + "strconv" + "github.com/jfrog/jfrog-cli-artifactory/cliutils/cmddefs" commonCliUtils "github.com/jfrog/jfrog-cli-core/v2/common/cliutils" pluginsCommon "github.com/jfrog/jfrog-cli-core/v2/plugins/common" "github.com/jfrog/jfrog-cli-core/v2/plugins/components" - "strconv" ) const ( @@ -323,6 +324,7 @@ const ( // Build tool flags deploymentThreads = "deployment-threads" skipLogin = "skip-login" + validateSha = "validate-sha" // Unique docker promote flags dockerPromotePrefix = "docker-promote-" @@ -654,7 +656,7 @@ var commandFlags = map[string][]string{ }, ContainerPush: { BuildName, BuildNumber, module, url, user, password, accessToken, sshPassphrase, sshKeyPath, - serverId, skipLogin, threads, Project, detailedSummary, + serverId, skipLogin, threads, Project, detailedSummary, validateSha, }, ContainerPull: { BuildName, BuildNumber, module, url, user, password, accessToken, sshPassphrase, sshKeyPath, @@ -972,6 +974,7 @@ var flagsMap = map[string]components.Flag{ // Docker specific commands flags skipLogin: components.NewBoolFlag(skipLogin, "[Default: false] Set to true if you'd like the command to skip performing docker login.", components.WithBoolDefaultValueFalse()), + validateSha: components.NewBoolFlag(validateSha, "[Default: false] Set to true to enable SHA validation during Docker push.", components.WithBoolDefaultValueFalse()), watches: components.NewStringFlag(watches, "[Optional] A comma-separated(,) list of Xray watches, to determine Xray's violations creation.", components.SetMandatoryFalse()), repoPath: components.NewStringFlag(repoPath, "[Optional] Target repo path, to enable Xray to determine watches accordingly.", components.SetMandatoryFalse()), licenses: components.NewBoolFlag(licenses, "[Default: false] Set to true if you'd like to receive licenses from Xray scanning.", components.WithBoolDefaultValueFalse()), diff --git a/go.mod b/go.mod index bbf543fe..c16eccc7 100644 --- a/go.mod +++ b/go.mod @@ -132,7 +132,7 @@ require ( sigs.k8s.io/yaml v1.4.0 // indirect ) -//replace github.com/jfrog/jfrog-cli-core/v2 => github.com/jfrog/jfrog-cli-core/v2 v2.31.1-0.20250429081008-4c70b8d467b9 +replace github.com/jfrog/jfrog-cli-core/v2 => github.com/jfrog/jfrog-cli-core/v2 v2.31.1-0.20250527091824-60a3b4b741aa //replace github.com/jfrog/jfrog-client-go => github.com/jfrog/jfrog-client-go v1.28.1-0.20250508130334-f159cff9b11a diff --git a/go.sum b/go.sum index 00b5e308..68f187c6 100644 --- a/go.sum +++ b/go.sum @@ -137,8 +137,8 @@ github.com/jfrog/froggit-go v1.17.0 h1:20Ie787WO27SwB2MOHDvsR6yN7fA5WfRnuAbmUqz1 github.com/jfrog/froggit-go v1.17.0/go.mod h1:HvDkfFfJwIdsXFdqaB+utvD2cLDRmaC3kF8otYb6Chw= github.com/jfrog/gofrog v1.7.6 h1:QmfAiRzVyaI7JYGsB7cxfAJePAZTzFz0gRWZSE27c6s= github.com/jfrog/gofrog v1.7.6/go.mod h1:ntr1txqNOZtHplmaNd7rS4f8jpA5Apx8em70oYEe7+4= -github.com/jfrog/jfrog-cli-core/v2 v2.58.7 h1:njRlkJjNZ1cvG25S/6T4h+ouI+ZRABN6xZN87UIzB/M= -github.com/jfrog/jfrog-cli-core/v2 v2.58.7/go.mod h1:ZXcipUeTTEQ/phqHdbCh4wJ5Oo4QVDxzQBREQ0J9mDc= +github.com/jfrog/jfrog-cli-core/v2 v2.31.1-0.20250527091824-60a3b4b741aa h1:ybm7alLNcgeVRaM4a4Ma1kjzFN5jxe5yIMvdmDATEWM= +github.com/jfrog/jfrog-cli-core/v2 v2.31.1-0.20250527091824-60a3b4b741aa/go.mod h1:ZXcipUeTTEQ/phqHdbCh4wJ5Oo4QVDxzQBREQ0J9mDc= github.com/jfrog/jfrog-client-go v1.53.1 h1:GDRLUDs6hhfGNjqbI+bjc3ApgBHnpVwURM+f26PVfyw= github.com/jfrog/jfrog-client-go v1.53.1/go.mod h1:XxYs2QtlTm92yqJ5O4j4vzWI8d4sDtKQUT1miNHMgnw= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=