discovery: add STACKIT SD (#16401)
parent
5a1cce4fbb
commit
ceaa3bd6f9
@ -0,0 +1,4 @@ |
||||
scrape_configs: |
||||
- job_name: stackit |
||||
stackit_sd_configs: |
||||
- endpoint: "://invalid" |
@ -0,0 +1,32 @@ |
||||
// Copyright 2015 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 ( |
||||
"github.com/prometheus/prometheus/discovery" |
||||
) |
||||
|
||||
var _ discovery.DiscovererMetrics = (*stackitMetrics)(nil) |
||||
|
||||
type stackitMetrics struct { |
||||
refreshMetrics discovery.RefreshMetricsInstantiator |
||||
} |
||||
|
||||
// Register implements discovery.DiscovererMetrics.
|
||||
func (m *stackitMetrics) Register() error { |
||||
return nil |
||||
} |
||||
|
||||
// Unregister implements discovery.DiscovererMetrics.
|
||||
func (m *stackitMetrics) Unregister() {} |
@ -0,0 +1,162 @@ |
||||
// 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 ( |
||||
"bytes" |
||||
"fmt" |
||||
"io" |
||||
"net/http" |
||||
"net/http/httptest" |
||||
"testing" |
||||
) |
||||
|
||||
// SDMock is the interface for the STACKIT IAAS API mock.
|
||||
type SDMock struct { |
||||
t *testing.T |
||||
Server *httptest.Server |
||||
Mux *http.ServeMux |
||||
} |
||||
|
||||
// NewSDMock returns a new SDMock.
|
||||
func NewSDMock(t *testing.T) *SDMock { |
||||
return &SDMock{ |
||||
t: t, |
||||
} |
||||
} |
||||
|
||||
// Endpoint returns the URI to the mock server.
|
||||
func (m *SDMock) Endpoint() string { |
||||
return m.Server.URL + "/" |
||||
} |
||||
|
||||
// Setup creates the mock server.
|
||||
func (m *SDMock) Setup() { |
||||
m.Mux = http.NewServeMux() |
||||
m.Server = httptest.NewServer(m.Mux) |
||||
m.t.Cleanup(m.Server.Close) |
||||
} |
||||
|
||||
// ShutdownServer creates the mock server.
|
||||
func (m *SDMock) ShutdownServer() { |
||||
m.Server.Close() |
||||
} |
||||
|
||||
const ( |
||||
testToken = "LRK9DAWQ1ZAEFSrCNEEzLCUwhYX1U3g7wMg4dTlkkDC96fyDuyJ39nVbVjCKSDfj" |
||||
testProjectID = "00000000-0000-0000-0000-000000000000" |
||||
) |
||||
|
||||
// HandleServers mocks the STACKIT IAAS API.
|
||||
func (m *SDMock) HandleServers() { |
||||
// /token endpoint mocks the token endpoint for service account authentication.
|
||||
// It checks if the request body starts with "assertion=ey" to simulate a valid assertion
|
||||
// as defined in RFC 7523.
|
||||
m.Mux.HandleFunc("/token", func(w http.ResponseWriter, r *http.Request) { |
||||
reqBody, err := io.ReadAll(r.Body) |
||||
if err != nil { |
||||
w.WriteHeader(http.StatusInternalServerError) |
||||
_, _ = fmt.Fprint(w, err) |
||||
return |
||||
} |
||||
|
||||
// Expecting HTTP form encoded body with the field assertion.
|
||||
// JWT always start with "ey" (base64url encoded).
|
||||
if !bytes.HasPrefix(reqBody, []byte("assertion=ey")) { |
||||
w.WriteHeader(http.StatusUnauthorized) |
||||
return |
||||
} |
||||
|
||||
w.Header().Add("content-type", "application/json; charset=utf-8") |
||||
w.WriteHeader(http.StatusOK) |
||||
|
||||
_, _ = fmt.Fprintf(w, `{"access_token": "%s"}`, testToken) |
||||
}) |
||||
|
||||
m.Mux.HandleFunc(fmt.Sprintf("/v1/projects/%s/servers", testProjectID), func(w http.ResponseWriter, r *http.Request) { |
||||
if r.Header.Get("Authorization") != fmt.Sprintf("Bearer %s", testToken) { |
||||
w.WriteHeader(http.StatusUnauthorized) |
||||
return |
||||
} |
||||
|
||||
w.Header().Add("content-type", "application/json; charset=utf-8") |
||||
w.WriteHeader(http.StatusOK) |
||||
|
||||
_, _ = fmt.Fprint(w, ` |
||||
{ |
||||
"items": [ |
||||
{ |
||||
"availabilityZone": "eu01-3", |
||||
"bootVolume": { |
||||
"deleteOnTermination": false, |
||||
"id": "1c15e4cc-8474-46be-b875-b473ea9fe80c" |
||||
}, |
||||
"createdAt": "2025-03-12T14:48:17Z", |
||||
"id": "b4176700-596a-4f80-9fc8-5f9c58a606e1", |
||||
"labels": { |
||||
"provisionSTACKITServerAgent": "true", |
||||
"stackit_project_id": "00000000-0000-0000-0000-000000000000" |
||||
}, |
||||
"launchedAt": "2025-03-12T14:48:52Z", |
||||
"machineType": "g1.1", |
||||
"name": "runcommandtest", |
||||
"nics": [ |
||||
{ |
||||
"ipv4": "10.0.0.153", |
||||
"mac": "fa:16:4f:42:1c:d3", |
||||
"networkId": "3173494f-2f6c-490d-8c12-4b3c86b4338b", |
||||
"networkName": "test", |
||||
"publicIp": "192.0.2.1", |
||||
"nicId": "b36097c5-e1c5-4e12-ae97-c03e144db127", |
||||
"nicSecurity": true, |
||||
"securityGroups": [ |
||||
"6e60809f-bed3-46c6-a39c-adddd6455674" |
||||
] |
||||
} |
||||
], |
||||
"powerStatus": "STOPPED", |
||||
"serviceAccountMails": [], |
||||
"status": "INACTIVE", |
||||
"updatedAt": "2025-03-13T07:08:29Z", |
||||
"userData": null, |
||||
"volumes": [ |
||||
"1c15e4cc-8474-46be-b875-b473ea9fe80c" |
||||
] |
||||
}, |
||||
{ |
||||
"availabilityZone": "eu01-m", |
||||
"bootVolume": { |
||||
"deleteOnTermination": false, |
||||
"id": "1e3ffe2b-878f-46e5-b39e-372e13a09551" |
||||
}, |
||||
"createdAt": "2025-04-10T16:45:25Z", |
||||
"id": "ee337436-1f15-4647-a03e-154009966179", |
||||
"labels": {}, |
||||
"launchedAt": "2025-04-10T16:46:00Z", |
||||
"machineType": "t1.1", |
||||
"name": "server1", |
||||
"nics": [], |
||||
"powerStatus": "RUNNING", |
||||
"serviceAccountMails": [], |
||||
"status": "ACTIVE", |
||||
"updatedAt": "2025-04-10T16:46:00Z", |
||||
"volumes": [ |
||||
"1e3ffe2b-878f-46e5-b39e-372e13a09551" |
||||
] |
||||
} |
||||
] |
||||
}`, |
||||
) |
||||
}) |
||||
} |
@ -0,0 +1,222 @@ |
||||
// 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 |
||||
} |
@ -0,0 +1,131 @@ |
||||
// 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" |
||||
"crypto/rand" |
||||
"crypto/rsa" |
||||
"crypto/x509" |
||||
"encoding/pem" |
||||
"testing" |
||||
|
||||
"github.com/prometheus/common/model" |
||||
"github.com/prometheus/common/promslog" |
||||
"github.com/stretchr/testify/require" |
||||
) |
||||
|
||||
type serverSDTestSuite struct { |
||||
Mock *SDMock |
||||
} |
||||
|
||||
func (s *serverSDTestSuite) SetupTest(t *testing.T) { |
||||
s.Mock = NewSDMock(t) |
||||
s.Mock.Setup() |
||||
|
||||
s.Mock.HandleServers() |
||||
} |
||||
|
||||
func TestServerSDRefresh(t *testing.T) { |
||||
for _, tc := range []struct { |
||||
name string |
||||
cfg SDConfig |
||||
}{ |
||||
{ |
||||
name: "default with token", |
||||
cfg: func() SDConfig { |
||||
cfg := DefaultSDConfig |
||||
cfg.HTTPClientConfig.BearerToken = testToken |
||||
|
||||
return cfg |
||||
}(), |
||||
}, |
||||
{ |
||||
name: "default with service account key", |
||||
cfg: func() SDConfig { |
||||
// Generate a new RSA key pair with a size of 2048 bits
|
||||
key, err := rsa.GenerateKey(rand.Reader, 2048) |
||||
require.NoError(t, err) |
||||
|
||||
cfg := DefaultSDConfig |
||||
cfg.PrivateKey = string(pem.EncodeToMemory(&pem.Block{ |
||||
Type: "RSA PRIVATE KEY", |
||||
Bytes: x509.MarshalPKCS1PrivateKey(key), |
||||
})) |
||||
|
||||
cfg.ServiceAccountKey = `{ |
||||
"Active": true, |
||||
"CreatedAt": "2025-04-05T12:34:56Z", |
||||
"Credentials": { |
||||
"Aud": "https://stackit-service-account-prod.apps.01.cf.eu01.stackit.cloud", |
||||
"Iss": "stackit@sa.stackit.cloud", |
||||
"Kid": "123e4567-e89b-12d3-a456-426614174000", |
||||
"Sub": "123e4567-e89b-12d3-a456-426614174001" |
||||
}, |
||||
"ID": "123e4567-e89b-12d3-a456-426614174002", |
||||
"KeyAlgorithm": "RSA_2048", |
||||
"KeyOrigin": "USER_PROVIDED", |
||||
"KeyType": "USER_MANAGED", |
||||
"PublicKey": "...", |
||||
"ValidUntil": "2025-04-05T13:34:56Z" |
||||
}` |
||||
|
||||
return cfg |
||||
}(), |
||||
}, |
||||
} { |
||||
t.Run(tc.name, func(t *testing.T) { |
||||
suite := &serverSDTestSuite{} |
||||
suite.SetupTest(t) |
||||
defer suite.Mock.ShutdownServer() |
||||
|
||||
tc.cfg.Endpoint = suite.Mock.Endpoint() |
||||
tc.cfg.tokenURL = suite.Mock.Endpoint() + "token" |
||||
tc.cfg.Project = testProjectID |
||||
|
||||
d, err := newServerDiscovery(&tc.cfg, promslog.NewNopLogger()) |
||||
require.NoError(t, err) |
||||
|
||||
targetGroups, err := d.refresh(context.Background()) |
||||
require.NoError(t, err) |
||||
require.Len(t, targetGroups, 1) |
||||
|
||||
targetGroup := targetGroups[0] |
||||
require.NotNil(t, targetGroup, "targetGroup should not be nil") |
||||
require.NotNil(t, targetGroup.Targets, "targetGroup.targets should not be nil") |
||||
require.Len(t, targetGroup.Targets, 1) |
||||
|
||||
for i, labelSet := range []model.LabelSet{ |
||||
{ |
||||
"__address__": model.LabelValue("192.0.2.1:80"), |
||||
"__meta_stackit_project": model.LabelValue("00000000-0000-0000-0000-000000000000"), |
||||
"__meta_stackit_id": model.LabelValue("b4176700-596a-4f80-9fc8-5f9c58a606e1"), |
||||
"__meta_stackit_type": model.LabelValue("g1.1"), |
||||
"__meta_stackit_private_ipv4_test": model.LabelValue("10.0.0.153"), |
||||
"__meta_stackit_public_ipv4": model.LabelValue("192.0.2.1"), |
||||
"__meta_stackit_labelpresent_provisionSTACKITServerAgent": model.LabelValue("true"), |
||||
"__meta_stackit_label_provisionSTACKITServerAgent": model.LabelValue("true"), |
||||
"__meta_stackit_labelpresent_stackit_project_id": model.LabelValue("true"), |
||||
"__meta_stackit_name": model.LabelValue("runcommandtest"), |
||||
"__meta_stackit_availability_zone": model.LabelValue("eu01-3"), |
||||
"__meta_stackit_status": model.LabelValue("INACTIVE"), |
||||
"__meta_stackit_power_status": model.LabelValue("STOPPED"), |
||||
"__meta_stackit_label_stackit_project_id": model.LabelValue("00000000-0000-0000-0000-000000000000"), |
||||
}, |
||||
} { |
||||
require.Equal(t, labelSet, targetGroup.Targets[i]) |
||||
} |
||||
}) |
||||
} |
||||
} |
@ -0,0 +1,153 @@ |
||||
// 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" |
||||
"errors" |
||||
"fmt" |
||||
"log/slog" |
||||
"net/url" |
||||
"time" |
||||
|
||||
"github.com/prometheus/client_golang/prometheus" |
||||
"github.com/prometheus/common/config" |
||||
"github.com/prometheus/common/model" |
||||
"github.com/prometheus/common/version" |
||||
|
||||
"github.com/prometheus/prometheus/discovery" |
||||
"github.com/prometheus/prometheus/discovery/refresh" |
||||
"github.com/prometheus/prometheus/discovery/targetgroup" |
||||
) |
||||
|
||||
const ( |
||||
stackitLabelPrefix = model.MetaLabelPrefix + "stackit_" |
||||
stackitLabelProject = stackitLabelPrefix + "project" |
||||
stackitLabelID = stackitLabelPrefix + "id" |
||||
stackitLabelName = stackitLabelPrefix + "name" |
||||
stackitLabelStatus = stackitLabelPrefix + "status" |
||||
stackitLabelPowerStatus = stackitLabelPrefix + "power_status" |
||||
stackitLabelAvailabilityZone = stackitLabelPrefix + "availability_zone" |
||||
stackitLabelPublicIPv4 = stackitLabelPrefix + "public_ipv4" |
||||
) |
||||
|
||||
var userAgent = version.PrometheusUserAgent() |
||||
|
||||
// DefaultSDConfig is the default STACKIT SD configuration.
|
||||
var DefaultSDConfig = SDConfig{ |
||||
Region: "eu01", |
||||
Port: 80, |
||||
RefreshInterval: model.Duration(60 * time.Second), |
||||
HTTPClientConfig: config.DefaultHTTPClientConfig, |
||||
} |
||||
|
||||
func init() { |
||||
discovery.RegisterConfig(&SDConfig{}) |
||||
} |
||||
|
||||
// SDConfig is the configuration for STACKIT based service discovery.
|
||||
type SDConfig struct { |
||||
HTTPClientConfig config.HTTPClientConfig `yaml:",inline"` |
||||
|
||||
Project string `yaml:"project"` |
||||
RefreshInterval model.Duration `yaml:"refresh_interval,omitempty"` |
||||
Port int `yaml:"port,omitempty"` |
||||
Region string `yaml:"region,omitempty"` |
||||
Endpoint string `yaml:"endpoint,omitempty"` |
||||
ServiceAccountKey string `yaml:"service_account_key,omitempty"` |
||||
PrivateKey string `yaml:"private_key,omitempty"` |
||||
ServiceAccountKeyPath string `yaml:"service_account_key_path,omitempty"` |
||||
PrivateKeyPath string `yaml:"private_key_path,omitempty"` |
||||
CredentialsFilePath string `yaml:"credentials_file_path,omitempty"` |
||||
|
||||
// For testing only
|
||||
tokenURL string |
||||
} |
||||
|
||||
// NewDiscovererMetrics implements discovery.Config.
|
||||
func (*SDConfig) NewDiscovererMetrics(_ prometheus.Registerer, rmi discovery.RefreshMetricsInstantiator) discovery.DiscovererMetrics { |
||||
return &stackitMetrics{ |
||||
refreshMetrics: rmi, |
||||
} |
||||
} |
||||
|
||||
// Name returns the name of the Config.
|
||||
func (*SDConfig) Name() string { return "stackit" } |
||||
|
||||
// NewDiscoverer returns a Discoverer for the Config.
|
||||
func (c *SDConfig) NewDiscoverer(opts discovery.DiscovererOptions) (discovery.Discoverer, error) { |
||||
return NewDiscovery(c, opts.Logger, opts.Metrics) |
||||
} |
||||
|
||||
type refresher interface { |
||||
refresh(context.Context) ([]*targetgroup.Group, error) |
||||
} |
||||
|
||||
// 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.Endpoint == "" && c.Region == "" { |
||||
return errors.New("stackit_sd: endpoint and region missing") |
||||
} |
||||
|
||||
if _, err = url.Parse(c.Endpoint); err != nil { |
||||
return fmt.Errorf("stackit_sd: invalid endpoint %q: %w", c.Endpoint, err) |
||||
} |
||||
|
||||
return c.HTTPClientConfig.Validate() |
||||
} |
||||
|
||||
// SetDirectory joins any relative file paths with dir.
|
||||
func (c *SDConfig) SetDirectory(dir string) { |
||||
c.HTTPClientConfig.SetDirectory(dir) |
||||
} |
||||
|
||||
// Discovery periodically performs STACKIT API requests. It implements
|
||||
// the Discoverer interface.
|
||||
type Discovery struct { |
||||
*refresh.Discovery |
||||
} |
||||
|
||||
// NewDiscovery returns a new Discovery which periodically refreshes its targets.
|
||||
func NewDiscovery(conf *SDConfig, logger *slog.Logger, metrics discovery.DiscovererMetrics) (*refresh.Discovery, error) { |
||||
m, ok := metrics.(*stackitMetrics) |
||||
if !ok { |
||||
return nil, errors.New("invalid discovery metrics type") |
||||
} |
||||
|
||||
r, err := newRefresher(conf, logger) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
return refresh.NewDiscovery( |
||||
refresh.Options{ |
||||
Logger: logger, |
||||
Mech: "stackit", |
||||
Interval: time.Duration(conf.RefreshInterval), |
||||
RefreshF: r.refresh, |
||||
MetricsInstantiator: m.refreshMetrics, |
||||
}, |
||||
), nil |
||||
} |
||||
|
||||
func newRefresher(conf *SDConfig, l *slog.Logger) (refresher, error) { |
||||
return newServerDiscovery(conf, l) |
||||
} |
@ -0,0 +1,38 @@ |
||||
// 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 |
||||
|
||||
// ServerListResponse Response object for server list request.
|
||||
// https://docs.api.eu01.stackit.cloud/documentation/iaas/version/v1#tag/Servers/operation/v1ListServersInProject
|
||||
type ServerListResponse struct { |
||||
Items *[]Server `json:"items"` |
||||
} |
||||
|
||||
type Server struct { |
||||
AvailabilityZone string `json:"availabilityZone"` |
||||
ID string `json:"id"` |
||||
Labels map[string]interface{} `json:"labels"` |
||||
MachineType string `json:"machineType"` |
||||
Name string `json:"name"` |
||||
Nics []ServerNetwork `json:"nics"` |
||||
PowerStatus string `json:"powerStatus"` |
||||
Status string `json:"status"` |
||||
} |
||||
|
||||
// ServerNetwork Describes the object that matches servers to its networks.
|
||||
type ServerNetwork struct { |
||||
NetworkName string `json:"networkName"` |
||||
IPv4 *string `json:"ipv4,omitempty"` |
||||
PublicIP *string `json:"publicIp,omitempty"` |
||||
} |
@ -0,0 +1,34 @@ |
||||
# A example scrape configuration for running Prometheus with |
||||
# STACKIT. |
||||
|
||||
scrape_configs: |
||||
# Make Prometheus scrape itself for metrics. |
||||
- job_name: "prometheus" |
||||
static_configs: |
||||
- targets: ["localhost:9090"] |
||||
|
||||
# Discover Node Exporter instances to scrape. |
||||
- job_name: "node" |
||||
|
||||
stackit_sd_configs: |
||||
- project: 11111111-1111-1111-1111-111111111111 |
||||
authorization: |
||||
credentials: "<replace with a STACKIT ServiceAccount Token>" |
||||
relabel_configs: |
||||
# Use the public IPv4 and port 9100 to scrape the target. |
||||
- source_labels: [__meta_stackit_public_ipv4] |
||||
target_label: __address__ |
||||
replacement: "$1:9100" |
||||
|
||||
# Discover Node Exporter instances to scrape using a STACKIT Subnet called mynet. |
||||
- job_name: "node_private" |
||||
|
||||
stackit_sd_configs: |
||||
- project: 11111111-1111-1111-1111-111111111111 |
||||
authorization: |
||||
credentials: "<replace with a STACKIT ServiceAccount Token>" |
||||
relabel_configs: |
||||
# Use the private IPv4 within the STACKIT Subnet and port 9100 to scrape the target. |
||||
- source_labels: [__meta_stackit_private_ipv4_mynet] |
||||
target_label: __address__ |
||||
replacement: "$1:9100" |
Loading…
Reference in new issue