prometheus/discovery/scaleway/scaleway.go

263 lines
7.5 KiB
Go

// 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"
"errors"
"fmt"
"log/slog"
"net/http"
"os"
"strings"
"time"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/common/config"
"github.com/prometheus/common/model"
"github.com/scaleway/scaleway-sdk-go/scw"
"github.com/prometheus/prometheus/discovery"
"github.com/prometheus/prometheus/discovery/refresh"
"github.com/prometheus/prometheus/discovery/targetgroup"
)
// metaLabelPrefix is the meta prefix used for all meta labels.
// in this discovery.
const (
metaLabelPrefix = model.MetaLabelPrefix + "scaleway_"
separator = ","
)
// role is the role of the target within the Scaleway Ecosystem.
type role string
// The valid options for role.
const (
// Scaleway Elements Baremetal
// https://www.scaleway.com/en/bare-metal-servers/
roleBaremetal role = "baremetal"
// Scaleway Elements Instance
// https://www.scaleway.com/en/virtual-instances/
roleInstance role = "instance"
)
// UnmarshalYAML implements the yaml.Unmarshaler interface.
func (c *role) UnmarshalYAML(unmarshal func(interface{}) error) error {
if err := unmarshal((*string)(c)); err != nil {
return err
}
switch *c {
case roleInstance, roleBaremetal:
return nil
default:
return fmt.Errorf("unknown role %q", *c)
}
}
// DefaultSDConfig is the default Scaleway Service Discovery configuration.
var DefaultSDConfig = SDConfig{
Port: 80,
RefreshInterval: model.Duration(60 * time.Second),
HTTPClientConfig: config.DefaultHTTPClientConfig,
Zone: scw.ZoneFrPar1.String(),
APIURL: "https://api.scaleway.com",
}
type SDConfig struct {
// Project: The Scaleway Project ID used to filter discovery on.
Project string `yaml:"project_id"`
// APIURL: URL of the Scaleway API to use.
APIURL string `yaml:"api_url,omitempty"`
// Zone: The zone of the scrape targets.
// If you need to configure multiple zones use multiple scaleway_sd_configs
Zone string `yaml:"zone"`
// AccessKey used to authenticate on Scaleway APIs.
AccessKey string `yaml:"access_key"`
// SecretKey used to authenticate on Scaleway APIs.
SecretKey config.Secret `yaml:"secret_key"`
// SecretKey used to authenticate on Scaleway APIs.
SecretKeyFile string `yaml:"secret_key_file"`
// NameFilter to filter on during the ListServers.
NameFilter string `yaml:"name_filter,omitempty"`
// TagsFilter to filter on during the ListServers.
TagsFilter []string `yaml:"tags_filter,omitempty"`
HTTPClientConfig config.HTTPClientConfig `yaml:",inline"`
RefreshInterval model.Duration `yaml:"refresh_interval"`
Port int `yaml:"port"`
// Role can be either instance or baremetal
Role role `yaml:"role"`
}
// NewDiscovererMetrics implements discovery.Config.
func (*SDConfig) NewDiscovererMetrics(reg prometheus.Registerer, rmi discovery.RefreshMetricsInstantiator) discovery.DiscovererMetrics {
return &scalewayMetrics{
refreshMetrics: rmi,
}
}
func (c SDConfig) Name() string {
return "scaleway"
}
// secretKeyForConfig returns a secret key that looks like a UUID, even if we
// take the actual secret from a file.
func (c SDConfig) secretKeyForConfig() string {
if c.SecretKeyFile != "" {
return "00000000-0000-0000-0000-000000000000"
}
return string(c.SecretKey)
}
// UnmarshalYAML implements the yaml.Unmarshaler interface.
func (c *SDConfig) UnmarshalYAML(unmarshal func(interface{}) error) error {
*c = DefaultSDConfig
type plain SDConfig
err := unmarshal((*plain)(c))
if err != nil {
return err
}
if c.Role == "" {
return errors.New("role missing (one of: instance, baremetal)")
}
if c.Project == "" {
return errors.New("project_id is mandatory")
}
if c.SecretKey == "" && c.SecretKeyFile == "" {
return errors.New("one of secret_key & secret_key_file must be configured")
}
if c.SecretKey != "" && c.SecretKeyFile != "" {
return errors.New("at most one of secret_key & secret_key_file must be configured")
}
if c.AccessKey == "" {
return errors.New("access_key is mandatory")
}
profile, err := loadProfile(c)
if err != nil {
return err
}
_, err = scw.NewClient(
scw.WithProfile(profile),
)
if err != nil {
return err
}
return c.HTTPClientConfig.Validate()
}
func (c SDConfig) NewDiscoverer(options discovery.DiscovererOptions) (discovery.Discoverer, error) {
return NewDiscovery(&c, options.Logger, options.Metrics)
}
// SetDirectory joins any relative file paths with dir.
func (c *SDConfig) SetDirectory(dir string) {
c.SecretKeyFile = config.JoinDir(dir, c.SecretKeyFile)
c.HTTPClientConfig.SetDirectory(dir)
}
func init() {
discovery.RegisterConfig(&SDConfig{})
}
// Discovery periodically performs Scaleway requests. It implements
// the Discoverer interface.
type Discovery struct{}
func NewDiscovery(conf *SDConfig, logger *slog.Logger, metrics discovery.DiscovererMetrics) (*refresh.Discovery, error) {
m, ok := metrics.(*scalewayMetrics)
if !ok {
return nil, errors.New("invalid discovery metrics type")
}
r, err := newRefresher(conf)
if err != nil {
return nil, err
}
return refresh.NewDiscovery(
refresh.Options{
Logger: logger,
Mech: "scaleway",
Interval: time.Duration(conf.RefreshInterval),
RefreshF: r.refresh,
MetricsInstantiator: m.refreshMetrics,
},
), nil
}
type refresher interface {
refresh(context.Context) ([]*targetgroup.Group, error)
}
func newRefresher(conf *SDConfig) (refresher, error) {
switch conf.Role {
case roleBaremetal:
return newBaremetalDiscovery(conf)
case roleInstance:
return newInstanceDiscovery(conf)
}
return nil, errors.New("unknown Scaleway discovery role")
}
func loadProfile(sdConfig *SDConfig) (*scw.Profile, error) {
// Profile coming from Prometheus Configuration file
prometheusConfigProfile := &scw.Profile{
DefaultZone: scw.StringPtr(sdConfig.Zone),
APIURL: scw.StringPtr(sdConfig.APIURL),
SecretKey: scw.StringPtr(sdConfig.secretKeyForConfig()),
AccessKey: scw.StringPtr(sdConfig.AccessKey),
DefaultProjectID: scw.StringPtr(sdConfig.Project),
SendTelemetry: scw.BoolPtr(false),
}
return prometheusConfigProfile, nil
}
type authTokenFileRoundTripper struct {
authTokenFile string
rt http.RoundTripper
}
// newAuthTokenFileRoundTripper adds the auth token read from the file to a request.
func newAuthTokenFileRoundTripper(tokenFile string, rt http.RoundTripper) (http.RoundTripper, error) {
// fail-fast if we can't read the file.
_, err := os.ReadFile(tokenFile)
if err != nil {
return nil, fmt.Errorf("unable to read auth token file %s: %w", tokenFile, err)
}
return &authTokenFileRoundTripper{tokenFile, rt}, nil
}
func (rt *authTokenFileRoundTripper) RoundTrip(request *http.Request) (*http.Response, error) {
b, err := os.ReadFile(rt.authTokenFile)
if err != nil {
return nil, fmt.Errorf("unable to read auth token file %s: %w", rt.authTokenFile, err)
}
authToken := strings.TrimSpace(string(b))
request.Header.Set("X-Auth-Token", authToken)
return rt.rt.RoundTrip(request)
}