feat(init): Implement 'ls' command (#721)

Fixes #719

Signed-off-by: Seán C McCord <ulexus@gmail.com>
This commit is contained in:
Seán C. McCord 2019-06-07 13:19:20 -04:00 committed by Andrew Rynhard
parent f5969d2c6c
commit 532a53bfaf
4 changed files with 213 additions and 0 deletions

107
cmd/osctl/cmd/ls.go Normal file
View File

@ -0,0 +1,107 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package cmd
import (
"fmt"
"io"
"os"
"text/tabwriter"
"time"
"github.com/spf13/cobra"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"github.com/talos-systems/talos/cmd/osctl/pkg/client"
"github.com/talos-systems/talos/cmd/osctl/pkg/helpers"
"github.com/talos-systems/talos/internal/app/osd/proto"
)
// lsCmd represents the ls command
var lsCmd = &cobra.Command{
Use: "ls [path]",
Short: "Retrieve a directory listing",
Long: ``,
Run: func(cmd *cobra.Command, args []string) {
setupClient(func(c *client.Client) {
rootDir := "/"
if len(args) > 0 {
rootDir = args[0]
}
long, err := cmd.Flags().GetBool("long")
if err != nil {
helpers.Fatalf("failed to parse long flag: %v", err)
}
recurse, err := cmd.Flags().GetBool("recurse")
if err != nil {
helpers.Fatalf("failed to parse recurse flag: %v", err)
}
recursionDepth, err := cmd.Flags().GetInt32("depth")
if err != nil {
helpers.Fatalf("failed to parse depth flag: %v", err)
}
stream, err := c.LS(globalCtx, proto.LSRequest{
Root: rootDir,
Recurse: recurse,
RecursionDepth: recursionDepth,
})
if err != nil {
helpers.Fatalf("error fetching logs: %s", err)
}
if !long {
for {
info, err := stream.Recv()
if err != nil {
if err == io.EOF || status.Code(err) == codes.Canceled {
return
}
helpers.Fatalf("error streaming logs: %s", err)
}
if info.Error != "" {
fmt.Fprintf(os.Stderr, "error reading file %s: %s\n", info.Name, info.Error)
} else {
fmt.Println(info.Name)
}
}
}
w := tabwriter.NewWriter(os.Stdout, 0, 0, 3, ' ', 0)
fmt.Fprintln(w, "MODE\tSIZE(B)\tLASTMOD\tNAME")
for {
info, err := stream.Recv()
if err != nil {
if err == io.EOF || status.Code(err) == codes.Canceled {
helpers.Should(w.Flush())
return
}
helpers.Fatalf("error streaming logs: %s", err)
}
if info.Error != "" {
fmt.Fprintf(os.Stderr, "error reading file %s: %s\n", info.Name, info.Error)
} else {
fmt.Fprintf(w, "%s\t%d\t%s\t%s\n",
os.FileMode(info.Mode).String(),
info.Size,
time.Unix(info.Modified, 0).Format("Jan 2 2006"),
info.Name,
)
}
}
})
},
}
func init() {
lsCmd.Flags().StringVarP(&target, "target", "t", "", "target the specificed node")
lsCmd.Flags().BoolP("long", "l", false, "display additional file details")
lsCmd.Flags().BoolP("recurse", "r", false, "recurse into subdirectories")
lsCmd.Flags().Int32P("depth", "d", 0, "maximum recursion depth")
rootCmd.AddCommand(lsCmd)
}

View File

@ -220,6 +220,11 @@ func (c *Client) DF(ctx context.Context) (*proto.DFReply, error) {
return c.client.DF(ctx, &empty.Empty{})
}
// LS implements the proto.OSDClient interface.
func (c *Client) LS(ctx context.Context, req proto.LSRequest) (stream proto.OSD_LSClient, err error) {
return c.client.LS(ctx, &req)
}
// Upgrade initiates a Talos upgrade ... and implements the proto.OSDClient
// interface
func (c *Client) Upgrade(ctx context.Context, asseturl string) (string, error) {

View File

@ -39,6 +39,9 @@ import (
"github.com/talos-systems/talos/pkg/userdata"
)
// OSPathSeparator is the string version of the os.PathSeparator
const OSPathSeparator = string(os.PathSeparator)
// Registrator is the concrete type that implements the factory.Registrator and
// proto.OSDServer interfaces.
type Registrator struct {
@ -449,6 +452,57 @@ func (r *Registrator) DF(ctx context.Context, in *empty.Empty) (reply *proto.DFR
return reply, multiErr.ErrorOrNil()
}
// LS implements the proto.OSDServer interface.
func (r *Registrator) LS(req *proto.LSRequest, s proto.OSD_LSServer) error {
if req == nil {
req = new(proto.LSRequest)
}
if !strings.HasPrefix(req.Root, OSPathSeparator) {
// Make sure we use complete paths
req.Root = OSPathSeparator + req.Root
}
req.Root = strings.TrimSuffix(req.Root, OSPathSeparator)
if req.Root == "" {
req.Root = "/"
}
var maxDepth int
if req.Recurse {
if req.RecursionDepth == 0 {
maxDepth = -1
} else {
maxDepth = int(req.RecursionDepth)
}
}
return filepath.Walk(req.Root, func(path string, info os.FileInfo, err error) error {
if err != nil {
// Send errors upstream so that we do not abort the path walk
return s.Send(&proto.FileInfo{
Name: path,
Error: err.Error(),
})
}
err = s.Send(&proto.FileInfo{
Name: path,
Size: info.Size(),
Mode: uint32(info.Mode()),
Modified: info.ModTime().Unix(),
IsDir: info.IsDir(),
})
if err != nil {
return err
}
if info.IsDir() && atMaxDepth(maxDepth, req.Root, path) {
return filepath.SkipDir
}
return nil
})
}
func k8slogs(ctx context.Context, req *proto.LogsRequest) (string, error) {
inspector, err := containers.NewInspector(ctx, req.Namespace)
if err != nil {
@ -482,3 +536,14 @@ func k8slogs(ctx context.Context, req *proto.LogsRequest) (string, error) {
return "", nil
}
func atMaxDepth(max int, root, cur string) bool {
if max < 0 {
return false
}
if root == cur {
// always recurse the root directory
return false
}
return (strings.Count(cur, OSPathSeparator) - strings.Count(root, OSPathSeparator)) >= max
}

View File

@ -13,6 +13,7 @@ service OSD {
rpc Dmesg(google.protobuf.Empty) returns (Data) {}
rpc Kubeconfig(google.protobuf.Empty) returns (Data) {}
rpc Logs(LogsRequest) returns (stream Data) {}
rpc LS(LSRequest) returns (stream FileInfo) {}
rpc Processes(ProcessesRequest) returns (ProcessesReply) {}
rpc Reset(google.protobuf.Empty) returns (ResetReply) {}
rpc Restart(RestartRequest) returns (RestartReply) {}
@ -99,3 +100,38 @@ message DFStat {
uint64 available = 3;
string mounted_on = 4;
}
// LSRequest describes a request to list the contents of a directory
message LSRequest {
// Root indicates the root directory for the list. If not indicated, '/' is presumed.
string root = 1;
// Recurse indicates that subdirectories should be recursed.
bool recurse = 2;
// RecursionDepth indicates how many levels of subdirectories should be recursed. The default (0) indicates that no limit should be enforced.
int32 recursion_depth = 3;
}
// FileInfo describes a file or directory's information
message FileInfo {
// Name is the name (including prefixed path) of the file or directory
string name = 1;
// Size indicates the number of bytes contained within the file
int64 size = 2;
// Mode is the bitmap of UNIX mode/permission flags of the file
uint32 mode = 3;
// Modified indicates the UNIX timestamp at which the file was last modified
int64 modified = 4; // TODO: unix timestamp or include proto's Date type
// IsDir indicates that the file is a directory
bool is_dir = 5;
// Error describes any error encountered while trying to read the file information.
string error = 6;
}