Repository: prometheus/prometheus
File: discovery/scaleway/instance.go (Go)

Code before:
// Copyright 2021 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 scaleway

import (
	"context"
	"fmt"
	"net"
	"net/http"
	"strconv"
	"strings"
	"time"

	"github.com/prometheus/common/config"
	"github.com/prometheus/common/model"
	"github.com/prometheus/common/version"
	"github.com/scaleway/scaleway-sdk-go/api/instance/v1"
	"github.com/scaleway/scaleway-sdk-go/scw"

	"github.com/prometheus/prometheus/discovery/refresh"
	"github.com/prometheus/prometheus/discovery/targetgroup"
)

const (
	instanceLabelPrefix = metaLabelPrefix + "instance_"

	instanceBootTypeLabel          = instanceLabelPrefix + "boot_type"
	instanceHostnameLabel          = instanceLabelPrefix + "hostname"
	instanceIDLabel                = instanceLabelPrefix + "id"
	instanceImageArchLabel         = instanceLabelPrefix + "image_arch"
	instanceImageIDLabel           = instanceLabelPrefix + "image_id"
	instanceImageNameLabel         = instanceLabelPrefix + "image_name"
	instanceLocationClusterID      = instanceLabelPrefix + "location_cluster_id"
	instanceLocationHypervisorID   = instanceLabelPrefix + "location_hypervisor_id"
	instanceLocationNodeID         = instanceLabelPrefix + "location_node_id"
	instanceNameLabel              = instanceLabelPrefix + "name"
	instanceOrganizationLabel      = instanceLabelPrefix + "organization_id"
	instancePrivateIPv4Label       = instanceLabelPrefix + "private_ipv4"
	instanceProjectLabel           = instanceLabelPrefix + "project_id"
	instancePublicIPv4Label        = instanceLabelPrefix + "public_ipv4"
	instancePublicIPv6Label        = instanceLabelPrefix + "public_ipv6"
	instanceSecurityGroupIDLabel   = instanceLabelPrefix + "security_group_id"
	instanceSecurityGroupNameLabel = instanceLabelPrefix + "security_group_name"
	instanceStateLabel             = instanceLabelPrefix + "status"
	instanceTagsLabel              = instanceLabelPrefix + "tags"
	instanceTypeLabel              = instanceLabelPrefix + "type"
	instanceZoneLabel              = instanceLabelPrefix + "zone"
	instanceRegionLabel            = instanceLabelPrefix + "region"
)

type instanceDiscovery struct {
	*refresh.Discovery
	client     *scw.Client
	port       int
	zone       string
	project    string
	accessKey  string
	secretKey  string
	nameFilter string
	tagsFilter []string
}

func newInstanceDiscovery(conf *SDConfig) (*instanceDiscovery, error) {
	d := &instanceDiscovery{
		port:       conf.Port,
		zone:       conf.Zone,
		project:    conf.Project,
		accessKey:  conf.AccessKey,
		secretKey:  conf.secretKeyForConfig(),
		nameFilter: conf.NameFilter,
		tagsFilter: conf.TagsFilter,
	}

	rt, err := config.NewRoundTripperFromConfig(conf.HTTPClientConfig, "scaleway_sd")
	if err != nil {
		return nil, err
	}

	if conf.SecretKeyFile != "" {
		rt, err = newAuthTokenFileRoundTripper(conf.SecretKeyFile, rt)
		if err != nil {
			return nil, err
		}
	}

	profile, err := loadProfile(conf)
	if err != nil {
		return nil, err
	}

	d.client, err = scw.NewClient(
		scw.WithHTTPClient(&http.Client{
			Transport: rt,
			Timeout:   time.Duration(conf.RefreshInterval),
		}),
		scw.WithUserAgent(fmt.Sprintf("Prometheus/%s", version.Version)),
		scw.WithProfile(profile),
	)
	if err != nil {
		return nil, fmt.Errorf("error setting up scaleway client: %w", err)
	}

	return d, nil
}

