Skip to content

Commit 52d1f01

Browse files
authored
feat: [CI-17135]: Add push-only mode and tar file support to Drone Buildx Plugin PR Description (#62)
* introduced push-only support * Updated docker.go and app.go * removed cleanup * Add uts for the util functions
1 parent 5083e2b commit 52d1f01

File tree

3 files changed

+226
-5
lines changed

3 files changed

+226
-5
lines changed

app.go

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ func Run() {
2828
cli.BoolFlag{
2929
Name: "dry-run",
3030
Usage: "dry run disables docker push",
31-
EnvVar: "PLUGIN_DRY_RUN",
31+
EnvVar: "PLUGIN_DRY_RUN, PLUGIN_NO_PUSH",
3232
},
3333
cli.StringFlag{
3434
Name: "remote.url",
@@ -415,6 +415,21 @@ func Run() {
415415
Usage: "additional options to pass directly to the buildx command",
416416
EnvVar: "PLUGIN_BUILDX_OPTIONS",
417417
},
418+
cli.BoolFlag{
419+
Name: "push-only",
420+
Usage: "push only mode, skips build process",
421+
EnvVar: "PLUGIN_PUSH_ONLY",
422+
},
423+
cli.StringFlag{
424+
Name: "source-tar-path",
425+
Usage: "path to Docker image tar file to load and push",
426+
EnvVar: "PLUGIN_SOURCE_TAR_PATH",
427+
},
428+
cli.StringFlag{
429+
Name: "tar-path",
430+
Usage: "path to save Docker image as tar file",
431+
EnvVar: "PLUGIN_TAR_PATH, PLUGIN_DESTINATION_TAR_PATH",
432+
},
418433
}
419434

420435
if err := app.Run(os.Args); err != nil {
@@ -510,6 +525,9 @@ func run(c *cli.Context) error {
510525
BaseImageRegistry: c.String("docker.baseimageregistry"),
511526
BaseImageUsername: c.String("docker.baseimageusername"),
512527
BaseImagePassword: c.String("docker.baseimagepassword"),
528+
PushOnly: c.Bool("push-only"),
529+
SourceTarPath: c.String("source-tar-path"),
530+
TarPath: c.String("tar-path"),
513531
}
514532

515533
if c.Bool("tags.auto") {

docker.go

Lines changed: 147 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,9 @@ type (
112112
BaseImageRegistry string // Docker registry to pull base image
113113
BaseImageUsername string // Docker registry username to pull base image
114114
BaseImagePassword string // Docker registry password to pull base image
115+
PushOnly bool // Push only mode, skips build process
116+
SourceTarPath string // Path to Docker image tar file to load and push
117+
TarPath string // Path to save Docker image as tar file
115118
}
116119

117120
Card []struct {
@@ -349,6 +352,11 @@ func (p Plugin) Exec() error {
349352
}()
350353
}
351354

355+
// Handle push-only mode if requested
356+
if p.PushOnly {
357+
return p.pushOnly()
358+
}
359+
352360
// add proxy build args
353361
addProxyBuildArgs(&p.Build)
354362

@@ -358,7 +366,7 @@ func (p Plugin) Exec() error {
358366
cmds = append(cmds, commandInfo()) // docker info
359367

360368
// Command to build, tag and push
361-
cmds = append(cmds, commandBuildx(p.Build, p.Builder, p.Dryrun, p.MetadataFile)) // docker build
369+
cmds = append(cmds, commandBuildx(p.Build, p.Builder, p.Dryrun, p.MetadataFile, p.TarPath)) // docker build
362370

363371
// execute all commands in batch mode.
364372
for _, cmd := range cmds {
@@ -410,6 +418,38 @@ func (p Plugin) Exec() error {
410418
}
411419
}
412420

421+
if p.TarPath != "" && p.Dryrun {
422+
if len(p.Build.Tags) > 0 {
423+
tag := p.Build.Tags[0]
424+
fullImageName := fmt.Sprintf("%s:%s", p.Build.Repo, tag)
425+
426+
if !imageExists(fullImageName) {
427+
return fmt.Errorf("error: image %s not found in local daemon, cannot save to tar", fullImageName)
428+
}
429+
430+
// Make sure the directory exists
431+
dir := filepath.Dir(p.TarPath)
432+
if err := os.MkdirAll(dir, 0755); err != nil {
433+
return fmt.Errorf("error: failed to create directory for tar file: %v", err)
434+
}
435+
436+
// Save the image
437+
fmt.Println("Saving image to tar:", p.TarPath)
438+
saveCmd := commandSaveTar(fullImageName, p.TarPath)
439+
saveCmd.Stdout = os.Stdout
440+
saveCmd.Stderr = os.Stderr
441+
trace(saveCmd)
442+
443+
if err := saveCmd.Run(); err != nil {
444+
return fmt.Errorf("error: failed to save image to tar: %v", err)
445+
}
446+
447+
fmt.Printf("Successfully saved image to %s\n", p.TarPath)
448+
} else {
449+
return fmt.Errorf("error: cannot save image to tar, no tags specified")
450+
}
451+
}
452+
413453
// output the adaptive card
414454
if p.Builder.Driver == defaultDriver {
415455
if err := p.writeCard(); err != nil {
@@ -559,7 +599,7 @@ func commandInfo() *exec.Cmd {
559599
}
560600

561601
// helper function to create the docker buildx command.
562-
func commandBuildx(build Build, builder Builder, dryrun bool, metadataFile string) *exec.Cmd {
602+
func commandBuildx(build Build, builder Builder, dryrun bool, metadataFile string, tarPath string) *exec.Cmd {
563603
args := []string{
564604
"buildx",
565605
"build",
@@ -576,7 +616,7 @@ func commandBuildx(build Build, builder Builder, dryrun bool, metadataFile strin
576616
args = append(args, "-t", fmt.Sprintf("%s:%s", build.Repo, t))
577617
}
578618
if dryrun {
579-
if build.BuildxLoad {
619+
if build.BuildxLoad || tarPath != "" {
580620
args = append(args, "--load")
581621
}
582622
} else {
@@ -882,6 +922,28 @@ func commandLoad() *exec.Cmd {
882922
return exec.Command(dockerExe, "image", "load")
883923
}
884924

925+
func commandLoadTar(tarPath string) *exec.Cmd {
926+
return exec.Command(dockerExe, "load", "-i", tarPath)
927+
}
928+
929+
func commandSaveTar(tag string, tarPath string) *exec.Cmd {
930+
return exec.Command(dockerExe, "save", "-o", tarPath, tag)
931+
}
932+
933+
func imageExists(tag string) bool {
934+
cmd := exec.Command(dockerExe, "image", "inspect", tag)
935+
return cmd.Run() == nil
936+
}
937+
938+
func getDigestAfterPush(tag string) (string, error) {
939+
cmd := exec.Command(dockerExe, "inspect", "--format", "{{ index (split (index .RepoDigests 0) \"@\") 1 }}", tag)
940+
output, err := cmd.Output()
941+
if err != nil {
942+
return "", fmt.Errorf("failed to get digest for %s: %w", tag, err)
943+
}
944+
return strings.TrimSpace(string(output)), nil
945+
}
946+
885947
func writeSSHPrivateKey(key string) (path string, err error) {
886948
home, err := os.UserHomeDir()
887949
if err != nil {
@@ -913,3 +975,85 @@ func updateImageVersion(driverOpts *[]string, version string) {
913975
}
914976
}
915977
}
978+
979+
// pushOnly handles pushing images without building them
980+
func (p Plugin) pushOnly() error {
981+
// If source tar path is provided, load the image first
982+
if p.SourceTarPath != "" {
983+
fileInfo, err := os.Stat(p.SourceTarPath)
984+
if err != nil {
985+
if os.IsNotExist(err) {
986+
return fmt.Errorf("source image tar file %s does not exist", p.SourceTarPath)
987+
}
988+
return fmt.Errorf("failed to access source image tar file: %w", err)
989+
}
990+
991+
if !fileInfo.Mode().IsRegular() {
992+
return fmt.Errorf("source image tar %s is not a regular file", p.SourceTarPath)
993+
}
994+
995+
fmt.Println("Loading image from tar:", p.SourceTarPath)
996+
loadCmd := commandLoadTar(p.SourceTarPath)
997+
loadCmd.Stdout = os.Stdout
998+
loadCmd.Stderr = os.Stderr
999+
trace(loadCmd)
1000+
if err := loadCmd.Run(); err != nil {
1001+
return fmt.Errorf("failed to load image from tar: %w", err)
1002+
}
1003+
}
1004+
1005+
// For each tag, verify image exists and push
1006+
var digest string
1007+
for _, tag := range p.Build.Tags {
1008+
fullImageName := fmt.Sprintf("%s:%s", p.Build.Repo, tag)
1009+
1010+
// Check if image exists in local daemon
1011+
if !imageExists(fullImageName) {
1012+
return fmt.Errorf("image %s not found, cannot push", fullImageName)
1013+
}
1014+
1015+
// Push image
1016+
fmt.Println("Pushing image:", fullImageName)
1017+
pushCmd := commandPush(p.Build, tag)
1018+
pushCmd.Stdout = os.Stdout
1019+
pushCmd.Stderr = os.Stderr
1020+
trace(pushCmd)
1021+
if err := pushCmd.Run(); err != nil {
1022+
return fmt.Errorf("failed to push image %s: %w", fullImageName, err)
1023+
}
1024+
1025+
// Get the digest after push (we only need one)
1026+
if digest == "" {
1027+
d, err := getDigestAfterPush(fullImageName)
1028+
if err == nil {
1029+
digest = d
1030+
} else {
1031+
fmt.Printf("Warning: Could not get digest for %s: %v\n", fullImageName, err)
1032+
}
1033+
}
1034+
}
1035+
1036+
// Output the adaptive card
1037+
if p.Builder.Driver == defaultDriver {
1038+
if err := p.writeCard(); err != nil {
1039+
fmt.Printf("Could not create adaptive card. %s\n", err)
1040+
}
1041+
}
1042+
1043+
// Write to artifact file
1044+
if p.ArtifactFile != "" && digest != "" {
1045+
if err := drone.WritePluginArtifactFile(
1046+
p.Daemon.RegistryType,
1047+
p.ArtifactFile,
1048+
p.Daemon.ArtifactRegistry,
1049+
p.Build.Repo,
1050+
digest,
1051+
p.Build.Tags,
1052+
); err != nil {
1053+
fmt.Printf("Failed to write plugin artifact file at path: %s with error: %s\n",
1054+
p.ArtifactFile, err)
1055+
}
1056+
}
1057+
1058+
return nil
1059+
}

docker_test.go

Lines changed: 60 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -212,7 +212,7 @@ func TestCommandBuildx(t *testing.T) {
212212
for _, tc := range tcs {
213213
tc := tc
214214
t.Run(tc.name, func(t *testing.T) {
215-
cmd := commandBuildx(tc.build, tc.builder, tc.dryrun, tc.metadata)
215+
cmd := commandBuildx(tc.build, tc.builder, tc.dryrun, tc.metadata, "")
216216
if !reflect.DeepEqual(cmd.String(), tc.want.String()) {
217217
t.Errorf("Got cmd %v, want %v", cmd, tc.want)
218218
}
@@ -339,3 +339,62 @@ func TestGetDigest(t *testing.T) {
339339
})
340340
}
341341
}
342+
343+
func TestCommandLoadTar(t *testing.T) {
344+
tests := []struct {
345+
name string
346+
tarPath string
347+
want *exec.Cmd
348+
}{
349+
{
350+
name: "simple path",
351+
tarPath: "/path/to/image.tar",
352+
want: exec.Command(dockerExe, "load", "-i", "/path/to/image.tar"),
353+
},
354+
{
355+
name: "path with spaces",
356+
tarPath: "/path with spaces/image.tar",
357+
want: exec.Command(dockerExe, "load", "-i", "/path with spaces/image.tar"),
358+
},
359+
}
360+
361+
for _, tt := range tests {
362+
t.Run(tt.name, func(t *testing.T) {
363+
cmd := commandLoadTar(tt.tarPath)
364+
if !reflect.DeepEqual(cmd.String(), tt.want.String()) {
365+
t.Errorf("commandLoadTar() = %v, want %v", cmd, tt.want)
366+
}
367+
})
368+
}
369+
}
370+
371+
func TestCommandSaveTar(t *testing.T) {
372+
tests := []struct {
373+
name string
374+
tag string
375+
tarPath string
376+
want *exec.Cmd
377+
}{
378+
{
379+
name: "simple inputs",
380+
tag: "myimage:latest",
381+
tarPath: "/path/to/output.tar",
382+
want: exec.Command(dockerExe, "save", "-o", "/path/to/output.tar", "myimage:latest"),
383+
},
384+
{
385+
name: "complex registry",
386+
tag: "registry.example.com/project/image:v1.2.3",
387+
tarPath: "/output/dir/image.tar",
388+
want: exec.Command(dockerExe, "save", "-o", "/output/dir/image.tar", "registry.example.com/project/image:v1.2.3"),
389+
},
390+
}
391+
392+
for _, tt := range tests {
393+
t.Run(tt.name, func(t *testing.T) {
394+
cmd := commandSaveTar(tt.tag, tt.tarPath)
395+
if !reflect.DeepEqual(cmd.String(), tt.want.String()) {
396+
t.Errorf("commandSaveTar() = %v, want %v", cmd, tt.want)
397+
}
398+
})
399+
}
400+
}

0 commit comments

Comments
 (0)