Skip to content

Commit f91fa71

Browse files
authored
Merge pull request #13 from drone-plugins/CI-18178
feat: [CI-18178]: Add tar save and load capability with Docker archive support for buildah plugin
2 parents 576fa63 + a84121e commit f91fa71

File tree

2 files changed

+179
-10
lines changed

2 files changed

+179
-10
lines changed

cmd/drone-docker/main.go

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ func main() {
2929
cli.BoolFlag{
3030
Name: "dry-run",
3131
Usage: "dry run disables docker push",
32-
EnvVar: "PLUGIN_DRY_RUN",
32+
EnvVar: "PLUGIN_DRY_RUN,PLUGIN_NO_PUSH",
3333
},
3434
cli.StringFlag{
3535
Name: "remote.url",
@@ -222,6 +222,21 @@ func main() {
222222
Usage: "User Layers",
223223
EnvVar: "PLUGIN_LAYERS",
224224
},
225+
cli.BoolFlag{
226+
Name: "push-only",
227+
Usage: "Push existing Docker images without building",
228+
EnvVar: "PLUGIN_PUSH_ONLY",
229+
},
230+
cli.StringFlag{
231+
Name: "source-tar-path",
232+
Usage: "Path to Docker image tar file to load and push",
233+
EnvVar: "PLUGIN_SOURCE_TAR_PATH",
234+
},
235+
cli.StringFlag{
236+
Name: "tar-path",
237+
Usage: "Path to save Docker image as tar file",
238+
EnvVar: "PLUGIN_TAR_PATH,PLUGIN_DESTINATION_TAR_PATH",
239+
},
225240
}
226241

227242
if err := app.Run(os.Args); err != nil {
@@ -231,8 +246,11 @@ func main() {
231246

232247
func run(c *cli.Context) error {
233248
plugin := docker.Plugin{
234-
Dryrun: c.Bool("dry-run"),
235-
Cleanup: c.BoolT("docker.purge"),
249+
Dryrun: c.Bool("dry-run"),
250+
Cleanup: c.BoolT("docker.purge"),
251+
PushOnly: c.Bool("push-only"),
252+
SourceTarPath: c.String("source-tar-path"),
253+
TarPath: c.String("tar-path"),
236254
Login: docker.Login{
237255
Registry: c.String("docker.registry"),
238256
Username: c.String("docker.username"),

docker.go

Lines changed: 158 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -57,10 +57,13 @@ type (
5757

5858
// Plugin defines the Docker plugin parameters.
5959
Plugin struct {
60-
Login Login // Docker login configuration
61-
Build Build // Docker build configuration
62-
Dryrun bool // Docker push is skipped
63-
Cleanup bool // Docker purge is enabled
60+
Login Login // Docker login configuration
61+
Build Build // Docker build configuration
62+
Dryrun bool // Docker push is skipped
63+
Cleanup bool // Docker purge is enabled
64+
PushOnly bool // Push only mode, skips build process
65+
SourceTarPath string // Path to Docker image tar file to load and push
66+
TarPath string // Path to save Docker image as tar file
6467
}
6568
)
6669

@@ -105,6 +108,11 @@ func (p Plugin) Exec() error {
105108
fmt.Println("Registry credentials or Docker config not provided. Guest mode enabled.")
106109
}
107110

111+
// Check if we're in push-only mode
112+
if p.PushOnly {
113+
return p.pushOnly()
114+
}
115+
108116
// add proxy build args
109117
addProxyBuildArgs(&p.Build)
110118

@@ -122,11 +130,23 @@ func (p Plugin) Exec() error {
122130
for _, tag := range p.Build.Tags {
123131
cmds = append(cmds, commandTag(p.Build, tag)) // docker tag
124132

125-
if p.Dryrun == false {
133+
if !p.Dryrun {
126134
cmds = append(cmds, commandPush(p.Build, tag)) // docker push
127135
}
128136
}
129137

138+
// If TarPath is specified and Dryrun is enabled, save the image to a tar file
139+
if p.TarPath != "" && p.Dryrun && len(p.Build.Tags) > 0 {
140+
// Ensure parent directories exist
141+
if err := os.MkdirAll(filepath.Dir(p.TarPath), 0755); err != nil {
142+
return fmt.Errorf("failed to create parent directories for tar path: %s", err)
143+
}
144+
145+
imageToSave := fmt.Sprintf("%s:%s", p.Build.Repo, p.Build.Tags[0])
146+
fmt.Println("Saving image to tar:", p.TarPath)
147+
cmds = append(cmds, commandSaveTar(imageToSave, p.TarPath))
148+
}
149+
130150
if p.Cleanup {
131151
cmds = append(cmds, commandRmi(p.Build.Name)) // buildah rmi
132152
}
@@ -184,14 +204,14 @@ func commandLoginEmail(login Login) *exec.Cmd {
184204
)
185205
}
186206

187-
// helper function to create the docker info command.
207+
// helper function to create the docker version command.
188208
func commandVersion() *exec.Cmd {
189209
return exec.Command(buildahExe, "version")
190210
}
191211

192212
// helper function to create the docker info command.
193213
func commandInfo() *exec.Cmd {
194-
return exec.Command(buildahExe, "info")
214+
return exec.Command(buildahExe, "--storage-driver", "vfs", "info")
195215
}
196216

197217
// helper function to create the docker build command.
@@ -365,3 +385,134 @@ func commandRmi(tag string) *exec.Cmd {
365385
func trace(cmd *exec.Cmd) {
366386
fmt.Fprintf(os.Stdout, "+ %s\n", strings.Join(cmd.Args, " "))
367387
}
388+
389+
// pushOnly handles pushing images without building them
390+
func (p Plugin) pushOnly() error {
391+
// If source tar path is provided, load the image first
392+
if p.SourceTarPath != "" {
393+
fileInfo, err := os.Stat(p.SourceTarPath)
394+
if err != nil {
395+
if os.IsNotExist(err) {
396+
return fmt.Errorf("source image tar file %s does not exist", p.SourceTarPath)
397+
}
398+
return fmt.Errorf("failed to access source image tar file: %s", err)
399+
}
400+
401+
if !fileInfo.Mode().IsRegular() {
402+
return fmt.Errorf("source image tar %s is not a regular file", p.SourceTarPath)
403+
}
404+
405+
fmt.Println("Loading image from tar:", p.SourceTarPath)
406+
loadCmd := commandLoadTar(p.SourceTarPath)
407+
loadCmd.Stdout = os.Stdout
408+
loadCmd.Stderr = os.Stderr
409+
trace(loadCmd)
410+
if err := loadCmd.Run(); err != nil {
411+
return fmt.Errorf("failed to load image from tar: %s", err)
412+
}
413+
}
414+
415+
// Check for required tags
416+
if len(p.Build.Tags) == 0 {
417+
return fmt.Errorf("no tags specified for push")
418+
}
419+
420+
// Use the repository name as the source image name
421+
sourceImageName := p.Build.Repo
422+
sourceTags := p.Build.Tags
423+
424+
// For each source tag and target tag combination
425+
taggedForPush := make(map[string]bool)
426+
427+
for _, sourceTag := range sourceTags {
428+
sourceFullImageName := fmt.Sprintf("%s:%s", sourceImageName, sourceTag)
429+
430+
// Check if the source image exists in local storage
431+
existsCmd := commandImageExists(sourceFullImageName)
432+
existsCmd.Stdout = nil // suppress output, we only care about the exit code
433+
existsCmd.Stderr = os.Stderr
434+
trace(existsCmd)
435+
436+
if err := existsCmd.Run(); err != nil {
437+
fmt.Printf("Warning: Source image %s not found\n", sourceFullImageName)
438+
// Continue to the next source tag if available, otherwise return error
439+
if len(sourceTags) > 1 {
440+
continue
441+
}
442+
return fmt.Errorf("source image %s not found, cannot push", sourceFullImageName)
443+
}
444+
445+
// For each target tag, tag and push
446+
for _, targetTag := range p.Build.Tags {
447+
targetFullImageName := fmt.Sprintf("%s:%s", p.Build.Repo, targetTag)
448+
449+
// Skip if source and target are identical
450+
if sourceFullImageName == targetFullImageName {
451+
fmt.Printf("Source and target image names are identical: %s\n", sourceFullImageName)
452+
taggedForPush[targetFullImageName] = true
453+
} else {
454+
// Tag the source image with the target name
455+
fmt.Printf("Tagging %s as %s\n", sourceFullImageName, targetFullImageName)
456+
tagCmd := exec.Command(buildahExe, "--storage-driver", "vfs", "tag", sourceFullImageName, targetFullImageName)
457+
tagCmd.Stdout = os.Stdout
458+
tagCmd.Stderr = os.Stderr
459+
trace(tagCmd)
460+
if err := tagCmd.Run(); err == nil {
461+
taggedForPush[targetFullImageName] = true
462+
} else {
463+
fmt.Printf("Warning: Failed to tag %s as %s: %s\n", sourceFullImageName, targetFullImageName, err)
464+
}
465+
}
466+
}
467+
}
468+
469+
// If no images were tagged or found, we can't proceed
470+
if len(taggedForPush) == 0 {
471+
return fmt.Errorf("no images found or tagged for repository %s, cannot push", p.Build.Repo)
472+
}
473+
474+
var cmds []*exec.Cmd
475+
476+
// Push all tagged images
477+
for tag := range taggedForPush {
478+
// Extract tag from the full image name
479+
_, tagOnly, found := strings.Cut(tag, ":")
480+
if !found {
481+
continue
482+
}
483+
484+
// Push the image if not in dry-run mode
485+
if !p.Dryrun {
486+
cmds = append(cmds, commandPush(p.Build, tagOnly))
487+
}
488+
}
489+
490+
// Execute all commands
491+
for _, cmd := range cmds {
492+
cmd.Stdout = os.Stdout
493+
cmd.Stderr = os.Stderr
494+
trace(cmd)
495+
if err := cmd.Run(); err != nil {
496+
return fmt.Errorf("command failed: %s", err)
497+
}
498+
}
499+
500+
return nil
501+
}
502+
503+
504+
505+
// commandLoadTar creates a command to load an image from a tar file
506+
func commandLoadTar(tarPath string) *exec.Cmd {
507+
return exec.Command(buildahExe, "--storage-driver", "vfs", "pull", "docker-archive:"+tarPath)
508+
}
509+
510+
// commandImageExists creates a command to check if an image exists
511+
func commandImageExists(image string) *exec.Cmd {
512+
return exec.Command(buildahExe, "inspect", "--storage-driver", "vfs", "--type", "image", image)
513+
}
514+
515+
// commandSaveTar creates a command to save an image to a tar file
516+
func commandSaveTar(image string, tarPath string) *exec.Cmd {
517+
return exec.Command(buildahExe, "push", "--storage-driver", "vfs", image, "docker-archive:"+tarPath)
518+
}

0 commit comments

Comments
 (0)