You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
223 lines
6.6 KiB
223 lines
6.6 KiB
![]()
1 week ago
|
// Copyright 2020 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 stackit
|
||
|
|
||
|
import (
|
||
|
"context"
|
||
|
"encoding/json"
|
||
|
"fmt"
|
||
|
"io"
|
||
|
"log/slog"
|
||
|
"net"
|
||
|
"net/http"
|
||
|
"net/url"
|
||
|
"strconv"
|
||
|
"time"
|
||
|
|
||
|
"github.com/prometheus/common/config"
|
||
|
"github.com/prometheus/common/model"
|
||
|
"github.com/stackitcloud/stackit-sdk-go/core/auth"
|
||
|
stackitconfig "github.com/stackitcloud/stackit-sdk-go/core/config"
|
||
|
|
||
|
"github.com/prometheus/prometheus/discovery/refresh"
|
||
|
"github.com/prometheus/prometheus/discovery/targetgroup"
|
||
|
"github.com/prometheus/prometheus/util/strutil"
|
||
|
)
|
||
|
|
||
|
const (
|
||
|
stackitAPIEndpoint = "https://iaas.api.%s.stackit.cloud"
|
||
|
|
||
|
stackitLabelPrivateIPv4 = stackitLabelPrefix + "private_ipv4_"
|
||
|
stackitLabelType = stackitLabelPrefix + "type"
|
||
|
stackitLabelLabel = stackitLabelPrefix + "label_"
|
||
|
stackitLabelLabelPresent = stackitLabelPrefix + "labelpresent_"
|
||
|
)
|
||
|
|
||
|
// Discovery periodically performs STACKIT Cloud requests.
|
||
|
// It implements the Discoverer interface.
|
||
|
type iaasDiscovery struct {
|
||
|
*refresh.Discovery
|
||
|
httpClient *http.Client
|
||
|
logger *slog.Logger
|
||
|
apiEndpoint string
|
||
|
project string
|
||
|
port int
|
||
|
}
|
||
|
|
||
|
// newServerDiscovery returns a new iaasDiscovery, which periodically refreshes its targets.
|
||
|
func newServerDiscovery(conf *SDConfig, logger *slog.Logger) (*iaasDiscovery, error) {
|
||
|
d := &iaasDiscovery{
|
||
|
project: conf.Project,
|
||
|
port: conf.Port,
|
||
|
apiEndpoint: conf.Endpoint,
|
||
|
logger: logger,
|
||
|
}
|
||
|
|
||
|
rt, err := config.NewRoundTripperFromConfig(conf.HTTPClientConfig, "stackit_sd")
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
|
||
|
d.apiEndpoint = conf.Endpoint
|
||
|
if d.apiEndpoint == "" {
|
||
|
d.apiEndpoint = fmt.Sprintf(stackitAPIEndpoint, conf.Region)
|
||
|
}
|
||
|
|
||
|
servers := stackitconfig.ServerConfigurations{stackitconfig.ServerConfiguration{
|
||
|
URL: d.apiEndpoint,
|
||
|
Description: "STACKIT IAAS API",
|
||
|
}}
|
||
|
|
||
|
d.httpClient = &http.Client{
|
||
|
Timeout: time.Duration(conf.RefreshInterval),
|
||
|
Transport: rt,
|
||
|
}
|
||
|
|
||
|
stackitConfiguration := &stackitconfig.Configuration{
|
||
|
UserAgent: userAgent,
|
||
|
HTTPClient: d.httpClient,
|
||
|
Servers: servers,
|
||
|
NoAuth: conf.ServiceAccountKey == "" && conf.ServiceAccountKeyPath == "",
|
||
|
|
||
|
ServiceAccountKey: conf.ServiceAccountKey,
|
||
|
PrivateKey: conf.PrivateKey,
|
||
|
ServiceAccountKeyPath: conf.ServiceAccountKeyPath,
|
||
|
PrivateKeyPath: conf.PrivateKeyPath,
|
||
|
CredentialsFilePath: conf.CredentialsFilePath,
|
||
|
}
|
||
|
|
||
|
if conf.tokenURL != "" {
|
||
|
stackitConfiguration.TokenCustomUrl = conf.tokenURL
|
||
|
}
|
||
|
|
||
|
authRoundTripper, err := auth.SetupAuth(stackitConfiguration)
|
||
|
if err != nil {
|
||
|
return nil, fmt.Errorf("setting up authentication: %w", err)
|
||
|
}
|
||
|
|
||
|
d.httpClient.Transport = authRoundTripper
|
||
|
|
||
|
return d, nil
|
||
|
}
|
||
|
|
||
|
func (i *iaasDiscovery) refresh(ctx context.Context) ([]*targetgroup.Group, error) {
|
||
|
apiURL, err := url.Parse(i.apiEndpoint)
|
||
|
if err != nil {
|
||
|
return nil, fmt.Errorf("invalid API endpoint URL %s: %w", i.apiEndpoint, err)
|
||
|
}
|
||
|
|
||
|
apiURL.Path, err = url.JoinPath(apiURL.Path, "v1", "projects", i.project, "servers")
|
||
|
if err != nil {
|
||
|
return nil, fmt.Errorf("joining URL path: %w", err)
|
||
|
}
|
||
|
|
||
|
q := apiURL.Query()
|
||
|
q.Set("details", "true")
|
||
|
apiURL.RawQuery = q.Encode()
|
||
|
|
||
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, apiURL.String(), nil)
|
||
|
if err != nil {
|
||
|
return nil, fmt.Errorf("creating request: %w", err)
|
||
|
}
|
||
|
|
||
|
req.Header.Set("Accept", "application/json")
|
||
|
|
||
|
res, err := i.httpClient.Do(req)
|
||
|
if err != nil {
|
||
|
return nil, fmt.Errorf("sending request: %w", err)
|
||
|
}
|
||
|
|
||
|
defer res.Body.Close()
|
||
|
|
||
|
if res.StatusCode != http.StatusOK {
|
||
|
errorMessage, _ := io.ReadAll(res.Body)
|
||
|
|
||
|
return nil, fmt.Errorf("unexpected status code %d: %s", res.StatusCode, string(errorMessage))
|
||
|
}
|
||
|
|
||
|
var serversResponse *ServerListResponse
|
||
|
|
||
|
if err := json.NewDecoder(res.Body).Decode(&serversResponse); err != nil {
|
||
|
return nil, fmt.Errorf("decoding response: %w", err)
|
||
|
}
|
||
|
|
||
|
if serversResponse == nil || serversResponse.Items == nil || len(*serversResponse.Items) == 0 {
|
||
|
return []*targetgroup.Group{{Source: "stackit", Targets: []model.LabelSet{}}}, nil
|
||
|
}
|
||
|
|
||
|
targets := make([]model.LabelSet, 0, len(*serversResponse.Items))
|
||
|
for _, server := range *serversResponse.Items {
|
||
|
if server.Nics == nil {
|
||
|
i.logger.Debug("server has no network interfaces. Skipping", slog.String("server_id", server.ID))
|
||
|
continue
|
||
|
}
|
||
|
|
||
|
labels := model.LabelSet{
|
||
|
stackitLabelProject: model.LabelValue(i.project),
|
||
|
stackitLabelID: model.LabelValue(server.ID),
|
||
|
stackitLabelName: model.LabelValue(server.Name),
|
||
|
stackitLabelAvailabilityZone: model.LabelValue(server.AvailabilityZone),
|
||
|
stackitLabelStatus: model.LabelValue(server.Status),
|
||
|
stackitLabelPowerStatus: model.LabelValue(server.PowerStatus),
|
||
|
stackitLabelType: model.LabelValue(server.MachineType),
|
||
|
}
|
||
|
|
||
|
var (
|
||
|
addressLabel string
|
||
|
serverPublicIP string
|
||
|
)
|
||
|
|
||
|
for _, nic := range server.Nics {
|
||
|
if nic.PublicIP != nil && *nic.PublicIP != "" && serverPublicIP == "" {
|
||
|
serverPublicIP = *nic.PublicIP
|
||
|
addressLabel = serverPublicIP
|
||
|
}
|
||
|
|
||
|
if nic.IPv4 != nil && *nic.IPv4 != "" {
|
||
|
networkLabel := model.LabelName(stackitLabelPrivateIPv4 + strutil.SanitizeLabelName(nic.NetworkName))
|
||
|
labels[networkLabel] = model.LabelValue(*nic.IPv4)
|
||
|
if addressLabel == "" {
|
||
|
addressLabel = *nic.IPv4
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if addressLabel == "" {
|
||
|
// Skip servers without IPs.
|
||
|
continue
|
||
|
}
|
||
|
|
||
|
// Public IPs for servers are optional.
|
||
|
if serverPublicIP != "" {
|
||
|
labels[stackitLabelPublicIPv4] = model.LabelValue(serverPublicIP)
|
||
|
}
|
||
|
|
||
|
labels[model.AddressLabel] = model.LabelValue(net.JoinHostPort(addressLabel, strconv.FormatUint(uint64(i.port), 10)))
|
||
|
|
||
|
for labelKey, labelValue := range server.Labels {
|
||
|
if labelStringValue, ok := labelValue.(string); ok {
|
||
|
presentLabel := model.LabelName(stackitLabelLabelPresent + strutil.SanitizeLabelName(labelKey))
|
||
|
labels[presentLabel] = "true"
|
||
|
|
||
|
label := model.LabelName(stackitLabelLabel + strutil.SanitizeLabelName(labelKey))
|
||
|
labels[label] = model.LabelValue(labelStringValue)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
targets = append(targets, labels)
|
||
|
}
|
||
|
|
||
|
return []*targetgroup.Group{{Source: "stackit", Targets: targets}}, nil
|
||
|
}
|