mirror of
https://github.com/kairos-io/kairos-agent.git
synced 2025-09-13 13:10:22 +00:00
2462 debugging logs (#830)
* [refactoring] simplify method and make it more efficient
Signed-off-by: Dimitris Karakasilis <dimitris@karakasilis.me>
* [WIP] Introduce `logs` command to collects logs from various places
Signed-off-by: Dimitris Karakasilis <dimitris@karakasilis.me>
* Handle globs properly and merge default logs with user provided ones
Signed-off-by: Dimitris Karakasilis <dimitris@karakasilis.me>
* Change default logs location to be the current directory
Signed-off-by: Dimitris Karakasilis <dimitris@karakasilis.me>
* Skip new field in the schema tests
TODO: Update the schema and re-enable
Signed-off-by: Dimitris Karakasilis <dimitris@karakasilis.me>
* Remove test focus
Signed-off-by: Dimitris Karakasilis <dimitris@karakasilis.me>
* Add more default services
Signed-off-by: Dimitris Karakasilis <dimitris@karakasilis.me>
* Don't try to run journactl on non systemd distros
Signed-off-by: Dimitris Karakasilis <dimitris@karakasilis.me>
* Add more files (for openrc)
c6fdf6ee67/pkg/bundled/cloudconfigs/09_openrc_services.yaml (L52)
Signed-off-by: Dimitris Karakasilis <dimitris@karakasilis.me>
* Use standard library for globbing
Signed-off-by: Dimitris Karakasilis <dimitris@karakasilis.me>
* Capture all files under `/var/log`
because there is also k3s.log (maybe also k0s) etc. Better have them all
than missing some.
Signed-off-by: Dimitris Karakasilis <dimitris@karakasilis.me>
---------
Signed-off-by: Dimitris Karakasilis <dimitris@karakasilis.me>
This commit is contained in:
committed by
GitHub
parent
41b52e9970
commit
d85d7985fe
231
internal/agent/logs.go
Normal file
231
internal/agent/logs.go
Normal file
@@ -0,0 +1,231 @@
|
|||||||
|
package agent
|
||||||
|
|
||||||
|
import (
|
||||||
|
"archive/tar"
|
||||||
|
"compress/gzip"
|
||||||
|
"fmt"
|
||||||
|
"io/fs"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/kairos-io/kairos-agent/v2/pkg/config"
|
||||||
|
"github.com/kairos-io/kairos-agent/v2/pkg/constants"
|
||||||
|
v1 "github.com/kairos-io/kairos-agent/v2/pkg/types/v1"
|
||||||
|
fsutils "github.com/kairos-io/kairos-agent/v2/pkg/utils/fs"
|
||||||
|
"github.com/kairos-io/kairos-sdk/collector"
|
||||||
|
"github.com/kairos-io/kairos-sdk/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
// LogsResult represents the collected logs
|
||||||
|
type LogsResult struct {
|
||||||
|
Journal map[string][]byte `yaml:"-"`
|
||||||
|
Files map[string][]byte `yaml:"-"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// LogsCollector handles the collection of logs from various sources
|
||||||
|
type LogsCollector struct {
|
||||||
|
config *config.Config
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewLogsCollector creates a new LogsCollector instance
|
||||||
|
func NewLogsCollector(cfg *config.Config) *LogsCollector {
|
||||||
|
return &LogsCollector{
|
||||||
|
config: cfg,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func defaultLogsConfig() *config.LogsConfig {
|
||||||
|
return &config.LogsConfig{
|
||||||
|
Journal: []string{
|
||||||
|
"kairos-agent",
|
||||||
|
"kairos-installer",
|
||||||
|
"kairos-webui",
|
||||||
|
"cos-setup-boot",
|
||||||
|
"cos-setup-fs",
|
||||||
|
"cos-setup-network",
|
||||||
|
"cos-setup-reconcile",
|
||||||
|
"k3s",
|
||||||
|
"k3s-agent",
|
||||||
|
"k0scontroller",
|
||||||
|
"k0sworker",
|
||||||
|
},
|
||||||
|
Files: []string{
|
||||||
|
"/var/log/kairos/*.log",
|
||||||
|
"/var/log/*.log",
|
||||||
|
"/run/immucore/*.log",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// isSystemdAvailable checks if systemd is available on the system
|
||||||
|
func (lc *LogsCollector) isSystemdAvailable() bool {
|
||||||
|
// Check for systemctl in common locations
|
||||||
|
for _, path := range []string{"/sbin/systemctl", "/bin/systemctl", "/usr/sbin/systemctl", "/usr/bin/systemctl"} {
|
||||||
|
if _, err := lc.config.Fs.Stat(path); err == nil {
|
||||||
|
lc.config.Logger.Debugf("Found systemd at %s", path)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
lc.config.Logger.Debugf("systemd not found, skipping journal log collection")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Collect gathers logs based on the configuration stored in the LogsCollector
|
||||||
|
func (lc *LogsCollector) Collect() (*LogsResult, error) {
|
||||||
|
result := &LogsResult{
|
||||||
|
Journal: make(map[string][]byte),
|
||||||
|
Files: make(map[string][]byte),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Define default configuration
|
||||||
|
logsConfig := defaultLogsConfig()
|
||||||
|
|
||||||
|
// Merge user configuration with defaults
|
||||||
|
if lc.config.Logs != nil {
|
||||||
|
logsConfig.Journal = append(logsConfig.Journal, lc.config.Logs.Journal...)
|
||||||
|
logsConfig.Files = append(logsConfig.Files, lc.config.Logs.Files...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if systemd is available before collecting journal logs
|
||||||
|
if lc.isSystemdAvailable() {
|
||||||
|
// Collect journal logs
|
||||||
|
for _, service := range logsConfig.Journal {
|
||||||
|
output, err := lc.config.Runner.Run("journalctl", "-u", service, "--no-pager", "-o", "cat")
|
||||||
|
if err != nil {
|
||||||
|
lc.config.Logger.Warnf("Failed to collect journal logs for service %s: %v", service, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip services with no journal entries
|
||||||
|
if len(output) == 0 || string(output) == "-- No entries --" {
|
||||||
|
lc.config.Logger.Debugf("No journal entries found for service %s, skipping", service)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
result.Journal[service] = output
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
lc.config.Logger.Infof("systemd not available, skipping journal log collection")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Collect file logs with globbing support
|
||||||
|
for _, pattern := range logsConfig.Files {
|
||||||
|
matches, err := lc.globFiles(pattern)
|
||||||
|
if err != nil {
|
||||||
|
lc.config.Logger.Warnf("Failed to glob pattern %s: %v", pattern, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, file := range matches {
|
||||||
|
content, err := lc.config.Fs.ReadFile(file)
|
||||||
|
if err != nil {
|
||||||
|
lc.config.Logger.Warnf("Failed to read file %s: %v", file, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
result.Files[file] = content
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateTarball creates a compressed tarball from the collected logs
|
||||||
|
func (lc *LogsCollector) CreateTarball(result *LogsResult, outputPath string) error {
|
||||||
|
// Create output directory if it doesn't exist
|
||||||
|
outputDir := filepath.Dir(outputPath)
|
||||||
|
if err := fsutils.MkdirAll(lc.config.Fs, outputDir, constants.DirPerm); err != nil {
|
||||||
|
return fmt.Errorf("failed to create output directory: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the tarball file
|
||||||
|
file, err := lc.config.Fs.Create(outputPath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create tarball file: %w", err)
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
// Create gzip writer
|
||||||
|
gw := gzip.NewWriter(file)
|
||||||
|
defer gw.Close()
|
||||||
|
|
||||||
|
// Create tar writer
|
||||||
|
tw := tar.NewWriter(gw)
|
||||||
|
defer tw.Close()
|
||||||
|
|
||||||
|
// Add journal logs to tarball
|
||||||
|
for service, content := range result.Journal {
|
||||||
|
header := &tar.Header{
|
||||||
|
Name: fmt.Sprintf("journal/%s.log", service),
|
||||||
|
Mode: constants.FilePerm,
|
||||||
|
Size: int64(len(content)),
|
||||||
|
}
|
||||||
|
if err := tw.WriteHeader(header); err != nil {
|
||||||
|
return fmt.Errorf("failed to write journal header: %w", err)
|
||||||
|
}
|
||||||
|
if _, err := tw.Write(content); err != nil {
|
||||||
|
return fmt.Errorf("failed to write journal content: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add file logs to tarball
|
||||||
|
for filePath, content := range result.Files {
|
||||||
|
// Remove leading slash and use full path structure
|
||||||
|
relativePath := strings.TrimPrefix(filePath, "/")
|
||||||
|
header := &tar.Header{
|
||||||
|
Name: fmt.Sprintf("files/%s", relativePath),
|
||||||
|
Mode: constants.FilePerm,
|
||||||
|
Size: int64(len(content)),
|
||||||
|
}
|
||||||
|
if err := tw.WriteHeader(header); err != nil {
|
||||||
|
return fmt.Errorf("failed to write file header: %w", err)
|
||||||
|
}
|
||||||
|
if _, err := tw.Write(content); err != nil {
|
||||||
|
return fmt.Errorf("failed to write file content: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// globFiles expands glob patterns to matching files using the standard library
|
||||||
|
func (lc *LogsCollector) globFiles(pattern string) ([]string, error) {
|
||||||
|
matches, err := fs.Glob(lc.config.Fs, pattern)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return matches, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExecuteLogsCommand executes the logs command with the given parameters
|
||||||
|
func ExecuteLogsCommand(fs v1.FS, logger types.KairosLogger, runner v1.Runner, outputPath string) error {
|
||||||
|
// Scan for configuration from default locations
|
||||||
|
cfg, err := config.Scan(collector.Directories(constants.GetUserConfigDirs()...), collector.NoLogs)
|
||||||
|
if err != nil {
|
||||||
|
logger.Warnf("Failed to load configuration, using defaults: %v", err)
|
||||||
|
// Create a minimal config with just the required components
|
||||||
|
cfg = config.NewConfig(
|
||||||
|
config.WithFs(fs),
|
||||||
|
config.WithLogger(logger),
|
||||||
|
config.WithRunner(runner),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
// Update the scanned config with the provided components
|
||||||
|
cfg.Fs = fs
|
||||||
|
cfg.Logger = logger
|
||||||
|
cfg.Runner = runner
|
||||||
|
}
|
||||||
|
|
||||||
|
collector := NewLogsCollector(cfg)
|
||||||
|
|
||||||
|
result, err := collector.Collect()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to collect logs: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := collector.CreateTarball(result, outputPath); err != nil {
|
||||||
|
return fmt.Errorf("failed to create tarball: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Infof("Logs collected successfully to %s", outputPath)
|
||||||
|
return nil
|
||||||
|
}
|
614
internal/agent/logs_test.go
Normal file
614
internal/agent/logs_test.go
Normal file
@@ -0,0 +1,614 @@
|
|||||||
|
package agent
|
||||||
|
|
||||||
|
import (
|
||||||
|
"archive/tar"
|
||||||
|
"bytes"
|
||||||
|
"compress/gzip"
|
||||||
|
"io"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"github.com/kairos-io/kairos-agent/v2/pkg/config"
|
||||||
|
"github.com/kairos-io/kairos-agent/v2/pkg/constants"
|
||||||
|
fsutils "github.com/kairos-io/kairos-agent/v2/pkg/utils/fs"
|
||||||
|
v1mock "github.com/kairos-io/kairos-agent/v2/tests/mocks"
|
||||||
|
"github.com/kairos-io/kairos-sdk/types"
|
||||||
|
. "github.com/onsi/ginkgo/v2"
|
||||||
|
. "github.com/onsi/gomega"
|
||||||
|
"github.com/twpayne/go-vfs/v5/vfst"
|
||||||
|
)
|
||||||
|
|
||||||
|
var _ = Describe("Logs Command", Label("logs", "cmd"), func() {
|
||||||
|
var (
|
||||||
|
fs *vfst.TestFS
|
||||||
|
cleanup func()
|
||||||
|
err error
|
||||||
|
logger types.KairosLogger
|
||||||
|
runner *v1mock.FakeRunner
|
||||||
|
)
|
||||||
|
|
||||||
|
// Helper function to create a mock systemctl file for systemd detection
|
||||||
|
createMockSystemctl := func() {
|
||||||
|
err := fsutils.MkdirAll(fs, "/usr/bin", constants.DirPerm)
|
||||||
|
Expect(err).ToNot(HaveOccurred())
|
||||||
|
err = fs.WriteFile("/usr/bin/systemctl", []byte("#!/bin/sh\necho 'mock systemctl'"), constants.FilePerm)
|
||||||
|
Expect(err).ToNot(HaveOccurred())
|
||||||
|
}
|
||||||
|
|
||||||
|
BeforeEach(func() {
|
||||||
|
fs, cleanup, err = vfst.NewTestFS(nil)
|
||||||
|
Expect(err).ToNot(HaveOccurred())
|
||||||
|
|
||||||
|
logger = types.NewNullLogger()
|
||||||
|
runner = v1mock.NewFakeRunner()
|
||||||
|
|
||||||
|
// Create mock systemctl for systemd detection in all tests
|
||||||
|
createMockSystemctl()
|
||||||
|
})
|
||||||
|
|
||||||
|
AfterEach(func() {
|
||||||
|
cleanup()
|
||||||
|
})
|
||||||
|
|
||||||
|
Describe("LogsCollector", func() {
|
||||||
|
var collector *LogsCollector
|
||||||
|
|
||||||
|
BeforeEach(func() {
|
||||||
|
cfg := config.NewConfig(
|
||||||
|
config.WithFs(fs),
|
||||||
|
config.WithLogger(logger),
|
||||||
|
config.WithRunner(runner),
|
||||||
|
)
|
||||||
|
collector = NewLogsCollector(cfg)
|
||||||
|
})
|
||||||
|
|
||||||
|
It("should collect journal logs", func() {
|
||||||
|
// Mock journalctl command output
|
||||||
|
runner.SideEffect = func(command string, args ...string) ([]byte, error) {
|
||||||
|
if command == "journalctl" && len(args) > 0 && args[0] == "-u" {
|
||||||
|
service := args[1]
|
||||||
|
return []byte("journal logs for " + service), nil
|
||||||
|
}
|
||||||
|
return []byte{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set logs config in the main config
|
||||||
|
collector.config.Logs = &config.LogsConfig{
|
||||||
|
Journal: []string{"myservice"},
|
||||||
|
Files: []string{},
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := collector.Collect()
|
||||||
|
Expect(err).ToNot(HaveOccurred())
|
||||||
|
Expect(result).ToNot(BeNil())
|
||||||
|
|
||||||
|
// Verify journalctl was called for both default and user-defined services
|
||||||
|
// Default services: kairos-agent, kairos-installer, kairos-webui, cos-setup-boot, cos-setup-fs, cos-setup-network, cos-setup-reconcile, k3s, k3s-agent, k0scontroller, k0sworker
|
||||||
|
// User service: myservice
|
||||||
|
Expect(runner.CmdsMatch([][]string{
|
||||||
|
{"journalctl", "-u", "kairos-agent", "--no-pager", "-o", "cat"},
|
||||||
|
{"journalctl", "-u", "kairos-installer", "--no-pager", "-o", "cat"},
|
||||||
|
{"journalctl", "-u", "kairos-webui", "--no-pager", "-o", "cat"},
|
||||||
|
{"journalctl", "-u", "cos-setup-boot", "--no-pager", "-o", "cat"},
|
||||||
|
{"journalctl", "-u", "cos-setup-fs", "--no-pager", "-o", "cat"},
|
||||||
|
{"journalctl", "-u", "cos-setup-network", "--no-pager", "-o", "cat"},
|
||||||
|
{"journalctl", "-u", "cos-setup-reconcile", "--no-pager", "-o", "cat"},
|
||||||
|
{"journalctl", "-u", "k3s", "--no-pager", "-o", "cat"},
|
||||||
|
{"journalctl", "-u", "k3s-agent", "--no-pager", "-o", "cat"},
|
||||||
|
{"journalctl", "-u", "k0scontroller", "--no-pager", "-o", "cat"},
|
||||||
|
{"journalctl", "-u", "k0sworker", "--no-pager", "-o", "cat"},
|
||||||
|
{"journalctl", "-u", "myservice", "--no-pager", "-o", "cat"},
|
||||||
|
})).To(BeNil())
|
||||||
|
|
||||||
|
// Verify that both default and user-defined services are in the result
|
||||||
|
Expect(result.Journal).To(HaveKey("kairos-agent"))
|
||||||
|
Expect(result.Journal).To(HaveKey("kairos-installer"))
|
||||||
|
Expect(result.Journal).To(HaveKey("kairos-webui"))
|
||||||
|
Expect(result.Journal).To(HaveKey("cos-setup-boot"))
|
||||||
|
Expect(result.Journal).To(HaveKey("cos-setup-fs"))
|
||||||
|
Expect(result.Journal).To(HaveKey("cos-setup-network"))
|
||||||
|
Expect(result.Journal).To(HaveKey("cos-setup-reconcile"))
|
||||||
|
Expect(result.Journal).To(HaveKey("k3s"))
|
||||||
|
Expect(result.Journal).To(HaveKey("k3s-agent"))
|
||||||
|
Expect(result.Journal).To(HaveKey("k0scontroller"))
|
||||||
|
Expect(result.Journal).To(HaveKey("k0sworker"))
|
||||||
|
Expect(result.Journal).To(HaveKey("myservice"))
|
||||||
|
Expect(result.Journal).To(HaveLen(12))
|
||||||
|
})
|
||||||
|
|
||||||
|
It("should collect file logs with globbing", func() {
|
||||||
|
// Create test files
|
||||||
|
testFiles := []string{
|
||||||
|
"/var/log/test1.log",
|
||||||
|
"/var/log/test2.log",
|
||||||
|
"/var/log/subdir/test3.log",
|
||||||
|
"/var/log/kairos/agent.log", // Default pattern file
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, file := range testFiles {
|
||||||
|
err := fsutils.MkdirAll(fs, filepath.Dir(file), constants.DirPerm)
|
||||||
|
Expect(err).ToNot(HaveOccurred())
|
||||||
|
|
||||||
|
err = fs.WriteFile(file, []byte("log content for "+file), constants.FilePerm)
|
||||||
|
Expect(err).ToNot(HaveOccurred())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set logs config in the main config
|
||||||
|
collector.config.Logs = &config.LogsConfig{
|
||||||
|
Journal: []string{},
|
||||||
|
Files: []string{"/var/log/*.log", "/var/log/subdir/*.log"},
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := collector.Collect()
|
||||||
|
Expect(err).ToNot(HaveOccurred())
|
||||||
|
Expect(result).ToNot(BeNil())
|
||||||
|
|
||||||
|
// Verify files were collected (both default and user-defined patterns)
|
||||||
|
// Default pattern: /var/log/kairos/* (matches agent.log)
|
||||||
|
// User patterns: /var/log/*.log, /var/log/subdir/*.log
|
||||||
|
Expect(result.Files).To(HaveLen(4))
|
||||||
|
Expect(result.Files).To(HaveKey("/var/log/test1.log"))
|
||||||
|
Expect(result.Files).To(HaveKey("/var/log/test2.log"))
|
||||||
|
Expect(result.Files).To(HaveKey("/var/log/subdir/test3.log"))
|
||||||
|
Expect(result.Files).To(HaveKey("/var/log/kairos/agent.log"))
|
||||||
|
})
|
||||||
|
|
||||||
|
It("should handle nested directories correctly", func() {
|
||||||
|
// Create test files with same basename in different directories
|
||||||
|
testFiles := []string{
|
||||||
|
"/var/log/test.log",
|
||||||
|
"/var/log/subdir/test.log", // Same basename, different directory
|
||||||
|
"/var/log/kairos/agent.log", // Default pattern file
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, file := range testFiles {
|
||||||
|
err := fsutils.MkdirAll(fs, filepath.Dir(file), constants.DirPerm)
|
||||||
|
Expect(err).ToNot(HaveOccurred())
|
||||||
|
|
||||||
|
err = fs.WriteFile(file, []byte("log content for "+file), constants.FilePerm)
|
||||||
|
Expect(err).ToNot(HaveOccurred())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set logs config in the main config
|
||||||
|
collector.config.Logs = &config.LogsConfig{
|
||||||
|
Journal: []string{},
|
||||||
|
Files: []string{"/var/log/test.log", "/var/log/subdir/test.log"},
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := collector.Collect()
|
||||||
|
Expect(err).ToNot(HaveOccurred())
|
||||||
|
Expect(result).ToNot(BeNil())
|
||||||
|
|
||||||
|
// Verify both files are collected with full paths as keys (including default pattern)
|
||||||
|
Expect(result.Files).To(HaveKey("/var/log/test.log"))
|
||||||
|
Expect(result.Files).To(HaveKey("/var/log/subdir/test.log"))
|
||||||
|
Expect(result.Files).To(HaveKey("/var/log/kairos/agent.log"))
|
||||||
|
Expect(result.Files).To(HaveLen(3))
|
||||||
|
|
||||||
|
// Create tarball
|
||||||
|
tarballPath := "/tmp/logs.tar.gz"
|
||||||
|
err = collector.CreateTarball(result, tarballPath)
|
||||||
|
Expect(err).ToNot(HaveOccurred())
|
||||||
|
|
||||||
|
// Read and verify tarball contents
|
||||||
|
tarballData, err := fs.ReadFile(tarballPath)
|
||||||
|
Expect(err).ToNot(HaveOccurred())
|
||||||
|
|
||||||
|
// Extract and verify tarball structure
|
||||||
|
gzr, err := gzip.NewReader(bytes.NewReader(tarballData))
|
||||||
|
Expect(err).ToNot(HaveOccurred())
|
||||||
|
defer gzr.Close()
|
||||||
|
|
||||||
|
tr := tar.NewReader(gzr)
|
||||||
|
files := make(map[string]bool)
|
||||||
|
|
||||||
|
for {
|
||||||
|
header, err := tr.Next()
|
||||||
|
if err == io.EOF {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
Expect(err).ToNot(HaveOccurred())
|
||||||
|
files[header.Name] = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify that all files are preserved with their full directory structure
|
||||||
|
Expect(files).To(HaveKey("files/var/log/test.log"))
|
||||||
|
Expect(files).To(HaveKey("files/var/log/subdir/test.log"))
|
||||||
|
Expect(files).To(HaveKey("files/var/log/kairos/agent.log"))
|
||||||
|
Expect(files).To(HaveLen(3))
|
||||||
|
})
|
||||||
|
|
||||||
|
It("should create tarball with proper structure", func() {
|
||||||
|
// Mock journalctl output
|
||||||
|
runner.SideEffect = func(command string, args ...string) ([]byte, error) {
|
||||||
|
if command == "journalctl" {
|
||||||
|
return []byte("journal logs content"), nil
|
||||||
|
}
|
||||||
|
return []byte{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create test file
|
||||||
|
testFile := "/var/log/test.log"
|
||||||
|
err := fsutils.MkdirAll(fs, filepath.Dir(testFile), constants.DirPerm)
|
||||||
|
Expect(err).ToNot(HaveOccurred())
|
||||||
|
|
||||||
|
err = fs.WriteFile(testFile, []byte("file log content"), constants.FilePerm)
|
||||||
|
Expect(err).ToNot(HaveOccurred())
|
||||||
|
|
||||||
|
// Set logs config in the main config
|
||||||
|
collector.config.Logs = &config.LogsConfig{
|
||||||
|
Journal: []string{"myservice"},
|
||||||
|
Files: []string{testFile},
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := collector.Collect()
|
||||||
|
Expect(err).ToNot(HaveOccurred())
|
||||||
|
|
||||||
|
// Create tarball
|
||||||
|
tarballPath := "/tmp/logs.tar.gz"
|
||||||
|
err = collector.CreateTarball(result, tarballPath)
|
||||||
|
Expect(err).ToNot(HaveOccurred())
|
||||||
|
|
||||||
|
// Verify tarball exists and has correct structure
|
||||||
|
exists, err := fsutils.Exists(fs, tarballPath)
|
||||||
|
Expect(err).ToNot(HaveOccurred())
|
||||||
|
Expect(exists).To(BeTrue())
|
||||||
|
|
||||||
|
// Read and verify tarball contents
|
||||||
|
tarballData, err := fs.ReadFile(tarballPath)
|
||||||
|
Expect(err).ToNot(HaveOccurred())
|
||||||
|
|
||||||
|
// Extract and verify tarball structure
|
||||||
|
gzr, err := gzip.NewReader(bytes.NewReader(tarballData))
|
||||||
|
Expect(err).ToNot(HaveOccurred())
|
||||||
|
defer gzr.Close()
|
||||||
|
|
||||||
|
tr := tar.NewReader(gzr)
|
||||||
|
files := make(map[string]bool)
|
||||||
|
|
||||||
|
for {
|
||||||
|
header, err := tr.Next()
|
||||||
|
if err == io.EOF {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
Expect(err).ToNot(HaveOccurred())
|
||||||
|
files[header.Name] = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify expected structure (both default and user-defined)
|
||||||
|
// Default services: kairos-agent, kairos-installer, kairos-webui, cos-setup-boot, cos-setup-fs, cos-setup-network, cos-setup-reconcile, k3s, k3s-agent, k0scontroller, k0sworker
|
||||||
|
// User service: myservice
|
||||||
|
// Default files: /var/log/kairos-*.log (if exists)
|
||||||
|
// User file: /var/log/test.log
|
||||||
|
Expect(files).To(HaveKey("journal/kairos-agent.log"))
|
||||||
|
Expect(files).To(HaveKey("journal/kairos-installer.log"))
|
||||||
|
Expect(files).To(HaveKey("journal/kairos-webui.log"))
|
||||||
|
Expect(files).To(HaveKey("journal/cos-setup-boot.log"))
|
||||||
|
Expect(files).To(HaveKey("journal/cos-setup-fs.log"))
|
||||||
|
Expect(files).To(HaveKey("journal/cos-setup-network.log"))
|
||||||
|
Expect(files).To(HaveKey("journal/cos-setup-reconcile.log"))
|
||||||
|
Expect(files).To(HaveKey("journal/k3s.log"))
|
||||||
|
Expect(files).To(HaveKey("journal/k3s-agent.log"))
|
||||||
|
Expect(files).To(HaveKey("journal/k0scontroller.log"))
|
||||||
|
Expect(files).To(HaveKey("journal/k0sworker.log"))
|
||||||
|
Expect(files).To(HaveKey("journal/myservice.log"))
|
||||||
|
Expect(files).To(HaveKey("files/var/log/test.log"))
|
||||||
|
})
|
||||||
|
|
||||||
|
It("should handle missing files gracefully", func() {
|
||||||
|
// Create a default pattern file to ensure it's collected
|
||||||
|
defaultFile := "/var/log/kairos/agent.log"
|
||||||
|
err := fsutils.MkdirAll(fs, filepath.Dir(defaultFile), constants.DirPerm)
|
||||||
|
Expect(err).ToNot(HaveOccurred())
|
||||||
|
err = fs.WriteFile(defaultFile, []byte("default log content"), constants.FilePerm)
|
||||||
|
Expect(err).ToNot(HaveOccurred())
|
||||||
|
|
||||||
|
// Set logs config in the main config
|
||||||
|
collector.config.Logs = &config.LogsConfig{
|
||||||
|
Journal: []string{},
|
||||||
|
Files: []string{"/var/log/nonexistent.log"},
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := collector.Collect()
|
||||||
|
Expect(err).ToNot(HaveOccurred())
|
||||||
|
// Should have collected the default pattern file even though user file doesn't exist
|
||||||
|
Expect(result.Files).To(HaveLen(1))
|
||||||
|
Expect(result.Files).To(HaveKey("/var/log/kairos/agent.log"))
|
||||||
|
})
|
||||||
|
|
||||||
|
It("should handle journal service errors gracefully", func() {
|
||||||
|
// Mock journalctl to return no entries for non-existent service
|
||||||
|
runner.SideEffect = func(command string, args ...string) ([]byte, error) {
|
||||||
|
if command == "journalctl" && len(args) > 1 && args[1] == "nonexistentservice" {
|
||||||
|
return []byte("-- No entries --"), nil
|
||||||
|
}
|
||||||
|
if command == "journalctl" {
|
||||||
|
return []byte("journal logs content"), nil
|
||||||
|
}
|
||||||
|
return []byte{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set logs config in the main config
|
||||||
|
collector.config.Logs = &config.LogsConfig{
|
||||||
|
Journal: []string{"nonexistentservice"},
|
||||||
|
Files: []string{},
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := collector.Collect()
|
||||||
|
Expect(err).ToNot(HaveOccurred())
|
||||||
|
// Should have collected from default services but not the non-existent user service
|
||||||
|
Expect(result.Journal).To(HaveLen(11))
|
||||||
|
Expect(result.Journal).To(HaveKey("kairos-agent"))
|
||||||
|
Expect(result.Journal).To(HaveKey("kairos-installer"))
|
||||||
|
Expect(result.Journal).To(HaveKey("kairos-webui"))
|
||||||
|
Expect(result.Journal).To(HaveKey("cos-setup-boot"))
|
||||||
|
Expect(result.Journal).To(HaveKey("cos-setup-fs"))
|
||||||
|
Expect(result.Journal).To(HaveKey("cos-setup-network"))
|
||||||
|
Expect(result.Journal).To(HaveKey("cos-setup-reconcile"))
|
||||||
|
Expect(result.Journal).To(HaveKey("k3s"))
|
||||||
|
Expect(result.Journal).To(HaveKey("k3s-agent"))
|
||||||
|
Expect(result.Journal).To(HaveKey("k0scontroller"))
|
||||||
|
Expect(result.Journal).To(HaveKey("k0sworker"))
|
||||||
|
Expect(result.Journal).ToNot(HaveKey("nonexistentservice"))
|
||||||
|
})
|
||||||
|
|
||||||
|
It("should handle empty journal output gracefully", func() {
|
||||||
|
// Mock journalctl to return empty output for user service, normal for defaults
|
||||||
|
runner.SideEffect = func(command string, args ...string) ([]byte, error) {
|
||||||
|
if command == "journalctl" && len(args) > 1 && args[1] == "emptyservice" {
|
||||||
|
return []byte{}, nil
|
||||||
|
}
|
||||||
|
if command == "journalctl" {
|
||||||
|
return []byte("journal logs content"), nil
|
||||||
|
}
|
||||||
|
return []byte{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set logs config in the main config
|
||||||
|
collector.config.Logs = &config.LogsConfig{
|
||||||
|
Journal: []string{"emptyservice"},
|
||||||
|
Files: []string{},
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := collector.Collect()
|
||||||
|
Expect(err).ToNot(HaveOccurred())
|
||||||
|
// Should have collected from default services but not the empty user service
|
||||||
|
Expect(result.Journal).To(HaveLen(11))
|
||||||
|
Expect(result.Journal).To(HaveKey("kairos-agent"))
|
||||||
|
Expect(result.Journal).To(HaveKey("kairos-installer"))
|
||||||
|
Expect(result.Journal).To(HaveKey("kairos-webui"))
|
||||||
|
Expect(result.Journal).To(HaveKey("cos-setup-boot"))
|
||||||
|
Expect(result.Journal).To(HaveKey("cos-setup-fs"))
|
||||||
|
Expect(result.Journal).To(HaveKey("cos-setup-network"))
|
||||||
|
Expect(result.Journal).To(HaveKey("cos-setup-reconcile"))
|
||||||
|
Expect(result.Journal).To(HaveKey("k3s"))
|
||||||
|
Expect(result.Journal).To(HaveKey("k3s-agent"))
|
||||||
|
Expect(result.Journal).To(HaveKey("k0scontroller"))
|
||||||
|
Expect(result.Journal).To(HaveKey("k0sworker"))
|
||||||
|
Expect(result.Journal).ToNot(HaveKey("emptyservice"))
|
||||||
|
})
|
||||||
|
|
||||||
|
It("should not create tarball files for services with no journal entries", func() {
|
||||||
|
// Mock journalctl to return no entries for one service and content for another
|
||||||
|
runner.SideEffect = func(command string, args ...string) ([]byte, error) {
|
||||||
|
if command == "journalctl" && len(args) > 1 && args[1] == "existingservice" {
|
||||||
|
return []byte("journal logs content"), nil
|
||||||
|
}
|
||||||
|
if command == "journalctl" && len(args) > 1 && args[1] == "nonexistentservice" {
|
||||||
|
return []byte("-- No entries --"), nil
|
||||||
|
}
|
||||||
|
if command == "journalctl" {
|
||||||
|
return []byte("default journal logs"), nil
|
||||||
|
}
|
||||||
|
return []byte{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set logs config in the main config
|
||||||
|
collector.config.Logs = &config.LogsConfig{
|
||||||
|
Journal: []string{"existingservice", "nonexistentservice"},
|
||||||
|
Files: []string{},
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := collector.Collect()
|
||||||
|
Expect(err).ToNot(HaveOccurred())
|
||||||
|
// Should have collected from default services and the existing user service
|
||||||
|
Expect(result.Journal).To(HaveLen(12))
|
||||||
|
Expect(result.Journal).To(HaveKey("kairos-agent"))
|
||||||
|
Expect(result.Journal).To(HaveKey("kairos-installer"))
|
||||||
|
Expect(result.Journal).To(HaveKey("kairos-webui"))
|
||||||
|
Expect(result.Journal).To(HaveKey("cos-setup-boot"))
|
||||||
|
Expect(result.Journal).To(HaveKey("cos-setup-fs"))
|
||||||
|
Expect(result.Journal).To(HaveKey("cos-setup-network"))
|
||||||
|
Expect(result.Journal).To(HaveKey("cos-setup-reconcile"))
|
||||||
|
Expect(result.Journal).To(HaveKey("k3s"))
|
||||||
|
Expect(result.Journal).To(HaveKey("k3s-agent"))
|
||||||
|
Expect(result.Journal).To(HaveKey("k0scontroller"))
|
||||||
|
Expect(result.Journal).To(HaveKey("k0sworker"))
|
||||||
|
Expect(result.Journal).To(HaveKey("existingservice"))
|
||||||
|
Expect(result.Journal).ToNot(HaveKey("nonexistentservice"))
|
||||||
|
|
||||||
|
// Create tarball and verify contents
|
||||||
|
tarballPath := "/tmp/logs.tar.gz"
|
||||||
|
err = collector.CreateTarball(result, tarballPath)
|
||||||
|
Expect(err).ToNot(HaveOccurred())
|
||||||
|
|
||||||
|
// Read and verify tarball contents
|
||||||
|
tarballData, err := fs.ReadFile(tarballPath)
|
||||||
|
Expect(err).ToNot(HaveOccurred())
|
||||||
|
|
||||||
|
// Extract and verify tarball structure
|
||||||
|
gzr, err := gzip.NewReader(bytes.NewReader(tarballData))
|
||||||
|
Expect(err).ToNot(HaveOccurred())
|
||||||
|
defer gzr.Close()
|
||||||
|
|
||||||
|
tr := tar.NewReader(gzr)
|
||||||
|
files := make(map[string]bool)
|
||||||
|
|
||||||
|
for {
|
||||||
|
header, err := tr.Next()
|
||||||
|
if err == io.EOF {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
Expect(err).ToNot(HaveOccurred())
|
||||||
|
files[header.Name] = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify that default services and existing user service files are created
|
||||||
|
Expect(files).To(HaveKey("journal/kairos-agent.log"))
|
||||||
|
Expect(files).To(HaveKey("journal/kairos-installer.log"))
|
||||||
|
Expect(files).To(HaveKey("journal/kairos-webui.log"))
|
||||||
|
Expect(files).To(HaveKey("journal/cos-setup-boot.log"))
|
||||||
|
Expect(files).To(HaveKey("journal/cos-setup-fs.log"))
|
||||||
|
Expect(files).To(HaveKey("journal/cos-setup-network.log"))
|
||||||
|
Expect(files).To(HaveKey("journal/cos-setup-reconcile.log"))
|
||||||
|
Expect(files).To(HaveKey("journal/k3s.log"))
|
||||||
|
Expect(files).To(HaveKey("journal/k3s-agent.log"))
|
||||||
|
Expect(files).To(HaveKey("journal/k0scontroller.log"))
|
||||||
|
Expect(files).To(HaveKey("journal/k0sworker.log"))
|
||||||
|
Expect(files).To(HaveKey("journal/existingservice.log"))
|
||||||
|
Expect(files).ToNot(HaveKey("journal/nonexistentservice.log"))
|
||||||
|
Expect(files).To(HaveLen(12))
|
||||||
|
})
|
||||||
|
|
||||||
|
It("should use default log sources when no config provided", func() {
|
||||||
|
// Mock journalctl output
|
||||||
|
runner.SideEffect = func(command string, args ...string) ([]byte, error) {
|
||||||
|
if command == "journalctl" {
|
||||||
|
return []byte("default journal logs"), nil
|
||||||
|
}
|
||||||
|
return []byte{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Don't set logs config - should use defaults
|
||||||
|
collector.config.Logs = nil
|
||||||
|
|
||||||
|
result, err := collector.Collect()
|
||||||
|
Expect(err).ToNot(HaveOccurred())
|
||||||
|
Expect(result).ToNot(BeNil())
|
||||||
|
|
||||||
|
// Should have collected from default services
|
||||||
|
Expect(runner.CmdsMatch([][]string{
|
||||||
|
{"journalctl", "-u", "kairos-agent", "--no-pager", "-o", "cat"},
|
||||||
|
{"journalctl", "-u", "kairos-installer", "--no-pager", "-o", "cat"},
|
||||||
|
{"journalctl", "-u", "kairos-webui", "--no-pager", "-o", "cat"},
|
||||||
|
{"journalctl", "-u", "cos-setup-boot", "--no-pager", "-o", "cat"},
|
||||||
|
{"journalctl", "-u", "cos-setup-fs", "--no-pager", "-o", "cat"},
|
||||||
|
{"journalctl", "-u", "cos-setup-network", "--no-pager", "-o", "cat"},
|
||||||
|
{"journalctl", "-u", "cos-setup-reconcile", "--no-pager", "-o", "cat"},
|
||||||
|
{"journalctl", "-u", "k3s", "--no-pager", "-o", "cat"},
|
||||||
|
{"journalctl", "-u", "k3s-agent", "--no-pager", "-o", "cat"},
|
||||||
|
{"journalctl", "-u", "k0scontroller", "--no-pager", "-o", "cat"},
|
||||||
|
{"journalctl", "-u", "k0sworker", "--no-pager", "-o", "cat"},
|
||||||
|
})).To(BeNil())
|
||||||
|
})
|
||||||
|
|
||||||
|
It("should merge user logs config with defaults", func() {
|
||||||
|
// Mock journalctl output
|
||||||
|
runner.SideEffect = func(command string, args ...string) ([]byte, error) {
|
||||||
|
if command == "journalctl" && len(args) > 0 && args[0] == "-u" {
|
||||||
|
service := args[1]
|
||||||
|
return []byte("journal logs for " + service), nil
|
||||||
|
}
|
||||||
|
return []byte{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set logs config in the main config with user-defined services
|
||||||
|
collector.config.Logs = &config.LogsConfig{
|
||||||
|
Journal: []string{"myservice", "myotherservice"},
|
||||||
|
Files: []string{"/var/log/mycustom.log"},
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := collector.Collect()
|
||||||
|
Expect(err).ToNot(HaveOccurred())
|
||||||
|
Expect(result).ToNot(BeNil())
|
||||||
|
|
||||||
|
// Verify that both default and user-defined services were collected
|
||||||
|
// Default services: kairos-agent, kairos-installer, kairos-webui, cos-setup-boot, cos-setup-fs, cos-setup-network, cos-setup-reconcile, k3s, k3s-agent, k0scontroller, k0sworker
|
||||||
|
// User services: myservice, myotherservice
|
||||||
|
Expect(runner.CmdsMatch([][]string{
|
||||||
|
{"journalctl", "-u", "kairos-agent", "--no-pager", "-o", "cat"},
|
||||||
|
{"journalctl", "-u", "kairos-installer", "--no-pager", "-o", "cat"},
|
||||||
|
{"journalctl", "-u", "kairos-webui", "--no-pager", "-o", "cat"},
|
||||||
|
{"journalctl", "-u", "cos-setup-boot", "--no-pager", "-o", "cat"},
|
||||||
|
{"journalctl", "-u", "cos-setup-fs", "--no-pager", "-o", "cat"},
|
||||||
|
{"journalctl", "-u", "cos-setup-network", "--no-pager", "-o", "cat"},
|
||||||
|
{"journalctl", "-u", "cos-setup-reconcile", "--no-pager", "-o", "cat"},
|
||||||
|
{"journalctl", "-u", "k3s", "--no-pager", "-o", "cat"},
|
||||||
|
{"journalctl", "-u", "k3s-agent", "--no-pager", "-o", "cat"},
|
||||||
|
{"journalctl", "-u", "k0scontroller", "--no-pager", "-o", "cat"},
|
||||||
|
{"journalctl", "-u", "k0sworker", "--no-pager", "-o", "cat"},
|
||||||
|
{"journalctl", "-u", "myservice", "--no-pager", "-o", "cat"},
|
||||||
|
{"journalctl", "-u", "myotherservice", "--no-pager", "-o", "cat"},
|
||||||
|
})).To(BeNil())
|
||||||
|
|
||||||
|
// Verify that both default and user-defined files are in the result
|
||||||
|
Expect(result.Journal).To(HaveKey("kairos-agent"))
|
||||||
|
Expect(result.Journal).To(HaveKey("kairos-installer"))
|
||||||
|
Expect(result.Journal).To(HaveKey("kairos-webui"))
|
||||||
|
Expect(result.Journal).To(HaveKey("cos-setup-boot"))
|
||||||
|
Expect(result.Journal).To(HaveKey("cos-setup-fs"))
|
||||||
|
Expect(result.Journal).To(HaveKey("cos-setup-network"))
|
||||||
|
Expect(result.Journal).To(HaveKey("cos-setup-reconcile"))
|
||||||
|
Expect(result.Journal).To(HaveKey("k3s"))
|
||||||
|
Expect(result.Journal).To(HaveKey("k3s-agent"))
|
||||||
|
Expect(result.Journal).To(HaveKey("k0scontroller"))
|
||||||
|
Expect(result.Journal).To(HaveKey("k0sworker"))
|
||||||
|
Expect(result.Journal).To(HaveKey("myservice"))
|
||||||
|
Expect(result.Journal).To(HaveKey("myotherservice"))
|
||||||
|
Expect(result.Journal).To(HaveLen(13))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
Describe("CLI Command", func() {
|
||||||
|
It("should execute logs command successfully", func() {
|
||||||
|
// Mock journalctl output
|
||||||
|
runner.SideEffect = func(command string, args ...string) ([]byte, error) {
|
||||||
|
if command == "journalctl" {
|
||||||
|
return []byte("journal logs content"), nil
|
||||||
|
}
|
||||||
|
return []byte{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create test config file
|
||||||
|
configContent := `
|
||||||
|
logs:
|
||||||
|
journal:
|
||||||
|
- myservice
|
||||||
|
files:
|
||||||
|
- /var/log/test.log
|
||||||
|
`
|
||||||
|
configPath := "/etc/kairos/config.yaml"
|
||||||
|
err := fsutils.MkdirAll(fs, filepath.Dir(configPath), constants.DirPerm)
|
||||||
|
Expect(err).ToNot(HaveOccurred())
|
||||||
|
|
||||||
|
err = fs.WriteFile(configPath, []byte(configContent), constants.FilePerm)
|
||||||
|
Expect(err).ToNot(HaveOccurred())
|
||||||
|
|
||||||
|
// Create test log file
|
||||||
|
testFile := "/var/log/test.log"
|
||||||
|
err = fsutils.MkdirAll(fs, filepath.Dir(testFile), constants.DirPerm)
|
||||||
|
Expect(err).ToNot(HaveOccurred())
|
||||||
|
|
||||||
|
// Execute logs command
|
||||||
|
outputPath := "/tmp/logs.tar.gz"
|
||||||
|
err = ExecuteLogsCommand(fs, logger, runner, outputPath)
|
||||||
|
Expect(err).ToNot(HaveOccurred())
|
||||||
|
|
||||||
|
// Verify output file was created
|
||||||
|
exists, err := fsutils.Exists(fs, outputPath)
|
||||||
|
Expect(err).ToNot(HaveOccurred())
|
||||||
|
Expect(exists).To(BeTrue())
|
||||||
|
})
|
||||||
|
|
||||||
|
It("should handle missing config gracefully", func() {
|
||||||
|
// Execute logs command without config
|
||||||
|
outputPath := "/tmp/logs.tar.gz"
|
||||||
|
err := ExecuteLogsCommand(fs, logger, runner, outputPath)
|
||||||
|
Expect(err).ToNot(HaveOccurred())
|
||||||
|
|
||||||
|
// Should still create output with default sources
|
||||||
|
exists, err := fsutils.Exists(fs, outputPath)
|
||||||
|
Expect(err).ToNot(HaveOccurred())
|
||||||
|
Expect(exists).To(BeTrue())
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
45
main.go
45
main.go
@@ -1075,6 +1075,46 @@ The validate command expects a configuration file as its only argument. Local fi
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
Name: "logs",
|
||||||
|
Usage: "Collect logs from the system",
|
||||||
|
Description: `Collect logs from various sources on the Kairos system and create a compressed tarball.
|
||||||
|
|
||||||
|
The command will collect logs from:
|
||||||
|
- Journal logs from specified services (default: kairos-agent, systemd, k3s)
|
||||||
|
- Log files from specified paths with globbing support
|
||||||
|
|
||||||
|
Configuration can be provided in the Kairos config file under the 'logs' section:
|
||||||
|
|
||||||
|
logs:
|
||||||
|
journal:
|
||||||
|
- myservice
|
||||||
|
- myotherservice
|
||||||
|
files:
|
||||||
|
- /var/log/mybinary/*
|
||||||
|
- /var/log/something.log
|
||||||
|
|
||||||
|
The output will be a tarball with logs organized by type (journal/, files/).`,
|
||||||
|
Flags: []cli.Flag{
|
||||||
|
&cli.StringFlag{
|
||||||
|
Name: "output",
|
||||||
|
Usage: "Output path for the logs tarball",
|
||||||
|
Value: "./kairos-logs.tar.gz",
|
||||||
|
Aliases: []string{"o"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Action: func(c *cli.Context) error {
|
||||||
|
outputPath := c.String("output")
|
||||||
|
|
||||||
|
// Get the filesystem and runner from the config
|
||||||
|
cfg, err := agentConfig.Scan(collector.Directories(constants.GetUserConfigDirs()...), collector.NoLogs)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to scan config: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return agent.ExecuteLogsCommand(cfg.Fs, cfg.Logger, cfg.Runner, outputPath)
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
@@ -1230,11 +1270,10 @@ func moreThanOneEnabled(bools ...bool) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func noneOfEnabled(bools ...bool) bool {
|
func noneOfEnabled(bools ...bool) bool {
|
||||||
count := 0
|
|
||||||
for _, b := range bools {
|
for _, b := range bools {
|
||||||
if b {
|
if b {
|
||||||
count++
|
return false // Found at least one true, so not "none of enabled"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return count == 0
|
return true // No true values found, so "none of enabled"
|
||||||
}
|
}
|
||||||
|
@@ -33,6 +33,12 @@ const (
|
|||||||
FilePrefix = "file://"
|
FilePrefix = "file://"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// LogsConfig represents the configuration for log collection
|
||||||
|
type LogsConfig struct {
|
||||||
|
Journal []string `yaml:"journal,omitempty"`
|
||||||
|
Files []string `yaml:"files,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
type Install struct {
|
type Install struct {
|
||||||
Auto bool `yaml:"auto,omitempty"`
|
Auto bool `yaml:"auto,omitempty"`
|
||||||
Reboot bool `yaml:"reboot,omitempty"`
|
Reboot bool `yaml:"reboot,omitempty"`
|
||||||
@@ -157,6 +163,7 @@ type Config struct {
|
|||||||
UkiMaxEntries int `yaml:"uki-max-entries,omitempty" mapstructure:"uki-max-entries"`
|
UkiMaxEntries int `yaml:"uki-max-entries,omitempty" mapstructure:"uki-max-entries"`
|
||||||
BindPCRs []string `yaml:"bind-pcrs,omitempty" mapstructure:"bind-pcrs"`
|
BindPCRs []string `yaml:"bind-pcrs,omitempty" mapstructure:"bind-pcrs"`
|
||||||
BindPublicPCRs []string `yaml:"bind-public-pcrs,omitempty" mapstructure:"bind-public-pcrs"`
|
BindPublicPCRs []string `yaml:"bind-public-pcrs,omitempty" mapstructure:"bind-public-pcrs"`
|
||||||
|
Logs *LogsConfig `yaml:"logs,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// WriteInstallState writes the state.yaml file to the given state and recovery paths
|
// WriteInstallState writes the state.yaml file to the given state and recovery paths
|
||||||
|
@@ -17,11 +17,12 @@ package config_test
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
sdkTypes "github.com/kairos-io/kairos-sdk/types"
|
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"reflect"
|
"reflect"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
sdkTypes "github.com/kairos-io/kairos-sdk/types"
|
||||||
|
|
||||||
pkgConfig "github.com/kairos-io/kairos-agent/v2/pkg/config"
|
pkgConfig "github.com/kairos-io/kairos-agent/v2/pkg/config"
|
||||||
"github.com/kairos-io/kairos-agent/v2/pkg/constants"
|
"github.com/kairos-io/kairos-agent/v2/pkg/constants"
|
||||||
v1 "github.com/kairos-io/kairos-agent/v2/pkg/types/v1"
|
v1 "github.com/kairos-io/kairos-agent/v2/pkg/types/v1"
|
||||||
@@ -93,8 +94,8 @@ func structFieldsContainedInOtherStruct(left, right interface{}) {
|
|||||||
leftTagName := getTagName(leftTypes.Field(i).Tag.Get("yaml"))
|
leftTagName := getTagName(leftTypes.Field(i).Tag.Get("yaml"))
|
||||||
leftFieldName := leftTypes.Field(i).Name
|
leftFieldName := leftTypes.Field(i).Name
|
||||||
if leftTypes.Field(i).IsExported() {
|
if leftTypes.Field(i).IsExported() {
|
||||||
It(fmt.Sprintf("Checks that the new schema contians the field %s", leftFieldName), func() {
|
It(fmt.Sprintf("Checks that the new schema contains the field %s", leftFieldName), func() {
|
||||||
if leftFieldName == "Source" || leftFieldName == "NoUsers" || leftFieldName == "BindPublicPCRs" || leftFieldName == "BindPCRs" {
|
if leftFieldName == "Source" || leftFieldName == "NoUsers" || leftFieldName == "BindPublicPCRs" || leftFieldName == "BindPCRs" || leftFieldName == "Logs" {
|
||||||
Skip("Schema not updated yet")
|
Skip("Schema not updated yet")
|
||||||
}
|
}
|
||||||
Expect(
|
Expect(
|
||||||
|
Reference in New Issue
Block a user