github.com/chanzuckerberg/terraform@v0.11.12-beta1/svchost/disco/host.go (about)

     1  package disco
     2  
     3  import (
     4  	"encoding/json"
     5  	"fmt"
     6  	"log"
     7  	"net/http"
     8  	"net/url"
     9  	"os"
    10  	"strconv"
    11  	"strings"
    12  	"time"
    13  
    14  	"github.com/hashicorp/go-version"
    15  	"github.com/hashicorp/terraform/httpclient"
    16  )
    17  
    18  const versionServiceID = "versions.v1"
    19  
    20  // Host represents a service discovered host.
    21  type Host struct {
    22  	discoURL  *url.URL
    23  	hostname  string
    24  	services  map[string]interface{}
    25  	transport http.RoundTripper
    26  }
    27  
    28  // Constraints represents the version constraints of a service.
    29  type Constraints struct {
    30  	Service   string   `json:"service"`
    31  	Product   string   `json:"product"`
    32  	Minimum   string   `json:"minimum"`
    33  	Maximum   string   `json:"maximum"`
    34  	Excluding []string `json:"excluding"`
    35  }
    36  
    37  // ErrServiceNotProvided is returned when the service is not provided.
    38  type ErrServiceNotProvided struct {
    39  	hostname string
    40  	service  string
    41  }
    42  
    43  // Error returns a customized error message.
    44  func (e *ErrServiceNotProvided) Error() string {
    45  	if e.hostname == "" {
    46  		return fmt.Sprintf("host does not provide a %s service", e.service)
    47  	}
    48  	return fmt.Sprintf("host %s does not provide a %s service", e.hostname, e.service)
    49  }
    50  
    51  // ErrVersionNotSupported is returned when the version is not supported.
    52  type ErrVersionNotSupported struct {
    53  	hostname string
    54  	service  string
    55  	version  string
    56  }
    57  
    58  // Error returns a customized error message.
    59  func (e *ErrVersionNotSupported) Error() string {
    60  	if e.hostname == "" {
    61  		return fmt.Sprintf("host does not support %s version %s", e.service, e.version)
    62  	}
    63  	return fmt.Sprintf("host %s does not support %s version %s", e.hostname, e.service, e.version)
    64  }
    65  
    66  // ErrNoVersionConstraints is returned when checkpoint was disabled
    67  // or the endpoint to query for version constraints was unavailable.
    68  type ErrNoVersionConstraints struct {
    69  	disabled bool
    70  }
    71  
    72  // Error returns a customized error message.
    73  func (e *ErrNoVersionConstraints) Error() string {
    74  	if e.disabled {
    75  		return "checkpoint disabled"
    76  	}
    77  	return "unable to contact versions service"
    78  }
    79  
    80  // ServiceURL returns the URL associated with the given service identifier,
    81  // which should be of the form "servicename.vN".
    82  //
    83  // A non-nil result is always an absolute URL with a scheme of either HTTPS
    84  // or HTTP.
    85  func (h *Host) ServiceURL(id string) (*url.URL, error) {
    86  	svc, ver, err := parseServiceID(id)
    87  	if err != nil {
    88  		return nil, err
    89  	}
    90  
    91  	// No services supported for an empty Host.
    92  	if h == nil || h.services == nil {
    93  		return nil, &ErrServiceNotProvided{service: svc}
    94  	}
    95  
    96  	urlStr, ok := h.services[id].(string)
    97  	if !ok {
    98  		// See if we have a matching service as that would indicate
    99  		// the service is supported, but not the requested version.
   100  		for serviceID := range h.services {
   101  			if strings.HasPrefix(serviceID, svc+".") {
   102  				return nil, &ErrVersionNotSupported{
   103  					hostname: h.hostname,
   104  					service:  svc,
   105  					version:  ver.Original(),
   106  				}
   107  			}
   108  		}
   109  
   110  		// No discovered services match the requested service.
   111  		return nil, &ErrServiceNotProvided{hostname: h.hostname, service: svc}
   112  	}
   113  
   114  	u, err := url.Parse(urlStr)
   115  	if err != nil {
   116  		return nil, fmt.Errorf("Failed to parse service URL: %v", err)
   117  	}
   118  
   119  	// Make relative URLs absolute using our discovery URL.
   120  	if !u.IsAbs() {
   121  		u = h.discoURL.ResolveReference(u)
   122  	}
   123  
   124  	if u.Scheme != "https" && u.Scheme != "http" {
   125  		return nil, fmt.Errorf("Service URL is using an unsupported scheme: %s", u.Scheme)
   126  	}
   127  	if u.User != nil {
   128  		return nil, fmt.Errorf("Embedded username/password information is not permitted")
   129  	}
   130  
   131  	// Fragment part is irrelevant, since we're not a browser.
   132  	u.Fragment = ""
   133  
   134  	return h.discoURL.ResolveReference(u), nil
   135  }
   136  
   137  // VersionConstraints returns the contraints for a given service identifier
   138  // (which should be of the form "servicename.vN") and product.
   139  //
   140  // When an exact (service and version) match is found, the constraints for
   141  // that service are returned.
   142  //
   143  // When the requested version is not provided but the service is, we will
   144  // search for all alternative versions. If mutliple alternative versions
   145  // are found, the contrains of the latest available version are returned.
   146  //
   147  // When a service is not provided at all an error will be returned instead.
   148  //
   149  // When checkpoint is disabled or when a 404 is returned after making the
   150  // HTTP call, an ErrNoVersionConstraints error will be returned.
   151  func (h *Host) VersionConstraints(id, product string) (*Constraints, error) {
   152  	svc, _, err := parseServiceID(id)
   153  	if err != nil {
   154  		return nil, err
   155  	}
   156  
   157  	// Return early if checkpoint is disabled.
   158  	if disabled := os.Getenv("CHECKPOINT_DISABLE"); disabled != "" {
   159  		return nil, &ErrNoVersionConstraints{disabled: true}
   160  	}
   161  
   162  	// No services supported for an empty Host.
   163  	if h == nil || h.services == nil {
   164  		return nil, &ErrServiceNotProvided{service: svc}
   165  	}
   166  
   167  	// Try to get the service URL for the version service and
   168  	// return early if the service isn't provided by the host.
   169  	u, err := h.ServiceURL(versionServiceID)
   170  	if err != nil {
   171  		return nil, err
   172  	}
   173  
   174  	// Check if we have an exact (service and version) match.
   175  	if _, ok := h.services[id].(string); !ok {
   176  		// If we don't have an exact match, we search for all matching
   177  		// services and then use the service ID of the latest version.
   178  		var services []string
   179  		for serviceID := range h.services {
   180  			if strings.HasPrefix(serviceID, svc+".") {
   181  				services = append(services, serviceID)
   182  			}
   183  		}
   184  
   185  		if len(services) == 0 {
   186  			// No discovered services match the requested service.
   187  			return nil, &ErrServiceNotProvided{hostname: h.hostname, service: svc}
   188  		}
   189  
   190  		// Set id to the latest service ID we found.
   191  		var latest *version.Version
   192  		for _, serviceID := range services {
   193  			if _, ver, err := parseServiceID(serviceID); err == nil {
   194  				if latest == nil || latest.LessThan(ver) {
   195  					id = serviceID
   196  					latest = ver
   197  				}
   198  			}
   199  		}
   200  	}
   201  
   202  	// Set a default timeout of 1 sec for the versions request (in milliseconds)
   203  	timeout := 1000
   204  	if _, err := strconv.Atoi(os.Getenv("CHECKPOINT_TIMEOUT")); err == nil {
   205  		timeout, _ = strconv.Atoi(os.Getenv("CHECKPOINT_TIMEOUT"))
   206  	}
   207  
   208  	client := &http.Client{
   209  		Transport: h.transport,
   210  		Timeout:   time.Duration(timeout) * time.Millisecond,
   211  	}
   212  
   213  	// Prepare the service URL by setting the service and product.
   214  	v := u.Query()
   215  	v.Set("product", product)
   216  	u.Path += id
   217  	u.RawQuery = v.Encode()
   218  
   219  	// Create a new request.
   220  	req, err := http.NewRequest("GET", u.String(), nil)
   221  	if err != nil {
   222  		return nil, fmt.Errorf("Failed to create version constraints request: %v", err)
   223  	}
   224  	req.Header.Set("Accept", "application/json")
   225  	req.Header.Set("User-Agent", httpclient.UserAgentString())
   226  
   227  	log.Printf("[DEBUG] Retrieve version constraints for service %s and product %s", id, product)
   228  
   229  	resp, err := client.Do(req)
   230  	if err != nil {
   231  		return nil, fmt.Errorf("Failed to request version constraints: %v", err)
   232  	}
   233  	defer resp.Body.Close()
   234  
   235  	if resp.StatusCode == 404 {
   236  		return nil, &ErrNoVersionConstraints{disabled: false}
   237  	}
   238  
   239  	if resp.StatusCode != 200 {
   240  		return nil, fmt.Errorf("Failed to request version constraints: %s", resp.Status)
   241  	}
   242  
   243  	// Parse the constraints from the response body.
   244  	result := &Constraints{}
   245  	if err := json.NewDecoder(resp.Body).Decode(result); err != nil {
   246  		return nil, fmt.Errorf("Error parsing version constraints: %v", err)
   247  	}
   248  
   249  	return result, nil
   250  }
   251  
   252  func parseServiceID(id string) (string, *version.Version, error) {
   253  	parts := strings.SplitN(id, ".", 2)
   254  	if len(parts) != 2 {
   255  		return "", nil, fmt.Errorf("Invalid service ID format (i.e. service.vN): %s", id)
   256  	}
   257  
   258  	version, err := version.NewVersion(parts[1])
   259  	if err != nil {
   260  		return "", nil, fmt.Errorf("Invalid service version: %v", err)
   261  	}
   262  
   263  	return parts[0], version, nil
   264  }