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

Improved logging for store copy / Updated store info to handle multi-arch images #146

Merged
merged 2 commits into from
Dec 14, 2023
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
4 changes: 2 additions & 2 deletions cmd/hauler/cli/store/copy.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ func CopyCmd(ctx context.Context, o *CopyOpts, s *store.Layout, targetRef string
}
}

err := cosign.LoadImage(ctx, s, components[1], ropts)
err := cosign.LoadImages(ctx, s, components[1], ropts)
if err != nil {
return err
}
Expand All @@ -72,6 +72,6 @@ func CopyCmd(ctx context.Context, o *CopyOpts, s *store.Layout, targetRef string
return fmt.Errorf("detecting protocol from [%s]", targetRef)
}

l.Infof("Copied artifacts to [%s]", components[1])
l.Infof("copied artifacts to [%s]", components[1])
return nil
}
145 changes: 110 additions & 35 deletions cmd/hauler/cli/store/info.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@ import (
"context"
"encoding/json"
"fmt"
"strings"
"text/tabwriter"
"github.com/olekukonko/tablewriter"
Copy link
Contributor

Choose a reason for hiding this comment

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

MIT license, okay good.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'm a fan of his tablewriter. I've used it in a few projects now.

"os"
"sort"

ocispec "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/spf13/cobra"
Expand Down Expand Up @@ -44,49 +45,109 @@ func InfoCmd(ctx context.Context, o *InfoOpts, s *store.Layout) error {
}
defer rc.Close()

var m ocispec.Manifest
if err := json.NewDecoder(rc).Decode(&m); err != nil {
return err
}
i := newItem(s, desc, m)
var emptyItem item
if i != emptyItem {
items = append(items, i)
// handle multi-arch images
if desc.MediaType == consts.OCIImageIndexSchema || desc.MediaType == consts.DockerManifestListSchema2 {
Copy link
Contributor

Choose a reason for hiding this comment

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

I imagine that these media type comparisons/checks will be with us for some years. I wonder if longer term than this particular change-set if it makes sense to unify them conceptually? i.e. for this one, possibly something like hauler.IsImageIndex() / hauler.IsImageShema()

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I like that idea.

Copy link
Contributor

Choose a reason for hiding this comment

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

The idea isn't for saving characters/lines of code so much as conceptual concision.

var idx ocispec.Index
if err := json.NewDecoder(rc).Decode(&idx); err != nil {
return err
}

for _, internalDesc := range idx.Manifests {
rc, err := s.Fetch(ctx, internalDesc)
if err != nil {
return err
}
defer rc.Close()

var internalManifest ocispec.Manifest
if err := json.NewDecoder(rc).Decode(&internalManifest); err != nil {
return err
}

i := newItem(s, desc, internalManifest, internalDesc.Platform.Architecture)
var emptyItem item
if i != emptyItem {
items = append(items, i)
}
}
// handle single arch docker images
} else if desc.MediaType == consts.DockerManifestSchema2 {
var m ocispec.Manifest
if err := json.NewDecoder(rc).Decode(&m); err != nil {
return err
}

rc, err := s.FetchManifest(ctx, m)
if err != nil {
return err
}
defer rc.Close()

// Unmarshal the OCI image content
var internalManifest ocispec.Image
if err := json.NewDecoder(rc).Decode(&internalManifest); err != nil {
return err
}

i := newItem(s, desc, m, internalManifest.Architecture)
var emptyItem item
if i != emptyItem {
items = append(items, i)
}
// handle the rest
} else {
var m ocispec.Manifest
if err := json.NewDecoder(rc).Decode(&m); err != nil {
return err
}

i := newItem(s, desc, m, "-")
var emptyItem item
if i != emptyItem {
items = append(items, i)
}
}

return nil
}); err != nil {
return err
}

// sort items by ref and arch
sort.Sort(byReferenceAndArch(items))

var msg string
switch o.OutputFormat {
case "json":
msg = buildJson(items...)

fmt.Println(msg)
default:
msg = buildTable(items...)
buildTable(items...)
}
fmt.Println(msg)
return nil
}

