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
20 changes: 19 additions & 1 deletion app.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ func Run() {
cli.BoolFlag{
Name: "dry-run",
Usage: "dry run disables docker push",
EnvVar: "PLUGIN_DRY_RUN",
EnvVar: "PLUGIN_DRY_RUN, PLUGIN_NO_PUSH",
},
cli.StringFlag{
Name: "remote.url",
Expand Down Expand Up @@ -415,6 +415,21 @@ func Run() {
Usage: "additional options to pass directly to the buildx command",
EnvVar: "PLUGIN_BUILDX_OPTIONS",
},
cli.BoolFlag{
Name: "push-only",
Usage: "push only mode, skips build process",
EnvVar: "PLUGIN_PUSH_ONLY",
},
cli.StringFlag{
Name: "source-tar-path",
Usage: "path to Docker image tar file to load and push",
EnvVar: "PLUGIN_SOURCE_TAR_PATH",
},
cli.StringFlag{
Name: "tar-path",
Usage: "path to save Docker image as tar file",
EnvVar: "PLUGIN_TAR_PATH, PLUGIN_DESTINATION_TAR_PATH",
},
}

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

if c.Bool("tags.auto") {
Expand Down
150 changes: 147 additions & 3 deletions docker.go
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,9 @@ type (
BaseImageRegistry string // Docker registry to pull base image
BaseImageUsername string // Docker registry username to pull base image
BaseImagePassword string // Docker registry password to pull base image
PushOnly bool // Push only mode, skips build process
SourceTarPath string // Path to Docker image tar file to load and push
TarPath string // Path to save Docker image as tar file
}

Card []struct {
Expand Down Expand Up @@ -349,6 +352,11 @@ func (p Plugin) Exec() error {
}()
}

// Handle push-only mode if requested
if p.PushOnly {
return p.pushOnly()
}

// add proxy build args
addProxyBuildArgs(&p.Build)

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

// Command to build, tag and push
cmds = append(cmds, commandBuildx(p.Build, p.Builder, p.Dryrun, p.MetadataFile)) // docker build
cmds = append(cmds, commandBuildx(p.Build, p.Builder, p.Dryrun, p.MetadataFile, p.TarPath)) // docker build

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

if p.TarPath != "" && p.Dryrun {
if len(p.Build.Tags) > 0 {
tag := p.Build.Tags[0]
fullImageName := fmt.Sprintf("%s:%s", p.Build.Repo, tag)

if !imageExists(fullImageName) {
return fmt.Errorf("error: image %s not found in local daemon, cannot save to tar", fullImageName)
}

// Make sure the directory exists
dir := filepath.Dir(p.TarPath)
if err := os.MkdirAll(dir, 0755); err != nil {
return fmt.Errorf("error: failed to create directory for tar file: %v", err)
}

// Save the image
fmt.Println("Saving image to tar:", p.TarPath)
saveCmd := commandSaveTar(fullImageName, p.TarPath)
saveCmd.Stdout = os.Stdout
saveCmd.Stderr = os.Stderr
trace(saveCmd)

if err := saveCmd.Run(); err != nil {
return fmt.Errorf("error: failed to save image to tar: %v", err)
}

fmt.Printf("Successfully saved image to %s\n", p.TarPath)
} else {
return fmt.Errorf("error: cannot save image to tar, no tags specified")
}
}

// output the adaptive card
if p.Builder.Driver == defaultDriver {
if err := p.writeCard(); err != nil {
Expand Down Expand Up @@ -559,7 +599,7 @@ func commandInfo() *exec.Cmd {
}

// helper function to create the docker buildx command.
func commandBuildx(build Build, builder Builder, dryrun bool, metadataFile string) *exec.Cmd {
func commandBuildx(build Build, builder Builder, dryrun bool, metadataFile string, tarPath string) *exec.Cmd {
args := []string{
"buildx",
"build",
Expand All @@ -576,7 +616,7 @@ func commandBuildx(build Build, builder Builder, dryrun bool, metadataFile strin
args = append(args, "-t", fmt.Sprintf("%s:%s", build.Repo, t))
}
if dryrun {
if build.BuildxLoad {
if build.BuildxLoad || tarPath != "" {
args = append(args, "--load")
}
} else {
Expand Down Expand Up @@ -882,6 +922,28 @@ func commandLoad() *exec.Cmd {
return exec.Command(dockerExe, "image", "load")
}

func commandLoadTar(tarPath string) *exec.Cmd {
return exec.Command(dockerExe, "load", "-i", tarPath)
}

func commandSaveTar(tag string, tarPath string) *exec.Cmd {
return exec.Command(dockerExe, "save", "-o", tarPath, tag)
}

func imageExists(tag string) bool {
cmd := exec.Command(dockerExe, "image", "inspect", tag)
return cmd.Run() == nil
}

func getDigestAfterPush(tag string) (string, error) {
cmd := exec.Command(dockerExe, "inspect", "--format", "{{ index (split (index .RepoDigests 0) \"@\") 1 }}", tag)
output, err := cmd.Output()
if err != nil {
return "", fmt.Errorf("failed to get digest for %s: %w", tag, err)
}
return strings.TrimSpace(string(output)), nil
}

func writeSSHPrivateKey(key string) (path string, err error) {
home, err := os.UserHomeDir()
if err != nil {
Expand Down Expand Up @@ -913,3 +975,85 @@ func updateImageVersion(driverOpts *[]string, version string) {
}
}
}

// pushOnly handles pushing images without building them
func (p Plugin) pushOnly() error {
// If source tar path is provided, load the image first
if p.SourceTarPath != "" {
fileInfo, err := os.Stat(p.SourceTarPath)
if err != nil {
if os.IsNotExist(err) {
return fmt.Errorf("source image tar file %s does not exist", p.SourceTarPath)
}
return fmt.Errorf("failed to access source image tar file: %w", err)
}

if !fileInfo.Mode().IsRegular() {
return fmt.Errorf("source image tar %s is not a regular file", p.SourceTarPath)
}

fmt.Println("Loading image from tar:", p.SourceTarPath)
loadCmd := commandLoadTar(p.SourceTarPath)
loadCmd.Stdout = os.Stdout
loadCmd.Stderr = os.Stderr
trace(loadCmd)
if err := loadCmd.Run(); err != nil {
return fmt.Errorf("failed to load image from tar: %w", err)
}
}

// For each tag, verify image exists and push
var digest string
for _, tag := range p.Build.Tags {
fullImageName := fmt.Sprintf("%s:%s", p.Build.Repo, tag)

// Check if image exists in local daemon
if !imageExists(fullImageName) {
return fmt.Errorf("image %s not found, cannot push", fullImageName)
}

// Push image
fmt.Println("Pushing image:", fullImageName)
pushCmd := commandPush(p.Build, tag)
pushCmd.Stdout = os.Stdout
pushCmd.Stderr = os.Stderr
trace(pushCmd)
if err := pushCmd.Run(); err != nil {
return fmt.Errorf("failed to push image %s: %w", fullImageName, err)
}

// Get the digest after push (we only need one)
if digest == "" {
d, err := getDigestAfterPush(fullImageName)
if err == nil {
digest = d
} else {
fmt.Printf("Warning: Could not get digest for %s: %v\n", fullImageName, err)
}
}
}

// Output the adaptive card
if p.Builder.Driver == defaultDriver {
if err := p.writeCard(); err != nil {
fmt.Printf("Could not create adaptive card. %s\n", err)
}
}

// Write to artifact file
if p.ArtifactFile != "" && digest != "" {
if err := drone.WritePluginArtifactFile(
p.Daemon.RegistryType,
p.ArtifactFile,
p.Daemon.ArtifactRegistry,
p.Build.Repo,
digest,
p.Build.Tags,
); err != nil {
fmt.Printf("Failed to write plugin artifact file at path: %s with error: %s\n",
p.ArtifactFile, err)
}
}

return nil
}
61 changes: 60 additions & 1 deletion docker_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -212,7 +212,7 @@ func TestCommandBuildx(t *testing.T) {
for _, tc := range tcs {
tc := tc
t.Run(tc.name, func(t *testing.T) {
cmd := commandBuildx(tc.build, tc.builder, tc.dryrun, tc.metadata)
cmd := commandBuildx(tc.build, tc.builder, tc.dryrun, tc.metadata, "")
if !reflect.DeepEqual(cmd.String(), tc.want.String()) {
t.Errorf("Got cmd %v, want %v", cmd, tc.want)
}
Expand Down Expand Up @@ -339,3 +339,62 @@ func TestGetDigest(t *testing.T) {
})
}
}

func TestCommandLoadTar(t *testing.T) {
tests := []struct {
name string
tarPath string
want *exec.Cmd
}{
{
name: "simple path",
tarPath: "/path/to/image.tar",
want: exec.Command(dockerExe, "load", "-i", "/path/to/image.tar"),
},
{
name: "path with spaces",
tarPath: "/path with spaces/image.tar",
want: exec.Command(dockerExe, "load", "-i", "/path with spaces/image.tar"),
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
cmd := commandLoadTar(tt.tarPath)
if !reflect.DeepEqual(cmd.String(), tt.want.String()) {
t.Errorf("commandLoadTar() = %v, want %v", cmd, tt.want)
}
})
}
}

func TestCommandSaveTar(t *testing.T) {
tests := []struct {
name string
tag string
tarPath string
want *exec.Cmd
}{
{
name: "simple inputs",
tag: "myimage:latest",
tarPath: "/path/to/output.tar",
want: exec.Command(dockerExe, "save", "-o", "/path/to/output.tar", "myimage:latest"),
},
{
name: "complex registry",
tag: "registry.example.com/project/image:v1.2.3",
tarPath: "/output/dir/image.tar",
want: exec.Command(dockerExe, "save", "-o", "/output/dir/image.tar", "registry.example.com/project/image:v1.2.3"),
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
cmd := commandSaveTar(tt.tag, tt.tarPath)
if !reflect.DeepEqual(cmd.String(), tt.want.String()) {
t.Errorf("commandSaveTar() = %v, want %v", cmd, tt.want)
}
})
}
}