Skip to content

Commit 7e1c8d9

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 865a893 commit 7e1c8d9

File tree

10 files changed

+177
-46
lines changed

10 files changed

+177
-46
lines changed

pkg/features/kube_features.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1504,8 +1504,8 @@ var defaultVersionedKubernetesFeatureGates = map[featuregate.Feature]featuregate
15041504
},
15051505

15061506
NodeLogQuery: {
1507-
{Version: version.MustParse("1.27"), Default: false, PreRelease: featuregate.Alpha},
1508-
{Version: version.MustParse("1.30"), Default: false, PreRelease: featuregate.Beta},
1507+
{Version: version.MustParse("1.27"), Default: true, PreRelease: featuregate.Alpha},
1508+
{Version: version.MustParse("1.30"), Default: true, PreRelease: featuregate.Beta},
15091509
},
15101510

15111511
NodeSwap: {

pkg/features/kube_features_test.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,10 @@ func TestEnsureAlphaGatesAreNotSwitchedOnByDefault(t *testing.T) {
7777
if feature == "WindowsHostNetwork" {
7878
return
7979
}
80+
// OpenShift-specific
81+
if feature == "NodeLogQuery" {
82+
return
83+
}
8084
if spec.PreRelease == featuregate.Alpha && spec.Default {
8185
t.Errorf("The alpha feature gate %q is switched on by default", feature)
8286
}

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -388,6 +388,7 @@ func TestValidateKubeletConfiguration(t *testing.T) {
388388
conf.CrashLoopBackOff = kubeletconfig.CrashLoopBackOffConfig{
389389
MaxContainerRestartPeriod: &metav1.Duration{Duration: 0 * time.Second},
390390
}
391+
391392
return conf
392393
},
393394
errMsg: "invalid configuration: CrashLoopBackOff.MaxContainerRestartPeriod (got: 0 seconds) must be set between 1s and 300s",
@@ -601,6 +602,7 @@ func TestValidateKubeletConfiguration(t *testing.T) {
601602
}, {
602603
name: "enableSystemLogQuery is enabled without NodeLogQuery feature gate",
603604
configure: func(conf *kubeletconfig.KubeletConfiguration) *kubeletconfig.KubeletConfiguration {
605+
conf.FeatureGates = map[string]bool{"NodeLogQuery": false}
604606
conf.EnableSystemLogQuery = true
605607
return conf
606608
},

pkg/kubelet/kubelet.go

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1739,16 +1739,13 @@ func (kl *Kubelet) Run(updates <-chan kubetypes.PodUpdate) {
17391739
http.Error(w, errs.ToAggregate().Error(), http.StatusBadRequest)
17401740
return
17411741
} else if nlq != nil {
1742-
if req.URL.Path != "/" && req.URL.Path != "" {
1743-
http.Error(w, "path not allowed in query mode", http.StatusNotAcceptable)
1744-
return
1745-
}
17461742
if errs := nlq.validate(); len(errs) > 0 {
17471743
http.Error(w, errs.ToAggregate().Error(), http.StatusNotAcceptable)
17481744
return
17491745
}
17501746
// Validation ensures that the request does not query services and files at the same time
1751-
if len(nlq.Services) > 0 {
1747+
// OCP: Presence of journal in the path indicates it is a query for service(s)
1748+
if len(nlq.Services) > 0 || req.URL.Path == "journal" || req.URL.Path == "journal/" {
17521749
journal.ServeHTTP(w, req)
17531750
return
17541751
}

pkg/kubelet/kubelet_server_journal.go

Lines changed: 96 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ import (
3434
"strings"
3535
"time"
3636

37+
"k8s.io/apimachinery/pkg/util/sets"
3738
utilvalidation "k8s.io/apimachinery/pkg/util/validation"
3839
"k8s.io/apimachinery/pkg/util/validation/field"
3940
)
@@ -52,6 +53,7 @@ var (
5253
// character cannot be used to create invalid sequences. This is intended as a broad defense against malformed
5354
// input that could cause an escape.
5455
reServiceNameUnsafeCharacters = regexp.MustCompile(`[^a-zA-Z\-_.:0-9@]+`)
56+
reRelativeDate = regexp.MustCompile(`^(\+|\-)?[\d]+(s|m|h|d)$`)
5557
)
5658

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

117132
// newNodeLogQuery parses query values and converts all known options into nodeLogQuery
@@ -120,7 +135,7 @@ func newNodeLogQuery(query url.Values) (*nodeLogQuery, field.ErrorList) {
120135
var nlq nodeLogQuery
121136
var err error
122137

123-
queries, ok := query["query"]
138+
queries, okQuery := query["query"]
124139
if len(queries) > 0 {
125140
for _, q := range queries {
126141
// The presence of / or \ is a hint that the query is for a log file. If the query is for foo.log without a
@@ -132,11 +147,20 @@ func newNodeLogQuery(query url.Values) (*nodeLogQuery, field.ErrorList) {
132147
}
133148
}
134149
}
150+
units, okUnit := query["unit"]
151+
if len(units) > 0 {
152+
for _, u := range units {
153+
// We don't check for files as the heuristics do not apply to unit
154+
if strings.TrimSpace(u) != "" { // Prevent queries with just spaces
155+
nlq.Services = append(nlq.Services, u)
156+
}
157+
}
158+
}
135159

136160
// Prevent specifying an empty or blank space query.
137161
// Example: kubectl get --raw /api/v1/nodes/$node/proxy/logs?query=" "
138-
if ok && (len(nlq.Files) == 0 && len(nlq.Services) == 0) {
139-
allErrs = append(allErrs, field.Invalid(field.NewPath("query"), queries, "may not be empty"))
162+
if (okQuery || okUnit) && (len(nlq.Files) == 0 && len(nlq.Services) == 0) {
163+
allErrs = append(allErrs, field.Invalid(field.NewPath("unit"), queries, "unit cannot be empty"))
140164
}
141165

142166
var sinceTime time.Time
@@ -174,6 +198,9 @@ func newNodeLogQuery(query url.Values) (*nodeLogQuery, field.ErrorList) {
174198

175199
var tailLines int
176200
tailLinesValue := query.Get("tailLines")
201+
if len(tailLinesValue) == 0 {
202+
tailLinesValue = query.Get("tail")
203+
}
177204
if len(tailLinesValue) > 0 {
178205
tailLines, err = strconv.Atoi(tailLinesValue)
179206
if err != nil {
@@ -184,15 +211,28 @@ func newNodeLogQuery(query url.Values) (*nodeLogQuery, field.ErrorList) {
184211
}
185212

186213
pattern := query.Get("pattern")
214+
if len(pattern) == 0 {
215+
pattern = query.Get("grep")
216+
}
187217
if len(pattern) > 0 {
188218
nlq.Pattern = pattern
219+
caseSensitiveValue := query.Get("case-sensitive")
220+
if len(caseSensitiveValue) > 0 {
221+
caseSensitive, err := strconv.ParseBool(query.Get("case-sensitive"))
222+
if err != nil {
223+
allErrs = append(allErrs, field.Invalid(field.NewPath("case-sensitive"), query.Get("case-sensitive"),
224+
err.Error()))
225+
} else {
226+
nlq.CaseSensitive = caseSensitive
227+
}
228+
}
189229
}
190230

191-
if len(allErrs) > 0 {
192-
return nil, allErrs
193-
}
231+
nlq.Since = query.Get("since")
232+
nlq.Until = query.Get("until")
233+
nlq.Format = query.Get("output")
194234

195-
if reflect.DeepEqual(nlq, nodeLogQuery{}) {
235+
if len(allErrs) > 0 {
196236
return nil, allErrs
197237
}
198238

@@ -217,14 +257,13 @@ func validateServices(services []string) field.ErrorList {
217257
func (n *nodeLogQuery) validate() field.ErrorList {
218258
allErrs := validateServices(n.Services)
219259
switch {
220-
case len(n.Files) == 0 && len(n.Services) == 0:
221-
allErrs = append(allErrs, field.Required(field.NewPath("query"), "cannot be empty with options"))
260+
// OCP: Allow len(n.Files) == 0 && len(n.Services) == 0 as we want to be able to return all journal / WinEvent logs
222261
case len(n.Files) > 0 && len(n.Services) > 0:
223262
allErrs = append(allErrs, field.Invalid(field.NewPath("query"), fmt.Sprintf("%v, %v", n.Files, n.Services),
224263
"cannot specify a file and service"))
225264
case len(n.Files) > 1:
226265
allErrs = append(allErrs, field.Invalid(field.NewPath("query"), n.Files, "cannot specify more than one file"))
227-
case len(n.Files) == 1 && n.options != (options{}):
266+
case len(n.Files) == 1 && !reflect.DeepEqual(n.options, options{}):
228267
allErrs = append(allErrs, field.Invalid(field.NewPath("query"), n.Files, "cannot specify file with options"))
229268
case len(n.Files) == 1:
230269
if root, err := os.OpenRoot(nodeLogDir); err != nil {
@@ -260,6 +299,35 @@ func (n *nodeLogQuery) validate() field.ErrorList {
260299
allErrs = append(allErrs, field.Invalid(field.NewPath("pattern"), n.Pattern, err.Error()))
261300
}
262301

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

@@ -282,19 +350,20 @@ func (n *nodeLogQuery) copyForBoot(ctx context.Context, w io.Writer, previousBoo
282350
return
283351
}
284352
nativeLoggers, fileLoggers := n.splitNativeVsFileLoggers(ctx)
285-
if len(nativeLoggers) > 0 {
286-
n.copyServiceLogs(ctx, w, nativeLoggers, previousBoot)
287-
}
288353

289-
if len(fileLoggers) > 0 && n.options != (options{}) {
354+
if len(fileLoggers) > 0 && !reflect.DeepEqual(n.options, options{}) {
290355
fmt.Fprintf(w, "\noptions present and query resolved to log files for %v\ntry without specifying options\n",
291356
fileLoggers)
292357
return
293358
}
294359

295360
if len(fileLoggers) > 0 {
296361
copyFileLogs(ctx, w, fileLoggers)
362+
return
297363
}
364+
// OCP: Return all logs in the case where nativeLoggers == ""
365+
n.copyServiceLogs(ctx, w, nativeLoggers, previousBoot)
366+
298367
}
299368

300369
// splitNativeVsFileLoggers checks if each service logs to native OS logs or to a file and returns a list of services
@@ -440,3 +509,16 @@ func safeServiceName(s string) error {
440509
}
441510
return nil
442511
}
512+
513+
func validateDate(date string) error {
514+
if len(date) == 0 {
515+
return nil
516+
}
517+
if reRelativeDate.MatchString(date) {
518+
return nil
519+
}
520+
if _, err := time.Parse(dateLayout, date); err == nil {
521+
return nil
522+
}
523+
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")
524+
}

pkg/kubelet/kubelet_server_journal_linux.go

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -35,14 +35,20 @@ func getLoggingCmd(n *nodeLogQuery, services []string) (cmd string, args []strin
3535
args = []string{
3636
"--utc",
3737
"--no-pager",
38-
"--output=short-precise",
3938
}
40-
if n.SinceTime != nil {
39+
40+
if len(n.Since) > 0 {
41+
args = append(args, fmt.Sprintf("--since=%s", n.Since))
42+
} else if n.SinceTime != nil {
4143
args = append(args, fmt.Sprintf("--since=%s", n.SinceTime.Format(dateLayout)))
4244
}
43-
if n.UntilTime != nil {
44-
args = append(args, fmt.Sprintf("--until=%s", n.UntilTime.Format(dateLayout)))
45+
46+
if len(n.Until) > 0 {
47+
args = append(args, fmt.Sprintf("--until=%s", n.Until))
48+
} else if n.UntilTime != nil {
49+
args = append(args, fmt.Sprintf("--until=%s", n.SinceTime.Format(dateLayout)))
4550
}
51+
4652
if n.TailLines != nil {
4753
args = append(args, "--pager-end", fmt.Sprintf("--lines=%d", *n.TailLines))
4854
}
@@ -53,12 +59,21 @@ func getLoggingCmd(n *nodeLogQuery, services []string) (cmd string, args []strin
5359
}
5460
if len(n.Pattern) > 0 {
5561
args = append(args, "--grep="+n.Pattern)
62+
args = append(args, fmt.Sprintf("--case-sensitive=%t", n.CaseSensitive))
5663
}
5764

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

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

0 commit comments

Comments
 (0)