-
Notifications
You must be signed in to change notification settings - Fork 41
feat: add logs subcommand for mgrctl (Fixes #398) #724
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
base: main
Are you sure you want to change the base?
Changes from all commits
360d0cf
14290f4
584095f
a976487
082c28c
176ad35
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,147 @@ | ||
| // SPDX-FileCopyrightText: 2024 SUSE LLC | ||
| // | ||
| // SPDX-License-Identifier: Apache-2.0 | ||
|
|
||
| package logs | ||
|
|
||
| import ( | ||
| "errors" | ||
| "fmt" | ||
| "os" | ||
| os_exec "os/exec" | ||
| "strings" | ||
|
|
||
| "github.com/spf13/cobra" | ||
| "github.com/uyuni-project/uyuni-tools/shared" | ||
| "github.com/uyuni-project/uyuni-tools/shared/kubernetes" | ||
| . "github.com/uyuni-project/uyuni-tools/shared/l10n" | ||
| "github.com/uyuni-project/uyuni-tools/shared/podman" | ||
| "github.com/uyuni-project/uyuni-tools/shared/types" | ||
| "github.com/uyuni-project/uyuni-tools/shared/utils" | ||
| ) | ||
|
|
||
| type flagpole struct { | ||
| Taskomatic bool | ||
| Web bool | ||
| Salt bool | ||
| Reposync bool | ||
| Files string | ||
| Follow bool | ||
| Backend string | ||
| } | ||
|
|
||
| // runCmd allows unit tests to mock or skip the main execution loop. | ||
| var runCmd = run | ||
|
|
||
| // NewCommand returns a new cobra.Command for logs. | ||
| func NewCommand(globalFlags *types.GlobalFlags) *cobra.Command { | ||
| var flags flagpole | ||
|
|
||
| logsCmd := &cobra.Command{ | ||
| Use: "logs", | ||
| Short: L("Show or follow logs of Uyuni services inside the container"), | ||
| RunE: func(cmd *cobra.Command, args []string) error { | ||
| return utils.CommandHelper(globalFlags, cmd, args, &flags, nil, runCmd) | ||
| }, | ||
| } | ||
|
|
||
| logsCmd.Flags().Bool("taskomatic", false, L("Show Taskomatic logs")) | ||
| logsCmd.Flags().Bool("web", false, L("Show Web UI logs")) | ||
| logsCmd.Flags().Bool("salt", false, L("Show Salt logs")) | ||
| logsCmd.Flags().Bool("reposync", false, L("Show Reposync logs")) | ||
| logsCmd.Flags().String("files", "", | ||
| L("Regular expression to filter reposync log files (matches against the full file path)")) | ||
| logsCmd.Flags().BoolP("follow", "f", false, L("Follow log output")) | ||
|
|
||
| utils.AddBackendFlag(logsCmd) | ||
| return logsCmd | ||
| } | ||
|
|
||
| func validateFlags(flags *flagpole) error { | ||
| if flags.Files != "" && !flags.Reposync { | ||
| return errors.New(L("--files flag can only be used with --reposync")) | ||
| } | ||
| return nil | ||
| } | ||
|
|
||
| func getLogPaths(flags *flagpole) []string { | ||
| var paths []string | ||
| if flags.Taskomatic { | ||
| paths = append(paths, "/var/log/rhn/rhn_tasko*.log") | ||
| } | ||
| if flags.Web { | ||
| paths = append(paths, "/var/log/rhn/rhn_web*.log") | ||
| } | ||
| if flags.Salt { | ||
| // Minion does not run inside the Uyuni server container | ||
| paths = append(paths, "/var/log/salt/api", "/var/log/salt/master") | ||
| } | ||
| if flags.Reposync { | ||
| if flags.Files != "" { | ||
| script := "files=$(find /var/log/rhn/reposync/ -type f | grep -E %q); " + | ||
| "if [ -z \"$files\" ]; then " | ||
| // Use literal path on follow so tail can wait for file creation | ||
| if flags.Follow { | ||
| script += "echo \"/var/log/rhn/reposync/*.log\"; else echo $files; fi" | ||
| } else { | ||
| script += "echo 'No matching reposync logs found.' >&2; exit 1; fi; echo $files" | ||
| } | ||
| paths = append(paths, fmt.Sprintf("$(%s)", fmt.Sprintf(script, flags.Files))) | ||
| } else { | ||
| paths = append(paths, "/var/log/rhn/reposync/*.log") | ||
| } | ||
| } | ||
| return paths | ||
| } | ||
|
|
||
| func run(_ *types.GlobalFlags, flags *flagpole, _ *cobra.Command, _ []string) error { | ||
| if err := validateFlags(flags); err != nil { | ||
| return err | ||
| } | ||
|
|
||
| paths := getLogPaths(flags) | ||
| if len(paths) == 0 { | ||
| return errors.New(L("please specify at least one service to get logs for (e.g., --web, --salt)")) | ||
| } | ||
|
Comment on lines
+36
to
+105
|
||
|
|
||
| cnx := shared.NewConnection(flags.Backend, podman.ServerContainerName, kubernetes.ServerFilter) | ||
| podName, err := cnx.GetPodName() | ||
| if err != nil { | ||
| return err | ||
| } | ||
|
|
||
| command, err := cnx.GetCommand() | ||
| if err != nil { | ||
| return err | ||
| } | ||
|
|
||
| tailCmd := "tail" | ||
| if flags.Follow { | ||
| tailCmd += " -F" | ||
| } | ||
|
|
||
| shCmd := fmt.Sprintf("%s %s", tailCmd, strings.Join(paths, " ")) | ||
| commandArgs := []string{"exec", "-i", "-t"} | ||
|
|
||
| if command == "kubectl" { | ||
| namespace, err := cnx.GetNamespace("") | ||
| if err != nil { | ||
| return err | ||
| } | ||
| commandArgs = append(commandArgs, "-n", namespace, "-c", "uyuni", podName, "--") | ||
| } else { | ||
| commandArgs = append(commandArgs, podName) | ||
| } | ||
|
|
||
| commandArgs = append(commandArgs, "bash", "-c", shCmd) | ||
|
|
||
| _, err = utils.NewRunner(command, commandArgs...).Exec() | ||
| if err != nil { | ||
| var exitErr *os_exec.ExitError | ||
| if errors.As(err, &exitErr) { | ||
| os.Exit(exitErr.ExitCode()) | ||
| } | ||
| return err | ||
| } | ||
| return nil | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,50 @@ | ||
| // SPDX-FileCopyrightText: 2024 SUSE LLC | ||
| // | ||
| // SPDX-License-Identifier: Apache-2.0 | ||
|
|
||
| package logs | ||
|
|
||
| import ( | ||
| "strings" | ||
| "testing" | ||
| ) | ||
|
|
||
| func TestValidateFlags(t *testing.T) { | ||
| // Test --files without --reposync (Should Fail) | ||
| invalidFlags := &flagpole{Files: "myregex"} | ||
| err := validateFlags(invalidFlags) | ||
| if err == nil { | ||
| t.Errorf("Expected error when using --files without --reposync, got nil") | ||
| } | ||
|
|
||
| // Test --files with --reposync (Should Pass) | ||
| validFlags := &flagpole{Reposync: true, Files: "myregex"} | ||
| err = validateFlags(validFlags) | ||
| if err != nil { | ||
| t.Errorf("Expected no error when using --files with --reposync, got %v", err) | ||
| } | ||
| } | ||
|
|
||
| func TestGetLogPaths(t *testing.T) { | ||
| flags := &flagpole{ | ||
| Salt: true, | ||
| } | ||
|
|
||
| paths := getLogPaths(flags) | ||
|
|
||
| // Test 1: Ensure salt paths exist but minion is absent | ||
| pathStr := strings.Join(paths, " ") | ||
| if !strings.Contains(pathStr, "/var/log/salt/api") || !strings.Contains(pathStr, "/var/log/salt/master") { | ||
| t.Errorf("Expected salt api and master paths, got: %v", paths) | ||
| } | ||
| if strings.Contains(pathStr, "/var/log/salt/minion") { | ||
| t.Errorf("Did not expect minion path in salt logs, got: %v", paths) | ||
| } | ||
|
|
||
| // Test 2: Ensure empty paths if no flags are set | ||
| emptyFlags := &flagpole{} | ||
| emptyPaths := getLogPaths(emptyFlags) | ||
| if len(emptyPaths) != 0 { | ||
| t.Errorf("Expected 0 paths for empty flags, got: %v", emptyPaths) | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
--filesis silently ignored unless--reposyncis also set, which is confusing given the flag help text. It would be better to validate this combination (e.g., return an error if--filesis provided without--reposync).