1
16
17 package kubelet
18
19 import (
20 "compress/gzip"
21 "context"
22 "errors"
23 "fmt"
24 "io"
25 "net/http"
26 "net/url"
27 "os"
28 "os/exec"
29 "reflect"
30 "regexp"
31 "regexp/syntax"
32 "runtime"
33 "strconv"
34 "strings"
35 "time"
36
37 securejoin "github.com/cyphar/filepath-securejoin"
38
39 utilvalidation "k8s.io/apimachinery/pkg/util/validation"
40 "k8s.io/apimachinery/pkg/util/validation/field"
41 )
42
43 const (
44 dateLayout = "2006-1-2 15:4:5"
45 maxTailLines = 100000
46 maxServiceLength = 256
47 maxServices = 4
48 nodeLogDir = "/var/log/"
49 )
50
51 var (
52 journal = journalServer{}
53
54
55
56 reServiceNameUnsafeCharacters = regexp.MustCompile(`[^a-zA-Z\-_.:0-9@]+`)
57 )
58
59
60
61
62 type journalServer struct{}
63
64
65
66
67 func (journalServer) ServeHTTP(w http.ResponseWriter, req *http.Request) {
68 var out io.Writer = w
69
70 nlq, errs := newNodeLogQuery(req.URL.Query())
71 if len(errs) > 0 {
72 http.Error(w, errs.ToAggregate().Error(), http.StatusBadRequest)
73 return
74 }
75
76
77
78 w.Header().Set("Content-Type", "text/plain;charset=UTF-8")
79 if req.Header.Get("Accept-Encoding") == "gzip" {
80 w.Header().Set("Content-Encoding", "gzip")
81
82 gz, err := gzip.NewWriterLevel(out, gzip.BestSpeed)
83 if err != nil {
84 fmt.Fprintf(w, "\nfailed to get gzip writer: %v\n", err)
85 return
86 }
87 defer gz.Close()
88 out = gz
89 }
90 nlq.Copy(out)
91 }
92
93
94 type nodeLogQuery struct {
95
96 Services []string
97
98 Files []string
99 options
100 }
101
102
103 type options struct {
104
105 SinceTime *time.Time
106
107 UntilTime *time.Time
108
109
110 TailLines *int
111
112
113 Boot *int
114
115
116 Pattern string
117 }
118
119
120 func newNodeLogQuery(query url.Values) (*nodeLogQuery, field.ErrorList) {
121 allErrs := field.ErrorList{}
122 var nlq nodeLogQuery
123 var err error
124
125 queries, ok := query["query"]
126 if len(queries) > 0 {
127 for _, q := range queries {
128
129
130 if strings.ContainsAny(q, `/\`) {
131 nlq.Files = append(nlq.Files, q)
132 } else if strings.TrimSpace(q) != "" {
133 nlq.Services = append(nlq.Services, q)
134 }
135 }
136 }
137
138
139
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"))
142 }
143
144 var sinceTime time.Time
145 sinceTimeValue := query.Get("sinceTime")
146 if len(sinceTimeValue) > 0 {
147 sinceTime, err = time.Parse(time.RFC3339, sinceTimeValue)
148 if err != nil {
149 allErrs = append(allErrs, field.Invalid(field.NewPath("sinceTime"), sinceTimeValue, "invalid time format"))
150 } else {
151 nlq.SinceTime = &sinceTime
152 }
153 }
154
155 var untilTime time.Time
156 untilTimeValue := query.Get("untilTime")
157 if len(untilTimeValue) > 0 {
158 untilTime, err = time.Parse(time.RFC3339, untilTimeValue)
159 if err != nil {
160 allErrs = append(allErrs, field.Invalid(field.NewPath("untilTime"), untilTimeValue, "invalid time format"))
161 } else {
162 nlq.UntilTime = &untilTime
163 }
164 }
165
166 var boot int
167 bootValue := query.Get("boot")
168 if len(bootValue) > 0 {
169 boot, err = strconv.Atoi(bootValue)
170 if err != nil {
171 allErrs = append(allErrs, field.Invalid(field.NewPath("boot"), bootValue, err.Error()))
172 } else {
173 nlq.Boot = &boot
174 }
175 }
176
177 var tailLines int
178 tailLinesValue := query.Get("tailLines")
179 if len(tailLinesValue) > 0 {
180 tailLines, err = strconv.Atoi(tailLinesValue)
181 if err != nil {
182 allErrs = append(allErrs, field.Invalid(field.NewPath("tailLines"), tailLinesValue, err.Error()))
183 } else {
184 nlq.TailLines = &tailLines
185 }
186 }
187
188 pattern := query.Get("pattern")
189 if len(pattern) > 0 {
190 nlq.Pattern = pattern
191 }
192
193 if len(allErrs) > 0 {
194 return nil, allErrs
195 }
196
197 if reflect.DeepEqual(nlq, nodeLogQuery{}) {
198 return nil, allErrs
199 }
200
201 return &nlq, allErrs
202 }
203
204 func validateServices(services []string) field.ErrorList {
205 allErrs := field.ErrorList{}
206
207 for _, s := range services {
208 if err := safeServiceName(s); err != nil {
209 allErrs = append(allErrs, field.Invalid(field.NewPath("query"), s, err.Error()))
210 }
211 }
212
213 if len(services) > maxServices {
214 allErrs = append(allErrs, field.TooMany(field.NewPath("query"), len(services), maxServices))
215 }
216 return allErrs
217 }
218
219 func (n *nodeLogQuery) validate() field.ErrorList {
220 allErrs := validateServices(n.Services)
221 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"))
224 case len(n.Files) > 0 && len(n.Services) > 0:
225 allErrs = append(allErrs, field.Invalid(field.NewPath("query"), fmt.Sprintf("%v, %v", n.Files, n.Services),
226 "cannot specify a file and service"))
227 case len(n.Files) > 1:
228 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{}):
230 allErrs = append(allErrs, field.Invalid(field.NewPath("query"), n.Files, "cannot specify file with options"))
231 case len(n.Files) == 1:
232 if fullLogFilename, err := securejoin.SecureJoin(nodeLogDir, n.Files[0]); err != nil {
233 allErrs = append(allErrs, field.Invalid(field.NewPath("query"), n.Files, err.Error()))
234 } else if _, err := os.Stat(fullLogFilename); err != nil {
235 allErrs = append(allErrs, field.Invalid(field.NewPath("query"), n.Files, err.Error()))
236 }
237 }
238
239 if n.SinceTime != nil && n.UntilTime != nil && (n.SinceTime.After(*n.UntilTime)) {
240 allErrs = append(allErrs, field.Invalid(field.NewPath("untilTime"), n.UntilTime, "must be after `sinceTime`"))
241 }
242
243 if n.Boot != nil && runtime.GOOS == "windows" {
244 allErrs = append(allErrs, field.Invalid(field.NewPath("boot"), *n.Boot, "boot is not supported on Windows"))
245 }
246
247 if n.Boot != nil && *n.Boot > 0 {
248 allErrs = append(allErrs, field.Invalid(field.NewPath("boot"), *n.Boot, "must be less than 1"))
249 }
250
251 if n.TailLines != nil {
252 if err := utilvalidation.IsInRange((int)(*n.TailLines), 0, maxTailLines); err != nil {
253 allErrs = append(allErrs, field.Invalid(field.NewPath("tailLines"), *n.TailLines, err[0]))
254 }
255 }
256
257 if _, err := syntax.Parse(n.Pattern, syntax.Perl); err != nil {
258 allErrs = append(allErrs, field.Invalid(field.NewPath("pattern"), n.Pattern, err.Error()))
259 }
260
261 return allErrs
262 }
263
264
265
266 func (n *nodeLogQuery) Copy(w io.Writer) {
267
268 ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(30*time.Second))
269 defer cancel()
270 boot := 0
271 if n.Boot != nil {
272 boot = *n.Boot
273 }
274 n.copyForBoot(ctx, w, boot)
275 }
276
277
278 func (n *nodeLogQuery) copyForBoot(ctx context.Context, w io.Writer, previousBoot int) {
279 if ctx.Err() != nil {
280 return
281 }
282 nativeLoggers, fileLoggers := n.splitNativeVsFileLoggers(ctx)
283 if len(nativeLoggers) > 0 {
284 n.copyServiceLogs(ctx, w, nativeLoggers, previousBoot)
285 }
286
287 if len(fileLoggers) > 0 && n.options != (options{}) {
288 fmt.Fprintf(w, "\noptions present and query resolved to log files for %v\ntry without specifying options\n",
289 fileLoggers)
290 return
291 }
292
293 if len(fileLoggers) > 0 {
294 copyFileLogs(ctx, w, fileLoggers)
295 }
296 }
297
298
299
300 func (n *nodeLogQuery) splitNativeVsFileLoggers(ctx context.Context) ([]string, []string) {
301 var nativeLoggers []string
302 var fileLoggers []string
303
304 for _, service := range n.Services {
305
306
307 if checkForNativeLogger(ctx, service) {
308 nativeLoggers = append(nativeLoggers, service)
309 } else {
310 fileLoggers = append(fileLoggers, service)
311 }
312 }
313 return nativeLoggers, fileLoggers
314 }
315
316
317
318 func (n *nodeLogQuery) copyServiceLogs(ctx context.Context, w io.Writer, services []string, previousBoot int) {
319 cmdStr, args, err := getLoggingCmd(n, services)
320 if err != nil {
321 fmt.Fprintf(w, "\nfailed to get logging cmd: %v\n", err)
322 return
323 }
324 cmd := exec.CommandContext(ctx, cmdStr, args...)
325 cmd.Stdout = w
326 cmd.Stderr = w
327
328 if err := cmd.Run(); err != nil {
329 if _, ok := err.(*exec.ExitError); ok {
330 return
331 }
332 if previousBoot == 0 {
333 fmt.Fprintf(w, "\nerror: journal output not available\n")
334 }
335 }
336 }
337
338
339 func copyFileLogs(ctx context.Context, w io.Writer, services []string) {
340 if ctx.Err() != nil {
341 fmt.Fprintf(w, "\ncontext error: %v\n", ctx.Err())
342 return
343 }
344
345 for _, service := range services {
346 heuristicsCopyFileLogs(ctx, w, service)
347 }
348 }
349
350
351
352
353
354
355 func heuristicsCopyFileLogs(ctx context.Context, w io.Writer, service string) {
356 logFileNames := [3]string{
357 service,
358 fmt.Sprintf("%s.log", service),
359 fmt.Sprintf("%s/%s.log", service, service),
360 }
361
362 var err error
363 for _, logFileName := range logFileNames {
364 var logFile string
365 logFile, err = securejoin.SecureJoin(nodeLogDir, logFileName)
366 if err != nil {
367 break
368 }
369 err = heuristicsCopyFileLog(ctx, w, logFile)
370 if err == nil {
371 break
372 } else if errors.Is(err, os.ErrNotExist) {
373 continue
374 } else {
375 break
376 }
377 }
378
379 if err != nil {
380
381 if errors.Is(err, os.ErrNotExist) {
382 fmt.Fprintf(w, "\nlog not found for %s\n", service)
383 return
384 }
385 fmt.Fprintf(w, "\nerror getting log for %s: %v\n", service, err)
386 }
387 }
388
389
390 type readerCtx struct {
391 ctx context.Context
392 io.Reader
393 }
394
395 func (r *readerCtx) Read(p []byte) (n int, err error) {
396 if err := r.ctx.Err(); err != nil {
397 return 0, err
398 }
399 return r.Reader.Read(p)
400 }
401
402
403 func newReaderCtx(ctx context.Context, r io.Reader) io.Reader {
404 return &readerCtx{
405 ctx: ctx,
406 Reader: r,
407 }
408 }
409
410
411 func heuristicsCopyFileLog(ctx context.Context, w io.Writer, logFile string) error {
412 fInfo, err := os.Stat(logFile)
413 if err != nil {
414 return err
415 }
416
417
418 if fInfo.IsDir() {
419 return os.ErrNotExist
420 }
421
422 f, err := os.Open(logFile)
423 if err != nil {
424 return err
425 }
426 defer f.Close()
427
428 if _, err := io.Copy(w, newReaderCtx(ctx, f)); err != nil {
429 return err
430 }
431 return nil
432 }
433
434 func safeServiceName(s string) error {
435
436 if len(s) > maxServiceLength {
437 return fmt.Errorf("length must be less than 100")
438 }
439
440 if reServiceNameUnsafeCharacters.MatchString(s) {
441 return fmt.Errorf("input contains unsupported characters")
442 }
443 return nil
444 }
445
View as plain text