common/remotedatasource: accept specific TLS configuration
Some checks failed
CI / 🤖 Check dependabot status (push) Has been cancelled
CI / 🐧 Test on Linux (${{ github.ref_type == 'tag' }}, misc) (push) Has been cancelled
CI / 🐧 Test on Linux (coverage) (push) Has been cancelled
CI / 🐧 Test on Linux (regular) (push) Has been cancelled
CI / ❄️ Build on Nix (push) Has been cancelled
CI / 🍏 Build and test on macOS (push) Has been cancelled
CI / 🧪 End-to-end testing (push) Has been cancelled
CI / 🔍 Upload code coverage (push) Has been cancelled
CI / 🔬 Test only Go (push) Has been cancelled
CI / 🔬 Test only JS (${{ needs.dependabot.outputs.package-ecosystem }}, 20) (push) Has been cancelled
CI / 🔬 Test only JS (${{ needs.dependabot.outputs.package-ecosystem }}, 22) (push) Has been cancelled
CI / 🔬 Test only JS (${{ needs.dependabot.outputs.package-ecosystem }}, 24) (push) Has been cancelled
CI / ⚖️ Check licenses (push) Has been cancelled
CI / 🐋 Build Docker images (push) Has been cancelled
CI / 🐋 Tag Docker images (push) Has been cancelled
CI / 🚀 Publish release (push) Has been cancelled
Update Nix dependency hashes / Update dependency hashes (push) Has been cancelled

This commit is contained in:
Vincent Bernat
2025-10-29 22:33:06 +01:00
parent ea3d5a9f28
commit a2339312ac
8 changed files with 198 additions and 3 deletions

View File

