diff --git a/discovery/openstack/loadbalancer.go b/discovery/openstack/loadbalancer.go new file mode 100644 index 0000000000..469fe84275 --- /dev/null +++ b/discovery/openstack/loadbalancer.go @@ -0,0 +1,201 @@ +// 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 ( + "context" + "fmt" + "log/slog" + "net" + "strconv" + "strings" + + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/openstack" + "github.com/gophercloud/gophercloud/openstack/loadbalancer/v2/listeners" + "github.com/gophercloud/gophercloud/openstack/loadbalancer/v2/loadbalancers" + "github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/layer3/floatingips" + "github.com/prometheus/common/model" + "github.com/prometheus/common/promslog" + + "github.com/prometheus/prometheus/discovery/targetgroup" +) + +const ( + openstackLabelLoadBalancerID = openstackLabelPrefix + "loadbalancer_id" + openstackLabelLoadBalancerName = openstackLabelPrefix + "loadbalancer_name" + openstackLabelLoadBalancerOperatingStatus = openstackLabelPrefix + "loadbalancer_operating_status" + openstackLabelLoadBalancerProvisioningStatus = openstackLabelPrefix + "loadbalancer_provisioning_status" + openstackLabelLoadBalancerAvailabilityZone = openstackLabelPrefix + "loadbalancer_availability_zone" + openstackLabelLoadBalancerFloatingIP = openstackLabelPrefix + "loadbalancer_floating_ip" + openstackLabelLoadBalancerVIP = openstackLabelPrefix + "loadbalancer_vip" + openstackLabelLoadBalancerProvider = openstackLabelPrefix + "loadbalancer_provider" + openstackLabelLoadBalancerTags = openstackLabelPrefix + "loadbalancer_tags" +) + +// LoadBalancerDiscovery discovers OpenStack load balancers. +type LoadBalancerDiscovery struct { + provider *gophercloud.ProviderClient + authOpts *gophercloud.AuthOptions + region string + logger *slog.Logger + availability gophercloud.Availability +} + +// NewLoadBalancerDiscovery returns a new loadbalancer discovery. +func newLoadBalancerDiscovery(provider *gophercloud.ProviderClient, opts *gophercloud.AuthOptions, + region string, availability gophercloud.Availability, l *slog.Logger, +) *LoadBalancerDiscovery { + if l == nil { + l = promslog.NewNopLogger() + } + return &LoadBalancerDiscovery{ + provider: provider, authOpts: opts, + region: region, availability: availability, logger: l, + } +} + +func (i *LoadBalancerDiscovery) refresh(ctx context.Context) ([]*targetgroup.Group, error) { + i.provider.Context = ctx + err := openstack.Authenticate(i.provider, *i.authOpts) + if err != nil { + return nil, fmt.Errorf("could not authenticate to OpenStack: %w", err) + } + + client, err := openstack.NewLoadBalancerV2(i.provider, gophercloud.EndpointOpts{ + Region: i.region, Availability: i.availability, + }) + if err != nil { + return nil, fmt.Errorf("could not create OpenStack load balancer session: %w", err) + } + + networkClient, err := openstack.NewNetworkV2(i.provider, gophercloud.EndpointOpts{ + Region: i.region, Availability: i.availability, + }) + if err != nil { + return nil, fmt.Errorf("could not create OpenStack network session: %w", err) + } + + allPages, err := loadbalancers.List(client, loadbalancers.ListOpts{}).AllPages() + if err != nil { + return nil, fmt.Errorf("failed to list load balancers: %w", err) + } + + allLBs, err := loadbalancers.ExtractLoadBalancers(allPages) + if err != nil { + return nil, fmt.Errorf("failed to extract load balancers: %w", err) + } + + // Fetch all listeners in one API call + listenerPages, err := listeners.List(client, listeners.ListOpts{}).AllPages() + if err != nil { + return nil, fmt.Errorf("failed to list all listeners: %w", err) + } + + allListeners, err := listeners.ExtractListeners(listenerPages) + if err != nil { + return nil, fmt.Errorf("failed to extract all listeners: %w", err) + } + + // Create a map to group listeners by Load Balancer ID + listenerMap := make(map[string][]listeners.Listener) + for _, listener := range allListeners { + // Iterate through each associated Load Balancer ID in the Loadbalancers array + for _, lb := range listener.Loadbalancers { + listenerMap[lb.ID] = append(listenerMap[lb.ID], listener) + } + } + + // Fetch all floating IPs + fipPages, err := floatingips.List(networkClient, floatingips.ListOpts{}).AllPages() + if err != nil { + return nil, fmt.Errorf("failed to list all fips: %w", err) + } + if err != nil { + return nil, fmt.Errorf("failed to list floating IPs: %w", err) + } + + allFIPs, err := floatingips.ExtractFloatingIPs(fipPages) + if err != nil { + return nil, fmt.Errorf("failed to extract floating IPs: %w", err) + } + + // Create a map to associate floating IPs with their resource IDs + fipMap := make(map[string]string) // Key: LoadBalancerID/PortID, Value: Floating IP + for _, fip := range allFIPs { + if fip.PortID != "" { + fipMap[fip.PortID] = fip.FloatingIP + } + } + + tg := &targetgroup.Group{ + Source: "OS_" + i.region, + } + + for _, lb := range allLBs { + // Retrieve listeners for this load balancer from the map + lbListeners, exists := listenerMap[lb.ID] + if !exists || len(lbListeners) == 0 { + i.logger.Debug("Got no listener", "loadbalancer", lb.ID) + continue + } + + // Variable to store the port of the first PROMETHEUS listener + var listenerPort int + hasPrometheusListener := false + + // Check if any listener has the PROMETHEUS protocol + for _, listener := range lbListeners { + if listener.Protocol == "PROMETHEUS" { + hasPrometheusListener = true + listenerPort = listener.ProtocolPort + break + } + } + + // Skip LBs without PROMETHEUS listener protocol + if !hasPrometheusListener { + i.logger.Debug("Got no PROMETHEUS listener", "loadbalancer", lb.ID) + continue + } + + labels := model.LabelSet{} + addr := net.JoinHostPort(lb.VipAddress, strconv.Itoa(listenerPort)) + labels[model.AddressLabel] = model.LabelValue(addr) + labels[openstackLabelLoadBalancerID] = model.LabelValue(lb.ID) + labels[openstackLabelLoadBalancerName] = model.LabelValue(lb.Name) + labels[openstackLabelLoadBalancerOperatingStatus] = model.LabelValue(lb.OperatingStatus) + labels[openstackLabelLoadBalancerProvisioningStatus] = model.LabelValue(lb.ProvisioningStatus) + labels[openstackLabelLoadBalancerAvailabilityZone] = model.LabelValue(lb.AvailabilityZone) + labels[openstackLabelLoadBalancerVIP] = model.LabelValue(lb.VipAddress) + labels[openstackLabelLoadBalancerProvider] = model.LabelValue(lb.Provider) + labels[openstackLabelProjectID] = model.LabelValue(lb.ProjectID) + + if len(lb.Tags) > 0 { + labels[openstackLabelLoadBalancerTags] = model.LabelValue(strings.Join(lb.Tags, ",")) + } + + if floatingIP, exists := fipMap[lb.VipPortID]; exists { + labels[openstackLabelLoadBalancerFloatingIP] = model.LabelValue(floatingIP) + } + + tg.Targets = append(tg.Targets, labels) + } + + if err != nil { + return nil, err + } + + return []*targetgroup.Group{tg}, nil +} diff --git a/discovery/openstack/loadbalancer_test.go b/discovery/openstack/loadbalancer_test.go new file mode 100644 index 0000000000..eee21b9831 --- /dev/null +++ b/discovery/openstack/loadbalancer_test.go @@ -0,0 +1,137 @@ +// 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 ( + "context" + "fmt" + "testing" + + "github.com/prometheus/common/model" + "github.com/stretchr/testify/require" +) + +type OpenstackSDLoadBalancerTestSuite struct { + Mock *SDMock +} + +func (s *OpenstackSDLoadBalancerTestSuite) SetupTest(t *testing.T) { + s.Mock = NewSDMock(t) + s.Mock.Setup() + + s.Mock.HandleLoadBalancerListSuccessfully() + s.Mock.HandleListenersListSuccessfully() + s.Mock.HandleFloatingIPListSuccessfully() + + s.Mock.HandleVersionsSuccessfully() + s.Mock.HandleAuthSuccessfully() +} + +func (s *OpenstackSDLoadBalancerTestSuite) openstackAuthSuccess() (refresher, error) { + conf := SDConfig{ + IdentityEndpoint: s.Mock.Endpoint(), + Password: "test", + Username: "test", + DomainName: "12345", + Region: "RegionOne", + Role: "loadbalancer", + } + return newRefresher(&conf, nil) +} + +func TestOpenstackSDLoadBalancerRefresh(t *testing.T) { + mock := &OpenstackSDLoadBalancerTestSuite{} + mock.SetupTest(t) + + instance, err := mock.openstackAuthSuccess() + require.NoError(t, err) + + ctx := context.Background() + tgs, err := instance.refresh(ctx) + + require.NoError(t, err) + require.Len(t, tgs, 1) + + tg := tgs[0] + require.NotNil(t, tg) + require.NotNil(t, tg.Targets) + require.Len(t, tg.Targets, 4) + + for i, lbls := range []model.LabelSet{ + { + "__address__": model.LabelValue("10.0.0.32:9273"), + "__meta_openstack_loadbalancer_id": model.LabelValue("ef079b0c-e610-4dfb-b1aa-b49f07ac48e5"), + "__meta_openstack_loadbalancer_name": model.LabelValue("lb1"), + "__meta_openstack_loadbalancer_operating_status": model.LabelValue("ONLINE"), + "__meta_openstack_loadbalancer_provisioning_status": model.LabelValue("ACTIVE"), + "__meta_openstack_loadbalancer_availability_zone": model.LabelValue("az1"), + "__meta_openstack_loadbalancer_floating_ip": model.LabelValue("192.168.1.2"), + "__meta_openstack_loadbalancer_vip": model.LabelValue("10.0.0.32"), + "__meta_openstack_loadbalancer_provider": model.LabelValue("amphora"), + "__meta_openstack_loadbalancer_tags": model.LabelValue("tag1,tag2"), + "__meta_openstack_project_id": model.LabelValue("fcad67a6189847c4aecfa3c81a05783b"), + }, + { + "__address__": model.LabelValue("10.0.2.78:8080"), + "__meta_openstack_loadbalancer_id": model.LabelValue("d92c471e-8d3e-4b9f-b2b5-9c72a9e3ef54"), + "__meta_openstack_loadbalancer_name": model.LabelValue("lb3"), + "__meta_openstack_loadbalancer_operating_status": model.LabelValue("ONLINE"), + "__meta_openstack_loadbalancer_provisioning_status": model.LabelValue("ACTIVE"), + "__meta_openstack_loadbalancer_availability_zone": model.LabelValue("az3"), + "__meta_openstack_loadbalancer_floating_ip": model.LabelValue("192.168.3.4"), + "__meta_openstack_loadbalancer_vip": model.LabelValue("10.0.2.78"), + "__meta_openstack_loadbalancer_provider": model.LabelValue("amphora"), + "__meta_openstack_loadbalancer_tags": model.LabelValue("tag5,tag6"), + "__meta_openstack_project_id": model.LabelValue("ac57f03dba1a4fdebff3e67201bc7a85"), + }, + { + "__address__": model.LabelValue("10.0.3.99:9090"), + "__meta_openstack_loadbalancer_id": model.LabelValue("f5c7e918-df38-4a5a-a7d4-d9c27ab2cf67"), + "__meta_openstack_loadbalancer_name": model.LabelValue("lb4"), + "__meta_openstack_loadbalancer_operating_status": model.LabelValue("ONLINE"), + "__meta_openstack_loadbalancer_provisioning_status": model.LabelValue("ACTIVE"), + "__meta_openstack_loadbalancer_availability_zone": model.LabelValue("az1"), + "__meta_openstack_loadbalancer_floating_ip": model.LabelValue("192.168.4.5"), + "__meta_openstack_loadbalancer_vip": model.LabelValue("10.0.3.99"), + "__meta_openstack_loadbalancer_provider": model.LabelValue("amphora"), + "__meta_openstack_project_id": model.LabelValue("fa8c372dfe4d4c92b0c4e3a2d9b3c9fa"), + }, + { + "__address__": model.LabelValue("10.0.4.88:9876"), + "__meta_openstack_loadbalancer_id": model.LabelValue("e83a6d92-7a3e-4567-94b3-20c83b32a75e"), + "__meta_openstack_loadbalancer_name": model.LabelValue("lb5"), + "__meta_openstack_loadbalancer_operating_status": model.LabelValue("ONLINE"), + "__meta_openstack_loadbalancer_provisioning_status": model.LabelValue("ACTIVE"), + "__meta_openstack_loadbalancer_availability_zone": model.LabelValue("az4"), + "__meta_openstack_loadbalancer_vip": model.LabelValue("10.0.4.88"), + "__meta_openstack_loadbalancer_provider": model.LabelValue("amphora"), + "__meta_openstack_project_id": model.LabelValue("a5d3b2e1e6f34cd9a5f7c2f01a6b8e29"), + }, + } { + t.Run(fmt.Sprintf("item %d", i), func(t *testing.T) { + require.Equal(t, lbls, tg.Targets[i]) + }) + } +} + +func TestOpenstackSDLoadBalancerRefreshWithDoneContext(t *testing.T) { + mock := &OpenstackSDLoadBalancerTestSuite{} + mock.SetupTest(t) + + loadbalancer, _ := mock.openstackAuthSuccess() + ctx, cancel := context.WithCancel(context.Background()) + cancel() + _, err := loadbalancer.refresh(ctx) + require.ErrorContains(t, err, context.Canceled.Error(), "%q doesn't contain %q", err, context.Canceled) +} diff --git a/discovery/openstack/mock_test.go b/discovery/openstack/mock_test.go index 2d12dbc0df..36620defeb 100644 --- a/discovery/openstack/mock_test.go +++ b/discovery/openstack/mock_test.go @@ -149,7 +149,20 @@ func (m *SDMock) HandleAuthSuccessfully() { ], "id": "589f3d99a3d94f5f871e9f5cf206d2e8", "type": "network" - } + }, + { + "endpoints": [ + { + "id": "39dc322ce86c1234b4f06c2eeae0841b", + "interface": "public", + "region": "RegionOne", + "region_id": "RegionOne", + "url": "%s" + } + ], + "id": "26968f704a68417bbddd29508455ff90", + "type": "load-balancer" + } ], "expires_at": "2013-02-27T18:30:59.999999Z", "is_domain": false, @@ -186,7 +199,7 @@ func (m *SDMock) HandleAuthSuccessfully() { } } } - `, m.Endpoint(), m.Endpoint()) + `, m.Endpoint(), m.Endpoint(), m.Endpoint()) }) } @@ -711,6 +724,63 @@ const listOutput = ` "tags": [], "created_at": "2024-01-24T13:30:50Z", "updated_at": "2024-01-24T13:30:51Z" + }, + { + "id": "fea7332d-9027-4cf9-bf62-c3c4c6ebaf84", + "tenant_id": "fcad67a6189847c4aecfa3c81a05783b", + "floating_ip_address": "192.168.1.2", + "floating_network_id": "d02c4f18-d606-4864-b12a-1c9b39a46be2", + "router_id": "f03af93b-4e8f-4f55-adcf-a0317782ede2", + "port_id": "b47c39f5-238d-4b17-ae87-9b5d19af8a2e", + "fixed_ip_address": "10.0.0.32", + "status": "ACTIVE", + "description": "", + "dns_domain": "", + "dns_name": "", + "port_forwardings": [], + "tags": [], + "created_at": "2023-08-30T15:11:37Z", + "updated_at": "2023-08-30T15:11:38Z", + "revision_number": 1, + "project_id": "fcad67a6189847c4aecfa3c81a05783b" + }, + { + "id": "febb9554-cf83-4f9b-94d9-1b3c34be357f", + "tenant_id": "ac57f03dba1a4fdebff3e67201bc7a85", + "floating_ip_address": "192.168.3.4", + "floating_network_id": "d02c4f18-d606-4864-b12a-1c9b39a46be2", + "router_id": "f03af93b-4e8f-4f55-adcf-a0317782ede2", + "port_id": "c83b6e12-4e5d-4673-a4b3-5bc72a7f3ef9", + "fixed_ip_address": "10.0.2.78", + "status": "ACTIVE", + "description": "", + "dns_domain": "", + "dns_name": "", + "port_forwardings": [], + "tags": [], + "created_at": "2023-08-30T15:11:37Z", + "updated_at": "2023-08-30T15:11:38Z", + "revision_number": 1, + "project_id": "ac57f03dba1a4fdebff3e67201bc7a85" + }, + { + "id": "febb9554-cf83-4f9b-94d9-1b3c34be357f", + "tenant_id": "fa8c372dfe4d4c92b0c4e3a2d9b3c9fa", + "floating_ip_address": "192.168.4.5", + "floating_network_id": "d02c4f18-d606-4864-b12a-1c9b39a46be2", + "router_id": "f03af93b-4e8f-4f55-adcf-a0317782ede2", + "port_id": "f9e8b6e12-7e4d-4963-a5b3-6cd82a7f3ff6", + "fixed_ip_address": "10.0.3.99", + "status": "ACTIVE", + "description": "", + "dns_domain": "", + "dns_name": "", + "port_forwardings": [], + "tags": [], + "created_at": "2023-08-30T15:11:37Z", + "updated_at": "2023-08-30T15:11:38Z", + "revision_number": 1, + "project_id": "fa8c372dfe4d4c92b0c4e3a2d9b3c9fa" } ] } @@ -863,3 +933,471 @@ func (m *SDMock) HandlePortsListSuccessfully() { fmt.Fprint(w, portsListBody) }) } + +const lbListBody = ` +{ + "loadbalancers": [ + { + "id": "ef079b0c-e610-4dfb-b1aa-b49f07ac48e5", + "name": "lb1", + "description": "", + "provisioning_status": "ACTIVE", + "operating_status": "ONLINE", + "admin_state_up": true, + "project_id": "fcad67a6189847c4aecfa3c81a05783b", + "created_at": "2024-12-01T10:00:00", + "updated_at": "2024-12-01T10:30:00", + "vip_address": "10.0.0.32", + "vip_port_id": "b47c39f5-238d-4b17-ae87-9b5d19af8a2e", + "vip_subnet_id": "14a4c6a5-fe71-4a94-9071-4cd12fb8337f", + "vip_network_id": "d02c4f18-d606-4864-b12a-1c9b39a46be2", + "tags": ["tag1", "tag2"], + "availability_zone": "az1", + "vip_vnic_type": "normal", + "provider": "amphora", + "listeners": [ + { + "id": "c4146b54-febc-4caf-a53f-ed1cab6faba5" + }, + { + "id": "a058d20e-82de-4eff-bb65-5c76a8554435" + } + ], + "tenant_id": "fcad67a6189847c4aecfa3c81a05783b" + }, + { + "id": "d92c471e-8d3e-4b9f-b2b5-9c72a9e3ef54", + "name": "lb3", + "description": "", + "provisioning_status": "ACTIVE", + "operating_status": "ONLINE", + "admin_state_up": true, + "project_id": "ac57f03dba1a4fdebff3e67201bc7a85", + "created_at": "2024-12-01T12:00:00", + "updated_at": "2024-12-01T12:45:00", + "vip_address": "10.0.2.78", + "vip_port_id": "c83b6e12-4e5d-4673-a4b3-5bc72a7f3ef9", + "vip_subnet_id": "36c5e9f6-e7a2-4975-a8c6-3b8e4f93cf45", + "vip_network_id": "g03c6f27-e617-4975-c8f7-4c9f3f94cf68", + "tags": ["tag5", "tag6"], + "availability_zone": "az3", + "vip_vnic_type": "normal", + "provider": "amphora", + "listeners": [ + { + "id": "5b9529a4-6cbf-48f8-a006-d99cbc717da0" + }, + { + "id": "5d26333b-74d1-4b2a-90ab-2b2c0f5a8048" + } + ], + "tenant_id": "ac57f03dba1a4fdebff3e67201bc7a85" + }, + { + "id": "f5c7e918-df38-4a5a-a7d4-d9c27ab2cf67", + "name": "lb4", + "description": "", + "provisioning_status": "ACTIVE", + "operating_status": "ONLINE", + "admin_state_up": true, + "project_id": "fa8c372dfe4d4c92b0c4e3a2d9b3c9fa", + "created_at": "2024-12-01T13:00:00", + "updated_at": "2024-12-01T13:20:00", + "vip_address": "10.0.3.99", + "vip_port_id": "f9e8b6e12-7e4d-4963-a5b3-6cd82a7f3ff6", + "vip_subnet_id": "47d6f8f9-f7b2-4876-a9d8-4e8f4g95df79", + "vip_network_id": "h04d7f38-f718-4876-d9g8-5d8g5h95df89", + "tags": [], + "availability_zone": "az1", + "vip_vnic_type": "normal", + "provider": "amphora", + "listeners": [ + { + "id": "84c87596-1ff0-4f6d-b151-0a78e1f407a3" + }, + { + "id": "fe460a7c-16a9-4984-9fe6-f6e5153ebab1" + } + ], + "tenant_id": "fa8c372dfe4d4c92b0c4e3a2d9b3c9fa" + }, + { + "id": "e83a6d92-7a3e-4567-94b3-20c83b32a75e", + "name": "lb5", + "description": "", + "provisioning_status": "ACTIVE", + "operating_status": "ONLINE", + "admin_state_up": true, + "project_id": "a5d3b2e1e6f34cd9a5f7c2f01a6b8e29", + "created_at": "2024-12-01T11:00:00", + "updated_at": "2024-12-01T11:15:00", + "vip_address": "10.0.4.88", + "vip_port_id": "d83a6d92-7a3e-4567-94b3-20c83b32a75e", + "vip_subnet_id": "25b4d8e5-fe81-4a87-9071-4cc12fb8337f", + "vip_network_id": "f02c5e19-c507-4864-b16e-2b7a39e56be3", + "tags": [], + "availability_zone": "az4", + "vip_vnic_type": "normal", + "provider": "amphora", + "listeners": [ + { + "id": "50902e62-34b8-46b2-9ed4-9053e7ad46dc" + }, + { + "id": "98a867ad-ff07-4880-b05f-32088866a68a" + } + ], + "tenant_id": "a5d3b2e1e6f34cd9a5f7c2f01a6b8e29" + } + ] +} +` + +// HandleLoadBalancerListSuccessfully mocks the load balancer list API. +func (m *SDMock) HandleLoadBalancerListSuccessfully() { + m.Mux.HandleFunc("/v2.0/lbaas/loadbalancers", func(w http.ResponseWriter, r *http.Request) { + testMethod(m.t, r, http.MethodGet) + testHeader(m.t, r, "X-Auth-Token", tokenID) + + w.Header().Add("Content-Type", "application/json") + fmt.Fprint(w, lbListBody) + }) +} + +const listenerListBody = ` +{ + "listeners": [ + { + "id": "c4146b54-febc-4caf-a53f-ed1cab6faba5", + "name": "stats-listener", + "description": "", + "provisioning_status": "ACTIVE", + "operating_status": "ONLINE", + "admin_state_up": true, + "protocol": "PROMETHEUS", + "protocol_port": 9273, + "connection_limit": -1, + "default_tls_container_ref": null, + "sni_container_refs": [], + "project_id": "fcad67a6189847c4aecfa3c81a05783b", + "default_pool_id": null, + "l7policies": [], + "insert_headers": {}, + "created_at": "2024-08-29T18:05:24", + "updated_at": "2024-12-04T21:21:10", + "loadbalancers": [ + { + "id": "ef079b0c-e610-4dfb-b1aa-b49f07ac48e5" + } + ], + "timeout_client_data": 50000, + "timeout_member_connect": 5000, + "timeout_member_data": 50000, + "timeout_tcp_inspect": 0, + "tags": [], + "client_ca_tls_container_ref": null, + "client_authentication": "NONE", + "client_crl_container_ref": null, + "allowed_cidrs": null, + "tls_ciphers": null, + "tls_versions": null, + "alpn_protocols": null, + "hsts_max_age": null, + "hsts_include_subdomains": null, + "hsts_preload": null, + "tenant_id": "fcad67a6189847c4aecfa3c81a05783b" + }, + { + "id": "5b9529a4-6cbf-48f8-a006-d99cbc717da0", + "name": "stats-listener2", + "description": "", + "provisioning_status": "ACTIVE", + "operating_status": "ONLINE", + "admin_state_up": true, + "protocol": "PROMETHEUS", + "protocol_port": 8080, + "connection_limit": -1, + "default_tls_container_ref": null, + "sni_container_refs": [], + "project_id": "ac57f03dba1a4fdebff3e67201bc7a85", + "default_pool_id": null, + "l7policies": [], + "insert_headers": {}, + "created_at": "2024-08-29T18:05:24", + "updated_at": "2024-12-04T21:21:10", + "loadbalancers": [ + { + "id": "d92c471e-8d3e-4b9f-b2b5-9c72a9e3ef54" + } + ], + "timeout_client_data": 50000, + "timeout_member_connect": 5000, + "timeout_member_data": 50000, + "timeout_tcp_inspect": 0, + "tags": [], + "client_ca_tls_container_ref": null, + "client_authentication": "NONE", + "client_crl_container_ref": null, + "allowed_cidrs": null, + "tls_ciphers": null, + "tls_versions": null, + "alpn_protocols": null, + "hsts_max_age": null, + "hsts_include_subdomains": null, + "hsts_preload": null, + "tenant_id": "ac57f03dba1a4fdebff3e67201bc7a85" + }, + { + "id": "84c87596-1ff0-4f6d-b151-0a78e1f407a3", + "name": "stats-listener3", + "description": "", + "provisioning_status": "ACTIVE", + "operating_status": "ONLINE", + "admin_state_up": true, + "protocol": "PROMETHEUS", + "protocol_port": 9090, + "connection_limit": -1, + "default_tls_container_ref": null, + "sni_container_refs": [], + "project_id": "fa8c372dfe4d4c92b0c4e3a2d9b3c9fa", + "default_pool_id": null, + "l7policies": [], + "insert_headers": {}, + "created_at": "2024-08-29T18:05:24", + "updated_at": "2024-12-04T21:21:10", + "loadbalancers": [ + { + "id": "f5c7e918-df38-4a5a-a7d4-d9c27ab2cf67" + } + ], + "timeout_client_data": 50000, + "timeout_member_connect": 5000, + "timeout_member_data": 50000, + "timeout_tcp_inspect": 0, + "tags": [], + "client_ca_tls_container_ref": null, + "client_authentication": "NONE", + "client_crl_container_ref": null, + "allowed_cidrs": null, + "tls_ciphers": null, + "tls_versions": null, + "alpn_protocols": null, + "hsts_max_age": null, + "hsts_include_subdomains": null, + "hsts_preload": null, + "tenant_id": "fa8c372dfe4d4c92b0c4e3a2d9b3c9fa" + }, + { + "id": "50902e62-34b8-46b2-9ed4-9053e7ad46dc", + "name": "stats-listener4", + "description": "", + "provisioning_status": "ACTIVE", + "operating_status": "ONLINE", + "admin_state_up": true, + "protocol": "PROMETHEUS", + "protocol_port": 9876, + "connection_limit": -1, + "default_tls_container_ref": null, + "sni_container_refs": [], + "project_id": "a5d3b2e1e6f34cd9a5f7c2f01a6b8e29", + "default_pool_id": null, + "l7policies": [], + "insert_headers": {}, + "created_at": "2024-08-29T18:05:24", + "updated_at": "2024-12-04T21:21:10", + "loadbalancers": [ + { + "id": "e83a6d92-7a3e-4567-94b3-20c83b32a75e" + } + ], + "timeout_client_data": 50000, + "timeout_member_connect": 5000, + "timeout_member_data": 50000, + "timeout_tcp_inspect": 0, + "tags": [], + "client_ca_tls_container_ref": null, + "client_authentication": "NONE", + "client_crl_container_ref": null, + "allowed_cidrs": null, + "tls_ciphers": null, + "tls_versions": null, + "alpn_protocols": null, + "hsts_max_age": null, + "hsts_include_subdomains": null, + "hsts_preload": null, + "tenant_id": "a5d3b2e1e6f34cd9a5f7c2f01a6b8e29" + }, + { + "id": "a058d20e-82de-4eff-bb65-5c76a8554435", + "name": "port6443", + "description": "", + "provisioning_status": "ACTIVE", + "operating_status": "ONLINE", + "admin_state_up": true, + "protocol": "TCP", + "protocol_port": 6443, + "connection_limit": -1, + "default_tls_container_ref": null, + "sni_container_refs": [], + "project_id": "a5d3b2e1e6f34cd9a5f7c2f01a6b8e29", + "default_pool_id": "5643208b-b691-4b1f-a6b8-356f14903e56", + "l7policies": [], + "insert_headers": {}, + "created_at": "2024-10-02T19:32:48", + "updated_at": "2024-12-04T21:44:34", + "loadbalancers": [ + { + "id": "ef079b0c-e610-4dfb-b1aa-b49f07ac48e5" + } + ], + "timeout_client_data": 50000, + "timeout_member_connect": 5000, + "timeout_member_data": 50000, + "timeout_tcp_inspect": 0, + "tags": [], + "client_ca_tls_container_ref": null, + "client_authentication": "NONE", + "client_crl_container_ref": null, + "allowed_cidrs": null, + "tls_ciphers": null, + "tls_versions": null, + "alpn_protocols": null, + "hsts_max_age": null, + "hsts_include_subdomains": null, + "hsts_preload": null, + "tenant_id": "a5d3b2e1e6f34cd9a5f7c2f01a6b8e29" + }, + { + "id": "5d26333b-74d1-4b2a-90ab-2b2c0f5a8048", + "name": "port6444", + "description": "", + "provisioning_status": "ACTIVE", + "operating_status": "ONLINE", + "admin_state_up": true, + "protocol": "TCP", + "protocol_port": 6444, + "connection_limit": -1, + "default_tls_container_ref": null, + "sni_container_refs": [], + "project_id": "ac57f03dba1a4fdebff3e67201bc7a85", + "default_pool_id": "5643208b-b691-4b1f-a6b8-356f14903e56", + "l7policies": [], + "insert_headers": {}, + "created_at": "2024-10-02T19:32:48", + "updated_at": "2024-12-04T21:44:34", + "loadbalancers": [ + { + "id": "d92c471e-8d3e-4b9f-b2b5-9c72a9e3ef54" + } + ], + "timeout_client_data": 50000, + "timeout_member_connect": 5000, + "timeout_member_data": 50000, + "timeout_tcp_inspect": 0, + "tags": [], + "client_ca_tls_container_ref": null, + "client_authentication": "NONE", + "client_crl_container_ref": null, + "allowed_cidrs": null, + "tls_ciphers": null, + "tls_versions": null, + "alpn_protocols": null, + "hsts_max_age": null, + "hsts_include_subdomains": null, + "hsts_preload": null, + "tenant_id": "ac57f03dba1a4fdebff3e67201bc7a85" + }, + { + "id": "fe460a7c-16a9-4984-9fe6-f6e5153ebab1", + "name": "port6445", + "description": "", + "provisioning_status": "ACTIVE", + "operating_status": "ONLINE", + "admin_state_up": true, + "protocol": "TCP", + "protocol_port": 6445, + "connection_limit": -1, + "default_tls_container_ref": null, + "sni_container_refs": [], + "project_id": "fa8c372dfe4d4c92b0c4e3a2d9b3c9fa", + "default_pool_id": "5643208b-b691-4b1f-a6b8-356f14903e56", + "l7policies": [], + "insert_headers": {}, + "created_at": "2024-10-02T19:32:48", + "updated_at": "2024-12-04T21:44:34", + "loadbalancers": [ + { + "id": "f5c7e918-df38-4a5a-a7d4-d9c27ab2cf67" + } + ], + "timeout_client_data": 50000, + "timeout_member_connect": 5000, + "timeout_member_data": 50000, + "timeout_tcp_inspect": 0, + "tags": [], + "client_ca_tls_container_ref": null, + "client_authentication": "NONE", + "client_crl_container_ref": null, + "allowed_cidrs": null, + "tls_ciphers": null, + "tls_versions": null, + "alpn_protocols": null, + "hsts_max_age": null, + "hsts_include_subdomains": null, + "hsts_preload": null, + "tenant_id": "fa8c372dfe4d4c92b0c4e3a2d9b3c9fa" + }, + { + "id": "98a867ad-ff07-4880-b05f-32088866a68a", + "name": "port6446", + "description": "", + "provisioning_status": "ACTIVE", + "operating_status": "ONLINE", + "admin_state_up": true, + "protocol": "TCP", + "protocol_port": 6446, + "connection_limit": -1, + "default_tls_container_ref": null, + "sni_container_refs": [], + "project_id": "a5d3b2e1e6f34cd9a5f7c2f01a6b8e29", + "default_pool_id": "5643208b-b691-4b1f-a6b8-356f14903e56", + "l7policies": [], + "insert_headers": {}, + "created_at": "2024-10-02T19:32:48", + "updated_at": "2024-12-04T21:44:34", + "loadbalancers": [ + { + "id": "e83a6d92-7a3e-4567-94b3-20c83b32a75e" + } + ], + "timeout_client_data": 50000, + "timeout_member_connect": 5000, + "timeout_member_data": 50000, + "timeout_tcp_inspect": 0, + "tags": [], + "client_ca_tls_container_ref": null, + "client_authentication": "NONE", + "client_crl_container_ref": null, + "allowed_cidrs": null, + "tls_ciphers": null, + "tls_versions": null, + "alpn_protocols": null, + "hsts_max_age": null, + "hsts_include_subdomains": null, + "hsts_preload": null, + "tenant_id": "a5d3b2e1e6f34cd9a5f7c2f01a6b8e29" + } + ] +} +` + +// HandleListenersListSuccessfully mocks the listeners endpoint. +func (m *SDMock) HandleListenersListSuccessfully() { + m.Mux.HandleFunc("/v2.0/lbaas/listeners", func(w http.ResponseWriter, r *http.Request) { + testMethod(m.t, r, http.MethodGet) + testHeader(m.t, r, "X-Auth-Token", tokenID) + + w.Header().Add("Content-Type", "application/json") + fmt.Fprint(w, listenerListBody) + }) +} diff --git a/discovery/openstack/openstack.go b/discovery/openstack/openstack.go index cd0bcc1267..e1dba211cf 100644 --- a/discovery/openstack/openstack.go +++ b/discovery/openstack/openstack.go @@ -97,6 +97,9 @@ const ( // OpenStack document reference // https://docs.openstack.org/horizon/pike/user/launch-instances.html OpenStackRoleInstance Role = "instance" + // Openstack document reference + // https://docs.openstack.org/openstacksdk/rocky/user/resources/load_balancer/index.html + OpenStackRoleLoadBalancer Role = "loadbalancer" ) // UnmarshalYAML implements the yaml.Unmarshaler interface. @@ -105,7 +108,7 @@ func (c *Role) UnmarshalYAML(unmarshal func(interface{}) error) error { return err } switch *c { - case OpenStackRoleHypervisor, OpenStackRoleInstance: + case OpenStackRoleHypervisor, OpenStackRoleInstance, OpenStackRoleLoadBalancer: return nil default: return fmt.Errorf("unknown OpenStack SD role %q", *c) @@ -128,7 +131,7 @@ func (c *SDConfig) UnmarshalYAML(unmarshal func(interface{}) error) error { } if c.Role == "" { - return errors.New("role missing (one of: instance, hypervisor)") + return errors.New("role missing (one of: instance, hypervisor, loadbalancer)") } if c.Region == "" { return errors.New("openstack SD configuration requires a region") @@ -211,6 +214,8 @@ func newRefresher(conf *SDConfig, l *slog.Logger) (refresher, error) { return newHypervisorDiscovery(client, &opts, conf.Port, conf.Region, availability, l), nil case OpenStackRoleInstance: return newInstanceDiscovery(client, &opts, conf.Port, conf.Region, conf.AllTenants, availability, l), nil + case OpenStackRoleLoadBalancer: + return newLoadBalancerDiscovery(client, &opts, conf.Region, availability, l), nil } return nil, errors.New("unknown OpenStack discovery role") } diff --git a/docs/configuration/configuration.md b/docs/configuration/configuration.md index 168c99d3ca..10fb8f9269 100644 --- a/docs/configuration/configuration.md +++ b/docs/configuration/configuration.md @@ -1208,6 +1208,25 @@ The following meta labels are available on targets during [relabeling](#relabel_ * `__meta_openstack_tag_`: each metadata item of the instance, with any unsupported characters converted to an underscore. * `__meta_openstack_user_id`: the user account owning the tenant. +#### `loadbalancer` + +The `loadbalancer` role discovers one target per Octavia loadbalancer with a +`PROMETHEUS` listener. The target address defaults to the VIP address +of the load balancer. + +The following meta labels are available on targets during [relabeling](#relabel_config): + +* `__meta_openstack_loadbalancer_availability_zone`: the availability zone of the OpenStack load balancer. +* `__meta_openstack_loadbalancer_floating_ip`: the floating IP of the OpenStack load balancer. +* `__meta_openstack_loadbalancer_id`: the OpenStack load balancer ID. +* `__meta_openstack_loadbalancer_name`: the OpenStack load balancer name. +* `__meta_openstack_loadbalancer_provider`: the Octavia provider of the OpenStack load balancer. +* `__meta_openstack_loadbalancer_operating_status`: the operating status of the OpenStack load balancer. +* `__meta_openstack_loadbalancer_provisioning_status`: the provisioning status of the OpenStack load balancer. +* `__meta_openstack_loadbalancer_tags`: comma separated list of the OpenStack load balancer. +* `__meta_openstack_loadbalancer_vip`: the VIP of the OpenStack load balancer. +* `__meta_openstack_project_id`: the project (tenant) owning this load balancer. + See below for the configuration options for OpenStack discovery: ```yaml