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 }