github.com/m3db/m3@v1.5.0/src/x/config/hostid/hostid.go (about) 1 // Copyright (c) 2017 Uber Technologies, Inc. 2 // 3 // Permission is hereby granted, free of charge, to any person obtaining a copy 4 // of this software and associated documentation files (the "Software"), to deal 5 // in the Software without restriction, including without limitation the rights 6 // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 // copies of the Software, and to permit persons to whom the Software is 8 // furnished to do so, subject to the following conditions: 9 // 10 // The above copyright notice and this permission notice shall be included in 11 // all copies or substantial portions of the Software. 12 // 13 // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 // THE SOFTWARE. 20 21 // Package hostid provides a configuration struct for resolving 22 // a host ID from YAML. 23 package hostid 24 25 import ( 26 "bytes" 27 "errors" 28 "fmt" 29 "io/ioutil" 30 "os" 31 "strings" 32 "text/template" 33 "time" 34 ) 35 36 const ( 37 defaultFileCheckInterval = time.Second 38 ) 39 40 var ( 41 errHostIDFileEmpty = errors.New("host ID file is empty") 42 ) 43 44 // Resolver is a type of host ID resolver 45 type Resolver string 46 47 const ( 48 // HostnameResolver resolves host using the hostname returned by OS 49 HostnameResolver Resolver = "hostname" 50 // ConfigResolver resolves host using a value provided in config 51 ConfigResolver Resolver = "config" 52 // EnvironmentResolver resolves host using an environment variable 53 // of which the name is provided in config 54 EnvironmentResolver Resolver = "environment" 55 // FileResolver reads its identity from a non-empty file. 56 FileResolver Resolver = "file" 57 ) 58 59 // IDResolver represents a method of resolving host identity. 60 type IDResolver interface { 61 ID() (string, error) 62 } 63 64 // Configuration is the configuration for resolving a host ID. 65 type Configuration struct { 66 // Resolver is the resolver for the host ID. 67 Resolver Resolver `yaml:"resolver"` 68 69 // Value is the config specified host ID if using config host ID resolver. 70 Value *string `yaml:"value"` 71 72 // EnvVarName is the environment specified host ID if using environment host ID resolver. 73 EnvVarName *string `yaml:"envVarName"` 74 75 // File is the file config. 76 File *FileConfig `yaml:"file"` 77 78 // Hostname is the hostname config. 79 Hostname *HostnameConfig `yaml:"hostname"` 80 } 81 82 // FileConfig contains the info needed to construct a FileResolver. 83 type FileConfig struct { 84 // Path of the file containing the host ID. 85 Path string `yaml:"path"` 86 87 // Timeout to wait for the file to be non-empty. 88 Timeout *time.Duration `yaml:"timeout"` 89 } 90 91 // HostnameConfig contains the info needed to construct a HostnameConfig. 92 type HostnameConfig struct { 93 // Format is a custom format to use, using go templates. 94 // i.e. Using the M3DB operator if using no ID based on the disk 95 // you can format it how the operator would expect: 96 // '{"name":"{{.Hostname}}"}' 97 Format string `yaml:"format"` 98 } 99 100 func (c Configuration) resolver() (IDResolver, error) { 101 switch c.Resolver { 102 case HostnameResolver: 103 var format string 104 if c.Hostname != nil { 105 format = c.Hostname.Format 106 } 107 return hostnameResolver{format: format}, nil 108 case ConfigResolver: 109 return &configResolver{value: c.Value}, nil 110 case EnvironmentResolver: 111 return &environmentResolver{envVarName: c.EnvVarName}, nil 112 case FileResolver: 113 if c.File == nil { 114 return nil, errors.New("file resolver requires config, cannot be nil") 115 } 116 return &file{ 117 path: c.File.Path, 118 timeout: c.File.Timeout, 119 }, nil 120 } 121 return nil, fmt.Errorf("unknown host ID resolver: resolver=%s", 122 string(c.Resolver)) 123 } 124 125 // Resolve returns the resolved host ID given the configuration. 126 func (c Configuration) Resolve() (string, error) { 127 r, err := c.resolver() 128 if err != nil { 129 return "", err 130 } 131 return r.ID() 132 } 133 134 type hostnameResolver struct { 135 format string 136 } 137 138 func (h hostnameResolver) ID() (string, error) { 139 v, err := os.Hostname() 140 if err != nil { 141 return "", err 142 } 143 144 if h.format == "" { 145 return v, nil 146 } 147 148 tmpl, err := template.New("format").Parse(h.format) 149 if err != nil { 150 return "", fmt.Errorf("problem parsing host resolver template: %v", err) 151 } 152 153 buff := bytes.NewBuffer(nil) 154 if err := tmpl.Execute(buff, struct { 155 Hostname string 156 }{ 157 Hostname: v, 158 }); err != nil { 159 return "", fmt.Errorf("problem executing host resolver template: %v", err) 160 } 161 162 return buff.String(), nil 163 } 164 165 type configResolver struct { 166 value *string 167 } 168 169 func (c *configResolver) ID() (string, error) { 170 if c.value == nil { 171 err := fmt.Errorf("missing host ID using: resolver=%s", string(ConfigResolver)) 172 return "", err 173 } 174 return *c.value, nil 175 } 176 177 type environmentResolver struct { 178 envVarName *string 179 } 180 181 func (c *environmentResolver) ID() (string, error) { 182 if c.envVarName == nil { 183 err := fmt.Errorf("missing host ID env var name using: resolver=%s", 184 string(EnvironmentResolver)) 185 return "", err 186 } 187 v := os.Getenv(*c.envVarName) 188 if v == "" { 189 err := fmt.Errorf("missing host ID env var value using: resolver=%s, name=%s", 190 string(EnvironmentResolver), *c.envVarName) 191 return "", err 192 } 193 return v, nil 194 } 195 196 type file struct { 197 path string 198 interval time.Duration 199 timeout *time.Duration 200 } 201 202 // ID attempts to parse an ID from a file. It will optionally wait a timeout to 203 // find the value, as in some environments the file may be dynamically generated 204 // from external metadata and not immediately available when the instance starts 205 // up. 206 func (c *file) ID() (string, error) { 207 checkF := func() (string, error) { 208 f, err := os.Open(c.path) 209 if err != nil { 210 return "", err 211 } 212 213 data, err := ioutil.ReadAll(f) 214 if err != nil { 215 return "", err 216 } 217 218 val := strings.TrimSpace(string(data)) 219 if len(val) == 0 { 220 return "", errHostIDFileEmpty 221 } 222 223 return val, nil 224 } 225 226 if c.timeout == nil { 227 return checkF() 228 } 229 230 interval := c.interval 231 if interval == 0 { 232 interval = defaultFileCheckInterval 233 } 234 235 startT := time.Now() 236 for time.Since(startT) < *c.timeout { 237 v, err := checkF() 238 if err == nil { 239 return v, nil 240 } 241 242 time.Sleep(interval) 243 } 244 245 return "", fmt.Errorf("did not find value in %s within %s", c.path, c.timeout) 246 }