github.com/newrelic/go-agent@v3.26.0+incompatible/internal/sysinfo/docker.go (about)

     1  // Copyright 2020 New Relic Corporation. All rights reserved.
     2  // SPDX-License-Identifier: Apache-2.0
     3  
     4  package sysinfo
     5  
     6  import (
     7  	"bufio"
     8  	"bytes"
     9  	"errors"
    10  	"fmt"
    11  	"io"
    12  	"os"
    13  	"regexp"
    14  	"runtime"
    15  )
    16  
    17  var (
    18  	// ErrDockerNotFound is returned if a Docker ID is not found in
    19  	// /proc/self/cgroup
    20  	ErrDockerNotFound = errors.New("Docker ID not found")
    21  )
    22  
    23  // DockerID attempts to detect Docker.
    24  func DockerID() (string, error) {
    25  	if "linux" != runtime.GOOS {
    26  		return "", ErrFeatureUnsupported
    27  	}
    28  
    29  	f, err := os.Open("/proc/self/cgroup")
    30  	if err != nil {
    31  		return "", err
    32  	}
    33  	defer f.Close()
    34  
    35  	return parseDockerID(f)
    36  }
    37  
    38  var (
    39  	// The DockerID must be a 64-character lowercase hex string
    40  	// be greedy and match anything 64-characters or longer to spot invalid IDs
    41  	dockerIDLength   = 64
    42  	dockerIDRegexRaw = fmt.Sprintf("[0-9a-f]{%d,}", dockerIDLength)
    43  	dockerIDRegex    = regexp.MustCompile(dockerIDRegexRaw)
    44  )
    45  
    46  func parseDockerID(r io.Reader) (string, error) {
    47  	// Each line in the cgroup file consists of three colon delimited fields.
    48  	//   1. hierarchy ID  - we don't care about this
    49  	//   2. subsystems    - comma separated list of cgroup subsystem names
    50  	//   3. control group - control group to which the process belongs
    51  	//
    52  	// Example
    53  	//   5:cpuacct,cpu,cpuset:/daemons
    54  
    55  	var id string
    56  
    57  	for scanner := bufio.NewScanner(r); scanner.Scan(); {
    58  		line := scanner.Bytes()
    59  		cols := bytes.SplitN(line, []byte(":"), 3)
    60  
    61  		if len(cols) < 3 {
    62  			continue
    63  		}
    64  
    65  		//  We're only interested in the cpu subsystem.
    66  		if !isCPUCol(cols[1]) {
    67  			continue
    68  		}
    69  
    70  		id = dockerIDRegex.FindString(string(cols[2]))
    71  
    72  		if err := validateDockerID(id); err != nil {
    73  			// We can stop searching at this point, the CPU
    74  			// subsystem should only occur once, and its cgroup is
    75  			// not docker or not a format we accept.
    76  			return "", err
    77  		}
    78  		return id, nil
    79  	}
    80  
    81  	return "", ErrDockerNotFound
    82  }
    83  
    84  func isCPUCol(col []byte) bool {
    85  	// Sometimes we have multiple subsystems in one line, as in this example
    86  	// from:
    87  	// https://source.datanerd.us/newrelic/cross_agent_tests/blob/master/docker_container_id/docker-1.1.2-native-driver-systemd.txt
    88  	//
    89  	// 3:cpuacct,cpu:/system.slice/docker-67f98c9e6188f9c1818672a15dbe46237b6ee7e77f834d40d41c5fb3c2f84a2f.scope
    90  	splitCSV := func(r rune) bool { return r == ',' }
    91  	subsysCPU := []byte("cpu")
    92  
    93  	for _, subsys := range bytes.FieldsFunc(col, splitCSV) {
    94  		if bytes.Equal(subsysCPU, subsys) {
    95  			return true
    96  		}
    97  	}
    98  	return false
    99  }
   100  
   101  func isHex(r rune) bool {
   102  	return ('0' <= r && r <= '9') || ('a' <= r && r <= 'f')
   103  }
   104  
   105  func validateDockerID(id string) error {
   106  	if len(id) != 64 {
   107  		return fmt.Errorf("%s is not %d characters long", id, dockerIDLength)
   108  	}
   109  
   110  	for _, c := range id {
   111  		if !isHex(c) {
   112  			return fmt.Errorf("Character: %c is not hex in string %s", c, id)
   113  		}
   114  	}
   115  
   116  	return nil
   117  }