github.com/nathants/docker-trace@v0.0.0-20220831131939-668bc05a257b/lib/lib.go (about) 1 package lib 2 3 import ( 4 "archive/tar" 5 "bytes" 6 "context" 7 "crypto/sha256" 8 "encoding/hex" 9 "encoding/json" 10 "fmt" 11 "io" 12 "io/fs" 13 "os" 14 "os/signal" 15 "path" 16 "reflect" 17 "regexp" 18 "runtime" 19 "sort" 20 "strconv" 21 "strings" 22 "syscall" 23 "time" 24 "unicode/utf8" 25 26 "github.com/avast/retry-go" 27 "github.com/docker/docker/client" 28 "github.com/mattn/go-isatty" 29 ) 30 31 func Atoi(x string) int { 32 y, err := strconv.Atoi(x) 33 if err != nil { 34 panic(err) 35 } 36 return y 37 } 38 39 func DataDir() string { 40 dir := fmt.Sprintf("%s/.docker-trace", os.Getenv("HOME")) 41 if !Exists(dir) { 42 err := os.Mkdir(dir, os.ModePerm) 43 if err != nil { 44 panic(err) 45 } 46 } 47 return dir 48 } 49 50 var Commands = make(map[string]func()) 51 52 type ArgsStruct interface { 53 Description() string 54 } 55 56 var Args = make(map[string]ArgsStruct) 57 58 type Manifest struct { 59 Config string 60 Layers []string 61 RepoTags []string 62 } 63 64 type DockerfileHistory struct { 65 CreatedBy string `json:"created_by"` 66 } 67 68 type DockerfileConfig struct { 69 History []DockerfileHistory `json:"history"` 70 } 71 72 func SignalHandler(cancel func()) { 73 c := make(chan os.Signal, 1) 74 signal.Reset(os.Interrupt, syscall.SIGTERM) 75 signal.Notify(c, os.Interrupt, syscall.SIGTERM) 76 go func() { 77 // defer func() {}() 78 <-c 79 cancel() 80 }() 81 } 82 83 func functionName(i interface{}) string { 84 return runtime.FuncForPC(reflect.ValueOf(i).Pointer()).Name() 85 } 86 87 func DropLinesWithAny(s string, tokens ...string) string { 88 var lines []string 89 outer: 90 for _, line := range strings.Split(s, "\n") { 91 for _, token := range tokens { 92 if strings.Contains(line, token) { 93 continue outer 94 } 95 } 96 lines = append(lines, line) 97 } 98 return strings.Join(lines, "\n") 99 } 100 101 func Pformat(i interface{}) string { 102 val, err := json.MarshalIndent(i, "", " ") 103 if err != nil { 104 panic(err) 105 } 106 return string(val) 107 } 108 109 func Retry(ctx context.Context, fn func() error) error { 110 count := 0 111 attempts := 6 112 return retry.Do( 113 func() error { 114 if count != 0 { 115 Logger.Printf("retry %d/%d for %v\n", count, attempts-1, functionName(fn)) 116 } 117 count++ 118 err := fn() 119 if err != nil { 120 return err 121 } 122 return nil 123 }, 124 retry.Context(ctx), 125 retry.LastErrorOnly(true), 126 retry.Attempts(uint(attempts)), 127 retry.Delay(150*time.Millisecond), 128 ) 129 } 130 131 func Assert(cond bool, format string, a ...interface{}) { 132 if !cond { 133 panic(fmt.Sprintf(format, a...)) 134 } 135 } 136 137 func Panic1(err error) { 138 if err != nil { 139 panic(err) 140 } 141 } 142 143 func Panic2(x interface{}, e error) interface{} { 144 if e != nil { 145 Logger.Fatalf("fatal: %s\n", e) 146 } 147 return x 148 } 149 150 func Contains(parts []string, part string) bool { 151 for _, p := range parts { 152 if p == part { 153 return true 154 } 155 } 156 return false 157 } 158 159 func Chunk(xs []string, chunkSize int) [][]string { 160 var xss [][]string 161 xss = append(xss, []string{}) 162 for _, x := range xs { 163 xss[len(xss)-1] = append(xss[len(xss)-1], x) 164 if len(xss[len(xss)-1]) == chunkSize { 165 xss = append(xss, []string{}) 166 } 167 } 168 return xss 169 } 170 171 func Exists(path string) bool { 172 _, err := os.Stat(path) 173 return err == nil 174 } 175 176 func StringOr(s *string, d string) string { 177 if s == nil { 178 return d 179 } 180 return *s 181 } 182 183 func color(code int) func(string) string { 184 return func(s string) string { 185 if isatty.IsTerminal(os.Stdout.Fd()) { 186 return fmt.Sprintf("\033[%dm%s\033[0m", code, s) 187 } 188 return s 189 } 190 } 191 192 var ( 193 Red = color(31) 194 Green = color(32) 195 Yellow = color(33) 196 Blue = color(34) 197 Magenta = color(35) 198 Cyan = color(36) 199 White = color(37) 200 ) 201 202 func FindManifest(manifests []Manifest, name string) (Manifest, error) { 203 // when pulling a previously unknown image by digest, there will be only one 204 if len(manifests) == 1 { 205 return manifests[0], nil 206 } 207 for _, m := range manifests { 208 // find by imageID 209 if strings.HasPrefix(m.Config, name) { 210 return m, nil 211 } 212 // find by tag 213 if strings.Contains(name, ":") { 214 for _, tag := range m.RepoTags { 215 if tag == name { 216 return m, nil 217 } 218 } 219 } else { 220 err := fmt.Errorf("name must include a tag or be an imageID, got: %s", name) 221 Logger.Println("error:", err) 222 return Manifest{}, err 223 } 224 } 225 err := fmt.Errorf(Pformat(manifests) + "\ntag not found in manifest") 226 Logger.Println("error:", err) 227 return Manifest{}, err 228 } 229 230 func Scan(ctx context.Context, name string, tarball string, checkData bool) ([]*ScanFile, map[string]int, error) { 231 cli, err := client.NewClientWithOpts(client.FromEnv) 232 if err != nil { 233 Logger.Println("error:", err) 234 return nil, nil, err 235 } 236 var manifests []Manifest 237 var files []*ScanFile 238 var r io.ReadCloser 239 if tarball != "" { 240 r, err = os.Open(tarball) 241 if err != nil { 242 Logger.Println("error:", err) 243 return nil, nil, err 244 } 245 } else { 246 r, err = cli.ImageSave(ctx, []string{name}) 247 if err != nil { 248 Logger.Println("error:", err) 249 return nil, nil, err 250 } 251 } 252 defer func() { _ = r.Close() }() 253 tr := tar.NewReader(r) 254 for { 255 header, err := tr.Next() 256 if err == io.EOF { 257 break 258 } 259 if err != nil { 260 Logger.Println("error:", err) 261 return nil, nil, err 262 } 263 if header == nil { 264 continue 265 } 266 switch header.Typeflag { 267 case tar.TypeReg: 268 if path.Base(header.Name) == "layer.tar" { 269 layerFiles, err := ScanLayer(header.Name, tr, checkData) 270 if err != nil { 271 Logger.Println("error:", err) 272 return nil, nil, err 273 } 274 files = append(files, layerFiles...) 275 } else if header.Name == "manifest.json" { 276 var data bytes.Buffer 277 _, err := io.Copy(&data, tr) 278 if err != nil { 279 Logger.Println("error:", err) 280 return nil, nil, err 281 } 282 err = json.Unmarshal(data.Bytes(), &manifests) 283 if err != nil { 284 Logger.Println("error:", err) 285 return nil, nil, err 286 } 287 } 288 } 289 } 290 291 manifest, err := FindManifest(manifests, name) 292 if err != nil { 293 Logger.Println("error:", err) 294 return nil, nil, err 295 } 296 297 layers := make(map[string]int) 298 for i, layer := range manifest.Layers { 299 layers[layer] = i 300 } 301 302 for _, f := range files { 303 i, ok := layers[f.Layer] 304 if !ok { 305 err := fmt.Errorf("error: no layer %s", f.Layer) 306 Logger.Println("error:", err) 307 return nil, nil, err 308 } 309 f.LayerIndex = i 310 f.Layer = "" 311 } 312 313 sort.Slice(files, func(i, j int) bool { return files[i].LayerIndex < files[j].LayerIndex }) 314 sort.SliceStable(files, func(i, j int) bool { return files[i].Path < files[j].Path }) 315 316 // keep only last update to the file, not all updates across all layers 317 var result []*ScanFile 318 var last *ScanFile 319 for _, f := range files { 320 if last != nil && f.Path != last.Path { 321 result = append(result, last) 322 } 323 last = f 324 } 325 if last.Path != result[len(result)-1].Path { 326 result = append(result, last) 327 } 328 return result, layers, nil 329 } 330 331 type ScanFile struct { 332 LayerIndex int 333 Layer string 334 Path string 335 LinkTarget string 336 Mode fs.FileMode 337 Size int64 338 ModTime time.Time 339 Hash string 340 ContentType string 341 Uid int 342 Gid int 343 } 344 345 func ScanLayer(layer string, r io.Reader, checkData bool) ([]*ScanFile, error) { 346 var result []*ScanFile 347 tr := tar.NewReader(r) 348 for { 349 header, err := tr.Next() 350 if err == io.EOF { 351 break 352 } 353 if err != nil { 354 Logger.Println("error:", err) 355 return nil, err 356 } 357 if header == nil { 358 continue 359 } 360 switch header.Typeflag { 361 case tar.TypeReg: 362 var data bytes.Buffer 363 contentType := "" 364 hash := "" 365 if checkData { 366 _, err := io.Copy(&data, tr) 367 if err != nil { 368 Logger.Println("error:", err) 369 return nil, err 370 } 371 contentType = "binary" 372 if utf8.Valid(data.Bytes()) { 373 contentType = "utf8" 374 } 375 sum := sha256.Sum256(data.Bytes()) 376 hash = hex.EncodeToString(sum[:]) 377 } 378 result = append(result, &ScanFile{ 379 Layer: layer, 380 Path: "/" + header.Name, 381 Mode: header.FileInfo().Mode(), 382 Size: header.Size, 383 ModTime: header.ModTime, 384 Hash: hash, 385 ContentType: contentType, 386 Uid: header.Uid, 387 Gid: header.Gid, 388 }) 389 case tar.TypeSymlink: 390 result = append(result, &ScanFile{ 391 Layer: layer, 392 Path: "/" + header.Name, 393 Mode: header.FileInfo().Mode(), 394 ModTime: header.ModTime, 395 LinkTarget: header.Linkname, 396 Uid: header.Uid, 397 Gid: header.Gid, 398 }) 399 case tar.TypeLink: 400 result = append(result, &ScanFile{ 401 Layer: layer, 402 Path: "/" + header.Name, 403 Mode: header.FileInfo().Mode(), 404 ModTime: header.ModTime, 405 LinkTarget: "/" + header.Linkname, // todo, verify: hard links in docker are always absolute and do not include leading / 406 Uid: header.Uid, 407 Gid: header.Gid, 408 }) 409 case tar.TypeDir: 410 result = append(result, &ScanFile{ 411 Layer: layer, 412 Path: "/" + header.Name, 413 Mode: header.FileInfo().Mode(), 414 ModTime: header.ModTime, 415 Uid: header.Uid, 416 Gid: header.Gid, 417 }) 418 default: 419 fmt.Fprintln(os.Stderr, "ignoring tar entry:", Pformat(header)) 420 } 421 } 422 return result, nil 423 } 424 425 func Dockerfile(ctx context.Context, name string, tarball string) ([]string, error) { 426 cli, err := client.NewClientWithOpts(client.FromEnv) 427 if err != nil { 428 Logger.Println("error:", err) 429 return nil, err 430 } 431 var manifests []Manifest 432 configs := make(map[string]*DockerfileConfig) 433 var r io.ReadCloser 434 if tarball != "" { 435 r, err = os.Open(tarball) 436 if err != nil { 437 Logger.Println("error:", err) 438 return nil, err 439 } 440 } else { 441 r, err = cli.ImageSave(ctx, []string{name}) 442 if err != nil { 443 Logger.Println("error:", err) 444 return nil, err 445 } 446 } 447 defer func() { _ = r.Close() }() 448 tr := tar.NewReader(r) 449 for { 450 header, err := tr.Next() 451 if err == io.EOF { 452 break 453 } 454 if err != nil { 455 Logger.Println("error:", err) 456 return nil, err 457 } 458 if header == nil { 459 continue 460 } 461 switch header.Typeflag { 462 case tar.TypeReg: 463 if header.Name == "manifest.json" { 464 var data bytes.Buffer 465 _, err := io.Copy(&data, tr) 466 if err != nil { 467 Logger.Println("error:", err) 468 return nil, err 469 } 470 err = json.Unmarshal(data.Bytes(), &manifests) 471 if err != nil { 472 Logger.Println("error:", err) 473 return nil, err 474 } 475 } else if strings.HasSuffix(header.Name, ".json") { 476 var data bytes.Buffer 477 _, err := io.Copy(&data, tr) 478 if err != nil { 479 Logger.Println("error:", err) 480 return nil, err 481 } 482 config := DockerfileConfig{} 483 err = json.Unmarshal(data.Bytes(), &config) 484 if err != nil { 485 Logger.Println("error:", err) 486 return nil, err 487 } 488 configs[header.Name] = &config 489 } 490 default: 491 } 492 } 493 494 manifest, err := FindManifest(manifests, name) 495 if err != nil { 496 Logger.Println("error:", err) 497 return nil, err 498 } 499 500 config, ok := configs[manifest.Config] 501 if !ok { 502 err := fmt.Errorf("no such config: %s", manifest.Config) 503 Logger.Println("error:", err) 504 return nil, err 505 } 506 507 var result []string 508 509 for _, h := range config.History { 510 line := h.CreatedBy 511 line = last(strings.Split(line, " #(nop) ")) 512 line = strings.Split(line, " # buildkit")[0] 513 line = strings.TrimLeft(line, " ") 514 line = strings.ReplaceAll(line, `" `, `", `) 515 regex := regexp.MustCompile(`^[A-Z]`) 516 if regex.FindString(line) != "" && !strings.HasPrefix(line, "ADD ") && !strings.HasPrefix(line, "COPY ") && !strings.HasPrefix(line, "RUN ") && !strings.HasPrefix(line, "LABEL ") { 517 if strings.HasPrefix(line, "EXPOSE ") && strings.Contains(line, " map[") { 518 regex := regexp.MustCompile(`[0-9]+`) 519 ports := regex.FindAllString("EXPOSE map[8080/4545]", -1) 520 line = "EXPOSE " + strings.Join(ports, " ") 521 } 522 if strings.HasPrefix(line, "ENV ") { 523 parts := strings.SplitN(line, "=", 2) 524 line = parts[0] + `="` + parts[1] + `"` 525 } 526 result = append(result, line) 527 } 528 } 529 return result, nil 530 } 531 532 func Max(i, j int) int { 533 if i > j { 534 return i 535 } 536 return j 537 } 538 539 type File struct { 540 Syscall string 541 Cgroup string 542 Pid string 543 Ppid string 544 Comm string 545 Errno string 546 File string 547 } 548 549 func FilesParseLine(line string) File { 550 parts := strings.Split(line, "\t") 551 file := File{} 552 if len(parts) != 7 { 553 Logger.Printf("skipping bpftrace line: %s\n", line) 554 return file 555 } 556 file.Syscall = parts[0] 557 file.Cgroup = parts[1] 558 file.Pid = parts[2] 559 file.Ppid = parts[3] 560 file.Comm = parts[4] 561 file.Errno = parts[5] 562 file.File = parts[6] 563 // sometimes file paths include the fs driver paths 564 // 565 // /mnt/docker-data/overlay2/1b7b19463b59ac563677fda461918ae2faed45d86000fc68cf0eb8052687c121/merged/etc/hosts 566 // /var/lib/docker/zfs/graph/825b1c966c9421a50e0200fe3a9d7fe0beddebdd745ea2b976d4c7cf8d1b2e8e/etc/hosts 567 // 568 if strings.Contains(file.File, "/overlay2/") { 569 file.File = last(strings.Split(file.File, "/overlay2/")) 570 parts := strings.Split(file.File, "/") 571 if len(parts) > 2 { 572 file.File = "/" + strings.Join(parts[2:], "/") 573 } 574 } else if strings.Contains(file.File, "/zfs/graph/") { 575 file.File = last(strings.Split(file.File, "/zfs/graph/")) 576 parts := strings.Split(file.File, "/") 577 if len(parts) > 1 { 578 file.File = "/" + strings.Join(parts[1:], "/") 579 } 580 } 581 // 582 return file 583 } 584 585 func last(xs []string) string { 586 return xs[len(xs)-1] 587 } 588 589 func FilesHandleLine(cwds, cgroups map[string]string, line string) { 590 file := FilesParseLine(line) 591 if file.Syscall == "cgroup_mkdir" { 592 // track cgroups of docker containers as they start 593 // 594 // /sys/fs/cgroup/system.slice/docker-425428dfb2644cfd111d406b5f8f68a7596731a451f0169caa7393f3a39db9ca.scope 595 // 596 part := last(strings.Split(file.File, "/")) 597 if strings.HasPrefix(part, "docker-") { 598 cgroups[file.Cgroup] = part[7 : 64+7] 599 } 600 } else if cgroups[file.Cgroup] != "" && file.File != "" && file.Errno == "0" { 601 // pids start at cwd of parent 602 _, ok := cwds[file.Pid] 603 if !ok { 604 _, ok := cwds[file.Ppid] 605 if ok { 606 cwds[file.Pid] = cwds[file.Ppid] 607 } else { 608 cwds[file.Pid] = "/" 609 } 610 } 611 // update cwd when chdir succeeds 612 if file.Syscall == "chdir" { 613 if file.File[:1] == "/" { 614 cwds[file.Pid] = file.File 615 } else { 616 cwds[file.Pid] = path.Join(cwds[file.Pid], file.File) 617 } 618 } 619 // join any relative paths to pid cwd 620 if file.File[:1] != "/" { 621 cwd, ok := cwds[file.Pid] 622 if !ok { 623 panic(cwds) 624 } 625 file.File = path.Join(cwd, file.File) 626 } 627 // 628 // _, _ = fmt.Fprintln(os.Stderr, file.Pid, file.Ppid, fmt.Sprintf("%-40s", file.File), fmt.Sprintf("%-10s", file.Comm), file.Errno, file.Syscall) 629 fmt.Println(cgroups[file.Cgroup], file.File) 630 } 631 }