Skip to content

Commit 8bb7c58

Browse files
aravindhpbertinatto
authored andcommitted
UPSTREAM: <carry>: Extend NodeLogQuery feature
Extend the NodeLogQuery feature to support oc adm node-logs options: - Default NodeLogQuery feature gate to true - Add support for --since, --until, --case-sensitive, --output, options UPSTREAM: <carry>: Extend NodeLogQuery feature Fix handling of the "until" parameter when generating the journalctl command. This was incorrectly being passed with the "since" value.
1 parent 1aed8e2 commit 8bb7c58

10 files changed

+173
-49
lines changed

pkg/features/kube_features.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -1539,8 +1539,8 @@ var defaultVersionedKubernetesFeatureGates = map[featuregate.Feature]featuregate
15391539
},
15401540

15411541
NodeLogQuery: {
1542-
{Version: version.MustParse("1.27"), Default: false, PreRelease: featuregate.Alpha},
1543-
{Version: version.MustParse("1.30"), Default: false, PreRelease: featuregate.Beta},
1542+
{Version: version.MustParse("1.27"), Default: true, PreRelease: featuregate.Alpha},
1543+
{Version: version.MustParse("1.30"), Default: true, PreRelease: featuregate.Beta},
15441544
},
15451545

15461546
NodeSwap: {

pkg/features/kube_features_test.go

+1
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ func TestEnsureAlphaGatesAreNotSwitchedOnByDefault(t *testing.T) {
7777
if feature == "WindowsHostNetwork" {
7878
return
7979
}
80+
// OpenShift-specific
8081
if feature == "NodeLogQuery" {
8182
return
8283
}

pkg/kubelet/apis/config/validation/validation_test.go

+2
Original file line numberDiff line numberDiff line change
@@ -396,6 +396,7 @@ func TestValidateKubeletConfiguration(t *testing.T) {
396396
conf.CrashLoopBackOff = kubeletconfig.CrashLoopBackOffConfig{
397397
MaxContainerRestartPeriod: &metav1.Duration{Duration: 0 * time.Second},
398398
}
399+
399400
return conf
400401
},
401402
errMsg: "invalid configuration: CrashLoopBackOff.MaxContainerRestartPeriod (got: 0 seconds) must be set between 1s and 300s",
@@ -620,6 +621,7 @@ func TestValidateKubeletConfiguration(t *testing.T) {
620621
}, {
621622
name: "enableSystemLogQuery is enabled without NodeLogQuery feature gate",
622623
configure: func(conf *kubeletconfig.KubeletConfiguration) *kubeletconfig.KubeletConfiguration {
624+
conf.FeatureGates = map[string]bool{"NodeLogQuery": false}
623625
conf.EnableSystemLogQuery = true
624626
return conf
625627
},

pkg/kubelet/kubelet.go

+2-5
Original file line numberDiff line numberDiff line change
@@ -1724,16 +1724,13 @@ func (kl *Kubelet) Run(updates <-chan kubetypes.PodUpdate) {
17241724
http.Error(w, errs.ToAggregate().Error(), http.StatusBadRequest)
17251725
return
17261726
} else if nlq != nil {
1727-
if req.URL.Path != "/" && req.URL.Path != "" {
1728-
http.Error(w, "path not allowed in query mode", http.StatusNotAcceptable)
1729-
return
1730-
}
17311727
if errs := nlq.validate(); len(errs) > 0 {
17321728
http.Error(w, errs.ToAggregate().Error(), http.StatusNotAcceptable)
17331729
return
17341730
}
17351731
// Validation ensures that the request does not query services and files at the same time
1736-
if len(nlq.Services) > 0 {
1732+
// OCP: Presence of journal in the path indicates it is a query for service(s)
1733+
if len(nlq.Services) > 0 || req.URL.Path == "journal" || req.URL.Path == "journal/" {
17371734
journal.ServeHTTP(w, req)
17381735
return
17391736
}

pkg/kubelet/kubelet_server_journal.go

+96-15
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ import (
3535
"time"
3636

3737
securejoin "github.com/cyphar/filepath-securejoin"
38-
38+
"k8s.io/apimachinery/pkg/util/sets"
3939
utilvalidation "k8s.io/apimachinery/pkg/util/validation"
4040
"k8s.io/apimachinery/pkg/util/validation/field"
4141
)
@@ -54,6 +54,7 @@ var (
5454
// character cannot be used to create invalid sequences. This is intended as a broad defense against malformed
5555
// input that could cause an escape.
5656
reServiceNameUnsafeCharacters = regexp.MustCompile(`[^a-zA-Z\-_.:0-9@]+`)
57+
reRelativeDate = regexp.MustCompile(`^(\+|\-)?[\d]+(s|m|h|d)$`)
5758
)
5859

5960
// journalServer returns text output from the OS specific service logger to view
@@ -114,6 +115,19 @@ type options struct {
114115
// Pattern filters log entries by the provided regex pattern. On Linux nodes, this pattern will be read as a
115116
// PCRE2 regex, on Windows nodes it will be read as a PowerShell regex. Support for this is implementation specific.
116117
Pattern string
118+
ocAdm
119+
}
120+
121+
// ocAdm encapsulates the oc adm node-logs specific options
122+
type ocAdm struct {
123+
// Since is an ISO timestamp or relative date from which to show logs
124+
Since string
125+
// Until is an ISO timestamp or relative date until which to show logs
126+
Until string
127+
// Format is the alternate format (short, cat, json, short-unix) to display journal logs
128+
Format string
129+
// CaseSensitive controls the case sensitivity of pattern searches
130+
CaseSensitive bool
117131
}
118132

119133
// newNodeLogQuery parses query values and converts all known options into nodeLogQuery
@@ -122,7 +136,7 @@ func newNodeLogQuery(query url.Values) (*nodeLogQuery, field.ErrorList) {
122136
var nlq nodeLogQuery
123137
var err error
124138

125-
queries, ok := query["query"]
139+
queries, okQuery := query["query"]
126140
if len(queries) > 0 {
127141
for _, q := range queries {
128142
// The presence of / or \ is a hint that the query is for a log file. If the query is for foo.log without a
@@ -134,11 +148,20 @@ func newNodeLogQuery(query url.Values) (*nodeLogQuery, field.ErrorList) {
134148
}
135149
}
136150
}
151+
units, okUnit := query["unit"]
152+
if len(units) > 0 {
153+
for _, u := range units {
154+
// We don't check for files as the heuristics do not apply to unit
155+
if strings.TrimSpace(u) != "" { // Prevent queries with just spaces
156+
nlq.Services = append(nlq.Services, u)
157+
}
158+
}
159+
}
137160

138161
// Prevent specifying an empty or blank space query.
139162
// Example: kubectl get --raw /api/v1/nodes/$node/proxy/logs?query=" "
140-
if ok && (len(nlq.Files) == 0 && len(nlq.Services) == 0) {
141-
allErrs = append(allErrs, field.Invalid(field.NewPath("query"), queries, "query cannot be empty"))
163+
if (okQuery || okUnit) && (len(nlq.Files) == 0 && len(nlq.Services) == 0) {
164+
allErrs = append(allErrs, field.Invalid(field.NewPath("unit"), queries, "unit cannot be empty"))
142165
}
143166

144167
var sinceTime time.Time
@@ -176,6 +199,9 @@ func newNodeLogQuery(query url.Values) (*nodeLogQuery, field.ErrorList) {
176199

177200
var tailLines int
178201
tailLinesValue := query.Get("tailLines")
202+
if len(tailLinesValue) == 0 {
203+
tailLinesValue = query.Get("tail")
204+
}
179205
if len(tailLinesValue) > 0 {
180206
tailLines, err = strconv.Atoi(tailLinesValue)
181207
if err != nil {
@@ -186,15 +212,28 @@ func newNodeLogQuery(query url.Values) (*nodeLogQuery, field.ErrorList) {
186212
}
187213

188214
pattern := query.Get("pattern")
215+
if len(pattern) == 0 {
216+
pattern = query.Get("grep")
217+
}
189218
if len(pattern) > 0 {
190219
nlq.Pattern = pattern
220+
caseSensitiveValue := query.Get("case-sensitive")
221+
if len(caseSensitiveValue) > 0 {
222+
caseSensitive, err := strconv.ParseBool(query.Get("case-sensitive"))
223+
if err != nil {
224+
allErrs = append(allErrs, field.Invalid(field.NewPath("case-sensitive"), query.Get("case-sensitive"),
225+
err.Error()))
226+
} else {
227+
nlq.CaseSensitive = caseSensitive
228+
}
229+
}
191230
}
192231

193-
if len(allErrs) > 0 {
194-
return nil, allErrs
195-
}
232+
nlq.Since = query.Get("since")
233+
nlq.Until = query.Get("until")
234+
nlq.Format = query.Get("output")
196235

197-
if reflect.DeepEqual(nlq, nodeLogQuery{}) {
236+
if len(allErrs) > 0 {
198237
return nil, allErrs
199238
}
200239

@@ -219,14 +258,13 @@ func validateServices(services []string) field.ErrorList {
219258
func (n *nodeLogQuery) validate() field.ErrorList {
220259
allErrs := validateServices(n.Services)
221260
switch {
222-
case len(n.Files) == 0 && len(n.Services) == 0:
223-
allErrs = append(allErrs, field.Required(field.NewPath("query"), "cannot be empty with options"))
261+
// OCP: Allow len(n.Files) == 0 && len(n.Services) == 0 as we want to be able to return all journal / WinEvent logs
224262
case len(n.Files) > 0 && len(n.Services) > 0:
225263
allErrs = append(allErrs, field.Invalid(field.NewPath("query"), fmt.Sprintf("%v, %v", n.Files, n.Services),
226264
"cannot specify a file and service"))
227265
case len(n.Files) > 1:
228266
allErrs = append(allErrs, field.Invalid(field.NewPath("query"), n.Files, "cannot specify more than one file"))
229-
case len(n.Files) == 1 && n.options != (options{}):
267+
case len(n.Files) == 1 && !reflect.DeepEqual(n.options, options{}):
230268
allErrs = append(allErrs, field.Invalid(field.NewPath("query"), n.Files, "cannot specify file with options"))
231269
case len(n.Files) == 1:
232270
if fullLogFilename, err := securejoin.SecureJoin(nodeLogDir, n.Files[0]); err != nil {
@@ -258,6 +296,35 @@ func (n *nodeLogQuery) validate() field.ErrorList {
258296
allErrs = append(allErrs, field.Invalid(field.NewPath("pattern"), n.Pattern, err.Error()))
259297
}
260298

299+
// "oc adm node-logs" specific validation
300+
301+
if n.SinceTime != nil && (len(n.Since) > 0 || len(n.Until) > 0) {
302+
allErrs = append(allErrs, field.Forbidden(field.NewPath("sinceTime"),
303+
"`since or until` and `sinceTime` cannot be specified"))
304+
}
305+
306+
if n.UntilTime != nil && (len(n.Since) > 0 || len(n.Until) > 0) {
307+
allErrs = append(allErrs, field.Forbidden(field.NewPath("untilTime"),
308+
"`since or until` and `untilTime` cannot be specified"))
309+
}
310+
311+
if err := validateDate(n.Since); err != nil {
312+
allErrs = append(allErrs, field.Invalid(field.NewPath("since"), n.Since, err.Error()))
313+
}
314+
315+
if err := validateDate(n.Until); err != nil {
316+
allErrs = append(allErrs, field.Invalid(field.NewPath("until"), n.Until, err.Error()))
317+
}
318+
319+
allowedFormats := sets.New[string]("short-precise", "json", "short", "short-unix", "short-iso",
320+
"short-iso-precise", "cat", "")
321+
if len(n.Format) > 0 && runtime.GOOS == "windows" {
322+
allErrs = append(allErrs, field.Invalid(field.NewPath("output"), n.Format,
323+
"output is not supported on Windows"))
324+
} else if !allowedFormats.Has(n.Format) {
325+
allErrs = append(allErrs, field.NotSupported(field.NewPath("output"), n.Format, allowedFormats.UnsortedList()))
326+
}
327+
261328
return allErrs
262329
}
263330

@@ -280,19 +347,20 @@ func (n *nodeLogQuery) copyForBoot(ctx context.Context, w io.Writer, previousBoo
280347
return
281348
}
282349
nativeLoggers, fileLoggers := n.splitNativeVsFileLoggers(ctx)
283-
if len(nativeLoggers) > 0 {
284-
n.copyServiceLogs(ctx, w, nativeLoggers, previousBoot)
285-
}
286350

287-
if len(fileLoggers) > 0 && n.options != (options{}) {
351+
if len(fileLoggers) > 0 && !reflect.DeepEqual(n.options, options{}) {
288352
fmt.Fprintf(w, "\noptions present and query resolved to log files for %v\ntry without specifying options\n",
289353
fileLoggers)
290354
return
291355
}
292356

293357
if len(fileLoggers) > 0 {
294358
copyFileLogs(ctx, w, fileLoggers)
359+
return
295360
}
361+
// OCP: Return all logs in the case where nativeLoggers == ""
362+
n.copyServiceLogs(ctx, w, nativeLoggers, previousBoot)
363+
296364
}
297365

298366
// splitNativeVsFileLoggers checks if each service logs to native OS logs or to a file and returns a list of services
@@ -414,3 +482,16 @@ func safeServiceName(s string) error {
414482
}
415483
return nil
416484
}
485+
486+
func validateDate(date string) error {
487+
if len(date) == 0 {
488+
return nil
489+
}
490+
if reRelativeDate.MatchString(date) {
491+
return nil
492+
}
493+
if _, err := time.Parse(dateLayout, date); err == nil {
494+
return nil
495+
}
496+
return fmt.Errorf("date must be a relative time of the form '(+|-)[0-9]+(s|m|h|d)' or a date in 'YYYY-MM-DD HH:MM:SS' form")
497+
}

pkg/kubelet/kubelet_server_journal_linux.go

+19-4
Original file line numberDiff line numberDiff line change
@@ -40,14 +40,20 @@ func getLoggingCmd(n *nodeLogQuery, services []string) (cmd string, args []strin
4040
args = []string{
4141
"--utc",
4242
"--no-pager",
43-
"--output=short-precise",
4443
}
45-
if n.SinceTime != nil {
44+
45+
if len(n.Since) > 0 {
46+
args = append(args, fmt.Sprintf("--since=%s", n.Since))
47+
} else if n.SinceTime != nil {
4648
args = append(args, fmt.Sprintf("--since=%s", n.SinceTime.Format(dateLayout)))
4749
}
48-
if n.UntilTime != nil {
49-
args = append(args, fmt.Sprintf("--until=%s", n.UntilTime.Format(dateLayout)))
50+
51+
if len(n.Until) > 0 {
52+
args = append(args, fmt.Sprintf("--until=%s", n.Until))
53+
} else if n.UntilTime != nil {
54+
args = append(args, fmt.Sprintf("--until=%s", n.SinceTime.Format(dateLayout)))
5055
}
56+
5157
if n.TailLines != nil {
5258
args = append(args, "--pager-end", fmt.Sprintf("--lines=%d", *n.TailLines))
5359
}
@@ -58,12 +64,21 @@ func getLoggingCmd(n *nodeLogQuery, services []string) (cmd string, args []strin
5864
}
5965
if len(n.Pattern) > 0 {
6066
args = append(args, "--grep="+n.Pattern)
67+
args = append(args, fmt.Sprintf("--case-sensitive=%t", n.CaseSensitive))
6168
}
6269

6370
if n.Boot != nil {
6471
args = append(args, "--boot", fmt.Sprintf("%d", *n.Boot))
6572
}
6673

74+
var output string
75+
if len(n.Format) > 0 {
76+
output = n.Format
77+
} else {
78+
output = "short-precise"
79+
}
80+
args = append(args, fmt.Sprintf("--output=%s", output))
81+
6782
return "journalctl", args, nil, nil
6883
}
6984

0 commit comments

Comments
 (0)