func (d *instanceDiscovery) refresh(ctx context.Context) ([]*targetgroup.Group, error) {
	api := instance.NewAPI(d.client)

	req := &instance.ListServersRequest{}

	if d.nameFilter != "" {
		req.Name = scw.StringPtr(d.nameFilter)
	}

	if d.tagsFilter != nil {
		req.Tags = d.tagsFilter
	}

	servers, err := api.ListServers(req, scw.WithAllPages(), scw.WithContext(ctx))
	if err != nil {
		return nil, err
	}

	var targets []model.LabelSet
	for _, server := range servers.Servers {
		labels := model.LabelSet{
			instanceBootTypeLabel:     model.LabelValue(server.BootType),
			instanceHostnameLabel:     model.LabelValue(server.Hostname),
			instanceIDLabel:           model.LabelValue(server.ID),
			instanceNameLabel:         model.LabelValue(server.Name),
			instanceOrganizationLabel: model.LabelValue(server.Organization),
			instanceProjectLabel:      model.LabelValue(server.Project),
			instanceStateLabel:        model.LabelValue(server.State),
			instanceTypeLabel:         model.LabelValue(server.CommercialType),
			instanceZoneLabel:         model.LabelValue(server.Zone.String()),
		}

		if server.Image != nil {
			labels[instanceImageArchLabel] = model.LabelValue(server.Image.Arch)
			labels[instanceImageIDLabel] = model.LabelValue(server.Image.ID)
			labels[instanceImageNameLabel] = model.LabelValue(server.Image.Name)
		}

		if server.Location != nil {
			labels[instanceLocationClusterID] = model.LabelValue(server.Location.ClusterID)
			labels[instanceLocationHypervisorID] = model.LabelValue(server.Location.HypervisorID)
			labels[instanceLocationNodeID] = model.LabelValue(server.Location.NodeID)
		}

		if server.SecurityGroup != nil {
			labels[instanceSecurityGroupIDLabel] = model.LabelValue(server.SecurityGroup.ID)
			labels[instanceSecurityGroupNameLabel] = model.LabelValue(server.SecurityGroup.Name)
		}

		if region, err := server.Zone.Region(); err == nil {
			labels[instanceRegionLabel] = model.LabelValue(region.String())
		}

		if len(server.Tags) > 0 {
			// We surround the separated list with the separator as well. This way regular expressions
			// in relabeling rules don't have to consider tag positions.
			tags := separator + strings.Join(server.Tags, separator) + separator
			labels[instanceTagsLabel] = model.LabelValue(tags)
		}

		addr := ""
		if server.IPv6 != nil { //nolint:staticcheck
			labels[instancePublicIPv6Label] = model.LabelValue(server.IPv6.Address.String()) //nolint:staticcheck
			addr = server.IPv6.Address.String()                                              //nolint:staticcheck
		}

		if server.PublicIP != nil { //nolint:staticcheck
			labels[instancePublicIPv4Label] = model.LabelValue(server.PublicIP.Address.String()) //nolint:staticcheck
			addr = server.PublicIP.Address.String()                                              //nolint:staticcheck
		}

		if server.PrivateIP != nil {
			labels[instancePrivateIPv4Label] = model.LabelValue(*server.PrivateIP)
			addr = *server.PrivateIP
		}

		if addr != "" {
			addr := net.JoinHostPort(addr, strconv.FormatUint(uint64(d.port), 10))
			labels[model.AddressLabel] = model.LabelValue(addr)
			targets = append(targets, labels)
		}
	}

	return []*targetgroup.Group{{Source: "scaleway", Targets: targets}}, nil
}


Review comment (bug):
I understand, but I'm afraid a long-lived bug can become a feature as they say, maybe we're relying on it for relabeling or sth even when it yields an IPv6.

We do not guarantee any stability for those things, but if we can avoid "breaking" changes when possible it'd be great.

Let's just add a comment around that code and let's fix if someone (other than us) screams out of it or once `PublicIP` and `IPv6` fields disappear, whichever of the two comes first.

Code after:
				} else if ip.Family == instance.ServerIPIPFamilyInet6 {
					ipv6Addresses = append(ipv6Addresses, ip.Address.String())
				}
			}

			if len(ipv6Addresses) > 0 {
				labels[instancePublicIPv6AddressesLabel] = model.LabelValue(
					separator +
						strings.Join(ipv6Addresses, separator) +
						separator)
			}
			if len(ipv4Addresses) > 0 {
				labels[instancePublicIPv4AddressesLabel] = model.LabelValue(
					separator +
						strings.Join(ipv4Addresses, separator) +
						separator)
			}
		}

		if server.IPv6 != nil { //nolint:staticcheck
			labels[instancePublicIPv6Label] = model.LabelValue(server.IPv6.Address.String()) //nolint:staticcheck
			addr = server.IPv6.Address.String()                                              //nolint:staticcheck
		}

		if server.PublicIP != nil { //nolint:staticcheck
			if server.PublicIP.Family != instance.ServerIPIPFamilyInet6 { //nolint:staticcheck
				labels[instancePublicIPv4Label] = model.LabelValue(server.PublicIP.Address.String()) //nolint:staticcheck
			}
			addr = server.PublicIP.Address.String() //nolint:staticcheck
		}

		if server.PrivateIP != nil {
			labels[instancePrivateIPv4Label] = model.LabelValue(*server.PrivateIP)
			addr = *server.PrivateIP
		}

		if addr != "" {
			addr := net.JoinHostPort(addr, strconv.FormatUint(uint64(d.port), 10))
			labels[model.AddressLabel] = model.LabelValue(addr)
			targets = append(targets, labels)
		}
	}

	return []*targetgroup.Group{{Source: "scaleway", Targets: targets}}, nil
}