Skip to content

Commit 085c395

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 883c728 commit 085c395

8 files changed

+165
-45
lines changed

pkg/features/versioned_kube_features.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -542,8 +542,8 @@ var defaultVersionedKubernetesFeatureGates = map[featuregate.Feature]featuregate
542542
},
543543

544544
NodeLogQuery: {
545-
{Version: version.MustParse("1.27"), Default: false, PreRelease: featuregate.Alpha},
546-
{Version: version.MustParse("1.30"), Default: false, PreRelease: featuregate.Beta},
545+
{Version: version.MustParse("1.27"), Default: true, PreRelease: featuregate.Alpha},
546+
{Version: version.MustParse("1.30"), Default: true, PreRelease: featuregate.Beta},
547547
},
548548

549549
NodeSwap: {

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

+2
Original file line numberDiff line numberDiff line change
@@ -389,6 +389,7 @@ func TestValidateKubeletConfiguration(t *testing.T) {
389389
conf.CrashLoopBackOff = kubeletconfig.CrashLoopBackOffConfig{
390390
MaxContainerRestartPeriod: &metav1.Duration{Duration: 0 * time.Second},
391391
}
392+
392393
return conf
393394
},
394395
errMsg: "invalid configuration: CrashLoopBackOff.MaxContainerRestartPeriod (got: 0 seconds) must be set between 1s and 300s",
@@ -613,6 +614,7 @@ func TestValidateKubeletConfiguration(t *testing.T) {
613614
}, {
614615
name: "enableSystemLogQuery is enabled without NodeLogQuery feature gate",
615616
configure: func(conf *kubeletconfig.KubeletConfiguration) *kubeletconfig.KubeletConfiguration {
617+
conf.FeatureGates = map[string]bool{"NodeLogQuery": false}
616618
conf.EnableSystemLogQuery = true
617619
return conf
618620
},

pkg/kubelet/kubelet.go

+2-5
Original file line numberDiff line numberDiff line change
@@ -1685,16 +1685,13 @@ func (kl *Kubelet) Run(updates <-chan kubetypes.PodUpdate) {
16851685
http.Error(w, errs.ToAggregate().Error(), http.StatusBadRequest)
16861686
return
16871687
} else if nlq != nil {
1688-
if req.URL.Path != "/" && req.URL.Path != "" {
1689-
http.Error(w, "path not allowed in query mode", http.StatusNotAcceptable)
1690-
return
1691-
}
16921688
if errs := nlq.validate(); len(errs) > 0 {
16931689
http.Error(w, errs.ToAggregate().Error(), http.StatusNotAcceptable)
16941690
return
16951691
}
16961692
// Validation ensures that the request does not query services and files at the same time
1697-
if len(nlq.Services) > 0 {
1693+
// OCP: Presence of journal in the path indicates it is a query for service(s)
1694+
if len(nlq.Services) > 0 || req.URL.Path == "journal" || req.URL.Path == "journal/" {
16981695
journal.ServeHTTP(w, req)
16991696
return
17001697
}

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
@@ -413,3 +481,16 @@ func safeServiceName(s string) error {
413481
}
414482
return nil
415483
}
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+
}

pkg/kubelet/kubelet_server_journal_linux.go

+19-4
Original file line numberDiff line numberDiff line change
@@ -36,14 +36,20 @@ func getLoggingCmd(n *nodeLogQuery, services []string) (string, []string, error)
3636
args := []string{
3737
"--utc",
3838
"--no-pager",
39-
"--output=short-precise",
4039
}
41-
if n.SinceTime != nil {
40+
41+
if len(n.Since) > 0 {
42+
args = append(args, fmt.Sprintf("--since=%s", n.Since))
43+
} else if n.SinceTime != nil {
4244
args = append(args, fmt.Sprintf("--since=%s", n.SinceTime.Format(dateLayout)))
4345
}
44-
if n.UntilTime != nil {
45-
args = append(args, fmt.Sprintf("--until=%s", n.UntilTime.Format(dateLayout)))
46+
47+
if len(n.Until) > 0 {
48+
args = append(args, fmt.Sprintf("--until=%s", n.Until))
49+
} else if n.UntilTime != nil {
50+
args = append(args, fmt.Sprintf("--until=%s", n.SinceTime.Format(dateLayout)))
4651
}
52+
4753
if n.TailLines != nil {
4854
args = append(args, "--pager-end", fmt.Sprintf("--lines=%d", *n.TailLines))
4955
}
@@ -54,12 +60,21 @@ func getLoggingCmd(n *nodeLogQuery, services []string) (string, []string, error)
5460
}
5561
if len(n.Pattern) > 0 {
5662
args = append(args, "--grep="+n.Pattern)
63+
args = append(args, fmt.Sprintf("--case-sensitive=%t", n.CaseSensitive))
5764
}
5865

5966
if n.Boot != nil {
6067
args = append(args, "--boot", fmt.Sprintf("%d", *n.Boot))
6168
}
6269

70+
var output string
71+
if len(n.Format) > 0 {
72+
output = n.Format
73+
} else {
74+
output = "short-precise"
75+
}
76+
args = append(args, fmt.Sprintf("--output=%s", output))
77+
6378
return "journalctl", args, nil
6479
}
6580

0 commit comments

Comments
 (0)