From fe9260f412ccd853ce5021994130eae5a90abbf7 Mon Sep 17 00:00:00 2001 From: Harsha Narayana Date: Wed, 17 Jul 2024 19:47:28 +0530 Subject: [PATCH] debug: enable log export feature --- cmd/cluster/cluster.go | 3 +- cmd/debug/debug.go | 42 ++++++++++ cmd/node/node.go | 3 +- pkg/runtimes/docker/docker.go | 27 +++++++ pkg/runtimes/docker/node.go | 47 ++++++++++++ pkg/runtimes/docker/util.go | 139 ++++++++++++++++++++++++++++++++++ pkg/runtimes/runtime.go | 1 + 7 files changed, 260 insertions(+), 2 deletions(-) diff --git a/cmd/cluster/cluster.go b/cmd/cluster/cluster.go index c6f931147..f9221f416 100644 --- a/cmd/cluster/cluster.go +++ b/cmd/cluster/cluster.go @@ -48,7 +48,8 @@ func NewCmdCluster() *cobra.Command { NewCmdClusterStop(), NewCmdClusterDelete(), NewCmdClusterList(), - NewCmdClusterEdit()) + NewCmdClusterEdit(), + ) // add flags diff --git a/cmd/debug/debug.go b/cmd/debug/debug.go index aa584674d..32bc6126e 100644 --- a/cmd/debug/debug.go +++ b/cmd/debug/debug.go @@ -23,6 +23,8 @@ package debug import ( "fmt" + "os" + "path/filepath" "github.com/spf13/cobra" "sigs.k8s.io/yaml" @@ -34,6 +36,10 @@ import ( "github.com/k3d-io/k3d/v5/pkg/types" ) +var nodeName string +var exportPath string +var components []string + // NewCmdDebug returns a new cobra command func NewCmdDebug() *cobra.Command { cmd := &cobra.Command{ @@ -50,6 +56,42 @@ func NewCmdDebug() *cobra.Command { } cmd.AddCommand(NewCmdDebugLoadbalancer()) + cmd.AddCommand(NewCmdExportLogs()) + return cmd +} + +func NewCmdExportLogs() *cobra.Command { + cmd := &cobra.Command{ + Use: "export-logs CLUSTER", + Short: "Export logs of a k3d cluster", + Long: "Export logs of a k3d cluster for all nodes or selective nodes using filters", + ValidArgsFunction: util.ValidArgsAvailableClusters, + Run: func(cmd *cobra.Command, args []string) { + if len(args) == 0 || len(args[0]) == 0 { + l.Log().Fatalln("Cluster name is required") + } + cluster, err := client.ClusterGet(cmd.Context(), runtimes.SelectedRuntime, &types.Cluster{Name: args[0]}) + if err != nil { + l.Log().Fatalln(err) + } + exportPath = filepath.Join(exportPath, fmt.Sprintf("debug-logs-%s", cluster.Name)) + for _, node := range cluster.Nodes { + if nodeName == "" || nodeName == node.Name { + if err := runtimes.SelectedRuntime.ExportLogsFromNode(cmd.Context(), node, exportPath, components); err != nil { + l.Log().Fatalln(err) + } + } + } + }, + } + + cwd, err := os.Getwd() + if err != nil { + l.Log().Fatalln(err) + } + + cmd.Flags().StringVarP(&nodeName, "node", "n", "", "Node name to export logs from") + cmd.Flags().StringVarP(&exportPath, "path", "p", cwd, "Path to export the logs to") return cmd } diff --git a/cmd/node/node.go b/cmd/node/node.go index 132f8f979..1781298d6 100644 --- a/cmd/node/node.go +++ b/cmd/node/node.go @@ -47,7 +47,8 @@ func NewCmdNode() *cobra.Command { NewCmdNodeStop(), NewCmdNodeDelete(), NewCmdNodeList(), - NewCmdNodeEdit()) + NewCmdNodeEdit(), + ) // add flags diff --git a/pkg/runtimes/docker/docker.go b/pkg/runtimes/docker/docker.go index 5e2d28b8a..01534f330 100644 --- a/pkg/runtimes/docker/docker.go +++ b/pkg/runtimes/docker/docker.go @@ -29,6 +29,7 @@ import ( "os" l "github.com/k3d-io/k3d/v5/pkg/logger" + k3d "github.com/k3d-io/k3d/v5/pkg/types" ) type Docker struct{} @@ -42,6 +43,32 @@ func (d Docker) ID() string { return "docker" } +type commandInfo struct { + Command []string + FileName string +} + +var roleBasedExportPath = map[k3d.Role][]string{ + k3d.ServerRole: {"/var/log", "/var/lib/rancher/k3s/agent/containerd/containerd.log"}, + k3d.AgentRole: {"/var/log", "/var/lib/rancher/k3s/agent/containerd/containerd.log"}, +} + +var roleBasedCommandsToExecute = map[k3d.Role][]commandInfo{ + k3d.ServerRole: { + {Command: []string{"crictl", "images"}, FileName: "crictl-images.log"}, + {Command: []string{"crictl", "ps", "-a"}, FileName: "crictl-ps.log"}, + {Command: []string{"kubectl", "cluster-info"}, FileName: "cluster-info.log"}, + {Command: []string{"kubectl", "version"}, FileName: "kubectl-version.log"}, + {Command: []string{"kubectl", "get", "node", "-o", "yaml"}, FileName: "kubectl-get-node.yaml"}, + {Command: []string{"ps", "-aef"}, FileName: "ps-aef.log"}, + }, + k3d.AgentRole: { + {Command: []string{"crictl", "images"}, FileName: "crictl-images.log"}, + {Command: []string{"crictl", "ps", "-a"}, FileName: "crictl-ps.log"}, + {Command: []string{"ps", "-aef"}, FileName: "ps-aef.log"}, + }, +} + // GetHost returns the docker daemon host func (d Docker) GetHost() string { // a) docker-machine diff --git a/pkg/runtimes/docker/node.go b/pkg/runtimes/docker/node.go index 5de0e45bd..457d32647 100644 --- a/pkg/runtimes/docker/node.go +++ b/pkg/runtimes/docker/node.go @@ -29,6 +29,8 @@ import ( "errors" "fmt" "io" + "os" + "path/filepath" "time" "github.com/docker/docker/api/types" @@ -493,3 +495,48 @@ func (d Docker) RenameNode(ctx context.Context, node *k3d.Node, newName string) return docker.ContainerRename(ctx, container.ID, newName) } + +func (d Docker) ExportLogsFromNode(ctx context.Context, node *k3d.Node, dstPath string, components []string) error { + destinationPath := filepath.Join(dstPath, node.Name) + + err := os.MkdirAll(destinationPath, os.FileMode(os.ModePerm)) + if err != nil { + return fmt.Errorf("failed to create destination directory '%s': %w", destinationPath, err) + } + c, err := getNodeContainer(ctx, node) + if err != nil { + return fmt.Errorf("failed to get container for node '%s': %w", node.Name, err) + } + + // create docker client + docker, err := GetDockerClient() + if err != nil { + return fmt.Errorf("failed to get docker client: %w", err) + } + defer docker.Close() + + reader, err := docker.ContainerLogs(ctx, c.ID, container.LogsOptions{Details: true, ShowStdout: true, ShowStderr: true}) + if err != nil { + return fmt.Errorf("failed to get logs from container '%s': %w", c.ID, err) + } + defer reader.Close() + + err = readerToFile(reader, filepath.Join(dstPath, fmt.Sprintf("%s.log", node.Name))) + if err != nil { + return fmt.Errorf("failed to write logs to file: %w", err) + } + + for _, srcPath := range roleBasedExportPath[node.Role] { + contentReader, _, err := docker.CopyFromContainer(ctx, c.ID, srcPath) + if err != nil { + return fmt.Errorf("failed to copy files from container '%s': %w", c.ID, err) + } + if err := untarReader(ctx, docker, c.ID, contentReader, destinationPath); err != nil { + return fmt.Errorf("failed to untar files from container '%s': %w", c.ID, err) + } + } + for _, command := range roleBasedCommandsToExecute[node.Role] { + d.executeCommandInContainer(ctx, docker, c.ID, command.Command, filepath.Join(destinationPath, command.FileName)) + } + return nil +} diff --git a/pkg/runtimes/docker/util.go b/pkg/runtimes/docker/util.go index 5ece57823..9b7bf7b0d 100644 --- a/pkg/runtimes/docker/util.go +++ b/pkg/runtimes/docker/util.go @@ -23,11 +23,13 @@ package docker import ( "archive/tar" + "bufio" "bytes" "context" "fmt" "io" "os" + "path/filepath" "regexp" "strings" @@ -36,6 +38,7 @@ import ( "github.com/docker/cli/cli/command" "github.com/docker/cli/cli/flags" "github.com/docker/docker/api/types/filters" + "github.com/docker/docker/api/types/strslice" "github.com/docker/docker/client" "github.com/docker/docker/pkg/archive" l "github.com/k3d-io/k3d/v5/pkg/logger" @@ -180,6 +183,51 @@ func (d Docker) ReadFromNode(ctx context.Context, path string, node *k3d.Node) ( return reader, err } +func (d Docker) executeCommandInContainer(ctx context.Context, cli client.APIClient, containerID string, command []string, outputFilePath string) error { + execConfig := container.ExecOptions{ + Cmd: strslice.StrSlice(command), + AttachStdout: true, + AttachStderr: true, + Tty: false, + } + + execIDResp, err := cli.ContainerExecCreate(ctx, containerID, execConfig) + if err != nil { + return fmt.Errorf("error creating exec instance: %v", err) + } + + resp, err := cli.ContainerExecAttach(ctx, execIDResp.ID, container.ExecStartOptions{Tty: false}) + if err != nil { + return fmt.Errorf("error attaching to exec instance: %v", err) + } + defer resp.Close() + + outputFile, err := os.Create(outputFilePath) + if err != nil { + return fmt.Errorf("error creating output file: %v", err) + } + defer outputFile.Close() + + writer := bufio.NewWriter(outputFile) + + var stdoutBuf bytes.Buffer + + _, err = io.Copy(&stdoutBuf, resp.Reader) + if err != nil { + return fmt.Errorf("Error copying stdout: %v", err) + } + + if _, err := writer.WriteString(stdoutBuf.String()); err != nil { + return fmt.Errorf("Error writing stdout to file: %v", err) + } + + if err := writer.Flush(); err != nil { + return fmt.Errorf("Error flushing writer: %v", err) + } + + return nil +} + // GetDockerClient returns a docker client func GetDockerClient() (client.APIClient, error) { dockerCli, err := command.NewDockerCli(command.WithStandardStreams()) @@ -211,3 +259,94 @@ func isAttachedToNetwork(node *k3d.Node, network string) bool { } return false } + +func readerToFile(reader io.Reader, target string) error { + file, err := os.OpenFile(target, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, os.FileMode(0600)) + if err != nil { + return err + } + defer file.Close() + if _, err := io.Copy(file, reader); err != nil { + return err + } + + return nil +} + +func copyFile(tarReader *tar.Reader, target string, mode os.FileMode) error { + file, err := os.OpenFile(target, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, mode) + if err != nil { + return err + } + defer file.Close() + + if _, err := io.Copy(file, tarReader); err != nil { + return err + } + + return nil +} + +func copyOriginalContent(cli client.APIClient, ctx context.Context, containerID, linkTarget, target string) error { + reader, _, err := cli.CopyFromContainer(ctx, containerID, linkTarget) + if err != nil { + return err + } + defer reader.Close() + + tarReader := tar.NewReader(reader) + header, err := tarReader.Next() + if err != nil { + return err + } + + if header.Typeflag == tar.TypeReg { + if err := copyFile(tarReader, target, os.FileMode(header.Mode)); err != nil { + return err + } + } else if header.Typeflag == tar.TypeDir { + if err := untarReader(ctx, cli, containerID, reader, target); err != nil { + return err + } + } else { + return fmt.Errorf("unsupported symlink target file type: %v", header.Typeflag) + } + + return nil +} + +func untarReader(ctx context.Context, cli client.APIClient, containerID string, reader io.Reader, dstPath string) error { + tarReader := tar.NewReader(reader) + + for { + header, err := tarReader.Next() + if err == io.EOF { + break + } + if err != nil { + return err + } + + target := filepath.Join(dstPath, header.Name) + + switch header.Typeflag { + case tar.TypeDir: + if err := os.MkdirAll(target, os.FileMode(header.Mode)); err != nil { + return err + } + case tar.TypeReg: + if err := copyFile(tarReader, target, os.FileMode(header.Mode)); err != nil { + return err + } + case tar.TypeSymlink: + linkTarget := header.Linkname + if err := copyOriginalContent(cli, ctx, containerID, linkTarget, target); err != nil { + return err + } + default: + return fmt.Errorf("unsupported file type: %v", header.Typeflag) + } + } + + return nil +} diff --git a/pkg/runtimes/runtime.go b/pkg/runtimes/runtime.go index 52f2fce06..91c6fb2f1 100644 --- a/pkg/runtimes/runtime.go +++ b/pkg/runtimes/runtime.go @@ -81,6 +81,7 @@ type Runtime interface { DisconnectNodeFromNetwork(context.Context, *k3d.Node, string) error // @param context, node, network name Info() (*runtimeTypes.RuntimeInfo, error) GetNetwork(context.Context, *k3d.ClusterNetwork) (*k3d.ClusterNetwork, error) // @param context, network (so we can filter by name or by id) + ExportLogsFromNode(context.Context, *k3d.Node, string, []string) error // @param context, node, source, destination, components (This is meant for future use once we identify what/how to filter) } // GetRuntime checks, if a given name is represented by an implemented k3d runtime and returns it