Skip to content
This repository has been archived by the owner on Feb 17, 2024. It is now read-only.

Commit

Permalink
Merge pull request #34 from stealthrocket/timecraft-export
Browse files Browse the repository at this point in the history
timecraft: add export command
  • Loading branch information
achille-roussel committed May 30, 2023
2 parents 28d9618 + 3e1b914 commit a8ae16a
Show file tree
Hide file tree
Showing 6 changed files with 145 additions and 50 deletions.
21 changes: 7 additions & 14 deletions internal/cmd/describe.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,29 +63,22 @@ func describe(ctx context.Context, args []string) error {
if len(args) == 0 {
return errors.New(`expected a resource type as argument`)
}
resourceTypeLookup := args[0]
resourceIDs := args[1:]

resource, ok := findResource(resourceTypeLookup, resources[:])
if !ok {
matchingResources := findMatchingResources(resourceTypeLookup, resources[:])
if len(matchingResources) == 0 {
return fmt.Errorf(`no resources matching '%s'`+useGet(), resourceTypeLookup)
}
return fmt.Errorf(`no resources matching '%s'

Did you mean?%s`, resourceTypeLookup, joinResourceTypes(matchingResources, "\n "))
}

registry, err := openRegistry(registryPath)
resource, err := findResource("describe", args[0])
if err != nil {
return err
}

resourceIDs := args[1:]
if len(resourceIDs) == 0 {
return fmt.Errorf(`no resources were specified, use 'timecraft describe %s <resources ids...>'`, resource.typ)
}

registry, err := openRegistry(registryPath)
if err != nil {
return err
}

var lookup func(context.Context, *timemachine.Registry, string) (any, error)
var writer stream.WriteCloser[any]
switch output {
Expand Down
98 changes: 98 additions & 0 deletions internal/cmd/export.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
package cmd

import (
"context"
"errors"
"io"
"os"

"github.com/google/uuid"
"github.com/stealthrocket/timecraft/format"
"github.com/stealthrocket/timecraft/internal/print/human"
)

const exportUsage = `
Usage: timecraft export <resource type> <resource id> <output file>
The export command reads resources from the time machine registry and writes
them to local files. This command is useful to extract data generated by
timecraft and visualize it with other tools.
The last argument is the location where the resource is exported, typically
a path on the file system. The special value "-" may be set to write the
resource to stdout.
Options:
-h, --help Show this usage information
-r, --registry path Path to the timecraft registry (default to ~/.timecraft)
`

func export(ctx context.Context, args []string) error {
var (
registryPath = human.Path("~/.timecraft")
)

flagSet := newFlagSet("timecraft export", exportUsage)
customVar(flagSet, &registryPath, "r", "registry")
args = parseFlags(flagSet, args)

if len(args) != 3 {
return errors.New(`expected resource type, id, and output file as argument` + useCmd("export"))
}

resource, err := findResource("describe", args[0])
if err != nil {
return err
}

registry, err := openRegistry(registryPath)
if err != nil {
return err
}

if resource.typ == "log" {
// How should we handle logs?
// - write the manifest.json + segments to a tar archive?
// - combines the segments into a single log file?
// - ???
return errors.New(`TODO`)
}

var hash format.Hash
if resource.typ == "process" {
processID, err := uuid.Parse(args[1])
if err != nil {
return errors.New(`malformed process id (not a UUID)`)
}
manifest, err := registry.LookupLogManifest(ctx, processID)
if err != nil {
return err
}
hash = manifest.Process.Digest
} else {
desc, err := registry.LookupDescriptor(ctx, format.ParseHash(args[1]))
if err != nil {
return err
}
hash = desc.Digest
}

r, err := registry.LookupResource(ctx, hash)
if err != nil {
return err
}
defer r.Close()

w := io.Writer(os.Stdout)
if outputFile := args[2]; outputFile != "-" {
f, err := os.Create(outputFile)
if err != nil {
return err
}
defer f.Close()
w = f
}

_, err = io.Copy(w, r)
return err
}
62 changes: 27 additions & 35 deletions internal/cmd/get.go
Original file line number Diff line number Diff line change
Expand Up @@ -127,18 +127,11 @@ func get(ctx context.Context, args []string) error {
args = parseFlags(flagSet, args)

if len(args) != 1 {
return errors.New(`expected exactly one resource type as argument` + useGet())
return errors.New(`expected exactly one resource type as argument` + useCmd("get"))
}
resourceTypeLookup := args[0]
resource, ok := findResource(resourceTypeLookup, resources[:])
if !ok {
matchingResources := findMatchingResources(resourceTypeLookup, resources[:])
if len(matchingResources) == 0 {
return fmt.Errorf(`no resources matching '%s'`+useGet(), resourceTypeLookup)
}
return fmt.Errorf(`no resources matching '%s'
Did you mean?%s`, resourceTypeLookup, joinResourceTypes(matchingResources, "\n "))
resource, err := findResource("get", args[0])
if err != nil {
return err
}

registry, err := openRegistry(registryPath)
Expand Down Expand Up @@ -348,27 +341,35 @@ func newTableWriter[T1, T2 any](w io.Writer, orderBy func(T1, T1) bool, conv fun
return stream.NewWriteCloser(cw, tw)
}

func findResource(typ string, options []resource) (resource, bool) {
for _, option := range options {
if option.typ == typ {
return option, true
func findResource(cmd, typ string) (*resource, error) {
for i, resource := range resources {
if resource.typ == typ {
return &resources[i], nil
}
for _, alt := range option.alt {
for _, alt := range resource.alt {
if alt == typ {
return option, true
return &resources[i], nil
}
}
}
return resource{}, false
}

func findMatchingResources(typ string, options []resource) (matches []resource) {
for _, option := range options {
if prefixLength(option.typ, typ) > 1 || prefixLength(typ, option.typ) > 1 {
matches = append(matches, option)
var matchingResources []*resource
for i, resource := range resources {
if prefixLength(resource.typ, typ) > 1 || prefixLength(typ, resource.typ) > 1 {
matchingResources = append(matchingResources, &resources[i])
}
}
return matches
if len(matchingResources) == 0 {
return nil, fmt.Errorf(`no resources matching '%s'%s`, typ, useCmd(cmd))
}

var resourceTypes strings.Builder
for _, r := range matchingResources {
resourceTypes.WriteString("\n ")
resourceTypes.WriteString(r.typ)
}

return nil, fmt.Errorf("no resources matching '%s'\n\nDid you mean?%s", typ, &resourceTypes)
}

func prefixLength(base, prefix string) int {
Expand All @@ -379,19 +380,10 @@ func prefixLength(base, prefix string) int {
return n
}

func joinResourceTypes(resources []resource, prefix string) string {
s := new(strings.Builder)
for _, r := range resources {
s.WriteString(prefix)
s.WriteString(r.typ)
}
return s.String()
}

func useGet() string {
func useCmd(cmd string) string {
s := new(strings.Builder)
s.WriteString("\n\n")
s.WriteString(`Use 'timecraft get <resource type>' where the supported resource types are:`)
s.WriteString(`Use 'timecraft ` + cmd + ` <resource type>' where the supported resource types are:`)
for _, r := range resources {
s.WriteString("\n ")
s.WriteString(r.typ)
Expand Down
8 changes: 7 additions & 1 deletion internal/cmd/help.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ Usage: timecraft <command> [options]
Registry Commands:
describe Show detailed information about specific resources
export Export resources to local files
get Display resources from the time machine registry
Runtime Commands:
Expand All @@ -28,6 +29,9 @@ For a description of each command, run 'timecraft help <command>'.`
func help(ctx context.Context, args []string) error {
flagSet := newFlagSet("timecraft help", helpUsage)
args = parseFlags(flagSet, args)
if len(args) == 0 {
args = []string{"help"}
}

for i, cmd := range args {
var msg string
Expand All @@ -39,9 +43,11 @@ func help(ctx context.Context, args []string) error {
switch cmd {
case "describe":
msg = describeUsage
case "export":
msg = exportUsage
case "get":
msg = getUsage
case "help", "":
case "help":
msg = helpUsage
case "profile":
msg = profileUsage
Expand Down
2 changes: 2 additions & 0 deletions internal/cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,8 @@ func Root(ctx context.Context, args ...string) int {
switch cmd {
case "describe":
err = describe(ctx, args)
case "export":
err = export(ctx, args)
case "get":
err = get(ctx, args)
case "help":
Expand Down
4 changes: 4 additions & 0 deletions internal/timemachine/registry.go
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,10 @@ func (reg *Registry) LookupDescriptor(ctx context.Context, hash format.Hash) (*f
return reg.lookupDescriptor(ctx, hash)
}

func (reg *Registry) LookupResource(ctx context.Context, hash format.Hash) (io.ReadCloser, error) {
return reg.Store.ReadObject(ctx, reg.objectKey(hash))
}

func (reg *Registry) ListModules(ctx context.Context, timeRange TimeRange, tags ...object.Tag) stream.ReadCloser[*format.Descriptor] {
return reg.listObjects(ctx, format.TypeTimecraftModule, timeRange, tags)
}
Expand Down

0 comments on commit a8ae16a

Please sign in to comment.