Skip to content

Commit

Permalink
debug: enable log export feature
Browse files Browse the repository at this point in the history
  • Loading branch information
harshanarayana committed Jan 17, 2025
1 parent 03952c0 commit fe9260f
Show file tree
Hide file tree
Showing 7 changed files with 260 additions and 2 deletions.
3 changes: 2 additions & 1 deletion cmd/cluster/cluster.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,8 @@ func NewCmdCluster() *cobra.Command {
NewCmdClusterStop(),
NewCmdClusterDelete(),
NewCmdClusterList(),
NewCmdClusterEdit())
NewCmdClusterEdit(),
)

// add flags

Expand Down
42 changes: 42 additions & 0 deletions cmd/debug/debug.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ package debug

import (
"fmt"
"os"
"path/filepath"

"github.com/spf13/cobra"
"sigs.k8s.io/yaml"
Expand All @@ -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{
Expand All @@ -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
}
Expand Down
3 changes: 2 additions & 1 deletion cmd/node/node.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,8 @@ func NewCmdNode() *cobra.Command {
NewCmdNodeStop(),
NewCmdNodeDelete(),
NewCmdNodeList(),
NewCmdNodeEdit())
NewCmdNodeEdit(),
)

// add flags

Expand Down
27 changes: 27 additions & 0 deletions pkg/runtimes/docker/docker.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{}
Expand All @@ -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
Expand Down
47 changes: 47 additions & 0 deletions pkg/runtimes/docker/node.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ import (
"errors"
"fmt"
"io"
"os"
"path/filepath"
"time"

"github.com/docker/docker/api/types"
Expand Down Expand Up @@ -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
}
139 changes: 139 additions & 0 deletions pkg/runtimes/docker/util.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,13 @@ package docker

import (
"archive/tar"
"bufio"
"bytes"
"context"
"fmt"
"io"
"os"
"path/filepath"
"regexp"
"strings"

Expand All @@ -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"
Expand Down Expand Up @@ -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())
Expand Down Expand Up @@ -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
}
1 change: 1 addition & 0 deletions pkg/runtimes/runtime.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down

0 comments on commit fe9260f

Please sign in to comment.