diff --git a/cmd/upload-single.go b/cmd/upload-single.go new file mode 100644 index 0000000..ffa59de --- /dev/null +++ b/cmd/upload-single.go @@ -0,0 +1,132 @@ +package cmd + +import ( + "errors" + "fmt" + "github.com/google/go-containerregistry/pkg/authn" + "github.com/google/go-containerregistry/pkg/name" + v1 "github.com/google/go-containerregistry/pkg/v1" + "github.com/google/go-containerregistry/pkg/v1/empty" + "github.com/google/go-containerregistry/pkg/v1/mutate" + "github.com/google/go-containerregistry/pkg/v1/remote" + "github.com/google/go-containerregistry/pkg/v1/tarball" + v2 "github.com/opencontainers/image-spec/specs-go/v1" + "github.com/spf13/cobra" + "log" + "os" + "path" + "time" +) + +func UploadSingleCmd() *cobra.Command { + command := &cobra.Command{ + Use: "upload-single [source artifact] [destination registry]", + Short: "Upload single archive to an OCI registry", + Args: cobra.ExactArgs(2), + Run: func(cmd *cobra.Command, args []string) { + downloadCmd := &uploadSingleArtifact{} + err := downloadCmd.run(cmd, args) + if err != nil { + log.Fatal(err) + } + }, + } + + return command +} + +type uploadSingleArtifact struct { +} + +func (d *uploadSingleArtifact) validateArgs(args []string) (*uploadConfig, error) { + // Validate the first arg is a directory. + archive := args[0] + if !path.IsAbs(archive) { + workDir, err := os.Getwd() + if err != nil { + return nil, fmt.Errorf("failed to get current working directory: %v", err) + } + archive = path.Join(workDir, archive) + } + + fi, err := os.Stat(archive) + if err != nil { + if errors.Is(err, os.ErrNotExist) { + return nil, fmt.Errorf("source archive does not exist: %v", err) + } + + return nil, fmt.Errorf("failed to stat archive: %v", err) + } + + if fi.IsDir() { + + return nil, fmt.Errorf("source archive is a directory") + } + + // Validate the second arg is a valid OCI registry + repositoryName := args[1] + tag, err := name.NewTag(repositoryName, name.StrictValidation) + if err != nil { + return nil, fmt.Errorf("failed to parse repository: %v", err) + } + + return &uploadConfig{ + source: archive, + tag: tag, + }, nil +} + +func (d *uploadSingleArtifact) run(cmd *cobra.Command, args []string) error { + config, err := d.validateArgs(args) + if err != nil { + return err + } + + // We'll use the filename as the image title for the moment + title := path.Base(config.source) + + // We could add more annotations later based on flags or potentially a config file. + // Right now we push what we know. + index := mutate.Annotations(empty.Index, map[string]string{ + "org.opencontainers.image.created": time.Now().UTC().Format("2006-01-02T15:04:05Z"), + "org.opencontainers.image.title": title, + "org.opencontainers.image.ref.name": config.tag.TagStr(), + "org.opencontainers.image.version": config.tag.TagStr(), + }).(v1.ImageIndex) + + layer, err := tarball.LayerFromFile(config.source) + if err != nil { + log.Fatalf("failed to create tarball layer: %v", err) + } + + // Create an OCI image with the layer + img, err := mutate.Append(empty.Image, mutate.Addendum{ + MediaType: v2.MediaTypeImageLayerGzip, + Annotations: map[string]string{ + "org.opencontainers.image.ref.name": title, + }, + Layer: layer, + }) + if err != nil { + return err + } + + if err := remote.Write( + config.tag, + img, + remote.WithAuthFromKeychain(authn.DefaultKeychain), + ); err != nil { + return err + } + + index = mutate.AppendManifests(index, mutate.IndexAddendum{ + Add: img, + }) + + err = remote.WriteIndex(config.tag, index, remote.WithAuthFromKeychain(authn.DefaultKeychain)) + if err != nil { + return err + } + + return nil +} diff --git a/cmd/upload.go b/cmd/upload.go index 0a7b7f0..08bfaa1 100644 --- a/cmd/upload.go +++ b/cmd/upload.go @@ -37,8 +37,8 @@ func UploadReleaseCmd() *cobra.Command { } type uploadConfig struct { - directory string - tag name.Tag + source string + tag name.Tag } type uploadRelease struct { @@ -76,8 +76,8 @@ func (d *uploadRelease) validateArgs(args []string) (*uploadConfig, error) { } return &uploadConfig{ - directory: archiveDir, - tag: tag, + source: archiveDir, + tag: tag, }, nil } @@ -87,7 +87,7 @@ func (d *uploadRelease) run(cmd *cobra.Command, args []string) error { return err } - data, err := metadata.New(config.directory) + data, err := metadata.New(config.source) if err != nil { return err } diff --git a/cmd/upload_test.go b/cmd/upload_test.go index b32ea73..573eb18 100644 --- a/cmd/upload_test.go +++ b/cmd/upload_test.go @@ -76,7 +76,7 @@ func Test_UploadCmd_ValidateArgs_Response(t *testing.T) { if err != nil { t.Errorf("uploadCmd.validateArgs() error = %v", err) } - if config.directory != path.Join(workDir, validDirectory) { + if config.source != path.Join(workDir, validDirectory) { t.Errorf("uploadCmd.validateArgs() config = %v", config) } }) diff --git a/main.go b/main.go index 67b1b7b..ceace45 100644 --- a/main.go +++ b/main.go @@ -13,6 +13,7 @@ func main() { } rootCmd.AddCommand(cmd.UploadReleaseCmd()) + rootCmd.AddCommand(cmd.UploadSingleCmd()) rootCmd.AddCommand(cmd.DownloadReleaseCmd()) rootCmd.AddCommand(cmd.LoginCmd()) rootCmd.AddCommand(cmd.LogoutCmd()) diff --git a/pkg/oci/download.go b/pkg/oci/download.go index 01dae16..8a0c761 100644 --- a/pkg/oci/download.go +++ b/pkg/oci/download.go @@ -4,13 +4,11 @@ import ( "archive/tar" "github.com/google/go-containerregistry/pkg/authn" "github.com/google/go-containerregistry/pkg/name" - v1 "github.com/google/go-containerregistry/pkg/v1" "github.com/google/go-containerregistry/pkg/v1/remote" "io" "os" "path" "path/filepath" - "runtime" ) type Downloader struct { @@ -30,14 +28,11 @@ func NewDownloader(source name.Tag, destination string) (Downloader, error) { // Download executes the download of the OCI artifact into memory, untars it and write it to a directory. // This will need to be updated at some point when we are working with OCI artifacts rather than images, // to take slightly different actions based on the artifact type we receive from the registry (image / binary / fs) -func (dl *Downloader) Download() error { +func (dl *Downloader) Download(option ...remote.Option) error { opts := []remote.Option{ remote.WithAuthFromKeychain(authn.DefaultKeychain), - remote.WithPlatform(v1.Platform{ - Architecture: runtime.GOARCH, - OS: runtime.GOOS, - }), } + opts = append(opts, option...) img, err := remote.Image(dl.reference, opts...) if err != nil { return err