func (h *Host) VersionConstraints(id, product string) (*Constraints, error) {
	svc, _, err := parseServiceID(id)
	if err != nil {
		return nil, err
	}
	if disabled := os.Getenv("CHECKPOINT_DISABLE"); disabled != "" {
		return nil, &ErrNoVersionConstraints{disabled: true}
	}
	if h == nil || h.services == nil {
		return nil, &ErrServiceNotProvided{service: svc}
	}
	u, err := h.ServiceURL(versionServiceID)
	if err != nil {
		return nil, err
	}
	if _, ok := h.services[id].(string); !ok {
		var services []string
		for serviceID := range h.services {
			if strings.HasPrefix(serviceID, svc+".") {
				services = append(services, serviceID)
			}
		}
		if len(services) == 0 {
			return nil, &ErrServiceNotProvided{hostname: h.hostname, service: svc}
		}
		var latest *version.Version
		for _, serviceID := range services {
			if _, ver, err := parseServiceID(serviceID); err == nil {
				if latest == nil || latest.LessThan(ver) {
					id = serviceID
					latest = ver
				}
			}
		}
	}
	timeout := 1000
	if v, err := strconv.Atoi(os.Getenv("CHECKPOINT_TIMEOUT")); err == nil {
		timeout = v
	}
	client := &http.Client{
		Transport: h.transport,
		Timeout:   time.Duration(timeout) * time.Millisecond,
	}
	v := u.Query()
	v.Set("product", product)
	u.Path += id
	u.RawQuery = v.Encode()
	req, err := http.NewRequest("GET", u.String(), nil)
	if err != nil {
		return nil, fmt.Errorf("Failed to create version constraints request: %v", err)
	}
	req.Header.Set("Accept", "application/json")
	log.Printf("[DEBUG] Retrieve version constraints for service %s and product %s", id, product)
	resp, err := client.Do(req)
	if err != nil {
		return nil, fmt.Errorf("Failed to request version constraints: %v", err)
	}
	defer resp.Body.Close()
	if resp.StatusCode == 404 {
		return nil, &ErrNoVersionConstraints{disabled: false}
	}
	if resp.StatusCode != 200 {
		return nil, fmt.Errorf("Failed to request version constraints: %s", resp.Status)
	}
	result := &Constraints{}
	if err := json.NewDecoder(resp.Body).Decode(result); err != nil {
		return nil, fmt.Errorf("Error parsing version constraints: %v", err)
	}
	return result, nil
}
VersionConstraints returns the contraints for a given service identifier
(which should be of the form "servicename.vN") and product.

When an exact (service and version) match is found, the constraints for
that service are returned.

When the requested version is not provided but the service is, we will
search for all alternative versions. If mutliple alternative versions
are found, the contrains of the latest available version are returned.

When a service is not provided at all an error will be returned instead.

