github.com/anth0d/nomad@v0.0.0-20221214183521-ae3a0a2cad06/client/fingerprint/env_digitalocean.go (about)

     1  package fingerprint
     2  
     3  import (
     4  	"fmt"
     5  	"io/ioutil"
     6  	"net/http"
     7  	"net/url"
     8  	"os"
     9  	"strings"
    10  	"time"
    11  
    12  	cleanhttp "github.com/hashicorp/go-cleanhttp"
    13  	log "github.com/hashicorp/go-hclog"
    14  
    15  	"github.com/hashicorp/nomad/helper/useragent"
    16  	"github.com/hashicorp/nomad/nomad/structs"
    17  )
    18  
    19  const (
    20  	// DigitalOceanMetadataURL is where the DigitalOcean metadata api normally resides.
    21  	// https://docs.digitalocean.com/products/droplets/how-to/retrieve-droplet-metadata/#how-to-retrieve-droplet-metadata
    22  	DigitalOceanMetadataURL = "http://169.254.169.254/metadata/v1/"
    23  
    24  	// DigitalOceanMetadataTimeout is the timeout used when contacting the DigitalOcean metadata
    25  	// services.
    26  	DigitalOceanMetadataTimeout = 2 * time.Second
    27  )
    28  
    29  type DigitalOceanMetadataPair struct {
    30  	path   string
    31  	unique bool
    32  }
    33  
    34  // EnvDigitalOceanFingerprint is used to fingerprint DigitalOcean metadata
    35  type EnvDigitalOceanFingerprint struct {
    36  	StaticFingerprinter
    37  	client      *http.Client
    38  	logger      log.Logger
    39  	metadataURL string
    40  }
    41  
    42  // NewEnvDigitalOceanFingerprint is used to create a fingerprint from DigitalOcean metadata
    43  func NewEnvDigitalOceanFingerprint(logger log.Logger) Fingerprint {
    44  	// Read the internal metadata URL from the environment, allowing test files to
    45  	// provide their own
    46  	metadataURL := os.Getenv("DO_ENV_URL")
    47  	if metadataURL == "" {
    48  		metadataURL = DigitalOceanMetadataURL
    49  	}
    50  
    51  	// assume 2 seconds is enough time for inside DigitalOcean network
    52  	client := &http.Client{
    53  		Timeout:   DigitalOceanMetadataTimeout,
    54  		Transport: cleanhttp.DefaultTransport(),
    55  	}
    56  
    57  	return &EnvDigitalOceanFingerprint{
    58  		client:      client,
    59  		logger:      logger.Named("env_digitalocean"),
    60  		metadataURL: metadataURL,
    61  	}
    62  }
    63  
    64  func (f *EnvDigitalOceanFingerprint) Get(attribute string, format string) (string, error) {
    65  	reqURL := f.metadataURL + attribute
    66  	parsedURL, err := url.Parse(reqURL)
    67  	if err != nil {
    68  		return "", err
    69  	}
    70  
    71  	req := &http.Request{
    72  		Method: http.MethodGet,
    73  		URL:    parsedURL,
    74  		Header: http.Header{
    75  			"User-Agent": []string{useragent.String()},
    76  		},
    77  	}
    78  
    79  	res, err := f.client.Do(req)
    80  	if err != nil {
    81  		f.logger.Debug("failed to request metadata", "attribute", attribute, "error", err)
    82  		return "", err
    83  	}
    84  
    85  	body, err := ioutil.ReadAll(res.Body)
    86  	res.Body.Close()
    87  	if err != nil {
    88  		f.logger.Error("failed to read metadata", "attribute", attribute, "error", err, "resp_code", res.StatusCode)
    89  		return "", err
    90  	}
    91  
    92  	if res.StatusCode != http.StatusOK {
    93  		f.logger.Debug("could not read value for attribute", "attribute", attribute, "resp_code", res.StatusCode)
    94  		return "", fmt.Errorf("error reading attribute %s. digitalocean metadata api returned an error: resp_code: %d, resp_body: %s", attribute, res.StatusCode, body)
    95  	}
    96  
    97  	return string(body), nil
    98  }
    99  
   100  func (f *EnvDigitalOceanFingerprint) Fingerprint(request *FingerprintRequest, response *FingerprintResponse) error {
   101  	cfg := request.Config
   102  
   103  	// Check if we should tighten the timeout
   104  	if cfg.ReadBoolDefault(TightenNetworkTimeoutsConfig, false) {
   105  		f.client.Timeout = 1 * time.Millisecond
   106  	}
   107  
   108  	if !f.isDigitalOcean() {
   109  		return nil
   110  	}
   111  
   112  	// Keys and whether they should be namespaced as unique. Any key whose value
   113  	// uniquely identifies a node, such as ip, should be marked as unique. When
   114  	// marked as unique, the key isn't included in the computed node class.
   115  	keys := map[string]DigitalOceanMetadataPair{
   116  		"id":           {unique: true, path: "id"},
   117  		"hostname":     {unique: true, path: "hostname"},
   118  		"region":       {unique: false, path: "region"},
   119  		"private-ipv4": {unique: true, path: "interfaces/private/0/ipv4/address"},
   120  		"public-ipv4":  {unique: true, path: "interfaces/public/0/ipv4/address"},
   121  		"private-ipv6": {unique: true, path: "interfaces/private/0/ipv6/address"},
   122  		"public-ipv6":  {unique: true, path: "interfaces/public/0/ipv6/address"},
   123  		"mac":          {unique: true, path: "interfaces/public/0/mac"},
   124  	}
   125  
   126  	for k, attr := range keys {
   127  		resp, err := f.Get(attr.path, "text")
   128  		v := strings.TrimSpace(resp)
   129  		if err != nil {
   130  			f.logger.Warn("failed to read attribute", "attribute", k, "error", err)
   131  			continue
   132  		} else if v == "" {
   133  			f.logger.Debug("read an empty value", "attribute", k)
   134  			continue
   135  		}
   136  
   137  		// assume we want blank entries
   138  		key := "platform.digitalocean." + strings.ReplaceAll(k, "/", ".")
   139  		if attr.unique {
   140  			key = structs.UniqueNamespace(key)
   141  		}
   142  		response.AddAttribute(key, v)
   143  	}
   144  
   145  	// copy over network specific information
   146  	if val, ok := response.Attributes["unique.platform.digitalocean.local-ipv4"]; ok && val != "" {
   147  		response.AddAttribute("unique.network.ip-address", val)
   148  	}
   149  
   150  	// populate Links
   151  	if id, ok := response.Attributes["unique.platform.digitalocean.id"]; ok {
   152  		response.AddLink("digitalocean", id)
   153  	}
   154  
   155  	response.Detected = true
   156  	return nil
   157  }
   158  
   159  func (f *EnvDigitalOceanFingerprint) isDigitalOcean() bool {
   160  	v, err := f.Get("region", "text")
   161  	v = strings.TrimSpace(v)
   162  	return err == nil && v != ""
   163  }