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

runtimes: enable log export feature #1471

Merged
merged 1 commit into from
Jan 17, 2025
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
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