When checkpoint is disabled or when a 404 is returned after making the
HTTP call, an ErrNoVersionConstraints error will be returned.
package disco
import (
	"encoding/json"
	"fmt"
	"log"
	"net/http"
	"net/url"
	"os"
	"strconv"
	"strings"
	"time"
	"github.com/hashicorp/go-version"
)
const versionServiceID = "versions.v1"
type Host struct {
	discoURL  *url.URL
	hostname  string
	services  map[string]interface{}
	transport http.RoundTripper
}
type Constraints struct {
	Service   string   `json:"service"`
	Product   string   `json:"product"`
	Minimum   string   `json:"minimum"`
	Maximum   string   `json:"maximum"`
	Excluding []string `json:"excluding"`
}
type ErrServiceNotProvided struct {
	hostname string
	service  string
}
func (e *ErrServiceNotProvided) Error() string {
	if e.hostname == "" {
		return fmt.Sprintf("host does not provide a %s service", e.service)
	}
	return fmt.Sprintf("host %s does not provide a %s service", e.hostname, e.service)
}
type ErrVersionNotSupported struct {
	hostname string
	service  string
	version  string
}
func (e *ErrVersionNotSupported) Error() string {
	if e.hostname == "" {
		return fmt.Sprintf("host does not support %s version %s", e.service, e.version)
	}
	return fmt.Sprintf("host %s does not support %s version %s", e.hostname, e.service, e.version)
}
type ErrNoVersionConstraints struct {
	disabled bool
}
func (e *ErrNoVersionConstraints) Error() string {
	if e.disabled {
		return "checkpoint disabled"
	}
	return "unable to contact versions service"
}
func (h *Host) ServiceURL(id string) (*url.URL, error) {
	svc, ver, err := parseServiceID(id)
	if err != nil {
		return nil, err
	}
	if h == nil || h.services == nil {
		return nil, &ErrServiceNotProvided{service: svc}
	}
	urlStr, ok := h.services[id].(string)
	if !ok {
		for serviceID := range h.services {
			if strings.HasPrefix(serviceID, svc+".") {
				return nil, &ErrVersionNotSupported{
					hostname: h.hostname,
					service:  svc,
					version:  ver.Original(),
				}
			}
		}
		return nil, &ErrServiceNotProvided{hostname: h.hostname, service: svc}
	}
	u, err := h.parseURL(urlStr)
	if err != nil {
		return nil, fmt.Errorf("Failed to parse service URL: %v", err)
	}
	return u, nil
}
func (h *Host) ServiceOAuthClient(id string) (*OAuthClient, error) {
	svc, ver, err := parseServiceID(id)
	if err != nil {
		return nil, err
	}
	if h == nil || h.services == nil {
		return nil, &ErrServiceNotProvided{service: svc}
	}
	if _, ok := h.services[id]; !ok {
		for serviceID := range h.services {
			if strings.HasPrefix(serviceID, svc+".") {
				return nil, &ErrVersionNotSupported{
					hostname: h.hostname,
					service:  svc,
					version:  ver.Original(),
				}
			}
		}
		return nil, &ErrServiceNotProvided{hostname: h.hostname, service: svc}
	}
	var raw map[string]interface{}
	switch v := h.services[id].(type) {
	case map[string]interface{}:
		raw = v  
	case []map[string]interface{}:
		raw = v[0]
	default:
		log.Printf("[DEBUG] The definition for %s has Go type %T", id, h.services[id])
		return nil, fmt.Errorf("Service %s must be declared with an object value in the service discovery document", id)
	}
	var grantTypes OAuthGrantTypeSet
	if rawGTs, ok := raw["grant_types"]; ok {
		if gts, ok := rawGTs.([]interface{}); ok {
			var kws []string
			for _, gtI := range gts {
				gt, ok := gtI.(string)
				if !ok {
					continue
				}
				kws = append(kws, gt)
			}
			grantTypes = NewOAuthGrantTypeSet(kws...)
		} else {
			return nil, fmt.Errorf("Service %s is defined with invalid grant_types property: must be an array of grant type strings", id)
		}
	} else {
		grantTypes = NewOAuthGrantTypeSet("authz_code")
	}
	ret := &OAuthClient{
		SupportedGrantTypes: grantTypes,
	}
	if clientIDStr, ok := raw["client"].(string); ok {
		ret.ID = clientIDStr
	} else {
		return nil, fmt.Errorf("Service %s definition is missing required property \"client\"", id)
	}
	if urlStr, ok := raw["authz"].(string); ok {
		u, err := h.parseURL(urlStr)
		if err != nil {
			return nil, fmt.Errorf("Failed to parse authorization URL: %v", err)
		}
		ret.AuthorizationURL = u
	} else {
		if grantTypes.RequiresAuthorizationEndpoint() {
			return nil, fmt.Errorf("Service %s definition is missing required property \"authz\"", id)
		}
	}
	if urlStr, ok := raw["token"].(string); ok {
		u, err := h.parseURL(urlStr)
		if err != nil {
			return nil, fmt.Errorf("Failed to parse token URL: %v", err)
		}
		ret.TokenURL = u
	} else {
		if grantTypes.RequiresTokenEndpoint() {
			return nil, fmt.Errorf("Service %s definition is missing required property \"token\"", id)
		}
	}
	if portsRaw, ok := raw["ports"].([]interface{}); ok {
		if len(portsRaw) != 2 {
			return nil, fmt.Errorf("Invalid \"ports\" definition for service %s: must be a two-element array", id)
		}
		invalidPortsErr := fmt.Errorf("Invalid \"ports\" definition for service %s: both ports must be whole numbers between 1024 and 65535", id)
		ports := make([]uint16, 2)
		for i := range ports {
			switch v := portsRaw[i].(type) {
			case float64:
				if float64(uint16(v)) != v || v < 1024 {
					return nil, invalidPortsErr
				}
				ports[i] = uint16(v)
			case int:
				if v < 1024 || v > 65535 {
					return nil, invalidPortsErr
				}
				ports[i] = uint16(v)
			default:
				log.Printf("[DEBUG] Port value %d has Go type %T", i, portsRaw[i])
				return nil, invalidPortsErr
			}
		}
		if ports[1] < ports[0] {
			return nil, fmt.Errorf("Invalid \"ports\" definition for service %s: minimum port cannot be greater than maximum port", id)
		}
		ret.MinPort = ports[0]
		ret.MaxPort = ports[1]
	} else {
		ret.MinPort = 1024
		ret.MaxPort = 65535
	}
	return ret, nil
}
func (h *Host) parseURL(urlStr string) (*url.URL, error) {
	u, err := url.Parse(urlStr)
	if err != nil {
		return nil, err
	}
	if !u.IsAbs() {
		u = h.discoURL.ResolveReference(u)
	}
	if u.Scheme != "https" && u.Scheme != "http" {
		return nil, fmt.Errorf("unsupported scheme %s", u.Scheme)
	}
	if u.User != nil {
		return nil, fmt.Errorf("embedded username/password information is not permitted")
	}
	u.Fragment = ""
	return u, nil
}