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

enhance store info command to actually show useful information #95

Merged
merged 4 commits into from
Dec 8, 2021
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
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