1// Copyright 2017 The Prometheus Authors 2// Licensed under the Apache License, Version 2.0 (the "License"); 3// you may not use this file except in compliance with the License. 4// You may obtain a copy of the License at 5// 6// http://www.apache.org/licenses/LICENSE-2.0 7// 8// Unless required by applicable law or agreed to in writing, software 9// distributed under the License is distributed on an "AS IS" BASIS, 10// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11// See the License for the specific language governing permissions and 12// limitations under the License. 13 14package triton 15 16import ( 17 "context" 18 "encoding/json" 19 "fmt" 20 "io" 21 "io/ioutil" 22 "net/http" 23 "net/url" 24 "strings" 25 "time" 26 27 "github.com/go-kit/log" 28 conntrack "github.com/mwitkow/go-conntrack" 29 "github.com/pkg/errors" 30 "github.com/prometheus/common/config" 31 "github.com/prometheus/common/model" 32 33 "github.com/prometheus/prometheus/discovery" 34 "github.com/prometheus/prometheus/discovery/refresh" 35 "github.com/prometheus/prometheus/discovery/targetgroup" 36) 37 38const ( 39 tritonLabel = model.MetaLabelPrefix + "triton_" 40 tritonLabelGroups = tritonLabel + "groups" 41 tritonLabelMachineID = tritonLabel + "machine_id" 42 tritonLabelMachineAlias = tritonLabel + "machine_alias" 43 tritonLabelMachineBrand = tritonLabel + "machine_brand" 44 tritonLabelMachineImage = tritonLabel + "machine_image" 45 tritonLabelServerID = tritonLabel + "server_id" 46) 47 48// DefaultSDConfig is the default Triton SD configuration. 49var DefaultSDConfig = SDConfig{ 50 Role: "container", 51 Port: 9163, 52 RefreshInterval: model.Duration(60 * time.Second), 53 Version: 1, 54} 55 56func init() { 57 discovery.RegisterConfig(&SDConfig{}) 58} 59 60// SDConfig is the configuration for Triton based service discovery. 61type SDConfig struct { 62 Account string `yaml:"account"` 63 Role string `yaml:"role"` 64 DNSSuffix string `yaml:"dns_suffix"` 65 Endpoint string `yaml:"endpoint"` 66 Groups []string `yaml:"groups,omitempty"` 67 Port int `yaml:"port"` 68 RefreshInterval model.Duration `yaml:"refresh_interval,omitempty"` 69 TLSConfig config.TLSConfig `yaml:"tls_config,omitempty"` 70 Version int `yaml:"version"` 71} 72 73// Name returns the name of the Config. 74func (*SDConfig) Name() string { return "triton" } 75 76// NewDiscoverer returns a Discoverer for the Config. 77func (c *SDConfig) NewDiscoverer(opts discovery.DiscovererOptions) (discovery.Discoverer, error) { 78 return New(opts.Logger, c) 79} 80 81// SetDirectory joins any relative file paths with dir. 82func (c *SDConfig) SetDirectory(dir string) { 83 c.TLSConfig.SetDirectory(dir) 84} 85 86// UnmarshalYAML implements the yaml.Unmarshaler interface. 87func (c *SDConfig) UnmarshalYAML(unmarshal func(interface{}) error) error { 88 *c = DefaultSDConfig 89 type plain SDConfig 90 err := unmarshal((*plain)(c)) 91 if err != nil { 92 return err 93 } 94 if c.Role != "container" && c.Role != "cn" { 95 return errors.New("triton SD configuration requires role to be 'container' or 'cn'") 96 } 97 if c.Account == "" { 98 return errors.New("triton SD configuration requires an account") 99 } 100 if c.DNSSuffix == "" { 101 return errors.New("triton SD configuration requires a dns_suffix") 102 } 103 if c.Endpoint == "" { 104 return errors.New("triton SD configuration requires an endpoint") 105 } 106 if c.RefreshInterval <= 0 { 107 return errors.New("triton SD configuration requires RefreshInterval to be a positive integer") 108 } 109 return nil 110} 111 112// DiscoveryResponse models a JSON response from the Triton discovery. 113type DiscoveryResponse struct { 114 Containers []struct { 115 Groups []string `json:"groups"` 116 ServerUUID string `json:"server_uuid"` 117 VMAlias string `json:"vm_alias"` 118 VMBrand string `json:"vm_brand"` 119 VMImageUUID string `json:"vm_image_uuid"` 120 VMUUID string `json:"vm_uuid"` 121 } `json:"containers"` 122} 123 124// ComputeNodeDiscoveryResponse models a JSON response from the Triton discovery /gz/ endpoint. 125type ComputeNodeDiscoveryResponse struct { 126 ComputeNodes []struct { 127 ServerUUID string `json:"server_uuid"` 128 ServerHostname string `json:"server_hostname"` 129 } `json:"cns"` 130} 131 132// Discovery periodically performs Triton-SD requests. It implements 133// the Discoverer interface. 134type Discovery struct { 135 *refresh.Discovery 136 client *http.Client 137 interval time.Duration 138 sdConfig *SDConfig 139} 140 141// New returns a new Discovery which periodically refreshes its targets. 142func New(logger log.Logger, conf *SDConfig) (*Discovery, error) { 143 tls, err := config.NewTLSConfig(&conf.TLSConfig) 144 if err != nil { 145 return nil, err 146 } 147 148 transport := &http.Transport{ 149 TLSClientConfig: tls, 150 DialContext: conntrack.NewDialContextFunc( 151 conntrack.DialWithTracing(), 152 conntrack.DialWithName("triton_sd"), 153 ), 154 } 155 client := &http.Client{Transport: transport} 156 157 d := &Discovery{ 158 client: client, 159 interval: time.Duration(conf.RefreshInterval), 160 sdConfig: conf, 161 } 162 d.Discovery = refresh.NewDiscovery( 163 logger, 164 "triton", 165 time.Duration(conf.RefreshInterval), 166 d.refresh, 167 ) 168 return d, nil 169} 170 171// triton-cmon has two discovery endpoints: 172// https://github.com/joyent/triton-cmon/blob/master/lib/endpoints/discover.js 173// 174// The default endpoint exposes "containers", otherwise called "virtual machines" in triton, 175// which are (branded) zones running on the triton platform. 176// 177// The /gz/ endpoint exposes "compute nodes", also known as "servers" or "global zones", 178// on which the "containers" are running. 179// 180// As triton is not internally consistent in using these names, 181// the terms as used in triton-cmon are used here. 182 183func (d *Discovery) refresh(ctx context.Context) ([]*targetgroup.Group, error) { 184 var endpointFormat string 185 switch d.sdConfig.Role { 186 case "container": 187 endpointFormat = "https://%s:%d/v%d/discover" 188 case "cn": 189 endpointFormat = "https://%s:%d/v%d/gz/discover" 190 default: 191 return nil, errors.New(fmt.Sprintf("unknown role '%s' in configuration", d.sdConfig.Role)) 192 } 193 var endpoint = fmt.Sprintf(endpointFormat, d.sdConfig.Endpoint, d.sdConfig.Port, d.sdConfig.Version) 194 if len(d.sdConfig.Groups) > 0 { 195 groups := url.QueryEscape(strings.Join(d.sdConfig.Groups, ",")) 196 endpoint = fmt.Sprintf("%s?groups=%s", endpoint, groups) 197 } 198 199 req, err := http.NewRequest("GET", endpoint, nil) 200 if err != nil { 201 return nil, err 202 } 203 req = req.WithContext(ctx) 204 resp, err := d.client.Do(req) 205 if err != nil { 206 return nil, errors.Wrap(err, "an error occurred when requesting targets from the discovery endpoint") 207 } 208 209 defer func() { 210 io.Copy(ioutil.Discard, resp.Body) 211 resp.Body.Close() 212 }() 213 214 data, err := ioutil.ReadAll(resp.Body) 215 if err != nil { 216 return nil, errors.Wrap(err, "an error occurred when reading the response body") 217 } 218 219 // The JSON response body is different so it needs to be processed/mapped separately. 220 switch d.sdConfig.Role { 221 case "container": 222 return d.processContainerResponse(data, endpoint) 223 case "cn": 224 return d.processComputeNodeResponse(data, endpoint) 225 default: 226 return nil, errors.New(fmt.Sprintf("unknown role '%s' in configuration", d.sdConfig.Role)) 227 } 228} 229 230func (d *Discovery) processContainerResponse(data []byte, endpoint string) ([]*targetgroup.Group, error) { 231 tg := &targetgroup.Group{ 232 Source: endpoint, 233 } 234 235 dr := DiscoveryResponse{} 236 err := json.Unmarshal(data, &dr) 237 if err != nil { 238 return nil, errors.Wrap(err, "an error occurred unmarshaling the discovery response json") 239 } 240 241 for _, container := range dr.Containers { 242 labels := model.LabelSet{ 243 tritonLabelMachineID: model.LabelValue(container.VMUUID), 244 tritonLabelMachineAlias: model.LabelValue(container.VMAlias), 245 tritonLabelMachineBrand: model.LabelValue(container.VMBrand), 246 tritonLabelMachineImage: model.LabelValue(container.VMImageUUID), 247 tritonLabelServerID: model.LabelValue(container.ServerUUID), 248 } 249 addr := fmt.Sprintf("%s.%s:%d", container.VMUUID, d.sdConfig.DNSSuffix, d.sdConfig.Port) 250 labels[model.AddressLabel] = model.LabelValue(addr) 251 252 if len(container.Groups) > 0 { 253 name := "," + strings.Join(container.Groups, ",") + "," 254 labels[tritonLabelGroups] = model.LabelValue(name) 255 } 256 257 tg.Targets = append(tg.Targets, labels) 258 } 259 260 return []*targetgroup.Group{tg}, nil 261} 262 263func (d *Discovery) processComputeNodeResponse(data []byte, endpoint string) ([]*targetgroup.Group, error) { 264 tg := &targetgroup.Group{ 265 Source: endpoint, 266 } 267 268 dr := ComputeNodeDiscoveryResponse{} 269 err := json.Unmarshal(data, &dr) 270 if err != nil { 271 return nil, errors.Wrap(err, "an error occurred unmarshaling the compute node discovery response json") 272 } 273 274 for _, cn := range dr.ComputeNodes { 275 labels := model.LabelSet{ 276 tritonLabelMachineID: model.LabelValue(cn.ServerUUID), 277 tritonLabelMachineAlias: model.LabelValue(cn.ServerHostname), 278 } 279 addr := fmt.Sprintf("%s.%s:%d", cn.ServerUUID, d.sdConfig.DNSSuffix, d.sdConfig.Port) 280 labels[model.AddressLabel] = model.LabelValue(addr) 281 282 tg.Targets = append(tg.Targets, labels) 283 } 284 285 return []*targetgroup.Group{tg}, nil 286} 287