Skip to content
This repository was archived by the owner on Mar 18, 2025. It is now read-only.

Commit 166752f

Browse files
authored
feat: support sigv4 signing (#169)
* feat: support sigv4 signing --------- Signed-off-by: obanby <[email protected]>
1 parent bb07c3d commit 166752f

File tree

9 files changed

+826
-0
lines changed

9 files changed

+826
-0
lines changed

Diff for: pkg/remote/client.go

+10
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ import (
1212
"net/url"
1313
"time"
1414

15+
"github.com/grafana/xk6-output-prometheus-remote/pkg/sigv4"
16+
1517
prompb "buf.build/gen/go/prometheus/prometheus/protocolbuffers/go"
1618
"github.com/klauspost/compress/snappy"
1719
"google.golang.org/protobuf/proto"
@@ -22,6 +24,7 @@ type HTTPConfig struct {
2224
Timeout time.Duration
2325
TLSConfig *tls.Config
2426
BasicAuth *BasicAuth
27+
SigV4 *sigv4.Config
2528
Headers http.Header
2629
}
2730

@@ -60,6 +63,13 @@ func NewWriteClient(endpoint string, cfg *HTTPConfig) (*WriteClient, error) {
6063
TLSClientConfig: cfg.TLSConfig,
6164
}
6265
}
66+
if cfg.SigV4 != nil {
67+
tripper, err := sigv4.NewRoundTripper(cfg.SigV4, wc.hc.Transport)
68+
if err != nil {
69+
return nil, err
70+
}
71+
wc.hc.Transport = tripper
72+
}
6373
return wc, nil
6474
}
6575

Diff for: pkg/remotewrite/config.go

+64
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,15 @@ package remotewrite
33
import (
44
"crypto/tls"
55
"encoding/json"
6+
"errors"
67
"fmt"
78
"net/http"
89
"strconv"
910
"strings"
1011
"time"
1112

13+
"github.com/grafana/xk6-output-prometheus-remote/pkg/sigv4"
14+
1215
"github.com/grafana/xk6-output-prometheus-remote/pkg/remote"
1316
"go.k6.io/k6/lib/types"
1417
"gopkg.in/guregu/null.v3"
@@ -68,6 +71,15 @@ type Config struct {
6871
TrendStats []string `json:"trendStats"`
6972

7073
StaleMarkers null.Bool `json:"staleMarkers"`
74+
75+
// SigV4Region is the AWS region where the workspace is.
76+
SigV4Region null.String `json:"sigV4Region"`
77+
78+
// SigV4AccessKey is the AWS access key.
79+
SigV4AccessKey null.String `json:"sigV4AccessKey"`
80+
81+
// SigV4SecretKey is the AWS secret key.
82+
SigV4SecretKey null.String `json:"sigV4SecretKey"`
7183
}
7284

7385
// NewConfig creates an Output's configuration.
@@ -81,6 +93,9 @@ func NewConfig() Config {
8193
Headers: make(map[string]string),
8294
TrendStats: defaultTrendStats,
8395
StaleMarkers: null.BoolFrom(false),
96+
SigV4Region: null.NewString("", false),
97+
SigV4AccessKey: null.NewString("", false),
98+
SigV4SecretKey: null.NewString("", false),
8499
}
85100
}
86101

@@ -110,6 +125,22 @@ func (conf Config) RemoteConfig() (*remote.HTTPConfig, error) {
110125
hc.TLSConfig.Certificates = []tls.Certificate{cert}
111126
}
112127