@@ -13,7 +13,7 @@ import (
// TLSConfiguration defines TLS configuration.
type TLSConfiguration struct {
// Enable says if TLS should be used to connect to brokers
// Enable says if TLS should be used to connect to remote servers.
Enable bool `validate:"required_with=CAFile CertFile KeyFile"`
// Verify says if we need to check remote certificates
Verify bool

View File

@@ -31,6 +31,8 @@ type Source struct {
Transform TransformQuery
// Interval tells how much time to wait before updating the source.
Interval time.Duration `validate:"min=1m"`
// TLS defines the TLS configuration if the URL needs it.
TLS helpers.TLSConfiguration
}
// TransformQuery represents a jq query to transform data.
@@ -66,6 +68,10 @@ func DefaultSourceConfiguration() Source {
return Source{
Method: "GET",
Timeout: time.Minute,
TLS: helpers.TLSConfiguration{
Enable: false,
Verify: true,
},
}
}

View File

@@ -28,6 +28,9 @@ func TestSourceDecode(t *testing.T) {
Method: "GET",
Timeout: time.Minute,
Interval: 10 * time.Minute,
TLS: helpers.TLSConfiguration{
Verify: true,
},
},
}, {
Description: "Simple transform",
@@ -45,6 +48,9 @@ func TestSourceDecode(t *testing.T) {
Timeout: time.Minute,
Interval: 10 * time.Minute,
Transform: MustParseTransformQuery(".[]"),
TLS: helpers.TLSConfiguration{
Verify: true,
},
},
}, {
Description: "Use POST",
@@ -64,6 +70,33 @@ func TestSourceDecode(t *testing.T) {
Timeout: 2 * time.Minute,
Interval: 10 * time.Minute,
Transform: MustParseTransformQuery(".[]"),
TLS: helpers.TLSConfiguration{
Verify: true,
},
},
}, {
Description: "With TLS configuration",
Initial: func() any { return Source{} },
Configuration: func() any {
return gin.H{
"url": "https://example.net",
"interval": "10m",
"tls": gin.H{
"enable": true,
"ca-file": "something.crt",
},
}
},
Expected: Source{
URL: "https://example.net",
Method: "GET",
Timeout: time.Minute,
Interval: 10 * time.Minute,
TLS: helpers.TLSConfiguration{
Enable: true,
Verify: false, // TODO this should be fixed
CAFile: "something.crt",
},
},
}, {
Description: "Complex transform",
@@ -85,6 +118,9 @@ func TestSourceDecode(t *testing.T) {
Transform: MustParseTransformQuery(`
.prefixes[] | {prefix: .ip_prefix, tenant: "amazon", region: .region, role: .service|ascii_downcase}
`),
TLS: helpers.TLSConfiguration{
Verify: true,
},
},
}, {
Description: "Incorrect transform",

View File

@@ -73,6 +73,9 @@ func New[T any](r *reporter.Reporter, provider ProviderFunc, dataType string, da
source.Transform.Query, _ = gojq.Parse(".")
c.dataSources[k] = source
}
if _, err := source.TLS.MakeTLSConfig(); err != nil {
return nil, err
}
}
c.initMetrics()
@@ -88,8 +91,10 @@ func (c *Component[T]) Fetch(ctx context.Context, name string, source Source) ([
l := c.r.With().Str("name", name).Str("url", source.URL).Logger()
l.Info().Msg("update data source")
tlsConfig, _ := source.TLS.MakeTLSConfig()
client := &http.Client{Transport: &http.Transport{
Proxy: http.ProxyFromEnvironment,
Proxy: http.ProxyFromEnvironment,
TLSClientConfig: tlsConfig,
}}
req, err := http.NewRequestWithContext(ctx, source.Method, source.URL, nil)
if err != nil {

View File

@@ -5,7 +5,14 @@ package remotedatasource
import (
"context"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/tls"
"crypto/x509"
"crypto/x509/pkix"
"fmt"
"math/big"
"net"
"net/http"
"strconv"
@@ -211,3 +218,128 @@ func TestSource(t *testing.T) {
t.Fatalf("Metrics (-got, +want):\n%s", diff)
}
}
// generateSelfSignedCert generates a self-signed certificate for testing
func generateSelfSignedCert(t *testing.T) tls.Certificate {
t.Helper()
// Generate a private key
privateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
t.Fatalf("ecdsa.GenerateKey() error:\n%+v", err)
}
// Create a certificate template
template := x509.Certificate{
SerialNumber: big.NewInt(1),
Subject: pkix.Name{
Organization: []string{"Test Organization"},
},
NotBefore: time.Now(),
NotAfter: time.Now().Add(time.Hour),
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
BasicConstraintsValid: true,
IPAddresses: []net.IP{net.ParseIP("127.0.0.1")},
}
// Create the certificate
certDER, err := x509.CreateCertificate(rand.Reader, &template, &template, &privateKey.PublicKey, privateKey)
if err != nil {
t.Fatalf("x509.CreateCertificate() error:\n%+v", err)
}
return tls.Certificate{
Certificate: [][]byte{certDER},
PrivateKey: privateKey,
}
}
func TestSourceWithTLS(t *testing.T) {
cert := generateSelfSignedCert(t)
// Setup TLS server
mux := http.NewServeMux()
mux.Handle("/data.json", http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.Header().Add("Content-Type", "application/json")
w.WriteHeader(200)
w.Write([]byte(`{"results": [{"name": "secure", "description": "tls test"}]}`))
}))
listener, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatalf("Listen() error:\n%+v", err)
}
server := &http.Server{
Handler: mux,
TLSConfig: &tls.Config{
Certificates: []tls.Certificate{cert},
},
}
address := listener.Addr().String()
go server.ServeTLS(listener, "", "")
defer server.Shutdown(context.Background())
t.Run("WithoutTLSConfig", func(t *testing.T) {
r := reporter.NewMock(t)
config := map[string]Source{
"secure": {
URL: fmt.Sprintf("https://%s/data.json", address),
Method: "GET",
Timeout: 1 * time.Second,
Interval: 1 * time.Minute,
Transform: MustParseTransformQuery(".results[]"),
},
}
handler := remoteDataHandler{
data: []remoteData{},
}
handler.fetcher, _ = New[remoteData](r, handler.UpdateData, "test", config)
ctx, cancel := context.WithTimeout(t.Context(), time.Second)
defer cancel()
_, err := handler.fetcher.Fetch(ctx, "secure", config["secure"])
if err == nil {
t.Fatal("Fetch() should have errored with certificate error")
}
})
t.Run("WithTLSSkipVerify", func(t *testing.T) {
r := reporter.NewMock(t)
config := map[string]Source{
"secure": {
URL: fmt.Sprintf("https://%s/data.json", address),
Method: "GET",
Timeout: 1 * time.Second,
Interval: 1 * time.Minute,
TLS: helpers.TLSConfiguration{
Enable: true,
Verify: false,
},
Transform: MustParseTransformQuery(".results[]"),
},
}
handler := remoteDataHandler{
data: []remoteData{},
}
handler.fetcher, _ = New[remoteData](r, handler.UpdateData, "test", config)
ctx, cancel := context.WithTimeout(t.Context(), time.Second)
defer cancel()
results, err := handler.fetcher.Fetch(ctx, "secure", config["secure"])
if err != nil {
t.Fatalf("Fetch() error:\n%+v", err)
}
expected := []remoteData{
{
Name: "secure",
Description: "tls test",
},
}
if diff := helpers.Diff(results, expected); diff != "" {
t.Fatalf("Fetch() (-got, +want):\n%s", diff)
}
})
}