Skip to content

Commit

Permalink
Merge pull request #95 from joshrwolf/info
Browse files Browse the repository at this point in the history
enhance `store info` command to actually show useful information
  • Loading branch information
joshrwolf authored Dec 8, 2021
2 parents e6e7ff6 + 6c1640f commit 61cbc6f
Show file tree
Hide file tree
Showing 8 changed files with 216 additions and 87 deletions.
127 changes: 112 additions & 15 deletions cmd/hauler/cli/store/info.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,40 +2,137 @@ package store

import (
"context"
"encoding/json"
"fmt"
"os"
"strings"
"text/tabwriter"

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

"github.com/rancherfederal/hauler/pkg/consts"
"github.com/rancherfederal/hauler/pkg/store"
)

type InfoOpts struct{}
type InfoOpts struct {
OutputFormat string
SizeUnit string
}

func (o *InfoOpts) AddFlags(cmd *cobra.Command) {
_ = cmd.Flags()
f := cmd.Flags()

f.StringVarP(&o.OutputFormat, "output", "o", "table", "Output format (table, json)")

// TODO: Regex/globbing
}

func InfoCmd(ctx context.Context, o *InfoOpts, s *store.Store) error {
refs, err := s.List(ctx)
if err != nil {
return err
}
var items []item
if err := s.Walk(func(desc ocispec.Descriptor) error {
if _, ok := desc.Annotations[ocispec.AnnotationRefName]; !ok {
return nil
}

tw := tabwriter.NewWriter(os.Stdout, 0, 8, 0, '\t', 0)
defer tw.Flush()
rc, err := s.Open(ctx, desc)
if err != nil {
return err
}
defer rc.Close()

fmt.Fprintf(tw, "Reference\tTag/Digest\tType\n")
fmt.Fprintf(tw, "---------\t----------\t----\n")
for _, r := range refs {
if _, ok := r.Annotations[ocispec.AnnotationRefName]; !ok {
continue
var m ocispec.Manifest
if err := json.NewDecoder(rc).Decode(&m); err != nil {
return err
}
fmt.Fprintf(tw, "%s\t%s\n", r.Annotations[ocispec.AnnotationRefName], "")

i := newItem(s, desc, m)
items = append(items, i)

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

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

default:
msg = 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")

for _, i := range items {
fmt.Fprintf(tw, "%s\t%s\t%d\t%s\n",
i.Reference, i.Type, i.Layers, i.Size,
)
}
tw.Flush()
return b.String()
}

func buildJson(item ...item) string {
data, err := json.MarshalIndent(item, "", " ")
if err != nil {
return ""
}
return string(data)
}

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

func newItem(s *store.Store, desc ocispec.Descriptor, m ocispec.Manifest) item {
var size int64 = 0
for _, l := range m.Layers {
size = +l.Size
}

// Generate a human-readable content type
var ctype string
switch m.Config.MediaType {
case consts.DockerConfigJSON:
ctype = "image"
case consts.ChartConfigMediaType:
ctype = "chart"
case consts.FileLocalConfigMediaType, consts.FileHttpConfigMediaType:
ctype = "file"
default:
ctype = "unknown"
}

return item{
Reference: desc.Annotations[ocispec.AnnotationRefName],
Type: ctype,
Layers: len(m.Layers),
Size: byteCountSI(size),
}
}

func byteCountSI(b int64) string {
const unit = 1000
if b < unit {
return fmt.Sprintf("%d B", b)
}
div, exp := int64(unit), 0
for n := b / unit; n >= unit; n /= unit {
div *= unit
exp++
}
return fmt.Sprintf("%.1f %cB",
float64(b)/float64(div), "kMGTPE"[exp])
}
14 changes: 14 additions & 0 deletions internal/getter/directory.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ import (

"github.com/opencontainers/go-digest"
"github.com/pkg/errors"

"github.com/rancherfederal/hauler/pkg/artifact"
"github.com/rancherfederal/hauler/pkg/consts"
)

type directory struct {
Expand Down Expand Up @@ -68,6 +71,17 @@ func (d directory) Detect(u *url.URL) bool {
return fi.IsDir()
}

func (d directory) Config(u *url.URL) artifact.Config {
c := &directoryConfig{
config{Reference: u.String()},
}
return artifact.ToConfig(c, artifact.WithConfigMediaType(consts.FileDirectoryConfigMediaType))
}

type directoryConfig struct {
config `json:",inline,omitempty"`
}

func tarDir(root string, prefix string, w io.Writer, stripTimes bool) error {
tw := tar.NewWriter(w)
defer tw.Close()
Expand Down
8 changes: 1 addition & 7 deletions internal/getter/file.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,6 @@ import (
"os"
"path/filepath"

"github.com/google/go-containerregistry/pkg/v1/types"

"github.com/rancherfederal/hauler/pkg/artifact"
"github.com/rancherfederal/hauler/pkg/consts"
)
Expand Down Expand Up @@ -47,13 +45,9 @@ func (f File) Config(u *url.URL) artifact.Config {
c := &fileConfig{
config{Reference: u.String()},
}
return artifact.ToConfig(c)
return artifact.ToConfig(c, artifact.WithConfigMediaType(consts.FileLocalConfigMediaType))
}

type fileConfig struct {
config `json:",inline,omitempty"`
}

func (c *fileConfig) MediaType() (types.MediaType, error) {
return consts.FileLocalConfigMediaType, nil
}
8 changes: 1 addition & 7 deletions internal/getter/https.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,6 @@ import (
"path/filepath"
"strings"

"github.com/google/go-containerregistry/pkg/v1/types"

"github.com/rancherfederal/hauler/pkg/artifact"
"github.com/rancherfederal/hauler/pkg/consts"
)
Expand Down Expand Up @@ -61,13 +59,9 @@ func (h *Http) Config(u *url.URL) artifact.Config {
c := &httpConfig{
config{Reference: u.String()},
}
return artifact.ToConfig(c)
return artifact.ToConfig(c, artifact.WithConfigMediaType(consts.FileHttpConfigMediaType))
}

type httpConfig struct {
config `json:",inline,omitempty"`
}

func (c *httpConfig) MediaType() (types.MediaType, error) {
return consts.FileHttpConfigMediaType, nil
}
5 changes: 3 additions & 2 deletions pkg/consts/consts.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,9 @@ const (
FileLayerMediaType = "application/vnd.content.hauler.file.layer.v1"

// FileLocalConfigMediaType is the reserved media type for File config
FileLocalConfigMediaType = "application/vnd.content.hauler.file.local.config.v1+json"
FileHttpConfigMediaType = "application/vnd.content.hauler.file.http.config.v1+json"
FileLocalConfigMediaType = "application/vnd.content.hauler.file.local.config.v1+json"
FileDirectoryConfigMediaType = "application/vnd.content.hauler.file.directory.config.v1+json"
FileHttpConfigMediaType = "application/vnd.content.hauler.file.http.config.v1+json"

// WasmArtifactLayerMediaType is the reserved media type for WASM artifact layers
WasmArtifactLayerMediaType = "application/vnd.wasm.content.layer.v1+wasm"
Expand Down
2 changes: 1 addition & 1 deletion pkg/content/file/file.go
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ func (f *file) compute() error {
return err
}

cfg := f.config
cfg := f.client.Config(f.ref)
if cfg == nil {
cfg = f.client.Config(f.ref)
}
Expand Down
98 changes: 79 additions & 19 deletions pkg/content/file/file_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,41 +7,74 @@ import (
"net/http"
"net/http/httptest"
"net/url"
"os"
"path/filepath"
"testing"

"github.com/spf13/afero"

"github.com/rancherfederal/hauler/internal/getter"
"github.com/rancherfederal/hauler/pkg/consts"
"github.com/rancherfederal/hauler/pkg/content/file"
)

func Test_file_Layers(t *testing.T) {
filename := "myfile.yaml"
data := []byte(`data`)
var (
filename = "myfile.yaml"
data = []byte(`data`)

mfs := afero.NewMemMapFs()
afero.WriteFile(mfs, filename, data, 0644)
ts *httptest.Server
tfs afero.Fs
mc *getter.Client
)

mf := &mockFile{File: getter.NewFile(), fs: mfs}
func TestMain(m *testing.M) {
teardown := setup()
defer teardown()
code := m.Run()
os.Exit(code)
}

mockHttp := getter.NewHttp()
func Test_file_Config(t *testing.T) {
tests := []struct {
name string
ref string
want string
wantErr bool
}{
{
name: "should properly type local file",
ref: filename,
want: consts.FileLocalConfigMediaType,
wantErr: false,
},
{
name: "should properly type remote file",
ref: ts.URL + "/" + filename,
want: consts.FileHttpConfigMediaType,
wantErr: false,
},
// TODO: Add directory test
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
f := file.NewFile(tt.ref, file.WithClient(mc))

mhttp := afero.NewHttpFs(mfs)
fileserver := http.FileServer(mhttp.Dir("."))
http.Handle("/", fileserver)
f.MediaType()

s := httptest.NewServer(fileserver)
defer s.Close()
m, err := f.Manifest()
if err != nil {
t.Fatal(err)
}

mc := &getter.Client{
Options: getter.ClientOptions{},
Getters: map[string]getter.Getter{
"file": mf,
"http": mockHttp,
},
got := string(m.Config.MediaType)
if got != tt.want {
t.Errorf("Expected mediatype %s | got %s", got, tt.want)
}
})
}
}

func Test_file_Layers(t *testing.T) {
tests := []struct {
name string
ref string
Expand All @@ -56,7 +89,7 @@ func Test_file_Layers(t *testing.T) {
},
{
name: "should load a remote file and preserve contents",
ref: s.URL + "/" + filename,
ref: ts.URL + "/" + filename,
want: data,
wantErr: false,
},
Expand Down Expand Up @@ -89,6 +122,33 @@ func Test_file_Layers(t *testing.T) {
}
}

func setup() func() {
tfs = afero.NewMemMapFs()
afero.WriteFile(tfs, filename, data, 0644)

mf := &mockFile{File: getter.NewFile(), fs: tfs}

mockHttp := getter.NewHttp()
mhttp := afero.NewHttpFs(tfs)
fileserver := http.FileServer(mhttp.Dir("."))
http.Handle("/", fileserver)
ts = httptest.NewServer(fileserver)

mc = &getter.Client{
Options: getter.ClientOptions{},
Getters: map[string]getter.Getter{
"file": mf,
"http": mockHttp,
},
}

teardown := func() {
defer ts.Close()
}

return teardown
}

type mockFile struct {
*getter.File
fs afero.Fs
Expand Down
Loading

0 comments on commit 61cbc6f

Please sign in to comment.