Skip to content

Commit

Permalink
feat: support pull templating
Browse files Browse the repository at this point in the history
Signed-off-by: Billy Zha <[email protected]>
  • Loading branch information
qweeah committed Dec 1, 2023
1 parent e41c804 commit 7433ab7
Show file tree
Hide file tree
Showing 3 changed files with 105 additions and 19 deletions.
11 changes: 5 additions & 6 deletions cmd/oras/internal/display/print.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ import (
"fmt"
"io"
"os"
"reflect"
"sync"

ocispec "github.com/opencontainers/image-spec/specs-go/v1"
Expand All @@ -31,19 +30,19 @@ import (

var (
printLock sync.Mutex
to io.Writer
to *os.File
)

func init() {
to = os.Stdout
}

// Set sets the output writer for printing.
func Set(template string, tty io.Writer) {
func Set(template string, tty *os.File) {
printLock.Lock()
defer printLock.Unlock()
if template != "" {
to = tty
if template != "" || tty != nil {
to = nil
}
}

Expand All @@ -55,7 +54,7 @@ func Print(a ...any) error {
printLock.Lock()
defer printLock.Unlock()

if reflect.ValueOf(to).IsNil() {
if to == nil {
return nil
}
_, err := fmt.Fprintln(to, a...)
Expand Down
60 changes: 60 additions & 0 deletions cmd/oras/internal/meta/pull.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
/*
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 meta

import (
"path/filepath"

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

type File struct {
Path string `json:"path"`
Descriptor
}

// NewFile creates a new file metadata.
func NewFile(name string, outputDir string, desc ocispec.Descriptor, digestReference string) File {
path := name
if !filepath.IsAbs(name) {
// ignore error since it's successfully written to file store
path, _ = filepath.Abs(filepath.Join(outputDir, name))
}
return File{
Path: path,
Descriptor: Descriptor{
Descriptor: desc,
DigestReference: DigestReference{
Reference: digestReference,
},
},
}
}

// Metadata for push command
type pull struct {
DigestReference
Files []File `json:"files"`
}

func NewPull(digestReference string, files []File) pull {
return pull{
DigestReference: DigestReference{
Reference: digestReference,
},
Files: files,
}
}
53 changes: 40 additions & 13 deletions cmd/oras/root/pull.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import (
"errors"
"fmt"
"io"
"os"
"sync"
"sync/atomic"

Expand All @@ -31,6 +32,7 @@ import (
"oras.land/oras/cmd/oras/internal/display"
"oras.land/oras/cmd/oras/internal/display/track"
"oras.land/oras/cmd/oras/internal/fileref"
"oras.land/oras/cmd/oras/internal/meta"
"oras.land/oras/cmd/oras/internal/option"
"oras.land/oras/internal/graph"
)
Expand All @@ -40,6 +42,7 @@ type pullOptions struct {
option.Common
option.Platform
option.Target
option.Format

concurrency int
KeepOldFiles bool
Expand Down Expand Up @@ -90,6 +93,7 @@ Example - Pull artifact files from an OCI layout archive 'layout.tar':
return option.Parse(&opts)
},
RunE: func(cmd *cobra.Command, args []string) error {
display.Set(opts.Template, opts.TTY)
return runPull(cmd.Context(), opts)
},
}
Expand All @@ -104,6 +108,13 @@ Example - Pull artifact files from an OCI layout archive 'layout.tar':
return cmd
}

type pullResult struct {
files []meta.File
root ocispec.Descriptor
layerSkipped bool
filesLock *sync.Mutex
}

func runPull(ctx context.Context, opts pullOptions) error {
ctx, logger := opts.WithContext(ctx)
// Copy Options
Expand Down Expand Up @@ -131,7 +142,7 @@ func runPull(ctx context.Context, opts pullOptions) error {
dst.AllowPathTraversalOnWrite = opts.PathTraversal
dst.DisableOverwrite = opts.KeepOldFiles

desc, layerSkipped, err := doPull(ctx, src, dst, copyOptions, &opts)
result, err := doPull(ctx, src, dst, copyOptions, &opts)
if err != nil {
if errors.Is(err, file.ErrPathTraversalDisallowed) {
err = fmt.Errorf("%s: %w", "use flag --allow-path-traversal to allow insecurely pulling files outside of working directory", err)
Expand All @@ -140,23 +151,25 @@ func runPull(ctx context.Context, opts pullOptions) error {
}

// suggest oras copy for pulling layers without annotation
if layerSkipped {
fmt.Printf("Skipped pulling layers without file name in %q\n", ocispec.AnnotationTitle)
fmt.Printf("Use 'oras copy %s --to-oci-layout <layout-dir>' to pull all layers.\n", opts.RawReference)
if result.layerSkipped {
display.Print("Skipped pulling layers without file name in", ocispec.AnnotationTitle)
display.Print("Use 'oras copy", opts.RawReference, "--to-oci-layout <layout-dir>' to pull all layers.")
} else {
fmt.Println("Pulled", opts.AnnotatedReference())
fmt.Println("Digest:", desc.Digest)
display.Print("Pulled", opts.AnnotatedReference())
display.Print("Digest:", result.root.Digest)
}
return nil
return opts.WriteTo(os.Stdout, meta.NewPull(fmt.Sprintf("%s@%s", opts.Path, result.root.Digest), result.files))
}

func doPull(ctx context.Context, src oras.ReadOnlyTarget, dst oras.GraphTarget, opts oras.CopyOptions, po *pullOptions) (ocispec.Descriptor, bool, error) {
func doPull(ctx context.Context, src oras.ReadOnlyTarget, dst oras.GraphTarget, opts oras.CopyOptions, po *pullOptions) (pullResult, error) {
var result pullResult
result.filesLock = &sync.Mutex{}
var configPath, configMediaType string
var err error
if po.ManifestConfigRef != "" {
configPath, configMediaType, err = fileref.Parse(po.ManifestConfigRef, "")
if err != nil {
return ocispec.Descriptor{}, false, err
return result, err
}
}

Expand All @@ -172,7 +185,7 @@ func doPull(ctx context.Context, src oras.ReadOnlyTarget, dst oras.GraphTarget,
var tracked track.GraphTarget
dst, tracked, err = getTrackedTarget(dst, po.TTY, "Downloading", "Pulled ")
if err != nil {
return ocispec.Descriptor{}, false, err
return result, err
}
if tracked != nil {
defer tracked.Close()
Expand Down Expand Up @@ -275,7 +288,10 @@ func doPull(ctx context.Context, src oras.ReadOnlyTarget, dst oras.GraphTarget,
return err
}
for _, s := range successors {
if _, ok := s.Annotations[ocispec.AnnotationTitle]; ok {
if name, ok := s.Annotations[ocispec.AnnotationTitle]; ok {
result.filesLock.Lock()
result.files = append(result.files, meta.NewFile(name, po.Output, desc, fmt.Sprintf("%s@%s", po.Path, desc.Digest)))
result.filesLock.Unlock()
if err := printOnce(&printed, s, promptRestored, po.Verbose, tracked); err != nil {
return err
}
Expand All @@ -287,14 +303,25 @@ func doPull(ctx context.Context, src oras.ReadOnlyTarget, dst oras.GraphTarget,
return nil
}
name = desc.MediaType
} else {
result.filesLock.Lock()
result.files = append(result.files, meta.NewFile(name, po.Output, desc, fmt.Sprintf("%s@%s", po.Path, desc.Digest)))
result.filesLock.Unlock()
}
printed.Store(generateContentKey(desc), true)
return display.Print(promptDownloaded, display.ShortDigest(desc), name)
if po.TTY == nil {
// none TTY, print status log for downloaded
return display.Print(promptDownloaded, display.ShortDigest(desc), name)
}
// TTY
return nil
}

// Copy
desc, err := oras.Copy(ctx, src, po.Reference, dst, po.Reference, opts)
return desc, layerSkipped.Load(), err
result.root = desc
result.layerSkipped = layerSkipped.Load()
return result, err
}

// generateContentKey generates a unique key for each content descriptor, using
Expand Down

0 comments on commit 7433ab7

Please sign in to comment.