diff --git a/internal/cmd/dockerpush/main.go b/internal/cmd/dockerpush/main.go index 33effc955..d09b32bd7 100644 --- a/internal/cmd/dockerpush/main.go +++ b/internal/cmd/dockerpush/main.go @@ -39,14 +39,12 @@ func run(basedir string, dockerOrg string) error { return nil // nothing to push } for _, includedPlugin := range includedPlugins { - output, err := docker.Push(ctx, includedPlugin, dockerOrg) - if err != nil { + if err := docker.Push(ctx, includedPlugin, dockerOrg); err != nil { log.Printf( - "docker push of plugin %s:%s failed with err %v:\noutput:\n%s", + "docker push of plugin %s:%s failed: %v", includedPlugin.Name, includedPlugin.PluginVersion, err, - string(output), ) return err } diff --git a/internal/docker/push.go b/internal/docker/push.go index d3854a5da..155448566 100644 --- a/internal/docker/push.go +++ b/internal/docker/push.go @@ -2,20 +2,50 @@ package docker import ( "context" + "errors" + "fmt" + "os" "os/exec" + "github.com/google/go-containerregistry/pkg/authn" + "github.com/google/go-containerregistry/pkg/name" + "github.com/google/go-containerregistry/pkg/v1/remote" + "github.com/google/go-containerregistry/pkg/v1/tarball" + "github.com/bufbuild/plugins/internal/plugin" ) // Push pushes a docker image for the given plugin to the Docker organization. // It assumes it has already been built in a previous step. -func Push(ctx context.Context, plugin *plugin.Plugin, dockerOrg string) ([]byte, error) { - imageName := ImageName(plugin, dockerOrg) - cmd := exec.CommandContext( - ctx, - "docker", - "push", - imageName, - ) - return cmd.CombinedOutput() +// +// Images are saved from the local Docker daemon via "docker save" and pushed +// using go-containerregistry to preserve Docker distribution manifest v2 format. +func Push(ctx context.Context, pluginToPush *plugin.Plugin, dockerOrg string) (retErr error) { + imageName := ImageName(pluginToPush, dockerOrg) + tmpFile, err := os.CreateTemp("", "plugin-image-*.tar") + if err != nil { + return fmt.Errorf("create temp file: %w", err) + } + defer func() { + retErr = errors.Join(retErr, os.Remove(tmpFile.Name())) + }() + if err := tmpFile.Close(); err != nil { + return fmt.Errorf("close temp file: %w", err) + } + cmd := exec.CommandContext(ctx, "docker", "save", imageName, "-o", tmpFile.Name()) + if output, err := cmd.CombinedOutput(); err != nil { + return fmt.Errorf("docker save %q: %w\noutput: %s", imageName, err, output) + } + image, err := tarball.ImageFromPath(tmpFile.Name(), nil) + if err != nil { + return fmt.Errorf("load image from tarball: %w", err) + } + tag, err := name.NewTag(imageName) + if err != nil { + return fmt.Errorf("parse image reference %q: %w", imageName, err) + } + if err := remote.Write(tag, image, remote.WithAuthFromKeychain(authn.DefaultKeychain), remote.WithContext(ctx)); err != nil { + return fmt.Errorf("push image %q: %w", imageName, err) + } + return nil }