Skip to content

Commit 5854f53

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 6cebb7f commit 5854f53

File tree

7 files changed

+161
-41
lines changed

7 files changed

+161
-41
lines changed

pkg/features/kube_features.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1097,7 +1097,7 @@ var defaultKubernetesFeatureGates = map[featuregate.Feature]featuregate.FeatureS
10971097

10981098
NewVolumeManagerReconstruction: {Default: true, PreRelease: featuregate.Beta},
10991099

1100-
NodeLogQuery: {Default: false, PreRelease: featuregate.Alpha},
1100+
NodeLogQuery: {Default: true, PreRelease: featuregate.Alpha},
11011101

11021102
NodeOutOfServiceVolumeDetach: {Default: true, PreRelease: featuregate.GA, LockToDefault: true}, // remove in 1.31
11031103

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -508,6 +508,7 @@ func TestValidateKubeletConfiguration(t *testing.T) {
508508
}, {
509509
name: "enableSystemLogQuery is enabled without NodeLogQuery feature gate",
510510
configure: func(conf *kubeletconfig.KubeletConfiguration) *kubeletconfig.KubeletConfiguration {
511+
conf.FeatureGates = map[string]bool{"NodeLogQuery": false}
511512
conf.EnableSystemLogQuery = true
512513
return conf
513514
},

pkg/kubelet/kubelet.go

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1548,16 +1548,13 @@ func (kl *Kubelet) Run(updates <-chan kubetypes.PodUpdate) {
15481548
http.Error(w, errs.ToAggregate().Error(), http.StatusBadRequest)
15491549
return
15501550
} else if nlq != nil {
1551-
if req.URL.Path != "/" && req.URL.Path != "" {
1552-
http.Error(w, "path not allowed in query mode", http.StatusNotAcceptable)
1553-
return
1554-
}
15551551
if errs := nlq.validate(); len(errs) > 0 {
15561552
http.Error(w, errs.ToAggregate().Error(), http.StatusNotAcceptable)
15571553
return
15581554
}
15591555
// Validation ensures that the request does not query services and files at the same time
1560-
if len(nlq.Services) > 0 {
1556+
// OCP: Presence of journal in the path indicates it is a query for service(s)
1557+
if len(nlq.Services) > 0 || req.URL.Path == "journal" || req.URL.Path == "journal/" {
15611558
journal.ServeHTTP(w, req)
15621559
return
15631560
}

pkg/kubelet/kubelet_server_journal.go

Lines changed: 96 additions & 15 deletions
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
@@ -442,3 +510,16 @@ func safeServiceName(s string) error {
442510
}
443511
return nil
444512
}
513+
514+
func validateDate(date string) error {
515+
if len(date) == 0 {
516+
return nil
517+
}
518+
if reRelativeDate.MatchString(date) {
519+
return nil
520+
}
521+
if _, err := time.Parse(dateLayout, date); err == nil {
522+
return nil
523+
}
524+
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")
525+
}

pkg/kubelet/kubelet_server_journal_linux.go

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,14 +31,20 @@ func getLoggingCmd(n *nodeLogQuery, services []string) (string, []string, error)
3131
args := []string{
3232
"--utc",
3333
"--no-pager",
34-
"--output=short-precise",
3534
}
36-
if n.SinceTime != nil {
35+
36+
if len(n.Since) > 0 {
37+
args = append(args, fmt.Sprintf("--since=%s", n.Since))
38+
} else if n.SinceTime != nil {
3739
args = append(args, fmt.Sprintf("--since=%s", n.SinceTime.Format(dateLayout)))
3840
}
39-
if n.UntilTime != nil {
41+
42+
if len(n.Until) > 0 {
43+
args = append(args, fmt.Sprintf("--until=%s", n.Until))
44+
} else if n.UntilTime != nil {
4045
args = append(args, fmt.Sprintf("--until=%s", n.SinceTime.Format(dateLayout)))
4146
}
47+
4248
if n.TailLines != nil {
4349
args = append(args, "--pager-end", fmt.Sprintf("--lines=%d", *n.TailLines))
4450
}
@@ -49,12 +55,21 @@ func getLoggingCmd(n *nodeLogQuery, services []string) (string, []string, error)
4955
}
5056
if len(n.Pattern) > 0 {
5157
args = append(args, "--grep="+n.Pattern)
58+
args = append(args, fmt.Sprintf("--case-sensitive=%t", n.CaseSensitive))
5259
}
5360

5461
if n.Boot != nil {
5562
args = append(args, "--boot", fmt.Sprintf("%d", *n.Boot))
5663
}
5764

65+
var output string
66+
if len(n.Format) > 0 {
67+
output = n.Format
68+
} else {
69+
output = "short-precise"
70+
}
71+
args = append(args, fmt.Sprintf("--output=%s", output))
72+
5873
return "journalctl", args, nil
5974
}
6075

pkg/kubelet/kubelet_server_journal_test.go

Lines changed: 36 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -73,18 +73,18 @@ func Test_newNodeLogQuery(t *testing.T) {
7373
want *nodeLogQuery
7474
wantErr bool
7575
}{
76-
{name: "empty", query: url.Values{}, want: nil},
77-
{query: url.Values{"unknown": []string{"true"}}, want: nil},
76+
{name: "empty", query: url.Values{}, want: &nodeLogQuery{}},
77+
{query: url.Values{"unknown": []string{"true"}}, want: &nodeLogQuery{}},
7878

79-
{query: url.Values{"sinceTime": []string{""}}, want: nil},
79+
{query: url.Values{"sinceTime": []string{""}}, want: &nodeLogQuery{}},
8080
{query: url.Values{"sinceTime": []string{"2019-12-04 02:00:00"}}, wantErr: true},
8181
{query: url.Values{"sinceTime": []string{"2019-12-04 02:00:00.000"}}, wantErr: true},
8282
{query: url.Values{"sinceTime": []string{"2019-12-04 02"}}, wantErr: true},
8383
{query: url.Values{"sinceTime": []string{"2019-12-04 02:00"}}, wantErr: true},
8484
{query: url.Values{"sinceTime": []string{validTimeValue}},
8585
want: &nodeLogQuery{options: options{SinceTime: &validT}}},
8686

87-
{query: url.Values{"untilTime": []string{""}}, want: nil},
87+
{query: url.Values{"untilTime": []string{""}}, want: &nodeLogQuery{}},
8888
{query: url.Values{"untilTime": []string{"2019-12-04 02:00:00"}}, wantErr: true},
8989
{query: url.Values{"untilTime": []string{"2019-12-04 02:00:00.000"}}, wantErr: true},
9090
{query: url.Values{"untilTime": []string{"2019-12-04 02"}}, wantErr: true},
@@ -98,7 +98,7 @@ func Test_newNodeLogQuery(t *testing.T) {
9898

9999
{query: url.Values{"pattern": []string{"foo"}}, want: &nodeLogQuery{options: options{Pattern: "foo"}}},
100100

101-
{query: url.Values{"boot": []string{""}}, want: nil},
101+
{query: url.Values{"boot": []string{""}}, want: &nodeLogQuery{}},
102102
{query: url.Values{"boot": []string{"0"}}, want: &nodeLogQuery{options: options{Boot: intPtr(0)}}},
103103
{query: url.Values{"boot": []string{"-23"}}, want: &nodeLogQuery{options: options{Boot: intPtr(-23)}}},
104104
{query: url.Values{"boot": []string{"foo"}}, wantErr: true},
@@ -111,6 +111,11 @@ func Test_newNodeLogQuery(t *testing.T) {
111111
{query: url.Values{"query": []string{"foo", "/bar"}}, want: &nodeLogQuery{Services: []string{"foo"},
112112
Files: []string{"/bar"}}},
113113
{query: url.Values{"query": []string{"/foo", `\bar`}}, want: &nodeLogQuery{Files: []string{"/foo", `\bar`}}},
114+
{query: url.Values{"unit": []string{""}}, wantErr: true},
115+
{query: url.Values{"unit": []string{" ", " "}}, wantErr: true},
116+
{query: url.Values{"unit": []string{"foo"}}, want: &nodeLogQuery{Services: []string{"foo"}}},
117+
{query: url.Values{"unit": []string{"foo", "bar"}}, want: &nodeLogQuery{Services: []string{"foo", "bar"}}},
118+
{query: url.Values{"unit": []string{"foo", "/bar"}}, want: &nodeLogQuery{Services: []string{"foo", "/bar"}}},
114119
}
115120
for _, tt := range tests {
116121
t.Run(tt.query.Encode(), func(t *testing.T) {
@@ -165,10 +170,12 @@ func Test_nodeLogQuery_validate(t *testing.T) {
165170
pattern = "foo"
166171
invalid = "foo\\"
167172
)
168-
since, err := time.Parse(time.RFC3339, "2023-01-04T02:00:00Z")
173+
sinceTime, err := time.Parse(time.RFC3339, "2023-01-04T02:00:00Z")
169174
assert.NoError(t, err)
170-
until, err := time.Parse(time.RFC3339, "2023-02-04T02:00:00Z")
175+
untilTime, err := time.Parse(time.RFC3339, "2023-02-04T02:00:00Z")
171176
assert.NoError(t, err)
177+
since := "2019-12-04 02:00:00"
178+
until := "2019-12-04 03:00:00"
172179

173180
tests := []struct {
174181
name string
@@ -177,23 +184,37 @@ func Test_nodeLogQuery_validate(t *testing.T) {
177184
options options
178185
wantErr bool
179186
}{
180-
{name: "empty", wantErr: true},
181-
{name: "empty with options", options: options{SinceTime: &since}, wantErr: true},
187+
{name: "empty"},
188+
{name: "empty with options", options: options{SinceTime: &sinceTime}},
182189
{name: "one service", Services: []string{service1}},
183190
{name: "two services", Services: []string{service1, service2}},
184191
{name: "one service one file", Services: []string{service1}, Files: []string{file1}, wantErr: true},
185192
{name: "two files", Files: []string{file1, file2}, wantErr: true},
186193
{name: "one file options", Files: []string{file1}, options: options{Pattern: pattern}, wantErr: true},
187194
{name: "invalid pattern", Services: []string{service1}, options: options{Pattern: invalid}, wantErr: true},
188-
{name: "since", Services: []string{service1}, options: options{SinceTime: &since}},
189-
{name: "until", Services: []string{service1}, options: options{UntilTime: &until}},
190-
{name: "since until", Services: []string{service1}, options: options{SinceTime: &until, UntilTime: &since},
191-
wantErr: true},
192-
// boot is not supported on Windows.
193-
{name: "boot", Services: []string{service1}, options: options{Boot: intPtr(-1)}, wantErr: runtime.GOOS == "windows"},
195+
{name: "sinceTime", Services: []string{service1}, options: options{SinceTime: &sinceTime}},
196+
{name: "untilTime", Services: []string{service1}, options: options{UntilTime: &untilTime}},
197+
{name: "sinceTime untilTime", Services: []string{service1}, options: options{SinceTime: &untilTime,
198+
UntilTime: &sinceTime}, wantErr: true},
199+
{name: "boot", Services: []string{service1}, options: options{Boot: intPtr(-1)}},
194200
{name: "boot out of range", Services: []string{service1}, options: options{Boot: intPtr(1)}, wantErr: true},
195201
{name: "tailLines", Services: []string{service1}, options: options{TailLines: intPtr(100)}},
196202
{name: "tailLines out of range", Services: []string{service1}, options: options{TailLines: intPtr(100000)}},
203+
{name: "since", Services: []string{service1}, options: options{ocAdm: ocAdm{Since: since}}},
204+
{name: "since RFC3339", Services: []string{service1}, options: options{ocAdm: ocAdm{Since: sinceTime.String()}}, wantErr: true},
205+
{name: "until", Services: []string{service1}, options: options{ocAdm: ocAdm{Until: until}}},
206+
{name: "until RFC3339", Services: []string{service1}, options: options{ocAdm: ocAdm{Until: untilTime.String()}}, wantErr: true},
207+
{name: "since sinceTime", Services: []string{service1}, options: options{SinceTime: &sinceTime,
208+
ocAdm: ocAdm{Since: since}}, wantErr: true},
209+
{name: "until sinceTime", Services: []string{service1}, options: options{SinceTime: &sinceTime,
210+
ocAdm: ocAdm{Until: until}}, wantErr: true},
211+
{name: "since untilTime", Services: []string{service1}, options: options{UntilTime: &untilTime,
212+
ocAdm: ocAdm{Since: since}}, wantErr: true},
213+
{name: "until untilTime", Services: []string{service1}, options: options{UntilTime: &untilTime,
214+
ocAdm: ocAdm{Until: until}}, wantErr: true},
215+
{name: "format", Services: []string{service1}, options: options{ocAdm: ocAdm{Format: "cat"}}},
216+
{name: "format invalid", Services: []string{service1}, options: options{ocAdm: ocAdm{Format: "foo"}},
217+
wantErr: true},
197218
}
198219
for _, tt := range tests {
199220
t.Run(tt.name, func(t *testing.T) {

0 commit comments

Comments
 (0)