diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md index b79bc9a3f9..7ab49fe3cb 100644 --- a/.github/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE.md @@ -1,4 +1,15 @@ -[NOTICE]: <> (If your question is around usage and not a bug in Prometheus please use: https://groups.google.com/forum/#!forum/prometheus-users) + **What did you do?** @@ -12,7 +23,7 @@ insert output of `uname -srm` here -* Prometheus version: +* Prometheus version: insert output of `prometheus -version` here diff --git a/config/config.go b/config/config.go index a4df23d252..1633072e21 100644 --- a/config/config.go +++ b/config/config.go @@ -1184,6 +1184,7 @@ type OpenstackSDConfig struct { ProjectID string `yaml:"project_id"` DomainName string `yaml:"domain_name"` DomainID string `yaml:"domain_id"` + Role OpenStackRole `yaml:"role"` Region string `yaml:"region"` RefreshInterval model.Duration `yaml:"refresh_interval,omitempty"` Port int `yaml:"port"` @@ -1192,6 +1193,32 @@ type OpenstackSDConfig struct { XXX map[string]interface{} `yaml:",inline"` } +// OpenStackRole is role of the target in OpenStack. +type OpenStackRole string + +// The valid options for OpenStackRole. +const ( + // OpenStack document reference + // https://docs.openstack.org/nova/pike/admin/arch.html#hypervisors + OpenStackRoleHypervisor OpenStackRole = "hypervisor" + // OpenStack document reference + // https://docs.openstack.org/horizon/pike/user/launch-instances.html + OpenStackRoleInstance OpenStackRole = "instance" +) + +// UnmarshalYAML implements the yaml.Unmarshaler interface. +func (c *OpenStackRole) UnmarshalYAML(unmarshal func(interface{}) error) error { + if err := unmarshal((*string)(c)); err != nil { + return err + } + switch *c { + case OpenStackRoleHypervisor, OpenStackRoleInstance: + return nil + default: + return fmt.Errorf("Unknown OpenStack SD role %q", *c) + } +} + // UnmarshalYAML implements the yaml.Unmarshaler interface. func (c *OpenstackSDConfig) UnmarshalYAML(unmarshal func(interface{}) error) error { *c = DefaultOpenstackSDConfig @@ -1200,6 +1227,9 @@ func (c *OpenstackSDConfig) UnmarshalYAML(unmarshal func(interface{}) error) err if err != nil { return err } + if c.Role == "" { + return fmt.Errorf("role missing (one of: instance, hypervisor)") + } return checkOverflow(c.XXX, "openstack_sd_config") } diff --git a/config/config_test.go b/config/config_test.go index 075eb325af..0417a2eb9e 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -540,15 +540,27 @@ func TestLoadConfig(t *testing.T) { if !reflect.DeepEqual(c, expectedConf) { t.Fatalf("%s: unexpected config result: \n\n%s\n expected\n\n%s", "testdata/conf.good.yml", bgot, bexp) } +} - // String method must not reveal authentication credentials. - s := c.String() - secretRe := regexp.MustCompile("") - matches := secretRe.FindAllStringIndex(s, -1) - if len(matches) != 6 || strings.Contains(s, "mysecret") { - t.Fatalf("config's String method reveals authentication credentials.") +// YAML marshalling must not reveal authentication credentials. +func TestElideSecrets(t *testing.T) { + c, err := LoadFile("testdata/conf.good.yml") + if err != nil { + t.Fatalf("Error parsing %s: %s", "testdata/conf.good.yml", err) } + secretRe := regexp.MustCompile(`\\u003csecret\\u003e|`) + + config, err := yaml.Marshal(c) + if err != nil { + t.Fatal(err) + } + yamlConfig := string(config) + + matches := secretRe.FindAllStringIndex(yamlConfig, -1) + if len(matches) != 6 || strings.Contains(yamlConfig, "mysecret") { + t.Fatalf("yaml marshal reveals authentication credentials.") + } } func TestLoadConfigRuleFilesAbsolutePath(t *testing.T) { diff --git a/discovery/consul/consul.go b/discovery/consul/consul.go index d842ca2813..fbb78086aa 100644 --- a/discovery/consul/consul.go +++ b/discovery/consul/consul.go @@ -27,6 +27,7 @@ import ( "github.com/prometheus/common/model" "github.com/prometheus/prometheus/config" "github.com/prometheus/prometheus/util/httputil" + "github.com/prometheus/prometheus/util/strutil" "golang.org/x/net/context" ) @@ -38,6 +39,8 @@ const ( addressLabel = model.MetaLabelPrefix + "consul_address" // nodeLabel is the name for the label containing a target's node name. nodeLabel = model.MetaLabelPrefix + "consul_node" + // metaDataLabel is the prefix for the labels mapping to a target's metadata. + metaDataLabel = model.MetaLabelPrefix + "consul_metadata_" // tagsLabel is the name of the label containing the tags assigned to the target. tagsLabel = model.MetaLabelPrefix + "consul_tags" // serviceLabel is the name of the label containing the service name. @@ -294,7 +297,7 @@ func (srv *consulService) watch(ctx context.Context, ch chan<- []*config.TargetG addr = net.JoinHostPort(node.Address, fmt.Sprintf("%d", node.ServicePort)) } - tgroup.Targets = append(tgroup.Targets, model.LabelSet{ + labels := model.LabelSet{ model.AddressLabel: model.LabelValue(addr), addressLabel: model.LabelValue(node.Address), nodeLabel: model.LabelValue(node.Node), @@ -302,7 +305,15 @@ func (srv *consulService) watch(ctx context.Context, ch chan<- []*config.TargetG serviceAddressLabel: model.LabelValue(node.ServiceAddress), servicePortLabel: model.LabelValue(strconv.Itoa(node.ServicePort)), serviceIDLabel: model.LabelValue(node.ServiceID), - }) + } + + // Add all key/value pairs from the node's metadata as their own labels + for k, v := range node.NodeMeta { + name := strutil.SanitizeLabelName(k) + labels[metaDataLabel+model.LabelName(name)] = model.LabelValue(v) + } + + tgroup.Targets = append(tgroup.Targets, labels) } // Check context twice to ensure we always catch cancelation. select { diff --git a/discovery/discovery.go b/discovery/discovery.go index bf2e1cecdd..b5cc910b9d 100644 --- a/discovery/discovery.go +++ b/discovery/discovery.go @@ -98,7 +98,7 @@ func ProvidersFromConfig(cfg config.ServiceDiscoveryConfig, logger log.Logger) m app("ec2", i, ec2.NewDiscovery(c, logger)) } for i, c := range cfg.OpenstackSDConfigs { - openstackd, err := openstack.NewDiscovery(c) + openstackd, err := openstack.NewDiscovery(c, logger) if err != nil { log.Errorf("Cannot initialize OpenStack discovery: %s", err) continue diff --git a/discovery/file/file.go b/discovery/file/file.go index b74c6e5102..137871bd9d 100644 --- a/discovery/file/file.go +++ b/discovery/file/file.go @@ -15,6 +15,7 @@ package file import ( "encoding/json" + "errors" "fmt" "io/ioutil" "path/filepath" @@ -256,6 +257,11 @@ func readFile(filename string) ([]*config.TargetGroup, error) { } for i, tg := range targetGroups { + if tg == nil { + err = errors.New("nil target group item found") + return nil, err + } + tg.Source = fileSource(filename, i) if tg.Labels == nil { tg.Labels = model.LabelSet{} diff --git a/discovery/file/file_test.go b/discovery/file/file_test.go index 65c53c23b8..6563b6a942 100644 --- a/discovery/file/file_test.go +++ b/discovery/file/file_test.go @@ -29,13 +29,17 @@ import ( ) func TestFileSD(t *testing.T) { - defer os.Remove("fixtures/_test.yml") - defer os.Remove("fixtures/_test.json") - testFileSD(t, ".yml") - testFileSD(t, ".json") + defer os.Remove("fixtures/_test_valid.yml") + defer os.Remove("fixtures/_test_valid.json") + defer os.Remove("fixtures/_test_invalid_nil.json") + defer os.Remove("fixtures/_test_invalid_nil.yml") + testFileSD(t, "valid", ".yml", true) + testFileSD(t, "valid", ".json", true) + testFileSD(t, "invalid_nil", ".json", false) + testFileSD(t, "invalid_nil", ".yml", false) } -func testFileSD(t *testing.T, ext string) { +func testFileSD(t *testing.T, prefix, ext string, expect bool) { // As interval refreshing is more of a fallback, we only want to test // whether file watches work as expected. var conf config.FileSDConfig @@ -56,13 +60,13 @@ func testFileSD(t *testing.T, ext string) { t.Fatalf("Unexpected target groups in file discovery: %s", tgs) } - newf, err := os.Create("fixtures/_test" + ext) + newf, err := os.Create("fixtures/_test_" + prefix + ext) if err != nil { t.Fatal(err) } defer newf.Close() - f, err := os.Open("fixtures/valid" + ext) + f, err := os.Open("fixtures/" + prefix + ext) if err != nil { t.Fatal(err) } @@ -80,8 +84,17 @@ retry: for { select { case <-timeout: - t.Fatalf("Expected new target group but got none") + if expect { + t.Fatalf("Expected new target group but got none") + } else { + // invalid type fsd should always broken down. + break retry + } case tgs := <-ch: + if !expect { + t.Fatalf("Unexpected target groups %s, we expected a failure here.", tgs) + } + if len(tgs) != 2 { continue retry // Potentially a partial write, just retry. } @@ -90,12 +103,12 @@ retry: if _, ok := tg.Labels["foo"]; !ok { t.Fatalf("Label not parsed") } - if tg.String() != filepath.FromSlash(fmt.Sprintf("fixtures/_test%s:0", ext)) { + if tg.String() != filepath.FromSlash(fmt.Sprintf("fixtures/_test_%s%s:0", prefix, ext)) { t.Fatalf("Unexpected target group %s", tg) } tg = tgs[1] - if tg.String() != filepath.FromSlash(fmt.Sprintf("fixtures/_test%s:1", ext)) { + if tg.String() != filepath.FromSlash(fmt.Sprintf("fixtures/_test_%s%s:1", prefix, ext)) { t.Fatalf("Unexpected target groups %s", tg) } break retry @@ -135,7 +148,7 @@ retry: } newf.Close() - os.Rename(newf.Name(), "fixtures/_test"+ext) + os.Rename(newf.Name(), "fixtures/_test_"+prefix+ext) cancel() <-drained diff --git a/discovery/file/fixtures/invalid_nil.json b/discovery/file/fixtures/invalid_nil.json new file mode 100644 index 0000000000..0534ba48a3 --- /dev/null +++ b/discovery/file/fixtures/invalid_nil.json @@ -0,0 +1,9 @@ +[ + { + "targets": ["localhost:9090", "example.org:443"], + "labels": { + "foo": "bar" + } + }, + null +] diff --git a/discovery/file/fixtures/invalid_nil.yml b/discovery/file/fixtures/invalid_nil.yml new file mode 100644 index 0000000000..761857294b --- /dev/null +++ b/discovery/file/fixtures/invalid_nil.yml @@ -0,0 +1,5 @@ +- targets: ['localhost:9090', 'example.org:443'] + labels: + foo: bar + +- null diff --git a/discovery/openstack/hypervisor.go b/discovery/openstack/hypervisor.go new file mode 100644 index 0000000000..54c5f3f6db --- /dev/null +++ b/discovery/openstack/hypervisor.go @@ -0,0 +1,145 @@ +// Copyright 2017 The Prometheus Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package openstack + +import ( + "fmt" + "net" + "time" + + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/openstack" + "github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/hypervisors" + "github.com/gophercloud/gophercloud/pagination" + "github.com/prometheus/common/log" + "github.com/prometheus/common/model" + "golang.org/x/net/context" + + "github.com/prometheus/prometheus/config" +) + +const ( + openstackLabelHypervisorHostIP = openstackLabelPrefix + "hypervisor_host_ip" + openstackLabelHypervisorHostName = openstackLabelPrefix + "hypervisor_hostname" + openstackLabelHypervisorStatus = openstackLabelPrefix + "hypervisor_status" + openstackLabelHypervisorState = openstackLabelPrefix + "hypervisor_state" + openstackLabelHypervisorType = openstackLabelPrefix + "hypervisor_type" +) + +// HypervisorDiscovery discovers OpenStack hypervisors. +type HypervisorDiscovery struct { + authOpts *gophercloud.AuthOptions + region string + interval time.Duration + logger log.Logger + port int +} + +// NewHypervisorDiscovery returns a new hypervisor discovery. +func NewHypervisorDiscovery(opts *gophercloud.AuthOptions, + interval time.Duration, port int, region string, l log.Logger) *HypervisorDiscovery { + return &HypervisorDiscovery{authOpts: opts, + region: region, interval: interval, port: port, logger: l} +} + +// Run implements the TargetProvider interface. +func (h *HypervisorDiscovery) Run(ctx context.Context, ch chan<- []*config.TargetGroup) { + // Get an initial set right away. + tg, err := h.refresh() + if err != nil { + h.logger.Error(err) + } else { + select { + case ch <- []*config.TargetGroup{tg}: + case <-ctx.Done(): + return + } + } + + ticker := time.NewTicker(h.interval) + defer ticker.Stop() + + for { + select { + case <-ticker.C: + tg, err := h.refresh() + if err != nil { + h.logger.Error(err) + continue + } + + select { + case ch <- []*config.TargetGroup{tg}: + case <-ctx.Done(): + return + } + case <-ctx.Done(): + return + } + } +} + +func (h *HypervisorDiscovery) refresh() (*config.TargetGroup, error) { + var err error + t0 := time.Now() + defer func() { + refreshDuration.Observe(time.Since(t0).Seconds()) + if err != nil { + refreshFailuresCount.Inc() + } + }() + + provider, err := openstack.AuthenticatedClient(*h.authOpts) + if err != nil { + return nil, fmt.Errorf("could not create OpenStack session: %s", err) + } + client, err := openstack.NewComputeV2(provider, gophercloud.EndpointOpts{ + Region: h.region, + }) + if err != nil { + return nil, fmt.Errorf("could not create OpenStack compute session: %s", err) + } + + tg := &config.TargetGroup{ + Source: fmt.Sprintf("OS_" + h.region), + } + // OpenStack API reference + // https://developer.openstack.org/api-ref/compute/#list-hypervisors-details + pagerHypervisors := hypervisors.List(client) + err = pagerHypervisors.EachPage(func(page pagination.Page) (bool, error) { + hypervisorList, err := hypervisors.ExtractHypervisors(page) + if err != nil { + return false, fmt.Errorf("could not extract hypervisors: %s", err) + } + for _, hypervisor := range hypervisorList { + labels := model.LabelSet{ + openstackLabelHypervisorHostIP: model.LabelValue(hypervisor.HostIP), + } + addr := net.JoinHostPort(hypervisor.HostIP, fmt.Sprintf("%d", h.port)) + labels[model.AddressLabel] = model.LabelValue(addr) + labels[openstackLabelHypervisorHostName] = model.LabelValue(hypervisor.HypervisorHostname) + labels[openstackLabelHypervisorHostIP] = model.LabelValue(hypervisor.HostIP) + labels[openstackLabelHypervisorStatus] = model.LabelValue(hypervisor.Status) + labels[openstackLabelHypervisorState] = model.LabelValue(hypervisor.State) + labels[openstackLabelHypervisorType] = model.LabelValue(hypervisor.HypervisorType) + tg.Targets = append(tg.Targets, labels) + } + return true, nil + }) + if err != nil { + return nil, err + } + + return tg, nil +} diff --git a/discovery/openstack/hypervisor_test.go b/discovery/openstack/hypervisor_test.go new file mode 100644 index 0000000000..4146e4227b --- /dev/null +++ b/discovery/openstack/hypervisor_test.go @@ -0,0 +1,84 @@ +// Copyright 2017 The Prometheus Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package openstack + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/stretchr/testify/suite" + + "github.com/prometheus/common/log" + "github.com/prometheus/common/model" + "github.com/prometheus/prometheus/config" +) + +type OpenstackSDHypervisorTestSuite struct { + suite.Suite + Mock *SDMock +} + +func (s *OpenstackSDHypervisorTestSuite) TearDownSuite() { + s.Mock.ShutdownServer() +} + +func (s *OpenstackSDHypervisorTestSuite) SetupTest() { + s.Mock = NewSDMock(s.T()) + s.Mock.Setup() + + s.Mock.HandleHypervisorListSuccessfully() + + s.Mock.HandleVersionsSuccessfully() + s.Mock.HandleAuthSuccessfully() +} + +func TestOpenstackSDHypervisorSuite(t *testing.T) { + suite.Run(t, new(OpenstackSDHypervisorTestSuite)) +} + +func (s *OpenstackSDHypervisorTestSuite) openstackAuthSuccess() (Discovery, error) { + conf := config.OpenstackSDConfig{ + IdentityEndpoint: s.Mock.Endpoint(), + Password: "test", + Username: "test", + DomainName: "12345", + Region: "RegionOne", + Role: "hypervisor", + } + return NewDiscovery(&conf, log.Base()) +} + +func (s *OpenstackSDHypervisorTestSuite) TestOpenstackSDHypervisorRefresh() { + hypervisor, _ := s.openstackAuthSuccess() + tg, err := hypervisor.refresh() + assert.Nil(s.T(), err) + require.NotNil(s.T(), tg) + require.NotNil(s.T(), tg.Targets) + require.Len(s.T(), tg.Targets, 2) + + assert.Equal(s.T(), tg.Targets[0]["__address__"], model.LabelValue("172.16.70.14:0")) + assert.Equal(s.T(), tg.Targets[0]["__meta_openstack_hypervisor_hostname"], model.LabelValue("nc14.cloud.com")) + assert.Equal(s.T(), tg.Targets[0]["__meta_openstack_hypervisor_type"], model.LabelValue("QEMU")) + assert.Equal(s.T(), tg.Targets[0]["__meta_openstack_hypervisor_host_ip"], model.LabelValue("172.16.70.14")) + assert.Equal(s.T(), tg.Targets[0]["__meta_openstack_hypervisor_state"], model.LabelValue("up")) + assert.Equal(s.T(), tg.Targets[0]["__meta_openstack_hypervisor_status"], model.LabelValue("enabled")) + + assert.Equal(s.T(), tg.Targets[1]["__address__"], model.LabelValue("172.16.70.13:0")) + assert.Equal(s.T(), tg.Targets[1]["__meta_openstack_hypervisor_hostname"], model.LabelValue("cc13.cloud.com")) + assert.Equal(s.T(), tg.Targets[1]["__meta_openstack_hypervisor_type"], model.LabelValue("QEMU")) + assert.Equal(s.T(), tg.Targets[1]["__meta_openstack_hypervisor_host_ip"], model.LabelValue("172.16.70.13")) + assert.Equal(s.T(), tg.Targets[1]["__meta_openstack_hypervisor_state"], model.LabelValue("up")) + assert.Equal(s.T(), tg.Targets[1]["__meta_openstack_hypervisor_status"], model.LabelValue("enabled")) +} diff --git a/discovery/openstack/instance.go b/discovery/openstack/instance.go new file mode 100644 index 0000000000..1022cf5a30 --- /dev/null +++ b/discovery/openstack/instance.go @@ -0,0 +1,211 @@ +// Copyright 2017 The Prometheus Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package openstack + +import ( + "fmt" + "net" + "time" + + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/openstack" + "github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/floatingips" + "github.com/gophercloud/gophercloud/openstack/compute/v2/servers" + "github.com/gophercloud/gophercloud/pagination" + "github.com/prometheus/common/log" + "github.com/prometheus/common/model" + "golang.org/x/net/context" + + "github.com/prometheus/prometheus/config" + "github.com/prometheus/prometheus/util/strutil" +) + +const ( + openstackLabelPrefix = model.MetaLabelPrefix + "openstack_" + openstackLabelInstanceID = openstackLabelPrefix + "instance_id" + openstackLabelInstanceName = openstackLabelPrefix + "instance_name" + openstackLabelInstanceStatus = openstackLabelPrefix + "instance_status" + openstackLabelInstanceFlavor = openstackLabelPrefix + "instance_flavor" + openstackLabelPublicIP = openstackLabelPrefix + "public_ip" + openstackLabelPrivateIP = openstackLabelPrefix + "private_ip" + openstackLabelTagPrefix = openstackLabelPrefix + "tag_" +) + +// InstanceDiscovery discovers OpenStack instances. +type InstanceDiscovery struct { + authOpts *gophercloud.AuthOptions + region string + interval time.Duration + logger log.Logger + port int +} + +// NewInstanceDiscovery returns a new instance discovery. +func NewInstanceDiscovery(opts *gophercloud.AuthOptions, + interval time.Duration, port int, region string, l log.Logger) *InstanceDiscovery { + return &InstanceDiscovery{authOpts: opts, + region: region, interval: interval, port: port, logger: l} +} + +// Run implements the TargetProvider interface. +func (i *InstanceDiscovery) Run(ctx context.Context, ch chan<- []*config.TargetGroup) { + // Get an initial set right away. + tg, err := i.refresh() + if err != nil { + i.logger.Error(err) + } else { + select { + case ch <- []*config.TargetGroup{tg}: + case <-ctx.Done(): + return + } + } + + ticker := time.NewTicker(i.interval) + defer ticker.Stop() + + for { + select { + case <-ticker.C: + tg, err := i.refresh() + if err != nil { + i.logger.Error(err) + continue + } + + select { + case ch <- []*config.TargetGroup{tg}: + case <-ctx.Done(): + return + } + case <-ctx.Done(): + return + } + } +} + +func (i *InstanceDiscovery) refresh() (*config.TargetGroup, error) { + var err error + t0 := time.Now() + defer func() { + refreshDuration.Observe(time.Since(t0).Seconds()) + if err != nil { + refreshFailuresCount.Inc() + } + }() + + provider, err := openstack.AuthenticatedClient(*i.authOpts) + if err != nil { + return nil, fmt.Errorf("could not create OpenStack session: %s", err) + } + client, err := openstack.NewComputeV2(provider, gophercloud.EndpointOpts{ + Region: i.region, + }) + if err != nil { + return nil, fmt.Errorf("could not create OpenStack compute session: %s", err) + } + + // OpenStack API reference + // https://developer.openstack.org/api-ref/compute/#list-floating-ips + pagerFIP := floatingips.List(client) + floatingIPList := make(map[string][]string) + err = pagerFIP.EachPage(func(page pagination.Page) (bool, error) { + result, err := floatingips.ExtractFloatingIPs(page) + if err != nil { + return false, fmt.Errorf("could not extract floatingips: %s", err) + } + for _, ip := range result { + // Skip not associated ips + if ip.InstanceID != "" { + floatingIPList[ip.InstanceID] = append(floatingIPList[ip.InstanceID], ip.IP) + } + } + return true, nil + }) + if err != nil { + return nil, err + } + + // OpenStack API reference + // https://developer.openstack.org/api-ref/compute/#list-servers + opts := servers.ListOpts{} + pager := servers.List(client, opts) + tg := &config.TargetGroup{ + Source: fmt.Sprintf("OS_" + i.region), + } + err = pager.EachPage(func(page pagination.Page) (bool, error) { + instanceList, err := servers.ExtractServers(page) + if err != nil { + return false, fmt.Errorf("could not extract instances: %s", err) + } + + for _, s := range instanceList { + labels := model.LabelSet{ + openstackLabelInstanceID: model.LabelValue(s.ID), + } + if len(s.Addresses) == 0 { + i.logger.Info("Got no IP address for instance %s", s.ID) + continue + } + for _, address := range s.Addresses { + md, ok := address.([]interface{}) + if !ok { + i.logger.Warn("Invalid type for address, expected array") + continue + } + if len(md) == 0 { + i.logger.Debugf("Got no IP address for instance %s", s.ID) + continue + } + md1, ok := md[0].(map[string]interface{}) + if !ok { + i.logger.Warn("Invalid type for address, expected dict") + continue + } + addr, ok := md1["addr"].(string) + if !ok { + i.logger.Warn("Invalid type for address, expected string") + continue + } + labels[openstackLabelPrivateIP] = model.LabelValue(addr) + addr = net.JoinHostPort(addr, fmt.Sprintf("%d", i.port)) + labels[model.AddressLabel] = model.LabelValue(addr) + // Only use first private IP + break + } + if val, ok := floatingIPList[s.ID]; ok && len(val) > 0 { + labels[openstackLabelPublicIP] = model.LabelValue(val[0]) + } + labels[openstackLabelInstanceStatus] = model.LabelValue(s.Status) + labels[openstackLabelInstanceName] = model.LabelValue(s.Name) + id, ok := s.Flavor["id"].(string) + if !ok { + i.logger.Warn("Invalid type for instance id, excepted string") + continue + } + labels[openstackLabelInstanceFlavor] = model.LabelValue(id) + for k, v := range s.Metadata { + name := strutil.SanitizeLabelName(k) + labels[openstackLabelTagPrefix+model.LabelName(name)] = model.LabelValue(v) + } + tg.Targets = append(tg.Targets, labels) + } + return true, nil + }) + if err != nil { + return nil, err + } + + return tg, nil +} diff --git a/discovery/openstack/openstack_test.go b/discovery/openstack/instance_test.go similarity index 82% rename from discovery/openstack/openstack_test.go rename to discovery/openstack/instance_test.go index a6285be2da..1f16fb6cf3 100644 --- a/discovery/openstack/openstack_test.go +++ b/discovery/openstack/instance_test.go @@ -20,20 +20,21 @@ import ( "github.com/stretchr/testify/require" "github.com/stretchr/testify/suite" + "github.com/prometheus/common/log" "github.com/prometheus/common/model" "github.com/prometheus/prometheus/config" ) -type OpenstackSDTestSuite struct { +type OpenstackSDInstanceTestSuite struct { suite.Suite Mock *SDMock } -func (s *OpenstackSDTestSuite) TearDownSuite() { +func (s *OpenstackSDInstanceTestSuite) TearDownSuite() { s.Mock.ShutdownServer() } -func (s *OpenstackSDTestSuite) SetupTest() { +func (s *OpenstackSDInstanceTestSuite) SetupTest() { s.Mock = NewSDMock(s.T()) s.Mock.Setup() @@ -44,26 +45,26 @@ func (s *OpenstackSDTestSuite) SetupTest() { s.Mock.HandleAuthSuccessfully() } -func TestOpenstackSDSuite(t *testing.T) { - suite.Run(t, new(OpenstackSDTestSuite)) +func TestOpenstackSDInstanceSuite(t *testing.T) { + suite.Run(t, new(OpenstackSDInstanceTestSuite)) } -func (s *OpenstackSDTestSuite) openstackAuthSuccess() (*Discovery, error) { +func (s *OpenstackSDInstanceTestSuite) openstackAuthSuccess() (Discovery, error) { conf := config.OpenstackSDConfig{ IdentityEndpoint: s.Mock.Endpoint(), Password: "test", Username: "test", DomainName: "12345", Region: "RegionOne", + Role: "instance", } - - return NewDiscovery(&conf) + return NewDiscovery(&conf, log.Base()) } -func (s *OpenstackSDTestSuite) TestOpenstackSDRefresh() { - d, _ := s.openstackAuthSuccess() +func (s *OpenstackSDInstanceTestSuite) TestOpenstackSDInstanceRefresh() { + instance, _ := s.openstackAuthSuccess() + tg, err := instance.refresh() - tg, err := d.refresh() assert.Nil(s.T(), err) require.NotNil(s.T(), tg) require.NotNil(s.T(), tg.Targets) @@ -83,5 +84,4 @@ func (s *OpenstackSDTestSuite) TestOpenstackSDRefresh() { assert.Equal(s.T(), tg.Targets[1]["__meta_openstack_instance_name"], model.LabelValue("derp")) assert.Equal(s.T(), tg.Targets[1]["__meta_openstack_instance_status"], model.LabelValue("ACTIVE")) assert.Equal(s.T(), tg.Targets[1]["__meta_openstack_private_ip"], model.LabelValue("10.0.0.31")) - } diff --git a/discovery/openstack/mock.go b/discovery/openstack/mock.go index 9cb53f5992..0cd975a9eb 100644 --- a/discovery/openstack/mock.go +++ b/discovery/openstack/mock.go @@ -182,9 +182,139 @@ func (m *SDMock) HandleAuthSuccessfully() { }) } +const hypervisorListBody = ` +{ + "hypervisors": [ + { + "status": "enabled", + "service": { + "host": "nc14.cloud.com", + "disabled_reason": null, + "id": 16 + }, + "vcpus_used": 18, + "hypervisor_type": "QEMU", + "local_gb_used": 84, + "vcpus": 24, + "hypervisor_hostname": "nc14.cloud.com", + "memory_mb_used": 24064, + "memory_mb": 96484, + "current_workload": 1, + "state": "up", + "host_ip": "172.16.70.14", + "cpu_info": "{\"vendor\": \"Intel\", \"model\": \"IvyBridge\", \"arch\": \"x86_64\", \"features\": [\"pge\", \"avx\", \"clflush\", \"sep\", \"syscall\", \"vme\", \"dtes64\", \"msr\", \"fsgsbase\", \"xsave\", \"vmx\", \"erms\", \"xtpr\", \"cmov\", \"smep\", \"ssse3\", \"est\", \"pat\", \"monitor\", \"smx\", \"pbe\", \"lm\", \"tsc\", \"nx\", \"fxsr\", \"tm\", \"sse4.1\", \"pae\", \"sse4.2\", \"pclmuldq\", \"acpi\", \"tsc-deadline\", \"mmx\", \"osxsave\", \"cx8\", \"mce\", \"de\", \"tm2\", \"ht\", \"dca\", \"lahf_lm\", \"popcnt\", \"mca\", \"pdpe1gb\", \"apic\", \"sse\", \"f16c\", \"pse\", \"ds\", \"invtsc\", \"pni\", \"rdtscp\", \"aes\", \"sse2\", \"ss\", \"ds_cpl\", \"pcid\", \"fpu\", \"cx16\", \"pse36\", \"mtrr\", \"pdcm\", \"rdrand\", \"x2apic\"], \"topology\": {\"cores\": 6, \"cells\": 2, \"threads\": 2, \"sockets\": 1}}", + "running_vms": 10, + "free_disk_gb": 315, + "hypervisor_version": 2003000, + "disk_available_least": 304, + "local_gb": 399, + "free_ram_mb": 72420, + "id": 1 + }, + { + "status": "enabled", + "service": { + "host": "cc13.cloud.com", + "disabled_reason": null, + "id": 17 + }, + "vcpus_used": 1, + "hypervisor_type": "QEMU", + "local_gb_used": 20, + "vcpus": 24, + "hypervisor_hostname": "cc13.cloud.com", + "memory_mb_used": 2560, + "memory_mb": 96484, + "current_workload": 0, + "state": "up", + "host_ip": "172.16.70.13", + "cpu_info": "{\"vendor\": \"Intel\", \"model\": \"IvyBridge\", \"arch\": \"x86_64\", \"features\": [\"pge\", \"avx\", \"clflush\", \"sep\", \"syscall\", \"vme\", \"dtes64\", \"msr\", \"fsgsbase\", \"xsave\", \"vmx\", \"erms\", \"xtpr\", \"cmov\", \"smep\", \"ssse3\", \"est\", \"pat\", \"monitor\", \"smx\", \"pbe\", \"lm\", \"tsc\", \"nx\", \"fxsr\", \"tm\", \"sse4.1\", \"pae\", \"sse4.2\", \"pclmuldq\", \"acpi\", \"tsc-deadline\", \"mmx\", \"osxsave\", \"cx8\", \"mce\", \"de\", \"tm2\", \"ht\", \"dca\", \"lahf_lm\", \"popcnt\", \"mca\", \"pdpe1gb\", \"apic\", \"sse\", \"f16c\", \"pse\", \"ds\", \"invtsc\", \"pni\", \"rdtscp\", \"aes\", \"sse2\", \"ss\", \"ds_cpl\", \"pcid\", \"fpu\", \"cx16\", \"pse36\", \"mtrr\", \"pdcm\", \"rdrand\", \"x2apic\"], \"topology\": {\"cores\": 6, \"cells\": 2, \"threads\": 2, \"sockets\": 1}}", + "running_vms": 0, + "free_disk_gb": 379, + "hypervisor_version": 2003000, + "disk_available_least": 384, + "local_gb": 399, + "free_ram_mb": 93924, + "id": 721 + } + ] +}` + +// HandleHypervisorListSuccessfully mocks os-hypervisors detail call +func (m *SDMock) HandleHypervisorListSuccessfully() { + m.Mux.HandleFunc("/os-hypervisors/detail", func(w http.ResponseWriter, r *http.Request) { + testMethod(m.t, r, "GET") + testHeader(m.t, r, "X-Auth-Token", tokenID) + + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, hypervisorListBody) + }) +} + const serverListBody = ` { "servers": [ + { + "status": "ERROR", + "updated": "2014-09-25T13:10:10Z", + "hostId": "29d3c8c896a45aa4c34e52247875d7fefc3d94bbcc9f622b5d204362", + "OS-EXT-SRV-ATTR:host": "devstack", + "addresses": {}, + "links": [ + { + "href": "http://104.130.131.164:8774/v2/fcad67a6189847c4aecfa3c81a05783b/servers/af9bcad9-3c87-477d-9347-b291eabf480e", + "rel": "self" + }, + { + "href": "http://104.130.131.164:8774/fcad67a6189847c4aecfa3c81a05783b/servers/af9bcad9-3c87-477d-9347-b291eabf480e", + "rel": "bookmark" + } + ], + "key_name": null, + "image": { + "id": "f90f6034-2570-4974-8351-6b49732ef2eb", + "links": [ + { + "href": "http://104.130.131.164:8774/fcad67a6189847c4aecfa3c81a05783b/images/f90f6034-2570-4974-8351-6b49732ef2eb", + "rel": "bookmark" + } + ] + }, + "OS-EXT-STS:task_state": null, + "OS-EXT-STS:vm_state": "error", + "OS-EXT-SRV-ATTR:instance_name": "instance-00000010", + "OS-SRV-USG:launched_at": "2014-09-25T13:10:10.000000", + "OS-EXT-SRV-ATTR:hypervisor_hostname": "devstack", + "flavor": { + "id": "1", + "links": [ + { + "href": "http://104.130.131.164:8774/fcad67a6189847c4aecfa3c81a05783b/flavors/1", + "rel": "bookmark" + } + ] + }, + "id": "af9bcad9-3c87-477d-9347-b291eabf480e", + "security_groups": [ + { + "name": "default" + } + ], + "OS-SRV-USG:terminated_at": null, + "OS-EXT-AZ:availability_zone": "nova", + "user_id": "9349aff8be7545ac9d2f1d00999a23cd", + "name": "herp2", + "created": "2014-09-25T13:10:02Z", + "tenant_id": "fcad67a6189847c4aecfa3c81a05783b", + "OS-DCF:diskConfig": "MANUAL", + "os-extended-volumes:volumes_attached": [], + "accessIPv4": "", + "accessIPv6": "", + "progress": 0, + "OS-EXT-STS:power_state": 1, + "config_drive": "", + "metadata": {} + }, { "status": "ACTIVE", "updated": "2014-09-25T13:10:10Z", diff --git a/discovery/openstack/openstack.go b/discovery/openstack/openstack.go index 4011b9253b..46b0fe1881 100644 --- a/discovery/openstack/openstack.go +++ b/discovery/openstack/openstack.go @@ -14,33 +14,15 @@ package openstack import ( - "fmt" - "net" + "errors" "time" "github.com/gophercloud/gophercloud" - "github.com/gophercloud/gophercloud/openstack" - "github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/floatingips" - "github.com/gophercloud/gophercloud/openstack/compute/v2/servers" - "github.com/gophercloud/gophercloud/pagination" "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/common/log" - "github.com/prometheus/common/model" "golang.org/x/net/context" "github.com/prometheus/prometheus/config" - "github.com/prometheus/prometheus/util/strutil" -) - -const ( - openstackLabelPrefix = model.MetaLabelPrefix + "openstack_" - openstackLabelInstanceID = openstackLabelPrefix + "instance_id" - openstackLabelInstanceName = openstackLabelPrefix + "instance_name" - openstackLabelInstanceStatus = openstackLabelPrefix + "instance_status" - openstackLabelInstanceFlavor = openstackLabelPrefix + "instance_flavor" - openstackLabelPublicIP = openstackLabelPrefix + "public_ip" - openstackLabelPrivateIP = openstackLabelPrefix + "private_ip" - openstackLabelTagPrefix = openstackLabelPrefix + "tag_" ) var ( @@ -63,15 +45,13 @@ func init() { // Discovery periodically performs OpenStack-SD requests. It implements // the TargetProvider interface. -type Discovery struct { - authOpts *gophercloud.AuthOptions - region string - interval time.Duration - port int +type Discovery interface { + Run(ctx context.Context, ch chan<- []*config.TargetGroup) + refresh() (tg *config.TargetGroup, err error) } // NewDiscovery returns a new OpenStackDiscovery which periodically refreshes its targets. -func NewDiscovery(conf *config.OpenstackSDConfig) (*Discovery, error) { +func NewDiscovery(conf *config.OpenstackSDConfig, l log.Logger) (Discovery, error) { opts := gophercloud.AuthOptions{ IdentityEndpoint: conf.IdentityEndpoint, Username: conf.Username, @@ -82,175 +62,16 @@ func NewDiscovery(conf *config.OpenstackSDConfig) (*Discovery, error) { DomainName: conf.DomainName, DomainID: conf.DomainID, } - - return &Discovery{ - authOpts: &opts, - region: conf.Region, - interval: time.Duration(conf.RefreshInterval), - port: conf.Port, - }, nil -} - -// Run implements the TargetProvider interface. -func (d *Discovery) Run(ctx context.Context, ch chan<- []*config.TargetGroup) { - // Get an initial set right away. - tg, err := d.refresh() - if err != nil { - log.Error(err) - } else { - select { - case ch <- []*config.TargetGroup{tg}: - case <-ctx.Done(): - return - } - } - - ticker := time.NewTicker(d.interval) - defer ticker.Stop() - - for { - select { - case <-ticker.C: - tg, err := d.refresh() - if err != nil { - log.Error(err) - continue - } - - select { - case ch <- []*config.TargetGroup{tg}: - case <-ctx.Done(): - return - } - case <-ctx.Done(): - return - } + switch conf.Role { + case config.OpenStackRoleHypervisor: + hypervisor := NewHypervisorDiscovery(&opts, + time.Duration(conf.RefreshInterval), conf.Port, conf.Region, l) + return hypervisor, nil + case config.OpenStackRoleInstance: + instance := NewInstanceDiscovery(&opts, + time.Duration(conf.RefreshInterval), conf.Port, conf.Region, l) + return instance, nil + default: + return nil, errors.New("unknown OpenStack discovery role") } } - -func (d *Discovery) refresh() (tg *config.TargetGroup, err error) { - t0 := time.Now() - defer func() { - refreshDuration.Observe(time.Since(t0).Seconds()) - if err != nil { - refreshFailuresCount.Inc() - } - }() - - provider, err := openstack.AuthenticatedClient(*d.authOpts) - - if err != nil { - return nil, fmt.Errorf("could not create OpenStack session: %s", err) - } - client, err := openstack.NewComputeV2(provider, gophercloud.EndpointOpts{ - Region: d.region, - }) - - if err != nil { - return nil, fmt.Errorf("could not create OpenStack compute session: %s", err) - } - - opts := servers.ListOpts{} - pager := servers.List(client, opts) - - tg = &config.TargetGroup{ - Source: fmt.Sprintf("OS_%s", d.region), - } - - pagerFIP := floatingips.List(client) - floatingIPList := make(map[string][]string) - - err = pagerFIP.EachPage(func(page pagination.Page) (bool, error) { - result, err := floatingips.ExtractFloatingIPs(page) - if err != nil { - log.Warn(err) - } - for _, ip := range result { - // Skip not associated ips - if ip.InstanceID != "" { - floatingIPList[ip.InstanceID] = append(floatingIPList[ip.InstanceID], ip.IP) - } - } - return true, nil - }) - - if err != nil { - return nil, fmt.Errorf("could not describe floating IPs: %s", err) - } - - err = pager.EachPage(func(page pagination.Page) (bool, error) { - serverList, err := servers.ExtractServers(page) - if err != nil { - return false, fmt.Errorf("could not extract servers: %s", err) - } - - for _, s := range serverList { - labels := model.LabelSet{ - openstackLabelInstanceID: model.LabelValue(s.ID), - } - - for _, address := range s.Addresses { - md, ok := address.([]interface{}) - if !ok { - log.Warn("Invalid type for address, expected array") - continue - } - - if len(md) == 0 { - log.Debugf("Got no IP address for instance %s", s.ID) - continue - } - - md1, ok := md[0].(map[string]interface{}) - if !ok { - log.Warn("Invalid type for address, expected dict") - continue - } - - addr, ok := md1["addr"].(string) - if !ok { - log.Warn("Invalid type for address, expected string") - continue - } - - labels[openstackLabelPrivateIP] = model.LabelValue(addr) - - addr = net.JoinHostPort(addr, fmt.Sprintf("%d", d.port)) - - labels[model.AddressLabel] = model.LabelValue(addr) - - // Only use first private IP - break - } - - if val, ok := floatingIPList[s.ID]; ok { - if len(val) > 0 { - labels[openstackLabelPublicIP] = model.LabelValue(val[0]) - } - } - - labels[openstackLabelInstanceStatus] = model.LabelValue(s.Status) - labels[openstackLabelInstanceName] = model.LabelValue(s.Name) - id, ok := s.Flavor["id"].(string) - if !ok { - log.Warn("Invalid type for instance id, excepted string") - continue - } - labels[openstackLabelInstanceFlavor] = model.LabelValue(id) - - for k, v := range s.Metadata { - name := strutil.SanitizeLabelName(k) - labels[openstackLabelTagPrefix+model.LabelName(name)] = model.LabelValue(v) - } - - tg.Targets = append(tg.Targets, labels) - } - return true, nil - }) - - if err != nil { - return nil, fmt.Errorf("could not describe instances: %s", err) - } - - return tg, nil -} diff --git a/documentation/examples/remote_storage/remote_storage_adapter/main.go b/documentation/examples/remote_storage/remote_storage_adapter/main.go index 3ea30bd8f5..b4bcfb8702 100644 --- a/documentation/examples/remote_storage/remote_storage_adapter/main.go +++ b/documentation/examples/remote_storage/remote_storage_adapter/main.go @@ -179,6 +179,7 @@ func buildClients(cfg *config) ([]writer, []reader) { writers = append(writers, c) readers = append(readers, c) } + log.Info("Starting up...") return writers, readers } @@ -186,18 +187,21 @@ func serve(addr string, writers []writer, readers []reader) error { http.HandleFunc("/write", func(w http.ResponseWriter, r *http.Request) { compressed, err := ioutil.ReadAll(r.Body) if err != nil { + log.Errorln("Read error:", err) http.Error(w, err.Error(), http.StatusInternalServerError) return } reqBuf, err := snappy.Decode(nil, compressed) if err != nil { + log.Errorln("Decode error:", err) http.Error(w, err.Error(), http.StatusBadRequest) return } var req prompb.WriteRequest if err := proto.Unmarshal(reqBuf, &req); err != nil { + log.Errorln("Unmarshal error:", err) http.Error(w, err.Error(), http.StatusBadRequest) return } @@ -219,18 +223,21 @@ func serve(addr string, writers []writer, readers []reader) error { http.HandleFunc("/read", func(w http.ResponseWriter, r *http.Request) { compressed, err := ioutil.ReadAll(r.Body) if err != nil { + log.Errorln("Read error:", err) http.Error(w, err.Error(), http.StatusInternalServerError) return } reqBuf, err := snappy.Decode(nil, compressed) if err != nil { + log.Errorln("Decode error:", err) http.Error(w, err.Error(), http.StatusBadRequest) return } var req prompb.ReadRequest if err := proto.Unmarshal(reqBuf, &req); err != nil { + log.Errorln("Unmarshal error:", err) http.Error(w, err.Error(), http.StatusBadRequest) return } diff --git a/notifier/notifier_test.go b/notifier/notifier_test.go index 29a6286e14..e915c52684 100644 --- a/notifier/notifier_test.go +++ b/notifier/notifier_test.go @@ -416,7 +416,7 @@ func TestLabelSetNotReused(t *testing.T) { func makeInputTargetGroup() *config.TargetGroup { return &config.TargetGroup{ Targets: []model.LabelSet{ - model.LabelSet{ + { model.AddressLabel: model.LabelValue("1.1.1.1:9090"), model.LabelName("notcommon1"): model.LabelValue("label"), }, diff --git a/promql/functions.go b/promql/functions.go index 98a667e6f9..71dffd3b4d 100644 --- a/promql/functions.go +++ b/promql/functions.go @@ -938,7 +938,7 @@ func dateWrapper(ev *evaluator, args Expressions, f func(time.Time) float64) Val v = Vector{ Sample{ Metric: labels.Labels{}, - Point: Point{V: float64(ev.Timestamp) / 1000}, + Point: Point{V: float64(ev.Timestamp) / 1000, T: ev.Timestamp}, }, } } else { diff --git a/util/testutil/roundtrip.go b/util/testutil/roundtrip.go index 42c1095278..996d11f368 100644 --- a/util/testutil/roundtrip.go +++ b/util/testutil/roundtrip.go @@ -37,7 +37,7 @@ func (rt *roundTripCheckRequest) RoundTrip(r *http.Request) (*http.Response, err } // NewRoundTripCheckRequest creates a new instance of a type that implements http.RoundTripper, -// wich before returning theResponse and theError, executes checkRequest against a http.Request. +// which before returning theResponse and theError, executes checkRequest against a http.Request. func NewRoundTripCheckRequest(checkRequest func(*http.Request), theResponse *http.Response, theError error) http.RoundTripper { return &roundTripCheckRequest{ checkRequest: checkRequest, diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/hypervisors/doc.go b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/hypervisors/doc.go new file mode 100644 index 0000000000..026f3ddf75 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/hypervisors/doc.go @@ -0,0 +1,3 @@ +// Package hypervisors gives information and control of the os-hypervisors +// portion of the compute API +package hypervisors diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/hypervisors/requests.go b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/hypervisors/requests.go new file mode 100644 index 0000000000..57cc19a71f --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/hypervisors/requests.go @@ -0,0 +1,13 @@ +package hypervisors + +import ( + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/pagination" +) + +// List makes a request against the API to list hypervisors. +func List(client *gophercloud.ServiceClient) pagination.Pager { + return pagination.NewPager(client, hypervisorsListDetailURL(client), func(r pagination.PageResult) pagination.Page { + return HypervisorPage{pagination.SinglePageBase(r)} + }) +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/hypervisors/results.go b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/hypervisors/results.go new file mode 100644 index 0000000000..844aa65c50 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/hypervisors/results.go @@ -0,0 +1,161 @@ +package hypervisors + +import ( + "encoding/json" + "fmt" + + "github.com/gophercloud/gophercloud/pagination" +) + +type Topology struct { + Sockets int `json:"sockets"` + Cores int `json:"cores"` + Threads int `json:"threads"` +} + +type CPUInfo struct { + Vendor string `json:"vendor"` + Arch string `json:"arch"` + Model string `json:"model"` + Features []string `json:"features"` + Topology Topology `json:"topology"` +} + +type Service struct { + Host string `json:"host"` + ID int `json:"id"` + DisabledReason string `json:"disabled_reason"` +} + +type Hypervisor struct { + // A structure that contains cpu information like arch, model, vendor, features and topology + CPUInfo CPUInfo `json:"-"` + // The current_workload is the number of tasks the hypervisor is responsible for. + // This will be equal or greater than the number of active VMs on the system + // (it can be greater when VMs are being deleted and the hypervisor is still cleaning up). + CurrentWorkload int `json:"current_workload"` + // Status of the hypervisor, either "enabled" or "disabled" + Status string `json:"status"` + // State of the hypervisor, either "up" or "down" + State string `json:"state"` + // Actual free disk on this hypervisor in GB + DiskAvailableLeast int `json:"disk_available_least"` + // The hypervisor's IP address + HostIP string `json:"host_ip"` + // The free disk remaining on this hypervisor in GB + FreeDiskGB int `json:"-"` + // The free RAM in this hypervisor in MB + FreeRamMB int `json:"free_ram_mb"` + // The hypervisor host name + HypervisorHostname string `json:"hypervisor_hostname"` + // The hypervisor type + HypervisorType string `json:"hypervisor_type"` + // The hypervisor version + HypervisorVersion int `json:"-"` + // Unique ID of the hypervisor + ID int `json:"id"` + // The disk in this hypervisor in GB + LocalGB int `json:"-"` + // The disk used in this hypervisor in GB + LocalGBUsed int `json:"local_gb_used"` + // The memory of this hypervisor in MB + MemoryMB int `json:"memory_mb"` + // The memory used in this hypervisor in MB + MemoryMBUsed int `json:"memory_mb_used"` + // The number of running vms on this hypervisor + RunningVMs int `json:"running_vms"` + // The hypervisor service object + Service Service `json:"service"` + // The number of vcpu in this hypervisor + VCPUs int `json:"vcpus"` + // The number of vcpu used in this hypervisor + VCPUsUsed int `json:"vcpus_used"` +} + +func (r *Hypervisor) UnmarshalJSON(b []byte) error { + + type tmp Hypervisor + var s struct { + tmp + CPUInfo interface{} `json:"cpu_info"` + HypervisorVersion interface{} `json:"hypervisor_version"` + FreeDiskGB interface{} `json:"free_disk_gb"` + LocalGB interface{} `json:"local_gb"` + } + + err := json.Unmarshal(b, &s) + if err != nil { + return err + } + + *r = Hypervisor(s.tmp) + + // Newer versions pass the CPU into around as the correct types, this just needs + // converting and copying into place. Older versions pass CPU info around as a string + // and can simply be unmarshalled by the json parser + var tmpb []byte + + switch t := s.CPUInfo.(type) { + case string: + tmpb = []byte(t) + case map[string]interface{}: + tmpb, err = json.Marshal(t) + if err != nil { + return err + } + default: + return fmt.Errorf("CPUInfo has unexpected type: %T", t) + } + + err = json.Unmarshal(tmpb, &r.CPUInfo) + if err != nil { + return err + } + + // These fields may be passed in in scientific notation + switch t := s.HypervisorVersion.(type) { + case int: + r.HypervisorVersion = t + case float64: + r.HypervisorVersion = int(t) + default: + return fmt.Errorf("Hypervisor version of unexpected type") + } + + switch t := s.FreeDiskGB.(type) { + case int: + r.FreeDiskGB = t + case float64: + r.FreeDiskGB = int(t) + default: + return fmt.Errorf("Free disk GB of unexpected type") + } + + switch t := s.LocalGB.(type) { + case int: + r.LocalGB = t + case float64: + r.LocalGB = int(t) + default: + return fmt.Errorf("Local GB of unexpected type") + } + + return nil +} + +type HypervisorPage struct { + pagination.SinglePageBase +} + +func (page HypervisorPage) IsEmpty() (bool, error) { + va, err := ExtractHypervisors(page) + return len(va) == 0, err +} + +func ExtractHypervisors(p pagination.Page) ([]Hypervisor, error) { + var h struct { + Hypervisors []Hypervisor `json:"hypervisors"` + } + err := (p.(HypervisorPage)).ExtractInto(&h) + return h.Hypervisors, err +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/hypervisors/urls.go b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/hypervisors/urls.go new file mode 100644 index 0000000000..5e6f679e96 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/hypervisors/urls.go @@ -0,0 +1,7 @@ +package hypervisors + +import "github.com/gophercloud/gophercloud" + +func hypervisorsListDetailURL(c *gophercloud.ServiceClient) string { + return c.ServiceURL("os-hypervisors", "detail") +} diff --git a/vendor/github.com/hashicorp/consul/api/agent.go b/vendor/github.com/hashicorp/consul/api/agent.go index 87a6c10016..1893d1cf35 100644 --- a/vendor/github.com/hashicorp/consul/api/agent.go +++ b/vendor/github.com/hashicorp/consul/api/agent.go @@ -1,6 +1,7 @@ package api import ( + "bufio" "fmt" ) @@ -73,6 +74,8 @@ type AgentServiceCheck struct { HTTP string `json:",omitempty"` TCP string `json:",omitempty"` Status string `json:",omitempty"` + Notes string `json:",omitempty"` + TLSSkipVerify bool `json:",omitempty"` // In Consul 0.7 and later, checks that are associated with a service // may also contain this optional DeregisterCriticalServiceAfter field, @@ -114,6 +117,17 @@ func (a *Agent) Self() (map[string]map[string]interface{}, error) { return out, nil } +// Reload triggers a configuration reload for the agent we are connected to. +func (a *Agent) Reload() error { + r := a.c.newRequest("PUT", "/v1/agent/reload") + _, resp, err := requireOK(a.c.doRequest(r)) + if err != nil { + return err + } + resp.Body.Close() + return nil +} + // NodeName is used to get the node name of the agent func (a *Agent) NodeName() (string, error) { if a.nodeName != "" { @@ -345,6 +359,17 @@ func (a *Agent) Join(addr string, wan bool) error { return nil } +// Leave is used to have the agent gracefully leave the cluster and shutdown +func (a *Agent) Leave() error { + r := a.c.newRequest("PUT", "/v1/agent/leave") + _, resp, err := requireOK(a.c.doRequest(r)) + if err != nil { + return err + } + resp.Body.Close() + return nil +} + // ForceLeave is used to have the agent eject a failed node func (a *Agent) ForceLeave(node string) error { r := a.c.newRequest("PUT", "/v1/agent/force-leave/"+node) @@ -409,3 +434,38 @@ func (a *Agent) DisableNodeMaintenance() error { resp.Body.Close() return nil } + +// Monitor returns a channel which will receive streaming logs from the agent +// Providing a non-nil stopCh can be used to close the connection and stop the +// log stream +func (a *Agent) Monitor(loglevel string, stopCh chan struct{}, q *QueryOptions) (chan string, error) { + r := a.c.newRequest("GET", "/v1/agent/monitor") + r.setQueryOptions(q) + if loglevel != "" { + r.params.Add("loglevel", loglevel) + } + _, resp, err := requireOK(a.c.doRequest(r)) + if err != nil { + return nil, err + } + + logCh := make(chan string, 64) + go func() { + defer resp.Body.Close() + + scanner := bufio.NewScanner(resp.Body) + for { + select { + case <-stopCh: + close(logCh) + return + default: + } + if scanner.Scan() { + logCh <- scanner.Text() + } + } + }() + + return logCh, nil +} diff --git a/vendor/github.com/hashicorp/consul/api/api.go b/vendor/github.com/hashicorp/consul/api/api.go index dd811fde4b..9a59b724cb 100644 --- a/vendor/github.com/hashicorp/consul/api/api.go +++ b/vendor/github.com/hashicorp/consul/api/api.go @@ -20,6 +20,28 @@ import ( "github.com/hashicorp/go-cleanhttp" ) +const ( + // HTTPAddrEnvName defines an environment variable name which sets + // the HTTP address if there is no -http-addr specified. + HTTPAddrEnvName = "CONSUL_HTTP_ADDR" + + // HTTPTokenEnvName defines an environment variable name which sets + // the HTTP token. + HTTPTokenEnvName = "CONSUL_HTTP_TOKEN" + + // HTTPAuthEnvName defines an environment variable name which sets + // the HTTP authentication header. + HTTPAuthEnvName = "CONSUL_HTTP_AUTH" + + // HTTPSSLEnvName defines an environment variable name which sets + // whether or not to use HTTPS. + HTTPSSLEnvName = "CONSUL_HTTP_SSL" + + // HTTPSSLVerifyEnvName defines an environment variable name which sets + // whether or not to disable certificate checking. + HTTPSSLVerifyEnvName = "CONSUL_HTTP_SSL_VERIFY" +) + // QueryOptions are used to parameterize a query type QueryOptions struct { // Providing a datacenter overwrites the DC provided @@ -52,6 +74,11 @@ type QueryOptions struct { // that node. Setting this to "_agent" will use the agent's node // for the sort. Near string + + // NodeMeta is used to filter results by nodes with the given + // metadata key/value pairs. Currently, only one key/value pair can + // be provided for filtering. + NodeMeta map[string]string } // WriteOptions are used to parameterize a write @@ -181,15 +208,15 @@ func defaultConfig(transportFn func() *http.Transport) *Config { }, } - if addr := os.Getenv("CONSUL_HTTP_ADDR"); addr != "" { + if addr := os.Getenv(HTTPAddrEnvName); addr != "" { config.Address = addr } - if token := os.Getenv("CONSUL_HTTP_TOKEN"); token != "" { + if token := os.Getenv(HTTPTokenEnvName); token != "" { config.Token = token } - if auth := os.Getenv("CONSUL_HTTP_AUTH"); auth != "" { + if auth := os.Getenv(HTTPAuthEnvName); auth != "" { var username, password string if strings.Contains(auth, ":") { split := strings.SplitN(auth, ":", 2) @@ -205,10 +232,10 @@ func defaultConfig(transportFn func() *http.Transport) *Config { } } - if ssl := os.Getenv("CONSUL_HTTP_SSL"); ssl != "" { + if ssl := os.Getenv(HTTPSSLEnvName); ssl != "" { enabled, err := strconv.ParseBool(ssl) if err != nil { - log.Printf("[WARN] client: could not parse CONSUL_HTTP_SSL: %s", err) + log.Printf("[WARN] client: could not parse %s: %s", HTTPSSLEnvName, err) } if enabled { @@ -216,10 +243,10 @@ func defaultConfig(transportFn func() *http.Transport) *Config { } } - if verify := os.Getenv("CONSUL_HTTP_SSL_VERIFY"); verify != "" { + if verify := os.Getenv(HTTPSSLVerifyEnvName); verify != "" { doVerify, err := strconv.ParseBool(verify) if err != nil { - log.Printf("[WARN] client: could not parse CONSUL_HTTP_SSL_VERIFY: %s", err) + log.Printf("[WARN] client: could not parse %s: %s", HTTPSSLVerifyEnvName, err) } if !doVerify { @@ -364,6 +391,11 @@ func (r *request) setQueryOptions(q *QueryOptions) { if q.Near != "" { r.params.Set("near", q.Near) } + if len(q.NodeMeta) > 0 { + for key, value := range q.NodeMeta { + r.params.Add("node-meta", key+":"+value) + } + } } // durToMsec converts a duration to a millisecond specified string. If the diff --git a/vendor/github.com/hashicorp/consul/api/catalog.go b/vendor/github.com/hashicorp/consul/api/catalog.go index 337772ec0b..10e93b42d9 100644 --- a/vendor/github.com/hashicorp/consul/api/catalog.go +++ b/vendor/github.com/hashicorp/consul/api/catalog.go @@ -4,18 +4,22 @@ type Node struct { Node string Address string TaggedAddresses map[string]string + Meta map[string]string } type CatalogService struct { Node string Address string TaggedAddresses map[string]string + NodeMeta map[string]string ServiceID string ServiceName string ServiceAddress string ServiceTags []string ServicePort int ServiceEnableTagOverride bool + CreateIndex uint64 + ModifyIndex uint64 } type CatalogNode struct { @@ -27,6 +31,7 @@ type CatalogRegistration struct { Node string Address string TaggedAddresses map[string]string + NodeMeta map[string]string Datacenter string Service *AgentService Check *AgentCheck diff --git a/vendor/github.com/hashicorp/consul/api/health.go b/vendor/github.com/hashicorp/consul/api/health.go index 74da949c8d..8abe2393ad 100644 --- a/vendor/github.com/hashicorp/consul/api/health.go +++ b/vendor/github.com/hashicorp/consul/api/health.go @@ -2,6 +2,7 @@ package api import ( "fmt" + "strings" ) const ( @@ -11,6 +12,15 @@ const ( HealthPassing = "passing" HealthWarning = "warning" HealthCritical = "critical" + HealthMaint = "maintenance" +) + +const ( + // NodeMaint is the special key set by a node in maintenance mode. + NodeMaint = "_node_maintenance" + + // ServiceMaintPrefix is the prefix for a service in maintenance mode. + ServiceMaintPrefix = "_service_maintenance:" ) // HealthCheck is used to represent a single check @@ -25,11 +35,56 @@ type HealthCheck struct { ServiceName string } +// HealthChecks is a collection of HealthCheck structs. +type HealthChecks []*HealthCheck + +// AggregatedStatus returns the "best" status for the list of health checks. +// Because a given entry may have many service and node-level health checks +// attached, this function determines the best representative of the status as +// as single string using the following heuristic: +// +// maintenance > critical > warning > passing +// +func (c HealthChecks) AggregatedStatus() string { + var passing, warning, critical, maintenance bool + for _, check := range c { + id := string(check.CheckID) + if id == NodeMaint || strings.HasPrefix(id, ServiceMaintPrefix) { + maintenance = true + continue + } + + switch check.Status { + case HealthPassing: + passing = true + case HealthWarning: + warning = true + case HealthCritical: + critical = true + default: + return "" + } + } + + switch { + case maintenance: + return HealthMaint + case critical: + return HealthCritical + case warning: + return HealthWarning + case passing: + return HealthPassing + default: + return HealthPassing + } +} + // ServiceEntry is used for the health service endpoint type ServiceEntry struct { Node *Node Service *AgentService - Checks []*HealthCheck + Checks HealthChecks } // Health can be used to query the Health endpoints @@ -43,7 +98,7 @@ func (c *Client) Health() *Health { } // Node is used to query for checks belonging to a given node -func (h *Health) Node(node string, q *QueryOptions) ([]*HealthCheck, *QueryMeta, error) { +func (h *Health) Node(node string, q *QueryOptions) (HealthChecks, *QueryMeta, error) { r := h.c.newRequest("GET", "/v1/health/node/"+node) r.setQueryOptions(q) rtt, resp, err := requireOK(h.c.doRequest(r)) @@ -56,7 +111,7 @@ func (h *Health) Node(node string, q *QueryOptions) ([]*HealthCheck, *QueryMeta, parseQueryMeta(resp, qm) qm.RequestTime = rtt - var out []*HealthCheck + var out HealthChecks if err := decodeBody(resp, &out); err != nil { return nil, nil, err } @@ -64,7 +119,7 @@ func (h *Health) Node(node string, q *QueryOptions) ([]*HealthCheck, *QueryMeta, } // Checks is used to return the checks associated with a service -func (h *Health) Checks(service string, q *QueryOptions) ([]*HealthCheck, *QueryMeta, error) { +func (h *Health) Checks(service string, q *QueryOptions) (HealthChecks, *QueryMeta, error) { r := h.c.newRequest("GET", "/v1/health/checks/"+service) r.setQueryOptions(q) rtt, resp, err := requireOK(h.c.doRequest(r)) @@ -77,7 +132,7 @@ func (h *Health) Checks(service string, q *QueryOptions) ([]*HealthCheck, *Query parseQueryMeta(resp, qm) qm.RequestTime = rtt - var out []*HealthCheck + var out HealthChecks if err := decodeBody(resp, &out); err != nil { return nil, nil, err } @@ -115,7 +170,7 @@ func (h *Health) Service(service, tag string, passingOnly bool, q *QueryOptions) // State is used to retrieve all the checks in a given state. // The wildcard "any" state can also be used for all checks. -func (h *Health) State(state string, q *QueryOptions) ([]*HealthCheck, *QueryMeta, error) { +func (h *Health) State(state string, q *QueryOptions) (HealthChecks, *QueryMeta, error) { switch state { case HealthAny: case HealthWarning: @@ -136,7 +191,7 @@ func (h *Health) State(state string, q *QueryOptions) ([]*HealthCheck, *QueryMet parseQueryMeta(resp, qm) qm.RequestTime = rtt - var out []*HealthCheck + var out HealthChecks if err := decodeBody(resp, &out); err != nil { return nil, nil, err } diff --git a/vendor/github.com/hashicorp/consul/api/kv.go b/vendor/github.com/hashicorp/consul/api/kv.go index 5262243a17..44e06bbb47 100644 --- a/vendor/github.com/hashicorp/consul/api/kv.go +++ b/vendor/github.com/hashicorp/consul/api/kv.go @@ -50,21 +50,21 @@ type KVOp string const ( KVSet KVOp = "set" - KVDelete = "delete" - KVDeleteCAS = "delete-cas" - KVDeleteTree = "delete-tree" - KVCAS = "cas" - KVLock = "lock" - KVUnlock = "unlock" - KVGet = "get" - KVGetTree = "get-tree" - KVCheckSession = "check-session" - KVCheckIndex = "check-index" + KVDelete KVOp = "delete" + KVDeleteCAS KVOp = "delete-cas" + KVDeleteTree KVOp = "delete-tree" + KVCAS KVOp = "cas" + KVLock KVOp = "lock" + KVUnlock KVOp = "unlock" + KVGet KVOp = "get" + KVGetTree KVOp = "get-tree" + KVCheckSession KVOp = "check-session" + KVCheckIndex KVOp = "check-index" ) // KVTxnOp defines a single operation inside a transaction. type KVTxnOp struct { - Verb string + Verb KVOp Key string Value []byte Flags uint64 @@ -156,7 +156,7 @@ func (k *KV) Keys(prefix, separator string, q *QueryOptions) ([]string, *QueryMe } func (k *KV) getInternal(key string, params map[string]string, q *QueryOptions) (*http.Response, *QueryMeta, error) { - r := k.c.newRequest("GET", "/v1/kv/"+key) + r := k.c.newRequest("GET", "/v1/kv/"+strings.TrimPrefix(key, "/")) r.setQueryOptions(q) for param, val := range params { r.params.Set(param, val) @@ -277,7 +277,7 @@ func (k *KV) DeleteTree(prefix string, w *WriteOptions) (*WriteMeta, error) { } func (k *KV) deleteInternal(key string, params map[string]string, q *WriteOptions) (bool, *WriteMeta, error) { - r := k.c.newRequest("DELETE", "/v1/kv/"+key) + r := k.c.newRequest("DELETE", "/v1/kv/"+strings.TrimPrefix(key, "/")) r.setWriteOptions(q) for param, val := range params { r.params.Set(param, val) diff --git a/vendor/github.com/hashicorp/consul/api/operator.go b/vendor/github.com/hashicorp/consul/api/operator.go index 48d74f3ca6..a8d04a38eb 100644 --- a/vendor/github.com/hashicorp/consul/api/operator.go +++ b/vendor/github.com/hashicorp/consul/api/operator.go @@ -43,6 +43,26 @@ type RaftConfiguration struct { Index uint64 } +// keyringRequest is used for performing Keyring operations +type keyringRequest struct { + Key string +} + +// KeyringResponse is returned when listing the gossip encryption keys +type KeyringResponse struct { + // Whether this response is for a WAN ring + WAN bool + + // The datacenter name this request corresponds to + Datacenter string + + // A map of the encryption keys to the number of nodes they're installed on + Keys map[string]int + + // The total number of nodes in this ring + NumNodes int +} + // RaftGetConfiguration is used to query the current Raft peer set. func (op *Operator) RaftGetConfiguration(q *QueryOptions) (*RaftConfiguration, error) { r := op.c.newRequest("GET", "/v1/operator/raft/configuration") @@ -79,3 +99,65 @@ func (op *Operator) RaftRemovePeerByAddress(address string, q *WriteOptions) err resp.Body.Close() return nil } + +// KeyringInstall is used to install a new gossip encryption key into the cluster +func (op *Operator) KeyringInstall(key string, q *WriteOptions) error { + r := op.c.newRequest("POST", "/v1/operator/keyring") + r.setWriteOptions(q) + r.obj = keyringRequest{ + Key: key, + } + _, resp, err := requireOK(op.c.doRequest(r)) + if err != nil { + return err + } + resp.Body.Close() + return nil +} + +// KeyringList is used to list the gossip keys installed in the cluster +func (op *Operator) KeyringList(q *QueryOptions) ([]*KeyringResponse, error) { + r := op.c.newRequest("GET", "/v1/operator/keyring") + r.setQueryOptions(q) + _, resp, err := requireOK(op.c.doRequest(r)) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + var out []*KeyringResponse + if err := decodeBody(resp, &out); err != nil { + return nil, err + } + return out, nil +} + +// KeyringRemove is used to remove a gossip encryption key from the cluster +func (op *Operator) KeyringRemove(key string, q *WriteOptions) error { + r := op.c.newRequest("DELETE", "/v1/operator/keyring") + r.setWriteOptions(q) + r.obj = keyringRequest{ + Key: key, + } + _, resp, err := requireOK(op.c.doRequest(r)) + if err != nil { + return err + } + resp.Body.Close() + return nil +} + +// KeyringUse is used to change the active gossip encryption key +func (op *Operator) KeyringUse(key string, q *WriteOptions) error { + r := op.c.newRequest("PUT", "/v1/operator/keyring") + r.setWriteOptions(q) + r.obj = keyringRequest{ + Key: key, + } + _, resp, err := requireOK(op.c.doRequest(r)) + if err != nil { + return err + } + resp.Body.Close() + return nil +} diff --git a/vendor/github.com/hashicorp/consul/api/prepared_query.go b/vendor/github.com/hashicorp/consul/api/prepared_query.go index 63e741e050..876e2e3b55 100644 --- a/vendor/github.com/hashicorp/consul/api/prepared_query.go +++ b/vendor/github.com/hashicorp/consul/api/prepared_query.go @@ -167,19 +167,18 @@ func (c *PreparedQuery) Get(queryID string, q *QueryOptions) ([]*PreparedQueryDe } // Delete is used to delete a specific prepared query. -func (c *PreparedQuery) Delete(queryID string, q *QueryOptions) (*QueryMeta, error) { +func (c *PreparedQuery) Delete(queryID string, q *WriteOptions) (*WriteMeta, error) { r := c.c.newRequest("DELETE", "/v1/query/"+queryID) - r.setQueryOptions(q) + r.setWriteOptions(q) rtt, resp, err := requireOK(c.c.doRequest(r)) if err != nil { return nil, err } defer resp.Body.Close() - qm := &QueryMeta{} - parseQueryMeta(resp, qm) - qm.RequestTime = rtt - return qm, nil + wm := &WriteMeta{} + wm.RequestTime = rtt + return wm, nil } // Execute is used to execute a specific prepared query. You can execute using diff --git a/vendor/vendor.json b/vendor/vendor.json index 55e0b9e50c..ed23fd4e55 100644 --- a/vendor/vendor.json +++ b/vendor/vendor.json @@ -563,6 +563,12 @@ "revision": "caf34a65f60295108141f62929245943bd00f237", "revisionTime": "2017-06-07T03:48:29Z" }, + { + "checksumSHA1": "SKxDWZElN5KwYPPf4QSs9pR0jKg=", + "path": "github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/hypervisors", + "revision": "caf34a65f60295108141f62929245943bd00f237", + "revisionTime": "2017-06-07T03:48:29Z" + }, { "checksumSHA1": "vTyXSR+Znw7/o/70UBOWG0F09r8=", "path": "github.com/gophercloud/gophercloud/openstack/compute/v2/flavors", @@ -645,10 +651,10 @@ "revisionTime": "2017-04-04T23:42:07Z" }, { - "checksumSHA1": "LclVLJYrBi03PBjsVPpgoMbUDQ8=", + "checksumSHA1": "/DReHn5j0caPm3thgFD9DmOmibQ=", "path": "github.com/hashicorp/consul/api", - "revision": "daacc4be8bee214e3fc4b32a6dd385f5ef1b4c36", - "revisionTime": "2016-10-28T04:06:46Z" + "revision": "23ce10f8891369f4c7758474c7c808f4e0262701", + "revisionTime": "2017-01-12T01:29:24Z" }, { "checksumSHA1": "Uzyon2091lmwacNsl1hCytjhHtg=", diff --git a/web/api/v1/api.go b/web/api/v1/api.go index e9ad59e5c3..a216119f39 100644 --- a/web/api/v1/api.go +++ b/web/api/v1/api.go @@ -28,6 +28,7 @@ import ( "github.com/prometheus/common/route" "golang.org/x/net/context" + "github.com/prometheus/prometheus/config" "github.com/prometheus/prometheus/pkg/labels" "github.com/prometheus/prometheus/pkg/timestamp" "github.com/prometheus/prometheus/promql" @@ -103,17 +104,19 @@ type API struct { targetRetriever targetRetriever alertmanagerRetriever alertmanagerRetriever - now func() time.Time + now func() time.Time + config func() config.Config } // NewAPI returns an initialized API type. -func NewAPI(qe *promql.Engine, q promql.Queryable, tr targetRetriever, ar alertmanagerRetriever) *API { +func NewAPI(qe *promql.Engine, q promql.Queryable, tr targetRetriever, ar alertmanagerRetriever, configFunc func() config.Config) *API { return &API{ QueryEngine: qe, Queryable: q, targetRetriever: tr, alertmanagerRetriever: ar, - now: time.Now, + now: time.Now, + config: configFunc, } } @@ -147,6 +150,8 @@ func (api *API) Register(r *route.Router) { r.Get("/targets", instr("targets", api.targets)) r.Get("/alertmanagers", instr("alertmanagers", api.alertmanagers)) + + r.Get("/status/config", instr("config", api.serveConfig)) } type queryData struct { @@ -425,6 +430,17 @@ func (api *API) alertmanagers(r *http.Request) (interface{}, *apiError) { return ams, nil } +type prometheusConfig struct { + YAML string `json:"yaml"` +} + +func (api *API) serveConfig(r *http.Request) (interface{}, *apiError) { + cfg := &prometheusConfig{ + YAML: api.config().String(), + } + return cfg, nil +} + func respond(w http.ResponseWriter, data interface{}) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) diff --git a/web/api/v1/api_test.go b/web/api/v1/api_test.go index 47a8919d2b..662accf7f4 100644 --- a/web/api/v1/api_test.go +++ b/web/api/v1/api_test.go @@ -29,6 +29,7 @@ import ( "github.com/prometheus/common/route" "golang.org/x/net/context" + "github.com/prometheus/prometheus/config" "github.com/prometheus/prometheus/pkg/labels" "github.com/prometheus/prometheus/pkg/timestamp" "github.com/prometheus/prometheus/promql" @@ -47,6 +48,15 @@ func (f alertmanagerRetrieverFunc) Alertmanagers() []*url.URL { return f() } +var samplePrometheusCfg = config.Config{ + GlobalConfig: config.GlobalConfig{}, + AlertingConfig: config.AlertingConfig{}, + RuleFiles: []string{}, + ScrapeConfigs: []*config.ScrapeConfig{}, + RemoteWriteConfigs: []*config.RemoteWriteConfig{}, + RemoteReadConfigs: []*config.RemoteReadConfig{}, +} + func TestEndpoints(t *testing.T) { suite, err := promql.NewTest(t, ` load 1m @@ -92,7 +102,8 @@ func TestEndpoints(t *testing.T) { QueryEngine: suite.QueryEngine(), targetRetriever: tr, alertmanagerRetriever: ar, - now: func() time.Time { return now }, + now: func() time.Time { return now }, + config: func() config.Config { return samplePrometheusCfg }, } start := time.Unix(0, 0) @@ -402,6 +413,19 @@ func TestEndpoints(t *testing.T) { endpoint: api.dropSeries, errType: errorInternal, }, + { + endpoint: api.targets, + response: &TargetDiscovery{ + ActiveTargets: []*Target{ + { + DiscoveredLabels: map[string]string{}, + Labels: map[string]string{}, + ScrapeURL: "http://example.com:8080/metrics", + Health: "unknown", + }, + }, + }, + }, { endpoint: api.alertmanagers, response: &AlertmanagerDiscovery{ @@ -412,6 +436,12 @@ func TestEndpoints(t *testing.T) { }, }, }, + { + endpoint: api.serveConfig, + response: &prometheusConfig{ + YAML: samplePrometheusCfg.String(), + }, + }, } for _, test := range tests { diff --git a/web/federate.go b/web/federate.go index 8307523929..a07e693c8f 100644 --- a/web/federate.go +++ b/web/federate.go @@ -121,7 +121,7 @@ func (h *Handler) federation(w http.ResponseWriter, req *http.Request) { sort.Sort(byName(vec)) - externalLabels := h.externalLabels.Clone() + externalLabels := h.config.GlobalConfig.ExternalLabels.Clone() if _, ok := externalLabels[model.InstanceLabel]; !ok { externalLabels[model.InstanceLabel] = "" } diff --git a/web/federate_test.go b/web/federate_test.go index 765c8ec54e..656bd541f3 100644 --- a/web/federate_test.go +++ b/web/federate_test.go @@ -23,6 +23,7 @@ import ( "testing" "github.com/prometheus/common/model" + "github.com/prometheus/prometheus/config" "github.com/prometheus/prometheus/promql" ) @@ -202,10 +203,13 @@ func TestFederation(t *testing.T) { storage: suite.Storage(), queryEngine: suite.QueryEngine(), now: func() model.Time { return 101 * 60 * 1000 }, // 101min after epoch. + config: &config.Config{ + GlobalConfig: config.GlobalConfig{}, + }, } for name, scenario := range scenarios { - h.externalLabels = scenario.externalLabels + h.config.GlobalConfig.ExternalLabels = scenario.externalLabels req, err := http.ReadRequest(bufio.NewReader(strings.NewReader( "GET http://example.org/federate?" + scenario.params + " HTTP/1.0\r\n\r\n", ))) diff --git a/web/ui/bindata.go b/web/ui/bindata.go index e60f9c3c9a..fdb41e0d52 100644 --- a/web/ui/bindata.go +++ b/web/ui/bindata.go @@ -108,7 +108,7 @@ func (fi bindataFileInfo) Sys() interface{} { return nil } -var _webUiTemplates_baseHtml = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\xbc\x56\xdd\x6e\xdc\x36\x13\xbd\xcf\x53\xcc\xc7\x04\x5f\xec\x0b\xad\x50\xe4\xa6\x88\x25\x15\x89\xe3\x34\x06\x82\x66\x61\x6f\x83\x16\x45\x61\xcc\x4a\x23\x89\x0e\x45\x2a\xe4\x48\xf5\x62\xb1\xef\x5e\x70\xf5\x53\xad\xec\xb5\xfb\x87\x5e\x89\xa2\x0e\xcf\x0c\xcf\x9c\xa1\x18\xfd\xef\xdd\xa7\xf3\xd5\xcf\xcb\x0b\x28\xb9\x52\xc9\xb3\xc8\x3f\x40\xa1\x2e\x62\x41\x5a\x24\xcf\x00\xa2\x92\x30\xf3\x03\x80\xa8\x22\x46\x28\x99\xeb\x80\xbe\x36\xb2\x8d\xc5\xb9\xd1\x4c\x9a\x83\xd5\xa6\x26\x01\x69\xf7\x16\x0b\xa6\x3b\x0e\x3d\xd5\x19\xa4\x25\x5a\x47\x1c\x37\x9c\x07\xdf\x8a\x9e\x87\x25\x2b\x4a\x96\xd6\x54\xc4\x25\x35\x0e\x56\xb2\x22\xb8\x26\x2b\xc9\xc1\xb9\x51\x8a\x52\x96\x46\x03\xea\x0c\x96\xd6\xa4\xe4\x9c\xd4\x85\x07\xb4\x64\xa3\xb0\x5b\xde\x51\x29\xa9\xbf\x80\x25\x15\x0b\x57\x1a\xcb\x69\xc3\x20\x53\xa3\x05\x94\x96\xf2\x58\x6c\xb7\x50\x23\x97\x4b\x4b\xb9\xbc\x83\xdd\x2e\x74\x8c\x2c\xd3\x50\x56\x45\x98\x63\xeb\xa1\x0b\x99\x9a\xef\xda\x78\xbb\x85\x75\x23\x55\xf6\x99\xac\xf3\xb1\x77\xbb\x21\x5b\x97\x5a\x59\x33\x38\x9b\x1e\xe7\x6b\x49\x67\xc6\x86\xb7\x2e\xbc\xfd\xda\x90\xdd\x2c\x2a\xa9\x17\xb7\xee\x08\x6f\x14\x76\x9c\x7f\x3d\xc0\xda\x18\x76\x6c\xb1\x0e\x5e\x2d\x5e\x2d\xbe\xf1\x01\xc7\xa9\x3f\x1b\x73\x22\x1c\x6f\x6a\xea\xcb\x95\x3a\x27\x7a\x21\x79\xa3\xc8\x95\x44\xfc\x94\x8a\x47\x92\x4a\xdd\x3c\xab\xd4\x1d\x4b\xeb\xdf\x4b\xc6\x47\xad\x47\x4b\x3d\x16\x72\xaa\x7a\x97\x00\x40\x8b\x16\x96\x6f\x56\x1f\x6e\x96\x57\x17\xef\x2f\x7f\x82\x18\xee\x05\x12\x67\x13\xec\xdb\x1f\x2f\x3f\xbe\xbb\xf9\x7c\x71\x75\x7d\xf9\xe9\x87\x1e\x3d\x8f\x34\xe0\x5f\x9c\xe4\x8d\xee\x1c\x7d\x72\x0a\xdb\x7e\xd6\xcf\xbf\xfc\x25\x43\xc6\x80\x4d\x51\x28\xbf\x77\x63\x14\xcb\x5a\xfc\xfa\xf2\x74\xd1\x8f\x4f\x4e\x7b\xf8\xae\x1b\xcc\xca\xb8\xdd\x32\x55\xb5\x42\x26\x10\xbe\x51\x05\x2c\x76\x3b\xdf\xb5\x61\xd7\xb6\x7e\xb8\x36\xd9\xa6\xd7\x59\x63\x0b\xa9\x42\xe7\x62\xa1\xb1\x5d\xa3\x85\xee\x11\x48\xdd\x92\x75\x34\xbc\xe6\xf2\x8e\xb2\x80\x4d\x2d\x06\x7d\xa2\x4c\x8e\x4b\x7d\x9f\xa3\xd4\x64\x83\x5c\x35\x32\x1b\x31\x87\xa8\x9e\xca\xe7\x41\x76\x82\xf1\x19\x35\xcc\x46\xf7\x05\xef\x5e\xc4\x6c\x59\x27\x09\xa4\x46\x29\xac\x1d\x65\x02\x0e\x94\x1a\xe6\x87\x69\xb4\x05\x71\x2c\x9e\x77\xab\x05\xa0\x95\x18\xd0\x5d\x8d\x3a\xa3\x2c\x16\x39\x2a\x8f\xdd\xcf\xfa\xec\xad\x51\x63\xa8\x83\xd4\xbc\x2f\x6a\xd4\x43\x32\xce\x06\x46\xab\x8d\x48\x56\x5d\x3a\x1a\x5b\x59\xa0\xaf\x64\x14\x7a\xdc\x23\x4b\xfd\xd1\x12\xec\xe9\xff\x2b\x68\x14\x76\x52\x1e\xcc\xe1\x4c\xd7\xb5\x45\x9d\x1d\x6d\x25\x31\x39\x94\xa3\x10\x27\x85\x0d\x33\xd9\xce\xea\x2c\xb3\x51\xc2\x59\x90\xa1\x3a\x63\xf9\x0e\xcb\xdf\xa8\x09\x7e\xb0\xdc\x64\xa8\x28\xe7\x59\x55\xb6\xdb\x17\xa9\xd1\xce\x28\x72\xf0\x3a\x86\x61\xbc\x44\x2e\xf7\x7e\x9f\x22\x65\x0e\x23\x78\xf6\x31\x52\x32\x89\x70\xdc\xfd\x04\x26\x92\xf3\x7e\xec\xf7\x1d\x85\x4a\xce\x13\x00\xd2\x19\x3c\xce\x37\x53\x13\x15\x59\x76\x22\x79\xb3\x7f\x3e\xcc\xfb\x38\x43\x61\xb1\x2e\x45\xf2\xbd\x7f\x1c\x5d\x3f\x88\x99\x59\x53\x67\xe6\x37\x3d\x93\x6e\x6f\x82\x8e\xff\xb9\x98\x63\xfb\x86\x9a\x75\xd7\xc8\x04\xd6\xa8\x49\x8b\xee\xfb\xa7\x44\x57\x9b\xba\xa9\x63\xc1\xb6\xa1\x23\xad\x96\x5c\x33\x72\xe3\x0e\xcd\x9b\xa2\x25\x1e\x9d\x7b\xe0\xaf\x7b\xce\x18\x13\xac\x48\x37\xf7\x76\xf4\x94\x6e\x6e\x1f\x5d\x24\x57\x8d\x66\x7f\xb5\xf8\x3f\x56\xf5\x19\xbc\xf5\xe7\x33\x5c\xea\xdc\xd8\xaa\x6f\xe2\x87\x24\x7d\x9a\x3e\x57\x58\x38\xef\x98\xaa\x42\x9d\x05\x1f\xa5\x26\x78\xef\xe7\xfe\x2e\x61\x6a\x74\x2e\x8b\xbd\x07\x73\x59\x34\xf6\x1f\x65\x67\x1b\x45\xfb\xbd\x1f\x35\xf3\xd3\x1c\xdd\x81\xea\x44\xb2\xea\x06\xc7\x78\xa2\xb0\x51\x33\x43\x3e\x68\xf1\x63\x8e\xf4\x97\x49\xf7\x3a\x9c\xfe\xb8\xa5\x11\x30\x9c\xe7\x37\x6b\x85\xfa\x8b\x48\x3e\x90\xaa\xef\xf9\x65\x1e\xe9\x30\x97\x83\x13\x6b\xf2\x12\x85\x1a\xdb\x07\xfe\x9e\xfd\xe5\xf5\x8f\x1f\x68\xf7\xdb\x8c\xc2\xee\x66\xfc\x7b\x00\x00\x00\xff\xff\x64\xff\x3c\x9c\x2a\x0b\x00\x00") +var _webUiTemplates_baseHtml = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\xbc\x56\xdd\x6e\xdc\x36\x13\xbd\xcf\x53\xcc\xc7\x04\x5f\xec\x0b\xad\x50\xe4\xa6\x88\x25\x15\x89\xe3\x34\x06\x82\x66\x61\x6f\x83\x16\x45\x61\xcc\x4a\x23\x89\x0e\x45\x2a\xe4\x48\xf5\x62\xb1\xef\x5e\x70\xf5\x53\xad\xec\xb5\xfb\x87\x5e\x89\xa2\x0e\xcf\x0c\xcf\x9c\xa1\x18\xfd\xef\xdd\xa7\xf3\xd5\xcf\xcb\x0b\x28\xb9\x52\xc9\xb3\xc8\x3f\x40\xa1\x2e\x62\x41\x5a\x24\xcf\x00\xa2\x92\x30\xf3\x03\x80\xa8\x22\x46\x28\x99\xeb\x80\xbe\x36\xb2\x8d\xc5\xb9\xd1\x4c\x9a\x83\xd5\xa6\x26\x01\x69\xf7\x16\x0b\xa6\x3b\x0e\x3d\xd5\x19\xa4\x25\x5a\x47\x1c\x37\x9c\x07\xdf\x8a\x9e\x87\x25\x2b\x4a\x96\xd6\x54\xc4\x25\x35\x0e\x56\xb2\x22\xb8\x26\x2b\xc9\xc1\xb9\x51\x8a\x52\x96\x46\x03\xea\x0c\x96\xd6\xa4\xe4\x9c\xd4\x85\x07\xb4\x64\xa3\xb0\x5b\xde\x51\x29\xa9\xbf\x80\x25\x15\x0b\x57\x1a\xcb\x69\xc3\x20\x53\xa3\x05\x94\x96\xf2\x58\x6c\xb7\x50\x23\x97\x4b\x4b\xb9\xbc\x83\xdd\x2e\x74\x8c\x2c\xd3\x50\x56\x45\x98\x63\xeb\xa1\x0b\x99\x9a\xef\xda\x78\xbb\x85\x75\x23\x55\xf6\x99\xac\xf3\xb1\x77\xbb\x21\x5b\x97\x5a\x59\x33\x38\x9b\x1e\xe7\x6b\x49\x67\xc6\x86\xb7\x2e\xbc\xfd\xda\x90\xdd\x2c\x2a\xa9\x17\xb7\xee\x08\x6f\x14\x76\x9c\x7f\x3d\xc0\xda\x18\x76\x6c\xb1\x0e\x5e\x2d\x5e\x2d\xbe\xf1\x01\xc7\xa9\x3f\x1b\x73\x22\x1c\x6f\x6a\xea\xcb\x95\x3a\x27\x7a\x21\x79\xa3\xc8\x95\x44\xfc\x94\x8a\x47\x92\x4a\xdd\x3c\xab\xd4\x1d\x4b\xeb\xdf\x4b\xc6\x47\xad\x47\x4b\x3d\x16\x72\xaa\x7a\x97\x00\x40\x8b\x16\x96\x6f\x56\x1f\x6e\x96\x57\x17\xef\x2f\x7f\x82\x18\xee\x05\x12\x67\x13\xec\xdb\x1f\x2f\x3f\xbe\xbb\xf9\x7c\x71\x75\x7d\xf9\xe9\x87\x1e\x3d\x8f\x34\xe0\x5f\x9c\xe4\x8d\xee\x1c\x7d\x72\x0a\xdb\x7e\xd6\xcf\xbf\xfc\x25\x43\xc6\x80\x4d\x51\x28\xbf\x77\x63\x14\xcb\x5a\xfc\xfa\xf2\x74\xd1\x8f\x4f\x4e\x7b\xf8\xae\x1b\xcc\xca\xb8\xdd\x32\x55\xb5\x42\x26\x10\xbe\x51\x05\x2c\x76\x3b\xdf\xb5\x61\xd7\xb6\x7e\xb8\x36\xd9\xa6\xd7\x59\x63\x0b\xa9\x42\xe7\x62\xa1\xb1\x5d\xa3\x85\xee\x11\x48\xdd\x92\x75\x34\xbc\xe6\xf2\x8e\xb2\x80\x4d\x2d\x06\x7d\xa2\x4c\x8e\x4b\x7d\x9f\xa3\xd4\x64\x83\x5c\x35\x32\x1b\x31\x87\xa8\x9e\xca\xe7\x41\x76\x82\xf1\x19\x35\xcc\x46\xf7\x05\xef\x5e\xc4\x6c\x59\x27\x09\xa4\x46\x29\xac\x1d\x65\x02\x0e\x94\x1a\xe6\x87\x69\xb4\x05\x71\x2c\x9e\x77\xab\x05\xa0\x95\x18\xd0\x5d\x8d\x3a\xa3\x2c\x16\x39\x2a\x8f\xdd\xcf\xfa\xec\xad\x51\x63\xa8\x83\xd4\xbc\x2f\x6a\xd4\x43\x32\xce\x06\x46\xab\x8d\x48\x56\x5d\x3a\x1a\x5b\x59\xa0\xaf\x64\x14\x7a\xdc\x23\x4b\xfd\xd1\x12\xec\xe9\xff\x2b\x68\x14\x76\x52\x1e\xcc\xe1\x4c\xd7\xb5\x45\x9d\x1d\x6d\x25\x31\x39\x94\xa3\x10\x27\x85\x0d\x33\xd9\xce\xea\x2c\xb3\x51\xc2\x59\x90\xa1\x3a\x63\xf9\x0e\xcb\xdf\xa8\x09\x7e\xb0\xdc\x64\xa8\x28\xe7\x59\x55\xb6\xdb\x17\xa9\xd1\xce\x28\x72\xf0\x3a\x86\x61\xbc\x44\x2e\xf7\x7e\x9f\x22\x65\x0e\x23\x78\xf6\x31\x52\x32\x89\x70\xdc\xfd\x04\x26\x92\xf3\x7e\xec\xf7\x1d\x85\x4a\xce\x13\x00\xd2\x19\x3c\xce\x37\x53\x13\x15\x59\x76\x22\x79\xb3\x7f\x3e\xcc\xfb\x38\x43\x61\xb1\x2e\x45\xf2\xbd\x7f\x1c\x5d\x3f\x88\x99\x59\x53\x67\xe6\x37\x3d\x93\x6e\x6f\x82\x8e\xff\xb9\x98\x63\xfb\x86\x9a\x75\xd7\xc8\x04\xd6\xa8\x49\x8b\xee\xfb\xa7\x44\x57\x9b\xba\xa9\x63\xc1\xb6\xa1\x23\xad\x96\x5c\x33\x72\xe3\x0e\xcd\x9b\xa2\x25\x1e\x9d\x7b\xe0\xaf\x7b\xce\x18\x13\xac\x48\x37\xf7\x76\xf4\x94\x6e\x6e\x1f\x5d\x24\x57\x8d\x66\x7f\xb5\xf8\x3f\x56\xf5\x19\xbc\xf5\xe7\x33\x5c\xea\xdc\xd8\xaa\x6f\xe2\x87\x24\x7d\x9a\x3e\x57\x58\x38\xef\x98\xaa\x42\x9d\x05\x1f\xa5\x26\x78\xef\xe7\xfe\x2e\x61\x6a\x74\x2e\x8b\xbd\x07\x73\x59\x34\xf6\x1f\x65\x67\x1b\x45\xfb\xbd\x1f\x35\xf3\xd3\x1c\xdd\x81\xea\x44\xb2\xea\x06\xc7\x78\xa2\xb0\x51\x33\x43\x3e\x68\xf1\x63\x8e\xf4\x97\x49\xf7\x3a\x9c\xfe\xb8\xa5\x09\x33\x93\x3a\x01\xc3\xa1\x7e\xb3\x56\xa8\xbf\x88\xe4\x03\xa9\xfa\x9e\x69\xe6\xe1\x0e\x13\x3a\x38\xb6\x26\x2f\x51\xa8\xb1\x7d\xe0\x17\xda\xdf\x60\xff\xf8\x8b\x76\xff\xce\x28\xec\xae\xc7\xbf\x07\x00\x00\xff\xff\x54\x18\xdc\x13\x2f\x0b\x00\x00") func webUiTemplates_baseHtmlBytes() ([]byte, error) { return bindataRead( @@ -123,7 +123,7 @@ func webUiTemplates_baseHtml() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "web/ui/templates/_base.html", size: 2858, mode: os.FileMode(420), modTime: time.Unix(1495630073, 0)} + info := bindataFileInfo{name: "web/ui/templates/_base.html", size: 2863, mode: os.FileMode(420), modTime: time.Unix(1504523927, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -143,7 +143,7 @@ func webUiTemplatesAlertsHtml() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "web/ui/templates/alerts.html", size: 1836, mode: os.FileMode(420), modTime: time.Unix(1502369604, 0)} + info := bindataFileInfo{name: "web/ui/templates/alerts.html", size: 1836, mode: os.FileMode(420), modTime: time.Unix(1504523923, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -223,7 +223,7 @@ func webUiTemplatesRulesHtml() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "web/ui/templates/rules.html", size: 283, mode: os.FileMode(420), modTime: time.Unix(1502369604, 0)} + info := bindataFileInfo{name: "web/ui/templates/rules.html", size: 283, mode: os.FileMode(420), modTime: time.Unix(1504523923, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -248,7 +248,7 @@ func webUiTemplatesStatusHtml() (*asset, error) { return a, nil } -var _webUiTemplatesTargetsHtml = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\xc4\x56\x4d\x6f\xe3\x36\x13\xbe\xfb\x57\x0c\xf4\x06\x2f\x5a\x60\x65\x01\x0b\xf4\x92\x52\x2a\xd0\x76\x81\x2d\x90\x16\xe9\x66\xb7\x87\x5e\x16\x94\x38\xb6\x98\x30\xa4\x4a\x8e\x8c\x35\xb8\xfc\xef\x05\x29\xc9\x76\x12\xc9\x69\xd3\x2e\x7a\x91\x4d\xce\xcc\x43\x3e\xc3\xf9\xf2\x5e\xe0\x46\x6a\x84\xac\x45\x2e\xb2\x10\x56\x4c\x49\x7d\x07\xb4\xef\xb0\xcc\x08\x3f\x51\xd1\x38\x97\x81\x45\x55\x66\x8e\xf6\x0a\x5d\x8b\x48\x19\xb4\x16\x37\x65\xe6\x3d\x74\x9c\xda\x6b\x8b\x1b\xf9\x09\x42\x28\x1c\x71\x92\x4d\xb4\x29\x88\xdb\x2d\x92\x5b\x37\xce\x7d\xb7\x2b\xbd\x87\xba\x97\x4a\xfc\x86\xd6\x49\xa3\x21\x84\xac\x5a\x31\xd7\x58\xd9\x11\x38\xdb\x2c\x63\xdd\x1e\xa1\x6e\x97\x90\x58\x31\x20\x55\x2b\xef\x51\x8b\x10\x56\xab\x23\xb3\xc6\x68\x42\x4d\x91\x1c\x00\x13\x72\x07\x8d\xe2\xce\x95\x49\xc0\xa5\x46\x9b\x6f\x54\x2f\x45\x56\xad\x00\x00\x58\xfb\x1a\xa4\x28\xb3\xf1\xd0\xac\x7a\x3f\xfc\x61\x45\xfb\x7a\xd0\x00\x60\xc4\x6b\x85\x13\xce\xb0\x48\xdf\xbc\x31\x5a\xa0\x76\x28\xc6\x75\x6d\xac\x40\x7b\x58\xb6\x66\x87\x36\x9b\x60\x00\xbc\xb7\x5c\x6f\x11\x2e\x6e\x4d\xfd\x0a\x2e\x3a\x63\x14\x5c\x96\xb0\x1e\xce\xbc\x36\x46\x39\x48\xf7\x3e\x1a\x5c\xb4\xc8\x15\xb5\xfb\xa8\xa7\xfb\xfb\xb7\xe3\x2a\xd9\x3e\x56\x25\x43\x3c\x01\x2a\xd4\x33\x1a\x91\x88\x9d\x58\xdc\x9a\xfa\x63\x0c\x02\xb4\xde\xcb\x0d\x28\x82\xc3\x49\x03\x4e\x08\x20\xe2\x65\xed\xe8\xe3\x13\x1a\x13\x98\x80\xc6\x28\xd7\x71\x5d\x66\xdf\x3c\x11\x03\x30\x39\x1d\x26\x1b\xa3\xf3\xa6\xc5\x9d\x35\x3a\xef\xbb\xf8\x82\xb2\x62\x3c\x39\xfe\xd6\xd4\xb9\xf7\xd1\x23\x21\x4c\x81\xf6\xbf\x07\x9b\xd5\xf4\x0f\xbe\x3a\xfa\x23\x84\x62\x62\x1c\x02\xf4\xdd\xd7\xac\xe0\x4f\x6e\x58\x90\x78\xb8\xc7\x0a\xb2\xd5\xbc\x4b\xa0\x84\xe4\x14\x81\xc4\xa5\x72\x73\x74\x67\x28\xbe\x34\x32\x1c\x59\xd9\x2d\xc6\xc9\xe9\x01\xf1\x91\xe6\x24\xe9\xea\xf3\x82\x64\x56\xbd\xd1\xa2\x33\x52\x13\x2b\xa8\x3d\xa7\x77\x43\x9c\xf0\x39\xa5\x2b\x5e\xa3\x72\xcf\x6b\x39\x82\x9b\xc6\xf2\xee\x59\xc0\x37\xd6\x1a\xbb\xac\xf4\xf4\xa1\x0e\xfb\x4b\x0e\x61\x54\x1b\xb1\x9f\x93\x1c\xd2\x6e\x26\x25\x8e\xd6\x67\x9c\xb9\xf0\x00\x49\xc8\x0f\xc5\x71\xfd\xe1\xdd\x15\x7c\x86\xad\x32\x35\x57\x1f\xde\x5d\x0d\xa1\x1b\x77\xd7\x37\x4d\x8b\xf7\x18\xc2\x65\x51\x8c\x3b\x6f\x8d\xa3\x10\xc6\xc5\x35\xa7\x36\x84\x18\xc1\xac\x5e\xbc\xc6\x09\x0f\x15\x5f\xe3\x15\x5c\xec\xb8\xea\xd1\xa5\x12\x12\x61\x7e\xed\xd1\xee\x61\x81\xe0\x23\x08\x39\x99\x47\xeb\x11\xe8\xac\x25\x00\x8b\xa9\x3e\xc5\x7a\xba\x02\xa4\x6f\xde\x59\x79\xcf\xed\x3e\x25\x6a\xda\x09\x21\xfa\x63\x40\x0d\x21\x63\x45\xb4\x5c\xe6\x15\xaf\x35\xd4\xf1\x97\xc9\x9f\xe6\xf9\x89\xec\xec\xe3\x9d\x32\xe2\x0a\x2d\x41\xfa\xe6\xde\xc3\x7a\x28\xb5\xf0\x19\x86\x8a\xf3\xde\xfc\x90\xca\x44\x08\x10\x5b\x15\x7e\x94\x5a\xc8\x86\x93\xb1\x10\x1b\x67\xde\x77\x1d\xda\x86\x3b\x9c\xcd\xe3\x23\x91\x11\xf7\x0c\xd9\xf3\xee\xfa\x77\xc8\x36\xbd\x75\xc6\xe6\xa9\x40\xa0\xcd\x40\x70\xe2\x39\x99\xed\x56\xc5\x41\xc0\x18\x45\xb2\xcb\x80\x24\xc5\xf5\x28\x6e\xe9\x5e\x95\x64\x7b\x1c\x96\xc6\xca\xad\xd4\x5c\xe5\xa3\x16\xab\xab\xef\x71\x63\x2c\xc6\xf1\x21\x46\x81\xd4\xdb\x4b\x56\xd4\xd5\x21\xe6\xee\x62\xcc\xa5\x68\xfd\x51\xba\x26\xd6\x3c\x14\x43\x61\x09\x21\x06\xbe\xf7\x17\x77\xd1\xdf\x74\xaf\xc6\x9f\x10\xca\xff\xff\xd1\x1b\xfa\x36\x46\xd3\x63\xd1\x24\x99\x6f\x4f\x0f\xbd\x3e\xc4\x65\x4a\x95\x54\x7a\x87\x63\x61\x3d\xfc\xae\x7f\xe6\x5d\xaa\xfe\xd9\x5f\x4b\x9e\x07\xf9\x97\x12\x48\x8d\x34\xfe\xc3\x04\x52\x0e\x5f\x7a\xbe\xc0\x0d\xef\x15\x65\x95\x36\x1a\xff\x79\xb6\x7e\xa1\x00\x4e\x83\xca\x3a\xf6\x98\xa1\xc5\xac\x7f\x72\xbf\xa3\x35\x21\xfc\x82\xbb\x34\xa6\x24\x0f\x78\xef\xa4\x6e\xf0\x54\x31\x04\xe0\x5b\xf3\x85\x6a\xc8\xf1\x56\xa9\xa5\x9d\x73\xcb\x52\xb5\x19\xe6\xac\xc7\x65\x25\x35\x8f\x13\xdc\xe7\xde\xe5\xa5\xfc\x96\x5a\xed\x32\x1e\x2b\x16\x5a\x2d\x2b\xd2\x3c\xf3\x77\xe7\xb0\x87\x27\x9d\x80\xb0\x42\xc8\xdd\x61\xca\xff\x33\x00\x00\xff\xff\x65\x7b\xa9\x65\xbe\x0c\x00\x00") +var _webUiTemplatesTargetsHtml = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\xc4\x56\x4d\x8f\xdb\x36\x13\xbe\xfb\x57\x0c\xf4\x1a\x2f\x5a\x20\xb2\x80\x00\xbd\x6c\x29\x15\x68\x1b\x20\x05\xb6\xc5\x36\x9b\xf4\xd0\x4b\x40\x89\x63\x8b\xbb\x5c\x52\x25\x47\x46\x0c\x86\xff\xbd\x20\x25\xf9\x63\xd7\xf2\xb6\xdb\x06\xbd\xc8\x26\x67\xe6\xe1\x3c\xc3\xf9\xa0\xf7\x02\xd7\x52\x23\x64\x2d\x72\x91\x85\xb0\x60\x4a\xea\x7b\xa0\x5d\x87\x65\x46\xf8\x89\x8a\xc6\xb9\x0c\x2c\xaa\x32\x73\xb4\x53\xe8\x5a\x44\xca\xa0\xb5\xb8\x2e\x33\xef\xa1\xe3\xd4\xde\x58\x5c\xcb\x4f\x10\x42\xe1\x88\x93\x6c\xa2\x4d\x41\xdc\x6e\x90\xdc\xaa\x71\xee\xbb\x6d\xe9\x3d\xd4\xbd\x54\xe2\x37\xb4\x4e\x1a\x0d\x21\x64\xd5\x82\xb9\xc6\xca\x8e\xc0\xd9\x66\x1e\xeb\xee\x00\x75\x37\x87\xc4\x8a\x01\xa9\x5a\x78\x8f\x5a\x84\xb0\x58\x1c\x98\x35\x46\x13\x6a\x8a\xe4\x00\x98\x90\x5b\x68\x14\x77\xae\x4c\x02\x2e\x35\xda\x7c\xad\x7a\x29\xb2\x6a\x01\x00\xc0\xda\xd7\x20\x45\x99\x8d\x87\x66\xd5\xfb\xe1\x0f\x2b\xda\xd7\x83\x06\x00\x23\x5e\x2b\x9c\x70\x86\x45\xfa\xe6\x8d\xd1\x02\xb5\x43\x31\xae\x6b\x63\x05\xda\xfd\xb2\x35\x5b\xb4\xd9\x04\x03\xe0\xbd\xe5\x7a\x83\xb0\xbc\x33\xf5\x2b\x58\x76\xc6\x28\xb8\x2a\x61\x35\x9c\x79\x63\x8c\x72\x90\xfc\x3e\x18\x2c\x5b\xe4\x8a\xda\x5d\xd4\xd3\xfd\xc3\xdb\x71\x95\x6c\x1f\xab\x92\x21\x9e\x00\x15\xea\x33\x1a\x91\x88\x9d\x58\xdc\x99\xfa\x63\x4c\x02\xb4\xde\xcb\x35\x28\x82\xfd\x49\x03\x4e\x08\x20\xa2\xb3\x76\x8c\xf1\x11\x8d\x09\x4c\x40\x63\x94\xeb\xb8\x2e\xb3\x6f\x9e\x88\x01\x98\x9c\x0e\x93\x8d\xd1\x79\xd3\xe2\xd6\x1a\x9d\xf7\x5d\xbc\x41\x59\x31\x9e\x02\x7f\x67\xea\xdc\xfb\x18\x91\x10\xa6\x44\xfb\xdf\xc9\x66\x35\xfd\x83\xaf\x0e\xf1\x08\xa1\x98\x18\x87\x00\x7d\xf7\x35\x2b\xf8\x13\x0f\x0b\x12\xa7\x7b\xac\x20\x5b\x5d\x0a\x89\x40\xe2\x52\xb9\x73\x64\xcf\x10\x7c\x69\x5e\x38\xb2\xb2\x9b\xcd\x92\xe3\x03\xe2\x15\x9d\x93\x24\xc7\xcf\x0b\x92\x59\xf5\x46\x8b\xce\x48\x4d\xac\xa0\xf6\x92\xde\x2d\x71\xc2\xe7\x94\xae\x79\x8d\xca\x3d\xaf\xe5\x08\x6e\x1b\xcb\xbb\x67\x01\xdf\x58\x6b\xec\xbc\xd2\xd3\x6b\xda\xef\xcf\x05\x84\x51\x6d\xc4\xee\x9c\x64\x5f\x74\x67\x0a\xe2\x60\x7d\x21\x98\x33\x17\x90\x84\x7c\xdf\x1a\x57\x1f\xde\x5d\xc3\x67\xd8\x28\x53\x73\xf5\xe1\xdd\xf5\x90\xb8\x71\x77\x75\xdb\xb4\xf8\x80\x21\x5c\x15\xc5\xb8\xf3\xd6\x38\x0a\x61\x5c\xdc\x70\x6a\x43\x88\xf9\xcb\xea\x59\x37\x8e\x78\xa8\x78\x1b\xaf\x60\xb9\xe5\xaa\x47\x97\x1a\x48\x84\xf9\xb5\x47\xbb\x83\x19\x82\x8f\x20\xe4\x64\x1e\xad\x47\xa0\x8b\x96\x00\x2c\x16\xfa\x94\xeb\xc9\x05\x48\xdf\xbc\xb3\xf2\x81\xdb\x5d\x2a\xd3\xb4\x13\x42\x8c\xc7\x80\x1a\x42\xc6\x8a\x68\x39\xcf\x2b\xba\x35\x74\xf1\x97\xc9\x9f\x56\xf9\x91\xec\xe2\xe5\x1d\x33\xe2\x0a\x2d\x41\xfa\xe6\xde\xc3\x6a\x68\xb4\xf0\x19\x86\x7e\xf3\xde\xfc\x10\xf5\x20\x04\x88\x83\x0a\x3f\x4a\x2d\x64\xc3\xc9\x58\x88\x63\x33\xef\xbb\x0e\x6d\xc3\x1d\x9e\xad\xe3\x03\x91\x11\xf7\x02\xd9\xcb\xe1\xfa\x77\xc8\x36\xbd\x75\xc6\xe6\xa9\x41\xa0\xcd\x40\x70\xe2\x39\x99\xcd\x46\xc5\x67\x80\x31\x8a\x64\x97\x01\x49\x8a\xeb\x51\xdc\xd2\x83\x2a\xc9\xf6\x38\x2c\x8d\x95\x1b\xa9\xb9\xca\x47\x2d\x56\x57\xdf\xe3\xda\x58\x8c\x8f\x87\x98\x05\x52\x6f\xae\x58\x51\x57\xfb\x9c\xbb\x8f\x39\x97\xb2\xf5\x47\xe9\x9a\xd8\xf3\x50\x0c\x8d\x25\x84\x98\xf8\xde\x2f\x71\x3b\xe4\x63\x0c\x3b\x3d\xa8\x58\x21\xcb\xfb\x10\xca\xff\xff\xd1\x1b\xfa\x36\x29\x84\x30\x2d\xce\x4f\xa5\xd3\x70\x0f\x09\x99\x6a\x24\xf5\xdc\xe1\x3c\x58\x0d\xbf\xab\x9f\x79\x07\xb1\xed\x67\x7f\xad\x6a\x4e\x0a\x2f\x79\xaa\x46\xff\xff\xc3\xca\x51\x0e\x5f\x7a\xbe\xc0\x35\xef\x15\x65\x95\x36\x1a\xff\x79\x99\x7e\xa1\xcc\x4d\xef\x93\x55\x1c\x2e\xc3\x6c\x59\xfd\xe4\x7e\x47\x6b\x42\xf8\x05\xb7\xe9\x75\x92\x22\xe0\xbd\x93\xba\xc1\x63\xc5\x10\x80\x6f\xcc\x17\x6a\x1e\x07\xaf\xd2\x2c\xbb\x14\x96\xb9\x36\x33\x3c\xaf\x1e\xf7\x93\x34\x35\x8e\x70\x9f\xbb\x97\x97\xf2\x9b\x9b\xb1\xf3\x78\xac\x98\x99\xb1\xac\x48\x0f\x99\xbf\xfb\xfc\x3a\x3d\xe9\x08\x84\x15\x42\x6e\xf7\x8f\xfb\x3f\x03\x00\x00\xff\xff\xab\xec\xff\xc4\xb5\x0c\x00\x00") func webUiTemplatesTargetsHtmlBytes() ([]byte, error) { return bindataRead( @@ -263,7 +263,7 @@ func webUiTemplatesTargetsHtml() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "web/ui/templates/targets.html", size: 3262, mode: os.FileMode(420), modTime: time.Unix(1502370142, 0)} + info := bindataFileInfo{name: "web/ui/templates/targets.html", size: 3253, mode: os.FileMode(420), modTime: time.Unix(1504524566, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -343,7 +343,7 @@ func webUiStaticCssPrometheusCss() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "web/ui/static/css/prometheus.css", size: 322, mode: os.FileMode(420), modTime: time.Unix(1502369610, 0)} + info := bindataFileInfo{name: "web/ui/static/css/prometheus.css", size: 322, mode: os.FileMode(420), modTime: time.Unix(1502452035, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -363,7 +363,7 @@ func webUiStaticCssTargetsCss() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "web/ui/static/css/targets.css", size: 182, mode: os.FileMode(420), modTime: time.Unix(1502369610, 0)} + info := bindataFileInfo{name: "web/ui/static/css/targets.css", size: 182, mode: os.FileMode(420), modTime: time.Unix(1502452035, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -443,7 +443,7 @@ func webUiStaticJsGraphJs() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "web/ui/static/js/graph.js", size: 27439, mode: os.FileMode(420), modTime: time.Unix(1502369610, 0)} + info := bindataFileInfo{name: "web/ui/static/js/graph.js", size: 27439, mode: os.FileMode(420), modTime: time.Unix(1502452035, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -503,7 +503,7 @@ func webUiStaticJsTargetsJs() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "web/ui/static/js/targets.js", size: 983, mode: os.FileMode(420), modTime: time.Unix(1502369610, 0)} + info := bindataFileInfo{name: "web/ui/static/js/targets.js", size: 983, mode: os.FileMode(420), modTime: time.Unix(1502452035, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -583,7 +583,7 @@ func webUiStaticVendorBootstrap331FontsGlyphiconsHalflingsRegularSvg() (*asset, return nil, err } - info := bindataFileInfo{name: "web/ui/static/vendor/bootstrap-3.3.1/fonts/glyphicons-halflings-regular.svg", size: 62926, mode: os.FileMode(420), modTime: time.Unix(1461244866, 0)} + info := bindataFileInfo{name: "web/ui/static/vendor/bootstrap-3.3.1/fonts/glyphicons-halflings-regular.svg", size: 62926, mode: os.FileMode(420), modTime: time.Unix(1502447168, 0)} a := &asset{bytes: bytes, info: info} return a, nil } diff --git a/web/ui/templates/_base.html b/web/ui/templates/_base.html index 209d3dd266..79eead86f9 100644 --- a/web/ui/templates/_base.html +++ b/web/ui/templates/_base.html @@ -52,7 +52,7 @@
  • - Help + Help
  • diff --git a/web/ui/templates/targets.html b/web/ui/templates/targets.html index 5b227bd0fb..2f819dc666 100644 --- a/web/ui/templates/targets.html +++ b/web/ui/templates/targets.html @@ -15,7 +15,7 @@ {{$job}} ({{$healthy}}/{{$total}} up) - + @@ -44,7 +44,7 @@
    - + {{$labels := stripLabels .Labels.Map "job"}} {{range $label, $value := $labels}} {{$label}}="{{$value}}" diff --git a/web/web.go b/web/web.go index 9b21236c34..8f916bb193 100644 --- a/web/web.go +++ b/web/web.go @@ -21,6 +21,7 @@ import ( "io/ioutil" "net" "net/http" + "net/http/pprof" "net/url" "os" "path" @@ -80,6 +81,7 @@ type Handler struct { quitCh chan struct{} reloadCh chan chan error options *Options + config *config.Config configString string versionInfo *PrometheusVersion birth time.Time @@ -93,13 +95,12 @@ type Handler struct { ready uint32 // ready is uint32 rather than boolean to be able to use atomic functions. } -// ApplyConfig updates the status state as the new config requires. +// ApplyConfig updates the config field of the Handler struct func (h *Handler) ApplyConfig(conf *config.Config) error { h.mtx.Lock() defer h.mtx.Unlock() - h.externalLabels = conf.GlobalConfig.ExternalLabels - h.configString = conf.String() + h.config = conf return nil } @@ -166,11 +167,18 @@ func New(o *Options) *Handler { storage: ptsdb.Adapter(o.Storage), notifier: o.Notifier, - now: model.Now, + now: model.Now, + ready: 0, } - h.apiV1 = api_v1.NewAPI(h.queryEngine, h.storage, h.targetManager, h.notifier) + h.apiV1 = api_v1.NewAPI(h.queryEngine, h.storage, h.targetManager, h.notifier, + func() config.Config { + h.mtx.RLock() + defer h.mtx.RUnlock() + return *h.config + }, + ) if o.RoutePrefix != "/" { // If the prefix is missing for the root path, prepend it. @@ -192,7 +200,7 @@ func New(o *Options) *Handler { router.Get("/graph", readyf(instrf("graph", h.graph))) router.Get("/status", readyf(instrf("status", h.status))) router.Get("/flags", readyf(instrf("flags", h.flags))) - router.Get("/config", readyf(instrf("config", h.config))) + router.Get("/config", readyf(instrf("config", h.serveConfig))) router.Get("/rules", readyf(instrf("rules", h.rules))) router.Get("/targets", readyf(instrf("targets", h.targets))) router.Get("/version", readyf(instrf("version", h.version))) @@ -235,8 +243,8 @@ func New(o *Options) *Handler { w.Write([]byte("Only POST requests allowed")) }) - router.Get("/debug/*subpath", readyf(http.DefaultServeMux.ServeHTTP)) - router.Post("/debug/*subpath", readyf(http.DefaultServeMux.ServeHTTP)) + router.Get("/debug/*subpath", readyf(serveDebug)) + router.Post("/debug/*subpath", readyf(serveDebug)) router.Get("/-/healthy", func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) @@ -264,6 +272,26 @@ func setCORS(w http.ResponseWriter) { } } +func serveDebug(w http.ResponseWriter, req *http.Request) { + ctx := req.Context() + subpath := route.Param(ctx, "subpath") + + // Based off paths from init() in golang.org/src/net/http/pprof/pprof.go + if subpath == "/pprof/" { + pprof.Index(w, req) + } else if subpath == "/pprof/cmdline" { + pprof.Cmdline(w, req) + } else if subpath == "/pprof/profile" { + pprof.Profile(w, req) + } else if subpath == "/pprof/symbol" { + pprof.Symbol(w, req) + } else if subpath == "/pprof/trace" { + pprof.Trace(w, req) + } else { + http.NotFound(w, req) + } +} + func serveStaticAsset(w http.ResponseWriter, req *http.Request) { fp := route.Param(req.Context(), "filepath") fp = filepath.Join("web/ui/static", fp) @@ -489,11 +517,11 @@ func (h *Handler) flags(w http.ResponseWriter, r *http.Request) { h.executeTemplate(w, "flags.html", h.flagsMap) } -func (h *Handler) config(w http.ResponseWriter, r *http.Request) { +func (h *Handler) serveConfig(w http.ResponseWriter, r *http.Request) { h.mtx.RLock() defer h.mtx.RUnlock() - h.executeTemplate(w, "config.html", h.configString) + h.executeTemplate(w, "config.html", h.config.String()) } func (h *Handler) rules(w http.ResponseWriter, r *http.Request) {