Merge f936dac379 into a77024aad4
This commit is contained in:
commit
cea884d760
7 changed files with 295 additions and 0 deletions
71
cmd/collect_docker.go
Normal file
71
cmd/collect_docker.go
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/Snider/Borg/pkg/docker"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// collectDockerCmd represents the collect docker command
|
||||
var collectDockerCmd = NewCollectDockerCmd()
|
||||
|
||||
func init() {
|
||||
GetCollectCmd().AddCommand(GetCollectDockerCmd())
|
||||
}
|
||||
|
||||
func GetCollectDockerCmd() *cobra.Command {
|
||||
return collectDockerCmd
|
||||
}
|
||||
|
||||
func NewCollectDockerCmd() *cobra.Command {
|
||||
collectDockerCmd := &cobra.Command{
|
||||
Use: "docker [image]",
|
||||
Short: "Collect a Docker image",
|
||||
Long: `Collect a Docker image and save it as an OCI tarball.`,
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
imageRef := args[0]
|
||||
outputFile, err := cmd.Flags().GetString("output")
|
||||
if err != nil {
|
||||
return fmt.Errorf("error getting output flag: %w", err)
|
||||
}
|
||||
allTags, err := cmd.Flags().GetBool("all-tags")
|
||||
if err != nil {
|
||||
return fmt.Errorf("error getting all-tags flag: %w", err)
|
||||
}
|
||||
platform, err := cmd.Flags().GetString("platform")
|
||||
if err != nil {
|
||||
return fmt.Errorf("error getting platform flag: %w", err)
|
||||
}
|
||||
registry, err := cmd.Flags().GetString("registry")
|
||||
if err != nil {
|
||||
return fmt.Errorf("error getting registry flag: %w", err)
|
||||
}
|
||||
|
||||
if outputFile == "" && !allTags {
|
||||
// Create a default output file name from the image ref
|
||||
// by replacing slashes and colons with underscores.
|
||||
// e.g., letheanmovement/chain:v1.0.0 -> letheanmovement_chain_v1.0.0.tar
|
||||
safeRef := strings.ReplaceAll(imageRef, "/", "_")
|
||||
safeRef = strings.ReplaceAll(safeRef, ":", "_")
|
||||
outputFile = safeRef + ".tar"
|
||||
}
|
||||
|
||||
err := docker.Collect(imageRef, outputFile, allTags, platform, registry)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error collecting docker image: %w", err)
|
||||
}
|
||||
|
||||
fmt.Fprintln(cmd.OutOrStdout(), "Docker image saved to", outputFile)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
collectDockerCmd.Flags().StringP("output", "o", "", "Output file for the OCI tarball")
|
||||
collectDockerCmd.Flags().Bool("all-tags", false, "Collect all available tags")
|
||||
collectDockerCmd.Flags().String("platform", "", "Specific platform (e.g., linux/amd64)")
|
||||
collectDockerCmd.Flags().String("registry", "", "Custom registry URL")
|
||||
|
||||
return collectDockerCmd
|
||||
}
|
||||
78
cmd/collect_docker_test.go
Normal file
78
cmd/collect_docker_test.go
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
package cmd
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestCollectDockerCmd_Good(t *testing.T) {
|
||||
t.Run("Good", func(t *testing.T) {
|
||||
t.Cleanup(func() {
|
||||
// Reset the command's state after the test.
|
||||
RootCmd.SetArgs([]string{})
|
||||
})
|
||||
// Use a small, public image for testing
|
||||
imageRef := "hello-world"
|
||||
|
||||
// Create a temporary directory to store the output
|
||||
tmpDir := t.TempDir()
|
||||
outputFile := filepath.Join(tmpDir, "hello-world.tar")
|
||||
|
||||
// Execute the command
|
||||
output, err := executeCommand(RootCmd, "collect", "docker", imageRef, "--output", outputFile)
|
||||
if err != nil {
|
||||
t.Fatalf("executeCommand() returned an unexpected error: %v, output: %s", err, output)
|
||||
}
|
||||
|
||||
// Check if the output file was created
|
||||
fileInfo, err := os.Stat(outputFile)
|
||||
if os.IsNotExist(err) {
|
||||
t.Fatalf("collect docker command did not create the output file: %s", outputFile)
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatalf("error stating output file: %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Platform", func(t *testing.T) {
|
||||
t.Cleanup(func() {
|
||||
// Reset the command's state after the test.
|
||||
RootCmd.SetArgs([]string{})
|
||||
})
|
||||
// Use a multi-platform image for testing
|
||||
imageRef := "nginx"
|
||||
platform := "linux/arm64"
|
||||
|
||||
// Create a temporary directory to store the output
|
||||
tmpDir := t.TempDir()
|
||||
outputFile := filepath.Join(tmpDir, "nginx.tar")
|
||||
|
||||
// Execute the command
|
||||
output, err := executeCommand(RootCmd, "collect", "docker", imageRef, "--output", outputFile, "--platform", platform)
|
||||
if err != nil {
|
||||
t.Fatalf("executeCommand() returned an unexpected error: %v, output: %s", err, output)
|
||||
}
|
||||
|
||||
// Check if the output file was created
|
||||
fileInfo, err := os.Stat(outputFile)
|
||||
if os.IsNotExist(err) {
|
||||
t.Fatalf("collect docker command did not create the output file: %s", outputFile)
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatalf("error stating output file: %v", err)
|
||||
}
|
||||
|
||||
// Check if the file is not empty
|
||||
if fileInfo.Size() == 0 {
|
||||
t.Errorf("the created output file is empty")
|
||||
}
|
||||
|
||||
// Check for the success message in the output
|
||||
expectedOutput := "Docker image saved to"
|
||||
if !strings.Contains(output, expectedOutput) {
|
||||
t.Errorf("expected output to contain %q, but got %q", expectedOutput, output)
|
||||
}
|
||||
})
|
||||
}
|
||||
1
go.mod
1
go.mod
|
|
@ -31,6 +31,7 @@ require (
|
|||
github.com/go-ole/go-ole v1.3.0 // indirect
|
||||
github.com/godbus/dbus/v5 v5.1.0 // indirect
|
||||
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
|
||||
github.com/google/go-containerregistry v0.20.7 // indirect
|
||||
github.com/google/go-querystring v1.1.0 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/gorilla/websocket v1.5.3 // indirect
|
||||
|
|
|
|||
2
go.sum
2
go.sum
|
|
@ -51,6 +51,8 @@ github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
|
|||
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/go-containerregistry v0.20.7 h1:24VGNpS0IwrOZ2ms2P1QE3Xa5X9p4phx0aUgzYzHW6I=
|
||||
github.com/google/go-containerregistry v0.20.7/go.mod h1:Lx5LCZQjLH1QBaMPeGwsME9biPeo1lPx6lbGj/UmzgM=
|
||||
github.com/google/go-github/v39 v39.2.0 h1:rNNM311XtPOz5rDdsJXAp2o8F67X9FnROXTvto3aSnQ=
|
||||
github.com/google/go-github/v39 v39.2.0/go.mod h1:C1s8C5aCC9L+JXIYpJM5GYytdX52vC1bLvHEF1IhBrE=
|
||||
github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
|
||||
|
|
|
|||
14
go.work.sum
14
go.work.sum
|
|
@ -30,9 +30,17 @@ github.com/charmbracelet/x/ansi v0.1.4 h1:IEU3D6+dWwPSgZ6HBH+v6oUuZ/nVawMiWj5831
|
|||
github.com/charmbracelet/x/ansi v0.1.4/go.mod h1:dk73KoMTT5AX5BsX0KrqhsTqAnhZZoCBjs7dGWp4Ktw=
|
||||
github.com/containerd/console v1.0.3 h1:lIr7SlA5PxZyMV30bDW0MGbiOPXwc63yRuCP0ARubLw=
|
||||
github.com/containerd/console v1.0.3/go.mod h1:7LqA/THxQ86k76b8c/EMSiaJ3h1eZkMkXar0TQ1gf3U=
|
||||
github.com/containerd/stargz-snapshotter/estargz v0.18.1 h1:cy2/lpgBXDA3cDKSyEfNOFMA/c10O1axL69EU7iirO8=
|
||||
github.com/containerd/stargz-snapshotter/estargz v0.18.1/go.mod h1:ALIEqa7B6oVDsrF37GkGN20SuvG/pIMm7FwP7ZmRb0Q=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.6 h1:XJtiaUW6dEEqVuZiMTn1ldk455QWwEIsMIJlo5vtkx0=
|
||||
github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI=
|
||||
github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
||||
github.com/docker/cli v29.0.3+incompatible h1:8J+PZIcF2xLd6h5sHPsp5pvvJA+Sr2wGQxHkRl53a1E=
|
||||
github.com/docker/cli v29.0.3+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
|
||||
github.com/docker/distribution v2.8.3+incompatible h1:AtKxIZ36LoNK51+Z6RpzLpddBirtxJnzDrHLEKxTAYk=
|
||||
github.com/docker/distribution v2.8.3+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
|
||||
github.com/docker/docker-credential-helpers v0.9.3 h1:gAm/VtF9wgqJMoxzT3Gj5p4AqIjCBS4wrsOh9yRqcz8=
|
||||
github.com/docker/docker-credential-helpers v0.9.3/go.mod h1:x+4Gbw9aGmChi3qTLZj8Dfn0TD20M/fuWy0E5+WDeCo=
|
||||
github.com/flytam/filenamify v1.2.0 h1:7RiSqXYR4cJftDQ5NuvljKMfd/ubKnW/j9C6iekChgI=
|
||||
github.com/flytam/filenamify v1.2.0/go.mod h1:Dzf9kVycwcsBlr2ATg6uxjqiFgKGH+5SKFuhdeP5zu8=
|
||||
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
|
||||
|
|
@ -78,6 +86,10 @@ github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6
|
|||
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8=
|
||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
|
||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
|
||||
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
|
||||
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
|
||||
github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040=
|
||||
github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M=
|
||||
github.com/pterm/pterm v0.12.80 h1:mM55B+GnKUnLMUSqhdINe4s6tOuVQIetQ3my8JGyAIg=
|
||||
github.com/pterm/pterm v0.12.80/go.mod h1:c6DeF9bSnOSeFPZlfs4ZRAFcf5SCoTwvwQ5xaKGQlHo=
|
||||
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
|
||||
|
|
@ -98,6 +110,8 @@ github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs=
|
|||
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
|
||||
github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY=
|
||||
github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28=
|
||||
github.com/vbatts/tar-split v0.12.2 h1:w/Y6tjxpeiFMR47yzZPlPj/FcPLpXbTUi/9H7d3CPa4=
|
||||
github.com/vbatts/tar-split v0.12.2/go.mod h1:eF6B6i6ftWQcDqEn3/iGFRFRo8cBIMSJVOpnNdfTMFA=
|
||||
github.com/wzshiming/ctc v1.2.3 h1:q+hW3IQNsjIlOFBTGZZZeIXTElFM4grF4spW/errh/c=
|
||||
github.com/wzshiming/ctc v1.2.3/go.mod h1:2tVAtIY7SUyraSk0JxvwmONNPFL4ARavPuEsg5+KA28=
|
||||
github.com/wzshiming/winseq v0.0.0-20200112104235-db357dc107ae h1:tpXvBXC3hpQBDCc9OojJZCQMVRAbT3TTdUMP8WguXkY=
|
||||
|
|
|
|||
79
pkg/docker/docker.go
Normal file
79
pkg/docker/docker.go
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
package docker
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/google/go-containerregistry/pkg/authn"
|
||||
"github.com/google/go-containerregistry/pkg/crane"
|
||||
v1 "github.com/google/go-containerregistry/pkg/v1"
|
||||
)
|
||||
|
||||
// Collect pulls a Docker image and saves it as an OCI tarball.
|
||||
func Collect(imageRef, outputFile string, allTags bool, platform, registry string) error {
|
||||
if outputFile == "" && !allTags {
|
||||
return fmt.Errorf("output file not specified")
|
||||
}
|
||||
|
||||
if registry != "" {
|
||||
imageRef = fmt.Sprintf("%s/%s", registry, imageRef)
|
||||
}
|
||||
|
||||
var options []crane.Option
|
||||
options = append(options, crane.WithAuthFromKeychain(authn.DefaultKeychain))
|
||||
if platform != "" {
|
||||
p, err := v1.ParsePlatform(platform)
|
||||
if err != nil {
|
||||
return fmt.Errorf("parsing platform %q: %w", platform, err)
|
||||
}
|
||||
options = append(options, crane.WithPlatform(p))
|
||||
}
|
||||
|
||||
if allTags {
|
||||
repo := imageRef
|
||||
if strings.Contains(repo, ":") {
|
||||
repo = strings.Split(repo, ":")[0]
|
||||
}
|
||||
tags, err := crane.ListTags(repo, options...)
|
||||
if err != nil {
|
||||
return fmt.Errorf("listing tags for %q: %w", repo, err)
|
||||
}
|
||||
|
||||
for _, tag := range tags {
|
||||
taggedImageRef := fmt.Sprintf("%s:%s", repo, tag)
|
||||
img, err := crane.Pull(taggedImageRef, options...)
|
||||
if err != nil {
|
||||
return fmt.Errorf("pulling image %q: %w", taggedImageRef, err)
|
||||
}
|
||||
|
||||
// a tar file can't have a colon in the name
|
||||
safeTag := strings.ReplaceAll(tag, ":", "_")
|
||||
outputFile := fmt.Sprintf("%s_%s.tar", repo, safeTag)
|
||||
f, err := os.Create(outputFile)
|
||||
if err != nil {
|
||||
return fmt.Errorf("creating file %q: %w", outputFile, err)
|
||||
}
|
||||
defer f.Close()
|
||||
if err := crane.Export(img, f); err != nil {
|
||||
return fmt.Errorf("saving image %q to %q: %w", taggedImageRef, outputFile, err)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
img, err := crane.Pull(imageRef, options...)
|
||||
if err != nil {
|
||||
return fmt.Errorf("pulling image %q: %w", imageRef, err)
|
||||
}
|
||||
f, err := os.Create(outputFile)
|
||||
if err != nil {
|
||||
return fmt.Errorf("creating file %q: %w", outputFile, err)
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
if err := crane.Export(img, f); err != nil {
|
||||
return fmt.Errorf("saving image %q to %q: %w", imageRef, outputFile, err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
50
pkg/docker/docker_test.go
Normal file
50
pkg/docker/docker_test.go
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
package docker
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestCollect(t *testing.T) {
|
||||
t.Run("Good", func(t *testing.T) {
|
||||
// Use a small, public image for testing
|
||||
imageRef := "hello-world"
|
||||
|
||||
// Create a temporary directory to store the output
|
||||
tmpDir := t.TempDir()
|
||||
outputFile := filepath.Join(tmpDir, "hello-world.tar")
|
||||
|
||||
// Call the Collect function
|
||||
err := Collect(imageRef, outputFile, false, "", "")
|
||||
if err != nil {
|
||||
t.Fatalf("Collect() returned an unexpected error: %v", err)
|
||||
}
|
||||
|
||||
// Check if the output file was created
|
||||
if _, err := os.Stat(outputFile); os.IsNotExist(err) {
|
||||
t.Fatalf("Collect() did not create the output file: %s", outputFile)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Platform", func(t *testing.T) {
|
||||
// Use a multi-platform image for testing
|
||||
imageRef := "nginx"
|
||||
platform := "linux/arm64"
|
||||
|
||||
// Create a temporary directory to store the output
|
||||
tmpDir := t.TempDir()
|
||||
outputFile := filepath.Join(tmpDir, "nginx.tar")
|
||||
|
||||
// Call the Collect function
|
||||
err := Collect(imageRef, outputFile, false, platform, "")
|
||||
if err != nil {
|
||||
t.Fatalf("Collect() returned an unexpected error: %v", err)
|
||||
}
|
||||
|
||||
// Check if the output file was created
|
||||
if _, err := os.Stat(outputFile); os.IsNotExist(err) {
|
||||
t.Fatalf("Collect() did not create the output file: %s", outputFile)
|
||||
}
|
||||
})
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue