@@ -35,7 +35,7 @@ import (
35
35
"time"
36
36
37
37
securejoin "github.com/cyphar/filepath-securejoin"
38
-
38
+ "k8s.io/apimachinery/pkg/util/sets"
39
39
utilvalidation "k8s.io/apimachinery/pkg/util/validation"
40
40
"k8s.io/apimachinery/pkg/util/validation/field"
41
41
)
54
54
// character cannot be used to create invalid sequences. This is intended as a broad defense against malformed
55
55
// input that could cause an escape.
56
56
reServiceNameUnsafeCharacters = regexp .MustCompile (`[^a-zA-Z\-_.:0-9@]+` )
57
+ reRelativeDate = regexp .MustCompile (`^(\+|\-)?[\d]+(s|m|h|d)$` )
57
58
)
58
59
59
60
// journalServer returns text output from the OS specific service logger to view
@@ -114,6 +115,19 @@ type options struct {
114
115
// Pattern filters log entries by the provided regex pattern. On Linux nodes, this pattern will be read as a
115
116
// PCRE2 regex, on Windows nodes it will be read as a PowerShell regex. Support for this is implementation specific.
116
117
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
117
131
}
118
132
119
133
// newNodeLogQuery parses query values and converts all known options into nodeLogQuery
@@ -122,7 +136,7 @@ func newNodeLogQuery(query url.Values) (*nodeLogQuery, field.ErrorList) {
122
136
var nlq nodeLogQuery
123
137
var err error
124
138
125
- queries , ok := query ["query" ]
139
+ queries , okQuery := query ["query" ]
126
140
if len (queries ) > 0 {
127
141
for _ , q := range queries {
128
142
// 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) {
134
148
}
135
149
}
136
150
}
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
+ }
137
160
138
161
// Prevent specifying an empty or blank space query.
139
162
// 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" ))
142
165
}
143
166
144
167
var sinceTime time.Time
@@ -176,6 +199,9 @@ func newNodeLogQuery(query url.Values) (*nodeLogQuery, field.ErrorList) {
176
199
177
200
var tailLines int
178
201
tailLinesValue := query .Get ("tailLines" )
202
+ if len (tailLinesValue ) == 0 {
203
+ tailLinesValue = query .Get ("tail" )
204
+ }
179
205
if len (tailLinesValue ) > 0 {
180
206
tailLines , err = strconv .Atoi (tailLinesValue )
181
207
if err != nil {
@@ -186,15 +212,28 @@ func newNodeLogQuery(query url.Values) (*nodeLogQuery, field.ErrorList) {
186
212
}
187
213
188
214
pattern := query .Get ("pattern" )
215
+ if len (pattern ) == 0 {
216
+ pattern = query .Get ("grep" )
217
+ }
189
218
if len (pattern ) > 0 {
190
219
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
+ }
191
230
}
192
231
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" )
196
235
197
- if reflect . DeepEqual ( nlq , nodeLogQuery {}) {
236
+ if len ( allErrs ) > 0 {
198
237
return nil , allErrs
199
238
}
200
239
@@ -219,14 +258,13 @@ func validateServices(services []string) field.ErrorList {
219
258
func (n * nodeLogQuery ) validate () field.ErrorList {
220
259
allErrs := validateServices (n .Services )
221
260
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
224
262
case len (n .Files ) > 0 && len (n .Services ) > 0 :
225
263
allErrs = append (allErrs , field .Invalid (field .NewPath ("query" ), fmt .Sprintf ("%v, %v" , n .Files , n .Services ),
226
264
"cannot specify a file and service" ))
227
265
case len (n .Files ) > 1 :
228
266
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 {}):
230
268
allErrs = append (allErrs , field .Invalid (field .NewPath ("query" ), n .Files , "cannot specify file with options" ))
231
269
case len (n .Files ) == 1 :
232
270
if fullLogFilename , err := securejoin .SecureJoin (nodeLogDir , n .Files [0 ]); err != nil {
@@ -258,6 +296,35 @@ func (n *nodeLogQuery) validate() field.ErrorList {
258
296
allErrs = append (allErrs , field .Invalid (field .NewPath ("pattern" ), n .Pattern , err .Error ()))
259
297
}
260
298
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
+
261
328
return allErrs
262
329
}
263
330
@@ -280,19 +347,20 @@ func (n *nodeLogQuery) copyForBoot(ctx context.Context, w io.Writer, previousBoo
280
347
return
281
348
}
282
349
nativeLoggers , fileLoggers := n .splitNativeVsFileLoggers (ctx )
283
- if len (nativeLoggers ) > 0 {
284
- n .copyServiceLogs (ctx , w , nativeLoggers , previousBoot )
285
- }
286
350
287
- if len (fileLoggers ) > 0 && n .options != ( options {}) {
351
+ if len (fileLoggers ) > 0 && ! reflect . DeepEqual ( n .options , options {}) {
288
352
fmt .Fprintf (w , "\n options present and query resolved to log files for %v\n try without specifying options\n " ,
289
353
fileLoggers )
290
354
return
291
355
}
292
356
293
357
if len (fileLoggers ) > 0 {
294
358
copyFileLogs (ctx , w , fileLoggers )
359
+ return
295
360
}
361
+ // OCP: Return all logs in the case where nativeLoggers == ""
362
+ n .copyServiceLogs (ctx , w , nativeLoggers , previousBoot )
363
+
296
364
}
297
365
298
366
// splitNativeVsFileLoggers checks if each service logs to native OS logs or to a file and returns a list of services
@@ -413,3 +481,16 @@ func safeServiceName(s string) error {
413
481
}
414
482
return nil
415
483
}
484
+
485
+ func validateDate (date string ) error {
486
+ if len (date ) == 0 {
487
+ return nil
488
+ }
489
+ if reRelativeDate .MatchString (date ) {
490
+ return nil
491
+ }
492
+ if _ , err := time .Parse (dateLayout , date ); err == nil {
493
+ return nil
494
+ }
495
+ 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" )
496
+ }
0 commit comments