func buildTable(items ...item) string {
b := strings.Builder{}
tw := tabwriter.NewWriter(&b, 1, 1, 3, ' ', 0)

fmt.Fprintf(tw, "Reference\tType\t# Layers\tSize\n")
fmt.Fprintf(tw, "---------\t----\t--------\t----\n")
func buildTable(items ...item) {
// Create a table for the results
table := tablewriter.NewWriter(os.Stdout)
table.SetHeader([]string{"Reference", "Type", "Arch", "# Layers", "Size"})
table.SetHeaderAlignment(tablewriter.ALIGN_LEFT)
table.SetRowLine(false)
table.SetAutoMergeCellsByColumnIndex([]int{0})

for _, i := range items {
if i.Type != "" {
fmt.Fprintf(tw, "%s\t%s\t%d\t%s\n",
i.Reference, i.Type, i.Layers, i.Size,
)
row := []string{
i.Reference,
i.Type,
i.Architecture,
fmt.Sprintf("%d", i.Layers),
i.Size,
}
table.Append(row)
}
}
tw.Flush()
return b.String()
table.Render()
}

func buildJson(item ...item) string {
Expand All @@ -98,16 +159,29 @@ func buildJson(item ...item) string {
}

type item struct {
Reference string
Type string
Layers int
Size string
Reference string
Type string
Architecture string
Layers int
Size string
}

type byReferenceAndArch []item

func (a byReferenceAndArch) Len() int { return len(a) }
func (a byReferenceAndArch) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
func (a byReferenceAndArch) Less(i, j int) bool {
if a[i].Reference == a[j].Reference {
return a[i].Architecture < a[j].Architecture
}
return a[i].Reference < a[j].Reference
}

func newItem(s *store.Layout, desc ocispec.Descriptor, m ocispec.Manifest) item {
func newItem(s *store.Layout, desc ocispec.Descriptor, m ocispec.Manifest, arch string) item {
// skip listing cosign items
if desc.Annotations["kind"] == "dev.cosignproject.cosign/atts" ||
desc.Annotations["kind"] == "dev.cosignproject.cosign/sigs" ||
desc.Annotations["kind"] == "dev.cosignproject.cosign/sboms" {
desc.Annotations["kind"] == "dev.cosignproject.cosign/sigs" ||
desc.Annotations["kind"] == "dev.cosignproject.cosign/sboms" {
return item{}
}

Expand Down Expand Up @@ -135,10 +209,11 @@ func newItem(s *store.Layout, desc ocispec.Descriptor, m ocispec.Manifest) item
}

return item{
Reference: ref.Name(),
Type: ctype,
Layers: len(m.Layers),
Size: byteCountSI(size),
Reference: ref.Name(),
Type: ctype,
Architecture: arch,
Layers: len(m.Layers),
Size: byteCountSI(size),
}
}

Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ require (
github.com/gorilla/mux v1.8.0
github.com/mholt/archiver/v3 v3.5.1
github.com/mitchellh/go-homedir v1.1.0
github.com/olekukonko/tablewriter v0.0.5
github.com/opencontainers/go-digest v1.0.0
github.com/opencontainers/image-spec v1.1.0-rc5
github.com/pkg/errors v0.9.1
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -417,6 +417,8 @@ github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8m
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/nwaples/rardecode v1.1.0 h1:vSxaY8vQhOcVr4mm5e8XllHWTiM4JF507A0Katqw7MQ=
github.com/nwaples/rardecode v1.1.0/go.mod h1:5DzqNKiOdpKKBH87u8VlvAnPZMXcGRhxWkRpHbbfGS0=
github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
github.com/onsi/ginkgo/v2 v2.9.4 h1:xR7vG4IXt5RWx6FfIjyAtsoMAtnc3C/rFXBBd2AjZwE=
github.com/onsi/ginkgo/v2 v2.9.4/go.mod h1:gCQYp2Q+kSoIj7ykSVb9nskRSsR6PUj4AiLywzIhbKM=
github.com/onsi/gomega v1.27.6 h1:ENqfyGeS5AX/rlXDd/ETokDz93u0YufY1Pgxuy/PvWE=
Expand Down
1 change: 1 addition & 0 deletions pkg/consts/consts.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ const (
OCIManifestSchema1 = "application/vnd.oci.image.manifest.v1+json"
DockerManifestSchema2 = "application/vnd.docker.distribution.manifest.v2+json"
DockerManifestListSchema2 = "application/vnd.docker.distribution.manifest.list.v2+json"
OCIImageIndexSchema = "application/vnd.oci.image.index.v1+json"

DockerConfigJSON = "application/vnd.docker.container.image.v1+json"
DockerLayer = "application/vnd.docker.image.rootfs.diff.tar.gzip"
Expand Down
16 changes: 16 additions & 0 deletions pkg/content/oci.go
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,14 @@ func (o *OCI) Fetch(ctx context.Context, desc ocispec.Descriptor) (io.ReadCloser
return readerAt, nil
}

func (o *OCI) FetchManifest(ctx context.Context, manifest ocispec.Manifest) (io.ReadCloser, error) {
readerAt, err := o.manifestBlobReaderAt(manifest)
if err != nil {
return nil, err
}
return readerAt, nil
}

// Pusher returns a new pusher for the provided reference
// The returned Pusher should satisfy content.Ingester and concurrent attempts
// to push the same blob using the Ingester API should result in ErrUnavailable.
Expand Down Expand Up @@ -208,6 +216,14 @@ func (o *OCI) blobReaderAt(desc ocispec.Descriptor) (*os.File, error) {
return os.Open(blobPath)
}

func (o *OCI) manifestBlobReaderAt(manifest ocispec.Manifest) (*os.File, error) {
blobPath, err := o.ensureBlob(string(manifest.Config.Digest.Algorithm().String()), manifest.Config.Digest.Hex())
if err != nil {
return nil, err
}
return os.Open(blobPath)
}

func (o *OCI) blobWriterAt(desc ocispec.Descriptor) (*os.File, error) {
blobPath, err := o.ensureBlob(desc.Digest.Algorithm().String(), desc.Digest.Hex())
if err != nil {
Expand Down
95 changes: 62 additions & 33 deletions pkg/cosign/cosign.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (
"strings"
"encoding/json"
"time"
"bufio"

ocispec "github.com/opencontainers/image-spec/specs-go/v1"
"oras.land/oras-go/pkg/content"
Expand Down Expand Up @@ -69,53 +70,81 @@ func SaveImage(ctx context.Context, s *store.Layout, ref string) error {
}

// LoadImage loads store to a remote registry.
func LoadImage(ctx context.Context, s *store.Layout, registry string, ropts content.RegistryOptions) error {
operation := func() error {
cosignBinaryPath, err := ensureCosignBinary(ctx, s)
if err != nil {
return err
}
func LoadImages(ctx context.Context, s *store.Layout, registry string, ropts content.RegistryOptions) error {
l := log.FromContext(ctx)

cmd := exec.Command(cosignBinaryPath, "load", "--registry", registry, "--dir", s.Root)
cosignBinaryPath, err := ensureCosignBinary(ctx, s)
if err != nil {
return err
}

// Conditionally add extra registry flags.
if ropts.Insecure {
cmd.Args = append(cmd.Args, "--allow-insecure-registry=true")
}
if ropts.PlainHTTP {
cmd.Args = append(cmd.Args, "--allow-http-registry=true")
}
cmd := exec.Command(cosignBinaryPath, "load", "--registry", registry, "--dir", s.Root)

output, err := cmd.CombinedOutput()
if err != nil {
return fmt.Errorf("error copying store: %v, output: %s", err, output)
}
// Conditionally add extra registry flags.
if ropts.Insecure {
cmd.Args = append(cmd.Args, "--allow-insecure-registry=true")
}
if ropts.PlainHTTP {
cmd.Args = append(cmd.Args, "--allow-http-registry=true")
}

return nil
stdout, err := cmd.StdoutPipe()
if err != nil {
return err
}
stderr, err := cmd.StderrPipe()
if err != nil {
return err
}
// start the command after having set up the pipe
if err := cmd.Start(); err != nil {
return err
}

// read command's stdout line by line
output := bufio.NewScanner(stdout)
for output.Scan() {
l.Infof(output.Text()) // write each line to your log, or anything you need
}
if err := output.Err(); err != nil {
cmd.Wait()
return err
}

return RetryOperation(ctx, operation)
// read command's stderr line by line
errors := bufio.NewScanner(stderr)
for errors.Scan() {
l.Errorf(errors.Text()) // write each line to your log, or anything you need
}
if err := errors.Err(); err != nil {
cmd.Wait()
return err
}

// Wait for the command to finish
err = cmd.Wait()
if err != nil {
return err
}

return nil
}

// RegistryLogin - performs cosign login
func RegistryLogin(ctx context.Context, s *store.Layout, registry string, ropts content.RegistryOptions) error {
operation := func() error {
cosignBinaryPath, err := ensureCosignBinary(ctx, s)
if err != nil {
return err
}
cosignBinaryPath, err := ensureCosignBinary(ctx, s)
if err != nil {
return err
}

cmd := exec.Command(cosignBinaryPath, "login", registry, "-u", ropts.Username, "-p", ropts.Password)
cmd := exec.Command(cosignBinaryPath, "login", registry, "-u", ropts.Username, "-p", ropts.Password)

output, err := cmd.CombinedOutput()
if err != nil {
return fmt.Errorf("error logging into registry: %v, output: %s", err, output)
}

return nil
output, err := cmd.CombinedOutput()
if err != nil {
return fmt.Errorf("error logging into registry: %v, output: %s", err, output)
}

return RetryOperation(ctx, operation)
return nil
}

func RetryOperation(ctx context.Context, operation func() error) error {
Expand Down
Loading