Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: support push a blob to a remote registry #489

Merged
merged 20 commits into from
Sep 13, 2022
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
1 change: 1 addition & 0 deletions cmd/oras/blob/cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ func Cmd() *cobra.Command {

cmd.AddCommand(
fetchCmd(),
pushCmd(),
)
return cmd
}
3 changes: 1 addition & 2 deletions cmd/oras/blob/fetch.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ limitations under the License.
package blob

import (
"encoding/json"
"errors"
"fmt"
"io"
Expand Down Expand Up @@ -150,7 +149,7 @@ func fetchBlob(opts fetchBlobOptions) (fetchErr error) {

// outputs blob's descriptor if `--descriptor` is used
if opts.OutputDescriptor {
descJSON, err := json.Marshal(desc)
descJSON, err := opts.Marshal(desc)
if err != nil {
return err
}
Expand Down
146 changes: 146 additions & 0 deletions cmd/oras/blob/push.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
/*
Copyright The ORAS Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package blob

import (
"errors"
"fmt"
"os"

ocispec "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/spf13/cobra"
"oras.land/oras/cmd/oras/internal/display"
"oras.land/oras/cmd/oras/internal/option"
"oras.land/oras/internal/file"
)

type pushBlobOptions struct {
option.Common
option.Descriptor
option.Pretty
option.Remote

fileRef string
mediaType string
size int64
targetRef string
}

func pushCmd() *cobra.Command {
var opts pushBlobOptions
cmd := &cobra.Command{
Use: "push [flags] name[@digest] file",
Short: "[Preview] Push a blob to a remote registry",
Long: `[Preview] Push a blob to a remote registry

** This command is in preview and under development. **

Example - Push blob "hi.txt":
oras blob push localhost:5000/hello hi.txt

Example - Push blob "hi.txt" with the specific digest:
oras blob push localhost:5000/hello@sha256:9a201d228ebd966211f7d1131be19f152be428bd373a92071c71d8deaf83b3e5 hi.txt

Example - Push blob from stdin with blob size and digest:
oras blob push --size 12 localhost:5000/hello@sha256:9a201d228ebd966211f7d1131be19f152be428bd373a92071c71d8deaf83b3e5 -

Example - Push blob "hi.txt" and output the descriptor:
oras blob push --descriptor localhost:5000/hello hi.txt

Example - Push blob "hi.txt" with the specific returned media type in the descriptor:
oras blob push --media-type application/vnd.oci.image.config.v1+json --descriptor localhost:5000/hello hi.txt

Example - Push blob "hi.txt" and output the prettified descriptor:
oras blob push --descriptor --pretty localhost:5000/hello hi.txt

Example - Push blob without TLS:
oras blob push --insecure localhost:5000/hello hi.txt
`,
Args: cobra.ExactArgs(2),
PreRunE: func(cmd *cobra.Command, args []string) error {
opts.targetRef = args[0]
opts.fileRef = args[1]
if opts.fileRef == "-" {
if opts.PasswordFromStdin {
return errors.New("`-` read file from input and `--password-stdin` read password from input cannot be both used")
}
if opts.size < 0 {
return errors.New("`--size` must be provided if the blob is read from stdin")
}
}
return opts.ReadPassword()
},
RunE: func(cmd *cobra.Command, args []string) error {
return pushBlob(opts)
},
}

cmd.Flags().Int64VarP(&opts.size, "size", "", -1, "provide the blob size")
cmd.Flags().StringVarP(&opts.mediaType, "media-type", "", ocispec.MediaTypeImageLayer, "specify the returned media type in the descriptor if `--descriptor` is used")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@FeynmanZhou @yizha1 What's the default media type for blobs? Currently, it is ocispec.MediaTypeImageLayer, which is consistent with oras push. We need to review it later.

option.ApplyFlags(&opts, cmd.Flags())
return cmd
}

func pushBlob(opts pushBlobOptions) (err error) {
ctx, _ := opts.SetLoggerLevel()

repo, err := opts.NewRepository(opts.targetRef, opts.Common)
lizMSFT marked this conversation as resolved.
Show resolved Hide resolved
if err != nil {
return err
}

// prepare blob content
desc, rc, err := file.PrepareContent(opts.fileRef, opts.mediaType, repo.Reference.Reference, opts.size)
if err != nil {
return err
}
defer rc.Close()

exists, err := repo.Exists(ctx, desc)
if err != nil {
return err
}
verbose := opts.Verbose && !opts.OutputDescriptor
if exists {
if err := display.PrintStatus(desc, "Exists", verbose); err != nil {
return err
}
} else {
if err := display.PrintStatus(desc, "Uploading", verbose); err != nil {
return err
}
if err = repo.Push(ctx, desc, rc); err != nil {
return err
}
lizMSFT marked this conversation as resolved.
Show resolved Hide resolved
if err := display.PrintStatus(desc, "Uploaded ", verbose); err != nil {
return err
}
}

// outputs blob's descriptor
if opts.OutputDescriptor {
descJSON, err := opts.Marshal(desc)
if err != nil {
return err
}
return opts.Output(os.Stdout, descJSON)
}

fmt.Println("Pushed", opts.targetRef)
fmt.Println("Digest:", desc.Digest)

return nil
}
4 changes: 3 additions & 1 deletion cmd/oras/file.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,13 @@ func loadFiles(ctx context.Context, store *file.Store, annotations map[string]ma
var files []ocispec.Descriptor
for _, fileRef := range fileRefs {
filename, mediaType := parseFileReference(fileRef, "")

// get shortest absolute path as unique name
name := filepath.Clean(filename)
if !filepath.IsAbs(name) {
// convert to slash-separated path unless it is absolute path
name = filepath.ToSlash(name)
}

if verbose {
fmt.Println("Preparing", name)
}
Expand Down
96 changes: 96 additions & 0 deletions internal/file/file.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
/*
Copyright The ORAS Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package file

import (
"errors"
"fmt"
"io"
"os"

digest "github.com/opencontainers/go-digest"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
)

// PrepareContent prepares the content descriptor from the file path or stdin.
// Use the input digest and size if they are provided. Will return error if the
// content is from stdin but the content digest and size are missing.
func PrepareContent(path string, mediaType string, dgstStr string, size int64) (desc ocispec.Descriptor, rc io.ReadCloser, prepareErr error) {
if path == "" {
return ocispec.Descriptor{}, nil, errors.New("missing file name")
}

// validate digest
var dgst digest.Digest
if dgstStr != "" {
var err error
dgst, err = digest.Parse(dgstStr)
if err != nil {
return ocispec.Descriptor{}, nil, fmt.Errorf("invalid digest %s: %w", dgstStr, err)
}
}

// prepares the content descriptor from stdin
if path == "-" {
// throw err if size or digest is not provided.
if size < 0 {
return ocispec.Descriptor{}, nil, errors.New("content size must be provided if it is read from stdin")
}
if dgst == "" {
return ocispec.Descriptor{}, nil, errors.New("content digest must be provided if it is read from stdin")
}
return ocispec.Descriptor{
MediaType: mediaType,
Digest: dgst,
Size: size,
}, os.Stdin, nil
}

file, err := os.Open(path)
if err != nil {
return ocispec.Descriptor{}, nil, fmt.Errorf("failed to open %s: %w", path, err)
}
defer func() {
if prepareErr != nil {
file.Close()
}
}()

fi, err := file.Stat()
if err != nil {
return ocispec.Descriptor{}, nil, fmt.Errorf("failed to stat %s: %w", path, err)
}
actualSize := fi.Size()
if size >= 0 && size != actualSize {
return ocispec.Descriptor{}, nil, fmt.Errorf("input size %d does not match the actual content size %d", size, actualSize)
}

if dgst == "" {
dgst, err = digest.FromReader(file)
if err != nil {
lizMSFT marked this conversation as resolved.
Show resolved Hide resolved
return ocispec.Descriptor{}, nil, err
}
if _, err = file.Seek(0, io.SeekStart); err != nil {
lizMSFT marked this conversation as resolved.
Show resolved Hide resolved
return ocispec.Descriptor{}, nil, err
}
}

return ocispec.Descriptor{
MediaType: mediaType,
Digest: dgst,
Size: actualSize,
}, file, nil
}
Loading