github.com/m3db/m3@v1.5.1-0.20231129193456-75a402aa583b/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  }