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  }