kubelet: use env vars in node log query PS command

- Use environment variables to pass string arguments in the node log
  query PS command
- Split getLoggingCmd into getLoggingCmdEnv and getLoggingCmdArgs
  for better modularization
This commit is contained in:
Aravindh Puthiyaparambil 2024-08-06 15:46:15 -07:00
parent 1043bf333c
commit c94919d68b
No known key found for this signature in database
GPG Key ID: DCF73FCC697065AF
9 changed files with 142 additions and 37 deletions

View File

@ -524,7 +524,8 @@ const (
// alpha: v1.27 // alpha: v1.27
// beta: v1.30 // beta: v1.30
// //
// Enables querying logs of node services using the /logs endpoint // Enables querying logs of node services using the /logs endpoint. Enabling this feature has security implications.
// The recommendation is to enable it on a need basis for debugging purposes and disabling otherwise.
NodeLogQuery featuregate.Feature = "NodeLogQuery" NodeLogQuery featuregate.Feature = "NodeLogQuery"
// owner: @xing-yang @sonasingh46 // owner: @xing-yang @sonasingh46

View File

@ -62689,7 +62689,7 @@ func schema_k8sio_kubelet_config_v1beta1_KubeletConfiguration(ref common.Referen
}, },
"enableSystemLogQuery": { "enableSystemLogQuery": {
SchemaProps: spec.SchemaProps{ SchemaProps: spec.SchemaProps{
Description: "enableSystemLogQuery enables the node log query feature on the /logs endpoint. EnableSystemLogHandler has to be enabled in addition for this feature to work. Default: false", Description: "enableSystemLogQuery enables the node log query feature on the /logs endpoint. EnableSystemLogHandler has to be enabled in addition for this feature to work. Enabling this feature has security implications. The recommendation is to enable it on a need basis for debugging purposes and disabling otherwise. Default: false",
Type: []string{"boolean"}, Type: []string{"boolean"},
Format: "", Format: "",
}, },

View File

@ -408,6 +408,8 @@ type KubeletConfiguration struct {
EnableSystemLogHandler bool EnableSystemLogHandler bool
// EnableSystemLogQuery enables the node log query feature on the /logs endpoint. // EnableSystemLogQuery enables the node log query feature on the /logs endpoint.
// EnableSystemLogHandler has to be enabled in addition for this feature to work. // EnableSystemLogHandler has to be enabled in addition for this feature to work.
// Enabling this feature has security implications. The recommendation is to enable it on a need basis for debugging
// purposes and disabling otherwise.
// +featureGate=NodeLogQuery // +featureGate=NodeLogQuery
// +optional // +optional
EnableSystemLogQuery bool EnableSystemLogQuery bool

View File

@ -316,7 +316,7 @@ func (n *nodeLogQuery) splitNativeVsFileLoggers(ctx context.Context) ([]string,
// copyServiceLogs invokes journalctl or Get-WinEvent with the provided args. Note that // copyServiceLogs invokes journalctl or Get-WinEvent with the provided args. Note that
// services are explicitly passed here to account for the heuristics. // services are explicitly passed here to account for the heuristics.
func (n *nodeLogQuery) copyServiceLogs(ctx context.Context, w io.Writer, services []string, previousBoot int) { func (n *nodeLogQuery) copyServiceLogs(ctx context.Context, w io.Writer, services []string, previousBoot int) {
cmdStr, args, err := getLoggingCmd(n, services) cmdStr, args, cmdEnv, err := getLoggingCmd(n, services)
if err != nil { if err != nil {
fmt.Fprintf(w, "\nfailed to get logging cmd: %v\n", err) fmt.Fprintf(w, "\nfailed to get logging cmd: %v\n", err)
return return
@ -324,6 +324,7 @@ func (n *nodeLogQuery) copyServiceLogs(ctx context.Context, w io.Writer, service
cmd := exec.CommandContext(ctx, cmdStr, args...) cmd := exec.CommandContext(ctx, cmdStr, args...)
cmd.Stdout = w cmd.Stdout = w
cmd.Stderr = w cmd.Stderr = w
cmd.Env = append(os.Environ(), cmdEnv...)
if err := cmd.Run(); err != nil { if err := cmd.Run(); err != nil {
if _, ok := err.(*exec.ExitError); ok { if _, ok := err.(*exec.ExitError); ok {

View File

@ -26,9 +26,13 @@ import (
) )
// getLoggingCmd returns the journalctl cmd and arguments for the given nodeLogQuery and boot. Note that // getLoggingCmd returns the journalctl cmd and arguments for the given nodeLogQuery and boot. Note that
// services are explicitly passed here to account for the heuristics // services are explicitly passed here to account for the heuristics.
func getLoggingCmd(n *nodeLogQuery, services []string) (string, []string, error) { // The return values are:
args := []string{ // - cmd: the command to be executed
// - args: arguments to the command
// - cmdEnv: environment variables when the command will be executed
func getLoggingCmd(n *nodeLogQuery, services []string) (cmd string, args []string, cmdEnv []string, err error) {
args = []string{
"--utc", "--utc",
"--no-pager", "--no-pager",
"--output=short-precise", "--output=short-precise",
@ -55,7 +59,7 @@ func getLoggingCmd(n *nodeLogQuery, services []string) (string, []string, error)
args = append(args, "--boot", fmt.Sprintf("%d", *n.Boot)) args = append(args, "--boot", fmt.Sprintf("%d", *n.Boot))
} }
return "journalctl", args, nil return "journalctl", args, nil, nil
} }
// checkForNativeLogger checks journalctl output for a service // checkForNativeLogger checks journalctl output for a service

View File

@ -24,8 +24,8 @@ import (
) )
// getLoggingCmd on unsupported operating systems returns the echo command and a warning message (as strings) // getLoggingCmd on unsupported operating systems returns the echo command and a warning message (as strings)
func getLoggingCmd(n *nodeLogQuery, services []string) (string, []string, error) { func getLoggingCmd(n *nodeLogQuery, services []string) (cmd string, args []string, cmdEnv []string, err error) {
return "", []string{}, errors.New("Operating System Not Supported") return "", args, cmdEnv, errors.New("Operating System Not Supported")
} }
// checkForNativeLogger on unsupported operating systems returns false // checkForNativeLogger on unsupported operating systems returns false

View File

@ -30,31 +30,62 @@ import (
) )
func Test_getLoggingCmd(t *testing.T) { func Test_getLoggingCmd(t *testing.T) {
var emptyCmdEnv []string
tests := []struct { tests := []struct {
name string name string
args nodeLogQuery args nodeLogQuery
services []string
wantLinux []string wantLinux []string
wantWindows []string wantWindows []string
wantOtherOS []string wantLinuxCmdEnv []string
wantWindowsCmdEnv []string
}{ }{
{ {
name: "basic",
args: nodeLogQuery{}, args: nodeLogQuery{},
services: []string{},
wantLinux: []string{"--utc", "--no-pager", "--output=short-precise"}, wantLinux: []string{"--utc", "--no-pager", "--output=short-precise"},
wantLinuxCmdEnv: emptyCmdEnv,
wantWindows: []string{"-NonInteractive", "-ExecutionPolicy", "Bypass", "-Command", "Get-WinEvent -FilterHashtable @{LogName='Application'} | Sort-Object TimeCreated | Format-Table -AutoSize -Wrap"}, wantWindows: []string{"-NonInteractive", "-ExecutionPolicy", "Bypass", "-Command", "Get-WinEvent -FilterHashtable @{LogName='Application'} | Sort-Object TimeCreated | Format-Table -AutoSize -Wrap"},
wantWindowsCmdEnv: emptyCmdEnv,
},
{
name: "two providers",
args: nodeLogQuery{},
services: []string{"p1", "p2"},
wantLinux: []string{"--utc", "--no-pager", "--output=short-precise", "--unit=p1", "--unit=p2"},
wantLinuxCmdEnv: emptyCmdEnv,
wantWindows: []string{"-NonInteractive", "-ExecutionPolicy", "Bypass", "-Command", "Get-WinEvent -FilterHashtable @{LogName='Application'; ProviderName=$Env:kubelet_provider0,$Env:kubelet_provider1} | Sort-Object TimeCreated | Format-Table -AutoSize -Wrap"},
wantWindowsCmdEnv: []string{"kubelet_provider0=p1", "kubelet_provider1=p2"},
},
{
name: "empty provider",
args: nodeLogQuery{},
services: []string{"p1", "", "p2"},
wantLinux: []string{"--utc", "--no-pager", "--output=short-precise", "--unit=p1", "--unit=p2"},
wantLinuxCmdEnv: emptyCmdEnv,
wantWindows: []string{"-NonInteractive", "-ExecutionPolicy", "Bypass", "-Command", "Get-WinEvent -FilterHashtable @{LogName='Application'; ProviderName=$Env:kubelet_provider0,$Env:kubelet_provider2} | Sort-Object TimeCreated | Format-Table -AutoSize -Wrap"},
wantWindowsCmdEnv: []string{"kubelet_provider0=p1", "kubelet_provider2=p2"},
}, },
} }
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
_, got, err := getLoggingCmd(&tt.args, []string{}) _, got, gotCmdEnv, err := getLoggingCmd(&tt.args, tt.services)
switch os := runtime.GOOS; os { switch os := runtime.GOOS; os {
case "linux": case "linux":
if !reflect.DeepEqual(got, tt.wantLinux) { if !reflect.DeepEqual(got, tt.wantLinux) {
t.Errorf("getLoggingCmd() = %v, want %v", got, tt.wantLinux) t.Errorf("getLoggingCmd() = %v, want %v", got, tt.wantLinux)
} }
if !reflect.DeepEqual(gotCmdEnv, tt.wantLinuxCmdEnv) {
t.Errorf("gotCmdEnv %v, wantLinuxCmdEnv %v", gotCmdEnv, tt.wantLinuxCmdEnv)
}
case "windows": case "windows":
if !reflect.DeepEqual(got, tt.wantWindows) { if !reflect.DeepEqual(got, tt.wantWindows) {
t.Errorf("getLoggingCmd() = %v, want %v", got, tt.wantWindows) t.Errorf("getLoggingCmd() = %v, want %v", got, tt.wantWindows)
} }
if !reflect.DeepEqual(gotCmdEnv, tt.wantWindowsCmdEnv) {
t.Errorf("gotCmdEnv %v, wantWindowsCmdEnv %v", gotCmdEnv, tt.wantWindowsCmdEnv)
}
default: default:
if err == nil { if err == nil {
t.Errorf("getLoggingCmd() = %v, want err", got) t.Errorf("getLoggingCmd() = %v, want err", got)

View File

@ -27,43 +27,107 @@ import (
const powershellExe = "PowerShell.exe" const powershellExe = "PowerShell.exe"
// getLoggingCmd returns the powershell cmd and arguments for the given nodeLogQuery and boot // getLoggingCmd returns the powershell cmd, arguments, and environment variables for the given nodeLogQuery and boot.
func getLoggingCmd(n *nodeLogQuery, services []string) (string, []string, error) { // All string inputs are environment variables to stop subcommands expressions from being executed.
args := []string{ // The return values are:
// - cmd: the command to be executed
// - args: arguments to the command
// - cmdEnv: environment variables when the command will be executed
func getLoggingCmd(n *nodeLogQuery, services []string) (cmd string, args []string, cmdEnv []string, err error) {
cmdEnv = getLoggingCmdEnv(n, services)
var includeSinceTime, includeUntilTime, includeTailLines, includePattern bool
if n.SinceTime != nil {
includeSinceTime = true
}
if n.UntilTime != nil {
includeUntilTime = true
}
if n.TailLines != nil {
includeTailLines = true
}
if len(n.Pattern) > 0 {
includePattern = true
}
var includeServices []bool
for _, service := range services {
includeServices = append(includeServices, len(service) > 0)
}
args = getLoggingCmdArgs(includeSinceTime, includeUntilTime, includeTailLines, includePattern, includeServices)
return powershellExe, args, cmdEnv, nil
}
// getLoggingCmdArgs returns arguments that need to be passed to powershellExe
func getLoggingCmdArgs(includeSinceTime, includeUntilTime, includeTailLines, includePattern bool, services []bool) (args []string) {
args = []string{
"-NonInteractive", "-NonInteractive",
"-ExecutionPolicy", "Bypass", "-ExecutionPolicy", "Bypass",
"-Command", "-Command",
} }
psCmd := "Get-WinEvent -FilterHashtable @{LogName='Application'" psCmd := `Get-WinEvent -FilterHashtable @{LogName='Application'`
if n.SinceTime != nil {
psCmd += fmt.Sprintf("; StartTime='%s'", n.SinceTime.Format(dateLayout)) if includeSinceTime {
psCmd += fmt.Sprintf(`; StartTime="$Env:kubelet_sinceTime"`)
} }
if n.UntilTime != nil { if includeUntilTime {
psCmd += fmt.Sprintf("; EndTime='%s'", n.UntilTime.Format(dateLayout)) psCmd += fmt.Sprintf(`; EndTime="$Env:kubelet_untilTime"`)
} }
var providers []string var providers []string
for _, service := range services { for i := range services {
if len(service) > 0 { if services[i] {
providers = append(providers, "'"+service+"'") providers = append(providers, fmt.Sprintf("$Env:kubelet_provider%d", i))
} }
} }
if len(providers) > 0 { if len(providers) > 0 {
psCmd += fmt.Sprintf("; ProviderName=%s", strings.Join(providers, ",")) psCmd += fmt.Sprintf("; ProviderName=%s", strings.Join(providers, ","))
} }
psCmd += "}"
if n.TailLines != nil { psCmd += `}`
psCmd += fmt.Sprintf(" -MaxEvents %d", *n.TailLines) if includeTailLines {
psCmd += fmt.Sprint(` -MaxEvents $Env:kubelet_tailLines`)
} }
psCmd += " | Sort-Object TimeCreated" psCmd += ` | Sort-Object TimeCreated`
if len(n.Pattern) > 0 {
psCmd += fmt.Sprintf(" | Where-Object -Property Message -Match '%s'", n.Pattern) if includePattern {
psCmd += fmt.Sprintf(` | Where-Object -Property Message -Match "$Env:kubelet_pattern"`)
} }
psCmd += " | Format-Table -AutoSize -Wrap" psCmd += ` | Format-Table -AutoSize -Wrap`
args = append(args, psCmd) args = append(args, psCmd)
return powershellExe, args, nil return args
}
// getLoggingCmdEnv returns the environment variables that will be present when powershellExe is executed
func getLoggingCmdEnv(n *nodeLogQuery, services []string) (cmdEnv []string) {
if n.SinceTime != nil {
cmdEnv = append(cmdEnv, fmt.Sprintf("kubelet_sinceTime=%s", n.SinceTime.Format(dateLayout)))
}
if n.UntilTime != nil {
cmdEnv = append(cmdEnv, fmt.Sprintf("kubelet_untilTime=%s", n.UntilTime.Format(dateLayout)))
}
for i, service := range services {
if len(service) > 0 {
cmdEnv = append(cmdEnv, fmt.Sprintf("kubelet_provider%d=%s", i, service))
}
}
if n.TailLines != nil {
cmdEnv = append(cmdEnv, fmt.Sprintf("kubelet_tailLines=%d", *n.TailLines))
}
if len(n.Pattern) > 0 {
cmdEnv = append(cmdEnv, fmt.Sprintf("kubelet_pattern=%s", n.Pattern))
}
return cmdEnv
} }
// checkForNativeLogger always returns true for Windows // checkForNativeLogger always returns true for Windows

View File

@ -720,6 +720,8 @@ type KubeletConfiguration struct {
EnableSystemLogHandler *bool `json:"enableSystemLogHandler,omitempty"` EnableSystemLogHandler *bool `json:"enableSystemLogHandler,omitempty"`
// enableSystemLogQuery enables the node log query feature on the /logs endpoint. // enableSystemLogQuery enables the node log query feature on the /logs endpoint.
// EnableSystemLogHandler has to be enabled in addition for this feature to work. // EnableSystemLogHandler has to be enabled in addition for this feature to work.
// Enabling this feature has security implications. The recommendation is to enable it on a need basis for debugging
// purposes and disabling otherwise.
// Default: false // Default: false
// +featureGate=NodeLogQuery // +featureGate=NodeLogQuery
// +optional // +optional