128+
if isSigV4PartiallyConfigured(conf.SigV4Region, conf.SigV4AccessKey, conf.SigV4SecretKey) {
129+
return nil, errors.New(
130+
"sigv4 seems to be partially configured. All of " +
131+
"K6_PROMETHEUS_RW_SIGV4_REGION, K6_PROMETHEUS_RW_SIGV4_ACCESS_KEY, K6_PROMETHEUS_RW_SIGV4_SECRET_KEY " +
132+
"must all be set. Unset all to bypass sigv4",
133+
)
134+
}
135+
136+
if conf.SigV4Region.Valid && conf.SigV4AccessKey.Valid && conf.SigV4SecretKey.Valid {
137+
hc.SigV4 = &sigv4.Config{
138+
Region: conf.SigV4Region.String,
139+
AwsAccessKeyID: conf.SigV4AccessKey.String,
140+
AwsSecretAccessKey: conf.SigV4SecretKey.String,
141+
}
142+
}
143+
113144
if len(conf.Headers) > 0 {
114145
hc.Headers = make(http.Header)
115146
for k, v := range conf.Headers {
@@ -149,6 +180,18 @@ func (conf Config) Apply(applied Config) Config {
149180
conf.BearerToken = applied.BearerToken
150181
}
151182

183+
if applied.SigV4Region.Valid {
184+
conf.SigV4Region = applied.SigV4Region
185+
}
186+
187+
if applied.SigV4AccessKey.Valid {
188+
conf.SigV4AccessKey = applied.SigV4AccessKey
189+
}
190+
191+
if applied.SigV4SecretKey.Valid {
192+
conf.SigV4SecretKey = applied.SigV4SecretKey
193+
}
194+
152195
if applied.PushInterval.Valid {
153196
conf.PushInterval = applied.PushInterval
154197
}
@@ -299,6 +342,18 @@ func parseEnvs(env map[string]string) (Config, error) {
299342
}
300343
}
301344

345+
if sigV4Region, sigV4RegionDefined := env["K6_PROMETHEUS_RW_SIGV4_REGION"]; sigV4RegionDefined {
346+
c.SigV4Region = null.StringFrom(sigV4Region)
347+
}
348+
349+
if sigV4AccessKey, sigV4AccessKeyDefined := env["K6_PROMETHEUS_RW_SIGV4_ACCESS_KEY"]; sigV4AccessKeyDefined {
350+
c.SigV4AccessKey = null.StringFrom(sigV4AccessKey)
351+
}
352+
353+
if sigV4SecretKey, sigV4SecretKeyDefined := env["K6_PROMETHEUS_RW_SIGV4_SECRET_KEY"]; sigV4SecretKeyDefined {
354+
c.SigV4SecretKey = null.StringFrom(sigV4SecretKey)
355+
}
356+
302357
if b, err := envBool(env, "K6_PROMETHEUS_RW_TREND_AS_NATIVE_HISTOGRAM"); err != nil {
303358
return c, err
304359
} else if b.Valid {
@@ -384,3 +439,12 @@ func parseArg(text string) (Config, error) {
384439

385440
return c, nil
386441
}
442+
443+
func isSigV4PartiallyConfigured(region, accessKey, secretKey null.String) bool {
444+
hasRegion := region.Valid && len(strings.TrimSpace(region.String)) != 0
445+
hasAccessID := accessKey.Valid && len(strings.TrimSpace(accessKey.String)) != 0
446+
hasSecretAccessKey := secretKey.Valid && len(strings.TrimSpace(secretKey.String)) != 0
447+
// either they are all set, or all not set. False if partial
448+
isComplete := (hasRegion && hasAccessID && hasSecretAccessKey) || (!hasRegion && !hasAccessID && !hasSecretAccessKey)
449+
return !isComplete
450+
}

Diff for: pkg/sigv4/const.go

+23
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
package sigv4
2+
3+
const (
4+
// Amazon Managed Service for Prometheus
5+
awsServiceName = "aps"
6+
7+
signingAlgorithm = "AWS4-HMAC-SHA256"
8+
9+
authorizationHeaderKey = "Authorization"
10+
amzDateKey = "X-Amz-Date"
11+
12+
// emptyStringSHA256 is the hex encoded sha256 value of an empty string
13+
emptyStringSHA256 = `e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855`
14+
15+
// timeFormat is the time format to be used in the X-Amz-Date header or query parameter
16+
timeFormat = "20060102T150405Z"
17+
18+
// shortTimeFormat is the shorten time format used in the credential scope
19+
shortTimeFormat = "20060102"
20+
21+
// contentSHAKey is the SHA256 of request body
22+
contentSHAKey = "X-Amz-Content-Sha256"
23+
)

0 commit comments

Comments
 (0)