diff --git a/cmd/cluster/cluster.go b/cmd/cluster/cluster.go index c6f931147..1f843416f 100644 --- a/cmd/cluster/cluster.go +++ b/cmd/cluster/cluster.go @@ -48,7 +48,9 @@ func NewCmdCluster() *cobra.Command { NewCmdClusterStop(), NewCmdClusterDelete(), NewCmdClusterList(), - NewCmdClusterEdit()) + NewCmdClusterEdit(), + NewCmdClusterExportLogs(), + ) // add flags diff --git a/cmd/cluster/clusterExportLogs.go b/cmd/cluster/clusterExportLogs.go new file mode 100644 index 000000000..a6c239ed3 --- /dev/null +++ b/cmd/cluster/clusterExportLogs.go @@ -0,0 +1,68 @@ +/* +Copyright © 2020-2023 The k3d Author(s) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +*/ + +package cluster + +import ( + "github.com/k3d-io/k3d/v5/cmd/util" + "github.com/k3d-io/k3d/v5/pkg/client" + l "github.com/k3d-io/k3d/v5/pkg/logger" + "github.com/k3d-io/k3d/v5/pkg/runtimes" + k3d "github.com/k3d-io/k3d/v5/pkg/types" + "github.com/spf13/cobra" +) + +var exportPath string + +func NewCmdClusterExportLogs() *cobra.Command { + cmd := &cobra.Command{ + Use: "export-logs CLUSTER [PATH]", + Short: "Export logs of a k3d cluster", + Long: `Export logs of a k3d cluster.`, + ValidArgsFunction: util.ValidArgsAvailableClusters, + Run: func(cmd *cobra.Command, args []string) { + cluster := parseExportLogsCmd(cmd, args) + + for _, node := range cluster.Nodes { + if err := runtimes.SelectedRuntime.ExportLogsFromNode(cmd.Context(), node, exportPath); err != nil { + l.Log().Fatalln(err) + } + } + }, + } + + cmd.Flags().StringVarP(&exportPath, "path", "p", "", "Path to export the logs to") + + return cmd +} + +func parseExportLogsCmd(cmd *cobra.Command, args []string) *k3d.Cluster { + if len(args) == 0 || len(args[0]) == 0 { + l.Log().Fatalln("No cluster name given") + } + + cluster, err := client.ClusterGet(cmd.Context(), runtimes.SelectedRuntime, &k3d.Cluster{Name: args[0]}) + if err != nil { + l.Log().Fatalln(err) + } + return cluster +} diff --git a/cmd/node/node.go b/cmd/node/node.go index 132f8f979..f0a6e21ab 100644 --- a/cmd/node/node.go +++ b/cmd/node/node.go @@ -47,7 +47,9 @@ func NewCmdNode() *cobra.Command { NewCmdNodeStop(), NewCmdNodeDelete(), NewCmdNodeList(), - NewCmdNodeEdit()) + NewCmdNodeEdit(), + NewCmdNodeExportLogs(), + ) // add flags diff --git a/cmd/node/nodeExportLogs.go b/cmd/node/nodeExportLogs.go new file mode 100644 index 000000000..79e33a78e --- /dev/null +++ b/cmd/node/nodeExportLogs.go @@ -0,0 +1,69 @@ +/* +Copyright © 2020-2024 The k3d Author(s) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +*/ + +package node + +import ( + "os" + + "github.com/k3d-io/k3d/v5/cmd/util" + "github.com/k3d-io/k3d/v5/pkg/client" + l "github.com/k3d-io/k3d/v5/pkg/logger" + "github.com/k3d-io/k3d/v5/pkg/runtimes" + k3d "github.com/k3d-io/k3d/v5/pkg/types" + "github.com/spf13/cobra" +) + +var exportPath string + +func NewCmdNodeExportLogs() *cobra.Command { + cmd := &cobra.Command{ + Use: "export-logs NODE", + Short: "Export logs of a k3d node", + Long: `Export logs of a k3d node.`, + ValidArgsFunction: util.ValidArgsAvailableNodes, + Run: func(cmd *cobra.Command, args []string) { + node := parseExportLogsCmd(cmd, args) + l.Log().Fatalln(runtimes.SelectedRuntime.ExportLogsFromNode(cmd.Context(), node, exportPath)) + }, + } + + cwd, err := os.Getwd() + if err != nil { + l.Log().Fatalln(err) + } + cmd.Flags().StringVarP(&exportPath, "path", "p", cwd, "Path to export the logs to") + + return cmd +} + +func parseExportLogsCmd(cmd *cobra.Command, args []string) *k3d.Node { + if len(args) == 0 || len(args[0]) == 0 { + l.Log().Fatalln("No node name given") + } + + node, err := client.NodeGet(cmd.Context(), runtimes.SelectedRuntime, &k3d.Node{Name: args[0]}) + if err != nil { + l.Log().Fatalln(err) + } + return node +} diff --git a/pkg/runtimes/docker/docker.go b/pkg/runtimes/docker/docker.go index 5e2d28b8a..4c812d7b4 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,11 @@ func (d Docker) ID() string { return "docker" } +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"}, +} + // 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..0e66d113e 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,45 @@ 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) 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) + } + } + return nil +} diff --git a/pkg/runtimes/docker/util.go b/pkg/runtimes/docker/util.go index 5ece57823..d427ee86e 100644 --- a/pkg/runtimes/docker/util.go +++ b/pkg/runtimes/docker/util.go @@ -28,6 +28,7 @@ import ( "fmt" "io" "os" + "path/filepath" "regexp" "strings" @@ -211,3 +212,96 @@ 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 the link target is a directory, recursively copy its contents + 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: + // Resolve the symlink and copy the original content + 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..82b7822ac 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) error // @param context, node, source, destination } // GetRuntime checks, if a given name is represented by an implemented k3d runtime and returns it